Usage Tutorial

At the core of robocluster is the concept of a “device”. If you are familiar with embedded systems, you can think of a device as a virtual microcontroller running a real time operating system. A device abstracts the act of communicating with other devices and managing tasks that run periodically. Using robocluster generally consists of creating some devices and attaching some callback functions and tasks to them and then running the devices to let them do their work.

Basic Device Creation

Lets create a device:

from robocluster import Device

device = Device('device', 'rover')

The first parameter to the Device constructor is the name that you want to assign to the device. This name identifies the device on the network. The second parameter is a group name. The group name is used behind the scenes to select an ip address and port number to bind an IP Multicast socket. Devices that have the same group name can talk to each other using the publish mechanism.

Next we’ll create a task that prints “Hello world” and attach it to the device:

from robocluster import Device

device = Device('device', 'rover')

@device.task
def hello():
    print("Hello world!")

device.run()

device.task is a decorator function that attaches or registers the hello() function to be called by the device. device.run() Starts the device and calls the hello function. What the run method is doing behind the scenes is it creates an asyncio event loop and calls the run_forever() method of the event loop. The point of robocluster devices is to handle most of the details of the python asyncio library, but if you are new to asyncio I would recommend learning it as the tasks and callbacks we write in the near future will use some asyncio syntax and assume that you know what the await keyword is doing. Here are some resources on asyncio:

Python asyncio home: This is the table of contents for all the asyncio information. Don’t read the whole thing unless you want to as there is a lot of information, some of it assuming intermediate to advanced knowledge of Python.

Python Tasks and coroutines: This has some examples of coroutines work, and is a pretty good explanation of how write and use basic coroutines.

If you google stuff on asyncio, you’ll find a lot of people talking about how confusing asyncio is, so if you don’t understand the official documentation, that’s normal :). I’ve found that in practice asyncio isn’t all that difficult to use if you don’t think about the details of the event loop and generators and what not.

Back to our example, if you run this python code it should print “Hello world” and just sit there doing nothing. Devices run forever in the foreground by default because most of the time you will want them to react events triggered over the network. You can stop the program with Control-C. It will throw a KeyboardInterrupt exception which you can avoid by catching the exception like so:

try:
    device.run()
except KeyboardInterrupt:
    pass

Lets make this a tiny bit more interesting by changing the hello() function to run every second. Change the function definition to this:

@device.every('1s')
def hello():
    print("Hello world!")

Now the device will call print “Hello world!” every second. The device.every() decorator takes a few different parameters described in robocluster.util.duration_to_seconds().

Multiple Devices

Lets start a new example that involves multiple devices. We’ll create a program where one device publishes “Hello {}” to the network, and another device receives this message and fills in its own name and prints it to the console.

from robocluster import Device

deviceA = Device('deviceA', 'rover')
deviceB = Device('deviceB', 'rover')

This just creates two devices.

@deviceA.every('1s')
async def send_message():
    await deviceA.publish('Greeting', 'Hello {}')

This creates a task for deviceA that runs every second which we have seen before. The body of the function publishes the message/event to the network. Device.publish is a coroutine, so you need to use the await keyword on it. When send_message calls that line it will let other functions run while it waits for deviceA.publish(..) to finish. If you don’t use await, send_message will finish before deviceA.publish can run, and nothing will happen. The async keyword on the function definition is required to use the await keyword, just part of Python 3.5+ syntax rules.

Messages that are published in robocluster have two main components, event and data. The event in this case is “Greeting”, and is just a tag to identify the message by. The data in this case is “Hello {}”. The data can be almost whatever you want, as long as it can be encoded. By default devices encode data in JSON format, so if you stick to strings, numbers, lists and dictionaries, you shouldn’t run into problems. Robocluster does support formats other than JSON which we may cover later.

@deviceB.on('deviceA/Greeting')
async def print_greeting(event, data):
    print(data.format(deviceB.name))

This sets up deviceB to call the print_greeting function when ever deviceA publishes the “Greeting” message. If we wanted to listen for the “Greeting” message from any device, we could use @deviceB.on('*/Greeting'). The on decorator supports unix filename globbing syntax.

The print_greeting() function takes two arguments, event and data. These are the event and data that deviceB sent, but note that on the receiving end, event has the name of the sending device prepended. This is useful if you use wild cards such as '*/Greeting' and want to do different things depending on who the sender was. When deviceA published the “Greeting” message, robocluster automatically prepended the device name to “Greeting”.

try:
    deviceA.start()
    deviceB.start()
    deviceA.wait()
    deviceB.wait()
except KeyboardInterrupt:
    deviceA.stop()
    deviceB.stop()

This starts the devices and waits for you to press Control-C. We didn’t use device.run() in this case because the run() method blocks, and wouldn’t let us start multiple devices. The device.start() method allows you to start up a device and continue doing other things while it runs.

You should see “Hello deviceB” printed to the console every second when you run this code.

As an exercise to check that you understand how to do this message passing thing, change deviceB to publish the modified string it got from deviceA, and create a new deviceC that does the printing of the final message to the console. Then modify deviceA to randomly choose between “Hello {}” and “Goodbye {}”.

Sending Data Directly

For messages that contain a lot of data or are sent at a high frequency, it is probably not a good idea to broadcast that to every device on the network. In this case it is more useful to send the message directly to the target device. Currently send uses TCP to transmit data. Lets create two devices:

device_a = Device('device_a', 'rover')
device_b = Device('device_b', 'rover')

Create a callback on device_b for “direct-msg” from any device:

@device_b.on('*/direct-msg')
async def callback(event, data):
    print('device_b got message: {}'.format(data))

Create a periodic task for device_a that sends a number to device_b:

@device_a.every('1s')
async def transmit():
    await device_a.send('device_b', 'direct-msg', 1234)

And start the devices:

try:
    device_a.start()
    device_b.start()
    device_a.wait()
    device_b.wait()
except KeyboardInterrupt:
    device_a.stop()
    device_b.stop()

The device.send() method takes 3 parameters, the first is the name of the device that you are sending to, the second is a data identifier just like the publish method, and the third parameter is the data its self.

Request Data from a device

You can also request data directly from another device.

Lets create two devices:

deviceA = Device('deviceA', 'rover')
deviceB = Device('deviceB', 'rover')

And set up deviceA to reply to the “request” event with some data:

@deviceA.on('*/request')
async def reply(event, data):
    await deviceA.reply(event, 1234)

Then set up deviceB to request the data every second:

@deviceB.every('1s')
async def get_data():
    data = await deviceB.request('deviceA', 'request')
    print(data)

And finally run the devices:

try:
    deviceA.start()
    deviceB.start()
    deviceA.wait()
    deviceB.wait()
except KeyboardInterrupt:
    deviceA.stop()
    deviceB.stop()