iRobot Create Python Example
Currently Robot Raconteur is not natively supported by commercial hardware so it is necessary to “wrap” the provided APIs with a Robot Raconteur service. For this example, we are going to wrap the serial Create Open Interface (OI) with a service. The sample code is by no means exhaustive of all the capabilities the robot has to offer; it is intended to be instructive on the use of Robot Raconteur. The user is encouraged to fill out the functionality by adding more members to the service definition!
Simple service
The first step in using Robot Raconteur is to develop an object that implements the service definition. The following
example shows a non-Robot Raconteur program that contains a class Create_impl
that
implements the service definition “experimental.create3” presented in Service Definition.
Table Members
lists the members and the functionality that will be implemented.
import serial
import struct
import time
import RobotRaconteur as RR
#Convenience shorthand to the default node.
#RRN is equivalent to RR.RobotRaconteurNode.s
RRN=RR.RobotRaconteurNode.s
import thread
import threading
serial_port_name="/dev/ttyUSB0"
class Create_impl(object):
def __init__(self):
self.Bump=RR.EventHook()
self._lock=threading.RLock()
self._packets=None
self._play_callback=None
def drive(self, velocity, radius):
with self._lock:
dat=struct.pack(">B2h",137,int(velocity*1e3),int(radius*1e3))
self._serial.write(dat)
@property
def distance_traveled(self):
return 0
@property
def angle_traveled(self):
return 0
@property
def bumpers(self):
return 0
@property
def play_callback(self):
return self._play_callback
@play_callback.setter
def play_callback(self,value):
self._play_callback=value
def init(self,port):
self._serial=serial.Serial(port="/dev/ttyUSB0",baudrate=57600)
dat=struct.pack(">2B",128,131)
self._serial.write(dat)
def shutdown(self):
self._serial.close()
def main():
#Initialize the object in the service
obj=Create_impl()
obj.Init(serial_port_name)
#Drive a bit to show that it works
obj.drive(200,1000)
time.sleep(1)
obj.drive(0,0)
#Shutdown
obj.Shutdown()
if __name__ == '__main__':
main()
See the completed service implementation in irobot_create_service.py.
The above example shows the members implemented, but not yet exposed as a service. Properties and functions are simply
properties and functions in Python, events are implemented through the EventHook
class that must be present as a
variable in the class. The Wire
and Callback
objects are implemented as properties and initialized to None
and will be set by the Robot Raconteur node when the object is exposed as a service. The main function in this example
will drive the robot a few feet to demonstrate that the service works. Replace “/dev/ttyUSB0” with the appropriate
device (COM1, COM2, etc on Windows). The class shown above is mostly a skeleton class that needs to be filled in further
to have functionality beyond simply driving.
The function “Drive” has a with self._lock
block protecting the code within the function. Robot Raconteur is
multi-threaded, meaning that all members including functions can be called concurrently. If there is an
operation or data structure that can be corrupted by simultaneous access, it is necessary to use a thread lock,
also-known-as a mutex. In the __init__
for class Create_impl
, the self._lock
variable is set to a new
instance of threading.RLock()
. When used with the with
statement, it will lock itself so only one block can
execute at a time with one thread. If all the functions in the class use the same “with lock”, only one thread at a time
will be able to access the class. If you are not familiar with multi-threading, it is best to have one global lock for
all your functions to prevent collisions.
Now that there is a basic object implemented, it is time to expose it as a Robot Raconteur service. The
following example shows a replacement for the main
function that instead of simply driving the
robot, exposes the service.
def main():
obj=Create_impl()
comm_port=sys.argv[1]
obj.Init(comm_port)
with RR.ServerNodeSetup("experimental.create3.Create",2354):
RRN.RegisterServiceTypeFromFile("experimental.create3.robdef")
RRN.RegisterService("Create","experimental.create3.Create",obj)
raw_input("Server started, press enter to quit...")
obj.Shutdown()
if __name__ == '__main__':
main()
A Robot Raconteur node requires a few steps to initialize the service:
Use ServerNodeSetup to initialize the node. Use the with statement so it will also automatically shut down the node. The setup classes will also automatically check command line arguments to modify options for the node.
Register the relevant service types (robdef)
Register the root object for the service
Each Robot Raconteur node is uniquely identified by a 128-bit UUID “NodeID”. UUIDs are a widely used concept, and are statistically guaranteed to be unique when randomly generated (See Wikipedia UUID for more information on UUIDs.) A node also has a name, the “NodeName”. A “NodeName” is intended to help clients find relevant services, and is not guaranteed to be unique. For client nodes, the “NodeID” is typically allowed to be automatically generated when needed, with the “NodeName” left emtpy. For a server node, the “NodeName” is normally specified, with the “NodeID” retrieved from a local cache based on the “NodeName”. The “NodeID” is randomly generated the first time the “NodeName” is used, and is retrieved from the cache subsequently. TLS certificates for Robot Raconteur are assigned to the “NodeID”, and guarantee the identify of the node based on its “NodeID”.
“Transports” are used to communicate between nodes. The currently available transports are TcpTransport
for
communication over a TCP/IP network, LocalTransport
for communication between nodes running on the same computer,
and HardwareTransport
for communication over USB, Bluetooth, and PCIe, and IntraTransport
for communication
within the same process. For most server nodes, the TcpTransport
and LocalTransport
are configured to listen for incoming clients. The TcpTransport
will listen for
connections on a TCP port, while the LocalTransport
listens for connections on a file handle that is identified
by the “NodeName” or “NodeID” of the server node. If a TLS certificate is available, it can be loaded into the TCP
transport. This is done using command line arguments to the node, or using SecureServerNodeSetup
.
For most use cases, the Python class ServerNodeSetup
can be used to initialize the server node. The
ServerNodeSetup
takes the “NodeName”, the TCP listen port, and an optional set of flags as parameters. In Python,
the ServerNodeSetup
is used with the with
statement. When the with
statement scope is exited, the node is
shut down.
Service types (stored in robdef files) can either be included in the Python source file as strings, or can be loaded
from file. In this example, the service definition is loaded from a file using the
RRN.RegisterServiceDefinitionFromFile
function.
Once the identification and transports have been initialized, the object is registered for use. The first parameter in
RRN.RegisterService
is the name of the service, the second parameter is the fully qualified Robot Raconteur type of
the object, and the last parameter is the object to expose as a service. (Note that a node can have multiple services
registered as long as they have different names).
After initialization, the program waits for the user to press “Enter” to stop the server. The service is now available for use by a client!
Simple client
While there are several steps to starting a service, connecting as a client is very simple. The following is an example of driving the robot over a network using the service example above:
from RobotRaconteur.Client import *
#Connect to the service
obj=RRN.ConnectService("rr+tcp://101.2.2.2?service=Create")
#Drive a bit
obj.drive(200,1000)
time.sleep(1)
obj.drive(0,0)
The example registers uses the RobotRaconteur.Client
convenience module to configure for the most common client
operations. This module creates a variable “RR” that contains the Robot Raconteur module, and “RRN” that is the default
node. It also registers the transports TcpTransport
, LocalTransport
, HardwareTransport
, and
IntraTransport
.
Robot Raconteur uses URLs to connect to services. The most common URLs are for local and TCP cases.
The url format for the LocalTransport
is:
rr+local:///?nodename=TargetNodeName&service=ServiceName
and the url format for the TcpTransport
is:
rr+tcp://hostname:port?service=ServiceName
The standard URL format is used, and the target service is passed as part of the “query” portion of the URL. Often it is necessary to specify the node to connect. For instance, the local transport requires the “nodename” to be specified because there can be multiple nodes running locally. The target node can be identified by NodeName, by NodeID, or by both. The NodeID should be the UUID of the node without curly braces. This is due to the limitations of URL syntax.
For instance, these are all valid URLs for the local transport to connect to the CreateService (replace the UUID with the one generated for your service):
rr+local:///?nodename=experimental.create.Create&service=Create
rr+local:///?nodeid=6f6706c9-91cc-d448-ae8c-c5a2acac198c&service=Create
rr+local:///?nodeid=6f6706c9-91cc-d448-ae8c-c5a2acac198c&nodename=experimental.create.Create&service=Create
The following are valid URLs to connect to the CreateServer using tcp:
rr+tcp://localhost:2354/?service=Create
rr+tcp://localhost:2354/?nodename=experimental.create.Create&service=Create
rr+tcp://localhost:2354/?nodeid=6f6706c9-91cc-d448-ae8c-c5a2acac198c&service=Create
rr+tcp://localhost:2354/?nodeid=6f6706c9-91cc-d448-ae8c-c5a2acac198c&nodename=experimental.create.Create&service=Create
Replace “localhost” with the IP address or hostname of a foreign computer if accessing over a network.
Note that for the TCP connections, the “rr+tcp” can be connected to “rrs+tcp” to enable TLS to encrypt the communication. See the Robot Raconteur Security using TLS and Certificates manual for details on using TLS.
See Robot Raconteur URLs for details on how to use URLs for more advanced cases.
A reference to the service object is returned, and it can now be used to access the members. In this example, the robot is driven a bit to demonstrate how to use a function.
iRobot Create Service
The initial service shown above only fills in the drive
member. The example
irobot_create_service.py
on GitHub shows a complete service that fills in all of the members. This is not intended to
be exhaustive for the full features of the iRobot Create; it is instead intended to be used to demonstrate features of
Robot Raconteur. Because of the length of the code it is printed in the appendix and will be referred to throughout this
section.
The background thread receives data from the serial port
and transmits the data to the bump
event, the create_state
wire, or the play_callback
where appropriate.
The function
_receive_sensor_packets
implements the ability to receive and parse the packets. This function is dedicated
to handling the serial data from the robot and calls the _fire_bump
function to fire the bump
event, the
_parse_sensor_packets
function to parse the new value of the create_state
wire, or the _play
function to handle when
the Play button is pressed on the robot. It also keeps a running tally of distance and angle traveled in the
_distance_traveled
and _angle_traveled
variables. The rest of this section will discuss the implementation of the
different members. It stores the Bump data in the _bumpers
variable.
The bumpers
, distance_traveled
, and angle_traveled
properties are implemented as standard Python properties
using the @property
decorator. Because these are read only, the setters throw an exception. Properties transparently
transmit exceptions back to the client. Functions also transparently transmit exceptions to the client. All Robot
Raconteur calls should be surrounded with try/except blocks that catch Exception
meaning it will catch and process
any thrown exception.
EventHook()
class. The __init__
function ofCreate_impl
sets:self.Bump==RR.EventHook()
This line creates the EventHook
object that is used to connect events. The fire_Bump
function then fires this
event. The Robot Raconteur node will transmit this event to all connected clients. Note that the fire
command of
EventHook
may contain parameters if the event has parameters.
The packets
wire is implemented by the node when the object is registered as a service. Because the wire is marked
readonly
using a member modifier and the create_state
object attribute is not set, the node will assume that we want
a WireBroadcaster
. The node will create the attribute and assign a WireBroadcaster
. The WireBroadcaster
class is designed to send the same value to all connected clients. If the wire is marked writeonly
, the node will
provide a WireUnicastReceiver
object. If the wire does not specify a direction, A WireServer
is passed to the
object through a property, which must be implemented by the object to receive the WireServer
.
The _SendSensorPackets
function is called by the serial receive thread when there is a new data packet. The
_SendSensorPackets
uses the OutValue
of the WireBroadcaster
to send the new value to all connected clients.
The packet data is stored in a experimental.create.SensorPacket
structure that is defined in the service definition.
The RRN.NewStructure
command is used to initialize a new Robot Raconteur structure in Python. If there is an error,
assume that the wire has been closed and delete it from the dictionary.
Wires use the InValue
and OutValue
in WireConnection
to send and receive values. For a readonly
wire,
the client will use the InValue
while the service will use the OutValue
property. Fir a writeonly
wire,
these roles are reversed and the client will use the OutValue
property while the service will use the InValue
property. If the wire does not specify the direction, both the client and service can use InValue
and OutValue
.
As of Version 0.9, wire clients can also “peek” and “poke” values. The peek and poke read the value synchronously
without creating a streaming connection. (The behavior of “peek” and “poke” is similar to the behavior of properties.)
PeekInValue
is used to read the in value, while PeekOutValue
and PokeOutValue
are used to read and write the
out value. (The “in” and “out” directions in the peek/poke functions are relative to the client.)
WireConnection
also has the LastValueReceivedTime
and LastValueSentTime
to determine the last time that
values were updated. These are relative to InValue
and OutValue
when using streaming data, and are received from
the peek and poke functions as part of the return from the functions.
The play_callback
member is assigned to the texttt_play_callback attribute of the Create_impl
object by the
node when the object is registered as a service. The _play
function demonstrates how to use the callback. The
claim_play_callback()
function contains the following line:
self._ep=RR.ServerEndpoint.GetCurrentEndpoint()
This line is used to determine the “endpoint” of the current client that is calling the function. The endpoint is used
to uniquely identify the client. When a callback is used, it is necessary to specify which client to call because there
may be multiple connected clients. The client is identified using the endpoint. The _play
function contains the
following lines, which executes the callback on the client:
cb_func=self.play_callback.GetClientFunction(self._ep)
notes=cb_func(self._distanceTraveled, self._angleTraveled)
The first line retrieves the a function handle to call the client based on the stored endpoint. The second line executes this function, which is actually implemented by calling the client with the supplied parameters and then returning the result. Note that exceptions are also transmitted transparently by callbacks from the client to the service.
The ServerNodeSetup
class by default will call EnableNodeAnnounce
. This initializes the auto-discovery system to
send out beacon packets so that client nodes can find the service.
iRobot Create Client
An example client irobot_create_client.py
on GitHub utilizes the service.
The client is similar to the previous example client, however it adds functionality using the pump
, create_state
, and
play_callback
member. The line:
c.bump += bumped
adds the function bumped
as a handler when the event is fired. The line:
wire=c.create_state.Connect()
connects to the packets
wire and returns a WireConnection object that is stored in the wire
variable. This
WireConnection has the same functionality as the one provided to the service object in the previous section. In this
example, the WireValueChange
event is used. The line:
wire.WireValueChanged+=wire_changed
adds the wire_changed
function as a handler and is called when the service provides a new value for the wire. This
event is also available on the service however in this application it is not needed. The final step in the configuration
is to set the function play_callback
as the callback function for the play_callback
member through the following
line:
c.play_callback.Function=play_callback
This function will now be called by the service when the service calls this client’s callback.
After the setup the robot is driven a bit and then pauses to allow the user to try out the functionality. The
RobotRaconteurNode
is shutdown automatically when the program exits.