From 3e6e99bfad8230a01c46eb02d8a4a92b0821ab3a Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 10 Nov 2026 00:32:08 +0100 Subject: [PATCH 1/2] Add BITalino six-channel serial acquisition and emulated lifecycle tests --- .github/workflows/run_unix.yml | 3 + .../brainflow/board_controller_library.cs | 3 +- docs/SupportedBoards.rst | 44 +++++ .../bitalino_serial_test.py | 104 ++++++++++ .../src/main/java/brainflow/BoardIds.java | 3 +- julia_package/brainflow/src/board_shim.jl | 1 + matlab_package/brainflow/BoardIds.m | 1 + nodejs_package/brainflow/brainflow.types.ts | 3 +- python_package/brainflow/board_shim.py | 1 + rust_package/brainflow/src/ffi/constants.rs | 1 + src/board_controller/bitalino/bitalino.cpp | 183 ++++++++++++++++++ src/board_controller/bitalino/inc/bitalino.h | 33 ++++ .../bitalino/inc/bitalino_parser.h | 72 +++++++ src/board_controller/board_controller.cpp | 4 + src/board_controller/brainflow_boards.cpp | 12 ++ src/board_controller/build.cmake | 2 + .../board_controller/bitalino_unittest.cpp | 61 ++++++ src/tests/build.cmake | 1 + src/utils/inc/brainflow_constants.h | 3 +- .../Sources/BrainFlow/BrainFlowEnums.swift | 1 + 20 files changed, 532 insertions(+), 4 deletions(-) create mode 100644 emulator/brainflow_emulator/bitalino_serial_test.py create mode 100644 src/board_controller/bitalino/bitalino.cpp create mode 100644 src/board_controller/bitalino/inc/bitalino.h create mode 100644 src/board_controller/bitalino/inc/bitalino_parser.h create mode 100644 src/tests/board_controller/bitalino_unittest.cpp diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index db6accc74..50ef9fd8b 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -261,6 +261,9 @@ jobs: # start testing - name: Run unit tests run: $GITHUB_WORKSPACE/build/tests/brainflow_tests + - name: BITalino serial emulator + if: runner.os == 'Linux' + run: python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/bitalino_serial_test.py $GITHUB_WORKSPACE/installed/lib/libBoardController.so - name: Synthetic C# Test MacOS if: runner.os == 'macOS' run: | diff --git a/csharp_package/brainflow/brainflow/board_controller_library.cs b/csharp_package/brainflow/brainflow/board_controller_library.cs index 83d134a4c..8ec77128a 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -124,7 +124,8 @@ public enum BoardIds IRONBCI_32_BOARD = 65, NEUROPAWN_KNIGHT_BOARD_IMU = 66, MUSE_S_ATHENA_BOARD = 67, - SHIMMER3_BOARD = 68 + SHIMMER3_BOARD = 68, + BITALINO_BOARD = 70 }; diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst index 533d8fda3..0f26062de 100644 --- a/docs/SupportedBoards.rst +++ b/docs/SupportedBoards.rst @@ -1581,3 +1581,47 @@ Common sensor bits: - :code:`0x000008`, EXG2 24-bit - :code:`0x000004`, GSR, exposed as EDA - :code:`0x002000`, battery + +BITalino serial / Bluetooth Classic +----------------------------------- + +``BoardIds.BITALINO_BOARD`` (70) acquires all six analog channels at 1000 Hz +using the original BITalino / BITalino (r)evolution serial protocol. Set +``serial_port`` to a USB or operating-system-paired Bluetooth Classic RFCOMM +port (115200 baud). BLE-only models are not supported. ``timeout`` is the +handshake and first-sample timeout in seconds (default 5). + +The six ``analog_channels`` contain **raw ADC counts**, not volts or microvolts: +A1-A4 are 10-bit and A5-A6 are 6-bit. The attached sensor determines the physical +quantity and transfer function; apply the appropriate sensor calibration before +signal analysis. The four ``other_channels`` contain D1-D4 as 0/1. The package +number is the device's four-bit sequence counter, wrapping every 16 samples. +Timestamps use host receive time, not a synchronized device clock. + +Set ``other_info = "simulated"`` to request the device firmware's simulated +signal mode. This still needs a connected BITalino. ``config_board("get_version")`` +returns the firmware identification cached during preparation. Other sampling +rates, channel subsets, output control and battery configuration are not exposed +by this initial fixed-profile driver. + +.. code-block:: python + + from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds + + params = BrainFlowInputParams() + params.serial_port = "/dev/rfcomm0" # or a paired COM port on Windows + board = BoardShim(BoardIds.BITALINO_BOARD, params) + board.prepare_session() + try: + board.start_stream() + # Read raw samples with get_board_data(). + board.stop_stream() + finally: + board.release_session() + +The parser checks CRC4 and requires two consecutive valid frames to acquire or +recover alignment. CRC4 has limited error-detection strength. Tests use golden +protocol vectors and a pseudo-terminal device emulator. Physical-device and +Bluetooth serial validation remain outstanding. + +Protocol reference: https://github.com/BITalinoWorld/cpp-api diff --git a/emulator/brainflow_emulator/bitalino_serial_test.py b/emulator/brainflow_emulator/bitalino_serial_test.py new file mode 100644 index 000000000..a616d208f --- /dev/null +++ b/emulator/brainflow_emulator/bitalino_serial_test.py @@ -0,0 +1,104 @@ +"""Exercise the native driver with a POSIX pseudo-terminal, no device required. + +Usage: python3 bitalino_serial_test.py /path/to/libBoardController.so +""" +import ctypes +import json +import os +import pty +import select +import sys +import threading +import time + + +def main(library): + lib = ctypes.CDLL(library) + lib.prepare_session.argtypes = [ctypes.c_int, ctypes.c_char_p] + lib.start_stream.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p] + lib.stop_stream.argtypes = lib.prepare_session.argtypes + lib.release_session.argtypes = lib.prepare_session.argtypes + lib.get_current_board_data.argtypes = [ctypes.c_int, ctypes.c_int, + ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_int), + ctypes.c_int, ctypes.c_char_p] + master, slave = pty.openpty() + params = json.dumps(dict(serial_port=os.ttyname(slave), mac_address='', + ip_address='', ip_address_aux='', ip_address_anc='', ip_port=0, + ip_port_aux=0, ip_port_anc=0, ip_protocol=0, other_info='', timeout=1, + serial_number='', file='', file_aux='', file_anc='', master_board=-100)).encode() + stop = threading.Event() + active = threading.Event() + + commands = [] + failures = [] + + def device(): + table = [0, 3, 6, 5, 12, 15, 10, 9, 11, 8, 13, 14, 7, 4, 1, 2] + streaming = False + seq = 0 + try: + while not stop.is_set(): + readable, _, _ = select.select([master], [], [], 0.002) + if readable: + for command in os.read(master, 64): + commands.append(command) + if command == 0: + streaming = False + elif command == 7: + os.write(master, b'BITalino_v5.0\n') + elif command in (0xfd, 0xfe): + assert len(commands) >= 2 and commands[-2] == 0xc3 + streaming = True + seq = 0 + else: + assert command == 0xc3 + if streaming and active.is_set(): + packet = [209, 175, 106, 85, 0, 254, 175, seq << 4] + crc = 0 + for value in packet[:-1]: + crc = table[crc] ^ (value >> 4) + crc = table[crc] ^ (value & 15) + packet[-1] |= table[table[crc] ^ seq] + os.write(master, bytes(packet[:3])) + os.write(master, bytes(packet[3:])) + seq = (seq + 1) % 16 + except Exception as exc: + failures.append(exc) + + worker = threading.Thread(target=device) + worker.start() + try: + assert lib.prepare_session(70, params) == 0 + # No raw samples: start must fail, and its worker must be joined. + assert lib.start_stream(1024, b'', 70, params) == 18 + active.set() + for _ in range(2): + assert lib.start_stream(1024, b'', 70, params) == 0 + assert lib.start_stream(1024, b'', 70, params) == 8 + time.sleep(0.05) + data = (ctypes.c_double * (13 * 1024))() + count = ctypes.c_int() + assert lib.get_current_board_data(1024, 0, data, ctypes.byref(count), 70, params) == 0 + assert count.value > 1 + expected = [1023, 512, 341, 682, 63, 17, 1, 0, 1, 0] + for channel, value in enumerate(expected, 1): + assert all(data[channel * count.value + i] == value for i in range(count.value)) + assert lib.stop_stream(70, params) == 0 + assert lib.stop_stream(70, params) == 11 + assert lib.release_session(70, params) == 0 + # Opening the port again after release must work. + assert lib.prepare_session(70, params) == 0 + assert lib.start_stream(1024, b'', 70, params) == 0 + finally: + lib.release_session(70, params) + stop.set() + worker.join() + os.close(master) + os.close(slave) + assert not failures, failures + assert commands[:2] == [0, 7] + print('BITalino serial lifecycle passed') + + +if __name__ == '__main__': + 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 e6b24ab11..44c764f86 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardIds.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardIds.java @@ -74,7 +74,8 @@ public enum BoardIds IRONBCI_32_BOARD(65), NEUROPAWN_KNIGHT_BOARD_IMU(66), MUSE_S_ATHENA_BOARD(67), - SHIMMER3_BOARD(68); + SHIMMER3_BOARD(68), + BITALINO_BOARD(70); 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 b1e05f60c..cdeeb5174 100644 --- a/julia_package/brainflow/src/board_shim.jl +++ b/julia_package/brainflow/src/board_shim.jl @@ -70,6 +70,7 @@ export BrainFlowInputParams NEUROPAWN_KNIGHT_BOARD_IMU = 66 MUSE_S_ATHENA_BOARD = 67 SHIMMER3_BOARD = 68 + BITALINO_BOARD = 70 end diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m index a30390909..81bb18311 100644 --- a/matlab_package/brainflow/BoardIds.m +++ b/matlab_package/brainflow/BoardIds.m @@ -68,5 +68,6 @@ NEUROPAWN_KNIGHT_BOARD_IMU(66) MUSE_S_ATHENA_BOARD(67) SHIMMER3_BOARD(68) + BITALINO_BOARD(70) end end diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index 5aabeaed5..21913de13 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -77,7 +77,8 @@ export enum BoardIds { IRONBCI_32_BOARD = 65, NEUROPAWN_KNIGHT_BOARD_IMU = 66, MUSE_S_ATHENA_BOARD = 67, - SHIMMER3_BOARD = 68 + SHIMMER3_BOARD = 68, + BITALINO_BOARD = 70 } export enum IpProtocolTypes { diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index e18211334..353bdab8c 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -83,6 +83,7 @@ class BoardIds(enum.IntEnum): NEUROPAWN_KNIGHT_BOARD_IMU = 66 #: MUSE_S_ATHENA_BOARD = 67 #: SHIMMER3_BOARD = 68 #: + BITALINO_BOARD = 70 #: class IpProtocolTypes(enum.IntEnum): diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index 37d78ddfa..69d925ee3 100644 --- a/rust_package/brainflow/src/ffi/constants.rs +++ b/rust_package/brainflow/src/ffi/constants.rs @@ -104,6 +104,7 @@ pub enum BoardIds { NeuropawnKnightBoardImu = 66, MuseSAthenaBoard = 67, Shimmer3Board = 68, + BitalinoBoard = 70, } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] diff --git a/src/board_controller/bitalino/bitalino.cpp b/src/board_controller/bitalino/bitalino.cpp new file mode 100644 index 000000000..68f4b619c --- /dev/null +++ b/src/board_controller/bitalino/bitalino.cpp @@ -0,0 +1,183 @@ +#include "bitalino.h" +#include "bitalino_parser.h" +#include "timestamp.h" + +#include + +BITalino::BITalino (struct BrainFlowInputParams params) + : Board ((int)BoardIds::BITALINO_BOARD, params), keep_alive (false), ready (false) +{ +} + +BITalino::~BITalino () +{ + skip_logs = true; + release_session (); +} + +bool BITalino::send_command (unsigned char command) +{ + // The firmware needs a gap between commands (also used by the vendor API). + std::this_thread::sleep_for (std::chrono::milliseconds (150)); + return serial->send_to_serial_port (&command, 1) == 1; +} + +bool BITalino::read_version () +{ + if (!send_command (0x07)) + return false; + const std::string prefix = "BITalino"; + std::string response; + auto deadline = std::chrono::steady_clock::now () + + std::chrono::seconds (params.timeout ? params.timeout : 5); + while (std::chrono::steady_clock::now () < deadline) + { + char byte; + int size = serial->read_from_serial_port (&byte, 1); + if (size < 0) + return false; + if (size == 0) + continue; + if (response.size () < prefix.size ()) + { + if (byte == prefix[response.size ()]) + response += byte; + else + response = (byte == 'B') ? "B" : ""; + } + else if (byte == '\n') + { + version = response; + return true; + } + else + { + response += byte; + if (response.size () > 256) + return false; + } + } + return false; +} + +int BITalino::prepare_session () +{ + if (serial) + return (int)BrainFlowExitCodes::STATUS_OK; + if (params.serial_port.empty () || (params.timeout < 0) || (params.timeout > 600) || + (!params.other_info.empty () && (params.other_info != "simulated"))) + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + serial.reset (Serial::create (params.serial_port.c_str (), this)); + if (serial->open_serial_port () != SerialExitCodes::OK) + { + serial.reset (); + return (int)BrainFlowExitCodes::UNABLE_TO_OPEN_PORT_ERROR; + } + if ((serial->set_serial_port_settings (100) != SerialExitCodes::OK) || + (serial->set_custom_baudrate (115200) != SerialExitCodes::OK)) + { + release_session (); + return (int)BrainFlowExitCodes::SET_PORT_ERROR; + } + if (!send_command (0x00) || !read_version ()) + { + release_session (); + return (int)BrainFlowExitCodes::INITIAL_MSG_ERROR; + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int BITalino::start_stream (int buffer_size, const char *streamer_params) +{ + if (!serial) + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + if (worker.joinable ()) + return (int)BrainFlowExitCodes::STREAM_ALREADY_RUN_ERROR; + int result = prepare_for_acquisition (buffer_size, streamer_params); + if (result != (int)BrainFlowExitCodes::STATUS_OK) + return result; + serial->flush_buffer (); + if (!send_command (0xc3) || !send_command (params.other_info == "simulated" ? 0xfe : 0xfd)) + return (int)BrainFlowExitCodes::BOARD_WRITE_ERROR; + ready = false; + keep_alive = true; + worker = std::thread (&BITalino::read_thread, this); + std::unique_lock lock (mutex); + bool success = + received.wait_for (lock, std::chrono::seconds (params.timeout ? params.timeout : 5), + [this] { return ready || !keep_alive; }); + success = success && ready; + lock.unlock (); + if (!success) + { + stop_stream (); + return (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +void BITalino::read_thread () +{ + bitalino::Parser parser; + while (keep_alive) + { + uint8_t byte = 0; + int size = serial->read_from_serial_port (&byte, 1); + if (size < 0) + break; + if (size == 0) + continue; + auto samples = parser.feed (byte); + double timestamp = get_timestamp (); + for (size_t i = 0; i < samples.size (); ++i) + { + double package[13] = {0}; + for (size_t channel = 0; channel < samples[i].size (); ++channel) + package[channel] = samples[i][channel]; + package[11] = timestamp - (samples.size () - 1 - i) / 1000.0; + push_package (package); + } + if (!samples.empty ()) + { + std::lock_guard lock (mutex); + ready = true; + received.notify_all (); + } + } + keep_alive = false; + received.notify_all (); +} + +int BITalino::stop_stream () +{ + if (!worker.joinable ()) + return (int)BrainFlowExitCodes::STREAM_THREAD_IS_NOT_RUNNING; + keep_alive = false; + worker.join (); + if (!send_command (0x00) || !read_version ()) + return (int)BrainFlowExitCodes::BOARD_WRITE_ERROR; + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int BITalino::release_session () +{ + if (worker.joinable ()) + stop_stream (); + if (serial) + { + serial->close_serial_port (); + serial.reset (); + } + free_packages (); + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int BITalino::config_board (std::string config, std::string &response) +{ + if (!serial) + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + if (config != "get_version") + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + response = version; + return (int)BrainFlowExitCodes::STATUS_OK; +} diff --git a/src/board_controller/bitalino/inc/bitalino.h b/src/board_controller/bitalino/inc/bitalino.h new file mode 100644 index 000000000..a5599a200 --- /dev/null +++ b/src/board_controller/bitalino/inc/bitalino.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "board.h" +#include "serial.h" + +class BITalino : public Board +{ + std::unique_ptr serial; + std::atomic keep_alive; + std::thread worker; + std::mutex mutex; + std::condition_variable received; + bool ready; + std::string version; + bool send_command (unsigned char command); + bool read_version (); + void read_thread (); + +public: + explicit BITalino (struct BrainFlowInputParams params); + ~BITalino () override; + int prepare_session () override; + int start_stream (int buffer_size, const char *streamer_params) override; + int stop_stream () override; + int release_session () override; + int config_board (std::string config, std::string &response) override; +}; diff --git a/src/board_controller/bitalino/inc/bitalino_parser.h b/src/board_controller/bitalino/inc/bitalino_parser.h new file mode 100644 index 000000000..c6b3d5b00 --- /dev/null +++ b/src/board_controller/bitalino/inc/bitalino_parser.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +// Six-channel BITalino live/simulated frame: 52 analog bits, four digital bits, +// CRC4 and a four-bit sequence number. Analog values are uncalibrated ADC counts. +namespace bitalino +{ + using Sample = std::array; + + inline bool valid (const uint8_t *data) + { + unsigned remainder = 0; + for (size_t byte = 0; byte < 8; ++byte) + { + unsigned value = (byte == 7) ? (data[byte] & 0xf0) : data[byte]; + for (int bit = 7; bit >= 0; --bit) + { + remainder = (remainder << 1) | ((value >> bit) & 1); + if (remainder & 0x10) + remainder ^= 0x13; // x^4 + x + 1 + } + } + return remainder == (data[7] & 15); + } + + inline Sample decode (const uint8_t *data) + { + uint64_t word = 0; + for (size_t i = 0; i < 8; ++i) + word |= uint64_t (data[i]) << (8 * i); + Sample sample = {{double (word >> 60)}}; + const unsigned shifts[] = {42, 32, 22, 12, 6, 0}; + for (size_t i = 0; i < 6; ++i) + sample[i + 1] = double ((word >> shifts[i]) & ((i < 4) ? 1023 : 63)); + for (size_t i = 0; i < 4; ++i) + sample[i + 7] = double ((word >> (55 - i)) & 1); + return sample; + } + + class Parser + { + std::vector buffer; + bool synchronized = false; + + public: + std::vector feed (uint8_t byte) + { + std::vector samples; + buffer.push_back (byte); + while (buffer.size () >= (synchronized ? 8u : 16u)) + { + bool ok = valid (buffer.data ()); + if (!synchronized) + ok = ok && valid (buffer.data () + 8) && + (((buffer[7] >> 4) + 1) % 16 == (buffer[15] >> 4)); + if (!ok) + { + synchronized = false; + buffer.erase (buffer.begin ()); + continue; + } + synchronized = true; + samples.push_back (decode (buffer.data ())); + buffer.erase (buffer.begin (), buffer.begin () + 8); + } + return samples; + } + }; +} diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index 612965994..72d5df699 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -17,6 +17,7 @@ #include "aavaa_v3.h" #include "ant_neuro.h" #include "biolistener.h" +#include "bitalino.h" #include "board.h" #include "board_controller.h" #include "board_info_getter.h" @@ -315,6 +316,9 @@ int prepare_session (int board_id, const char *json_brainflow_input_params) board = std::shared_ptr ( new KnightIMU ((int)BoardIds::NEUROPAWN_KNIGHT_BOARD_IMU, params)); break; + case BoardIds::BITALINO_BOARD: + board = std::shared_ptr (new BITalino (params)); + break; case BoardIds::SHIMMER3_BOARD: board = std::shared_ptr (new Shimmer3 (params)); break; diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index 9493e3f9a..915778088 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -1217,6 +1217,18 @@ BrainFlowBoards::BrainFlowBoards() {"battery_channel", 16}, {"other_channels", {17}} }; + brainflow_boards_json["boards"]["70"]["default"] = + { + {"name", "BITalino"}, + {"sampling_rate", 1000}, + {"package_num_channel", 0}, + {"analog_channels", {1, 2, 3, 4, 5, 6}}, + {"other_channels", {7, 8, 9, 10}}, + {"timestamp_channel", 11}, + {"marker_channel", 12}, + {"num_rows", 13} + }; + } BrainFlowBoards boards_struct; diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake index f3fb0d790..8a423d591 100644 --- a/src/board_controller/build.cmake +++ b/src/board_controller/build.cmake @@ -88,6 +88,7 @@ SET (BOARD_CONTROLLER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/knight_base.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/neuropawn/knight_imu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/biolistener.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/bitalino/bitalino.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/shimmer/shimmer3.cpp ) @@ -157,6 +158,7 @@ target_include_directories ( ${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 + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/bitalino/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/shimmer/inc ) diff --git a/src/tests/board_controller/bitalino_unittest.cpp b/src/tests/board_controller/bitalino_unittest.cpp new file mode 100644 index 000000000..0ae31d03e --- /dev/null +++ b/src/tests/board_controller/bitalino_unittest.cpp @@ -0,0 +1,61 @@ +#include "bitalino_parser.h" +#include + +namespace +{ + // Golden vectors generated with the vendor API's CRC lookup table. + const std::vector zero = {209, 175, 106, 85, 0, 254, 175, 15}; + const std::vector one = {209, 175, 106, 85, 0, 254, 175, 28}; + const std::vector fifteen = {209, 175, 106, 85, 0, 254, 175, 253}; + std::vector feed (bitalino::Parser &parser, const std::vector &data) + { + std::vector result; + for (auto byte : data) + { + auto samples = parser.feed (byte); + result.insert (result.end (), samples.begin (), samples.end ()); + } + return result; + } +} + +TEST (BITalino, SixChannelsDigitalAndCrc) +{ + ASSERT_TRUE (bitalino::valid (zero.data ())); + auto sample = bitalino::decode (zero.data ()); + bitalino::Sample expected = {{0, 1023, 512, 341, 682, 63, 17, 1, 0, 1, 0}}; + EXPECT_EQ (sample, expected); + for (size_t byte = 0; byte < zero.size (); ++byte) + for (int bit = 0; bit < 8; ++bit) + { + auto corrupt = zero; + corrupt[byte] ^= uint8_t (1 << bit); + EXPECT_FALSE (bitalino::valid (corrupt.data ())); + } +} + +TEST (BITalino, SynchronizesAcrossSequenceWrapAndFragmentation) +{ + bitalino::Parser parser; + EXPECT_TRUE (feed (parser, fifteen).empty ()); + auto samples = feed (parser, zero); + ASSERT_EQ (samples.size (), 2u); + EXPECT_EQ (samples[0][0], 15); + EXPECT_EQ (samples[1][0], 0); + samples = feed (parser, one); + ASSERT_EQ (samples.size (), 1u); + EXPECT_EQ (samples[0][0], 1); +} + +TEST (BITalino, RecoversFromLostBytesWithoutPublishingCorruptFrame) +{ + bitalino::Parser parser; + auto corrupt = zero; + corrupt[0] ^= 1; + EXPECT_TRUE (feed (parser, corrupt).empty ()); + EXPECT_TRUE (feed (parser, zero).empty ()); + auto samples = feed (parser, one); + ASSERT_EQ (samples.size (), 2u); + EXPECT_EQ (samples[0], bitalino::decode (zero.data ())); + EXPECT_EQ (samples[1], bitalino::decode (one.data ())); +} diff --git a/src/tests/build.cmake b/src/tests/build.cmake index 37af0e0fe..e80c4d9cc 100644 --- a/src/tests/build.cmake +++ b/src/tests/build.cmake @@ -42,6 +42,7 @@ SET (TESTS_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/multicast_server.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/socket_client_udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/timestamp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/tests/board_controller/bitalino_unittest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/tests/board_controller/brainalive_unittest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/tests/utils/bluetooth/socket_bluetooth_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/tests/utils/bluetooth/bluetooth_functions_unittest.cpp diff --git a/src/utils/inc/brainflow_constants.h b/src/utils/inc/brainflow_constants.h index 8b31b0df2..f0ff531ad 100644 --- a/src/utils/inc/brainflow_constants.h +++ b/src/utils/inc/brainflow_constants.h @@ -97,9 +97,10 @@ enum class BoardIds : int NEUROPAWN_KNIGHT_BOARD_IMU = 66, MUSE_S_ATHENA_BOARD = 67, SHIMMER3_BOARD = 68, + BITALINO_BOARD = 70, // use it to iterate FIRST = PLAYBACK_FILE_BOARD, - LAST = SHIMMER3_BOARD + LAST = BITALINO_BOARD }; enum class IpProtocolTypes : int diff --git a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift index 9e98fa9a0..3aeaa8b8e 100644 --- a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift +++ b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift @@ -66,6 +66,7 @@ public enum BoardIds: Int, CaseIterable, Sendable { case NEUROPAWN_KNIGHT_BOARD_IMU = 66 case MUSE_S_ATHENA_BOARD = 67 case SHIMMER3_BOARD = 68 + case BITALINO_BOARD = 70 public var code: Int { rawValue } } From 4c7bf76969f822364152f05152a21f34efd1a195 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 10 Nov 2026 01:03:41 +0100 Subject: [PATCH 2/2] Make protocol parser header independently includable --- src/board_controller/bitalino/inc/bitalino_parser.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/board_controller/bitalino/inc/bitalino_parser.h b/src/board_controller/bitalino/inc/bitalino_parser.h index c6b3d5b00..fc91364f6 100644 --- a/src/board_controller/bitalino/inc/bitalino_parser.h +++ b/src/board_controller/bitalino/inc/bitalino_parser.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include