Tutorial 1: Creating a new driver for the acquisition server

  • NB: Document updated for OpenViBE 2.0.0 (22-dec-2017)

Introduction

This page is intended to driver developers and shows how to create a new acquisition driver for the OpenViBE acquisition server. The driver is a single object that interacts with the hardware acquisition peripheral and formats the acquired measures and information in a way the acquisition server understands. Once the acquisition server receives measures and information, it is able to send them to one or more connected clients.

The connection type between the hardware and the driver does not really matter to the acquisition server. Some hardware manufacturer will provide an API so that the driver has direct access to the peripheral through a physical connection (maybe USB, serial port or so). Some will provide a proprietary acquisition server, which allows connections through TCP/IP to stream the measures in real time. Whatever your hardware manufacturer provides, the OpenViBE driver is in charge of collecting the necessary information and data from the device and format all of them according to the format of OpenViBE.

The implementation starting point

There are essentially two approaches to implement a new driver. One is to take an existing driver (such as Generic Oscillator or some driver for a real device) and modify its code to suit your purposes. The alternative is to start with an ‘empty’ driver. For that we provide a developer tool named Skeleton-generator, that generates empty template files that you can fill to suit your needs. Which is the better choice may depend on your personal tastes.

In the following we first describe the main parts of the driver, and then explain the process with the Skeleton Generator. Many developers may find it more intuitive to first play with some existing driver, look at its code and then use this document as a reference to explain the contents of the driver.

How is the driver used ?

First, the driver has to declare its name to the acquisition server. This will be used in the acquisition server GUI to easily identify the hardware the user is working with. This is why you should give a precise name to the driver, using for example, the hardware manufacturer name and the hardware model name.

The driver essentially deals with two kinds of data :

  • the header
  • the buffer

The header is the part of data that does not change along time. It contains several identifiers about the experiment being done, information about the channels being acquired and so on. See OpenViBEAcquisitionServer::IHeader for more details about this header.

The buffer is the part of data that changes along time. It contains the different samples for each channel on a given time period which depend on the number of samples per sent channel. This value is given to the driver during the initialization phase.

To deal with these pieces of data, the driver is called by the acquisition server at different stages of the execution. The three most important are :

  • configuration
  • initialization / uninitialization
  • acquisition

The configuration stage can be used by the driver to request the header information from the user that won’t be available from the hardware. Depending on the hardware, all the information for the header may be found in the streamed data, resulting in an non-configurable driver (if possible, this may be easier for the acquisition server user). For example, a driver with a physical connection may not provide subject age or gender. Such a driver will need a configuration phase if such information needs to be delivered.

In the initialization stage, the driver requests readiness status from the hardware. Once the driver is initialized (this means OpenViBEAcquisitionServer::IDriver::initialize returns), the driver should have a complete header ready to be sent to the acquisition server. The driver’s OpenViBEAcquisitionServer::IDriver::loop is regularly called so that the driver may be able to keep the connection to the device alive, dropping some data if needed.

The acquisition stage comes when OpenViBEAcquisitionServer::IDriver::start is called. From this stage, the driver is regularly requested to provide new data within the OpenViBEAcquisitionServer::IDriver::loop function.

A schematic representation of the automaton for a driver is here below :


The driver automaton

Using the Skeleton-Generator

Skeleton Generator will provide you empty .cpp/.h templates for implementing your driver. This is an alternative to making a driver by customizing an existing driver.

The Skeleton-generator needs the following information :

  • Author name
  • Company/Affiliation
  • Driver name
  • Class name
  • Min & Max channel count
  • Sampling frequencies
  • Directory where the files will be generated

And will generates the following files :

  • the driver class code (.h and .cpp)
  • the configuration class code (.h and .cpp)
  • the GtkBuilder interface file (.ui)
  • a readme.txt file that explains how to use the generated files.

The skeleton generator GUI

Each field has its own conditions, use the “Help” buttons for detailed examples. Once all fields are filled, you must check it by pressing the dedicated button. A report will be displayed, asking you to modify the wrong fields if any. If everything is good, you can press the button “Generate!” to generate the files.

Coding the driver

Coding the driver consists in implementing an OpenViBEAcquisitionServer::IDriver object.

Lets look at an skeleton driver, such as one obtained from Skeleton Generator.

The header

The header would look like this :

#include "../ovasIDriver.h"
#include "../ovasCHeader.h"
#include <openvibe/ov_all.h>

namespace OpenViBEAcquisitionServer
{
        class CDriverTestSkGenerator : public OpenViBEAcquisitionServer::IDriver
        {
        public:

                CDriverTestSkGenerator(OpenViBEAcquisitionServer
                                ::IDriverContext& rDriverContext);
                virtual ~CDriverTestSkGenerator(void);
                virtual const char* getName(void);

                virtual OpenViBE::boolean initialize(
                        const OpenViBE::uint32 ui32SampleCountPerSentBlock,
                        OpenViBEAcquisitionServer::IDriverCallback& rCallback);
                virtual OpenViBE::boolean uninitialize(void);

