diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e4807f7b..6daf08d10 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ option (BUILD_OYMOTION_SDK "BUILD_OYMOTION_SDK" OFF) option (BUILD_SYNCHRONI_SDK "BUILD_SYNCHRONI_SDK" ON) option (BUILD_BLUETOOTH "BUILD_BLUETOOTH" OFF) option (BUILD_BLE "BUILD_BLE" OFF) +option (BUILD_ANT_EDX "BUILD_ANT_EDX" OFF) option (BUILD_ONNX "BUILD_ONNX" OFF) option (BUILD_TESTS "BUILD_TESTS" OFF) option (BUILD_PERIPHERY "BUILD_PERIPHERY" OFF) @@ -76,4 +77,4 @@ install ( EXPORT ${TARGETS_EXPORT_NAME} NAMESPACE brainflow:: DESTINATION ${CONFIG_INSTALL_DIR} -) \ No newline at end of file +) diff --git a/cpp_package/src/board_shim.cpp b/cpp_package/src/board_shim.cpp index 3ec1fc74c..f138ba924 100644 --- a/cpp_package/src/board_shim.cpp +++ b/cpp_package/src/board_shim.cpp @@ -602,4 +602,4 @@ std::string BoardShim::get_version () std::string verion_str (version, string_len); return verion_str; -} \ No newline at end of file +} diff --git a/csharp_package/brainflow/brainflow/board_controller_library.cs b/csharp_package/brainflow/brainflow/board_controller_library.cs index c363db037..c695cd788 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace brainflow @@ -18,7 +18,8 @@ public enum IpProtocolTypes { NO_IP_PROTOCOL = 0, UDP = 1, - TCP = 2 + TCP = 2, + EDX = 3 }; public enum BrainFlowPresets @@ -122,7 +123,21 @@ public enum BoardIds OB3000_24_CHANNELS_BOARD = 63, BIOLISTENER_BOARD = 64, IRONBCI_32_BOARD = 65, - NEUROPAWN_KNIGHT_BOARD_IMU = 66 + NEUROPAWN_KNIGHT_BOARD_IMU = 66, + ANT_NEURO_EE_410_EDX_BOARD = 68, + ANT_NEURO_EE_411_EDX_BOARD = 69, + ANT_NEURO_EE_430_EDX_BOARD = 70, + ANT_NEURO_EE_211_EDX_BOARD = 71, + ANT_NEURO_EE_212_EDX_BOARD = 72, + ANT_NEURO_EE_213_EDX_BOARD = 73, + ANT_NEURO_EE_214_EDX_BOARD = 74, + ANT_NEURO_EE_215_EDX_BOARD = 75, + ANT_NEURO_EE_221_EDX_BOARD = 76, + ANT_NEURO_EE_222_EDX_BOARD = 77, + ANT_NEURO_EE_223_EDX_BOARD = 78, + ANT_NEURO_EE_224_EDX_BOARD = 79, + ANT_NEURO_EE_225_EDX_BOARD = 80, + ANT_NEURO_EE_511_EDX_BOARD = 81 }; @@ -874,3 +889,4 @@ public static int release_all_sessions () } } } + diff --git a/csharp_package/brainflow/brainflow/board_shim.cs b/csharp_package/brainflow/brainflow/board_shim.cs index 24211f8f3..a01465ac4 100644 --- a/csharp_package/brainflow/brainflow/board_shim.cs +++ b/csharp_package/brainflow/brainflow/board_shim.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.IO; -using System.Runtime.Serialization.Json; using System.Text; namespace brainflow @@ -987,3 +986,4 @@ public int get_board_data_count (int preset = (int)BrainFlowPresets.DEFAULT_PRES } } } + diff --git a/docs/BuildBrainFlow.rst b/docs/BuildBrainFlow.rst index c7ca532cb..86bc93b4a 100644 --- a/docs/BuildBrainFlow.rst +++ b/docs/BuildBrainFlow.rst @@ -193,8 +193,8 @@ Windows ~~~~~~~~ - Install CMake>=3.16 you can install it from PYPI via pip or from `CMake website `_ -- Install Visual Studio 2019(preferred) or Visual Studio 2017. Other versions may work but not tested -- In VS installer make sure you selected "Visual C++ ATL support" +- Install Visual Studio with C++ build tools +- In Visual Studio installer make sure you selected "Visual C++ ATL support" - Build it as a standard CMake project, you don't need to set any options .. compound:: @@ -208,6 +208,18 @@ Windows # to get info about args and configure your build you can run python build.py --help +.. compound:: + + EDX profile on Windows (gRPC transport) example: :: + + # make sure gRPC and protobuf are available in your CMake toolchain, + # for example via vcpkg or another preinstalled toolchain package + + mkdir build-edx + cd build-edx + cmake -G "Visual Studio 17 2022" -A x64 -DBUILD_ANT_EDX=ON -DMSVC_RUNTIME=dynamic -DCMAKE_INSTALL_PREFIX=../installed-edx .. + cmake --build . --target install --config Release -j 2 --parallel 2 + Linux ~~~~~~ @@ -227,6 +239,23 @@ Linux # to get info about args and configure your build you can run python3 build.py --help +.. compound:: + + EDX profile on Linux example: :: + + # install grpc/protobuf development dependencies first + # protobuf-compiler-grpc provides grpc_cpp_plugin used by CMake + sudo apt-get update + sudo apt-get install -y libprotobuf-dev protobuf-compiler libgrpc++-dev protobuf-compiler-grpc + + mkdir build-edx + cd build-edx + cmake -DBUILD_ANT_EDX=ON -DBUILD_SYNCHRONI_SDK=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../installed-edx .. + make + make install + +For EDX board configuration details, see :ref:`ant-neuro-edx-label`. + MacOS ~~~~~~~ diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst index 5cafde9e3..42ffa121e 100644 --- a/docs/SupportedBoards.rst +++ b/docs/SupportedBoards.rst @@ -1148,6 +1148,136 @@ Available commands: For more information about Ant Neuro boards please refer to their User Manual. +Ant Neuro EDX +~~~~~~~~~~~~~~ + +EDX is a transport board that exposes ANT Neuro amplifiers through an external gRPC service. + +Use board id: + +- explicit self-describing EDX ids for known amplifiers: + + - :code:`BoardIds.ANT_NEURO_EE_410_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_411_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_430_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_211_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_212_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_213_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_214_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_215_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_221_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_222_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_223_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_224_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_225_EDX_BOARD` + - :code:`BoardIds.ANT_NEURO_EE_511_EDX_BOARD` + +Use one of the explicit EDX ids when you know the amplifier model. + +Required BrainFlowInputParams fields: + +- :code:`ip_address`, EDX service host (for example :code:`localhost`) +- :code:`ip_port`, EDX service port (for example :code:`3390`) + +Optional fields: + +- :code:`ip_protocol`, optional; :code:`IpProtocolTypes.EDX` is accepted for clarity +- :code:`timeout`, timeout for discovery and session operations, default is 15 sec + +Important notes: + +- Explicit EDX ids are self-describing and do not require :code:`master_board`. +- Available sampling rates and signal ranges are discovered from the amplifier at runtime via :code:`board.config_board("edx:get_capabilities")`. + +Available commands: + +- Get runtime capabilities: :code:`board.config_board("edx:get_capabilities")` +- Set sampling rate: :code:`board.config_board("sampling_rate:500")` +- Set reference range: :code:`board.config_board("reference_range:0.15")` +- Set bipolar range: :code:`board.config_board("bipolar_range:2.5")` +- Set impedance mode: :code:`board.config_board("impedance_mode:1")`, mode 0 or 1 + +Initialization example (Python): + +.. code-block:: python + + params = BrainFlowInputParams() + params.ip_address = "localhost" + params.ip_port = 3390 + params.ip_protocol = IpProtocolTypes.EDX + board = BoardShim(BoardIds.ANT_NEURO_EE_511_EDX_BOARD, params) + +Configuration example (Python): + +.. code-block:: python + + params = BrainFlowInputParams() + params.ip_address = "localhost" + params.ip_port = 3390 + params.ip_protocol = IpProtocolTypes.EDX + board = BoardShim(BoardIds.ANT_NEURO_EE_511_EDX_BOARD, params) + board.prepare_session() + print(board.config_board("edx:get_capabilities")) + board.config_board("sampling_rate:500") + board.config_board("reference_range:0.15") + board.config_board("bipolar_range:2.5") + board.start_stream() + +Initialization example (C++): + +.. code-block:: cpp + + BrainFlowInputParams params; + params.ip_address = "localhost"; + params.ip_port = 3390; + params.ip_protocol = (int)IpProtocolTypes::EDX; + BoardShim board ((int)BoardIds::ANT_NEURO_EE_511_EDX_BOARD, params); + +Configuration example (C++): + +.. code-block:: cpp + + BrainFlowInputParams params; + params.ip_address = "localhost"; + params.ip_port = 3390; + params.ip_protocol = (int)IpProtocolTypes::EDX; + + BoardShim board ((int)BoardIds::ANT_NEURO_EE_511_EDX_BOARD, params); + board.prepare_session (); + std::string caps = board.config_board ("edx:get_capabilities"); + board.config_board ("sampling_rate:500"); + board.config_board ("reference_range:0.15"); + board.config_board ("bipolar_range:2.5"); + board.start_stream (); + +Initialization example (Rust): + +.. code-block:: rust + + let params = BrainFlowInputParamsBuilder::default() + .ip_address("localhost") + .ip_port(3390) + .ip_protocol(IpProtocolTypes::Edx) + .build(); + let board = BoardShim::new(BoardIds::AntNeuroEe511EdxBoard, params)?; + +Configuration example (Rust): + +.. code-block:: rust + + let params = BrainFlowInputParamsBuilder::default() + .ip_address("localhost") + .ip_port(3390) + .ip_protocol(IpProtocolTypes::Edx) + .build(); + let board = BoardShim::new(BoardIds::AntNeuroEe511EdxBoard, params)?; + board.prepare_session()?; + let caps = board.config_board("edx:get_capabilities")?; + board.config_board("sampling_rate:500")?; + board.config_board("reference_range:0.15")?; + board.config_board("bipolar_range:2.5")?; + board.start_stream(45000, "")?; + Enophone --------- @@ -1560,4 +1690,4 @@ Supported platforms: - Windows - Linux - MacOS -- Devices like Raspberry Pi \ No newline at end of file +- Devices like Raspberry Pi diff --git a/java_package/brainflow/src/main/java/brainflow/BoardIds.java b/java_package/brainflow/src/main/java/brainflow/BoardIds.java index 97f3b5684..22f345dd8 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardIds.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardIds.java @@ -72,7 +72,21 @@ public enum BoardIds OB3000_24_CHANNELS_BOARD(63), BIOLISTENER_BOARD(64), IRONBCI_32_BOARD(65), - NEUROPAWN_KNIGHT_BOARD_IMU(66); + NEUROPAWN_KNIGHT_BOARD_IMU(66), + ANT_NEURO_EE_410_EDX_BOARD(68), + ANT_NEURO_EE_411_EDX_BOARD(69), + ANT_NEURO_EE_430_EDX_BOARD(70), + ANT_NEURO_EE_211_EDX_BOARD(71), + ANT_NEURO_EE_212_EDX_BOARD(72), + ANT_NEURO_EE_213_EDX_BOARD(73), + ANT_NEURO_EE_214_EDX_BOARD(74), + ANT_NEURO_EE_215_EDX_BOARD(75), + ANT_NEURO_EE_221_EDX_BOARD(76), + ANT_NEURO_EE_222_EDX_BOARD(77), + ANT_NEURO_EE_223_EDX_BOARD(78), + ANT_NEURO_EE_224_EDX_BOARD(79), + ANT_NEURO_EE_225_EDX_BOARD(80), + ANT_NEURO_EE_511_EDX_BOARD(81); private final int board_id; private static final Map bi_map = new HashMap (); @@ -106,3 +120,4 @@ public static BoardIds from_code (final int code) } } } + diff --git a/java_package/brainflow/src/main/java/brainflow/BoardShim.java b/java_package/brainflow/src/main/java/brainflow/BoardShim.java index 7603178ae..aacb931fa 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardShim.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardShim.java @@ -8,6 +8,7 @@ import org.apache.commons.lang3.SystemUtils; import com.google.gson.Gson; +import com.google.gson.JsonObject; import com.sun.jna.JNIEnv; import com.sun.jna.Library; import com.sun.jna.Native; @@ -18,7 +19,6 @@ @SuppressWarnings ("deprecation") public class BoardShim { - private interface DllInterface extends Library { int prepare_session (int board_id, String params); @@ -1303,7 +1303,8 @@ public BoardShim (int board_id, BrainFlowInputParams params) { if (params.get_master_board () == BoardIds.NO_BOARD.get_code ()) { - throw new BrainFlowError ("need to set master board attribute in BrainFlowInputParams", + throw new BrainFlowError ( + "need to set master board attribute in BrainFlowInputParams", BrainFlowExitCode.INVALID_ARGUMENTS_ERROR.get_code ()); } else { @@ -1328,7 +1329,8 @@ public BoardShim (BoardIds board_id, BrainFlowInputParams params) { if (params.get_master_board () == BoardIds.NO_BOARD.get_code ()) { - throw new BrainFlowError ("need to set master board attribute in BrainFlowInputParams", + throw new BrainFlowError ( + "need to set master board attribute in BrainFlowInputParams", BrainFlowExitCode.INVALID_ARGUMENTS_ERROR.get_code ()); } else { @@ -1640,3 +1642,4 @@ public double[][] get_board_data (int num_datapoints) throws BrainFlowError return get_board_data (num_datapoints, BrainFlowPresets.DEFAULT_PRESET); } } + diff --git a/java_package/brainflow/src/main/java/brainflow/IpProtocolTypes.java b/java_package/brainflow/src/main/java/brainflow/IpProtocolTypes.java index 956b3789b..c6dda0719 100644 --- a/java_package/brainflow/src/main/java/brainflow/IpProtocolTypes.java +++ b/java_package/brainflow/src/main/java/brainflow/IpProtocolTypes.java @@ -8,7 +8,8 @@ public enum IpProtocolTypes NO_IP_PROTOCOL (0), UDP (1), - TCP (2); + TCP (2), + EDX (3); private final int protocol; private static final Map ip_map = new HashMap (); diff --git a/julia_package/brainflow/src/board_shim.jl b/julia_package/brainflow/src/board_shim.jl index cbc9c4081..6b5ebfc7a 100644 --- a/julia_package/brainflow/src/board_shim.jl +++ b/julia_package/brainflow/src/board_shim.jl @@ -68,6 +68,20 @@ export BrainFlowInputParams BIOLISTENER_BOARD = 64 IRONBCI_32_BOARD = 65 NEUROPAWN_KNIGHT_BOARD_IMU = 66 + ANT_NEURO_EE_410_EDX_BOARD = 68 + ANT_NEURO_EE_411_EDX_BOARD = 69 + ANT_NEURO_EE_430_EDX_BOARD = 70 + ANT_NEURO_EE_211_EDX_BOARD = 71 + ANT_NEURO_EE_212_EDX_BOARD = 72 + ANT_NEURO_EE_213_EDX_BOARD = 73 + ANT_NEURO_EE_214_EDX_BOARD = 74 + ANT_NEURO_EE_215_EDX_BOARD = 75 + ANT_NEURO_EE_221_EDX_BOARD = 76 + ANT_NEURO_EE_222_EDX_BOARD = 77 + ANT_NEURO_EE_223_EDX_BOARD = 78 + ANT_NEURO_EE_224_EDX_BOARD = 79 + ANT_NEURO_EE_225_EDX_BOARD = 80 + ANT_NEURO_EE_511_EDX_BOARD = 81 end @@ -78,6 +92,7 @@ BoardIdType = Union{BoardIds, Integer} NO_IP_PROTOCOL = 0 UDP = 1 TCP = 2 + EDX = 3 end @@ -229,7 +244,11 @@ struct BoardShim function BoardShim(id::Integer, params::BrainFlowInputParams) master_id = id - if id == Integer(STREAMING_BOARD) || id == Integer(PLAYBACK_FILE_BOARD) + if (id == Integer(STREAMING_BOARD) || id == Integer(PLAYBACK_FILE_BOARD)) + if Integer(params.master_board) == Integer(NO_BOARD) + throw(BrainFlowError("master board id is required for streaming or playback boards", + Integer(INVALID_ARGUMENTS_ERROR))) + end master_id = Integer(params.master_board) end new(master_id, id, JSON.json(params)) @@ -335,4 +354,5 @@ end num_samples, Int32(preset), val, data_size, board_shim.board_id, board_shim.input_json) value = transpose(reshape(val[1:data_size[1] * num_rows], (data_size[1], num_rows))) return value -end \ No newline at end of file +end + diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m index 0234835f1..15d212577 100644 --- a/matlab_package/brainflow/BoardIds.m +++ b/matlab_package/brainflow/BoardIds.m @@ -66,5 +66,19 @@ BIOLISTENER_BOARD(64) IRONBCI_32_BOARD(65) NEUROPAWN_KNIGHT_BOARD_IMU(66) + ANT_NEURO_EE_410_EDX_BOARD(68) + ANT_NEURO_EE_411_EDX_BOARD(69) + ANT_NEURO_EE_430_EDX_BOARD(70) + ANT_NEURO_EE_211_EDX_BOARD(71) + ANT_NEURO_EE_212_EDX_BOARD(72) + ANT_NEURO_EE_213_EDX_BOARD(73) + ANT_NEURO_EE_214_EDX_BOARD(74) + ANT_NEURO_EE_215_EDX_BOARD(75) + ANT_NEURO_EE_221_EDX_BOARD(76) + ANT_NEURO_EE_222_EDX_BOARD(77) + ANT_NEURO_EE_223_EDX_BOARD(78) + ANT_NEURO_EE_224_EDX_BOARD(79) + ANT_NEURO_EE_225_EDX_BOARD(80) + ANT_NEURO_EE_511_EDX_BOARD(81) end -end \ No newline at end of file +end diff --git a/matlab_package/brainflow/BoardShim.m b/matlab_package/brainflow/BoardShim.m index 248e2aba3..c8ab78efa 100644 --- a/matlab_package/brainflow/BoardShim.m +++ b/matlab_package/brainflow/BoardShim.m @@ -500,3 +500,4 @@ function insert_marker(obj, value, preset) end end + diff --git a/matlab_package/brainflow/IpProtocolTypes.m b/matlab_package/brainflow/IpProtocolTypes.m index 6ab629d7a..1902bc496 100644 --- a/matlab_package/brainflow/IpProtocolTypes.m +++ b/matlab_package/brainflow/IpProtocolTypes.m @@ -4,5 +4,6 @@ NO_IP_PROTOCOL(0) UDP(1) TCP(2) + EDX(3) end -end \ No newline at end of file +end diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index cd1ba9bcc..6d0a835e6 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -74,13 +74,29 @@ export enum BoardIds { SYNCHRONI_UNO_1_CHANNELS_BOARD = 62, OB3000_24_CHANNELS_BOARD = 63, BIOLISTENER_BOARD = 64, - IRONBCI_32_BOARD = 65 + IRONBCI_32_BOARD = 65, + NEUROPAWN_KNIGHT_BOARD_IMU = 66, + ANT_NEURO_EE_410_EDX_BOARD = 68, + ANT_NEURO_EE_411_EDX_BOARD = 69, + ANT_NEURO_EE_430_EDX_BOARD = 70, + ANT_NEURO_EE_211_EDX_BOARD = 71, + ANT_NEURO_EE_212_EDX_BOARD = 72, + ANT_NEURO_EE_213_EDX_BOARD = 73, + ANT_NEURO_EE_214_EDX_BOARD = 74, + ANT_NEURO_EE_215_EDX_BOARD = 75, + ANT_NEURO_EE_221_EDX_BOARD = 76, + ANT_NEURO_EE_222_EDX_BOARD = 77, + ANT_NEURO_EE_223_EDX_BOARD = 78, + ANT_NEURO_EE_224_EDX_BOARD = 79, + ANT_NEURO_EE_225_EDX_BOARD = 80, + ANT_NEURO_EE_511_EDX_BOARD = 81 } export enum IpProtocolTypes { NO_IP_PROTOCOL = 0, UDP = 1, TCP = 2, + EDX = 3, } export enum BrainFlowPresets { @@ -267,3 +283,4 @@ export interface IBrainFlowModelParams { outputName: string; maxArraySize: number; } + diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index faf57da7e..571fc6978 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -81,6 +81,20 @@ class BoardIds(enum.IntEnum): BIOLISTENER_BOARD = 64 #: IRONBCI_32_BOARD = 65 #: NEUROPAWN_KNIGHT_BOARD_IMU = 66 #: + ANT_NEURO_EE_410_EDX_BOARD = 68 #: + ANT_NEURO_EE_411_EDX_BOARD = 69 #: + ANT_NEURO_EE_430_EDX_BOARD = 70 #: + ANT_NEURO_EE_211_EDX_BOARD = 71 #: + ANT_NEURO_EE_212_EDX_BOARD = 72 #: + ANT_NEURO_EE_213_EDX_BOARD = 73 #: + ANT_NEURO_EE_214_EDX_BOARD = 74 #: + ANT_NEURO_EE_215_EDX_BOARD = 75 #: + ANT_NEURO_EE_221_EDX_BOARD = 76 #: + ANT_NEURO_EE_222_EDX_BOARD = 77 #: + ANT_NEURO_EE_223_EDX_BOARD = 78 #: + ANT_NEURO_EE_224_EDX_BOARD = 79 #: + ANT_NEURO_EE_225_EDX_BOARD = 80 #: + ANT_NEURO_EE_511_EDX_BOARD = 81 #: class IpProtocolTypes(enum.IntEnum): @@ -89,6 +103,7 @@ class IpProtocolTypes(enum.IntEnum): NO_IP_PROTOCOL = 0 #: UDP = 1 #: TCP = 2 #: + EDX = 3 #: class BrainFlowPresets(enum.IntEnum): @@ -581,12 +596,12 @@ def __init__(self, board_id: int, input_params: BrainFlowInputParams) -> None: except BaseException: self.input_json = input_params.to_json() self.board_id = board_id - # we need it for streaming board if board_id == BoardIds.STREAMING_BOARD.value or board_id == BoardIds.PLAYBACK_FILE_BOARD.value: if input_params.master_board != BoardIds.NO_BOARD: self._master_board_id = input_params.master_board else: - raise BrainFlowError('you need set master board id in BrainFlowInputParams', + raise BrainFlowError( + 'you need set master board id in BrainFlowInputParams', BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.value) else: self._master_board_id = self.board_id @@ -1420,3 +1435,5 @@ def config_board_with_bytes(self, bytes_to_send) -> None: res = BoardControllerDLL.get_instance().config_board_with_bytes(bytes_to_send, len(bytes_to_send), self.board_id, self.input_json) if res != BrainFlowExitCodes.STATUS_OK.value: raise BrainFlowError('unable to config board', res) + + diff --git a/python_package/examples/tests/edx_full_lifecycle.py b/python_package/examples/tests/edx_full_lifecycle.py new file mode 100644 index 000000000..81d8124e5 --- /dev/null +++ b/python_package/examples/tests/edx_full_lifecycle.py @@ -0,0 +1,263 @@ +""" +ANT Neuro EDX: full device lifecycle test. + +Exercises the complete BrainFlow API surface for a direct EDX board id +in a single sequence: connect, EEG stream, trigger output, marker check, +impedance mode, mode switch recovery, stop, release, and reconnect probe. + +Requires a real ANT Neuro device and the EDX gRPC server running. + +Usage: + python python_package/examples/tests/test_edx_full_lifecycle.py + python python_package/examples/tests/test_edx_full_lifecycle.py --ip 192.168.1.100 --port 3390 + python python_package/examples/tests/test_edx_full_lifecycle.py --board-id 81 --verbose +""" + +import argparse +import json +import sys +import time +from collections import Counter + +import numpy as np +from brainflow.board_shim import BoardShim, BrainFlowInputParams, IpProtocolTypes + + +def parse_args(): + p = argparse.ArgumentParser(description="ANT EDX full lifecycle test") + p.add_argument("--ip", default="localhost", help="EDX gRPC host (default: localhost)") + p.add_argument("--port", type=int, default=3390, help="EDX gRPC port (default: 3390)") + p.add_argument("--board-id", type=int, default=81, help="BrainFlow board id (default: 81 = EE511 EDX)") + p.add_argument("--timeout", type=int, default=5, help="BrainFlow timeout seconds (default: 5)") + p.add_argument("--verbose", action="store_true", help="Enable BrainFlow debug logging") + return p.parse_args() + + +def make_params(args): + params = BrainFlowInputParams() + params.ip_address = args.ip + params.ip_port = args.port + params.ip_protocol = IpProtocolTypes.EDX.value + params.timeout = args.timeout + return params + + +class LifecycleTest: + def __init__(self, args): + self.args = args + self.params = make_params(args) + self.board = None + self.data_board = args.board_id + self.passed = [] + self.failed = [] + + def step(self, name, fn): + print(f"\n{'=' * 60}") + print(f" STEP: {name}") + print(f"{'=' * 60}") + try: + fn() + self.passed.append(name) + print(" -> PASS") + except Exception as e: + self.failed.append((name, str(e))) + print(f" -> FAIL: {e}") + + def run(self): + self.step("1. prepare_session (connect)", self.test_connect) + self.step("2. start_stream (EEG)", self.test_start_stream) + self.step("3. get_board_data (valid EEG frames)", self.test_eeg_data) + self.step("4. trigger config + start", self.test_trigger_send) + self.step("5. verify trigger markers in data", self.test_trigger_receive) + self.step("6. impedance_mode:1 (switch to impedance)", self.test_impedance_on) + self.step("7. get_board_data (valid impedance values)", self.test_impedance_data) + self.step("8. impedance_mode:0 (switch back to EEG)", self.test_impedance_off) + self.step("9. get_board_data (EEG resumes)", self.test_eeg_resumes) + self.step("10. stop_stream", self.test_stop_stream) + self.step("11. release_session (dispose)", self.test_release) + self.step("12. reconnect probe (amp released)", self.test_reconnect_probe) + self.print_summary() + + def test_connect(self): + self.board = BoardShim(self.args.board_id, self.params) + self.board.prepare_session() + resp = self.board.config_board("edx:get_capabilities") + caps = json.loads(resp) + model = caps.get("selected_model", "unknown") + n_ch = len(caps.get("active_channels", [])) + print(f" Device: {model}, {n_ch} active channels") + assert n_ch > 0, "No active channels reported" + + def test_start_stream(self): + self.board.start_stream() + time.sleep(1.0) + print(" Streaming started, waited 1s for stabilization") + + def test_eeg_data(self): + self.board.get_board_data() + time.sleep(1.0) + data = self.board.get_board_data() + n_samples = data.shape[1] + print(f" Got {n_samples} samples, shape={data.shape}") + assert n_samples > 0, "No EEG samples received" + + eeg_channels = BoardShim.get_eeg_channels(self.data_board) + print(f" EEG channels: {eeg_channels[:5]}... ({len(eeg_channels)} total)") + eeg_data = data[eeg_channels, :] + nonzero_fraction = np.count_nonzero(eeg_data) / eeg_data.size + print(f" Non-zero fraction: {nonzero_fraction:.3f}") + assert nonzero_fraction > 0.5, f"EEG data mostly zeros ({nonzero_fraction:.3f})" + + def test_trigger_send(self): + config_cmd = "edx:trigger_config:0,50.0,10.0,5,1.0,3" + resp = self.board.config_board(config_cmd) + resp_obj = json.loads(resp) + assert resp_obj.get("status") == "ok", f"trigger_config failed: {resp}" + print(f" Trigger configured: {resp}") + + start_cmd = "edx:trigger_start:0" + resp = self.board.config_board(start_cmd) + resp_obj = json.loads(resp) + assert resp_obj.get("status") == "ok", f"trigger_start failed: {resp}" + print(f" Trigger started: {resp}") + + def test_trigger_receive(self): + marker_ch = BoardShim.get_marker_channel(self.data_board) + print(f" Marker channel index: {marker_ch}") + + marker_counts = Counter() + for i in range(6): + time.sleep(1.0) + data = self.board.get_board_data() + if data.shape[1] > 0: + markers = data[marker_ch] + for v in markers: + if v != 0.0: + marker_counts[int(v)] += 1 + print(f" [{i + 1}s] +{data.shape[1]} samples, markers: {dict(marker_counts)}") + + total_markers = sum(marker_counts.values()) + print(f" Total non-zero markers: {total_markers}") + if total_markers == 0: + print(" [WARN] No markers observed — use trigger loopback cable to verify") + + def test_impedance_on(self): + resp = self.board.config_board("impedance_mode:1") + print(f" Response: {resp}") + time.sleep(2.0) + print(" Waited 2s for impedance mode transition") + + def test_impedance_data(self): + self.board.get_board_data() + time.sleep(2.0) + data = self.board.get_board_data() + n_samples = data.shape[1] + print(f" Got {n_samples} samples in impedance mode, shape={data.shape}") + assert n_samples > 0, "No impedance samples received" + + descr = BoardShim.get_board_descr(self.args.board_id) + res_ch = descr.get("resistance_channels", []) + ref_res_ch = descr.get("ref_resistance_channels", []) + gnd_res_ch = descr.get("gnd_resistance_channels", []) + print(f" Resistance channels: {len(res_ch)} electrode, {len(ref_res_ch)} ref, {len(gnd_res_ch)} gnd") + + if res_ch: + res_data = data[res_ch, :] + mean_vals = np.mean(res_data, axis=1) + valid = np.sum(mean_vals > 0) + print(f" Mean resistance values (first 5): {mean_vals[:5]}") + print(f" Channels with positive resistance: {valid}/{len(res_ch)}") + assert valid > 0, "No channels have positive resistance values" + + def test_impedance_off(self): + resp = self.board.config_board("impedance_mode:0") + print(f" Response: {resp}") + time.sleep(2.0) + print(" Waited 2s for EEG mode transition") + + def test_eeg_resumes(self): + self.board.get_board_data() + time.sleep(1.0) + data = self.board.get_board_data() + n_samples = data.shape[1] + print(f" Got {n_samples} samples after returning to EEG mode") + assert n_samples > 0, "No EEG samples after impedance->EEG transition" + + eeg_channels = BoardShim.get_eeg_channels(self.data_board) + eeg_data = data[eeg_channels, :] + nonzero_fraction = np.count_nonzero(eeg_data) / eeg_data.size + print(f" Non-zero fraction: {nonzero_fraction:.3f}") + assert nonzero_fraction > 0.5, f"EEG data mostly zeros after impedance ({nonzero_fraction:.3f})" + + def test_stop_stream(self): + t0 = time.time() + self.board.stop_stream() + dt = time.time() - t0 + print(f" stop_stream completed in {dt:.3f}s") + + def test_release(self): + t0 = time.time() + self.board.release_session() + dt = time.time() - t0 + print(f" release_session completed in {dt:.3f}s") + self.board = None + + def test_reconnect_probe(self): + time.sleep(1.0) + probe = BoardShim(self.args.board_id, self.params) + t0 = time.time() + probe.prepare_session() + dt = time.time() - t0 + print(f" Reconnected in {dt:.3f}s (no 'Amplifier in use' error)") + + resp = probe.config_board("edx:get_capabilities") + caps = json.loads(resp) + print(f" Device: {caps.get('selected_model', '?')}, channels: {len(caps.get('active_channels', []))}") + + probe.release_session() + print(" Probe session released cleanly") + + def print_summary(self): + print(f"\n{'=' * 60}") + print("FULL LIFECYCLE TEST SUMMARY") + print(f"{'=' * 60}") + print(f" Passed: {len(self.passed)}/{len(self.passed) + len(self.failed)}") + for name in self.passed: + print(f" [PASS] {name}") + for name, err in self.failed: + print(f" [FAIL] {name}: {err}") + print(f"{'=' * 60}") + + if self.failed: + print("\nRESULT: FAIL") + sys.exit(1) + else: + print("\nRESULT: ALL PASSED") + + +def main(): + args = parse_args() + if args.verbose: + BoardShim.enable_dev_board_logger() + else: + BoardShim.enable_board_logger() + + test = LifecycleTest(args) + try: + test.run() + except KeyboardInterrupt: + print("\n[interrupted]") + if test.board: + try: + test.board.stop_stream() + except Exception: + pass + try: + test.board.release_session() + except Exception: + pass + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python_package/examples/tests/edx_impedance.py b/python_package/examples/tests/edx_impedance.py new file mode 100644 index 000000000..999d0a86d --- /dev/null +++ b/python_package/examples/tests/edx_impedance.py @@ -0,0 +1,124 @@ +""" +ANT Neuro EDX: impedance measurement test. + +Verifies that BrainFlow impedance mode produces valid data on +resistance_channels, ref_resistance_channels, and gnd_resistance_channels +for a direct EDX board id. + +Flow: connect -> start EEG -> switch to impedance -> poll resistance +values -> switch back to EEG -> verify EEG resumes -> cleanup. + +Requires a real ANT Neuro device and the EDX gRPC server running. + +Usage: + python python_package/examples/tests/test_edx_impedance.py + python python_package/examples/tests/test_edx_impedance.py --duration 5 --verbose +""" + +import argparse +import sys +import time + +from brainflow.board_shim import BoardShim, BrainFlowInputParams, IpProtocolTypes + + +def main(): + p = argparse.ArgumentParser(description="BrainFlow impedance data test") + p.add_argument("--ip", default="localhost") + p.add_argument("--port", type=int, default=3390) + p.add_argument("--board-id", type=int, default=81) + p.add_argument("--duration", type=float, default=3.0) + p.add_argument("--verbose", action="store_true") + args = p.parse_args() + + if args.verbose: + BoardShim.enable_dev_board_logger() + else: + BoardShim.enable_board_logger() + + resistance_ch = BoardShim.get_resistance_channels(args.board_id) + ref_resistance_ch = [] + gnd_resistance_ch = [] + try: + descr = BoardShim.get_board_descr(args.board_id) + ref_resistance_ch = descr.get("ref_resistance_channels", []) + gnd_resistance_ch = descr.get("gnd_resistance_channels", []) + except Exception: + pass + + print(f"resistance_channels: {resistance_ch}") + print(f"ref_resistance_channels: {ref_resistance_ch}") + print(f"gnd_resistance_channels: {gnd_resistance_ch}") + + all_rows = list(resistance_ch) + list(ref_resistance_ch) + list(gnd_resistance_ch) + if not all_rows: + print("[FAIL] No resistance channels defined for this board") + sys.exit(1) + + params = BrainFlowInputParams() + params.ip_address = args.ip + params.ip_port = args.port + params.ip_protocol = IpProtocolTypes.EDX.value + params.timeout = 5 + + board = BoardShim(args.board_id, params) + board.prepare_session() + print("[ok] session prepared") + + board.start_stream() + print("[ok] streaming started (EEG mode)") + time.sleep(1.0) + + print("\n[switch] config_board('impedance_mode:1')") + resp = board.config_board("impedance_mode:1") + print(f"[switch] response: {resp}") + + print(f"\n[collect] polling impedance for {args.duration}s ...") + total = 0 + nonzero_count = 0 + last_values = None + + t_start = time.time() + while time.time() - t_start < args.duration: + time.sleep(0.5) + data = board.get_board_data() + n = data.shape[1] + total += n + if n > 0: + last_col = data[:, -1] + values = {f"row{r}": last_col[r] for r in all_rows if r < len(last_col)} + nonzero = sum(1 for v in values.values() if v != 0.0) + nonzero_count += nonzero + last_values = values + print(f" +{n} samples, resistance values: {values}") + else: + print(" +0 samples") + + print("\n[switch] config_board('impedance_mode:0')") + resp = board.config_board("impedance_mode:0") + print(f"[switch] response: {resp}") + time.sleep(0.5) + + data = board.get_board_data() + print(f"[verify] EEG samples after mode switch: {data.shape[1]}") + + board.stop_stream() + board.release_session() + print("[ok] cleaned up") + + print(f"\n{'=' * 50}") + print("IMPEDANCE DATA TEST") + print(f"{'=' * 50}") + print(f" Total samples: {total}") + print(f" Nonzero readings: {nonzero_count}") + print(f" Last values: {last_values}") + ok = total > 0 and nonzero_count > 0 + print(f" Result: {'OK' if ok else 'FAIL - no impedance data'}") + print(f"{'=' * 50}") + + if not ok: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python_package/examples/tests/edx_mode_transitions.py b/python_package/examples/tests/edx_mode_transitions.py new file mode 100644 index 000000000..524240ffc --- /dev/null +++ b/python_package/examples/tests/edx_mode_transitions.py @@ -0,0 +1,125 @@ +""" +ANT Neuro EDX: standard API mode transition test. + +Documents actual behavior of the standard BrainFlow API for mode switching. +No custom edx: commands -- only config_board("impedance_mode:X"), +start_stream(), stop_stream(), release_session(). + +Usage: + python python_package/examples/tests/test_mode_transitions.py + python python_package/examples/tests/test_mode_transitions.py --board-id 81 --verbose +""" + +import argparse +import time + +from brainflow.board_shim import BoardShim, BrainFlowInputParams, IpProtocolTypes + + +def make_board(args): + params = BrainFlowInputParams() + params.ip_address = args.ip + params.ip_port = args.port + params.ip_protocol = IpProtocolTypes.EDX.value + params.timeout = args.timeout + return BoardShim(args.board_id, params) + + +def step(name): + print(f"\n--- {name} ---") + + +def main(): + p = argparse.ArgumentParser(description="BrainFlow standard mode transition test") + p.add_argument("--ip", default="localhost") + p.add_argument("--port", type=int, default=3390) + p.add_argument("--board-id", type=int, default=81) + p.add_argument("--timeout", type=int, default=15) + p.add_argument("--verbose", action="store_true") + args = p.parse_args() + + if args.verbose: + BoardShim.enable_dev_board_logger() + else: + BoardShim.enable_board_logger() + + board = make_board(args) + + step("1. prepare_session (connect)") + board.prepare_session() + print(" OK") + + step("2. start_stream (EEG)") + board.start_stream() + time.sleep(1.0) + data = board.get_board_data() + print(f" EEG samples: {data.shape[1]}") + assert data.shape[1] > 0, "No EEG data" + + step("3. config_board('impedance_mode:1') while streaming") + t0 = time.time() + resp = board.config_board("impedance_mode:1") + dt = time.time() - t0 + print(f" Response: {resp}") + print(f" Took: {dt:.3f}s") + time.sleep(2.0) + board.get_board_data() + time.sleep(1.0) + data = board.get_board_data() + print(f" Impedance samples: {data.shape[1]}") + + step("4. config_board('impedance_mode:0') while streaming") + t0 = time.time() + resp = board.config_board("impedance_mode:0") + dt = time.time() - t0 + print(f" Response: {resp}") + print(f" Took: {dt:.3f}s") + time.sleep(1.0) + board.get_board_data() + time.sleep(1.0) + data = board.get_board_data() + print(f" EEG samples after switch back: {data.shape[1]}") + assert data.shape[1] > 0, "No EEG data after impedance->EEG" + + step("5. stop_stream (goes to idle)") + t0 = time.time() + board.stop_stream() + dt = time.time() - t0 + print(f" Took: {dt:.3f}s") + + step("6. start_stream again after stop") + t0 = time.time() + board.start_stream() + dt = time.time() - t0 + print(f" Took: {dt:.3f}s") + time.sleep(1.0) + data = board.get_board_data() + print(f" EEG samples after restart: {data.shape[1]}") + assert data.shape[1] > 0, "No EEG data after restart" + + step("7. stop_stream + release_session") + t0 = time.time() + board.stop_stream() + dt_stop = time.time() - t0 + t0 = time.time() + board.release_session() + dt_release = time.time() - t0 + print(f" stop: {dt_stop:.3f}s, release: {dt_release:.3f}s") + + step("8. reconnect probe (verify clean dispose)") + time.sleep(1.0) + board2 = make_board(args) + t0 = time.time() + board2.prepare_session() + dt = time.time() - t0 + print(f" Reconnected in {dt:.3f}s -- no 'Amplifier in use' error") + board2.release_session() + print(" Probe released cleanly") + + print(f"\n{'=' * 50}") + print("ALL STEPS PASSED") + print(f"{'=' * 50}") + + +if __name__ == "__main__": + main() diff --git a/rust_package/brainflow/src/board_shim.rs b/rust_package/brainflow/src/board_shim.rs index 9abf1dec3..60235b053 100644 --- a/rust_package/brainflow/src/board_shim.rs +++ b/rust_package/brainflow/src/board_shim.rs @@ -30,7 +30,12 @@ impl BoardShim { let json_brainflow_input_params = CString::new(json_brainflow_input_params)?; let master_board_id = if let BoardIds::StreamingBoard | BoardIds::PlaybackFileBoard = board_id { - num::FromPrimitive::from_usize(*input_params.master_board()).unwrap() + let master_board_raw = *input_params.master_board(); + if master_board_raw == BoardIds::NoBoard as usize { + return Err(crate::BrainFlowError::InvalidArgumentsError.into()); + } + num::FromPrimitive::from_usize(master_board_raw) + .ok_or(crate::BrainFlowError::InvalidArgumentsError)? } else { board_id }; @@ -167,7 +172,7 @@ impl BoardShim { /// Get board data and remove data from ringbuffer pub fn get_board_data(&self, n_data_points: Option, preset: BrainFlowPresets) -> Result> { - let num_rows = get_num_rows(self.board_id, preset)?; + let num_rows = get_num_rows(self.master_board_id, preset)?; let num_samples = if let Some(n) = n_data_points { self.get_board_data_count(preset)?.min(n) } else { @@ -194,7 +199,7 @@ impl BoardShim { /// Get specified amount of data or less if there is not enough data, doesnt remove data from ringbuffer. pub fn get_current_board_data(&self, num_samples: usize, preset: BrainFlowPresets) -> Result> { - let num_rows = get_num_rows(self.board_id, preset)?; + let num_rows = get_num_rows(self.master_board_id, preset)?; let capacity = num_samples * num_rows; let mut len = 0; let mut data_buf = Vec::with_capacity(capacity); @@ -592,4 +597,4 @@ mod tests { } } -} \ No newline at end of file +} diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index 0719e3331..997196e43 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::NeuropawnKnightBoardImu; + pub const LAST: BoardIds = BoardIds::AntNeuroEe511EdxBoard; } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] @@ -102,6 +102,20 @@ pub enum BoardIds { BiolistenerBoard = 64, Ironbci32Board = 65, NeuropawnKnightBoardImu = 66, + AntNeuroEe410EdxBoard = 68, + AntNeuroEe411EdxBoard = 69, + AntNeuroEe430EdxBoard = 70, + AntNeuroEe211EdxBoard = 71, + AntNeuroEe212EdxBoard = 72, + AntNeuroEe213EdxBoard = 73, + AntNeuroEe214EdxBoard = 74, + AntNeuroEe215EdxBoard = 75, + AntNeuroEe221EdxBoard = 76, + AntNeuroEe222EdxBoard = 77, + AntNeuroEe223EdxBoard = 78, + AntNeuroEe224EdxBoard = 79, + AntNeuroEe225EdxBoard = 80, + AntNeuroEe511EdxBoard = 81, } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] @@ -109,6 +123,7 @@ pub enum IpProtocolTypes { NoIpProtocol = 0, Udp = 1, Tcp = 2, + Edx = 3, } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] @@ -260,3 +275,5 @@ pub enum WaveletTypes { Sym9 = 43, Sym10 = 44, } + + diff --git a/src/board_controller/ant_neuro_edx/ant_neuro_edx.cpp b/src/board_controller/ant_neuro_edx/ant_neuro_edx.cpp new file mode 100644 index 000000000..6529b4f55 --- /dev/null +++ b/src/board_controller/ant_neuro_edx/ant_neuro_edx.cpp @@ -0,0 +1,1529 @@ +#include "ant_neuro_edx.h" + +#ifdef BUILD_ANT_EDX + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "json.hpp" +#include "timestamp.h" + +using json = nlohmann::json; + +namespace +{ +bool is_ant_master_board (int board_id) +{ + return ((board_id >= (int)BoardIds::ANT_NEURO_EE_410_BOARD && + board_id <= (int)BoardIds::ANT_NEURO_EE_225_BOARD) || + (board_id == (int)BoardIds::ANT_NEURO_EE_511_BOARD)); +} + +int explicit_edx_to_master_board (int board_id) +{ + switch ((BoardIds)board_id) + { + case BoardIds::ANT_NEURO_EE_410_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_410_BOARD; + case BoardIds::ANT_NEURO_EE_411_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_411_BOARD; + case BoardIds::ANT_NEURO_EE_430_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_430_BOARD; + case BoardIds::ANT_NEURO_EE_211_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_211_BOARD; + case BoardIds::ANT_NEURO_EE_212_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_212_BOARD; + case BoardIds::ANT_NEURO_EE_213_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_213_BOARD; + case BoardIds::ANT_NEURO_EE_214_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_214_BOARD; + case BoardIds::ANT_NEURO_EE_215_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_215_BOARD; + case BoardIds::ANT_NEURO_EE_221_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_221_BOARD; + case BoardIds::ANT_NEURO_EE_222_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_222_BOARD; + case BoardIds::ANT_NEURO_EE_223_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_223_BOARD; + case BoardIds::ANT_NEURO_EE_224_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_224_BOARD; + case BoardIds::ANT_NEURO_EE_225_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_225_BOARD; + case BoardIds::ANT_NEURO_EE_511_EDX_BOARD: + return (int)BoardIds::ANT_NEURO_EE_511_BOARD; + default: + return (int)BoardIds::NO_BOARD; + } +} + +std::string to_upper (std::string s) +{ + std::transform (s.begin (), s.end (), s.begin (), + [] (unsigned char c) { return (char)std::toupper (c); }); + return s; +} + +std::vector expected_tokens (int master_board) +{ + switch ((BoardIds)master_board) + { + case BoardIds::ANT_NEURO_EE_511_BOARD: + return {"EE-511", "EE-5XX"}; + case BoardIds::ANT_NEURO_EE_410_BOARD: + case BoardIds::ANT_NEURO_EE_411_BOARD: + case BoardIds::ANT_NEURO_EE_430_BOARD: + return {"EE-4XX"}; + default: + return {"EE-2XX"}; + } +} + +std::set extract_tokens (const std::string &value) +{ + std::set result; + std::regex re ("EE[\\-_ ]?([245][0-9X]{2})"); + std::string upper = to_upper (value); + auto begin = std::sregex_iterator (upper.begin (), upper.end (), re); + auto end = std::sregex_iterator (); + for (auto it = begin; it != end; ++it) + { + std::string suffix = (*it)[1].str (); + std::string token = "EE-" + suffix; + result.insert (token); + if (suffix.size () == 3 && std::isdigit ((unsigned char)suffix[0])) + { + result.insert (std::string ("EE-") + suffix[0] + "XX"); + } + } + return result; +} + +bool has_match (const std::set &tokens, const std::vector &need) +{ + for (const auto &token : need) + { + if (tokens.find (token) != tokens.end ()) + { + return true; + } + } + return false; +} + +double ts_to_unix (const google::protobuf::Timestamp &ts) +{ + return (double)ts.seconds () + ((double)ts.nanos () / 1000000000.0); +} + +int map_status (const grpc::Status &status) +{ + if (status.ok ()) + { + return (int)BrainFlowExitCodes::STATUS_OK; + } + Board::board_logger->error ("gRPC error: code={} message='{}' details='{}'", + (int)status.error_code (), status.error_message (), status.error_details ()); + if (status.error_code () == grpc::StatusCode::DEADLINE_EXCEEDED) + { + return (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; + } + if (status.error_code () == grpc::StatusCode::INVALID_ARGUMENT) + { + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + return (int)BrainFlowExitCodes::GENERAL_ERROR; +} + +std::vector try_get_vec (const json &obj, const char *key) +{ + try + { + return obj[key].get> (); + } + catch (...) + { + return {}; + } +} + +size_t expected_active_channel_count (const json &board_default) +{ + std::set channels; + const char *keys[] = {"eeg_channels", "emg_channels", "ecg_channels", "eog_channels"}; + for (const char *key : keys) + { + for (int channel : try_get_vec (board_default, key)) + { + channels.insert (channel); + } + } + return channels.size (); +} +} // namespace + +AntNeuroEdxBoard::AntNeuroEdxBoard (int board_id, struct BrainFlowInputParams params) + : Board (board_id, params) +{ + keep_alive = false; + initialized = false; + is_streaming = false; + state = (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; + amplifier_handle = -1; + requested_master_board = explicit_edx_to_master_board (board_id); + package_num = 0; + impedance_mode = false; + sampling_rate = -1; + reference_range = -1.0; + bipolar_range = -1.0; + trigger_channel_index = -1; + missing_start_frame_count = 0; + fallback_timestamp_count = 0; + non_monotonic_timestamp_count = 0; + large_gap_count = 0; + last_emitted_timestamp = -1.0; + impedance_sample_count = 0; + mode_request.store (EdxModeRequest::None); + mode_result.store ((int)BrainFlowExitCodes::STATUS_OK); +#ifdef BUILD_ANT_EDX + streaming_context = nullptr; +#endif +} + +AntNeuroEdxBoard::~AntNeuroEdxBoard () +{ + skip_logs = true; + release_session (); +} + +int AntNeuroEdxBoard::validate_master_board () +{ + if (requested_master_board == (int)BoardIds::NO_BOARD) + { + safe_logger (spdlog::level::err, + "failed to map explicit EDX board {} to ANT master board", board_id); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + if ((params.master_board != (int)BoardIds::NO_BOARD) && + (params.master_board != requested_master_board)) + { + safe_logger (spdlog::level::err, + "explicit EDX board {} conflicts with master_board {} (expected {})", + board_id, params.master_board, requested_master_board); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + if (!is_ant_master_board (requested_master_board)) + { + safe_logger (spdlog::level::err, "invalid master_board for EDX: {}", requested_master_board); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int AntNeuroEdxBoard::ensure_connected () +{ + if (!params.other_info.empty () && params.ip_address.empty () && (params.ip_port <= 0)) + { + safe_logger (spdlog::level::err, + "EDX endpoint in other_info is no longer supported, use ip_address/ip_port"); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + if (params.ip_address.empty ()) + { + safe_logger (spdlog::level::err, "EDX requires ip_address"); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + if ((params.ip_port <= 0) || (params.ip_port > 65535)) + { + safe_logger (spdlog::level::err, "EDX requires valid ip_port (1..65535), got {}", params.ip_port); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + if (!params.other_info.empty ()) + { + safe_logger (spdlog::level::warn, + "EDX endpoint in other_info is no longer supported, use ip_address/ip_port"); + } + + if ((params.ip_address.find ("://") != std::string::npos) || + (params.ip_address.find ("/") != std::string::npos)) + { + safe_logger (spdlog::level::err, + "EDX requires host-only ip_address, got URI-like value '{}'", + params.ip_address); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + endpoint = "dns:///" + params.ip_address + ":" + std::to_string (params.ip_port); + + grpc_channel = grpc::CreateChannel (endpoint, grpc::InsecureChannelCredentials ()); + stub = EdigRPC::gen::EdigRPC::NewStub (grpc_channel); + return stub ? (int)BrainFlowExitCodes::STATUS_OK : (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; +} + +int AntNeuroEdxBoard::connect_and_create_device () +{ + EdigRPC::gen::DeviceManager_GetDevicesRequest req; + EdigRPC::gen::DeviceManager_GetDevicesResponse resp; + grpc::ClientContext ctx; + ctx.set_deadline ( + std::chrono::system_clock::now () + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->DeviceManager_GetDevices (&ctx, req, &resp); + if (!status.ok ()) + { + return map_status (status); + } + + std::vector need = expected_tokens (requested_master_board); + std::vector matched; + std::vector serial_matched; + + for (const auto &info : resp.deviceinfolist ()) + { + std::set tokens = extract_tokens (info.key () + " " + info.serial ()); + if (!has_match (tokens, need)) + { + continue; + } + matched.push_back (info); + if (!params.serial_number.empty ()) + { + std::string serial_u = to_upper (params.serial_number); + if (to_upper (info.key ()).find (serial_u) != std::string::npos || + to_upper (info.serial ()).find (serial_u) != std::string::npos) + { + serial_matched.push_back (info); + } + } + } + + if (matched.empty () || (!params.serial_number.empty () && serial_matched.empty ())) + { + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + + EdigRPC::gen::DeviceInfo selected = + (!params.serial_number.empty ()) ? serial_matched.front () : matched.front (); + selected_device_key = selected.key (); + selected_device_serial = selected.serial (); + std::set tokens = extract_tokens (selected_device_key); + if (!tokens.empty ()) + { + selected_model = *tokens.begin (); + } + + EdigRPC::gen::Controller_CreateDeviceRequest create_req; + *create_req.add_deviceinfolist () = selected; + EdigRPC::gen::Controller_CreateDeviceResponse create_resp; + grpc::ClientContext create_ctx; + create_ctx.set_deadline ( + std::chrono::system_clock::now () + std::chrono::seconds (std::max (1, params.timeout))); + status = stub->Controller_CreateDevice (&create_ctx, create_req, &create_resp); + if (!status.ok ()) + { + return map_status (status); + } + + amplifier_handle = create_resp.amplifierhandle (); + return (amplifier_handle < 0) ? (int)BrainFlowExitCodes::GENERAL_ERROR : + (int)BrainFlowExitCodes::STATUS_OK; +} + +int AntNeuroEdxBoard::load_capabilities () +{ + EdigRPC::gen::Amplifier_GetChannelsAvailableRequest channels_req; + channels_req.set_amplifierhandle (amplifier_handle); + EdigRPC::gen::Amplifier_GetChannelsAvailableResponse channels_resp; + grpc::ClientContext channels_ctx; + channels_ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->Amplifier_GetChannelsAvailable (&channels_ctx, channels_req, &channels_resp); + if (!status.ok ()) + { + return map_status (status); + } + + channel_meta.clear (); + active_channel_indices.clear (); + trigger_channel_index = -1; + for (const auto &channel : channels_resp.channellist ()) + { + EdxChannelMeta meta; + meta.index = channel.channelindex (); + meta.polarity = channel.channelpolarity (); + meta.name = channel.name (); + channel_meta.push_back (meta); + if (meta.polarity == EdigRPC::gen::ChannelPolarity::Referential || + meta.polarity == EdigRPC::gen::ChannelPolarity::Bipolar) + { + active_channel_indices.push_back (meta.index); + } + if (to_upper (meta.name).find ("TRIGGER") != std::string::npos || + meta.polarity == EdigRPC::gen::ChannelPolarity::Receiver || + meta.polarity == EdigRPC::gen::ChannelPolarity::Transmitter) + { + trigger_channel_index = meta.index; + } + } + + EdigRPC::gen::Amplifier_GetSamplingRatesAvailableRequest rates_req; + rates_req.set_amplifierhandle (amplifier_handle); + EdigRPC::gen::Amplifier_GetSamplingRatesAvailableResponse rates_resp; + grpc::ClientContext rates_ctx; + rates_ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + status = stub->Amplifier_GetSamplingRatesAvailable (&rates_ctx, rates_req, &rates_resp); + if (!status.ok ()) + { + return map_status (status); + } + sampling_rates_available.clear (); + for (double rate : rates_resp.ratelist ()) + { + sampling_rates_available.push_back ((int)std::lround (rate)); + } + if (!sampling_rates_available.empty ()) + { + sampling_rate = *std::max_element ( + sampling_rates_available.begin (), sampling_rates_available.end ()); + } + + EdigRPC::gen::Amplifier_GetRangesAvailableRequest ranges_req; + ranges_req.set_amplifierhandle (amplifier_handle); + EdigRPC::gen::Amplifier_GetRangesAvailableResponse ranges_resp; + grpc::ClientContext ranges_ctx; + ranges_ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + status = stub->Amplifier_GetRangesAvailable (&ranges_ctx, ranges_req, &ranges_resp); + if (!status.ok ()) + { + return map_status (status); + } + reference_ranges_available.clear (); + bipolar_ranges_available.clear (); + for (const auto &entry : ranges_resp.rangemap ()) + { + if (entry.first == (int)EdigRPC::gen::ChannelPolarity::Referential) + { + reference_ranges_available.assign (entry.second.values ().begin (), entry.second.values ().end ()); + } + else if (entry.first == (int)EdigRPC::gen::ChannelPolarity::Bipolar) + { + bipolar_ranges_available.assign (entry.second.values ().begin (), entry.second.values ().end ()); + } + } + + EdigRPC::gen::Amplifier_GetModesAvailableRequest modes_req; + modes_req.set_amplifierhandle (amplifier_handle); + EdigRPC::gen::Amplifier_GetModesAvailableResponse modes_resp; + grpc::ClientContext modes_ctx; + modes_ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + status = stub->Amplifier_GetModesAvailable (&modes_ctx, modes_req, &modes_resp); + if (!status.ok ()) + { + return map_status (status); + } + modes_available.clear (); + for (auto mode : modes_resp.modelist ()) + { + modes_available.push_back ((EdigRPC::gen::AmplifierMode)mode); + } + + if (reference_range <= 0.0 && !reference_ranges_available.empty ()) + { + reference_range = reference_ranges_available.front (); + } + if (bipolar_range <= 0.0 && !bipolar_ranges_available.empty ()) + { + bipolar_range = bipolar_ranges_available.front (); + } + if (selected_model == "EE-511" || to_upper (selected_device_serial).find ("EE511") != std::string::npos) + { + reference_range = 1.0; + bipolar_range = 2.5; + } + + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int AntNeuroEdxBoard::prepare_session () +{ + if (initialized) + { + return (int)BrainFlowExitCodes::STATUS_OK; + } + + int res = validate_master_board (); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + res = ensure_connected (); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + + try + { + int descr_board_id = board_id; + board_descr = + boards_struct.brainflow_boards_json["boards"][std::to_string (descr_board_id)]; + sampling_rate = board_descr["default"]["sampling_rate"]; + } + catch (...) + { + return (int)BrainFlowExitCodes::GENERAL_ERROR; + } + + res = connect_and_create_device (); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + res = load_capabilities (); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + release_session (); + return res; + } + size_t expected_channels = expected_active_channel_count (board_descr["default"]); + if (expected_channels > 0 && expected_channels != active_channel_indices.size ()) + { + safe_logger (spdlog::level::err, + "EDX board {} expected {} active EXG channels from descriptor but device reports {}", + board_id, expected_channels, active_channel_indices.size ()); + release_session (); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + int expected_rate = board_descr["default"]["sampling_rate"]; + if (!sampling_rates_available.empty () && + std::find (sampling_rates_available.begin (), sampling_rates_available.end (), expected_rate) == + sampling_rates_available.end ()) + { + std::ostringstream rates_stream; + for (size_t i = 0; i < sampling_rates_available.size (); i++) + { + if (i > 0) + { + rates_stream << ", "; + } + rates_stream << sampling_rates_available[i]; + } + safe_logger (spdlog::level::err, + "EDX board {} expected sampling rate {} but device supports [{}]", + board_id, expected_rate, rates_stream.str ()); + release_session (); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + // Device-discovered capabilities refine validation and configuration. + // Explicit EDX board ids remain self-describing. + + initialized = true; + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int AntNeuroEdxBoard::configure_stream_params (void *request_ptr) +{ + EdigRPC::gen::Amplifier_SetModeRequest *request = + static_cast (request_ptr); + auto *stream_params = request->mutable_streamparams (); + stream_params->set_samplingrate (sampling_rate); + stream_params->set_datareadypercentage (5); + stream_params->set_buffersize (1024); + stream_params->clear_ranges (); + for (int channel_index : active_channel_indices) + { + stream_params->add_activechannels (channel_index); + } + if (reference_range > 0.0) + { + (*stream_params->mutable_ranges ())[(int)EdigRPC::gen::ChannelPolarity::Referential] = + reference_range; + } + if (bipolar_range > 0.0) + { + (*stream_params->mutable_ranges ())[(int)EdigRPC::gen::ChannelPolarity::Bipolar] = + bipolar_range; + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int AntNeuroEdxBoard::set_mode () +{ + EdigRPC::gen::Amplifier_SetModeRequest request; + request.set_amplifierhandle (amplifier_handle); + request.set_mode ( + impedance_mode ? EdigRPC::gen::AmplifierMode::AmplifierMode_Impedance : + EdigRPC::gen::AmplifierMode::AmplifierMode_Eeg); + configure_stream_params (&request); + + EdigRPC::gen::Amplifier_SetModeResponse response; + grpc::ClientContext ctx; + ctx.set_deadline ( + std::chrono::system_clock::now () + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->Amplifier_SetMode (&ctx, request, &response); + return map_status (status); +} + +int AntNeuroEdxBoard::set_idle_mode () +{ + if (!stub || amplifier_handle < 0) + { + return (int)BrainFlowExitCodes::STATUS_OK; + } + + EdigRPC::gen::Amplifier_SetModeRequest request; + request.set_amplifierhandle (amplifier_handle); + request.set_mode (EdigRPC::gen::AmplifierMode::AmplifierMode_Idle); + // Allocate an empty StreamParams so the server doesn't dereference null, + // but don't populate channels/rates — idle doesn't need them. + request.mutable_streamparams (); + EdigRPC::gen::Amplifier_SetModeResponse response; + + grpc::ClientContext ctx; + ctx.set_deadline ( + std::chrono::system_clock::now () + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->Amplifier_SetMode (&ctx, request, &response); + return map_status (status); +} + +int AntNeuroEdxBoard::apply_mode_change () +{ + if (!initialized) + { + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + + bool was_streaming = is_streaming || keep_alive; + if (was_streaming) + { + safe_logger (spdlog::level::info, "EDX apply_mode_change: stopping current stream (impedance_mode={})", + impedance_mode ? 1 : 0); + keep_alive = false; + is_streaming = false; + if (streaming_thread.joinable ()) + { + streaming_thread.join (); + } + } + + safe_logger (spdlog::level::info, "EDX apply_mode_change: calling set_mode (impedance_mode={})", + impedance_mode ? 1 : 0); + int res = set_mode (); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + safe_logger (spdlog::level::err, "EDX apply_mode_change: set_mode failed with {}", res); + return res; + } + + if (!was_streaming) + { + safe_logger (spdlog::level::info, "EDX apply_mode_change: was not streaming, done"); + return (int)BrainFlowExitCodes::STATUS_OK; + } + + last_emitted_timestamp = -1.0; + non_monotonic_timestamp_count = 0; + large_gap_count = 0; + fallback_timestamp_count = 0; + missing_start_frame_count = 0; + impedance_sample_count = 0; + keep_alive = true; + state = (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; + streaming_thread = std::thread ([this] { read_thread (); }); + + // Impedance mode needs longer to produce first frame (amplifier settling) + int wait_secs = std::max (1, params.timeout); + if (impedance_mode && wait_secs < 30) + { + wait_secs = 30; + } + safe_logger (spdlog::level::info, "EDX apply_mode_change: waiting up to {}s for first frame", wait_secs); + std::unique_lock lk (wait_mutex); + if (wait_cv.wait_for ( + lk, std::chrono::seconds (wait_secs), + [this] { return state != (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; })) + { + if (state == (int)BrainFlowExitCodes::STATUS_OK) + { + is_streaming = true; + safe_logger (spdlog::level::info, "EDX apply_mode_change: streaming started"); + } + return state; + } + + safe_logger (spdlog::level::err, "EDX apply_mode_change: timed out after {}s waiting for first frame", wait_secs); + keep_alive = false; + if (streaming_thread.joinable ()) + { + streaming_thread.join (); + } + return (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; +} + +void AntNeuroEdxBoard::read_thread () +{ + int sleep_time_ms = impedance_mode ? 100 : 5; + int wait_attempts = 0; + int wait_secs = std::max (1, params.timeout); + if (impedance_mode && wait_secs < 30) + { + wait_secs = 30; + } + int max_wait_attempts = wait_secs * 1000 / sleep_time_ms; + + while (keep_alive) + { + // Process any pending mode-change request between frames. + // This is the ONLY safe place to call Amplifier_SetMode — the EDX + // gRPC server corrupts its state if SetMode races with GetFrame. + process_mode_request (); + + int res = process_frames (); + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + if (state != (int)BrainFlowExitCodes::STATUS_OK) + { + { + std::lock_guard lk (wait_mutex); + state = (int)BrainFlowExitCodes::STATUS_OK; + } + wait_cv.notify_one (); + } + wait_attempts = 0; + } + else + { + wait_attempts++; + if (wait_attempts >= max_wait_attempts) + { + { + std::lock_guard lk (wait_mutex); + state = res; + } + wait_cv.notify_one (); + break; + } + std::this_thread::sleep_for (std::chrono::milliseconds (sleep_time_ms)); + } + } + + // Handle any pending request on exit + process_mode_request (); +} + +void AntNeuroEdxBoard::process_mode_request () +{ + EdxModeRequest req = mode_request.load (); + if (req == EdxModeRequest::None) + { + return; + } + + int res; + switch (req) + { + case EdxModeRequest::Idle: + res = set_idle_mode (); + break; + case EdxModeRequest::Eeg: + impedance_mode = false; + res = set_mode (); + break; + case EdxModeRequest::Impedance: + impedance_mode = true; + res = set_mode (); + break; + default: + res = (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + break; + } + + { + std::lock_guard lk (mode_mutex); + mode_result.store (res); + mode_request.store (EdxModeRequest::None); + } + mode_cv.notify_one (); +} + +int AntNeuroEdxBoard::process_frames () +{ + EdigRPC::gen::Amplifier_GetFrameRequest request; + request.set_amplifierhandle (amplifier_handle); + EdigRPC::gen::Amplifier_GetFrameResponse response; + + grpc::ClientContext ctx; + // Impedance frames arrive less frequently; use longer deadline to avoid + // premature DEADLINE_EXCEEDED that masks valid settling-time delays. + int deadline_ms = impedance_mode ? 3000 : 500; + ctx.set_deadline (std::chrono::system_clock::now () + std::chrono::milliseconds (deadline_ms)); + streaming_context = &ctx; + grpc::Status status = stub->Amplifier_GetFrame (&ctx, request, &response); + streaming_context = nullptr; + if (!status.ok ()) + { + return map_status (status); + } + if (response.framelist ().empty ()) + { + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + + int num_rows = board_descr["default"]["num_rows"]; + int package_num_channel = board_descr["default"]["package_num_channel"]; + int timestamp_channel = board_descr["default"]["timestamp_channel"]; + int marker_channel = board_descr["default"]["marker_channel"]; + std::vector eeg_channels = try_get_vec (board_descr["default"], "eeg_channels"); + std::vector emg_channels = try_get_vec (board_descr["default"], "emg_channels"); + std::vector resistance_channels = try_get_vec (board_descr["default"], "resistance_channels"); + std::vector ref_resistance_channels = + try_get_vec (board_descr["default"], "ref_resistance_channels"); + std::vector gnd_resistance_channels = + try_get_vec (board_descr["default"], "gnd_resistance_channels"); + std::vector other_channels = try_get_vec (board_descr["default"], "other_channels"); + std::vector package ((size_t)num_rows, 0.0); + const double sample_dt = (sampling_rate > 0) ? (1.0 / (double)sampling_rate) : 0.0; + const double large_gap_threshold = 1.0; + + for (const auto &frame : response.framelist ()) + { + const bool has_start = frame.has_start (); + const double frame_base_ts = has_start ? ts_to_unix (frame.start ()) : get_timestamp (); + const int frame_marker_count = frame.timemarkers_size (); + if (!has_start) + { + missing_start_frame_count++; + } + + bool impedance_frame = + impedance_mode || frame.frametype () == EdigRPC::gen::AmplifierFrameType::AmplifierFrameType_ImpedanceVoltages; + if (impedance_frame) + { + std::fill (package.begin (), package.end (), 0.0); + for (size_t i = 0; + i < frame.impedance ().channels ().size () && i < resistance_channels.size (); + i++) + { + package[(size_t)resistance_channels[i]] = + (double)frame.impedance ().channels ((int)i).value (); + } + for (size_t i = 0; + i < frame.impedance ().reference ().size () && + i < ref_resistance_channels.size (); + i++) + { + package[(size_t)ref_resistance_channels[i]] = + (double)frame.impedance ().reference ((int)i).value (); + } + for (size_t i = 0; + i < frame.impedance ().ground ().size () && + i < gnd_resistance_channels.size (); + i++) + { + package[(size_t)gnd_resistance_channels[i]] = + (double)frame.impedance ().ground ((int)i).value (); + } + package[(size_t)package_num_channel] = (double)package_num++; + package[(size_t)timestamp_channel] = frame_base_ts; + if (!has_start) + { + fallback_timestamp_count++; + } + if (last_emitted_timestamp > 0.0) + { + if (package[(size_t)timestamp_channel] < last_emitted_timestamp) + { + non_monotonic_timestamp_count++; + } + if ((package[(size_t)timestamp_channel] - last_emitted_timestamp) > large_gap_threshold) + { + large_gap_count++; + } + } + last_emitted_timestamp = package[(size_t)timestamp_channel]; + impedance_sample_count++; + push_package (package.data ()); + continue; + } + + int cols = frame.matrix ().cols (); + int rows = frame.matrix ().rows (); + if (cols <= 0 || rows <= 0 || cols * rows != frame.matrix ().data_size ()) + { + return (int)BrainFlowExitCodes::GENERAL_ERROR; + } + + // Map each TimeMarker to its sample row via timestamp offset, then inject + // via insert_marker so push_package picks it up from the queue. + // Using long long avoids int overflow when TimeMarker.Start is unset (zero). + std::unordered_map marker_at_row; + if (frame_marker_count > 0 && has_start && sample_dt > 0.0) + { + for (int mi = 0; mi < frame_marker_count; mi++) + { + const auto &tm = frame.timemarkers (mi); + double marker_ts = ts_to_unix (tm.start ()); + double offset = marker_ts - frame_base_ts; + long long target_row_ll = (long long)std::round (offset / sample_dt); + int target_row = (int)std::max (0LL, std::min ((long long)(rows - 1), target_row_ll)); + marker_at_row[target_row] = (double)tm.timemarkercode (); + } + } + else if (frame_marker_count > 0) + { + // No frame Start or zero sample_dt: place first marker at row 0. + marker_at_row[0] = (double)frame.timemarkers (0).timemarkercode (); + } + + for (int row = 0; row < rows; row++) + { + std::fill (package.begin (), package.end (), 0.0); + int eeg_counter = 0; + int emg_counter = 0; + for (int col = 0; col < cols; col++) + { + double value = frame.matrix ().data (row * cols + col); + int channel_index = (col < (int)active_channel_indices.size ()) ? + active_channel_indices[(size_t)col] : + col; + + EdigRPC::gen::ChannelPolarity polarity = EdigRPC::gen::ChannelPolarity::Referential; + for (const auto &meta : channel_meta) + { + if (meta.index == channel_index) + { + polarity = meta.polarity; + break; + } + } + + if (polarity == EdigRPC::gen::ChannelPolarity::Referential && + eeg_counter < (int)eeg_channels.size ()) + { + package[(size_t)eeg_channels[(size_t)eeg_counter++]] = value; + } + else if (polarity == EdigRPC::gen::ChannelPolarity::Bipolar && + emg_counter < (int)emg_channels.size ()) + { + package[(size_t)emg_channels[(size_t)emg_counter++]] = value; + } + if (channel_index == trigger_channel_index && !other_channels.empty ()) + { + package[(size_t)other_channels[0]] = value; + } + } + package[(size_t)package_num_channel] = (double)package_num++; + package[(size_t)timestamp_channel] = frame_base_ts + (double)row * sample_dt; + if (!has_start) + { + fallback_timestamp_count++; + } + if (last_emitted_timestamp > 0.0) + { + if (package[(size_t)timestamp_channel] < last_emitted_timestamp) + { + non_monotonic_timestamp_count++; + } + if ((package[(size_t)timestamp_channel] - last_emitted_timestamp) > large_gap_threshold) + { + large_gap_count++; + } + } + last_emitted_timestamp = package[(size_t)timestamp_channel]; + auto it = marker_at_row.find (row); + if (it != marker_at_row.end ()) + { + insert_marker (it->second, (int)BrainFlowPresets::DEFAULT_PRESET); + } + push_package (package.data ()); + } + } + + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int AntNeuroEdxBoard::start_stream (int buffer_size, const char *streamer_params) +{ + if (!initialized) + { + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + if (is_streaming) + { + return (int)BrainFlowExitCodes::STREAM_ALREADY_RUN_ERROR; + } + + int res = prepare_for_acquisition (buffer_size, streamer_params); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + + res = set_mode (); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + + last_emitted_timestamp = -1.0; + non_monotonic_timestamp_count = 0; + large_gap_count = 0; + fallback_timestamp_count = 0; + missing_start_frame_count = 0; + keep_alive = true; + state = (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; + streaming_thread = std::thread ([this] { read_thread (); }); + + std::unique_lock lk (wait_mutex); + auto sec = std::chrono::seconds (std::max (1, params.timeout)); + if (wait_cv.wait_for ( + lk, sec, [this] { return state != (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; })) + { + if (state == (int)BrainFlowExitCodes::STATUS_OK) + { + is_streaming = true; + } + return state; + } + + keep_alive = false; + if (streaming_thread.joinable ()) + { + streaming_thread.join (); + } + free_packages (); + return (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; +} + +// Request a mode change from the read_thread, which processes it between +// Amplifier_GetFrame calls where the gRPC server state is clean. +// If the thread is not running, calls the mode function directly. +// Waits up to timeout seconds for the thread to process the request. +int AntNeuroEdxBoard::request_mode_from_thread (EdxModeRequest req) +{ + if (!keep_alive) + { + // Thread not running — call directly (no race possible) + switch (req) + { + case EdxModeRequest::Idle: + return set_idle_mode (); + case EdxModeRequest::Eeg: + impedance_mode = false; + return set_mode (); + case EdxModeRequest::Impedance: + impedance_mode = true; + return set_mode (); + default: + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + } + + // Post the request for the read_thread + mode_request.store (req); + + // Wait for the thread to process it + std::unique_lock lk (mode_mutex); + auto deadline = std::chrono::seconds (std::max (2, params.timeout)); + if (mode_cv.wait_for (lk, deadline, + [this] { return mode_request.load () == EdxModeRequest::None; })) + { + return mode_result.load (); + } + + safe_logger (spdlog::level::warn, "EDX mode change request timed out"); + mode_request.store (EdxModeRequest::None); + return (int)BrainFlowExitCodes::GENERAL_ERROR; +} + +int AntNeuroEdxBoard::stop_stream () +{ + if (!is_streaming && !keep_alive) + { + return (int)BrainFlowExitCodes::STREAM_THREAD_IS_NOT_RUNNING; + } + + // Request idle mode via the read_thread state machine. + // The thread processes it between GetFrame calls where the gRPC server + // state is clean. If it fails (server already broken from sample loss), + // cancel the in-flight GetFrame so the thread can exit promptly. + int idle_res = request_mode_from_thread (EdxModeRequest::Idle); + if (idle_res != (int)BrainFlowExitCodes::STATUS_OK) + { + safe_logger (spdlog::level::warn, + "EDX set_idle via thread failed ({}), cancelling in-flight GetFrame", idle_res); + if (streaming_context != nullptr) + { + streaming_context->TryCancel (); + } + } + + keep_alive = false; + is_streaming = false; + if (streaming_thread.joinable ()) + { + streaming_thread.join (); + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int AntNeuroEdxBoard::release_session () +{ + if (is_streaming || keep_alive) + { + stop_stream (); + } + + // Try to set idle and dispose with the current handle. + int old_handle = amplifier_handle; + int idle_res = set_idle_mode (); + bool disposed = false; + + if (idle_res == (int)BrainFlowExitCodes::STATUS_OK && stub && amplifier_handle >= 0) + { + // Handle is good — dispose normally + EdigRPC::gen::Amplifier_DisposeRequest request; + request.set_amplifierhandle (amplifier_handle); + EdigRPC::gen::Amplifier_DisposeResponse response; + grpc::ClientContext ctx; + ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status s = stub->Amplifier_Dispose (&ctx, request, &response); + disposed = s.ok (); + } + + if (!disposed && stub && old_handle >= 0) + { + // set_idle failed (server state corrupted, e.g. after sample loss). + // Try dispose directly on the old handle without idle — the server + // may still accept dispose even when SetMode fails. + safe_logger (spdlog::level::warn, + "EDX set_idle failed ({}), trying dispose directly on handle {}", idle_res, old_handle); + EdigRPC::gen::Amplifier_DisposeRequest request; + request.set_amplifierhandle (old_handle); + EdigRPC::gen::Amplifier_DisposeResponse response; + grpc::ClientContext ctx; + ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status s = stub->Amplifier_Dispose (&ctx, request, &response); + safe_logger (spdlog::level::info, + "EDX direct dispose ok={} msg={}", s.ok (), s.error_message ()); + disposed = s.ok (); + } + + if (!disposed && old_handle >= 0) + { + // Both idle and dispose failed on the old connection. + // Open a fresh gRPC channel and try dispose with the OLD handle number. + // Do NOT call connect_and_create_device — the server still holds the + // old handle and would reject "Amplifier in use". + safe_logger (spdlog::level::warn, + "EDX direct dispose failed, trying fresh gRPC connection with old handle {}", old_handle); + stub.reset (); + grpc_channel.reset (); + + int conn_res = ensure_connected (); + if (conn_res == (int)BrainFlowExitCodes::STATUS_OK) + { + // Try dispose with old handle on fresh connection + EdigRPC::gen::Amplifier_DisposeRequest request; + request.set_amplifierhandle (old_handle); + EdigRPC::gen::Amplifier_DisposeResponse response; + grpc::ClientContext ctx; + ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status s = stub->Amplifier_Dispose (&ctx, request, &response); + safe_logger (spdlog::level::info, + "EDX fresh-conn dispose ok={} msg={}", s.ok (), s.error_message ()); + disposed = s.ok (); + } + else + { + safe_logger (spdlog::level::warn, "EDX recovery: ensure_connected failed = {}", conn_res); + } + } + + free_packages (); + initialized = false; + amplifier_handle = -1; + stub.reset (); + grpc_channel.reset (); + return (int)BrainFlowExitCodes::STATUS_OK; +} + +bool AntNeuroEdxBoard::parse_bool_flag (const std::string &value, bool &flag) +{ + if (value == "0") + { + flag = false; + return true; + } + if (value == "1") + { + flag = true; + return true; + } + return false; +} + +int AntNeuroEdxBoard::validate_sampling_rate (int value) +{ + if (sampling_rates_available.empty ()) + { + return (int)BrainFlowExitCodes::STATUS_OK; + } + return (std::find (sampling_rates_available.begin (), sampling_rates_available.end (), value) != + sampling_rates_available.end ()) ? + (int)BrainFlowExitCodes::STATUS_OK : + (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; +} + +int AntNeuroEdxBoard::validate_reference_range (double value) +{ + if (reference_ranges_available.empty ()) + { + return (int)BrainFlowExitCodes::STATUS_OK; + } + return (std::find (reference_ranges_available.begin (), reference_ranges_available.end (), value) != + reference_ranges_available.end ()) ? + (int)BrainFlowExitCodes::STATUS_OK : + (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; +} + +int AntNeuroEdxBoard::validate_bipolar_range (double value) +{ + if (bipolar_ranges_available.empty ()) + { + return (int)BrainFlowExitCodes::STATUS_OK; + } + return (std::find (bipolar_ranges_available.begin (), bipolar_ranges_available.end (), value) != + bipolar_ranges_available.end ()) ? + (int)BrainFlowExitCodes::STATUS_OK : + (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; +} + +int AntNeuroEdxBoard::parse_edx_command (const std::string &config, std::string &response) +{ + std::vector parts; + std::stringstream ss (config); + std::string token; + while (std::getline (ss, token, ':')) + { + parts.push_back (token); + } + if (parts.size () < 2 || parts[0] != "edx") + { + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + json out; + out["op"] = parts[1]; + if (parts[1] == "get_capabilities") + { + out["sampling_rates"] = sampling_rates_available; + out["active_channels"] = active_channel_indices; + out["selected_model"] = selected_model; + out["reference_ranges"] = reference_ranges_available; + out["bipolar_ranges"] = bipolar_ranges_available; + json channels = json::array (); + for (const auto &meta : channel_meta) + { + json ch; + ch["index"] = meta.index; + ch["name"] = meta.name; + ch["polarity"] = (int)meta.polarity; + channels.push_back (ch); + } + out["channels"] = channels; + } + else if (parts[1] == "get_mode") + { + EdigRPC::gen::Amplifier_GetModeRequest request; + request.set_amplifierhandle (amplifier_handle); + EdigRPC::gen::Amplifier_GetModeResponse mode_response; + grpc::ClientContext ctx; + ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->Amplifier_GetMode (&ctx, request, &mode_response); + if (!status.ok ()) + { + return map_status (status); + } + std::vector modes; + for (auto mode : mode_response.modelist ()) + { + modes.push_back ((int)mode); + } + out["mode_list"] = modes; + } + else if (parts[1] == "get_power") + { + EdigRPC::gen::Amplifier_GetPowerRequest request; + request.set_amplifierhandle (amplifier_handle); + EdigRPC::gen::Amplifier_GetPowerResponse power_response; + grpc::ClientContext ctx; + ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->Amplifier_GetPower (&ctx, request, &power_response); + if (!status.ok ()) + { + return map_status (status); + } + if (power_response.powerlist_size () > 0) + { + out["battery_level"] = power_response.powerlist (0).batterylevel (); + } + } + else if (parts[1] == "trigger_config" && parts.size () >= 3) + { + // "edx:trigger_config:,,,,," + std::vector vals; + std::stringstream csv (parts[2]); + std::string item; + while (std::getline (csv, item, ',')) + { + vals.push_back (std::stod (item)); + } + if (vals.size () < 6) + { + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + EdigRPC::gen::Amplifier_SetOutputTriggerChannelsRequest request; + request.set_amplifierhandle (amplifier_handle); + auto *info = request.add_infos (); + info->set_channelindex ((int)vals[0]); + info->set_channeltype (EdigRPC::gen::OutputChannelType_TriggerOutput); + auto *params_map = info->mutable_parameters (); + (*params_map)["dutyCycle"] = vals[1]; + (*params_map)["pulseFrequency"] = vals[2]; + (*params_map)["pulseCount"] = vals[3]; + (*params_map)["burstFrequency"] = vals[4]; + (*params_map)["burstCount"] = vals[5]; + EdigRPC::gen::Amplifier_SetOutputTriggerChannelsResponse resp; + grpc::ClientContext ctx; + ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->Amplifier_SetOutputTriggerChannels (&ctx, request, &resp); + if (!status.ok ()) + { + return map_status (status); + } + out["channel"] = (int)vals[0]; + } + else if (parts[1] == "trigger_start" && parts.size () >= 3) + { + EdigRPC::gen::Amplifier_StartOutputTriggerRequest request; + request.set_amplifierhandle (amplifier_handle); + std::stringstream csv (parts[2]); + std::string item; + while (std::getline (csv, item, ',')) + { + request.add_channels (std::stoi (item)); + } + EdigRPC::gen::Amplifier_StartOutputTriggerResponse resp; + grpc::ClientContext ctx; + ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->Amplifier_StartOutputTrigger (&ctx, request, &resp); + if (!status.ok ()) + { + return map_status (status); + } + } + else if (parts[1] == "trigger_stop" && parts.size () >= 3) + { + EdigRPC::gen::Amplifier_StopOutputTriggerRequest request; + request.set_amplifierhandle (amplifier_handle); + std::stringstream csv (parts[2]); + std::string item; + while (std::getline (csv, item, ',')) + { + request.add_channels (std::stoi (item)); + } + EdigRPC::gen::Amplifier_StopOutputTriggerResponse resp; + grpc::ClientContext ctx; + ctx.set_deadline (std::chrono::system_clock::now () + + std::chrono::seconds (std::max (1, params.timeout))); + grpc::Status status = stub->Amplifier_StopOutputTrigger (&ctx, request, &resp); + if (!status.ok ()) + { + return map_status (status); + } + } + else + { + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + out["status"] = "ok"; + response = out.dump (); + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int AntNeuroEdxBoard::config_board (std::string config, std::string &response) +{ + if (!initialized) + { + return (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + + if (config.rfind ("edx:", 0) == 0) + { + return parse_edx_command (config, response); + } + if (config.find ("sampling_rate:") == 0) + { + int value = std::stoi (config.substr (14)); + int res = validate_sampling_rate (value); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + sampling_rate = value; + return (int)BrainFlowExitCodes::STATUS_OK; + } + if (config.find ("reference_range:") == 0) + { + reference_range = std::stod (config.substr (16)); + int res = validate_reference_range (reference_range); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + return (int)BrainFlowExitCodes::STATUS_OK; + } + if (config.find ("bipolar_range:") == 0) + { + bipolar_range = std::stod (config.substr (14)); + int res = validate_bipolar_range (bipolar_range); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + return (int)BrainFlowExitCodes::STATUS_OK; + } + if (config.find ("impedance_mode:") == 0) + { + bool mode = false; + if (!parse_bool_flag (config.substr (15), mode)) + { + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + if (impedance_mode == mode) + { + return (int)BrainFlowExitCodes::STATUS_OK; + } + impedance_mode = mode; + return apply_mode_change (); + } + if (config == "get_info") + { + json info; + info["endpoint"] = endpoint; + info["master_board"] = requested_master_board; + info["sampling_rate"] = sampling_rate; + info["selected_model"] = selected_model; + info["selected_key"] = selected_device_key; + info["selected_serial"] = selected_device_serial; + info["impedance_mode"] = impedance_mode; + info["impedance_sample_count"] = impedance_sample_count; + info["timing"] = { + {"missing_start_frame_count", missing_start_frame_count}, + {"fallback_timestamp_count", fallback_timestamp_count}, + {"non_monotonic_timestamp_count", non_monotonic_timestamp_count}, + {"large_gap_count", large_gap_count}, + {"last_emitted_timestamp", last_emitted_timestamp}}; + response = info.dump (); + return (int)BrainFlowExitCodes::STATUS_OK; + } + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; +} + +#else + +#include "brainflow_constants.h" + +AntNeuroEdxBoard::AntNeuroEdxBoard (int board_id, struct BrainFlowInputParams params) + : Board (board_id, params) +{ +} + +AntNeuroEdxBoard::~AntNeuroEdxBoard () +{ +} + +int AntNeuroEdxBoard::prepare_session () +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::start_stream (int, const char *) +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::stop_stream () +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::release_session () +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::config_board (std::string, std::string &) +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::validate_master_board () +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::ensure_connected () +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::set_mode () +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::configure_stream_params (void *) +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::process_frames () +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +int AntNeuroEdxBoard::parse_edx_command (const std::string &, std::string &) +{ + return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; +} + +void AntNeuroEdxBoard::read_thread () +{ +} + +bool AntNeuroEdxBoard::parse_bool_flag (const std::string &, bool &) +{ + return false; +} + +#endif diff --git a/src/board_controller/ant_neuro_edx/inc/ant_neuro_edx.h b/src/board_controller/ant_neuro_edx/inc/ant_neuro_edx.h new file mode 100644 index 000000000..68ca864eb --- /dev/null +++ b/src/board_controller/ant_neuro_edx/inc/ant_neuro_edx.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "board.h" + +#ifdef BUILD_ANT_EDX +#include + +#include "EdigRPC.grpc.pb.h" +#endif + + +// Mode-change requests processed by the read_thread between gRPC frames. +// The read_thread is the only safe place to call Amplifier_SetMode because the +// EDX gRPC server corrupts its internal state if SetMode races with GetFrame. +enum class EdxModeRequest : int +{ + None = 0, // No pending request + Idle = 1, // Transition to idle mode + Eeg = 2, // Transition to EEG streaming mode + Impedance = 3 // Transition to impedance mode +}; + + +class AntNeuroEdxBoard : public Board +{ +private: +#ifdef BUILD_ANT_EDX + struct EdxChannelMeta + { + int index; + EdigRPC::gen::ChannelPolarity polarity; + std::string name; + }; +#endif + + volatile bool keep_alive; + bool initialized; + bool is_streaming; + std::thread streaming_thread; + std::mutex wait_mutex; + std::condition_variable wait_cv; + volatile int state; + + // Atomic mode-change state machine. + // Callers set mode_request, the read_thread processes it between frames + // and writes the result to mode_result, then notifies via mode_cv. + std::atomic mode_request; + std::atomic mode_result; + std::mutex mode_mutex; + std::condition_variable mode_cv; + + int amplifier_handle; + int requested_master_board; + int package_num; + int sampling_rate; + double reference_range; + double bipolar_range; + bool impedance_mode; + std::vector active_channel_indices; + std::string endpoint; + int trigger_channel_index; + // Timing diagnostics are exposed via get_info and do not alter stream shape. + uint64_t missing_start_frame_count; + uint64_t fallback_timestamp_count; + uint64_t non_monotonic_timestamp_count; + uint64_t large_gap_count; + double last_emitted_timestamp; + uint64_t impedance_sample_count; +#ifdef BUILD_ANT_EDX + grpc::ClientContext *volatile streaming_context; + std::shared_ptr grpc_channel; + std::unique_ptr stub; + std::vector channel_meta; + std::vector sampling_rates_available; + std::vector reference_ranges_available; + std::vector bipolar_ranges_available; + std::vector modes_available; + std::string selected_model; + std::string selected_device_key; + std::string selected_device_serial; +#endif + + int validate_master_board (); + int ensure_connected (); + int set_mode (); + int configure_stream_params (void *request_ptr); + int process_frames (); + int apply_mode_change (); + int parse_edx_command (const std::string &config, std::string &response); + void read_thread (); + int request_mode_from_thread (EdxModeRequest request); + void process_mode_request (); + bool parse_bool_flag (const std::string &value, bool &flag); +#ifdef BUILD_ANT_EDX + int connect_and_create_device (); + int load_capabilities (); + int set_idle_mode (); + int validate_sampling_rate (int value); + int validate_reference_range (double value); + int validate_bipolar_range (double value); +#endif + +public: + AntNeuroEdxBoard (int board_id, struct BrainFlowInputParams params); + ~AntNeuroEdxBoard (); + + 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/ant_neuro_edx/proto/EdigRPC.proto b/src/board_controller/ant_neuro_edx/proto/EdigRPC.proto new file mode 100644 index 000000000..f7e55a79f --- /dev/null +++ b/src/board_controller/ant_neuro_edx/proto/EdigRPC.proto @@ -0,0 +1,309 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +package EdigRPC.gen; +/////////////////////////////////////////////////////////////////////////////// +message DoubleList { + repeated double Values = 1; +} +/////////////////////////////////////////////////////////////////////////////// +enum DeviceConnection { + Usb = 0; + RIO = 1; + File = 2; + gRPC = 3; +} +enum EegDeviceType { + UNKNOWN = 0; +} +enum UnitType { + Volt = 0; + MicroVolt = 1; +} +enum AmplifierMode { + AmplifierMode_Disconnect = 0; + AmplifierMode_PowerOff = 1; + AmplifierMode_Eeg = 2; + AmplifierMode_Impedance = 3; + AmplifierMode_OpenLine = 4; + AmplifierMode_Idle = 5; +} +enum AmplifierFrameType { + AmplifierFrameType_EEG = 0; + AmplifierFrameType_ImpedanceVoltages = 1; + AmplifierFrameType_OpenLine = 2; + AmplifierFrameType_Stimulation = 3; +} +enum OutputChannelType { + OutputChannelType_TriggerOutput = 0; + OutputChannelType_Stimulation = 1; +} +enum AmplifierNotifyType { + None = 0; + ModeChange = 1; + PowerChange = 2; +} +enum ChannelConnectionState +{ + Connected = 0; + Undefined = 1; + Disconnected = 2; +} +enum ChannelPolarity { + Referential = 0; + Bipolar = 1; + Auxiliary = 2; + Receiver = 3; + Transmitter = 4; + Disabled = 5; +} + +message DeviceInfo { + EegDeviceType AmplifierType = 1; + string Key = 2; + string Serial = 3; +} +message DeviceParameters { + bool HasBattery = 1; +} +message PowerInfo { + int32 BatteryLevel = 1; + bool isBatteryCharging = 2; + bool isPowerOn = 3; +} +message ChannelType { + int32 ChannelIndex = 1; + ChannelPolarity ChannelPolarity = 2; + UnitType UnitType = 3; + string Name = 4; + string Reference = 5; +} +message StreamParams { + repeated int32 ActiveChannels = 1; + map Ranges = 2; + double SamplingRate = 3; + int32 BufferSize = 4; + int32 DataReadyPercentage = 5; +} +message DoubleMatrix { + int32 Cols = 1; // channels + int32 Rows = 2; // samples + repeated double Data = 3; +} +message Impedances { + repeated TupleImpledanceChannel Channels = 1; + repeated TupleImpledanceChannel Reference = 2; + repeated TupleImpledanceChannel Ground = 3; +} +message TupleImpledanceChannel{ + uint32 Value = 1; + ChannelConnectionState ChannelState = 2; +} +message TimeMarker { + google.protobuf.Timestamp Start = 1; + int32 TimeMarkerCode = 2; +} +message AmplifierFrame { + int32 BufferLoadCapacity = 1; + AmplifierFrameType FrameType = 2; + Impedances Impedance = 3; + google.protobuf.Timestamp Start = 4; + google.protobuf.Timestamp StartPcTime = 5; + DoubleMatrix Matrix = 6; + repeated TimeMarker TimeMarkers = 7; +} +message OutputTriggerChannelInfo { + int32 ChannelIndex = 1; + OutputChannelType ChannelType = 2; + map Parameters = 3; +} +/////////////////////////////////////////////////////////////////////////////// +// +message Controller_CreateDeviceRequest { + repeated DeviceInfo DeviceInfoList = 1; +} +message Controller_CreateDeviceResponse { + int32 AmplifierHandle = 1; +} +// +message DeviceManager_GetDevicesRequest { +} +message DeviceManager_GetDevicesResponse { + repeated DeviceInfo DeviceInfoList = 1; +} +// +message DeviceManager_DeviceAttachRequest { +} +message DeviceManager_DeviceAttachResponse { + DeviceInfo DeviceInfo = 1; +} +// +message DeviceManager_DeviceDetachRequest { +} +message DeviceManager_DeviceDetachResponse { + DeviceInfo DeviceInfo = 1; +} +// +message Amplifier_DisposeRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_DisposeResponse { +} +// +message Amplifier_GetDeviceInformationRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetDeviceInformationResponse { + repeated DeviceInfo DeviceInformation = 1; +} +// +message Amplifier_GetDeviceParametersRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetDeviceParametersResponse { + repeated DeviceParameters DeviceParameters = 1; +} +// +message Amplifier_GetChannelsAvailableRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetChannelsAvailableResponse { + repeated ChannelType ChannelList = 1; +} +// +message Amplifier_GetSamplingRatesAvailableRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetSamplingRatesAvailableResponse { + repeated double RateList = 1; +} +// +message Amplifier_GetModesAvailableRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetModesAvailableResponse { + repeated AmplifierMode ModeList = 1; +} +// +message Amplifier_GetRangesAvailableRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetRangesAvailableResponse { + map RangeMap = 1; +} +// +message Amplifier_GetModeRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetModeResponse { + repeated AmplifierMode ModeList = 1; +} +// +message Amplifier_SetModeRequest { + int32 AmplifierHandle = 1; + AmplifierMode Mode = 2; + StreamParams StreamParams = 3; + string StimParams = 4; +} +message Amplifier_SetModeResponse { +} +// +message Amplifier_GetFrameRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetFrameResponse { + repeated AmplifierFrame FrameList = 1; +} +// +message Amplifier_GetPowerRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_GetPowerResponse { + repeated PowerInfo PowerList = 1; +} +// +message Amplifier_NotifyEventRequest { + int32 AmplifierHandle = 1; +} +message Amplifier_NotifyEventResponse { + AmplifierNotifyType NotifyType = 1; +} +// +message Amplifier_SetOutputTriggerChannelsRequest { + int32 AmplifierHandle = 1; + repeated OutputTriggerChannelInfo infos = 2; +} +message Amplifier_SetOutputTriggerChannelsResponse { +} +// +message Amplifier_SetStimulationParametersRequest { + int32 AmplifierHandle = 1; + string StimlationParams = 2; +} +message Amplifier_SetStimulationParametersResponse { +} +// +message Amplifier_StartOutputTriggerRequest { + int32 AmplifierHandle = 1; + repeated int32 Channels = 2; +} +message Amplifier_StartOutputTriggerResponse { +} +// +message Amplifier_StopOutputTriggerRequest { + int32 AmplifierHandle = 1; + repeated int32 Channels = 2; +} +message Amplifier_StopOutputTriggerResponse { +} +// +message Amplifier_StartStimulationRequest { + int32 AmplifierHandle = 1; + repeated int32 Channels = 2; +} +message Amplifier_StartStimulationResponse { +} +// +message Amplifier_StopStimulationRequest { + int32 AmplifierHandle = 1; + repeated int32 Channels = 2; +} +message Amplifier_StopStimulationResponse { +} +// +message GetStateRequest { +} +message GetStateResponse { + string Id = 1; +} +/////////////////////////////////////////////////////////////////////////////// +service EdigRPC { + rpc Controller_CreateDevice(Controller_CreateDeviceRequest) returns (Controller_CreateDeviceResponse); + + rpc DeviceManager_GetDevices(DeviceManager_GetDevicesRequest) returns (DeviceManager_GetDevicesResponse); + rpc DeviceManager_DeviceAttach(DeviceManager_DeviceAttachRequest) returns (stream DeviceManager_DeviceAttachResponse); + rpc DeviceManager_DeviceDetach(DeviceManager_DeviceDetachRequest) returns (stream DeviceManager_DeviceDetachResponse); + + rpc Amplifier_Dispose(Amplifier_DisposeRequest) returns (Amplifier_DisposeResponse); + rpc Amplifier_GetDeviceInformation(Amplifier_GetDeviceInformationRequest) returns (Amplifier_GetDeviceInformationResponse); + rpc Amplifier_GetDeviceParameters(Amplifier_GetDeviceParametersRequest) returns (Amplifier_GetDeviceParametersResponse); + rpc Amplifier_GetChannelsAvailable(Amplifier_GetChannelsAvailableRequest) returns (Amplifier_GetChannelsAvailableResponse); + rpc Amplifier_GetSamplingRatesAvailable(Amplifier_GetSamplingRatesAvailableRequest) returns (Amplifier_GetSamplingRatesAvailableResponse); + rpc Amplifier_GetModesAvailable(Amplifier_GetModesAvailableRequest) returns (Amplifier_GetModesAvailableResponse); + rpc Amplifier_GetRangesAvailable(Amplifier_GetRangesAvailableRequest) returns (Amplifier_GetRangesAvailableResponse); + rpc Amplifier_GetMode(Amplifier_GetModeRequest) returns (Amplifier_GetModeResponse); + rpc Amplifier_SetMode(Amplifier_SetModeRequest) returns (Amplifier_SetModeResponse); + rpc Amplifier_GetFrame(Amplifier_GetFrameRequest) returns (Amplifier_GetFrameResponse); + rpc Amplifier_GetPower(Amplifier_GetPowerRequest) returns (Amplifier_GetPowerResponse); + rpc Amplifier_SetOutputTriggerChannels(Amplifier_SetOutputTriggerChannelsRequest) returns (Amplifier_SetOutputTriggerChannelsResponse); + rpc Amplifier_SetStimulationParameters(Amplifier_SetStimulationParametersRequest) returns (Amplifier_SetStimulationParametersResponse); + rpc Amplifier_StartOutputTrigger(Amplifier_StartOutputTriggerRequest) returns (Amplifier_StartOutputTriggerResponse); + rpc Amplifier_StopOutputTrigger(Amplifier_StopOutputTriggerRequest) returns (Amplifier_StopOutputTriggerResponse); + rpc Amplifier_StartStimulation(Amplifier_StartStimulationRequest) returns (Amplifier_StartStimulationResponse); + rpc Amplifier_StopStimulation(Amplifier_StopStimulationRequest) returns (Amplifier_StopStimulationResponse); + + rpc Amplifier_NotifyEvent(Amplifier_NotifyEventRequest) returns (stream Amplifier_NotifyEventResponse); + + rpc GetState(GetStateRequest) returns (GetStateResponse); +} diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index 3e52792af..58900a9df 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -16,6 +16,9 @@ #include "aavaa_v3.h" #include "ant_neuro.h" +#ifdef BUILD_ANT_EDX +#include "ant_neuro_edx.h" +#endif #include "biolistener.h" #include "board.h" #include "board_controller.h" @@ -260,6 +263,24 @@ int prepare_session (int board_id, const char *json_brainflow_input_params) board = std::shared_ptr ( new AntNeuroBoard ((int)BoardIds::ANT_NEURO_EE_511_BOARD, params)); break; +#ifdef BUILD_ANT_EDX + case BoardIds::ANT_NEURO_EE_410_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_411_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_430_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_211_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_212_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_213_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_214_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_215_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_221_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_222_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_223_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_224_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_225_EDX_BOARD: + case BoardIds::ANT_NEURO_EE_511_EDX_BOARD: + board = std::shared_ptr (new AntNeuroEdxBoard (board_id, params)); + break; +#endif case BoardIds::NTL_WIFI_BOARD: board = std::shared_ptr (new NtlWifi (params)); break; diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index 5d86d6a59..28baf7a17 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -84,7 +84,22 @@ BrainFlowBoards::BrainFlowBoards() {"63", json::object()}, {"64", json::object()}, {"65", json::object()}, - {"66", json::object()} + {"66", json::object()}, + {"67", json::object()}, + {"68", json::object()}, + {"69", json::object()}, + {"70", json::object()}, + {"71", json::object()}, + {"72", json::object()}, + {"73", json::object()}, + {"74", json::object()}, + {"75", json::object()}, + {"76", json::object()}, + {"77", json::object()}, + {"78", json::object()}, + {"79", json::object()}, + {"80", json::object()}, + {"81", json::object()}, } }}; @@ -924,7 +939,9 @@ BrainFlowBoards::BrainFlowBoards() {"num_rows", 34}, {"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}}, {"emg_channels", {25, 26, 27, 28}}, - {"resistance_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, 29, 30}}, + {"resistance_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}}, + {"ref_resistance_channels", {29}}, + {"gnd_resistance_channels", {30}}, {"other_channels", {31}} }; brainflow_boards_json["boards"]["52"]["default"] = @@ -1159,6 +1176,27 @@ BrainFlowBoards::BrainFlowBoards() {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, {"other_channels", {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}} }; + auto clone_ant_edx_board = [this] (int edx_board_id, int master_board_id, const char *name) + { + json descr = + brainflow_boards_json["boards"][std::to_string (master_board_id)]["default"]; + descr["name"] = name; + brainflow_boards_json["boards"][std::to_string (edx_board_id)]["default"] = descr; + }; + clone_ant_edx_board (68, 24, "AntNeuroEE410EDX"); + clone_ant_edx_board (69, 25, "AntNeuroEE411EDX"); + clone_ant_edx_board (70, 26, "AntNeuroEE430EDX"); + clone_ant_edx_board (71, 27, "AntNeuroEE211EDX"); + clone_ant_edx_board (72, 28, "AntNeuroEE212EDX"); + clone_ant_edx_board (73, 29, "AntNeuroEE213EDX"); + clone_ant_edx_board (74, 30, "AntNeuroEE214EDX"); + clone_ant_edx_board (75, 31, "AntNeuroEE215EDX"); + clone_ant_edx_board (76, 32, "AntNeuroEE221EDX"); + clone_ant_edx_board (77, 33, "AntNeuroEE222EDX"); + clone_ant_edx_board (78, 34, "AntNeuroEE223EDX"); + clone_ant_edx_board (79, 35, "AntNeuroEE224EDX"); + clone_ant_edx_board (80, 36, "AntNeuroEE225EDX"); + clone_ant_edx_board (81, 51, "AntNeuroEE511EDX"); } BrainFlowBoards boards_struct; diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake index 886ebfb1a..ad05ea8bd 100644 --- a/src/board_controller/build.cmake +++ b/src/board_controller/build.cmake @@ -89,6 +89,44 @@ SET (BOARD_CONTROLLER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/biolistener.cpp ) +if (BUILD_ANT_EDX) + # Prefer package-config mode (vcpkg/Conan), but fall back to CMake's + # built-in FindProtobuf module for distro system packages. + find_package (Protobuf CONFIG QUIET) + if (NOT Protobuf_FOUND) + find_package (Protobuf REQUIRED) + endif () + find_package (gRPC CONFIG REQUIRED) + + set (ANT_EDX_PROTO_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro_edx/proto/EdigRPC.proto) + set (ANT_EDX_GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated/ant_neuro_edx) + file (MAKE_DIRECTORY ${ANT_EDX_GENERATED_DIR}) + + set (ANT_EDX_PROTO_SRCS ${ANT_EDX_GENERATED_DIR}/EdigRPC.pb.cc) + set (ANT_EDX_PROTO_HDRS ${ANT_EDX_GENERATED_DIR}/EdigRPC.pb.h) + set (ANT_EDX_GRPC_SRCS ${ANT_EDX_GENERATED_DIR}/EdigRPC.grpc.pb.cc) + set (ANT_EDX_GRPC_HDRS ${ANT_EDX_GENERATED_DIR}/EdigRPC.grpc.pb.h) + + add_custom_command ( + OUTPUT ${ANT_EDX_PROTO_SRCS} ${ANT_EDX_PROTO_HDRS} ${ANT_EDX_GRPC_SRCS} ${ANT_EDX_GRPC_HDRS} + COMMAND protobuf::protoc + ARGS + --proto_path=${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro_edx/proto + --cpp_out=${ANT_EDX_GENERATED_DIR} + --grpc_out=${ANT_EDX_GENERATED_DIR} + --plugin=protoc-gen-grpc=$ + ${ANT_EDX_PROTO_FILE} + DEPENDS ${ANT_EDX_PROTO_FILE} + VERBATIM + ) + + set (BOARD_CONTROLLER_SRC ${BOARD_CONTROLLER_SRC} + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro_edx/ant_neuro_edx.cpp + ${ANT_EDX_PROTO_SRCS} + ${ANT_EDX_GRPC_SRCS} + ) +endif (BUILD_ANT_EDX) + include (${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro/build.cmake) include (${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/openbci/ganglion_bglib/build.cmake) include (${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/gtec/build.cmake) @@ -144,6 +182,7 @@ target_include_directories ( ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/freeeeg/inc ${CMAKE_CURRENT_SOURCE_DIR}/third_party/ant_neuro ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro/inc + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro_edx/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/enophone/inc ${CMAKE_CURRENT_SOURCE_DIR}/third_party/SimpleBLE/simpleble/include ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/brainalive/inc @@ -157,6 +196,16 @@ target_include_directories ( ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/biolistener/inc ) +if (BUILD_ANT_EDX) + target_include_directories ( + ${BOARD_CONTROLLER_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro_edx/inc + ${ANT_EDX_GENERATED_DIR} + ) + target_link_libraries (${BOARD_CONTROLLER_NAME} PRIVATE gRPC::grpc++ protobuf::libprotobuf) + target_compile_definitions (${BOARD_CONTROLLER_NAME} PRIVATE BUILD_ANT_EDX) +endif (BUILD_ANT_EDX) + target_compile_definitions(${BOARD_CONTROLLER_NAME} PRIVATE NOMINMAX BRAINFLOW_VERSION=${BRAINFLOW_VERSION}) set_target_properties (${BOARD_CONTROLLER_NAME} diff --git a/src/utils/inc/brainflow_constants.h b/src/utils/inc/brainflow_constants.h index f6883168c..8faf03ed0 100644 --- a/src/utils/inc/brainflow_constants.h +++ b/src/utils/inc/brainflow_constants.h @@ -95,16 +95,31 @@ enum class BoardIds : int BIOLISTENER_BOARD = 64, IRONBCI_32_BOARD = 65, NEUROPAWN_KNIGHT_BOARD_IMU = 66, + ANT_NEURO_EE_410_EDX_BOARD = 68, + ANT_NEURO_EE_411_EDX_BOARD = 69, + ANT_NEURO_EE_430_EDX_BOARD = 70, + ANT_NEURO_EE_211_EDX_BOARD = 71, + ANT_NEURO_EE_212_EDX_BOARD = 72, + ANT_NEURO_EE_213_EDX_BOARD = 73, + ANT_NEURO_EE_214_EDX_BOARD = 74, + ANT_NEURO_EE_215_EDX_BOARD = 75, + ANT_NEURO_EE_221_EDX_BOARD = 76, + ANT_NEURO_EE_222_EDX_BOARD = 77, + ANT_NEURO_EE_223_EDX_BOARD = 78, + ANT_NEURO_EE_224_EDX_BOARD = 79, + ANT_NEURO_EE_225_EDX_BOARD = 80, + ANT_NEURO_EE_511_EDX_BOARD = 81, // use it to iterate FIRST = PLAYBACK_FILE_BOARD, - LAST = IRONBCI_32_BOARD + LAST = ANT_NEURO_EE_511_EDX_BOARD }; enum class IpProtocolTypes : int { NO_IP_PROTOCOL = 0, UDP = 1, - TCP = 2 + TCP = 2, + EDX = 3 }; enum class FilterTypes : int