From f10b94b6f000251c8b3428e38f8ae5607c4dcb63 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Fri, 24 Apr 2026 20:09:05 +0700 Subject: [PATCH 1/3] codex: bug fixes and cleanup Signed-off-by: Andrey Parfenov --- cpp_package/src/board_shim.cpp | 12 +++-- .../brainflow/board_controller_library.cs | 40 +++++++------- .../brainflow/brainflow/board_shim.cs | 8 +-- .../src/main/java/brainflow/BoardShim.java | 19 +++---- matlab_package/brainflow/BoardShim.m | 16 ++++-- nodejs_package/brainflow/board_shim.ts | 15 +++--- nodejs_package/brainflow/functions.types.ts | 14 +++-- python_package/brainflow/board_shim.py | 23 +++++--- rust_package/brainflow/src/board_shim.rs | 27 ++++++++-- .../brainflow/src/ffi/board_controller.rs | 4 ++ src/board_controller/board_controller.cpp | 32 ++++++++++-- src/board_controller/board_info_getter.cpp | 52 ++++++++++++++----- src/board_controller/inc/board_controller.h | 3 +- src/board_controller/inc/board_info_getter.h | 6 +-- .../inc/playback_file_board.h | 1 + src/board_controller/playback_file_board.cpp | 36 +++++++++++++ src/board_controller/synthetic_board.cpp | 2 +- src/ml/onnx/onnx_classifier.cpp | 6 +-- 18 files changed, 225 insertions(+), 91 deletions(-) diff --git a/cpp_package/src/board_shim.cpp b/cpp_package/src/board_shim.cpp index 3ec1fc74c..1581195df 100644 --- a/cpp_package/src/board_shim.cpp +++ b/cpp_package/src/board_shim.cpp @@ -235,7 +235,8 @@ std::string BoardShim::config_board (std::string config) int response_len = 0; char response[8192]; int res = ::config_board ( - config.c_str (), response, &response_len, board_id, serialized_params.c_str ()); + config.c_str (), response, &response_len, sizeof (response), board_id, + serialized_params.c_str ()); if (res != (int)BrainFlowExitCodes::STATUS_OK) { throw BrainFlowException ("failed to config board", res); @@ -289,7 +290,8 @@ json BoardShim::get_board_descr (int board_id, int preset) { char board_descr_str[16000]; int string_len = 0; - int res = ::get_board_descr (board_id, preset, board_descr_str, &string_len); + int res = + ::get_board_descr (board_id, preset, board_descr_str, &string_len, sizeof (board_descr_str)); if (res != (int)BrainFlowExitCodes::STATUS_OK) { throw BrainFlowException ("failed to get board info", res); @@ -368,7 +370,7 @@ std::vector BoardShim::get_eeg_names (int board_id, int preset) { char eeg_names[4096]; int string_len = 0; - int res = ::get_eeg_names (board_id, preset, eeg_names, &string_len); + int res = ::get_eeg_names (board_id, preset, eeg_names, &string_len, sizeof (eeg_names)); if (res != (int)BrainFlowExitCodes::STATUS_OK) { throw BrainFlowException ("failed to get board info", res); @@ -401,7 +403,7 @@ std::string BoardShim::get_device_name (int board_id, int preset) { char name[4096]; int string_len = 0; - int res = ::get_device_name (board_id, preset, name, &string_len); + int res = ::get_device_name (board_id, preset, name, &string_len, sizeof (name)); if (res != (int)BrainFlowExitCodes::STATUS_OK) { throw BrainFlowException ("failed to get board info", res); @@ -602,4 +604,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..9854e5c0e 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -151,7 +151,7 @@ public static class BoardControllerLibrary64 [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int log_message_board_controller (int log_level, string message); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] - public static extern int config_board (string config, byte[] response, int[] len, int board_id, string input_json); + public static extern int config_board (string config, byte[] response, int[] len, int max_len, int board_id, string input_json); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int config_board_with_bytes (byte[] bytes, int len, int board_id, string input_json); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] @@ -195,7 +195,7 @@ public static class BoardControllerLibrary64 [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int is_prepared (int[] prepared, int board_id, string input_json); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] - public static extern int get_eeg_names (int board_id, int preset, byte[] eeg_names, int[] len); + public static extern int get_eeg_names (int board_id, int preset, byte[] eeg_names, int[] len, int max_len); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_resistance_channels (int board_id, int preset, int[] channels, int[] len); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] @@ -203,11 +203,11 @@ public static class BoardControllerLibrary64 [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_exg_channels (int board_id, int preset, int[] channels, int[] len); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] - public static extern int get_device_name (int board_id, int preset, byte[] name, int[] len); + public static extern int get_device_name (int board_id, int preset, byte[] name, int[] len, int max_len); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int insert_marker (double value, int preset, int board_id, string input_json); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] - public static extern int get_board_descr (int board_id, int preset, byte[] board_descr, int[] len); + public static extern int get_board_descr (int board_id, int preset, byte[] board_descr, int[] len, int max_len); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int release_all_sessions (); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] @@ -241,7 +241,7 @@ public static class BoardControllerLibrary32 [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int log_message_board_controller (int log_level, string message); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] - public static extern int config_board (string config, byte[] response, int[] len, int board_id, string input_json); + public static extern int config_board (string config, byte[] response, int[] len, int max_len, int board_id, string input_json); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int config_board_with_bytes (byte[] bytes, int len, int board_id, string input_json); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] @@ -285,17 +285,17 @@ public static class BoardControllerLibrary32 [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int is_prepared (int[] prepared, int board_id, string input_json); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] - public static extern int get_eeg_names (int board_id, int preset, byte[] eeg_names, int[] len); + public static extern int get_eeg_names (int board_id, int preset, byte[] eeg_names, int[] len, int max_len); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_resistance_channels (int board_id, int preset, int[] channels, int[] len); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_exg_channels (int board_id, int preset, int[] channels, int[] len); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] - public static extern int get_device_name (int board_id, int preset, byte[] name, int[] len); + public static extern int get_device_name (int board_id, int preset, byte[] name, int[] len, int max_len); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int insert_marker (double value, int preset, int board_id, string input_json); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] - public static extern int get_board_descr (int board_id, int preset, byte[] board_descr, int[] len); + public static extern int get_board_descr (int board_id, int preset, byte[] board_descr, int[] len, int max_len); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int release_all_sessions (); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] @@ -457,14 +457,14 @@ public static int insert_marker (double value, int preset, int board_id, string } - public static int config_board (string config, byte[] str, int[] len, int board_id, string input_json) + public static int config_board (string config, byte[] str, int[] len, int max_len, int board_id, string input_json) { switch (PlatformHelper.get_library_environment ()) { case LibraryEnvironment.x64: - return BoardControllerLibrary64.config_board (config, str, len, board_id, input_json); + return BoardControllerLibrary64.config_board (config, str, len, max_len, board_id, input_json); case LibraryEnvironment.x86: - return BoardControllerLibrary32.config_board (config, str, len, board_id, input_json); + return BoardControllerLibrary32.config_board (config, str, len, max_len, board_id, input_json); } return (int)BrainFlowExitCodes.GENERAL_ERROR; @@ -600,14 +600,14 @@ public static int get_marker_channel (int board_id, int preset, int[] marker_cha return (int)BrainFlowExitCodes.GENERAL_ERROR; } - public static int get_eeg_names (int board_id, int preset, byte[] names, int[] len) + public static int get_eeg_names (int board_id, int preset, byte[] names, int[] len, int max_len) { switch (PlatformHelper.get_library_environment ()) { case LibraryEnvironment.x64: - return BoardControllerLibrary64.get_eeg_names (board_id, preset, names, len); + return BoardControllerLibrary64.get_eeg_names (board_id, preset, names, len, max_len); case LibraryEnvironment.x86: - return BoardControllerLibrary32.get_eeg_names (board_id, preset, names, len); + return BoardControllerLibrary32.get_eeg_names (board_id, preset, names, len, max_len); } return (int)BrainFlowExitCodes.GENERAL_ERROR; @@ -626,27 +626,27 @@ public static int get_board_presets (int board_id, int[] names, int[] len) return (int)BrainFlowExitCodes.GENERAL_ERROR; } - public static int get_board_descr (int board_id, int preset, byte[] descr, int[] len) + public static int get_board_descr (int board_id, int preset, byte[] descr, int[] len, int max_len) { switch (PlatformHelper.get_library_environment ()) { case LibraryEnvironment.x64: - return BoardControllerLibrary64.get_board_descr (board_id, preset, descr, len); + return BoardControllerLibrary64.get_board_descr (board_id, preset, descr, len, max_len); case LibraryEnvironment.x86: - return BoardControllerLibrary32.get_board_descr (board_id, preset, descr, len); + return BoardControllerLibrary32.get_board_descr (board_id, preset, descr, len, max_len); } return (int)BrainFlowExitCodes.GENERAL_ERROR; } - public static int get_device_name (int board_id, int preset, byte[] name, int[] len) + public static int get_device_name (int board_id, int preset, byte[] name, int[] len, int max_len) { switch (PlatformHelper.get_library_environment ()) { case LibraryEnvironment.x64: - return BoardControllerLibrary64.get_device_name (board_id, preset, name, len); + return BoardControllerLibrary64.get_device_name (board_id, preset, name, len, max_len); case LibraryEnvironment.x86: - return BoardControllerLibrary32.get_device_name (board_id, preset, name, len); + return BoardControllerLibrary32.get_device_name (board_id, preset, name, len, max_len); } return (int)BrainFlowExitCodes.GENERAL_ERROR; diff --git a/csharp_package/brainflow/brainflow/board_shim.cs b/csharp_package/brainflow/brainflow/board_shim.cs index 24211f8f3..47191e7d7 100644 --- a/csharp_package/brainflow/brainflow/board_shim.cs +++ b/csharp_package/brainflow/brainflow/board_shim.cs @@ -179,7 +179,7 @@ public static string[] get_eeg_names (int board_id, int preset = (int)BrainFlowP { int[] len = new int[1]; byte[] str = new byte[4096]; - int res = BoardControllerLibrary.get_eeg_names (board_id, preset, str, len); + int res = BoardControllerLibrary.get_eeg_names (board_id, preset, str, len, str.Length); if (res != (int)BrainFlowExitCodes.STATUS_OK) { throw new BrainFlowError (res); @@ -222,7 +222,7 @@ public static T get_board_descr (int board_id, int preset = (int)BrainFlowPre { int[] len = new int[1]; byte[] str = new byte[16000]; - int res = BoardControllerLibrary.get_board_descr (board_id, preset, str, len); + int res = BoardControllerLibrary.get_board_descr (board_id, preset, str, len, str.Length); if (res != (int)BrainFlowExitCodes.STATUS_OK) { throw new BrainFlowError (res); @@ -246,7 +246,7 @@ public static string get_device_name (int board_id, int preset = (int)BrainFlowP { int[] len = new int[1]; byte[] str = new byte[4096]; - int res = BoardControllerLibrary.get_device_name (board_id, preset, str, len); + int res = BoardControllerLibrary.get_device_name (board_id, preset, str, len, str.Length); if (res != (int)BrainFlowExitCodes.STATUS_OK) { throw new BrainFlowError (res); @@ -717,7 +717,7 @@ public string config_board (string config) { int[] len = new int[1]; byte[] str = new byte[4096]; - int res = BoardControllerLibrary.config_board (config, str, len, board_id, input_json); + int res = BoardControllerLibrary.config_board (config, str, len, str.Length, board_id, input_json); if (res != (int)BrainFlowExitCodes.STATUS_OK) { throw new BrainFlowError (res); diff --git a/java_package/brainflow/src/main/java/brainflow/BoardShim.java b/java_package/brainflow/src/main/java/brainflow/BoardShim.java index 7603178ae..bc02522a5 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardShim.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardShim.java @@ -23,7 +23,8 @@ private interface DllInterface extends Library { int prepare_session (int board_id, String params); - int config_board (String config, byte[] names, int[] len, int board_id, String params); + int config_board ( + String config, byte[] names, int[] len, int max_len, int board_id, String params); int config_board_with_bytes (byte[] bytes, int len, int board_id, String params); @@ -100,11 +101,11 @@ int get_current_board_data (int num_samples, int preset, double[] data_buf, int[ int is_prepared (int[] prepared, int board_id, String params); - int get_eeg_names (int board_id, int preset, byte[] names, int[] len); + int get_eeg_names (int board_id, int preset, byte[] names, int[] len, int max_len); - int get_board_descr (int board_id, int preset, byte[] names, int[] len); + int get_board_descr (int board_id, int preset, byte[] names, int[] len, int max_len); - int get_device_name (int board_id, int preset, byte[] name, int[] len); + int get_device_name (int board_id, int preset, byte[] name, int[] len, int max_len); int get_board_presets (int board_id, int[] presets, int[] len); @@ -509,7 +510,7 @@ public static String[] get_eeg_names (int board_id, BrainFlowPresets preset) thr { int[] len = new int[1]; byte[] str = new byte[4096]; - int ec = instance.get_eeg_names (board_id, preset.get_code (), str, len); + int ec = instance.get_eeg_names (board_id, preset.get_code (), str, len, str.length); if (ec != BrainFlowExitCode.STATUS_OK.get_code ()) { throw new BrainFlowError ("Error in board info getter", ec); @@ -574,7 +575,7 @@ public static T get_board_descr (Class type, int board_id, BrainFlowPrese { int[] len = new int[1]; byte[] str = new byte[16000]; - int ec = instance.get_board_descr (board_id, preset.get_code (), str, len); + int ec = instance.get_board_descr (board_id, preset.get_code (), str, len, str.length); if (ec != BrainFlowExitCode.STATUS_OK.get_code ()) { throw new BrainFlowError ("Error in board info getter", ec); @@ -617,7 +618,7 @@ public static String get_device_name (int board_id, BrainFlowPresets preset) thr { int[] len = new int[1]; byte[] str = new byte[4096]; - int ec = instance.get_device_name (board_id, preset.get_code (), str, len); + int ec = instance.get_device_name (board_id, preset.get_code (), str, len, str.length); if (ec != BrainFlowExitCode.STATUS_OK.get_code ()) { throw new BrainFlowError ("Error in board info getter", ec); @@ -1217,7 +1218,7 @@ public static int[] get_gyro_channels (int board_id, BrainFlowPresets preset) th */ public static int[] get_gyro_channels (int board_id) throws BrainFlowError { - return get_gyro_channels (board_id); + return get_gyro_channels (board_id, BrainFlowPresets.DEFAULT_PRESET); } /** @@ -1410,7 +1411,7 @@ public String config_board (String config) throws BrainFlowError { int[] len = new int[1]; byte[] str = new byte[4096]; - int ec = instance.config_board (config, str, len, board_id, input_json); + int ec = instance.config_board (config, str, len, str.length, board_id, input_json); if (ec != BrainFlowExitCode.STATUS_OK.get_code ()) { throw new BrainFlowError ("Error in config_board", ec); diff --git a/matlab_package/brainflow/BoardShim.m b/matlab_package/brainflow/BoardShim.m index 248e2aba3..629f878ec 100644 --- a/matlab_package/brainflow/BoardShim.m +++ b/matlab_package/brainflow/BoardShim.m @@ -147,10 +147,12 @@ function log_message(log_level, message) % get board descr for provided board id task_name = 'get_board_descr'; lib_name = BoardShim.load_lib(); + str_len = libpointer('int32Ptr', 0); % no way to understand how it works in matlab, used this link % https://nl.mathworks.com/matlabcentral/answers/131446-what-data-type-do-i-need-to-calllib-with-pointer-argument-char% - [exit_code, board_descr] = calllib(lib_name, task_name, board_id, preset, blanks(16000), 16000); + [exit_code, board_descr] = calllib(lib_name, task_name, board_id, preset, blanks(16000), str_len, 16000); BoardShim.check_ec(exit_code, task_name); + board_descr = extractBefore(board_descr, str_len.Value + 1); board_descr = jsondecode(board_descr); end @@ -158,10 +160,12 @@ function log_message(log_level, message) % get eeg names for provided board id task_name = 'get_eeg_names'; lib_name = BoardShim.load_lib(); + str_len = libpointer('int32Ptr', 0); % no way to understand how it works in matlab, used this link % https://nl.mathworks.com/matlabcentral/answers/131446-what-data-type-do-i-need-to-calllib-with-pointer-argument-char% - [exit_code, eeg_names] = calllib(lib_name, task_name, board_id, preset, blanks(4096), 4096); + [exit_code, eeg_names] = calllib(lib_name, task_name, board_id, preset, blanks(4096), str_len, 4096); BoardShim.check_ec(exit_code, task_name); + eeg_names = extractBefore(eeg_names, str_len.Value + 1); eeg_names = split(eeg_names, ','); end @@ -191,10 +195,12 @@ function log_message(log_level, message) % get device name for provided board id task_name = 'get_device_name'; lib_name = BoardShim.load_lib(); + str_len = libpointer('int32Ptr', 0); % no way to understand how it works in matlab used this link % https://nl.mathworks.com/matlabcentral/answers/131446-what-data-type-do-i-need-to-calllib-with-pointer-argument-char% - [exit_code, device_name] = calllib(lib_name, task_name, board_id, preset, blanks(4096), 4096); + [exit_code, device_name] = calllib(lib_name, task_name, board_id, preset, blanks(4096), str_len, 4096); BoardShim.check_ec(exit_code, task_name); + device_name = extractBefore(device_name, str_len.Value + 1); end function eeg_channels = get_eeg_channels(board_id, preset) @@ -380,10 +386,12 @@ function prepare_session(obj) % send string to the board task_name = 'config_board'; lib_name = BoardShim.load_lib(); + str_len = libpointer('int32Ptr', 0); % no way to understand how it works in matlab used this link % https://nl.mathworks.com/matlabcentral/answers/131446-what-data-type-do-i-need-to-calllib-with-pointer-argument-char% - [exit_code, tmp, response] = calllib(lib_name, task_name, config, blanks(4096), 4096, obj.board_id, obj.input_params_json); + [exit_code, tmp, response] = calllib(lib_name, task_name, config, blanks(4096), str_len, 4096, obj.board_id, obj.input_params_json); BoardShim.check_ec(exit_code, task_name); + response = extractBefore(response, str_len.Value + 1); end function config_board_with_bytes(obj, bytes) diff --git a/nodejs_package/brainflow/board_shim.ts b/nodejs_package/brainflow/board_shim.ts index 2a94e924e..1ac7ba63c 100644 --- a/nodejs_package/brainflow/board_shim.ts +++ b/nodejs_package/brainflow/board_shim.ts @@ -279,7 +279,7 @@ export class BoardShim const len = [0]; let out = ['\0'.repeat(4096)]; const res = BoardControllerDLL.getInstance().configBoard( - config, out, len, this.boardId, this.inputJson); + config, out, len, out[0].length, this.boardId, this.inputJson); if (res !== BrainFlowExitCodes.STATUS_OK) { throw new BrainFlowError (res, 'Could not config board'); @@ -674,8 +674,9 @@ export class BoardShim public static getDeviceName(boardId: BoardIds, preset = BrainFlowPresets.DEFAULT_PRESET): string { const len = [0]; - let out = ['\0'.repeat(512)]; - const res = BoardControllerDLL.getInstance().getDeviceName(boardId, preset, out, len); + let out = ['\0'.repeat(4096)]; + const res = + BoardControllerDLL.getInstance().getDeviceName(boardId, preset, out, len, out[0].length); if (res !== BrainFlowExitCodes.STATUS_OK) { throw new BrainFlowError (res, 'Could not get device info'); @@ -687,7 +688,8 @@ export class BoardShim { const len = [0]; let out = ['\0'.repeat(4096)]; - const res = BoardControllerDLL.getInstance().getEegNames(boardId, preset, out, len); + const res = + BoardControllerDLL.getInstance().getEegNames(boardId, preset, out, len, out[0].length); if (res !== BrainFlowExitCodes.STATUS_OK) { throw new BrainFlowError (res, 'Could not get device info'); @@ -698,8 +700,9 @@ export class BoardShim public static getBoardDescr(boardId: BoardIds, preset = BrainFlowPresets.DEFAULT_PRESET): Object { const len = [0]; - let out = ['\0'.repeat(4096)]; - const res = BoardControllerDLL.getInstance().getBoardDescr(boardId, preset, out, len); + let out = ['\0'.repeat(16000)]; + const res = + BoardControllerDLL.getInstance().getBoardDescr(boardId, preset, out, len, out[0].length); if (res !== BrainFlowExitCodes.STATUS_OK) { throw new BrainFlowError (res, 'Could not get device info'); diff --git a/nodejs_package/brainflow/functions.types.ts b/nodejs_package/brainflow/functions.types.ts index 7d171cc1d..7c60ead29 100644 --- a/nodejs_package/brainflow/functions.types.ts +++ b/nodejs_package/brainflow/functions.types.ts @@ -32,7 +32,7 @@ export enum BoardControllerCLikeFunctions { add_streamer = 'int add_streamer (const char *streamer, int preset, int board_id, const char *json_brainflow_input_params)', config_board = - 'int config_board (const char *config, _Inout_ char *response, _Inout_ int *resp_len, int board_id, const char *json_brainflow_input_params)', + 'int config_board (const char *config, _Inout_ char *response, _Inout_ int *resp_len, int response_max_len, int board_id, const char *json_brainflow_input_params)', config_board_with_bytes = 'int config_board_with_bytes (const char *bytes, int len, int board_id, const char *json_brainflow_input_params)', delete_streamer = @@ -88,11 +88,11 @@ export enum BoardControllerCLikeFunctions { get_rotation_channels = 'int get_rotation_channels (int board_id, int preset, _Inout_ int *channels, _Inout_ int *len)', get_eeg_names = - 'int get_eeg_names (int board_id, int preset, _Inout_ char *eeg_names, _Inout_ int *len)', + 'int get_eeg_names (int board_id, int preset, _Inout_ char *eeg_names, _Inout_ int *len, int max_len)', get_device_name = - 'int get_device_name (int board_id, int preset, _Inout_ char *device_name, _Inout_ int *len)', + 'int get_device_name (int board_id, int preset, _Inout_ char *device_name, _Inout_ int *len, int max_len)', get_board_descr = - 'int get_board_descr (int board_id, int preset, _Inout_ char *descr, _Inout_ int *len)', + 'int get_board_descr (int board_id, int preset, _Inout_ char *descr, _Inout_ int *len, int max_len)', get_board_presets = 'int get_board_presets (int board_id, _Inout_ int *presets, _Inout_ int *len)', } @@ -138,6 +138,7 @@ export class BoardControllerFunctions config: string, response: string[], responseLen: number[], + responseMaxLen: number, boardId: BoardIds, inputJson: string, ) => BrainFlowExitCodes; @@ -276,12 +277,14 @@ export class BoardControllerFunctions preset: BrainFlowPresets, names: string[], len: number[], + maxLen: number, ) => BrainFlowExitCodes; getDeviceName!: ( boardId: BoardIds, preset: BrainFlowPresets, name: string[], len: number[], + maxLen: number, ) => BrainFlowExitCodes; getBoardPresets!: ( boardId: BoardIds, @@ -293,6 +296,7 @@ export class BoardControllerFunctions preset: BrainFlowPresets, descr: string[], len: number[], + maxLen: number, ) => BrainFlowExitCodes; } @@ -471,4 +475,4 @@ export class MLModuleFunctions inputJson: string) => BrainFlowExitCodes; releaseAll!: () => BrainFlowExitCodes; release!: (inputJson: string) => BrainFlowExitCodes; -} \ No newline at end of file +} diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index faf57da7e..0bcdbf110 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -327,6 +327,7 @@ def __init__(self): ndpointer(ctypes.c_ubyte), ndpointer(ctypes.c_int32), ctypes.c_int, + ctypes.c_int, ctypes.c_char_p ] @@ -393,7 +394,8 @@ def __init__(self): ctypes.c_int, ctypes.c_int, ndpointer(ctypes.c_ubyte), - ndpointer(ctypes.c_int32) + ndpointer(ctypes.c_int32), + ctypes.c_int ] self.get_board_presets = self.lib.get_board_presets @@ -418,7 +420,8 @@ def __init__(self): ctypes.c_int, ctypes.c_int, ndpointer(ctypes.c_ubyte), - ndpointer(ctypes.c_int32) + ndpointer(ctypes.c_int32), + ctypes.c_int ] self.get_device_name = self.lib.get_device_name @@ -427,7 +430,8 @@ def __init__(self): ctypes.c_int, ctypes.c_int, ndpointer(ctypes.c_ubyte), - ndpointer(ctypes.c_int32) + ndpointer(ctypes.c_int32), + ctypes.c_int ] self.get_eeg_channels = self.lib.get_eeg_channels @@ -783,7 +787,8 @@ def get_eeg_names(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRE string = numpy.zeros(4096).astype(numpy.ubyte) string_len = numpy.zeros(1).astype(numpy.int32) - res = BoardControllerDLL.get_instance().get_eeg_names(board_id, preset, string, string_len) + res = BoardControllerDLL.get_instance().get_eeg_names( + board_id, preset, string, string_len, string.size) if res != BrainFlowExitCodes.STATUS_OK.value: raise BrainFlowError('unable to request info about this board', res) return string.tobytes().decode('utf-8')[0:string_len[0]].split(',') @@ -838,7 +843,8 @@ def get_board_descr(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_P string = numpy.zeros(16000).astype(numpy.ubyte) string_len = numpy.zeros(1).astype(numpy.int32) - res = BoardControllerDLL.get_instance().get_board_descr(board_id, preset, string, string_len) + res = BoardControllerDLL.get_instance().get_board_descr( + board_id, preset, string, string_len, string.size) if res != BrainFlowExitCodes.STATUS_OK.value: raise BrainFlowError('unable to request info about this board', res) return json.loads(string.tobytes().decode('utf-8')[0:string_len[0]]) @@ -858,7 +864,8 @@ def get_device_name(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_P string = numpy.zeros(4096).astype(numpy.ubyte) string_len = numpy.zeros(1).astype(numpy.int32) - res = BoardControllerDLL.get_instance().get_device_name(board_id, preset, string, string_len) + res = BoardControllerDLL.get_instance().get_device_name( + board_id, preset, string, string_len, string.size) if res != BrainFlowExitCodes.STATUS_OK.value: raise BrainFlowError('unable to request info about this board', res) return string.tobytes().decode('utf-8')[0:string_len[0]] @@ -1405,8 +1412,8 @@ def config_board(self, config) -> str: string = numpy.zeros(4096).astype(numpy.ubyte) string_len = numpy.zeros(1).astype(numpy.int32) - res = BoardControllerDLL.get_instance().config_board(config_string, string, string_len, self.board_id, - self.input_json) + res = BoardControllerDLL.get_instance().config_board( + config_string, string, string_len, string.size, self.board_id, self.input_json) if res != BrainFlowExitCodes.STATUS_OK.value: raise BrainFlowError('unable to config board', res) return string.tobytes().decode('utf-8')[0:string_len[0]] diff --git a/rust_package/brainflow/src/board_shim.rs b/rust_package/brainflow/src/board_shim.rs index 9abf1dec3..87859441b 100644 --- a/rust_package/brainflow/src/board_shim.rs +++ b/rust_package/brainflow/src/board_shim.rs @@ -226,6 +226,7 @@ impl BoardShim { config, result_char_buffer.as_mut_ptr(), &mut response_len, + result_char_buffer.len() as c_int, self.board_id as c_int, self.json_brainflow_input_params.as_ptr(), ); @@ -374,7 +375,13 @@ pub fn get_board_descr(board_id: BoardIds, preset: BrainFlowPresets) -> Result Result Result ::std::os::raw::c_int; } extern "C" { @@ -59,6 +60,7 @@ extern "C" { preset: ::std::os::raw::c_int, eeg_names: *mut ::std::os::raw::c_char, len: *mut ::std::os::raw::c_int, + max_len: ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } extern "C" { @@ -187,6 +189,7 @@ extern "C" { preset: ::std::os::raw::c_int, name: *mut ::std::os::raw::c_char, len: *mut ::std::os::raw::c_int, + max_len: ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } extern "C" { @@ -254,6 +257,7 @@ extern "C" { config: *const ::std::os::raw::c_char, response: *mut ::std::os::raw::c_char, response_len: *mut ::std::os::raw::c_int, + response_max_len: ::std::os::raw::c_int, board_id: ::std::os::raw::c_int, json_brainflow_input_params: *const ::std::os::raw::c_char, ) -> ::std::os::raw::c_int; diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index 3e52792af..9b27ef4b6 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -69,6 +69,8 @@ static int check_board_session (int board_id, const char *json_brainflow_input_p std::pair &key, bool log_error = true); static int string_to_brainflow_input_params ( const char *json_brainflow_input_params, struct BrainFlowInputParams *params); +static int copy_string_to_buffer ( + const std::string &source, char *destination, int *destination_len, int max_len); int prepare_session (int board_id, const char *json_brainflow_input_params) @@ -482,11 +484,12 @@ int java_set_jnienv (JNIEnv *java_jnienv) return (int)BrainFlowExitCodes::STATUS_OK; } -int config_board (const char *config, char *response, int *response_len, int board_id, +int config_board (const char *config, char *response, int *response_len, int response_max_len, + int board_id, const char *json_brainflow_input_params) { std::lock_guard lock (mutex); - if ((config == NULL) || (response == NULL) || (response_len == NULL)) + if ((config == NULL) || (response == NULL) || (response_len == NULL) || (response_max_len < 1)) { return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; } @@ -503,8 +506,7 @@ int config_board (const char *config, char *response, int *response_len, int boa res = board_it->second->config_board (conf, resp); if (res == (int)BrainFlowExitCodes::STATUS_OK) { - *response_len = (int)resp.length (); - strcpy (response, resp.c_str ()); + res = copy_string_to_buffer (resp, response, response_len, response_max_len); } return res; } @@ -547,6 +549,28 @@ int add_streamer ( return board_it->second->add_streamer (streamer, preset); } +static int copy_string_to_buffer ( + const std::string &source, char *destination, int *destination_len, int max_len) +{ + if ((destination == NULL) || (destination_len == NULL) || (max_len < 1)) + { + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + *destination_len = (int)source.size (); + if (((int)source.size () + 1) > max_len) + { + destination[0] = '\0'; + Board::board_logger->error ( + "provided output buffer is too small, required {}, got {}", source.size () + 1, + max_len); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + memcpy (destination, source.c_str (), source.size () + 1); + return (int)BrainFlowExitCodes::STATUS_OK; +} + int delete_streamer ( const char *streamer, int preset, int board_id, const char *json_brainflow_input_params) { diff --git a/src/board_controller/board_info_getter.cpp b/src/board_controller/board_info_getter.cpp index 4ddd5a1e9..679f6b678 100644 --- a/src/board_controller/board_info_getter.cpp +++ b/src/board_controller/board_info_getter.cpp @@ -13,10 +13,12 @@ static int get_single_value ( int board_id, int preset, const char *param_name, int *value, bool use_logger = true); static int get_string_value (int board_id, int preset, const char *param_name, char *string, - int *len, bool use_logger = true); + int *len, int max_len, bool use_logger = true); static int get_array_value (int board_id, int preset, const char *param_name, int *output_array, int *len, bool use_logger = true); static std::string get_preset_str (int preset); +static int copy_string_value ( + const std::string &value, char *destination, int *len, int max_len, bool use_logger); int get_board_presets (int board_id, int *presets, int *len) { @@ -63,7 +65,7 @@ int get_board_presets (int board_id, int *presets, int *len) } } -int get_board_descr (int board_id, int preset, char *board_descr, int *len) +int get_board_descr (int board_id, int preset, char *board_descr, int *len, int max_len) { std::string preset_str = get_preset_str (preset); try @@ -77,9 +79,7 @@ int get_board_descr (int board_id, int preset, char *board_descr, int *len) } else { - strcpy (board_descr, res.c_str ()); - *len = (int)strlen (res.c_str ()); - return (int)BrainFlowExitCodes::STATUS_OK; + return copy_string_value (res, board_descr, len, max_len, true); } } catch (json::exception &e) @@ -121,14 +121,14 @@ int get_timestamp_channel (int board_id, int preset, int *timestamp_channel) return get_single_value (board_id, preset, "timestamp_channel", timestamp_channel); } -int get_eeg_names (int board_id, int preset, char *eeg_names, int *len) +int get_eeg_names (int board_id, int preset, char *eeg_names, int *len, int max_len) { - return get_string_value (board_id, preset, "eeg_names", eeg_names, len); + return get_string_value (board_id, preset, "eeg_names", eeg_names, len, max_len); } -int get_device_name (int board_id, int preset, char *name, int *len) +int get_device_name (int board_id, int preset, char *name, int *len, int max_len) { - return get_string_value (board_id, preset, "name", name, len); + return get_string_value (board_id, preset, "name", name, len, max_len); } int get_eeg_channels (int board_id, int preset, int *eeg_channels, int *len) @@ -295,7 +295,8 @@ static int get_array_value ( } static int get_string_value ( - int board_id, int preset, const char *param_name, char *string, int *len, bool use_logger) + int board_id, int preset, const char *param_name, char *string, int *len, int max_len, + bool use_logger) { std::string preset_str = get_preset_str (preset); if (preset_str.empty ()) @@ -307,9 +308,7 @@ static int get_string_value ( std::string val = boards_struct .brainflow_boards_json["boards"][std::to_string (board_id)][preset_str][param_name]; - strcpy (string, val.c_str ()); - *len = (int)strlen (val.c_str ()); - return (int)BrainFlowExitCodes::STATUS_OK; + return copy_string_value (val, string, len, max_len, use_logger); } catch (json::exception &e) { @@ -344,4 +343,29 @@ static std::string get_preset_str (int preset) Board::board_logger->error ("unknown preset"); } return preset_str; -} \ No newline at end of file +} + +static int copy_string_value ( + const std::string &value, char *destination, int *len, int max_len, bool use_logger) +{ + if ((destination == NULL) || (len == NULL) || (max_len < 1)) + { + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + *len = (int)value.size (); + if (((int)value.size () + 1) > max_len) + { + destination[0] = '\0'; + if (use_logger) + { + Board::board_logger->error ( + "provided output buffer is too small, required {}, got {}", value.size () + 1, + max_len); + } + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + memcpy (destination, value.c_str (), value.size () + 1); + return (int)BrainFlowExitCodes::STATUS_OK; +} diff --git a/src/board_controller/inc/board_controller.h b/src/board_controller/inc/board_controller.h index d909a9df8..232a8a355 100644 --- a/src/board_controller/inc/board_controller.h +++ b/src/board_controller/inc/board_controller.h @@ -23,7 +23,8 @@ extern "C" SHARED_EXPORT int CALLING_CONVENTION get_board_data (int data_count, int preset, double *data_buf, int board_id, const char *json_brainflow_input_params); SHARED_EXPORT int CALLING_CONVENTION config_board (const char *config, char *response, - int *response_len, int board_id, const char *json_brainflow_input_params); + int *response_len, int response_max_len, int board_id, + const char *json_brainflow_input_params); SHARED_EXPORT int CALLING_CONVENTION config_board_with_bytes ( const char *bytes, int len, int board_id, const char *json_brainflow_input_params); SHARED_EXPORT int CALLING_CONVENTION is_prepared ( diff --git a/src/board_controller/inc/board_info_getter.h b/src/board_controller/inc/board_info_getter.h index 727040d3f..34ed7854a 100644 --- a/src/board_controller/inc/board_info_getter.h +++ b/src/board_controller/inc/board_info_getter.h @@ -8,7 +8,7 @@ extern "C" #endif // data and board desc methods SHARED_EXPORT int CALLING_CONVENTION get_board_descr ( - int board_id, int preset, char *board_descr, int *len); + int board_id, int preset, char *board_descr, int *len, int max_len); SHARED_EXPORT int CALLING_CONVENTION get_sampling_rate ( int board_id, int preset, int *sampling_rate); SHARED_EXPORT int CALLING_CONVENTION get_package_num_channel ( @@ -21,7 +21,7 @@ extern "C" int board_id, int preset, int *battery_channel); SHARED_EXPORT int CALLING_CONVENTION get_num_rows (int board_id, int preset, int *num_rows); SHARED_EXPORT int CALLING_CONVENTION get_eeg_names ( - int board_id, int preset, char *eeg_names, int *len); + int board_id, int preset, char *eeg_names, int *len, int max_len); SHARED_EXPORT int CALLING_CONVENTION get_exg_channels ( int board_id, int preset, int *exg_channels, int *len); SHARED_EXPORT int CALLING_CONVENTION get_eeg_channels ( @@ -53,7 +53,7 @@ extern "C" SHARED_EXPORT int CALLING_CONVENTION get_magnetometer_channels ( int board_id, int preset, int *magnetometer_channels, int *len); SHARED_EXPORT int CALLING_CONVENTION get_device_name ( - int board_id, int preset, char *name, int *len); + int board_id, int preset, char *name, int *len, int max_len); SHARED_EXPORT int CALLING_CONVENTION get_board_presets (int board_id, int *presets, int *len); #ifdef __cplusplus diff --git a/src/board_controller/inc/playback_file_board.h b/src/board_controller/inc/playback_file_board.h index cb59ba2d6..b3531b251 100644 --- a/src/board_controller/inc/playback_file_board.h +++ b/src/board_controller/inc/playback_file_board.h @@ -21,6 +21,7 @@ class PlaybackFileBoard : public Board bool initialized; std::vector> file_offsets; + int validate_file_access (const std::string &filename); void read_thread (int preset, std::string filename); int get_file_offsets (std::string filename, std::vector &offsets); diff --git a/src/board_controller/playback_file_board.cpp b/src/board_controller/playback_file_board.cpp index 847d9ad6f..38ea42067 100644 --- a/src/board_controller/playback_file_board.cpp +++ b/src/board_controller/playback_file_board.cpp @@ -132,6 +132,30 @@ int PlaybackFileBoard::start_stream (int buffer_size, const char *streamer_param { return res; } + if (!params.file.empty ()) + { + res = validate_file_access (params.file); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + } + if (!params.file_aux.empty ()) + { + res = validate_file_access (params.file_aux); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + } + if (!params.file_anc.empty ()) + { + res = validate_file_access (params.file_anc); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + } keep_alive = true; if (!params.file.empty ()) @@ -153,6 +177,18 @@ int PlaybackFileBoard::start_stream (int buffer_size, const char *streamer_param return (int)BrainFlowExitCodes::STATUS_OK; } +int PlaybackFileBoard::validate_file_access (const std::string &filename) +{ + FILE *fp = fopen (filename.c_str (), "rb"); + if (fp == NULL) + { + safe_logger (spdlog::level::err, "failed to open file {}", filename); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + fclose (fp); + return (int)BrainFlowExitCodes::STATUS_OK; +} + int PlaybackFileBoard::stop_stream () { if (keep_alive) diff --git a/src/board_controller/synthetic_board.cpp b/src/board_controller/synthetic_board.cpp index 09be30a02..9677ca130 100644 --- a/src/board_controller/synthetic_board.cpp +++ b/src/board_controller/synthetic_board.cpp @@ -61,8 +61,8 @@ int SyntheticBoard::start_stream (int buffer_size, const char *streamer_params) } keep_alive = true; - streaming_thread = std::thread ([this] { this->read_thread (); }); is_streaming = true; + streaming_thread = std::thread ([this] { this->read_thread (); }); return (int)BrainFlowExitCodes::STATUS_OK; } diff --git a/src/ml/onnx/onnx_classifier.cpp b/src/ml/onnx/onnx_classifier.cpp index c168ecce9..02d99724b 100644 --- a/src/ml/onnx/onnx_classifier.cpp +++ b/src/ml/onnx/onnx_classifier.cpp @@ -364,7 +364,7 @@ int OnnxClassifier::predict (double *data, int data_len, double *output, int *ou default: { safe_logger (spdlog::level::err, "unknown output type"); - res = (int)BrainFlowExitCodes::STATUS_OK; + res = (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; } } } @@ -387,7 +387,7 @@ int OnnxClassifier::predict (double *data, int data_len, double *output, int *ou delete[] float_data; } - return (int)BrainFlowExitCodes::STATUS_OK; + return res; } int OnnxClassifier::release () @@ -887,4 +887,4 @@ int OnnxClassifier::get_output_info () } return res; -} \ No newline at end of file +} From 5bfaaab43fe79ea6e90bff313f9d484f694de5ec Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 28 Apr 2026 21:31:54 +0700 Subject: [PATCH 2/3] generic improvements in muse device support Signed-off-by: Andrey Parfenov --- docs/SupportedBoards.rst | 244 ++++++++++++++++++ .../examples/tests/muse_anthena_eeg_p21.py | 41 +++ .../tests/muse_anthena_optics_plot.py | 118 +++++++++ .../examples/tests/muse_anthena_save_all.py | 48 ++++ src/board_controller/brainflow_boards.cpp | 12 +- src/board_controller/muse/inc/muse.h | 2 + src/board_controller/muse/inc/muse_anthena.h | 2 +- .../muse/inc/muse_anthena_constants.h | 7 +- src/board_controller/muse/inc/muse_options.h | 188 ++++++++++++++ src/board_controller/muse/muse.cpp | 14 +- src/board_controller/muse/muse_anthena.cpp | 174 +++---------- .../muse/muse_bglib/inc/muse_bglib_helper.h | 2 + .../muse/muse_bglib/muse_bglib_helper.cpp | 12 +- src/board_controller/muse/muse_bled.cpp | 14 +- 14 files changed, 731 insertions(+), 147 deletions(-) create mode 100644 python_package/examples/tests/muse_anthena_eeg_p21.py create mode 100644 python_package/examples/tests/muse_anthena_optics_plot.py create mode 100644 python_package/examples/tests/muse_anthena_save_all.py create mode 100644 src/board_controller/muse/inc/muse_options.h diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst index afe5a398d..603ac4ac9 100644 --- a/docs/SupportedBoards.rst +++ b/docs/SupportedBoards.rst @@ -841,6 +841,176 @@ Supported platforms: Muse ------ +.. _muse-presets-table: + +Muse preset commands: + +The table below summarizes startup Muse commands accepted by BrainFlow and the Muse device families where they are intended to be used. + +.. list-table:: + :header-rows: 1 + + * - Preset + - BrainFlow support + - Muse devices + - Confidence / use + - Description + * - :code:`p20` + - Accepted + - Muse 2016, 2018, 2019, 2021, 2024 + - Unverified for MuseS Anthena; older Muse preset. + - 5 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz. + * - :code:`p21` + - Accepted + - Muse 2016, 2018, 2019, 2021, 2024 + - Important EEG-only preset; tested/recommended for 4-channel EEG. + - 4 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz. + * - :code:`p50` + - Accepted + - Muse 2018, 2019, 2021, 2024 + - Unverified for MuseS Anthena; older Muse preset. + - 5 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz, PPG at 64 Hz. + * - :code:`p51` + - Accepted + - Muse 2018, 2019, 2021, 2024 + - Unverified for MuseS Anthena; older Muse preset. + - 4 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz, PPG at 64 Hz. + * - :code:`p60` + - Accepted + - Muse 2019, 2021 + - Unverified for MuseS Anthena; older Muse preset. + - 5 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz, PPG at 64 Hz, thermistor at 16 Hz. + * - :code:`p61` + - Accepted + - Muse 2019, 2021 + - Unverified for MuseS Anthena; older Muse preset. + - 4 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz, PPG at 64 Hz, thermistor at 16 Hz. + * - :code:`p1021` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz. + * - :code:`p1022` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 8 EEG channels, 14-bit EEG at 256 Hz, battery at 1 Hz, DRL/REF at 32 Hz. + * - :code:`p1023` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - Battery only at 5 Hz. + * - :code:`p1024` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - Accelerometer and gyro at 52 Hz. + * - :code:`p1025` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 16 optics channels at 64 Hz, low power. + * - :code:`p1026` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 16 optics channels at 64 Hz, high power. + * - :code:`p1027` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 8 optics channels at 64 Hz, low power. + * - :code:`p1028` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 8 optics channels at 64 Hz, high power. + * - :code:`p1029` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 4 optics channels at 64 Hz, low power. + * - :code:`p102a` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 4 optics channels at 64 Hz, high power. + * - :code:`p1031` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 16 optics channels at 64 Hz, low power. + * - :code:`p1032` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 16 optics channels at 64 Hz, high power. + * - :code:`p1033` + - Not accepted + - MuseS Anthena + - Known MuseS Anthena command, not BrainFlow-enabled yet. + - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 8 optics channels at 64 Hz, low power. + * - :code:`p1034` + - Accepted + - MuseS Anthena + - Accepted but not device-tested in this work. + - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 8 optics channels at 64 Hz, high power. + * - :code:`p1035` + - Accepted + - MuseS Anthena + - Important/tested; recommended for 4 EEG plus 4 optics. + - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 4 optics channels at 64 Hz, low power. + * - :code:`p1036` + - Not accepted + - MuseS Anthena + - Important missing high-power pair for :code:`p1035`. + - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 4 optics channels at 64 Hz, high power. + * - :code:`p1041` + - Accepted + - MuseS Anthena + - Important/tested; default recommended full-data preset. + - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 16 optics channels at 64 Hz, low power. This is the default BrainFlow preset. + * - :code:`p1042` + - Accepted + - MuseS Anthena + - Accepted but not device-tested in this work. + - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 16 optics channels at 64 Hz, high power. + * - :code:`p1043` + - Accepted + - MuseS Anthena + - Accepted but not device-tested in this work. + - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 8 optics channels at 64 Hz, low power. + * - :code:`p1044` + - Accepted + - MuseS Anthena + - Accepted but not device-tested in this work. + - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 8 optics channels at 64 Hz, high power. + * - :code:`p1045` + - Accepted + - MuseS Anthena + - Tested; useful for comparing 8 EEG plus 4 optics low power. + - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 4 optics channels at 64 Hz, low power. + * - :code:`p1046` + - Accepted + - MuseS Anthena + - Tested; high-power pair for :code:`p1045`. + - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 4 optics channels at 64 Hz, high power. + * - :code:`p4129` + - Accepted + - Unknown + - Uncertain; accepted by BrainFlow but not tied to a documented Muse device family. + - Not tied to a documented Muse device family. + +Low power and high power optics presets: + +- Low power and high power variants have the same sampling rate and the same number of channels. +- Low power uses lower optical emitter intensity. It uses less battery and is less likely to saturate the optical signal. +- High power uses stronger optical emitter intensity. It can produce larger optical values and may help with weaker optical contact, but uses more battery and has a higher risk of saturation. +- BrainFlow does not document the exact optical emitter current for these modes. + +For legacy Muse boards, :code:`BrainFlowInputParams.other_info` can select the startup Muse command. Use a shorthand such as :code:`p21` or a key-value form such as :code:`preset=p21`. If :code:`other_info` is empty, BrainFlow uses :code:`p21`. Muse 2016 boards accept :code:`p20` and :code:`p21`; Muse 2 boards accept :code:`p20`, :code:`p21`, :code:`p50`, and :code:`p51`; Muse S boards accept :code:`p20`, :code:`p21`, :code:`p50`, :code:`p51`, :code:`p60`, and :code:`p61`. :code:`low_latency` is only supported for MuseS Anthena. + + Muse S BLED ~~~~~~~~~~~~~~ @@ -859,6 +1029,7 @@ To create such board you need to specify the following board ID and fields of Br - :code:`BoardIds.MUSE_S_BLED_BOARD` - :code:`serial_port`, e.g. COM3, /dev/ttyACM0 - *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps +- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p61` or :code:`preset=p61` Initialization Example: @@ -875,6 +1046,8 @@ Supported platforms: - Linux - Devices like Raspberry Pi +BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets. + Available :ref:`presets-label`: - :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, to enable 5th EEG channel use :code:`board.config_board("p50")` @@ -900,6 +1073,7 @@ To create such board you need to specify the following board ID and fields of Br - :code:`BoardIds.MUSE_2_BLED_BOARD` - :code:`serial_port`, e.g. COM3, /dev/ttyACM0 - *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps +- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p51` or :code:`preset=p51` Initialization Example: @@ -917,6 +1091,8 @@ Supported platforms: - Linux - Devices like Raspberry Pi +BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets. + Available :ref:`presets-label`: - :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, to enable 5th EEG channel use :code:`board.config_board("p50")` @@ -942,6 +1118,7 @@ To create such board you need to specify the following board ID and fields of Br - :code:`BoardIds.MUSE_2016_BLED_BOARD` - :code:`serial_port`, e.g. COM3, /dev/ttyACM0 - *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps +- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p21` or :code:`preset=p21` Initialization Example: @@ -958,6 +1135,8 @@ Supported platforms: - Linux - Devices like Raspberry Pi +BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets. + Available :ref:`presets-label`: - :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data @@ -985,6 +1164,7 @@ To create such board you need to specify the following board ID and fields of Br - :code:`BoardIds.MUSE_S_BOARD` - *optional:* :code:`mac_address`, mac address of the device to connect - *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps +- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p61` or :code:`preset=p61` Initialization Example: @@ -1000,6 +1180,8 @@ Supported platforms: - Linux, compilation from source code probably will be needed - Devices like Raspberry Pi +BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets. + Available :ref:`presets-label`: - :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, to enable 5th EEG channel use :code:`board.config_board("p50")` @@ -1007,6 +1189,62 @@ Available :ref:`presets-label`: - :code:`BrainFlowPresets.ANCILLARY_PRESET`, it contains PPG data, to enable it use :code:`board.config_board("p61")` +MuseS Anthena +~~~~~~~~~~~~~~ + +.. image:: https://live.staticflickr.com/65535/55236436914_6e442f3192.jpg + :width: 500px + :height: 500px + +`Muse Website `_ + +.. compound:: + + On Linux systems you may need to install `libdbus` and we recommend to compile BrainFlow from the source code: :: + + sudo apt-get install libdbus-1-dev # for ubuntu + sudo yum install dbus-devel # for centos + python3 tools/build.py --ble # to compile + +To create such board you need to specify the following board ID and fields of BrainFlowInputParams object: + +- :code:`BoardIds.MUSE_S_ANTHENA_BOARD` +- *optional:* :code:`mac_address`, mac address of the device to connect +- *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discovered via mobile apps +- *optional:* :code:`other_info`, MuseS Anthena startup options + +Initialization Example: + +.. code-block:: python + + params = BrainFlowInputParams() + params.other_info = "preset=p1041;low_latency=true" + board = BoardShim(BoardIds.MUSE_S_ANTHENA_BOARD, params) + +Supported platforms: + +- Windows 10.0.19041.0+ +- MacOS 10.15+, 12.0 to 12.2 have known issues while scanning, you need to update to 12.3+. On MacOS 12+ you may need to configure Bluetooth permissions for your application +- Linux, compilation from source code probably will be needed +- Devices like Raspberry Pi + +Available :code:`other_info` options: + +- If :code:`other_info` is empty, BrainFlow uses :code:`preset=p1041;low_latency=true`. +- :code:`other_info` can be a preset shorthand, for example :code:`p1041`. +- :code:`other_info` can be a semicolon-separated key-value string, for example :code:`preset=p1041;low_latency=false`. +- :code:`preset` selects the Muse streaming preset. BrainFlow accepts :code:`p20`, :code:`p21`, :code:`p50`, :code:`p51`, :code:`p60`, :code:`p61`, :code:`p1034`, :code:`p1035`, :code:`p1041`, :code:`p1042`, :code:`p1043`, :code:`p1044`, :code:`p1045`, :code:`p1046`, and :code:`p4129`. +- :code:`low_latency` can be :code:`true` or :code:`false`. If enabled, BrainFlow sends the :code:`L1` command after starting the stream. + +BrainFlow uses Muse command :code:`p1041` by default for this board. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets. + +Available :ref:`presets-label`: + +- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, sampling rate is 256 Hz. For 4-channel Muse presets BrainFlow exposes :code:`TP9`, :code:`AF7`, :code:`AF8`, and :code:`TP10` as EEG channels. For 8-channel Muse presets the additional Muse EEG values are exposed as other channels. +- :code:`BrainFlowPresets.AUXILIARY_PRESET`, it contains Accelerometer and Gyro data, sampling rate is 52 Hz. +- :code:`BrainFlowPresets.ANCILLARY_PRESET`, it contains optics and battery data. Optics sampling rate is 64 Hz. MuseS Anthena uses optics data for PPG, and BrainFlow exposes this data as optical channels instead of PPG channels. Depending on selected Muse preset, the stream contains 4, 8, or 16 optical channels. + + Muse 2 ~~~~~~~~~~~~~~ @@ -1029,6 +1267,7 @@ To create such board you need to specify the following board ID and fields of Br - :code:`BoardIds.MUSE_2_BOARD` - *optional:* :code:`mac_address`, mac address of the device to connect - *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps +- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p51` or :code:`preset=p51` Initialization Example: @@ -1044,6 +1283,8 @@ Supported platforms: - Linux, compilation from source code probably will be needed - Devices like Raspberry Pi +BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets. + Available :ref:`presets-label`: - :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, to enable 5th EEG channel use :code:`board.config_board("p50")` @@ -1073,6 +1314,7 @@ To create such board you need to specify the following board ID and fields of Br - :code:`BoardIds.MUSE_2016_BOARD` - *optional:* :code:`mac_address`, mac address of the device to connect - *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps +- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p21` or :code:`preset=p21` Initialization Example: @@ -1088,6 +1330,8 @@ Supported platforms: - Linux, compilation from source code probably will be needed - Devices like Raspberry Pi +BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets. + Available :ref:`presets-label`: - :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data diff --git a/python_package/examples/tests/muse_anthena_eeg_p21.py b/python_package/examples/tests/muse_anthena_eeg_p21.py new file mode 100644 index 000000000..471e96960 --- /dev/null +++ b/python_package/examples/tests/muse_anthena_eeg_p21.py @@ -0,0 +1,41 @@ +import argparse +import time + +from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets + + +def main(): + BoardShim.enable_dev_board_logger() + + parser = argparse.ArgumentParser() + parser.add_argument('--duration', type=int, required=False, default=10) + parser.add_argument('--mac-address', type=str, required=False, default='') + parser.add_argument('--serial-number', type=str, required=False, default='') + parser.add_argument('--timeout', type=int, required=False, default=0) + args = parser.parse_args() + + params = BrainFlowInputParams() + params.mac_address = args.mac_address + params.serial_number = args.serial_number + params.timeout = args.timeout + params.other_info = 'preset=p21;low_latency=true' + + board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value + board = BoardShim(board_id, params) + + try: + board.prepare_session() + board.start_stream() + try: + time.sleep(args.duration) + eeg_data = board.get_board_data(preset=BrainFlowPresets.DEFAULT_PRESET) + print(eeg_data) + finally: + board.stop_stream() + finally: + if board.is_prepared(): + board.release_session() + + +if __name__ == '__main__': + main() diff --git a/python_package/examples/tests/muse_anthena_optics_plot.py b/python_package/examples/tests/muse_anthena_optics_plot.py new file mode 100644 index 000000000..9c1f88658 --- /dev/null +++ b/python_package/examples/tests/muse_anthena_optics_plot.py @@ -0,0 +1,118 @@ +import argparse +import math +import time + +import matplotlib + +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import numpy as np + +from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets + + +def print_summary(optics_data, timestamps, sampling_rate, channel_labels): + print(f'optics samples: {optics_data.shape[1]}') + + if timestamps.size > 1 and np.all(np.isfinite(timestamps)): + timestamp_diffs = np.diff(timestamps) + timestamp_diffs = timestamp_diffs[timestamp_diffs > 0] + if timestamp_diffs.size > 0: + actual_rate = 1.0 / np.median(timestamp_diffs) + duration = timestamps[-1] - timestamps[0] + print(f'timestamp duration: {duration:.3f} sec') + print(f'timestamp sampling rate: {actual_rate:.2f} Hz, expected: {sampling_rate} Hz') + + nan_count = int(np.isnan(optics_data).sum()) + print(f'nan values: {nan_count}') + + for label, row in zip(channel_labels, optics_data): + finite = row[np.isfinite(row)] + if finite.size == 0: + print(f'{label}: no finite values') + continue + stddev = float(np.std(finite)) + print( + f'{label}: min={np.min(finite):.3f}, max={np.max(finite):.3f}, ' + f'mean={np.mean(finite):.3f}, std={stddev:.3f}' + ) + if stddev < 1e-9: + print(f'{label}: flat signal') + + +def plot_optics(optics_data, timestamps, sampling_rate, output_file, channel_labels): + if timestamps.size == optics_data.shape[1] and timestamps.size > 0 and np.all(np.isfinite(timestamps)): + x_axis = timestamps - timestamps[0] + x_label = 'Time, sec' + else: + x_axis = np.arange(optics_data.shape[1]) / sampling_rate + x_label = 'Samples' + + num_channels = optics_data.shape[0] + cols = min(4, num_channels) + rows = math.ceil(num_channels / cols) + fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 2.4 * rows), sharex=True) + axes = np.atleast_1d(axes).reshape(-1) + + for index, axis in enumerate(axes): + if index < num_channels: + axis.plot(x_axis, optics_data[index]) + axis.set_title(channel_labels[index]) + axis.grid(True) + else: + axis.axis('off') + + for axis in axes[-cols:]: + axis.set_xlabel(x_label) + + fig.tight_layout() + fig.savefig(output_file, dpi=150) + print(f'wrote plot: {output_file}') + + +def main(): + BoardShim.enable_board_logger() + + parser = argparse.ArgumentParser() + parser.add_argument('--duration', type=int, required=False, default=20) + parser.add_argument('--mac-address', type=str, required=False, default='') + parser.add_argument('--serial-number', type=str, required=False, default='') + parser.add_argument('--timeout', type=int, required=False, default=0) + parser.add_argument('--other-info', type=str, required=False, default='preset=p1035;low_latency=true') + parser.add_argument('--output-file', type=str, required=False, default='muse_anthena_optics_p1035.png') + args = parser.parse_args() + + params = BrainFlowInputParams() + params.mac_address = args.mac_address + params.serial_number = args.serial_number + params.timeout = args.timeout + params.other_info = args.other_info + + board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value + preset = BrainFlowPresets.ANCILLARY_PRESET + optical_channels = BoardShim.get_optical_channels(board_id, preset) + timestamp_channel = BoardShim.get_timestamp_channel(board_id, preset) + sampling_rate = BoardShim.get_sampling_rate(board_id, preset) + + board = BoardShim(board_id, params) + try: + board.prepare_session() + board.start_stream() + try: + time.sleep(args.duration) + data = board.get_board_data(preset=preset) + finally: + board.stop_stream() + finally: + if board.is_prepared(): + board.release_session() + + optics_data = data[optical_channels, :] + timestamps = data[timestamp_channel, :] + channel_labels = [f'Optics {channel}' for channel in optical_channels] + print_summary(optics_data, timestamps, sampling_rate, channel_labels) + plot_optics(optics_data, timestamps, sampling_rate, args.output_file, channel_labels) + + +if __name__ == '__main__': + main() diff --git a/python_package/examples/tests/muse_anthena_save_all.py b/python_package/examples/tests/muse_anthena_save_all.py new file mode 100644 index 000000000..d3a86b94f --- /dev/null +++ b/python_package/examples/tests/muse_anthena_save_all.py @@ -0,0 +1,48 @@ +import argparse +import time + +from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets +from brainflow.data_filter import DataFilter + + +def main(): + BoardShim.enable_dev_board_logger() + + parser = argparse.ArgumentParser() + parser.add_argument('--duration', type=int, required=False, default=10) + parser.add_argument('--mac-address', type=str, required=False, default='') + parser.add_argument('--serial-number', type=str, required=False, default='') + parser.add_argument('--timeout', type=int, required=False, default=0) + parser.add_argument('--output-prefix', type=str, required=False, default='muse_anthena') + args = parser.parse_args() + + params = BrainFlowInputParams() + params.mac_address = args.mac_address + params.serial_number = args.serial_number + params.timeout = args.timeout + params.other_info = 'preset=p1041;low_latency=true' + + board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value + board = BoardShim(board_id, params) + + try: + board.prepare_session() + board.start_stream() + try: + time.sleep(args.duration) + eeg_data = board.get_board_data(preset=BrainFlowPresets.DEFAULT_PRESET) + accel_data = board.get_board_data(preset=BrainFlowPresets.AUXILIARY_PRESET) + optics_data = board.get_board_data(preset=BrainFlowPresets.ANCILLARY_PRESET) + finally: + board.stop_stream() + finally: + if board.is_prepared(): + board.release_session() + + DataFilter.write_file(eeg_data, f'{args.output_prefix}_eeg.csv', 'w') + DataFilter.write_file(accel_data, f'{args.output_prefix}_accel_gyro.csv', 'w') + DataFilter.write_file(optics_data, f'{args.output_prefix}_optics_battery.csv', 'w') + + +if __name__ == '__main__': + main() diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index 42fa18bc5..388a9a175 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -420,7 +420,8 @@ BrainFlowBoards::BrainFlowBoards() {"marker_channel", 5}, {"package_num_channel", 0}, {"num_rows", 6}, - {"ppg_channels", {1, 2, 3}} + {"ppg_channels", {1, 2, 3}}, + {"optical_channels", {1, 2, 3}} }; brainflow_boards_json["boards"]["22"]["default"] = { @@ -453,7 +454,8 @@ BrainFlowBoards::BrainFlowBoards() {"marker_channel", 5}, {"package_num_channel", 0}, {"num_rows", 6}, - {"ppg_channels", {1, 2, 3}} + {"ppg_channels", {1, 2, 3}}, + {"optical_channels", {1, 2, 3}} }; brainflow_boards_json["boards"]["23"]["default"] = { @@ -681,7 +683,8 @@ BrainFlowBoards::BrainFlowBoards() {"marker_channel", 5}, {"package_num_channel", 0}, {"num_rows", 6}, - {"ppg_channels", {1, 2, 3}} + {"ppg_channels", {1, 2, 3}}, + {"optical_channels", {1, 2, 3}} }; brainflow_boards_json["boards"]["39"]["default"] = { @@ -714,7 +717,8 @@ BrainFlowBoards::BrainFlowBoards() {"marker_channel", 5}, {"package_num_channel", 0}, {"num_rows", 6}, - {"ppg_channels", {1, 2, 3}} + {"ppg_channels", {1, 2, 3}}, + {"optical_channels", {1, 2, 3}} }; brainflow_boards_json["boards"]["40"]["default"] = { diff --git a/src/board_controller/muse/inc/muse.h b/src/board_controller/muse/inc/muse.h index 31a281715..3be703401 100644 --- a/src/board_controller/muse/inc/muse.h +++ b/src/board_controller/muse/inc/muse.h @@ -3,6 +3,7 @@ #include "ble_lib_board.h" #include #include +#include #include #include @@ -29,6 +30,7 @@ class Muse : public BLELibBoard double last_ppg_timestamp; // used for timestamp correction double last_eeg_timestamp; // used for timestamp correction double last_aux_timestamp; // used for timestamp correction + std::string muse_preset; public: Muse (int board_id, struct BrainFlowInputParams params); diff --git a/src/board_controller/muse/inc/muse_anthena.h b/src/board_controller/muse/inc/muse_anthena.h index 984d507c7..b9c2ce797 100644 --- a/src/board_controller/muse/inc/muse_anthena.h +++ b/src/board_controller/muse/inc/muse_anthena.h @@ -57,7 +57,7 @@ class MuseAnthena : public BLELibBoard std::string bytes_to_string (const uint8_t *data, size_t size); void handle_data_notification (const uint8_t *data, size_t size); void parse_sensor_payload ( - uint8_t tag, uint8_t sequence_num, double host_timestamp, const uint8_t *data, size_t size); + uint8_t tag, uint32_t package_num, double host_timestamp, const uint8_t *data, size_t size); bool get_sensor_config (uint8_t tag, SensorConfig &config); int get_optics_canonical_index (uint8_t tag, int channel); void reset_timestamps (); diff --git a/src/board_controller/muse/inc/muse_anthena_constants.h b/src/board_controller/muse/inc/muse_anthena_constants.h index 9c5c0692e..c004b82b1 100644 --- a/src/board_controller/muse/inc/muse_anthena_constants.h +++ b/src/board_controller/muse/inc/muse_anthena_constants.h @@ -9,7 +9,8 @@ #define MUSE_ANTHENA_GATT_DATA_2 "273e0014-4c4d-454d-96be-f03bac821358" // info for equations -#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.0000610352 -#define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.0074768 -#define MUSE_ANTHENA_EEG_SCALE_FACTOR (1450.0 / 16383.0) +#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.00006103515625 +#define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.007476806640625 +#define MUSE_ANTHENA_EEG_SCALE_FACTOR 0.40293040293040294 #define MUSE_ANTHENA_OPTICS_SCALE_FACTOR 1.0 +#define MUSE_ANTHENA_BATTERY_PERCENT_SCALE_FACTOR (1.0 / 512.0) diff --git a/src/board_controller/muse/inc/muse_options.h b/src/board_controller/muse/inc/muse_options.h new file mode 100644 index 000000000..880b250b2 --- /dev/null +++ b/src/board_controller/muse/inc/muse_options.h @@ -0,0 +1,188 @@ +#pragma once + +#include +#include +#include +#include + +#include "brainflow_constants.h" + + +namespace MuseOptions +{ + enum class PresetFamily + { + Legacy, + Anthena + }; + + inline std::string trim_string (const std::string &value) + { + size_t first = value.find_first_not_of (" \t\r\n"); + if (first == std::string::npos) + { + return ""; + } + size_t last = value.find_last_not_of (" \t\r\n"); + return value.substr (first, last - first + 1); + } + + inline std::string to_lower (const std::string &value) + { + std::string lower_value = value; + std::transform (lower_value.begin (), lower_value.end (), lower_value.begin (), + [] (unsigned char ch) { return (char)std::tolower (ch); }); + return lower_value; + } + + inline bool parse_bool_option (const std::string &value, bool &parsed) + { + std::string lower_value = to_lower (trim_string (value)); + if (lower_value == "true") + { + parsed = true; + return true; + } + if (lower_value == "false") + { + parsed = false; + return true; + } + return false; + } + + inline bool is_valid_anthena_preset (const std::string &preset) + { + static const char *valid_presets[] = {"p20", "p21", "p50", "p51", "p60", "p61", "p1034", + "p1035", "p1041", "p1042", "p1043", "p1044", "p1045", "p1046", "p4129"}; + + for (size_t i = 0; i < sizeof (valid_presets) / sizeof (valid_presets[0]); i++) + { + if (preset == valid_presets[i]) + { + return true; + } + } + return false; + } + + inline bool is_valid_legacy_preset (int board_id, const std::string &preset) + { + if ((preset == "p20") || (preset == "p21")) + { + return true; + } + + bool is_muse_2016 = (board_id == (int)BoardIds::MUSE_2016_BOARD) || + (board_id == (int)BoardIds::MUSE_2016_BLED_BOARD); + if ((preset == "p50") || (preset == "p51")) + { + return !is_muse_2016; + } + + bool is_muse_s = (board_id == (int)BoardIds::MUSE_S_BOARD) || + (board_id == (int)BoardIds::MUSE_S_BLED_BOARD); + if ((preset == "p60") || (preset == "p61")) + { + return is_muse_s; + } + + return false; + } + + inline bool is_valid_preset (int board_id, PresetFamily family, const std::string &preset) + { + if (family == PresetFamily::Anthena) + { + return is_valid_anthena_preset (preset); + } + return is_valid_legacy_preset (board_id, preset); + } + + inline bool parse_preset_options (const std::string &other_info, int board_id, + PresetFamily family, bool allow_low_latency, std::string &preset, bool &enable_low_latency, + std::string &error) + { + std::string trimmed = trim_string (other_info); + if (trimmed.empty ()) + { + return true; + } + + if ((trimmed.find ('=') == std::string::npos) && (trimmed.find (';') == std::string::npos)) + { + std::string parsed_preset = to_lower (trimmed); + if (!is_valid_preset (board_id, family, parsed_preset)) + { + error = "invalid Muse preset: " + trimmed; + return false; + } + preset = parsed_preset; + return true; + } + + bool has_options = false; + std::stringstream ss (trimmed); + std::string token; + while (std::getline (ss, token, ';')) + { + token = trim_string (token); + if (token.empty ()) + { + continue; + } + + size_t separator = token.find ('='); + if ((separator == std::string::npos) || + (token.find ('=', separator + 1) != std::string::npos)) + { + error = "invalid Muse other_info option: " + token + ". Expected key=value"; + return false; + } + + std::string key = to_lower (trim_string (token.substr (0, separator))); + std::string value = trim_string (token.substr (separator + 1)); + if (key.empty () || value.empty ()) + { + error = "invalid Muse other_info option: " + token + ". Empty key or value"; + return false; + } + + if (key == "preset") + { + value = to_lower (value); + if (!is_valid_preset (board_id, family, value)) + { + error = "invalid Muse preset: " + value; + return false; + } + preset = value; + has_options = true; + } + else if ((key == "low_latency") && allow_low_latency) + { + bool parsed = false; + if (!parse_bool_option (value, parsed)) + { + error = "invalid Muse low_latency value: " + value; + return false; + } + enable_low_latency = parsed; + has_options = true; + } + else + { + error = "unknown Muse other_info option: " + key; + return false; + } + } + + if (!has_options) + { + error = "empty Muse other_info"; + return false; + } + + return true; + } +} // namespace MuseOptions diff --git a/src/board_controller/muse/muse.cpp b/src/board_controller/muse/muse.cpp index 390bbf5ff..5839b5f43 100644 --- a/src/board_controller/muse/muse.cpp +++ b/src/board_controller/muse/muse.cpp @@ -4,6 +4,7 @@ #include "custom_cast.h" #include "muse.h" #include "muse_constants.h" +#include "muse_options.h" #include "timestamp.h" #include @@ -85,6 +86,7 @@ Muse::Muse (int board_id, struct BrainFlowInputParams params) : BLELibBoard (boa last_ppg_timestamp = -1.0; last_eeg_timestamp = -1.0; last_aux_timestamp = -1.0; + muse_preset = "p21"; } Muse::~Muse () @@ -104,6 +106,16 @@ int Muse::prepare_session () { params.timeout = 6; } + muse_preset = "p21"; + bool unused_low_latency = false; + std::string parse_error; + if (!MuseOptions::parse_preset_options (params.other_info, board_id, + MuseOptions::PresetFamily::Legacy, false, muse_preset, unused_low_latency, parse_error)) + { + safe_logger (spdlog::level::err, "Invalid Muse other_info: {}", parse_error); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + safe_logger (spdlog::level::info, "Use Muse preset {}", muse_preset); safe_logger (spdlog::level::info, "Use timeout for discovery: {}", params.timeout); if (!init_dll_loader ()) { @@ -426,7 +438,7 @@ int Muse::prepare_session () } if (res == (int)BrainFlowExitCodes::STATUS_OK) { - res = config_board ("p21"); + res = config_board (muse_preset); } else { diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp index dd1b29e8a..28c1c9aff 100644 --- a/src/board_controller/muse/muse_anthena.cpp +++ b/src/board_controller/muse/muse_anthena.cpp @@ -1,7 +1,5 @@ #include "muse_anthena.h" -#include -#include #include #include #include @@ -12,6 +10,7 @@ #include "custom_cast.h" #include "muse_anthena_constants.h" +#include "muse_options.h" #include "timestamp.h" @@ -60,13 +59,15 @@ bool MuseAnthena::get_sensor_config (uint8_t tag, SensorConfig &config) config = SensorConfig (SensorType::ACC_GYRO, 6, 3, 52.0, 36); return true; case 0x53: - config = SensorConfig (SensorType::UNKNOWN, 0, 0, 0.0, 24); + // DRL/REF: 2 channels, 6 samples at 32 Hz. BrainFlow does not expose it for + // Muse Anthena, but the fixed length is needed to skip the block correctly. + config = SensorConfig (SensorType::UNKNOWN, 2, 6, 32.0, 24); return true; case 0x88: - config = SensorConfig (SensorType::BATTERY, 1, 1, 0.2, 0, true); + config = SensorConfig (SensorType::BATTERY, 10, 1, 1.0, 20); return true; case 0x98: - config = SensorConfig (SensorType::BATTERY, 1, 1, 1.0, 20); + config = SensorConfig (SensorType::BATTERY, 10, 1, 0.1, 20); return true; default: return false; @@ -75,17 +76,21 @@ bool MuseAnthena::get_sensor_config (uint8_t tag, SensorConfig &config) int MuseAnthena::get_optics_canonical_index (uint8_t tag, int channel) { - static const int optics4_indexes[] = {4, 5, 6, 7}; - + int num_channels = 0; if (tag == 0x34) { - return (channel >= 0 && channel < 4) ? optics4_indexes[channel] : -1; + num_channels = 4; } - if ((tag == 0x35) && (channel >= 0) && (channel < 8)) + else if (tag == 0x35) { - return channel; + num_channels = 8; } - if ((tag == 0x36) && (channel >= 0) && (channel < 16)) + else if (tag == 0x36) + { + num_channels = 16; + } + + if ((channel >= 0) && (channel < num_channels)) { return channel; } @@ -94,52 +99,22 @@ int MuseAnthena::get_optics_canonical_index (uint8_t tag, int channel) std::string MuseAnthena::trim_string (const std::string &value) { - size_t first = value.find_first_not_of (" \t\r\n"); - if (first == std::string::npos) - { - return ""; - } - size_t last = value.find_last_not_of (" \t\r\n"); - return value.substr (first, last - first + 1); + return MuseOptions::trim_string (value); } std::string MuseAnthena::to_lower (const std::string &value) { - std::string lower_value = value; - std::transform (lower_value.begin (), lower_value.end (), lower_value.begin (), - [] (unsigned char ch) { return (char)std::tolower (ch); }); - return lower_value; + return MuseOptions::to_lower (value); } bool MuseAnthena::is_valid_muse_preset (const std::string &preset) { - static const char *valid_presets[] = {"p20", "p21", "p50", "p51", "p60", "p61", "p1034", - "p1035", "p1041", "p1042", "p1043", "p1044", "p1045", "p1046", "p4129"}; - - for (size_t i = 0; i < sizeof (valid_presets) / sizeof (valid_presets[0]); i++) - { - if (preset == valid_presets[i]) - { - return true; - } - } - return false; + return MuseOptions::is_valid_anthena_preset (preset); } bool MuseAnthena::parse_bool_option (const std::string &value, bool &parsed) { - std::string lower_value = to_lower (trim_string (value)); - if (lower_value == "true") - { - parsed = true; - return true; - } - if (lower_value == "false") - { - parsed = false; - return true; - } - return false; + return MuseOptions::parse_bool_option (value, parsed); } int MuseAnthena::parse_muse_options () @@ -147,89 +122,11 @@ int MuseAnthena::parse_muse_options () muse_preset = "p1041"; enable_low_latency = true; - std::string other_info = trim_string (params.other_info); - if (other_info.empty ()) - { - return (int)BrainFlowExitCodes::STATUS_OK; - } - - if ((other_info.find ('=') == std::string::npos) && - (other_info.find (';') == std::string::npos)) - { - std::string preset = to_lower (other_info); - if (!is_valid_muse_preset (preset)) - { - safe_logger ( - spdlog::level::err, "Invalid MuseAnthena preset in other_info: {}", other_info); - return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; - } - muse_preset = preset; - return (int)BrainFlowExitCodes::STATUS_OK; - } - - bool has_options = false; - std::stringstream ss (other_info); - std::string token; - while (std::getline (ss, token, ';')) - { - token = trim_string (token); - if (token.empty ()) - { - continue; - } - - size_t separator = token.find ('='); - if ((separator == std::string::npos) || - (token.find ('=', separator + 1) != std::string::npos)) - { - safe_logger (spdlog::level::err, - "Invalid MuseAnthena other_info option: {}. Expected key=value", token); - return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; - } - - std::string key = to_lower (trim_string (token.substr (0, separator))); - std::string value = trim_string (token.substr (separator + 1)); - if (key.empty () || value.empty ()) - { - safe_logger (spdlog::level::err, - "Invalid MuseAnthena other_info option: {}. Empty key or value", token); - return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; - } - - if (key == "preset") - { - value = to_lower (value); - if (!is_valid_muse_preset (value)) - { - safe_logger ( - spdlog::level::err, "Invalid MuseAnthena preset in other_info: {}", value); - return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; - } - muse_preset = value; - has_options = true; - } - else if (key == "low_latency") - { - bool parsed = false; - if (!parse_bool_option (value, parsed)) - { - safe_logger (spdlog::level::err, - "Invalid MuseAnthena low_latency value in other_info: {}", value); - return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; - } - enable_low_latency = parsed; - has_options = true; - } - else - { - safe_logger (spdlog::level::err, "Unknown MuseAnthena other_info option: {}", key); - return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; - } - } - - if (!has_options) + std::string parse_error; + if (!MuseOptions::parse_preset_options (params.other_info, board_id, + MuseOptions::PresetFamily::Anthena, true, muse_preset, enable_low_latency, parse_error)) { - safe_logger (spdlog::level::err, "Invalid MuseAnthena other_info: {}", params.other_info); + safe_logger (spdlog::level::err, "Invalid MuseAnthena other_info: {}", parse_error); return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; } @@ -739,9 +636,12 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size) } const uint8_t *packet = data + offset; - uint8_t packet_index = packet[1]; + uint16_t packet_index = + cast_16bit_to_uint16_little_endian ((const unsigned char *)(packet + 1)); double packet_host_timestamp = get_timestamp (); uint8_t primary_tag = packet[9]; + uint8_t primary_block_index = packet[10]; + uint32_t primary_package_num = ((uint32_t)packet_index << 8) | primary_block_index; const uint8_t *packet_data = packet + PACKET_HEADER_SIZE; size_t packet_data_size = packet_len - PACKET_HEADER_SIZE; size_t packet_data_offset = 0; @@ -753,8 +653,8 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size) primary_config.variable_length ? packet_data_size : primary_config.data_len; if ((primary_data_len > 0) && (primary_data_len <= packet_data_size)) { - parse_sensor_payload (primary_tag, packet_index, packet_host_timestamp, packet_data, - primary_data_len); + parse_sensor_payload (primary_tag, primary_package_num, packet_host_timestamp, + packet_data, primary_data_len); packet_data_offset = primary_data_len; } else @@ -776,6 +676,7 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size) { uint8_t tag = packet_data[packet_data_offset]; uint8_t subpacket_index = packet_data[packet_data_offset + 1]; + uint32_t subpacket_package_num = ((uint32_t)packet_index << 8) | subpacket_index; SensorConfig config; if (!get_sensor_config (tag, config)) { @@ -794,7 +695,7 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size) break; } - parse_sensor_payload (tag, subpacket_index, packet_host_timestamp, + parse_sensor_payload (tag, subpacket_package_num, packet_host_timestamp, packet_data + packet_data_offset + SUBPACKET_HEADER_SIZE, sensor_data_len); packet_data_offset += SUBPACKET_HEADER_SIZE + sensor_data_len; } @@ -804,7 +705,7 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size) } void MuseAnthena::parse_sensor_payload ( - uint8_t tag, uint8_t sequence_num, double host_timestamp, const uint8_t *data, size_t size) + uint8_t tag, uint32_t package_num, double host_timestamp, const uint8_t *data, size_t size) { SensorConfig config; if (!get_sensor_config (tag, config)) @@ -824,7 +725,8 @@ void MuseAnthena::parse_sensor_payload ( if (size >= 2) { last_battery = - (double)cast_16bit_to_uint16_little_endian ((const unsigned char *)data) / 256.0; + (double)cast_16bit_to_uint16_little_endian ((const unsigned char *)data) * + MUSE_ANTHENA_BATTERY_PERCENT_SCALE_FACTOR; } return; } @@ -847,7 +749,7 @@ void MuseAnthena::parse_sensor_payload ( for (int sample = 0; sample < config.n_samples; sample++) { std::vector package ((size_t)num_rows, 0.0); - package[(size_t)package_num_channel] = (double)sequence_num; + package[(size_t)package_num_channel] = (double)package_num; for (int channel = 0; channel < config.n_channels; channel++) { size_t bit_start = (size_t)(sample * config.n_channels + channel) * 14; @@ -886,7 +788,7 @@ void MuseAnthena::parse_sensor_payload ( for (int sample = 0; sample < config.n_samples; sample++) { std::vector package ((size_t)num_rows, 0.0); - package[(size_t)package_num_channel] = (double)sequence_num; + package[(size_t)package_num_channel] = (double)package_num; for (int channel = 0; channel < 3; channel++) { int16_t raw = cast_16bit_to_int16_little_endian ( @@ -926,7 +828,7 @@ void MuseAnthena::parse_sensor_payload ( for (int sample = 0; sample < config.n_samples; sample++) { std::vector package ((size_t)num_rows, 0.0); - package[(size_t)package_num_channel] = (double)sequence_num; + package[(size_t)package_num_channel] = (double)package_num; package[(size_t)battery_channel] = last_battery; for (int channel = 0; channel < config.n_channels; channel++) diff --git a/src/board_controller/muse/muse_bglib/inc/muse_bglib_helper.h b/src/board_controller/muse/muse_bglib/inc/muse_bglib_helper.h index 5464ce9f0..2779feff6 100644 --- a/src/board_controller/muse/muse_bglib/inc/muse_bglib_helper.h +++ b/src/board_controller/muse/muse_bglib/inc/muse_bglib_helper.h @@ -61,6 +61,7 @@ class MuseBGLibHelper double last_ppg_timestamp; // used for timestamp correction double last_eeg_timestamp; // used for timestamp correction double last_aux_timestamp; // used for timestamp correction + std::string muse_preset; void thread_worker (); @@ -86,6 +87,7 @@ class MuseBGLibHelper last_aux_timestamp = -1.0; last_eeg_timestamp = -1.0; last_ppg_timestamp = -1.0; + muse_preset = "p21"; } virtual ~MuseBGLibHelper () diff --git a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp index 943d4f18a..b442024c3 100644 --- a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp +++ b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp @@ -6,6 +6,7 @@ #include "custom_cast.h" #include "muse_bglib_helper.h" #include "muse_constants.h" +#include "muse_options.h" #include "timestamp.h" #include "uart.h" @@ -21,6 +22,15 @@ int MuseBGLibHelper::initialize (struct BrainFlowInputParams params) if (!initialized) { input_params = params; + muse_preset = "p21"; + bool unused_low_latency = false; + std::string parse_error; + if (!MuseOptions::parse_preset_options (input_params.other_info, board_id, + MuseOptions::PresetFamily::Legacy, false, muse_preset, unused_low_latency, + parse_error)) + { + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } exit_code = (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR; int buffer_size_default = board_descr["default"]["num_rows"].get (); int buffer_size_aux = board_descr["auxiliary"]["num_rows"].get (); @@ -94,7 +104,7 @@ int MuseBGLibHelper::open_device () } if (res == (int)BrainFlowExitCodes::STATUS_OK) { - res = config_device ("p21"); + res = config_device (muse_preset.c_str ()); } return res; diff --git a/src/board_controller/muse/muse_bled.cpp b/src/board_controller/muse/muse_bled.cpp index 7671d05a5..50e5f0087 100644 --- a/src/board_controller/muse/muse_bled.cpp +++ b/src/board_controller/muse/muse_bled.cpp @@ -3,6 +3,7 @@ #include "get_dll_dir.h" #include "muse_bled.h" +#include "muse_options.h" #include "brainflow_constants.h" @@ -79,6 +80,17 @@ int MuseBLED::prepare_session () safe_logger (spdlog::level::err, "you need to specify dongle port"); return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; } + std::string muse_preset = "p21"; + bool unused_low_latency = false; + std::string parse_error; + if (!MuseOptions::parse_preset_options (params.other_info, board_id, + MuseOptions::PresetFamily::Legacy, false, muse_preset, unused_low_latency, parse_error)) + { + safe_logger (spdlog::level::err, "Invalid MuseBLED other_info: {}", parse_error); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + params.other_info = muse_preset; + safe_logger (spdlog::level::info, "Use MuseBLED preset {}", muse_preset); return DynLibBoard::prepare_session (); } @@ -211,4 +223,4 @@ void MuseBLED::read_thread () { delete[] data_anc; } -} \ No newline at end of file +} From a34139e526fd6b2f0b2968df92cc2073e90ea5bb Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 28 Apr 2026 22:23:28 +0700 Subject: [PATCH 3/3] add more examples for pulse detection for muse anthena Signed-off-by: Andrey Parfenov --- .../muse_anthena_analyze_optics_pulse.py | 276 ++++++++++++++++++ .../tests/muse_anthena_optics_plot.py | 118 -------- .../tests/muse_anthena_record_optics.py | 77 +++++ 3 files changed, 353 insertions(+), 118 deletions(-) create mode 100644 python_package/examples/tests/muse_anthena_analyze_optics_pulse.py delete mode 100644 python_package/examples/tests/muse_anthena_optics_plot.py create mode 100644 python_package/examples/tests/muse_anthena_record_optics.py diff --git a/python_package/examples/tests/muse_anthena_analyze_optics_pulse.py b/python_package/examples/tests/muse_anthena_analyze_optics_pulse.py new file mode 100644 index 000000000..970c74f30 --- /dev/null +++ b/python_package/examples/tests/muse_anthena_analyze_optics_pulse.py @@ -0,0 +1,276 @@ +import argparse +import math +from pathlib import Path + +import matplotlib + +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import numpy as np + +from brainflow.board_shim import BoardShim, BoardIds, BrainFlowPresets +from brainflow.data_filter import DataFilter, DetrendOperations, FilterTypes + + +def build_time_axis(data, timestamp_channel, sampling_rate): + timestamps = data[timestamp_channel, :] + if timestamps.size == data.shape[1] and timestamps.size > 1 and np.all(np.isfinite(timestamps)): + diffs = np.diff(timestamps) + if np.count_nonzero(diffs > 0) > timestamps.size * 0.8: + return timestamps - timestamps[0], 1.0 / np.median(diffs[diffs > 0]) + return np.arange(data.shape[1], dtype=np.float64) / sampling_rate, float(sampling_rate) + + +def get_active_channels(optics_data, optical_channels): + active = [] + active_labels = [] + for index, channel in enumerate(optical_channels): + row = optics_data[index, :] + finite = row[np.isfinite(row)] + if finite.size > 0 and np.std(finite) > 1e-9: + active.append(index) + active_labels.append(f'Optics {channel}') + return active, active_labels + + +def zscore(data): + stddev = np.std(data) + if stddev < 1e-12: + return np.zeros(data.shape) + return (data - np.mean(data)) / stddev + + +def filter_active_channels(optics_data, active_indexes, sampling_rate, low_cut, high_cut): + filtered = [] + for index in active_indexes: + row = np.asarray(optics_data[index, :], dtype=np.float64).copy() + finite = np.isfinite(row) + if not np.all(finite): + row[~finite] = np.interp(np.flatnonzero(~finite), np.flatnonzero(finite), row[finite]) + DataFilter.detrend(row, DetrendOperations.LINEAR.value) + DataFilter.perform_bandpass( + row, + int(round(sampling_rate)), + low_cut, + high_cut, + 4, + FilterTypes.BUTTERWORTH_ZERO_PHASE.value, + 0.0, + ) + filtered.append(row) + return np.asarray(filtered) + + +def find_local_peaks(signal, sampling_rate, min_bpm, max_bpm): + if signal.size < 3: + return np.asarray([], dtype=np.int64) + + min_distance = max(1, int(round(sampling_rate * 60.0 / max_bpm))) + threshold = np.percentile(signal, 65.0) + candidates = np.flatnonzero((signal[1:-1] > signal[:-2]) & (signal[1:-1] >= signal[2:])) + 1 + candidates = candidates[signal[candidates] > threshold] + + peaks = [] + for candidate in candidates: + if not peaks or candidate - peaks[-1] >= min_distance: + peaks.append(int(candidate)) + elif signal[candidate] > signal[peaks[-1]]: + peaks[-1] = int(candidate) + peaks = np.asarray(peaks, dtype=np.int64) + + if peaks.size < 3: + return peaks + + intervals = np.diff(peaks) / sampling_rate + valid = (intervals >= 60.0 / max_bpm) & (intervals <= 60.0 / min_bpm) + keep = np.concatenate(([True], valid)) + return peaks[keep] + + +def score_peaks(peaks, signal, sampling_rate, min_bpm, max_bpm): + if peaks.size < 3: + return -math.inf + intervals = np.diff(peaks) / sampling_rate + valid = (intervals >= 60.0 / max_bpm) & (intervals <= 60.0 / min_bpm) + if np.count_nonzero(valid) < 2: + return -math.inf + valid_intervals = intervals[valid] + rr_cv = np.std(valid_intervals) / np.mean(valid_intervals) + return float(np.mean(signal[peaks]) - rr_cv) + + +def select_pulse_signal(combined, sampling_rate, min_bpm, max_bpm): + best_signal = combined + best_peaks = find_local_peaks(combined, sampling_rate, min_bpm, max_bpm) + best_score = score_peaks(best_peaks, combined, sampling_rate, min_bpm, max_bpm) + + inverted = -combined + inverted_peaks = find_local_peaks(inverted, sampling_rate, min_bpm, max_bpm) + inverted_score = score_peaks(inverted_peaks, inverted, sampling_rate, min_bpm, max_bpm) + + if inverted_score > best_score: + best_signal = inverted + best_peaks = inverted_peaks + + return best_signal, best_peaks + + +def bpm_from_peaks(peaks, time_axis): + if peaks.size < 3: + return None + intervals = np.diff(time_axis[peaks]) + intervals = intervals[intervals > 0] + if intervals.size == 0: + return None + return 60.0 / np.median(intervals) + + +def spectrum(signal, sampling_rate, low_cut, high_cut): + if signal.size < 4: + return np.asarray([]), np.asarray([]), None + centered = signal - np.mean(signal) + windowed = centered * np.hanning(centered.size) + freqs = np.fft.rfftfreq(windowed.size, d=1.0 / sampling_rate) + power = np.abs(np.fft.rfft(windowed)) ** 2 + band = (freqs >= low_cut) & (freqs <= high_cut) + if not np.any(band): + return freqs, power, None + peak_freq = freqs[band][np.argmax(power[band])] + return freqs, power, peak_freq * 60.0 + + +def save_raw_plot(time_axis, optics_data, active_indexes, labels, output_file): + fig, axes = plt.subplots(len(active_indexes), 1, figsize=(12, 2.1 * len(active_indexes)), sharex=True) + axes = np.atleast_1d(axes) + for axis, index, label in zip(axes, active_indexes, labels): + axis.plot(time_axis, optics_data[index, :], linewidth=1.0) + axis.set_ylabel(label) + axis.grid(True) + axes[-1].set_xlabel('Time, sec') + fig.tight_layout() + fig.savefig(output_file, dpi=150) + plt.close(fig) + + +def save_filtered_plot(time_axis, filtered, labels, output_file): + fig, axes = plt.subplots(filtered.shape[0], 1, figsize=(12, 2.1 * filtered.shape[0]), sharex=True) + axes = np.atleast_1d(axes) + for axis, row, label in zip(axes, filtered, labels): + axis.plot(time_axis, zscore(row), linewidth=1.0) + axis.set_ylabel(label) + axis.grid(True) + axes[-1].set_xlabel('Time, sec') + fig.tight_layout() + fig.savefig(output_file, dpi=150) + plt.close(fig) + + +def save_pulse_plot(time_axis, pulse_signal, peaks, peak_bpm, spectrum_bpm, output_file): + title_parts = [] + if peak_bpm is not None: + title_parts.append(f'peaks {peak_bpm:.1f} BPM') + if spectrum_bpm is not None: + title_parts.append(f'spectrum {spectrum_bpm:.1f} BPM') + + fig, axis = plt.subplots(1, 1, figsize=(12, 4)) + axis.plot(time_axis, pulse_signal, linewidth=1.0) + if peaks.size > 0: + axis.plot(time_axis[peaks], pulse_signal[peaks], 'ro', markersize=3) + axis.set_xlabel('Time, sec') + axis.set_ylabel('Combined filtered optics, z-score') + axis.set_title(', '.join(title_parts) if title_parts else 'Combined filtered optics') + axis.grid(True) + fig.tight_layout() + fig.savefig(output_file, dpi=150) + plt.close(fig) + + +def save_spectrum_plot(freqs, power, spectrum_bpm, output_file): + fig, axis = plt.subplots(1, 1, figsize=(10, 4)) + if freqs.size > 0: + axis.plot(freqs * 60.0, power, linewidth=1.0) + if spectrum_bpm is not None: + axis.axvline(spectrum_bpm, color='r', linestyle='--', linewidth=1.0) + axis.set_xlabel('Frequency, BPM') + axis.set_ylabel('Power') + axis.set_xlim(30, 210) + axis.grid(True) + fig.tight_layout() + fig.savefig(output_file, dpi=150) + plt.close(fig) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--input-file', type=str, required=False, default='muse_anthena_optics_recording.csv') + parser.add_argument('--output-prefix', type=str, required=False, default='') + parser.add_argument('--discard-seconds', type=float, required=False, default=5.0) + parser.add_argument('--low-cut', type=float, required=False, default=0.7) + parser.add_argument('--high-cut', type=float, required=False, default=3.5) + parser.add_argument('--min-bpm', type=float, required=False, default=40.0) + parser.add_argument('--max-bpm', type=float, required=False, default=180.0) + args = parser.parse_args() + + data = DataFilter.read_file(args.input_file) + board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value + preset = BrainFlowPresets.ANCILLARY_PRESET + optical_channels = BoardShim.get_optical_channels(board_id, preset) + timestamp_channel = BoardShim.get_timestamp_channel(board_id, preset) + expected_sampling_rate = BoardShim.get_sampling_rate(board_id, preset) + + time_axis, actual_sampling_rate = build_time_axis(data, timestamp_channel, expected_sampling_rate) + start_index = int(np.searchsorted(time_axis, args.discard_seconds)) + if start_index >= data.shape[1] - 4: + start_index = 0 + data = data[:, start_index:] + time_axis = time_axis[start_index:] - time_axis[start_index] + + optics_data = data[optical_channels, :] + active_indexes, active_labels = get_active_channels(optics_data, optical_channels) + if not active_indexes: + raise RuntimeError('no active optical channels found') + + filtered = filter_active_channels( + optics_data, + active_indexes, + actual_sampling_rate, + args.low_cut, + args.high_cut, + ) + combined = np.mean(np.asarray([zscore(row) for row in filtered]), axis=0) + pulse_signal, peaks = select_pulse_signal(combined, actual_sampling_rate, args.min_bpm, args.max_bpm) + peak_bpm = bpm_from_peaks(peaks, time_axis) + freqs, power, spectrum_bpm = spectrum(pulse_signal, actual_sampling_rate, args.low_cut, args.high_cut) + + prefix = args.output_prefix or str(Path(args.input_file).with_suffix('')) + raw_plot = f'{prefix}_raw.png' + filtered_plot = f'{prefix}_filtered.png' + pulse_plot = f'{prefix}_pulse.png' + spectrum_plot = f'{prefix}_spectrum.png' + + save_raw_plot(time_axis, optics_data, active_indexes, active_labels, raw_plot) + save_filtered_plot(time_axis, filtered, active_labels, filtered_plot) + save_pulse_plot(time_axis, pulse_signal, peaks, peak_bpm, spectrum_bpm, pulse_plot) + save_spectrum_plot(freqs, power, spectrum_bpm, spectrum_plot) + + print(f'input file: {args.input_file}') + print(f'samples analyzed: {data.shape[1]}') + print(f'duration analyzed: {time_axis[-1] - time_axis[0]:.3f} sec') + print(f'sampling rate: {actual_sampling_rate:.2f} Hz, expected: {expected_sampling_rate} Hz') + print(f'active optical channels: {active_labels}') + if peak_bpm is not None: + print(f'peak estimate: {peak_bpm:.1f} BPM from {peaks.size} peaks') + else: + print('peak estimate: unavailable') + if spectrum_bpm is not None: + print(f'spectrum estimate: {spectrum_bpm:.1f} BPM') + else: + print('spectrum estimate: unavailable') + print(f'wrote plot: {raw_plot}') + print(f'wrote plot: {filtered_plot}') + print(f'wrote plot: {pulse_plot}') + print(f'wrote plot: {spectrum_plot}') + + +if __name__ == '__main__': + main() diff --git a/python_package/examples/tests/muse_anthena_optics_plot.py b/python_package/examples/tests/muse_anthena_optics_plot.py deleted file mode 100644 index 9c1f88658..000000000 --- a/python_package/examples/tests/muse_anthena_optics_plot.py +++ /dev/null @@ -1,118 +0,0 @@ -import argparse -import math -import time - -import matplotlib - -matplotlib.use('Agg') -import matplotlib.pyplot as plt -import numpy as np - -from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets - - -def print_summary(optics_data, timestamps, sampling_rate, channel_labels): - print(f'optics samples: {optics_data.shape[1]}') - - if timestamps.size > 1 and np.all(np.isfinite(timestamps)): - timestamp_diffs = np.diff(timestamps) - timestamp_diffs = timestamp_diffs[timestamp_diffs > 0] - if timestamp_diffs.size > 0: - actual_rate = 1.0 / np.median(timestamp_diffs) - duration = timestamps[-1] - timestamps[0] - print(f'timestamp duration: {duration:.3f} sec') - print(f'timestamp sampling rate: {actual_rate:.2f} Hz, expected: {sampling_rate} Hz') - - nan_count = int(np.isnan(optics_data).sum()) - print(f'nan values: {nan_count}') - - for label, row in zip(channel_labels, optics_data): - finite = row[np.isfinite(row)] - if finite.size == 0: - print(f'{label}: no finite values') - continue - stddev = float(np.std(finite)) - print( - f'{label}: min={np.min(finite):.3f}, max={np.max(finite):.3f}, ' - f'mean={np.mean(finite):.3f}, std={stddev:.3f}' - ) - if stddev < 1e-9: - print(f'{label}: flat signal') - - -def plot_optics(optics_data, timestamps, sampling_rate, output_file, channel_labels): - if timestamps.size == optics_data.shape[1] and timestamps.size > 0 and np.all(np.isfinite(timestamps)): - x_axis = timestamps - timestamps[0] - x_label = 'Time, sec' - else: - x_axis = np.arange(optics_data.shape[1]) / sampling_rate - x_label = 'Samples' - - num_channels = optics_data.shape[0] - cols = min(4, num_channels) - rows = math.ceil(num_channels / cols) - fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 2.4 * rows), sharex=True) - axes = np.atleast_1d(axes).reshape(-1) - - for index, axis in enumerate(axes): - if index < num_channels: - axis.plot(x_axis, optics_data[index]) - axis.set_title(channel_labels[index]) - axis.grid(True) - else: - axis.axis('off') - - for axis in axes[-cols:]: - axis.set_xlabel(x_label) - - fig.tight_layout() - fig.savefig(output_file, dpi=150) - print(f'wrote plot: {output_file}') - - -def main(): - BoardShim.enable_board_logger() - - parser = argparse.ArgumentParser() - parser.add_argument('--duration', type=int, required=False, default=20) - parser.add_argument('--mac-address', type=str, required=False, default='') - parser.add_argument('--serial-number', type=str, required=False, default='') - parser.add_argument('--timeout', type=int, required=False, default=0) - parser.add_argument('--other-info', type=str, required=False, default='preset=p1035;low_latency=true') - parser.add_argument('--output-file', type=str, required=False, default='muse_anthena_optics_p1035.png') - args = parser.parse_args() - - params = BrainFlowInputParams() - params.mac_address = args.mac_address - params.serial_number = args.serial_number - params.timeout = args.timeout - params.other_info = args.other_info - - board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value - preset = BrainFlowPresets.ANCILLARY_PRESET - optical_channels = BoardShim.get_optical_channels(board_id, preset) - timestamp_channel = BoardShim.get_timestamp_channel(board_id, preset) - sampling_rate = BoardShim.get_sampling_rate(board_id, preset) - - board = BoardShim(board_id, params) - try: - board.prepare_session() - board.start_stream() - try: - time.sleep(args.duration) - data = board.get_board_data(preset=preset) - finally: - board.stop_stream() - finally: - if board.is_prepared(): - board.release_session() - - optics_data = data[optical_channels, :] - timestamps = data[timestamp_channel, :] - channel_labels = [f'Optics {channel}' for channel in optical_channels] - print_summary(optics_data, timestamps, sampling_rate, channel_labels) - plot_optics(optics_data, timestamps, sampling_rate, args.output_file, channel_labels) - - -if __name__ == '__main__': - main() diff --git a/python_package/examples/tests/muse_anthena_record_optics.py b/python_package/examples/tests/muse_anthena_record_optics.py new file mode 100644 index 000000000..a215bbbba --- /dev/null +++ b/python_package/examples/tests/muse_anthena_record_optics.py @@ -0,0 +1,77 @@ +import argparse +import time + +import numpy as np + +from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets +from brainflow.data_filter import DataFilter + + +def print_summary(data, optical_channels, timestamp_channel, sampling_rate): + print(f'samples: {data.shape[1]}') + + timestamps = data[timestamp_channel, :] + if timestamps.size > 1 and np.all(np.isfinite(timestamps)): + diffs = np.diff(timestamps) + diffs = diffs[diffs > 0] + if diffs.size > 0: + print(f'timestamp duration: {timestamps[-1] - timestamps[0]:.3f} sec') + print(f'timestamp sampling rate: {1.0 / np.median(diffs):.2f} Hz') + print(f'expected sampling rate: {sampling_rate} Hz') + + active_channels = [] + for channel in optical_channels: + row = data[channel, :] + finite = row[np.isfinite(row)] + if finite.size == 0: + continue + if np.std(finite) > 1e-9: + active_channels.append(channel) + + print(f'active optical channels: {active_channels}') + + +def main(): + BoardShim.enable_board_logger() + + parser = argparse.ArgumentParser() + parser.add_argument('--duration', type=int, required=False, default=60) + parser.add_argument('--mac-address', type=str, required=False, default='') + parser.add_argument('--serial-number', type=str, required=False, default='') + parser.add_argument('--timeout', type=int, required=False, default=0) + parser.add_argument('--other-info', type=str, required=False, default='preset=p1035;low_latency=true') + parser.add_argument('--output-file', type=str, required=False, default='muse_anthena_optics_recording.csv') + args = parser.parse_args() + + params = BrainFlowInputParams() + params.mac_address = args.mac_address + params.serial_number = args.serial_number + params.timeout = args.timeout + params.other_info = args.other_info + + board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value + preset = BrainFlowPresets.ANCILLARY_PRESET + optical_channels = BoardShim.get_optical_channels(board_id, preset) + timestamp_channel = BoardShim.get_timestamp_channel(board_id, preset) + sampling_rate = BoardShim.get_sampling_rate(board_id, preset) + + board = BoardShim(board_id, params) + try: + board.prepare_session() + board.start_stream() + try: + time.sleep(args.duration) + data = board.get_board_data(preset=preset) + finally: + board.stop_stream() + finally: + if board.is_prepared(): + board.release_session() + + DataFilter.write_file(data, args.output_file, 'w') + print(f'wrote raw ancillary data: {args.output_file}') + print_summary(data, optical_channels, timestamp_channel, sampling_rate) + + +if __name__ == '__main__': + main()