New data acquisition module/source module question

Forum for software developers to discuss BCI2000 software development
Locked
andrej
Posts: 2
Joined: 05 Sep 2012, 12:11

New data acquisition module/source module question

Post by andrej » 24 Sep 2012, 14:54

Hi,
We are building 8 electrode EEG cap and amplifier at my university and I want to connect it with BCI2000 through COM port in order to see how it works by using P3 speller.
Since we are building amplifier from scratch, I assume that we cannot use any of the supported data acquisition filters/source modules (or I could if I found one that is made for amplifier(A/D converter) that is similar to ours?).

I read several tutorials on BCI2000 wiki page, but I am not sure if I need to build new source module, as it was explained here:
http://www.bci2000.org/wiki/index.php/P ... a_template
or data acquisition module as explained here:
http://www.bci2000.org/wiki/index.php/P ... ion_Module
or both, or something third?

One more thing, in data acquisition module tutorial, example scenario is that my A/D card comes with C-style software interface declared in its header file with several functions that configure the card, start acquisition, store data, stream data and so on. But what should I do if my A/D card doesn’t come with it?

Could you give me some guidelines so that I can continue my work?
Thanks,
Andrej

mellinger
Posts: 1163
Joined: 12 Feb 2003, 11:06

Re: New data acquisition module/source module question

Post by mellinger » 25 Sep 2012, 13:14

Hi,

BCI2000 supports A/D converters made by National Instruments, and by Measurement Computing. In order to connect to any other A/D hardware, you will need to write a BCI2000 source module.

Code: Select all

One more thing, in data acquisition module tutorial, example scenario is that my A/D card comes with C-style software interface declared in its header file with several functions that configure the card, start acquisition, store data, stream data and so on. But what should I do if my A/D card doesn’t come with it?
If the card is connected via a COM port, and comes without a client library, then it will be necessary for your source module to implement the communication protocol, rather than calling library functions. Still, the protocol will define byte sequences that start or stop data acquisition, configure the card, etc. Instead of calling a library function, you send the corresponding byte sequence to the COM port.

BCI2000 provides a class that allows for convenient communication over COM ports. It's called serialstream, and allows you to write into/read from a std::iostream that is tied to a COM port (src/shared/utils/SerialStream). However, you will also need to configure the COM port to be compatible with your card. This must be done using the means of the operating system, which differ strongly between operating systems. On Windows, you may use the SetDefaultCommConfig() API function in order to configure COM port settings. Then, you may open a SerialStream object, which will open the COM port with those default settings, and perform actual communication for you.
The following example code may be helpful:

Code: Select all

// MyADC.h:
#include "BufferedADC.h"
#include "SerialStream.h"
class MyADC : public BufferedADC
{
...
 private:
  bool ConnectToAD( SerialStream& ) const;

  SerialStream mPort;
...
};

// MyADC.cpp
...
#include <windows.h>
...

void
MyADC::OnPreflight( ... ) const
{
  ...
  SerialStream port;
  if( !ConnectToAD( port ) )
    bcierr << "Cannot connect to AD on port " << Parameter( "COMport" ) << endl;
  ...
}

void
MyADC::OnInitialize( ... )
{
  ...
  ConnectToAD( mPort );
  ...
}

void
MyADC::OnStartAcquisition()
{
  mPort << "SENDDATA\n" << flush;
}

void
MyADC::OnAcquireData( GenericSignal& Output )
{
#if DATA_SENT_AS_SPACE_SEPARATED_ASCII_NUMBERS
  double value;
  for( int ch = 0; ch < Output.Channels(); ++ch )
    for( int sample = 0; sample < Output.Elements(); ++sample )
    {
      mPort >> ws >> value;
      Output( ch, sample ) = value;
    }
#elseif DATA_SENT_AS_BINARY_FLOATS_IN_LITTLE_ENDIAN_FORMAT
  float32_t value;
  union { float32_t* f; uint32_t* i; } valuePtr = { &value };
  for( int ch = 0; ch < Output.Channels(); ++ch )
    for( int sample = 0; sample < Output.Elements(); ++sample )
    {
      mPort.read( valuePtr.i, sizeof( *valuePtr.i ) );
      Output( ch, sample ) = *valuePtr.f;
    }
#endif
}

