Tutorial 1: Creating a new driver for the acquisition server

  • NB: Document based on OpenViBE 0.8.0 (30-sept-2010), small tweaks for 1.2.2 (22-nov-2016)

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 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 the multiple connected clients.

The connection between the hardware and the driver does not really matters to the acquisition server. Some hardware manufacturer will provide an API so that the driver has direct access to the peripheral through 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 a set of information / measures and to format all of them according to the format of OpenViBE.

How is the driver used ?

First, the driver has to give a 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, containing 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 data, the driver is used by the acquisition server at different stages of the execution. The three most important are :

  • configuration
  • intialization / uninitialization
  • acquisition

The configuration stage can be used by the driver to request the header’s information 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 (may be easier for the acquisition server user). For example, a driver with physical connection may not provide subject age or gender. Such a driver will need a configuration phase before it can be used.

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

Now that you know what a driver is, we shall start coding it. We provide a developer tool named Skeleton-generator, that generates for you all the files you will need to build the driver.

Using the Skeleton-Generator

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. Let’s take a closer look at the skeletons now.

Coding the driver

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

Here comes the driver skeleton generated.

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:

                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 in details the implementation of these functions. 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_pCallback(NULL)
        ,m_ui32SampleCountPerSentBlock(0)
        ,m_pSample(NULL)
{
        m_oHeader.setSamplingFrequency(128);
        m_oHeader.setChannelCount(4);
}

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.

 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;
        }
        return true;
}

Acquisition

To acquire the data, we must firstly initialize the driver. The initialize function will be called by the acquisition server with a given callback, to be feeded with samples. If the hardware needs initialization, put the dedicated code in this function. The uninitialize function desallocate properly everything.

 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 EEG 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 to the generic oscillator. This driver does not connect to any hardware. Instead, it produces the samples itself using 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 bring your driver folder to the attention of 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 adds them to the build if so. This usually means headers and libraries of your device API. 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,

 #include "your-driver-name/ovasCDriverYourDriverName.h"

 // ...

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

   m_vDriver.push_back(new OpenViBEAcquisitionServer::CDriverYourDriverName(m_pAcquisitionServer->getDriverContext()));

   // ...
 }

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

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.