diff --git a/docs/SupportedBoards.rst b/docs/SupportedBoards.rst
index afe5a398d..603ac4ac9 100644
--- a/docs/SupportedBoards.rst
+++ b/docs/SupportedBoards.rst
@@ -841,6 +841,176 @@ Supported platforms:
Muse
------
+.. _muse-presets-table:
+
+Muse preset commands:
+
+The table below summarizes startup Muse commands accepted by BrainFlow and the Muse device families where they are intended to be used.
+
+.. list-table::
+ :header-rows: 1
+
+ * - Preset
+ - BrainFlow support
+ - Muse devices
+ - Confidence / use
+ - Description
+ * - :code:`p20`
+ - Accepted
+ - Muse 2016, 2018, 2019, 2021, 2024
+ - Unverified for MuseS Anthena; older Muse preset.
+ - 5 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz.
+ * - :code:`p21`
+ - Accepted
+ - Muse 2016, 2018, 2019, 2021, 2024
+ - Important EEG-only preset; tested/recommended for 4-channel EEG.
+ - 4 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz.
+ * - :code:`p50`
+ - Accepted
+ - Muse 2018, 2019, 2021, 2024
+ - Unverified for MuseS Anthena; older Muse preset.
+ - 5 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz, PPG at 64 Hz.
+ * - :code:`p51`
+ - Accepted
+ - Muse 2018, 2019, 2021, 2024
+ - Unverified for MuseS Anthena; older Muse preset.
+ - 4 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz, PPG at 64 Hz.
+ * - :code:`p60`
+ - Accepted
+ - Muse 2019, 2021
+ - Unverified for MuseS Anthena; older Muse preset.
+ - 5 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz, PPG at 64 Hz, thermistor at 16 Hz.
+ * - :code:`p61`
+ - Accepted
+ - Muse 2019, 2021
+ - Unverified for MuseS Anthena; older Muse preset.
+ - 4 EEG channels, 12-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 0.1 Hz, DRL/REF at 32 Hz, PPG at 64 Hz, thermistor at 16 Hz.
+ * - :code:`p1021`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz.
+ * - :code:`p1022`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 8 EEG channels, 14-bit EEG at 256 Hz, battery at 1 Hz, DRL/REF at 32 Hz.
+ * - :code:`p1023`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - Battery only at 5 Hz.
+ * - :code:`p1024`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - Accelerometer and gyro at 52 Hz.
+ * - :code:`p1025`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 16 optics channels at 64 Hz, low power.
+ * - :code:`p1026`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 16 optics channels at 64 Hz, high power.
+ * - :code:`p1027`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 8 optics channels at 64 Hz, low power.
+ * - :code:`p1028`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 8 optics channels at 64 Hz, high power.
+ * - :code:`p1029`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 4 optics channels at 64 Hz, low power.
+ * - :code:`p102a`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 4 optics channels at 64 Hz, high power.
+ * - :code:`p1031`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 16 optics channels at 64 Hz, low power.
+ * - :code:`p1032`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 16 optics channels at 64 Hz, high power.
+ * - :code:`p1033`
+ - Not accepted
+ - MuseS Anthena
+ - Known MuseS Anthena command, not BrainFlow-enabled yet.
+ - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 8 optics channels at 64 Hz, low power.
+ * - :code:`p1034`
+ - Accepted
+ - MuseS Anthena
+ - Accepted but not device-tested in this work.
+ - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 8 optics channels at 64 Hz, high power.
+ * - :code:`p1035`
+ - Accepted
+ - MuseS Anthena
+ - Important/tested; recommended for 4 EEG plus 4 optics.
+ - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 4 optics channels at 64 Hz, low power.
+ * - :code:`p1036`
+ - Not accepted
+ - MuseS Anthena
+ - Important missing high-power pair for :code:`p1035`.
+ - 4 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 4 optics channels at 64 Hz, high power.
+ * - :code:`p1041`
+ - Accepted
+ - MuseS Anthena
+ - Important/tested; default recommended full-data preset.
+ - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 16 optics channels at 64 Hz, low power. This is the default BrainFlow preset.
+ * - :code:`p1042`
+ - Accepted
+ - MuseS Anthena
+ - Accepted but not device-tested in this work.
+ - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 16 optics channels at 64 Hz, high power.
+ * - :code:`p1043`
+ - Accepted
+ - MuseS Anthena
+ - Accepted but not device-tested in this work.
+ - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 8 optics channels at 64 Hz, low power.
+ * - :code:`p1044`
+ - Accepted
+ - MuseS Anthena
+ - Accepted but not device-tested in this work.
+ - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 8 optics channels at 64 Hz, high power.
+ * - :code:`p1045`
+ - Accepted
+ - MuseS Anthena
+ - Tested; useful for comparing 8 EEG plus 4 optics low power.
+ - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 4 optics channels at 64 Hz, low power.
+ * - :code:`p1046`
+ - Accepted
+ - MuseS Anthena
+ - Tested; high-power pair for :code:`p1045`.
+ - 8 EEG channels, 14-bit EEG at 256 Hz, accelerometer and gyro at 52 Hz, battery at 1 Hz, DRL/REF at 32 Hz, 4 optics channels at 64 Hz, high power.
+ * - :code:`p4129`
+ - Accepted
+ - Unknown
+ - Uncertain; accepted by BrainFlow but not tied to a documented Muse device family.
+ - Not tied to a documented Muse device family.
+
+Low power and high power optics presets:
+
+- Low power and high power variants have the same sampling rate and the same number of channels.
+- Low power uses lower optical emitter intensity. It uses less battery and is less likely to saturate the optical signal.
+- High power uses stronger optical emitter intensity. It can produce larger optical values and may help with weaker optical contact, but uses more battery and has a higher risk of saturation.
+- BrainFlow does not document the exact optical emitter current for these modes.
+
+For legacy Muse boards, :code:`BrainFlowInputParams.other_info` can select the startup Muse command. Use a shorthand such as :code:`p21` or a key-value form such as :code:`preset=p21`. If :code:`other_info` is empty, BrainFlow uses :code:`p21`. Muse 2016 boards accept :code:`p20` and :code:`p21`; Muse 2 boards accept :code:`p20`, :code:`p21`, :code:`p50`, and :code:`p51`; Muse S boards accept :code:`p20`, :code:`p21`, :code:`p50`, :code:`p51`, :code:`p60`, and :code:`p61`. :code:`low_latency` is only supported for MuseS Anthena.
+
+
Muse S BLED
~~~~~~~~~~~~~~
@@ -859,6 +1029,7 @@ To create such board you need to specify the following board ID and fields of Br
- :code:`BoardIds.MUSE_S_BLED_BOARD`
- :code:`serial_port`, e.g. COM3, /dev/ttyACM0
- *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps
+- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p61` or :code:`preset=p61`
Initialization Example:
@@ -875,6 +1046,8 @@ Supported platforms:
- Linux
- Devices like Raspberry Pi
+BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets.
+
Available :ref:`presets-label`:
- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, to enable 5th EEG channel use :code:`board.config_board("p50")`
@@ -900,6 +1073,7 @@ To create such board you need to specify the following board ID and fields of Br
- :code:`BoardIds.MUSE_2_BLED_BOARD`
- :code:`serial_port`, e.g. COM3, /dev/ttyACM0
- *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps
+- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p51` or :code:`preset=p51`
Initialization Example:
@@ -917,6 +1091,8 @@ Supported platforms:
- Linux
- Devices like Raspberry Pi
+BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets.
+
Available :ref:`presets-label`:
- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, to enable 5th EEG channel use :code:`board.config_board("p50")`
@@ -942,6 +1118,7 @@ To create such board you need to specify the following board ID and fields of Br
- :code:`BoardIds.MUSE_2016_BLED_BOARD`
- :code:`serial_port`, e.g. COM3, /dev/ttyACM0
- *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps
+- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p21` or :code:`preset=p21`
Initialization Example:
@@ -958,6 +1135,8 @@ Supported platforms:
- Linux
- Devices like Raspberry Pi
+BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets.
+
Available :ref:`presets-label`:
- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data
@@ -985,6 +1164,7 @@ To create such board you need to specify the following board ID and fields of Br
- :code:`BoardIds.MUSE_S_BOARD`
- *optional:* :code:`mac_address`, mac address of the device to connect
- *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps
+- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p61` or :code:`preset=p61`
Initialization Example:
@@ -1000,6 +1180,8 @@ Supported platforms:
- Linux, compilation from source code probably will be needed
- Devices like Raspberry Pi
+BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets.
+
Available :ref:`presets-label`:
- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, to enable 5th EEG channel use :code:`board.config_board("p50")`
@@ -1007,6 +1189,62 @@ Available :ref:`presets-label`:
- :code:`BrainFlowPresets.ANCILLARY_PRESET`, it contains PPG data, to enable it use :code:`board.config_board("p61")`
+MuseS Anthena
+~~~~~~~~~~~~~~
+
+.. image:: https://live.staticflickr.com/65535/55236436914_6e442f3192.jpg
+ :width: 500px
+ :height: 500px
+
+`Muse Website `_
+
+.. compound::
+
+ On Linux systems you may need to install `libdbus` and we recommend to compile BrainFlow from the source code: ::
+
+ sudo apt-get install libdbus-1-dev # for ubuntu
+ sudo yum install dbus-devel # for centos
+ python3 tools/build.py --ble # to compile
+
+To create such board you need to specify the following board ID and fields of BrainFlowInputParams object:
+
+- :code:`BoardIds.MUSE_S_ANTHENA_BOARD`
+- *optional:* :code:`mac_address`, mac address of the device to connect
+- *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discovered via mobile apps
+- *optional:* :code:`other_info`, MuseS Anthena startup options
+
+Initialization Example:
+
+.. code-block:: python
+
+ params = BrainFlowInputParams()
+ params.other_info = "preset=p1041;low_latency=true"
+ board = BoardShim(BoardIds.MUSE_S_ANTHENA_BOARD, params)
+
+Supported platforms:
+
+- Windows 10.0.19041.0+
+- MacOS 10.15+, 12.0 to 12.2 have known issues while scanning, you need to update to 12.3+. On MacOS 12+ you may need to configure Bluetooth permissions for your application
+- Linux, compilation from source code probably will be needed
+- Devices like Raspberry Pi
+
+Available :code:`other_info` options:
+
+- If :code:`other_info` is empty, BrainFlow uses :code:`preset=p1041;low_latency=true`.
+- :code:`other_info` can be a preset shorthand, for example :code:`p1041`.
+- :code:`other_info` can be a semicolon-separated key-value string, for example :code:`preset=p1041;low_latency=false`.
+- :code:`preset` selects the Muse streaming preset. BrainFlow accepts :code:`p20`, :code:`p21`, :code:`p50`, :code:`p51`, :code:`p60`, :code:`p61`, :code:`p1034`, :code:`p1035`, :code:`p1041`, :code:`p1042`, :code:`p1043`, :code:`p1044`, :code:`p1045`, :code:`p1046`, and :code:`p4129`.
+- :code:`low_latency` can be :code:`true` or :code:`false`. If enabled, BrainFlow sends the :code:`L1` command after starting the stream.
+
+BrainFlow uses Muse command :code:`p1041` by default for this board. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets.
+
+Available :ref:`presets-label`:
+
+- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, sampling rate is 256 Hz. For 4-channel Muse presets BrainFlow exposes :code:`TP9`, :code:`AF7`, :code:`AF8`, and :code:`TP10` as EEG channels. For 8-channel Muse presets the additional Muse EEG values are exposed as other channels.
+- :code:`BrainFlowPresets.AUXILIARY_PRESET`, it contains Accelerometer and Gyro data, sampling rate is 52 Hz.
+- :code:`BrainFlowPresets.ANCILLARY_PRESET`, it contains optics and battery data. Optics sampling rate is 64 Hz. MuseS Anthena uses optics data for PPG, and BrainFlow exposes this data as optical channels instead of PPG channels. Depending on selected Muse preset, the stream contains 4, 8, or 16 optical channels.
+
+
Muse 2
~~~~~~~~~~~~~~
@@ -1029,6 +1267,7 @@ To create such board you need to specify the following board ID and fields of Br
- :code:`BoardIds.MUSE_2_BOARD`
- *optional:* :code:`mac_address`, mac address of the device to connect
- *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps
+- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p51` or :code:`preset=p51`
Initialization Example:
@@ -1044,6 +1283,8 @@ Supported platforms:
- Linux, compilation from source code probably will be needed
- Devices like Raspberry Pi
+BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets.
+
Available :ref:`presets-label`:
- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data, to enable 5th EEG channel use :code:`board.config_board("p50")`
@@ -1073,6 +1314,7 @@ To create such board you need to specify the following board ID and fields of Br
- :code:`BoardIds.MUSE_2016_BOARD`
- *optional:* :code:`mac_address`, mac address of the device to connect
- *optional:* :code:`serial_number`, device name, can be printed on the Muse device or discoovered via mobile apps
+- *optional:* :code:`other_info`, startup Muse preset, for example :code:`p21` or :code:`preset=p21`
Initialization Example:
@@ -1088,6 +1330,8 @@ Supported platforms:
- Linux, compilation from source code probably will be needed
- Devices like Raspberry Pi
+BrainFlow initializes this board with Muse command :code:`p21` by default. See :ref:`muse-presets-table` for Muse command availability, device support, and tested/default presets.
+
Available :ref:`presets-label`:
- :code:`BrainFlowPresets.DEFAULT_PRESET`, it contains EEG data
diff --git a/python_package/examples/tests/muse_anthena_analyze_optics_pulse.py b/python_package/examples/tests/muse_anthena_analyze_optics_pulse.py
new file mode 100644
index 000000000..970c74f30
--- /dev/null
+++ b/python_package/examples/tests/muse_anthena_analyze_optics_pulse.py
@@ -0,0 +1,276 @@
+import argparse
+import math
+from pathlib import Path
+
+import matplotlib
+
+matplotlib.use('Agg')
+import matplotlib.pyplot as plt
+import numpy as np
+
+from brainflow.board_shim import BoardShim, BoardIds, BrainFlowPresets
+from brainflow.data_filter import DataFilter, DetrendOperations, FilterTypes
+
+
+def build_time_axis(data, timestamp_channel, sampling_rate):
+ timestamps = data[timestamp_channel, :]
+ if timestamps.size == data.shape[1] and timestamps.size > 1 and np.all(np.isfinite(timestamps)):
+ diffs = np.diff(timestamps)
+ if np.count_nonzero(diffs > 0) > timestamps.size * 0.8:
+ return timestamps - timestamps[0], 1.0 / np.median(diffs[diffs > 0])
+ return np.arange(data.shape[1], dtype=np.float64) / sampling_rate, float(sampling_rate)
+
+
+def get_active_channels(optics_data, optical_channels):
+ active = []
+ active_labels = []
+ for index, channel in enumerate(optical_channels):
+ row = optics_data[index, :]
+ finite = row[np.isfinite(row)]
+ if finite.size > 0 and np.std(finite) > 1e-9:
+ active.append(index)
+ active_labels.append(f'Optics {channel}')
+ return active, active_labels
+
+
+def zscore(data):
+ stddev = np.std(data)
+ if stddev < 1e-12:
+ return np.zeros(data.shape)
+ return (data - np.mean(data)) / stddev
+
+
+def filter_active_channels(optics_data, active_indexes, sampling_rate, low_cut, high_cut):
+ filtered = []
+ for index in active_indexes:
+ row = np.asarray(optics_data[index, :], dtype=np.float64).copy()
+ finite = np.isfinite(row)
+ if not np.all(finite):
+ row[~finite] = np.interp(np.flatnonzero(~finite), np.flatnonzero(finite), row[finite])
+ DataFilter.detrend(row, DetrendOperations.LINEAR.value)
+ DataFilter.perform_bandpass(
+ row,
+ int(round(sampling_rate)),
+ low_cut,
+ high_cut,
+ 4,
+ FilterTypes.BUTTERWORTH_ZERO_PHASE.value,
+ 0.0,
+ )
+ filtered.append(row)
+ return np.asarray(filtered)
+
+
+def find_local_peaks(signal, sampling_rate, min_bpm, max_bpm):
+ if signal.size < 3:
+ return np.asarray([], dtype=np.int64)
+
+ min_distance = max(1, int(round(sampling_rate * 60.0 / max_bpm)))
+ threshold = np.percentile(signal, 65.0)
+ candidates = np.flatnonzero((signal[1:-1] > signal[:-2]) & (signal[1:-1] >= signal[2:])) + 1
+ candidates = candidates[signal[candidates] > threshold]
+
+ peaks = []
+ for candidate in candidates:
+ if not peaks or candidate - peaks[-1] >= min_distance:
+ peaks.append(int(candidate))
+ elif signal[candidate] > signal[peaks[-1]]:
+ peaks[-1] = int(candidate)
+ peaks = np.asarray(peaks, dtype=np.int64)
+
+ if peaks.size < 3:
+ return peaks
+
+ intervals = np.diff(peaks) / sampling_rate
+ valid = (intervals >= 60.0 / max_bpm) & (intervals <= 60.0 / min_bpm)
+ keep = np.concatenate(([True], valid))
+ return peaks[keep]
+
+
+def score_peaks(peaks, signal, sampling_rate, min_bpm, max_bpm):
+ if peaks.size < 3:
+ return -math.inf
+ intervals = np.diff(peaks) / sampling_rate
+ valid = (intervals >= 60.0 / max_bpm) & (intervals <= 60.0 / min_bpm)
+ if np.count_nonzero(valid) < 2:
+ return -math.inf
+ valid_intervals = intervals[valid]
+ rr_cv = np.std(valid_intervals) / np.mean(valid_intervals)
+ return float(np.mean(signal[peaks]) - rr_cv)
+
+
+def select_pulse_signal(combined, sampling_rate, min_bpm, max_bpm):
+ best_signal = combined
+ best_peaks = find_local_peaks(combined, sampling_rate, min_bpm, max_bpm)
+ best_score = score_peaks(best_peaks, combined, sampling_rate, min_bpm, max_bpm)
+
+ inverted = -combined
+ inverted_peaks = find_local_peaks(inverted, sampling_rate, min_bpm, max_bpm)
+ inverted_score = score_peaks(inverted_peaks, inverted, sampling_rate, min_bpm, max_bpm)
+
+ if inverted_score > best_score:
+ best_signal = inverted
+ best_peaks = inverted_peaks
+
+ return best_signal, best_peaks
+
+
+def bpm_from_peaks(peaks, time_axis):
+ if peaks.size < 3:
+ return None
+ intervals = np.diff(time_axis[peaks])
+ intervals = intervals[intervals > 0]
+ if intervals.size == 0:
+ return None
+ return 60.0 / np.median(intervals)
+
+
+def spectrum(signal, sampling_rate, low_cut, high_cut):
+ if signal.size < 4:
+ return np.asarray([]), np.asarray([]), None
+ centered = signal - np.mean(signal)
+ windowed = centered * np.hanning(centered.size)
+ freqs = np.fft.rfftfreq(windowed.size, d=1.0 / sampling_rate)
+ power = np.abs(np.fft.rfft(windowed)) ** 2
+ band = (freqs >= low_cut) & (freqs <= high_cut)
+ if not np.any(band):
+ return freqs, power, None
+ peak_freq = freqs[band][np.argmax(power[band])]
+ return freqs, power, peak_freq * 60.0
+
+
+def save_raw_plot(time_axis, optics_data, active_indexes, labels, output_file):
+ fig, axes = plt.subplots(len(active_indexes), 1, figsize=(12, 2.1 * len(active_indexes)), sharex=True)
+ axes = np.atleast_1d(axes)
+ for axis, index, label in zip(axes, active_indexes, labels):
+ axis.plot(time_axis, optics_data[index, :], linewidth=1.0)
+ axis.set_ylabel(label)
+ axis.grid(True)
+ axes[-1].set_xlabel('Time, sec')
+ fig.tight_layout()
+ fig.savefig(output_file, dpi=150)
+ plt.close(fig)
+
+
+def save_filtered_plot(time_axis, filtered, labels, output_file):
+ fig, axes = plt.subplots(filtered.shape[0], 1, figsize=(12, 2.1 * filtered.shape[0]), sharex=True)
+ axes = np.atleast_1d(axes)
+ for axis, row, label in zip(axes, filtered, labels):
+ axis.plot(time_axis, zscore(row), linewidth=1.0)
+ axis.set_ylabel(label)
+ axis.grid(True)
+ axes[-1].set_xlabel('Time, sec')
+ fig.tight_layout()
+ fig.savefig(output_file, dpi=150)
+ plt.close(fig)
+
+
+def save_pulse_plot(time_axis, pulse_signal, peaks, peak_bpm, spectrum_bpm, output_file):
+ title_parts = []
+ if peak_bpm is not None:
+ title_parts.append(f'peaks {peak_bpm:.1f} BPM')
+ if spectrum_bpm is not None:
+ title_parts.append(f'spectrum {spectrum_bpm:.1f} BPM')
+
+ fig, axis = plt.subplots(1, 1, figsize=(12, 4))
+ axis.plot(time_axis, pulse_signal, linewidth=1.0)
+ if peaks.size > 0:
+ axis.plot(time_axis[peaks], pulse_signal[peaks], 'ro', markersize=3)
+ axis.set_xlabel('Time, sec')
+ axis.set_ylabel('Combined filtered optics, z-score')
+ axis.set_title(', '.join(title_parts) if title_parts else 'Combined filtered optics')
+ axis.grid(True)
+ fig.tight_layout()
+ fig.savefig(output_file, dpi=150)
+ plt.close(fig)
+
+
+def save_spectrum_plot(freqs, power, spectrum_bpm, output_file):
+ fig, axis = plt.subplots(1, 1, figsize=(10, 4))
+ if freqs.size > 0:
+ axis.plot(freqs * 60.0, power, linewidth=1.0)
+ if spectrum_bpm is not None:
+ axis.axvline(spectrum_bpm, color='r', linestyle='--', linewidth=1.0)
+ axis.set_xlabel('Frequency, BPM')
+ axis.set_ylabel('Power')
+ axis.set_xlim(30, 210)
+ axis.grid(True)
+ fig.tight_layout()
+ fig.savefig(output_file, dpi=150)
+ plt.close(fig)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--input-file', type=str, required=False, default='muse_anthena_optics_recording.csv')
+ parser.add_argument('--output-prefix', type=str, required=False, default='')
+ parser.add_argument('--discard-seconds', type=float, required=False, default=5.0)
+ parser.add_argument('--low-cut', type=float, required=False, default=0.7)
+ parser.add_argument('--high-cut', type=float, required=False, default=3.5)
+ parser.add_argument('--min-bpm', type=float, required=False, default=40.0)
+ parser.add_argument('--max-bpm', type=float, required=False, default=180.0)
+ args = parser.parse_args()
+
+ data = DataFilter.read_file(args.input_file)
+ board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value
+ preset = BrainFlowPresets.ANCILLARY_PRESET
+ optical_channels = BoardShim.get_optical_channels(board_id, preset)
+ timestamp_channel = BoardShim.get_timestamp_channel(board_id, preset)
+ expected_sampling_rate = BoardShim.get_sampling_rate(board_id, preset)
+
+ time_axis, actual_sampling_rate = build_time_axis(data, timestamp_channel, expected_sampling_rate)
+ start_index = int(np.searchsorted(time_axis, args.discard_seconds))
+ if start_index >= data.shape[1] - 4:
+ start_index = 0
+ data = data[:, start_index:]
+ time_axis = time_axis[start_index:] - time_axis[start_index]
+
+ optics_data = data[optical_channels, :]
+ active_indexes, active_labels = get_active_channels(optics_data, optical_channels)
+ if not active_indexes:
+ raise RuntimeError('no active optical channels found')
+
+ filtered = filter_active_channels(
+ optics_data,
+ active_indexes,
+ actual_sampling_rate,
+ args.low_cut,
+ args.high_cut,
+ )
+ combined = np.mean(np.asarray([zscore(row) for row in filtered]), axis=0)
+ pulse_signal, peaks = select_pulse_signal(combined, actual_sampling_rate, args.min_bpm, args.max_bpm)
+ peak_bpm = bpm_from_peaks(peaks, time_axis)
+ freqs, power, spectrum_bpm = spectrum(pulse_signal, actual_sampling_rate, args.low_cut, args.high_cut)
+
+ prefix = args.output_prefix or str(Path(args.input_file).with_suffix(''))
+ raw_plot = f'{prefix}_raw.png'
+ filtered_plot = f'{prefix}_filtered.png'
+ pulse_plot = f'{prefix}_pulse.png'
+ spectrum_plot = f'{prefix}_spectrum.png'
+
+ save_raw_plot(time_axis, optics_data, active_indexes, active_labels, raw_plot)
+ save_filtered_plot(time_axis, filtered, active_labels, filtered_plot)
+ save_pulse_plot(time_axis, pulse_signal, peaks, peak_bpm, spectrum_bpm, pulse_plot)
+ save_spectrum_plot(freqs, power, spectrum_bpm, spectrum_plot)
+
+ print(f'input file: {args.input_file}')
+ print(f'samples analyzed: {data.shape[1]}')
+ print(f'duration analyzed: {time_axis[-1] - time_axis[0]:.3f} sec')
+ print(f'sampling rate: {actual_sampling_rate:.2f} Hz, expected: {expected_sampling_rate} Hz')
+ print(f'active optical channels: {active_labels}')
+ if peak_bpm is not None:
+ print(f'peak estimate: {peak_bpm:.1f} BPM from {peaks.size} peaks')
+ else:
+ print('peak estimate: unavailable')
+ if spectrum_bpm is not None:
+ print(f'spectrum estimate: {spectrum_bpm:.1f} BPM')
+ else:
+ print('spectrum estimate: unavailable')
+ print(f'wrote plot: {raw_plot}')
+ print(f'wrote plot: {filtered_plot}')
+ print(f'wrote plot: {pulse_plot}')
+ print(f'wrote plot: {spectrum_plot}')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python_package/examples/tests/muse_anthena_eeg_p21.py b/python_package/examples/tests/muse_anthena_eeg_p21.py
new file mode 100644
index 000000000..471e96960
--- /dev/null
+++ b/python_package/examples/tests/muse_anthena_eeg_p21.py
@@ -0,0 +1,41 @@
+import argparse
+import time
+
+from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets
+
+
+def main():
+ BoardShim.enable_dev_board_logger()
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--duration', type=int, required=False, default=10)
+ parser.add_argument('--mac-address', type=str, required=False, default='')
+ parser.add_argument('--serial-number', type=str, required=False, default='')
+ parser.add_argument('--timeout', type=int, required=False, default=0)
+ args = parser.parse_args()
+
+ params = BrainFlowInputParams()
+ params.mac_address = args.mac_address
+ params.serial_number = args.serial_number
+ params.timeout = args.timeout
+ params.other_info = 'preset=p21;low_latency=true'
+
+ board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value
+ board = BoardShim(board_id, params)
+
+ try:
+ board.prepare_session()
+ board.start_stream()
+ try:
+ time.sleep(args.duration)
+ eeg_data = board.get_board_data(preset=BrainFlowPresets.DEFAULT_PRESET)
+ print(eeg_data)
+ finally:
+ board.stop_stream()
+ finally:
+ if board.is_prepared():
+ board.release_session()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python_package/examples/tests/muse_anthena_record_optics.py b/python_package/examples/tests/muse_anthena_record_optics.py
new file mode 100644
index 000000000..a215bbbba
--- /dev/null
+++ b/python_package/examples/tests/muse_anthena_record_optics.py
@@ -0,0 +1,77 @@
+import argparse
+import time
+
+import numpy as np
+
+from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets
+from brainflow.data_filter import DataFilter
+
+
+def print_summary(data, optical_channels, timestamp_channel, sampling_rate):
+ print(f'samples: {data.shape[1]}')
+
+ timestamps = data[timestamp_channel, :]
+ if timestamps.size > 1 and np.all(np.isfinite(timestamps)):
+ diffs = np.diff(timestamps)
+ diffs = diffs[diffs > 0]
+ if diffs.size > 0:
+ print(f'timestamp duration: {timestamps[-1] - timestamps[0]:.3f} sec')
+ print(f'timestamp sampling rate: {1.0 / np.median(diffs):.2f} Hz')
+ print(f'expected sampling rate: {sampling_rate} Hz')
+
+ active_channels = []
+ for channel in optical_channels:
+ row = data[channel, :]
+ finite = row[np.isfinite(row)]
+ if finite.size == 0:
+ continue
+ if np.std(finite) > 1e-9:
+ active_channels.append(channel)
+
+ print(f'active optical channels: {active_channels}')
+
+
+def main():
+ BoardShim.enable_board_logger()
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--duration', type=int, required=False, default=60)
+ parser.add_argument('--mac-address', type=str, required=False, default='')
+ parser.add_argument('--serial-number', type=str, required=False, default='')
+ parser.add_argument('--timeout', type=int, required=False, default=0)
+ parser.add_argument('--other-info', type=str, required=False, default='preset=p1035;low_latency=true')
+ parser.add_argument('--output-file', type=str, required=False, default='muse_anthena_optics_recording.csv')
+ args = parser.parse_args()
+
+ params = BrainFlowInputParams()
+ params.mac_address = args.mac_address
+ params.serial_number = args.serial_number
+ params.timeout = args.timeout
+ params.other_info = args.other_info
+
+ board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value
+ preset = BrainFlowPresets.ANCILLARY_PRESET
+ optical_channels = BoardShim.get_optical_channels(board_id, preset)
+ timestamp_channel = BoardShim.get_timestamp_channel(board_id, preset)
+ sampling_rate = BoardShim.get_sampling_rate(board_id, preset)
+
+ board = BoardShim(board_id, params)
+ try:
+ board.prepare_session()
+ board.start_stream()
+ try:
+ time.sleep(args.duration)
+ data = board.get_board_data(preset=preset)
+ finally:
+ board.stop_stream()
+ finally:
+ if board.is_prepared():
+ board.release_session()
+
+ DataFilter.write_file(data, args.output_file, 'w')
+ print(f'wrote raw ancillary data: {args.output_file}')
+ print_summary(data, optical_channels, timestamp_channel, sampling_rate)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python_package/examples/tests/muse_anthena_save_all.py b/python_package/examples/tests/muse_anthena_save_all.py
new file mode 100644
index 000000000..d3a86b94f
--- /dev/null
+++ b/python_package/examples/tests/muse_anthena_save_all.py
@@ -0,0 +1,48 @@
+import argparse
+import time
+
+from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets
+from brainflow.data_filter import DataFilter
+
+
+def main():
+ BoardShim.enable_dev_board_logger()
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--duration', type=int, required=False, default=10)
+ parser.add_argument('--mac-address', type=str, required=False, default='')
+ parser.add_argument('--serial-number', type=str, required=False, default='')
+ parser.add_argument('--timeout', type=int, required=False, default=0)
+ parser.add_argument('--output-prefix', type=str, required=False, default='muse_anthena')
+ args = parser.parse_args()
+
+ params = BrainFlowInputParams()
+ params.mac_address = args.mac_address
+ params.serial_number = args.serial_number
+ params.timeout = args.timeout
+ params.other_info = 'preset=p1041;low_latency=true'
+
+ board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value
+ board = BoardShim(board_id, params)
+
+ try:
+ board.prepare_session()
+ board.start_stream()
+ try:
+ time.sleep(args.duration)
+ eeg_data = board.get_board_data(preset=BrainFlowPresets.DEFAULT_PRESET)
+ accel_data = board.get_board_data(preset=BrainFlowPresets.AUXILIARY_PRESET)
+ optics_data = board.get_board_data(preset=BrainFlowPresets.ANCILLARY_PRESET)
+ finally:
+ board.stop_stream()
+ finally:
+ if board.is_prepared():
+ board.release_session()
+
+ DataFilter.write_file(eeg_data, f'{args.output_prefix}_eeg.csv', 'w')
+ DataFilter.write_file(accel_data, f'{args.output_prefix}_accel_gyro.csv', 'w')
+ DataFilter.write_file(optics_data, f'{args.output_prefix}_optics_battery.csv', 'w')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp
index 42fa18bc5..388a9a175 100644
--- a/src/board_controller/brainflow_boards.cpp
+++ b/src/board_controller/brainflow_boards.cpp
@@ -420,7 +420,8 @@ BrainFlowBoards::BrainFlowBoards()
{"marker_channel", 5},
{"package_num_channel", 0},
{"num_rows", 6},
- {"ppg_channels", {1, 2, 3}}
+ {"ppg_channels", {1, 2, 3}},
+ {"optical_channels", {1, 2, 3}}
};
brainflow_boards_json["boards"]["22"]["default"] =
{
@@ -453,7 +454,8 @@ BrainFlowBoards::BrainFlowBoards()
{"marker_channel", 5},
{"package_num_channel", 0},
{"num_rows", 6},
- {"ppg_channels", {1, 2, 3}}
+ {"ppg_channels", {1, 2, 3}},
+ {"optical_channels", {1, 2, 3}}
};
brainflow_boards_json["boards"]["23"]["default"] =
{
@@ -681,7 +683,8 @@ BrainFlowBoards::BrainFlowBoards()
{"marker_channel", 5},
{"package_num_channel", 0},
{"num_rows", 6},
- {"ppg_channels", {1, 2, 3}}
+ {"ppg_channels", {1, 2, 3}},
+ {"optical_channels", {1, 2, 3}}
};
brainflow_boards_json["boards"]["39"]["default"] =
{
@@ -714,7 +717,8 @@ BrainFlowBoards::BrainFlowBoards()
{"marker_channel", 5},
{"package_num_channel", 0},
{"num_rows", 6},
- {"ppg_channels", {1, 2, 3}}
+ {"ppg_channels", {1, 2, 3}},
+ {"optical_channels", {1, 2, 3}}
};
brainflow_boards_json["boards"]["40"]["default"] =
{
diff --git a/src/board_controller/muse/inc/muse.h b/src/board_controller/muse/inc/muse.h
index 31a281715..3be703401 100644
--- a/src/board_controller/muse/inc/muse.h
+++ b/src/board_controller/muse/inc/muse.h
@@ -3,6 +3,7 @@
#include "ble_lib_board.h"
#include
#include
+#include
#include
#include
@@ -29,6 +30,7 @@ class Muse : public BLELibBoard
double last_ppg_timestamp; // used for timestamp correction
double last_eeg_timestamp; // used for timestamp correction
double last_aux_timestamp; // used for timestamp correction
+ std::string muse_preset;
public:
Muse (int board_id, struct BrainFlowInputParams params);
diff --git a/src/board_controller/muse/inc/muse_anthena.h b/src/board_controller/muse/inc/muse_anthena.h
index 984d507c7..b9c2ce797 100644
--- a/src/board_controller/muse/inc/muse_anthena.h
+++ b/src/board_controller/muse/inc/muse_anthena.h
@@ -57,7 +57,7 @@ class MuseAnthena : public BLELibBoard
std::string bytes_to_string (const uint8_t *data, size_t size);
void handle_data_notification (const uint8_t *data, size_t size);
void parse_sensor_payload (
- uint8_t tag, uint8_t sequence_num, double host_timestamp, const uint8_t *data, size_t size);
+ uint8_t tag, uint32_t package_num, double host_timestamp, const uint8_t *data, size_t size);
bool get_sensor_config (uint8_t tag, SensorConfig &config);
int get_optics_canonical_index (uint8_t tag, int channel);
void reset_timestamps ();
diff --git a/src/board_controller/muse/inc/muse_anthena_constants.h b/src/board_controller/muse/inc/muse_anthena_constants.h
index 9c5c0692e..c004b82b1 100644
--- a/src/board_controller/muse/inc/muse_anthena_constants.h
+++ b/src/board_controller/muse/inc/muse_anthena_constants.h
@@ -9,7 +9,8 @@
#define MUSE_ANTHENA_GATT_DATA_2 "273e0014-4c4d-454d-96be-f03bac821358"
// info for equations
-#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.0000610352
-#define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.0074768
-#define MUSE_ANTHENA_EEG_SCALE_FACTOR (1450.0 / 16383.0)
+#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.00006103515625
+#define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.007476806640625
+#define MUSE_ANTHENA_EEG_SCALE_FACTOR 0.40293040293040294
#define MUSE_ANTHENA_OPTICS_SCALE_FACTOR 1.0
+#define MUSE_ANTHENA_BATTERY_PERCENT_SCALE_FACTOR (1.0 / 512.0)
diff --git a/src/board_controller/muse/inc/muse_options.h b/src/board_controller/muse/inc/muse_options.h
new file mode 100644
index 000000000..880b250b2
--- /dev/null
+++ b/src/board_controller/muse/inc/muse_options.h
@@ -0,0 +1,188 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "brainflow_constants.h"
+
+
+namespace MuseOptions
+{
+ enum class PresetFamily
+ {
+ Legacy,
+ Anthena
+ };
+
+ inline std::string trim_string (const std::string &value)
+ {
+ size_t first = value.find_first_not_of (" \t\r\n");
+ if (first == std::string::npos)
+ {
+ return "";
+ }
+ size_t last = value.find_last_not_of (" \t\r\n");
+ return value.substr (first, last - first + 1);
+ }
+
+ inline std::string to_lower (const std::string &value)
+ {
+ std::string lower_value = value;
+ std::transform (lower_value.begin (), lower_value.end (), lower_value.begin (),
+ [] (unsigned char ch) { return (char)std::tolower (ch); });
+ return lower_value;
+ }
+
+ inline bool parse_bool_option (const std::string &value, bool &parsed)
+ {
+ std::string lower_value = to_lower (trim_string (value));
+ if (lower_value == "true")
+ {
+ parsed = true;
+ return true;
+ }
+ if (lower_value == "false")
+ {
+ parsed = false;
+ return true;
+ }
+ return false;
+ }
+
+ inline bool is_valid_anthena_preset (const std::string &preset)
+ {
+ static const char *valid_presets[] = {"p20", "p21", "p50", "p51", "p60", "p61", "p1034",
+ "p1035", "p1041", "p1042", "p1043", "p1044", "p1045", "p1046", "p4129"};
+
+ for (size_t i = 0; i < sizeof (valid_presets) / sizeof (valid_presets[0]); i++)
+ {
+ if (preset == valid_presets[i])
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ inline bool is_valid_legacy_preset (int board_id, const std::string &preset)
+ {
+ if ((preset == "p20") || (preset == "p21"))
+ {
+ return true;
+ }
+
+ bool is_muse_2016 = (board_id == (int)BoardIds::MUSE_2016_BOARD) ||
+ (board_id == (int)BoardIds::MUSE_2016_BLED_BOARD);
+ if ((preset == "p50") || (preset == "p51"))
+ {
+ return !is_muse_2016;
+ }
+
+ bool is_muse_s = (board_id == (int)BoardIds::MUSE_S_BOARD) ||
+ (board_id == (int)BoardIds::MUSE_S_BLED_BOARD);
+ if ((preset == "p60") || (preset == "p61"))
+ {
+ return is_muse_s;
+ }
+
+ return false;
+ }
+
+ inline bool is_valid_preset (int board_id, PresetFamily family, const std::string &preset)
+ {
+ if (family == PresetFamily::Anthena)
+ {
+ return is_valid_anthena_preset (preset);
+ }
+ return is_valid_legacy_preset (board_id, preset);
+ }
+
+ inline bool parse_preset_options (const std::string &other_info, int board_id,
+ PresetFamily family, bool allow_low_latency, std::string &preset, bool &enable_low_latency,
+ std::string &error)
+ {
+ std::string trimmed = trim_string (other_info);
+ if (trimmed.empty ())
+ {
+ return true;
+ }
+
+ if ((trimmed.find ('=') == std::string::npos) && (trimmed.find (';') == std::string::npos))
+ {
+ std::string parsed_preset = to_lower (trimmed);
+ if (!is_valid_preset (board_id, family, parsed_preset))
+ {
+ error = "invalid Muse preset: " + trimmed;
+ return false;
+ }
+ preset = parsed_preset;
+ return true;
+ }
+
+ bool has_options = false;
+ std::stringstream ss (trimmed);
+ std::string token;
+ while (std::getline (ss, token, ';'))
+ {
+ token = trim_string (token);
+ if (token.empty ())
+ {
+ continue;
+ }
+
+ size_t separator = token.find ('=');
+ if ((separator == std::string::npos) ||
+ (token.find ('=', separator + 1) != std::string::npos))
+ {
+ error = "invalid Muse other_info option: " + token + ". Expected key=value";
+ return false;
+ }
+
+ std::string key = to_lower (trim_string (token.substr (0, separator)));
+ std::string value = trim_string (token.substr (separator + 1));
+ if (key.empty () || value.empty ())
+ {
+ error = "invalid Muse other_info option: " + token + ". Empty key or value";
+ return false;
+ }
+
+ if (key == "preset")
+ {
+ value = to_lower (value);
+ if (!is_valid_preset (board_id, family, value))
+ {
+ error = "invalid Muse preset: " + value;
+ return false;
+ }
+ preset = value;
+ has_options = true;
+ }
+ else if ((key == "low_latency") && allow_low_latency)
+ {
+ bool parsed = false;
+ if (!parse_bool_option (value, parsed))
+ {
+ error = "invalid Muse low_latency value: " + value;
+ return false;
+ }
+ enable_low_latency = parsed;
+ has_options = true;
+ }
+ else
+ {
+ error = "unknown Muse other_info option: " + key;
+ return false;
+ }
+ }
+
+ if (!has_options)
+ {
+ error = "empty Muse other_info";
+ return false;
+ }
+
+ return true;
+ }
+} // namespace MuseOptions
diff --git a/src/board_controller/muse/muse.cpp b/src/board_controller/muse/muse.cpp
index 390bbf5ff..5839b5f43 100644
--- a/src/board_controller/muse/muse.cpp
+++ b/src/board_controller/muse/muse.cpp
@@ -4,6 +4,7 @@
#include "custom_cast.h"
#include "muse.h"
#include "muse_constants.h"
+#include "muse_options.h"
#include "timestamp.h"
#include
@@ -85,6 +86,7 @@ Muse::Muse (int board_id, struct BrainFlowInputParams params) : BLELibBoard (boa
last_ppg_timestamp = -1.0;
last_eeg_timestamp = -1.0;
last_aux_timestamp = -1.0;
+ muse_preset = "p21";
}
Muse::~Muse ()
@@ -104,6 +106,16 @@ int Muse::prepare_session ()
{
params.timeout = 6;
}
+ muse_preset = "p21";
+ bool unused_low_latency = false;
+ std::string parse_error;
+ if (!MuseOptions::parse_preset_options (params.other_info, board_id,
+ MuseOptions::PresetFamily::Legacy, false, muse_preset, unused_low_latency, parse_error))
+ {
+ safe_logger (spdlog::level::err, "Invalid Muse other_info: {}", parse_error);
+ return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+ }
+ safe_logger (spdlog::level::info, "Use Muse preset {}", muse_preset);
safe_logger (spdlog::level::info, "Use timeout for discovery: {}", params.timeout);
if (!init_dll_loader ())
{
@@ -426,7 +438,7 @@ int Muse::prepare_session ()
}
if (res == (int)BrainFlowExitCodes::STATUS_OK)
{
- res = config_board ("p21");
+ res = config_board (muse_preset);
}
else
{
diff --git a/src/board_controller/muse/muse_anthena.cpp b/src/board_controller/muse/muse_anthena.cpp
index dd1b29e8a..28c1c9aff 100644
--- a/src/board_controller/muse/muse_anthena.cpp
+++ b/src/board_controller/muse/muse_anthena.cpp
@@ -1,7 +1,5 @@
#include "muse_anthena.h"
-#include
-#include
#include
#include
#include
@@ -12,6 +10,7 @@
#include "custom_cast.h"
#include "muse_anthena_constants.h"
+#include "muse_options.h"
#include "timestamp.h"
@@ -60,13 +59,15 @@ bool MuseAnthena::get_sensor_config (uint8_t tag, SensorConfig &config)
config = SensorConfig (SensorType::ACC_GYRO, 6, 3, 52.0, 36);
return true;
case 0x53:
- config = SensorConfig (SensorType::UNKNOWN, 0, 0, 0.0, 24);
+ // DRL/REF: 2 channels, 6 samples at 32 Hz. BrainFlow does not expose it for
+ // Muse Anthena, but the fixed length is needed to skip the block correctly.
+ config = SensorConfig (SensorType::UNKNOWN, 2, 6, 32.0, 24);
return true;
case 0x88:
- config = SensorConfig (SensorType::BATTERY, 1, 1, 0.2, 0, true);
+ config = SensorConfig (SensorType::BATTERY, 10, 1, 1.0, 20);
return true;
case 0x98:
- config = SensorConfig (SensorType::BATTERY, 1, 1, 1.0, 20);
+ config = SensorConfig (SensorType::BATTERY, 10, 1, 0.1, 20);
return true;
default:
return false;
@@ -75,17 +76,21 @@ bool MuseAnthena::get_sensor_config (uint8_t tag, SensorConfig &config)
int MuseAnthena::get_optics_canonical_index (uint8_t tag, int channel)
{
- static const int optics4_indexes[] = {4, 5, 6, 7};
-
+ int num_channels = 0;
if (tag == 0x34)
{
- return (channel >= 0 && channel < 4) ? optics4_indexes[channel] : -1;
+ num_channels = 4;
}
- if ((tag == 0x35) && (channel >= 0) && (channel < 8))
+ else if (tag == 0x35)
{
- return channel;
+ num_channels = 8;
}
- if ((tag == 0x36) && (channel >= 0) && (channel < 16))
+ else if (tag == 0x36)
+ {
+ num_channels = 16;
+ }
+
+ if ((channel >= 0) && (channel < num_channels))
{
return channel;
}
@@ -94,52 +99,22 @@ int MuseAnthena::get_optics_canonical_index (uint8_t tag, int channel)
std::string MuseAnthena::trim_string (const std::string &value)
{
- size_t first = value.find_first_not_of (" \t\r\n");
- if (first == std::string::npos)
- {
- return "";
- }
- size_t last = value.find_last_not_of (" \t\r\n");
- return value.substr (first, last - first + 1);
+ return MuseOptions::trim_string (value);
}
std::string MuseAnthena::to_lower (const std::string &value)
{
- std::string lower_value = value;
- std::transform (lower_value.begin (), lower_value.end (), lower_value.begin (),
- [] (unsigned char ch) { return (char)std::tolower (ch); });
- return lower_value;
+ return MuseOptions::to_lower (value);
}
bool MuseAnthena::is_valid_muse_preset (const std::string &preset)
{
- static const char *valid_presets[] = {"p20", "p21", "p50", "p51", "p60", "p61", "p1034",
- "p1035", "p1041", "p1042", "p1043", "p1044", "p1045", "p1046", "p4129"};
-
- for (size_t i = 0; i < sizeof (valid_presets) / sizeof (valid_presets[0]); i++)
- {
- if (preset == valid_presets[i])
- {
- return true;
- }
- }
- return false;
+ return MuseOptions::is_valid_anthena_preset (preset);
}
bool MuseAnthena::parse_bool_option (const std::string &value, bool &parsed)
{
- std::string lower_value = to_lower (trim_string (value));
- if (lower_value == "true")
- {
- parsed = true;
- return true;
- }
- if (lower_value == "false")
- {
- parsed = false;
- return true;
- }
- return false;
+ return MuseOptions::parse_bool_option (value, parsed);
}
int MuseAnthena::parse_muse_options ()
@@ -147,89 +122,11 @@ int MuseAnthena::parse_muse_options ()
muse_preset = "p1041";
enable_low_latency = true;
- std::string other_info = trim_string (params.other_info);
- if (other_info.empty ())
- {
- return (int)BrainFlowExitCodes::STATUS_OK;
- }
-
- if ((other_info.find ('=') == std::string::npos) &&
- (other_info.find (';') == std::string::npos))
- {
- std::string preset = to_lower (other_info);
- if (!is_valid_muse_preset (preset))
- {
- safe_logger (
- spdlog::level::err, "Invalid MuseAnthena preset in other_info: {}", other_info);
- return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
- }
- muse_preset = preset;
- return (int)BrainFlowExitCodes::STATUS_OK;
- }
-
- bool has_options = false;
- std::stringstream ss (other_info);
- std::string token;
- while (std::getline (ss, token, ';'))
- {
- token = trim_string (token);
- if (token.empty ())
- {
- continue;
- }
-
- size_t separator = token.find ('=');
- if ((separator == std::string::npos) ||
- (token.find ('=', separator + 1) != std::string::npos))
- {
- safe_logger (spdlog::level::err,
- "Invalid MuseAnthena other_info option: {}. Expected key=value", token);
- return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
- }
-
- std::string key = to_lower (trim_string (token.substr (0, separator)));
- std::string value = trim_string (token.substr (separator + 1));
- if (key.empty () || value.empty ())
- {
- safe_logger (spdlog::level::err,
- "Invalid MuseAnthena other_info option: {}. Empty key or value", token);
- return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
- }
-
- if (key == "preset")
- {
- value = to_lower (value);
- if (!is_valid_muse_preset (value))
- {
- safe_logger (
- spdlog::level::err, "Invalid MuseAnthena preset in other_info: {}", value);
- return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
- }
- muse_preset = value;
- has_options = true;
- }
- else if (key == "low_latency")
- {
- bool parsed = false;
- if (!parse_bool_option (value, parsed))
- {
- safe_logger (spdlog::level::err,
- "Invalid MuseAnthena low_latency value in other_info: {}", value);
- return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
- }
- enable_low_latency = parsed;
- has_options = true;
- }
- else
- {
- safe_logger (spdlog::level::err, "Unknown MuseAnthena other_info option: {}", key);
- return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
- }
- }
-
- if (!has_options)
+ std::string parse_error;
+ if (!MuseOptions::parse_preset_options (params.other_info, board_id,
+ MuseOptions::PresetFamily::Anthena, true, muse_preset, enable_low_latency, parse_error))
{
- safe_logger (spdlog::level::err, "Invalid MuseAnthena other_info: {}", params.other_info);
+ safe_logger (spdlog::level::err, "Invalid MuseAnthena other_info: {}", parse_error);
return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
}
@@ -739,9 +636,12 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size)
}
const uint8_t *packet = data + offset;
- uint8_t packet_index = packet[1];
+ uint16_t packet_index =
+ cast_16bit_to_uint16_little_endian ((const unsigned char *)(packet + 1));
double packet_host_timestamp = get_timestamp ();
uint8_t primary_tag = packet[9];
+ uint8_t primary_block_index = packet[10];
+ uint32_t primary_package_num = ((uint32_t)packet_index << 8) | primary_block_index;
const uint8_t *packet_data = packet + PACKET_HEADER_SIZE;
size_t packet_data_size = packet_len - PACKET_HEADER_SIZE;
size_t packet_data_offset = 0;
@@ -753,8 +653,8 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size)
primary_config.variable_length ? packet_data_size : primary_config.data_len;
if ((primary_data_len > 0) && (primary_data_len <= packet_data_size))
{
- parse_sensor_payload (primary_tag, packet_index, packet_host_timestamp, packet_data,
- primary_data_len);
+ parse_sensor_payload (primary_tag, primary_package_num, packet_host_timestamp,
+ packet_data, primary_data_len);
packet_data_offset = primary_data_len;
}
else
@@ -776,6 +676,7 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size)
{
uint8_t tag = packet_data[packet_data_offset];
uint8_t subpacket_index = packet_data[packet_data_offset + 1];
+ uint32_t subpacket_package_num = ((uint32_t)packet_index << 8) | subpacket_index;
SensorConfig config;
if (!get_sensor_config (tag, config))
{
@@ -794,7 +695,7 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size)
break;
}
- parse_sensor_payload (tag, subpacket_index, packet_host_timestamp,
+ parse_sensor_payload (tag, subpacket_package_num, packet_host_timestamp,
packet_data + packet_data_offset + SUBPACKET_HEADER_SIZE, sensor_data_len);
packet_data_offset += SUBPACKET_HEADER_SIZE + sensor_data_len;
}
@@ -804,7 +705,7 @@ void MuseAnthena::handle_data_notification (const uint8_t *data, size_t size)
}
void MuseAnthena::parse_sensor_payload (
- uint8_t tag, uint8_t sequence_num, double host_timestamp, const uint8_t *data, size_t size)
+ uint8_t tag, uint32_t package_num, double host_timestamp, const uint8_t *data, size_t size)
{
SensorConfig config;
if (!get_sensor_config (tag, config))
@@ -824,7 +725,8 @@ void MuseAnthena::parse_sensor_payload (
if (size >= 2)
{
last_battery =
- (double)cast_16bit_to_uint16_little_endian ((const unsigned char *)data) / 256.0;
+ (double)cast_16bit_to_uint16_little_endian ((const unsigned char *)data) *
+ MUSE_ANTHENA_BATTERY_PERCENT_SCALE_FACTOR;
}
return;
}
@@ -847,7 +749,7 @@ void MuseAnthena::parse_sensor_payload (
for (int sample = 0; sample < config.n_samples; sample++)
{
std::vector package ((size_t)num_rows, 0.0);
- package[(size_t)package_num_channel] = (double)sequence_num;
+ package[(size_t)package_num_channel] = (double)package_num;
for (int channel = 0; channel < config.n_channels; channel++)
{
size_t bit_start = (size_t)(sample * config.n_channels + channel) * 14;
@@ -886,7 +788,7 @@ void MuseAnthena::parse_sensor_payload (
for (int sample = 0; sample < config.n_samples; sample++)
{
std::vector package ((size_t)num_rows, 0.0);
- package[(size_t)package_num_channel] = (double)sequence_num;
+ package[(size_t)package_num_channel] = (double)package_num;
for (int channel = 0; channel < 3; channel++)
{
int16_t raw = cast_16bit_to_int16_little_endian (
@@ -926,7 +828,7 @@ void MuseAnthena::parse_sensor_payload (
for (int sample = 0; sample < config.n_samples; sample++)
{
std::vector package ((size_t)num_rows, 0.0);
- package[(size_t)package_num_channel] = (double)sequence_num;
+ package[(size_t)package_num_channel] = (double)package_num;
package[(size_t)battery_channel] = last_battery;
for (int channel = 0; channel < config.n_channels; channel++)
diff --git a/src/board_controller/muse/muse_bglib/inc/muse_bglib_helper.h b/src/board_controller/muse/muse_bglib/inc/muse_bglib_helper.h
index 5464ce9f0..2779feff6 100644
--- a/src/board_controller/muse/muse_bglib/inc/muse_bglib_helper.h
+++ b/src/board_controller/muse/muse_bglib/inc/muse_bglib_helper.h
@@ -61,6 +61,7 @@ class MuseBGLibHelper
double last_ppg_timestamp; // used for timestamp correction
double last_eeg_timestamp; // used for timestamp correction
double last_aux_timestamp; // used for timestamp correction
+ std::string muse_preset;
void thread_worker ();
@@ -86,6 +87,7 @@ class MuseBGLibHelper
last_aux_timestamp = -1.0;
last_eeg_timestamp = -1.0;
last_ppg_timestamp = -1.0;
+ muse_preset = "p21";
}
virtual ~MuseBGLibHelper ()
diff --git a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp
index 943d4f18a..b442024c3 100644
--- a/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp
+++ b/src/board_controller/muse/muse_bglib/muse_bglib_helper.cpp
@@ -6,6 +6,7 @@
#include "custom_cast.h"
#include "muse_bglib_helper.h"
#include "muse_constants.h"
+#include "muse_options.h"
#include "timestamp.h"
#include "uart.h"
@@ -21,6 +22,15 @@ int MuseBGLibHelper::initialize (struct BrainFlowInputParams params)
if (!initialized)
{
input_params = params;
+ muse_preset = "p21";
+ bool unused_low_latency = false;
+ std::string parse_error;
+ if (!MuseOptions::parse_preset_options (input_params.other_info, board_id,
+ MuseOptions::PresetFamily::Legacy, false, muse_preset, unused_low_latency,
+ parse_error))
+ {
+ return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+ }
exit_code = (int)BrainFlowExitCodes::SYNC_TIMEOUT_ERROR;
int buffer_size_default = board_descr["default"]["num_rows"].get ();
int buffer_size_aux = board_descr["auxiliary"]["num_rows"].get ();
@@ -94,7 +104,7 @@ int MuseBGLibHelper::open_device ()
}
if (res == (int)BrainFlowExitCodes::STATUS_OK)
{
- res = config_device ("p21");
+ res = config_device (muse_preset.c_str ());
}
return res;
diff --git a/src/board_controller/muse/muse_bled.cpp b/src/board_controller/muse/muse_bled.cpp
index 7671d05a5..50e5f0087 100644
--- a/src/board_controller/muse/muse_bled.cpp
+++ b/src/board_controller/muse/muse_bled.cpp
@@ -3,6 +3,7 @@
#include "get_dll_dir.h"
#include "muse_bled.h"
+#include "muse_options.h"
#include "brainflow_constants.h"
@@ -79,6 +80,17 @@ int MuseBLED::prepare_session ()
safe_logger (spdlog::level::err, "you need to specify dongle port");
return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
}
+ std::string muse_preset = "p21";
+ bool unused_low_latency = false;
+ std::string parse_error;
+ if (!MuseOptions::parse_preset_options (params.other_info, board_id,
+ MuseOptions::PresetFamily::Legacy, false, muse_preset, unused_low_latency, parse_error))
+ {
+ safe_logger (spdlog::level::err, "Invalid MuseBLED other_info: {}", parse_error);
+ return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR;
+ }
+ params.other_info = muse_preset;
+ safe_logger (spdlog::level::info, "Use MuseBLED preset {}", muse_preset);
return DynLibBoard::prepare_session ();
}
@@ -211,4 +223,4 @@ void MuseBLED::read_thread ()
{
delete[] data_anc;
}
-}
\ No newline at end of file
+}