Skip to content

Commit 5bfaaab

Browse files
committed
generic improvements in muse device support
Signed-off-by: Andrey Parfenov <a1994ndrey@gmail.com>
1 parent ce6cea2 commit 5bfaaab

14 files changed

Lines changed: 731 additions & 147 deletions

docs/SupportedBoards.rst

Lines changed: 244 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import argparse
2+
import time
3+
4+
from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets
5+
6+
7+
def main():
8+
BoardShim.enable_dev_board_logger()
9+
10+
parser = argparse.ArgumentParser()
11+
parser.add_argument('--duration', type=int, required=False, default=10)
12+
parser.add_argument('--mac-address', type=str, required=False, default='')
13+
parser.add_argument('--serial-number', type=str, required=False, default='')
14+
parser.add_argument('--timeout', type=int, required=False, default=0)
15+
args = parser.parse_args()
16+
17+
params = BrainFlowInputParams()
18+
params.mac_address = args.mac_address
19+
params.serial_number = args.serial_number
20+
params.timeout = args.timeout
21+
params.other_info = 'preset=p21;low_latency=true'
22+
23+
board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value
24+
board = BoardShim(board_id, params)
25+
26+
try:
27+
board.prepare_session()
28+
board.start_stream()
29+
try:
30+
time.sleep(args.duration)
31+
eeg_data = board.get_board_data(preset=BrainFlowPresets.DEFAULT_PRESET)
32+
print(eeg_data)
33+
finally:
34+
board.stop_stream()
35+
finally:
36+
if board.is_prepared():
37+
board.release_session()
38+
39+
40+
if __name__ == '__main__':
41+
main()
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import argparse
2+
import math
3+
import time
4+
5+
import matplotlib
6+
7+
matplotlib.use('Agg')
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
11+
from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets
12+
13+
14+
def print_summary(optics_data, timestamps, sampling_rate, channel_labels):
15+
print(f'optics samples: {optics_data.shape[1]}')
16+
17+
if timestamps.size > 1 and np.all(np.isfinite(timestamps)):
18+
timestamp_diffs = np.diff(timestamps)
19+
timestamp_diffs = timestamp_diffs[timestamp_diffs > 0]
20+
if timestamp_diffs.size > 0:
21+
actual_rate = 1.0 / np.median(timestamp_diffs)
22+
duration = timestamps[-1] - timestamps[0]
23+
print(f'timestamp duration: {duration:.3f} sec')
24+
print(f'timestamp sampling rate: {actual_rate:.2f} Hz, expected: {sampling_rate} Hz')
25+
26+
nan_count = int(np.isnan(optics_data).sum())
27+
print(f'nan values: {nan_count}')
28+
29+
for label, row in zip(channel_labels, optics_data):
30+
finite = row[np.isfinite(row)]
31+
if finite.size == 0:
32+
print(f'{label}: no finite values')
33+
continue
34+
stddev = float(np.std(finite))
35+
print(
36+
f'{label}: min={np.min(finite):.3f}, max={np.max(finite):.3f}, '
37+
f'mean={np.mean(finite):.3f}, std={stddev:.3f}'
38+
)
39+
if stddev < 1e-9:
40+
print(f'{label}: flat signal')
41+
42+
43+
def plot_optics(optics_data, timestamps, sampling_rate, output_file, channel_labels):
44+
if timestamps.size == optics_data.shape[1] and timestamps.size > 0 and np.all(np.isfinite(timestamps)):
45+
x_axis = timestamps - timestamps[0]
46+
x_label = 'Time, sec'
47+
else:
48+
x_axis = np.arange(optics_data.shape[1]) / sampling_rate
49+
x_label = 'Samples'
50+
51+
num_channels = optics_data.shape[0]
52+
cols = min(4, num_channels)
53+
rows = math.ceil(num_channels / cols)
54+
fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 2.4 * rows), sharex=True)
55+
axes = np.atleast_1d(axes).reshape(-1)
56+
57+
for index, axis in enumerate(axes):
58+
if index < num_channels:
59+
axis.plot(x_axis, optics_data[index])
60+
axis.set_title(channel_labels[index])
61+
axis.grid(True)
62+
else:
63+
axis.axis('off')
64+
65+
for axis in axes[-cols:]:
66+
axis.set_xlabel(x_label)
67+
68+
fig.tight_layout()
69+
fig.savefig(output_file, dpi=150)
70+
print(f'wrote plot: {output_file}')
71+
72+
73+
def main():
74+
BoardShim.enable_board_logger()
75+
76+
parser = argparse.ArgumentParser()
77+
parser.add_argument('--duration', type=int, required=False, default=20)
78+
parser.add_argument('--mac-address', type=str, required=False, default='')
79+
parser.add_argument('--serial-number', type=str, required=False, default='')
80+
parser.add_argument('--timeout', type=int, required=False, default=0)
81+
parser.add_argument('--other-info', type=str, required=False, default='preset=p1035;low_latency=true')
82+
parser.add_argument('--output-file', type=str, required=False, default='muse_anthena_optics_p1035.png')
83+
args = parser.parse_args()
84+
85+
params = BrainFlowInputParams()
86+
params.mac_address = args.mac_address
87+
params.serial_number = args.serial_number
88+
params.timeout = args.timeout
89+
params.other_info = args.other_info
90+
91+
board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value
92+
preset = BrainFlowPresets.ANCILLARY_PRESET
93+
optical_channels = BoardShim.get_optical_channels(board_id, preset)
94+
timestamp_channel = BoardShim.get_timestamp_channel(board_id, preset)
95+
sampling_rate = BoardShim.get_sampling_rate(board_id, preset)
96+
97+
board = BoardShim(board_id, params)
98+
try:
99+
board.prepare_session()
100+
board.start_stream()
101+
try:
102+
time.sleep(args.duration)
103+
data = board.get_board_data(preset=preset)
104+
finally:
105+
board.stop_stream()
106+
finally:
107+
if board.is_prepared():
108+
board.release_session()
109+
110+
optics_data = data[optical_channels, :]
111+
timestamps = data[timestamp_channel, :]
112+
channel_labels = [f'Optics {channel}' for channel in optical_channels]
113+
print_summary(optics_data, timestamps, sampling_rate, channel_labels)
114+
plot_optics(optics_data, timestamps, sampling_rate, args.output_file, channel_labels)
115+
116+
117+
if __name__ == '__main__':
118+
main()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import argparse
2+
import time
3+
4+
from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds, BrainFlowPresets
5+
from brainflow.data_filter import DataFilter
6+
7+
8+
def main():
9+
BoardShim.enable_dev_board_logger()
10+
11+
parser = argparse.ArgumentParser()
12+
parser.add_argument('--duration', type=int, required=False, default=10)
13+
parser.add_argument('--mac-address', type=str, required=False, default='')
14+
parser.add_argument('--serial-number', type=str, required=False, default='')
15+
parser.add_argument('--timeout', type=int, required=False, default=0)
16+
parser.add_argument('--output-prefix', type=str, required=False, default='muse_anthena')
17+
args = parser.parse_args()
18+
19+
params = BrainFlowInputParams()
20+
params.mac_address = args.mac_address
21+
params.serial_number = args.serial_number
22+
params.timeout = args.timeout
23+
params.other_info = 'preset=p1041;low_latency=true'
24+
25+
board_id = BoardIds.MUSE_S_ANTHENA_BOARD.value
26+
board = BoardShim(board_id, params)
27+
28+
try:
29+
board.prepare_session()
30+
board.start_stream()
31+
try:
32+
time.sleep(args.duration)
33+
eeg_data = board.get_board_data(preset=BrainFlowPresets.DEFAULT_PRESET)
34+
accel_data = board.get_board_data(preset=BrainFlowPresets.AUXILIARY_PRESET)
35+
optics_data = board.get_board_data(preset=BrainFlowPresets.ANCILLARY_PRESET)
36+
finally:
37+
board.stop_stream()
38+
finally:
39+
if board.is_prepared():
40+
board.release_session()
41+
42+
DataFilter.write_file(eeg_data, f'{args.output_prefix}_eeg.csv', 'w')
43+
DataFilter.write_file(accel_data, f'{args.output_prefix}_accel_gyro.csv', 'w')
44+
DataFilter.write_file(optics_data, f'{args.output_prefix}_optics_battery.csv', 'w')
45+
46+
47+
if __name__ == '__main__':
48+
main()

