Communication with Thymio II robot via the Thymio Device Manager
Project description
tdmclient
Python package to connect to a Thymio II robot via the Thymio Device Manager (TDM), a component of the Thymio Suite. The connection between Python and the TDM is done over TCP to the port number advertised by zeroconf. Simple Python programs can run directly on the Thymio thanks to a transpiler.
Introduction
There are basically three ways to use tdmclient. From easy to more advanced:
-
If there is no need to communicate with the robot while the program runs, or if it's limited to messages and values displayed by the function
print()
, Python programs in .py files can be run on the Thymio with the following terminal command:python3 -m tdmclient run program.py
The program is converted from Python to Aseba, the main programming language of the Thymio. The Thymio microcontroller has very limited processing power and memory; only a small part of Python is supported. For more information, see section Python-to-Aseba transpiler.
Instead of a terminal window, you can run Python programs from the simple programming environment Thonny with the plug-in tdmclient-ty.
In a terminal, tdmclient offers other tools to list robots, watch the Thymio internal variables, etc. They're described in section Tools.
-
Jupyter is a programming environment based on documents which contain text, executable code, graphics, etc. called notebooks (ipynb files). Specific support for Jupyter is provided by tdmclient to interact directly with Thymio variables, embed Thymio programs in Python, run them, display results, and exchange data between the robot and the computer to benefit from the strengths of both: the Thymio for very reactive code close to its sensors and actuators, and your computer for its much more powerful processing capabilities, the complete Python language and ecosystem, graphics and user interface, files, the Internet... You can also control multiple robots.
In Jupyter notebooks, the most basic way to execute Python code is one statement at a time. This is also possible directly with Python itself, without Jupyter, with what is known as repl (read-eval-print loop): Python reads the statement you type, it evaluates it when you hit the Return key, it prints the result, and it loops by prompting you for more. The tdmclient module can also be used at that level, where the internal Thymio variables (leds, proximity sensors, motors...) are synchronized with Python and you have a direct access to them as if you were running the repl directly on the robot. You can also create Python programs running on the Thymio from the repl. This is explained in section Python repl for Thymio.
-
With
Client
andNode
objects.Client
is the base Python class for objects which manage the connection between your Python program and Thymio Suite, or more specifically the Thymio Device Manager (tdm), the part involved with the communication with the robots.Node
is the base Python class for objects which are the counterpart of robots on the other side of the connection. Controlling robots with these objects is described in section tdmclient classes and objects.
The package available on pypi.org, which can be installed with pip
, contains just tdmclient and its documentation. The Github code repository also contains Jupyter notebooks (begin with intro.ipynb) and examples of programs for the Thymio to run with run
.
Installation
Make sure that Thymio Suite, Python 3, and pip, the package installer for Python, are installed on your computer. You can find instructions at https://www.thymio.org/program/, https://www.python.org/downloads/ and https://pypi.org/project/pip/, respectively.
Then in a terminal window, install tdmclient by typing
python3 -m pip install tdmclient
Tools
Connect a robot to your computer via a USB cable or the RF dongle and launch Thymio Suite. In Thymio Suite, you can click the Aseba Studio icon to check that the Thymio is recognized, and, also optionally, start Aseba Studio (select the robot and click the button "Program with Aseba Studio"). Only one client can control the robot at the same time to change a variable or run a program. If that's what you want to do from Python, either don't start Aseba Studio or unlock the robot by clicking the little lock icon in the tab title near the top left corner of the Aseba Studio window.
Some features of the library can be accessed directly from the command window by typing python3 -m tdmclient abc arguments
, where abc
is the name of the tool. To get the list of tools:
python3 -m tdmclient --help
tdmdiscovery
Display the address and port of TDM advertised by zeroconf until control-C is typed:
python3 -m tdmclient tdmdiscovery
list
Display the list of nodes with their id, group id, name, status, capability, and firmware version:
python3 -m tdmclient list
Display options:
python3 -m tdmclient list --help
run
Run an Aseba program on the first Thymio II robot and store it into the scratchpad so that it's seen in Aseba Studio:
python3 -m tdmclient run --scratchpad examples/blink.aseba
Stop the program:
python3 -m tdmclient run --stop
To avoid having to learn the Aseba language, a small subset of Python can also be used:
python3 -m tdmclient run --scratchpad examples/blink.py
The print
statement, with scalar numbers and constant strings, is supported. Work is shared between the robot and the PC.
python3 -m tdmclient run --scratchpad examples/print.py
Display other options:
python3 -m tdmclient run --help
sendevent
Send an event to a robot:
python3 -m tdmclient sendevent --event foo --data 123
This assumes that the program running on the robot accepts events with name foo
and a payload of length 1. The robot must be in the available state (not locked). You can run the progam below in Aseba Studio. In the Events list on the right, click the +
button and set the name to foo
and the number of arguments to 1. Once the program is running, unlock the robot (clock the little lock icon in the panel tab) and observe the content of variable x
when you execute tdmclient sendevent
.
var x
onevent foo
x = event.args[0]
watch
Display all node changes (variables, events and program in the scratchpad) until control-C is typed:
python3 -m tdmclient watch
gui
Run the variable browser in a window. The GUI is implemented with TK.
python3 -m tdmclient gui
At launch, the robot is unlocked, i.e. the variables are just fetched and displayed: Observe is displayed in the status area at the bottom of the window. To be able to change them, activate menu Robot>Control. Then you can click any variable, change its value and type Return to confirm or Esc to cancel.
Python-to-Aseba transpiler
The official programming language of the Thymio is Aseba, a rudimentary event-driven text language. In the current official software environment, it's compiled by the TDM to machine code for a virtual processor, which is itself a program which runs on the Thymio. Virtual processors are common on many platforms; they're often referred as VM (Virtual Machine), and their machine code as bytecode.
Most programming languages for the Thymio eventually involve bytecode running on its VM. They can be divided into two main categories:
- Programs compiled to bytecode running autonomously in the VM on the microcontroller of the Thymio. In Thymio Suite and its predecessor Aseba Studio, this includes the Aseba language; and VPL, VPL 3, and Blockly, where programs are made of graphical representations of programming constructs and converted to bytecode in two steps, first to Aseba, then from Aseba to bytecode with the standard TDM Aseba compiler.
- Programs running on the computer and controlling the Thymio remotely. In Thymio Suite and Aseba Studio, this includes Scratch. Python programs which use
thymiodirect
ortdmclient
can also belong to this category. A small Aseba program runs on the Thymio, receives commands and sends back data via events.
Exceptions would include programs which control remotely the Thymio exclusively by fetching and changing variables; and changing the Thymio firmware, the low-level program compiled directly for its microcontroller.
Remote programs can rely on much greater computing power and virtually unlimited data storage capacity. On the other hand, communication is slow, especially with the USB radio dongle. It restricts what can be achieved in feedback loops when commands to actuators are based on sensor measurements.
Alternative compilers belonging to the first category, not needing the approval of Mobsya, are possible and have actually been implemented. While the Thymio VM is tightly coupled to the requirements of the Aseba language, some of the most glaring Aseba language limitations can be circumvented. Ultimately, the principal restrictions are the amount of code and data memory and the execution speed.
Sending arbitrary bytecode to the Thymio cannot be done by the TDM. The TDM accepts only Aseba source code, compiles it itself, and sends the resulting bytecode to the Thymio. So with the TDM, to support alternative languages, we must convert them to Aseba programs. To send bytecode, or assembly code which is a bytecode text representation easier to understand for humans, an alternative would be the Python package thymiodirect
.
Converting source code (the original text program representation) from a language to another one is known as transpilation (or transcompilation). This document describes a transpiler which converts programs from Python to Aseba. Its goal is to run Python programs locally on the Thymio, be it for autonomous programs or for control and data acquisition in cooperation with the computer via events. Only a small subset of Python is supported. Most limitations of Aseba are still present.
Features
The transpiler is implemented in class ATranspiler
, completely independently of other tdmclient
functionality. The input is a complete program in Python; the output is an Aseba program.
Here are the implemented features:
- Python syntax. The official Python parser is used, hence no surprise should be expected, including with spaces, tabs, parentheses, and comments.
- Integer and boolean base types. Both are stored as signed 16-bit numbers, without error on overflow.
- Global variables. Variables are collected from the left-hand side part of plain assignments (assignment to variables without indexing). For arrays, there must exist at least one assignment of a list, directly or indirectly (i.e.
a=[1,2];b=a
is valid). Size conflicts are flagged as errors. - Expressions with scalar arithmetic, comparisons (including chained comparisons), and boolean logic and conditional expressions with short-circuit evaluation. Numbers and booleans can be mixed freely. The following Python operators and functions are supported: infix operators
+
,-
,*
,//
(integer division),%
(converted to modulo instead of remainder, whose sign can differ with negative operands),&
,|
,^
,<<
,>>
,==
,!=
,>
,>=
,<
,<=
,and
,or
; prefix operators+
,-
,~
,not
; and functionsabs
andlen
. - Constants
False
andTrue
. - Assignments of scalars to scalar variables or array elements; or lists to whole array variables.
- Augmented assignments
+=
,-=
,*=
,//=
,%=
,&=
,|=
,^=
,<<=
,>>=
. - Lists as the values of assignments to list variables, the argument of
len
, or the arguments of native functions which expect arrays. Lists can be list variables, values between square brackets ([expr1, expr2, ...]
), or product of a number with values between square brackets (2 * [expr1, expr2, ...]
or[expr1, expr2, ...] * 3
). - Programming constructs
if
elif
else
,while
else
,for
in range
else
,pass
,return
. Thefor
loop must use arange
generator with 1, 2 or 3 arguments. - Functions with scalar arguments, with or without return value (either a scalar value in all
return
statement; or noreturn
statement or only without value, and call from the top level of expression statements, i.e. not at a place where a value is expected). Variable-length arguments*args
and**kwargs
, default values and multiple arguments with the same name are forbidden. Variables are local unless declared as global or not assigned to. Thymio predefined variables must also be declared explicitly as global when used in functions. In Python, dots are replaced by underscores; e.g.leds_top
in Python corresponds toleds.top
in Aseba. - Function definitions for event handlers with the
@onevent
decorator oronevent
function. The function name must match the event name (such asdef timer0():
for the first timer event); except that dots are replaced by underscores in Python (e.g.def button_left():
). Alternatively, functions can be declared as event handlers by callingonevent(timer0)
oronevent(fun,"timer0")
. In all cases, the declaration is processed at transpilation time, statically: it cannot be conditional and only a single event handler for each event can be defined. Arguments are supported for custom events; they're initialized toevent.args[0]
,event.args[1]
, etc. (the values passed toemit
). Variables in event handlers behave like in plain function definitions. - Option to check that local variables in plain functions or
@onevent
don't hide variables defined in the outer scope, which could result from forgetting to declare them global. This is implemented inmissing_global_decl
intdmclient.atranspiler_warnings
and enabled in command-line tools and Jupyter support with option--warning-missing-global
. - Function call
emit("name")
oremit("name", param1, param2, ...)
to emit an event without or with parameters. The first argument must be a literal string, delimited with single or double quotes. Raw strings (prefixed withr
) are allowed, f-strings or byte strings are not. Remaining arguments, if any, must be scalar expressions and are passed as event data. - Function call
exit()
orexit(code)
. An event_exit
is emitted with the code value (0 by default). It's up to the program on the PC side to accept events, recognize those named_exit
, stop the Thymio, and handle the code in a suitable way. The toolrun
exits with the code value as its status. - Function call
print
with arguments which can be any number of constant strings and scalar values. An event_print
is emitted where the first value is the print statement index, following values are scalar values (int or boolean sent as signed 16-bit integer), with possibly additional 0 to have the same number of values for all the print statements. Format strings, built by concatenating the string arguments ofprint
and'%d'
to stand for numbers, can be retrieved separately. E.g.print("left",motor_left_target)
could be transpiled toemit _print [0, motor.left.target]
and the corresponding format string is'left %d'
. It's up to the program on the PC side to accept events, recognize those named_print
, extract the format string index and the arguments, and produce an output string with the%
operator. The toolrun
and the Jupyter notebook handle that. - In expression statements, in addition to function calls, the ellipsis
...
can be used as a synonym ofpass
.
Perhaps the most noticeable yet basic missing features are the non-integer division operator /
(Python has operator //
for the integer division), and the break
and continue
statements, also missing in Aseba and difficult to transpile to sane code without goto
. More generally, everything related to object-oriented programming, dynamic types, strings, and nested functions is not supported.
The transpilation is mostly straightforward. Mixing numeric and boolean expressions often requires splitting them into multiple statements and using temporary variables. The for
loop is transpiled to an Aseba while
loop because in Aseba, for
is limited to constant ranges. Comments are lost because the official Python parser used for the first phase ignores them. Since functions are transpiled to subroutines, recursive functions are forbidden.
Examples
Blinking
Blinking top RGB led:
on = False
timer_period[0] = 500
@onevent
def timer0():
global on, leds_top
on = not on
if on:
leds_top = [32, 32, 0]
else:
leds_top = [0, 0, 0]
To transpile this program, assuming it's stored in examples/blink.py
:
python3 -m tdmclient transpile examples/blink.py
The result is
var on
var _timer0__tmp[1]
on = 0
timer.period[0] = 500
onevent timer0
if on == 0 then
_timer0__tmp[0] = 1
else
_timer0__tmp[0] = 0
end
on = _timer0__tmp[0]
if on != 0 then
leds.top = [32, 32, 0]
else
leds.top = [0, 0, 0]
end
To run this program:
python3 -m tdmclient run examples/blink.py
Constant strings and numeric values can be displayed on the computer with the print
function. Here is an example which increments a counter every second and prints its value and whether it's odd or even:
i = 0
timer_period[0] = 1000
@onevent
def timer0():
global i, leds_top
i += 1
is_odd = i % 2 == 1
if is_odd:
print(i, "odd")
leds_top = [0, 32, 32]
else:
print(i, "even")
leds_top = [0, 0, 0]
Running this program can also be done with run
. Assuming it's stored in examples/print.py
:
python3 -m tdmclient run examples/print.py
run
continues running forever to receive and display the outcome of print
. To interrupt it, type control-C.
To understand what happens behind the scenes, display the transpiled program:
python3 -m tdmclient transpile examples/print.py
The result is
var i
var _timer0_is_odd
var _timer0__tmp[1]
i = 0
timer.period[0] = 1000
onevent timer0
i += 1
if i % 2 == 1 then
_timer0__tmp[0] = 1
else
_timer0__tmp[0] = 0
end
_timer0_is_odd = _timer0__tmp[0]
if _timer0_is_odd != 0 then
emit _print [0, i]
leds.top = [0, 32, 32]
else
emit _print [1, i]
leds.top = [0, 0, 0]
end
Each print
statement in Python is converted to emit _print
. The event data contains the print
statement index (numbers 0, 1, 2, ...) and the numeric values. The string values aren't sent, because the Aseba programming language doesn't support strings. It's the responsibility of the receiver of the event, i.e. tool run
on the computer, to use the print
statement index and assemble the text to be displayed from the constant strings and the numeric values received from the robot.
With the option --print
, transpile
shows the Python dictionary which contains the format string for each print
statement and the number of numeric arguments:
python3 -m tdmclient transpile --print examples/print.py
The result is
{0: ('%d odd', 1), 1: ('%d even', 1)}
Feature comparison
The table below shows a mapping between Aseba and Python features. Empty cells stand for lack of a direct equivalent. Prefixes const_
, numeric_
or bool_
indicate restrictions on what's permitted. Standard Python features which are missing are not transpiled; they cause an error.
Aseba | Python |
---|---|
infix + - * / |
infix + - * // |
infix % (remainder) |
infix % (modulo) |
infix << >> | & ^ |
infix << >> | & ^ |
prefix - ~ not |
prefix - ~ not |
prefix + |
|
== != < <= > >= |
== != < <= > >= |
a < b < c (chained comparisons) |
|
and or (without shortcut) |
and or (with shortcut) |
val1 if test else val2 |
|
prefix abs |
function abs(expr) |
len(variable) |
|
var v |
no declarations |
var a[size] |
|
var a[] = [...] |
a = [...] |
a = number * [...] a = [...] * number |
|
v = numeric_expr |
v = any_expr |
+= -= *= /= %= <<= >>= &= |= |
+= -= *= //= %= <<= >>= &= |= |
v++ v-- |
v += 1 v -= 1 |
a = b (array assignment) |
a = b |
a[index_expr] |
a[index_expr] |
a[constant_range] |
|
if bool_expr then |
if any_expr: |
elseif bool_expr then |
elif any_expr: |
else |
else: |
end |
indenting |
when bool_expr do |
|
while bool_expr do |
while any_expr: |
for v in 0 : const_b - 1 do |
for v in range(expr_b): |
for v in const_a : const_b - 1 do |
for v in range(expr_a, expr_b): |
for v in const_a : const_b -/+ 1 step const_s do |
for v in range(expr_a, expr_b, expr_s): |
sub fun |
def fun(): |
all variables are global | global g |
assigned variables are local by default | |
def fun(arg1, arg2, ...): |
|
return |
return |
return expr |
|
callsub fun |
fun() |
fun(expr1, expr2, ...) |
|
fun(...) in expression |
|
onevent name |
@onevent def name(): |
onevent name |
def name(): onevent(name) |
onevent name |
def fun(): onevent(fun, "name") |
onevent name arg1=event.args[0] ... |
@onevent def name(arg1, ...): |
all variables are global | global g |
assigned variables are local by default | |
emit name |
emit("name") |
emit name [expr1, expr2, ...] |
emit("name", expr1, expr2, ...) |
explicit event declaration outside program | no event declaration |
print(...) |
|
call natfun(expr1, expr2, ...) |
nf_natfun(expr1, expr2, ...) (see below) |
natfun(expr1, ...) in expressions |
In Python, the names of native functions have underscores instead of dots. Many native functions can be called with the syntax of a plain function call, with a name prefixed with nf_
and the same arguments as in Aseba. In the table below, uppercase letters stand for arrays (lists in Python), lowercase letters for scalar values, A
, B
, a
and b
for inputs, R
and r
for result, and P
for both input and result. Lists can be variables or lists of numbers and/or booleans.
Arguments are the same in the same order, except for _system.settings.read
which returns a single scalar value. In Python, scalar numbers are passed by value and not by reference, contrary to Aseba; therefore the result is passed as a return value and can be used directly in any expression. Note also that in Python, lists (arrays) of length 1 are not interchangeable with scalars, contrary to Aseba.
Aseba | Python |
---|---|
call math.copy(R, A) |
nf_math_copy(R, A) |
call math.fill(R, a) |
nf_math_fill(R, a) |
call math.addscalar(R, A, b) |
nf_math_addscalar(R, A, b) |
call math.add(R, A, B) |
nf_math_add(R, A, B) |
call math.sub(R, A, B) |
nf_math_sub(R, A, B) |
call math.mul(R, A, B) |
nf_math_mul(R, A, B) |
call math.div(R, A, B) |
nf_math_div(R, A, B) |
call math.min(R, A, B) |
nf_math_min(R, A, B) |
call math.max(R, A, B) |
nf_math_max(R, A, B) |
call math.clamp(R, A, B, C) |
nf_math_clamp(R, A, B, C) |
call math.rand(R) |
nf_math_rand(R) |
call math.sort(P) |
nf_math_sort(P) |
call math.muldiv(R, A, B, C) |
nf_math_muldiv(R, A, B, C) |
call math.atan2(R, A, B) |
nf_math_atan2(R, A, B) |
call math.sin(R, A) |
nf_math_sin(R, A) |
call math.cos(R, A) |
nf_math_cos(R, A) |
call math.rot2(R, A, b) |
nf_math_rot2(R, A, b) |
call math.sqrt(R, A) |
nf_math_sqrt(R, A) |
call _leds.set(a, b) |
nf__leds_set(a, b) |
call _poweroff() |
nf__poweroff() |
call _system.reboot() |
nf__system_reboot() |
call _system.settings.read(a, r) |
r = nf__system_settings_read(a) |
call _system.settings.write(a, b) |
nf__system_settings_write(a, b) |
call _leds.set(i, br) |
nf__leds_set(i, br) |
call sound.record(i) |
nf_sound_record(i) |
call sound.play(i) |
nf_sound_play(i) |
call sound.replay(i) |
nf_sound_replay(i) |
call sound.duration(i, d) |
d = nf_sound_duration(i) |
call sound.system(i) |
nf_sound_system(i) |
call leds.circle(br0,br1,br2,br3,br4,br5,br6,br7) |
nf_leds_circle(br0,br1,br2,br3,br4,br5,br6,br7) |
call leds.top(r, g, b) |
nf_leds_top(r, g, b) |
call leds.bottom.right(r, g, b) |
nf_leds_bottom.right(r, g, b) |
call leds.bottom.left(r, g, b) |
nf_leds_bottom_left(r, g, b) |
call leds.buttons(br0,br1,br2,br3) |
nf_leds_buttons(br0,br1,br2,br3) |
call leds.leds.prox.h(br0,br1,br2,br3,br4,br5,br6,br7) |
nf_leds_prox_h(br0,br1,br2,br3,br4,br5,br6,br7) |
call leds.leds.prox.v(br0, br1) |
nf_leds_prox_v(br0, br1) |
call leds.rc(br) |
nf_leds_rc(br) |
call leds.sound(br) |
nf_leds_sound(br) |
call leds.temperature(r, g) |
nf_leds_temperature(r, g) |
call sound.freq(f, d) |
nf_sound_freq(f, d) |
call sound.wave(W) |
nf_sound_wave(W) |
call prox.comm.enable(en) |
nf_prox_comm_enable(en) |
call sd.open(i, status) |
status = nf_sd_open(i) |
call sd.write(data, n) |
n = nf_sd_write(data) |
call sd.read(data, n) |
n = nf_sd_read(data) |
call sd.seek(pos, status) |
status = nf_sd_seek(pos) |
call deque.size(queue, n) |
n = nf_deque_size(queue) |
call deque.push_front(queue, data) |
nf_deque_push_front(queue, data) |
call deque.push_back(queue, data) |
nf_deque_push_back(queue, data) |
call deque.pop_front(queue, data) |
nf_deque_pop_front(queue, data) |
call deque.pop_back(queue, data) |
nf_deque_pop_back(queue, data) |
call deque.get(queue, data, i) |
nf_deque_get(queue, data, i) |
call deque.set(queue, data, i) |
nf_deque_set(queue, data, i) |
call deque.insert(queue, data, i) |
nf_deque_insert(queue, data, i) |
call deque.erase(queue, i, len) |
nf_deque_erase(queue, i, len) |
Some math functions have an alternative name without the nf_
prefix, scalar arguments and a single scalar result. They can be used in assignments or other expressions.
Aseba native function | Python function call |
---|---|
math.min |
math_min(a, b) |
math.max |
math_max(a, b) |
math.clamp |
math_clamp(a, b, c) |
math.rand |
math_rand() |
math.muldiv |
math_muldiv(a, b, c) |
math.atan2 |
math_atan2(a, b) |
math.sin |
math_sin(a) |
math.cos |
math_cos(a) |
math.sqrt |
math_sqrt(a) |
Thymio variables and native functions
Thymio variables and native functions are mapped to Thymio's. Their names contain underscores _
instead of dots '.'; e.g. leds_top
in Python instead of leds.top
in Aseba. By default, they're predefined in the global scope. Alternatively, with option --nothymio
in tools transpile
or run
, they aren't, but can be imported from module thymio
as follows:
import thymio
in the global scope: variables can be accessed everywhere in expressions or assignments as e.g.thymio.leds_top
.import thymio as A
in global scope: variables can be accessed everywhere in expressions or assignments as e.g.A.leds_top
(A
can be any valid symbol).import thymio
orimport thymio as A
in function definition scope: variables can be accessed in expressions or assignments in the function.from thymio import s1, s2, ...
in the global scope: variables can be accessed in expressions everywhere (except in functions where a local variables with the same name is assigned to), in assignments in the global scope, and in functions wheres1
,s2
etc. are declared global.from thymio import *
in the global scope: all Thymio symbols are imported and can be accessed directly by their name.from thymio import s1 as a1, s2 as a2, ...
in the global scope: same as above, but variables (or only some of them) are aliased to a different name.from thymio import ...
in function definition scope: variables can be accessed in expressions or assignments in the function.
In other words, the expected Python rules apply.
In addition to variables and native functions, the following constants are defined:
Name | Value |
---|---|
BLACK |
[0, 0, 0] |
BLUE |
[0, 0, 32] |
CYAN |
[0, 32, 32] |
GREEN |
[0, 32, 0] |
MAGENTA |
[32, 0, 32] |
RED |
[32, 0, 0] |
WHITE |
[32, 32, 32] |
YELLOW |
[32, 32, 0] |
Functions emit
and onevent
and decorator @onevent
are always predefined. This is also the case for abs
, exit
, len
and print
, like in plain Python.
Here are examples which all transpile to the same Aseba program leds.top = [32, 0, 0]
:
import thymio
thymio.leds_top = thymio.RED
from thymio import leds_top, RED
leds_top = RED
from thymio import leds_top
from thymio import RED
leds_top = RED
import thymio
from thymio import leds_top
leds_top = thymio.RED
from thymio import *
leds_top = RED
from thymio import leds_top as led, RED as color
led = color
Module clock
In addition to thymio
, there is one other module available. The module clock
provides functions to get the current time since the start of the program or the last call to its function reset()
. It can be used to measure the time between two events or to add time information to data sent from the robot to the PC with events.
The module implements the following functions:
Function | Description |
---|---|
reset() |
reset the tick counter (restart times from 0) |
seconds() |
time in second |
ticks_50Hz() |
time in 1/50 second |
The values are based on a counter incremented 50 times per second. Since it's stored in a signed 16-bit integer, like all Thymio variables, there is an overflow after 32767/50 seconds, or 5 minutes and 55 seconds. If your program runs longer, use the clock to measure smaller intervals and reset it for each new interval.
Python repl for Thymio
The easiest way to explore the Thymio from Python is to use a special version of the Python repl customized so that you're almost on the robot. A repl is a read-eval-print loop, i.e. the interactive environment where you type some code fragment (an expression, an assignment, a function call, or a longer piece of program), you hit the Return key and Python will evaluate your code, print the result and wait for more input.
To start the TDM repl, make sure that Thymio Suite is running and that a Thymio is connected. Then type the following command in your shell:
python3 -m tdmclient repl
A connection with the TDM will be established, the first robot will be found and locked, a message will be displayed to confirm that everything is fine, and you'll get the familiar Python prompt:
TDM: 192.168.1.20:57785
Robot: AA003
Robot ID: 36d6627a-d1af-9571-9458-d9192d951664
>>>
Everything will work as expected: you can evaluate expressions, assign values to variables, create objects, define functions and classes, import modules and packages, open files on your hard disk to read or write to them, connect to the Internet... There are just two differences:
- Every time you type a command, the repl will check if your variables are known to the Thymio. Those whose name matches are synchronized with the Thymio before being used in expressions or after being assigned to. Python names are the same as Thymio name as they appear in the documentation and in Aseba Studio, except that dots are replaced with underscores (
leds.top
on the Thymio becomesleds_top
in the repl). And the source code of function definitions will be remembered in case we need it later. - A few functions specific to the Thymio are defined.
Here are a few examples of what you can do. Check that you can still use all the functions of Python:
>>> 1 + 2
3
>>> import math
>>> 2.3 * math.sin(3)
0.3245760185376946
>>>
Change the color of the RGB led on the top of the Thymio:
>>> leds_top = [0, 0, 32]
>>>
Get the Thymio temperature and convert it from tenths of degree Celsius to Kelvin. Notice we've waited a little too long between the two commands: the temperature has changed, or maybe the sensor measurement is corrupted by noise.
>>> temperature
281
>>> temp_K = temperature / 10 + 273.15
>>> temp_K
301.65
>>>
The function sleep(t)
, specific to the TDM repl, waits for t
seconds. The argument t
is expressed in seconds and can be fractional and less than 1. The next code example stores 10 samples of the front proximity sensor, acquired with a sampling period of 0.5. We brought a hand close to the front of the robot twice during the 5 seconds the loop lasted.
>>> p = []
>>> for i in range(10):
... p.append(prox_horizontal[2])
... sleep(0.5)
...
>>> p
[0, 0, 0, 0, 2639, 3440, 0, 1273, 2974, 4444]
>>>
The code above runs on the computer, not on the Thymio. This is fine as long as the communication is fast enough for our needs. If you want to scan a barcode with the ground sensor by moving over it, depending on the robot speed, the sampling rate must be higher than what's allowed by the variable synchronization, especially between the TDM and the Thymio if you have a wireless dongle.
It's also possible to run code on the Thymio. You can define functions with the function decorator @onevent
to specify that it should be called when the event which corresponds to the function name is emitted. Here is an example where the robot toggles its top RGB led between yellow and switched off every 0.5 second.
on = False
timer_period[0] = 500
@onevent
def timer0():
global on, leds_top
on = not on
if on:
leds_top = [32, 32, 0]
else:
leds_top = [0, 0, 0]
You can copy-paste the code above to the repl. We show it below with the repl prompts, which as usual you must not type:
>>> on = False
>>> timer_period[0] = 500
>>> @onevent
... def timer0():
... global on, leds_top
... on = not on
... if on:
... leds_top = [32, 32, 0]
... else:
... leds_top = [0, 0, 0]
...
>>>
Once you've defined the function in Python running on your computer, nothing more happens. On the Thymio, there is just the variable timer.period[0]
which has been set to 500. The magic happens with the run()
function:
>>> run()
>>>
The TDM repl will gather all the functions decorated with @onevent
, all the Thymio variables which have been assigned to, global variables and other functions called from @onevent
functions (directly or not), and make a Python program for the Thymio with all that. Then this program is converted to the Aseba programming language (the language accepted by the TDM), sent to the Thymio and executed.
If you want to check what run()
does behind the scenes, call robot_code()
to get the Python program, or robot_code("aseba")
to get its conversion to Aseba:
>>> print(robot_code())
timer_period = [500, 0]
@onevent
def timer0():
global on, leds_top
on = not on
if on:
leds_top = [32, 32, 0]
else:
leds_top = [0, 0, 0]
on = False
>>> print(robot_code("aseba"))
var on
var _timer0__tmp[1]
timer.period = [500, 0]
on = 0
onevent timer0
if on == 0 then
_timer0__tmp[0] = 1
else
_timer0__tmp[0] = 0
end
on = _timer0__tmp[0]
if on != 0 then
leds.top = [32, 32, 0]
else
leds.top = [0, 0, 0]
end
>>>
To retrieve data from the robot and process them further or store them on your computer, you can send events with emit
. Let's write a short demonstration. But first, to avoid any interference with our previous definitions, we ask Python to forget the list of @onevent
functions and assignments to Thymio's variables:
robot_code_new()
Here is a short program which collects 20 samples of the front proximity sensor, one every 200ms (5 per second), i.e. during 4 seconds:
i = 0
timer_period[0] = 200
@onevent
def timer0():
global i, prox_horizontal
i += 1
if i > 20:
exit()
emit("front", prox_horizontal[2])
Note how the Thymio program terminates with a call to the exit()
function. Running it is done as usual with run()
. Since the program emits events, run
continues running to process the events it receives until it receives _exit
(emitted by exit()
) or you type control-C. All events, except for _exit
and _print
, are collected with their data. Event data are retrieved with get_event_data(event_name)
:
>>> run() # 4 seconds to move your hand in front of the robot
>>> get_event_data("front")
[[0], [0], [0], [0], [1732], [2792], [4182], [4325], [3006], [1667], [0], [1346], [2352], [3972], [4533], [2644], [1409], [0], [0], [0], [0]]
You can send events with different names. You can also reset an event collection by calling clear_event_data(event_name)
, or without argument to clear all the events:
>>> clear_event_data()
We've mentionned the _print
event. It's emitted by the print()
function, an easy way to check what the program does. The Thymio robot is limited to handling integer numbers, but print
still accepts constant strings. The robot and the computer work together to display what's expected.
>>> robot_code_new()
>>> @onevent
... def button_forward():
... print("Temperature:", temperature)
...
>>> run() # press the forward button a few times, then control-C
Temperature: 292
Temperature: 293
^C
Jupyter notebooks
Jupyter notebooks offer a nice alternative to the plain Python prompt in a terminal. In notebooks, you can easily store a sequence of commands, edit them, repeat them, document them, have larger code fragments, produce graphics, or have interactive controls such as sliders and checkboxes. This section describes features specific to the use of tdmclient to connect and interact with a Thymio II robot. For general informations about Jupyter, how to install it and how to open an existing notebook or create a new one, please refer to its documentation.
The next subsections describe how to install tdmclient in the context of a notebook, how to connect and interact with a robot with the classes and methods of tdmclient, and how to have a notebook where variables are synchronized with the robot's (the equivalent of the TDM repl).
Installing tdmclient in a notebook
In notebooks, the context of Python is not the same as when you run it in a terminal window. To make sure that tdmclient is available, you can have a code cell with the following content at the beginning of your notebook and evaluate it before importing tdmclient:
%pip install --upgrade tdmclient
This will make sure you have the last version available at https://pypi.org.
Alternatively, if you develop your own version of tdmclient, you can make a .whl
file by typing the following command in a terminal:
python3 setup.py bdist_wheel
Then in your notebook, replace the %pip
cell above with
%pip install --quiet --force-reinstall /.../tdm-python/dist/tdmclient-0.1.3-py3-none-any.whl
replacing /.../tdm-python/dist/tdmclient-0.1.3-py3-none-any.whl
with the actual location of the .whl
file.
Using tdmclient classes and methods
This section describes the use of the class ClientAsync
in a notebook.
The main difference between using tdmclient in a notebook and in the standard Python repl (read-eval-print loop) is that you can use directly the await
keyword to execute async
methods and wait for their result. Therefore:
- You can avoid writing async functions if it's just to run them with
run_async_program
. - If you still write async functions, run them with
await prog()
instead ofclient.run_async_program(prog)
.
In the code fragments below, you can put separate statements in distinct cells and intersperse text cells. Only larger Python constructs such as with
, loops, or function definitions, must be contained as a whole in a single cell.
First, import what's needed from the tdmclient package, create client and node object, and lock the node to be able to set variables or run programs:
from tdmclient import ClientAsync
client = ClientAsync()
node = await client.wait_for_node()
await node.lock()
Then continue as with the Python repl, just replacing aw(...)
with await ...
.
Synchronized variables
This section parallels the TDM repl where the Thymio variables are shared with your local Python session.
First, import what's needed from the tdmclient package and start the session:
import tdmclient.notebook
await tdmclient.notebook.start()
Then the variables which match the robot's are synchronized in both directions. Dots are replaced by underscores: leds.top
on Thymio becomes leds_top
in Python.
# cyan
leds_top = [0, 32, 32]
You can also define event handlers with functions decorated with @onevent
:
on = False
timer_period[0] = 500
@onevent
def timer0():
global on, leds_top
on = not on
if on:
leds_top = [32, 32, 0]
else:
leds_top = [0, 0, 0]
Run and stop them with run()
and stop()
, respectively.
run()
collects all the event handlers, the functions they call, the Thymio variables which have been set, and other global variables they use to make a Python program to be converted from Python to Aseba, compiled, and run on the Thymio. But you can also provide program code as a whole in a cell and perform these steps separately:
%%run_python
v = [32, 0, 32, 0, 32, 0, 32, 0]
leds_circle = v
%%run_aseba
var v[] = [32, 32, 32, 0, 0, 0, 32, 32]
leds.circle = v
%%transpile_to_aseba
v = [32, 0, 32, 0, 32, 0, 32, 0]
leds_circle = v
Variables accessed or changed in the notebook are synchronized with the robot only if the statements are located directly in notebook cells. This isn't automatically the case for functions, to let you decide when it's efficient to receive or send values to the robot. There are two ways to do it:
-
Function
get_var("var1", "var2", ...)
retrieves the value of variablesvar1
,var2
etc. and returns them in a tuple in the same order. The typical use is to unpack the tuple directly into variables in an assignment:var1,var2,...=get_var("var1","var2",...)
; beware to have a trailing comma if you retrieve only one variable, else you'll get a plain assignment of the tuple itself.Function
set_var(var1=value1,var2=value2,...)
sends new values to the robot.Here is an example which sets the color of the robot to red or blue depending on its temperature:
def f(temp_limit=30): temperature, = get_var("temperature") if temperature > temp_limit * 10: set_var(leds_top=[32, 0, 0]) else: set_var(leds_top=[0, 10, 32])
-
To synchronized global variables whose names match the robot's, the function can be decorated with
@tdmclient.notebook.sync_var
. The effect of the decorator is to extend the function so that these variables are fetched at the beginning and sent back to the robot before the function returns.Here is the same example:
@tdmclient.notebook.sync_var def f(temp_limit=30): global temperature, leds_top if temperature > temp_limit * 10: leds_top = [32, 0, 0] else: leds_top = [0, 10, 32]
Passing results to the robot
To define a variable in a program for the robot with a value calculated in the notebook, the simplest way is to add an option to %%run_python
or %%run_aseba
. In the first example, we pass the variable speed
:
speed = 20
%%run_python --var speed=speed
motor_left_target = speed
motor_right_target = speed
In the assignment which follows --var
, what's on the left side of =
is the name of the variable to define in the robot program; what's on the right side is any expression (a constant, a variable, or something more complicated) evaluated in the context of the notebook. If the assignment contains spaces, you must quote it:
%%run_python --var "speed = 2 * speed"
motor_left_target = speed
motor_right_target = speed
The expression can also evaluate to a list:
color = [0, 32, 0] # green
%%run_python --var "c = [32-component for component in color]"
leds_top = c # [32,0,32] = magenta
To pass values during the execution once or multiple times, use custom events or change variables after the program has started, as explained below.
Custom events
To retrieve data from the robot and process them further in your notebook, you can send events with emit
. In the program below, we collects 20 samples of the front proximity sensor, one every 200ms (5 per second), i.e. during 4 seconds.
%%run_python --wait
i = 0
timer_period[0] = 200
@onevent
def timer0():
global i, prox_horizontal
i += 1
if i > 20:
exit()
emit("front", prox_horizontal[2])
Events received by the computer are collected automatically. We retrieve them with get_event_data(event_name)
, a list of all the data sent by emit
, which are lists themselves.
data = get_event_data("front")
print(data)
You can send events with different names. You can also reset an event collection by calling clear_event_data(event_name)
, or without argument to clear all the events:
clear_event_data()
Instead of calling clear_event_data()
without argument, the option --clear-event-data
of the magic command %%run_python
has the same effect and avoids to evaluate a separate notebook cell.
You can also send events in the other direction, from the notebook to the robot. This can be useful for instance if you implement a low-level behavior on the robot, such as obstacle avoidance and sensor acquisition, and send at a lower rate high-level commands which require more computing power available only on the PC.
The Thymio program below listens for events named color
and changes the top RGB led color based on a single number. Bits 0, 1 and 2 represent the red, green, and blue components respectively.
%%run_python
@onevent
def color(c):
global leds_top
leds_top[0] = 32 if c & 1 else 0
leds_top[1] = 32 if c & 2 else 0
leds_top[2] = 32 if c & 4 else 0
Now that the program runs on the robot, we can send it color
events. The number of values in send_event
should match the @onevent
declaration. They can be passed as numeric arguments or as arrays.
for col in range(8):
send_event("color", col)
sleep(0.5)
Connection and disconnection
Usually, once we've imported the notebook support with import tdmclient.notebook
, we would connect to the first robot, assuming there is just one. It's also possible to get the list of robots:
await tdmclient.notebook.list()
id: 67a5510c-d1af-4386-9458-d9145d951664
group id: 7dcf1f69-85a8-4fd4-9c4b-b74d155a1246
name: AA003
status: 2 (available)
cap: 7
firmware: 14
When you start the notebook session, you can add options to tdmclient.notebook.start
to specify which robot to use. Robots can be identified by their id, which is unique hence unambiguous but difficult to type and remember, or by their name which you can define yourself.
Since we don't know the id or name of your robot, we'll cheat by picking the actual id and name of the first robot. To get the list of robots (or nodes), instead of tdmclient.notebook.list
as above where the result is displayed in a nice list of properties, we call tdmclient.notebook.get_nodes
which returns a list.
nodes = await tdmclient.notebook.get_nodes()
id_first_node = nodes[0].id_str
name_first_node = nodes[0].props["name"]
print(f"id: {id_first_node}")
print(f"name: {name_first_node}")
id: 67a5510c-d1af-4386-9458-d9145d951664
name: AA003
Then you can specify the robot id:
await tdmclient.notebook.start(node_id=id_first_node)
We want to show you how to use the robot's name instead of its id, but first we must close the connection:
await tdmclient.notebook.stop()
Now the robot is available again.
await tdmclient.notebook.start(node_name=name_first_node)
Addressing robots
When several robots are connected to the computer, access to the default (first or specified in start()
) robot's variables, running or stopping a program is done as if there was no other robot connected to the TDM. To refer to another robot, you have to specify it, with options in magic commands %%run_python
or %%run_aseba
, and with key arguments in functions run()
or stop()
. In all cases, you can do it with the node id, the node name, or the node index, a number which is 0 for the first robot (the default robot used by tdmclient.notebook.start()
), 1 for the second robot and so on.
Robot specification | Magic command option | Function key argument |
---|---|---|
id | --robotid ... |
robot_id="..." |
name | --robotname ... |
robot_name="..." |
index | --robotindex ... |
robot_index="..." |
If the robot name contains spaces, enclose it between double-quotes also for the magic command option:
%%run_python --robotname "my Thymio"
...
To have a notebook which works unmodified with any robots, not only those of its author, we'll use the robot index. We'll also include it for the default robot (robot_index=0
) to make clear it's one among a group of two robots.
To illustrate running programs on specific robots, here is how to change the color of the top led to blue on robot 0 and green on robot 1:
%%run_python --robotindex 0
leds_top = [0, 0, 32]
%%run_python --robotindex 1
leds_top = [0, 20, 0]
If you want to run the same program on multiple robots, you can do it with a single %%run_python
or %%run_aseba
cell by specifying the id, name or index of all the target robots separated with commas, without additional spaces. If the robot names contain spaces, enclose the whole list of names between double-quotes, keeping exactly the spaces in the names but without additional spaces around the commas.
%%run_aseba --robotindex 0,1
leds.bottom.right = [32, 0, 32] # purple
leds.bottom.left = [32, 16, 0] # orange
When the option --wait
is specified, the cell execution proceeds until each program has called exit()
(in Python), or the execution is interrupted. The output of print()
functions, and exit(status)
functions with a non-zero status, is prefixed with the index of the robot among those the program run on. (In the following program, since exit()
just sends an _exit
event to the computer without terminating immediately itself its own execution on the robot, print()
is executed one more time).
%%run_python --robotindex 0,1 --wait
timer_period[0] = 250
i = 0
@onevent
def timer0():
global i
i += 1
if i > 3:
exit(1)
print(i)
[R0] 1
[R0] 2
[R0] 3
[R1] 1
[R0] Exit, status=1
[R1] 3
[R1] Exit, status=1
[R1] 4
Controlling multiple robots with events from Jupyter
Function get_event_data()
retrieves the events sent by a robot. It can take a key argument robot_id
, robot_name
or robot_index
to specify which robot is concerned.
To illustrate this, here is a program which emits an event "b"
with data suitable for leds_circle
. It accepts an event "c"
to set leds_circle
. We run it on both robots (--robotindex 0,1
).
%%run_python --robotindex 0,1 --clear-event-data
@onevent
def button_center():
emit("b", 0, 0, 0, 0, 0, 0, 0, 0)
@onevent
def button_forward():
emit("b", 32, 32, 0, 0, 0, 0, 0, 32)
@onevent
def button_right():
emit("b", 0, 32, 32, 32, 0, 0, 0, 0)
@onevent
def button_backward():
emit("b", 0, 0, 0, 32, 32, 32, 0, 0)
@onevent
def button_left():
emit("b", 0, 0, 0, 0, 0, 32, 32, 32)
@onevent
def c(l0, l1, l2, l3, l4, l5, l6, l7):
global leds_circle
leds_circle = [l0, l1, l2, l3, l4, l5, l6, l7]
We make the robot communicate by forwarding the messages in Jupyter. When Jupyter receives events, the robot sender is identified by a node object. In order to deduce which is the receiver robot, we first get the list of all nodes.
nodes = await tdmclient.notebook.get_nodes()
If the sender is node
then its index is nodes.index(node)
and the index of the receiver (the other among the first two robots) is 1-nodes.index(node)
.
Here is a program to forward the events. Touch the buttons on one robot to switch corresponding leds on the other one. The loop runs until you interrupt it with the Interrupt button (little black square).
def on_event_data(node, event_name):
src_index = nodes.index(node)
dest_index = 1 - src_index
event_data_list = get_event_data("b", robot_index=src_index)
for data in event_data_list:
send_event("c", *data, robot_index=dest_index)
clear_event_data("b", robot_index=src_index)
tdmclient.notebook.process_events(all_nodes=True, on_event_data=on_event_data)
Infrared communication between robots
Simple messages made of a single number can be sent between robots via the same infrared leds and sensors as those used as active proximity sensors. The program below reproduces the same behavior as the robot and computer programs in the previous section, where touching a button switches on the corresponding yellow leds of the other robot. Once the robot programs are launched, the computer isn't involved anymore.
%%run_python --robotindex 0,1
nf_prox_comm_enable(True)
def send_msg(code):
global prox_comm_tx
prox_comm_tx = code
@onevent
def button_center():
send_msg(99)
@onevent
def button_forward():
send_msg(1)
@onevent
def button_right():
send_msg(2)
@onevent
def button_backward():
send_msg(3)
@onevent
def button_left():
send_msg(4)
@onevent
def prox_comm():
global prox_comm_rx, leds_circle
msg = prox_comm_rx
if msg == 99:
leds_circle = [0, 0, 0, 0, 0, 0, 0, 0]
elif msg == 1:
leds_circle = [32, 32, 0, 0, 0, 0, 0, 32]
elif msg == 2:
leds_circle = [0, 32, 32, 32, 0, 0, 0, 0]
elif msg == 3:
leds_circle = [0, 0, 0, 32, 32, 32, 0, 0]
elif msg == 4:
leds_circle = [0, 0, 0, 0, 0, 32, 32, 32]
Direct use of node objects
Once connected, the node object used to communicate with the robot can be obtained with get_node()
:
robot = tdmclient.notebook.get_node()
Then all the methods and properties defined for ClientAsyncCacheNode
objects can be used. For example, you can get the list of its variables:
robot_variables = list(await robot.var_description())
robot_variables
['_id',
'event.source',
'event.args',
...
'sd.present']
Or set the content of the scratchpad, used by the tdm to share the source code amoung all the clients. No need to use the actual source code, you can set it to any string. Check then in Aseba Studio.
await robot.set_scratchpad("Hello from a notebook, Studio!")
The client object is also available as a ClientAsync
object:
client = tdmclient.notebook.get_client()
The client object doesn't have many intersting usages, because there are simpler alternatives with higher-level functions. Let's check whether the tdm is local:
client.localhost_peer
True
Interactive widgets
This section illustrates the use of tdmclient.notebook
with interactive widgets provided by the ipywidgets
package.
Import the required classes and connect to the robot. In addition to tdmclient.notebook
, ipywidgets
provides support for interactive widgets, i.e. GUI elements which you can control with the mouse.
import tdmclient.notebook
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
await tdmclient.notebook.start()
A function can be made interactive by adding a decorator @interact
which specifies the range of values of each argument. When the cell is executed, sliders are displayed for each interactive argument. (0,32,1)
means a range of integer values from 0 to 32 with a step of 1. Since the default value of the step is 1, we can just write (0,32)
. The initial value of the arguments is given by their default value in the function definition.
Thymio variables aren't synchronized automatically when they're located inside functions. By adding a decorator @tdmclient.notebook.sync_var
, all Thymio variables referenced in the function are fetched from the robot before the function execution and sent back to the robot afterwards. Note the order of the decorators: @tdmclient.notebook.sync_var
modifies the function to make its variables synchronized with the robot, and @interact
makes this modified function interactive.
@interact(red=(0,32), green=(0,32), blue=(0,32))
@tdmclient.notebook.sync_var
def rgb(red=0, green=0, blue=0):
global leds_top
leds_top = [red, green, blue]
Here are alternative ways for the same result. Instead of a decorator in front of the function, you can call interact
as a normal function, passing it the function whose arguments are manipulated interactively. Instead of decorating the function with @tdmclient.notebook.sync_var
, you can call explicitly set_var
to change the robot variables. And if your function is just a simple expression (a call to set_var
or to another function if the values of its arguments don't fit directly the sliders of interact
), you can replace it with a lambda expression.
interact(lambda red=0,green=0,blue=0: set_var(leds_top=[red,green,blue]), red=(0,32), green=(0,32), blue=(0,32));
You can combine a program running on the robot and interactive controls in the notebook to change variables. Here is a program which uses its front proximity sensor to remain at some distance from an obstacle. Put your hand or a white box in front of the Thymio before you run the cell, or be ready to catch it before it falls off the table.
%%run_python
prox0 = 1000
gain_prc = 2
timer_period[0] = 100
@onevent
def timer0():
global prox_horizontal, motor_left_target, motor_right_target, prox0, gain_prc
speed = math_muldiv(prox0 - prox_horizontal[2], gain_prc, 100)
motor_left_target = speed
motor_right_target = speed
The global variables created by the program are also synchronized with those in the notebook:
prox0
1000
gain_prc = 5
Changing the value of prox0
, which is related to the distance the robot will maintain with respect to the obstacle, can be done with a slider as for leds_top
above:
@interact(prox_target=(0, 4000, 10))
@tdmclient.notebook.sync_var
def change_prox0(prox_target):
global prox0
prox0 = prox_target
Change the value of the target value of the proximity sensor with the slider and observe how the robot moves backward or forward until it reaches a position where the expression prox0 - prox_horizontal[2]
is 0, hence the speed is 0. Actually because it's unlikely the sensor reading remains perfectly constant, the robot will continue making small adjustments.
When you've finished experimenting, stop the program:
stop()
Graphics
The usual Python module for graphics is matplotlib
. To plot a sensor value, or any computed value, as a function of time, you can retrieve the values with events.
import matplotlib.pyplot as plt
We can begin with the example presented to illustrate the use of events:
%%run_python --clear-event-data --wait
i = 0
timer_period[0] = 200
@onevent
def timer0():
global i, prox_horizontal
i += 1
if i > 20:
exit()
emit("front", prox_horizontal[2])
Then we retrieve and plot the event data:
%matplotlib inline
prox_front = get_event_data("front")
plt.plot(prox_front);
The horizontal scale shows the sample index, from 0 to 20 (the _exit
event sent by the call to exit()
is processed by the PC after the complete execution of timer0()
; thus the program emits values for i
from 1 to 21).
You may prefer to use a time scale. If the events are produced in a timer event at a known rate, the time can be computed in the notebook. But often it's more convenient to get the actual time on the robot by reading its clock. For that, we use the ticks_50Hz()
function defined in the clock
module, which returns a value incremented 50 times per second. Instead of counting samples, we stop when the clock reaches 4 seconds. Both clock.ticks_50Hz()
and clock.seconds()
are reset to 0 when the program starts or when clock.reset()
is called. Here is a new version of the robot program:
%%run_python --clear-event-data --wait
import clock
timer_period[0] = 200
@onevent
def timer0():
global prox_horizontal
if clock.seconds() >= 4:
exit()
emit("front", clock.ticks_50Hz(), prox_horizontal[2])
The events produced by emit()
contain 2 values, the number of ticks and the front proximity sensor. We can extract them into t
and y
with list comprehensions, a compact way to manipulate list values. The time is converted to seconds as fractional number, something which cannot be done on the Thymio where all numbers are integers.
%matplotlib inline
prox_front = get_event_data("front")
t = [data[0] / 50 for data in prox_front]
y = [data[1] for data in prox_front]
plt.plot(t, y);
Live graphics
Support for animated graphics, where new data are displayed when there're available, depends on the version of Jupyter and the extensions which are installed. This section describes one way to update a figure in JupyterLab without any extension.
We modify the program and plot above to run continuously with a sliding time window of 10 seconds. The call to exit()
is removed from the robot program, and we don't wait for the program to terminate.
%%run_python --clear-event-data
import clock
timer_period[0] = 200
@onevent
def timer0():
global prox_horizontal
emit("front", clock.ticks_50Hz(), prox_horizontal[2])
The figure below displays the last 10 seconds of data in a figure which is updated everytime new events are received. For each event received, the first data value is the time in 1/50 second, and the remaining values are displayed as separate lines. Thus you can keep the same code with different robot programs, as long as you emit events with a unique name and a fixed number of values.
Click the stop button in the toolbar above to interrupt the kernel (the Python session which executes the notebook cells).
from IPython.display import clear_output
from matplotlib import pyplot as plt
%matplotlib inline
def on_event_data(node, event_name):
def update_plot(t, y, time_span=10):
clear_output(wait=True)
plt.figure()
if len(t) > 1:
plt.plot(t, y)
t_last = t[-1]
plt.xlim(t_last - time_span, t_last)
plt.grid(True)
plt.show();
data_list = get_event_data(event_name)
t = [data[0] / 50 for data in data_list]
y = [data[1:] for data in data_list]
update_plot(t, y)
clear_event_data()
tdmclient.notebook.process_events(on_event_data)
tdmclient classes and objects
Interactive Python
This section describes only the use of ClientAsync
, the highest-level way to interact with a robot, with asynchronous methods which behave nicely in a non-blocking way if you need to perform other tasks such as running a user interface. All the tools described above use ClientAsync
, except for tdmdiscovery
which doesn't communicate with the robots.
First we'll type commands interactively by starting Python 3 without argument. To start Python 3, open a terminal window (Windows Terminal or Command Prompt in Windows, Terminal in macOS or Linux) and type python3
. TDM replies should arrive quicker than typing at the keyboard. Next section shows how to interact with the TDM from a program where you wait for replies and use them immediately to run as fast as possible.
Start Python 3, then import the required class. We also import the helper function aw
, an alias of the static method ClientAsync.aw
which is useful when typing commands interactively.
from tdmclient import ClientAsync, aw
Create a client object:
client = ClientAsync()
If the TDM runs on your local computer, its address and port number will be obtained from zeroconf. You can check their value:
client.tdm_addr
client.tdm_port
The client will connect to the TDM which will send messages to us, such as one to announce the existence of a robot. There are two ways to receive and process them:
- Call explicitly
If a robot is connected, you should find its description in an array of nodes in the client object:client.process_waiting_messages()
node = client.nodes[0]
- Call an asynchronous function in such a way that its result is waited for. This can be done in a coroutine, a special function which is executed at the same time as other tasks your program must perform, with the
await
Python keyword; or handled by the helper functionaw
. In plain Python, keywordawait
is valid only in a function, hence we cannot call it directly from the Python prompt (IPython, an improved command interpreter which is used in Jupyter, supports directlyawait
). In this section, we'll useaw
. Robots are associated to nodes. To get the first node once it's available (i.e. an object which refers to the first or only robot after having received and processed enough messages from the TDM to have this information), type
Avoiding calling yourselfnode = aw(client.wait_for_node())
process_waiting_messages()
is safer, because other methods likewait_for_node()
make sure to wait until the expected reply has been received from the TDM.
The value of node
is an object which contains some properties related to the robot and let you communicate with it. The node id is displayed when you just print the node:
node
or
print(node)
It's also available as a string:
node_id_str = node.id_str
The node properties are stored as a dict in node.props
. For example node.props["name"]
is the robot's name, which you can change:
aw(node.rename("my white Thymio"))
Lock the robot to change variables or run programs (make sure it isn't already used in Thymio Suite):
aw(node.lock())
Compile and load an Aseba program:
program = """
var on = 0 # 0=off, 1=on
timer.period[0] = 500
onevent timer0
on = 1 - on # "on = not on" with a syntax Aseba accepts
leds.top = [32 * on, 32 * on, 0]
"""
r = aw(node.compile(program))
The result r
is None if the call is successful, or an error number if it has failed. In interactive mode, we won't store anymore the result code if we don't expect and check errors anyway. But it's usually a good thing to be more careful in programs.
No need to store the actual source code for other clients, or anything at all.
aw(node.set_scratchpad("Hello, Studio!"))
Run the program compiled by compile
:
aw(node.run())
Stop it:
aw(node.stop())
Make the robot move forward by setting both variables motor.left.target
and motor.right.target
:
v = {
"motor.left.target": [50],
"motor.right.target": [50],
}
aw(node.set_variables(v))
Make the robot stop:
v = {
"motor.left.target": [0],
"motor.right.target": [0],
}
aw(node.set_variables(v))
Unlock the robot:
aw(node.unlock())
Getting variable values is done by observing changes, which requires a function; likewise to receive events. This is easier to do in a Python program file. We'll do it in the next section.
Here is how to send custom events from Python to the robot. The robot must run a program which defines an onevent
event handler; but in order to accept a custom event name, we have to declare it first to the TDM, outside the program. We'll define an event to send two values for the speed of the wheels, "speed"
. Method node.register_events
has one argument, an array of tuples where each tuple contains the event name and the size of its data between 0 for none and a maximum of 32. The robot must be locked if it isn't already to accept register_events
, compile
, run
, and send_events
.
aw(node.lock())
aw(node.register_events([("speed", 2)]))
Then we can send and run the program. The event data are obtained from variable event.args
; in our case only the first two elements are used.
program = """
onevent speed
motor.left.target = event.args[0]
motor.right.target = event.args[1]
"""
aw(node.compile(program))
aw(node.run())
Finally, the Python program can send events. Method node.send_events
has one argument, a dict where keys correspond to event names and values to event data.
# turn right
aw(node.send_events({"speed": [40, 20]}))
# wait 1 second, or wait yourself before typing the next command
aw(client.sleep(1))
# stop the robot
aw(node.send_events({"speed": [0, 0]}))
Python program
In a program, instead of executing asynchronous methods synchronously with aw
or ClientAsync.aw
, we put them in an async
function and we await
for their result. The whole async function is executed with method run_async_program
.
Moving forward, waiting for 2 seconds and stopping could be done with the following code. You can store it in a .py file or paste it directly into an interactive Python 3 session, as you prefer; but make sure you don't keep the robot locked, you wouldn't be able to lock it a second time. Quitting and restarting Python is a sure way to start from a clean state.
from tdmclient import ClientAsync
def motors(left, right):
return {
"motor.left.target": [left],
"motor.right.target": [right],
}
client = ClientAsync()
async def prog():
node = await client.wait_for_node()
await node.lock()
await node.set_variables(motors(50, 50))
await client.sleep(2)
await node.set_variables(motors(0, 0))
await node.unlock()
client.run_async_program(prog)
This can be simplified a little bit with the help of with
constructs:
from tdmclient import ClientAsync
def motors(left, right):
return {
"motor.left.target": [left],
"motor.right.target": [right],
}
with ClientAsync() as client:
async def prog():
with await client.lock() as node:
await node.set_variables(motors(50, 50))
await client.sleep(2)
await node.set_variables(motors(0, 0))
client.run_async_program(prog)
To read variables, the updates must be observed with a function. The following program calculates a motor speed based on the front proximity sensor to move backward when it detects an obstacle. Instead of calling the async method set_variables
which expects a result code in a message from the TDM, it just sends a message to change variables with send_set_variables
without expecting any reply. The TDM will send a reply anyway, but the client will ignore it without trying to associate it with the request message. sleep()
without argument (or with a negative duration) waits forever, until you interrupt it by typing control-C.
from tdmclient import ClientAsync
def motors(left, right):
return {
"motor.left.target": [left],
"motor.right.target": [right],
}
def on_variables_changed(node, variables):
try:
prox = variables["prox.horizontal"]
prox_front = prox[2]
speed = -prox_front // 10
node.send_set_variables(motors(speed, speed))
except KeyError:
pass # prox.horizontal not found
with ClientAsync() as client:
async def prog():
with await client.lock() as node:
await node.watch(variables=True)
node.add_variables_changed_listener(on_variables_changed)
await client.sleep()
client.run_async_program(prog)
Compare with an equivalent Python program running directly on the Thymio:
@onevent
def prox():
global prox_horizontal, motor_left_target, motor_right_target
prox_front = prox_horizontal[2]
speed = -prox_front // 10
motor_left_target = speed
motor_right_target = speed
You could save it as a .py file and run it with run
as explained above. If you want to do everything yourself, to understand precisely how tdmclient works or because you want to eventually combine processing on the Thymio and on your computer, here is a Python program running on the PC to convert it to Aseba, compile and load it, and run it.
from tdmclient import ClientAsync
from tdmclient.atranspiler import ATranspiler
thymio_program_python = r"""
@onevent
def prox():
global prox_horizontal, motor_left_target, motor_right_target
prox_front = prox_horizontal[2]
speed = -prox_front // 10
motor_left_target = speed
motor_right_target = speed
"""
# convert program from Python to Aseba
thymio_program_aseba = ATranspiler.simple_transpile(thymio_program_python)
with ClientAsync() as client:
async def prog():
with await client.lock() as node:
error = await node.compile(thymio_program_aseba)
error = await node.run()
client.run_async_program(prog)
Cached variables
tdmclient offers a simpler way, if slightly slower, to obtain and change Thymio variables. They're accessible as node["variable_name"]
or node.v.variable_name
, both for getting and setting values, also when variable_name
contains dots. Here is an alternative implementation of the remote control version of the program which makes the robot move backward when an obstacle is detected by the front proximity sensor.
from tdmclient import ClientAsync
with ClientAsync() as client:
async def prog():
with await client.lock() as node:
await node.wait_for_variables({"prox.horizontal"})
while True:
prox_front = node.v.prox.horizontal[2]
speed = -prox_front // 10
node.v.motor.left.target = speed
node.v.motor.right.target = speed
node.flush()
await client.sleep(0.1)
client.run_async_program(prog)
Scalar variables have an int
value. Array variables are iterable, i.e. they can be used in for
loops, converted to lists with function list
, and used by functions such as max
and sum
. They can be stored as a whole and retain their link with the robot: getting an element retieves the most current value, and setting an element caches the value so that it will be sent to the robot by the next call to node.flush()
.
Here is an interactive session which illustrates what can be done.
>>> from tdmclient import ClientAsync
>>> client = ClientAsync()
>>> node = client.aw(client.wait_for_node())
>>> client.aw(node.wait_for_variables({"leds.top"}))
>>> rgb = node.v.leds.top
>>> rgb
Node array variable leds.top[3]
>>> list(rgb)
[0, 0, 0]
>>> client.aw(node.lock_node())
>>> rgb[0] = 32 # red
>>> node.var_to_send
{'leds.top': [32, 0, 0]}
>>> node.flush() # robot turns red
You can also wait until all the variables have been received by calling wait_for_variables()
without argument. The in
and not in
operators test the existence of a variable.
>>> client.aw(node.wait_for_variables())
>>> "temperature" in node
True
>>> "light" not in node
True
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file tdmclient-0.1.21.tar.gz
.
File metadata
- Download URL: tdmclient-0.1.21.tar.gz
- Upload date:
- Size: 146.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.9.13
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2530981f4e0c2037baab92cbf0198ebab66646c9d59d4c2710e20c1c1a0227e3 |
|
MD5 | 0495475f3b9db14c1ffb44eca7af0421 |
|
BLAKE2b-256 | e618b70d23fcf4a6ec46984ec36d25a443ab316fe59db59ed332085297d9bd8b |
File details
Details for the file tdmclient-0.1.21-py3-none-any.whl
.
File metadata
- Download URL: tdmclient-0.1.21-py3-none-any.whl
- Upload date:
- Size: 109.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.9.13
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 25c9f7eecd54ebd7a26b5ed72f8ed9b2eae994d4b7d93e3f5991400114038bb0 |
|
MD5 | bd95bf2dc5296d2b471dac84043e2716 |
|
BLAKE2b-256 | 30cbe6201355e6959db3f2bd9c6ebd15d1033dc4c72afabe156fe0269532368c |