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