Programming Tutorial:Implementing a Source Module: Difference between revisions

From BCI2000 Wiki
Jump to navigation Jump to search
No edit summary
(28 intermediate revisions by 2 users not shown)
Line 1: Line 1:
Data acquisition modules are factored into code required for any
Source modules (data acquisition modules) are factored into  
hardware,
*code required for any hardware, and
and code required to access a specific hardware.
*code required to access a specific hardware.
You provide a function that waits for and reads A/D
You provide only specific code. This is in a function that waits for and reads A/D
data (line 3 in the EEG source pseudo code of [[Modules:Data Acquisition]]),
data (line 3 in the pseudo code shown at [[Technical Reference:Core Modules]]),
together with some helper functions that perform initialization and
together with some helper functions that perform initialization and
cleanup tasks.
cleanup tasks.
Line 9: Line 9:
<tt>GenericADC</tt>.
<tt>GenericADC</tt>.


==Scenario==
Depending on system load, it may be possible that the BCI2000 system cannot keep up with data
acquisition for short periods of time. To handle such cases gracefully, it is useful
to acquire data inside a separate thread, and to keep a buffer of multiple data blocks which
is going to be filled when data cannot be processed in a timely manner.
A framework for this kind of data acquisition is given by the <tt>BufferedADC</tt> class.
In the following, we will consider a source module that is derived from the <tt>BufferedADC</tt>
class.
 
To simplify the process of creating a source module, there is a helper program
located at <tt>build/NewBCI2000Module</tt>. When being run, it will create the files required for
a source module from templates. Still, the following may be helpful for you to read, even if you
are going to use <tt>NewBCI2000Module</tt>.
 
==Example Scenario==
Your ''Tachyon Corporation''  A/D card comes with a C-style
Your ''Tachyon Corporation''  A/D card comes with a C-style
software interface
software interface
Line 31: Line 44:
first argument.
first argument.
Each of the functions will return zero if everything went well, otherwise
Each of the functions will return zero if everything went well, otherwise
some error value will be given.
some error value will be returned.
Luckily, ''Tachyon Corporation''  gives you just
Luckily, ''Tachyon Corporation''  gives you just
what you need for a BCI2000 source module, so implementing the ADC
what you need for a BCI2000 source module, so implementing the ADC
Line 43: Line 56:
#define TACHYON_ADC_H
#define TACHYON_ADC_H


#include "GenericADC.h"
#include "BufferedADC.h"


