Arduino

The Pi can control certain hardware directly, but a dedicated microcontroller can be better for real-time tasks like controlling servos or certain sensors. The code here is dedicated to efficiently interfacing with microcontrollers, and was tested to work with both Arduino and ESP32, but should work with any hardware with a serial port and C compiler.

You can connect a Pi to an Arduino using a USB cable (easiest), or, with the help of a level shifter, directly through the Pi’s hardware serial pins (BCM 14 and 15, see here for details). The SerialConnection class is provided to easily send and receive asynchronous commands as your robot is doing other processing.

Basic Communication

Assuming that you have connected the Pi to an Arduino with a USB cable, you can read and write to the serial port as follows:

import asyncio
from rtcbot.arduino import SerialConnection

conn = SerialConnection("/dev/ttyAMA1")

async def sendAndReceive(conn):
    conn.put_nowait("Hello world!")
    while True:
        msg = await conn.get().decode('ascii')
        print(msg)
        await asyncio.sleep(1)

asyncio.ensure_future(sendAndReceive(conn))

asyncio.get_event_loop().run_forever()

This sends a Hello World to the Arduino, and then reads the incoming serial messages line by line. Given the corresponding Arduino code,

void setup() {
    Serial.begin(115200);
}
void loop() {
    if (Serial.available() > 0) {
        Serial.print("I received: ");
        Serial.println(Serial.read());
    }
}

you should get the messages:

I received: H
I received: e
...

By default, SerialConnection reads line by line. To get raw input as it comes in, you can set the readFormat to None:

conn = SerialConnection("/dev/ttyAMA0",readFormat=None)

While reading/writing strings is useful for debugging, for speed and robustness, it is recommended that communication with the Arduino be performed through C structs.

C Struct Messaging

When using a struct write format, a Python dict or tuple is directly encoded by the SerialConnection, and is read by the Arduino in a way that the values are directly available for use.

As an example, we will write control messages to the Arduino. On the arduino, you need to create an associated struct into which messages will be received:

#include <stdint.h>
typedef __attribute__ ((packed)) struct {
    int16_t value1;
    uint8_t value2;
} controlMessage;

The packed attribute ensures that the arduino’s struct is compatible with the encoding performed by Python.

From Python, you need to give the SerialConnection the structure shape in the format expected by Python’s structure packing library. The arduino is little endian (each string should start with “<”). For example, we need to tell the SerialConnection that first element of the struct is called “value1”, and is a 16 bit integer (the default int size on a standard Arduino). This corresponds to the format character “h” (see structure packing table of format values).

conn = SerialConnection(
    url="/dev/ttyAMA1",
    writeFormat="<hB",
    writeKeys=["value1","value2"]
)

With this format, you can send messages to the Arduino as dicts:

conn.put_nowait({"value1": -23,"value2": 101})

To decode them on the Arduino, you can read:

controlMessage msg;
Serial.read((char*)&msg,sizeof(msg));

Similarly, you can also send structs to Python from the Arduino:

typedef __attribute__ ((packed)) struct {
    uint8_t sensorID;
    uint16_t measurement;
} sensorMessage;

and:

sensorMessage msg = { .sensorID = 12, .measurement=123 };
Serial.write((char*)&msg,sizeof(msg));

You can then get the message directly as a Python dict:

conn = SerialConnection(
    url="/dev/ttyAMA1",
    readFormat="<BH",
    readKeys=["sensorID","measurement"]
)

# Run this in a coroutine
print(await conn.get() )
# {"sensorID": 12, "measurement": 123}

Full Example

The above can be demonstrated with a full example that sends and receives messages:

// The controlMessage comes from the pi
typedef __attribute__ ((packed)) struct {
    uint16_t value1;
    uint8_t value2;
} controlMessage;

// We write this back to the Pi
typedef __attribute__ ((packed)) struct {
    uint8_t value1;
    uint16_t value2;
} sensorMessage;

// These are the specific message instances
controlMessage cMsg;
sensorMessage sMsg;

void setup() {
    Serial.begin(115200);
}
void loop() {

    // Read the control message
    Serial.readBytes((char*)&cMsg,sizeof(cMsg));

    // set up the sensor message
    sMsg.value1 = cMsg.value2;
    sMsg.value2 = cMsg.value1;

    // Send it back!
    Serial.write((char*)&sMsg,sizeof(sMsg));
}

