Contributions:SerialInterface
Synopsis
An extension that allows flexible communication to and from serial-port devices such as programmable microcontrollers (e.g. Arduino, Teensy, Pico, etc.)
Location
http://www.bci2000.org/svn/trunk/src/contrib/Extensions/SerialInterface
Versioning
Authors
Jeremy Hill (hill@neurotechcenter.org)
Version History
- 2023-08-10: Initial public release
Source Code Revisions
- Initial development: r7523
- Known to compile under: r7523
- Broken since: --
Functional Description
Among the biosignal acquisition devices supported by BCI2000, a few allow generation of TTL pulses to trigger or otherwise synchronize with other devices; most, however, do not. Furthermore, many devices support acquisition of TTL pulses and other auxiliary information alongside biosignal data; however, some devices lack even this capability.
Where such functionality is lacking, one flexible option for adding it to your system is to build and program a custom solution based on a microcontroller such as an Arduino, Teensy or Pico (hereafter referred to as a "widget"). The SerialInterface extension allows you to interface with such widgets.
The primary intended purpose of the SerialInterface is to send arbitrary byte strings to a widget over a serial port whenever specified BCI2000 Expressions become true---this is a simple way in which digital-output functionality may be supplied for hardware that lacks it. This mechanism could also allow your BCI system to control almost anything you can imagine attaching to a custom microcontroller. Since the outgoing byte strings can be configured arbitrarily, you might possibly be able to make this work even if you cannot (re-)program the widget yourself.
A secondary function of the SerialInterface is to receive and log Event information. For this, the widget must be programmed to output strings in a way that BCI2000 will understand. Optionally, the widget may also define its own Parameters and Events. Together, these mechanisms allow information from arbitrary sensors to be sent to BCI2000, mediated by a programmable widget.
An example BCI2000-compatible microcontroller sketch, for the Arduino IDE, is provided in the TTLExampleSketch subdirectory. It makes use of 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.
Enabling SerialInterface
Like all extensions, SerialInterface is only available if your signal source module was compiled with the appropriate CMake flag enabled: in this case, EXTENSIONS_SERIALINTERFACE=ON.
The SerialInterface must be enabled by supplying a value for the `--SerialPort` parameter on the command-line. For example,
start executable SignalGenerator --local --SerialPort=COM4:baud=9600,dtr=on
In the example above, note that a suffix has been appended to the usual serial-port address COM4, attached by a colon. In this optional suffix, you can specify a comma-separated list of options as understood by the Windows MODE command (see https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-buildcommdcba). These options may or may not be necessary for your widget---for example, we have found that the Teensy does not seem to care whether the dtr option is on or off; however, the Pico will not work properly unless this option is explicitly turned on, whereas the ItsyBitsy M4 will fail to communicate if it is turned on.
Defining BCI2000 Parameters and Events from the Widget
If you want your widget to define its own BCI2000 Parameters and Events, the widget must be programmed to send this information to BCI2000 on command, and you can specify exactly what sequence of bytes the command comprises. To ensure that the chosen command is sent to the widget during the "publish" phase of BCI2000 startup, you should specify it as the --PublishCommand parameter, again on the command line:
start executable SignalGenerator --local --SerialPort=COM4:baud=9600,dtr=on --PublishCommand=publish\n
The particular byte sequence publish\n works with TTLExampleSketch.ino.
When the widget receives the publish command, it must reply with one or more lines of text. BCI2000 will attempt to interpret any line containing the = character as a Parameter definition, and any other non-empty line as an Event definition. The widget must send a blank line (terminated with \n) to signify the end of the definitions (if your sketch omits this, BCI2000 will hang indefinitely, waiting for more definitions).
A widget Parameter definition may be specified as (readonly), in which case the Parameter may be of any type and will not be editable by the user. If (readonly) is not found in the Parameter definition comment, the Parameter will actually be configurable by the user: in this case, only int, float and string Parameters are allowed. If the widget defines configurable Parameters, Parameter values will be sent to the widget when the user presses "Set Config", using strings of the following format:
foo=1\n bar=3.0\n boo="this is a string parameter value"\n
Your widget sketch must be able to interpret such commands (the Keyhole library makes this easy, as shown in the example sketch).
Parameters
If enabled using the --SerialPort command-line parameter, the full set of SerialInterface's parameters (described below) will appear in the Source tab. In addition, any Parameters defined by the widget itself, in response to a --PublishCommand, will appear wherever in whichever tabs and sections their definitions dictate.
Note that, wherever a SerialInterface Parameter specifies a byte string to be sent to the widget, the byte string may be expressed using backslash escapes familiar to the C/C++ or Python programmer: \n, \r, \t, \0, \\ and \xNN are all recognized (where NN stands for a pair of hex digits---these are interpreted like Python, not like C: in other words, the maximum expected number of digits is 2). Other backslash escapes are not supported.
SerialPort
The port address, optionally followed by a colon and a comma-delimited sequence of serial port options as described above. This must be specified on the command-line and cannot be changed without quitting and relaunching BCI2000.
PublishCommand
The optional sequence of bytes that prompts the widget to send its Parameter and Event definitions. This must be specified on the command-line and cannot be changed without quitting and relaunching BCI2000.
StartCommand
If specified, this sequence of bytes is sent to the widget whenever a run is started (e.g. when Start or Resume is pressed).
In the example sketch, the string mute=0\n can be used, although it is not necessary for most microcontrollers.
StopCommand
If specified, this sequence of bytes is sent to the widget whenever a run stops (e.g. when Suspend is pressed).
In the example sketch, the string mute=1\n can be used, although it is not necessary for most microcontrollers.
SerialOutputs
This matrix must comprise two columns. The first column contains Expressions. The second column contains (backslash-escaped) byte strings. Expressions are evaluated at the beginning of each sample-block. If an Expression was previously zero and now evaluates to non-zero, the corresponding byte string is immediately sent to the widget. In this way, you can link a BCI2000 State Variable to a widget command that, for example, causes a TTL pulse to be generated.
ElseIf
This parameter dictates how the rows of the SerialOutputs Parameter are processed. You may choose to process all rows on every sample block (and potentially send all byte strings, one after the other). or to stop at the first match (so, on any given sample-block, at most one of the byte strings will be sent).
State Variables
The SerialInterface does not define any State Variables or Events of its own, but will define any Events the widget tells it to define in response to a --PublishCommand.
During a run, a widget may change an Event value at any time by sending the name of the event, followed by a space, followed by the value expressed as decimal text, followed by a newline. For example, the example sketch will send either
TTLInput 0\n
or
TTLInput 1\n
whenever the voltage on its input pin changes from high to low, or low to high, respectively.
Note that the Event in question does not have to have been defined by the widget---it could have been added by one of the other filters and loggers, or using an add event command before the startup system line in your BCI2000 script. So, even if you want to log Events from a widget, you do not necessarily have to implement the widget's response to a --PublishCommand.