bool
MyADC::ConnectToAD( SerialStream& outStream ) const
{
  outStream.clear();
  outStream.close();
  string comport = Parameter( "COMport" );
  COMMCONFIG config = { sizeof( COMMCONFIG ), 1 };
  config.dwProviderSubtype = PST_RS232;
  ::BuildCommDCB( "baud=1200 parity=N data=8 stop=1", &config.dcb );
  bool success = ::SetDefaultCommConfig( comport.c_str(), &config, sizeof( config ) );
  if( success )
  {
    outStream.open( comport.c_str() );
    success = outStream.is_open();
  }
  if( success )
  { 
    outStream << "GETVERSION\n" << flush;
    string response;
    success = getline( outStream, response, '\n' );
  }
  if( success )
    bcidbg << "Connected to AD, version is " << response << endl;
  return success;
}
Obviously, you will need to replace the example commands such as GETVERSION with the codes described in your AD documentation. Also make sure to use correct termination with both commands and responses, which need not be '\n', might be a sequence of characters such as "\r\n", or the protocol is totally different, using binary commands. In that case, you would do

Code: Select all

...
  enum { Start = 0x01, Stop = 0x02, GetVersion = 0x03, ... };
...
  mPort.put( GetVersion ).flush();
  uint8_t version = mPort.get();
...
  mPort.put( Start ).flush();
...
  mPort.put( Stop ).flush();
...
Unfortunately, the tutorial is a bit outdated. For a new source module, you should derive from the BufferedADC base class rather than directly deriving from GenericADC, as still suggested in the tutorial. This way, you will not lose data if one of your BCI2000 modules should temporarily require more computation time than allowed for by sample block duration. (But note that this violation of the realtime constraint may be only temporary, and mean processing time must stay below a block duration.)

The BufferedADC class has slightly different member functions which you need to implement in your own ADC class. But using the information from the tutorial, and reading the information at the top of the BufferedADC header file, and from the example code in this post, it will be clear for you to know how to implement those functions.

If you have further questions, let me know.
Juergen

andrej
Posts: 2
Joined: 05 Sep 2012, 12:11

Re: New data acquisition module/source module question

Post by andrej » 04 Oct 2012, 02:53

Hi Juergen,

Thank you very much for your detailed response. It really helped me to get going.
I tried to implement the code that you presented and even though I am sure that my code has a lot of errors, visual studio is not reporting them to me so I managed to build the exe file.
Unfortunately I still don't understand several details.

Since we will not have amplifier ready until the end of the semester, I will not be able to check if my code is working, so I will try to attach output of signal generator (sine wave) to serial port of my computer and see if I am getting any response in BCI2000.
But now I am trying to at least get the software to go through the Preflight phase and it is not working because I don't know what exactly is "example command GETVERSION" from the code that you gave me:
outStream << "GETVERSION\n" << flush;
(by the way, is senddata from the line: mPort << "SENDDATA\n" << flush; also example command or I can leave it like that?)


this is what I have done so far, and it gives me the error:

Code: Select all

#include "PCHIncludes.h"
#pragma hdrstop

#include<iostream>
#include "PolyBuffADC.h"
#include "BCIError.h"

#include "SerialStream.h"
#include "ThreadUtils.h" // for SleepFor()

using namespace std;


RegisterFilter( PolyBuffADC, 1 );

PolyBuffADC::PolyBuffADC():
mSourceCh(0),
     mSampleBlockSize(0),
     mSamplingRate(0)
{

 BEGIN_PARAMETER_DEFINITIONS

    "Source:Signal%20Properties int SourceCh= 1 "
       "// number of digitized and stored channels",

    "Source:Signal%20Properties int SampleBlockSize= 32 "
       "// number of samples transmitted at a time",

    "Source:Signal%20Properties float SamplingRate= 512Hz "
       "// sample rate",
		
	 "Source string COMport= COM3"
      "// COMport for PolyAmp",
	
 END_PARAMETER_DEFINITIONS


 BEGIN_STATE_DEFINITIONS

 "Running 1 0 0 0"
   
 END_STATE_DEFINITIONS

}