The above code echoes the values sent to it, with value1 and value2 switched. The python code to read it is:

import asyncio
from rtcbot.arduino import SerialConnection

loop = asyncio.get_event_loop()

sc = SerialConnection(
    url="/dev/ttyAMA1",
    writeFormat="<HB",
    writeKeys=["value1", "value2"],
    readFormat="<BH",
    readKeys=["value1", "value2"],
    loop=loop
)

async def sendAndReceive(sc):
    while True:
        sc.put_nowait({"value1": 1003,"value2": 2})
        msg = await sc.get()
        print("Received:",msg)
        await asyncio.sleep(1)

asyncio.ensure_future(sendAndReceive(sc))

try:
    loop.run_forever()
finally:
    print("Exiting Event Loop")
    loop.run_until_complete(loop.shutdown_asyncgens())
    loop.close()

Running this program, you get:

Received: {"value1": 2,"value2": 1003}
Received: {"value1": 2,"value2": 1003}
Received: {"value1": 2,"value2": 1003}

API

class rtcbot.arduino.SerialConnection(url='/dev/ttyS0', readFormat='\n', writeFormat=None, baudrate=115200, writeKeys=None, readKeys=None, startByte=None, delayWriteStart=0, loop=None)[source]

Bases: SubscriptionProducerConsumer

Handles sending and receiving commands to/from a a serial port. Has built-in support for sending structs to/from Arduinos.

By default, reads and writes bytes from/to the serial port, splitting incoming messages by newline. To return raw messages (without splitting), use readFormat=None.

If a writeFormat or readFormat is given, they are interpreted as struct format strings, and all incoming or outgoing messages are assumed to conform to the given format. Without setting readKeys or writeKeys, the messages are assumed to be tuples or lists.

When given a list of strings for readKeys or writeKeys, the write or read formats are assumed to come from objects with the given keys. Using these, a SerialConnection can read/write python dicts to the associated structure formats.

close()

Cleans up and closes the object.

property closed

Returns whether the object was closed. This includes both thrown exceptions, and clean exits.

property error

If there is an error that causes the underlying process to crash, this property will hold the actual Exception that was thrown:

if myobject.error is not None:
    print("Oh no! There was an error:",myobject.error)

This property is offered for convenience, but usually, you will want to subscribe to the error by using onError(), which will notify your app when the issue happens.

Note

If the error is not None, the object is considered crashed, and no longer processing data.

async get()

Behaves similarly to subscribe().get(). On the first call, creates a default subscription, and all subsequent calls to get() use that subscription.

If unsubscribe() is called, the subscription is deleted, so a subsequent call to get() will create a new one:

data = await myobj.get() # Creates subscription on first call
data = await myobj.get() # Same subscription
myobj.unsubscribe()
data2 = await myobj.get() # A new subscription

The above code is equivalent to the following:

defaultSubscription = myobj.subscribe()
data = await defaultSubscription.get()
data = await defaultSubscription.get()
myobj.unsubscribe(defaultSubscription)
newDefaultSubscription = myobj.subscribe()
data = await newDefaultSubscription.get()
onClose(subscription=None)

This is mainly useful for connections - they can be closed remotely. This allows handling the close event.