                virtual OpenViBE::boolean start(void);
                virtual OpenViBE::boolean stop(void);
                virtual OpenViBE::boolean loop(void);

                virtual OpenViBE::boolean isConfigurable(void);
                virtual OpenViBE::boolean configure(void);
                virtual const OpenViBEAcquisitionServer::IHeader* getHeader(void)
                {
                        return &m_oHeader;
                }

                virtual OpenViBE::boolean isFlagSet(
                        const OpenViBEAcquisitionServer::EDriverFlag eFlag) const
                {
                        return eFlag==DriverFlag_IsUnstable;
                }

        protected:

                SettingsHelper m_oSettings;

                OpenViBEAcquisitionServer::IDriverCallback* m_pCallback;

                // Replace this generic Header with any specific
                // header you might have written
                OpenViBEAcquisitionServer::CHeader m_oHeader;

                OpenViBE::uint32 m_ui32SampleCountPerSentBlock;
                OpenViBE::float32* m_pSample;

        private:

                /*
                 * Insert here all specific attributes, such as USB port
                 * number or device ID.
                 * Example :
                 */
                // OpenViBE::uint32 m_ui32USBPort;
        };
};

We will now describe the implementation of these functions in detail. The implementation can be divided in 3 phases : the preliminary, the configuration, the acquisition.

Preliminary

The first part of the cpp file contains the constructor, the destructor and the getName function. The code is relatively simple and speaks for itself.

CDriverTestSkGenerator::CDriverTestSkGenerator(IDriverContext& rDriverContext)
        :IDriver(rDriverContext)
	,m_oSettings("AcquisitionServer_Driver_TestSkGenerator", m_rDriverContext.getConfigurationManager())
        ,m_pCallback(NULL)
        ,m_ui32SampleCountPerSentBlock(0)
        ,m_pSample(NULL)
{
        m_oHeader.setSamplingFrequency(128);
        m_oHeader.setChannelCount(4);

        // The following class allows saving and loading driver settings from the acquisition server .conf file
        m_oSettings.add("Header", &m_oHeader);
        // To save your custom driver settings, register each variable to the SettingsHelper
        //m_oSettings.add("SettingName", &variable);
        m_oSettings.load();	
}

CDriverTestSkGenerator::~CDriverTestSkGenerator(void)
{
}

const char* CDriverTestSkGenerator::getName(void)
{
        return "Simple test driver (p1)";
}

Configuration

The configuration phase calls the dedicated GUI. The CConfiguration object fills the header with information from the GUI : subject identifier, number of channel, channel names, sampling frequency etc. If the device has user-settable parameters that are expected to persist from one run of the server to another, the driver can declare these and their values (see above; and get them automatically saved/loaded by the Acquisition Server). This is done using a helper member SettingsHelper m_oSettings;. The usage of this class can be seen from the existing drivers.

 boolean CDriverTestSkGenerator::isConfigurable(void)
{
        return true; // change to false if your device is not configurable
}

boolean CDriverTestSkGenerator::configure(void)
{
        CConfigurationTestSkGenerator m_oConfiguration(m_rDriverContext,
                        OpenViBE::Directories::getDataDir() + "/applications/acquisition-server/interface-TestSkGenerator.ui");
        if(!m_oConfiguration.configure(m_oHeader))
        {
                return false;
        }
        m_oSettings.save();
        return true;
}

Acquisition

To acquire the data, we must first initialize the driver. The initialize function will be called by the acquisition server with a given callback. The callback is used later in loop() provide the sample buffers to the acquisition server. If the hardware needs initialization, put the dedicated code in the initialize function. The uninitialize function unallocate everything properly.

 boolean CDriverTestSkGenerator::initialize(
        const uint32 ui32SampleCountPerSentBlock,
        IDriverCallback& rCallback)
{
        if(m_rDriverContext.isConnected()) return false;
        if(!m_oHeader.isChannelCountSet()
                ||!m_oHeader.isSamplingFrequencySet()) return false;

        // Builds up a buffer to store
        // acquired samples. This buffer
        // will be sent to the acquisition
        // server later...
        m_pSample=new float32[m_oHeader.getChannelCount()
                        *ui32SampleCountPerSentBlock];
        if(!m_pSample)
        {
                delete [] m_pSample;
                m_pSample=NULL;
                return false;
        }

        // ...
        // initialize hardware and get
        // available header information
        // from it
        // ...

        // Saves parameters
        m_pCallback=&rCallback;
        m_ui32SampleCountPerSentBlock=ui32SampleCountPerSentBlock;
        return true;
}

boolean CDriverTestSkGenerator::uninitialize(void)
{
        if(!m_rDriverContext.isConnected()) return false;
        if(m_rDriverContext.isStarted()) return false;

        // ...
        // uninitialize hardware here
        // ...

        delete [] m_pSample;
        m_pSample=NULL;
        m_pCallback=NULL;

        return true;
}

