Contributions:BCI2000RemoteNET
Synopsis
BCI2000RemoteNET is a library for controlling BCI2000 from .NET programs such as Unity. With C# top-level declarations, it can also be used as a replacement for User Reference:BCI2000Shell with the added step of compilation via csc.
Operation
BCI2000RemoteNET communicates with the BCI2000 Operator via operator module scripting commands, which are sent as text over TCP to a running Operator. It also provides functionality for starting the Operator directly, so it can control the Operator entirely from startup to shutdown.
BCI2000RemoteNET consists of two classes, BCI2000Connection, which provides the basic functionality necessary for starting and communicating with BCI2000, and BCI2000Remote, which provides a set of methods for controlling a BCI2000 experiment, such as interacting with parameters and events, as well as starting modules and experiments.
BCI2000RemoteNET comes in two versions, targeting .NET 8.0 and .NET Standard 2.1. This is due to compatibility with programs such as Unity, which require .NET Standard binaries. There is little difference in functionality between the two versions, other than the behavior of the Execute() method, whose differences are described below.
Documentation
Documentation for the classes can be found here:
.NET 8.0
.NET Standard 2.1
Installation
Acquire the latest binary from GitHub, or build from source from the source code on GitHub or in the main BCI2000 Subversion repository (Keep in mind that the version in the Subversion repository will likely be behind the version on GitHub.)
Usage
Starting the operator
If you plan on starting the operator separately, skip this step, but make sure to start the operator with its --Telnet option set.
In order to start the operator, call the BCI2000Connection.StartOperator method with the path to your BCI2000 Operator executable.
BCI2000Connection conn = new BCI2000Connection();
conn.StartOperator("Path/To/BCI2000/prog/Operator.exe");
By default, this will start an operator listening on 127.0.0.1:3999. You can also set the Operator to listen on another port:
conn.StartOperator("path", port: 51101);
Or if you wish to accept connections from other computers you can set BCI2000 to listen on all addresses:
conn.StartOperator("path", address: "0.0.0.0");
Or listen on a specific network interface:
conn.StartOperator("path", address: "10.11.198.2");
Connecting to the operator
Connect to the operator with the BCI2000Connection.Connect() method. If you started a local operator with default settings, then you can call Connect with its default parameters, which will connect to the operator at 127.0.0.1:3999. If you are connecting to an operator on another machine, say at IP address 11.11.11.11,or one listening on a port other than 3999, call Connect() with its optional parameters.
Please see the #Note On Security for an important caveat of setting a non-default parameter here.
BCI2000Connection conn = new BCI2000Connection(); //Connect to operator on the same machine on the default port. conn.Connect(); //Connect to operator at address 11.11.11.11:3999 conn.Connect(address: "11.11.11.11"); BCI2000Remote bci = new(conn);
Adding parameters and events
At this point you can add parameters and events to BCI2000. Parameters are values which can be changed in the BCI2000 GUI and can be used to change how experiments behave. Parameters can either be loaded from a file:
//path is relative to the Operator's working directory, that is, the /prog directory of the BCI2000 installation
bci.LoadParameters("path/to/parameterfile.prm");
or added individually:
//Parameter in the ParamArea section within the Application tab, with default value 0
bci.AddParameter("Application:ParamArea", "Param", "0");
//Parameter in the ParamArea section within the Application tab, with default value "Green"
bci.AddParameter("Application:ParamArea", "Param2", "Green");
//Parameter in the ParamArea2 section within the Application tab, with default value 0 and range 0-10
bci.AddParameter("Application:ParamArea", "Param3", "0", "0", "10");
Events are values logged by BCI2000 alongside the signal data. They have a temporal resolution equivalent to the sample rate, and can be set and read throughout data collection. Events are unsigned integers with a bit width between 1 and 32:
//An event called "condition" representing a boolean value with possible values 0 and 1. Will be zero by default.
bci.AddEvent("condition", 1);
//An 32-bit event with default value 100, and range 0 to 2^32 - 1.
bci.AddEvent("value1", 32, 100);
Starting BCI2000 Modules
BCI2000 modules are started using the StartModules() method, given a dictionary of module names and argument lists. For example, to open SignalGenerator, DummySignalProcessing, and DummyApplication with keyboard and mouse logging:
bci.StartupModules(new Dictionary<string, IEnumerable<string>?>()
{
{"SignalGenerator", new List<string>() {"LogKeyboard=1", "LogMouse=1"}},
{"DummySignalProcessing", null},
{"DummyApplication", null}
});
Setting Parameters and Visualizing Values
If using BCI2000RemoteNET as a substitute for BCI2000Shell for setting up the basic environment, this would usually be where you continue using BCI2000 through its own GUI.
At this point parameters can be set via SetParameter, for example, setting up experiment parameters:
bci.SetParameter("DataDirectory", "../data/theDirectory");
bci.SetParameter("SubjectName", "APatient");
bci.SetParameter("SubjectSession", "004");
You can also set values such as events to be shown in a graph view. For example, since we set the source module to start with mouse logging enabled, we can view the values of the mouse position events.
bci.Visualize("MousePosX");
bci.Visualize("MousePosY");
We can also visualize the events we added earlier.
bci.Visualize("condition");
bci.Visualize("value1");
Collecting Data
Start collecting data with the Start() method. This will implicitly call SetConfig(), but it can be called separately as well.
bci.Start();
If you instead want to start BCI2000 through the GUI, and wait for it in this program, you can instead wait for BCI2000 to be in the Running state.
bci.WaitForSystemState(BCI2000Remote.SystemState.Running);
During a run you can interact with BCI2000 by setting event variables:
bci.SetEvent("value1", 400);
You can also pulse events, setting their value for one sample duration before setting them back.
bci.PulseEvent("condition", 1);
You can also get values from BCI2000. As we set the source module to log the mouse position earlier, we can retrieve its value via GetEvent().
uint mousePositionX = bci.GetEvent("MousePosX");
uint mousePositionY = bci.GetEvent("MousePosY");
You can also retrieve the value of the hardware signal.
//Get value of element 2 of channel 1 of signal double signal11 = bci.GetSignal(1, 2);X");
Stopping BCI2000
When you are finished collecting data, stop BCI2000 with the BCI2000Connection.Stop() method.
bci.connection.Stop();
You can also shut down BCI2000.
bci.connection.Quit();
If you instead want to stop BCI2000 via the GUI, you can wait for the system to be in the Suspended state.
bci.WaitForSystemState(BCI2000Remote.SystemState.Connected);
Or if you want to poll the system state repeatedly (in a loop, for example);
bool loop = true;
while (loop) {
...
if (bci.GetSystemState() == BCI2000Remote.SystemState.Suspended) {
loop = false;
}
}
Using other BCI2000 Commands
An arbitrary operator command can be sent using the BCI2000Connection.Execute method. In the .NET 8.0 version, Execute() and Execute<T>() are used. Execute() executes the command and throws an exception if it receives anything other than an empty response, whereas Execute<T>, where T is a type that implements IParsable, executes the command and attempts to parse the response as type T.
For example, setting the BCI2000 log level:
bci.Execute("set LogLevel 0");
Checking if an event exists:
bool eventExists = bci.Execute<bool>("exists event EventName");
Receiving the BCI2000 system version info:
Console.WriteLine(bci.Execute<string>("get system version"));
In the .NET Standard 2.1 version, there is no generic Execute<T>(), because the version of C# used by .NET Standard 2.1 does not include the IParsable interface. Instead, ExecuteString, ExecuteBool, ExecuteUInt32, and ExecuteDouble provide the functionality of parsing responses as basic types. If you need to interpret the response as a different type, use ExecuteString and parse the result.
Examples
Embedding BCI2000RemoteNET within another program is largely covered above, so this section focuses on alternative usecases.
Using BCI2000RemoteNET as a substitute for BCI2000Shell
BCI2000RemoteNET can be used as a replacement for BCI2000Shell when starting up BCI2000, if you need more advanced language capabilities than what the operator scripting language can provide.
This is the existing startup script for the Stimulus Presentation task with the software Signal Generator.
/batch/StimulusPresentation_SignalGenerator.bat
#! ../prog/BCI2000Shell
@cls & ..\prog\BCI2000Shell %0 %* #! && exit /b 0 || exit /b 1\n
Change directory $BCI2000LAUNCHDIR
Show window; Set title ${Extract file base $0}
Reset system
Startup system localhost
Start executable SignalGenerator --local --LogKeyboard=1 --SpinningWheel=1 --ShowDisplayStatistics=1
Start executable P3SignalProcessing --local
Start executable StimulusPresentation --local
Wait for Connected
Load parameterfile "../parms/examples/StimulusPresentation_SignalGenerator.prm"
This is the equivalent C# code using BCI2000RemoteNET:
/batch/StimulusPresentation_SignalGenerator.cs
using BCI2000RemoteNET;
using System.RuntimeInformation.InteropServices;
BCI2000Remote bci = new(new BCI2000Connection());
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
bci.connection.StartOperator("../prog/Operator" + isWindows ? ".exe" : ""); //Add file extension if on windows
bci.connection.Connect();
bci.LoadParameters("../parms/examples/StimulusPresentation_SignalGenerator.prm");
bci.StartupModules(new Dictionary<string, IEnumerable<string>?> {
{"SignalGenerator", new() {"LogKeyboard=1", "SpinningWheel=1", "ShowDisplayStatistics=1"}},
{"P3SignalProcessing", null},
{"StimulusPresentation", null}
});
C# startup scripts have access to all of the constructs of the C# language, so different actions can be taken depending on conditions such as the operating system, as seen in the above script, or take other input such as command line options.
Note that this will need to be part of a C# project, created via dotnet new console with its accompanied .csproj file.
An alternative would be to instead use a PowerShell or F# script, which can then be run via pwsh or dotnet fsi. This is the equivalent F# code:
/batch/StimulusPresentation_SignalGenerator.fsx
#r "../BCI2000RemoteNET.dll"
open BCI2000RemoteNET
open System.Runtime.InteropServices
let bci = new BCI2000Remote(new BCI2000Connection()) in
let isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) in
let operatorPath = "../../bci2000/prog/Operator" + if isWindows then ".exe" else "" in
bci.connection.StartOperator operatorPath;
bci.connection.Connect ();
bci.LoadParameters "../parms/examples/StimulusPresentation_SignalGenerator.prm";
bci.StartupModules <| Map [
("SignalGenerator", ["LogKeyboard=1"; "SpinningWheel=1"; "ShowDisplayStatistics=1"]);
("P3SignalProcessing", null);
("StimulusPresentation", null);
]
This can be run directly:
dotnet fsi StimulusPresentation_SignalGenerator.fsx
Note On Security
The BCI2000 command line interface can be used to run arbitrary system shell commands. It is also entirely unsecured and unencrypted. Anyone who has access to it effectively has full access to the computer on which it runs. This is not a problem if running locally, and is generally not a problem if running on a firewalled local network. However, due to the unsecured nature of the interface, running BCI2000 across the internet is highly discouraged. That is, if you set BCI2000 to listen at an address of an external network interface, or to listen at all connections by passing the address 0.0.0.0, make sure that port 3999 (or whichever port BCI2000 is listening on), is not forwarded beyond the local network by your router. Support for securely connecting to BCI2000 is planned for the future.