class TachyonADC : public GenericADC
class TachyonADC : public BufferedADC
{
{
  public:
  public:
Line 51: Line 64:
   ~TachyonADC();
   ~TachyonADC();


   void Preflight( const SignalProperties&, SignalProperties& ) const;
   void OnPublish() override;
   void Initialize();
  void OnPreflight(SignalProperties&) const override;
   void Process( const GenericSignal*, GenericSignal* );
  void OnInitialize(const SignalProperties&) override;
   void Halt();
   void OnStartAcquisition() override;
   void OnStopAcquisition() override;
   void DoAcquire(GenericSignal&) override;


  private:
  private:
   int mSourceCh,
  void* mHandle;
   int mSourceCh,
         mSampleBlockSize,
         mSampleBlockSize,
         mSamplingRate;
         mSamplingRate;
Line 70: Line 86:
#include "TachyonADC.h"
#include "TachyonADC.h"
#include "Tachyon/TachyonLib.h"
#include "Tachyon/TachyonLib.h"
#include "BCIError.h"
#include "BCIStream.h"


using namespace std;
using namespace std;
Line 76: Line 92:
RegisterFilter( TachyonADC, 1 );
RegisterFilter( TachyonADC, 1 );
</pre>
</pre>
From the constructor, you request parameters and states that your ADC
From the constructor, you initialize your class variables to safe default
needs;
values (note they are not automatically initialized to zero by the compiler);
from the destructor, you call <tt>Halt</tt> to make sure that your
from the destructor, you deallocate memory and other resources, if any;
board stops
from the <tt>OnPublish()</tt> function, you request parameters and states that your ADC
acquiring data whenever your class instance gets destructed:
needs:
<pre>
<pre>
TachyonADC::TachyonADC()
TachyonADC::TachyonADC()
Line 86: Line 102:
   mSampleBlockSize( 0 ),
   mSampleBlockSize( 0 ),
   mSamplingRate( 0 )
   mSamplingRate( 0 )
{
}
TachyonADC::~TachyonADC()
{
}
void TachyonADC::OnPublish()
{
{
   BEGIN_PARAMETER_DEFINITIONS
   BEGIN_PARAMETER_DEFINITIONS
Line 95: Line 119:
         "// this is the sample rate",
         "// this is the sample rate",
   END_PARAMETER_DEFINITIONS
   END_PARAMETER_DEFINITIONS
}
TachyonADC::~TachyonADC()
{
  Halt();
}
}
</pre>
</pre>


==ADC Initialization==
==ADC Initialization==
Your <tt>Preflight</tt> function will check whether the board works
Your <tt>OnPreflight</tt> function will check whether the board works
with the
with the
parameters requested, and communicate the dimensions of its output
parameters requested, and communicate the dimensions of its output
signal:
signal:
<pre>
<pre>
void TachyonADC::Preflight( const SignalProperties&,
void TachyonADC::OnPreflight( SignalProperties& outputProperties ) const
                              SignalProperties& outputProperties )
                              const
{
{
   if( TACHYON_NO_ERROR !=  
   if( TACHYON_NO_ERROR != TachyonStart( Parameter( "SamplingRate" ), Parameter( "SourceCh" ) ) )
        TachyonStart( Parameter( "SamplingRate" ), Parameter( "SourceCh" ) ) )
     bcierr << "SamplingRate and/or SourceCh parameters are not compatible"
     bcierr << "SamplingRate and/or SourceCh parameters are not compatible"
           << " with the A/D card"
           << " with the A/D card"
           << endl;
           << endl;
  TachyonStop();
   outputProperties = SignalProperties( Parameter( "SourceCh" ),
   outputProperties = SignalProperties( Parameter( "SourceCh" ),
                           Parameter( "SampleBlockSize" ),
                           Parameter( "SampleBlockSize" ),
Line 129: Line 144:
but also the format of the <tt>dat</tt> file written by the source
but also the format of the <tt>dat</tt> file written by the source
module.
module.
For the <tt>Initialize</tt> function, it will only be
 
called if
You might want to write <tt>SignalType::int32</tt> or <tt>SignalType::float32</tt> instead if your data acquisition hardware acquires data in one of those formats.
<tt>Preflight</tt> did not report any errors. If everything works
 
fine with
The actual <tt>OnInitialize</tt> function will only be called if <tt>OnPreflight</tt> did not report any errors.  
the parameters, you may skip any checks, and write  
Thus, you may skip any further checks, and write  
<pre>
<pre>
void TachyonADC::Initialize()
void TachyonADC::OnInitialize( const SignalProperties& )
{
{
   mSourceCh = Parameter( "SourceCh" );
   mSourceCh = Parameter( "SourceCh" );
   mSampleBlockSize = Parameter( "SampleBlockSize" );
   mSampleBlockSize = Parameter( "SampleBlockSize" );
   mSamplingRate = Parameter( "SamplingRate" );
   mSamplingRate = Parameter( "SamplingRate" );
  TachyonStart( mSamplingRate, mSourceCh );
}
}
</pre>
</pre>
Balancing the <tt>TachyonStart</tt> call in the <tt>Initialize</tt> function,
 
your <tt>Halt</tt> function should stop all asynchronous activity that
==Start and Stop==
your ADC code initiates:
In <tt>OnStartAcquisition</tt>, the hardware is supposed to be set into a state such that it acquires data, whereas
<tt>OnStopAcquisition</tt> is supposed to stop all data acquisition activity from the device.
 
Both <tt>OnStartAcquisition</tt> and <tt>OnStopAcquisition</tt> are called from the same separate data acquisition
thread that <tt>DoAcquire</tt> is called from.
<pre>
<pre>
void TachyonADC::Halt()
void TachyonADC::OnStartAcquisition()
{
{
   TachyonStop();
   int err = TachyonStart( mSourceCh, mSamplingRate );
  if( err != TACHYON_NO_ERROR )
    bcierr << "Could not start acquisition due to error " << err;
}
 
void TachyonADC::OnStopAcquisition()
{
  int err = TachyonStop();
  if( err != TACHYON_NO_ERROR )
    bcierr << "Could not stop acquisition due to error " << err;
}
}
</pre>
</pre>


==Data Acquisition==
==Data Acquisition==
Note that the function may not return unless the output signal is filled with data, so it is
Note that the <tt>DoAcquire</tt> function may not return unless the output signal is filled with data, so it is
crucial that <tt>TachyonWaitForData</tt> is a blocking function.
crucial that <tt>TachyonWaitForData</tt> is a blocking function.
(If your card does not provide such a function, and you need to poll
(If your card does not provide such a function, and you need to poll
for data, don't forget to call <tt>Sleep( 0 )</tt> inside your polling loop to
for data, don't forget to call <tt>Sleep( 0 )</tt> inside your polling loop to
avoid hogging the CPU.)
avoid tying up the CPU.)
<pre>
<pre>
void TachyonADC::Process( const GenericSignal*, GenericSignal* outputSignal )
void TachyonADC::DoAcquire( GenericSignal& outputSignal )
{
{
   int valuesToRead = mSampleBlockSize * mSourceCh;
   int valuesToRead = mSampleBlockSize * mSourceCh;
   short* buffer;
   short* buffer;
   if( TACHYON_NO_ERROR == TachyonWaitForData( &buffer, valuesToRead ) )
   int err = TachyonWaitForData( &buffer, valuesToRead );
  if( err == TACHYON_NO_ERROR )
   {
   {
     int i = 0;
     int i = 0;
     for( int channel = 0; channel < mSourceCh; ++channel )
     for( int channel = 0; channel < mSourceCh; ++channel )
       for( int sample = 0; sample < mSampleBlockSize; ++sample )
       for( int sample = 0; sample < mSampleBlockSize; ++sample )
         ( *outputSignal )( channel, sample ) = buffer[ i++ ];
         outputSignal( channel, sample ) = buffer[ i++ ];
   }
   }
   else
   else
     bcierr << "Error reading data" << endl;
     bcierr << "Error reading data: " << err;
}
}
</pre>
</pre>
==Adding the ''SourceFilter''==
Most measurement equipment comes with hardware filters that allow you to filter out line noise.
For equipment that does not offer such an option, consider adding the [[User Reference:SourceFilter|SourceFilter]] to your data acquisition module as described [[User_Reference:SourceFilter#Remarks|here]].


==Finished==
==Finished==
Line 181: Line 213:
descendant in an existing source module, add the
descendant in an existing source module, add the
<tt>TachyonADC.lib</tt> shipped
<tt>TachyonADC.lib</tt> shipped
with your card to the project, compile, link, and find the bugs...
with your card to the project, compile, and link.
 
[[Category:Tutorial]][[Category:Development]][[Category:Framework API]][[Category:Data Acquisition]]

Revision as of 16:55, 17 November 2020

Source modules (data acquisition modules) are factored into

  • code required for any hardware, and
  • code required to access a specific hardware.

You provide only specific code. This is in a function that waits for and reads A/D data (line 3 in the pseudo code shown at Technical Reference:Core Modules), together with some helper functions that perform initialization and cleanup tasks. Together these functions form a class derived from GenericADC.

Depending on system load, it may be possible that the BCI2000 system cannot keep up with data acquisition for short periods of time. To handle such cases gracefully, it is useful to acquire data inside a separate thread, and to keep a buffer of multiple data blocks which is going to be filled when data cannot be processed in a timely manner. A framework for this kind of data acquisition is given by the BufferedADC class. In the following, we will consider a source module that is derived from the BufferedADC class.

To simplify the process of creating a source module, there is a helper program located at build/NewBCI2000Module. When being run, it will create the files required for a source module from templates. Still, the following may be helpful for you to read, even if you are going to use NewBCI2000Module.

Example Scenario

Your Tachyon Corporation A/D card comes with a C-style software interface declared in a header file "TachyonLib.h" that consists of three functions

#define TACHYON_NO_ERROR 0
int TachyonStart( int inSamplingRate, int inNumberOfChannels );
int TachyonStop( void );
int TachyonWaitForData( short** outBuffer, int inCount );

From the library help file, you learn that TachyonStart configures the card and starts acquisition to some internal buffer; that TachyonStop stops acquisition to the buffer, and that TachyonWaitForData will block execution until the specified amount of data has been acquired, and that it will return a pointer to a buffer containing the data in its first argument. Each of the functions will return zero if everything went well, otherwise some error value will be returned. Luckily, Tachyon Corporation gives you just what you need for a BCI2000 source module, so implementing the ADC class is quite straightforward.

Writing the ADC Header File

In your class' header file, "TachyonADC.h", you write

#ifndef TACHYON_ADC_H
#define TACHYON_ADC_H

#include "BufferedADC.h"

class TachyonADC : public BufferedADC
{
 public:
   TachyonADC();
   ~TachyonADC();

   void OnPublish() override;
   void OnPreflight(SignalProperties&) const override;
   void OnInitialize(const SignalProperties&) override;
   void OnStartAcquisition() override;
   void OnStopAcquisition() override;
   void DoAcquire(GenericSignal&) override;

 private:
   void* mHandle;
   int mSourceCh,
        mSampleBlockSize,
        mSamplingRate;
};
#endif // TACHYON_ADC_H

ADC Implementation

In the .cpp file, you will need some #includes, and a filter registration:

#include "TachyonADC.h"
#include "Tachyon/TachyonLib.h"
#include "BCIStream.h"

using namespace std;

RegisterFilter( TachyonADC, 1 );

From the constructor, you initialize your class variables to safe default values (note they are not automatically initialized to zero by the compiler); from the destructor, you deallocate memory and other resources, if any; from the OnPublish() function, you request parameters and states that your ADC needs:

TachyonADC::TachyonADC()
: mSourceCh( 0 ),
  mSampleBlockSize( 0 ),
  mSamplingRate( 0 )
{
}

TachyonADC::~TachyonADC()
{
}

void TachyonADC::OnPublish()
{
  BEGIN_PARAMETER_DEFINITIONS
    "Source int SourceCh=        64 64 1 128 "
        "// this is the number of digitized channels",
    "Source int SampleBlockSize= 16 5 1 128 "
        "// this is the number of samples transmitted at a time",
    "Source int SamplingRate=    128 128 1 4000 "
        "// this is the sample rate",
  END_PARAMETER_DEFINITIONS
}

ADC Initialization

Your OnPreflight function will check whether the board works with the parameters requested, and communicate the dimensions of its output signal:

void TachyonADC::OnPreflight( SignalProperties& outputProperties ) const
{
  if( TACHYON_NO_ERROR != TachyonStart( Parameter( "SamplingRate" ), Parameter( "SourceCh" ) ) )
    bcierr << "SamplingRate and/or SourceCh parameters are not compatible"
           << " with the A/D card"
           << endl;
  outputProperties = SignalProperties( Parameter( "SourceCh" ),
                          Parameter( "SampleBlockSize" ),
                          SignalType::int16 );
}

Here, the last argument of the SignalProperties constructor determines not only the type of the signal propagated to the BCI2000 filters but also the format of the dat file written by the source module.

You might want to write SignalType::int32 or SignalType::float32 instead if your data acquisition hardware acquires data in one of those formats.

The actual OnInitialize function will only be called if OnPreflight did not report any errors. Thus, you may skip any further checks, and write

void TachyonADC::OnInitialize( const SignalProperties& )
{
  mSourceCh = Parameter( "SourceCh" );
  mSampleBlockSize = Parameter( "SampleBlockSize" );
  mSamplingRate = Parameter( "SamplingRate" );
}

Start and Stop

In OnStartAcquisition, the hardware is supposed to be set into a state such that it acquires data, whereas OnStopAcquisition is supposed to stop all data acquisition activity from the device.

Both OnStartAcquisition and OnStopAcquisition are called from the same separate data acquisition thread that DoAcquire is called from.

void TachyonADC::OnStartAcquisition()
{
  int err = TachyonStart( mSourceCh, mSamplingRate );
  if( err != TACHYON_NO_ERROR )
    bcierr << "Could not start acquisition due to error " << err;
}

void TachyonADC::OnStopAcquisition()
{
  int err = TachyonStop();
  if( err != TACHYON_NO_ERROR )
    bcierr << "Could not stop acquisition due to error " << err;
}

Data Acquisition

Note that the DoAcquire function may not return unless the output signal is filled with data, so it is crucial that TachyonWaitForData is a blocking function. (If your card does not provide such a function, and you need to poll for data, don't forget to call Sleep( 0 ) inside your polling loop to avoid tying up the CPU.)

void TachyonADC::DoAcquire( GenericSignal& outputSignal )
{
  int valuesToRead = mSampleBlockSize * mSourceCh;
  short* buffer;
  int err = TachyonWaitForData( &buffer, valuesToRead );
  if( err == TACHYON_NO_ERROR )
  {
    int i = 0;
    for( int channel = 0; channel < mSourceCh; ++channel )
      for( int sample = 0; sample < mSampleBlockSize; ++sample )
        outputSignal( channel, sample ) = buffer[ i++ ];
  }
  else
    bcierr << "Error reading data: " << err;
}

Adding the SourceFilter

Most measurement equipment comes with hardware filters that allow you to filter out line noise. For equipment that does not offer such an option, consider adding the SourceFilter to your data acquisition module as described here.

Finished

You are done! Use your TachyonADC.cpp to replace the GenericADC descendant in an existing source module, add the TachyonADC.lib shipped with your card to the project, compile, and link.