Programming Tutorial:Implementing an Input Logger

From BCI2000 Wiki
Revision as of 09:56, 5 June 2023 by Mellinger (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

In this tutorial, you will learn how to implement a BCI2000 Input Logger component. Here, Input Logging refers to recording the state of input devices, such as joysticks, keyboards, or mice, into BCI2000 state variables.

Overview

In BCI2000, input logging can be done with per-sample resolution. Typically, BCI2000 data acquisition, signal processing, and application feedback code runs in a pipe synchronously, being called once per BCI2000 sample block, and cannot detect state changes in an input device more frequently than that.

To support input logging with per-sample resolution, BCI2000 allows code to post so-called events asynchronously from a separate thread, which are time-stamped internally and matched against the current data block's time stamp in order to associate them with individual samples.

In this tutorial, we will discuss how to implement input Logging for a device by polling its state in regular intervals. Generally, relying on OS events to detect changes in device state is preferred over polling; however, whether and how device state is available via OS events depends strongly on the device's driver software, and it is thus difficult to provide a valid example. Readers interested in input logging via OS events should read this tutorial first, and then proceed to the key logger component's source code for a non-polling example.

Implementation

An input logger component consists of a combination of a few existing software components, which are all provided by BCI2000 except the device API itself.

Device API

The device API provides functions that allow to read, or manipulate, the state of the input device. Typically, it consists of a library (DLL), and an associated header file.

For the sake of this tutorial, we will assume that the device has the shape of thumb wheel, and has one continuous degree of freedom. Its header file, ThumbWheel.h, provides a C-style interface:

 #define THUMB_WHEEL_MAX_POS 32767
 enum { ThumbOK = 0, ThumbBusy, ThumbUnavailable };
 int ThumbWheelInit();
 int ThumbWheelGetPos();

In order to connect to the thumb wheel, we call ThumbWheelInit(), receiving ThumbOK if everything is fine. The ThumbWheelGetPos() function will return the wheel's current position, as an integer between zero and THUMB_WHEEL_MAX_POS.

Event Interface

Using the BCI2000 event interface, device state may be written into BCI2000 event states asynchronously. We will use the event interface to record the wheel's position into a state called ThumbWheelPos, writing

 #include "BCIEvent.h"
 ...
 bcievent << "ThumbWheelPos " << ThumbWheelGetPos();

Thread Interface

In order to observe the wheel's state independently of BCI2000's processing of data blocks, we create a thread that polls wheel state in regular intervals. We will use BCI2000's Thread class to implement that separate thread.

 #include "Thread.h"
 #include "ThumbWheel.h"
 
 class ThumbThread : public Thread
 {
   ThumbThread()
     {}
   ~ThumbThread()
     {}
   int OnExecute() override
     {
       if( ThumbOK == ThumbWheelInit() )
       {
         int lastWheelPos = -1;
         while( !Terminating() )
         {
           ThreadUtils::SleepForMs( 1 );
           int curWheelPos = ThumbWheelGetPos();
           if( curWheelPos != lastWheelPos )
             bcievent << "ThumbWheelPos " << ThumbWheelGetPos();
           lastWheelPos = curWheelPos;
         }
       }
       return 0;
     }
 };

Note that we avoid sending events if there is no change in position. Otherwise, the event queue will grow very large, increasing overall processing and memory load even if there is no information to record.

EnvironmentExtension Class

The EnvironmentExtension Class is a base class for BCI2000 components ("extensions") that are not filters. Such extensions do not process signals but still have access to BCI2000 parameters and state variables, and are notified of system events such as Preflight, Initialize, and StartRun.

This is the extension's header file:

  #ifndef THUMBWHEEL_LOGGER_H
  #define THUMBWHEEL_LOGGER_H

  #include "Environment.h"
  #include "ThumbThread.h"

  class ThumbWheelLogger : public EnvironmentExtension
  {
    public:
     ThumbWheelLogger()
      : mLogThumbWheel(false),
        mpThumbWheelThread(nullptr)
      {}
     ~ThumbWheelLogger() {}
     void Publish() override;
     void Preflight() const override;
     void Initialize() override;
     void StartRun() override;
     void StopRun() override;
     void Halt() override;

   private:
    bool mLogThumbWheel;
    ThumbWheelThread* mpThumbWheelThread;
  };
  #endif // THUMBWHEEL_LOGGER_H

In our extension component's Publish() member function, we test for a parameter LogThumbWheel, and only request the "ThumbWheelPos" state variable if logging is actually enabled. The LogThumbWheel parameter will be available if the module has been started up with --LogThumbWheel=1 specified on the command line; this way, logging may be enabled and disabled, with no state variable allocated when logging is disabled. Note that we request the LogThumbWheel parameter even if it already exists; this has the effect of providing appropriate auxiliary information about that parameter, i.e. its section, type, and comment fields.

 void ThumbWheelLogger::Publish()
 {
   if (OptionalParameter("LogThumbWheel") > 0)
   {
     BEGIN_PARAMETER_DEFINITIONS
       "Source:Log%20Input int LogThumbWheel= 1 0 0 1 "
       " // record thumb wheel to state (boolean)",
     END_PARAMETER_DEFINITIONS

     BEGIN_EVENT_DEFINITIONS
      "ThumbWheelPos   15 0 0 0",
     END_EVENT_DEFINITIONS
   }
 }

From the Preflight() member function, we check whether the thumb wheel is available:

 void ThumbWheelLogger::Preflight() const
 {
   if (OptionalParameter("LogThumbWheel") > 0)
     if(ThumbOK != ThumbWheelInit())
       bcierr << "ThumbWheel device unavailable";
 }

In Initialize(), we read the LogThumbWheel parameter's value into a class member:

 void ThumbWheelLogger::Initialize()
 {
   mLogThumbWheel = (OptionalParameter("LogThumbWheel") > 0);
 }

From the component's StartRun() member function, we instantiate the thumb wheel thread class declared above, thereby running its Execute() member in a new thread:

 void ThumbWheelLogger::StartRun()
 {
   if(mLogThumbWheel)
   {
      mpThumbWheelThread = new ThumbWheelThread;
      mpThumbWheelThread->Start();
   }
}

Mirroring StartRun(), StopRun() disposes of the thumbwheel logging thread.

 void ThumbWheelLogger::StopRun()
 {
   if (mpThumbWheelThread)
   {
     mpThumbWheelThread->Terminate();
     delete mpThumbWheelThread;
     mpThumbWheelThread = nullptr;
   }
 }

We also forward StopRun() functionality to the Halt() member to ensure appropriate halting of asynchronous activity:

 void ThumbWheelLogger::Halt()
 {
   StopRun();
 }

Finally, to make sure there exists an object of our ThumbWheelLogger class, we use the Extension macro at the top of its .cpp file:

Extension(ThumbWheelLogger);

Finished

Now, when we add the ThumbWheelLogger.cpp file to a source module, then the module will contain an object of our newly created class, and it will listen to the --LogThumbWheel=1 command line option.

See also

Programming Reference:EnvironmentExtension Class, Programming Reference:Thread Class, Programming Reference:Events