next up previous contents
Next: Bibliography Up: 8. Architecture Previous: 8.1 Server Structure   Contents

Subsections

8.2 Adding a new device driver

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.

8.2.1 Constructors

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: 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: 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;

8.2.2 Locking access to buffers

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: 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).

8.2.4 Setup

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().

8.2.5 Shutdown

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.

8.2.6 Thread management

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).

8.2.7 Data access methods

Most drivers can use the default CDevice implementations of the following methods; however, they are virtual and can be overridden if necessary.

8.2.7.1 PutData

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: 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).

8.2.7.2 GetNumData

When a client wants to read data from your driver, the server will first call GetNumData():
    virtual size_t GetNumData(void* client);
Arguments are: 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.

8.2.7.3 GetData

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: 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.

8.2.8 Command access methods

Most devices can use the default CDevice implementations of the following methods; however, they are virtual and can be overridden if necessary.

8.2.8.1 PutCommand

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: The default implementation of PutCommand() will Lock() access, memcpy() the new command into device_command, save len, and Unlock() access.

8.2.8.2 GetCommand

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: 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.

8.2.9 Configuration access methods

Most drivers can use the default CDevice implementations of the following methods; however, they are virtual and can be overridden if necessary.

8.2.9.1 PutConfig

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: 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.

8.2.9.2 GetConfig

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: 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().

8.2.9.3 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: 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:

If ts is NULL, then the current time is filled in. Zero-length replies are valid (and frequent).

8.2.9.4 GetReply

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: 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: 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 up previous contents
Next: Bibliography Up: 8. Architecture Previous: 8.1 Server Structure   Contents
2004-06-02