Programming Tutorial:Implementing a Source Module: Difference between revisions
(26 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
Source modules (data acquisition modules) are factored into | |||
*code required for any hardware, and | *code required for any hardware, and | ||
*code required to access a specific hardware. | *code required to access a specific hardware. | ||
Line 8: | Line 8: | ||
Together these functions form a class derived from | Together these functions form a class derived from | ||
<tt>GenericADC</tt>. | <tt>GenericADC</tt>. | ||
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, with extensive comments detailing implementation steps and decisions. | |||
Still, the following may be helpful for you to read, even if you | |||
are going to use <tt>NewBCI2000Module</tt>. | |||
==Example Scenario== | ==Example Scenario== | ||
Line 17: | Line 31: | ||
<pre> | <pre> | ||
#define TACHYON_NO_ERROR 0 | #define TACHYON_NO_ERROR 0 | ||
int TachyonStart( int inSamplingRate | void* TachyonOpen(); | ||
int TachyonStop( void ); | int TachyonClose(void* handle); | ||
int TachyonWaitForData( short** outBuffer, int inCount ); | int TachyonGetNumberOfChannels(void* handle); | ||
int TachyonStart(void* handle, int inSamplingRate); | |||
int TachyonStop(void* handle ); | |||
int TachyonWaitForData(void* handle, short** outBuffer, int inCount); | |||
</pre> | </pre> | ||
From the library help file, you learn that <tt>TachyonStart</tt> | From the library help file, you learn that <tt>TachyonStart</tt> | ||
Line 43: | Line 60: | ||
#define TACHYON_ADC_H | #define TACHYON_ADC_H | ||
#include " | #include "BufferedADC.h" | ||
class TachyonADC : public | class TachyonADC : public BufferedADC | ||
{ | { | ||
public: | public: | ||
Line 51: | Line 68: | ||
~TachyonADC(); | ~TachyonADC(); | ||
void | void OnPublish() override; | ||
void | void OnAutoConfig() override; | ||
void | void OnPreflight(SignalProperties&) const override; | ||
void | void OnInitialize(const SignalProperties&) override; | ||
void OnStartAcquisition() override; | |||
void OnStopAcquisition() override; | |||
void DoAcquire(GenericSignal&) override; | |||
private: | private: | ||
int | void* mHandle; | ||
int mSourceCh, | |||
mSampleBlockSize, | mSampleBlockSize, | ||
mSamplingRate; | mSamplingRate; | ||
Line 70: | Line 91: | ||
#include "TachyonADC.h" | #include "TachyonADC.h" | ||
#include "Tachyon/TachyonLib.h" | #include "Tachyon/TachyonLib.h" | ||
#include " | #include "BCIStream.h" | ||
RegisterFilter( TachyonADC, 1 ); | RegisterFilter( TachyonADC, 1 ); | ||
</pre> | </pre> | ||
From the constructor, you | 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 | from the destructor, you deallocate memory and other resources, if any; | ||
from the <tt>OnPublish()</tt> function, you request parameters and states that your ADC | |||
needs: | |||
<pre> | <pre> | ||
TachyonADC::TachyonADC() | TachyonADC::TachyonADC() | ||
: mSourceCh( 0 ), | : mSourceCh(0), | ||
mSampleBlockSize( 0 ), | mSampleBlockSize(0), | ||
mSamplingRate( 0 ) | mSamplingRate(0), | ||
mHandle(nullptr) | |||
{ | |||
} | |||
TachyonADC::~TachyonADC() | |||
{ | |||
if (mHandle) | |||
TachyonClose(mHandle); | |||
} | |||
void TachyonADC::OnPublish() | |||
{ | { | ||
BEGIN_PARAMETER_DEFINITIONS | BEGIN_PARAMETER_DEFINITIONS | ||
"Source int SourceCh= | "Source int SourceCh= auto auto % % " | ||
"// this is the number of digitized channels", | "// this is the number of digitized channels", | ||
"Source int SampleBlockSize= 16 5 1 128 " | "Source int SampleBlockSize= 16 5 1 128 " | ||
"// this is the number of samples transmitted at a time", | "// this is the number of samples transmitted at a time", | ||
"Source int SamplingRate= | "Source int SamplingRate= 128Hz 128Hz 1 4000 " | ||
"// this is the sample rate", | "// this is the sample rate", | ||
END_PARAMETER_DEFINITIONS | END_PARAMETER_DEFINITIONS | ||
BEGIN_STREAM_DEFINITIONS | |||
"DigitalInput 8 0 0 0", // a stream that holds digital input data | |||
END_STREAM_DEFINITIONS | |||
} | } | ||
</pre> | |||
The <tt>STREAM_DEFINITION</tt> defines a digital input stream that is synchronous with input data. | |||
If your amplifier/DAC has digital inputs, use such a stream to record them and make them available | |||
to BCI2000 components through the [[Technical_Reference:State_Definition#State_Concept|<tt>State</tt>]] interface. | |||
TachyonADC:: | ==ADC Auto-Configuration== | ||
In the <tt>OnAutoConfig</tt> function, it is possible to set parameters that have been set to a value of "auto". | |||
This way, hardware information may be read from the amplifier, avoiding double effort to synchronize user settings | |||
with hardware properties. | |||
In this example, we get the number of channels through the <tt>TachyonGetNumberOfChannels()</tt> function, and | |||
assign it to the <tt>SourceCh</tt> parameter. If the user wants to override this, it is still possible to specify an actual | |||
number instead of "auto" in the parameter file/Operator config dialog. | |||
<pre> | |||
void TachyonADC::OnAutoConfig() | |||
{ | { | ||
if (mHandle) | |||
TachyonClose(mHandle); | |||
mHandle = TachyonOpen(); | |||
if (!mHandle) | |||
throw bcierr << "Could not connect to Tachyon hardware"; | |||
Parameter("SourceCh") = TachyonGetNumberOfChannels(mHandle); | |||
} | } | ||
</pre> | </pre> | ||
==ADC Initialization== | ==ADC Initialization== | ||
Your <tt> | 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:: | void TachyonADC::OnPreflight(SignalProperties& outputProperties) const | ||
{ | { | ||
if( TACHYON_NO_ERROR != | if(TACHYON_NO_ERROR != TachyonStart(mHandle, Parameter("SamplingRate").InHertz())) | ||
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"; | ||
TachyonStop(mHandle); | |||
TachyonStop(); | outputProperties = SignalProperties(Parameter("SourceCh") + 1, | ||
outputProperties = SignalProperties( Parameter( "SourceCh" ), | Parameter("SampleBlockSize"), | ||
Parameter( "SampleBlockSize" ), | |||
SignalType::int16 ); | SignalType::int16 ); | ||
outputProperties.ChannelLabels()[Parameter( "SourceCh" )] = "@DigitalInput"; | |||
} | } | ||
</pre> | </pre> | ||
Line 131: | Line 181: | ||
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. | 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. | ||
The actual <tt> | Note that we allocate one more channel than the number of signal channels. This channel is for the digital input, which is a | ||
stream state, and as such not accessible from the helper thread our acquisition code runs in. | |||
By naming it <tt>@DigitalInput</tt>, we tell the framework to copy the channel's content into the stream state called "DigitalInput". | |||
The actual <tt>OnInitialize</tt> function will only be called if <tt>OnPreflight</tt> did not report any errors. | |||
Thus, you may skip any further checks, and write | Thus, you may skip any further checks, and write | ||
<pre> | <pre> | ||
void TachyonADC:: | void TachyonADC::OnInitialize(const SignalProperties&) | ||
{ | { | ||
mSourceCh = Parameter( "SourceCh" ); | mSourceCh = Parameter("SourceCh"); | ||
mSampleBlockSize = Parameter( "SampleBlockSize" ); | mSampleBlockSize = Parameter("SampleBlockSize"); | ||
mSamplingRate = Parameter( "SamplingRate" | mSamplingRate = Parameter("SamplingRate"); | ||
} | } | ||
</pre> | </pre> | ||
==Start and Stop== | |||
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:: | void TachyonADC::OnStartAcquisition() | ||
{ | |||
int err = TachyonStart(mHandle, mSamplingRate); | |||
if( err != TACHYON_NO_ERROR ) | |||
bcierr << "Could not start acquisition due to error " << err; | |||
} | |||
void TachyonADC::OnStopAcquisition() | |||
{ | { | ||
TachyonStop(); | 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 | ||
Line 159: | Line 225: | ||
avoid tying up the CPU.) | avoid tying up the CPU.) | ||
<pre> | <pre> | ||
void TachyonADC:: | void TachyonADC::DoAcquire( GenericSignal& outputSignal ) | ||
{ | { | ||
int valuesToRead = mSampleBlockSize * mSourceCh; | int valuesToRead = mSampleBlockSize * (mSourceCh + 1); // +1 for DigitalInput | ||
short* buffer; | short* buffer; | ||
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 + 1; ++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" << | bcierr << "Error reading data: " << err; | ||
} | } | ||
</pre> | </pre> | ||
As you can see, we read one more channel than the number of signal channels. This is because <tt>TachyonWaitForData()</tt> gives | |||
us one more value for the digital input, which we put into the last channel. Your code may look more complicated than this if you | |||
need to call a separate function to obtain the state of the digital input. | |||
In any case, it is important to understand that you cannot access states directly from <tt>DoAcquire()</tt> because it runs in a | |||
separate thread. Also, stream states such as <tt>DigitalInput</tt> need to be synchronized with signal data as it is acquired, rather | |||
than as it is sent from source to other BCI2000 components. | |||
==Adding the ''SourceFilter''== | ==Adding the ''SourceFilter''== |
Latest revision as of 15:47, 8 July 2022
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, with extensive comments detailing implementation steps and decisions. 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 void* TachyonOpen(); int TachyonClose(void* handle); int TachyonGetNumberOfChannels(void* handle); int TachyonStart(void* handle, int inSamplingRate); int TachyonStop(void* handle ); int TachyonWaitForData(void* handle, 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 OnAutoConfig() 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" 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), mHandle(nullptr) { } TachyonADC::~TachyonADC() { if (mHandle) TachyonClose(mHandle); } void TachyonADC::OnPublish() { BEGIN_PARAMETER_DEFINITIONS "Source int SourceCh= auto auto % % " "// 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= 128Hz 128Hz 1 4000 " "// this is the sample rate", END_PARAMETER_DEFINITIONS BEGIN_STREAM_DEFINITIONS "DigitalInput 8 0 0 0", // a stream that holds digital input data END_STREAM_DEFINITIONS }
The STREAM_DEFINITION defines a digital input stream that is synchronous with input data. If your amplifier/DAC has digital inputs, use such a stream to record them and make them available to BCI2000 components through the State interface.
ADC Auto-Configuration
In the OnAutoConfig function, it is possible to set parameters that have been set to a value of "auto". This way, hardware information may be read from the amplifier, avoiding double effort to synchronize user settings with hardware properties. In this example, we get the number of channels through the TachyonGetNumberOfChannels() function, and assign it to the SourceCh parameter. If the user wants to override this, it is still possible to specify an actual number instead of "auto" in the parameter file/Operator config dialog.
void TachyonADC::OnAutoConfig() { if (mHandle) TachyonClose(mHandle); mHandle = TachyonOpen(); if (!mHandle) throw bcierr << "Could not connect to Tachyon hardware"; Parameter("SourceCh") = TachyonGetNumberOfChannels(mHandle); }
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(mHandle, Parameter("SamplingRate").InHertz())) bcierr << "SamplingRate and/or SourceCh parameters are not compatible" << " with the A/D card"; TachyonStop(mHandle); outputProperties = SignalProperties(Parameter("SourceCh") + 1, Parameter("SampleBlockSize"), SignalType::int16 ); outputProperties.ChannelLabels()[Parameter( "SourceCh" )] = "@DigitalInput"; }
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.
Note that we allocate one more channel than the number of signal channels. This channel is for the digital input, which is a stream state, and as such not accessible from the helper thread our acquisition code runs in. By naming it @DigitalInput, we tell the framework to copy the channel's content into the stream state called "DigitalInput".
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(mHandle, 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 + 1); // +1 for DigitalInput short* buffer; int err = TachyonWaitForData(&buffer, valuesToRead); if (err == TACHYON_NO_ERROR) { int i = 0; for (int channel = 0; channel < mSourceCh + 1; ++channel) for (int sample = 0; sample < mSampleBlockSize; ++sample) outputSignal(channel, sample) = buffer[i++]; } else bcierr << "Error reading data: " << err; }
As you can see, we read one more channel than the number of signal channels. This is because TachyonWaitForData() gives us one more value for the digital input, which we put into the last channel. Your code may look more complicated than this if you need to call a separate function to obtain the state of the digital input. In any case, it is important to understand that you cannot access states directly from DoAcquire() because it runs in a separate thread. Also, stream states such as DigitalInput need to be synchronized with signal data as it is acquired, rather than as it is sent from source to other BCI2000 components.
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.