PolyBuffADC::~PolyBuffADC()
{
	OnHalt();
}

void
PolyBuffADC::OnHalt()
{

}

void
PolyBuffADC::OnPreflight( SignalProperties& Output ) const
{
  // The user has pressed "Set Config" and we need to sanity-check everything.
  // For example, check that all necessary parameters and states are accessible:

	/*Local Variables: */
  int sourceCh = Parameter( "SourceCh" ),
  sampleBlockSize = Parameter( "SampleBlockSize" ),
  samplingRate = static_cast<int>( Parameter( "SamplingRate" ).InHertz() );	//not sure bout this one
	
	// Check that the user is asking for one channel
	//after, we are going to change it to 8 channels.
  if( sourceCh != 1 )
	bcierr << "PolyBuff only has one electrode for now.  Set SourceCh=1." << endl;
  if( samplingRate != 512 )
     bcierr << "PolyBuff has a sampling rate of 512Hz.  Set SamplingRate to 512." << endl;
  if( Parameter( "SampleBlockSize" ) >64 )
    bcierr << "Sample Block Size shouldnt be bigger than 64" << endl;


	string COMportParam = Parameter( "COMport" );
  if( COMportParam.empty() )
  {
    bcierr << "No COM port specified for device" << endl;
    return;
  }

	serialstream port;
	if( !ConnectToAD( port ) )
		bcierr << "Cannot connect to AD on port " << Parameter( "COMport" ) << endl;


  int numberOfChannels = Parameter( "SourceCh" );					//same as sourceCh from above
  int samplesPerBlock  = Parameter( "SampleBlockSize" );	//same as sampleBlockSize from above
  SignalType sigType = SignalType::float32;  // could also parameterize this
  Output = SignalProperties( numberOfChannels, samplesPerBlock, sigType );

}

bool PolyBuffADC::ConnectToAD(serialstream& outStream) const{
  outStream.clear();
  outStream.close();
  string comport = Parameter( "COMport" );
  COMMCONFIG config = { sizeof( COMMCONFIG ), 1 };
  config.dwProviderSubType = PST_RS232;
  ::BuildCommDCB( "baud=1200 parity=N data=8 stop=1", &config.dcb );
  bool success = ::SetDefaultCommConfig( comport.c_str(), &config, sizeof( config ) );
  if( success )
  {
		bcierr << "if 1" << endl;   //added if for debuging

    outStream.open( comport.c_str() );
    success = outStream.is_open();
  }
	string response;
  if( success )
  {
		bcierr << "if 2" << endl;   //added if for debuging
		outStream << "GETVERSION\n" << flush;
    success = getline( outStream, response, '\n' );
  }
  if( success )
		bcierr << "if 3" << endl;   //added if for debuging, never enters here
		bcidbg << "Connected to AD, version is " << response << endl;
  return success;
}

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

  serialstream mPort;
  ConnectToAD( mPort );
  mLastTime = PrecisionTime::Now();
}

void 
PolyBuffADC::OnStartAcquisition()
{
  mPort << "SENDDATA\n" << flush;
}

void
PolyBuffADC::DoAcquire( GenericSignal& Output )
{
	double value;
  for( int ch = 0; ch < Output.Channels(); ++ch )
    for( int sample = 0; sample < Output.Elements(); ++sample )
    {
      mPort >> ws >> value;
      Output( ch, sample ) = value;
    }



  /*given by default: */
  // For now, we output flat lines:
  for( int ch = 0; ch < Output.Channels(); ch++ )
    for( int el = 0; el < Output.Elements(); el++ )
      Output( ch, el ) = 0.0f;

  // Here is a wait loop to ensure that we do not deliver the signal faster than real-time
  // (In your final implementation, you should remove this: the hardware will play this role then.)
  while( PrecisionTime::UnsignedDiff( PrecisionTime::Now(), mLastTime ) < mMsecPerBlock ) ThreadUtils::SleepFor(1);
  mLastTime = PrecisionTime::Now();
}

