Writing a Player Plugin

From RoboWiki
Jump to: navigation, search

This article is a walkthrough for creating a plugin/driver for Player. It aims to cover the basics in enough detail that anyone with some familiarity with C++ can follow along and create their own plugin. It is recommended to review the driver architecture first but not necessary.

Contents

What is a plugin?

Player includes several pre-defined interfaces for your device or algorithm. The role of each plugin is to create the link between your device or algorithm and the pre-defined interface(s) that best represent it. These pre-defined interfaces are what provide the common framework that makes Player so modular and easy to work with. A list of the interfaces can be found Glutera, Been pink.

Sensors

The GPS interface, for example, provides a datatype that lists information like latitude, longitude, altitude, number of satellites, and other relavent GPS-specific information. Any GPS device on the market will provide this data, but each in its own particular way: through NMEA sentences over serial, a proprietary USB interface, or maybe even over bluetooth using a virtual serial port and a vendor-specific format. The role of the plugin driver in this case is to read, extract, process, and pass all related information from the sensor through the Player standard protocol to the Player server. Toko bunga jakarta, Toko bunga online

Devices

What if the device isn't passive and requires input information, like a motor controller would? Interfaces for devices like a robot arm or "limb" all provide functionality that facilitates communication with other devices. These drivers can react to input information, such as a target pose, and then act on the command. They can also provide feedback or other information. Say you told the arm to move somewhere it can't reach to. Your plugin can inform whichever device commanded it to move that the target is out of reach. Your plugin can also broadcast its current state, geometry, timestamps, and other useful information.

Algorithms

Sometimes what you're writing a plugin for isn't a physical device at all! A path planner, using the “planner” interface, may be a software algorithm that gets input from other sensors, generates a path to a waypoint, and provides its results to a device responsible for moving the robot. It may be hard to think of these higher-level algorithms on the same level as a device driver, but the functionality the plugin is providing is exactly the same. It is creating a structure where all of the information is abstracted into pre-defined datatypes. The data is then shared through a uniform format between different devices or algorithm instances.

Before you begin

The only prerequisite of developing your driver is to have Player correctly installed with all related development tools. Also make sure that the driver you need does not already exist; Check for a list of currently implemented drivers.

The first step of writing your driver is to decide which interfaces best represent your device. In some cases this is simple: If you have a laser range finder you would like to use, you can use the "laser" interface. If you're implementing a new path planning algorithm, you can use the "planner" interface.

It is also possible to include more than one interface in your plugin driver. If you have a robot that has a motor controller, analog range finders, and wheel encoders, you can write a plugin that uses the "position2d" and "ranger" functions. Position2d will handle motor commands and wheel odometry data, and "ranger" can handle the range finders. All of these interfaces in your plugin will be specified in the "provides" section of a configuration file.

Sometimes when you are writing a plugin, it will require information from other devices that are connected to the Player server. If you are writing a driver that will build a map of the area around a robot, your plugin might need direct access to laser and/or localization devices that are on your robot. You can write your plugin so that it will read directly from other devices that are running, which will be specified using the "requires" section of the config file.

The last step is to come up with a name for your driver. Usually you will want to use something that reflects what it does. If you are writing a driver for a hardware device like a SICK LMS200 you might use the name sicklms200 for your driver.

Now that you know what interfaces your device is going to provide and require, you can begin writing the plugin.

Laying the Framework

As mentioned before, plugin drivers are based on C++ and follow Object Oriented Design. This means that drivers follow a standard design defined by Player's base class.

To implement a driver, you need to create a new class that inherits the Driver base class and overload a few critical functions. This structure allows each plugin driver to conform to the same basic architecture, while allowing different functionality as needed.

These assume you're using the Driver class in 2.x and the ThreadedDriver class in 3.x

  • Driver(ConfigFile* cf, int section)
    • Constructor, providing configure file parsing
      • ConfigFile *cf: Represents the configuration file that is loading the driver.
      • int section: Used in configuration file parsing.
  • ~Driver ()
    • Destructor, release data/resources/connections
  • int Setup() (v2.x)
  • int MainSetup() (v3.x)
    • Allocates driver resources before it goes off into the Main loop; Different from the constructor since a driver may be stopped/started multiple times
  • int Shutdown() (v2.x)
  • void MainQuit() (v3.x);
    • Cleans up driver resources, but does not terminate the driver. Different from the destructor since a driver may be stopped/started multiple times
  • void Main()
    • Threaded main loop, core driver function. All of the magic happens here.
  • int ProcessMessages(QueuePointer &resp_queue, player_msghdr *hdr, void *data)
    • A function that processes any proxy messages
  • void Update() (v2.x)
    • (Advanced feature) If the driver is not threaded, this function is called during every refresh
  • void MainQuit() (v2.x)
    • Cleanup/release function for the Main() threaded main loop function