@myobj.onClose
def closeCallback():
    print("Closed!)

Be aware that this is equivalent to explicitly awaiting the object:

await myobj
onError(subscription=None)

Since most data processing happens in the background, the object might encounter an error, and the data processing might crash. If there is a crash, the object is considered dead, and no longer gathering data.

To catch these errors, when an unhandled exception happens, the error event is fired, with the associated Exception. This function allows you to subscribe to these events:

@myobj.onError
def error_happened(err):
    print("Crap, stuff just crashed: ",err)

The onError() function behaves in the same way as a subscribe(), which means that you can pass it a coroutine, or even directly await it:

err = await myobj.onError()
onReady(subscription=None)

Creating the class does not mean that the object is ready to process data. When created, the object starts an initialization procedure in the background, and once this procedure is complete, and any spawned background workers are ready to process data, it fires a ready event.

This function allows you to listen for this event:

@myobj.onReady
def readyCallback():
    print("Ready!)

The function works in exactly the same way as a subscribe(), meaning that you can pass it a coroutine, or even await it directly:

await myobj.onReady()

Note

The object will automatically handle any subscriptions or inserts that happen while it is initializing, so you generally don’t need to worry about the ready event, unless you need exact control.

putSubscription(subscription)

Given a subscription, such that await subscription.get() returns successive pieces of data, keeps reading the subscription forever:

q = asyncio.Queue() # an asyncio.Queue has a get() coroutine
myobj.putSubscription(q)

q.put_nowait(data)

Equivalent to doing the following in the background:

while True:
    myobj.put_nowait(await q.get())

You can replace a currently running subscription with a new one at any point in time:

q1 = asyncio.Queue()
myobj.putSubscription(q1)

assert myobj.subscription == q1

q2 = asyncio.Queue()
myobj.putSubscription(q2)

assert myobj.subscription == q2
put_nowait(data)

This function allows you to directly send data to the object, without needing to go through a subscription:

while True:
    data = get_data()
    myobj.put_nowait(data)

The put_nowait() method is the simplest way to process a new chunk of data.

Note

If there is currently an active subscription initialized through putSubscription(), it is immediately stopped, and the object waits only for put_nowait():

myobj.putSubscription(s)
myobj.put_nowait(mydata) # unsubscribes from s

assert myobj.subscription is None
property ready

This is True when the class has been fully initialized, and is ready to process data:

if not myobject.ready:
    print("Not ready to process data")

This property is offered for convenience, but if you want to be notifed when ready to process data, you will want to use the onReady() function, which will allow you to set up a callback/coroutine to wait until initialized.

Note

You usually don’t need to check the ready state, since all functions for getting/putting data will work even if the class is still starting up in the background.

stopSubscription()

Stops reading the current subscription:

q = asyncio.Queue()
myobj.putSubscription(q)

assert myobj.subscription == q

myobj.stopSubscription()

assert myobj.subscription is None

# You can then subscribe again (or put_nowait)
myobj.putSubscription(q)
assert myobj.subscription == q

The object is not affected, other than no longer listening to the subscription, and not processing new data until something is inserted.

subscribe(subscription=None)

Allows subscribing to new data as it comes in, returning a subscription (see Subscriptions):

s = myobj.subscribe()
while True:
    data = await s.get()
    print(data)

There can be multiple subscriptions active at the same time, each of which get identical data. Each call to subscribe() returns a new, independent subscription:

s1 = myobj.subscribe()
s2 = myobj.subscribe()
while True:
    assert await s1.get()== await s2.get()

This function can also be used as a callback:

@myobj.subscribe
def newData(data):
    print("Got data:",data)

If passed an argument, it attempts to use the given callback/coroutine/subscription to notify of incoming data.

Parameters

subscription (optional) –

An optional existing subscription to subscribe to. This can be one of 3 things:
  1. An object which has the method put_nowait (see Subscriptions):

    q = asyncio.Queue()
    myobj.subscribe(q)
    while True:
        data = await q.get()
        print(data)
    
  2. A callback function - this will be called the moment new data is inserted:

    @myobj.subscribe
    def myfunction(data):
        print(data)
    
  3. An coroutine callback - A future of this coroutine is created on each insert:

    @myobj.subscribe
    async def myfunction(data):
        await asyncio.sleep(5)
        print(data)
    

Returns

A subscription. If one was passed in, returns the passed in subscription:

q = asyncio.Queue()
ret = thing.subscribe(q)
assert ret==q

property subscription

Returns the currently active subscription:

q = asyncio.Queue()
myobj.putSubscription(q)
assert myobj.subscription == q

myobj.stopSubscription()
assert myobj.subscription is None

myobj.put_nowait(data)
assert myobj.subscription is None
unsubscribe(subscription=None)

Removes the given subscription, so that it no longer gets updated:

subs = myobj.subscribe()
myobj.unsubscribe(subs)

If no argument is given, removes the default subscription created by get(). If none exists, then does nothing.

Parameters

subscription (optional) – Anything that was passed into/returned from subscribe().

unsubscribeAll()

Removes all currently active subscriptions, including the default one if it was intialized.