Jump to content

Programming Tutorial:Implementing an Input Logger: Difference between revisions

From BCI2000 Wiki
Mellinger (talk | contribs)
Mellinger (talk | contribs)
 
(29 intermediate revisions by the same user not shown)
Line 2: Line 2:


==Overview==
==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 [[Programming Reference:Filter Chain|pipe]] ''synchronously'', being called once per [[BCI2000 Glossary#Sample_Block|BCI2000 sample block]], and cannot detect state changes in an input device more frequently than that.
In BCI2000, input logging can be done with per-sample resolution. Typically, BCI2000 data acquisition, signal processing, and application feedback code runs in a [[Programming Reference:Filter Chain|pipe]] '''synchronously''', being called once per [[BCI2000 Glossary#Sample_Block|BCI2000 sample block]], and cannot detect state changes in an input device more frequently than that.


To support ''Input Logging'' at a per-sample resolution, BCI2000 allows code to post so-called [[Programming Reference:Events|''events'']] 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.
To support input logging with per-sample resolution, BCI2000 allows code to post so-called [[Programming Reference:Events|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 [http://{{SERVERNAME}}/tracproj/browser/trunk/src/shared/modules/signalsource/KeyLogger.h|key logger component's source code] for a non-polling example.  
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 [https://{{SERVERNAME}}/tracproj/browser/trunk/src/shared/modules/signalsource/logging/KeyLogger.h key logger component's source code] for a non-polling example.


==Requisites==
==Implementation==
An input logger component consists mainly of a combination of a few existing software components, which are all provided by BCI2000 except the device API itself.
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===
===Device API===
Line 15: Line 15:
Typically, it consists of a library (DLL), and an associated header file.
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. In its header file, <tt>ThumbWheel.h</tt>, it comes with a C-style interface:
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, <tt>ThumbWheel.h</tt>, provides a C-style interface:
<pre>
  #define THUMB_WHEEL_MAX_POS 32767
  #define THUMB_WHEEL_MAX_POS 32767
  enum { ThumbOK = 0, ThumbBusy, ThumbUnavailable };
  enum { ThumbOK = 0, ThumbBusy, ThumbUnavailable };
  int ThumbWheelInit();
  int ThumbWheelInit();
  int ThumbWheelGetPos();
  int ThumbWheelGetPos();
</pre>
In order to connect to the thumb wheel, we call <tt>ThumbWheelInit()</tt>, receiving <tt>ThumbOK</tt> if everything is fine.
In order to connect to the thumb wheel, we call <tt>ThumbWheelInit()</tt>, receiving <tt>ThumbOK</tt> if everything is fine.
The <tt>ThumbWheelGetPos()</tt> function will return the wheel's current position, as an integer between zero and <tt>THUMB_WHEEL_MAX_POS</tt>.
The <tt>ThumbWheelGetPos()</tt> function will return the wheel's current position, as an integer between zero and <tt>THUMB_WHEEL_MAX_POS</tt>.


===Event Interface===
===Event Interface===
Using the [[Programming Reference:Events|BCI2000 event interface]], device state may be written into [[BCI2000 Glossary#State|BCI2000 states]] asynchronously.
Using the [[Programming Reference:Events|BCI2000 event interface]], device state may be written into [[BCI2000 Glossary#State|BCI2000 event states]] asynchronously.
We will use the event interface to record the wheel's position into a state called <tt>WheelPos</tt>, writing
We will use the event interface to record the wheel's position into a state called <tt>ThumbWheelPos</tt>, writing
<pre>
  #include "BCIEvent.h"
  #include "BCIEvent.h"
  ...
  ...
  bcievent << "WheelPos " << ThumbWheelGetPos();
  bcievent << "ThumbWheelPos " << ThumbWheelGetPos();
</pre>


===Thread Interface===
===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 a BCI2000 thread class ([http://{{SERVERNAME}}/tracproj/browser/trunk/src/shared/utils/OSThread.h BCI2000/src/shared/utils/OSThread]).
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 [[Programming Reference:Thread Class|Thread class]] to implement that separate thread.
 
<pre>
Note that we avoid sending events unless the device state has actually changed. Otherwise, the event queue will grow very large, increasing overall processing and memory load even if there is no information to record.
  #include "Thread.h"
 
  #include "OSThread.h"
  #include "ThumbWheel.h"
  #include "ThumbWheel.h"
   
   
  class ThumbThread : public OSThread
  class ThumbThread : public Thread
  {
  {
   ThumbThread()
   ThumbThread()
     {}
     {}
   virtual ~ThumbThread()
   ~ThumbThread()
     {}
     {}
   virtual int Execute()
   int OnExecute() override
     {
     {
       if( ThumbOK == ThumbWheelInit() )
       if( ThumbOK == ThumbWheelInit() )
       {
       {
         int lastWheelPos = -1;
         int lastWheelPos = -1;
         while( !IsTerminating() )
         while( !Terminating() )
         {
         {
           Sleep( 1 );
           ThreadUtils::SleepForMs( 1 );
           int curWheelPos = ThumbWheelGetPos();
           int curWheelPos = ThumbWheelGetPos();
           if( curWheelPos != lastWheelPos )
           if( curWheelPos != lastWheelPos )
Line 58: Line 60:
         }
         }
       }
       }
    return 0;
      return 0;
  }
    }
};
</pre>
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===
===EnvironmentExtension Class===
The [[Programming Reference:EnvironmentExtension Class|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 Glossary#Parameter|BCI2000 parameters]] and [[BCI2000 Glossary#State|state variables]], and are notified of system events such as ''Preflight'', ''Initialize'', and ''StartRun''.
The [[Programming Reference:EnvironmentExtension Class|EnvironmentExtension Class]] is a base class for BCI2000 components (''"extensions"'') that are not [[Programming Reference:GenericFilter Class|filters]]. Such extensions do not process signals but still have access to [[BCI2000 Glossary#Parameter|BCI2000 parameters]] and [[BCI2000 Glossary#State|state variables]], and are notified of system events such as ''Preflight'', ''Initialize'', and ''StartRun''.


This is the extension's class declaration:
This is the extension's header file:
#ifndef THUMBWHEEL_LOGGER_H
<pre>
#define THUMBWHEEL_LOGGER_H
  #ifndef THUMBWHEEL_LOGGER_H
  #define THUMBWHEEL_LOGGER_H


   #include "Environment.h"
   #include "Environment.h"
Line 74: Line 80:
   {
   {
     public:
     public:
     ThumbWheelLogger()
     ThumbWheelLogger();
      : mLogThumbWheel( false ),
     ~ThumbWheelLogger() {}
        mpThumbWheelThread( NULL )
     void Publish() override;
      {}
     void Preflight() const override;
     virtual ~ ThumbWheelLogger()
     void Initialize() override;
    {}
     void StartRun() override;
     virtual void Publish();
     void StopRun() override;
     virtual void Preflight() const;
     void Halt() override;
     virtual void Initialize();
     virtual void StartRun();
     virtual void StopRun();
     virtual void Halt();


   private:
   private:
     bool mLogThumbWheel;
     bool mLogThumbWheel = false;
     ThumbWheelThread* mpThumbWheelThread;
     ThumbWheelThread* mpThumbWheelThread = nullptr;
   };
   };
  #endif // THUMBWHEEL_LOGGER_H
</pre>
In the extension component's constructor function, we communicate our ''LogThumbWheel'' parameter as a so-called enabler parameter to the framework.
This way, the BCI2000Launcher program will be aware of our logger, and will know how to enable it at module startup:
<pre>
ThumbWheelLogger()
{
    PublishEnabler("LogThumbWheel");
}
</pre>


In our extension component's <tt>Publish()</tt> 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 <tt>--LogThumbWheel=1</tt> 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.
In our extension component's <tt>Publish()</tt> 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 <tt>--LogThumbWheel=1</tt> 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.
 
<pre>
  void ThumbWheelLogger::Publish()
  void ThumbWheelLogger::Publish()
  {
  {
   if( OptionalParameter( "LogThumbWheel" ) > 0 )
   if (OptionalParameter("LogThumbWheel") > 0)
   {
   {
     BEGIN_PARAMETER_DEFINITIONS
     BEGIN_PARAMETER_DEFINITIONS
Line 103: Line 117:
     END_PARAMETER_DEFINITIONS
     END_PARAMETER_DEFINITIONS


     BEGIN_STATE_DEFINITIONS
     BEGIN_EVENT_DEFINITIONS
       "ThumbWheelPos  15 0 0 0",
       "ThumbWheelPos  15 0 0 0",
     END_STATE_DEFINITIONS
     END_EVENT_DEFINITIONS
   }
   }
  }
  }
 
</pre>
From the <tt>Preflight()</tt> member function, we check whether the thumb wheel is available:
From the <tt>Preflight()</tt> member function, we check whether the thumb wheel is available:
<pre>
  void ThumbWheelLogger::Preflight() const
  void ThumbWheelLogger::Preflight() const
  {
  {
   if( OptionalState( "LogThumbWheel" ) > 0 )
   if (OptionalParameter("LogThumbWheel") > 0)
     if( ThumbOK != ThumbWheelInit() )
     if(ThumbOK != ThumbWheelInit())
       bcierr << "ThumbWheel device unavailable" << endl;
       bcierr << "ThumbWheel device unavailable";
  }
  }
 
</pre>
In <tt>Initialize()</tt>, we read the ''LogThumbWheel'' parameter's value into a class member:
In <tt>Initialize()</tt>, we read the ''LogThumbWheel'' parameter's value into a class member:
<pre>
  void ThumbWheelLogger::Initialize()
  void ThumbWheelLogger::Initialize()
  {
  {
   mLogThumbWheel = ( OptionalParameter( "LogThumbWheel" ) > 0 );
   mLogThumbWheel = (OptionalParameter("LogThumbWheel") > 0);
  }
  }
 
</pre>
From the component's <tt>StartRun()</tt> member function, we instantiate the thumb wheel thread class declared above, thereby running its <tt>Execute()</tt> member in a new thread:
From the component's <tt>StartRun()</tt> member function, we instantiate the thumb wheel thread class declared above, thereby running its <tt>Execute()</tt> member in a new thread:
<pre>
  void ThumbWheelLogger::StartRun()
  void ThumbWheelLogger::StartRun()
  {
  {
   if( mLogThumbWheel )
   if(mLogThumbWheel)
    mpThumbWheelThread = new ThumbWheelThread;
  {
      mpThumbWheelThread = new ThumbWheelThread;
      mpThumbWheelThread->Start();
  }
}
}
 
</pre>
Mirroring <tt>StartRun()</tt>, <tt>StopRun()</tt> disposes of the thumbwheel logging thread.
Mirroring <tt>StartRun()</tt>, <tt>StopRun()</tt> disposes of the thumbwheel logging thread.
<pre>
  void ThumbWheelLogger::StopRun()
  void ThumbWheelLogger::StopRun()
  {
  {
   if( mpThumbWheelThread != NULL )
   if (mpThumbWheelThread)
   {
   {
     mpThumbWheelThread->Terminate();
     mpThumbWheelThread->Terminate();
    while( !mpThumbWheelThread->IsTerminated() )
      Sleep( 1 );
     delete mpThumbWheelThread;
     delete mpThumbWheelThread;
     mpThumbWheelThread = NULL;
     mpThumbWheelThread = nullptr;
   }
   }
  }
  }
</pre>
We also forward <tt>StopRun()</tt> functionality to the <tt>Halt()</tt> member to ensure appropriate halting of asynchronous activity:
We also forward <tt>StopRun()</tt> functionality to the <tt>Halt()</tt> member to ensure appropriate halting of asynchronous activity:
<pre>
  void ThumbWheelLogger::Halt()
  void ThumbWheelLogger::Halt()
  {
  {
   StopRun();
   StopRun();
  }
  }
</pre>
Finally, to make sure there exists an object of our <tt>ThumbWheelLogger</tt> class, we use the <tt>Extension</tt> macro at the top of its <tt>.cpp</tt> file:
Extension(ThumbWheelLogger);


Finally, to make sure there exists an object of our <tt>ThumbWheelLogger</tt> class, we use the <tt>RegisterExtension</tt> macro at the top of its <tt>.cpp</tt> file:
==Finished==
RegisterExtension( ThumbWheelLogger );
Now, when we add the <tt>ThumbWheelLogger.cpp</tt> file to a source module, then the module will contain an object of our newly created class, and it will listen to the <tt>--LogThumbWheel=1</tt> command line option.
 
Now, when we add the <tt>ThumbWheelLogger.cpp</tt> file to a source module, it will contain an object of our newly created class, and it will listen to the <tt>--LogThumbWheel=1</tt> command line option.


==See also==
==See also==
[[Programming Reference:EnvironmentExtension Class]], [[Programming Reference:OSThread Class]], [[Programming Reference:Events]]
[[Programming Reference:EnvironmentExtension Class]], [[Programming Reference:Thread Class]], [[Programming Reference:Events]]


[[Category:Tutorial]][[Category:Development]][[Category:Framework API]][[Category:Data Acquisition]]
[[Category:Tutorial]][[Category:Development]][[Category:Framework API]][[Category:Data Acquisition]]

Latest revision as of 18:10, 2 February 2026

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();
     ~ThumbWheelLogger() {}
     void Publish() override;
     void Preflight() const override;
     void Initialize() override;
     void StartRun() override;
     void StopRun() override;
     void Halt() override;

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


In the extension component's constructor function, we communicate our LogThumbWheel parameter as a so-called enabler parameter to the framework. This way, the BCI2000Launcher program will be aware of our logger, and will know how to enable it at module startup:

 ThumbWheelLogger()
 {
    PublishEnabler("LogThumbWheel");
 }

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