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