Jump to content

Contributions:SerialWidgetADC: Difference between revisions

From BCI2000 Wiki
Jhill (talk | contribs)
Jhill (talk | contribs)
No edit summary
Line 59: Line 59:


The requirements for widget programming are somewhat more stringent than they were for compatibility with the SerialInterface Extension.
The requirements for widget programming are somewhat more stringent than they were for compatibility with the SerialInterface Extension.
An example SerialWidgetADC-compatible sketch is provided in [https://bci2000.org/svn/trunk/src/contrib/SignalSource/SerialWidget/ExampleSourceSketch/ the ExampleSourceSketch subdirectory] (programs in the Arduino IDE are called "sketches" for some reason). ''ExampleSourceSketch.ino'' makes use of [https://www.arduino.cc/reference/en/libraries/keyhole the Keyhole library], which can be installed via the IDE's library manager and which makes it easy for sketches to respond to serial-port commands and to allow their variables to be read and written. This sketch is an expanded version of the ''TTLExampleSketch.ino'' provided with the SerialInterface Extension, and it handles TTL input and output in the same way; however, courtesy of the additional module ''SignalAcquisition.cpp'', the sketch also fulfills the following additional requirements for SerialWidgetADC compatibility:
An example SerialWidgetADC-compatible sketch is provided in [https://bci2000.org/svn/trunk/src/contrib/SignalSource/SerialWidget/ExampleSourceSketch/ the ExampleSourceSketch subdirectory] (programs in the Arduino IDE are called "sketches" for some reason). ''ExampleSourceSketch.ino'' makes use of [https://www.arduino.cc/reference/en/libraries/signalacquisition SignalAcquisition] and [https://www.arduino.cc/reference/en/libraries/keyhole Keyhole] libraries, which can be installed via the IDE's library manager and which makes it easy for sketches to respond to serial-port commands and to allow their variables to be read and written. This sketch is an expanded version of the ''TTLExampleSketch.ino'' provided with the SerialInterface Extension, and it handles TTL input and output in the same way; however, courtesy of the ''SignalAcquisition'' library, the sketch also fulfills the following additional requirements for SerialWidgetADC compatibility:


* To enable signal acquisition, the sketch must support the following commands, sent over the serial port (the specific numbers are just examples, but the variable names and syntax must be supported as shown):
* To enable signal acquisition, the sketch must support the following commands, sent over the serial port (the specific numbers are just examples, but the variable names and syntax must be supported as shown):
Line 83: Line 83:
==Widget Timing==
==Widget Timing==


The particular implementation in the ''SignalAcquisition.cpp'' module accompanying ''ExampleSourceSketch.ino'' contains features that will help you verify and debug your widget's timing.
The particular implementation in the ''SignalAcquisition'' library contains features that will help you verify and debug your widget's timing.


#. In the Serial Monitor of the Arduino IDE, first send the commands to set the acquisition parameters the way you want them. (NB: where you see <code>\n</code> above, do not literally type a backslash followed by an <code>n</code>: the <code>\n</code> stands for the newline character, which the Serial Monitor will append when you press return.)
#. In the Serial Monitor of the Arduino IDE, first send the commands to set the acquisition parameters the way you want them. (NB: where you see <code>\n</code> above, do not literally type a backslash followed by an <code>n</code>: the <code>\n</code> stands for the newline character, which the Serial Monitor will append for you when you press return.)
#. Then, additionally send <code>commOverheadMicros=1</code> to enable measurement of the serial-port overhead.
#. Then, additionally send <code>commOverheadMicros=1</code> to enable measurement of the serial-port overhead.
#. Then send <code>mute=0</code> to begin sending sample-blocks.
#. Then send <code>mute=0</code> to begin sending sample-blocks.

Revision as of 15:11, 4 October 2023

Synopsis

An ADC that allows signals to be acquired from programmable serial-port devices such as the Arduino, Teensy, Pico, etc.


Location

http://www.bci2000.org/svn/trunk/src/contrib/SignalSource/SerialWidget


Versioning

Authors

Jeremy Hill (hill@neurotechcenter.org)

Version History

  • 2023-09-22: Initial public release

Source Code Revisions

  • Initial development: r7613
  • Known to compile under: r7613
  • Broken since: --


Functional Description

The SerialWidgetADC filter is the key component of the SerialWidget source module, and can acquire signals from a serial-port-equipped embedded system such as an Arduino, Teensy or Pico microcontroller (hereafter referred to as a "widget").

This ADC includes all the functionality of the SerialInterface Extension (which allows widgets to be used alongside other source acquisition hardware, providing auxiliary input and output). The difference is that the ADC allows the primary signal to be acquired from the widget as well. This provides cheap platform possibilities for realtime testing during development of BCI2000 setups (as an alternative to the SignalGenerator, SoundcardSource or FilePlayback modules) and also allows BCI2000 to be used as a development tool in microcontroller programming (when you need higher data rates, better timing stability or greater flexibility than the Arduino IDE's Serial Plotter can provide).

Developer note: the SerialInterface Extension and the SerialWidgetADC are aware of each other, and the Extension will automatically disable itself in the presence of the ADC, so there is no conflict if the Extension is turned on in CMake during the build.

Parameters

Parameters for handling widgets are described under Contributions:SerialInterface.

As in the SerialInterface Extension, the --SerialPort parameter must be supplied on the command-line and cannot be changed without relaunching BCI2000. For example, your BCI2000 script might contain the line:

 start executable SerialWidget --local --SerialPort=COM4:baud=9600,dtr=on

The same is true of the --PublishCommand parameter, if used.

The one departure from SerialInterface behavior is the handling of custom StartCommand and StopCommand messages. ADCs expect the signal to be delivered between runs as well as during runs. Therefore, if a StartCommand message is specified, the ADC sends it to the widget as soon as the user presses Set Config (whereas the SerialInterface Extension would wait until the user presses Start or Resume). Similarly, if a StopCommand message is specified, the ADC only sends it immediately prior to disconnecting and reconnecting on subsequent Set Config operations, and/or when BCI2000 quits (whereas the SerialInterface Extension would send it when the user presses Suspend).

Additional Parameters may be defined by the widget itself, if you specify a --PublishCommand on the command-line and the widget is programmed to support the specified command, as described in the SerialInterface documentation.


Parameters common to all source modules are described under User Reference:DataIOFilter. SerialWidgetADC adds only one parameter of its own:

SourceChPins

This is a list of integer values, one per channel. The widget interprets these numbers when deciding how to acquire signals. The interpretation depends entirely on the way the widget is programmed: the simplest way would be to interpret the numbers as indices of the pins from which signals should be read (for example, as arguments to analogRead() or digitalRead() in the Arduino language). The parameter takes its name from this idea. However, a more-sophisticated widget might be programmed to do its own digital signal processing, and/or to generate artificial signals in response to external sensor inputs, and the relationship between these signals and the SourceChPins values may be arbitrary, as long as the number of channels matches the number of SourceChPins values.

States

The SerialWidgetADC does not define any State Variables or Events of its own, but can create Events as directed by the widget, as described in the SerialInterface documentation.


Widget Programming

The requirements for widget programming are somewhat more stringent than they were for compatibility with the SerialInterface Extension. An example SerialWidgetADC-compatible sketch is provided in the ExampleSourceSketch subdirectory (programs in the Arduino IDE are called "sketches" for some reason). ExampleSourceSketch.ino makes use of SignalAcquisition and Keyhole libraries, which can be installed via the IDE's library manager and which makes it easy for sketches to respond to serial-port commands and to allow their variables to be read and written. This sketch is an expanded version of the TTLExampleSketch.ino provided with the SerialInterface Extension, and it handles TTL input and output in the same way; however, courtesy of the SignalAcquisition library, the sketch also fulfills the following additional requirements for SerialWidgetADC compatibility:

  • To enable signal acquisition, the sketch must support the following commands, sent over the serial port (the specific numbers are just examples, but the variable names and syntax must be supported as shown):
  samplesPerSecond=1000\n
  samplesPerBlock=40\n
  sourcePins="26 27"\n
  • The widget is responsible for sample timing. When the widget has acquired a complete sample-block (the specified number of signal samples from all the specified pins) it must send a sample-block header, which is either \x01\x00 or \x00\x01 followed by a line-ending (\n or \r\n). Disregarding the line-ending, the header is actually the 16-bit binary representation of the number 1 (the former version is little-endian, the latter big-endian). Based on the header, the ADC will automatically determine whether the subsequent sample data need to be endian-swapped. The header can be sent easily using the following Arduino code:
  const uint16_t endianMarker = 1;
  Serial.write( (uint8_t*)&endianMarker, sizeof(endianMarker) );
  Serial.println(); // don't forget the line-ending
 
  • Immediately after the sample-block header, the widget must send the raw sample data as packed 32-bit floats. The size of this payload is exactly (number of pins)*samplesPerBlock*sizeof(float) bytes. All the different channels' values for the first sample should be sent in the order specified by sourcePins, then all the channels' values for the second sample, and so on.
  Serial.write( (uint8_t *)data, numberOfBytes );
  Serial.println(); // a line-ending at this point is optional, but makes debugging easier in the Arduino IDE's Serial Monitor
  Serial.flush();   // important for good timing
  • Between sample-blocks, the widget may send other messages provided they end with a line-ending (\n or \r\n) and do not begin with \x01\x00 or \x00\x01. This allows it to send the same message types that the SerialInterface Extension allows, which are:
    • BCI2000 Event descriptor lines, to mark the timing of arbitrary events: BCI2000 will attempt to interpret any line that does not begin with { as an Event descriptor;
    • error messages: BCI2000 will issue a fatal error containing the text of any line it receives from the widget if that line begins with { and contains the substring _ERROR_ (which is true of any error message sent by the Keyhole library);
    • any other JSON output: BCI2000 will simply ignore any other line beginning with the { character.

Widget Timing

The particular implementation in the SignalAcquisition library contains features that will help you verify and debug your widget's timing.

  1. . In the Serial Monitor of the Arduino IDE, first send the commands to set the acquisition parameters the way you want them. (NB: where you see \n above, do not literally type a backslash followed by an n: the \n stands for the newline character, which the Serial Monitor will append for you when you press return.)
  2. . Then, additionally send commOverheadMicros=1 to enable measurement of the serial-port overhead.
  3. . Then send mute=0 to begin sending sample-blocks.
  4. . Send mute=1;? to stop the flow and examine variables.
  5. . Repeatedly alternate mute=0 with mute=1;? to take multiple readings.

Verify the following:

  1. . The idleLoops variable should be reliably greater than 0 when queried with mute=1;? . If it is 0, this indicates that your sketch may be unable to keep up with the desired sample rate. You need to reduce samplesPerSecond.
  2. . The commOverheadMicros variable will now contain a timing measurement indicating the number of microseconds it took to send data over the serial port for the last sample-block. This will be roughly proportional to the number of channels and the number of samples per block. It should not exceed the sample period in microseconds (1e6/samplesPerSecond): if it does, your data will contain readings that were unevenly sampled in time. In that case, you should reduce samplesPerSecond, reduce samplesPerBlock, or reduce the number of channels in sourcePins.

See also

User Reference:DataIOFilter, Programming Reference:GenericADC Class, Contributions:SerialInterface