diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index db6accc74..cbcce7827 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: BrainVision RDA emulator + if: runner.os == 'Linux' + run: python3 $GITHUB_WORKSPACE/emulator/brainflow_emulator/brainvision_rda_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..f959c3f8b 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, + BRAINVISION_RDA_BOARD = 71 }; diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst index 533d8fda3..b3bdbd381 100644 --- a/docs/SupportedBoards.rst +++ b/docs/SupportedBoards.rst @@ -1581,3 +1581,61 @@ Common sensor bits: - :code:`0x000008`, EXG2 24-bit - :code:`0x000004`, GSR, exposed as EDA - :code:`0x002000`, battery + +BrainVision RDA (32 channels, 1000 Hz) +------------------------------------- + +``BoardIds.BRAINVISION_RDA_BOARD`` (71) receives the TCP DATA32 stream from +BrainVision Recorder's Remote Data Access interface. This is a Recorder gateway, +not a direct USB driver. Enable RDA, configure **exactly 32 channels at 1000 Hz**, +and start Recorder acquisition before calling ``start_stream``. Other channel +counts/rates and DATA16 are rejected. Set ``ip_address`` to the Recorder IPv4 +address; ``ip_port`` defaults to 51244. ``timeout`` is the first-sample timeout +in seconds (default 5). Preparation checks connectivity; each start reconnects +to obtain fresh metadata and discard samples queued before recording. + +Rows 1-32 follow Recorder channel order and use its per-channel resolution to +convert to microvolts. Use ``config_board("get_channel_names")`` after starting +to get the channel names as JSON. Configure EEG-only channels: this profile does +not classify arbitrary auxiliary channels supplied by Recorder. + +``package_num_channel`` contains the RDA block number, repeated for each sample +in that block. ``other_channels`` contains the RDA marker count at each sample +and the zero-based sample offset within its block. ``config_board("get_marker")`` +pops one marker as JSON (block, position, points, channel, type, description), +or returns ``{}`` when empty. Channel -1 means all channels. The queue retains +256 events; overflow drops the oldest event with a warning. Coincident markers +remain separate. Each name is limited to 512 bytes and the complete JSON name list to 4000 bytes. +The combined marker type/description is limited to 512 bytes so each event fits +standard binding buffers. Invalid UTF-8 is replaced when serializing JSON. +The normal BrainFlow marker row remains available for ``insert_marker``. + +Timestamps anchor the last sample of each block to host receipt and backdate +samples at 1000 Hz. They are not hardware timestamps or clock synchronization. +A Recorder STOP, malformed message, unsupported profile or lost connection ends +the reader. Call ``stop_stream`` and ``start_stream`` to reconnect after restoring +Recorder acquisition; there is no silent automatic reconnect. + +.. code-block:: python + + from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds + + params = BrainFlowInputParams() + params.ip_address = "127.0.0.1" + board = BoardShim(BoardIds.BRAINVISION_RDA_BOARD, params) + board.prepare_session() + try: + board.start_stream() + print(board.config_board("get_channel_names")) + # Consume get_board_data() and drain get_marker while recording. + board.stop_stream() + finally: + board.release_session() + +Validation uses protocol fixtures and a loopback RDA server, including fragmented +TCP reads, scaling, coincident markers, profile rejection, timeouts and restarting. +Physical Recorder/amplifier validation remains outstanding. + +Protocol references: +https://github.com/brain-products/LSL-BrainVisionRDA and +https://github.com/fieldtrip/fieldtrip/blob/master/realtime/src/buffer/src/rdaserver.c diff --git a/emulator/brainflow_emulator/brainvision_rda_test.py b/emulator/brainflow_emulator/brainvision_rda_test.py new file mode 100644 index 000000000..4fab087f4 --- /dev/null +++ b/emulator/brainflow_emulator/brainvision_rda_test.py @@ -0,0 +1,132 @@ +"""Hardware-free RDA session test. Pass the native BoardController library path.""" +import ctypes +import json +import socket +import struct +import sys +import threading +import time + +UUID = bytes.fromhex('8e45584396c9864caf4a98bbf6c91450') + + +def message(kind, body): + return UUID + struct.pack('= 2 + assert all(data[count.value + i] == -1.75 for i in range(count.value)) + assert 2 in data[33 * count.value:34 * count.value] + response = ctypes.create_string_buffer(8192) + length = ctypes.c_int() + assert lib.config_board(b'get_marker', response, ctypes.byref(length), 8192, 71, params) == 0 + event = json.loads(response.value) + assert event['description'] == 'S 12' and event['position'] == 1 + assert lib.config_board(b'get_channel_names', response, ctypes.byref(length), 8192, 71, params) == 0 + assert json.loads(response.value) == ['C%d' % i for i in range(32)] + assert lib.stop_stream(71, params) == 0 + for scenario, expected in [('encoding', 0), ('profile', 5), ('oversized', 5), ('silent', 18), ('stop', 0)]: + mode[0] = scenario + before = time.monotonic() + assert lib.start_stream(1024, b'', 71, params) == expected, scenario + assert time.monotonic() - before < 2.5, 'stop did not interrupt recv' + if scenario == 'encoding': + assert lib.config_board(b'get_marker', response, ctypes.byref(length), 8192, 71, params) == 0 + assert json.loads(response.value)['description'] == 'S\ufffd12' + if expected == 0: + assert lib.stop_stream(71, params) == 0 + assert lib.release_session(71, params) == 0 + mode[0] = 'valid' + assert lib.prepare_session(71, params) == 0 + assert lib.start_stream(1024, b'', 71, params) == 0 + finally: + lib.release_session(71, params) + stop.set() + listener.join() + for worker in workers: + worker.join() + server.close() + assert not failures, failures + print('RDA protocol and session 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..68c86cbc0 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), + BRAINVISION_RDA_BOARD(71); 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..2fe851a88 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 + BRAINVISION_RDA_BOARD = 71 end diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m index a30390909..c82885f60 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) + BRAINVISION_RDA_BOARD(71) end end diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index 5aabeaed5..de90a9580 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, + BRAINVISION_RDA_BOARD = 71 } export enum IpProtocolTypes { diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index e18211334..9e4c49881 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 #: + BRAINVISION_RDA_BOARD = 71 #: class IpProtocolTypes(enum.IntEnum): diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index 37d78ddfa..f00a1ca73 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, + BrainvisionRdaBoard = 71, } #[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..854503391 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -26,6 +26,7 @@ #include "brainflow_constants.h" #include "brainflow_input_params.h" #include "brainflow_version.h" +#include "brainvision_rda.h" #include "callibri_ecg.h" #include "callibri_eeg.h" #include "callibri_emg.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::BRAINVISION_RDA_BOARD: + board = std::shared_ptr (new BrainVisionRDA (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..5566b2612 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"]["71"]["default"] = + { + {"name", "BrainVisionRDA32"}, + {"sampling_rate", 1000}, + {"package_num_channel", 0}, + {"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, 24, 25, 26, 27, 28, 29, 30, 31, 32}}, + {"other_channels", {33, 34}}, + {"timestamp_channel", 35}, + {"marker_channel", 36}, + {"num_rows", 37} + }; + } BrainFlowBoards boards_struct; diff --git a/src/board_controller/brainvision/brainvision_rda.cpp b/src/board_controller/brainvision/brainvision_rda.cpp new file mode 100644 index 000000000..d696774b9 --- /dev/null +++ b/src/board_controller/brainvision/brainvision_rda.cpp @@ -0,0 +1,212 @@ +#include "brainvision_rda.h" +#include "timestamp.h" + +#include + +BrainVisionRDA::BrainVisionRDA (struct BrainFlowInputParams params) + : Board ((int)BoardIds::BRAINVISION_RDA_BOARD, params) + , keep_alive (false) + , stream_error ((int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR) + , initialized (false) + , ready (false) +{ +} + +BrainVisionRDA::~BrainVisionRDA () +{ + skip_logs = true; + release_session (); +} + +bool BrainVisionRDA::connect_socket () +{ + socket.reset ( + new SocketClientTCP (params.ip_address.c_str (), params.ip_port ? params.ip_port : 51244)); + if (socket->connect () != (int)SocketClientTCPReturnCodes::STATUS_OK) + { + socket.reset (); + return false; + } + return true; +} + +int BrainVisionRDA::prepare_session () +{ + if (initialized) + return (int)BrainFlowExitCodes::STATUS_OK; + struct in_addr address; + if ((params.ip_address.size () > 31) || + (inet_pton (AF_INET, params.ip_address.c_str (), &address) != 1) || (params.ip_port < 0) || + (params.ip_port > 65535) || (params.timeout < 0) || (params.timeout > 600)) + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + if (!connect_socket ()) + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + initialized = true; + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int BrainVisionRDA::start_stream (int buffer_size, const char *streamer_params) +{ + if (!initialized) + 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; + // Reconnect on every start to obtain fresh START metadata and discard old queued data. + socket.reset (); + if (!connect_socket ()) + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + ready = false; + channel_names.clear (); + markers.clear (); + stream_error = (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; + keep_alive = true; + worker = std::thread (&BrainVisionRDA::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) + { + int error = stream_error; + stop_stream (); + return error; + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +bool BrainVisionRDA::read_exact (void *data, size_t size) +{ + size_t offset = 0; + while (keep_alive && (offset < size)) + { + int count = socket->recv (static_cast (data) + offset, int (size - offset)); + if (count <= 0) + return false; + offset += size_t (count); + } + return offset == size; +} + +void BrainVisionRDA::read_thread () +{ + rda::Decoder decoder; + bool has_block = false; + uint32_t previous_block = 0; + while (keep_alive) + { + uint8_t header[24]; + uint32_t size, type; + if (!read_exact (header, sizeof (header)) || !rda::header (header, size, type)) + break; + std::vector body (size - sizeof (header)); + if (!read_exact (body.data (), body.size ())) + break; + if (type == 1) + { + if (!decoder.start (body) || + (json (decoder.names) + .dump (-1, ' ', false, json::error_handler_t::replace) + .size () > 4000)) + { + safe_logger (spdlog::level::err, + "RDA requires 32 channels at 1000 Hz with valid resolutions"); + break; + } + std::lock_guard lock (mutex); + channel_names = decoder.names; + has_block = false; + } + else if (type == 4) + { + rda::Block block; + if (!decoder.data32 (body, block)) + break; + if (has_block && (block.number != uint32_t (previous_block + 1))) + safe_logger (spdlog::level::warn, "RDA block discontinuity: {} -> {}", + previous_block, block.number); + has_block = true; + previous_block = block.number; + double timestamp = get_timestamp (); + for (size_t i = 0; i < block.samples.size (); ++i) + { + block.samples[i][35] = timestamp - (block.samples.size () - 1 - i) / 1000.0; + push_package (block.samples[i].data ()); + } + std::lock_guard lock (mutex); + for (const auto &marker : block.markers) + { + if (markers.size () == 256) + { + markers.pop_front (); + safe_logger ( + spdlog::level::warn, "RDA marker queue full; oldest event discarded"); + } + markers.push_back (marker); + } + ready = true; + received.notify_all (); + } + else if ((type == 3) || (type == 2)) + { + safe_logger ( + spdlog::level::info, "RDA stopped or unsupported DATA16 stream (type {})", type); + break; + } + // Recorder state, INFO, impedance and keep-alive messages do not contain EEG. + } + stream_error = (int)BrainFlowExitCodes::INCOMMING_MSG_ERROR; + keep_alive = false; + received.notify_all (); +} + +int BrainVisionRDA::stop_stream () +{ + if (!worker.joinable ()) + return (int)BrainFlowExitCodes::STREAM_THREAD_IS_NOT_RUNNING; + keep_alive = false; + socket->shutdown (); + worker.join (); + socket.reset (); + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int BrainVisionRDA::release_session () +{ + if (worker.joinable ()) + stop_stream (); + socket.reset (); + initialized = false; + free_packages (); + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int BrainVisionRDA::config_board (std::string config, std::string &response) +{ + if (!initialized) + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + std::lock_guard lock (mutex); + if (config == "get_channel_names") + response = json (channel_names).dump (-1, ' ', false, json::error_handler_t::replace); + else if (config == "get_marker") + { + if (markers.empty ()) + response = "{}"; + else + { + auto marker = markers.front (); + response = json ({{"block", marker.block}, {"position", marker.position}, + {"points", marker.points}, {"channel", marker.channel}, + {"type", marker.type}, {"description", marker.description}}) + .dump (-1, ' ', false, json::error_handler_t::replace); + markers.pop_front (); + } + } + else + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + return (int)BrainFlowExitCodes::STATUS_OK; +} diff --git a/src/board_controller/brainvision/inc/brainvision_rda.h b/src/board_controller/brainvision/inc/brainvision_rda.h new file mode 100644 index 000000000..b12e52eb6 --- /dev/null +++ b/src/board_controller/brainvision/inc/brainvision_rda.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "board.h" +#include "rda_protocol.h" +#include "socket_client_tcp.h" + +class BrainVisionRDA : public Board +{ + std::unique_ptr socket; + std::atomic keep_alive; + std::atomic stream_error; + std::thread worker; + std::mutex mutex; + std::condition_variable received; + bool initialized; + bool ready; + std::vector channel_names; + std::deque markers; + bool connect_socket (); + bool read_exact (void *data, size_t size); + void read_thread (); + +public: + explicit BrainVisionRDA (struct BrainFlowInputParams params); + ~BrainVisionRDA () 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/brainvision/inc/rda_protocol.h b/src/board_controller/brainvision/inc/rda_protocol.h new file mode 100644 index 000000000..e682b88f3 --- /dev/null +++ b/src/board_controller/brainvision/inc/rda_protocol.h @@ -0,0 +1,167 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace rda +{ + inline uint32_t u32 (const uint8_t *data) + { + return uint32_t (data[0]) | (uint32_t (data[1]) << 8) | (uint32_t (data[2]) << 16) | + (uint32_t (data[3]) << 24); + } + + inline double f64 (const uint8_t *data) + { + uint64_t bits = uint64_t (u32 (data)) | (uint64_t (u32 (data + 4)) << 32); + double value; + std::memcpy (&value, &bits, sizeof (value)); + return value; + } + + inline float f32 (const uint8_t *data) + { + uint32_t bits = u32 (data); + float value; + std::memcpy (&value, &bits, sizeof (value)); + return value; + } + + inline bool header (const uint8_t *data, uint32_t &size, uint32_t &type) + { + const uint8_t uuid[] = {0x8e, 0x45, 0x58, 0x43, 0x96, 0xc9, 0x86, 0x4c, 0xaf, 0x4a, 0x98, + 0xbb, 0xf6, 0xc9, 0x14, 0x50}; + size = u32 (data + 16); + type = u32 (data + 20); + return std::memcmp (uuid, data, sizeof (uuid)) == 0 && size >= 24 && + size <= 8 * 1024 * 1024; + } + + inline bool text ( + const std::vector &data, size_t &offset, size_t end, std::string &value) + { + size_t start = offset; + while ((offset < end) && data[offset] && (offset - start <= 512)) + ++offset; + if ((offset == end) || (offset - start > 512)) + return false; + value.assign (data.begin () + start, data.begin () + offset); + ++offset; + return true; + } + + struct Marker + { + uint32_t block; + uint32_t position; + uint32_t points; + int64_t channel; + std::string type; + std::string description; + }; + + struct Block + { + uint32_t number; + std::vector> samples; + std::vector markers; + }; + + class Decoder + { + bool configured = false; + std::array resolutions; + + public: + std::vector names; + + bool start (const std::vector &data) + { + configured = false; + names.clear (); + if ((data.size () < 268) || (u32 (data.data ()) != 32)) + return false; + double interval = f64 (data.data () + 4); + if (!std::isfinite (interval) || (std::abs (interval - 1000.0) > 1e-6)) + return false; + for (size_t i = 0; i < 32; ++i) + { + resolutions[i] = f64 (data.data () + 12 + 8 * i); + if (!std::isfinite (resolutions[i]) || (resolutions[i] <= 0)) + return false; + } + size_t offset = 268; + for (size_t i = 0; i < 32; ++i) + { + std::string name; + if (!text (data, offset, data.size (), name)) + return false; + names.push_back (name); + } + configured = offset == data.size (); + return configured; + } + + bool data32 (const std::vector &data, Block &result) const + { + if (!configured || (data.size () < 12)) + return false; + uint32_t points = u32 (data.data () + 4); + uint32_t marker_count = u32 (data.data () + 8); + if ((points == 0) || (points > (data.size () - 12) / 128)) + return false; + Block pending; + pending.number = u32 (data.data ()); + pending.samples.resize (points); + size_t offset = 12; + for (size_t sample = 0; sample < points; ++sample) + { + auto &row = pending.samples[sample]; + row.fill (0); + row[0] = pending.number; + row[34] = double (sample); + for (size_t channel = 0; channel < 32; ++channel, offset += 4) + row[channel + 1] = double (f32 (data.data () + offset)) * resolutions[channel]; + } + // At least 16 fixed bytes and two terminating NULs per marker. + if (marker_count > (data.size () - offset) / 18) + return false; + for (uint32_t i = 0; i < marker_count; ++i) + { + if (data.size () - offset < 18) + return false; + uint32_t size = u32 (data.data () + offset); + if ((size < 18) || (size > data.size () - offset)) + return false; + size_t end = offset + size; + Marker marker; + marker.block = pending.number; + marker.position = u32 (data.data () + offset + 4); + marker.points = u32 (data.data () + offset + 8); + uint32_t channel = u32 (data.data () + offset + 12); + marker.channel = (channel == 0xffffffff) ? -1 : int64_t (channel); + if ((marker.position >= points) || + ((marker.channel < -1) || (marker.channel >= 32))) + return false; + offset += 16; + if (!text (data, offset, end, marker.type) || + !text (data, offset, end, marker.description) || (offset != end) || + (marker.type.size () + marker.description.size () > 512)) + return false; + pending.samples[marker.position][33] += 1; + pending.markers.push_back (marker); + } + if (offset != data.size ()) + return false; + result = std::move (pending); + return true; + } + }; +} diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake index f3fb0d790..7a8a2c5bb 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/brainvision/brainvision_rda.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/brainvision/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/shimmer/inc ) diff --git a/src/tests/board_controller/brainvision_rda_unittest.cpp b/src/tests/board_controller/brainvision_rda_unittest.cpp new file mode 100644 index 000000000..72bb9874e --- /dev/null +++ b/src/tests/board_controller/brainvision_rda_unittest.cpp @@ -0,0 +1,135 @@ +#include "rda_protocol.h" +#include + +namespace +{ + void add32 (std::vector &data, uint32_t value) + { + for (int i = 0; i < 4; ++i) + data.push_back (uint8_t (value >> (8 * i))); + } + void add64 (std::vector &data, double value) + { + uint64_t bits; + std::memcpy (&bits, &value, sizeof (bits)); + add32 (data, uint32_t (bits)); + add32 (data, uint32_t (bits >> 32)); + } + std::vector start () + { + std::vector data; + add32 (data, 32); + add64 (data, 1000.0); + for (int i = 0; i < 32; ++i) + add64 (data, 0.5); + for (int i = 0; i < 32; ++i) + data.insert (data.end (), {'E', uint8_t ('A' + i), 0}); + return data; + } + std::vector block () + { + std::vector data; + add32 (data, 42); + add32 (data, 2); + add32 (data, 2); + for (int i = 0; i < 64; ++i) + add32 (data, 0xc0600000); // -3.5, IEEE-754 binary32 + for (int i = 0; i < 2; ++i) + { + add32 (data, 30); + add32 (data, 1); + add32 (data, 1); + add32 (data, 0xffffffff); + const std::string label = "Stimulus"; + data.insert (data.end (), label.begin (), label.end ()); + data.insert (data.end (), {0, 'S', ' ', '1', '2', 0}); + } + return data; + } +} + +TEST (BrainVisionRDA, ChecksUuidAndBoundsBeforeAllocation) +{ + std::vector data = {0x8e, 0x45, 0x58, 0x43, 0x96, 0xc9, 0x86, 0x4c, 0xaf, 0x4a, 0x98, + 0xbb, 0xf6, 0xc9, 0x14, 0x50}; + add32 (data, 24); + add32 (data, 3); + uint32_t size, type; + ASSERT_TRUE (rda::header (data.data (), size, type)); + EXPECT_EQ (size, 24u); + EXPECT_EQ (type, 3u); + data[0] ^= 1; + EXPECT_FALSE (rda::header (data.data (), size, type)); + data[0] ^= 1; + data[16] = 23; + EXPECT_FALSE (rda::header (data.data (), size, type)); + data[19] = 0xff; + EXPECT_FALSE (rda::header (data.data (), size, type)); +} + +TEST (BrainVisionRDA, ScalesDataAndPreservesCoincidentMarkers) +{ + rda::Decoder decoder; + rda::Block result; + EXPECT_FALSE (decoder.data32 (block (), result)); + ASSERT_TRUE (decoder.start (start ())); + ASSERT_EQ (decoder.names.size (), 32u); + EXPECT_EQ (decoder.names[0], "EA"); + ASSERT_TRUE (decoder.data32 (block (), result)); + ASSERT_EQ (result.samples.size (), 2u); + EXPECT_EQ (result.number, 42u); + EXPECT_DOUBLE_EQ (result.samples[0][1], -1.75); + EXPECT_EQ (result.samples[0][33], 0); + EXPECT_EQ (result.samples[1][33], 2); + EXPECT_EQ (result.samples[1][34], 1); + ASSERT_EQ (result.markers.size (), 2u); + EXPECT_EQ (result.markers[0].position, 1u); + EXPECT_EQ (result.markers[0].channel, -1); + EXPECT_EQ (result.markers[0].description, "S 12"); +} + +TEST (BrainVisionRDA, RejectsOtherProfilesAndInvalidMetadata) +{ + rda::Decoder decoder; + auto data = start (); + data[0] = 31; + EXPECT_FALSE (decoder.start (data)); + data = start (); + std::vector rate; + add64 (rate, 2000.0); + std::copy (rate.begin (), rate.end (), data.begin () + 4); + EXPECT_FALSE (decoder.start (data)); + data = start (); + std::fill (data.begin () + 12, data.begin () + 20, 0); + EXPECT_FALSE (decoder.start (data)); + data = start (); + data.pop_back (); + EXPECT_FALSE (decoder.start (data)); +} + +TEST (BrainVisionRDA, RejectsTruncatedAndMaliciousMarkersAtomically) +{ + rda::Decoder decoder; + ASSERT_TRUE (decoder.start (start ())); + rda::Block result; + result.number = 99; + auto data = block (); + for (size_t size = 0; size < data.size (); ++size) + { + EXPECT_FALSE ( + decoder.data32 (std::vector (data.begin (), data.begin () + size), result)); + EXPECT_EQ (result.number, 99u); + } + data[268] = 60; // First marker consumes the second: must reject before reading past the buffer. + EXPECT_FALSE (decoder.data32 (data, result)); + data = block (); + data[272] = 2; // position == sample count + EXPECT_FALSE (decoder.data32 (data, result)); + data = block (); + data[280] = 32; + data[281] = data[282] = data[283] = 0; + EXPECT_FALSE (decoder.data32 (data, result)); + data = block (); + data.push_back (0); + EXPECT_FALSE (decoder.data32 (data, result)); +} diff --git a/src/tests/build.cmake b/src/tests/build.cmake index 37af0e0fe..5facf23f7 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/brainvision_rda_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..bb28b8df8 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, + BRAINVISION_RDA_BOARD = 71, // use it to iterate FIRST = PLAYBACK_FILE_BOARD, - LAST = SHIMMER3_BOARD + LAST = BRAINVISION_RDA_BOARD }; enum class IpProtocolTypes : int diff --git a/src/utils/inc/socket_client_tcp.h b/src/utils/inc/socket_client_tcp.h index 282c9c93f..8da980535 100644 --- a/src/utils/inc/socket_client_tcp.h +++ b/src/utils/inc/socket_client_tcp.h @@ -35,6 +35,8 @@ class SocketClientTCP int connect (); int send (const char *data, int size); int recv (void *data, int size); + // Interrupt a blocked receive without closing/reusing its descriptor before the reader joins. + void shutdown (); void close (); int get_local_ip_addr (const char *local_ip); char *get_ip_addr () diff --git a/src/utils/socket_client_tcp.cpp b/src/utils/socket_client_tcp.cpp index e74258273..67a240969 100644 --- a/src/utils/socket_client_tcp.cpp +++ b/src/utils/socket_client_tcp.cpp @@ -2,6 +2,15 @@ #include "socket_client_tcp.h" +void SocketClientTCP::shutdown () +{ +#ifdef _WIN32 + ::shutdown (connect_socket, SD_BOTH); +#else + ::shutdown (connect_socket, SHUT_RDWR); +#endif +} + /////////////////////////////// /////////// WINDOWS /////////// ////////////////////////////// diff --git a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift index 9e98fa9a0..58c345d88 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 BRAINVISION_RDA_BOARD = 71 public var code: Int { rawValue } }