src/board_controller/brainflow_boards.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,8 @@ BrainFlowBoards::BrainFlowBoards()
420420
{"marker_channel", 5},
421421
{"package_num_channel", 0},
422422
{"num_rows", 6},
423-
{"ppg_channels", {1, 2, 3}}
423+
{"ppg_channels", {1, 2, 3}},
424+
{"optical_channels", {1, 2, 3}}
424425
};
425426
brainflow_boards_json["boards"]["22"]["default"] =
426427
{
@@ -453,7 +454,8 @@ BrainFlowBoards::BrainFlowBoards()
453454
{"marker_channel", 5},
454455
{"package_num_channel", 0},
455456
{"num_rows", 6},
456-
{"ppg_channels", {1, 2, 3}}
457+
{"ppg_channels", {1, 2, 3}},
458+
{"optical_channels", {1, 2, 3}}
457459
};
458460
brainflow_boards_json["boards"]["23"]["default"] =
459461
{
@@ -681,7 +683,8 @@ BrainFlowBoards::BrainFlowBoards()
681683
{"marker_channel", 5},
682684
{"package_num_channel", 0},
683685
{"num_rows", 6},
684-
{"ppg_channels", {1, 2, 3}}
686+
{"ppg_channels", {1, 2, 3}},
687+
{"optical_channels", {1, 2, 3}}
685688
};
686689
brainflow_boards_json["boards"]["39"]["default"] =
687690
{
@@ -714,7 +717,8 @@ BrainFlowBoards::BrainFlowBoards()
714717
{"marker_channel", 5},
715718
{"package_num_channel", 0},
716719
{"num_rows", 6},
717-
{"ppg_channels", {1, 2, 3}}
720+
{"ppg_channels", {1, 2, 3}},
721+
{"optical_channels", {1, 2, 3}}
718722
};
719723
brainflow_boards_json["boards"]["40"]["default"] =
720724
{

src/board_controller/muse/inc/muse.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "ble_lib_board.h"
44
#include <condition_variable>
55
#include <mutex>
6+
#include <string>
67
#include <utility>
78
#include <vector>
89

@@ -29,6 +30,7 @@ class Muse : public BLELibBoard
2930
double last_ppg_timestamp; // used for timestamp correction
3031
double last_eeg_timestamp; // used for timestamp correction
3132
double last_aux_timestamp; // used for timestamp correction
33+
std::string muse_preset;
3234

3335
public:
3436
Muse (int board_id, struct BrainFlowInputParams params);

src/board_controller/muse/inc/muse_anthena.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class MuseAnthena : public BLELibBoard
5757
std::string bytes_to_string (const uint8_t *data, size_t size);
5858
void handle_data_notification (const uint8_t *data, size_t size);
5959
void parse_sensor_payload (
60-
uint8_t tag, uint8_t sequence_num, double host_timestamp, const uint8_t *data, size_t size);
60+
uint8_t tag, uint32_t package_num, double host_timestamp, const uint8_t *data, size_t size);
6161
bool get_sensor_config (uint8_t tag, SensorConfig &config);
6262
int get_optics_canonical_index (uint8_t tag, int channel);
6363
void reset_timestamps ();

src/board_controller/muse/inc/muse_anthena_constants.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
#define MUSE_ANTHENA_GATT_DATA_2 "273e0014-4c4d-454d-96be-f03bac821358"
1010

1111
// info for equations
12-
#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.0000610352
13-
#define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.0074768
14-
#define MUSE_ANTHENA_EEG_SCALE_FACTOR (1450.0 / 16383.0)
12+
#define MUSE_ANTHENA_ACCELEROMETER_SCALE_FACTOR 0.00006103515625
13+
#define MUSE_ANTHENA_GYRO_SCALE_FACTOR -0.007476806640625
14+
#define MUSE_ANTHENA_EEG_SCALE_FACTOR 0.40293040293040294
1515
#define MUSE_ANTHENA_OPTICS_SCALE_FACTOR 1.0
16+
#define MUSE_ANTHENA_BATTERY_PERCENT_SCALE_FACTOR (1.0 / 512.0)

0 commit comments

Comments
 (0)