Next: Bibliography
Up: 8. Architecture
Previous: 8.1 Server Structure
Contents
Subsections
Having described the internal workings of Player, we now give a short
tutorial on how you would go about extending the server by adding a new
device driver. As mentioned earlier, in lieu of a more complete prescription
for creating drivers, an examination of the code for the existing drivers
should provide you with sufficient examples. You should be familiar with C++,
class inheritance, and thread programming.
The first step in adding a new driver to Player is to decide which
interface(s) it will support. The existing interfaces are described in
Chapter 6 and their various message structures and
constants are defined in include/player.h. Although you can create a
new interface, you should try to fit your driver to an existing interface,
of which there are many. By deciding to support an existing interface,
you'll have less work to do in the server, and will likely have instant
client support for your driver in several languages.
To create a new driver, you should create a new class for the driver, which
should inherit from CDevice, declared in server/device.h and
implemented in server/device.cc. That base class defines a standard
interface, part of which the new driver must implement (other parts it
may choose to override). We now describe the salient aspects of the CDevice class.
There are two CDevice constructors available. Most drivers will
use the more expressive of the two:
CDevice::CDevice(size_t datasize, size_t commandsize,
int reqqueuelen, int repqueuelen);}
Arguments are:
- datasize : the size (in bytes) of the buffer to be allocated to
hold the current data from the driver
- commandsize : the size (in bytes) of the buffer to be allocated to
hold the current command for the driver
- reqqueuelen : the length (in number of elements) of the queue
to be allocated to hold incoming configuration requests for the driver
- repqueuelen : the length (in number of elements) of the queue to
be allocated to hold outgoing configuration replies from the driver
All requested buffers and queues will be allocated by the CDevice
constructor (we describe below where the pointers are stored in the object).
This constructor should be invoked in the preamble to the driver's own
constructor; for example, the sicklms200 driver, which produces
fixed-length data, accepts no commands, and uses incoming and outgoing
configuration queues both of length 1, has a constructor that begins:
CLaserDevice::CLaserDevice(int argc, char** argv) :
CDevice(sizeof(player_laser_data_t),0,1,1)
Now, you may want to allocte your own buffers and/or queues (e.g.,
CStageDevice does its own memory management in static mmap()ed
segments). If so, then your driver should not invoke a CDevice
constructor; the ``default'' zero-argument constructor will be invoked for you
and will properly initialize some class members. Even if you do allocate your
own buffers, you might benefit from letting CDevice know where they are,
in that you could still use the standard CDevice methods (described
below) to interface with your driver. You can do this by calling (usually in
your own constructor) SetupBuffers():
void CDevice::SetupBuffers(unsigned char* data, size_t datasize,
unsigned char* command, size_t commandsize,
unsigned char* reqqueue, int reqqueuelen,
unsigned char* repqueue, int repqueuelen);
Arguments are:
- data : the buffer allocated to hold the current data from the
driver
- datasize : the size (in bytes) of data
- command : the buffer allocated to hold the current command for the
driver
- commandsize : the size (in bytes) of command
- reqqueue : the buffer allocated to hold incoming configuration
requests; it should be an allocated as an array of playerqueue_elt_ts
- reqqueuelen : the length (in number of elements) of reqqueue
- repqueue : the buffer allocated to hold outgoing configuration
replies; it should be an allocated as an array of playerqueue_elt_ts
- repqueuelen : the length (in number of elements) of repqueue
In this case, SetupBuffers() will allocate PlayerQueue objects
to handle configurations; they will operate on the memory segments that you
provide.
Whether you let the CDevice constructor allocate your buffers or do it
yourself and then call SetupBuffers(), the relevant pointers and sizes
are stored in the following protected members of CDevice:
// buffers for data and command
unsigned char* device_data;
unsigned char* device_command;
// maximum sizes of data and command buffers
size_t device_datasize;
size_t device_commandsize;
// amount at last write into each respective buffer
size_t device_used_datasize;
size_t device_used_commandsize;
// queues for incoming requests and outgoing replies
PlayerQueue* device_reqqueue;
PlayerQueue* device_repqueue;
Because Player is a multi-threaded program, all access to shared buffers
must be protected by mutual exclusion locks. For this purpose, CDevice
contains a (private) pthread_mutex_t, and provides (protected) Lock() and Unlock() methods that call pthread_mutex_lock()
and pthread_mutex_unlock(), respectively (these methods are virtual;
thus a driver can override them in order to use a different mutual exclusion
mechanism). You should surround all accesses to any of a driver's shared
buffers or queues with calls to Lock() and Unlock(). The default
interface methods described in the following sections do exactly this.
8.2.3 Instantiation
Because Player supports multiple indexed instances of devices, your driver
should be prepared to be multiply instantiated (e.g., you generally
should not use global or other static variables) and you must provide
a function for instantiating it. When a new instance of a driver is
required, Player will call an appropriate instantiation function (see
Section 8.2.10 for how to register your instantiation
method). This function should return (as a CDevice*) a pointer to a
new instance of your device class. Since an object has not yet been created
when this function is called, it must be declared outside of the class
(or static within the class).
This function should match the following prototype:
CDevice* Foo_Init(char* interface, ConfigFile* cf, int section);
The arguments are:
- interface: the string name of the interface that the driver has
been requested to support
- cf: a object containing information gleaned from the user's
configuration file
- section: in which section of the configuration file your driver
was requested
You should check the requested interface to be sure that your driver can
support it. If you cannot, then return NULL. You should look in
the configuration file object cf for any options that may have been
specified for you driver (look at existing drivers and server/configfile.h for how to get options out).
When the first client subscribes to a device, the driver's Setup()
method is called. This method is set to NULL in CDevice:
virtual int CDevice::Setup() = 0;
Thus every driver must implement this method. After doing whatever is
required to initialize the device (e.g., open a serial port and spawn a thread
to interact with it), Setup() should return either zero to indicate that
the device was successfully setup, or non-zero to indicate that setup failed.
Since clients may immediately request data and since they may never send
commands, a driver's data buffer and command buffer should be sensibly
``zeroed'' in Setup().
When the last client unsubscribes from a device, the driver's
Shutdown() method is called. This method is set to NULL in CDevice:
virtual int CDevice::Shutdown() = 0;
Thus every driver must implement this method. After doing whatever
is required to stop the device (e.g., kill a thread and close a serial
port), Shutdown() should return either zero to indicate that device was
successfully shutdown, or non-zero to indicate that shutdown failed.
In order to leverage parallelism, most (but not all) devices use separate
threads to do their work. Because thread creation is not intuitively
compatible with C++ object context, some support is provided in CDevice
for starting and stopping threads. You are not required to use this support,
but you might find it useful.
The first step is to define in your class a public method Main() that
overrides the virtual declaration:
virtual void CDevice::Main();
Presumably your definition of Main() will contain a loop that executes
all device interaction. When you want to start your thread (probably in Setup()), call StartThread(). A new thread will be created;
in that thread your driver's Main() method will be invoked, with the
proper context of your driver's object. When you want to stop your thread
(probably in Shutdown()), call StopThread(), which will cancel and
join your thread; thus your thread should respond to cancellation requests
(even if it initially defers them) and should be in a joinable state (i.e., it
should not be detached).
Most drivers can use the default CDevice implementations of the
following methods; however, they are virtual and can be overridden if
necessary.
Whenever your driver has new data, it should call PutData():
virtual void CDevice::PutData(unsigned char* src, size_t len,
uint32_t timestamp_sec, uint32_t timestamp_usec);
Arguments are:
- src : pointer to the new data
- len : length (in bytes) of the new data
- timestamp_sec : the time at which the new data was produced
- timestamp_usec : the time at which the new data was produced
The default implementation of PutData() will Lock() access,
memcpy() your new data into device_data, save your len and
timestamp, and Unlock() access. If ts is NULL, then
PutData() will use the current time (either system or simulator,
as appropriate).
When a client wants to read data from your driver, the server will first
call GetNumData():
virtual size_t GetNumData(void* client);
Arguments are:
- client : a unique id for who wants the data
This method should return the number of data packets that are ready for the
given client at this time. The server will then call GetData() that
many times. The default implementation of GetNumData() simply returns
1, which is almost always the right thing. However, your driver can override
this method if you want.
When a client wants to read data from your driver, the server will
invoke GetData():
virtual size_t GetData(void* client, unsigned char* dest, size_t len,
uint32_t* timestamp_sec, uint32_t* timestamp_usec);
Arguments are:
- client : a unique id for who wants the data
- dest : pointer to a buffer into which to copy the data
- len : length (in bytes) of dest
- timestamp_sec : buffer into which to copy the time at which the data was
- timestamp_usec : buffer into which to copy the time at which the data was
produced
The default implementation of GetData() will Lock() access, memcpy() data from device_data into dest (up to len
bytes), retrieve timestamp information, Unlock() access, and return
how many bytes of data were copied.
Most devices can use the default CDevice implementations of the
following methods; however, they are virtual and can be overridden if
necessary.
When a client sends a new command for your device, the server will
invoke PutCommand():
virtual void PutCommand(void* client, unsigned char* src, size_t len);
Arguments are:
- client : a unique id for the source of the command
- src : pointer to the new command
- len : length (in bytes) of the new command
The default implementation of PutCommand() will Lock() access,
memcpy() the new command into device_command, save len,
and Unlock() access.
When you want the current command for your device, you should call
GetCommand():
virtual size_t CDevice::GetCommand(unsigned char* dest, size_t len);
Arguments are:
- dest : pointer to a buffer into which to copy the command
- len : length (in bytes) of dest
The default implementation of GetCommand() will Lock() access,
memcpy() the command from device_command into dest
(up to len bytes), Unlock() access, and return how many bytes of
command were copied.
Most drivers can use the default CDevice implementations of the
following methods; however, they are virtual and can be overridden if
necessary.
When a new configuration request arrives for your device, the server will
invoke PutConfig():
virtual int CDevice::PutConfig(player_device_id_t* device,
void* client,
void* data,
size_t len);
Arguments are:
- device : an identifier that indicates the device for whom the
request is intended
- client : a tag that will be used to route the reply back to the
right client (or other device)
- data : buffer containing the new request
- len : length (in bytes) of the new request
The default implementation of PutConfig will Lock() access,
push the request onto device_reqqueue, and Unlock() access. If
all is well, then zero is returned; otherwise (e.g., if the queue is full)
non-zero is returned and the server will send an error response message to the
waiting client.
To check for new configuration requests in your device, you should call
GetConfig():
virtual size_t CDevice::GetConfig(player_device_id_t* device,
void** client,
void *data,
size_t len);
Arguments are:
- device : an identifier that indicates the device for whom the
request is intended (useful when one queue is used for multiple devices,
as is the case with P2OS)
- client : a place to store a tag that will be used to route the
reply back to the right client (or other device)
- data : buffer into which to copy the new request
- len : length (in bytes) of data
For convenience, there is also a short form of GetConfig():
virtual size_t CDevice::GetConfig(void** client,
void* data,
size_t len);
The default implementation of GetConfig will Lock() access,
pop a request off device_reqqueue, and Unlock() access. If
there was a request to be popped then the size of the request is returned;
otherwise zero is returned, indicating that there are no pending requests.
If there is request then hang onto client because you will need to pass
it back in PutReply().
After servicing a request, you must generate an appropriate reply; you do this
by calling PutReply():
virtual int CDevice::PutReply(player_device_id_t* device,
void* client,
unsigned short type,
struct timeval* ts,
void* data,
size_t len);
Arguments are:
- device : an identifier that indicates from which the device the
reply comes (useful when one queue is used for multiple devices,
as is the case with P2OS)
- client : the tag that you received in GetConfig
- type : the type of the reply (see below)
- ts : pointer to time at which the configuration was executed
- data : the reply itself (if any)
- len : length (in bytes) of data
There are also two short forms of PutReply():
virtual int CDevice::PutReply(void* client,
unsigned short type,
struct timeval* ts,
void* data,
size_t len);
virtual int CDevice::PutReply(void* client,
unsigned short type);
The first short form assumes that the caller is the originator of the reply.
The second short form further assumes that the reply is zero-length and
that it should be stamped with the current time.
The default implementation of PutReply will Lock() access,
push the reply onto device_repqueue, and Unlock() access.
If the reply queue is full (which should not happen in practice) then -1
is returned; otherwise a non-negative integer is returned. The given type will be used as the message type for the reply that will be sent to
the client; it should be one of:
- PLAYER_MSGTYPE_RESP_ACK : the configuration was successful
- PLAYER_MSGTYPE_RESP_NACK : the configuration failed
If ts is NULL, then the current time is filled in. Zero-length
replies are valid (and frequent).
The server will periodically check for replies from your device by calling
GetReply():
virtual int CDevice::GetReply(player_device_id_t* device,
void* client,
unsigned short* type,
struct timeval* ts,
void* data,
size_t len);
Arguments are:
- device : an identifier indicating from which the device the reply
has come
- client : a tag to be matched
- type : place to copy the type of the reply
- ts : place to copy the time at which the configuration was executed
- data : buffer into which to copy the reply
- len : length (in bytes) of data
The default implementation of GetReply() will Lock() access,
pop a reply off device_repqueue, Unlock() access, and return the
length of the reply. Because zero-length replies are valid, GetReply()
will return -1 to indicate that no reply is available.
8.2.10 Registering your device
In order to inform the server about the availability of your driver,
you must add it to driverTable, a global table of drivers
that may be instantiated. You should add your driver by calling AddDevice() in the function register_devices(), declared in server/deviceregistry.cc:
int AddDriver(char* name, char access,
CDevice* (*initfunc)(char*,ConfigFile*,int));
Arguments are:
- name : the name of your driver
- access : the allowable access mode for your driver; should be
one of:
- PLAYER_READ_MODE
- PLAYER_WRITE_MODE
- PLAYER_ALL_MODE
- initfunc : a function that can be used to create
a new instance of your device (see Section 8.2.3)
You may find it convenient to write a registration function, e.g.:
void SickLMS200_Register(DriverTable* table)
{
table->AddDriver("sicklms200", PLAYER_READ_MODE, SickLMS200_Init);
}
You should also #include your device's class header in deviceregistry.cc. To encourage modularity of the server by allowing
drivers to be left out at compile-time, it is customary to make both the
#include and AddDriver() conditioned on compiler directives.
For example:
#ifdef INCLUDE_SICK
void SickLMS200_Register(DriverTable* table);
#endif
...
void
register_devices()
{
...
#ifdef INCLUDE_SICK
SickLMS200_Register(driverTable);
#endif
...
}
8.2.11 Compiling your device
Finally, you need to compile your device and link it into the server binary.
You really need to know something about GNU Autotools to do this. As such,
look at how the existing drivers are linked in.
8.2.12 Building a shared library
As an alternative to statically linking your device driver directly into the
Player binary, you can build your driver as a shared object and have Player
load it at run-time. If you choose to take this path, then you should still
follow most of the directions given in the previous sections, except for the
registration and compilation details in
Sections 8.2.10-8.2.11.
Instead of registering your device in deviceregistry.cc, you should
do so in an initialization function that will be invoked by the loader.
You must declare this initialization function, as well as a finalization
function, in your driver code. For example, in order to build the sicklms200 driver as a shared object, the following code is added
to sicklms200.cc:
#include <drivertable.h>
extern DriverTable* driverTable;
/* need the extern to avoid C++ name-mangling */
extern "C" {
void _init(void)
{
driverTable->AddDriver("sicklms200", PLAYER_READ_MODE, SickLMS200_Init);
}
void _fini(void)
{
/* probably don't need any code here; the destructor for the device
* will be called when Player shuts down. this function is only useful
* if you want to dlclose() the shared object before Player exits
*/
}
}
The _init() function will be invoked by the loader when Player
calls dlopen() to load your driver. The _fini() function
will be invoked when the library is dlclose()ed; however, Player never
closes your library explicitly, so _fini() will be called when Player
exits.
The details of building a shared object vary from system to system, but the
following example, which works with g++ on Linux, should get you started:
$ g++ -Wall -DPLAYER_LINUX -g3 -I$PLAYER_DIR/server -c sicklms200.cc
$ g++ -shared -nostartfiles -o sicklms200.so sicklms200.o
Having built your driver library, tell Player on the command-line to load it
(as described in Section 2.2), e.g.:
$ player -d sicklms200.so
Note that the dynamic loading functionality is still somewhat experimental,
and is not currently used by any core Player drivers. However, it should
work. If you use shared libraries, please let us know about your experiences.
Next: Bibliography
Up: 8. Architecture
Previous: 8.1 Server Structure
Contents
2004-06-02