From 5e9029f1bbe015f4d9a232654db573e2f367fa9e Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Sat, 23 Aug 2025 19:07:18 +0200 Subject: [PATCH 1/8] Add support for Muse S Anthena device Signed-off-by: Andrey Parfenov --- .../brainflow/board_controller_library.cs | 1 + .../src/main/java/brainflow/BoardIds.java | 3 +- julia_package/brainflow/src/board_shim.jl | 1 + matlab_package/brainflow/BoardIds.m | 1 + nodejs_package/brainflow/brainflow.types.ts | 3 +- python_package/brainflow/board_shim.py | 1 + src/board_controller/board_controller.cpp | 4 + src/board_controller/brainflow_boards.cpp | 26 +- src/board_controller/build.cmake | 1 + src/board_controller/muse/inc/muse_anthena.h | 44 ++ .../muse/inc/muse_anthena_constants.h | 13 + src/board_controller/muse/muse.cpp | 8 +- src/board_controller/muse/muse_anthena.cpp | 424 ++++++++++++++++++ src/utils/inc/brainflow_constants.h | 3 +- 14 files changed, 528 insertions(+), 5 deletions(-) create mode 100644 src/board_controller/muse/inc/muse_anthena.h create mode 100644 src/board_controller/muse/inc/muse_anthena_constants.h create mode 100644 src/board_controller/muse/muse_anthena.cpp diff --git a/csharp_package/brainflow/brainflow/board_controller_library.cs b/csharp_package/brainflow/brainflow/board_controller_library.cs index 10a910621..ff62deaf8 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -124,6 +124,7 @@ public enum BoardIds SYNCHRONI_UNO_1_CHANNELS_BOARD = 62, OB3000_24_CHANNELS_BOARD = 63, BIOLISTENER_BOARD = 64, + MUSE_S_ANTHENA_BOARD = 65 }; diff --git a/java_package/brainflow/src/main/java/brainflow/BoardIds.java b/java_package/brainflow/src/main/java/brainflow/BoardIds.java index 487d8fc5a..c672ca971 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardIds.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardIds.java @@ -73,7 +73,8 @@ public enum BoardIds SYNCHRONI_PENTO_8_CHANNELS_BOARD(61), SYNCHRONI_UNO_1_CHANNELS_BOARD(62), OB3000_24_CHANNELS_BOARD(63), - BIOLISTENER_BOARD(64); + BIOLISTENER_BOARD(64), + MUSE_S_ANTHENA_BOARD(65); private final int board_id; private static final Map bi_map = new HashMap (); diff --git a/julia_package/brainflow/src/board_shim.jl b/julia_package/brainflow/src/board_shim.jl index 3e7086093..ccc32ebd1 100644 --- a/julia_package/brainflow/src/board_shim.jl +++ b/julia_package/brainflow/src/board_shim.jl @@ -69,6 +69,7 @@ export BrainFlowInputParams SYNCHRONI_UNO_1_CHANNELS_BOARD = 62 OB3000_24_CHANNELS_BOARD = 63 BIOLISTENER_BOARD = 64 + MUSE_S_ANTHENA_BOARD = 65 end diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m index cc242952d..04305b712 100644 --- a/matlab_package/brainflow/BoardIds.m +++ b/matlab_package/brainflow/BoardIds.m @@ -67,5 +67,6 @@ SYNCHRONI_UNO_1_CHANNELS_BOARD(62) OB3000_24_CHANNELS_BOARD(63) BIOLISTENER_BOARD(64) + MUSE_S_ANTHENA_BOARD(65) end end \ No newline at end of file diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index f55f10187..77c70dda8 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -76,7 +76,8 @@ export enum BoardIds { SYNCHRONI_PENTO_8_CHANNELS_BOARD = 61, SYNCHRONI_UNO_1_CHANNELS_BOARD = 62, OB3000_24_CHANNELS_BOARD = 63, - BIOLISTENER_BOARD = 64 + BIOLISTENER_BOARD = 64, + MUSE_S_ANTHENA_BOARD = 65 } export enum IpProtocolTypes { diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index 0f98d001f..0a20b16c9 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -82,6 +82,7 @@ class BoardIds(enum.IntEnum): SYNCHRONI_UNO_1_CHANNELS_BOARD = 62 #: OB3000_24_CHANNELS_BOARD = 63 #: BIOLISTENER_BOARD = 64 #: + MUSE_S_ANTHENA_BOARD = 65 #: class IpProtocolTypes(enum.IntEnum): diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index 3cab800f5..6c491dd5d 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -49,6 +49,7 @@ #include "json.hpp" #include "knight.h" #include "muse.h" +#include "muse_anthena.h" #include "muse_bled.h" #include "notion_osc.h" #include "ntl_wifi.h" @@ -237,6 +238,9 @@ int prepare_session (int board_id, const char *json_brainflow_input_params) case BoardIds::MUSE_S_BOARD: board = std::shared_ptr (new Muse (board_id, params)); break; + case BoardIds::MUSE_S_ANTHENA_BOARD: + board = std::shared_ptr (new MuseAnthena (board_id, params)); + break; case BoardIds::BRAINALIVE_BOARD: board = std::shared_ptr (new BrainAlive (params)); break; diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index e3f0a5657..0e787468e 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -82,7 +82,8 @@ BrainFlowBoards::BrainFlowBoards() {"61", json::object()}, {"62", json::object()}, {"63", json::object()}, - {"64", json::object()} + {"64", json::object()}, + {"65", json::object()} } }}; @@ -1221,6 +1222,29 @@ BrainFlowBoards::BrainFlowBoards() {"battery_channel", 8}, {"other_channels", {9, 10}} }; + brainflow_boards_json["boards"]["65"]["default"] = + { + {"name", "MuseAnthena"}, + {"sampling_rate", 256}, + {"timestamp_channel", 6}, + {"marker_channel", 7}, + {"package_num_channel", 0}, + {"num_rows", 8}, + {"eeg_channels", {1, 2, 3, 4}}, + {"eeg_names", "TP9,AF7,AF8,TP10"}, + {"other_channels", {5}} + }; + brainflow_boards_json["boards"]["65"]["auxiliary"] = + { + {"name", "MuseAnthenaAux"}, + {"sampling_rate", 52}, + {"timestamp_channel", 7}, + {"marker_channel", 8}, + {"package_num_channel", 0}, + {"num_rows", 9}, + {"accel_channels", {1, 2, 3}}, + {"gyro_channels", {4, 5, 6}} + }; } BrainFlowBoards boards_struct; diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake index 5239cb2d6..998b70543 100644 --- a/src/board_controller/build.cmake +++ b/src/board_controller/build.cmake @@ -80,6 +80,7 @@ SET (BOARD_CONTROLLER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/enophone/enophone.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ble_lib_board.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/muse/muse.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/muse/muse_anthena.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/brainalive/brainalive.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/emotibit/emotibit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ntl/ntl_wifi.cpp diff --git a/src/board_controller/muse/inc/muse_anthena.h b/src/board_controller/muse/inc/muse_anthena.h new file mode 100644 index 000000000..fc0eeebd8 --- /dev/null +++ b/src/board_controller/muse/inc/muse_anthena.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include + +#include "ble_lib_board.h" + + +class MuseAnthena : public BLELibBoard +{ + +protected: + volatile simpleble_adapter_t muse_adapter; + volatile simpleble_peripheral_t muse_peripheral; + bool initialized; + bool is_streaming; + std::mutex m; + std::condition_variable cv; + std::vector> notified_characteristics; + std::pair control_characteristics; + + std::string bytes_to_string (const uint8_t *data, size_t size); + +public: + MuseAnthena (int board_id, struct BrainFlowInputParams params); + ~MuseAnthena (); + + int prepare_session (); + int start_stream (int buffer_size, const char *streamer_params); + int stop_stream (); + int release_session (); + int config_board (std::string config, std::string &response); + int config_board (std::string config); + + void adapter_on_scan_found (simpleble_adapter_t adapter, simpleble_peripheral_t peripheral); + void peripheral_on_eeg (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + simpleble_uuid_t characteristic, const uint8_t *data, size_t size); + void peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + simpleble_uuid_t characteristic, const uint8_t *data, size_t size); + void peripheral_on_status (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + simpleble_uuid_t characteristic, const uint8_t *data, size_t size); +}; diff --git a/src/board_controller/muse/inc/muse_anthena_constants.h b/src/board_controller/muse/inc/muse_anthena_constants.h new file mode 100644 index 000000000..e4517ef2d --- /dev/null +++ b/src/board_controller/muse/inc/muse_anthena_constants.h @@ -0,0 +1,13 @@ +#pragma once + + +// info about services and chars +#define MUSE_ANTHENA_SERVICE_UUID 0xFE8D + +#define MUSE_ANTHENA_GATT_ATTR_STREAM_TOGGLE "273e0001-4c4d-454d-96be-f03bac821358" +#define MUSE_ANTHENA_GATT_EEG "273e0013-4c4d-454d-96be-f03bac821358" +#define MUSE_ANTHENA_GATT_AUX "273e0014-4c4d-454d-96be-f03bac821358" + +// info for equations +#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.0000610352 +#define MUSE_ANTHENA_GYRO_SCALE_FACTOR 0.007476 diff --git a/src/board_controller/muse/muse.cpp b/src/board_controller/muse/muse.cpp index 97741313e..390bbf5ff 100644 --- a/src/board_controller/muse/muse.cpp +++ b/src/board_controller/muse/muse.cpp @@ -558,8 +558,14 @@ int Muse::config_board (std::string config) { return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR; } - uint8_t command[16]; + constexpr int max_size = 16; + uint8_t command[max_size]; size_t len = config.size (); + if (len + 2 >= max_size) + { + safe_logger (spdlog::level::err, "Invalid command, max size is {}", max_size); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } command[0] = (uint8_t)len + 1; for (size_t i = 0; i < len; i++) { diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp new file mode 100644 index 000000000..9dcc09930 --- /dev/null +++ b/src/board_controller/muse/muse_anthena.cpp @@ -0,0 +1,424 @@ +#include "muse_anthena.h" + +#include +#include +#include + +#include "custom_cast.h" +#include "muse_anthena_constants.h" +#include "timestamp.h" + + +void anthena_adapter_on_scan_found ( + simpleble_adapter_t adapter, simpleble_peripheral_t peripheral, void *board) +{ + ((MuseAnthena *)(board))->adapter_on_scan_found (adapter, peripheral); +} + +void anthena_peripheral_on_eeg (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + simpleble_uuid_t characteristic, const uint8_t *data, size_t size, void *board) +{ + ((MuseAnthena *)(board))->peripheral_on_eeg (peripheral, service, characteristic, data, size); +} + +void anthena_peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + simpleble_uuid_t characteristic, const uint8_t *data, size_t size, void *board) +{ + ((MuseAnthena *)(board))->peripheral_on_aux (peripheral, service, characteristic, data, size); +} + +MuseAnthena::MuseAnthena (int board_id, struct BrainFlowInputParams params) + : BLELibBoard (board_id, params) +{ + initialized = false; + muse_adapter = NULL; + muse_peripheral = NULL; + is_streaming = false; +} + +MuseAnthena::~MuseAnthena () +{ + skip_logs = true; + release_session (); +} + +int MuseAnthena::prepare_session () +{ + if (initialized) + { + safe_logger (spdlog::level::info, "Session is already prepared"); + return (int)BrainFlowExitCodes::STATUS_OK; + } + if (params.timeout < 1) + { + params.timeout = 6; + } + safe_logger (spdlog::level::info, "Use timeout for discovery: {}", params.timeout); + if (!init_dll_loader ()) + { + safe_logger (spdlog::level::err, "Failed to init dll_loader"); + return (int)BrainFlowExitCodes::GENERAL_ERROR; + } + size_t adapter_count = simpleble_adapter_get_count (); + if (adapter_count == 0) + { + safe_logger (spdlog::level::err, "No BLE adapters found"); + return (int)BrainFlowExitCodes::UNABLE_TO_OPEN_PORT_ERROR; + } + + safe_logger (spdlog::level::info, "found {} BLE adapter(s)", adapter_count); + + muse_adapter = simpleble_adapter_get_handle (0); + if (muse_adapter == NULL) + { + safe_logger (spdlog::level::err, "Adapter is NULL"); + return (int)BrainFlowExitCodes::UNABLE_TO_OPEN_PORT_ERROR; + } + + simpleble_adapter_set_callback_on_scan_found ( + muse_adapter, ::anthena_adapter_on_scan_found, (void *)this); + + if (!simpleble_adapter_is_bluetooth_enabled ()) + { + safe_logger (spdlog::level::warn, "Probably bluetooth is disabled."); + // dont throw an exception because of this + // https://github.com/OpenBluetoothToolbox/SimpleBLE/issues/115 + } + + simpleble_adapter_scan_start (muse_adapter); + int res = (int)BrainFlowExitCodes::STATUS_OK; + std::unique_lock lk (m); + auto sec = std::chrono::seconds (1); + if (cv.wait_for (lk, params.timeout * sec, [this] { return this->muse_peripheral != NULL; })) + { + safe_logger (spdlog::level::info, "Found MuseAnthena device"); + } + else + { + safe_logger (spdlog::level::err, "Failed to find MuseAnthena Device"); + res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + simpleble_adapter_scan_stop (muse_adapter); + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + // for safety + for (int i = 0; i < 3; i++) + { + if (simpleble_peripheral_connect (muse_peripheral) == SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::info, "Connected to MuseAnthena Device"); + res = (int)BrainFlowExitCodes::STATUS_OK; + break; + } + else + { + safe_logger ( + spdlog::level::warn, "Failed to connect to MuseAnthena Device: {}/3", i); + res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; +#ifdef _WIN32 + Sleep (1000); +#else + sleep (1); +#endif + } + } + } + +// https://github.com/OpenBluetoothToolbox/SimpleBLE/issues/26#issuecomment-955606799 +#ifdef _WIN32 + Sleep (1000); +#else + sleep (1); +#endif + + bool control_characteristics_found = false; + + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + size_t services_count = simpleble_peripheral_services_count (muse_peripheral); + for (size_t i = 0; i < services_count; i++) + { + simpleble_service_t service; + if (simpleble_peripheral_services_get (muse_peripheral, i, &service) != + SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::err, "failed to get service"); + res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + + safe_logger (spdlog::level::trace, "found servce {}", service.uuid.value); + for (size_t j = 0; j < service.characteristic_count; j++) + { + safe_logger (spdlog::level::trace, "found characteristic {}", + service.characteristics[j].uuid.value); + + // control characteristic + if (strcmp (service.characteristics[j].uuid.value, + MUSE_ANTHENA_GATT_ATTR_STREAM_TOGGLE) == 0) + { + control_characteristics = std::pair ( + service.uuid, service.characteristics[j].uuid); + control_characteristics_found = true; + safe_logger (spdlog::level::info, "found control characteristic"); + } + + // eeg characteristic + if (strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_EEG) == 0) + { + if (simpleble_peripheral_notify (muse_peripheral, service.uuid, + service.characteristics[j].uuid, ::anthena_peripheral_on_eeg, + (void *)this) == SIMPLEBLE_SUCCESS) + { + notified_characteristics.push_back ( + std::pair ( + service.uuid, service.characteristics[j].uuid)); + } + else + { + safe_logger (spdlog::level::err, "Failed to notify for {} {}", + service.uuid.value, service.characteristics[j].uuid.value); + res = (int)BrainFlowExitCodes::GENERAL_ERROR; + } + } + + // aux characteristic(acce, gyro, etc) + if (strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_AUX) == 0) + { + if (simpleble_peripheral_notify (muse_peripheral, service.uuid, + service.characteristics[j].uuid, ::anthena_peripheral_on_aux, + (void *)this) == SIMPLEBLE_SUCCESS) + { + notified_characteristics.push_back ( + std::pair ( + service.uuid, service.characteristics[j].uuid)); + } + else + { + safe_logger (spdlog::level::err, "Failed to notify for {} {}", + service.uuid.value, service.characteristics[j].uuid.value); + res = (int)BrainFlowExitCodes::GENERAL_ERROR; + } + } + } + } + } + + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + initialized = true; + // used to be v1 for old muse devices + res = config_board ("v4"); + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + // p21 - eeg only + res = config_board ("p21"); + } + else + { + release_session (); + } + + return res; +} + +int MuseAnthena::start_stream (int buffer_size, const char *streamer_params) +{ + if (!initialized) + { + return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR; + } + + int res = prepare_for_acquisition (buffer_size, streamer_params); + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + res = config_board ("dc001"); + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + is_streaming = true; + } + + return res; +} + +int MuseAnthena::stop_stream () +{ + if (muse_peripheral == NULL) + { + return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR; + } + int res = (int)BrainFlowExitCodes::STATUS_OK; + if (is_streaming) + { + res = config_board ("h"); + } + else + { + res = (int)BrainFlowExitCodes::STREAM_ALREADY_RUN_ERROR; + } + is_streaming = false; + return res; +} + +int MuseAnthena::release_session () +{ + if (initialized) + { + // repeat it multiple times, failure here may lead to a crash + for (int i = 0; i < 2; i++) + { + stop_stream (); + // need to wait for notifications to stop triggered before unsubscribing, otherwise + // macos fails inside simpleble with timeout +#ifdef _WIN32 + Sleep (2000); +#else + sleep (2); +#endif + for (auto notified : notified_characteristics) + { + if (simpleble_peripheral_unsubscribe ( + muse_peripheral, notified.first, notified.second) != SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::err, "failed to unsubscribe for {} {}", + notified.first.value, notified.second.value); + } + } + } + free_packages (); + initialized = false; + } + if (muse_peripheral != NULL) + { + bool is_connected = false; + if (simpleble_peripheral_is_connected (muse_peripheral, &is_connected) == SIMPLEBLE_SUCCESS) + { + if (is_connected) + { + simpleble_peripheral_disconnect (muse_peripheral); + } + } + simpleble_peripheral_release_handle (muse_peripheral); + muse_peripheral = NULL; + } + if (muse_adapter != NULL) + { + simpleble_adapter_release_handle (muse_adapter); + muse_adapter = NULL; + } + + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int MuseAnthena::config_board (std::string config, std::string &response) +{ + return config_board (config); +} + +int MuseAnthena::config_board (std::string config) +{ + if (!initialized) + { + return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR; + } + constexpr int max_size = 16; + uint8_t command[max_size]; + size_t len = config.size (); + if (len + 2 >= max_size) + { + safe_logger (spdlog::level::err, "Invalid command, max size is {}", max_size); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + command[0] = (uint8_t)len + 1; + for (size_t i = 0; i < len; i++) + { + command[i + 1] = uint8_t (config[i]); + } + command[len + 1] = 10; + safe_logger (spdlog::level::trace, "Command to send: {}", bytes_to_string (command, len + 2)); + if (simpleble_peripheral_write_command (muse_peripheral, control_characteristics.first, + control_characteristics.second, command, len + 2) != SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::err, "failed to send command {} to device", config.c_str ()); + return (int)BrainFlowExitCodes::BOARD_WRITE_ERROR; + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +void MuseAnthena::adapter_on_scan_found ( + simpleble_adapter_t adapter, simpleble_peripheral_t peripheral) +{ + char *peripheral_identified = simpleble_peripheral_identifier (peripheral); + char *peripheral_address = simpleble_peripheral_address (peripheral); + bool found = false; + if (!params.mac_address.empty ()) + { + if (strcmp (peripheral_address, params.mac_address.c_str ()) == 0) + { + found = true; + } + } + else + { + if (!params.serial_number.empty ()) + { + if (strcmp (peripheral_identified, params.serial_number.c_str ()) == 0) + { + found = true; + } + } + else + { + if (strncmp (peripheral_identified, "MuseS", 4) == 0) + { + found = true; + } + } + } + + safe_logger (spdlog::level::trace, "address {}", peripheral_address); + simpleble_free (peripheral_address); + safe_logger (spdlog::level::trace, "identifier {}", peripheral_identified); + simpleble_free (peripheral_identified); + + if (found) + { + { + std::lock_guard lk (m); + muse_peripheral = peripheral; + } + cv.notify_one (); + } + else + { + simpleble_peripheral_release_handle (peripheral); + } +} + +void MuseAnthena::peripheral_on_eeg (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + simpleble_uuid_t characteristic, const uint8_t *data, size_t size) +{ + safe_logger (spdlog::level::trace, "EEG packet: {}", bytes_to_string (data, size)); + // TODO parse it and fill default preset +} + +void MuseAnthena::peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + simpleble_uuid_t characteristic, const uint8_t *data, size_t size) +{ + safe_logger (spdlog::level::trace, "AUX packet: {}", bytes_to_string (data, size)); + // TODO parse it and fill aux preset +} + + +std::string MuseAnthena::bytes_to_string (const uint8_t *data, size_t size) +{ + std::ostringstream oss; + for (size_t i = 0; i < size; i++) + { + if (i > 0) + oss << " "; + oss << static_cast (data[i]); + } + return oss.str (); +} \ No newline at end of file diff --git a/src/utils/inc/brainflow_constants.h b/src/utils/inc/brainflow_constants.h index 021bbca7e..44d508b41 100644 --- a/src/utils/inc/brainflow_constants.h +++ b/src/utils/inc/brainflow_constants.h @@ -96,9 +96,10 @@ enum class BoardIds : int SYNCHRONI_UNO_1_CHANNELS_BOARD = 62, OB3000_24_CHANNELS_BOARD = 63, BIOLISTENER_BOARD = 64, + MUSE_S_ANTHENA_BOARD = 65, // use it to iterate FIRST = PLAYBACK_FILE_BOARD, - LAST = BIOLISTENER_BOARD + LAST = MUSE_S_ANTHENA_BOARD }; enum class IpProtocolTypes : int From 7b4da47a08bdd1ef272c5982d5e2e07f5e150f35 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 7 Oct 2025 23:58:50 +0200 Subject: [PATCH 2/8] wip Signed-off-by: Andrey Parfenov --- .../examples/tests/brainflow_get_data.py | 3 +- src/board_controller/brainflow_boards.cpp | 11 +-- src/board_controller/muse/inc/muse_anthena.h | 3 + .../muse/inc/muse_anthena_constants.h | 2 + src/board_controller/muse/muse_anthena.cpp | 95 ++++++++++++++++++- 5 files changed, 103 insertions(+), 11 deletions(-) diff --git a/python_package/examples/tests/brainflow_get_data.py b/python_package/examples/tests/brainflow_get_data.py index 145d8013e..d9c155626 100644 --- a/python_package/examples/tests/brainflow_get_data.py +++ b/python_package/examples/tests/brainflow_get_data.py @@ -40,7 +40,8 @@ def main(): board = BoardShim(args.board_id, params) board.prepare_session() - board.start_stream () + board.add_streamer("file://data.csv:w") + board.start_stream() time.sleep(10) # data = board.get_current_board_data (256) # get latest 256 packages or less, doesnt remove them from internal buffer data = board.get_board_data() # get all data and remove it from internal buffer diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index 0e787468e..106b1ac22 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -1237,13 +1237,12 @@ BrainFlowBoards::BrainFlowBoards() brainflow_boards_json["boards"]["65"]["auxiliary"] = { {"name", "MuseAnthenaAux"}, - {"sampling_rate", 52}, - {"timestamp_channel", 7}, - {"marker_channel", 8}, + {"sampling_rate", 64}, + {"timestamp_channel", 4}, + {"marker_channel", 5}, {"package_num_channel", 0}, - {"num_rows", 9}, - {"accel_channels", {1, 2, 3}}, - {"gyro_channels", {4, 5, 6}} + {"num_rows", 6}, + {"ppg_channels", {1, 2, 3}}, }; } diff --git a/src/board_controller/muse/inc/muse_anthena.h b/src/board_controller/muse/inc/muse_anthena.h index fc0eeebd8..76ff345fa 100644 --- a/src/board_controller/muse/inc/muse_anthena.h +++ b/src/board_controller/muse/inc/muse_anthena.h @@ -23,6 +23,9 @@ class MuseAnthena : public BLELibBoard std::string bytes_to_string (const uint8_t *data, size_t size); + void parse_eeg_packet (const uint8_t *data, size_t size); + void parse_ppg_packet (const uint8_t *data, size_t size); + public: MuseAnthena (int board_id, struct BrainFlowInputParams params); ~MuseAnthena (); diff --git a/src/board_controller/muse/inc/muse_anthena_constants.h b/src/board_controller/muse/inc/muse_anthena_constants.h index e4517ef2d..7b4237584 100644 --- a/src/board_controller/muse/inc/muse_anthena_constants.h +++ b/src/board_controller/muse/inc/muse_anthena_constants.h @@ -7,6 +7,8 @@ #define MUSE_ANTHENA_GATT_ATTR_STREAM_TOGGLE "273e0001-4c4d-454d-96be-f03bac821358" #define MUSE_ANTHENA_GATT_EEG "273e0013-4c4d-454d-96be-f03bac821358" #define MUSE_ANTHENA_GATT_AUX "273e0014-4c4d-454d-96be-f03bac821358" +#define EEG_PACKET_TYPE 0X12 +#define PPG_PACKET_TYPE 0x11 // info for equations #define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.0000610352 diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp index 9dcc09930..dca1220fd 100644 --- a/src/board_controller/muse/muse_anthena.cpp +++ b/src/board_controller/muse/muse_anthena.cpp @@ -211,7 +211,8 @@ int MuseAnthena::prepare_session () } if (res == (int)BrainFlowExitCodes::STATUS_OK) { - // p21 - eeg only + // enable everything by default + // p21(eeg only) and p1034(sleep mode) and p1035 are also available res = config_board ("p21"); } else @@ -399,15 +400,101 @@ void MuseAnthena::adapter_on_scan_found ( void MuseAnthena::peripheral_on_eeg (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size) { - safe_logger (spdlog::level::trace, "EEG packet: {}", bytes_to_string (data, size)); - // TODO parse it and fill default preset + safe_logger (spdlog::level::trace, "incoming packet: {}", bytes_to_string (data, size)); + if (size < 200) + { + safe_logger (spdlog::level::trace, "unexpected size: {}", size); + return; + } + // it ignores the 1st ble package since it has smth in the beginning and blocks with 0x11 + + // packet sub number later + // TODO: find out what is there in the beginning + if ((data[9] == 0x11) || (data[9] == 0x12)) + { + parse_eeg_packet (data, size); + } + else + { + safe_logger (spdlog::level::trace, "unsupported packet type: {}", data[9]); + } +} + +void MuseAnthena::parse_eeg_packet (const uint8_t *data, size_t size) +{ + int sub_number = (int)data[10]; + int last_index = 0; + int num_rows = board_descr["default"]["num_rows"]; + double *package = new double[num_rows]; + std::vector eeg_channels = board_descr["default"]["eeg_channels"]; + for (int i = 0; i < num_rows; i++) + { + package[i] = 0.0; + } + package[board_descr["default"]["package_num_channel"].get ()] = (double)data[1]; + package[board_descr["default"]["timestamp_channel"].get ()] = get_timestamp (); + + for (int i = 9; i < (int)size - 8; i++) + { + if ((data[i] == EEG_PACKET_TYPE) && (data[i + 1] == sub_number) && (last_index <= i - 9)) + { + double gain = 1.0; // random value, I am not sure in equations at all + double eeg_scale = (double)(4.5 / float ((pow (2, 16) - 1)) / gain * 1000000.); + int eeg1_value = eeg_scale * cast_16bit_to_int32_swap_order (data + i + 2); + int eeg2_value = eeg_scale * cast_16bit_to_int32_swap_order (data + i + 4); + int eeg3_value = eeg_scale * cast_16bit_to_int32_swap_order (data + i + 6); + int eeg4_value = eeg_scale * cast_16bit_to_int32_swap_order (data + i + 8); + sub_number++; + last_index = i; + package[eeg_channels[0]] = eeg1_value; + package[eeg_channels[1]] = eeg2_value; + package[eeg_channels[2]] = eeg3_value; + package[eeg_channels[3]] = eeg4_value; + } + } + + push_package (package, (int)BrainFlowPresets::DEFAULT_PRESET); + + delete[] package; +} + +void MuseAnthena::parse_ppg_packet (const uint8_t *data, size_t size) +{ + int sub_number = (int)data[10]; + int last_index = 0; + int num_rows = board_descr["auxiliary"]["num_rows"]; + double *package = new double[num_rows]; + std::vector ppg_channels = board_descr["default"]["ppg_channels"]; + for (int i = 0; i < num_rows; i++) + { + package[i] = 0.0; + } + package[board_descr["default"]["package_num_channel"].get ()] = (double)data[1]; + package[board_descr["default"]["timestamp_channel"].get ()] = get_timestamp (); + + for (int i = 9; i < (int)size - 8; i++) + { + if ((data[i] == PPG_PACKET_TYPE) && (data[i + 1] == sub_number) && (last_index <= i - 9)) + { + sub_number++; + last_index = i; + package[ppg_channels[0]] = (double)cast_24bit_to_int32 ((unsigned char *)&data[2 + i]); + package[ppg_channels[1]] = + (double)cast_24bit_to_int32 ((unsigned char *)&data[2 + i + 3]); + package[ppg_channels[2]] = + (double)cast_24bit_to_int32 ((unsigned char *)&data[2 + i + 6]); + } + } + + push_package (package, (int)BrainFlowPresets::AUXILIARY_PRESET); + + delete[] package; } void MuseAnthena::peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size) { + // never seen smth in this char, keep it here just in case safe_logger (spdlog::level::trace, "AUX packet: {}", bytes_to_string (data, size)); - // TODO parse it and fill aux preset } From a1d5c96a7f2f143aa9d4ac7ca081928ba73bfa5f Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Sat, 25 Oct 2025 14:25:32 +0200 Subject: [PATCH 3/8] wip Signed-off-by: Andrey Parfenov --- src/board_controller/muse/inc/muse_anthena.h | 6 +- .../muse/inc/muse_anthena_constants.h | 22 ++- src/board_controller/muse/muse_anthena.cpp | 175 ++++++++++-------- 3 files changed, 113 insertions(+), 90 deletions(-) diff --git a/src/board_controller/muse/inc/muse_anthena.h b/src/board_controller/muse/inc/muse_anthena.h index 76ff345fa..43bc1c728 100644 --- a/src/board_controller/muse/inc/muse_anthena.h +++ b/src/board_controller/muse/inc/muse_anthena.h @@ -23,8 +23,8 @@ class MuseAnthena : public BLELibBoard std::string bytes_to_string (const uint8_t *data, size_t size); - void parse_eeg_packet (const uint8_t *data, size_t size); - void parse_ppg_packet (const uint8_t *data, size_t size); + void parse_main_packet (const uint8_t *data, size_t start_pos, size_t size); + void parse_sensor_packet (const uint8_t *data, size_t start_pos, size_t size); public: MuseAnthena (int board_id, struct BrainFlowInputParams params); @@ -38,7 +38,7 @@ class MuseAnthena : public BLELibBoard int config_board (std::string config); void adapter_on_scan_found (simpleble_adapter_t adapter, simpleble_peripheral_t peripheral); - void peripheral_on_eeg (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + void peripheral_on_main (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size); void peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size); diff --git a/src/board_controller/muse/inc/muse_anthena_constants.h b/src/board_controller/muse/inc/muse_anthena_constants.h index 7b4237584..ee9a813ea 100644 --- a/src/board_controller/muse/inc/muse_anthena_constants.h +++ b/src/board_controller/muse/inc/muse_anthena_constants.h @@ -7,9 +7,21 @@ #define MUSE_ANTHENA_GATT_ATTR_STREAM_TOGGLE "273e0001-4c4d-454d-96be-f03bac821358" #define MUSE_ANTHENA_GATT_EEG "273e0013-4c4d-454d-96be-f03bac821358" #define MUSE_ANTHENA_GATT_AUX "273e0014-4c4d-454d-96be-f03bac821358" -#define EEG_PACKET_TYPE 0X12 -#define PPG_PACKET_TYPE 0x11 -// info for equations -#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.0000610352 -#define MUSE_ANTHENA_GYRO_SCALE_FACTOR 0.007476 +// info about packet type +#define MUSE_S_ANTHENA_EEG_FOUR_PACKET_TYPE 0x11 +#define MUSE_S_ANTHENA_EEG_EIGHT_PACKET_TYPE 0x12 +#define MUSE_S_ANTHENA_OPTICS_FOUR_PACKET_TYPE 0x34 +#define MUSE_S_ANTHENA_OPTICS_EIGHT_PACKET_TYPE 0x35 +#define MUSE_S_ANTHENA_OPTICS_SIXTEEN_PACKET_TYPE 0x36 +#define MUSE_S_ANTHENA_SENSOR_PACKET_TYPE 0x47 +#define MUSE_S_ANTHENA_STATUS_PACKET_TYPE 0x53 +#define MUSE_S_ANTHENA_BATTERY_PACKET_TYPE 0x98 + +#define MUSE_S_ANTHENA_EEG_FOUR_PACKET_SIZE 32 +#define MUSE_S_ANTHENA_EEG_EIGHT_PACKET_SIZE 32 +#define MUSE_S_ANTHENA_OPTICS_FOUR_PACKET_SIZE 34 +#define MUSE_S_ANTHENA_OPTICS_EIGHT_PACKET_SIZE 44 +#define MUSE_S_ANTHENA_OPTICS_SIXTEEN_PACKET_SIZE 44 +#define MUSE_S_ANTHENA_SENSOR_PACKET_SIZE 40 +#define MUSE_S_ANTHENA_STATUS_PACKET_SIZE 28 diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp index dca1220fd..21432578c 100644 --- a/src/board_controller/muse/muse_anthena.cpp +++ b/src/board_controller/muse/muse_anthena.cpp @@ -15,10 +15,10 @@ void anthena_adapter_on_scan_found ( ((MuseAnthena *)(board))->adapter_on_scan_found (adapter, peripheral); } -void anthena_peripheral_on_eeg (simpleble_peripheral_t peripheral, simpleble_uuid_t service, +void anthena_peripheral_on_main (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size, void *board) { - ((MuseAnthena *)(board))->peripheral_on_eeg (peripheral, service, characteristic, data, size); + ((MuseAnthena *)(board))->peripheral_on_main (peripheral, service, characteristic, data, size); } void anthena_peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, @@ -166,7 +166,7 @@ int MuseAnthena::prepare_session () if (strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_EEG) == 0) { if (simpleble_peripheral_notify (muse_peripheral, service.uuid, - service.characteristics[j].uuid, ::anthena_peripheral_on_eeg, + service.characteristics[j].uuid, ::anthena_peripheral_on_main, (void *)this) == SIMPLEBLE_SUCCESS) { notified_characteristics.push_back ( @@ -213,7 +213,7 @@ int MuseAnthena::prepare_session () { // enable everything by default // p21(eeg only) and p1034(sleep mode) and p1035 are also available - res = config_board ("p21"); + res = config_board ("p1035"); } else { @@ -397,7 +397,7 @@ void MuseAnthena::adapter_on_scan_found ( } } -void MuseAnthena::peripheral_on_eeg (simpleble_peripheral_t peripheral, simpleble_uuid_t service, +void MuseAnthena::peripheral_on_main (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size) { safe_logger (spdlog::level::trace, "incoming packet: {}", bytes_to_string (data, size)); @@ -406,88 +406,83 @@ void MuseAnthena::peripheral_on_eeg (simpleble_peripheral_t peripheral, simplebl safe_logger (spdlog::level::trace, "unexpected size: {}", size); return; } - // it ignores the 1st ble package since it has smth in the beginning and blocks with 0x11 + - // packet sub number later - // TODO: find out what is there in the beginning - if ((data[9] == 0x11) || (data[9] == 0x12)) - { - parse_eeg_packet (data, size); - } - else - { - safe_logger (spdlog::level::trace, "unsupported packet type: {}", data[9]); - } -} - -void MuseAnthena::parse_eeg_packet (const uint8_t *data, size_t size) -{ - int sub_number = (int)data[10]; - int last_index = 0; - int num_rows = board_descr["default"]["num_rows"]; - double *package = new double[num_rows]; - std::vector eeg_channels = board_descr["default"]["eeg_channels"]; - for (int i = 0; i < num_rows; i++) - { - package[i] = 0.0; - } - package[board_descr["default"]["package_num_channel"].get ()] = (double)data[1]; - package[board_descr["default"]["timestamp_channel"].get ()] = get_timestamp (); - for (int i = 9; i < (int)size - 8; i++) + for (size_t i = 9; i < size - 1;) { - if ((data[i] == EEG_PACKET_TYPE) && (data[i + 1] == sub_number) && (last_index <= i - 9)) + switch (static_cast (data[i])) { - double gain = 1.0; // random value, I am not sure in equations at all - double eeg_scale = (double)(4.5 / float ((pow (2, 16) - 1)) / gain * 1000000.); - int eeg1_value = eeg_scale * cast_16bit_to_int32_swap_order (data + i + 2); - int eeg2_value = eeg_scale * cast_16bit_to_int32_swap_order (data + i + 4); - int eeg3_value = eeg_scale * cast_16bit_to_int32_swap_order (data + i + 6); - int eeg4_value = eeg_scale * cast_16bit_to_int32_swap_order (data + i + 8); - sub_number++; - last_index = i; - package[eeg_channels[0]] = eeg1_value; - package[eeg_channels[1]] = eeg2_value; - package[eeg_channels[2]] = eeg3_value; - package[eeg_channels[3]] = eeg4_value; - } - } - - push_package (package, (int)BrainFlowPresets::DEFAULT_PRESET); - - delete[] package; -} - -void MuseAnthena::parse_ppg_packet (const uint8_t *data, size_t size) -{ - int sub_number = (int)data[10]; - int last_index = 0; - int num_rows = board_descr["auxiliary"]["num_rows"]; - double *package = new double[num_rows]; - std::vector ppg_channels = board_descr["default"]["ppg_channels"]; - for (int i = 0; i < num_rows; i++) - { - package[i] = 0.0; - } - package[board_descr["default"]["package_num_channel"].get ()] = (double)data[1]; - package[board_descr["default"]["timestamp_channel"].get ()] = get_timestamp (); - - for (int i = 9; i < (int)size - 8; i++) - { - if ((data[i] == PPG_PACKET_TYPE) && (data[i + 1] == sub_number) && (last_index <= i - 9)) - { - sub_number++; - last_index = i; - package[ppg_channels[0]] = (double)cast_24bit_to_int32 ((unsigned char *)&data[2 + i]); - package[ppg_channels[1]] = - (double)cast_24bit_to_int32 ((unsigned char *)&data[2 + i + 3]); - package[ppg_channels[2]] = - (double)cast_24bit_to_int32 ((unsigned char *)&data[2 + i + 6]); + case 0x11: + if (((data[i + 1] == (last_sensor_packet_id + 1)) || (data[i + 1] == 0)) && + (i + MUSE_S_ANTHENA_MAIN_PACKET_SIZE <= size)) + { + parse_main_packet (data, i, size); + last_sensor_packet_id = data[i + 1]; + i += (MUSE_S_ANTHENA_MAIN_PACKET_SIZE + 1); + } + else + { + // safe_logger (spdlog::level::debug, + // "new id: {} doesnt match old one: {} for sensor packet: {}", data[i + 1], + // last_sensor_packet_id, bytes_to_string (data, size)); + i++; + } + break; + case 0x47: + if (((data[i + 1] == (last_sensor_packet_id + 1)) || (data[i + 1] == 0)) && + (i + MUSE_S_ANTHENA_SENSOR_PACKET_SIZE <= size)) + { + parse_sensor_packet (data, i, size); + last_sensor_packet_id = data[i + 1]; + i += (MUSE_S_ANTHENA_SENSOR_PACKET_SIZE + 1); + } + else + { + // safe_logger (spdlog::level::debug, + // "new id: {} doesnt match old one: {} for sensor packet: {}", data[i + 1], + // last_sensor_packet_id, bytes_to_string (data, size)); + i++; + } + break; + case 0x53: + if (((data[i + 1] == (last_status_packet_id + 1)) || (data[i + 1] == 0)) && + (i + MUSE_S_ANTHENA_STATUS_PACKET_SIZE <= size)) + { + last_status_packet_id = data[i + 1]; + i += (MUSE_S_ANTHENA_STATUS_PACKET_SIZE + 1); + } + else + { + // safe_logger (spdlog::level::debug, + // "new id: {} doesnt match old one: {} for status packet: {}", data[i + 1], + // last_status_packet_id, bytes_to_string (data, size)); + i++; + } + break; + case 0x12: + if (((data[i + 1] == (last_traditional_packet_id + 1)) || (data[i + 1] == 0)) && + (i + MUSE_S_ANTHENA_TRADITIONAL_PACKET_SIZE <= size)) + { + last_traditional_packet_id = data[i + 1]; + i += (MUSE_S_ANTHENA_TRADITIONAL_PACKET_SIZE + 1); + } + else + { + // safe_logger (spdlog::level::debug, + // "new id: {} doesnt match old one: {} for last_traditional_packet_id " + // "packet: {}", + // data[i + 1], last_traditional_packet_id, bytes_to_string (data, size)); + i++; + } + break; + default: + // safe_logger ( + // spdlog::level::debug, "incoming packet: {}", bytes_to_string (data, size)); + // safe_logger ("Unknown packet type:" data[i]); + // increase just by one, alternative would be to drop the entire transaction + i++; + break; } } - - push_package (package, (int)BrainFlowPresets::AUXILIARY_PRESET); - - delete[] package; } void MuseAnthena::peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, @@ -497,6 +492,22 @@ void MuseAnthena::peripheral_on_aux (simpleble_peripheral_t peripheral, simplebl safe_logger (spdlog::level::trace, "AUX packet: {}", bytes_to_string (data, size)); } +void MuseAnthena::parse_main_packet (const uint8_t *data, size_t start_pos, size_t size) +{ +} + +void MuseAnthena::parse_sensor_packet (const uint8_t *data, size_t start_pos, size_t size) +{ + if ((data == NULL) || (start_pos + MUSE_S_ANTHENA_SENSOR_PACKET_SIZE > size) || + (data[start_pos] != MUSE_S_ANTHENA_SENSOR_PACKET_TYPE)) + { + // safe_logger (spdlog::level::warn, "invalid packet in parse_sensor_packet: {}", + // bytes_to_string (data, size)); + return; + } + int packet_num = (int)data[start_pos + 1]; + int packet_sub_type = (data[start_pos + 2] << 8) | data[start_pos + 3]; +} std::string MuseAnthena::bytes_to_string (const uint8_t *data, size_t size) { From 6c919e9faaaa875ea8713286f0002458ca355053 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Sun, 26 Apr 2026 13:46:21 +0700 Subject: [PATCH 4/8] Fix MuseS Anthena prefix match --- src/board_controller/muse/muse_anthena.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp index 6160aeca8..ba964abf8 100644 --- a/src/board_controller/muse/muse_anthena.cpp +++ b/src/board_controller/muse/muse_anthena.cpp @@ -380,7 +380,7 @@ void MuseAnthena::adapter_on_scan_found ( } else { - if (strncmp (peripheral_identified, "MuseS", 4) == 0) + if (strncmp (peripheral_identified, "MuseS", 5) == 0) { found = true; } From 12daeaf047066e49fba5fe2f484eba8cce99d6eb Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Sun, 26 Apr 2026 14:14:12 +0700 Subject: [PATCH 5/8] Restore Muse Anthena data parsing --- src/board_controller/brainflow_boards.cpp | 30 +- src/board_controller/muse/inc/muse_anthena.h | 64 +- .../muse/inc/muse_anthena_constants.h | 29 +- .../muse/inc/muse_anthena_types.h | 11 + src/board_controller/muse/muse_anthena.cpp | 762 ++++++++++++++---- src/utils/inc/custom_cast.h | 31 + 6 files changed, 742 insertions(+), 185 deletions(-) create mode 100644 src/board_controller/muse/inc/muse_anthena_types.h diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index 572ec73a1..e6e7d65f8 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -1164,23 +1164,35 @@ BrainFlowBoards::BrainFlowBoards() { {"name", "MuseAnthena"}, {"sampling_rate", 256}, - {"timestamp_channel", 6}, - {"marker_channel", 7}, + {"timestamp_channel", 9}, + {"marker_channel", 10}, {"package_num_channel", 0}, - {"num_rows", 8}, - {"eeg_channels", {1, 2, 3, 4}}, - {"eeg_names", "TP9,AF7,AF8,TP10"}, - {"other_channels", {5}} + {"num_rows", 11}, + {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, + {"eeg_names", "TP9,AF7,AF8,TP10,AUX1,AUX2,AUX3,AUX4"} }; brainflow_boards_json["boards"]["67"]["auxiliary"] = { {"name", "MuseAnthenaAux"}, + {"sampling_rate", 52}, + {"timestamp_channel", 7}, + {"marker_channel", 8}, + {"package_num_channel", 0}, + {"num_rows", 9}, + {"accel_channels", {1, 2, 3}}, + {"gyro_channels", {4, 5, 6}} + }; + brainflow_boards_json["boards"]["67"]["ancillary"] = + { + {"name", "MuseAnthenaAnc"}, {"sampling_rate", 64}, - {"timestamp_channel", 4}, - {"marker_channel", 5}, + {"timestamp_channel", 21}, + {"marker_channel", 22}, {"package_num_channel", 0}, - {"num_rows", 6}, + {"num_rows", 23}, {"ppg_channels", {1, 2, 3}}, + {"optical_channels", {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}}, + {"battery_channel", 20} }; } diff --git a/src/board_controller/muse/inc/muse_anthena.h b/src/board_controller/muse/inc/muse_anthena.h index be779be3f..a1bcdcb47 100644 --- a/src/board_controller/muse/inc/muse_anthena.h +++ b/src/board_controller/muse/inc/muse_anthena.h @@ -1,49 +1,83 @@ #pragma once +#include +#include +#include #include #include +#include #include #include #include "ble_lib_board.h" +#include "muse_anthena_types.h" class MuseAnthena : public BLELibBoard { protected: + static const size_t PACKET_HEADER_SIZE = 14; + static const size_t SUBPACKET_HEADER_SIZE = 5; + + struct SensorConfig + { + SensorType type; + int n_channels; + int n_samples; + double sampling_rate; + size_t data_len; + bool variable_length; + + SensorConfig (); + SensorConfig (SensorType type, int n_channels, int n_samples, double sampling_rate, + size_t data_len, bool variable_length = false); + }; + volatile simpleble_adapter_t muse_adapter; volatile simpleble_peripheral_t muse_peripheral; bool initialized; bool is_streaming; std::mutex m; + std::mutex callback_lock; std::condition_variable cv; std::vector> notified_characteristics; std::pair control_characteristics; - int last_sensor_packet_id; - int last_status_packet_id; - int last_traditional_packet_id; + bool timestamp_initialized; + uint32_t first_device_tick; + double first_host_timestamp; + double last_battery; + std::string muse_preset; + bool enable_low_latency; + static std::string trim_string (const std::string &value); + static std::string to_lower (const std::string &value); + static bool is_valid_muse_preset (const std::string &preset); + static bool parse_bool_option (const std::string &value, bool &parsed); + int parse_muse_options (); std::string bytes_to_string (const uint8_t *data, size_t size); - - void parse_main_packet (const uint8_t *data, size_t start_pos, size_t size); - void parse_sensor_packet (const uint8_t *data, size_t start_pos, 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, uint32_t device_tick, 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); + double average_present (const std::array &values, + const std::array &present, const int *indexes, int len); + double get_sample_timestamp (uint32_t device_tick, int sample_index, double sampling_rate); public: MuseAnthena (int board_id, struct BrainFlowInputParams params); - ~MuseAnthena (); + ~MuseAnthena () override; - int prepare_session (); - int start_stream (int buffer_size, const char *streamer_params); - int stop_stream (); - int release_session (); - int config_board (std::string config, std::string &response); + int prepare_session () override; + int start_stream (int buffer_size, const char *streamer_params) override; + int stop_stream () override; + int release_session () override; + int config_board (std::string config, std::string &response) override; int config_board (std::string config); void adapter_on_scan_found (simpleble_adapter_t adapter, simpleble_peripheral_t peripheral); - void peripheral_on_main (simpleble_peripheral_t peripheral, simpleble_uuid_t service, - simpleble_uuid_t characteristic, const uint8_t *data, size_t size); - void peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + void peripheral_on_data (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size); void peripheral_on_status (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size); diff --git a/src/board_controller/muse/inc/muse_anthena_constants.h b/src/board_controller/muse/inc/muse_anthena_constants.h index 4828002e9..877a14f94 100644 --- a/src/board_controller/muse/inc/muse_anthena_constants.h +++ b/src/board_controller/muse/inc/muse_anthena_constants.h @@ -5,25 +5,12 @@ #define MUSE_ANTHENA_SERVICE_UUID 0xFE8D #define MUSE_ANTHENA_GATT_ATTR_STREAM_TOGGLE "273e0001-4c4d-454d-96be-f03bac821358" -#define MUSE_ANTHENA_GATT_EEG "273e0013-4c4d-454d-96be-f03bac821358" -#define MUSE_ANTHENA_GATT_AUX "273e0014-4c4d-454d-96be-f03bac821358" +#define MUSE_ANTHENA_GATT_DATA_1 "273e0013-4c4d-454d-96be-f03bac821358" +#define MUSE_ANTHENA_GATT_DATA_2 "273e0014-4c4d-454d-96be-f03bac821358" -// info about packet type -#define MUSE_S_ANTHENA_EEG_FOUR_PACKET_TYPE 0x11 -#define MUSE_S_ANTHENA_EEG_EIGHT_PACKET_TYPE 0x12 -#define MUSE_S_ANTHENA_OPTICS_FOUR_PACKET_TYPE 0x34 -#define MUSE_S_ANTHENA_OPTICS_EIGHT_PACKET_TYPE 0x35 -#define MUSE_S_ANTHENA_OPTICS_SIXTEEN_PACKET_TYPE 0x36 -#define MUSE_S_ANTHENA_SENSOR_PACKET_TYPE 0x47 -#define MUSE_S_ANTHENA_STATUS_PACKET_TYPE 0x53 -#define MUSE_S_ANTHENA_BATTERY_PACKET_TYPE 0x98 - -#define MUSE_S_ANTHENA_EEG_FOUR_PACKET_SIZE 32 -#define MUSE_S_ANTHENA_EEG_EIGHT_PACKET_SIZE 32 -#define MUSE_S_ANTHENA_MAIN_PACKET_SIZE MUSE_S_ANTHENA_EEG_FOUR_PACKET_SIZE -#define MUSE_S_ANTHENA_TRADITIONAL_PACKET_SIZE MUSE_S_ANTHENA_EEG_EIGHT_PACKET_SIZE -#define MUSE_S_ANTHENA_OPTICS_FOUR_PACKET_SIZE 34 -#define MUSE_S_ANTHENA_OPTICS_EIGHT_PACKET_SIZE 44 -#define MUSE_S_ANTHENA_OPTICS_SIXTEEN_PACKET_SIZE 44 -#define MUSE_S_ANTHENA_SENSOR_PACKET_SIZE 40 -#define MUSE_S_ANTHENA_STATUS_PACKET_SIZE 28 +// info for equations +#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.0000610352 +#define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.0074768 +#define MUSE_ANTHENA_DEVICE_CLOCK_HZ 256000.0 +#define MUSE_ANTHENA_EEG_SCALE_FACTOR (1450.0 / 16383.0) +#define MUSE_ANTHENA_OPTICS_SCALE_FACTOR (1.0 / 32768.0) diff --git a/src/board_controller/muse/inc/muse_anthena_types.h b/src/board_controller/muse/inc/muse_anthena_types.h new file mode 100644 index 000000000..d50c508d7 --- /dev/null +++ b/src/board_controller/muse/inc/muse_anthena_types.h @@ -0,0 +1,11 @@ +#pragma once + + +enum class SensorType +{ + EEG, + ACC_GYRO, + OPTICS, + BATTERY, + UNKNOWN +}; diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp index ba964abf8..d7add8582 100644 --- a/src/board_controller/muse/muse_anthena.cpp +++ b/src/board_controller/muse/muse_anthena.cpp @@ -1,30 +1,278 @@ #include "muse_anthena.h" +#include +#include +#include +#include #include +#include #include #include +#include +#include #include "custom_cast.h" #include "muse_anthena_constants.h" #include "timestamp.h" +MuseAnthena::SensorConfig::SensorConfig () + : type (SensorType::UNKNOWN) + , n_channels (0) + , n_samples (0) + , sampling_rate (0.0) + , data_len (0) + , variable_length (false) +{ +} + +MuseAnthena::SensorConfig::SensorConfig (SensorType type, int n_channels, int n_samples, + double sampling_rate, size_t data_len, bool variable_length) + : type (type) + , n_channels (n_channels) + , n_samples (n_samples) + , sampling_rate (sampling_rate) + , data_len (data_len) + , variable_length (variable_length) +{ +} + + +bool MuseAnthena::get_sensor_config (uint8_t tag, SensorConfig &config) +{ + switch (tag) + { + case 0x11: + config = SensorConfig (SensorType::EEG, 4, 4, 256.0, 28); + return true; + case 0x12: + config = SensorConfig (SensorType::EEG, 8, 2, 256.0, 28); + return true; + case 0x34: + config = SensorConfig (SensorType::OPTICS, 4, 3, 64.0, 30); + return true; + case 0x35: + config = SensorConfig (SensorType::OPTICS, 8, 2, 64.0, 40); + return true; + case 0x36: + config = SensorConfig (SensorType::OPTICS, 16, 1, 64.0, 40); + return true; + case 0x47: + config = SensorConfig (SensorType::ACC_GYRO, 6, 3, 52.0, 36); + return true; + case 0x53: + config = SensorConfig (SensorType::UNKNOWN, 0, 0, 0.0, 24); + return true; + case 0x88: + config = SensorConfig (SensorType::BATTERY, 1, 1, 0.2, 0, true); + return true; + case 0x98: + config = SensorConfig (SensorType::BATTERY, 1, 1, 1.0, 20); + return true; + default: + return false; + } +} + +int MuseAnthena::get_optics_canonical_index (uint8_t tag, int channel) +{ + static const int optics4_indexes[] = {4, 5, 6, 7}; + + if (tag == 0x34) + { + return (channel >= 0 && channel < 4) ? optics4_indexes[channel] : -1; + } + if ((tag == 0x35) && (channel >= 0) && (channel < 8)) + { + return channel; + } + if ((tag == 0x36) && (channel >= 0) && (channel < 16)) + { + return channel; + } + return -1; +} + +double MuseAnthena::average_present (const std::array &values, + const std::array &present, const int *indexes, int len) +{ + double sum = 0.0; + int count = 0; + for (int i = 0; i < len; i++) + { + int index = indexes[i]; + if ((index >= 0) && (index < 16) && present[(size_t)index]) + { + sum += values[(size_t)index]; + count++; + } + } + return count > 0 ? sum / count : 0.0; +} + + +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); +} + +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; +} + +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; +} + +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; +} + +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) + { + safe_logger (spdlog::level::err, "Invalid MuseAnthena other_info: {}", params.other_info); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + return (int)BrainFlowExitCodes::STATUS_OK; +} + + void anthena_adapter_on_scan_found ( simpleble_adapter_t adapter, simpleble_peripheral_t peripheral, void *board) { ((MuseAnthena *)(board))->adapter_on_scan_found (adapter, peripheral); } -void anthena_peripheral_on_main (simpleble_peripheral_t peripheral, simpleble_uuid_t service, +void anthena_peripheral_on_data (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size, void *board) { - ((MuseAnthena *)(board))->peripheral_on_main (peripheral, service, characteristic, data, size); + ((MuseAnthena *)(board))->peripheral_on_data (peripheral, service, characteristic, data, size); } -void anthena_peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, +void anthena_peripheral_on_status (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size, void *board) { - ((MuseAnthena *)(board))->peripheral_on_aux (peripheral, service, characteristic, data, size); + ((MuseAnthena *)(board)) + ->peripheral_on_status (peripheral, service, characteristic, data, size); } MuseAnthena::MuseAnthena (int board_id, struct BrainFlowInputParams params) @@ -34,9 +282,12 @@ MuseAnthena::MuseAnthena (int board_id, struct BrainFlowInputParams params) muse_adapter = NULL; muse_peripheral = NULL; is_streaming = false; - last_sensor_packet_id = -1; - last_status_packet_id = -1; - last_traditional_packet_id = -1; + timestamp_initialized = false; + first_device_tick = 0; + first_host_timestamp = 0.0; + last_battery = 0.0; + muse_preset = "p1041"; + enable_low_latency = true; } MuseAnthena::~MuseAnthena () @@ -52,6 +303,13 @@ int MuseAnthena::prepare_session () safe_logger (spdlog::level::info, "Session is already prepared"); return (int)BrainFlowExitCodes::STATUS_OK; } + int res = parse_muse_options (); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + safe_logger (spdlog::level::info, "Use MuseAnthena preset {} and low_latency {}", + muse_preset, enable_low_latency); if (params.timeout < 1) { params.timeout = 6; @@ -89,7 +347,6 @@ int MuseAnthena::prepare_session () } simpleble_adapter_scan_start (muse_adapter); - int res = (int)BrainFlowExitCodes::STATUS_OK; std::unique_lock lk (m); auto sec = std::chrono::seconds (1); if (cv.wait_for (lk, params.timeout * sec, [this] { return this->muse_peripheral != NULL; })) @@ -118,21 +375,13 @@ int MuseAnthena::prepare_session () safe_logger ( spdlog::level::warn, "Failed to connect to MuseAnthena Device: {}/3", i); res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; -#ifdef _WIN32 - Sleep (1000); -#else - sleep (1); -#endif + std::this_thread::sleep_for (std::chrono::seconds (1)); } } } -// https://github.com/OpenBluetoothToolbox/SimpleBLE/issues/26#issuecomment-955606799 -#ifdef _WIN32 - Sleep (1000); -#else - sleep (1); -#endif + // https://github.com/OpenBluetoothToolbox/SimpleBLE/issues/26#issuecomment-955606799 + std::this_thread::sleep_for (std::chrono::seconds (1)); bool control_characteristics_found = false; @@ -149,13 +398,12 @@ int MuseAnthena::prepare_session () res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; } - safe_logger (spdlog::level::trace, "found servce {}", service.uuid.value); + safe_logger (spdlog::level::trace, "found service {}", service.uuid.value); for (size_t j = 0; j < service.characteristic_count; j++) { safe_logger (spdlog::level::trace, "found characteristic {}", service.characteristics[j].uuid.value); - // control characteristic if (strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_ATTR_STREAM_TOGGLE) == 0) { @@ -163,13 +411,8 @@ int MuseAnthena::prepare_session () service.uuid, service.characteristics[j].uuid); control_characteristics_found = true; safe_logger (spdlog::level::info, "found control characteristic"); - } - - // eeg characteristic - if (strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_EEG) == 0) - { if (simpleble_peripheral_notify (muse_peripheral, service.uuid, - service.characteristics[j].uuid, ::anthena_peripheral_on_main, + service.characteristics[j].uuid, ::anthena_peripheral_on_status, (void *)this) == SIMPLEBLE_SUCCESS) { notified_characteristics.push_back ( @@ -178,17 +421,18 @@ int MuseAnthena::prepare_session () } else { - safe_logger (spdlog::level::err, "Failed to notify for {} {}", + safe_logger (spdlog::level::warn, "Failed to notify for control {} {}", service.uuid.value, service.characteristics[j].uuid.value); - res = (int)BrainFlowExitCodes::GENERAL_ERROR; } } - // aux characteristic(acce, gyro, etc) - if (strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_AUX) == 0) + if ((strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_DATA_1) == 0) || + (strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_DATA_2) == 0)) { + // Athena multiplexes EEG, IMU, optics, and battery packets across data + // characteristics; use one parser intentionally and route by packet tag. if (simpleble_peripheral_notify (muse_peripheral, service.uuid, - service.characteristics[j].uuid, ::anthena_peripheral_on_aux, + service.characteristics[j].uuid, ::anthena_peripheral_on_data, (void *)this) == SIMPLEBLE_SUCCESS) { notified_characteristics.push_back ( @@ -206,19 +450,42 @@ int MuseAnthena::prepare_session () } } + if ((res == (int)BrainFlowExitCodes::STATUS_OK) && (!control_characteristics_found)) + { + safe_logger (spdlog::level::err, "failed to find control characteristic"); + res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) { initialized = true; - // used to be v1 for old muse devices - res = config_board ("v4"); + res = config_board ("v6"); } if (res == (int)BrainFlowExitCodes::STATUS_OK) { - // enable everything by default - // p21(eeg only) and p1034(sleep mode) and p1035 are also available - res = config_board ("p1035"); + std::this_thread::sleep_for (std::chrono::milliseconds (200)); + res = config_board ("s"); } - else + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + std::this_thread::sleep_for (std::chrono::milliseconds (200)); + res = config_board ("h"); + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + std::this_thread::sleep_for (std::chrono::milliseconds (200)); + res = config_board (muse_preset); + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + std::this_thread::sleep_for (std::chrono::milliseconds (200)); + res = config_board ("s"); + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + std::this_thread::sleep_for (std::chrono::milliseconds (200)); + } + if (res != (int)BrainFlowExitCodes::STATUS_OK) { release_session (); } @@ -236,14 +503,40 @@ int MuseAnthena::start_stream (int buffer_size, const char *streamer_params) int res = prepare_for_acquisition (buffer_size, streamer_params); if (res == (int)BrainFlowExitCodes::STATUS_OK) { - last_sensor_packet_id = -1; - last_status_packet_id = -1; - last_traditional_packet_id = -1; + std::lock_guard callback_guard (callback_lock); + timestamp_initialized = false; + first_device_tick = 0; + first_host_timestamp = 0.0; + last_battery = 0.0; + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + res = config_board ("dc001"); } if (res == (int)BrainFlowExitCodes::STATUS_OK) { + std::this_thread::sleep_for (std::chrono::milliseconds (50)); res = config_board ("dc001"); } + if ((res == (int)BrainFlowExitCodes::STATUS_OK) && enable_low_latency) + { + std::this_thread::sleep_for (std::chrono::milliseconds (100)); + int l1_res = config_board ("L1"); + if (l1_res != (int)BrainFlowExitCodes::STATUS_OK) + { + safe_logger (spdlog::level::warn, "Failed to enable MuseAnthena low latency mode"); + } + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + std::this_thread::sleep_for (std::chrono::milliseconds (300)); + int status_res = config_board ("s"); + if (status_res != (int)BrainFlowExitCodes::STATUS_OK) + { + safe_logger (spdlog::level::warn, "Failed to request MuseAnthena status after start"); + } + std::this_thread::sleep_for (std::chrono::milliseconds (200)); + } if (res == (int)BrainFlowExitCodes::STATUS_OK) { is_streaming = true; @@ -262,12 +555,25 @@ int MuseAnthena::stop_stream () if (is_streaming) { res = config_board ("h"); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + bool is_connected = false; + if ((simpleble_peripheral_is_connected (muse_peripheral, &is_connected) == + SIMPLEBLE_SUCCESS) && + (!is_connected)) + { + safe_logger (spdlog::level::warn, + "MuseAnthena device is already disconnected during stop_stream"); + res = (int)BrainFlowExitCodes::STATUS_OK; + } + } } else { res = (int)BrainFlowExitCodes::STREAM_ALREADY_RUN_ERROR; } is_streaming = false; + timestamp_initialized = false; return res; } @@ -278,26 +584,35 @@ int MuseAnthena::release_session () // repeat it multiple times, failure here may lead to a crash for (int i = 0; i < 2; i++) { + bool is_connected = true; + if ((muse_peripheral != NULL) && + (simpleble_peripheral_is_connected (muse_peripheral, &is_connected) == + SIMPLEBLE_SUCCESS) && + (!is_connected)) + { + break; + } + stop_stream (); - // need to wait for notifications to stop triggered before unsubscribing, otherwise - // macos fails inside simpleble with timeout -#ifdef _WIN32 - Sleep (2000); -#else - sleep (2); -#endif - for (auto notified : notified_characteristics) + if (muse_peripheral != NULL) { - if (simpleble_peripheral_unsubscribe ( - muse_peripheral, notified.first, notified.second) != SIMPLEBLE_SUCCESS) + // need to wait for notifications to stop triggered before unsubscribing, otherwise + // macos fails inside simpleble with timeout + std::this_thread::sleep_for (std::chrono::seconds (2)); + for (auto notified : notified_characteristics) { - safe_logger (spdlog::level::err, "failed to unsubscribe for {} {}", - notified.first.value, notified.second.value); + if (simpleble_peripheral_unsubscribe ( + muse_peripheral, notified.first, notified.second) != SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::err, "failed to unsubscribe for {} {}", + notified.first.value, notified.second.value); + } } } } free_packages (); initialized = false; + notified_characteristics.clear (); } if (muse_peripheral != NULL) { @@ -359,6 +674,7 @@ int MuseAnthena::config_board (std::string config) void MuseAnthena::adapter_on_scan_found ( simpleble_adapter_t adapter, simpleble_peripheral_t peripheral) { + (void)adapter; char *peripheral_identified = simpleble_peripheral_identifier (peripheral); char *peripheral_address = simpleble_peripheral_address (peripheral); bool found = false; @@ -406,116 +722,280 @@ void MuseAnthena::adapter_on_scan_found ( } } -void MuseAnthena::peripheral_on_main (simpleble_peripheral_t peripheral, simpleble_uuid_t service, +void MuseAnthena::peripheral_on_data (simpleble_peripheral_t peripheral, simpleble_uuid_t service, + simpleble_uuid_t characteristic, const uint8_t *data, size_t size) +{ + (void)peripheral; + (void)service; + (void)characteristic; + handle_data_notification (data, size); +} + +void MuseAnthena::peripheral_on_status (simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t *data, size_t size) { - safe_logger (spdlog::level::trace, "incoming packet: {}", bytes_to_string (data, size)); - if (size < 200) + (void)peripheral; + (void)service; + (void)characteristic; + safe_logger (spdlog::level::debug, "Status packet: {}", bytes_to_string (data, size)); +} + +void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size) +{ + std::lock_guard callback_guard (callback_lock); + + size_t offset = 0; + while (offset < size) + { + if (size - offset < PACKET_HEADER_SIZE) + { + safe_logger (spdlog::level::warn, "Short Athena message tail: {} bytes", size - offset); + return; + } + + uint8_t packet_len = data[offset]; + if ((packet_len < PACKET_HEADER_SIZE) || (offset + packet_len > size)) + { + safe_logger (spdlog::level::warn, "Invalid Athena packet length: {}", packet_len); + return; + } + + const uint8_t *packet = data + offset; + uint8_t packet_index = packet[1]; + uint32_t device_tick = + cast_32bit_to_uint32_little_endian ((const unsigned char *)(packet + 2)); + uint8_t primary_tag = packet[9]; + 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; + + SensorConfig primary_config; + if (get_sensor_config (primary_tag, primary_config)) + { + size_t primary_data_len = + 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, device_tick, packet_data, primary_data_len); + packet_data_offset = primary_data_len; + } + else + { + safe_logger (spdlog::level::warn, + "Invalid Athena primary payload for tag 0x{:02x}: {} bytes", + (unsigned int)primary_tag, packet_data_size); + packet_data_offset = packet_data_size; + } + } + else + { + safe_logger (spdlog::level::trace, "Unknown Athena primary tag: 0x{:02x}", + (unsigned int)primary_tag); + packet_data_offset = packet_data_size; + } + + while (packet_data_offset + SUBPACKET_HEADER_SIZE <= packet_data_size) + { + uint8_t tag = packet_data[packet_data_offset]; + uint8_t subpacket_index = packet_data[packet_data_offset + 1]; + SensorConfig config; + if (!get_sensor_config (tag, config)) + { + safe_logger (spdlog::level::trace, "Unknown Athena subpacket tag: 0x{:02x}", + (unsigned int)tag); + break; + } + + size_t remaining = packet_data_size - packet_data_offset - SUBPACKET_HEADER_SIZE; + size_t sensor_data_len = config.variable_length ? remaining : config.data_len; + if ((sensor_data_len == 0) || (sensor_data_len > remaining)) + { + safe_logger (spdlog::level::warn, + "Invalid Athena subpacket payload for tag 0x{:02x}: {} bytes", + (unsigned int)tag, remaining); + break; + } + + parse_sensor_payload (tag, subpacket_index, device_tick, + packet_data + packet_data_offset + SUBPACKET_HEADER_SIZE, sensor_data_len); + packet_data_offset += SUBPACKET_HEADER_SIZE + sensor_data_len; + } + + offset += packet_len; + } +} + +void MuseAnthena::parse_sensor_payload ( + uint8_t tag, uint8_t sequence_num, uint32_t device_tick, const uint8_t *data, size_t size) +{ + SensorConfig config; + if (!get_sensor_config (tag, config)) + { + return; + } + + if (config.type == SensorType::UNKNOWN) { - safe_logger (spdlog::level::trace, "unexpected size: {}", size); + safe_logger (spdlog::level::trace, "Skipping unknown Athena payload tag 0x{:02x}", + (unsigned int)tag); return; } - for (size_t i = 9; i < size - 1;) + if (config.type == SensorType::BATTERY) { - switch (static_cast (data[i])) + if (size >= 2) { - case 0x11: - if (((data[i + 1] == (last_sensor_packet_id + 1)) || (data[i + 1] == 0)) && - (i + MUSE_S_ANTHENA_MAIN_PACKET_SIZE <= size)) - { - parse_main_packet (data, i, size); - last_sensor_packet_id = data[i + 1]; - i += (MUSE_S_ANTHENA_MAIN_PACKET_SIZE + 1); - } - else - { - // safe_logger (spdlog::level::debug, - // "new id: {} doesnt match old one: {} for sensor packet: {}", data[i + 1], - // last_sensor_packet_id, bytes_to_string (data, size)); - i++; - } - break; - case 0x47: - if (((data[i + 1] == (last_sensor_packet_id + 1)) || (data[i + 1] == 0)) && - (i + MUSE_S_ANTHENA_SENSOR_PACKET_SIZE <= size)) - { - parse_sensor_packet (data, i, size); - last_sensor_packet_id = data[i + 1]; - i += (MUSE_S_ANTHENA_SENSOR_PACKET_SIZE + 1); - } - else - { - // safe_logger (spdlog::level::debug, - // "new id: {} doesnt match old one: {} for sensor packet: {}", data[i + 1], - // last_sensor_packet_id, bytes_to_string (data, size)); - i++; - } - break; - case 0x53: - if (((data[i + 1] == (last_status_packet_id + 1)) || (data[i + 1] == 0)) && - (i + MUSE_S_ANTHENA_STATUS_PACKET_SIZE <= size)) - { - last_status_packet_id = data[i + 1]; - i += (MUSE_S_ANTHENA_STATUS_PACKET_SIZE + 1); - } - else + last_battery = + (double)cast_16bit_to_uint16_little_endian ((const unsigned char *)data) / 256.0; + } + return; + } + + if ((!config.variable_length) && (size < config.data_len)) + { + safe_logger (spdlog::level::warn, "Short Athena payload for tag 0x{:02x}: {}", + (unsigned int)tag, size); + return; + } + + if (config.type == SensorType::EEG) + { + int num_rows = board_descr["default"]["num_rows"].get (); + std::vector eeg_channels = board_descr["default"]["eeg_channels"]; + int package_num_channel = board_descr["default"]["package_num_channel"].get (); + int timestamp_channel = board_descr["default"]["timestamp_channel"].get (); + + 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; + for (int channel = 0; channel < config.n_channels; channel++) + { + size_t bit_start = (size_t)(sample * config.n_channels + channel) * 14; + uint32_t raw = extract_lsb_bits ((const unsigned char *)data, bit_start, 14); + if ((size_t)channel < eeg_channels.size ()) { - // safe_logger (spdlog::level::debug, - // "new id: {} doesnt match old one: {} for status packet: {}", data[i + 1], - // last_status_packet_id, bytes_to_string (data, size)); - i++; + package[(size_t)eeg_channels[(size_t)channel]] = + (double)raw * MUSE_ANTHENA_EEG_SCALE_FACTOR; } - break; - case 0x12: - if (((data[i + 1] == (last_traditional_packet_id + 1)) || (data[i + 1] == 0)) && - (i + MUSE_S_ANTHENA_TRADITIONAL_PACKET_SIZE <= size)) + } + package[(size_t)timestamp_channel] = + get_sample_timestamp (device_tick, sample, config.sampling_rate); + push_package (package.data (), (int)BrainFlowPresets::DEFAULT_PRESET); + } + return; + } + + if (config.type == SensorType::ACC_GYRO) + { + int num_rows = board_descr["auxiliary"]["num_rows"].get (); + std::vector accel_channels = board_descr["auxiliary"]["accel_channels"]; + std::vector gyro_channels = board_descr["auxiliary"]["gyro_channels"]; + int package_num_channel = board_descr["auxiliary"]["package_num_channel"].get (); + int timestamp_channel = board_descr["auxiliary"]["timestamp_channel"].get (); + + 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; + for (int channel = 0; channel < 3; channel++) + { + int16_t raw = cast_16bit_to_int16_little_endian ( + (const unsigned char *)(data + (sample * config.n_channels + channel) * 2)); + if ((size_t)channel < accel_channels.size ()) { - last_traditional_packet_id = data[i + 1]; - i += (MUSE_S_ANTHENA_TRADITIONAL_PACKET_SIZE + 1); + package[(size_t)accel_channels[(size_t)channel]] = + (double)raw * MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR; } - else + } + for (int channel = 0; channel < 3; channel++) + { + int16_t raw = cast_16bit_to_int16_little_endian ((const unsigned char *)(data + + (sample * config.n_channels + channel + 3) * 2)); + if ((size_t)channel < gyro_channels.size ()) { - // safe_logger (spdlog::level::debug, - // "new id: {} doesnt match old one: {} for last_traditional_packet_id " - // "packet: {}", - // data[i + 1], last_traditional_packet_id, bytes_to_string (data, size)); - i++; + package[(size_t)gyro_channels[(size_t)channel]] = + (double)raw * MUSE_ANTHENA_GYRO_SCALE_FACTOR; } - break; - default: - // safe_logger ( - // spdlog::level::debug, "incoming packet: {}", bytes_to_string (data, size)); - // safe_logger ("Unknown packet type:" data[i]); - // increase just by one, alternative would be to drop the entire transaction - i++; - break; + } + package[(size_t)timestamp_channel] = + get_sample_timestamp (device_tick, sample, config.sampling_rate); + push_package (package.data (), (int)BrainFlowPresets::AUXILIARY_PRESET); } + return; } -} -void MuseAnthena::peripheral_on_aux (simpleble_peripheral_t peripheral, simpleble_uuid_t service, - simpleble_uuid_t characteristic, const uint8_t *data, size_t size) -{ - // never seen smth in this char, keep it here just in case - safe_logger (spdlog::level::trace, "AUX packet: {}", bytes_to_string (data, size)); -} + if (config.type == SensorType::OPTICS) + { + static const int nir_indexes[] = {0, 1, 4, 5}; + static const int red_indexes[] = {8, 9, 12, 13}; + static const int ir_indexes[] = {2, 3, 6, 7}; -void MuseAnthena::parse_main_packet (const uint8_t *data, size_t start_pos, size_t size) -{ + int num_rows = board_descr["ancillary"]["num_rows"].get (); + std::vector ppg_channels = board_descr["ancillary"]["ppg_channels"]; + std::vector optical_channels = board_descr["ancillary"]["optical_channels"]; + int package_num_channel = board_descr["ancillary"]["package_num_channel"].get (); + int timestamp_channel = board_descr["ancillary"]["timestamp_channel"].get (); + int battery_channel = board_descr["ancillary"]["battery_channel"].get (); + + for (int sample = 0; sample < config.n_samples; sample++) + { + std::array optical_values; + std::array optical_present; + optical_values.fill (0.0); + optical_present.fill (false); + + std::vector package ((size_t)num_rows, 0.0); + package[(size_t)package_num_channel] = (double)sequence_num; + package[(size_t)battery_channel] = last_battery; + + for (int channel = 0; channel < config.n_channels; channel++) + { + size_t bit_start = (size_t)(sample * config.n_channels + channel) * 20; + uint32_t raw = extract_lsb_bits ((const unsigned char *)data, bit_start, 20); + int canonical_index = get_optics_canonical_index (tag, channel); + if ((canonical_index >= 0) && (canonical_index < 16)) + { + double value = (double)raw * MUSE_ANTHENA_OPTICS_SCALE_FACTOR; + optical_values[(size_t)canonical_index] = value; + optical_present[(size_t)canonical_index] = true; + if ((size_t)canonical_index < optical_channels.size ()) + { + package[(size_t)optical_channels[(size_t)canonical_index]] = value; + } + } + } + + if (ppg_channels.size () >= 3) + { + package[(size_t)ppg_channels[0]] = + average_present (optical_values, optical_present, nir_indexes, 4); + package[(size_t)ppg_channels[1]] = + average_present (optical_values, optical_present, red_indexes, 4); + package[(size_t)ppg_channels[2]] = + average_present (optical_values, optical_present, ir_indexes, 4); + } + package[(size_t)timestamp_channel] = + get_sample_timestamp (device_tick, sample, config.sampling_rate); + push_package (package.data (), (int)BrainFlowPresets::ANCILLARY_PRESET); + } + } } -void MuseAnthena::parse_sensor_packet (const uint8_t *data, size_t start_pos, size_t size) +double MuseAnthena::get_sample_timestamp ( + uint32_t device_tick, int sample_index, double sampling_rate) { - if ((data == NULL) || (start_pos + MUSE_S_ANTHENA_SENSOR_PACKET_SIZE > size) || - (data[start_pos] != MUSE_S_ANTHENA_SENSOR_PACKET_TYPE)) + if (!timestamp_initialized) { - // safe_logger (spdlog::level::warn, "invalid packet in parse_sensor_packet: {}", - // bytes_to_string (data, size)); - return; + timestamp_initialized = true; + first_device_tick = device_tick; + first_host_timestamp = get_timestamp (); } - int packet_num = (int)data[start_pos + 1]; - int packet_sub_type = (data[start_pos + 2] << 8) | data[start_pos + 3]; + + uint32_t elapsed_ticks = device_tick - first_device_tick; + return first_host_timestamp + (double)elapsed_ticks / MUSE_ANTHENA_DEVICE_CLOCK_HZ + + (double)sample_index / sampling_rate; } std::string MuseAnthena::bytes_to_string (const uint8_t *data, size_t size) @@ -524,7 +1004,9 @@ std::string MuseAnthena::bytes_to_string (const uint8_t *data, size_t size) for (size_t i = 0; i < size; i++) { if (i > 0) + { oss << " "; + } oss << static_cast (data[i]); } return oss.str (); diff --git a/src/utils/inc/custom_cast.h b/src/utils/inc/custom_cast.h index 345447d5a..7ae2b501d 100644 --- a/src/utils/inc/custom_cast.h +++ b/src/utils/inc/custom_cast.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,22 @@ inline int32_t cast_16bit_to_int32_swap_order (const unsigned char *byte_array) return cast_16bit_to_int32 (swapped); } +inline uint16_t cast_16bit_to_uint16_little_endian (const unsigned char *byte_array) +{ + return (uint16_t)byte_array[0] | ((uint16_t)byte_array[1] << 8); +} + +inline int16_t cast_16bit_to_int16_little_endian (const unsigned char *byte_array) +{ + return (int16_t)cast_16bit_to_int32_swap_order (byte_array); +} + +inline uint32_t cast_32bit_to_uint32_little_endian (const unsigned char *byte_array) +{ + return (uint32_t)byte_array[0] | ((uint32_t)byte_array[1] << 8) | + ((uint32_t)byte_array[2] << 16) | ((uint32_t)byte_array[3] << 24); +} + inline int32_t cast_13bit_to_int32 (const unsigned char *byte_array) { int prefix = 0; @@ -90,6 +107,20 @@ inline void uchar_to_bits (unsigned char c, unsigned char *bits) } } +inline uint32_t extract_lsb_bits (const unsigned char *data, size_t bit_start, int bit_width) +{ + uint32_t value = 0; + for (int bit = 0; bit < bit_width; bit++) + { + size_t absolute_bit = bit_start + (size_t)bit; + if (((data[absolute_bit / 8] >> (absolute_bit % 8)) & 0x01) != 0) + { + value |= (uint32_t)1 << bit; + } + } + return value; +} + // this function is specific to the ganglion board, as it deals with its quirks // input array is an array of 0 and 1 (not the chartacters '0' and '1', // but 8-bit unsigned integers 0 and 1) From 8257691a713357a896838c2516f07d90ad6a4ee8 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Sun, 26 Apr 2026 17:57:38 +0700 Subject: [PATCH 6/8] fixing muse anthena implementation Signed-off-by: Andrey Parfenov --- cpp_package/src/board_shim.cpp | 12 +++++ cpp_package/src/inc/board_shim.h | 7 +++ .../brainflow/board_controller_library.cs | 17 +++++++ .../brainflow/brainflow/board_descr.cs | 3 ++ .../brainflow/brainflow/board_shim.cs | 24 +++++++++ .../src/main/java/brainflow/BoardDescr.java | 2 + .../src/main/java/brainflow/BoardShim.java | 46 +++++++++++++++++ julia_package/brainflow/src/board_shim.jl | 1 + matlab_package/brainflow/BoardShim.m | 11 ++++ nodejs_package/brainflow/board_shim.ts | 16 ++++++ nodejs_package/brainflow/functions.types.ts | 8 +++ python_package/brainflow/board_shim.py | 32 ++++++++++++ rust_package/brainflow/src/board_shim.rs | 4 ++ .../brainflow/src/ffi/board_controller.rs | 8 +++ src/board_controller/board_info_getter.cpp | 5 ++ src/board_controller/brainflow_boards.cpp | 16 +++--- src/board_controller/inc/board_info_getter.h | 2 + src/board_controller/muse/inc/muse_anthena.h | 3 -- .../muse/inc/muse_anthena_constants.h | 2 +- src/board_controller/muse/muse_anthena.cpp | 50 ++++--------------- 20 files changed, 217 insertions(+), 52 deletions(-) diff --git a/cpp_package/src/board_shim.cpp b/cpp_package/src/board_shim.cpp index b1f37f1da..2e743c343 100644 --- a/cpp_package/src/board_shim.cpp +++ b/cpp_package/src/board_shim.cpp @@ -508,6 +508,18 @@ std::vector BoardShim::get_ppg_channels (int board_id, int preset) return std::vector (channels, channels + len); } +std::vector BoardShim::get_optical_channels (int board_id, int preset) +{ + int channels[MAX_CHANNELS]; + int len = 0; + int res = ::get_optical_channels (board_id, preset, channels, &len); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + throw BrainFlowException ("failed to get board info", res); + } + return std::vector (channels, channels + len); +} + std::vector BoardShim::get_accel_channels (int board_id, int preset) { int channels[MAX_CHANNELS]; diff --git a/cpp_package/src/inc/board_shim.h b/cpp_package/src/inc/board_shim.h index 57c4a834c..6ccc7b008 100644 --- a/cpp_package/src/inc/board_shim.h +++ b/cpp_package/src/inc/board_shim.h @@ -138,6 +138,13 @@ class BoardShim */ static std::vector get_ppg_channels ( int board_id, int preset = (int)BrainFlowPresets::DEFAULT_PRESET); + /** + * get row indices which hold optical data + * @param board_id board id of your device + * @throw BrainFlowException If this board has no such data exit code is UNSUPPORTED_BOARD_ERROR + */ + static std::vector get_optical_channels ( + int board_id, int preset = (int)BrainFlowPresets::DEFAULT_PRESET); /** * get row indices which hold EDA data * @param board_id board id of your device diff --git a/csharp_package/brainflow/brainflow/board_controller_library.cs b/csharp_package/brainflow/brainflow/board_controller_library.cs index c2dad2a95..9f4257f3b 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -184,6 +184,8 @@ public static class BoardControllerLibrary64 [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_ppg_channels (int board_id, int preset, int[] channels, int[] len); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int get_optical_channels (int board_id, int preset, int[] channels, int[] len); + [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_accel_channels (int board_id, int preset, int[] channels, int[] len); [DllImport ("BoardController", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_rotation_channels (int board_id, int preset, int[] channels, int[] len); @@ -276,6 +278,8 @@ public static class BoardControllerLibrary32 [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_ppg_channels (int board_id, int preset, int[] channels, int[] len); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int get_optical_channels (int board_id, int preset, int[] channels, int[] len); + [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_accel_channels (int board_id, int preset, int[] channels, int[] len); [DllImport ("BoardController32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_rotation_channels (int board_id, int preset, int[] channels, int[] len); @@ -774,6 +778,19 @@ public static int get_ppg_channels (int board_id, int preset, int[] channels, in return (int)BrainFlowExitCodes.GENERAL_ERROR; } + public static int get_optical_channels (int board_id, int preset, int[] channels, int[] len) + { + switch (PlatformHelper.get_library_environment ()) + { + case LibraryEnvironment.x64: + return BoardControllerLibrary64.get_optical_channels (board_id, preset, channels, len); + case LibraryEnvironment.x86: + return BoardControllerLibrary32.get_optical_channels (board_id, preset, channels, len); + } + + return (int)BrainFlowExitCodes.GENERAL_ERROR; + } + public static int get_accel_channels (int board_id, int preset, int[] channels, int[] len) { switch (PlatformHelper.get_library_environment ()) diff --git a/csharp_package/brainflow/brainflow/board_descr.cs b/csharp_package/brainflow/brainflow/board_descr.cs index a48da22a5..8842bac45 100644 --- a/csharp_package/brainflow/brainflow/board_descr.cs +++ b/csharp_package/brainflow/brainflow/board_descr.cs @@ -18,6 +18,8 @@ public class BoardDescr [DataMember] public int[] ppg_channels; [DataMember] + public int[] optical_channels; + [DataMember] public int[] eda_channels; [DataMember] public int[] accel_channels; @@ -54,6 +56,7 @@ public BoardDescr () exg_channels = null; emg_channels = null; ppg_channels = null; + optical_channels = null; eda_channels = null; accel_channels = null; rotation_channels = null; diff --git a/csharp_package/brainflow/brainflow/board_shim.cs b/csharp_package/brainflow/brainflow/board_shim.cs index 17149bab1..fc0390680 100644 --- a/csharp_package/brainflow/brainflow/board_shim.cs +++ b/csharp_package/brainflow/brainflow/board_shim.cs @@ -441,6 +441,30 @@ public static int[] get_ppg_channels (int board_id, int preset = (int)BrainFlowP return result; } + /// + /// get row indices which hold optical data + /// + /// + /// preset for device + /// array of row nums + /// If this board has no such data exit code is UNSUPPORTED_BOARD_ERROR + public static int[] get_optical_channels (int board_id, int preset = (int)BrainFlowPresets.DEFAULT_PRESET) + { + int[] len = new int[1]; + int[] channels = new int[512]; + int res = BoardControllerLibrary.get_optical_channels (board_id, preset, channels, len); + if (res != (int)BrainFlowExitCodes.STATUS_OK) + { + throw new BrainFlowError (res); + } + int[] result = new int[len[0]]; + for (int i = 0; i < len[0]; i++) + { + result[i] = channels[i]; + } + return result; + } + /// /// get row indices which hold accel data /// diff --git a/java_package/brainflow/src/main/java/brainflow/BoardDescr.java b/java_package/brainflow/src/main/java/brainflow/BoardDescr.java index 66310a804..18dd75b23 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardDescr.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardDescr.java @@ -10,6 +10,7 @@ public class BoardDescr public List exg_channels; public List emg_channels; public List ppg_channels; + public List optical_channels; public List eda_channels; public List accel_channels; public List rotation_channels; @@ -33,6 +34,7 @@ public BoardDescr () exg_channels = null; emg_channels = null; ppg_channels = null; + optical_channels = null; eda_channels = null; accel_channels = null; rotation_channels = null; diff --git a/java_package/brainflow/src/main/java/brainflow/BoardShim.java b/java_package/brainflow/src/main/java/brainflow/BoardShim.java index 0c9bbcc23..c6b059ad9 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardShim.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardShim.java @@ -82,6 +82,8 @@ int get_current_board_data (int num_samples, int preset, double[] data_buf, int[ int get_ppg_channels (int board_id, int preset, int[] ppg_channels, int[] len); + int get_optical_channels (int board_id, int preset, int[] optical_channels, int[] len); + int get_accel_channels (int board_id, int preset, int[] accel_channels, int[] len); int get_rotation_channels (int board_id, int preset, int[] rotation_channels, int[] len); @@ -1108,6 +1110,50 @@ public static int[] get_ppg_channels (BoardIds board_id) throws BrainFlowError return get_ppg_channels (board_id.get_code ()); } + /** + * get row indices in returned by get_board_data() 2d array which contain + * optical data + */ + public static int[] get_optical_channels (int board_id, BrainFlowPresets preset) throws BrainFlowError + { + int[] len = new int[1]; + int[] channels = new int[512]; + int ec = instance.get_optical_channels (board_id, preset.get_code (), channels, len); + if (ec != BrainFlowExitCode.STATUS_OK.get_code ()) + { + throw new BrainFlowError ("Error in board info getter", ec); + } + + return Arrays.copyOfRange (channels, 0, len[0]); + } + + /** + * get row indices in returned by get_board_data() 2d array which contain + * optical data + */ + public static int[] get_optical_channels (int board_id) throws BrainFlowError + { + return get_optical_channels (board_id, BrainFlowPresets.DEFAULT_PRESET); + } + + /** + * get row indices in returned by get_board_data() 2d array which contain + * optical data + */ + public static int[] get_optical_channels (BoardIds board_id, BrainFlowPresets preset) throws BrainFlowError + { + return get_optical_channels (board_id.get_code (), preset); + } + + /** + * get row indices in returned by get_board_data() 2d array which contain + * optical data + */ + public static int[] get_optical_channels (BoardIds board_id) throws BrainFlowError + { + return get_optical_channels (board_id.get_code ()); + } + /** * get row indices in returned by get_board_data() 2d array which contain accel * data diff --git a/julia_package/brainflow/src/board_shim.jl b/julia_package/brainflow/src/board_shim.jl index 011f6f524..c0e3d43f8 100644 --- a/julia_package/brainflow/src/board_shim.jl +++ b/julia_package/brainflow/src/board_shim.jl @@ -202,6 +202,7 @@ channel_function_names = ( :get_eog_channels, :get_eda_channels, :get_ppg_channels, + :get_optical_channels, :get_accel_channels, :get_rotation_channels, :get_analog_channels, diff --git a/matlab_package/brainflow/BoardShim.m b/matlab_package/brainflow/BoardShim.m index d245558a4..6ef22779b 100644 --- a/matlab_package/brainflow/BoardShim.m +++ b/matlab_package/brainflow/BoardShim.m @@ -269,6 +269,17 @@ function log_message(log_level, message) ppg_channels = data.Value(1,1:num_channels.Value) + 1; end + function optical_channels = get_optical_channels(board_id, preset) + % get optical channels for provided board id + task_name = 'get_optical_channels'; + num_channels = libpointer('int32Ptr', 0); + data = libpointer('int32Ptr', zeros(1, 512)); + lib_name = BoardShim.load_lib(); + exit_code = calllib(lib_name, task_name, board_id, preset, data, num_channels); + BoardShim.check_ec(exit_code, task_name); + optical_channels = data.Value(1,1:num_channels.Value) + 1; + end + function eda_channels = get_eda_channels(board_id, preset) % get eda channels for provided board id task_name = 'get_eda_channels'; diff --git a/nodejs_package/brainflow/board_shim.ts b/nodejs_package/brainflow/board_shim.ts index bd5666e92..55e0791fb 100644 --- a/nodejs_package/brainflow/board_shim.ts +++ b/nodejs_package/brainflow/board_shim.ts @@ -89,6 +89,7 @@ class BoardControllerDLL extends BoardControllerFunctions this.getEogChannels = this.lib.func(CLike.get_eog_channels); this.getEcgChannels = this.lib.func(CLike.get_ecg_channels); this.getPpgChannels = this.lib.func(CLike.get_ppg_channels); + this.getOpticalChannels = this.lib.func(CLike.get_optical_channels); this.getEdaChannels = this.lib.func(CLike.get_eda_channels); this.getAccelChannels = this.lib.func(CLike.get_accel_channels); this.getRotationChannels = this.lib.func(CLike.get_rotation_channels); @@ -546,6 +547,21 @@ export class BoardShim return сhannels.slice(0, numChannels[0]); } + public static getOpticalChannels( + boardId: BoardIds, preset = BrainFlowPresets.DEFAULT_PRESET): number[] + { + const numChannels = [0]; + const сhannels = [...new Array (512).fill(0)]; + const res = + BoardControllerDLL.getInstance().getOpticalChannels( + boardId, preset, сhannels, numChannels); + if (res !== BrainFlowExitCodes.STATUS_OK) + { + throw new BrainFlowError (res, 'Could not get board info'); + } + return сhannels.slice(0, numChannels[0]); + } + public static getEdaChannels( boardId: BoardIds, preset = BrainFlowPresets.DEFAULT_PRESET): number[] { diff --git a/nodejs_package/brainflow/functions.types.ts b/nodejs_package/brainflow/functions.types.ts index bb6a40203..177b54aa9 100644 --- a/nodejs_package/brainflow/functions.types.ts +++ b/nodejs_package/brainflow/functions.types.ts @@ -71,6 +71,8 @@ export enum BoardControllerCLikeFunctions { 'int get_eog_channels (int board_id, int preset, _Inout_ int *channels, _Inout_ int *len)', get_ppg_channels = 'int get_ppg_channels (int board_id, int preset, _Inout_ int *channels, _Inout_ int *len)', + get_optical_channels = + 'int get_optical_channels (int board_id, int preset, _Inout_ int *channels, _Inout_ int *len)', get_eda_channels = 'int get_eda_channels (int board_id, int preset, _Inout_ int *channels, _Inout_ int *len)', get_accel_channels = @@ -226,6 +228,12 @@ export class BoardControllerFunctions ppgChannels: number[], len: number[], ) => BrainFlowExitCodes; + getOpticalChannels!: ( + boardId: BoardIds, + preset: BrainFlowPresets, + opticalChannels: number[], + len: number[], + ) => BrainFlowExitCodes; getEdaChannels!: ( boardId: BoardIds, preset: BrainFlowPresets, diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index 80256c346..89335f27a 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -498,6 +498,15 @@ def __init__(self): ndpointer(ctypes.c_int32) ] + self.get_optical_channels = self.lib.get_optical_channels + self.get_optical_channels.restype = ctypes.c_int + self.get_optical_channels.argtypes = [ + ctypes.c_int, + ctypes.c_int, + ndpointer(ctypes.c_int32), + ndpointer(ctypes.c_int32) + ] + self.get_eda_channels = self.lib.get_eda_channels self.get_eda_channels.restype = ctypes.c_int self.get_eda_channels.argtypes = [ @@ -1034,6 +1043,29 @@ def get_ppg_channels(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_ result = ppg_channels.tolist()[0:num_channels[0]] return result + @classmethod + def get_optical_channels(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET) -> List[int]: + """get list of optical channels in resulting data table for a board + + :param board_id: Board Id + :type board_id: int + :param preset: preset + :type preset: int + :return: list of optical channels in returned numpy array + :rtype: List[int] + :raises BrainFlowError: If this board has no such data exit code is UNSUPPORTED_BOARD_ERROR + """ + + num_channels = numpy.zeros(1).astype(numpy.int32) + optical_channels = numpy.zeros(512).astype(numpy.int32) + + res = BoardControllerDLL.get_instance().get_optical_channels( + board_id, preset, optical_channels, num_channels) + if res != BrainFlowExitCodes.STATUS_OK.value: + raise BrainFlowError('unable to request info about this board', res) + result = optical_channels.tolist()[0:num_channels[0]] + return result + @classmethod def get_accel_channels(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET) -> List[int]: """get list of accel channels in resulting data table for a board diff --git a/rust_package/brainflow/src/board_shim.rs b/rust_package/brainflow/src/board_shim.rs index 8e169c95c..3c12282bc 100644 --- a/rust_package/brainflow/src/board_shim.rs +++ b/rust_package/brainflow/src/board_shim.rs @@ -536,6 +536,10 @@ gen_vec_fn!( ppg_channels, "Get list of ppg channels in resulting data table for a board." ); +gen_vec_fn!( + optical_channels, + "Get list of optical channels in resulting data table for a board." +); gen_vec_fn!( accel_channels, "Get list of accel channels in resulting data table for a board." diff --git a/rust_package/brainflow/src/ffi/board_controller.rs b/rust_package/brainflow/src/ffi/board_controller.rs index dcadfb54d..16fdd052f 100644 --- a/rust_package/brainflow/src/ffi/board_controller.rs +++ b/rust_package/brainflow/src/ffi/board_controller.rs @@ -111,6 +111,14 @@ extern "C" { len: *mut ::std::os::raw::c_int, ) -> ::std::os::raw::c_int; } +extern "C" { + pub fn get_optical_channels( + board_id: ::std::os::raw::c_int, + preset: ::std::os::raw::c_int, + optical_channels: *mut ::std::os::raw::c_int, + len: *mut ::std::os::raw::c_int, + ) -> ::std::os::raw::c_int; +} extern "C" { pub fn get_eda_channels( board_id: ::std::os::raw::c_int, diff --git a/src/board_controller/board_info_getter.cpp b/src/board_controller/board_info_getter.cpp index 4815a8ce0..42804db0a 100644 --- a/src/board_controller/board_info_getter.cpp +++ b/src/board_controller/board_info_getter.cpp @@ -161,6 +161,11 @@ int get_ppg_channels (int board_id, int preset, int *ppg_channels, int *len) return get_array_value (board_id, preset, "ppg_channels", ppg_channels, len); } +int get_optical_channels (int board_id, int preset, int *optical_channels, int *len) +{ + return get_array_value (board_id, preset, "optical_channels", optical_channels, len); +} + int get_accel_channels (int board_id, int preset, int *accel_channels, int *len) { return get_array_value (board_id, preset, "accel_channels", accel_channels, len); diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index e6e7d65f8..42fa18bc5 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -1168,8 +1168,9 @@ BrainFlowBoards::BrainFlowBoards() {"marker_channel", 10}, {"package_num_channel", 0}, {"num_rows", 11}, - {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, - {"eeg_names", "TP9,AF7,AF8,TP10,AUX1,AUX2,AUX3,AUX4"} + {"eeg_channels", {1, 2, 3, 4}}, + {"eeg_names", "TP9,AF7,AF8,TP10"}, + {"other_channels", {5, 6, 7, 8}} }; brainflow_boards_json["boards"]["67"]["auxiliary"] = { @@ -1186,13 +1187,12 @@ BrainFlowBoards::BrainFlowBoards() { {"name", "MuseAnthenaAnc"}, {"sampling_rate", 64}, - {"timestamp_channel", 21}, - {"marker_channel", 22}, + {"timestamp_channel", 18}, + {"marker_channel", 19}, {"package_num_channel", 0}, - {"num_rows", 23}, - {"ppg_channels", {1, 2, 3}}, - {"optical_channels", {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}}, - {"battery_channel", 20} + {"num_rows", 20}, + {"optical_channels", {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}, + {"battery_channel", 17} }; } diff --git a/src/board_controller/inc/board_info_getter.h b/src/board_controller/inc/board_info_getter.h index 34ed7854a..56880dda7 100644 --- a/src/board_controller/inc/board_info_getter.h +++ b/src/board_controller/inc/board_info_getter.h @@ -34,6 +34,8 @@ extern "C" int board_id, int preset, int *eog_channels, int *len); SHARED_EXPORT int CALLING_CONVENTION get_ppg_channels ( int board_id, int preset, int *ppg_channels, int *len); + SHARED_EXPORT int CALLING_CONVENTION get_optical_channels ( + int board_id, int preset, int *optical_channels, int *len); SHARED_EXPORT int CALLING_CONVENTION get_eda_channels ( int board_id, int preset, int *eda_channels, int *len); SHARED_EXPORT int CALLING_CONVENTION get_accel_channels ( diff --git a/src/board_controller/muse/inc/muse_anthena.h b/src/board_controller/muse/inc/muse_anthena.h index a1bcdcb47..bf15264b7 100644 --- a/src/board_controller/muse/inc/muse_anthena.h +++ b/src/board_controller/muse/inc/muse_anthena.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -61,8 +60,6 @@ class MuseAnthena : public BLELibBoard uint8_t tag, uint8_t sequence_num, uint32_t device_tick, 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); - double average_present (const std::array &values, - const std::array &present, const int *indexes, int len); double get_sample_timestamp (uint32_t device_tick, int sample_index, double sampling_rate); public: diff --git a/src/board_controller/muse/inc/muse_anthena_constants.h b/src/board_controller/muse/inc/muse_anthena_constants.h index 877a14f94..9c98e4d5b 100644 --- a/src/board_controller/muse/inc/muse_anthena_constants.h +++ b/src/board_controller/muse/inc/muse_anthena_constants.h @@ -13,4 +13,4 @@ #define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.0074768 #define MUSE_ANTHENA_DEVICE_CLOCK_HZ 256000.0 #define MUSE_ANTHENA_EEG_SCALE_FACTOR (1450.0 / 16383.0) -#define MUSE_ANTHENA_OPTICS_SCALE_FACTOR (1.0 / 32768.0) +#define MUSE_ANTHENA_OPTICS_SCALE_FACTOR 1.0 diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp index d7add8582..3152a6bad 100644 --- a/src/board_controller/muse/muse_anthena.cpp +++ b/src/board_controller/muse/muse_anthena.cpp @@ -1,7 +1,6 @@ #include "muse_anthena.h" #include -#include #include #include #include @@ -93,24 +92,6 @@ int MuseAnthena::get_optics_canonical_index (uint8_t tag, int channel) return -1; } -double MuseAnthena::average_present (const std::array &values, - const std::array &present, const int *indexes, int len) -{ - double sum = 0.0; - int count = 0; - for (int i = 0; i < len; i++) - { - int index = indexes[i]; - if ((index >= 0) && (index < 16) && present[(size_t)index]) - { - sum += values[(size_t)index]; - count++; - } - } - return count > 0 ? sum / count : 0.0; -} - - std::string MuseAnthena::trim_string (const std::string &value) { size_t first = value.find_first_not_of (" \t\r\n"); @@ -863,6 +844,7 @@ void MuseAnthena::parse_sensor_payload ( { int num_rows = board_descr["default"]["num_rows"].get (); std::vector eeg_channels = board_descr["default"]["eeg_channels"]; + std::vector other_channels = board_descr["default"]["other_channels"]; int package_num_channel = board_descr["default"]["package_num_channel"].get (); int timestamp_channel = board_descr["default"]["timestamp_channel"].get (); @@ -879,6 +861,15 @@ void MuseAnthena::parse_sensor_payload ( package[(size_t)eeg_channels[(size_t)channel]] = (double)raw * MUSE_ANTHENA_EEG_SCALE_FACTOR; } + else + { + size_t other_channel = (size_t)channel - eeg_channels.size (); + if (other_channel < other_channels.size ()) + { + package[(size_t)other_channels[other_channel]] = + (double)raw * MUSE_ANTHENA_EEG_SCALE_FACTOR; + } + } } package[(size_t)timestamp_channel] = get_sample_timestamp (device_tick, sample, config.sampling_rate); @@ -928,12 +919,7 @@ void MuseAnthena::parse_sensor_payload ( if (config.type == SensorType::OPTICS) { - static const int nir_indexes[] = {0, 1, 4, 5}; - static const int red_indexes[] = {8, 9, 12, 13}; - static const int ir_indexes[] = {2, 3, 6, 7}; - int num_rows = board_descr["ancillary"]["num_rows"].get (); - std::vector ppg_channels = board_descr["ancillary"]["ppg_channels"]; std::vector optical_channels = board_descr["ancillary"]["optical_channels"]; int package_num_channel = board_descr["ancillary"]["package_num_channel"].get (); int timestamp_channel = board_descr["ancillary"]["timestamp_channel"].get (); @@ -941,11 +927,6 @@ void MuseAnthena::parse_sensor_payload ( for (int sample = 0; sample < config.n_samples; sample++) { - std::array optical_values; - std::array optical_present; - optical_values.fill (0.0); - optical_present.fill (false); - std::vector package ((size_t)num_rows, 0.0); package[(size_t)package_num_channel] = (double)sequence_num; package[(size_t)battery_channel] = last_battery; @@ -958,8 +939,6 @@ void MuseAnthena::parse_sensor_payload ( if ((canonical_index >= 0) && (canonical_index < 16)) { double value = (double)raw * MUSE_ANTHENA_OPTICS_SCALE_FACTOR; - optical_values[(size_t)canonical_index] = value; - optical_present[(size_t)canonical_index] = true; if ((size_t)canonical_index < optical_channels.size ()) { package[(size_t)optical_channels[(size_t)canonical_index]] = value; @@ -967,15 +946,6 @@ void MuseAnthena::parse_sensor_payload ( } } - if (ppg_channels.size () >= 3) - { - package[(size_t)ppg_channels[0]] = - average_present (optical_values, optical_present, nir_indexes, 4); - package[(size_t)ppg_channels[1]] = - average_present (optical_values, optical_present, red_indexes, 4); - package[(size_t)ppg_channels[2]] = - average_present (optical_values, optical_present, ir_indexes, 4); - } package[(size_t)timestamp_channel] = get_sample_timestamp (device_tick, sample, config.sampling_rate); push_package (package.data (), (int)BrainFlowPresets::ANCILLARY_PRESET); From 2edd365463d115c26bdf489f244518d0761c7c66 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Sun, 26 Apr 2026 18:17:11 +0700 Subject: [PATCH 7/8] Fix Muse Anthena formatting --- src/board_controller/muse/inc/muse_anthena.h | 2 +- src/board_controller/muse/muse_anthena.cpp | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/board_controller/muse/inc/muse_anthena.h b/src/board_controller/muse/inc/muse_anthena.h index bf15264b7..64c049a5b 100644 --- a/src/board_controller/muse/inc/muse_anthena.h +++ b/src/board_controller/muse/inc/muse_anthena.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include #include #include #include diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp index 3152a6bad..9cce37423 100644 --- a/src/board_controller/muse/muse_anthena.cpp +++ b/src/board_controller/muse/muse_anthena.cpp @@ -1,8 +1,8 @@ #include "muse_anthena.h" #include -#include #include +#include #include #include #include @@ -159,8 +159,8 @@ int MuseAnthena::parse_muse_options () 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); + safe_logger ( + spdlog::level::err, "Invalid MuseAnthena preset in other_info: {}", other_info); return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; } muse_preset = preset; @@ -289,8 +289,8 @@ int MuseAnthena::prepare_session () { return res; } - safe_logger (spdlog::level::info, "Use MuseAnthena preset {} and low_latency {}", - muse_preset, enable_low_latency); + safe_logger (spdlog::level::info, "Use MuseAnthena preset {} and low_latency {}", muse_preset, + enable_low_latency); if (params.timeout < 1) { params.timeout = 6; @@ -407,7 +407,8 @@ int MuseAnthena::prepare_session () } } - if ((strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_DATA_1) == 0) || + if ((strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_DATA_1) == + 0) || (strcmp (service.characteristics[j].uuid.value, MUSE_ANTHENA_GATT_DATA_2) == 0)) { // Athena multiplexes EEG, IMU, optics, and battery packets across data @@ -902,8 +903,8 @@ void MuseAnthena::parse_sensor_payload ( } for (int channel = 0; channel < 3; channel++) { - int16_t raw = cast_16bit_to_int16_little_endian ((const unsigned char *)(data + - (sample * config.n_channels + channel + 3) * 2)); + int16_t raw = cast_16bit_to_int16_little_endian ( + (const unsigned char *)(data + (sample * config.n_channels + channel + 3) * 2)); if ((size_t)channel < gyro_channels.size ()) { package[(size_t)gyro_channels[(size_t)channel]] = From 667bc493e30b5d35cfa5dfb837f06d17be769527 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Sun, 26 Apr 2026 18:21:01 +0700 Subject: [PATCH 8/8] Fix macOS ninja setup in CI --- .github/workflows/deploy_cpp_libs.yml | 8 +++++--- .github/workflows/run_unix.yml | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy_cpp_libs.yml b/.github/workflows/deploy_cpp_libs.yml index e1ab94215..13c0400c7 100644 --- a/.github/workflows/deploy_cpp_libs.yml +++ b/.github/workflows/deploy_cpp_libs.yml @@ -70,9 +70,11 @@ jobs: cmake-version: '3.21.x' - name: Install Ninja if: (matrix.os == 'macos-14') - uses: seanmiddleditch/gha-setup-ninja@master - with: - version: 1.10.2 + run: | + if ! command -v ninja; then + brew install ninja + fi + ninja --version # build simpleble outside from brainflow because of different deployment targets - name: Compile SimpleBLE MacOS if: (matrix.os == 'macos-14') diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index fd1ae005e..29f3f7746 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -45,9 +45,11 @@ jobs: npm install -g ts-node - name: Install Ninja if: (matrix.os == 'macos-14') - uses: seanmiddleditch/gha-setup-ninja@master - with: - version: 1.10.2 + run: | + if ! command -v ninja; then + brew install ninja + fi + ninja --version - name: Install Julia uses: julia-actions/setup-julia@v2 with: