From 5e70a703125996d654643d8184e10654364e4213 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 10 Nov 2026 00:30:42 +0100 Subject: [PATCH 1/2] Add NeuroSky ThinkGear serial acquisition with protocol tests --- .github/workflows/run_unix.yml | 3 + .../brainflow/board_controller_library.cs | 3 +- docs/SupportedBoards.rst | 42 ++++++ .../neurosky_serial_test.py | 71 +++++++++ .../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/board_controller.cpp | 4 + src/board_controller/brainflow_boards.cpp | 13 ++ src/board_controller/build.cmake | 2 + src/board_controller/neurosky/inc/neurosky.h | 30 ++++ .../neurosky/inc/thinkgear_parser.h | 123 ++++++++++++++++ src/board_controller/neurosky/neurosky.cpp | 137 ++++++++++++++++++ .../board_controller/neurosky_unittest.cpp | 88 +++++++++++ src/tests/build.cmake | 1 + src/utils/inc/brainflow_constants.h | 3 +- .../Sources/BrainFlow/BrainFlowEnums.swift | 1 + 20 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 emulator/brainflow_emulator/neurosky_serial_test.py create mode 100644 src/board_controller/neurosky/inc/neurosky.h create mode 100644 src/board_controller/neurosky/inc/thinkgear_parser.h create mode 100644 src/board_controller/neurosky/neurosky.cpp create mode 100644 src/tests/board_controller/neurosky_unittest.cpp diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index db6accc74..7dddbedea 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: NeuroSky serial emulator + if: runner.os == 'Linux' + run: python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/neurosky_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..96fa07af3 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, + NEUROSKY_BOARD = 69 }; diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst index 533d8fda3..0f3366260 100644 --- a/docs/SupportedBoards.rst +++ b/docs/SupportedBoards.rst @@ -1581,3 +1581,45 @@ Common sensor bits: - :code:`0x000008`, EXG2 24-bit - :code:`0x000004`, GSR, exposed as EDA - :code:`0x002000`, battery + +NeuroSky MindWave / TGAM +----------------------- + +``BoardIds.NEUROSKY_BOARD`` (69) reads the ThinkGear binary stream from a serial +port (USB or an operating-system-paired Bluetooth Classic RFCOMM port). Set +``serial_port`` to that port. The default baud rate is 57600; ``other_info`` may +be ``"57600"`` or ``"115200"``. ``timeout`` is the first-sample timeout in seconds +(default 5). The device must already emit 16-bit raw EEG at 512 Hz. +This driver does not configure Bluetooth pairing, use ThinkGear Connector, or +support BLE, TGEM's 128 Hz stream, 8-bit raw mode, or CardioChip. + +The EEG row is in microvolts using NeuroSky's documented TGAT/TGAM gain. +``other_channels`` contains poor-signal quality, attention, meditation, followed +by the eight ASIC band powers (delta, theta, low/high alpha, low/high beta, +low/mid gamma). These slower values are held until updated and are NaN until +first received. Band powers are device-relative values, not microvolts squared. +The package counter is generated by the host; timestamps are host receive times +(within-packet samples spaced at 512 Hz), not device-clock synchronization. + +.. 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.NEUROSKY_BOARD, params) + board.prepare_session() + try: + board.start_stream() + # Consume data with get_board_data() while recording. + board.stop_stream() + finally: + board.release_session() + +Validation covers published protocol fixtures, malformed-packet recovery and a +pseudo-terminal stream. Physical MindWave/TGAM and Bluetooth serial operation +still require validation by a device owner. + +Protocol: https://developer.neurosky.com/docs/doku.php?id=thinkgear_communications_protocol + +Voltage conversion: https://support.neurosky.com/kb/science/how-to-convert-raw-values-to-voltage diff --git a/emulator/brainflow_emulator/neurosky_serial_test.py b/emulator/brainflow_emulator/neurosky_serial_test.py new file mode 100644 index 000000000..af94e713c --- /dev/null +++ b/emulator/brainflow_emulator/neurosky_serial_test.py @@ -0,0 +1,71 @@ +"""Exercise the native driver with a POSIX pseudo-terminal, no device required. + +Usage: python3 neurosky_serial_test.py /path/to/libBoardController.so +""" +import ctypes +import json +import os +import pty +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() + + def device(): + # A raw value of 42; the checksum is independently specified. + packet = bytes([0xaa, 0xaa, 4, 0x80, 2, 0, 42, 0x53]) + while not stop.wait(0.002): + if active.is_set(): + os.write(master, packet[:3]) + os.write(master, packet[3:]) + + worker = threading.Thread(target=device) + worker.start() + try: + assert lib.prepare_session(69, params) == 0 + # No raw samples: start must fail, and its worker must be joined. + assert lib.start_stream(1024, b'', 69, params) == 18 + active.set() + for _ in range(2): + assert lib.start_stream(1024, b'', 69, params) == 0 + assert lib.start_stream(1024, b'', 69, params) == 8 + time.sleep(0.05) + data = (ctypes.c_double * (15 * 1024))() + count = ctypes.c_int() + assert lib.get_current_board_data(1024, 0, data, ctypes.byref(count), 69, params) == 0 + assert count.value > 1 + assert all(data[count.value + i] == 42 * 0.2197265625 for i in range(count.value)) + assert lib.stop_stream(69, params) == 0 + assert lib.stop_stream(69, params) == 11 + assert lib.release_session(69, params) == 0 + # Opening the port again after release must work. + assert lib.prepare_session(69, params) == 0 + assert lib.start_stream(1024, b'', 69, params) == 0 + finally: + lib.release_session(69, params) + stop.set() + worker.join() + os.close(master) + os.close(slave) + print('NeuroSky 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..5a86f6a12 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), + NEUROSKY_BOARD(69); 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..a6714d3c6 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 + NEUROSKY_BOARD = 69 end diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m index a30390909..2e0f39371 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) + NEUROSKY_BOARD(69) end end diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index 5aabeaed5..e7fe6c2d8 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, + NEUROSKY_BOARD = 69 } export enum IpProtocolTypes { diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index e18211334..c54594304 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 #: + NEUROSKY_BOARD = 69 #: class IpProtocolTypes(enum.IntEnum): diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index 37d78ddfa..47534f056 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, + NeuroskyBoard = 69, } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index 612965994..8e4e8f63d 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -49,6 +49,7 @@ #include "muse.h" #include "muse_athena.h" #include "muse_bled.h" +#include "neurosky.h" #include "notion_osc.h" #include "ntl_wifi.h" #include "pieeg_board.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::NEUROSKY_BOARD: + board = std::shared_ptr (new NeuroSky (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..0604fc6bd 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -1217,6 +1217,19 @@ BrainFlowBoards::BrainFlowBoards() {"battery_channel", 16}, {"other_channels", {17}} }; + brainflow_boards_json["boards"]["69"]["default"] = + { + {"name", "NeuroSky"}, + {"sampling_rate", 512}, + {"package_num_channel", 0}, + {"eeg_channels", {1}}, + {"eeg_names", "Fp1"}, + {"other_channels", {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}}, + {"timestamp_channel", 13}, + {"marker_channel", 14}, + {"num_rows", 15} + }; + } BrainFlowBoards boards_struct; diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake index f3fb0d790..65ca1122d 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/neurosky/neurosky.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/neurosky/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/shimmer/inc ) diff --git a/src/board_controller/neurosky/inc/neurosky.h b/src/board_controller/neurosky/inc/neurosky.h new file mode 100644 index 000000000..fdbb9fce4 --- /dev/null +++ b/src/board_controller/neurosky/inc/neurosky.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "board.h" +#include "serial.h" + +class NeuroSky : public Board +{ + std::unique_ptr serial; + std::atomic keep_alive; + std::thread worker; + std::mutex mutex; + std::condition_variable received; + bool ready; + void read_thread (); + +public: + explicit NeuroSky (struct BrainFlowInputParams params); + ~NeuroSky () 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/neurosky/inc/thinkgear_parser.h b/src/board_controller/neurosky/inc/thinkgear_parser.h new file mode 100644 index 000000000..294fb5558 --- /dev/null +++ b/src/board_controller/neurosky/inc/thinkgear_parser.h @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include +#include + +// ThinkGear binary protocol, TGAT/TGAM 16-bit EEG mode (512 Hz). +// https://developer.neurosky.com/docs/doku.php?id=thinkgear_communications_protocol +namespace thinkgear +{ + struct Sample + { + double eeg; + std::array auxiliary; // quality, attention, meditation, eight ASIC powers + }; + + class Parser + { + std::vector buffer; + std::array auxiliary; + + bool payload (const uint8_t *data, size_t size, std::vector &samples) + { + auto next = auxiliary; + std::vector pending; + size_t offset = 0; + while (offset < size) + { + bool extended = false; + while ((offset < size) && (data[offset] == 0x55)) + { + extended = true; + ++offset; + } + if (offset == size) + return false; + uint8_t code = data[offset++]; + size_t length = 1; + if (code >= 0x80) + { + if (offset == size) + return false; + length = data[offset++]; + } + if (length > size - offset) + return false; + if (!extended) + { + if ((code == 0x80) && (length != 2)) + return false; + if ((code == 0x83) && (length != 24)) + return false; + if (code == 0x80) + { + int raw = (int (data[offset]) << 8) | data[offset + 1]; + if (raw >= 32768) + raw -= 65536; + // NeuroSky's TGAT conversion: raw * (1.8 / 4096) / 2000 V. + pending.push_back ({raw * (1.8 / 4096.0 / 2000.0 * 1e6), next}); + } + else if (code == 0x02) + next[0] = data[offset]; + else if (code == 0x04) + next[1] = data[offset]; + else if (code == 0x05) + next[2] = data[offset]; + else if (code == 0x83) + { + for (size_t i = 0; i < 8; ++i) + next[i + 3] = (uint32_t (data[offset + i * 3]) << 16) | + (uint32_t (data[offset + i * 3 + 1]) << 8) | + data[offset + i * 3 + 2]; + } + } + offset += length; + } + auxiliary = next; + samples.insert (samples.end (), pending.begin (), pending.end ()); + return true; + } + + public: + Parser () + { + reset (); + } + + void reset () + { + buffer.clear (); + auxiliary.fill (std::numeric_limits::quiet_NaN ()); + } + + std::vector feed (uint8_t byte) + { + std::vector samples; + buffer.push_back (byte); + while (buffer.size () >= 3) + { + if ((buffer[0] != 0xaa) || (buffer[1] != 0xaa) || (buffer[2] > 169)) + { + buffer.erase (buffer.begin ()); + continue; + } + size_t length = buffer[2]; + if (buffer.size () < length + 4) + break; + uint8_t sum = 0; + for (size_t i = 3; i < length + 4; ++i) + sum = uint8_t (sum + buffer[i]); + if (sum != 0xff) + { + buffer.erase (buffer.begin ()); + continue; + } + payload (buffer.data () + 3, length, samples); + buffer.erase (buffer.begin (), buffer.begin () + length + 4); + } + return samples; + } + }; +} diff --git a/src/board_controller/neurosky/neurosky.cpp b/src/board_controller/neurosky/neurosky.cpp new file mode 100644 index 000000000..fbfe4c861 --- /dev/null +++ b/src/board_controller/neurosky/neurosky.cpp @@ -0,0 +1,137 @@ +#include "neurosky.h" +#include "thinkgear_parser.h" +#include "timestamp.h" + +#include + +NeuroSky::NeuroSky (struct BrainFlowInputParams params) + : Board ((int)BoardIds::NEUROSKY_BOARD, params), keep_alive (false), ready (false) +{ +} + +NeuroSky::~NeuroSky () +{ + skip_logs = true; + release_session (); +} + +int NeuroSky::prepare_session () +{ + if (serial) + return (int)BrainFlowExitCodes::STATUS_OK; + if (params.serial_port.empty () || (params.timeout < 0) || (params.timeout > 600)) + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + // A pre-paired serial/RFCOMM port is required. No proprietary SDK is loaded. + int baud = 57600; + if (!params.other_info.empty ()) + { + if (params.other_info == "115200") + baud = 115200; + else if (params.other_info != "57600") + 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 (baud) != SerialExitCodes::OK)) + { + serial->close_serial_port (); + serial.reset (); + return (int)BrainFlowExitCodes::SET_PORT_ERROR; + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int NeuroSky::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 (); + ready = false; + keep_alive = true; + worker = std::thread (&NeuroSky::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 NeuroSky::read_thread () +{ + thinkgear::Parser parser; + uint32_t counter = 0; + while (keep_alive) + { + uint8_t byte = 0; + // Read one byte: Windows serial timeouts also depend on requested length. + 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[15] = {0}; + package[0] = counter++; + package[1] = samples[i].eeg; + for (size_t channel = 0; channel < samples[i].auxiliary.size (); ++channel) + package[channel + 2] = samples[i].auxiliary[channel]; + package[13] = timestamp - (samples.size () - 1 - i) / 512.0; + push_package (package); + } + if (!samples.empty ()) + { + std::lock_guard lock (mutex); + ready = true; + received.notify_all (); + } + } + keep_alive = false; + received.notify_all (); +} + +int NeuroSky::stop_stream () +{ + if (!worker.joinable ()) + return (int)BrainFlowExitCodes::STREAM_THREAD_IS_NOT_RUNNING; + keep_alive = false; + worker.join (); + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int NeuroSky::release_session () +{ + if (worker.joinable ()) + stop_stream (); + if (serial) + { + serial->close_serial_port (); + serial.reset (); + } + free_packages (); + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int NeuroSky::config_board (std::string, std::string &) +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} diff --git a/src/tests/board_controller/neurosky_unittest.cpp b/src/tests/board_controller/neurosky_unittest.cpp new file mode 100644 index 000000000..4bf61c716 --- /dev/null +++ b/src/tests/board_controller/neurosky_unittest.cpp @@ -0,0 +1,88 @@ +#include "thinkgear_parser.h" +#include +#include + +namespace +{ + std::vector frame (std::vector payload) + { + uint8_t sum = 0; + for (auto byte : payload) + sum = uint8_t (sum + byte); + std::vector packet = {0xaa, 0xaa, uint8_t (payload.size ())}; + packet.insert (packet.end (), payload.begin (), payload.end ()); + packet.push_back (uint8_t (~sum)); + return packet; + } + std::vector feed (thinkgear::Parser &parser, std::vector bytes) + { + std::vector result; + for (auto byte : bytes) + { + auto samples = parser.feed (byte); + result.insert (result.end (), samples.begin (), samples.end ()); + } + return result; + } +} + +TEST (ThinkGear, PublishedMetadataPacketAndSignedRawSamples) +{ + thinkgear::Parser parser; + EXPECT_TRUE (feed (parser, {0xaa, 0xaa, 8, 2, 0x20, 1, 0x7e, 4, 0x12, 5, 0x60, 0xe3}).empty ()); + auto samples = feed (parser, {0xaa, 0xaa, 4, 0x80, 2, 0xff, 0xff, 0x7f}); + ASSERT_EQ (samples.size (), 1u); + EXPECT_DOUBLE_EQ (samples[0].eeg, -0.2197265625); + EXPECT_EQ (samples[0].auxiliary[0], 32); + EXPECT_EQ (samples[0].auxiliary[1], 18); + EXPECT_EQ (samples[0].auxiliary[2], 96); + EXPECT_TRUE (std::isnan (samples[0].auxiliary[3])); + samples = feed (parser, frame ({0x80, 2, 0x80, 0, 0x80, 2, 0x7f, 0xff})); + ASSERT_EQ (samples.size (), 2u); + EXPECT_DOUBLE_EQ (samples[0].eeg, -7200.0); + EXPECT_DOUBLE_EQ (samples[1].eeg, 32767 * 0.2197265625); +} + +TEST (ThinkGear, RecoversAfterNoiseChecksumErrorAndRepeatedSync) +{ + thinkgear::Parser parser; + auto bad = frame ({0x80, 2, 0, 1}); + bad.back () ^= 1; + EXPECT_TRUE (feed (parser, bad).empty ()); + EXPECT_TRUE (feed (parser, {0, 0xfe, 0xaa, 0xaa, 0xaa}).empty ()); + auto good = frame ({0x80, 2, 0, 2}); + for (size_t i = 0; i + 1 < good.size (); ++i) + EXPECT_TRUE (parser.feed (good[i]).empty ()); + auto samples = parser.feed (good.back ()); + ASSERT_EQ (samples.size (), 1u); + EXPECT_DOUBLE_EQ (samples[0].eeg, 0.439453125); +} + +TEST (ThinkGear, RejectsWholeMalformedPayloadAndSkipsExtendedRows) +{ + thinkgear::Parser parser; + EXPECT_TRUE (feed (parser, frame ({2, 200, 0x80, 2, 0, 1, 0x83, 24, 0})).empty ()); + auto samples = feed (parser, frame ({0x55, 2, 99, 0x90, 3, 1, 2, 3, 0x80, 2, 0, 1})); + ASSERT_EQ (samples.size (), 1u); + EXPECT_TRUE (std::isnan (samples[0].auxiliary[0])); + EXPECT_TRUE (feed (parser, frame ({0x80, 1, 0})).empty ()); + EXPECT_TRUE (feed (parser, frame ({0x55})).empty ()); + EXPECT_TRUE (feed (parser, frame ({0x90})).empty ()); +} + +TEST (ThinkGear, AsicPowersAndReset) +{ + thinkgear::Parser parser; + std::vector payload = {0x83, 24}; + for (int i = 0; i < 8; ++i) + payload.insert (payload.end (), {1, 2, uint8_t (i)}); + payload.insert (payload.end (), {0x80, 2, 0, 0}); + auto samples = feed (parser, frame (payload)); + ASSERT_EQ (samples.size (), 1u); + for (int i = 0; i < 8; ++i) + EXPECT_EQ (samples[0].auxiliary[i + 3], 66048 + i); + parser.reset (); + samples = feed (parser, frame ({0x80, 2, 0, 0})); + ASSERT_EQ (samples.size (), 1u); + EXPECT_TRUE (std::isnan (samples[0].auxiliary[3])); +} diff --git a/src/tests/build.cmake b/src/tests/build.cmake index 37af0e0fe..f811f5bf0 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/neurosky_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..61efa32b1 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, + NEUROSKY_BOARD = 69, // use it to iterate FIRST = PLAYBACK_FILE_BOARD, - LAST = SHIMMER3_BOARD + LAST = NEUROSKY_BOARD }; enum class IpProtocolTypes : int diff --git a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift index 9e98fa9a0..e5661b67e 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 NEUROSKY_BOARD = 69 public var code: Int { rawValue } } From 049ca5d6a4b83a0442e97e959aba3b7dd070492b Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 10 Nov 2026 01:03:22 +0100 Subject: [PATCH 2/2] Make protocol parser header independently includable --- src/board_controller/neurosky/inc/thinkgear_parser.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/board_controller/neurosky/inc/thinkgear_parser.h b/src/board_controller/neurosky/inc/thinkgear_parser.h index 294fb5558..3b8762a91 100644 --- a/src/board_controller/neurosky/inc/thinkgear_parser.h +++ b/src/board_controller/neurosky/inc/thinkgear_parser.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include