python-genomix
Setup
First, you need to run the components you wish to control as well as a genomix server (or a rosix server).
For example, to use the 'demo-ros' compiled for the ROS middleware, run the following commands:
$ roscore & # the ROS communication daemon.
$ genomixd & # A genomix server
$ demo-ros -b # The 'demo' component compiled for ROS.
Then, start a python interpreter and import the genomix
package:
$ python
>>> import genomix
>>>
If this raises an error, such as "ModuleNotFoundError"
, make sure the
python-genomix
package is installed and tune the PYTHONPATH
environment
variable so that it contains the lib/pythonX.Y/site-packages
directory under the installation prefix of the python-genomix
package, where
X.Y
is the python version that python-genomix
was configured with.
For interactive use, make sure you have the
readline python package,
that offers command line edition, interactive completion, etc.
|
Connecting to a server and loading clients
genomix.connect
A connection to the genomix server must be established before anything else.
To connect to a locally running server on the default 8080
port, simply use
genomix.connect()
. You can optionally specify a remote host name and a port
using the genomix.connect('host:port')
syntax:
>>> genomix.connect() # default connection to localhost:8080
<genomix connection at localhost:8080>
>>> genomix.connect('example.com:8080') # explicit remote host name and port
<genomix connection at example.com:8080>
>>>
The command returns a connection handle, identifying this particular connection. The handle must be stored in a variable for future use.
>>> handle = genomix.connect()
Multiple connections to different servers can be made simultaneously. The corresponding handles must be stored in different variables.
>>> handle_host1 = genomix.connect('host1.example.com:8080')
>>> handle_host2 = genomix.connect('host2.example.com:8080')
handle.load()
A connection handle is used to control one or several components. Each
communication client for each component must be loaded first, using the load
subcommand provided by the handle.
>>> handle.load('demo')
<demo component on localhost:8080>
If the client cannot be found automatically by genomix, you need to specify the
load path. This can be done either by using the 'rpath' handle subcommand (see
next section) or by specifying the full path to the client. genom3
clients
are found in the lib/genom/*/plugins
directory.
% handle.load('/home/jdoe/lib/genom/ros/plugins/demo.so') # full path
<demo component on localhost:8080>
The load command returns a component handle. It must be stored in a variable for future use.
% demo = handle.load('demo')
By default, the client connects to an instance of component with the same
name as the component. With the -i
optional argument, the instance name can
be changed. This is useful to control multiple instances of the same component
on the same host:
host$ demo-ros -i foo ;# runs a demo-ros instance named foo
host$ demo-ros -i bar ;# runs a demo-ros instance named bar
>>> foo = handle.load('demo', '-i', 'foo') # control the foo demo-ros instance
>>> bar = handle.load('demo', '-i', 'bar') # control the bar demo-ros instance
An interactive help describing options is available when passing the '-h' string:
% handle.load('demo', '-h')
handle.rpath()
An ordered list of load paths can be specified for a connection using the
rpath
handle subcommand:
>>> handle.rpath('/home/jdoe/lib/genom/ros/plugins')
>>> handle.rpath('/home/jdoe/lib/genom/pocolibs/plugins')
>>> handle.load('demo')
<demo component on localhost:8080>
The paths are searched in order and the first match wins.
Reading ports
Each output port of the component can be read using a dedicated method found
in the component handle. For instance, the Mobile
output port of the
demo
component can be read with the demo.Mobile()
method:
>>> demo.Mobile()
{'Mobile': {'position': 0, 'speed': 0}}
>>>
The returned value is a python dictionary, so that individual members of the port can be easily used in a script:
>>> data = demo.Mobile()
>>> data['Mobile']['position']
0
>>> data['Mobile']['speed']
0
The interactive help on port reading methods describes the output dictionary in details:
>>> help(demo.Mobile)
Mobile(*args, **kwargs) method of genomix.component.demo instance
Read "Mobile" port.
Output dictionary:
struct Mobile
| double Mobile.position
| double Mobile.speed
Invoking services
As for ports, components services are available as dedicated methods in the component handle. Services can be invoked synchronously or asynchronously, with an optional callback. An interactive help is available:
>>> help(demo.GotoPosition)
GotoPosition(*args, **kwargs) method of genomix.component.demo instance
Send GotoPosition request.
This returns a dictionary containing the output result or a
request handle, depending on the options.
Keywords arguments:
send=True Return a request handle immediately after
sending the request.
ack=True Return a request handle after sending the
request and it has been acknowledged remotely.
oneway=True Return None immediately after sending the request.
callback=... Arrange for the given callable to be called after
each status update (sent, done or error) of the
request. A request handle is returned.
timeout=... Maximum amount of time to wait.
args=True Return a dictionary with service metadata.
help=True Show this help.
Input dictionary:
double posRef: Goto position in m (0)
Synchronous call
In its simplest form, the service is invoked synchronously and the result (i.e. the output data structure) is returned after completion:
>>> result = demo.GetSpeed()
>>> print(result)
{'speedRef': '::demo::SLOW'}
The result is a regular python dictionary:
>>> result['speedRef']
'::demo::SLOW'
If the service has arguments, they can be passed as an ordered flat list, as a single python dictionary or as keywords arguments:
>>> demo.GotoPosition(1.0) # ordered flat list
>>> demo.GotoPosition({'posRef': 1.0}) # python dictionary
>>> demo.GotoPosition(posRef = 1.0) # keywords arguments
A timeout, in seconds, can be specified with the timeout
keyword argument. If
the timeout is reached before the service completes, the method will raise the
TimeoutError
exception:
>>> demo.GotoPosition({'posRef': 1.0}, timeout=0.1) # 100ms timeout
Traceback (most recent call last):
...
raise TimeoutError
TimeoutError
If the service raises an exception in the component, a GenoMError exception is
raised. When caught, the exception contains two keys: the exception type in the
attribute ex
and the detail (if any) in the attribute detail
. The detail
is itself a dictionary representing the same data structure as in the GenoM
component.
>>> try: demo.GotoPosition(2.0)
... except genomix.GenoMError as e:
... print('Exception type: ' + repr(e.ex))
... print('Exception detail: ' + repr(e.detail))
...
Exception type: '::demo::TOO_FAR_AWAY'
Exception detail: {'overshoot': 1}
Asynchronous call
In order to make more complex scripts, all services can be invoked asynchronously. The available options are the following:
-
send=True
: just send the request, do not wait for any acknowledgement. -
ack=True
: send the request, wait for acknowledgement and return.
The most useful form of asynchronous call is ack
that waits for
acknowledgment, by the component, that the service has successfully started:
>>> demo.GotoPosition({'posRef': 1.0}, ack=True)
<demo::0 sent>
The returned object is a request handle that can be used to track the status of the service. See the request handle section for how to use it.
request handle
The request handle returned by asynchronous calls is used to track the status of a running service. Interactive help lists available methods:
>>> request = demo.GotoPosition({'posRef': 1.0}, ack=True)
>>> help(request)
service status
The status of the running service is returned by the status
property:
>>> request.status
sent
The valid statuses are instances of genomix.Status
class:
-
genomix.Status.sent
: the service is still running -
genomix.Status.done
: the service has completed sucessfuly -
genomix.Status.error
: an exception was raised and the service is stopped
waiting for completion
A particular status of a running service can be waited for with wait
:
>>> request.wait()
>>> request.wait(status=genomix.Status.done) # equivalent
This runs the genomix event loop, that processes all events for all pending requests (accross all genomix instances). If callbacks have been set for some requests, they might run recursively during the request.wait() call.
The request handle can also be awaited for in an asyncio
context:
>>> import asyncio
>>> async def main():
... result = await demo.GotoPosition({'posRef': 1.0}, ack=True)
... result = await demo.GotoPosition({'posRef': -1.0}, ack=True)
... return result
...
>>> asyncio.run(main())
{}
This will run synchronously the two GotoPosition
requests, while processing
other events.
service result
When a service is in the done
or error
state, the result (i.e. the output
data structure) returned by the service (if any) can be retrieved with
result
. Note that if the service has not terminated yet, the call will raise
a RuntimeError.
>>> request = demo::GetSpeed(ack=True)
>>> request.status
done
>>> request.result
{'speedRef': '::demo::SLOW'}
If the service raised an exception instead of terminating successfully, the
request handle will indicate this by raising a GenoMError exception when
request.result
is read. The exception has two attributes: ex
containing the
exception type and detail
containing the exception detail (if any).
>>> request = demo.GotoPosition(2.0, ack=True)
>>> request.wait()
>>> request.result
...
genomix.event.GenoMError: ::demo::TOO_FAR_AWAY: {'overshoot': 1}
Asynchronous callback
When using the callback
keyword argument, the service is by default invoked
asynchronously. The callable passed as the callback
argument will be invoked
each time the status of the request changes (i.e. when it is sent, done or
raises an exception). The callable is invoked with a single argument that is
the request handle.
>>> def report(request): print('report: ' + repr(request.status))
...
>>> r = demo.GotoPosition({'posRef': 1.0}, callback=report)
report: sent
>>> r.wait()
report: done
Oneway call
The oneway call (keywoard oneway=True
) should normally not be used: it sends
a request blindly, without confirmation and does not track the status of the
request. This is useful only for performance reasons, when a service must be
invoked at a high frequency (e.g. several hundreds of time per second).
>>> demo.GotoPosition({'posRef': 1.0}, oneway=True) # fire and forget
genomix event loop
In order to update the status of asynchronous services, some event loop must
run. As already shown above, requests handles can be used in an asyncio
context. This is the most simple and consise way of running the genomix event
loop, since it also runs other asynchronous tasks unrelated with genomix.
The wait()
method of requests handles also implicitly runs the event loop.
All genomix events will be processed while the wait()
call runs, but only
those.
It is also possible to update genomix events manually, without relying on
asyncio
or waiting for a particular request, with the genomix.update
function:
>>> r = demo.GotoPosition({'posRef': 1.0}, send=True)
>>> r.status
None
>>> genomix.update()
True
>>> r.status
done
genomix.update
blocks until at least one new event is processed. A timeout
argument can specify a maximum amount of time to block.