Jump to content

Contributions:SerialInterface

From BCI2000 Wiki

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
  • 2025-06-13: SerialInput parameter added, expanding the options for two-way communication.

Source Code Revisions

  • Initial development: r7523
  • Known to compile under: r8888
  • Broken since: --

Functional Description

Among the biosignal acquisition devices supported by BCI2000, a few allow "digital output", i.e. the generation of TTL pulses that might be used to trigger or otherwise synchronize with other devices; most, however, do not offer this functionality. 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 BCI2000 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. This can be done in two ways: either by supplying a fixed set of byte strings that BCI2000 should respond to when it receives them from the widget, or by having the widget send actual BCI2000 event descriptors. For the latter, the widget must be programmed to output strings in the format that BCI2000 will understand. If you have the ability to re-program your widget, then you may also optionally make the widget define its own BCI2000 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.

Note that the SerialInterface Extension is intended to provide auxiliary input/output functionality to source modules that acquire their primary signal elsewhere. If you also want to acquire the primary signal from the same widget, you should use the closely-related SerialWidgetADC, which is part of the SerialWidget signal-source module and which requires a more-sophisticated sketch.

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.

Then, when you launch BCI2000, SerialInterface must be enabled by supplying a value for the --SerialPort parameter on the command-line. For example, in a BCI2000 script, you might use the line:

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 Microsoft's documentation for the configuration string passed to the BuildCommDCBA() function). These options may or may not be necessary for your widget—for example, we have found that the Teensy microcontroller 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 or \r\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. Such read-only parameters are a means by which the widget can make annotations that will be stored in data files' parameter headers (e.g. information about microcontroller model or sketch version). On the other hand, if the (readonly) flag 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

In that case, your widget sketch must be able to interpret such commands (the Keyhole library makes this easy, as shown in the example sketch).

You may also chose to specify a ParameterUpdateCommand: if so, the specified bytes will be sent to the widget in the "StopRun" phase, to request updates to the values of widget-defined parameters. When the widget receives the specified parameter-update command, just as with the publish command, it must reply with a sequence of lines terminated by a blank line. As before, if you choose to use this option, BCI2000 will hang until it receives that final blank line. This time, however, the non-blank lines are expected to have the same syntax as the lines that were sent to the widget on Set Config, e.g. Foo=123 or Boo="some string". Note that you can only update parameters that the widget itself has previously declared, and you cannot update a parameter that was declared as (readonly).

Parameters

If enabled using the --SerialPort command-line parameter, the full set of SerialInterface's parameters (described below) will appear in the Source tab of BCI2000's Config window. In addition, any Parameters defined by the widget itself, in response to a --PublishCommand, will appear 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 (so, use \x07 instead of \a, use\x0B instead of \v, etc.). If your widget sketch uses the Keyhole library, commands are expected to end with either a newline \n or a semicolon ; (but in a BCI2000 script, semicolons mean something to the script interpreter, so you would have to ensure they're used in quotes).

SerialPort

This string specifies 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 when the signal source module is launched and its value cannot be changed without quitting and relaunching BCI2000. If this parameter is absent or its value is empty, the SerialInterface extension is entirely disabled.

PublishCommand

This optional string specifies the sequence of bytes that should be sent by BCI2000 to request Parameter and Event definitions from the widget. If used, this must be specified on the command-line when the signal source module is launched. Its value 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). With TTLExampleSketch.ino, the string mute=0\n can be used, although the sketch will work on most microcontrollers even without a StartCommand and StopCommand.

StopCommand

If specified, this sequence of bytes is sent to the widget whenever a run stops (e.g. when Suspend is pressed) and also when Set Config is pressed. With TTLExampleSketch.ino, the string mute=1\n can be used, although the sketch will work on most microcontrollers even without a StartCommand and StopCommand.

ParameterUpdateCommand

If specified, this sequence of bytes is sent to the widget whenever a run stops (e.g. when Suspend is pressed). As with the PublishCommand, BCI2000 will then expect the widget to reply with a sequence of lines, terminated by a blank line. Each line is expected to be of the format ParameterName = VALUE, where ParameterName must denote a writeable parameter previously declared by the widget, and VALUE can be a simple numeric value or string (quoted and backslash-escaped if necessary). This allows new parameter values to be carried over from one run to the next.

SerialInputs

This matrix parameter gives BCI2000 the limited ability to respond to serial-port messages sent by the widget, even if you cannot re-program your widget.

