This commit is contained in:
2021-10-14 13:47:35 +02:00
commit 6625a8dfaa
4026 changed files with 844291 additions and 0 deletions
@@ -0,0 +1,111 @@
![OpenViBE logo][OpenViBELogo]
![OpenBCI logo][OpenBCILogo]
# OpenBCI driver for OpenViBE documentation #
(documentation stands for [OpenViBE][OpenViBE] **1.2.0-OpenBCI-RC1**)
## Welcome ##
Welcome in the documentation of the [OpenBCI][OpenBCI] driver for [OpenViBE][OpenViBE]. This document gives you step by step details on how to configure your system, hardware and software to get the best possible experience. For further details about OpenBCI and OpenViBE, please refer to their [respective][OpenBCI] [documentation][OpenViBE].
## Getting Started ##
This section gives guidelines on how to set-up your environment configuration to get the best experience using [OpenBCI][OpenBCI] with [OpenViBE][OpenViBE]
### Linux ###
In order to use the [OpenBCI][OpenBCI] driver on Linux, you need to be granted read/write access to the serial port that is used to communicate with the dongle. Unfortunately, this does not come as a standard configuration on many Linux distributions and you will need to configure your setup so that the user is granted these access. On [Ubuntu][UbuntuDotCom] and its derivative, as well as on [Debian][DebianDotOrg], this can simply be achieved by adding the user to the `dialog` group with the following command :
> sudo adduser *username* dialout
Please note that you may need to close and reopen the session for changes to take effect.
On [Fedora][FedoraDotOrg] and its derivative, a similar command should be used to add the user in the `dialout` group :
> sudo usermode -a -G dialout *username*
Again, please note that you may need to close and reopen the session for changes to take effect.
### Windows ###
It is necessary to walk through a litlle bit of configuration to have the best experience with [OpenBCI][OpenBCI] and [OpenViBE][OpenViBE] on Windows. Indeed, when Windows first installs a serial port, its default configuration is not optimal for realtime streaming of the data. It especially configures large buffering and latencies, causing delays in the communication which eventually lead in delays in the acquired signals. While this only delays the detection of Motor Imagery or SSVEP tasks by a few hundreds of milliseconds, this also delays the ERP (eventually leading to a misdetection) and in all cases, alters the neurophysiological validity of the acquired data by a significant fraction. Consequently, it is recommended to configure the serial ports so that they are suitable for realtime use.
Note that this configuration is necessary each time Windows creates a new serial port. This may unfortunately happen **every time** you move the dongle on a new USB port on the computer !
In order to open the configuration dialog of the serial port, please follow these steps:
- Right click on the `Computer` icon and chose `Manage`
- After the `Computer Management` tool is opened, browse the tree in the left pane to the `Device Manager` section
- After the `Device Manager` tree is opened, browse in the right pane to the `Ports (COM & LPT)` section
- Then double click your port (in my case, `USB Serial Port (COM3)`
![The Windows Device Manager][WindowsTweaking1]
- In the `USB Serial Port Properties` dialog, go to the `Port Settings` tab and click `Advanced`
![The Port Settings dialog][WindowsTweaking2]
- This opens the `Advanced Settings for your COM port`
![The Advanced Port Settings dialog][WindowsTweaking3]
From this window, there are three settings we need to change :
- The `Receive` and `Transmit` settings of the `USB Transfer Sizes` section should both be changed from 4096 to **64** as this will reduce the time taken by the OS to deliver the data to either the dongle or the [OpenViBE][OpenViBE] driver
- The `Latency Timer` of the `BM Options` section should be reduced down to **1** as this will dramatically reduce the latency induced by the OS.
After you changed the settings as shown on the following figure, please click `OK`.
![The tweaked configuration of COM ports to have best experienece with OpenViBE and OpenBCI][WindowsTweaking4]
It is *not necessary* to restart the computer for the changes to take effect.
## Configuration ##
The configuration dialog of the [OpenBCI][OpenBCI] driver comes with a number of settings that one can change or pick depending on his preference.
![The OpenBCI configuration dialog][DriverDialog]
The following table documents each option that is specific to the [OpenBCI][OpenBCI] device. Please refer to the [OpenViBE documentation][OpenViBEDoc] for anything generic to the [OpenViBE][OpenViBE] Acquisition Server.
| Option | Default Value | Documentation |
| :-------------------------: | :-------------------------: | :-----------------------------------------------------------------------------------|
| **Device** | *empty* | This allows you to pick a serial port to connect on. The drodown list shows the serial ports that can currently be opened on this computer. If no port is found, the mention *No valid serial port* is shown in this list. If you cannot find your device in this list, please refer . |
| **Use Daisy Module** | *false* | This allows you to configure the daisy module. Four cases should be considered. 1/ if the daisy module is present and this option is set to **true**, then the device will turn to 16 channels samples 125 Hz. 2/ if no daisy module is present and this option is set to **false**, then the device will turn to 8 channels, 250 Hz. 3/ if the daisy module is **not present** on the board and this option is set to **true**, then the initialization of the driver will **fail**. 4/ if the daisy module is **present** on the board and this option is set to **false**, the daisy module will be disabled and the acquisition will be done as if the daisy module was not present on the board, turning the device back to 8 channels sampled at 125 Hz. |
| **Custom Command On Initialization** | *empty* | This option contains additional commands to send to the device at initialization. You must use one line per command, some command may contain multiple characters. For details about the commands, please refer to the [OpenBCI protocol documentation][OpenBCIProto]. Be advised that this will increase the delay of initialization by an order of magnitude that is a direct relation of the number and types of commands you want to add. Finally, not all the commands take the same time to be executed, if you include custom commands, you should consider adjusting the timeout values. |
| **Board Reply Reading Timeout** | 5000 | This allows to define the maximum time until reading a reply from the board after sending a command times out. Many commands end with a **\$\$\$** pattern, which can handily be captured and release the waiting loop when reading the board reply, but not all the commands have this **\$\$\$** pattern. Consequently, it is necessary to have a timeout for the other commands. The default value has been chosen to behave well even with custom commands that need a long time to reply such as **?**. If you don't use such command in your *Custom Command On Initialization*, you may reduce that delay. But be aware that if you reduce it too much, the driver may miss the **\$\$\$** pattern even though the board has sent it, resulting in unexpected behavior. |
| **Board Reply Flushing Timeout** | 500 | This option allows to flush and get rid of the streaming buffer. This is especially used when the driver asks the board to stop streaming and makes the streaming state absolutely clean when the driver needs to send a new command after stopping the streaming. You may reduce this value to make (re)connection faster, but if the buffer came not to be completely flushed, the remaining would be taken as the begining of the next command and this may result in unexpected behavior. |
The Configuration Summary gives information on the current configuration, especially the number of channels and the sampling rate of the device.
## Advanced Configuration ##
In addition to the above settings, another few settings are available to the user as advanced configuration. They are not exposed in the GUI and should be directly set in the [OpenViBEConfig][OpenViBE configuration] file instead. Refere to the [Configuration Manager section][OpenViBEConfig] of the [OpenViBE documentation][OpenViBEDoc] for further details on the configuration file format, location and others.
| Token | Default Value | Documentation |
| :-------------------------: | :-------------------------: | :-----------------------------------------------------------------------------------|
| **AcquisitionDriver OpenBCI MissingSampleDelayBeforeReset** | *1000* | This defines the size of the window to continuously monitor reception of samples from the driver. If no sample is received within that timeframe, the board is requested to stop and restart streaming. While the non-reception of samples from the board may reflect an unexpected state in the board, this strategy seems to sometimes recover and let the streaming go back to normal. The default value allows a good compromise between dealing with buffering and actual transmission delays and recovering fast when something goes wrong. If you experience such unstability in the transmision, we recommend that you first explore anything that may (in)directly affect the quality of the transmission before tweaking this setting. |
| **AcquisitionDriver OpenBCI DroppedSampleCountBeforeReset** | *5* | This defines the number of sample loss events until a recovery is attempted. It happens that the board gets in an unstable state where some sample would be missing in the stream. Stopping and restarting the streaming has proved to recover well. The default setting has been set so that a few occasional sample loss may occur (due to e.g. quality transmission) and be corrected by the drift correction process, while not waiting too long to attempt recovery when too many sample are lost. |
| **AcquisitionDriver OpenBCI DroppedSampleSafetyDelayBeforeReset** | *1000* | This defines a sefety delay where no reset should be attempted because of sample loss (see **AcquisitionDriver OpenBCI DroppedSampleCountBeforeReset**). This prevents a reset on the first sample where the driver synchronises with the streaming protocol and may miss a few samples until it is perfectly synced with the header and tail of the protocol frame. |
[FedoraDotOrg]: http://www.fedora.org
[UbuntuDotCom]: http://www.ubuntu.com
[DebianDotOrg]: http://www.debian.org
[OpenViBE]: http://openvibe.inria.fr
[OpenViBEDoc]: http://openvibe.inria.fr/documentation-index
[OpenViBEConfig]: http://openvibe.inria.fr/the-configuration-manager
[OpenViBELogo]: http://openvibe.inria.fr/openvibe/wp-content/themes/openvibe/images/openvibe-banner.png
[OpenBCI]: http://docs.openbci.com
[OpenBCIProto]: http://docs.openbci.com/software/01-OpenBCI_SDK
[OpenBCILogo]: http://openbci.com/community/wp-content/uploads/2015/09/logo_wide_web_borders_BIG-copy-1024x137.png
[WindowsTweaking1]: ServerDriver_OpenBCI_windows_tweaking_1.png
[WindowsTweaking2]: ServerDriver_OpenBCI_windows_tweaking_2.png
[WindowsTweaking3]: ServerDriver_OpenBCI_windows_tweaking_3.png
[WindowsTweaking4]: ServerDriver_OpenBCI_windows_tweaking_4.png
[DriverDialog]: ServerDriver_OpenBCI_configuration.png
@@ -0,0 +1,735 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="2.16"/>
<!-- interface-naming-policy project-wide -->
<object class="GtkAdjustment" id="adjustment1">
<property name="lower">1</property>
<property name="upper">256</property>
<property name="value">8</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="adjustment2">
<property name="upper">100</property>
<property name="value">18</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="adjustment3">
<property name="upper">100</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="adjustment_flush_board_reply_timeout">
<property name="lower">1</property>
<property name="upper">30000</property>
<property name="value">500</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="adjustment_read_board_reply_timeout">
<property name="lower">1</property>
<property name="upper">30000</property>
<property name="value">5000</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="adjustment_sampling_frequency">
<property name="lower">4</property>
<property name="upper">4096</property>
<property name="value">250</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkListStore" id="model1">
<columns>
<!-- column-name gchararray -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">unspecified</col>
</row>
<row>
<col id="0" translatable="yes">female</col>
</row>
<row>
<col id="0" translatable="yes">male</col>
</row>
<row>
<col id="0" translatable="yes">unknown</col>
</row>
</data>
</object>
<object class="GtkWindow" id="openvibe-acquisition-server-dummy">
<property name="can_focus">False</property>
<child>
<object class="GtkTable" id="table1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="n_rows">10</property>
<property name="n_columns">2</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkLabel" id="label_number_of_channels">
<property name="can_focus">False</property>
<property name="label" translatable="yes">Number of channels :</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
</child>
<child>
<object class="GtkSpinButton" id="spinbutton_number_of_channels">
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="adjustment">adjustment1</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_sampling_frequency">
<property name="can_focus">False</property>
<property name="label" translatable="yes">Sampling frequency :</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="combobox_sampling_frequency">
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="adjustment">adjustment_sampling_frequency</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkDialog" id="openvibe-acquisition-server-settings">
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="title" translatable="yes">Device configuration</property>
<property name="window_position">center</property>
<property name="type_hint">dialog</property>
<property name="gravity">center</property>
<child internal-child="vbox">
<object class="GtkVBox" id="dialog-vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK</property>
<child>
<object class="GtkLabel" id="label_title">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;&lt;big&gt;OpenBCI reader&lt;/big&gt;&lt;/b&gt;
&lt;small&gt;Upon initialization, board responses will appear in the console
at &lt;b&gt;Trace&lt;/b&gt; level and should end by "&lt;b&gt;$$$&lt;/b&gt;".
The last 3 channels represent accelerometer values.
The sampling rate and the total number of channels are automatically
selected depending on the &lt;b&gt;Use Daisy Module&lt;/b&gt; option
(8 EEG + 3 acc. at 250Hz without, 16 EEG + 3 acc. at 125Hz with).
Use "&lt;b&gt;Custom Commands on Initialisation&lt;/b&gt;" to configure the board between
reset and streaming. Refer to OpenBCI doc for more details.&lt;/small&gt;</property>
<property name="use_markup">True</property>
<property name="justify">center</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="action_area">
<object class="GtkHButtonBox" id="dialog-action_area">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="button_apply">
<property name="label">gtk-apply</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_cancel">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="hseparator5">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">4</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkTable" id="table2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="n_rows">3</property>
<property name="n_columns">2</property>
<child>
<object class="GtkLabel" id="label_identifier">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Identifier :</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
</child>
<child>
<object class="GtkSpinButton" id="spinbutton_identifier">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="adjustment">adjustment3</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_gender">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Gender :</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="combobox_gender">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">model1</property>
<child>
<object class="GtkCellRendererText" id="renderer1"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_age">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Age :</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
<packing>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="spinbutton_age">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="adjustment">adjustment2</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="hseparator2">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">4</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkTable" id="table4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="n_rows">5</property>
<property name="n_columns">2</property>
<child>
<object class="GtkLabel" id="label_read_board_reply_timeout">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ypad">3</property>
<property name="label" translatable="yes">Board Reply Reading Timeout (ms)</property>
</object>
<packing>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_daisy_module">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Use Daisy Module :</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_device">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Device :</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
</child>
<child>
<object class="GtkSpinButton" id="spinbutton_read_board_reply_timeout">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="adjustment">adjustment_read_board_reply_timeout</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="checkbutton_daisy_module">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="combobox_device">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkCellRendererText" id="renderer2"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_flush_board_reply_timeout">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Board Reply Flushing Timeout (ms)</property>
</object>
<packing>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="spinbutton_flush_board_reply_timeout">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">•</property>
<property name="invisible_char_set">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="adjustment">adjustment_flush_board_reply_timeout</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow_com_init">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view_com_init">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkDrawingArea" id="drawingarea_com_init_1">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_com_init_1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Custom Commands on Initialization :</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_com_init_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">(1 command per line)</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkDrawingArea" id="drawingarea_com_init_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="hseparator3">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">4</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkTable" id="table3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="n_rows">3</property>
<property name="n_columns">2</property>
<child>
<object class="GtkLabel" id="label_device_status">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Configuration Summary:</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="bottom_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_status_eeg_channel_count">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">EEG Channels</property>
<property name="use_markup">True</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_status_acc_channel_count">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Accelerometer Channels</property>
<property name="use_markup">True</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label_status_sampling_rate">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Sampling Rate</property>
<property name="use_markup">True</property>
<property name="justify">right</property>
<property name="single_line_mode">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="hseparator1">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_change_channel_names">
<property name="label" translatable="yes">Change channel names</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">9</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="hseparator4">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">10</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="-10">button_apply</action-widget>
<action-widget response="-6">button_cancel</action-widget>
</action-widgets>
</object>
</interface>
@@ -0,0 +1,234 @@
/*
* OpenBCI driver for OpenViBE
*
* \author Jeremy Frey
* \author Yann Renard
*
*/
#include "ovasCConfigurationOpenBCI.h"
#include <algorithm>
#include <string>
#if defined TARGET_OS_Windows
#include <windows.h>
#include <winbase.h>
#include <cstdio>
#include <commctrl.h>
//#define TERM_SPEED 57600
#define TERM_SPEED CBR_115200 // OpenBCI is a bit faster than others
#elif defined TARGET_OS_Linux
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/select.h>
#include <netinet/in.h> // htons and co.
#include <unistd.h>
#define TERM_SPEED B115200
#else
#endif
namespace OpenViBE {
namespace AcquisitionServer {
#define MAXIMUM_SERIAL_TTY (32)
#define MAXIMUM_SERIAL_USB_TTY (256-MAXIMUM_SERIAL_TTY)
uint32_t CConfigurationOpenBCI::getMaximumTtyCount() { return MAXIMUM_SERIAL_USB_TTY + MAXIMUM_SERIAL_TTY; }
CString CConfigurationOpenBCI::getTTYFileName(const uint32_t ttyNumber)
{
char buffer[1024];
#if defined TARGET_OS_Windows
sprintf(buffer, "\\\\.\\COM%u", ttyNumber);
#elif defined TARGET_OS_Linux
if(ttyNumber<MAXIMUM_SERIAL_USB_TTY)
{
::sprintf(buffer, "/dev/ttyUSB%u", ttyNumber);
}
else
{
::sprintf(buffer, "/dev/ttyS%u", ttyNumber-MAXIMUM_SERIAL_USB_TTY);
}
#else
::sprintf(buffer, "");
#endif
return buffer;
}
bool CConfigurationOpenBCI::isTTYFile(const CString& filename)
{
#if defined TARGET_OS_Windows
HANDLE file = ::CreateFile(LPCSTR(filename), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE || file == nullptr) { return false; }
CloseHandle(file);
return true;
#elif defined TARGET_OS_Linux
int file=::open(filename, O_RDONLY);
if(file < 0) { return false; }
close(file);
return true;
#else
// Caution, this path will claim all serial ports do exist because there was no platform specific implementation
return true;
#endif
}
static void checkbutton_daisy_module_cb(GtkToggleButton* button, CConfigurationOpenBCI* data)
{
data->checkbuttonDaisyModuleCB(gtk_toggle_button_get_active(button) ? CConfigurationOpenBCI::EDaisyStatus::Active
: CConfigurationOpenBCI::EDaisyStatus::Inactive);
}
CConfigurationOpenBCI::CConfigurationOpenBCI(const char* gtkBuilderFilename, uint32_t& usbIdx)
: CConfigurationBuilder(gtkBuilderFilename), m_usbIdx(usbIdx) { m_listStore = gtk_list_store_new(1, G_TYPE_STRING); }
CConfigurationOpenBCI::~CConfigurationOpenBCI() { g_object_unref(m_listStore); }
bool CConfigurationOpenBCI::preConfigure()
{
if (!CConfigurationBuilder::preConfigure()) { return false; }
#if 0
::GtkEntry* m_entryComInit=GTK_ENTRY(gtk_builder_get_object(m_builder, "entry_com_init"));
::gtk_entry_set_text(m_entryComInit, m_additionalCmds.toASCIIString());
#else
std::string additionalCmds = m_additionalCmds.toASCIIString();
std::replace(additionalCmds.begin(), additionalCmds.end(), '\255', '\n');
GtkTextView* textViewComInit = GTK_TEXT_VIEW(gtk_builder_get_object(m_builder, "text_view_com_init"));
GtkTextBuffer* textBufferComInit = gtk_text_view_get_buffer(textViewComInit);
gtk_text_buffer_set_text(textBufferComInit, additionalCmds.c_str(), -1);
#endif
GtkSpinButton* buttonReadBoardReplyTimeout = GTK_SPIN_BUTTON(gtk_builder_get_object(m_builder, "spinbutton_read_board_reply_timeout"));
gtk_spin_button_set_value(buttonReadBoardReplyTimeout, m_readBoardReplyTimeout);
GtkSpinButton* buttonFlushBoardReplyTimeout = GTK_SPIN_BUTTON(gtk_builder_get_object(m_builder, "spinbutton_flush_board_reply_timeout"));
gtk_spin_button_set_value(buttonFlushBoardReplyTimeout, m_flushBoardReplyTimeout);
GtkToggleButton* buttonDaisyModule = GTK_TOGGLE_BUTTON(gtk_builder_get_object(m_builder, "checkbutton_daisy_module"));
gtk_toggle_button_set_active(buttonDaisyModule, m_daisyModule ? true : false);
::g_signal_connect(gtk_builder_get_object(m_builder, "checkbutton_daisy_module"), "toggled", G_CALLBACK(checkbutton_daisy_module_cb), this);
this->checkbuttonDaisyModuleCB(m_daisyModule ? EDaisyStatus::Active : EDaisyStatus::Inactive);
GtkComboBox* comboBox = GTK_COMBO_BOX(gtk_builder_get_object(m_builder, "combobox_device"));
g_object_unref(m_listStore);
m_listStore = gtk_list_store_new(1, G_TYPE_STRING);
m_comboSlotsIndexToSerialPort.clear();
gtk_combo_box_set_model(comboBox, GTK_TREE_MODEL(m_listStore));
bool selected = false;
m_comboSlotsIndexToSerialPort[0] = -1;
gtk_combo_box_append_text(comboBox, "Automatic");
for (uint32_t i = 0, j = 1; i < getMaximumTtyCount(); ++i)
{
CString filename = getTTYFileName(i);
if (isTTYFile(filename))
{
m_comboSlotsIndexToSerialPort[j] = i;
gtk_combo_box_append_text(comboBox, filename.toASCIIString());
if (m_usbIdx == i)
{
gtk_combo_box_set_active(comboBox, j);
selected = true;
}
j++;
}
}
if (!selected) { gtk_combo_box_set_active(comboBox, 0); }
return true;
}
bool CConfigurationOpenBCI::postConfigure()
{
GtkComboBox* comboBox = GTK_COMBO_BOX(gtk_builder_get_object(m_builder, "combobox_device"));
if (m_applyConfig)
{
const int idx = m_comboSlotsIndexToSerialPort[gtk_combo_box_get_active(comboBox)];
if (idx >= 0) { m_usbIdx = uint32_t(idx); }
else { m_usbIdx = uint32_t(-1); }
#if 0
::GtkEntry* m_entryComInit=GTK_ENTRY(gtk_builder_get_object(m_builder, "entry_com_init"));
m_additionalCmds=::gtk_entry_get_text(m_entryComInit);
#else
GtkTextView* textViewComInit = GTK_TEXT_VIEW(gtk_builder_get_object(m_builder, "text_view_com_init"));
GtkTextBuffer* textBufferComInit = gtk_text_view_get_buffer(textViewComInit);
GtkTextIter startIt;
GtkTextIter endIt;
gtk_text_buffer_get_start_iter(textBufferComInit, &startIt);
gtk_text_buffer_get_end_iter(textBufferComInit, &endIt);
std::string additionalCmds = gtk_text_buffer_get_text(textBufferComInit, &startIt, &endIt, FALSE);
std::replace(additionalCmds.begin(), additionalCmds.end(), '\n', '\255');
m_additionalCmds = additionalCmds.c_str();
#endif
GtkSpinButton* buttonReadBoardReplyTimeout = GTK_SPIN_BUTTON(
gtk_builder_get_object(m_builder, "spinbutton_read_board_reply_timeout"));
gtk_spin_button_update(GTK_SPIN_BUTTON(buttonReadBoardReplyTimeout));
m_readBoardReplyTimeout = gtk_spin_button_get_value_as_int(buttonReadBoardReplyTimeout);
GtkSpinButton* buttonFlushBoardReplyTimeout = GTK_SPIN_BUTTON(
gtk_builder_get_object(m_builder, "spinbutton_flush_board_reply_timeout"));
gtk_spin_button_update(GTK_SPIN_BUTTON(buttonFlushBoardReplyTimeout));
m_flushBoardReplyTimeout = gtk_spin_button_get_value_as_int(buttonFlushBoardReplyTimeout);
GtkToggleButton* buttonDaisyModule = GTK_TOGGLE_BUTTON(gtk_builder_get_object(m_builder, "checkbutton_daisy_module"));
m_daisyModule = gtk_toggle_button_get_active(buttonDaisyModule) ? true : false;
}
if (!CConfigurationBuilder::postConfigure()) { return false; }
return true;
}
void CConfigurationOpenBCI::checkbuttonDaisyModuleCB(const EDaisyStatus status) const
{
const daisy_Info_t info = this->getDaisyInformation(status);
std::string buffer = std::to_string(info.nEEGChannel) + " EEG Channels";
gtk_label_set_text(GTK_LABEL(gtk_builder_get_object(m_builder, "label_status_eeg_channel_count")), buffer.c_str());
buffer = std::to_string(info.nAccChannel) + " Accelerometer Channels";
gtk_label_set_text(GTK_LABEL(gtk_builder_get_object(m_builder, "label_status_acc_channel_count")), buffer.c_str());
buffer = std::to_string(info.sampling) + " Hz Sampling Rate";
gtk_label_set_text(GTK_LABEL(gtk_builder_get_object(m_builder, "label_status_sampling_rate")), buffer.c_str());
gtk_spin_button_set_value(GTK_SPIN_BUTTON(gtk_builder_get_object(m_builder, "spinbutton_number_of_channels")), info.nEEGChannel + info.nAccChannel);
}
CConfigurationOpenBCI::daisy_Info_t CConfigurationOpenBCI::getDaisyInformation(const EDaisyStatus status)
{
daisy_Info_t res;
switch (status)
{
case EDaisyStatus::Inactive: res.nEEGChannel = DEFAULT_N_EEG_CHANNEL;
res.nAccChannel = DEFAULT_N_ACC_CHANNEL;
res.sampling = DEFAULT_SAMPLING;
break;
case EDaisyStatus::Active: res.nEEGChannel = DEFAULT_N_EEG_CHANNEL * 2;
res.nAccChannel = DEFAULT_N_ACC_CHANNEL;
res.sampling = DEFAULT_SAMPLING / 2;
break;
default: res.nEEGChannel = 0;
res.nAccChannel = 0;
res.sampling = 0;
break;
}
return res;
}
} // namespace AcquisitionServer
} // namespace OpenViBE
@@ -0,0 +1,68 @@
/*
* OpenBCI driver for OpenViBE
*
* \author Jeremy Frey
* \author Yann Renard
*/
#pragma once
#include "../ovasCConfigurationBuilder.h"
#include <gtk/gtk.h>
#include <map>
namespace OpenViBE {
namespace AcquisitionServer {
class CConfigurationOpenBCI final : public CConfigurationBuilder
{
public:
const static uint16_t DEFAULT_SAMPLING = 250; // sampling rate with no daisy module (divided by 2 with daisy module)
const static uint16_t DEFAULT_N_EEG_CHANNEL = 8; // number of EEG channels with no daisy module (multiplied by 2 with daisy module)
const static uint16_t DEFAULT_N_ACC_CHANNEL = 3; // number of Acc channels (daisy module does not have an impact)
enum class EDaisyStatus { Active, Inactive };
typedef struct
{
int nEEGChannel;
int nAccChannel;
int sampling;
} daisy_Info_t;
static uint32_t getMaximumTtyCount();
static CString getTTYFileName(uint32_t ttyNumber);
static bool isTTYFile(const CString& filename);
CConfigurationOpenBCI(const char* gtkBuilderFilename, uint32_t& usbIdx);
~CConfigurationOpenBCI() override;
bool preConfigure() override;
bool postConfigure() override;
void setAdditionalCommands(const CString& cmds) { m_additionalCmds = cmds; }
CString getAdditionalCommands() const { return m_additionalCmds; }
void setReadBoardReplyTimeout(const uint32_t timeout) { m_readBoardReplyTimeout = timeout; }
uint32_t getReadBoardReplyTimeout() const { return m_readBoardReplyTimeout; }
void setFlushBoardReplyTimeout(const uint32_t timeout) { m_flushBoardReplyTimeout = timeout; }
uint32_t getFlushBoardReplyTimeout() const { return m_flushBoardReplyTimeout; }
void setDaisyModule(const bool module) { m_daisyModule = module; }
bool getDaisyModule() const { return m_daisyModule; }
void checkbuttonDaisyModuleCB(EDaisyStatus status) const;
static daisy_Info_t getDaisyInformation(EDaisyStatus status);
protected:
std::map<int, int> m_comboSlotsIndexToSerialPort;
uint32_t& m_usbIdx;
GtkListStore* m_listStore = nullptr;
GtkEntry* m_entryComInit = nullptr;
CString m_additionalCmds;
uint32_t m_readBoardReplyTimeout = 0;
uint32_t m_flushBoardReplyTimeout = 0;
bool m_daisyModule = false;
};
} // namespace AcquisitionServer
} // namespace OpenViBE
@@ -0,0 +1,967 @@
/*
* OpenBCI driver for OpenViBE
*
* \author Jeremy Frey
* \author Yann Renard
*
*/
#include "ovasCDriverOpenBCI.h"
#include "ovasCConfigurationOpenBCI.h"
#include <toolkit/ovtk_all.h>
#include <system/ovCTime.h>
#include <cmath>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <sstream>
#if defined TARGET_OS_Windows
#include <windows.h>
#include <winbase.h>
#include <cstdio>
#include <commctrl.h>
#include <winsock2.h> // htons and co.
//#define TERM_SPEED 57600
#define TERM_SPEED CBR_115200 // OpenBCI is a bit faster than others
#elif defined TARGET_OS_Linux
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/select.h>
#include <netinet/in.h> // htons and co.
#include <unistd.h>
#define TERM_SPEED B115200
#else
#endif
namespace OpenViBE {
namespace AcquisitionServer {
// packet number at initialization
#define UNINITIALIZED_PACKET_NUMBER -1
#define UNDEFINED_DEVICE_IDENTIFIER uint32_t(-1)
#define READ_ERROR uint32_t(-1)
#define WRITE_ERROR uint32_t(-1)
// start and stop bytes from OpenBCI protocl
#define SAMPLE_START_BYTE 0xA0
#define SAMPLE_STOP_BYTE 0xC0
// some constants related to the sendCommand
#define ADS1299_VREF 4.5 // reference voltage for ADC in ADS1299. set by its hardware
#define ADS1299_GAIN 24.0 //assumed gain setting for ADS1299. set by its Arduino code
// configuration tokens
#define Token_MissingSampleDelayBeforeReset "AcquisitionDriver_OpenBCI_MissingSampleDelayBeforeReset"
#define Token_DroppedSampleCountBeforeReset "AcquisitionDriver_OpenBCI_DroppedSampleCountBeforeReset"
#define Token_DroppedSampleSafetyDelayBeforeReset "AcquisitionDriver_OpenBCI_DroppedSampleSafetyDelayBeforeReset"
//___________________________________________________________________//
// Heavily inspired by OpenEEG code. Will override channel count and sampling late upon "daisy" selection. If daisy module is attached, will concatenate EEG values and average accelerometer values every two samples.
// //
CDriverOpenBCI::CDriverOpenBCI(IDriverContext& ctx)
: IDriver(ctx)
, m_settings("AcquisitionServer_Driver_OpenBCI", m_driverCtx.getConfigurationManager())
{
m_additionalCmds = "";
m_readBoardReplyTimeout = 5000;
m_flushBoardReplyTimeout = 500;
m_driverName = "OpenBCI";
m_settings.add("Header", &m_header);
m_settings.add("DeviceIdentifier", &m_deviceID);
m_settings.add("ComInit", &m_additionalCmds);
m_settings.add("ReadBoardReplyTimeout", &m_readBoardReplyTimeout);
m_settings.add("FlushBoardReplyTimeout", &m_flushBoardReplyTimeout);
m_settings.add("DaisyModule", &m_daisyModule);
m_settings.load();
m_missingSampleDelayBeforeReset = uint32_t(ctx.getConfigurationManager().expandAsUInteger(Token_MissingSampleDelayBeforeReset, 1000));
m_droppedSampleCountBeforeReset = uint32_t(ctx.getConfigurationManager().expandAsUInteger(Token_DroppedSampleCountBeforeReset, 5));
m_droppedSampleSafetyDelayBeforeReset = uint32_t(ctx.getConfigurationManager().expandAsUInteger(Token_DroppedSampleSafetyDelayBeforeReset, 1000));
// default parameter loaded, update channel count and frequency
this->updateDaisy(true);
}
//___________________________________________________________________//
// //
void CDriverOpenBCI::updateDaisy(const bool quietLogging)
{
// change channel and sampling rate according to daisy module
const auto info = CConfigurationOpenBCI::getDaisyInformation(m_daisyModule ? CConfigurationOpenBCI::EDaisyStatus::Active
: CConfigurationOpenBCI::EDaisyStatus::Inactive);
m_header.setSamplingFrequency(info.sampling);
m_header.setChannelCount(info.nEEGChannel + info.nAccChannel);
if (!quietLogging)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Status - " << CString(m_daisyModule ? "Daisy" : "** NO ** Daisy") <<
" module option enabled, " << m_header.getChannelCount() << " channels -- " << int((m_daisyModule ? 2 : 1) * EEG_VALUE_COUNT_PER_SAMPLE) <<
" EEG and " << int(ACC_VALUE_COUNT_PER_SAMPLE) << " accelerometer -- at " << m_header.getSamplingFrequency() << "Hz.\n";
}
// microvolt for EEG channels
for (int i = 0; i < info.nEEGChannel; ++i) { m_header.setChannelUnits(i, OVTK_UNIT_Volts, OVTK_FACTOR_Micro); }
// undefined for accelerometer/extra channels
for (int i = 0; i < info.nAccChannel; ++i) { m_header.setChannelUnits(info.nEEGChannel + i, OVTK_UNIT_Unspecified, OVTK_FACTOR_Base); }
}
bool CDriverOpenBCI::initialize(const uint32_t /*nSamplePerSentBlock*/, IDriverCallback& callback)
{
if (m_driverCtx.isConnected()) { return false; }
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Configured 'missing sample delay before reset' to " <<
m_missingSampleDelayBeforeReset << " ; this can be changed in the openvibe configuration file setting the " << CString(
Token_MissingSampleDelayBeforeReset) << " token\n";
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Configured 'dropped sample count before reset' to " <<
m_droppedSampleCountBeforeReset << " ; this can be changed in the openvibe configuration file setting the " << CString(
Token_DroppedSampleSafetyDelayBeforeReset) << " token\n";
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Configured 'dropped sample safety delay before reset' to " <<
m_droppedSampleSafetyDelayBeforeReset << " ; this can be changed in the openvibe configuration file setting the " << CString(
Token_DroppedSampleSafetyDelayBeforeReset) << " token\n";
m_nChannel = m_header.getChannelCount();
// Initializes buffer data structures
m_readBuffers.clear();
m_readBuffers.resize(1024 * 16); // 16 kbytes of read buffer
m_callbackSamples.clear();
// change channel and sampling rate according to daisy module
this->updateDaisy(false);
// init state
m_readState = ParserAutomaton_Default;
m_extractPosition = 0;
m_sampleNumber = -1;
m_seenPacketFooter = true; // let's say we will start with header
if (!this->openDevice(&m_fileDesc, m_deviceID)) { return false; }
// check board status and print response
if (!this->resetBoard(m_fileDesc, true))
{
this->closeDevice(m_fileDesc);
return false;
}
// prepare buffer for samples
m_sampleEEGBuffers.resize(EEG_VALUE_COUNT_PER_SAMPLE);
m_sampleEEGBuffersDaisy.resize(EEG_VALUE_COUNT_PER_SAMPLE);
m_sampleAccBuffers.resize(ACC_VALUE_COUNT_PER_SAMPLE);
m_sampleAccBuffersTemp.resize(ACC_VALUE_COUNT_PER_SAMPLE);
// init buffer for 1 EEG value and 1 accel value
m_eegValueBuffers.resize(EEG_VALUE_BUFFER_SIZE);
m_accValueBuffers.resize(ACC_VALUE_BUFFER_SIZE);
m_sampleBuffers.resize(m_nChannel);
m_callback = &callback;
m_lastPacketNumber = UNINITIALIZED_PACKET_NUMBER;
m_driverCtx.getLogManager() << Kernel::LogLevel_Debug << CString(this->getName()) << " driver initialized.\n";
// init scale factor
m_unitsToMicroVolts = float(float(ADS1299_VREF * 1000000) / ((pow(2., 23) - 1) * ADS1299_GAIN));
m_unitsToRadians = float(0.002 / pow(2., 4)); // @aj told me - this is undocumented and may have been taken from the OpenBCI plugin for processing
#if 0
uint32_t unitsToMicroVolts = (float) (ADS1299_VREF/(pow(2.,23)-1)/ADS1299_GAIN*1000000.); // $$$$ The notation here is ambiguous
::printf("CHECK THIS OUT : %g %g %g\n", unitsToMicroVolts-m_unitsToMicroVolts, unitsToMicroVolts, m_unitsToMicroVolts);
#endif
return true;
}
bool CDriverOpenBCI::start()
{
if (!m_driverCtx.isConnected() || m_driverCtx.isStarted()) { return false; }
m_driverCtx.getLogManager() << Kernel::LogLevel_Debug << CString(this->getName()) << " driver started.\n";
return true;
}
bool CDriverOpenBCI::loop()
{
if (!m_driverCtx.isConnected()) { return false; }
// try to awake the board if there's something wrong
if (System::Time::getTime() - m_tick > m_missingSampleDelayBeforeReset)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << "No response for " << uint32_t(m_missingSampleDelayBeforeReset) <<
"ms, will try recovery now (Note this may eventually be hopeless as the board may not reply to any command either).\n";
if (!this->resetBoard(m_fileDesc, false))
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << this->m_driverName << ": Did not succeed reseting board\n";
return false;
}
}
// read datastream from device
const uint32_t length = this->readFromDevice(m_fileDesc, &m_readBuffers[0], m_readBuffers.size());
if (length == READ_ERROR)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << this->m_driverName << ": Could not receive data from [" << m_ttyName << "]\n";
return false;
}
// pass bytes one by one to parser to extract samples
for (uint32_t i = 0; i < length; ++i)
{
// will have effect only if complete sample/packet
if (this->handleCurrentSample(this->parseByte(m_readBuffers[i])))
{
// One full packet was processed !
}
}
// now deal with acquired samples
if (!m_channelBuffers.empty())
{
if (m_driverCtx.isStarted())
{
m_callbackSamples.resize(m_nChannel * m_channelBuffers.size());
for (uint32_t i = 0, k = 0; i < m_nChannel; ++i)
{
for (uint32_t j = 0; j < m_channelBuffers.size(); ++j) { m_callbackSamples[k++] = m_channelBuffers[j][i]; }
}
m_callback->setSamples(&m_callbackSamples[0], m_channelBuffers.size());
m_driverCtx.correctDriftSampleCount(m_driverCtx.getSuggestedDriftCorrectionSampleCount());
}
m_channelBuffers.clear();
}
return true;
}
bool CDriverOpenBCI::stop()
{
if (!m_driverCtx.isConnected() || !m_driverCtx.isStarted()) { return false; }
m_driverCtx.getLogManager() << Kernel::LogLevel_Debug << CString(this->getName()) << " driver stopped.\n";
return true;
}
bool CDriverOpenBCI::uninitialize()
{
if (!m_driverCtx.isConnected() || m_driverCtx.isStarted()) { return false; }
this->closeDevice(m_fileDesc);
m_driverCtx.getLogManager() << Kernel::LogLevel_Debug << CString(this->getName()) << " driver closed.\n";
// Uninitializes data structures
m_readBuffers.clear();
m_callbackSamples.clear();
m_ttyName = "";
#if 0
delete [] m_sample;
m_sample= nullptr;
#endif
m_callback = nullptr;
return true;
}
//___________________________________________________________________//
// //
bool CDriverOpenBCI::configure()
{
CConfigurationOpenBCI config(Directories::getDataDir() + "/applications/acquisition-server/interface-OpenBCI.ui", m_deviceID);
config.setAdditionalCommands(m_additionalCmds);
config.setReadBoardReplyTimeout(m_readBoardReplyTimeout);
config.setFlushBoardReplyTimeout(m_flushBoardReplyTimeout);
config.setDaisyModule(m_daisyModule);
if (!config.configure(m_header)) { return false; }
m_additionalCmds = config.getAdditionalCommands();
m_readBoardReplyTimeout = config.getReadBoardReplyTimeout();
m_flushBoardReplyTimeout = config.getFlushBoardReplyTimeout();
m_daisyModule = config.getDaisyModule();
m_settings.save();
this->updateDaisy(false);
return true;
}
// Convert EEG value format from int24 MSB (network order) to int host
// TODO: check on big endian architecture
int CDriverOpenBCI::interpret24bitAsInt32(const std::vector<uint8_t>& byteBuffer)
{
// create a big endian so that we could adapt to host architecture later on
int newInt = (byteBuffer[2] << 24) | (byteBuffer[1] << 16) | byteBuffer[0] << 8;
// depending on most significant byte, set positive or negative value
if ((newInt & 0x00008000) > 0) { newInt |= 0x000000FF; }
else { newInt &= 0xFFFFFF00; }
// convert back from big endian (network order) to host
return htonl(newInt);
}
// Convert EEG value format from int16_t MSB (network order) to int host
int CDriverOpenBCI::interpret16bitAsInt32(const std::vector<uint8_t>& byteBuffer)
{
// create a big endian so that we could adapt to host architecture later on
int newInt = (byteBuffer[1] << 24) | byteBuffer[0] << 16;
// depending on most significant byte, set positive or negative value
if ((newInt & 0x00800000) > 0) { newInt |= 0x0000FFFF; }
else { newInt &= 0xFFFF0000; }
// convert back from big endian (network order) to host
return htonl(newInt);
}
// return sample number once one is received (between 0 and 255, -1 if none)
// NB: will wait to get footer and then header in a row, may miss a packet but will prevent a bad sync with stream (thx BrainBay for the tip!)
int16_t CDriverOpenBCI::parseByte(const uint8_t actbyte)
{
// finished to read sample or not
bool status = false;
switch (m_readState)
{
// Default state: wait for Start byte
case ParserAutomaton_Default:
// if first byte is not the one expected, won't go further
if (actbyte == SAMPLE_STOP_BYTE) { m_seenPacketFooter = true; }
else
{
if (actbyte == SAMPLE_START_BYTE && m_seenPacketFooter) { m_readState = ParserAutomaton_StartByteReceived; }
m_seenPacketFooter = false;
}
// reset sample info
m_sampleNumber = -1;
m_extractPosition = 0;
m_sampleBufferPosition = 0;
break;
// Start byte received, consider next byte as sample number
case ParserAutomaton_StartByteReceived: m_sampleNumber = actbyte;
m_readState = ParserAutomaton_SampleNumberReceived;
break;
// Sample number received, next bytes hold the EEG data
/*
* Note: values are 24-bit signed, MSB first
* Bytes 3-5: Data value for EEG channel 1
* Bytes 6-8: Data value for EEG channel 2
* Bytes 9-11: Data value for EEG channel 3
* Bytes 12-14: Data value for EEG channel 4
* Bytes 15-17: Data value for EEG channel 5
* Bytes 18-20: Data value for EEG channel 6
* Bytes 21-23: Data value for EEG channel 6
* Bytes 24-26: Data value for EEG channel 8
*/
case ParserAutomaton_SampleNumberReceived: if (m_extractPosition < EEG_VALUE_COUNT_PER_SAMPLE)
{
// fill EEG buffer
if (m_sampleBufferPosition < EEG_VALUE_BUFFER_SIZE)
{
m_eegValueBuffers[m_sampleBufferPosition] = actbyte;
m_sampleBufferPosition++;
}
// we got EEG value
if (m_sampleBufferPosition == EEG_VALUE_BUFFER_SIZE)
{
// fill EEG channel buffer, converting at the same time from 24 to 32 bits + scaling
m_sampleEEGBuffers[m_extractPosition] = float(this->interpret24bitAsInt32(m_eegValueBuffers)) * m_unitsToMicroVolts;
// reset for next value
m_sampleBufferPosition = 0;
m_extractPosition++;
}
}
// finished with EEG
if (m_extractPosition == EEG_VALUE_COUNT_PER_SAMPLE)
{
// next step: accelerometer
m_readState = ParserAutomaton_EEGSamplesReceived;
// re-use the same variable to know position inside accelerometer block (I know, I'm bad!).
m_extractPosition = 0;
m_nValidAccelerometer = 0;
}
break;
// EEG Samples received, next bytes hold the Accelerometer data
/*
* Note: values are 16-bit signed, MSB first
* Bytes 27-28: Data value for accelerometer channel X
* Bytes 29-30: Data value for accelerometer channel Y
* Bytes 31-32: Data value for accelerometer channel Z
*/
case ParserAutomaton_EEGSamplesReceived: if (m_extractPosition < ACC_VALUE_COUNT_PER_SAMPLE)
{
// fill Acc buffer
if (m_sampleBufferPosition < ACC_VALUE_BUFFER_SIZE)
{
m_accValueBuffers[m_sampleBufferPosition] = actbyte;
m_sampleBufferPosition++;
}
// we got Acc value
if (m_sampleBufferPosition == ACC_VALUE_BUFFER_SIZE)
{
// fill Acc channel buffer, converting at the same time from 16 to 32 bits
m_sampleAccBuffersTemp[m_extractPosition] = float(this->interpret16bitAsInt32(m_accValueBuffers)) * m_unitsToRadians;
if (m_sampleAccBuffersTemp[m_extractPosition] != 0) { m_nValidAccelerometer++; }
// reset for next value
m_sampleBufferPosition = 0;
m_extractPosition++;
}
}
// finished with acc
if (m_extractPosition == ACC_VALUE_COUNT_PER_SAMPLE)
{
// next step: footer
m_readState = ParserAutomaton_AccelerometerSamplesReceived;
if (m_nValidAccelerometer == ACC_VALUE_COUNT_PER_SAMPLE) { m_sampleAccBuffers.swap(m_sampleAccBuffersTemp); }
}
break;
// Accelerometer Samples received, now expect a stop byte to terminate the frame
case ParserAutomaton_AccelerometerSamplesReceived:
// expected footer: perfect, returns sample number
if (actbyte == SAMPLE_STOP_BYTE)
{
// we shall pass
status = true;
// we're ockay for next time
m_seenPacketFooter = true;
}
// if last byte is not the one expected, discard whole sample
else { }
// whatever happened, it'll be the end of this journey
m_readState = ParserAutomaton_Default;
break;
// uh-oh, should not be there
default: if (actbyte == SAMPLE_STOP_BYTE)
{
// we're ockay for next time
m_seenPacketFooter = true;
}
m_readState = ParserAutomaton_Default;
break;
}
// if it's a GO, returns sample number, may trigger channel push
if (status) { return m_sampleNumber; }
// by default we're not ready
return -1;
}
//___________________________________________________________________//
// //
// if waitForResponse, will wait response before leaving the function (until timeout is reached)
// if logResponse, the actual response is sent to log manager
// timeout: time to sleep between each character written (in ms)
bool CDriverOpenBCI::sendCommand(const FD_TYPE fileDesc, const char* cmd, const bool waitForResponse, const bool logResponse, const uint32_t timeout,
std::string& reply)
{
const uint32_t size = strlen(cmd);
reply = "";
// no command: don't go further
if (size == 0) { return true; }
// write command to the board
for (size_t i = 0; i < size; ++i)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Sending sequence to OpenBCI board [" << std::string(1, cmd[i]) << "]\n";
if (this->writeToDevice(fileDesc, &cmd[i], 1) == WRITE_ERROR) { return false; }
// wait for response
if (waitForResponse)
{
// buffer for serial reading
std::ostringstream readStream;
uint64_t out = timeout;
const uint64_t tStart = System::Time::getTime();
bool finished = false;
while (System::Time::getTime() - tStart < out && !finished)
{
const uint32_t readLength = this->readFromDevice(fileDesc, &m_readBuffers[0], m_readBuffers.size(), 10);
if (readLength == READ_ERROR) { return false; }
readStream.write(reinterpret_cast<const char*>(&m_readBuffers[0]), readLength);
// early stop when the "$$$" pattern is detected
const std::string& content = readStream.str();
const std::string earlyStop = "$$$";
if (content.size() >= earlyStop.size())
{
if (content.substr(content.size() - earlyStop.size(), earlyStop.size()) == earlyStop) { finished = true; }
}
}
if (!finished)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": After " << out <<
"ms, timed out while waiting for board response !\n";
}
// now log response to log manager
if (logResponse)
{
// readStream stream to std::string and then to const to please log manager
if (!readStream.str().empty())
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": " << (
finished ? "Board response" : "Partial board response") << " was (size=" << readStream.str().size() << ") :\n";
m_driverCtx.getLogManager() << readStream.str() << "\n";
}
else { m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Board did not reply !\n"; }
}
// saves reply
reply += readStream.str();
}
else
{
// When no reply is expected, wait at least 100ms that the commands hits the device
System::Time::sleep(100);
}
}
return true;
}
bool CDriverOpenBCI::resetBoard(const FD_TYPE fileDescriptor, const bool regularInitialization)
{
const uint32_t startTime = System::Time::getTime();
std::string reply;
// stop/reset/default board
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Stopping board streaming...\n";
if (!this->sendCommand(fileDescriptor, "s", true, false, m_flushBoardReplyTimeout, reply)
) // the waiting serves to flush pending samples after stopping the streaming
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << this->m_driverName << ": Did not succeed in stopping board !\n";
return false;
}
// regular initialization of the board (not for recovery)
if (regularInitialization)
{
std::string line;
// reset 32-bit board (no effect with 8bit board)
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Soft reseting of the board...\n";
if (!this->sendCommand(fileDescriptor, "v", true, true, m_readBoardReplyTimeout, reply))
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << this->m_driverName << ": Did not succeed in soft reseting board !\n";
return false;
}
// gets meaningful information from reply
// WARNING - The parsing here is not very robust
// The parsing here is very dependant on the current protocol definition
std::istringstream cmdReplyInputStringStream(reply);
memset(&m_deviceInfo, 0, sizeof(m_deviceInfo));
while (std::getline(cmdReplyInputStringStream, line, '\n'))
{
if (line != "$$$")
{
sscanf(line.c_str(), "OpenBCI V%u %u channel", &m_deviceInfo.deviceVersion, &m_deviceInfo.deviceChannelCount);
sscanf(line.c_str(), "On Board %s Device ID: 0x%x", m_deviceInfo.boardChipset, &m_deviceInfo.boardId);
sscanf(line.c_str(), "On Daisy %s Device ID: 0x%x", m_deviceInfo.daisyChipset, &m_deviceInfo.daisyId);
sscanf(line.c_str(), "%[^ ] Device ID: 0x%x", m_deviceInfo.mocapChipset, &m_deviceInfo.mocapId);
}
}
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Got board config\n";
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Version: " << m_deviceInfo.deviceVersion << "\n";
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Channel count: " << m_deviceInfo.deviceChannelCount << "\n";
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Board (devid:" << m_deviceInfo.boardId << ", devchip:" << m_deviceInfo.
boardChipset << ")\n";
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Daisy (devid:" << m_deviceInfo.daisyId << ", devchip:" << m_deviceInfo.
daisyChipset << ")\n";
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Mocap (devid:" << m_deviceInfo.mocapId << ", devchip:" << m_deviceInfo.
mocapChipset << ")\n";
// verifies openbci board presence
if (m_deviceInfo.deviceVersion == 0x00)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName << ": The attached device does not look like an OpenBCI device\n";
return false;
}
// verifies daisy module presence
if (m_deviceInfo.daisyId == 0x00 && m_daisyModule)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName <<
": The attached device does not match current configuration with Daisy module requested while not connected on the board\n";
return false;
}
// After discussin with @Aj May 2016 it has been decided to disable
// the daisy module when it was present and not requested instead of
// checking its presence
#if 0
// verifies daisy module absence
if(m_deviceInfo.daisyId!=0x00 && !m_daisyModule)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName << ": Configured without daisy module while present on the board, please unplug\n";
return false;
}
#else
// Disables daisy module when necessary
if (m_deviceInfo.daisyId != 0x00 && !m_daisyModule)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Trace << this->m_driverName << ": Daisy module present but not requested, will now be disabled\n";
if (!this->sendCommand(fileDescriptor, "c", true, true, m_readBoardReplyTimeout, reply))
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << this->m_driverName << ": Did not succeed in disabling daisy module !\n";
return false;
}
}
#endif
// sends additional commands if necessary
std::istringstream ss(m_additionalCmds.toASCIIString());
while (std::getline(ss, line, '\255'))
{
if (line.length() > 0)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Additional custom commands for initialization : [" << line << "]\n";
if (!this->sendCommand(fileDescriptor, line.c_str(), true, true, m_readBoardReplyTimeout, reply))
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << this->m_driverName << ": Did not succeed sending additional command [" << line
<< "] !\n";
return false;
}
}
}
}
// start stream
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Starting stream...\n";
if (!this->sendCommand(fileDescriptor, "b", false, false, m_readBoardReplyTimeout, reply))
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << this->m_driverName << ": Did not succeed starting stream\n";
return false;
}
// should start streaming!
m_startTime = System::Time::getTime();
m_tick = m_startTime;
m_lastPacketNumber = UNINITIALIZED_PACKET_NUMBER;
m_droppedSampleTimes.clear();
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Status ready (initialization took " << m_tick - startTime
<< "ms)\n";
return true;
}
// update internal state (lastPacket, number of packet processed, etc.).
// returns true if a new sample is created
bool CDriverOpenBCI::handleCurrentSample(const int packetNumber)
{
bool ok = false; // true if a sample is added to m_channelBuffers
// if == -1, current sample is incomplete or corrupted
if (packetNumber >= 0)
{
bool hasDroppedSamples = false;
// check packet drop
if ((m_lastPacketNumber != UNINITIALIZED_PACKET_NUMBER) && ((m_lastPacketNumber + 1) % 256 != packetNumber))
{
const uint32_t sampleTime = System::Time::getTime() - m_startTime;
while (!m_droppedSampleTimes.empty() && sampleTime - m_droppedSampleTimes.front() > 1000) { m_droppedSampleTimes.pop_front(); }
hasDroppedSamples = true;
m_driverCtx.getLogManager() << Kernel::LogLevel_Warning << this->m_driverName << ": Packet dropped! [Last,Current]=[" << int(m_lastPacketNumber) <<
"," << packetNumber << "] (that was " << uint32_t(m_droppedSampleTimes.size() + 1) << " dropped samples lately)\n";
if (sampleTime > m_droppedSampleSafetyDelayBeforeReset)
{
m_droppedSampleTimes.push_back(sampleTime);
if (m_droppedSampleTimes.size() > m_droppedSampleCountBeforeReset)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Warning << this->m_driverName <<
": Too many samples dropped, will now attempt to recover with a reset of the board\n";
if (!this->resetBoard(m_fileDesc, false))
{
m_driverCtx.getLogManager() << Kernel::LogLevel_ImportantWarning << this->m_driverName << ": Did not succeed reseting board\n";
return false;
}
}
}
}
// no daisy module: push directly values
if (!m_daisyModule)
{
// concatenate EEG and Acc
// YRD NOTE: OPENBCI ACCELEROMETERS ARE SAMPLED WAY BELOW EEG (~25Hz, depends upon firmware)
// YRD NOTE: CONSEQUENTLY, THIS CODE CREATES BLOCKS OF DUPLICATES ACCELEROMETERS SAMPLES
std::copy(m_sampleEEGBuffers.begin(), m_sampleEEGBuffers.end(), m_sampleBuffers.begin());
std::copy(m_sampleAccBuffers.begin(), m_sampleAccBuffers.end(), m_sampleBuffers.begin() + m_sampleEEGBuffers.size());
// copy them to current chunk
m_channelBuffers.push_back(m_sampleBuffers);
ok = true;
}
// even: daisy, odd: first 8 channels
else
{
// on odd packet, got complete sample
if (packetNumber % 2)
{
// won't concatenate if there was packet drop
if (!hasDroppedSamples)
{
// Concatenate standard and daisy EEG values and Acc
// YRD NOTE: OPENBCI ACCELEROMETERS ARE SAMPLED WAY BELOW EEG (~25Hz, depends upon firmware)
// YRD NOTE: CONSEQUENTLY, THIS CODE CREATES BLOCKS OF DUPLICATES ACCELEROMETERS SAMPLES
std::copy(m_sampleEEGBuffers.begin(), m_sampleEEGBuffers.end(), m_sampleBuffers.begin());
std::copy(m_sampleEEGBuffersDaisy.begin(), m_sampleEEGBuffersDaisy.end(), m_sampleBuffers.begin() + m_sampleEEGBuffers.size());
std::copy(m_sampleAccBuffers.begin(), m_sampleAccBuffers.end(),
m_sampleBuffers.begin() + m_sampleEEGBuffers.size() + m_sampleEEGBuffersDaisy.size());
// at last, add to chunk
m_channelBuffers.push_back(m_sampleBuffers);
ok = true;
}
}
// an even packet: it's Daisy, store values for later
else
{
// swap may modify origin, but it's faster
m_sampleEEGBuffersDaisy.swap(m_sampleEEGBuffers);
}
}
m_lastPacketNumber = packetNumber;
}
// something to read: won't have to poll before "long"
if (ok) { m_tick = System::Time::getTime(); }
return ok;
}
bool CDriverOpenBCI::openDevice(FD_TYPE* fileDesc, const uint32_t ttyNumber)
{
CString ttyName;
if (ttyNumber == UNDEFINED_DEVICE_IDENTIFIER)
{
// Tries to find an existing port to connect to
uint32_t i = 0;
bool success = false;
do
{
ttyName = CConfigurationOpenBCI::getTTYFileName(i++);
if (CConfigurationOpenBCI::isTTYFile(ttyName))
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Automatically picked port [" << i << ":" << ttyName << "]\n";
success = true;
}
} while (!success && i < CConfigurationOpenBCI::getMaximumTtyCount());
if (!success)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName << ": Port has not been configure and driver could not open any port\n";
return false;
}
}
else { ttyName = CConfigurationOpenBCI::getTTYFileName(ttyNumber); }
#if defined TARGET_OS_Windows
DCB dcb = { 0 };
*fileDesc = ::CreateFile(LPCSTR(ttyName.toASCIIString()), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
nullptr);
if (*fileDesc == INVALID_HANDLE_VALUE)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName << ": Could not open port [" << ttyName << "]\n";
return false;
}
if (!GetCommState(*fileDesc, &dcb))
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName << ": Could not get comm state on port [" << ttyName << "]\n";
return false;
}
// update DCB rate, byte size, parity, and stop bits size
dcb.DCBlength = sizeof(dcb);
dcb.BaudRate = TERM_SPEED;
dcb.ByteSize = 8;
dcb.Parity = NOPARITY;
dcb.StopBits = ONESTOPBIT;
dcb.EvtChar = '\0';
// update flow control settings
dcb.fDtrControl = DTR_CONTROL_ENABLE;
dcb.fRtsControl = RTS_CONTROL_ENABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fOutxDsrFlow = FALSE;
dcb.fDsrSensitivity = FALSE;
dcb.fOutX = FALSE;
dcb.fInX = FALSE;
dcb.fTXContinueOnXoff = FALSE;
dcb.XonChar = 0;
dcb.XoffChar = 0;
dcb.XonLim = 0;
dcb.XoffLim = 0;
dcb.fParity = FALSE;
SetCommState(*fileDesc, &dcb);
SetupComm(*fileDesc, 64/*1024*/, 64/*1024*/);
EscapeCommFunction(*fileDesc, SETDTR);
SetCommMask(*fileDesc, EV_RXCHAR | EV_CTS | EV_DSR | EV_RLSD | EV_RING);
#elif defined TARGET_OS_Linux
struct termios terminalAttributes;
if((*fileDesc=::open(ttyName.toASCIIString(), O_RDWR))==-1)
{
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName << ": Could not open port [" << ttyName << "]\n";
return false;
}
if(tcgetattr(*fileDesc, &terminalAttributes)!=0)
{
::close(*fileDesc);
*fileDesc=-1;
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName << ": terminal: tcgetattr() failed - did you use the right port [" << ttyName << "] ?\n";
return false;
}
/* terminalAttributes.c_cflag = TERM_SPEED | CS8 | CRTSCTS | CLOCAL | CREAD; */
terminalAttributes.c_cflag = TERM_SPEED | CS8 | CLOCAL | CREAD;
terminalAttributes.c_iflag = 0;
terminalAttributes.c_oflag = OPOST | ONLCR;
terminalAttributes.c_lflag = 0;
if(tcsetattr(*fileDesc, TCSAFLUSH, &terminalAttributes)!=0)
{
::close(*fileDesc);
*fileDesc=-1;
m_driverCtx.getLogManager() << Kernel::LogLevel_Error << this->m_driverName << ": terminal: tcsetattr() failed - did you use the right port [" << ttyName << "] ?\n";
return false;
}
#else
return false;
#endif
m_driverCtx.getLogManager() << Kernel::LogLevel_Info << this->m_driverName << ": Successfully opened port [" << ttyName << "]\n";
m_ttyName = ttyName;
return true;
}
void CDriverOpenBCI::closeDevice(const FD_TYPE fileDesc)
{
#if defined TARGET_OS_Windows
CloseHandle(fileDesc);
#elif defined TARGET_OS_Linux
::close(fileDesc);
#else
#endif
}
uint32_t CDriverOpenBCI::writeToDevice(const FD_TYPE fileDesc, const void* buffer, const uint32_t size)
{
#if defined TARGET_OS_Windows
DWORD length = 0;
if (FALSE == WriteFile(fileDesc, buffer, size, &length, nullptr)) { return WRITE_ERROR; }
const int count = int(length);
#elif defined TARGET_OS_Linux
const int count = ::write(fileDesc, buffer, size);
if(count < 0) { return WRITE_ERROR; }
#else
return WRITE_ERROR;
#endif
return uint32_t(count);
}
uint32_t CDriverOpenBCI::readFromDevice(const FD_TYPE fileDesc, void* buffer, const uint32_t size, const uint64_t timeOut)
{
#if defined TARGET_OS_Windows
uint32_t readLength = 0;
uint32_t readOk = 0;
struct _COMSTAT status;
DWORD state;
if (ClearCommError(fileDesc, &state, &status)) { readLength = (status.cbInQue < size ? status.cbInQue : size); }
if (readLength > 0)
{
if (FALSE == ReadFile(fileDesc, buffer, readLength, LPDWORD(&readOk), nullptr)) { return READ_ERROR; }
return readLength;
}
return 0;
#elif defined TARGET_OS_Linux
fd_set inputFileDescSet;
struct timeval val;
bool finished=false;
val.tv_sec=0;
val.tv_usec=((timeOut>>20)*1000*1000)>>12;
uint32_t bytesLeftToRead=size;
do
{
FD_ZERO(&inputFileDescSet);
FD_SET(fileDesc, &inputFileDescSet);
switch(select(fileDesc + 1, &inputFileDescSet, nullptr, nullptr, &val))
{
case -1: // error
return READ_ERROR;
case 0: // timeout
finished = true;
break;
default:
if(FD_ISSET(fileDesc, &inputFileDescSet))
{
size_t readLength=::read(fileDesc, reinterpret_cast<uint8_t*>(buffer)+size-bytesLeftToRead, bytesLeftToRead);
if(readLength <= 0) { finished = true; }
else { bytesLeftToRead-=uint32_t(readLength); }
}
else { finished = true; }
break;
}
}
while(!finished && bytesLeftToRead != 0);
return size - bytesLeftToRead;
#else
return 0;
#endif
}
} // namespace AcquisitionServer
} // namespace OpenViBE
@@ -0,0 +1,156 @@
/*
* OpenBCI driver for OpenViBE
*
* \author Jeremy Frey
* \author Yann Renard
*
*/
#pragma once
#include "ovasIDriver.h"
#include "../ovasCHeader.h"
#include "../ovasCSettingsHelper.h"
#include "../ovasCSettingsHelperOperators.h"
#if defined TARGET_OS_Windows
typedef void* FD_TYPE;
#elif defined TARGET_OS_Linux
typedef int FD_TYPE;
#else
#endif
#include <vector>
#include <deque>
namespace OpenViBE {
namespace AcquisitionServer {
/**
* \class CDriverOpenBCI
* \author Jeremy Frey
* \author Yann Renard
*/
class CDriverOpenBCI final : public IDriver
{
public:
explicit CDriverOpenBCI(IDriverContext& ctx);
void release() { delete this; }
const char* getName() override { return m_driverName.toASCIIString(); }
bool initialize(const uint32_t nSamplePerSentBlock, IDriverCallback& callback) override;
bool uninitialize() override;
bool start() override;
bool stop() override;
bool loop() override;
bool isConfigurable() override { return true; }
bool configure() override;
const IHeader* getHeader() override { return &m_header; }
typedef enum
{
ParserAutomaton_Default,
ParserAutomaton_StartByteReceived,
ParserAutomaton_SampleNumberReceived,
ParserAutomaton_EEGSamplesReceived,
ParserAutomaton_AccelerometerSamplesReceived,
} EParserAutomaton;
protected:
int interpret24bitAsInt32(const std::vector<uint8_t>& byteBuffer);
int interpret16bitAsInt32(const std::vector<uint8_t>& byteBuffer);
int16_t parseByte(uint8_t actbyte);
bool sendCommand(FD_TYPE fileDesc, const char* cmd, bool waitForResponse, bool logResponse, uint32_t timeout, std::string& reply);
bool resetBoard(FD_TYPE fileDescriptor, bool regularInitialization);
bool handleCurrentSample(int packetNumber); // will take car of samples fetch from OpenBCI board, dropping/merging packets if necessary
void updateDaisy(bool quietLogging); // update internal state regarding daisy module
bool openDevice(FD_TYPE* fileDesc, uint32_t ttyNumber);
static void closeDevice(FD_TYPE fileDesc);
static uint32_t writeToDevice(FD_TYPE fileDesc, const void* buffer, uint32_t size);
static uint32_t readFromDevice(FD_TYPE fileDesc, void* buffer, uint32_t size, uint64_t timeOut = 0);
SettingsHelper m_settings;
IDriverCallback* m_callback = nullptr;
CHeader m_header;
FD_TYPE m_fileDesc;
CString m_driverName = "OpenBCI";
CString m_ttyName;
CString m_additionalCmds; // string to send possibly upon initialisation
uint32_t m_nChannel = EEG_VALUE_BUFFER_SIZE + ACC_VALUE_BUFFER_SIZE;
uint32_t m_deviceID = uint32_t(-1);
uint32_t m_readBoardReplyTimeout = 5000; // parameter com init string
uint32_t m_flushBoardReplyTimeout = 500; // parameter com init string
bool m_daisyModule = false; // daisy module attached or not
// OpenBCI protocol related
int16_t m_sampleNumber = 0; // returned by the board
uint32_t m_readState = 0; // position in the sample (see doc)
uint8_t m_sampleBufferPosition = 0; // position in the buffer
std::vector<uint8_t> m_eegValueBuffers; // buffer for one EEG value (int24)
std::vector<uint8_t> m_accValueBuffers; // buffer for one accelerometer value (int16_t)
const static uint8_t EEG_VALUE_BUFFER_SIZE = 3; // int24 == 3 bytes
const static uint8_t ACC_VALUE_BUFFER_SIZE = 2; // int16_t == 2 bytes
const static uint8_t EEG_VALUE_COUNT_PER_SAMPLE = 8; // the board send EEG values 8 by 8 (will concatenate 2 samples with daisy module)
const static uint8_t ACC_VALUE_COUNT_PER_SAMPLE = 3; // 3 accelerometer data per sample
uint32_t m_missingSampleDelayBeforeReset = 0; // in ms - value acquired from configuration manager
uint32_t m_droppedSampleCountBeforeReset = 0; // in samples - value acquired from configuration manager
uint32_t m_droppedSampleSafetyDelayBeforeReset = 0; // in ms - value acquired from configuration manager
std::deque<uint32_t> m_droppedSampleTimes;
float m_unitsToMicroVolts = 0; // convert from int to microvolt
float m_unitsToRadians = 0; // converts from int16_t to radians
uint32_t m_extractPosition = 0; // used to situate sample reading both with EEG and accelerometer data
uint32_t m_nValidAccelerometer = 0;
int m_lastPacketNumber = 0; // used to detect consecutive packets when daisy module is used
// buffer for multibyte reading over serial connection
std::vector<uint8_t> m_readBuffers;
// buffer to store sample coming from OpenBCI -- filled by parseByte(), passed to handleCurrentSample()
std::vector<float> m_sampleEEGBuffers;
std::vector<float> m_sampleEEGBuffersDaisy;
std::vector<float> m_sampleAccBuffers;
std::vector<float> m_sampleAccBuffersTemp;
// buffer to store aggregated samples
std::vector<std::vector<float>> m_channelBuffers; // buffer to store channels & chunks
std::vector<float> m_sampleBuffers;
bool m_seenPacketFooter = false; // extra precaution to sync packets
// mechanism to call resetBoard() if no data are received
uint32_t m_tick = 0; // last tick for polling
uint32_t m_startTime = 0; // actual time since connection
// sample storing for device callback and serial i/o
std::vector<float> m_callbackSamples;
private:
typedef struct
{
uint32_t deviceVersion;
uint32_t deviceChannelCount;
uint32_t boardId;
uint32_t daisyId;
uint32_t mocapId;
char boardChipset[64];
char daisyChipset[64];
char mocapChipset[64];
} device_information_t;
device_information_t m_deviceInfo;
};
} // namespace AcquisitionServer
} // namespace OpenViBE