Implementing the Gadgeteer Wrapper Class

The Gadgeteer wrapper class has the job of passing samples read from the standalone driver off to the Input Manager. Depending on the device type, a given sample must be of a certain form. This is where sample buffers come into play. We will discuss sample buffers later in this section, but the possible sample buffer types are the following:

Choose the Base Class(es)

As discussed earlier in the section called “Device Types”, all device drivers in Gadgeteer must derive from one or more classes based on the device type. If a driver is to be used with the Remote Input Manager (i.e., there exists a desire to share a device between two or more computers), then the base class must be gadget::InputMixer<S, T> with appropriate device type classes given as the template parameters. If, for whatever reason, the device will not be used with the Remote Input Manager, it may derive from one or more of the device type classes directly using multiple inheritance.

For example, to make a driver that registers button presses, derive from gadget::Digital:

class ButtonDevice
   : public gadget::InputMixer<gadget::Input, gadget::Digital>

Suppose that a joystick driver supporting buttons and movement is needed. In this case, an additional component, this one for analog input, is needed for the X and Y axes. Since the device is both digital and analog, its class must derive from both gadget::Digital and gadget::Analog using C++ multiple inheritance:

class JoystickDevice
   : public gadget::InputMixer<gadget::Input,
                               gadget::InputMixer<gadget::Digital,
                                                  gadget::Analog> >

Note

To use the joystick in place of a tracker, it should derive instead from gadget::Position. This way, you can replace real trackers with your joystick “pseudo tracker”. The main idea is that to be able to replace one device with another, the alternate device class must derive from the same base classes as the original device.

Using basic class declaration for ButtonDevice from above, we will proceed with the implementation of the driver class. First, there are six member functions that must be implemented:

startSampling()

virtual bool startSampling();

Within this function, a new thread is started. This thread is used to sample the data from the device. The thread creation step may look something like the following:

vpr::ThreadMemberFunctor<ButtonDevice>* functor =
   new vpr::ThreadMemberFunctor<ButtonDevice>(this,
                                              &ButtonDevice::sampleFunction,
                                              NULL);
mThread = new vpr::Thread(functor);

The above creates a thread that will execute ButtonDevice::sampleFunction(), a non-static member function in the class ButtonDevice. The implementation of that method would be similar to the following in most cases

void ButtonDevice::sampleFunction(void* arg)
{
   // Keep working until mRunning becomes false.
   while ( mRunning )
   {
      this->sample();
   }
}

The thread can be tested for validity using the method vpr::BaseThread::valid().

stopSampling()

virtual bool stopSampling();

The job of this function is to kill the thread created in startSampling().

sample()

virtual bool sample();

This method reads data from the device and stores it for later use by getDigitalData(). Note that ButtonDevice::sampleFunction(), defined above, invokes this method.

Gadgeteer devices typically use triple-buffered data management. This is done to ensure that data is not being written into a buffer when the Input Manager is trying to read the most recent value. The gadget::Input class defines three variables to help programmers keep track of which buffer is in use at any given time: gadget::Input::current, gadget::Input::valid, and gadget::Input::progress. The sampled data would be read into a three-element array of the correct type (this is driver-specific). When writing the freshly sampled data into the array, use gadget::Input::progress:

mSampledDigitalData[gadget::Input::progress] = sampled_digital_value;

updateData()

virtual void updateData();

Triple-buffered device drivers use this method to swap the data indices. The member function is usually implemented as follows:

void ButtonDevice::updateData()
{
   vpr::Guard<vpr::Mutex> updateGuard(lock);

   // Copy the valid data to the current data so that both are valid
   mSampledDigitalData[current] = mSampledDigitalData[valid];

   // swap the indices for the tri-buffer pointers
   gadget::Input::swapCurrentIndexes();
}

Note the use of a vpr::Guard<> object to synchronize access to the mSampledDigitalData array. This is needed because the sampling and the reading are occurring in separate threads, but both threads need access to mSampledDigitalData.

getElementType()

static std::string getElementType();

In the getElementType() function, the element type of the device must be returned. Its name must be as it appears in the configuration definition file for the driver. For example, the implementation for the simple button driver would appear as:

std::string ButtonDevice::getElementType()
{
   return std:string("ButtonDevice");
}

