biosignal-device-interface 0.1.0__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/__init__.py +4 -0
- biosignal_device_interface/constants/__init__.py +0 -0
- biosignal_device_interface/constants/devices/__init__.py +0 -0
- biosignal_device_interface/constants/devices/core/base_device_constants.py +51 -0
- biosignal_device_interface/constants/devices/otb/otb_muovi_constants.py +129 -0
- biosignal_device_interface/constants/devices/otb/otb_quattrocento_constants.py +59 -0
- biosignal_device_interface/constants/plots/color_palette.py +59 -0
- biosignal_device_interface/devices/__init__.py +9 -0
- biosignal_device_interface/devices/core/__init__.py +0 -0
- biosignal_device_interface/devices/core/base_device.py +413 -0
- biosignal_device_interface/devices/otb/__init__.py +9 -0
- biosignal_device_interface/devices/otb/otb_muovi.py +291 -0
- biosignal_device_interface/devices/otb/otb_quattrocento.py +235 -0
- biosignal_device_interface/gui/device_template_widgets/__init__.py +6 -0
- biosignal_device_interface/gui/device_template_widgets/all_devices_widget.py +39 -0
- biosignal_device_interface/gui/device_template_widgets/core/base_device_widget.py +121 -0
- biosignal_device_interface/gui/device_template_widgets/core/base_multiple_devices_widget.py +105 -0
- biosignal_device_interface/gui/device_template_widgets/otb/__init__.py +10 -0
- biosignal_device_interface/gui/device_template_widgets/otb/otb_devices_widget.py +32 -0
- biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_plus_widget.py +158 -0
- biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_widget.py +158 -0
- biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_light_widget.py +170 -0
- biosignal_device_interface/gui/plot_widgets/biosignal_plot_widget.py +496 -0
- biosignal_device_interface/gui/ui/devices_template_widget.ui +38 -0
- biosignal_device_interface/gui/ui/otb_muovi_plus_template_widget.ui +171 -0
- biosignal_device_interface/gui/ui/otb_muovi_template_widget.ui +171 -0
- biosignal_device_interface/gui/ui/otb_quattrocento_light_template_widget.ui +266 -0
- biosignal_device_interface/gui/ui_compiled/devices_template_widget.py +56 -0
- biosignal_device_interface/gui/ui_compiled/otb_muovi_plus_template_widget.py +153 -0
- biosignal_device_interface/gui/ui_compiled/otb_muovi_template_widget.py +153 -0
- biosignal_device_interface/gui/ui_compiled/otb_quattrocento_light_template_widget.py +217 -0
- biosignal_device_interface-0.1.0.dist-info/LICENSE +395 -0
- biosignal_device_interface-0.1.0.dist-info/METADATA +138 -0
- biosignal_device_interface-0.1.0.dist-info/RECORD +35 -0
- biosignal_device_interface-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Python Libraries
|
|
2
|
+
"""
|
|
3
|
+
Device class for real-time interfacing the Muovi device.
|
|
4
|
+
Developer: Dominik I. Braun
|
|
5
|
+
Contact: dome.braun@fau.de
|
|
6
|
+
Last Update: 2024-06-05
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from typing import TYPE_CHECKING, Union, Dict
|
|
11
|
+
from PySide6.QtNetwork import QTcpSocket, QTcpServer, QHostAddress
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
# Local Libraries
|
|
15
|
+
from biosignal_device_interface.devices.core.base_device import BaseDevice
|
|
16
|
+
from biosignal_device_interface.constants.devices.core.base_device_constants import (
|
|
17
|
+
DeviceType,
|
|
18
|
+
DeviceChannelTypes,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Constants
|
|
22
|
+
from biosignal_device_interface.constants.devices.otb_muovi_constants import (
|
|
23
|
+
MUOVI_CONVERSION_FACTOR_DICT,
|
|
24
|
+
MuoviWorkingMode,
|
|
25
|
+
MuoviDetectionMode,
|
|
26
|
+
MUOVI_WORKING_MODE_CHARACTERISTICS_DICT,
|
|
27
|
+
MUOVI_SAMPLES_PER_FRAME_DICT,
|
|
28
|
+
MUOVI_AVAILABLE_CHANNELS_DICT,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
33
|
+
from aenum import Enum
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OTBMuovi(BaseDevice):
|
|
37
|
+
"""
|
|
38
|
+
Muovi device class derived from BaseDevice class.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
parent (Union[QMainWindow, QWidget], optional):
|
|
42
|
+
Parent widget to which the device is assigned to.
|
|
43
|
+
Defaults to None.
|
|
44
|
+
|
|
45
|
+
is_muovi_plus (bool):
|
|
46
|
+
True if the device is a Muovi Plus, False if not.
|
|
47
|
+
|
|
48
|
+
The Muovi class is using a TCP/IP protocol to communicate with the device.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
parent: Union[QMainWindow, QWidget] = None,
|
|
54
|
+
is_muovi_plus: bool = False,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Initialize the Muovi device.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
parent (Union[QMainWindow, QWidget], optional): Parent widget. Defaults to None.
|
|
61
|
+
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).
|
|
62
|
+
"""
|
|
63
|
+
super().__init__(parent)
|
|
64
|
+
|
|
65
|
+
# Device Parameters
|
|
66
|
+
self._device_type: DeviceType = (
|
|
67
|
+
DeviceType.OTB_MUOVI_PLUS if is_muovi_plus else DeviceType.OTB_MUOVI
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Connection Parameters
|
|
71
|
+
self._interface: QTcpServer = None
|
|
72
|
+
self._client_socket: QTcpSocket | None = None
|
|
73
|
+
|
|
74
|
+
# Configuration Parameters
|
|
75
|
+
self._working_mode: MuoviWorkingMode = MuoviWorkingMode.NONE
|
|
76
|
+
self._detection_mode: MuoviDetectionMode = MuoviDetectionMode.NONE
|
|
77
|
+
self._configuration_command: int | None = None
|
|
78
|
+
|
|
79
|
+
def _connect_to_device(self) -> bool:
|
|
80
|
+
super()._connect_to_device()
|
|
81
|
+
|
|
82
|
+
self._interface = QTcpServer(self)
|
|
83
|
+
self._received_bytes: bytearray = bytearray()
|
|
84
|
+
|
|
85
|
+
if not self._interface.listen(
|
|
86
|
+
QHostAddress(self._connection_settings[0]), self._connection_settings[1]
|
|
87
|
+
):
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
self._interface.newConnection.connect(self._make_request)
|
|
91
|
+
|
|
92
|
+
self._connection_timeout_timer.start()
|
|
93
|
+
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def _make_request(self) -> bool:
|
|
97
|
+
super()._make_request()
|
|
98
|
+
self._client_socket = self._interface.nextPendingConnection()
|
|
99
|
+
|
|
100
|
+
if self._client_socket:
|
|
101
|
+
|
|
102
|
+
self._client_socket.readyRead.connect(self._read_data)
|
|
103
|
+
|
|
104
|
+
if not self._is_connected:
|
|
105
|
+
self._is_connected = True
|
|
106
|
+
self.connect_toggled.emit(self._is_connected)
|
|
107
|
+
self._connection_timeout_timer.stop()
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
elif not self._is_configured:
|
|
111
|
+
self._is_configured = True
|
|
112
|
+
self.configure_toggled.emit(self._is_configured)
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def _disconnect_from_device(self) -> bool:
|
|
116
|
+
super()._disconnect_from_device()
|
|
117
|
+
|
|
118
|
+
if self._client_socket is not None:
|
|
119
|
+
self._client_socket.readyRead.disconnect(self._read_data)
|
|
120
|
+
self._client_socket.disconnectFromHost()
|
|
121
|
+
self._client_socket.close()
|
|
122
|
+
|
|
123
|
+
if self._interface is not None:
|
|
124
|
+
self._interface.close()
|
|
125
|
+
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
def configure_device(
|
|
129
|
+
self, settings: Dict[str, Union[Enum, Dict[str, Enum]]] # type: ignore
|
|
130
|
+
) -> None:
|
|
131
|
+
super().configure_device(settings)
|
|
132
|
+
|
|
133
|
+
if not self._is_connected or self._client_socket is None:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# Check if detection mode is valid for working mode (Case EEG -> MONOPOLAR_GAIN_4 => MONOPOLAR_GAIN_8)
|
|
137
|
+
if self._working_mode == MuoviWorkingMode.EEG:
|
|
138
|
+
if self._detection_mode == MuoviDetectionMode.MONOPOLAR_GAIN_4:
|
|
139
|
+
self._detection_mode = MuoviDetectionMode.MONOPOLAR_GAIN_8
|
|
140
|
+
|
|
141
|
+
self._conversion_factor_biosignal = MUOVI_CONVERSION_FACTOR_DICT[
|
|
142
|
+
self._detection_mode
|
|
143
|
+
]
|
|
144
|
+
self._conversion_factor_auxiliary = self._conversion_factor_biosignal
|
|
145
|
+
|
|
146
|
+
# Set configuration parameters for data transfer
|
|
147
|
+
working_mode_characteristics = MUOVI_WORKING_MODE_CHARACTERISTICS_DICT[
|
|
148
|
+
self._working_mode
|
|
149
|
+
]
|
|
150
|
+
self._sampling_frequency = working_mode_characteristics["sampling_frequency"]
|
|
151
|
+
self._bytes_per_sample = working_mode_characteristics["bytes_per_sample"]
|
|
152
|
+
self._samples_per_frame = MUOVI_SAMPLES_PER_FRAME_DICT[self._device_type][
|
|
153
|
+
self._working_mode
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
self._number_of_channels = MUOVI_AVAILABLE_CHANNELS_DICT[self._device_type][
|
|
157
|
+
DeviceChannelTypes.ALL
|
|
158
|
+
]
|
|
159
|
+
self._number_of_biosignal_channels = MUOVI_AVAILABLE_CHANNELS_DICT[
|
|
160
|
+
self._device_type
|
|
161
|
+
][DeviceChannelTypes.BIOSIGNAL]
|
|
162
|
+
self._biosignal_channel_indices = np.arange(self._number_of_biosignal_channels)
|
|
163
|
+
|
|
164
|
+
self._number_of_auxiliary_channels = MUOVI_AVAILABLE_CHANNELS_DICT[
|
|
165
|
+
self._device_type
|
|
166
|
+
][DeviceChannelTypes.AUXILIARY]
|
|
167
|
+
self._auxiliary_channel_indices = np.arange(
|
|
168
|
+
self._number_of_biosignal_channels,
|
|
169
|
+
self._number_of_biosignal_channels + self._number_of_auxiliary_channels,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self._buffer_size = (
|
|
173
|
+
self._number_of_channels * self._samples_per_frame * self._bytes_per_sample
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self._received_bytes = bytearray()
|
|
177
|
+
|
|
178
|
+
self._configure_command()
|
|
179
|
+
self._send_configuration_to_device()
|
|
180
|
+
|
|
181
|
+
def _send_configuration_to_device(self) -> None:
|
|
182
|
+
configuration_bytes = int(self._configuration_command).to_bytes(
|
|
183
|
+
1, byteorder="big"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
success = self._client_socket.write(configuration_bytes)
|
|
187
|
+
|
|
188
|
+
if success == -1:
|
|
189
|
+
self._disconnect_from_device()
|
|
190
|
+
|
|
191
|
+
def _configure_command(self) -> None:
|
|
192
|
+
self._configuration_command = self._working_mode.value << 2
|
|
193
|
+
self._configuration_command += self._detection_mode.value
|
|
194
|
+
|
|
195
|
+
def _start_streaming(self) -> None:
|
|
196
|
+
super()._start_streaming()
|
|
197
|
+
|
|
198
|
+
if self._configuration_command is None:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
self._configuration_command += 1
|
|
202
|
+
self._send_configuration_to_device()
|
|
203
|
+
|
|
204
|
+
def _stop_streaming(self) -> None:
|
|
205
|
+
super()._stop_streaming()
|
|
206
|
+
|
|
207
|
+
if self._configuration_command is None:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
self._configuration_command -= 1
|
|
211
|
+
self._send_configuration_to_device()
|
|
212
|
+
|
|
213
|
+
def clear_socket(self) -> None:
|
|
214
|
+
if self._client_socket is not None:
|
|
215
|
+
self._client_socket.readAll()
|
|
216
|
+
|
|
217
|
+
def _read_data(self) -> None:
|
|
218
|
+
super()._read_data()
|
|
219
|
+
|
|
220
|
+
if not self._is_streaming:
|
|
221
|
+
self.clear_socket()
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
while self._client_socket.bytesAvailable() > self._buffer_size:
|
|
225
|
+
packet = self._client_socket.read(self._buffer_size)
|
|
226
|
+
if not packet:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
self._received_bytes.extend(packet)
|
|
230
|
+
|
|
231
|
+
while len(self._received_bytes) >= self._buffer_size:
|
|
232
|
+
data_to_process = self._received_bytes[: self._buffer_size]
|
|
233
|
+
self._process_data(data_to_process)
|
|
234
|
+
self._received_bytes = self._received_bytes[self._buffer_size :]
|
|
235
|
+
|
|
236
|
+
def _process_data(self, data: bytearray) -> None:
|
|
237
|
+
super()._process_data(data)
|
|
238
|
+
|
|
239
|
+
decoded_data = self._bytes_to_integers(data)
|
|
240
|
+
|
|
241
|
+
processed_data = decoded_data.reshape(
|
|
242
|
+
self._number_of_channels, -1, order="F"
|
|
243
|
+
).astype(np.float32)
|
|
244
|
+
|
|
245
|
+
# Emit the data
|
|
246
|
+
self.data_available.emit(processed_data)
|
|
247
|
+
self.biosignal_data_available.emit(self._extract_biosignal_data(processed_data))
|
|
248
|
+
self.auxiliary_data_available.emit(self._extract_auxiliary_data(processed_data))
|
|
249
|
+
|
|
250
|
+
# Convert channels from bytes to integers
|
|
251
|
+
def _bytes_to_integers(
|
|
252
|
+
self,
|
|
253
|
+
data: bytearray,
|
|
254
|
+
) -> np.ndarray:
|
|
255
|
+
channel_values = []
|
|
256
|
+
# Separate channels from byte-string. One channel has
|
|
257
|
+
# "bytes_in_sample" many bytes in it.
|
|
258
|
+
for channel_index in range(len(data) // 2):
|
|
259
|
+
channel_start = channel_index * self._bytes_per_sample
|
|
260
|
+
channel_end = (channel_index + 1) * self._bytes_per_sample
|
|
261
|
+
channel = data[channel_start:channel_end]
|
|
262
|
+
|
|
263
|
+
# Convert channel's byte value to integer
|
|
264
|
+
match self._working_mode:
|
|
265
|
+
case MuoviWorkingMode.EMG:
|
|
266
|
+
value = self._decode_int16(channel)
|
|
267
|
+
case MuoviWorkingMode.EEG:
|
|
268
|
+
value = self._decode_int24(channel)
|
|
269
|
+
|
|
270
|
+
channel_values.append(value)
|
|
271
|
+
|
|
272
|
+
return np.array(channel_values)
|
|
273
|
+
|
|
274
|
+
def _decode_int16(self, bytes_value: bytearray) -> int:
|
|
275
|
+
value = None
|
|
276
|
+
# Combine 2 bytes to a 16 bit integer value
|
|
277
|
+
value = bytes_value[0] * 2**8 + bytes_value[1]
|
|
278
|
+
# See if the value is negative and make the two's complement
|
|
279
|
+
if value >= 2**15:
|
|
280
|
+
value -= 2**16
|
|
281
|
+
return value
|
|
282
|
+
|
|
283
|
+
# Convert byte-array value to an integer value and apply two's complement
|
|
284
|
+
def _decode_int24(self, bytes_value: bytearray) -> int:
|
|
285
|
+
value = None
|
|
286
|
+
# Combine 3 bytes to a 24 bit integer value
|
|
287
|
+
value = bytes_value[0] * 2**16 + bytes_value[1] * 2**8 + bytes_value[2]
|
|
288
|
+
# See if the value is negative and make the two's complement
|
|
289
|
+
if value >= 2**23:
|
|
290
|
+
value -= 2**24
|
|
291
|
+
return value
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
1) Quattrocento Light class for real-time interface to
|
|
3
|
+
Quattrocento using OT Biolab Light.
|
|
4
|
+
|
|
5
|
+
2) Quattrocento class for direct real-time interface to
|
|
6
|
+
Quattrocento without using OT Biolab Light.
|
|
7
|
+
|
|
8
|
+
Developer: Dominik I. Braun
|
|
9
|
+
Contact: dome.braun@fau.de
|
|
10
|
+
Last Update: 2023-06-05
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Python Libraries
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from typing import TYPE_CHECKING, Union, Dict
|
|
16
|
+
from PySide6.QtNetwork import QTcpSocket, QHostAddress
|
|
17
|
+
from PySide6.QtCore import QIODevice
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from biosignal_device_interface.devices.core.base_device import BaseDevice
|
|
22
|
+
from biosignal_device_interface.constants.devices.core.base_device_constants import (
|
|
23
|
+
DeviceType,
|
|
24
|
+
)
|
|
25
|
+
from biosignal_device_interface.constants.devices.otb_quattrocento_constants import (
|
|
26
|
+
COMMAND_START_STREAMING,
|
|
27
|
+
COMMAND_STOP_STREAMING,
|
|
28
|
+
CONNECTION_RESPONSE,
|
|
29
|
+
QUATTROCENTO_LIGHT_STREAMING_FREQUENCY_DICT,
|
|
30
|
+
QUATTROCENTO_SAMPLING_FREQUENCY_DICT,
|
|
31
|
+
QuattrocentoLightSamplingFrequency,
|
|
32
|
+
QuattrocentoLightStreamingFrequency,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
# Python Libraries
|
|
38
|
+
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
39
|
+
from aenum import Enum
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OTBQuattrocentoLight(BaseDevice):
|
|
43
|
+
"""
|
|
44
|
+
QuattrocentoLight device class derived from BaseDevice class.
|
|
45
|
+
The QuattrocentoLight is using a TCP/IP protocol to communicate with the device.
|
|
46
|
+
|
|
47
|
+
This class directly interfaces with the OT Biolab Light software from
|
|
48
|
+
OT Bioelettronica. The configured settings of the device have to
|
|
49
|
+
match the settings from the OT Biolab Light software!
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
parent: Union[QMainWindow, QWidget] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__(parent)
|
|
57
|
+
|
|
58
|
+
# Device Parameters
|
|
59
|
+
self._device_type: DeviceType = DeviceType.OTB_QUATTROCENTO_LIGHT
|
|
60
|
+
|
|
61
|
+
# Device Information
|
|
62
|
+
self._number_of_channels: int = 408 # Fix value
|
|
63
|
+
self._auxiliary_channel_start_index: int = 384 # Fix value
|
|
64
|
+
self._number_of_auxiliary_channels: int = 16 # Fix value
|
|
65
|
+
self._conversion_factor_biosignal: float = 5 / (2**16) / 150 * 1000 # in mV
|
|
66
|
+
self._conversion_factor_auxiliary: float = 5 / (2**16) / 0.5 # in mV
|
|
67
|
+
self._bytes_per_sample: int = 2 # Fix value
|
|
68
|
+
# Quattrocento unique parameters
|
|
69
|
+
self._streaming_frequency: int | None = None
|
|
70
|
+
|
|
71
|
+
# Connection Parameters
|
|
72
|
+
self._interface: QTcpSocket = QTcpSocket()
|
|
73
|
+
|
|
74
|
+
# Configuration Parameters
|
|
75
|
+
self._grids: list[int] | None = None
|
|
76
|
+
self._grid_size: int = 64 # TODO: This is only valid for the big electrodes
|
|
77
|
+
self._streaming_frequency_mode: QuattrocentoLightStreamingFrequency | None = (
|
|
78
|
+
None
|
|
79
|
+
)
|
|
80
|
+
self._sampling_frequency_mode: QuattrocentoLightSamplingFrequency | None = None
|
|
81
|
+
|
|
82
|
+
def _connect_to_device(self) -> bool:
|
|
83
|
+
super()._connect_to_device()
|
|
84
|
+
|
|
85
|
+
self._received_bytes: bytearray = bytearray()
|
|
86
|
+
return self._make_request()
|
|
87
|
+
|
|
88
|
+
def _make_request(self) -> bool:
|
|
89
|
+
super()._make_request()
|
|
90
|
+
# Signal self.connect_toggled is emitted in _read_data
|
|
91
|
+
self._interface.connectToHost(
|
|
92
|
+
QHostAddress(self._connection_settings[0]),
|
|
93
|
+
self._connection_settings[1],
|
|
94
|
+
QIODevice.ReadWrite,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not self._interface.waitForConnected(1000):
|
|
98
|
+
self._disconnect_from_device()
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
self._interface.readyRead.connect(self._read_data)
|
|
102
|
+
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def _disconnect_from_device(self) -> None:
|
|
106
|
+
super()._disconnect_from_device()
|
|
107
|
+
|
|
108
|
+
self._interface.disconnectFromHost()
|
|
109
|
+
self._interface.readyRead.disconnect(self._read_data)
|
|
110
|
+
self._interface.close()
|
|
111
|
+
|
|
112
|
+
def configure_device(
|
|
113
|
+
self, settings: Dict[str, Union[Enum, Dict[str, Enum]]] # type: ignore
|
|
114
|
+
) -> None:
|
|
115
|
+
super().configure_device(settings)
|
|
116
|
+
|
|
117
|
+
# Configure the device
|
|
118
|
+
self._number_of_biosignal_channels = len(self._grids) * self._grid_size
|
|
119
|
+
self._biosignal_channel_indices = np.array(
|
|
120
|
+
[
|
|
121
|
+
i * self._grid_size + j
|
|
122
|
+
for i in self._grids
|
|
123
|
+
for j in range(self._grid_size)
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self._auxiliary_channel_indices = np.array(
|
|
128
|
+
[
|
|
129
|
+
i + self._auxiliary_channel_start_index
|
|
130
|
+
for i in range(self._number_of_auxiliary_channels)
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self._streaming_frequency = QUATTROCENTO_LIGHT_STREAMING_FREQUENCY_DICT[
|
|
135
|
+
self._streaming_frequency_mode
|
|
136
|
+
]
|
|
137
|
+
self._sampling_frequency = QUATTROCENTO_SAMPLING_FREQUENCY_DICT[
|
|
138
|
+
self._sampling_frequency_mode
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
self._samples_per_frame = self._sampling_frequency // self._streaming_frequency
|
|
142
|
+
|
|
143
|
+
self._buffer_size = (
|
|
144
|
+
self._bytes_per_sample * self._number_of_channels * self._samples_per_frame
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
self._is_configured = True
|
|
148
|
+
self.configure_toggled.emit(True)
|
|
149
|
+
|
|
150
|
+
def _start_streaming(self) -> None:
|
|
151
|
+
super()._start_streaming()
|
|
152
|
+
|
|
153
|
+
self._interface.write(COMMAND_START_STREAMING)
|
|
154
|
+
|
|
155
|
+
def _stop_streaming(self) -> None:
|
|
156
|
+
super()._stop_streaming()
|
|
157
|
+
|
|
158
|
+
self._interface.write(COMMAND_STOP_STREAMING)
|
|
159
|
+
self._interface.waitForBytesWritten(1000)
|
|
160
|
+
|
|
161
|
+
def clear_socket(self) -> None:
|
|
162
|
+
super().clear_socket()
|
|
163
|
+
|
|
164
|
+
self._interface.readAll()
|
|
165
|
+
|
|
166
|
+
def _read_data(self) -> None:
|
|
167
|
+
super()._read_data()
|
|
168
|
+
|
|
169
|
+
# Wait for connection response
|
|
170
|
+
if not self._is_connected:
|
|
171
|
+
if self._interface.bytesAvailable() == len(CONNECTION_RESPONSE):
|
|
172
|
+
if self._interface.readAll() == CONNECTION_RESPONSE:
|
|
173
|
+
|
|
174
|
+
self._is_connected = True
|
|
175
|
+
self.connect_toggled.emit(True)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
if not self._is_streaming:
|
|
179
|
+
self.clear_socket()
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
while self._interface.bytesAvailable() > self._buffer_size:
|
|
183
|
+
packet = self._interface.read(self._buffer_size)
|
|
184
|
+
if not packet:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
self._received_bytes.extend(packet)
|
|
188
|
+
|
|
189
|
+
while len(self._received_bytes) >= self._buffer_size:
|
|
190
|
+
data_to_process = self._received_bytes[: self._buffer_size]
|
|
191
|
+
self._process_data(data_to_process)
|
|
192
|
+
self._received_bytes = self._received_bytes[self._buffer_size :]
|
|
193
|
+
|
|
194
|
+
def _process_data(self, data: bytearray) -> None:
|
|
195
|
+
super()._process_data(data)
|
|
196
|
+
|
|
197
|
+
# Decode the data
|
|
198
|
+
decoded_data = np.frombuffer(data, dtype=np.int16)
|
|
199
|
+
|
|
200
|
+
# Reshape it to the correct format
|
|
201
|
+
processed_data = decoded_data.reshape(
|
|
202
|
+
self._number_of_channels, -1, order="F"
|
|
203
|
+
).astype(np.float32)
|
|
204
|
+
|
|
205
|
+
# Emit the data
|
|
206
|
+
self.data_available.emit(processed_data)
|
|
207
|
+
self.biosignal_data_available.emit(self._extract_biosignal_data(processed_data))
|
|
208
|
+
self.auxiliary_data_available.emit(self._extract_auxiliary_data(processed_data))
|
|
209
|
+
|
|
210
|
+
def get_device_information(self) -> Dict[str, Enum | int | float | str]:
|
|
211
|
+
return super().get_device_information()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class OTBQuattrocento(BaseDevice):
|
|
215
|
+
"""
|
|
216
|
+
Quattrocento device class derived from BaseDevice class.
|
|
217
|
+
|
|
218
|
+
The Quattrocento class is using a TCP/IP protocol to communicate with the device.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
parent: Union[QMainWindow, QWidget] = None,
|
|
224
|
+
) -> None:
|
|
225
|
+
super().__init__(parent)
|
|
226
|
+
|
|
227
|
+
# Device Parameters
|
|
228
|
+
self._device_type: DeviceType = DeviceType.OTB_QUATTROCENTO
|
|
229
|
+
|
|
230
|
+
# Device Information
|
|
231
|
+
self._conversion_factor_biosignal: float = 5 / (2**16) / 150 * 1000
|
|
232
|
+
self._conversion_factor_auxiliary: float = 5 / (2**16) / 0.5
|
|
233
|
+
|
|
234
|
+
# Connection Parameters
|
|
235
|
+
self._interface: QTcpSocket = QTcpSocket()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template for the QWidget that enables the user to interact with all available devices.
|
|
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, Dict
|
|
10
|
+
|
|
11
|
+
from biosignal_device_interface.gui.device_template_widgets.core.base_multiple_devices_widget import (
|
|
12
|
+
BaseMultipleDevicesWidget,
|
|
13
|
+
)
|
|
14
|
+
from biosignal_device_interface.constants.devices.core.base_device_constants import (
|
|
15
|
+
DeviceType,
|
|
16
|
+
)
|
|
17
|
+
from biosignal_device_interface.gui.device_template_widgets import (
|
|
18
|
+
MuoviPlusWidget,
|
|
19
|
+
MuoviWidget,
|
|
20
|
+
QuattrocentoLightWidget,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from PySide6.QtWidgets import QWidget, QMainWindow
|
|
25
|
+
from biosignal_device_interface.gui.device_template_widgets.core.base_device_widget import (
|
|
26
|
+
BaseDeviceWidget,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AllDevicesWidget(BaseMultipleDevicesWidget):
|
|
31
|
+
def __init__(self, parent: QWidget | QMainWindow | None = None):
|
|
32
|
+
super().__init__(parent)
|
|
33
|
+
|
|
34
|
+
self._device_selection: Dict[DeviceType, BaseDeviceWidget] = {
|
|
35
|
+
DeviceType.OTB_QUATTROCENTO_LIGHT: QuattrocentoLightWidget(self),
|
|
36
|
+
DeviceType.OTB_MUOVI: MuoviWidget(self),
|
|
37
|
+
DeviceType.OTB_MUOVI_PLUS: MuoviPlusWidget(self),
|
|
38
|
+
}
|
|
39
|
+
self._set_devices(self._device_selection)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Device class for real-time interfaces to hardware devices.
|
|
3
|
+
Developer: Dominik I. Braun
|
|
4
|
+
Contact: dome.braun@fau.de
|
|
5
|
+
Last Update: 2024-06-05
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Python Libraries
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from typing import TYPE_CHECKING, Union, Dict
|
|
11
|
+
from abc import abstractmethod
|
|
12
|
+
from PySide6.QtWidgets import QWidget, QMainWindow
|
|
13
|
+
from PySide6.QtGui import QCloseEvent
|
|
14
|
+
from PySide6.QtCore import Signal
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
# Import Devices
|
|
18
|
+
from biosignal_device_interface.devices.core.base_device import BaseDevice
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from enum import Enum
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseDeviceWidget(QWidget):
|
|
25
|
+
# Signals
|
|
26
|
+
data_arrived: Signal = Signal(np.ndarray)
|
|
27
|
+
biosignal_data_arrived: Signal = Signal(np.ndarray)
|
|
28
|
+
auxiliary_data_arrived: Signal = Signal(np.ndarray)
|
|
29
|
+
connect_toggled: Signal = Signal(bool)
|
|
30
|
+
configure_toggled: Signal = Signal(bool)
|
|
31
|
+
stream_toggled: Signal = Signal(bool)
|
|
32
|
+
|
|
33
|
+
def __init__(self, parent: QWidget | QMainWindow | None = None):
|
|
34
|
+
super().__init__(parent)
|
|
35
|
+
|
|
36
|
+
self.parent_widget: QWidget | QMainWindow | None = parent
|
|
37
|
+
|
|
38
|
+
# Device Setup
|
|
39
|
+
self.device: BaseDevice | None = None
|
|
40
|
+
self._device_params: Dict[str, Union[str, int, float]] = {}
|
|
41
|
+
|
|
42
|
+
# GUI setup
|
|
43
|
+
self.ui: object = None
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def _toggle_connection(self) -> None:
|
|
47
|
+
""" """
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def _connection_toggled(self, is_connected: bool) -> None:
|
|
52
|
+
""" """
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def _toggle_configuration(self) -> None:
|
|
57
|
+
""" """
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def _configuration_toggled(self, is_configured: bool) -> None:
|
|
62
|
+
""" """
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def _toggle_configuration_group_boxes(self) -> None:
|
|
67
|
+
""" """
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def _toggle_stream(self) -> None:
|
|
72
|
+
""" """
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def _stream_toggled(self, is_streaming: bool) -> None:
|
|
77
|
+
""" """
|
|
78
|
+
self.stream_toggled.emit(is_streaming)
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def _initialize_device_params(self) -> None:
|
|
82
|
+
""" """
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def _initialize_ui(self) -> None:
|
|
87
|
+
""" """
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
def _set_device(self, device: BaseDevice) -> None:
|
|
91
|
+
""" """
|
|
92
|
+
# Device Setup
|
|
93
|
+
self.device: BaseDevice = device
|
|
94
|
+
self._initialize_device_params()
|
|
95
|
+
self._set_signals()
|
|
96
|
+
self._initialize_ui()
|
|
97
|
+
|
|
98
|
+
def _set_signals(self) -> None:
|
|
99
|
+
""" """
|
|
100
|
+
self.device.data_available.connect(self.data_arrived.emit)
|
|
101
|
+
self.device.biosignal_data_available.connect(self.biosignal_data_arrived.emit)
|
|
102
|
+
self.device.auxiliary_data_available.connect(self.auxiliary_data_arrived.emit)
|
|
103
|
+
|
|
104
|
+
def get_device_information(self) -> Dict[str, Enum | int | float | str]:
|
|
105
|
+
"""
|
|
106
|
+
Gets the current configuration of the device.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dict[str, Enum | int | float | str]:
|
|
110
|
+
Dictionary that holds information about the
|
|
111
|
+
current device configuration and status.
|
|
112
|
+
"""
|
|
113
|
+
return self.device.get_device_information()
|
|
114
|
+
|
|
115
|
+
def disconnect_device(self) -> None:
|
|
116
|
+
""" """
|
|
117
|
+
if self.device._is_connected or self.device._is_streaming:
|
|
118
|
+
self.device.toggle_connection()
|
|
119
|
+
|
|
120
|
+
def closeEvent(self, event: QCloseEvent) -> None:
|
|
121
|
+
self.disconnect_device()
|