diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml
index dc5ea949e..1c7a3d336 100644
--- a/.github/workflows/run_unix.yml
+++ b/.github/workflows/run_unix.yml
@@ -251,6 +251,8 @@ jobs:
run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1
- name: Streaming Python Markers
run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/streaming_board_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/markers.py --board-id -2 --ip-address 225.1.1.1 --ip-port 6677 --master-board -1
+ - name: BioListener Python
+ run: sudo -H python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/biolistener_emulator.py python3 $GITHUB_WORKSPACE/python_package/examples/tests/brainflow_get_data.py --board-id 64 --ip-address 127.0.0.1 --ip-port 12345
- name: Denoising Python
run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/denoising.py
- name: Serialization Python
diff --git a/.github/workflows/run_windows.yml b/.github/workflows/run_windows.yml
index 1fa6b4174..9db171151 100644
--- a/.github/workflows/run_windows.yml
+++ b/.github/workflows/run_windows.yml
@@ -186,6 +186,9 @@ jobs:
- name: KnightBoard Windows Python Test
run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\knightboard_windows.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 57 --serial-port
shell: cmd
+ - name: BioListener Windows Python Test
+ run: python %GITHUB_WORKSPACE%\emulator\brainflow_emulator\biolistener_emulator.py python %GITHUB_WORKSPACE%\python_package\examples\tests\brainflow_get_data.py --board-id 64 --ip-address 127.0.0.1 --ip-port 12345
+ shell: cmd
# Signal Processing Testing
- name: Serialization Rust Test
run: |
diff --git a/csharp_package/brainflow/brainflow/board_controller_library.cs b/csharp_package/brainflow/brainflow/board_controller_library.cs
index 7a5e60712..10a910621 100644
--- a/csharp_package/brainflow/brainflow/board_controller_library.cs
+++ b/csharp_package/brainflow/brainflow/board_controller_library.cs
@@ -122,7 +122,8 @@ public enum BoardIds
OB5000_8_CHANNELS_BOARD = 60,
SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61,
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62,
-
+ OB3000_24_CHANNELS_BOARD = 63,
+ BIOLISTENER_BOARD = 64,
};
diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst
index 1f188db83..3368ac24a 100644
--- a/docs/SupportedBoards.rst
+++ b/docs/SupportedBoards.rst
@@ -1347,3 +1347,47 @@ Supported platforms:
- Linux
- MacOS
- Devices like Raspberry Pi
+
+
+BioListener
+--------
+
+BioListener
+~~~~~~~~~~~~~
+
+.. image:: https://live.staticflickr.com/65535/54273076343_6a7eb99697_k.jpg
+ :width: 519px
+ :height: 389px
+
+`BioListener website `_
+
+To create such board you need to specify the following board ID and fields of BrainFlowInputParams object:
+
+- :code:`BoardIds.BIOLISTENER_BOARD`
+- *optional:* :code:`ip_address`, ip address of the machine running the BrainFlow server (not the end device). If not provided, the server will listen on all network interfaces (at `0.0.0.0`)
+- *optional:* :code:`ip_port`, any free local port. If the chosen port is in use, the next available free port will be used. If not provided, the search for a free port starts at `12345`
+- *optional:* :code:`timeout`, timeout for device discovery, default is 3sec
+
+Make sure to configure the BioListener board as stated in the `BioListener documentation `_ to connect to the BrainFlow server.
+
+Initialization Example:
+
+.. code-block:: python
+
+ params = BrainFlowInputParams()
+ params.ip_port = 12345
+ params.ip_address = "0.0.0.0"
+ board = BoardShim(BoardIds.BIOLISTENER_BOARD, params)
+
+Supported platforms:
+
+- Windows
+- Linux
+- MacOS
+- Devices like Raspberry Pi
+- Android
+
+Available :ref:`presets-label`:
+
+- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG (EMG, ECG, EOG) data
+- :code:`BrainFlowPresets.AUXILIARY_PRESET`, it contains Gyro, Accel, battery and ESP32 chip temperature data
diff --git a/emulator/brainflow_emulator/biolistener_emulator.py b/emulator/brainflow_emulator/biolistener_emulator.py
new file mode 100755
index 000000000..310744374
--- /dev/null
+++ b/emulator/brainflow_emulator/biolistener_emulator.py
@@ -0,0 +1,196 @@
+import datetime
+import enum
+import json
+import logging
+import random
+import socket
+import struct
+import subprocess
+import sys
+import threading
+import time
+
+from brainflow_emulator.emulate_common import TestFailureError, log_multilines
+
+BIOLISTENER_DATA_CHANNELS_COUNT = 8
+
+BIOLISTENER_DATA_PACKET_DEBUG = 0
+BIOLISTENER_DATA_PACKET_BIOSIGNALS = 1
+BIOLISTENER_DATA_PACKET_IMU = 2
+
+ADC_USED = 0 # ADS131M08
+
+
+class DataPacket:
+ FORMAT_STRING = f'=1B 1I 1B 1I 1B {BIOLISTENER_DATA_CHANNELS_COUNT}I 1B'
+
+ def __init__(self, ts, tp, n, s_id, data):
+ self.header = 0xA0
+ self.ts = ts
+ self.type = tp
+ self.n = n
+ self.s_id = s_id
+ self.data = data
+ self.footer = 0xC0
+
+ def pack(self):
+ return struct.pack(self.FORMAT_STRING, self.header, self.ts, self.type, self.n, self.s_id, *self.data, self.footer)
+
+ @classmethod
+ def unpack(cls, packed_data):
+ format_string = cls.FORMAT_STRING
+ unpacked_data = struct.unpack(format_string, packed_data)
+ return cls(*unpacked_data)
+
+ def __repr__(self):
+ return (f'DataPacket(header={self.header}, ts={self.ts}, type={self.type}, '
+ f'n={self.n}, s_id={self.s_id}, data={self.data}, footer={self.footer})')
+
+
+class State(enum.Enum):
+ wait = 'wait'
+ stream = 'stream'
+
+
+def test_socket(cmd_list):
+ logging.info('Running %s' % ' '.join([str(x) for x in cmd_list]))
+ process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = process.communicate()
+
+ log_multilines(logging.info, stdout)
+ log_multilines(logging.info, stderr)
+
+ if process.returncode != 0:
+ raise TestFailureError('Test failed with exit code %s' % str(process.returncode), process.returncode)
+
+ return stdout, stderr
+
+
+def run_socket_server():
+ thread = BioListenerEmulator()
+ thread.start()
+ return thread
+
+
+class BioListenerEmulator(threading.Thread):
+
+ def __init__(self):
+ threading.Thread.__init__(self)
+ self.local_ip = '127.0.0.1'
+ self.local_port = 12345
+ self.server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.server_socket.settimeout(1)
+ self.state = State.wait.value
+ self.package_num = 0
+ self.keep_alive = True
+ self.connection_established = False
+ logging.info(f"BioListener emulator started")
+
+ @staticmethod
+ def volts_to_data(ref, voltage, pga_gain, adc_resolution):
+ resolution = ref / (adc_resolution * pga_gain)
+
+ if voltage >= 0: # Positive range
+ raw_code = voltage / resolution
+ else: # Negative range
+ raw_code = (voltage + (ref / pga_gain)) / resolution
+
+ raw_code = int(raw_code)
+ raw_code = max(0, min(0xFFFFFF, raw_code)) # Ensure within 24 bit range
+
+ return raw_code
+
+ def run(self):
+ logging.info(f"BioListener emulator connecting to {self.local_ip}:{self.local_port}...")
+ while self.keep_alive and not self.connection_established:
+ try:
+ self.server_socket.connect((self.local_ip, self.local_port))
+ self.connection_established = True
+ break
+ except Exception as err:
+ logging.warning(f"Error connecting to {self.local_ip}:{self.local_port}: {err}")
+ # A failed connect may leave the socket unusable.
+ try:
+ self.server_socket.close()
+ except Exception:
+ pass
+ # Recreate the socket with the same options.
+ self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.server_socket.settimeout(1)
+ time.sleep(0.1)
+
+ if self.connection_established:
+ logging.info(f"BioListener emulator connected to {self.local_ip}:{self.local_port}")
+ else:
+ logging.error(f"BioListener emulator failed to connect to {self.local_ip}:{self.local_port}")
+ return
+
+ started_at = time.time()
+ while self.keep_alive:
+ new_data_packet = DataPacket(
+ ts=int((time.time() - started_at) * 1000),
+ tp=BIOLISTENER_DATA_PACKET_BIOSIGNALS,
+ n=self.package_num,
+ s_id=ADC_USED,
+ data=[
+ self.volts_to_data(
+ ref=2500000.0,
+ voltage=random.uniform(-1000, 1000),
+ pga_gain=8,
+ adc_resolution=16777216.0
+ ) for _ in range(BIOLISTENER_DATA_CHANNELS_COUNT)
+ ]
+ )
+ self.package_num += 1
+
+ try:
+ data = self.server_socket.recv(1024)
+ message = data.decode('utf-8').strip()
+
+ if message:
+ for message_part in message.split("\n"):
+ logging.info(f"BioListener received command: {message_part}")
+ json_str = json.loads(message_part)
+ if json_str["command"] in (1, 2, 3, 4):
+ logging.info("Command ignored - simulator supports only start and stop stream command")
+ elif json_str["command"] == 5:
+ logging.info("Start stream command received")
+ self.state = State.stream.value
+ elif json_str["command"] == 6:
+ logging.info("Stop stream command received")
+ self.state = State.wait.value
+ else:
+ logging.warning(f"Unknown command: {json_str['command']}")
+ except TimeoutError:
+ pass
+ except socket.timeout:
+ pass
+ except Exception as err:
+ logging.error(f"Error in recv thread: {err}")
+
+ try:
+ if self.state == State.stream.value:
+ self.server_socket.sendall(new_data_packet.pack())
+ except ConnectionResetError:
+ logging.error(f"Connection lost")
+ except Exception as e:
+ logging.error(f"Error: {e}")
+
+
+def main(cmd_list):
+ if not cmd_list:
+ raise Exception('No command to execute')
+ server_thread = run_socket_server()
+
+ try:
+ test_socket(cmd_list)
+ finally:
+ server_thread.keep_alive = False
+ server_thread.join()
+
+
+if __name__ == '__main__':
+ logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=logging.INFO)
+ main(sys.argv[1:])
diff --git a/java_package/brainflow/src/main/java/brainflow/BoardIds.java b/java_package/brainflow/src/main/java/brainflow/BoardIds.java
index d7e94212e..487d8fc5a 100644
--- a/java_package/brainflow/src/main/java/brainflow/BoardIds.java
+++ b/java_package/brainflow/src/main/java/brainflow/BoardIds.java
@@ -71,8 +71,9 @@ public enum BoardIds
SYNCHRONI_OCTO_8_CHANNELS_BOARD(59),
OB5000_8_CHANNELS_BOARD(60),
SYNCHRONI_PENTO_8_CHANNELS_BOARD(61),
- SYNCHRONI_UNO_1_CHANNELS_BOARD(62);
-
+ SYNCHRONI_UNO_1_CHANNELS_BOARD(62),
+ OB3000_24_CHANNELS_BOARD(63),
+ BIOLISTENER_BOARD(64);
private final int board_id;
private static final Map bi_map = new HashMap ();
diff --git a/julia_package/brainflow/src/board_shim.jl b/julia_package/brainflow/src/board_shim.jl
index 9e7960dbe..3e7086093 100644
--- a/julia_package/brainflow/src/board_shim.jl
+++ b/julia_package/brainflow/src/board_shim.jl
@@ -61,15 +61,14 @@ export BrainFlowInputParams
EXPLORE_PLUS_8_CHAN_BOARD = 54
EXPLORE_PLUS_32_CHAN_BOARD = 55
PIEEG_BOARD = 56
-
NEUROPAWN_KNIGHT_BOARD = 57
SYNCHRONI_TRIO_3_CHANNELS_BOARD = 58
SYNCHRONI_OCTO_8_CHANNELS_BOARD = 59
OB5000_8_CHANNELS_BOARD = 60
SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62
-
-
+ OB3000_24_CHANNELS_BOARD = 63
+ BIOLISTENER_BOARD = 64
end
diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m
index 876852528..cc242952d 100644
--- a/matlab_package/brainflow/BoardIds.m
+++ b/matlab_package/brainflow/BoardIds.m
@@ -59,13 +59,13 @@
EXPLORE_PLUS_8_CHAN_BOARD(54)
EXPLORE_PLUS_32_CHAN_BOARD(55)
PIEEG_BOARD(56)
-
NEUROPAWN_KNIGHT_BOARD(57)
SYNCHRONI_TRIO_3_CHANNELS_BOARD(58)
SYNCHRONI_OCTO_8_CHANNELS_BOARD(59)
OB5000_8_CHANNELS_BOARD(60)
SYNCHRONI_PENTO_8_CHANNELS_BOARD(61)
SYNCHRONI_UNO_1_CHANNELS_BOARD(62)
-
+ OB3000_24_CHANNELS_BOARD(63)
+ BIOLISTENER_BOARD(64)
end
end
\ No newline at end of file
diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts
index a4952aa11..f55f10187 100644
--- a/nodejs_package/brainflow/brainflow.types.ts
+++ b/nodejs_package/brainflow/brainflow.types.ts
@@ -69,15 +69,16 @@ export enum BoardIds {
EXPLORE_PLUS_8_CHAN_BOARD = 54,
EXPLORE_PLUS_32_CHAN_BOARD = 55,
PIEEG_BOARD = 56,
-
NEUROPAWN_KNIGHT_BOARD = 57,
SYNCHRONI_TRIO_3_CHANNELS_BOARD = 58,
SYNCHRONI_OCTO_CHANNELS_BOARD = 59,
OB5000_8_CHANNELS_BOARD = 60,
SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61,
- SYNCHRONI_UNO_1_CHANNELS_BOARD = 62
-
+ SYNCHRONI_UNO_1_CHANNELS_BOARD = 62,
+ OB3000_24_CHANNELS_BOARD = 63,
+ BIOLISTENER_BOARD = 64
}
+
export enum IpProtocolTypes {
NO_IP_PROTOCOL = 0,
UDP = 1,
diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py
index 11b089c11..0f98d001f 100644
--- a/python_package/brainflow/board_shim.py
+++ b/python_package/brainflow/board_shim.py
@@ -80,7 +80,8 @@ class BoardIds(enum.IntEnum):
OB5000_8_CHANNELS_BOARD = 60 #:
SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61 #:
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62 #:
-
+ OB3000_24_CHANNELS_BOARD = 63 #:
+ BIOLISTENER_BOARD = 64 #:
class IpProtocolTypes(enum.IntEnum):
diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs
index bd6195ace..49453de73 100644
--- a/rust_package/brainflow/src/ffi/constants.rs
+++ b/rust_package/brainflow/src/ffi/constants.rs
@@ -32,7 +32,7 @@ impl BoardIds {
pub const FIRST: BoardIds = BoardIds::PlaybackFileBoard;
}
impl BoardIds {
- pub const LAST: BoardIds = BoardIds::NeuropawnKnightBoard;
+ pub const LAST: BoardIds = BoardIds::BiolistenerBoard;
}
#[repr(i32)]
#[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)]
@@ -100,7 +100,9 @@ pub enum BoardIds {
SynchroniOcto8ChannelsBoard = 59,
OB50008CHannelsBoard= 60 ,
SynchroniPento8ChannelsBoard = 61,
- SynchroniUno1ChannelsBoard = 62
+ SynchroniUno1ChannelsBoard = 62,
+ OB300024ChannelsBoard = 63,
+ BiolistenerBoard = 64
}
#[repr(i32)]
#[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)]
diff --git a/src/board_controller/biolistener/biolistener.cpp b/src/board_controller/biolistener/biolistener.cpp
new file mode 100644
index 000000000..8b24c6a70
--- /dev/null
+++ b/src/board_controller/biolistener/biolistener.cpp
@@ -0,0 +1,546 @@
+#include "biolistener.h"
+
+#include
+#include
+#include
+
+#include "json.hpp"
+#include "network_interfaces.h"
+#include "timestamp.h"
+
+#include "biolistener_defines.h"
+
+using json = nlohmann::json;
+
+
+template
+BioListener::BioListener (
+ int board_id, struct BrainFlowInputParams params)
+ : Board (board_id, params)
+{
+ control_socket = NULL;
+ keep_alive = false;
+ initialized = false;
+ control_port = -1;
+ data_port = -1;
+ timestamp_offset = -1;
+
+ packet_size = sizeof (data_packet);
+
+ // Data channels gain to default value
+ for (int i = 0; i < BIOLISTENER_DATA_CHANNELS_COUNT; i++)
+ {
+ channels_gain[i] = BIOLISTENER_DEFAULT_PGA_GAIN;
+ }
+}
+
+template
+BioListener::~BioListener ()
+{
+ skip_logs = true;
+ release_session ();
+}
+
+template
+int BioListener::prepare_session ()
+{
+ if (initialized)
+ {
+ safe_logger (spdlog::level::info, "Session is already prepared");
+ return (int)BrainFlowExitCodes::STATUS_OK;
+ }
+
+ if (params.timeout < 2)
+ {
+ params.timeout = 3;
+ safe_logger (spdlog::level::warn, "Timeout is too low, setting to 3 sec");
+ }
+
+ if (params.ip_address.empty ())
+ {
+ params.ip_address = "0.0.0.0";
+ safe_logger (
+ spdlog::level::warn, "IP address is not set, listening on all network interfaces");
+ }
+
+ if (params.ip_port <= 0)
+ {
+ params.ip_port = 12345;
+ safe_logger (spdlog::level::warn, "IP port is not set, using default value 12345");
+ }
+
+ int res = create_control_connection ();
+
+ if (res == (int)BrainFlowExitCodes::STATUS_OK)
+ {
+ res = wait_for_connection ();
+ }
+ if (res == (int)BrainFlowExitCodes::STATUS_OK)
+ {
+ auto json_command = json {{"command", BIOLISTENER_COMMAND_RESET_ADC}};
+
+ res = send_control_msg ((json_command.dump () + PACKET_DELIMITER_CSV).c_str ());
+
+ std::this_thread::sleep_for (std::chrono::milliseconds (500));
+ }
+
+ if (res != (int)BrainFlowExitCodes::STATUS_OK)
+ {
+ if (control_socket != NULL)
+ {
+ delete control_socket;
+ control_socket = NULL;
+ }
+ }
+ else
+ {
+ initialized = true;
+ }
+
+ return res;
+}
+
+template
+int BioListener::config_board (
+ std::string conf, std::string &response)
+{
+ return send_control_msg (conf.c_str ());
+}
+
+template
+int BioListener::start_stream (
+ int buffer_size, const char *streamer_params)
+{
+ if (!initialized)
+ {
+ safe_logger (spdlog::level::err, "You need to call prepare_session before config_board");
+ return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR;
+ }
+ if (keep_alive)
+ {
+ safe_logger (spdlog::level::err, "Streaming thread already running");
+ return (int)BrainFlowExitCodes::STREAM_ALREADY_RUN_ERROR;
+ }
+ int res = prepare_for_acquisition (buffer_size, streamer_params);
+ if (res != (int)BrainFlowExitCodes::STATUS_OK)
+ {
+ return res;
+ }
+
+ auto json_command = json {{"command", BIOLISTENER_COMMAND_START_SAMPLING}};
+ res = send_control_msg ((json_command.dump () + PACKET_DELIMITER_CSV).c_str ());
+ if (res == (int)BrainFlowExitCodes::STATUS_OK)
+ {
+ keep_alive = true;
+ streaming_thread = std::thread ([this] { read_thread (); });
+ }
+ return (int)BrainFlowExitCodes::STATUS_OK;
+}
+
+template
+int BioListener::stop_stream ()
+{
+ if (keep_alive)
+ {
+ auto json_command = json {{"command", BIOLISTENER_COMMAND_STOP_SAMPLING}};
+ if (send_control_msg ((json_command.dump () + PACKET_DELIMITER_CSV).c_str ()) !=
+ (int)BrainFlowExitCodes::STATUS_OK)
+ {
+ safe_logger (spdlog::level::warn, "failed to stop stream");
+ }
+
+ keep_alive = false;
+ streaming_thread.join ();
+ return (int)BrainFlowExitCodes::STATUS_OK;
+ }
+ return (int)BrainFlowExitCodes::STREAM_THREAD_IS_NOT_RUNNING;
+}
+
+template
+int BioListener::release_session ()
+{
+ if (initialized)
+ {
+ if (keep_alive)
+ {
+ stop_stream ();
+ }
+ initialized = false;
+ free_packages ();
+ if (control_socket)
+ {
+ control_socket->close ();
+ delete control_socket;
+ control_socket = NULL;
+ }
+ control_port = -1;
+ data_port = -1;
+ }
+ return (int)BrainFlowExitCodes::STATUS_OK;
+}
+
+template
+bool BioListener::parse_tcp_buffer (const char *buffer,
+ size_t buffer_size, data_packet &parsed_packet)
+{
+ // Ensure the buffer size matches the packet size
+ if (buffer_size != packet_size)
+ {
+ safe_logger (spdlog::level::trace, "Buffer size mismatch!");
+ return false;
+ }
+
+ // Copy the raw buffer into the struct
+ std::memcpy (&parsed_packet, buffer, packet_size);
+
+ // Validate the header and footer
+ if (parsed_packet.header != BIOLISTENER_DATA_PACKET_HEADER)
+ {
+ safe_logger (spdlog::level::trace, "Invalid header! Expected: 0xA0");
+ return false;
+ }
+
+ if (parsed_packet.footer != BIOLISTENER_DATA_PACKET_FOOTER)
+ {
+ safe_logger (spdlog::level::trace, "Invalid footer! Expected: 0xC0");
+ return false;
+ }
+
+ return true;
+}
+
+
+// Convert raw ADC code (two's complement) to voltage
+// ref: reference voltage (X if milli volts output is needed, X * 1000 if micro volts output)
+// 1.2V for ADS131M08, 2.5V for AD7771
+// raw_code: raw ADC code
+// pga_gain: gain of the PGA
+// adc_resolution: resolution of the ADC (2^23 for ADS131M08, 2^24 for AD7771)
+template
+double BioListener::data_to_volts (
+ double ref, uint32_t raw_code, double pga_gain, double adc_resolution)
+{
+ // Calculate the resolution in millivolts
+ double resolution = ref / (adc_resolution * pga_gain);
+
+ // Compute the voltage in millivolts based on the raw ADC code
+ if (raw_code <= 0x7FFFFF)
+ { // Positive range
+ return resolution * (double)raw_code;
+ }
+ else
+ { // Negative range (two's complement)
+ return (resolution * (double)raw_code) - (ref / pga_gain);
+ }
+}
+
+
+template
+void BioListener::read_thread ()
+{
+ char message[sizeof (data_packet)];
+ int num_rows = board_descr["default"]["num_rows"];
+ double *package = new double[num_rows];
+ for (int i = 0; i < num_rows; i++)
+ {
+ package[i] = 0.0;
+ }
+
+ bool first_data_packet_received = false;
+
+ std::vector eeg_channels = board_descr["default"]["eeg_channels"];
+ std::vector other_channels = board_descr["default"]["other_channels"];
+ std::vector accel_channels = board_descr["auxiliary"]["accel_channels"];
+ std::vector gyro_channels = board_descr["auxiliary"]["gyro_channels"];
+ int temperature_channel = board_descr["auxiliary"]["temperature_channels"][0];
+
+ while (keep_alive)
+ {
+ int bytes_recv =
+ control_socket->recv (message, sizeof (data_packet));
+ if (bytes_recv < 1)
+ {
+ safe_logger (spdlog::level::trace, "no data received");
+ continue;
+ }
+
+ try
+ {
+ data_packet parsed_packet {};
+ if (!parse_tcp_buffer (message, bytes_recv, parsed_packet))
+ {
+ safe_logger (spdlog::level::err, "Failed to parse data packet");
+ continue;
+ }
+
+ if (!first_data_packet_received)
+ {
+ // ENHANCEMENT: can be replaced with more accurate timestamp based on ntp or similar
+ timestamp_offset = get_timestamp () - ((double)parsed_packet.ts / 1000.0);
+
+ // Check gain
+ for (int i = 0; i < BIOLISTENER_DATA_CHANNELS_COUNT; i++)
+ {
+ int sensor_id = parsed_packet.s_id;
+
+ double pga_gain;
+ {
+ std::lock_guard lock (m_channels_gain);
+ pga_gain = channels_gain[i];
+ }
+
+ if (parsed_packet.s_id == BIOLISTENER_ADC_AD7771)
+ {
+ // 1-8 OK
+ if (pga_gain < 1 || pga_gain > 8)
+ {
+ safe_logger (spdlog::level::critical, "Invalid pga gain for AD7771: {}",
+ pga_gain);
+ break;
+ }
+ }
+ else if (parsed_packet.s_id == BIOLISTENER_ADC_ADS131M08)
+ {
+ // 1-128 OK
+ if (pga_gain < 1 || pga_gain > 128)
+ {
+ safe_logger (spdlog::level::critical,
+ "Invalid pga gain for ADS131M08: {}", pga_gain);
+ break;
+ }
+ }
+ }
+
+ first_data_packet_received = true;
+ }
+
+ if (parsed_packet.type == BIOLISTENER_DATA_PACKET_BIOSIGNALS)
+ {
+ package[board_descr["default"]["timestamp_channel"].template get ()] =
+ timestamp_offset + ((double)parsed_packet.ts / 1000.0);
+ package[board_descr["default"]["package_num_channel"].template get ()] =
+ parsed_packet.n;
+
+ static uint32_t package_num_channel_last = parsed_packet.n - 1;
+
+ if (package_num_channel_last + 1 != parsed_packet.n)
+ {
+ safe_logger (spdlog::level::err,
+ "Package num mismatch BIOLISTENER_DATA_PACKET_BIOSIGNALS! Lost: {} packets",
+ parsed_packet.n - package_num_channel_last + 1);
+ }
+
+ package_num_channel_last = parsed_packet.n;
+
+ int sensor_id = parsed_packet.s_id;
+
+ for (int i = 0; i < BIOLISTENER_DATA_CHANNELS_COUNT; i++)
+ {
+ double pga_gain;
+ {
+ std::lock_guard lock (m_channels_gain);
+ pga_gain = channels_gain[i];
+ }
+
+ if (sensor_id == BIOLISTENER_ADC_AD7771)
+ {
+ static const double ref_microV = 2500000.0;
+ static const double adc_resolution = 16777216.0;
+ package[eeg_channels[i]] = data_to_volts (ref_microV, parsed_packet.data[i],
+ pga_gain, adc_resolution) *
+ 2.0;
+ }
+ else if (sensor_id == BIOLISTENER_ADC_ADS131M08)
+ {
+ static const double ref_microV = 1200000.0;
+ static const double adc_resolution = 16777216.0;
+ package[eeg_channels[i]] = data_to_volts (ref_microV, parsed_packet.data[i],
+ pga_gain, adc_resolution) *
+ 2.0;
+ }
+ else
+ {
+ safe_logger (spdlog::level::err, "Unknown sensor id: {}", sensor_id);
+ return;
+ }
+ }
+ push_package (package);
+ }
+ else if (parsed_packet.type == BIOLISTENER_DATA_PACKET_IMU)
+ {
+ package[board_descr["auxiliary"]["timestamp_channel"].template get ()] =
+ timestamp_offset + ((double)parsed_packet.ts / 1000.0);
+ package[board_descr["auxiliary"]["package_num_channel"].template get ()] =
+ parsed_packet.n;
+
+ static uint32_t package_num_channel_last = parsed_packet.n - 1;
+
+ if (package_num_channel_last + 1 != parsed_packet.n)
+ {
+ safe_logger (spdlog::level::err,
+ "Package num mismatch BIOLISTENER_DATA_PACKET_IMU! Lost: {} packets",
+ parsed_packet.n - package_num_channel_last + 1);
+ }
+
+ package_num_channel_last = parsed_packet.n;
+
+ for (int i = 0; i < 3; i++)
+ {
+ package[accel_channels[i]] = UINT32_TO_FLOAT (parsed_packet.data[i]);
+ package[gyro_channels[i]] = UINT32_TO_FLOAT (parsed_packet.data[i + 3]);
+ }
+
+ package[temperature_channel] = UINT32_TO_FLOAT (parsed_packet.data[6]);
+ package[board_descr["auxiliary"]["battery_channel"].template get ()] =
+ UINT32_TO_FLOAT (parsed_packet.data[7]);
+
+ push_package (package, (int)BrainFlowPresets::AUXILIARY_PRESET);
+ }
+ else
+ {
+ safe_logger (
+ spdlog::level::err, "Unknown data packet type: {}", parsed_packet.type);
+ }
+ }
+ catch (json::parse_error &e)
+ {
+ safe_logger (spdlog::level::err, "Failed to parse json: {}", e.what ());
+ }
+ }
+
+ delete[] package;
+}
+
+template
+int BioListener::create_control_connection ()
+{
+ char local_ip[80];
+
+ strncpy (local_ip, params.ip_address.c_str (), sizeof (local_ip) - 1);
+ local_ip[sizeof (local_ip) - 1] = '\0';
+ safe_logger (spdlog::level::info, "local ip addr is {}", local_ip);
+
+ int res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR;
+ for (int i = 0; i < BIOLISTENER_MAX_PORTS_TO_TRY_TILL_FREE_FOUND; i += 1)
+ {
+ control_port = params.ip_port + i;
+ control_socket = new SocketServerTCP (local_ip, control_port, true);
+ if (control_socket->bind () == ((int)SocketServerTCPReturnCodes::STATUS_OK))
+ {
+ safe_logger (spdlog::level::info, "use port {} for control", control_port);
+ res = (int)BrainFlowExitCodes::STATUS_OK;
+ break;
+ }
+ else
+ {
+ safe_logger (spdlog::level::warn, "failed to connect to {}", control_port);
+ }
+ control_socket->close ();
+ delete control_socket;
+ control_socket = NULL;
+ }
+ return res;
+}
+
+template
+int BioListener::send_control_msg (const char *msg)
+{
+ // should never happen
+ if (control_port < 0)
+ {
+ safe_logger (spdlog::level::info, "ports for control are not set");
+ return (int)BrainFlowExitCodes::GENERAL_ERROR;
+ }
+
+ // try to convert msg from json to string
+ try
+ {
+ auto j = json::parse (msg);
+ if (j.contains ("command") && j["command"] == BIOLISTENER_COMMAND_SET_ADC_CHANNEL_PGA)
+ {
+ int channel = j["channel"];
+ double pga = j["pga"];
+ if (channel >= 0 && channel < BIOLISTENER_DATA_CHANNELS_COUNT)
+ {
+ std::lock_guard lock (m_channels_gain);
+ channels_gain[channel] = pga;
+ safe_logger (spdlog::level::info, "Set gain for channel: {}", channel);
+ safe_logger (spdlog::level::info, "Gain: {}", pga);
+ }
+ }
+ }
+ catch (json::parse_error &e)
+ {
+ safe_logger (spdlog::level::err, "Failed to parse json: {}", e.what ());
+ return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+ }
+ catch (json::exception &e)
+ {
+ safe_logger (spdlog::level::err, "Failed to parse json: {}", e.what ());
+ return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+ }
+ catch (...)
+ {
+ safe_logger (spdlog::level::err, "Failed to parse json");
+ return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+ }
+
+ // convert msg to string just by copying
+ std::string package = msg;
+
+ int res = (int)BrainFlowExitCodes::STATUS_OK;
+ int bytes_send = control_socket->send (package.c_str (), (int)package.size ());
+ if (bytes_send != (int)package.size ())
+ {
+ safe_logger (spdlog::level::err, "failed to send control msg package: {}, res is {}", msg,
+ bytes_send);
+ res = (int)BrainFlowExitCodes::GENERAL_ERROR;
+ }
+ else
+ {
+ safe_logger (spdlog::level::info, "Message: {} sent", msg);
+ }
+ return res;
+}
+
+template
+int BioListener::wait_for_connection ()
+{
+ int res = (int)BrainFlowExitCodes::STATUS_OK;
+ int accept_res = control_socket->accept ();
+ if (accept_res != (int)SocketServerTCPReturnCodes::STATUS_OK)
+ {
+ safe_logger (spdlog::level::err, "error in accept");
+ res = (int)BrainFlowExitCodes::GENERAL_ERROR;
+ }
+ else
+ {
+ int max_attempts = (params.timeout * 1000) / BIOLISTENER_SLEEP_TIME_BETWEEN_SOCKET_TRIES_MS;
+ for (int i = 0; i < max_attempts; i++)
+ {
+ safe_logger (spdlog::level::trace, "waiting for accept {}/{}", i, max_attempts);
+ if (control_socket->client_connected)
+ {
+ safe_logger (spdlog::level::trace, "BioListener connected");
+ break;
+ }
+ else
+ {
+#ifdef _WIN32
+ Sleep (BIOLISTENER_SLEEP_TIME_BETWEEN_SOCKET_TRIES_MS);
+#else
+ usleep (BIOLISTENER_SLEEP_TIME_BETWEEN_SOCKET_TRIES_MS * 1000);
+#endif
+ }
+ }
+ if (!control_socket->client_connected)
+ {
+ safe_logger (spdlog::level::trace, "BioListener - failed to establish connection");
+ res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR;
+ }
+ }
+ return res;
+}
+
+template class BioListener<8>;
+template class BioListener<16>;
diff --git a/src/board_controller/biolistener/inc/biolistener.h b/src/board_controller/biolistener/inc/biolistener.h
new file mode 100644
index 000000000..704503fd8
--- /dev/null
+++ b/src/board_controller/biolistener/inc/biolistener.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include "board.h"
+#include "board_controller.h"
+
+#include "broadcast_server.h"
+#include "socket_client_udp.h"
+#include "socket_server_tcp.h"
+#include "socket_server_udp.h"
+
+
+#pragma pack(push, 1)
+template
+struct data_packet
+{
+ uint8_t header;
+ uint32_t ts;
+ uint8_t type;
+ uint32_t n;
+ uint8_t s_id;
+ uint32_t data[BIOLISTENER_DATA_CHANNELS_COUNT];
+ uint8_t footer;
+};
+#pragma pack(pop)
+
+
+template
+class BioListener : public Board
+{
+
+private:
+ volatile bool keep_alive;
+ volatile bool initialized;
+
+ std::string ip_address;
+ std::thread streaming_thread;
+ SocketServerTCP *control_socket;
+ std::mutex m;
+ std::condition_variable cv;
+ int control_port;
+ int data_port;
+
+ size_t packet_size;
+
+ double timestamp_offset;
+
+ std::mutex m_channels_gain;
+ double channels_gain[BIOLISTENER_DATA_CHANNELS_COUNT] {0};
+
+ void read_thread ();
+
+ int create_control_connection ();
+ int send_control_msg (const char *msg);
+ int wait_for_connection ();
+
+ static double data_to_volts (
+ double ref, uint32_t raw_code, double pga_gain, double adc_resolution);
+
+ bool parse_tcp_buffer (const char *buffer, size_t buffer_size,
+ data_packet &parsed_packet);
+
+public:
+ BioListener (int board_id, struct BrainFlowInputParams params);
+ ~BioListener ();
+
+ int prepare_session ();
+ int start_stream (int buffer_size, const char *streamer_params);
+ int stop_stream ();
+ int release_session ();
+ int config_board (std::string config, std::string &response);
+};
diff --git a/src/board_controller/biolistener/inc/biolistener_defines.h b/src/board_controller/biolistener/inc/biolistener_defines.h
new file mode 100644
index 000000000..a3765ba0e
--- /dev/null
+++ b/src/board_controller/biolistener/inc/biolistener_defines.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#define PACKET_DELIMITER_CSV '\n'
+
+#define BIOLISTENER_COMMAND_UNDEFINED 0
+#define BIOLISTENER_COMMAND_SET_ADC_DATA_RATE 1
+#define BIOLISTENER_COMMAND_SET_ADC_CHANNEL_ENABLE 2
+#define BIOLISTENER_COMMAND_SET_ADC_CHANNEL_PGA 3
+#define BIOLISTENER_COMMAND_RESET_ADC 4
+#define BIOLISTENER_COMMAND_START_SAMPLING 5
+#define BIOLISTENER_COMMAND_STOP_SAMPLING 6
+
+#define BIOLISTENER_DATA_PACKET_DEBUG 0
+#define BIOLISTENER_DATA_PACKET_BIOSIGNALS 1
+#define BIOLISTENER_DATA_PACKET_IMU 2
+
+#define BIOLISTENER_DATA_PACKET_HEADER 0xA0
+#define BIOLISTENER_DATA_PACKET_FOOTER 0xC0
+
+#define BIOLISTENER_ADC_ADS131M08 0
+#define BIOLISTENER_ADC_AD7771 1
+
+#define BIOLISTENER_DEFAULT_PGA_GAIN 8
+
+#define BIOLISTENER_MAX_PORTS_TO_TRY_TILL_FREE_FOUND 200
+#define BIOLISTENER_SLEEP_TIME_BETWEEN_SOCKET_TRIES_MS 300
+
+#define FLOAT_TO_UINT32(x) (*((uint32_t *)&(x)))
+#define UINT32_TO_FLOAT(x) (*((float *)&(x)))
diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp
index be50c9818..3cab800f5 100644
--- a/src/board_controller/board_controller.cpp
+++ b/src/board_controller/board_controller.cpp
@@ -16,6 +16,7 @@
#include "aavaa_v3.h"
#include "ant_neuro.h"
+#include "biolistener.h"
#include "board.h"
#include "board_controller.h"
#include "board_info_getter.h"
@@ -304,6 +305,9 @@ int prepare_session (int board_id, const char *json_brainflow_input_params)
board =
std::shared_ptr (new Knight ((int)BoardIds::NEUROPAWN_KNIGHT_BOARD, params));
break;
+ case BoardIds::BIOLISTENER_BOARD:
+ board = std::shared_ptr (new BioListener<8> (board_id, params));
+ break;
default:
return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR;
}
diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp
index c27d3ca9a..9c98da230 100644
--- a/src/board_controller/brainflow_boards.cpp
+++ b/src/board_controller/brainflow_boards.cpp
@@ -80,8 +80,9 @@ BrainFlowBoards::BrainFlowBoards()
{"59", json::object()},
{"60", json::object()},
{"61", json::object()},
- {"62", json::object()}
-
+ {"62", json::object()},
+ {"63", json::object()},
+ {"64", json::object()}
}
}};
@@ -1152,7 +1153,7 @@ BrainFlowBoards::BrainFlowBoards()
{"marker_channel", 10},
{"num_rows", 11},
{"eeg_channels", {1, 2, 3, 4, 5, 6, 7}},
- {"ecg_channels", {8}}
+ {"ecg_channels", {8}}
};
brainflow_boards_json["boards"]["60"]["default"] = {
{"name", "OB5000MAX"},
@@ -1170,7 +1171,7 @@ BrainFlowBoards::BrainFlowBoards()
{"timestamp_channel", 9},
{"marker_channel", 10},
{"num_rows", 11},
- {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}
+ {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}
};
brainflow_boards_json["boards"]["62"]["default"] = {
{"name", "Sync-Uno"},
@@ -1179,7 +1180,7 @@ BrainFlowBoards::BrainFlowBoards()
{"timestamp_channel", 2},
{"marker_channel", 3},
{"num_rows", 4},
- {"eeg_channels", {1}}
+ {"eeg_channels", {1}}
};
brainflow_boards_json["boards"]["63"]["default"] = {
{"name", "OB3000"},
@@ -1189,7 +1190,35 @@ BrainFlowBoards::BrainFlowBoards()
{"marker_channel", 26},
{"num_rows", 27},
{"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}},
- {"ecg_channels", {24}}
+ {"ecg_channels", {24}}
+ };
+ brainflow_boards_json["boards"]["64"]["default"] =
+ {
+ {"name", "BioListener"},
+ {"sampling_rate", 500},
+ {"timestamp_channel", 11},
+ {"marker_channel", 12},
+ {"package_num_channel", 0},
+ {"num_rows", 13},
+ {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}},
+ {"emg_channels", {1, 2, 3, 4, 5, 6, 7, 8}},
+ {"ecg_channels", {1, 2, 3, 4, 5, 6, 7, 8}},
+ {"eog_channels", {1, 2, 3, 4, 5, 6, 7, 8}},
+ {"other_channels", {9, 10}}
+ };
+ brainflow_boards_json["boards"]["64"]["auxiliary"] =
+ {
+ {"name", "BioListener"},
+ {"sampling_rate", 50},
+ {"timestamp_channel", 11},
+ {"marker_channel", 12},
+ {"package_num_channel", 0},
+ {"num_rows", 13},
+ {"accel_channels", {1, 2, 3}},
+ {"gyro_channels", {4, 5, 6}},
+ {"temperature_channels", {7}},
+ {"battery_channel", 8},
+ {"other_channels", {9, 10}}
};
}
diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake
index 58e99bd3b..5239cb2d6 100644
--- a/src/board_controller/build.cmake
+++ b/src/board_controller/build.cmake
@@ -87,6 +87,7 @@ SET (BOARD_CONTROLLER_SRC
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/pieeg/pieeg_board.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/synchroni/synchroni_board.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/knight.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/biolistener.cpp
)
include (${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro/build.cmake)
@@ -154,6 +155,7 @@ target_include_directories (
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/pieeg/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/synchroni/inc
${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/inc
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/inc
)
target_compile_definitions(${BOARD_CONTROLLER_NAME} PRIVATE NOMINMAX BRAINFLOW_VERSION=${BRAINFLOW_VERSION})
diff --git a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp
index 362fa1ad3..943d4f18a 100644
--- a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp
+++ b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp
@@ -1,4 +1,5 @@
#include
+#include
#include
#include
diff --git a/src/utils/inc/brainflow_constants.h b/src/utils/inc/brainflow_constants.h
index 6c52f5b1b..021bbca7e 100644
--- a/src/utils/inc/brainflow_constants.h
+++ b/src/utils/inc/brainflow_constants.h
@@ -95,10 +95,10 @@ enum class BoardIds : int
SYNCHRONI_NEO_8_CHANNELS_BOARD = 61,
SYNCHRONI_UNO_1_CHANNELS_BOARD = 62,
OB3000_24_CHANNELS_BOARD = 63,
+ BIOLISTENER_BOARD = 64,
// use it to iterate
FIRST = PLAYBACK_FILE_BOARD,
- LAST = OB3000_24_CHANNELS_BOARD
-
+ LAST = BIOLISTENER_BOARD
};
enum class IpProtocolTypes : int