If not empty, the matrix must have four columns. The first column contains (backslash-escaped) byte strings that the widget might send to BCI2000. The second column contains names of State Variables that have been declared as Events (perhaps using ADD EVENT in your launcher script). The third column contains the integer value that should be assigned to the Event when the byte string is received. In the fourth column, the entry should either be left blank (in which case, the Event will remain at the assigned value until it is changed again) or set to 0 (in which case the Event is transient, remaining at the assigned value only for one sample before automatically returning to 0). Thus, the second, third and fourth columns can be read from left to right as a BCI2000 Event descriptor, like MyEvent 123 or MyEvent 123 0

A given byte string is allowed to appear more than once in the table, to trigger multiple Events simultaneously. The Event name (second column) is allowed to be blank, to enable a particular byte string to be recognized but ignored.

Note that the SerialInputs mechanism is designed to be very generic, and as such it is not necessarily possible to tailor it to the particular protocol your widget uses. For example, BCI2000 will not know how the widget likes to terminate messages unless you explicitly include the terminator bytes (so, if shutdown and shutdown-gracefully are both configured, the longer command will never be recognized: BCI2000 will greedily recognize and obey shutdown before attempting to move on to -gracefully). The limitations of the SerialInputs table also make themselves felt if the widget sends a byte string that you have omitted from the table. Then, one of two compromises must be accepted (see the description of the SkipUnrecognizedBytes parameter, below). In general, it is not possible to support a serial protocol that includes variable values (unless you create a separate SerialInputs row for every possible combination of values).

SkipUnrecognizedBytes

This boolean parameter determines what happens if you are using the SerialInputs table to recognize messages from the widget, and the widget sends you a string that is not recognized. Either choice can have unfortunate consequences, depending on your widget's protocol.

If the SkipUnrecognizedBytes parameter is set to false (unchecked), then the data coming from the widget will continue to build into an ever-lengthening unrecognized command: BCI2000 will not recognize another SerialInputs byte string at all until the SerialInterface is reset (which happens at the start of a run if SerialInterface is running as an Extension, or when Set Config is pressed if you are running SerialWidgetADC).

If SkipUnrecognizedBytes is set to true (checked), then it will be possible to discard unrecognized bytes and match a known string, but with the danger that substrings of unrecognized commands could be falsely recognized as commands. For example, if you have enabled SkipUnrecognizedBytes, and you have listed the command arm_stimulator in SerialInputs but not disarm_stimulator, and the widget then happens to send disarm_stimulator, then BCI2000 will discard the bytes dis and recognize arm_stimulator, leading to the opposite of the intended effect. Clearly, this is a function of how the widget's protocol is designed. To avoid such situations, you must either (a) ensure that the entries in the SerialInputs table exhaustively cover the repertoire of possible widget outputs (don't worry, if a known longer string gets matched, its substrings will not get matched), or (b) accept the consequences of disabling SkipUnrecognizedBytes. An even more robust option, if possible, is (c) to leave the SerialInputs table empty and re-program your widget to send BCI2000 event descriptors directly as text (see the State Variables section, below).

SerialOutputs

This matrix parameter gives BCI2000 the ability to send messages to the widget whenever specified contingencies are met.

If not empty, the matrix must have 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.

If you are using TTLExampleSketch.ino, you would associate Expressions with the commands output=1\n and output=0\n. For example, let's assume you have defined your own State Variable called StimTrigger. Then your SerialOutputs parameter might look as shown in the screenshot:

ElseIf

This parameter dictates the logic by which 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); alternatively, you may choose to stop after the first matching row (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 an Event descriptor line: usually, this is the name of the event, followed by a space, followed by the value expressed as decimal text, followed by a line-ending. Optionally, an Event descriptor may contain an additional space and a zero, to indicate an instantaneous transient event. For example, TTLExampleSketch.ino will send

TTLInput 1\r\n

whenever the voltage on its input pin changes from low to high, and

TTLInput 0\r\n
PulseDurationMsec 123 0\r\n

whenever the voltage changes from high to low (where 123 is a place-holder for however long the input pulse in fact lasted, according to the widget's millisecond timer).

Note that it is not necessary for the Event in question to have been defined by the widget itself—it could have been added by one of the other filters and loggers, or in your BCI2000 script using an ADD EVENT command before the STARTUP SYSTEM line. This means you do not necessarily have to implement the widget's response to a --PublishCommand, even if you are planning to use the widget to log Events.

If you cannot re-program the widget at all, you may still have some (limited) options for triggering Events in response to messages from the widget, via the SerialInputs parameter.

See also

Contributions:SerialWidgetADC