void
PolyBuffADC::StartRun()
{
  // The user has just pressed "Start" (or "Resume")
  bciout << "Hello World!" << endl;

  mLastTime = PrecisionTime::Now();
}

void
PolyBuffADC::StopRun()
{
  // The Running state has been set to 0, either because the user has pressed "Suspend",
  // because the run has reached its natural end.
  bciout << "Goodbye World." << endl;
}

void
PolyBuffADC::OnStopAcquisition()
{
  // This method will always be called before OnHalt is called.
}
and this is what I get in the operator log:
2012-10-04T01:59:33 - BCI2000 Started
2012-10-04T01:59:35 - Waiting for configuration ...
2012-10-04T01:59:41 - Waiting for configuration ...
2012-10-04T01:59:43 - Waiting for configuration ...
2012-10-04T02:00:37 - Operator set configuration
2012-10-04T02:00:37 - DataIOFilter::Preflight: PolyBuffADC::Preflight: if 1.
2012-10-04T02:00:41 - DataIOFilter::Preflight: PolyBuffADC::Preflight: if 2.
2012-10-04T02:00:42 - DataIOFilter::Preflight: PolyBuffADC::Preflight: Cannot connect to AD on port COM3.

if 1 and if 2 are lines which I added to ConnectToAD(serialstream& outStream) so that I could localize the error(the line
outStream << "GETVERSION\n" << flush; causes the error since I dont understand what it does.)


So, Could you help me and tell me or maybe give me a hint about what I need to change so that I could read the data from my one channel signal generator connected to COM port?
Sorry for the long and maybe a bit messy post.

Thank you very much once more!
Best,
Andrej

mellinger
Posts: 1163
Joined: 12 Feb 2003, 11:06

Re: New data acquisition module/source module question

Post by mellinger » 04 Oct 2012, 08:46

Hi Andrej,
so I will try to attach output of signal generator (sine wave) to serial port of my computer and see if I am getting any response in BCI2000.
You cannot connect an analog signal to your computer's serial port. A serial port is a digital interface that connects to devices that follow the RS232 specification. Please google for "RS232" to understand what that means.
I don't know what exactly is "example command GETVERSION" from the code that you gave me:
outStream << "GETVERSION\n" << flush;
(by the way, is senddata from the line: mPort << "SENDDATA\n" << flush; also example command or I can leave it like that?)
No, you certainly cannot leave it like that. I was assuming that you were using an AD ("analog-to-digital") converter that was able to connect to a serial port, and that would thus provide a set of commands to send over the serial port. Please understand that, in order for an EEG amplifier to connect to a computer, an AD converter must be used to convert the amplified analog EEG signal into numbers, which may then be received by the computer. In your case, the AD converter might be built into the EEG device, or it may be a separate module. AD converters may connect to the computer via a serial port, an USB port, or they may be extension cards to be physically built into the computer. The vendor of the AD converter will typically provide a library that contains functions in the style of the library assumed in the source module tutorial. For AD converters connecting via the serial port, the vendor may provide an interfacing protocol rather than a library. My example code was meant for exactly that case, assuming that you had a manual for a certain AD converter that would tell you the actual commands to use instead of my example commands.

Now I have difficulties to understand what you meant when you wrote
We are building 8 electrode EEG cap and amplifier at my university and I want to connect it with BCI2000 through COM port in order to see how it works by using P3 speller.
Since we are building amplifier from scratch, I assume that we cannot use any of the supported data acquisition filters/source modules (or I could if I found one that is made for amplifier(A/D converter) that is similar to ours?).
So it seems that you have a certain AD converter in mind, or not? If yes, and if it connects via the serial port, then look up the appropriate commands in its manual. If not, then decide on which AD converter to use before writing a BCI2000 source module that connects to it.

Apart from that, a few comments on your code:
* Writing to bcierr will always result in an error message. For debugging, write to bciout, or better to bcidbg(0).
* In C and C++, curly braces must be used around multiple commands after if, for, while statements. Indentation is ignored by the compiler. Working through the first chapters of an introductory C++ textbook will help you avoid a lot of mistakes which will otherwise cost you weeks of frustration.

HTH,
Juergen

Locked

Who is online

Users browsing this forum: No registered users and 2 guests