emg-gui 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emg_gui/__init__.py +0 -0
- emg_gui/acquisition/__init__.py +0 -0
- emg_gui/acquisition/data_source.py +384 -0
- emg_gui/acquisition/dataset_files.py +20 -0
- emg_gui/acquisition/sensor_worker.py +108 -0
- emg_gui/app.py +21 -0
- emg_gui/config/Roboto-Black.ttf +0 -0
- emg_gui/config/__init__.py +0 -0
- emg_gui/config/constants.py +17 -0
- emg_gui/core/__init__.py +0 -0
- emg_gui/core/enums.py +28 -0
- emg_gui/core/logger.py +50 -0
- emg_gui/core/types.py +5 -0
- emg_gui/processing/__init__.py +0 -0
- emg_gui/processing/filter.py +44 -0
- emg_gui/processing/window_functions.py +16 -0
- emg_gui/ui/__init__.py +0 -0
- emg_gui/ui/control_panel.py +84 -0
- emg_gui/ui/window.py +95 -0
- emg_gui/visualizer/__init__.py +0 -0
- emg_gui/visualizer/open_gl_widget.py +139 -0
- emg_gui/visualizer/shaders/__init__.py +0 -0
- emg_gui/visualizer/shaders/shader_loader.py +26 -0
- emg_gui/visualizer/shaders/spec/fragment.glsl +9 -0
- emg_gui/visualizer/shaders/spec/vertex.glsl +13 -0
- emg_gui/visualizer/shaders/text/fragment.glsl +13 -0
- emg_gui/visualizer/shaders/text/vertex.glsl +13 -0
- emg_gui/visualizer/shaders/ticks/fragment.glsl +8 -0
- emg_gui/visualizer/shaders/ticks/vertex.glsl +9 -0
- emg_gui/visualizer/shaders/time_series/fragment.glsl +8 -0
- emg_gui/visualizer/shaders/time_series/vertex.glsl +8 -0
- emg_gui/visualizer/spectrogram_renderer.py +175 -0
- emg_gui/visualizer/text_renderer.py +172 -0
- emg_gui/visualizer/ticks_renderer.py +77 -0
- emg_gui/visualizer/time_series_renderer.py +144 -0
- emg_gui-0.0.5.dist-info/METADATA +206 -0
- emg_gui-0.0.5.dist-info/RECORD +40 -0
- emg_gui-0.0.5.dist-info/WHEEL +4 -0
- emg_gui-0.0.5.dist-info/entry_points.txt +3 -0
- emg_gui-0.0.5.dist-info/licenses/LICENSE.md +24 -0
emg_gui/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from brainflow.board_shim import BoardIds, BoardShim, BrainFlowInputParams
|
|
8
|
+
from brainflow.data_filter import DataFilter
|
|
9
|
+
from serial.tools import list_ports
|
|
10
|
+
|
|
11
|
+
from emg_gui.acquisition.dataset_files import get_all_files
|
|
12
|
+
from emg_gui.config.constants import (
|
|
13
|
+
IS_SYNTHETIC_BOARD,
|
|
14
|
+
MARKER_END_ACTIVITY,
|
|
15
|
+
MARKER_START_ACTIVITY,
|
|
16
|
+
SENSOR_POLL_INTERVAL_MS,
|
|
17
|
+
)
|
|
18
|
+
from emg_gui.core.enums import ActivityState, RecordingState, StreamingState
|
|
19
|
+
from emg_gui.core.logger import Logger
|
|
20
|
+
from emg_gui.core.types import EMGArray
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DataSource(Protocol):
|
|
24
|
+
|
|
25
|
+
def start_stream(self) -> None: ...
|
|
26
|
+
|
|
27
|
+
def stop_stream(self) -> None: ...
|
|
28
|
+
|
|
29
|
+
def start_recording(self) -> None: ...
|
|
30
|
+
|
|
31
|
+
def stop_recording(self) -> None: ...
|
|
32
|
+
|
|
33
|
+
def record(self, data) -> None: ...
|
|
34
|
+
|
|
35
|
+
def insert_start_marker(self) -> None: ...
|
|
36
|
+
|
|
37
|
+
def insert_stop_marker(self) -> None: ...
|
|
38
|
+
|
|
39
|
+
def get_data(self) -> EMGArray: ...
|
|
40
|
+
|
|
41
|
+
def extract_emg_data(self, emg_with_marker_data: EMGArray) -> EMGArray: ...
|
|
42
|
+
|
|
43
|
+
def write_to_csv(self, datasplit: str, folder: str) -> None: ...
|
|
44
|
+
|
|
45
|
+
def release(self) -> None: ...
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def emg_channel_count(self) -> int: ...
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_streaming(self) -> bool: ...
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_recording(self) -> bool: ...
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_active(self) -> bool: ...
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def sampling_rate(self) -> int: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OpenBCIBoard:
|
|
64
|
+
_BOARD_ID = BoardIds.SYNTHETIC_BOARD if IS_SYNTHETIC_BOARD else BoardIds.CYTON_BOARD
|
|
65
|
+
|
|
66
|
+
_PORT_NAMES = ("FT231X USB UART", "VCP")
|
|
67
|
+
|
|
68
|
+
_instance = None
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def _get_serial_port(cls) -> str:
|
|
72
|
+
ports = []
|
|
73
|
+
|
|
74
|
+
for port in list_ports.comports():
|
|
75
|
+
description = port.description or ""
|
|
76
|
+
|
|
77
|
+
if description.startswith(cls._PORT_NAMES):
|
|
78
|
+
if platform.system() == "Darwin" and port.device.startswith("/dev/tty"):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
ports.append(port.device)
|
|
82
|
+
|
|
83
|
+
if not ports:
|
|
84
|
+
raise RuntimeError("DATASOURCE: No OpenBCI Cyton dongle found")
|
|
85
|
+
|
|
86
|
+
return ports[0]
|
|
87
|
+
|
|
88
|
+
def __init__(self, logger: Logger) -> None:
|
|
89
|
+
BoardShim.enable_dev_board_logger()
|
|
90
|
+
self._logger = logger
|
|
91
|
+
|
|
92
|
+
_params = BrainFlowInputParams()
|
|
93
|
+
|
|
94
|
+
if not IS_SYNTHETIC_BOARD:
|
|
95
|
+
_params.serial_port = self._get_serial_port()
|
|
96
|
+
self._logger.info(f"DATASOURCE: Port - {_params.serial_port}")
|
|
97
|
+
|
|
98
|
+
self._brainflow_emg_channels = BoardShim.get_emg_channels(
|
|
99
|
+
board_id=self._BOARD_ID
|
|
100
|
+
)
|
|
101
|
+
brainflow_marker_channel = BoardShim.get_marker_channel(board_id=self._BOARD_ID)
|
|
102
|
+
|
|
103
|
+
self._data_channels = self._brainflow_emg_channels + [brainflow_marker_channel]
|
|
104
|
+
self._emg_channel_count = len(self._brainflow_emg_channels)
|
|
105
|
+
|
|
106
|
+
self._board = BoardShim(board_id=self._BOARD_ID, input_params=_params)
|
|
107
|
+
self._board.prepare_session()
|
|
108
|
+
if not self._board.is_prepared():
|
|
109
|
+
self._logger.error("DATASOURCE: Board cannot be initialized")
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
self._turn_off_srb(self._board)
|
|
113
|
+
self._logger.info("DATASOURCE: Board initialized and SRB channels turned-off")
|
|
114
|
+
|
|
115
|
+
self._streaming_state = StreamingState.IDLE
|
|
116
|
+
self._recording_state = RecordingState.IDLE
|
|
117
|
+
self._activity_state = ActivityState.INACTIVE
|
|
118
|
+
|
|
119
|
+
self._emg_recording: list[EMGArray] = []
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def get_instance(cls, logger: Logger) -> DataSource:
|
|
123
|
+
if cls._instance is None:
|
|
124
|
+
cls._instance = cls(logger)
|
|
125
|
+
return cls._instance
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def emg_channel_count(self) -> int:
|
|
129
|
+
return self._emg_channel_count
|
|
130
|
+
|
|
131
|
+
def start_stream(self) -> None:
|
|
132
|
+
if self.is_streaming:
|
|
133
|
+
raise RuntimeError("DATASOURCE: Streaming has already started")
|
|
134
|
+
|
|
135
|
+
if self._board.is_prepared():
|
|
136
|
+
self._board.start_stream()
|
|
137
|
+
self._logger.info(
|
|
138
|
+
f"DATASOURCE: Data Stream Started (synthetic data: {IS_SYNTHETIC_BOARD})"
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
self._logger.error("DATASOURCE: Unable to connect with OpenBCI board")
|
|
142
|
+
raise RuntimeError("DATASOURCE: Unable to connect with OpenBCI board")
|
|
143
|
+
|
|
144
|
+
self._streaming_state = StreamingState.STREAMING
|
|
145
|
+
|
|
146
|
+
def stop_stream(self) -> None:
|
|
147
|
+
if not self.is_streaming:
|
|
148
|
+
raise RuntimeError("DATASOURCE: Streaming has already stopped")
|
|
149
|
+
|
|
150
|
+
self._board.stop_stream()
|
|
151
|
+
self._logger.info("DATASOURCE: Data Stream closed successfully")
|
|
152
|
+
|
|
153
|
+
flush_data = self._board.get_board_data()
|
|
154
|
+
self._logger.info(
|
|
155
|
+
f"DATASOURCE: Brainflow ring buffer cleaned: {flush_data.shape[1]}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self._logger.info(
|
|
159
|
+
f"DATASOURCE: Brainflow ring buffer after cleaning: {self._board.get_board_data_count()}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._streaming_state = StreamingState.IDLE
|
|
163
|
+
|
|
164
|
+
def start_recording(self) -> None:
|
|
165
|
+
if self.is_recording:
|
|
166
|
+
raise RuntimeError("DATASOURCE: Recording has already started")
|
|
167
|
+
|
|
168
|
+
self._recording_state = RecordingState.RECORDING
|
|
169
|
+
|
|
170
|
+
def stop_recording(self) -> None:
|
|
171
|
+
if not self.is_recording:
|
|
172
|
+
raise RuntimeError("DATASOURCE: Recording has already stopped")
|
|
173
|
+
|
|
174
|
+
self._recording_state = RecordingState.IDLE
|
|
175
|
+
|
|
176
|
+
def record(self, data) -> None:
|
|
177
|
+
if self.is_recording:
|
|
178
|
+
self._emg_recording.append(data)
|
|
179
|
+
|
|
180
|
+
def insert_start_marker(self) -> None:
|
|
181
|
+
if self.is_active:
|
|
182
|
+
raise RuntimeError("DATASOURCE: Activity has already started")
|
|
183
|
+
|
|
184
|
+
self._board.insert_marker(MARKER_START_ACTIVITY)
|
|
185
|
+
self._activity_state = ActivityState.ACTIVE
|
|
186
|
+
|
|
187
|
+
def insert_stop_marker(self) -> None:
|
|
188
|
+
if not self.is_active:
|
|
189
|
+
raise RuntimeError("DATASOURCE: Activity has already stopped")
|
|
190
|
+
|
|
191
|
+
self._board.insert_marker(MARKER_END_ACTIVITY)
|
|
192
|
+
self._activity_state = ActivityState.INACTIVE
|
|
193
|
+
|
|
194
|
+
def get_data(self) -> EMGArray:
|
|
195
|
+
all_board_data = self._board.get_board_data()
|
|
196
|
+
active_channel_data = all_board_data[self._data_channels, :]
|
|
197
|
+
return active_channel_data
|
|
198
|
+
|
|
199
|
+
def extract_emg_data(self, emg_with_marker_data: EMGArray) -> EMGArray:
|
|
200
|
+
emg_data = emg_with_marker_data[
|
|
201
|
+
: self.emg_channel_count, :
|
|
202
|
+
] # assumes marker is last
|
|
203
|
+
return emg_data
|
|
204
|
+
|
|
205
|
+
def write_to_csv(self, datasplit: str, folder: str) -> None:
|
|
206
|
+
emg_numpy = np.concatenate(self._emg_recording, axis=1)
|
|
207
|
+
file_path = (
|
|
208
|
+
f"data/csv/{datasplit}/{folder}/{str(int(time.time()))}__{folder}.csv"
|
|
209
|
+
)
|
|
210
|
+
DataFilter.write_file(data=emg_numpy, file_name=file_path, file_mode="w")
|
|
211
|
+
file_ds = get_all_files(
|
|
212
|
+
pattern=f"data/csv/{datasplit}/{folder}/*.csv", shuffle_flag=False
|
|
213
|
+
) # hard-coded: [TECH DEBT]
|
|
214
|
+
self._logger.info(
|
|
215
|
+
f"DATASOURCE: File written @ {file_path} (Total files: {sum(1 for _ in file_ds)})"
|
|
216
|
+
)
|
|
217
|
+
self._emg_recording.clear()
|
|
218
|
+
self._logger.info("DATASOURCE: Source data storage cleaned(Reset)")
|
|
219
|
+
|
|
220
|
+
def release(self) -> None:
|
|
221
|
+
if self.is_streaming:
|
|
222
|
+
self.stop_stream()
|
|
223
|
+
if self._board.is_prepared():
|
|
224
|
+
self._board.release_session()
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def sampling_rate(self) -> int:
|
|
228
|
+
return self._board.get_sampling_rate(self._BOARD_ID)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def is_streaming(self) -> bool:
|
|
232
|
+
return self._streaming_state is StreamingState.STREAMING
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def is_recording(self) -> bool:
|
|
236
|
+
return self._recording_state is RecordingState.RECORDING
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def is_active(self) -> bool:
|
|
240
|
+
return self._activity_state is ActivityState.ACTIVE
|
|
241
|
+
|
|
242
|
+
def _turn_off_srb(self, board):
|
|
243
|
+
for channel in self._brainflow_emg_channels:
|
|
244
|
+
board_response = board.config_board(f"x{channel}060100X")[:1]
|
|
245
|
+
if board_response not in {"S", "C"}:
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class PlaybackRecording:
|
|
250
|
+
_instance = None
|
|
251
|
+
|
|
252
|
+
def __init__(self, logger: Logger, file_path: str) -> None:
|
|
253
|
+
self._logger = logger
|
|
254
|
+
self._file_path = file_path
|
|
255
|
+
|
|
256
|
+
self._data = np.loadtxt(self._file_path, delimiter="\t").T
|
|
257
|
+
self._current_idx = 0
|
|
258
|
+
self._max_idx = self._data.shape[1]
|
|
259
|
+
|
|
260
|
+
self._data_channels = list(range(self._data.shape[0]))
|
|
261
|
+
self._emg_channel_count = len(self._data_channels) - 1 # assumes marker is last
|
|
262
|
+
|
|
263
|
+
self._logger.info(
|
|
264
|
+
f"DATASOURCE: PlaybackRecording - file: {self._file_path} "
|
|
265
|
+
f"- Channels: {self._data_channels} "
|
|
266
|
+
f"- EMG Channels Count: {self._emg_channel_count}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
self._streaming_state = StreamingState.IDLE
|
|
270
|
+
self._recording_state = RecordingState.IDLE
|
|
271
|
+
self._activity_state = ActivityState.INACTIVE
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
def get_instance(cls, logger: Logger, file_path: str) -> DataSource:
|
|
275
|
+
if cls._instance is None:
|
|
276
|
+
cls._instance = cls(logger, file_path)
|
|
277
|
+
return cls._instance
|
|
278
|
+
|
|
279
|
+
def start_stream(self) -> None:
|
|
280
|
+
if self.is_streaming:
|
|
281
|
+
raise RuntimeError("DATASOURCE: Playback has already started")
|
|
282
|
+
|
|
283
|
+
self._logger.info("DATASOURCE: Playback Started (playing-recording)")
|
|
284
|
+
self._current_idx = 0
|
|
285
|
+
self._streaming_state = StreamingState.STREAMING
|
|
286
|
+
|
|
287
|
+
def stop_stream(self) -> None:
|
|
288
|
+
if not self.is_streaming:
|
|
289
|
+
raise RuntimeError("DATASOURCE: Playback has already stopped")
|
|
290
|
+
|
|
291
|
+
self._logger.info("DATASOURCE: Playback closed successfully")
|
|
292
|
+
|
|
293
|
+
self._streaming_state = StreamingState.IDLE
|
|
294
|
+
|
|
295
|
+
def start_recording(self) -> None:
|
|
296
|
+
if self.is_recording:
|
|
297
|
+
raise RuntimeError("DATASOURCE: Recording has already started")
|
|
298
|
+
|
|
299
|
+
self._recording_state = RecordingState.RECORDING
|
|
300
|
+
|
|
301
|
+
def stop_recording(self) -> None:
|
|
302
|
+
if not self.is_recording:
|
|
303
|
+
raise RuntimeError("DATASOURCE: Recording has already stopped")
|
|
304
|
+
|
|
305
|
+
self._recording_state = RecordingState.IDLE
|
|
306
|
+
|
|
307
|
+
def record(self, data) -> None:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
def insert_start_marker(self) -> None:
|
|
311
|
+
if self.is_active:
|
|
312
|
+
raise RuntimeError("DATASOURCE: Activity has already started")
|
|
313
|
+
|
|
314
|
+
self._activity_state = ActivityState.ACTIVE
|
|
315
|
+
|
|
316
|
+
def insert_stop_marker(self) -> None:
|
|
317
|
+
if not self.is_active:
|
|
318
|
+
raise RuntimeError("DATASOURCE: Activity has already stopped")
|
|
319
|
+
|
|
320
|
+
self._activity_state = ActivityState.INACTIVE
|
|
321
|
+
|
|
322
|
+
def get_data(self) -> EMGArray:
|
|
323
|
+
expected_sample_count = round(
|
|
324
|
+
self.sampling_rate * SENSOR_POLL_INTERVAL_MS / 1000
|
|
325
|
+
) # playback speed
|
|
326
|
+
|
|
327
|
+
end_idx = min(self._current_idx + expected_sample_count, self._max_idx)
|
|
328
|
+
board_data = self._data[:, self._current_idx : end_idx]
|
|
329
|
+
self._current_idx = end_idx
|
|
330
|
+
|
|
331
|
+
return self._add_padding(
|
|
332
|
+
board_data, len(self._data_channels), expected_sample_count
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def extract_emg_data(self, emg_with_marker_data: EMGArray) -> EMGArray:
|
|
336
|
+
emg_data = emg_with_marker_data[
|
|
337
|
+
: self.emg_channel_count, :
|
|
338
|
+
] # assumes marker is last
|
|
339
|
+
return emg_data
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def _add_padding(
|
|
343
|
+
board_data: EMGArray,
|
|
344
|
+
channel_count: int,
|
|
345
|
+
expected_samples: int,
|
|
346
|
+
) -> EMGArray:
|
|
347
|
+
received_samples = board_data.shape[-1]
|
|
348
|
+
|
|
349
|
+
if received_samples == expected_samples:
|
|
350
|
+
return board_data
|
|
351
|
+
|
|
352
|
+
padded_data = np.zeros(
|
|
353
|
+
(channel_count, expected_samples),
|
|
354
|
+
dtype=board_data.dtype,
|
|
355
|
+
)
|
|
356
|
+
padded_data[:, :received_samples] = board_data
|
|
357
|
+
|
|
358
|
+
return padded_data
|
|
359
|
+
|
|
360
|
+
def write_to_csv(self, datasplit: str, folder: str) -> None:
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
def release(self) -> None:
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def sampling_rate(self) -> int:
|
|
368
|
+
return 250 # assumes cyton board
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def emg_channel_count(self) -> int:
|
|
372
|
+
return self._emg_channel_count
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def is_streaming(self) -> bool:
|
|
376
|
+
return self._streaming_state is StreamingState.STREAMING
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def is_recording(self) -> bool:
|
|
380
|
+
return self._recording_state is RecordingState.RECORDING
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def is_active(self) -> bool:
|
|
384
|
+
return self._activity_state is ActivityState.ACTIVE
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
import random
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_CSV_PATH = Path("data/csv")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_all_labels(selected_type="train") -> list[str]: # Default from train
|
|
10
|
+
train_path = os.path.join(_CSV_PATH, selected_type)
|
|
11
|
+
return os.listdir(path=train_path)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_all_files(pattern: str, shuffle_flag: bool) -> list[str]:
|
|
15
|
+
files = glob.glob(pattern)
|
|
16
|
+
|
|
17
|
+
if shuffle_flag:
|
|
18
|
+
random.shuffle(files)
|
|
19
|
+
|
|
20
|
+
return files
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from dvg_ringbuffer import RingBuffer
|
|
3
|
+
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
|
4
|
+
|
|
5
|
+
from emg_gui.acquisition.data_source import DataSource
|
|
6
|
+
from emg_gui.config.constants import (
|
|
7
|
+
EDGE_ARTIFACT_BUFFER,
|
|
8
|
+
SENSOR_POLL_INTERVAL_MS,
|
|
9
|
+
TIME_WINDOW_SAMPLES,
|
|
10
|
+
)
|
|
11
|
+
from emg_gui.core.logger import Logger
|
|
12
|
+
from emg_gui.core.types import EMGArray
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SensorWorker(QObject):
|
|
16
|
+
|
|
17
|
+
stream_changed = pyqtSignal(str)
|
|
18
|
+
record_changed = pyqtSignal(str)
|
|
19
|
+
marker_changed = pyqtSignal(str)
|
|
20
|
+
reset_changed = pyqtSignal(str)
|
|
21
|
+
|
|
22
|
+
def __init__(self, logger: Logger, board: DataSource) -> None:
|
|
23
|
+
super().__init__()
|
|
24
|
+
self._logger = logger
|
|
25
|
+
self._board = board
|
|
26
|
+
|
|
27
|
+
_buffer_length = TIME_WINDOW_SAMPLES + EDGE_ARTIFACT_BUFFER
|
|
28
|
+
self._emg_channel_count = board.emg_channel_count
|
|
29
|
+
|
|
30
|
+
self._zero_window = np.zeros(
|
|
31
|
+
(_buffer_length, self._emg_channel_count), dtype=np.float64
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self._ring_buffer = RingBuffer(
|
|
35
|
+
capacity=_buffer_length,
|
|
36
|
+
dtype=(
|
|
37
|
+
np.float64,
|
|
38
|
+
self._emg_channel_count,
|
|
39
|
+
), # pyright: ignore[reportArgumentType]
|
|
40
|
+
)
|
|
41
|
+
self._ring_buffer.extend(self._zero_window)
|
|
42
|
+
|
|
43
|
+
self._latest_snapshot = self._zero_window.T.copy(order="C")
|
|
44
|
+
|
|
45
|
+
self._sensor_timer = QTimer(self)
|
|
46
|
+
self._sensor_timer.timeout.connect(self._receive_sensor_data)
|
|
47
|
+
self._sensor_timer.setInterval(SENSOR_POLL_INTERVAL_MS)
|
|
48
|
+
|
|
49
|
+
@pyqtSlot()
|
|
50
|
+
def _receive_sensor_data(self) -> None:
|
|
51
|
+
emg_with_marker_data = self._board.get_data()
|
|
52
|
+
self._board.record(emg_with_marker_data)
|
|
53
|
+
emg_data = self._board.extract_emg_data(emg_with_marker_data)
|
|
54
|
+
self._ring_buffer.extend(emg_data.T)
|
|
55
|
+
buffer_copy = self._ring_buffer[:].T.copy(order="C")
|
|
56
|
+
self._latest_snapshot = (
|
|
57
|
+
buffer_copy # atomic assignment swaps without locks: thread safe
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@pyqtSlot()
|
|
61
|
+
def publish_buffer_snapshot(self) -> tuple[EMGArray, bool]:
|
|
62
|
+
"""Called/Runs directly by the GUI thread without any locks."""
|
|
63
|
+
return (self._latest_snapshot, self._board.is_streaming)
|
|
64
|
+
|
|
65
|
+
@pyqtSlot()
|
|
66
|
+
def stream(self) -> None:
|
|
67
|
+
if self._board.is_streaming:
|
|
68
|
+
self._sensor_timer.stop()
|
|
69
|
+
self._ring_buffer.extend(self._zero_window)
|
|
70
|
+
self._latest_snapshot = self._zero_window.T.copy(order="C")
|
|
71
|
+
self._board.stop_stream()
|
|
72
|
+
self.stream_changed.emit("Stream")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
self._board.start_stream()
|
|
76
|
+
self._sensor_timer.start()
|
|
77
|
+
self.stream_changed.emit("Streaming")
|
|
78
|
+
|
|
79
|
+
@pyqtSlot()
|
|
80
|
+
def shutdown_timer(self) -> None:
|
|
81
|
+
if self._sensor_timer.isActive():
|
|
82
|
+
self._logger.info("SENSORWORKER: Killed QTimer in sensor thread")
|
|
83
|
+
self._sensor_timer.stop()
|
|
84
|
+
|
|
85
|
+
@pyqtSlot(str, str)
|
|
86
|
+
def record(self, selected_datasplit: str, selected_folder: str) -> None:
|
|
87
|
+
if self._board.is_recording:
|
|
88
|
+
self._board.stop_recording()
|
|
89
|
+
self._board.write_to_csv(selected_datasplit, selected_folder)
|
|
90
|
+
self.record_changed.emit("Record")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
self._board.start_recording()
|
|
94
|
+
self.record_changed.emit("Recording")
|
|
95
|
+
|
|
96
|
+
@pyqtSlot()
|
|
97
|
+
def marker(self) -> None:
|
|
98
|
+
if self._board.is_active:
|
|
99
|
+
self._board.insert_stop_marker()
|
|
100
|
+
self.marker_changed.emit("Start Movement")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
self._board.insert_start_marker()
|
|
104
|
+
self.marker_changed.emit("Stop Movement")
|
|
105
|
+
|
|
106
|
+
@pyqtSlot()
|
|
107
|
+
def reset(self) -> None:
|
|
108
|
+
self._ring_buffer.extend(self._zero_window)
|
emg_gui/app.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from emg_gui.acquisition.data_source import DataSource, OpenBCIBoard
|
|
2
|
+
from emg_gui.core.logger import ConsoleLogger, Logger
|
|
3
|
+
from emg_gui.ui.window import EMGVisualizerWindow
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
logger: Logger = ConsoleLogger.get_instance()
|
|
8
|
+
board: DataSource = OpenBCIBoard.get_instance(logger)
|
|
9
|
+
# board: DataSource = PlaybackRecording.get_instance(
|
|
10
|
+
# logger, "data/csv/train/sample/1785181476__sample.csv"
|
|
11
|
+
# )
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
EMGVisualizerWindow.run(logger, board)
|
|
15
|
+
finally:
|
|
16
|
+
board.release()
|
|
17
|
+
logger.release()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
IS_SYNTHETIC_BOARD = True
|
|
2
|
+
|
|
3
|
+
SENSOR_POLL_INTERVAL_MS = 40
|
|
4
|
+
|
|
5
|
+
TIME_WINDOW_SAMPLES = 1200
|
|
6
|
+
SPECTROGRAM_WINDOW = 100
|
|
7
|
+
EDGE_ARTIFACT_BUFFER = 500
|
|
8
|
+
|
|
9
|
+
MARKER_START_ACTIVITY = 1.12345
|
|
10
|
+
MARKER_END_ACTIVITY = 2.12345
|
|
11
|
+
|
|
12
|
+
MAJOR_PIXEL_TICKS = 20
|
|
13
|
+
MINOR_PIXEL_TICKS = 10
|
|
14
|
+
|
|
15
|
+
TEXT_PIXEL_TICKS = 20
|
|
16
|
+
TEXT_SCALE = 2
|
|
17
|
+
TEXT_FONT_SIZE = 12
|
emg_gui/core/__init__.py
ADDED
|
File without changes
|
emg_gui/core/enums.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StreamingState(Enum):
|
|
5
|
+
IDLE = auto()
|
|
6
|
+
STREAMING = auto()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RecordingState(Enum):
|
|
10
|
+
IDLE = auto()
|
|
11
|
+
RECORDING = auto()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ActivityState(Enum):
|
|
15
|
+
ACTIVE = auto()
|
|
16
|
+
INACTIVE = auto()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatasetSplit(str, Enum):
|
|
20
|
+
TRAIN = "train"
|
|
21
|
+
VALIDATE = "validate"
|
|
22
|
+
TEST = "test"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DataSourceType(str, Enum):
|
|
26
|
+
REAL = "real"
|
|
27
|
+
SYNTHETIC = "synthetic"
|
|
28
|
+
PLAYBACK = "playback"
|
emg_gui/core/logger.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Logger(Protocol):
|
|
6
|
+
def info(self, message: str) -> None: ...
|
|
7
|
+
|
|
8
|
+
def error(self, message: str) -> None: ...
|
|
9
|
+
|
|
10
|
+
def release(self) -> None: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConsoleLogger:
|
|
14
|
+
_instance = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def get_instance(cls) -> Logger:
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
cls._instance = cls()
|
|
20
|
+
return cls._instance
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._logger = logging.getLogger("emg-gui")
|
|
24
|
+
self._logger.setLevel(logging.INFO)
|
|
25
|
+
self._logger.propagate = False
|
|
26
|
+
|
|
27
|
+
if not self._logger.handlers:
|
|
28
|
+
self._logger.addHandler(ConsoleLogger._get_handler())
|
|
29
|
+
|
|
30
|
+
def info(self, message: str) -> None:
|
|
31
|
+
self._logger.info(msg=message)
|
|
32
|
+
|
|
33
|
+
def error(self, message: str) -> None:
|
|
34
|
+
self._logger.error(msg=message)
|
|
35
|
+
|
|
36
|
+
def release(self) -> None:
|
|
37
|
+
handlers = self._logger.handlers[:]
|
|
38
|
+
for handler in handlers:
|
|
39
|
+
self._logger.removeHandler(handler)
|
|
40
|
+
handler.close()
|
|
41
|
+
ConsoleLogger._instance = None
|
|
42
|
+
print("Logger resources released successfully.")
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _get_handler() -> logging.StreamHandler:
|
|
46
|
+
handler = logging.StreamHandler()
|
|
47
|
+
_format = "%(asctime)s - %(levelname)s - %(filename)s - %(message)s"
|
|
48
|
+
_formatter = logging.Formatter(_format)
|
|
49
|
+
handler.setFormatter(_formatter)
|
|
50
|
+
return handler
|
emg_gui/core/types.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from brainflow.board_shim import BoardIds, BoardShim
|
|
2
|
+
from brainflow.data_filter import AggOperations, DataFilter, FilterTypes, NoiseTypes
|
|
3
|
+
|
|
4
|
+
from emg_gui.config.constants import IS_SYNTHETIC_BOARD
|
|
5
|
+
from emg_gui.core.types import EMGArray
|
|
6
|
+
|
|
7
|
+
_BOARD_ID = BoardIds.SYNTHETIC_BOARD if IS_SYNTHETIC_BOARD else BoardIds.CYTON_BOARD
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def filter_data(time_series: EMGArray) -> None:
|
|
11
|
+
|
|
12
|
+
DataFilter.perform_rolling_filter(time_series, 3, AggOperations.MEAN.value)
|
|
13
|
+
|
|
14
|
+
DataFilter.perform_bandstop(
|
|
15
|
+
data=time_series,
|
|
16
|
+
sampling_rate=BoardShim.get_sampling_rate(_BOARD_ID),
|
|
17
|
+
start_freq=58.0,
|
|
18
|
+
stop_freq=62.0,
|
|
19
|
+
order=4,
|
|
20
|
+
filter_type=FilterTypes.BUTTERWORTH,
|
|
21
|
+
ripple=1.0,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
DataFilter.perform_bandpass(
|
|
25
|
+
data=time_series,
|
|
26
|
+
sampling_rate=BoardShim.get_sampling_rate(_BOARD_ID),
|
|
27
|
+
start_freq=10.0,
|
|
28
|
+
stop_freq=125.0,
|
|
29
|
+
order=4,
|
|
30
|
+
filter_type=FilterTypes.BUTTERWORTH,
|
|
31
|
+
ripple=1.0,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
DataFilter.remove_environmental_noise(
|
|
35
|
+
data=time_series,
|
|
36
|
+
sampling_rate=BoardShim.get_sampling_rate(_BOARD_ID),
|
|
37
|
+
noise_type=NoiseTypes.FIFTY.value,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
DataFilter.remove_environmental_noise(
|
|
41
|
+
data=time_series,
|
|
42
|
+
sampling_rate=BoardShim.get_sampling_rate(_BOARD_ID),
|
|
43
|
+
noise_type=NoiseTypes.SIXTY.value,
|
|
44
|
+
)
|