There are also three related C functions needed to register your plugin driver. These are still necessary to implement even when using C++.

  • Driver* PlayerDriver_Init(ConfigFile* cf, int section)
    • C-Style driver initialization
      • ConfigFile *cf: Represents the configuration file that is loading the driver.
      • int section: Used in configuration file parsing.
  • void PlayerDriver_Register(DriverTable* table)
    • C-Style driver registration
      • DriverTable* table: Table of loaded Player drivers
  • extern "C" { int player_driver_init(DriverTable* table) }
    • C-Style driver creation/instance function
      • DriverTable* table: Table of loaded Player drivers

The easiest way to get started with all of the correct formatting is by starting with the exampledriver.cc source file that comes with Player. Whether you downloaded the tarball or checked out the files from svn, the file should usually be located at the path examples/plugins/exampledriver/exampledriver.cc This file contains a framework for each of the required functions, and useful comments on where various segments of your code should go. Though the example does not separate code between header and source files, it is perfectly valid to do so. Do not worry if it looks overwhelming, we will go through each part individually throughout the article.

Registration functions

The example plugin driver provides a great framework for your plugin, but some of what is provided has to be changed. Let's begin with the class information for the driver. You can see that the plugin is creating a new class, called ExampleDriver, to represent the driver. At this point, you can use the find/replace command of your text editor to replace all instances of the string “ExampleDriver” with the name you came up with for your driver. Now the class name reflects what you're writing your device driver for. Throughout the rest of this walkthrough, I'll be using the string PlayerDriver.

All of the required functions are prototyped at the top of the file, don't forget to prototypes for any functions you may add. You can, and is recommended for complex drivers, separate these prototypes and source into header and source files, respectively.

The PlayerDriver_Init function does not need to be changed, it returns a pointer to an instance of the plugin driver you're creating.

The PlayerDriver_Register function inserts your driver into Player's internal Driver Table. As Player loads drivers on startup, it registers each driver loaded into an internal DriverTable. A little later, we will see that drivers use this table to get the information they need to connect to other drivers running in the same Player instance. By convention, the name string provided as the first parameter of AddDriver() should be all lower-case with no spaces or underscores. For instance, since my driver is called "PlayerDriver", I will use the string "playerdriver" in the AddDriver() function. This is to simplify driver naming in config files.

Constructor

The class constructor is where we will start to define what interfaces your plugin will provide and require, as well as process a given configuration file. This is not to be confused with your Setup function or with the start of the main loop.

Provides

Providing one interface

If your driver is providing a single interface, then the constructor function doesn't need much editing. Once you know the name of the interface (Interfaces are listed, you just need to fill in the correct interface ID/code where the example lists PLAYER_POSITION2D_CODE.

ExampleDriver::ExampleDriver(ConfigFile* cf, int section)
    : Driver(cf, section, false, PLAYER_MSGQUEUE_DEFAULT_MAXLEN, PLAYER_POSITION2D_CODE)
{
    // Constructor code here...
}

