Including a guide to updating your drivers. More...
Including a guide to updating your drivers.
Much changed in Player between 1.6 and 2.0, from the fundamental message model to the nuts and bolts of message formats. This page aims to ease the transition by explaining what changed (and sometimes why it changed). At least initially, the focus here will be on providing driver maintainers with the information necessary to update their code.
Fundamentals
Two core aspects of Player have changed:
TCP server vs. robot programming framework
Player 1.6.x was many things:
- A state-based message model.
- A list of interfaces, specifying the messages used to interact with a device.
- A format for transmitting these messages over a network.
- A TCP/IP client/server protocol for developing robot control programs.
- A C++ API for developing device drivers.
- A collection of device drivers that control common hardware and implement useful algorithms.
- A server that loads, configures, and provides client access to device drivers.
- A collection of libraries in various languages that facilitate the development of client programs.
Though we tried to keep these aspects of Player separate, they were really inextricably intertwined. While Player was useful to a lot of people, it couldn't easily be extended or reused.
Player 2.0 aims to clarify and compartmentalize its components in such a way that they can be extended, resused, and replaced, as the situation demands. At its core, Player 2.0 comprises 5 components:
- interface: The world of robotic devices is carved up into a set of interfaces. Each interface defines the syntax and semantics of the messages that a conforming device can consume and produce. This specification is written as a C header file, <player.h>, in which the messages are defined as C structs.
- libplayercore : A C++ library that defines a device driver API, facilities for instantiating drivers, and the message queues used to move messages around between drivers. This library also provides methods to parse configuration files and load plugin drivers from shared objects.
- libplayerdrivers : A C++ library that contains the device drivers that are included with the Player distribution (the drivers that were formerly "built-in" to the player server). The exact contents of this library vary from system to system, depending on which drivers' prerequisites are satisifed, as well as the user-supplied options to the configure script. Each driver is also built as a standalone C++ library.
- libplayerinterface : A C library that provides functions to translate Player messages between the native C struct format and the XDR-encoded format that can be safely sent over a network. This code is autogenerated by a program that parses <player.h>.
- libplayertcp : A C++ library that provides the facilities for servicing clients over TCP. This library moves messages between TCP sockets and message queues (as defined in libplayercore). All messages are XDR-encoded (with libplayerinterface) before network transmission.
Because it's just so darn useful, Player 2.0 still contains a TCP player server. As you can imagine, this server is a short C++ program that uses the above libraries to provide the ability to parse a configuration file, load/instantiate device drivers, and allow clients access to these devices via XDR-encoded messages over a TCP socket.
Use the player server if it's what you need, but don't hesitate to use the various components however you like. For example, currently in development are SWIG-generated Java bindings to libplayercore. Using these bindings, you can write a native Java program that instantiates and control device drivers. You might then plug that Java program into a Jini network and exchange Player messages as serialized Java objects, instead of XDR-encoded structs.
State-based vs. message-passing
From its inception through version 1.6.x, Player was a state-based system. In this model, each device is presumed to have some time-varying state, and the goal of Player is to provide the ability to read and (sometimes) write this state. The content of a device's state and whether it is mutable is defined by the interface(s) that the device supports. For example, a mobile robot that supports the position2d interface maintains as its state the robot's position and velocity. These data are reported in the data messages produced by the robot and can be changed by command messages sent to the robot. This model is conceptually simple but is inflexible and not always appropriate.
Player 2.0 employs a more general message-passing model. In this approach, each device can produce a certain set of messages, and it can consume a certain set of messages. These messages are still defined by the interface(s) that the device supports, but they no longer need to report the entire state of the device. For example, in addition to periodic updates on its current position and velocity, a mobile robot might send out a different message when it has reached a goal or when a motor current limit was exceeded.
Details
A variety of details have changed, including:
Message formats
Because of the techniques used to encode messages for network transmission, Player 1.6.x had frustrating limits on the data types that were allowed. Specifically, floating point values were not supported, so real numbers were represented as integers in fixed point.
Player 2.0 is far more flexible in this regard. Virtually all C data types are supported, as are nested message structures and fixed- and variable-length one-dimensional arrays. Multi-dimensional arrays are not supported.
Unless otherwise noted, all message fields are represented in MKS units.
Arrays
Some arrays in message structures are fixed-length and some are variable-length. An array 'foo' is variable-length if the message structure containing 'foo' also contains a uint32_t called 'foo_count' ('foo_count' must appear before 'foo' in the structure). Otherwise, it is fixed-length. Basically, small arrays (2 or 3 elements) are fixed-length and bigger ones are variable-length.
The name 'foo_count' is special. It tells you how many items are actually in the array 'foo'. This information is used by the XDR functions during (de)marshaling. As a result, you must *always* fill in this field when sending a message containing a variable-length array (and you should always consult this field when receiving such a message).
For example, when sending a camera image, put the data in 'image' and fill in 'image_count' with the actual size of the frame. Then only those bytes will be transferred. Another example: when asking for the list of available devices, you send a player_device_devlist, with the field 'devices_count' set to 0. The server responds with it filled in.
Message type namespace
Player 2.0 defines a 2-level namespace of messages. At the first level, there are 7 type
s:
- data (PLAYER_MSGTYPE_DATA) : A message emitted by a device, usually communicating something about the state of the device. Data messages are not acknowledged.
- command (PLAYER_MSGTYPE_CMD) : A message sent to a device, usually changing something about the state of the device. Command messages are not acknowledged.
- request (PLAYER_MSGTYPE_REQ) : A message sent to a device, usually requesting a configuration change or querying some information. Each request message is acknowledged by a response message.
- positive response (PLAYER_MSGTYPE_RESP_ACK) : Sent in response to a request message, a positive response message indicates that the request was successfully processed by the device. Any requested information is contained in the body of the message.
- negative response (PLAYER_MSGTYPE_RESP_NACK) : Sent in response to a request message, a negative response indicates that the request was received by the device but could not be processed (e.g., the request was incorrectly formatted, the device doesn't support that requst, or the underlying hardware refused to make the requested configuration change). The body of this response will be empty.
- error response (PLAYER_MSGTYPE_RESP_ERR) : Sent in response to a request, an error response indicates that the request was never sent to the device (e.g., the device's address is invalid, or the device's message queue is full). The body of this response will be empty.
- synch (PLAYER_MSGTYPE_SYNCH) : This message will likely be deprecated.
Each message also has an interface-specific subtype
. For example, the laser interface defines 2 data messages: one contains a scan, while the other contains both a scan and a pose. The subtype
field of the message header allows the recipient to disambiguate the two. The subtype
of a response message will be identical to the corresponding request message.
Dynamic libs
All libraries are now built using libtool in both static and dynamic versions (assuming your system supports building and loading shared libraries).
Low-level details (how to update your driver)
The amount of work required to update a Player 1.6.x driver to the Player 2.0 API varies greatly from driver to driver. Simple, single-interface drivers tend to be pretty easy to update, whereas complex, multi-interface drivers can be quite tedious. There is no tool for automatically updating driver code; writing such a tool would be very difficult indeed.
Because each driver can be structured differently and use different parts of the Driver API, I don't have step-by-step instructions for updating a driver. Instead, below is a list of tips, hints, and things to look out for. I find that a combination of systematically following the items on this list and compilation by attrition (i.e., fix each problem as the compiler finds and complains about it) does the job.
libplayerinterface/functiontable.c
must be updated manually to handle each message. The arrayinit_ftable
defined in that file contains a (interface, type, subtype, XDR packing function) tuple for each message. Any message without an entry in that table will not be sent or received.
- The player_device_id_t structure has been replaced with the player_devaddr_t structure. The
code
field is now calledinterf
, theport
field isrobot
, and ahost
field has been added.
- The player_msghdr_t structure has changed:
- The addressing information formerly stored in
code
andindex
is now contained inaddr
, which is of type player_devaddr_t. - As explained above (Message type namespace), the message header has
type
and asubtype
. - The timestamp is a double, containing the number of seconds since the epoch.
- The addressing information formerly stored in
- ConfigFile::ReadDeviceId is now ConfigFile::ReadDeviceAddr.
- The
position
interface is now the position2d interface, and its code is PLAYER_POSITION2D_CODE.
- The driver no longer specifies an allowable access mode, in either the Driver constructor or in Driver::AddInterface. The READ, WRITE, and ALL access modes have been collapsed into OPEN. A subscriber can simply OPEN or CLOSE a subscription to a device.
- The data/command buffers and request/reply queues have been replaced by general-purpose message queues, of type MessageQueue. Each driver has an incoming queue, Driver::InQueue. All messages to the driver, whether commands or requests, arrive on this queue. The queue has a maximum length, set in the Driver constructor or in Driver::AddInterface.
- All external access to a driver is done via the appropriate Device, NOT the Driver itself. For example, to subscribe to another device, call Device::Subscribe on the Device* (which you can retrieve from the deviceTable with DeviceTable::GetDevice).
- It is no longer to possible to block on another device to wait for data from that device. Instead, a driver can, via Driver::Wait, block on its own queue (Driver::InQueue). If any message are pending on the queue, Driver::Wait returns immediately; otherwise, it blocks until a new message is pushed onto the queue (or until the specified timeout).
- The Get/Put Data/Command/Config/Reply methods have been replaced with methods for pushing and popping messages on queues. To process incoming messages, override the default implementation of Driver::ProcessMessage; this method should handle a single message. The method Driver::ProcessMessages will pop all pending messages from Driver::InQueue, calling Driver::ProcessMessage once for each message. Driver::ProcessMessages facilitates sending replies and should be used in place of calling MessageQueue::Pop directly on Driver::InQueue (although you can certainly do this if you want). To send a message to a device (i.e., to push a message onto a device's queue), call Device::PutMsg.
- Non-threaded drivers MUST override Driver::ProcessMessage, and they MUST respond to each request message in place in this method. Driver::ProcessMessages will be invoked periodically on each non-threaded driver. Forwarding requests to an underlying driver from a non-threaded driver is tricky; look at the lasercspace driver for an example.
- Device::Request provides an easy way to send a request to another device and wait for the reply. Do NOT call this method inside Driver::ProcessMessage in a non-threaded driver, as it would block the caller, which could be a very bad thing.
- No more byte-swapping or unit-conversions in driver code. Drivers are transport-independent modules, and as such send and receive all messages in their native C struct formats (host byte-order, MKS units), as defined in <player.h>. Nobody should include <netinet/in.h>.
- Only one file should be included from Player: Use pkg-config (i.e.,
#include <libplayerinterface/player.h>
pkg-config --cflags libplayercore
) to get the appropriate compiler flags.
- For "built-in" drivers (i.e., those included in the Player distribution, NOT plugin drivers), the Makefile.am layout has changed:
- All drivers are now built as libtool libraries, with the extension
.la
. - An automake conditional tells you whether your driver should be built.
- All drivers are now built as libtool libraries, with the extension
- As an example, if your driver is called
foo
, and is built fromfoo.cc
, then your Makefile.am might look like this:noinst_LTLIBRARIES = if INCLUDE_FOO noinst_LTLIBRARIES += libfoo.la endif AM_CPPFLAGS = -Wall -I$(top_srcdir) libfoo_la_SOURCES = foo.cc