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: 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 a22c974fd..9f4257f3b 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -122,7 +122,8 @@ public enum BoardIds OB3000_24_CHANNELS_BOARD = 63, BIOLISTENER_BOARD = 64, IRONBCI_32_BOARD = 65, - NEUROPAWN_KNIGHT_BOARD_IMU = 66 + NEUROPAWN_KNIGHT_BOARD_IMU = 66, + MUSE_S_ANTHENA_BOARD = 67 }; @@ -183,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); @@ -275,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); @@ -773,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/docs/SupportedBoards.rst b/docs/SupportedBoards.rst index 5cafde9e3..afe5a398d 100644 --- a/docs/SupportedBoards.rst +++ b/docs/SupportedBoards.rst @@ -1435,7 +1435,7 @@ Initialization Example: params = BrainFlowInputParams() params.serial_port = "COM3" params.other_info = '{"gain": 6}' # optional: set gain to allowed values: 1, 2, 3, 4, 6, 8, 12 (default) - + board = BoardShim(BoardIds.NEUROPAWN_KNIGHT_BOARD, params) **On Unix-like systems you may need to configure permissions for serial port or run with sudo.** @@ -1470,7 +1470,7 @@ Initialization Example: params = BrainFlowInputParams() params.serial_port = "COM3" params.other_info = '{"gain": 6}' # optional: set gain to allowed values: 1, 2, 3, 4, 6, 8, 12 (default) - + board = BoardShim(BoardIds.NEUROPAWN_KNIGHT_BOARD_IMU, params) **On Unix-like systems you may need to configure permissions for serial port or run with sudo.** @@ -1560,4 +1560,4 @@ Supported platforms: - Windows - Linux - MacOS -- Devices like Raspberry Pi \ No newline at end of file +- Devices like Raspberry Pi diff --git a/emulator/brainflow_emulator/galea_manual.py b/emulator/brainflow_emulator/galea_manual.py index 810676f0c..1a4b56270 100644 --- a/emulator/brainflow_emulator/galea_manual.py +++ b/emulator/brainflow_emulator/galea_manual.py @@ -35,7 +35,7 @@ def __init__(self): self.channel_on_off = [1] * 24 self.channel_identifiers = [ '1', '2', '3', '4', '5', '6', '7', '8', - 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', + 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'A', 'S', 'D', 'G', 'H', 'J', 'K', 'L' ] @@ -147,4 +147,4 @@ def main(): if __name__ == '__main__': logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=logging.INFO) - main() \ No newline at end of file + main() 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/BoardIds.java b/java_package/brainflow/src/main/java/brainflow/BoardIds.java index 97f3b5684..e5de61fdc 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardIds.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardIds.java @@ -72,7 +72,8 @@ public enum BoardIds OB3000_24_CHANNELS_BOARD(63), BIOLISTENER_BOARD(64), IRONBCI_32_BOARD(65), - NEUROPAWN_KNIGHT_BOARD_IMU(66); + NEUROPAWN_KNIGHT_BOARD_IMU(66), + MUSE_S_ANTHENA_BOARD(67); private final int board_id; private static final Map bi_map = new HashMap (); 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 d657c6d6d..c0e3d43f8 100644 --- a/julia_package/brainflow/src/board_shim.jl +++ b/julia_package/brainflow/src/board_shim.jl @@ -68,6 +68,7 @@ export BrainFlowInputParams BIOLISTENER_BOARD = 64 IRONBCI_32_BOARD = 65 NEUROPAWN_KNIGHT_BOARD_IMU = 66 + MUSE_S_ANTHENA_BOARD = 67 end @@ -201,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/BoardIds.m b/matlab_package/brainflow/BoardIds.m index 0234835f1..79c3988c4 100644 --- a/matlab_package/brainflow/BoardIds.m +++ b/matlab_package/brainflow/BoardIds.m @@ -66,5 +66,6 @@ BIOLISTENER_BOARD(64) IRONBCI_32_BOARD(65) NEUROPAWN_KNIGHT_BOARD_IMU(66) + MUSE_S_ANTHENA_BOARD(67) end -end \ No newline at end of file +end 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/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index cd1ba9bcc..e41c3e275 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -74,7 +74,9 @@ export enum BoardIds { SYNCHRONI_UNO_1_CHANNELS_BOARD = 62, OB3000_24_CHANNELS_BOARD = 63, BIOLISTENER_BOARD = 64, - IRONBCI_32_BOARD = 65 + IRONBCI_32_BOARD = 65, + NEUROPAWN_KNIGHT_BOARD_IMU = 66, + MUSE_S_ANTHENA_BOARD = 67 } export enum IpProtocolTypes { 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 56133cf07..89335f27a 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -21,7 +21,7 @@ class BoardIds(enum.IntEnum): STREAMING_BOARD = -2 #: SYNTHETIC_BOARD = -1 #: CYTON_BOARD = 0 #: - GANGLION_BOARD = 1 #: + GANGLION_BOARD = 1 #: CYTON_DAISY_BOARD = 2 #: GALEA_BOARD = 3 #: GANGLION_WIFI_BOARD = 4 #: @@ -81,6 +81,7 @@ class BoardIds(enum.IntEnum): BIOLISTENER_BOARD = 64 #: IRONBCI_32_BOARD = 65 #: NEUROPAWN_KNIGHT_BOARD_IMU = 66 #: + MUSE_S_ANTHENA_BOARD = 67 #: class IpProtocolTypes(enum.IntEnum): @@ -497,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 = [ @@ -1033,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/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/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/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index 0719e3331..070c6f9bc 100644 --- a/rust_package/brainflow/src/ffi/constants.rs +++ b/rust_package/brainflow/src/ffi/constants.rs @@ -32,7 +32,7 @@ impl BoardIds { pub const FIRST: BoardIds = BoardIds::PlaybackFileBoard; } impl BoardIds { - pub const LAST: BoardIds = BoardIds::NeuropawnKnightBoardImu; + pub const LAST: BoardIds = BoardIds::MuseSAnthenaBoard; } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] @@ -102,6 +102,7 @@ pub enum BoardIds { BiolistenerBoard = 64, Ironbci32Board = 65, NeuropawnKnightBoardImu = 66, + MuseSAnthenaBoard = 67, } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index 045edeab4..be4ccc41f 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -47,6 +47,7 @@ #include "knight.h" #include "knight_imu.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/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 5d86d6a59..42fa18bc5 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -84,7 +84,8 @@ BrainFlowBoards::BrainFlowBoards() {"63", json::object()}, {"64", json::object()}, {"65", json::object()}, - {"66", json::object()} + {"66", json::object()}, + {"67", json::object()} } }}; @@ -1159,6 +1160,40 @@ BrainFlowBoards::BrainFlowBoards() {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, {"other_channels", {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}} }; + brainflow_boards_json["boards"]["67"]["default"] = + { + {"name", "MuseAnthena"}, + {"sampling_rate", 256}, + {"timestamp_channel", 9}, + {"marker_channel", 10}, + {"package_num_channel", 0}, + {"num_rows", 11}, + {"eeg_channels", {1, 2, 3, 4}}, + {"eeg_names", "TP9,AF7,AF8,TP10"}, + {"other_channels", {5, 6, 7, 8}} + }; + 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", 18}, + {"marker_channel", 19}, + {"package_num_channel", 0}, + {"num_rows", 20}, + {"optical_channels", {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}, + {"battery_channel", 17} + }; } BrainFlowBoards boards_struct; diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake index 6a2d3e2b5..9a4206f0c 100644 --- a/src/board_controller/build.cmake +++ b/src/board_controller/build.cmake @@ -77,6 +77,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/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 new file mode 100644 index 000000000..64c049a5b --- /dev/null +++ b/src/board_controller/muse/inc/muse_anthena.h @@ -0,0 +1,81 @@ +#pragma once + +#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; + 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 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 get_sample_timestamp (uint32_t device_tick, int sample_index, double sampling_rate); + +public: + MuseAnthena (int board_id, struct BrainFlowInputParams params); + ~MuseAnthena () override; + + int prepare_session () override; + int start_stream (int buffer_size, const char *streamer_params) override; + int stop_stream () override; + int release_session () override; + int config_board (std::string config, std::string &response) override; + int config_board (std::string config); + + void adapter_on_scan_found (simpleble_adapter_t adapter, simpleble_peripheral_t peripheral); + 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 new file mode 100644 index 000000000..9c98e4d5b --- /dev/null +++ b/src/board_controller/muse/inc/muse_anthena_constants.h @@ -0,0 +1,16 @@ +#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_DATA_1 "273e0013-4c4d-454d-96be-f03bac821358" +#define MUSE_ANTHENA_GATT_DATA_2 "273e0014-4c4d-454d-96be-f03bac821358" + +// info for equations +#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.0000610352 +#define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.0074768 +#define MUSE_ANTHENA_DEVICE_CLOCK_HZ 256000.0 +#define MUSE_ANTHENA_EEG_SCALE_FACTOR (1450.0 / 16383.0) +#define MUSE_ANTHENA_OPTICS_SCALE_FACTOR 1.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.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..9cce37423 --- /dev/null +++ b/src/board_controller/muse/muse_anthena.cpp @@ -0,0 +1,984 @@ +#include "muse_anthena.h" + +#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; +} + +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_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_data (peripheral, service, characteristic, data, size); +} + +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_status (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; + 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 () +{ + 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; + } + 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; + } + 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); + 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; + std::this_thread::sleep_for (std::chrono::seconds (1)); + } + } + } + + // https://github.com/OpenBluetoothToolbox/SimpleBLE/issues/26#issuecomment-955606799 + std::this_thread::sleep_for (std::chrono::seconds (1)); + + 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 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); + + 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"); + if (simpleble_peripheral_notify (muse_peripheral, service.uuid, + service.characteristics[j].uuid, ::anthena_peripheral_on_status, + (void *)this) == SIMPLEBLE_SUCCESS) + { + notified_characteristics.push_back ( + std::pair ( + service.uuid, service.characteristics[j].uuid)); + } + else + { + safe_logger (spdlog::level::warn, "Failed to notify for control {} {}", + service.uuid.value, service.characteristics[j].uuid.value); + } + } + + 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_data, + (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) && (!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; + res = config_board ("v6"); + } + 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)); + 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 (); + } + + 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) + { + 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; + } + + 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"); + 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; +} + +int MuseAnthena::release_session () +{ + if (initialized) + { + // 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 (); + if (muse_peripheral != NULL) + { + // 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) + { + 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) + { + 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) +{ + (void)adapter; + 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", 5) == 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_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) +{ + (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, "Skipping unknown Athena payload tag 0x{:02x}", + (unsigned int)tag); + return; + } + + if (config.type == SensorType::BATTERY) + { + if (size >= 2) + { + 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"]; + 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 (); + + 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 ()) + { + 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); + 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 ()) + { + package[(size_t)accel_channels[(size_t)channel]] = + (double)raw * MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR; + } + } + 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 ()) + { + package[(size_t)gyro_channels[(size_t)channel]] = + (double)raw * MUSE_ANTHENA_GYRO_SCALE_FACTOR; + } + } + package[(size_t)timestamp_channel] = + get_sample_timestamp (device_tick, sample, config.sampling_rate); + push_package (package.data (), (int)BrainFlowPresets::AUXILIARY_PRESET); + } + return; + } + + if (config.type == SensorType::OPTICS) + { + int num_rows = board_descr["ancillary"]["num_rows"].get (); + 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::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; + if ((size_t)canonical_index < optical_channels.size ()) + { + package[(size_t)optical_channels[(size_t)canonical_index]] = value; + } + } + } + + package[(size_t)timestamp_channel] = + get_sample_timestamp (device_tick, sample, config.sampling_rate); + push_package (package.data (), (int)BrainFlowPresets::ANCILLARY_PRESET); + } + } +} + +double MuseAnthena::get_sample_timestamp ( + uint32_t device_tick, int sample_index, double sampling_rate) +{ + if (!timestamp_initialized) + { + timestamp_initialized = true; + first_device_tick = device_tick; + first_host_timestamp = get_timestamp (); + } + + 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) +{ + std::ostringstream oss; + 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/brainflow_constants.h b/src/utils/inc/brainflow_constants.h index f6883168c..93aecb3ca 100644 --- a/src/utils/inc/brainflow_constants.h +++ b/src/utils/inc/brainflow_constants.h @@ -95,9 +95,10 @@ enum class BoardIds : int BIOLISTENER_BOARD = 64, IRONBCI_32_BOARD = 65, NEUROPAWN_KNIGHT_BOARD_IMU = 66, + MUSE_S_ANTHENA_BOARD = 67, // use it to iterate FIRST = PLAYBACK_FILE_BOARD, - LAST = IRONBCI_32_BOARD + LAST = MUSE_S_ANTHENA_BOARD }; enum class IpProtocolTypes : int 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)