Once the driver is initialized, the acquisition can start. The start and stop functions handle the connection and ask the hardware to start/stop sending the data.

boolean CDriverTestSkGenerator::start(void)
{
        if(!m_rDriverContext.isConnected()) return false;
        if(m_rDriverContext.isStarted()) return false;

        // ...
        // request hardware to start
        // sending data
        // ...

        return true;
}
boolean CDriverTestSkGenerator::stop(void)
{
        if(!m_rDriverContext.isConnected()) return false;
        if(!m_rDriverContext.isStarted()) return false;

        // ...
        // request the hardware to stop
        // sending data
        // ...

        return true;
}

Finally, the loop function reads the data and stores it in a buffer. This function is called as fast as possible by the acquisition server once the driver is connected (intialized).

boolean CDriverTestSkGenerator::loop(void)
{
        if(!m_rDriverContext.isConnected()) return false;
        if(!m_rDriverContext.isStarted()) return true;

        OpenViBE::CStimulationSet l_oStimulationSet;

        // ...
        // receive samples from hardware
        // put them the correct way in the sample array
        // whether the buffer is full, send it to the acquisition server
        //...
        m_pCallback->setSamples(m_pSample);

        // ...
        // receive events from hardware
        // and put them the correct way in a CStimulationSet object
        //...
        m_pCallback->setStimulationSet(l_oStimulationSet);

        return true;
}

For a simple working example of driver, please take a look at the generic oscillator. This driver does not connect to any hardware. Instead, it produces the samples itself using a sinusoidal signal. This can easily be tuned to match your needs for any specific driver and also could be used to test your OpenViBE platform.

Including the driver to the server

In order to get the driver visible in the Acquisition Server, you have to register the driver. It happens as follows,

  • Put your .h / .cpp files in the folder contrib/plugins/server-drivers/your-driver-name/src/.
  • Edit contrib/common/contribAcquisitionServer.inl. This inline file is compiled with Acquisition Server and registers the driver.
  • Edit contrib/common/contribAcquisitionServer.cmake. This script should make your driver visible to the build.
  • Edit contrib/common/contribAcquisitionServerLinkLibs.cmake. This script should call a CMake find script related to your driver. The called script should check if the components (if any) required by your driver are present in the system and add them to the build if so. The cmake file should make any headers and libraries required visible to the compilation, and install any files required runtime (.dlls for example) to the dist/bin/ directory. Please use include guards in your driver to ensure that the build does not try to compile your driver if its dependencies are not available.

You can use the code related to the previous drivers as examples.

The inline file usually has something like the following for your code,

 
#if TARGET_HAS_MyDriverStuff
#include "your-driver-name/ovasCDriverYourDriverName.h"
#endif

 // ...

 void initiateContributions(...)
 {
   // ...

#if TARGET_HAS_MyDriverStuff
   m_vDriver.push_back(new OpenViBEAcquisitionServer::CDriverYourDriverName(m_pAcquisitionServer->getDriverContext()));
#endif

   // ...
 }

If you used the skeleton-generator, please look at the readme file for further details.

Best practices

To ensure that your driver is of high quality and behaves well, the following practices should be used,

  • During operation in loop() the driver should not rely on any sleep() type call as their precision cannot be guaranteed on non-realtime operating systems. Instead, the driver should wait for your device to provide a hardware wakeup event indicating more data is available for reading, then read it and pass it with the callback, and return from the call as soon as possible. The hardware event wait should have a timeout.
  • If the driver creates any threads, these should be launched in the member function initialize() and terminated by uninitialize(). Do not launch threads in the driver constructor. This way the threads are not unnecessarily launched for users who are not using your driver.
  • The driver should carry all startup operations that have a non-negligible latency in the call initialize(), or if not possible, in start(). The function loop() is expected to run steadily from call to call, including the first call.
  • In normal conditions the driver should work well with the server’s drift correction feature disabled. If not, it may be indication of a problem which should be addressed. For example, if you see a steady negative drift right after you press Play in the server, this may be an indication that the first call to loop() has a significant latency, which it should not have (see above). The basic BCI examples (motor imagery, P300, SSVEP) bundled with OpenViBE should all work without drift correction. If the contrary is the case, please contact us.
  • If your driver relies on any material which is not part of the source tree (such as proprietary dlls and headers), surround all your .cpp/.h files with include guards so that OpenViBE can still be compiled if these materials are not present. The CMake script detecting the presence of your materials should declare the guard.
  • Finally, live testing with real subjects and use-cases should be performed to ensure the driver works.

Conforming to these practices is especially important for all drivers which are intended to be considered for inclusion in to the OpenViBE distribution.

Advanced developments

Your driver may be functionnal, but not complete. You should be able to connect it and start it in the Acquisition Server, however more developments are possible to improve your driver. The following tutotials will help you with more advanced developments:

Support

In case you have problem in developing a driver, you can try contacting us.

Enjoy OpenViBE !

This entry was posted in Acquisition drivers and tagged , , . Bookmark the permalink.