Player 3.x uses a slightly different structure. Instead of inheriting the Driver class, your plugin should inherit the ThreadedDriver class (assuming you're writing a threaded driver). It will make the code look like this:

ExampleDriver::ExampleDriver(ConfigFile* cf, int section)
    : ThreadedDriver(cf, section, false, PLAYER_MSGQUEUE_DEFAULT_MAXLEN, PLAYER_POSITION2D_CODE)
{
    // Constructor code here...
}

So, if you were to provide just a GPS interface, you would use the code PLAYER_GPS_CODE. This example uses PLAYER_POSITION2D_CODE, which means it's providing a position2d interface.

Providing multiple interfaces

If your driver is providing multiple interfaces, then you have to use a different set of parameters in the constructor. You don't want to specify just one interface in the constructor, so you will call a simpler version of the parent constructor. The only parameters you need to pass in this version are (cf, section). Let's say your driver is a robot arm, and is going to provide the limb and gripper interfaces. In this case your code might look like this:

PlayerDriver::PlayerDriver(ConfigFile* cf, int section)
    : Driver(cf, section)
{
    // Check if the configuration file asks us to provide a Limb interface
    if(cf->ReadDeviceAddr(&limbID, section, "provides", PLAYER_LIMB_CODE, -1, NULL) == 0)
    {
        // If the interface failed to correctly register
        if(AddInterface(limbID) != 0)
        {
            // Post an error string and quit the constructor
            PLAYER_ERROR("Error adding Limb interface\n");
            SetError(-1);
            return;
        }
    }
 
    // Check if the configuration file asks us to provide a Gripper interface
    if(cf->ReadDeviceAddr(&gripperID, section, "provides", PLAYER_GRIPPER_CODE, -1, NULL) == 0)
    {
        // If the interface failed to correctly register
        if(AddInterface(gripperID) != 0)
        {
            PLAYER_ERROR("Error adding Gripper interface");
            SetError(-1);
            return;
        }
    }
 
    // Constructor code here..
}

This segment of code has a few intricacies. First, the function ReadDeviceAddr is passed, which checks the configuration file's "provides" section for the correct interface and related information (PLAYER_LIMB_CODE, PLAYER_GRIPPER_CODE, etc). If there is indeed a device listed in the "provides" section with the correct interface, then the function will return 0, and the device address will be stored in the variable passed a pointer (gripperID, limbID). The code can then descend into the next if statement. The function AddInterface() uses the device address retrieved from the config file to associate itself with that specific interface. If it succeeds, then any messages sent to the device will be received by this driver. If adding the interface fails, then the plugin sets an error code (since a constructor cannot return a value), and exits the function early with a void return call. Player checks for an error code before continuing, and if it encounters one, will terminate the application posting relevant information.

You may also notice the function PLAYER_ERROR() being used to print out error messages we post. This is another built in feature of Player, it behaves similarly to printf. You should use it to convey errors in your plugin, they will be written to the terminal and to Player's runtime logfile (called .player in the directory you run player).

The device address variables limbID and gripperID are of type player_devaddr_t and should be stored within the class for later usage, like so:

private: // They could be stored as protected if this is to be a derived class
// Device Addresses
player_devaddr_t limbID;
player_devaddr_t gripperID;

You can read about the differences between public, private, and protected in the C++ article.

Requires

If your plugin requires information from other devices, you must search for the devices that will provide them within the constructor. This is similar to obtaining the "provides" from above. We'll use the example of requiring a laser interface from another device:

if (cf->ReadDeviceAddr(&laserID, section, "requires", PLAYER_LASER_CODE, -1, NULL) != 0)
{
    PLAYER_ERROR("Could not find laser interface!");
    SetError(-1);
    return;
}

If the code doesn't find laser's device address, it prints out an error message, sets an error code, and returns. Player will stop loading if it can't find the laser this plugin needs.

Similar to addresses in the "provides" section, the code is reading the device address of the laser from the configuration file. Again, the device address should be stored as a private or protected variable in the class, so we'll add it like so:

private:
// Device Addresses
player_devaddr_t limbID
player_devaddr_t gripperID
player_devaddr_t laserID

Now that we have the address of the laser we're subscribing to, we'll wait for the Setup command to use it.

Reading Config File Parameters

You may find it useful to set other parameters for your device in the config file. If your device requires opening a serial port, you may want to specify a port name and a baudrate. You can read a variety of different types of information out of the config file, by using some special functions provided from the. Although we'll cover config files in more detail later, we can use the following sample configuration file parameters:

port "/dev/ttyS0"
baudrate 9600
dimensions [2.3, 3.2]

When reading config file parameters, it's a good idea to store them as class-level variables. That way, they'll be available in all of the functions you call within the class, which will be useful later on. To read these config values into the variables, you can use the code:

// Read of type c-string/char buffer
this->port = cf->ReadString(section, "port", "/dev/ttyS0");
 
// Read of type integer
this->baudrate = cf->ReadInt(section, "baudrate", 9600);
 
// Read of type dimensions / double
this->length = cf->ReadTupleLength(section, "dimensions", 0, 0.0);
this->width = cf->ReadTupleLength(section, "dimensions", 1, 0.0);

The last parameter of each of these read functions is a default value, in case one of these parameters is missing from the config file. By default, all parameters are optional. If you decide there are parameters that are critical and cannot be ommited, you should check to make sure the Read function returned something other than the default value. If it doesn't, use SetError(-1); and return; to stop Player from continuing, though you should also post an error string with PLAYER_ERROR("Error string...");

The variables you use to store the values read out of the config file should, as mentioned above, be declared at the top of your class. We'll add them to what we've already declared in the protected section:

private:
 
// Device Addresses
player_devaddr_t limbID
player_devaddr_t gripperID
player_devaddr_t laserID
 
// Config File Options
char* port;
int baudrate;
float length, width;

Setup / MainSetup

The Setup function is called by Player once all of the drivers have successfully loaded. As the example driver comments note, Setup is where you initialize anything that is needed right before your main driver loop becomes active. It is a good place to verify that correct config file options were given and to set up serial ports and other hardware interfaces. The general rule of thumb is that if something only needs to be done once in order for your plugin to function properly, it should be put here. Otherwise, it will have to go into the Main function, which is the central threaded loop for processing.

The Setup function is also a place to signal that things went wrong before loading the main loop; For example checking if hardware initialization failed. If you encounter an error reading a serial port or don't receive the data you should be receiving, you can return a -1 and signal that the plugin didn't successfully Setup before the main loop, and should not load any further.

Subscribing to Other Interfaces

This is where we will take the devices we read from the "requires" section of the config file, and subscribe to them so we can receive the messages they publish. In the Requires section of this article, we decided that we were going to subscribe to a laser device, and stored its address in the variable laserID. Now we'll use that address to subscribe to the laser device, as shown:

// For deviceTable access, make sure you include <libplayercore/playercore.h>
 
// Check for access failure
if( (laserDevice = deviceTable->GetDevice(this->laserID)) == 0 )
{
    PLAYER_ERROR("Unable to locate the laser device");
    return -1; // Note how we can return a value from the Setup function
}
 
// Subscribe to the laser
if(laserDevice->Subscribe(this->InQueue) != 0)
{
    PLAYER_ERROR("unable to subscribe to the laser device");
    return -1;
}

You can see that this is actually a two step process. First we consult the device table and use the laser address we read out of the config file. This will return a pointer to the device's driver manages by Player. This device pointer is then used to subscribe to the messages that the laser driver is publishing, and put those published messages into our plugin's message queue. If the subscription process fails at any point, the function returns a -1, which will signal player that the plugin failed the Setup routine.

The Device pointer we used during subscription is another thing we have to add to our class declaration, which is getting rather long. Now the class member's section looks like:

private:
 
// Device Addresses
player_devaddr_t limbID
player_devaddr_t gripperID
player_devaddr_t laserID
 
// Config File Options
char* port;
int baudrate;
float length, width;
 
// Subscribed Device Pointers
Device* laserDevice;

After all of your configuration is done with, your plugin should call the function StartThread(). This signals to Player that all of your configuration is complete, and Player can start your driver's Main loop.

Running your Plugin on Startup

The setup function is called when Player starts a driver. By default, drivers are only started when another device tries to connect to your plugin. When all devices disconnect from your plugin, the plugin shuts down. This behavior keeps devices that aren't being used from taking resources from devices that are being used. But in some cases, it is preferable to keep the driver running indefinitely, especially if the setup process is very long or involved. By adding the line "alwayson 1" to your driver's config file, you can force Player to run the setup function and start the driver as soon as the plugin is loaded. The plugin will keep running until you terminate the Player process.

Shutdown / MainQuit

Player will run the Shutdown function when it is unloading and stopping your driver. This function gives you the opportunity to disconnect from any ports your plugin may be using, send messages to your devices to disconnect cleanly, de-allocate memory, save state information for later use, etc. Your plugin will be stopped when all other devices that were connected become disconnected, or when the Player server is terminated.

Process Message

Process Message provides the core functionality of the Player server. Different interfaces interact with each other by sending messages back and forth through Player. The different message types and their brief descriptions are listed. Each interface supports a certain subset of messages. In the interface specification, selecting a desired interface will bring up a list of enumerated entries (defined as variable-like macros), that use the format PLAYER_INTERFACE_MESSAGETYPE_MESSAGE. These commands are the ones that your plugin can parse and respond to. We'll go over each type below.

Commands

This message type is used to give instructions to your driver. The Command message is usually used when no response is necessary from your driver. The limb and gripper interfaces use the command for things like setting the limb position, opening and closing a gripper, etc. The position2d interface uses commands to receive new velocities or positions for motors, etc.

For example:

// Limb Home position message
if (Message::MatchMessage(hdr, PLAYER_MSGTYPE_CMD, PLAYER_LIMB_CMD_HOME, limbID))
{
    // Call the HomePosition function
    return HomePosition();
}

This is a simple example of a command to a limb interface to move the limb to its home position. The message is checked to see that it is indeed a command to go to the home position addressed to the correct device (limbID is the device's own address). Once that's verified, the plugin calls another function called HomePosition, which returns a 0 on success and -1 on failure. This result is returned to Player signaling that the message was able to be evaluated properly.

If a given command was not interpreted, it should return a -1.

Requests

Requests are a message from another driver to access data that isn't published normally. The request message type is usually used in several different ways:

  • Request information that isn't published regularly
  • Send a command that requires some kind of response

The first is fairly straightforward. Plugins typically publish a data packet on every iteration, but sometimes the information you desire has to be requested separately. The laser driver, for instance, publishes laser scan information on every iteration. But to get the laser's geometry, a special request has to be sent. This is because the laser's dimensions are not typically going to change, so publishing this information constantly just adds congestion to network traffic. The request allows your plugin to respond with the desired information only when necessary.

The next usage isn't so obvious, but is important to understand. The Command message type doesn't support any kind of response or feedback to the sender. To send a command that requires some kind of feedback, you must use the request message type. Some of the request messages effectively act like commands, but fall under the request message type only because they need to send feedback to the sender. In order to enable and disable the motor power in the position2d interface, you have to send a request with the desired state (enabled or disabled) in the message. The requested motor state is evaluated, and the plugin will respond with either an ACK (acknowledged) or NACK (not acknowledged) packet, based on whether or not the configuration change was successful. Unlike the geometry request, this particular request doesn't pass any data back to the requester. The message data field is empty or null, as the "ack" or "nack" section of the response message is sufficient feedback.

Let's use the motor power request for position2d as an example:

 
//Motor Power Message
if(Message::MatchMessage(hdr,PLAYER_MSGTYPE_REQ, PLAYER_POSITION2D_REQ_MOTOR_POWER, this->position2d_id))
{
    /* motor state change request
     *   1 = enable motors (any non-zero number will work)
     *   0 = disable motors (default)
     */
 
    // Match message size for validation
    if(hdr->size != sizeof(player_position2d_power_config_t))
    {
        PLAYER_WARN("Arg to motor state change request wrong size; ignoring");
        return(-1);
    }
 
    // Cast the given data into the correct datatype as a pointer
    player_position2d_power_config_t* power_config = (player_position2d_power_config_t*)data;
 
    // Assign the requested motor state to our device's internal motor state.
    motorPower = power_config->state;
 
    // Stop the motors if our power is 0
    if (this->motorPower == 0)
    {
        // System specific code to stop motors
        printf ("Disabling Motors...\n");
 
        FormMotorCmd(serialout_buff, 0, 0);
        write(roboteqplugin_fd, serialout_buff, strlen(serialout_buff)+1);
 
        tcdrain(roboteqplugin_fd);
    }
 
    // Print a message that the motors are enabled if our power state is nonzero
    else if (this->motorPower != 0)
    {
        printf ("Motors enabled!\n");
    }
 
    // Publish an ACK response, the motor state was successfully evaluated
    Publish(this->position2d_id, resp_queue, PLAYER_MSGTYPE_RESP_ACK, PLAYER_POSITION2D_REQ_MOTOR_POWER);
    return 0;
}

This snippet of code contains all of the logic to handle a motor power request. The MatchMessage function checks the incoming message to see if it is a Motor power request to the device with address position2d_id (which is the plugin's own device address). If the message checks out, the plugin checks to make sure that the message is the expected size, as a form of error checking. After that, the message's data is cast back into the correct datatype. The command is then evaluated, and a response is published acknowledging that the motor power command was successfully evaluated. Then a 0 is returned to signal to Player that the message was able to be handled correctly.

Data

Data messages are published on every iteration of the driver's Main loop, and therefore don't exactly fall under the ProcessMessage command. The Data message for each interface provides the most useful and variable information to the other interfaces. For example, a laser interface provides the data message PLAYER_LASER_DATA_SCAN, which publishes laser scan information on each loop iteration. The GPS interface, and many other interfaces, use a packet called STATE. These packets generally contain information that changes a lot, and is used by other devices and drivers for decision making, etc. Let's look at how a data packet is formed for a driver that takes two laser scans and combines them into one new laser scan:

// create a new data packet
player_laser_data_t data;
 
// fill in angle, range, and resolution info
data.min_angle       = DTOR(min_angle);
data.max_angle       = DTOR(max_angle);
data.max_range       = 1.0;
data.resolution      = DTOR(scan_res);
data.ranges_count    = LASER_POINTS;
data.intensity_count = LASER_POINTS;
 
// Compare the scan points, insert the smallest ones into the outgoing laser scan array
for (int i=0; i < LASER_POINTS; i++)
{
    if (this->laser1ranges[i] > this->laser2ranges[i])
    {
        data.intensity[i] = laser2intensity[i];
        data.ranges[i] =this->laser2ranges[i];
    }
    else
    {
        data.intensity[i] = laser1intensity[i];
        data.ranges[i] = this->laser1ranges[i];
    }
}
 
// Increment outgoing scan ID
data.id = this->scan_id++;
 
// Make data available to other devices
this->Publish(m_laser_addr, PLAYER_MSGTYPE_DATA, PLAYER_LASER_DATA_SCAN, (void*)&data, 0, &time);

This segment came out of the Main function of a plugin that presents a Laser interface. The code creates a new laser data scan object on the stack, fills in all of the range, resolution, and angle data from information contained elsewhere in the driver. The laser scanpoints and intensity values are then added to the laser data structure, and the data message is published to all listening devices. The data packet, since it's constantly sent out, also has an index that increments each time a message is published. This is useful for other drivers to make sure they're receiving all of the information from this plugin.

Why is there so much typecasting?

Because the messages can carry so many datatypes between different devices, they are all cast to void* (no specific datatype) when they are transported. This lets all of the different messages of different datatypes all use the same message transfer functions to pass around the Player server. The message type information, which are just enumerations as integers, are used at the receiving end to decode what the datatype should be, and then the void* data can be cast back into the correct format and processed as per normal. Every message has one datatype associated with it, which are all documented in the Interface Specifications.

Which Messages should I Process?

Each interface has a special set of messages that it can handle, all of which are listed in the interface specifications. But which messages you choose to handle are strictly up to you. Some plugins can handle all of the available messages, some implement only a subset, and some don't handle any messages at all. Your plugin should definitely publish its data message, but any other messages that you want to handle are determined by the desired functionality and capabilities of your device.

Main

The Main function is the threaded core of your plugin; Simply meaning that your plugin driver's main runs in parallel to other plugin. Most Main functions are entirely contained in a while(true) or for(;;) loop. Both are equivalent, though some argue for(;;) is a language hack and while(true) is more standard. The main function has to have a few specific function calls in it:

  • pthread_testcancel() checks to see whether or not your plugin's thread was killed. If it was, it says that Player is trying to stop the driver. The function will cause the driver to break out of the Main() loop and go to the Shutdown routine.
  • ProcessMessages() is a call to Player, which will call your plugin's ProcessMessage() function with each message waiting in your plugin's message queue. Do not confuse ProcessMessages() and ProcessMessage(...); one is a Player abstraction and one is a critical driver function that you need to write.
  • usleep() is a sleep command that keeps your plugin from running at all times, which could consume most or all of your system's resources. It's good practice to add the usleep in your main loop, to keep your plugin's thread from consuming more resources than it needs. Depending on the performance you need, allowing your driver to run between 10 and 100 times per second is sufficient.

The Main function is where you should be reading in information from your devices and doing any calculations and post-processing of your data, or running your algorithm if you're not directly talking to a hardware device. After all of the processing is done, you should be forming a data packet specific to your interface, and publishing it to Player.

Let's look at a GPS driver plugin's main loop:

void PlayerDriver::Main()
{
    while(true)
    {
        // Check to see if Player is trying to terminate the plugin thread
        pthread_testcancel();
 
        // Process messages
        ProcessMessages(); 
 
        // Poll the serial port, return an error if the serial port can't be reached
        if (poll(fds, fd_count, 100) < 0)
        {
            PLAYER_ERROR1("poll returned [%s]", strerror(errno));
            continue;
        }
 
        // Read incoming data from the GPS over the serial port
        if (fds[0].revents)
        {
            if(ReadSentence(buf,sizeof(buf)))
            {
                PLAYER_ERROR("Error while reading from GPS unit.  Exiting...\n");
                exit(-1); // Quit the thread and kill the parent process
            }
 
            // Parse GPS Sentence, publish PLAYER_GPS_DATA_STATE message
            ParseSentence((const char*)buf);
        }
 
        // Sleep this thread so that it releases system resources to other threads
        usleep(10);
    }
 
}

You can see that the loop is very simple. It calls the test cancel, processes messages, and then updates its state and sleeps. This driver has had all of the functionality of reading from the serial port moved into a separate function called ReadSentence. The ParseSentence function, which is a helper function made by us, takes care of pulling the useful information out of the sentence received over the serial port, and publishes the information using the PLAYER_GPS_DATA_STATE message.

Design Considerations

There are several different aspects of Player that, when used properly, can help you make a much more robust driver. Look at the high-level needs and requirements of your plugin. Then, research and find the proxies an end-user as a client program would need. Finally, find the matching messages and build the driver from that list.

Error Checking

Every function and routine in the Player library provides some sort of feedback as to whether or not the function was able to complete successfully. Most functions in Player that don't return specific data return either a -1 for failure or a 0 for success, as standard per Unix functions. You should always take advantage of this fact, and use the returned data for error-checking and recovery (if at all possible).

Player also offers you the facility to transmit errors in your plugin driver's execution back to the Player server. Player expects the functions Setup, ProcessMessage, and Shutdown to return a -1 for failure and 0 for success. As explained in the Setup, ProcessMessage, and Shutdown sections of this article, these signals have a large impact on what the Player does as it's running your code. The constructor allows you to use the SetError() function to signal that an error took place during the initialization routine.

Finally, because the data in all received messages do not have a datatype (they are given as a void* array of memory), it's a good idea to get into the habit of checking to make sure the data received is the correct size. Using the sizeof() function, you can compare the wanted datatype size with the received data's size.

Compiling your Plugin

Player plugins compile as shared object (*.so) library files on the Linux platform. In order to compile your driver code, you'll need to set up the proper build environment based on which Player version you're using.

Player 2.x

The exampleplugin.cc file should have a file called "makefile.example" in the same directory. This is a sample makefile that you can use to make and debug your plugin driver. This is a very basic makefile though, we've created

SRC = playerdriver.cc
OBJLIBS = playerdriver.so
OBJS = playerdriver.o

CXX = g++

all: $(OBJLIBS)

$(OBJS): $(SRC)
	echo Building the PlayerDriver plugin...
	$(CXX) -Wall -fpic -g3 `pkg-config --cflags playercore` -c $(SRC)

$(OBJLIBS): $(OBJS)
	$(CXX) -rdynamic -shared -nostartfiles -o $@ $^

clean:
	echo Cleaning up the PlayerDriver plugin...
	rm -f $(OBJS) $(OBJLIBS)

This makefile template allows you to add all of your source files to the SRC variable, name your output library with the OBJLIBS variable, and specify your objects with the OBJS variable. In this format, all of your source files get compiled into object files, so SRC and OBJS will have the same names with different extensions. This template also provides the necessary dependencies for "make" to only compile the library if you've changed the source file. If you call make without changing any source files, it will not attempt to re-compile your driver.

Player 3.x

Player 3.x changes the build system from autotools to CMake. You can still use the above pkg-config method to compile your plugins, but CMake adds the option to use a totally different infrastructure. To compile the driver with CMake, you have to start by creating a file called CMakeLists.txt in the driver's source directory. The exampledriver directory contains an example "CMakeLists.txt" which looks like this:

CMAKE_MINIMUM_REQUIRED (VERSION 2.4 FATAL_ERROR)
PROJECT (example_player_driver)

# Include this CMake module to get most of the settings needed to build
SET (CMAKE_MODULE_PATH "/usr/local/share/cmake/Modules")
INCLUDE (UsePlayerPlugin)

PLAYER_ADD_PLUGIN_DRIVER (playerdriver SOURCES playerdriver.cc)

The CMAKE_MODULE_PATH depends on where you installed Player. The directory where exampledriver.cc is installed (usually /usr/local/share/player/examples/plugins/exampledriver) should contain a CMakeLists.txt file with the correct CMAKE_MODULE_PATH filled in. If you used the default options, the path will be /usr/local/share/cmake/Modules. However, if you changed the CMAKE_INSTALL_PREFIX during build time, the path should expand to $PREFIX/share/cmake/Modules. If you're unsure where Player was installed, type "which player" into the command line. The PREFIX will be the file path preceeding /bin/player. So if "which player" returns /usr/local/bin/player, the PREFIX is /usr/local, and the CMAKE_MODULE_PATH is /usr/local/share/cmake/Modules.

The important lines here are "INCLUDE (UsePlayerPlugin)" and "PLAYER_ADD_PLUGIN_DRIVER." The file UsePlayerPlugin works kind of like pkg-config --cflags playercore, in that it specifies all of the required include files to compile against Player. If you need to add extra include and library files, you an do so by adding arguments to the PLAYER_ADD_PLUGIN_DRIVER call. The following arguments are supported:

  • SOURCES
  • INCLUDEDIRS
  • LIBDIRS
  • LINKFLAGS
  • CFLAGS

So, for example, if you want to create a library called "playerplugindriver.so", with the source files "playerplugin.cc" and "imageprocess.cpp" and add the include dir "/usr/include/opencv", then the command would look like this:

PLAYER_ADD_PLUGIN_DRIVER (playerplugindriver SOURCES playerplugin.cc imageprocess.cpp INCLUDEDIRS /usr/include/opencv)

Once your CMakeLists.txt file is written, you can issue the command "cmake ." in your plugin directory, followed by "make"

Writing your Config File

Writing a config file to run your Player driver is a simple ordeal. You need to create a text file, usually with the extention .cfg, that tells player several things:

  • What the name of your plugin is
  • Where it can find the library file that you compiled for the plugin
  • Which Player interfaces your plugin provides
  • Which Player interfaces your plugin requires
  • Any other config options you specified when you wrote your plugin driver

The config file also associates the provides and requires between all of the different drivers. That is to say, if your plugin requires a laser interface, you must provide a laser interface somewhere else in the config file and use the same index. Let's look at an example of a config file that provides just one position2d interface:

driver
(
  name "playerdriver"
  plugin "/path/to/libplayerdriver.so"
  provides ["position2d:0"]
  port "/dev/ttyUSB0"
  baudrate 9600
  dimensions [2.0 1.5]
)

This is a complete "driver block" from a configuration file. Config files can provide several driver blocks. The config options listed here are the same ones that we used earlier on in the Reading Config Files section. Because you're using a plugin driver, you need to specify the path to the library file that you compiled your driver into in the "plugin" section. When using a built in driver, this option isn't necessary.

Config files can also associate different devices to each other. Let's say you have a laser running in a local session of player, another laser driver running on a different computer, and you want to use a driver to merge them into a third laser device. We can see this functionality in this sample:

driver
(
  name "sicklms200b"
  plugin "libsicklms200b"
  provides [ "laser:0" ]
  port "/dev/ttyS0"
  resolution 50
  range_res 10
  serial_high_speed_mode 1
  serial_high_speed_baudremap 300
  connect_rate [ 9600 500000 500000]
  transfer_rate 500000
  retry 4
  alwayson 1
)

driver
(
  name "passthrough"
  requires [":remotehost:6665:laser:0"]
  provides ["laser:1"]
}

driver
(
  name "lasermerge"
  plugin "liblasermerge.so"
  requires ["laser:1" "laser:0"]
  provides ["laser:2"]
  alwayson 1
)

This sample loads the sicklms200b driver as laser0, and gets laser1 from a different computer through the "passthrough" driver included in Player. These drivers are then merged in the "lasermerge" plugin driver, which requires lasers 0 and 1, and then provides laser 2. Lasermerge comes last, because in order to load it requires lasers 0 and 1 to be loaded. Player loads the drivers in the order they're listed in the config file, so load provides before any drivers that require them.

Debugging with gdb

Using GDB is a great way to track down errors and segmentation faults that may arise when you are creating your plugin driver. To run player in gdb, first type "gdb player" into your console. When it loads type "run configfile.cfg" without quotes. This will run Player with your specified config file inside of the gdb environment. When you encounter an error or segmentation fault, you can use the "list" command to see where in the code the fault took place. The "bt" command will print a backtrace of the stack configuration when the error occured.

For more details on using gdb, see our GDB article.

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox