Project

General

Profile

python-genomix

This documentation presents the Python interface exposed by genom3 components through a genomix process. Throughout the documentation, a 'demo-ros' component is used as an example.

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.

Note 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}

Aborting a running service

A running service can be aborted with abort.

>>> request = demo.MoveDistance(1.0, ack=True)
>>> request.abort()

The result can then be queried in the usual way, using request.result.

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.