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.
CDevice::CDevice(size_t datasize, size_t commandsize,
int reqqueuelen, int repqueuelen);}
Arguments are:
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:
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;
CDevice* Foo_Init(char* interface, ConfigFile* cf, int section);The arguments are:
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().
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.
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).
virtual void CDevice::PutData(unsigned char* src, size_t len,
uint32_t timestamp_sec, uint32_t timestamp_usec);
Arguments are:
virtual size_t GetNumData(void* client);
Arguments are:
virtual size_t GetData(void* client, unsigned char* dest, size_t len,
uint32_t* timestamp_sec, uint32_t* timestamp_usec);
Arguments are:
virtual void PutCommand(void* client, unsigned char* src, size_t len);
Arguments are:
virtual size_t CDevice::GetCommand(unsigned char* dest, size_t len);
Arguments are:
virtual int CDevice::PutConfig(player_device_id_t* device,
void* client,
void* data,
size_t len);
Arguments are:
virtual size_t CDevice::GetConfig(player_device_id_t* device,
void** client,
void *data,
size_t len);
Arguments are:
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().
virtual int CDevice::PutReply(player_device_id_t* device,
void* client,
unsigned short type,
struct timeval* ts,
void* data,
size_t len);
Arguments are:
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:
virtual int CDevice::GetReply(player_device_id_t* device,
void* client,
unsigned short* type,
struct timeval* ts,
void* data,
size_t len);
Arguments are:
int AddDriver(char* name, char access,
CDevice* (*initfunc)(char*,ConfigFile*,int));
Arguments are:
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
...
}
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.