biosignal-device-interface 0.2.1a1__py3-none-any.whl → 0.2.1a2__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.
- biosignal_device_interface/constants/devices/__init__.py +3 -3
- biosignal_device_interface/constants/devices/core/base_device_constants.py +61 -61
- biosignal_device_interface/constants/devices/otb/otb_muovi_constants.py +129 -129
- biosignal_device_interface/constants/devices/otb/otb_quattrocento_constants.py +313 -313
- biosignal_device_interface/constants/devices/otb/otb_quattrocento_light_constants.py +59 -59
- biosignal_device_interface/constants/devices/otb/otb_syncstation_constants.py +233 -233
- biosignal_device_interface/constants/plots/color_palette.py +59 -59
- biosignal_device_interface/devices/__init__.py +17 -17
- biosignal_device_interface/devices/core/base_device.py +424 -412
- biosignal_device_interface/devices/otb/__init__.py +29 -29
- biosignal_device_interface/devices/otb/otb_muovi.py +290 -290
- biosignal_device_interface/devices/otb/otb_quattrocento.py +332 -332
- biosignal_device_interface/devices/otb/otb_quattrocento_light.py +210 -210
- biosignal_device_interface/devices/otb/otb_syncstation.py +407 -407
- biosignal_device_interface/gui/device_template_widgets/all_devices_widget.py +51 -51
- biosignal_device_interface/gui/device_template_widgets/core/base_device_widget.py +130 -130
- biosignal_device_interface/gui/device_template_widgets/core/base_multiple_devices_widget.py +108 -108
- biosignal_device_interface/gui/device_template_widgets/otb/otb_devices_widget.py +44 -44
- biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_plus_widget.py +158 -158
- biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_widget.py +158 -158
- biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_light_widget.py +174 -174
- biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_widget.py +260 -260
- biosignal_device_interface/gui/device_template_widgets/otb/otb_syncstation_widget.py +262 -262
- biosignal_device_interface/gui/plot_widgets/biosignal_plot_widget.py +501 -501
- biosignal_device_interface/gui/ui/devices_template_widget.ui +38 -38
- biosignal_device_interface/gui/ui/otb_muovi_plus_template_widget.ui +171 -171
- biosignal_device_interface/gui/ui/otb_muovi_template_widget.ui +171 -171
- biosignal_device_interface/gui/ui/otb_quattrocento_light_template_widget.ui +266 -266
- biosignal_device_interface/gui/ui/otb_quattrocento_template_widget.ui +415 -415
- biosignal_device_interface/gui/ui/otb_syncstation_template_widget.ui +732 -732
- biosignal_device_interface/gui/ui_compiled/devices_template_widget.py +56 -56
- biosignal_device_interface/gui/ui_compiled/otb_muovi_plus_template_widget.py +153 -153
- biosignal_device_interface/gui/ui_compiled/otb_muovi_template_widget.py +153 -153
- biosignal_device_interface/gui/ui_compiled/otb_quattrocento_light_template_widget.py +217 -217
- biosignal_device_interface/gui/ui_compiled/otb_quattrocento_template_widget.py +318 -318
- biosignal_device_interface/gui/ui_compiled/otb_syncstation_template_widget.py +495 -495
- {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.1a2.dist-info}/LICENSE +675 -675
- {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.1a2.dist-info}/METADATA +2 -2
- biosignal_device_interface-0.2.1a2.dist-info/RECORD +46 -0
- {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.1a2.dist-info}/WHEEL +1 -1
- biosignal_device_interface-0.2.1a1.dist-info/RECORD +0 -46
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
from biosignal_device_interface.devices.otb.otb_quattrocento import OTBQuattrocento
|
|
2
|
-
from biosignal_device_interface.devices.otb.otb_quattrocento_light import (
|
|
3
|
-
OTBQuattrocentoLight,
|
|
4
|
-
)
|
|
5
|
-
from biosignal_device_interface.devices.otb.otb_muovi import OTBMuovi
|
|
6
|
-
from biosignal_device_interface.devices.otb.otb_syncstation import OTBSyncStation
|
|
7
|
-
|
|
8
|
-
# Widgets
|
|
9
|
-
# All OTB Devices Widget
|
|
10
|
-
from biosignal_device_interface.gui.device_template_widgets.otb.otb_devices_widget import (
|
|
11
|
-
OTBDevicesWidget,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
# Individual OTB Device Widgets
|
|
15
|
-
from biosignal_device_interface.gui.device_template_widgets.otb.otb_muovi_widget import (
|
|
16
|
-
OTBMuoviWidget,
|
|
17
|
-
)
|
|
18
|
-
from biosignal_device_interface.gui.device_template_widgets.otb.otb_muovi_plus_widget import (
|
|
19
|
-
OTBMuoviPlusWidget,
|
|
20
|
-
)
|
|
21
|
-
from biosignal_device_interface.gui.device_template_widgets.otb.otb_quattrocento_widget import (
|
|
22
|
-
OTBQuattrocentoWidget,
|
|
23
|
-
)
|
|
24
|
-
from biosignal_device_interface.gui.device_template_widgets.otb.otb_quattrocento_light_widget import (
|
|
25
|
-
OTBQuattrocentoLightWidget,
|
|
26
|
-
)
|
|
27
|
-
from biosignal_device_interface.gui.device_template_widgets.otb.otb_syncstation_widget import (
|
|
28
|
-
OTBSyncStationWidget,
|
|
29
|
-
)
|
|
1
|
+
from biosignal_device_interface.devices.otb.otb_quattrocento import OTBQuattrocento
|
|
2
|
+
from biosignal_device_interface.devices.otb.otb_quattrocento_light import (
|
|
3
|
+
OTBQuattrocentoLight,
|
|
4
|
+
)
|
|
5
|
+
from biosignal_device_interface.devices.otb.otb_muovi import OTBMuovi
|
|
6
|
+
from biosignal_device_interface.devices.otb.otb_syncstation import OTBSyncStation
|
|
7
|
+
|
|
8
|
+
# Widgets
|
|
9
|
+
# All OTB Devices Widget
|
|
10
|
+
from biosignal_device_interface.gui.device_template_widgets.otb.otb_devices_widget import (
|
|
11
|
+
OTBDevicesWidget,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Individual OTB Device Widgets
|
|
15
|
+
from biosignal_device_interface.gui.device_template_widgets.otb.otb_muovi_widget import (
|
|
16
|
+
OTBMuoviWidget,
|
|
17
|
+
)
|
|
18
|
+
from biosignal_device_interface.gui.device_template_widgets.otb.otb_muovi_plus_widget import (
|
|
19
|
+
OTBMuoviPlusWidget,
|
|
20
|
+
)
|
|
21
|
+
from biosignal_device_interface.gui.device_template_widgets.otb.otb_quattrocento_widget import (
|
|
22
|
+
OTBQuattrocentoWidget,
|
|
23
|
+
)
|
|
24
|
+
from biosignal_device_interface.gui.device_template_widgets.otb.otb_quattrocento_light_widget import (
|
|
25
|
+
OTBQuattrocentoLightWidget,
|
|
26
|
+
)
|
|
27
|
+
from biosignal_device_interface.gui.device_template_widgets.otb.otb_syncstation_widget import (
|
|
28
|
+
OTBSyncStationWidget,
|
|
29
|
+
)
|
|
@@ -1,290 +1,290 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Device class for real-time interfacing the Muovi device.
|
|
3
|
-
Developer: Dominik I. Braun
|
|
4
|
-
Contact: dome.braun@fau.de
|
|
5
|
-
Last Update: 2024-06-05
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
from typing import TYPE_CHECKING, Union, Dict
|
|
10
|
-
from PySide6.QtNetwork import QTcpSocket, QTcpServer, QHostAddress
|
|
11
|
-
import numpy as np
|
|
12
|
-
|
|
13
|
-
# Local Libraries
|
|
14
|
-
from biosignal_device_interface.devices.core.base_device import BaseDevice
|
|
15
|
-
from biosignal_device_interface.constants.devices.core.base_device_constants import (
|
|
16
|
-
DeviceType,
|
|
17
|
-
DeviceChannelTypes,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
# Constants
|
|
21
|
-
from biosignal_device_interface.constants.devices.otb.otb_muovi_constants import (
|
|
22
|
-
MUOVI_CONVERSION_FACTOR_DICT,
|
|
23
|
-
MuoviWorkingMode,
|
|
24
|
-
MuoviDetectionMode,
|
|
25
|
-
MUOVI_WORKING_MODE_CHARACTERISTICS_DICT,
|
|
26
|
-
MUOVI_SAMPLES_PER_FRAME_DICT,
|
|
27
|
-
MUOVI_AVAILABLE_CHANNELS_DICT,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if TYPE_CHECKING:
|
|
31
|
-
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
32
|
-
from aenum import Enum
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class OTBMuovi(BaseDevice):
|
|
36
|
-
"""
|
|
37
|
-
Muovi device class derived from BaseDevice class.
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
parent (Union[QMainWindow, QWidget], optional):
|
|
41
|
-
Parent widget to which the device is assigned to.
|
|
42
|
-
Defaults to None.
|
|
43
|
-
|
|
44
|
-
is_muovi_plus (bool):
|
|
45
|
-
True if the device is a Muovi Plus, False if not.
|
|
46
|
-
|
|
47
|
-
The Muovi class is using a TCP/IP protocol to communicate with the device.
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
def __init__(
|
|
51
|
-
self,
|
|
52
|
-
parent: Union[QMainWindow, QWidget] = None,
|
|
53
|
-
is_muovi_plus: bool = False,
|
|
54
|
-
) -> None:
|
|
55
|
-
"""
|
|
56
|
-
Initialize the Muovi device.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
parent (Union[QMainWindow, QWidget], optional): Parent widget. Defaults to None.
|
|
60
|
-
is_muovi_plus (bool, optional): Boolean to initialize the Muovi device as Muovi+ (64 biosignal channels) or Muovi (32 biosignal channels). Defaults to False (Muovi).
|
|
61
|
-
"""
|
|
62
|
-
super().__init__(parent)
|
|
63
|
-
|
|
64
|
-
# Device Parameters
|
|
65
|
-
self._device_type: DeviceType = (
|
|
66
|
-
DeviceType.OTB_MUOVI_PLUS if is_muovi_plus else DeviceType.OTB_MUOVI
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# Connection Parameters
|
|
70
|
-
self._interface: QTcpServer = None
|
|
71
|
-
self._client_socket: QTcpSocket | None = None
|
|
72
|
-
|
|
73
|
-
# Configuration Parameters
|
|
74
|
-
self._working_mode: MuoviWorkingMode = MuoviWorkingMode.NONE
|
|
75
|
-
self._detection_mode: MuoviDetectionMode = MuoviDetectionMode.NONE
|
|
76
|
-
self._configuration_command: int | None = None
|
|
77
|
-
|
|
78
|
-
def _connect_to_device(self) -> bool:
|
|
79
|
-
super()._connect_to_device()
|
|
80
|
-
|
|
81
|
-
self._interface = QTcpServer(self)
|
|
82
|
-
self._received_bytes: bytearray = bytearray()
|
|
83
|
-
|
|
84
|
-
if not self._interface.listen(
|
|
85
|
-
QHostAddress(self._connection_settings[0]), self._connection_settings[1]
|
|
86
|
-
):
|
|
87
|
-
return False
|
|
88
|
-
|
|
89
|
-
self._interface.newConnection.connect(self._make_request)
|
|
90
|
-
|
|
91
|
-
self._connection_timeout_timer.start()
|
|
92
|
-
|
|
93
|
-
return True
|
|
94
|
-
|
|
95
|
-
def _make_request(self) -> bool:
|
|
96
|
-
super()._make_request()
|
|
97
|
-
self._client_socket = self._interface.nextPendingConnection()
|
|
98
|
-
|
|
99
|
-
if self._client_socket:
|
|
100
|
-
|
|
101
|
-
self._client_socket.readyRead.connect(self._read_data)
|
|
102
|
-
|
|
103
|
-
if not self.is_connected:
|
|
104
|
-
self.is_connected = True
|
|
105
|
-
self.connect_toggled.emit(self.is_connected)
|
|
106
|
-
self._connection_timeout_timer.stop()
|
|
107
|
-
return True
|
|
108
|
-
|
|
109
|
-
elif not self._is_configured:
|
|
110
|
-
self._is_configured = True
|
|
111
|
-
self.configure_toggled.emit(self._is_configured)
|
|
112
|
-
return True
|
|
113
|
-
|
|
114
|
-
def _disconnect_from_device(self) -> bool:
|
|
115
|
-
super()._disconnect_from_device()
|
|
116
|
-
|
|
117
|
-
if self._client_socket is not None:
|
|
118
|
-
self._client_socket.readyRead.disconnect(self._read_data)
|
|
119
|
-
self._client_socket.disconnectFromHost()
|
|
120
|
-
self._client_socket.close()
|
|
121
|
-
|
|
122
|
-
if self._interface is not None:
|
|
123
|
-
self._interface.close()
|
|
124
|
-
|
|
125
|
-
return True
|
|
126
|
-
|
|
127
|
-
def configure_device(
|
|
128
|
-
self, params: Dict[str, Union[Enum, Dict[str, Enum]]] # type: ignore
|
|
129
|
-
) -> None:
|
|
130
|
-
super().configure_device(params)
|
|
131
|
-
|
|
132
|
-
if not self.is_connected or self._client_socket is None:
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
# Check if detection mode is valid for working mode (Case EEG -> MONOPOLAR_GAIN_4 => MONOPOLAR_GAIN_8)
|
|
136
|
-
if self._working_mode == MuoviWorkingMode.EEG:
|
|
137
|
-
if self._detection_mode == MuoviDetectionMode.MONOPOLAR_GAIN_4:
|
|
138
|
-
self._detection_mode = MuoviDetectionMode.MONOPOLAR_GAIN_8
|
|
139
|
-
|
|
140
|
-
self._conversion_factor_biosignal = MUOVI_CONVERSION_FACTOR_DICT[
|
|
141
|
-
self._detection_mode
|
|
142
|
-
]
|
|
143
|
-
self._conversion_factor_auxiliary = self._conversion_factor_biosignal
|
|
144
|
-
|
|
145
|
-
# Set configuration parameters for data transfer
|
|
146
|
-
working_mode_characteristics = MUOVI_WORKING_MODE_CHARACTERISTICS_DICT[
|
|
147
|
-
self._working_mode
|
|
148
|
-
]
|
|
149
|
-
self._sampling_frequency = working_mode_characteristics["sampling_frequency"]
|
|
150
|
-
self._bytes_per_sample = working_mode_characteristics["bytes_per_sample"]
|
|
151
|
-
self._samples_per_frame = MUOVI_SAMPLES_PER_FRAME_DICT[self._device_type][
|
|
152
|
-
self._working_mode
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
self._number_of_channels = MUOVI_AVAILABLE_CHANNELS_DICT[self._device_type][
|
|
156
|
-
DeviceChannelTypes.ALL
|
|
157
|
-
]
|
|
158
|
-
self._number_of_biosignal_channels = MUOVI_AVAILABLE_CHANNELS_DICT[
|
|
159
|
-
self._device_type
|
|
160
|
-
][DeviceChannelTypes.BIOSIGNAL]
|
|
161
|
-
self._biosignal_channel_indices = np.arange(self._number_of_biosignal_channels)
|
|
162
|
-
|
|
163
|
-
self._number_of_auxiliary_channels = MUOVI_AVAILABLE_CHANNELS_DICT[
|
|
164
|
-
self._device_type
|
|
165
|
-
][DeviceChannelTypes.AUXILIARY]
|
|
166
|
-
self._auxiliary_channel_indices = np.arange(
|
|
167
|
-
self._number_of_biosignal_channels,
|
|
168
|
-
self._number_of_biosignal_channels + self._number_of_auxiliary_channels,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
self._buffer_size = (
|
|
172
|
-
self._number_of_channels * self._samples_per_frame * self._bytes_per_sample
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
self._received_bytes = bytearray()
|
|
176
|
-
|
|
177
|
-
self._configure_command()
|
|
178
|
-
self._send_configuration_to_device()
|
|
179
|
-
|
|
180
|
-
def _send_configuration_to_device(self) -> None:
|
|
181
|
-
configuration_bytes = int(self._configuration_command).to_bytes(
|
|
182
|
-
1, byteorder="big"
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
success = self._client_socket.write(configuration_bytes)
|
|
186
|
-
|
|
187
|
-
if success == -1:
|
|
188
|
-
self._disconnect_from_device()
|
|
189
|
-
|
|
190
|
-
def _configure_command(self) -> None:
|
|
191
|
-
self._configuration_command = self._working_mode.value << 2
|
|
192
|
-
self._configuration_command += self._detection_mode.value
|
|
193
|
-
|
|
194
|
-
def _start_streaming(self) -> None:
|
|
195
|
-
super()._start_streaming()
|
|
196
|
-
|
|
197
|
-
if self._configuration_command is None:
|
|
198
|
-
return
|
|
199
|
-
|
|
200
|
-
self._configuration_command += 1
|
|
201
|
-
self._send_configuration_to_device()
|
|
202
|
-
|
|
203
|
-
def _stop_streaming(self) -> None:
|
|
204
|
-
super()._stop_streaming()
|
|
205
|
-
|
|
206
|
-
if self._configuration_command is None:
|
|
207
|
-
return
|
|
208
|
-
|
|
209
|
-
self._configuration_command -= 1
|
|
210
|
-
self._send_configuration_to_device()
|
|
211
|
-
|
|
212
|
-
def clear_socket(self) -> None:
|
|
213
|
-
if self._client_socket is not None:
|
|
214
|
-
self._client_socket.readAll()
|
|
215
|
-
|
|
216
|
-
def _read_data(self) -> None:
|
|
217
|
-
super()._read_data()
|
|
218
|
-
|
|
219
|
-
if not self._is_streaming:
|
|
220
|
-
self.clear_socket()
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
while self._client_socket.bytesAvailable() > self._buffer_size:
|
|
224
|
-
packet = self._client_socket.read(self._buffer_size)
|
|
225
|
-
if not packet:
|
|
226
|
-
continue
|
|
227
|
-
|
|
228
|
-
self._received_bytes.extend(packet)
|
|
229
|
-
|
|
230
|
-
while len(self._received_bytes) >= self._buffer_size:
|
|
231
|
-
data_to_process = self._received_bytes[: self._buffer_size]
|
|
232
|
-
self._process_data(data_to_process)
|
|
233
|
-
self._received_bytes = self._received_bytes[self._buffer_size :]
|
|
234
|
-
|
|
235
|
-
def _process_data(self, input: bytearray) -> None:
|
|
236
|
-
super()._process_data(input)
|
|
237
|
-
|
|
238
|
-
decoded_data = self._bytes_to_integers(input)
|
|
239
|
-
|
|
240
|
-
processed_data = decoded_data.reshape(
|
|
241
|
-
self._number_of_channels, -1, order="F"
|
|
242
|
-
).astype(np.float32)
|
|
243
|
-
|
|
244
|
-
# Emit the data
|
|
245
|
-
self.data_available.emit(processed_data)
|
|
246
|
-
self.biosignal_data_available.emit(self._extract_biosignal_data(processed_data))
|
|
247
|
-
self.auxiliary_data_available.emit(self._extract_auxiliary_data(processed_data))
|
|
248
|
-
|
|
249
|
-
# Convert channels from bytes to integers
|
|
250
|
-
def _bytes_to_integers(
|
|
251
|
-
self,
|
|
252
|
-
data: bytearray,
|
|
253
|
-
) -> np.ndarray:
|
|
254
|
-
channel_values = []
|
|
255
|
-
# Separate channels from byte-string. One channel has
|
|
256
|
-
# "bytes_in_sample" many bytes in it.
|
|
257
|
-
for channel_index in range(len(data) // 2):
|
|
258
|
-
channel_start = channel_index * self._bytes_per_sample
|
|
259
|
-
channel_end = (channel_index + 1) * self._bytes_per_sample
|
|
260
|
-
channel = data[channel_start:channel_end]
|
|
261
|
-
|
|
262
|
-
# Convert channel's byte value to integer
|
|
263
|
-
match self._working_mode:
|
|
264
|
-
case MuoviWorkingMode.EMG:
|
|
265
|
-
value = self._decode_int16(channel)
|
|
266
|
-
case MuoviWorkingMode.EEG:
|
|
267
|
-
value = self._decode_int24(channel)
|
|
268
|
-
|
|
269
|
-
channel_values.append(value)
|
|
270
|
-
|
|
271
|
-
return np.array(channel_values)
|
|
272
|
-
|
|
273
|
-
def _decode_int16(self, bytes_value: bytearray) -> int:
|
|
274
|
-
value = None
|
|
275
|
-
# Combine 2 bytes to a 16 bit integer value
|
|
276
|
-
value = bytes_value[0] * 2**8 + bytes_value[1]
|
|
277
|
-
# See if the value is negative and make the two's complement
|
|
278
|
-
if value >= 2**15:
|
|
279
|
-
value -= 2**16
|
|
280
|
-
return value
|
|
281
|
-
|
|
282
|
-
# Convert byte-array value to an integer value and apply two's complement
|
|
283
|
-
def _decode_int24(self, bytes_value: bytearray) -> int:
|
|
284
|
-
value = None
|
|
285
|
-
# Combine 3 bytes to a 24 bit integer value
|
|
286
|
-
value = bytes_value[0] * 2**16 + bytes_value[1] * 2**8 + bytes_value[2]
|
|
287
|
-
# See if the value is negative and make the two's complement
|
|
288
|
-
if value >= 2**23:
|
|
289
|
-
value -= 2**24
|
|
290
|
-
return value
|
|
1
|
+
"""
|
|
2
|
+
Device class for real-time interfacing the Muovi device.
|
|
3
|
+
Developer: Dominik I. Braun
|
|
4
|
+
Contact: dome.braun@fau.de
|
|
5
|
+
Last Update: 2024-06-05
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import TYPE_CHECKING, Union, Dict
|
|
10
|
+
from PySide6.QtNetwork import QTcpSocket, QTcpServer, QHostAddress
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
# Local Libraries
|
|
14
|
+
from biosignal_device_interface.devices.core.base_device import BaseDevice
|
|
15
|
+
from biosignal_device_interface.constants.devices.core.base_device_constants import (
|
|
16
|
+
DeviceType,
|
|
17
|
+
DeviceChannelTypes,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Constants
|
|
21
|
+
from biosignal_device_interface.constants.devices.otb.otb_muovi_constants import (
|
|
22
|
+
MUOVI_CONVERSION_FACTOR_DICT,
|
|
23
|
+
MuoviWorkingMode,
|
|
24
|
+
MuoviDetectionMode,
|
|
25
|
+
MUOVI_WORKING_MODE_CHARACTERISTICS_DICT,
|
|
26
|
+
MUOVI_SAMPLES_PER_FRAME_DICT,
|
|
27
|
+
MUOVI_AVAILABLE_CHANNELS_DICT,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
32
|
+
from aenum import Enum
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OTBMuovi(BaseDevice):
|
|
36
|
+
"""
|
|
37
|
+
Muovi device class derived from BaseDevice class.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
parent (Union[QMainWindow, QWidget], optional):
|
|
41
|
+
Parent widget to which the device is assigned to.
|
|
42
|
+
Defaults to None.
|
|
43
|
+
|
|
44
|
+
is_muovi_plus (bool):
|
|
45
|
+
True if the device is a Muovi Plus, False if not.
|
|
46
|
+
|
|
47
|
+
The Muovi class is using a TCP/IP protocol to communicate with the device.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
parent: Union[QMainWindow, QWidget] = None,
|
|
53
|
+
is_muovi_plus: bool = False,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Initialize the Muovi device.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
parent (Union[QMainWindow, QWidget], optional): Parent widget. Defaults to None.
|
|
60
|
+
is_muovi_plus (bool, optional): Boolean to initialize the Muovi device as Muovi+ (64 biosignal channels) or Muovi (32 biosignal channels). Defaults to False (Muovi).
|
|
61
|
+
"""
|
|
62
|
+
super().__init__(parent)
|
|
63
|
+
|
|
64
|
+
# Device Parameters
|
|
65
|
+
self._device_type: DeviceType = (
|
|
66
|
+
DeviceType.OTB_MUOVI_PLUS if is_muovi_plus else DeviceType.OTB_MUOVI
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Connection Parameters
|
|
70
|
+
self._interface: QTcpServer = None
|
|
71
|
+
self._client_socket: QTcpSocket | None = None
|
|
72
|
+
|
|
73
|
+
# Configuration Parameters
|
|
74
|
+
self._working_mode: MuoviWorkingMode = MuoviWorkingMode.NONE
|
|
75
|
+
self._detection_mode: MuoviDetectionMode = MuoviDetectionMode.NONE
|
|
76
|
+
self._configuration_command: int | None = None
|
|
77
|
+
|
|
78
|
+
def _connect_to_device(self) -> bool:
|
|
79
|
+
super()._connect_to_device()
|
|
80
|
+
|
|
81
|
+
self._interface = QTcpServer(self)
|
|
82
|
+
self._received_bytes: bytearray = bytearray()
|
|
83
|
+
|
|
84
|
+
if not self._interface.listen(
|
|
85
|
+
QHostAddress(self._connection_settings[0]), self._connection_settings[1]
|
|
86
|
+
):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
self._interface.newConnection.connect(self._make_request)
|
|
90
|
+
|
|
91
|
+
self._connection_timeout_timer.start()
|
|
92
|
+
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
def _make_request(self) -> bool:
|
|
96
|
+
super()._make_request()
|
|
97
|
+
self._client_socket = self._interface.nextPendingConnection()
|
|
98
|
+
|
|
99
|
+
if self._client_socket:
|
|
100
|
+
|
|
101
|
+
self._client_socket.readyRead.connect(self._read_data)
|
|
102
|
+
|
|
103
|
+
if not self.is_connected:
|
|
104
|
+
self.is_connected = True
|
|
105
|
+
self.connect_toggled.emit(self.is_connected)
|
|
106
|
+
self._connection_timeout_timer.stop()
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
elif not self._is_configured:
|
|
110
|
+
self._is_configured = True
|
|
111
|
+
self.configure_toggled.emit(self._is_configured)
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def _disconnect_from_device(self) -> bool:
|
|
115
|
+
super()._disconnect_from_device()
|
|
116
|
+
|
|
117
|
+
if self._client_socket is not None:
|
|
118
|
+
self._client_socket.readyRead.disconnect(self._read_data)
|
|
119
|
+
self._client_socket.disconnectFromHost()
|
|
120
|
+
self._client_socket.close()
|
|
121
|
+
|
|
122
|
+
if self._interface is not None:
|
|
123
|
+
self._interface.close()
|
|
124
|
+
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
def configure_device(
|
|
128
|
+
self, params: Dict[str, Union[Enum, Dict[str, Enum]]] # type: ignore
|
|
129
|
+
) -> None:
|
|
130
|
+
super().configure_device(params)
|
|
131
|
+
|
|
132
|
+
if not self.is_connected or self._client_socket is None:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Check if detection mode is valid for working mode (Case EEG -> MONOPOLAR_GAIN_4 => MONOPOLAR_GAIN_8)
|
|
136
|
+
if self._working_mode == MuoviWorkingMode.EEG:
|
|
137
|
+
if self._detection_mode == MuoviDetectionMode.MONOPOLAR_GAIN_4:
|
|
138
|
+
self._detection_mode = MuoviDetectionMode.MONOPOLAR_GAIN_8
|
|
139
|
+
|
|
140
|
+
self._conversion_factor_biosignal = MUOVI_CONVERSION_FACTOR_DICT[
|
|
141
|
+
self._detection_mode
|
|
142
|
+
]
|
|
143
|
+
self._conversion_factor_auxiliary = self._conversion_factor_biosignal
|
|
144
|
+
|
|
145
|
+
# Set configuration parameters for data transfer
|
|
146
|
+
working_mode_characteristics = MUOVI_WORKING_MODE_CHARACTERISTICS_DICT[
|
|
147
|
+
self._working_mode
|
|
148
|
+
]
|
|
149
|
+
self._sampling_frequency = working_mode_characteristics["sampling_frequency"]
|
|
150
|
+
self._bytes_per_sample = working_mode_characteristics["bytes_per_sample"]
|
|
151
|
+
self._samples_per_frame = MUOVI_SAMPLES_PER_FRAME_DICT[self._device_type][
|
|
152
|
+
self._working_mode
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
self._number_of_channels = MUOVI_AVAILABLE_CHANNELS_DICT[self._device_type][
|
|
156
|
+
DeviceChannelTypes.ALL
|
|
157
|
+
]
|
|
158
|
+
self._number_of_biosignal_channels = MUOVI_AVAILABLE_CHANNELS_DICT[
|
|
159
|
+
self._device_type
|
|
160
|
+
][DeviceChannelTypes.BIOSIGNAL]
|
|
161
|
+
self._biosignal_channel_indices = np.arange(self._number_of_biosignal_channels)
|
|
162
|
+
|
|
163
|
+
self._number_of_auxiliary_channels = MUOVI_AVAILABLE_CHANNELS_DICT[
|
|
164
|
+
self._device_type
|
|
165
|
+
][DeviceChannelTypes.AUXILIARY]
|
|
166
|
+
self._auxiliary_channel_indices = np.arange(
|
|
167
|
+
self._number_of_biosignal_channels,
|
|
168
|
+
self._number_of_biosignal_channels + self._number_of_auxiliary_channels,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self._buffer_size = (
|
|
172
|
+
self._number_of_channels * self._samples_per_frame * self._bytes_per_sample
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self._received_bytes = bytearray()
|
|
176
|
+
|
|
177
|
+
self._configure_command()
|
|
178
|
+
self._send_configuration_to_device()
|
|
179
|
+
|
|
180
|
+
def _send_configuration_to_device(self) -> None:
|
|
181
|
+
configuration_bytes = int(self._configuration_command).to_bytes(
|
|
182
|
+
1, byteorder="big"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
success = self._client_socket.write(configuration_bytes)
|
|
186
|
+
|
|
187
|
+
if success == -1:
|
|
188
|
+
self._disconnect_from_device()
|
|
189
|
+
|
|
190
|
+
def _configure_command(self) -> None:
|
|
191
|
+
self._configuration_command = self._working_mode.value << 2
|
|
192
|
+
self._configuration_command += self._detection_mode.value
|
|
193
|
+
|
|
194
|
+
def _start_streaming(self) -> None:
|
|
195
|
+
super()._start_streaming()
|
|
196
|
+
|
|
197
|
+
if self._configuration_command is None:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
self._configuration_command += 1
|
|
201
|
+
self._send_configuration_to_device()
|
|
202
|
+
|
|
203
|
+
def _stop_streaming(self) -> None:
|
|
204
|
+
super()._stop_streaming()
|
|
205
|
+
|
|
206
|
+
if self._configuration_command is None:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
self._configuration_command -= 1
|
|
210
|
+
self._send_configuration_to_device()
|
|
211
|
+
|
|
212
|
+
def clear_socket(self) -> None:
|
|
213
|
+
if self._client_socket is not None:
|
|
214
|
+
self._client_socket.readAll()
|
|
215
|
+
|
|
216
|
+
def _read_data(self) -> None:
|
|
217
|
+
super()._read_data()
|
|
218
|
+
|
|
219
|
+
if not self._is_streaming:
|
|
220
|
+
self.clear_socket()
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
while self._client_socket.bytesAvailable() > self._buffer_size:
|
|
224
|
+
packet = self._client_socket.read(self._buffer_size)
|
|
225
|
+
if not packet:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
self._received_bytes.extend(packet)
|
|
229
|
+
|
|
230
|
+
while len(self._received_bytes) >= self._buffer_size:
|
|
231
|
+
data_to_process = self._received_bytes[: self._buffer_size]
|
|
232
|
+
self._process_data(data_to_process)
|
|
233
|
+
self._received_bytes = self._received_bytes[self._buffer_size :]
|
|
234
|
+
|
|
235
|
+
def _process_data(self, input: bytearray) -> None:
|
|
236
|
+
super()._process_data(input)
|
|
237
|
+
|
|
238
|
+
decoded_data = self._bytes_to_integers(input)
|
|
239
|
+
|
|
240
|
+
processed_data = decoded_data.reshape(
|
|
241
|
+
self._number_of_channels, -1, order="F"
|
|
242
|
+
).astype(np.float32)
|
|
243
|
+
|
|
244
|
+
# Emit the data
|
|
245
|
+
self.data_available.emit(processed_data)
|
|
246
|
+
self.biosignal_data_available.emit(self._extract_biosignal_data(processed_data))
|
|
247
|
+
self.auxiliary_data_available.emit(self._extract_auxiliary_data(processed_data))
|
|
248
|
+
|
|
249
|
+
# Convert channels from bytes to integers
|
|
250
|
+
def _bytes_to_integers(
|
|
251
|
+
self,
|
|
252
|
+
data: bytearray,
|
|
253
|
+
) -> np.ndarray:
|
|
254
|
+
channel_values = []
|
|
255
|
+
# Separate channels from byte-string. One channel has
|
|
256
|
+
# "bytes_in_sample" many bytes in it.
|
|
257
|
+
for channel_index in range(len(data) // 2):
|
|
258
|
+
channel_start = channel_index * self._bytes_per_sample
|
|
259
|
+
channel_end = (channel_index + 1) * self._bytes_per_sample
|
|
260
|
+
channel = data[channel_start:channel_end]
|
|
261
|
+
|
|
262
|
+
# Convert channel's byte value to integer
|
|
263
|
+
match self._working_mode:
|
|
264
|
+
case MuoviWorkingMode.EMG:
|
|
265
|
+
value = self._decode_int16(channel)
|
|
266
|
+
case MuoviWorkingMode.EEG:
|
|
267
|
+
value = self._decode_int24(channel)
|
|
268
|
+
|
|
269
|
+
channel_values.append(value)
|
|
270
|
+
|
|
271
|
+
return np.array(channel_values)
|
|
272
|
+
|
|
273
|
+
def _decode_int16(self, bytes_value: bytearray) -> int:
|
|
274
|
+
value = None
|
|
275
|
+
# Combine 2 bytes to a 16 bit integer value
|
|
276
|
+
value = bytes_value[0] * 2**8 + bytes_value[1]
|
|
277
|
+
# See if the value is negative and make the two's complement
|
|
278
|
+
if value >= 2**15:
|
|
279
|
+
value -= 2**16
|
|
280
|
+
return value
|
|
281
|
+
|
|
282
|
+
# Convert byte-array value to an integer value and apply two's complement
|
|
283
|
+
def _decode_int24(self, bytes_value: bytearray) -> int:
|
|
284
|
+
value = None
|
|
285
|
+
# Combine 3 bytes to a 24 bit integer value
|
|
286
|
+
value = bytes_value[0] * 2**16 + bytes_value[1] * 2**8 + bytes_value[2]
|
|
287
|
+
# See if the value is negative and make the two's complement
|
|
288
|
+
if value >= 2**23:
|
|
289
|
+
value -= 2**24
|
|
290
|
+
return value
|