Metadata-Version: 2.1
Name: tdmclient
Version: 0.1.6
Summary: Communication with Thymio II robot via the Thymio Device Manager
Home-page: https://github.com/epfl-mobots/tdm-python
Author: Yves Piguet
License: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: BSD License
Classifier: Intended Audience :: Education
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE

# tdmclient

Python package to connect to a [Thymio II robot](https://thymio.org) 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.

## Installation

Make sure that Thymio&nbsp;Suite, Python&nbsp;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.thymio.org/program/), [https://www.python.org/downloads/](https://www.python.org/downloads/) and [https://pypi.org/project/pip/](https://pypi.org/project/pip/), respectively.

Then in a terminal window, install tdmclient by typing
```
python3 -m pip install tdmclient
```

## Tutorial

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.tools.abc arguments`, where `abc` is the name of the tool.

### tdmclient.tools.tdmdiscovery

Display the address and port of TDM advertised by zeroconf until control-C is typed:
```
python3 -m tdmclient.tools.tdmdiscovery
```

### tdmclient.tools.list

Display the list of nodes with their id, group id, name, status, capability, and firmware version:
```
python3 -m tdmclient.tools.list
```

Display options:
```
python3 -m tdmclient.tools.list --help
```

### tdmclient.tools.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.tools.run --scratchpad examples/blink.aseba
```

Stop the program:
```
python3 -m tdmclient.tools.run --stop
```

To avoid having to learn the Aseba language, a small subset of Python can also be used:
```
python3 -m tdmclient.tools.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.tools.run --scratchpad examples/print.py
```

Display other options:
```
python3 -m tdmclient.tools.run --help
```

### tdmclient.tools.watch

Display all node changes (variables, events and program in the scratchpad) until control-C is typed:
```
python3 -m tdmclient.tools.watch
```

### tdmclient.tools.gui

Run the variable browser in a window. The GUI is implemented with TK.
```
python3 -m tdmclient.tools.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.

### Interactive Python

This section will describe 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 `tdmclient.tools.tdmdiscovery` which doesn't communicate with the robots.

First we'll type commands interactively by starting Python&nbsp;3 without argument. To start Python&nbsp;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&nbsp;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
    ```
    client.process_waiting_messages()
    ```
    If a robot is connected, you should find its description in an array of nodes in the client object:
    ```
    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 function `aw`. Keyword `await` is valid only in a function, hence we cannot call it directly from the Python prompt. In this section, we'll use `aw`. 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
    ```
    node = aw(client.wait_for_node())
    ```
    Avoiding calling yourself `process_waiting_messages()` is safer, because other methods like `wait_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&nbsp;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 `tdmclient.tools.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
```
## 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&nbsp;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` or `tdmclient` 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 functions `abs` and `len`.
- Constants `False` and `True`.
- Assignments of scalars to scalar variables or array elements; or lists to whole array variables.
- Augmented assignments `+=`, `-=`, `*=`, `//=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`.
- Programming constructs `if` `elif` `else`, `while` `else`, `for` `in range` `else`, `pass`, `return`. The `for` loop must use a `range` 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 no `return` 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 to `leds.top` in Aseba.
- Function definitions for event handlers with the `@onevent` decorator. The function name must match the event name (such as `def timer0():` for the first timer event); except that dots are replaced by underscores in Python (e.g. `def button_left():`). Arguments are not supported; otherwise variables in event handlers behave like in plain function definitions.
- Function call `emit("name")` or `emit("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 with `r`) 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()` or `exit(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 tool `tdmclient.tools.run` 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 of `print` and `'%d'` to stand for numbers, can be retrieved separately. E.g. `print("left",motor_left_target)` could be transpiled to `emit _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 tool `tdmclient.tools.run` and the Jupyter notebook handle that.
- In expression statements, in addition to function calls, the ellipsis `...` can be used as a synonym of `pass`.

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.tools.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.tools.run examples/blink.py
```

#### Print

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 `tdmclient.tools.run`. Assuming it's stored in `examples/print.py`:
```
python3 -m tdmclient.tools.run examples/print.py
```
`tdmclient.tools.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.tools.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. `tdmclient.tools.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`, `tdmclient.tools.transpile` shows the Python dictionary which contains the format string for each `print` statement and the number of numeric arguments:
```
python3 -m tdmclient.tools.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 `<<` `>>` <code>&#124;</code> `&` `^` | infix `<<` `>>` <code>&#124;</code> `&` `^`
| 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 = [...]`
| `v = numeric_expr` | `v = any_expr`
| `+=` `-=` `*=` `/=` `%=` `<<=` `>>=` `&=` <code>&#124;=</code> | `+=` `-=` `*=` `//=` `%=` `<<=` `>>=` `&=` <code>&#124;=</code>
| `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():`
| 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  `tdmclient.tools.transpile` or `tdmclient.tools.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` or `import 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 where `s1`, `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. No other module is available.

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]`

Function `emit` 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
```
## 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.tools.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 becomes `leds_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 still can 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 converted it from 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 + 273.15
>>> temp_K
556.15
>>>
```

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](https://jupyter.org/).

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](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 --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 of `client.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
```