At this time, it is useful to point out that every Gadgeteer device needs an element type associated with it. An element type is similar to a struct in C or C++. The data structure is defined in an configuration definition file (which usually has the extension .jdef). Once defined, the type for a new driver can be used in JCCL configuration files.

getDigitalData()

virtual int getDigitalData(int devNum = 0);

The Input Manager uses this method to read digital data sampled by the driver. This is when the triple-buffered data scheme becomes especially valuable. To provide the Input Manager with the most up-to-date sample, use gadget::Input::current as the index, as shown below:

int JoystickDevice::getDigitalData(int devNum)
{
  return mSampledDigitalData[current];
}

Note that in this example, the parameter devNum is ignored. This is not always the case. Indeed, this button driver would likely have support for more than one button, and in that case, we would use devNum as the index into an array or vector containing data sampled from all the buttons.

getAnalogData()

There are other methods that must be implemented depending on the classes from which a given driver class derives. In the joystick example given earlier, the method getAnalogData() would have to be implemented in addition to getDigitalData(). The prototype for getAnalogData() is:

virtual float getAnalogData(int devNum = 0)

The joystick driver would use this to return values for the X and Y axes. The data here is more complex because it would be for triple-buffered two-dimensional samples. An implementation might look similar to the following:

float JoystickDevice::getAnalogData(int axis)
{
   vprASSERT(axis >= 0 && axis <= 1 && "only 2 axes (x and y) available");
   return mSampledAnalogData[current][axis];
}

In this driver, the integer argument to the method is used to represent either the X or the Y axis. The assertion ensures that a valid axis index is passed.

Register the Driver with the Input Manager

Device driver registration is done through a template type called gadget::DeviceConstructor<T>. When this type is used with a special “factory function” called initDevice(), the driver can be used as a plug-in to the Input Manager. While there are some drivers that cannot currently be loaded dynamically, for those that can, we implement an “entry point” function named initDevice(). Because we are dealing with C++ code, we must indicate to the compiler that this is a C function, so no name mangling should occur when its symbol table entry is created. We do this by wrapping the function body in an extern "C" block. For cross-platform plug-in capabilities, we use the GADGET_DRIVER_EXPORT() macro. On Win32 systems, this will add the appropriate type modifiers to declare initDevice() as a function exported by the DLL that will be compiled. For other platforms, the macro simply evaluates to the void type. (These details are handled within the DriverConfig.h header.)

With all of that, we can now write the body for initDevice(). No declaration in a header file is needed because this function will be looked up dynamically at run time. The implementation of initDevice() will appear in ButtonDevice.cpp as follows:

#include <gadget/Devices/DriverConfig.h>
#include <gadget/Type/DeviceConstructor.h>
#include "ButtonDevice.h"

extern "C"
{

GADGET_DRIVER_EXPORT(void) initDevice(gadget::InputManager* inputMgr)
{
   new gadget::DeviceConstructor<ButtonDevice>(inputMgr);
}

}

The new device driver can be compiled into a standalone library (.so, .dll, or .dylib are the usual suffix choices for plug-ins). This library will act as the Input Manager plug-in. In this way, there is no need to modify the Gadgeteer source code to add a new driver. Thus, the driver code is collected into a cohesive unit that can be distributed as a plug-in (in other words, a component) for Gadgeteer.

Runtime driver registration depends on the Input Manager configuration. Assuming a UNIX-like environment, the Input Manager could be configured to load our driver plug-in using the following configuration file:

<?xml version="1.0" encoding="UTF-8"?>
<?org-vrjuggler-jccl-settings configuration.version="3.0"?>
<configuration
  xmlns="http://www.vrjuggler.org/jccl/xsd/3.0/configuration"
  name="Configuration"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.vrjuggler.org/jccl/xsd/3.0/configuration http://www.vrjuggler.org/jccl/xsd/3.0/configuration.xsd">
   <elements>
      <input_manager name="Button Device Input Manager" version="2">
         <driver_path>${HOME}</driver_path>
         <driver>ButtonDevice_drv</driver>
      </input_manager>
   </elements>
</configuration>

Here, the driver plug-in is named ButtonDevice_drv.so (or some other platform-specific name), and it is found in the user's home directory.