biosignal-device-interface 0.1.1a0__py3-none-any.whl → 0.1.4a1__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 +0 -4
- biosignal_device_interface/constants/devices/__init__.py +3 -0
- biosignal_device_interface/constants/devices/core/base_device_constants.py +10 -0
- biosignal_device_interface/constants/devices/otb/otb_constants.py +0 -0
- biosignal_device_interface/constants/devices/otb/otb_muovi_constants.py +3 -3
- biosignal_device_interface/constants/devices/otb/otb_quattrocento_constants.py +289 -35
- biosignal_device_interface/constants/devices/otb/otb_quattrocento_light_constants.py +59 -0
- biosignal_device_interface/constants/devices/otb/otb_syncstation_constants.py +233 -0
- biosignal_device_interface/devices/__init__.py +11 -3
- biosignal_device_interface/devices/core/base_device.py +8 -9
- biosignal_device_interface/devices/otb/__init__.py +27 -7
- biosignal_device_interface/devices/otb/otb_muovi.py +9 -10
- biosignal_device_interface/devices/otb/otb_quattrocento.py +215 -118
- biosignal_device_interface/devices/otb/otb_quattrocento_light.py +210 -0
- biosignal_device_interface/devices/otb/otb_syncstation.py +407 -0
- biosignal_device_interface/gui/device_template_widgets/__init__.py +0 -6
- biosignal_device_interface/gui/device_template_widgets/all_devices_widget.py +19 -7
- biosignal_device_interface/gui/device_template_widgets/core/base_device_widget.py +17 -8
- biosignal_device_interface/gui/device_template_widgets/core/base_multiple_devices_widget.py +7 -4
- biosignal_device_interface/gui/device_template_widgets/otb/__init__.py +0 -10
- biosignal_device_interface/gui/device_template_widgets/otb/otb_devices_widget.py +20 -8
- biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_plus_widget.py +12 -12
- biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_widget.py +12 -12
- biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_light_widget.py +61 -57
- biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_widget.py +260 -0
- biosignal_device_interface/gui/device_template_widgets/otb/otb_syncstation_widget.py +262 -0
- biosignal_device_interface/gui/plot_widgets/biosignal_plot_widget.py +9 -4
- biosignal_device_interface/gui/ui/otb_quattrocento_template_widget.ui +415 -0
- biosignal_device_interface/gui/ui/otb_syncstation_template_widget.ui +732 -0
- biosignal_device_interface/gui/ui_compiled/otb_quattrocento_template_widget.py +318 -0
- biosignal_device_interface/gui/ui_compiled/otb_syncstation_template_widget.py +495 -0
- biosignal_device_interface-0.1.4a1.dist-info/LICENSE +675 -0
- {biosignal_device_interface-0.1.1a0.dist-info → biosignal_device_interface-0.1.4a1.dist-info}/METADATA +7 -17
- biosignal_device_interface-0.1.4a1.dist-info/RECORD +46 -0
- biosignal_device_interface-0.1.1a0.dist-info/LICENSE +0 -395
- biosignal_device_interface-0.1.1a0.dist-info/RECORD +0 -35
- {biosignal_device_interface-0.1.1a0.dist-info → biosignal_device_interface-0.1.4a1.dist-info}/WHEEL +0 -0
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
2) Quattrocento class for direct real-time interface to
|
|
6
|
-
Quattrocento without using OT Biolab Light.
|
|
2
|
+
Quattrocento class for real-time direct interface to
|
|
3
|
+
Quattrocento.
|
|
7
4
|
|
|
8
5
|
Developer: Dominik I. Braun
|
|
9
6
|
Contact: dome.braun@fau.de
|
|
10
|
-
Last Update:
|
|
7
|
+
Last Update: 2025-01-14
|
|
11
8
|
"""
|
|
12
9
|
|
|
13
10
|
# Python Libraries
|
|
@@ -23,61 +20,75 @@ from biosignal_device_interface.constants.devices.core.base_device_constants imp
|
|
|
23
20
|
DeviceType,
|
|
24
21
|
)
|
|
25
22
|
from biosignal_device_interface.constants.devices.otb.otb_quattrocento_constants import (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
QUATTROCENTO_AUXILIARY_CHANNELS,
|
|
24
|
+
QUATTROCENTO_BYTES_PER_SAMPLE,
|
|
25
|
+
QUATTROCENTO_SAMPLES_PER_FRAME,
|
|
26
|
+
QUATTROCENTO_SUPPLEMENTARY_CHANNELS,
|
|
27
|
+
QuattrocentoAcqSettByte,
|
|
28
|
+
QuattrocentoINXConf2Byte,
|
|
29
|
+
QuattrocentoRecordingMode,
|
|
30
|
+
QuattrocentoSamplingFrequencyMode,
|
|
31
|
+
QuattrocentoNumberOfChannelsMode,
|
|
32
|
+
QuattrocentoLowPassFilterMode,
|
|
33
|
+
QuattrocentoHighPassFilterMode,
|
|
34
|
+
QuattrocentoDetectionMode,
|
|
33
35
|
)
|
|
34
36
|
|
|
35
|
-
|
|
36
37
|
if TYPE_CHECKING:
|
|
37
38
|
# Python Libraries
|
|
38
39
|
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
39
40
|
from aenum import Enum
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
class
|
|
43
|
+
class OTBQuattrocento(BaseDevice):
|
|
43
44
|
"""
|
|
44
|
-
|
|
45
|
-
The
|
|
45
|
+
Quattrocento device class derived from BaseDevice class.
|
|
46
|
+
The Quattrocento is using a TCP/IP protocol to communicate with the device.
|
|
46
47
|
|
|
47
|
-
This class directly interfaces with the
|
|
48
|
-
OT Bioelettronica.
|
|
49
|
-
match the settings from the OT Biolab Light software!
|
|
48
|
+
This class directly interfaces with the Quattrocento from
|
|
49
|
+
OT Bioelettronica.
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
-
def __init__(
|
|
53
|
-
self,
|
|
54
|
-
parent: Union[QMainWindow, QWidget] = None,
|
|
55
|
-
) -> None:
|
|
52
|
+
def __init__(self, parent: Union[QMainWindow, QWidget] = None):
|
|
56
53
|
super().__init__(parent)
|
|
57
54
|
|
|
58
|
-
# Device
|
|
59
|
-
self._device_type: DeviceType = DeviceType.
|
|
55
|
+
# Device Paramters
|
|
56
|
+
self._device_type: DeviceType = DeviceType.OTB_QUATTROCENTO
|
|
60
57
|
|
|
61
58
|
# 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
59
|
self._conversion_factor_biosignal: float = 5 / (2**16) / 150 * 1000 # in mV
|
|
66
60
|
self._conversion_factor_auxiliary: float = 5 / (2**16) / 0.5 # in mV
|
|
67
|
-
self.
|
|
68
|
-
# Quattrocento unique parameters
|
|
69
|
-
self._streaming_frequency: int | None = None
|
|
61
|
+
self._number_of_streamed_channels: int = None
|
|
70
62
|
|
|
71
63
|
# Connection Parameters
|
|
72
64
|
self._interface: QTcpSocket = QTcpSocket()
|
|
73
65
|
|
|
74
|
-
# Configuration
|
|
75
|
-
self.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
# Configuration parameters
|
|
67
|
+
self._acq_sett_configuration: QuattrocentoAcqSettByte = (
|
|
68
|
+
QuattrocentoAcqSettByte()
|
|
69
|
+
)
|
|
70
|
+
self._input_top_left_configuration: QuattrocentoINXConf2Byte = (
|
|
71
|
+
QuattrocentoINXConf2Byte()
|
|
72
|
+
)
|
|
73
|
+
self._input_top_right_configuration: QuattrocentoINXConf2Byte = (
|
|
74
|
+
QuattrocentoINXConf2Byte()
|
|
75
|
+
)
|
|
76
|
+
self._multiple_input_one_configuration: QuattrocentoINXConf2Byte = (
|
|
77
|
+
QuattrocentoINXConf2Byte()
|
|
79
78
|
)
|
|
80
|
-
self.
|
|
79
|
+
self._multiple_input_two_configuration: QuattrocentoINXConf2Byte = (
|
|
80
|
+
QuattrocentoINXConf2Byte()
|
|
81
|
+
)
|
|
82
|
+
self._multiple_input_three_configuration: QuattrocentoINXConf2Byte = (
|
|
83
|
+
QuattrocentoINXConf2Byte()
|
|
84
|
+
)
|
|
85
|
+
self._multiple_input_four_configuration: QuattrocentoINXConf2Byte = (
|
|
86
|
+
QuattrocentoINXConf2Byte()
|
|
87
|
+
)
|
|
88
|
+
self._grid_size: int = 64 # TODO: This is only valid for the big electrodes
|
|
89
|
+
self._grids: list[int] | None = None
|
|
90
|
+
|
|
91
|
+
self._configuration_command: bytearray = bytearray(40)
|
|
81
92
|
|
|
82
93
|
def _connect_to_device(self) -> bool:
|
|
83
94
|
super()._connect_to_device()
|
|
@@ -87,11 +98,9 @@ class OTBQuattrocentoLight(BaseDevice):
|
|
|
87
98
|
|
|
88
99
|
def _make_request(self) -> bool:
|
|
89
100
|
super()._make_request()
|
|
90
|
-
|
|
101
|
+
|
|
91
102
|
self._interface.connectToHost(
|
|
92
|
-
QHostAddress(self._connection_settings[0]),
|
|
93
|
-
self._connection_settings[1],
|
|
94
|
-
QIODevice.ReadWrite,
|
|
103
|
+
QHostAddress(self._connection_settings[0]), self._connection_settings[1]
|
|
95
104
|
)
|
|
96
105
|
|
|
97
106
|
if not self._interface.waitForConnected(1000):
|
|
@@ -100,6 +109,9 @@ class OTBQuattrocentoLight(BaseDevice):
|
|
|
100
109
|
|
|
101
110
|
self._interface.readyRead.connect(self._read_data)
|
|
102
111
|
|
|
112
|
+
self.is_connected = True
|
|
113
|
+
self.connect_toggled.emit(self.is_connected)
|
|
114
|
+
|
|
103
115
|
return True
|
|
104
116
|
|
|
105
117
|
def _disconnect_from_device(self) -> None:
|
|
@@ -110,11 +122,75 @@ class OTBQuattrocentoLight(BaseDevice):
|
|
|
110
122
|
self._interface.close()
|
|
111
123
|
|
|
112
124
|
def configure_device(
|
|
113
|
-
self,
|
|
114
|
-
)
|
|
115
|
-
super().configure_device(
|
|
125
|
+
self, params: Dict[str, Union[Enum, Dict[str, Enum]]] # type: ignore
|
|
126
|
+
):
|
|
127
|
+
super().configure_device(params)
|
|
128
|
+
|
|
129
|
+
self._sampling_frequency = self._acq_sett_configuration.get_sampling_frequency()
|
|
130
|
+
self._number_of_streamed_channels = (
|
|
131
|
+
self._acq_sett_configuration.get_number_of_channels()
|
|
132
|
+
)
|
|
116
133
|
|
|
134
|
+
self._configuration_command = bytearray(40)
|
|
117
135
|
# Configure the device
|
|
136
|
+
# Byte 1: ACQ_SETT
|
|
137
|
+
self._configuration_command[0] = int(self._acq_sett_configuration)
|
|
138
|
+
|
|
139
|
+
# Byte 2: Configure AN_OUT_IN_SEL
|
|
140
|
+
self._configuration_command[1] = 0 # TODO:
|
|
141
|
+
|
|
142
|
+
# Byte 3: Configure AN_OUT_CH_SEL
|
|
143
|
+
self._configuration_command[2] = 0 # TODO:
|
|
144
|
+
|
|
145
|
+
# Byte 4-15: Configure IN1-4 -> TODO: change that to individual configuration
|
|
146
|
+
for i in range(4):
|
|
147
|
+
config = int(self._input_top_left_configuration)
|
|
148
|
+
self._configuration_command[3 + i * 3] = (config >> 16) & 0xFF
|
|
149
|
+
self._configuration_command[4 + i * 3] = (config >> 8) & 0xFF
|
|
150
|
+
self._configuration_command[5 + i * 3] = config & 0xFF
|
|
151
|
+
|
|
152
|
+
# Byte 16-27: Configure IN5-8 -> TODO: change that to individual configuration
|
|
153
|
+
for i in range(4):
|
|
154
|
+
config = int(self._input_top_right_configuration)
|
|
155
|
+
self._configuration_command[15 + i * 3] = (config >> 16) & 0xFF
|
|
156
|
+
self._configuration_command[16 + i * 3] = (config >> 8) & 0xFF
|
|
157
|
+
self._configuration_command[17 + i * 3] = config & 0xFF
|
|
158
|
+
|
|
159
|
+
# Byte 28-30: Configure MULTIPLE IN 1
|
|
160
|
+
config = int(self._multiple_input_one_configuration)
|
|
161
|
+
self._configuration_command[27] = (config >> 16) & 0xFF
|
|
162
|
+
self._configuration_command[28] = (config >> 8) & 0xFF
|
|
163
|
+
self._configuration_command[29] = config & 0xFF
|
|
164
|
+
|
|
165
|
+
# Byte 31-33: Configure MULTIPLE IN 2
|
|
166
|
+
config = int(self._multiple_input_two_configuration)
|
|
167
|
+
self._configuration_command[30] = (config >> 16) & 0xFF
|
|
168
|
+
self._configuration_command[31] = (config >> 8) & 0xFF
|
|
169
|
+
self._configuration_command[32] = config & 0xFF
|
|
170
|
+
|
|
171
|
+
# Byte 34-36: Configure MULTIPLE IN 3
|
|
172
|
+
config = int(self._multiple_input_three_configuration)
|
|
173
|
+
self._configuration_command[33] = (config >> 16) & 0xFF
|
|
174
|
+
self._configuration_command[34] = (config >> 8) & 0xFF
|
|
175
|
+
self._configuration_command[35] = config & 0xFF
|
|
176
|
+
|
|
177
|
+
# Byte 37-39: Configure MULTIPLE IN 4
|
|
178
|
+
config = int(self._multiple_input_four_configuration)
|
|
179
|
+
self._configuration_command[36] = (config >> 16) & 0xFF
|
|
180
|
+
self._configuration_command[37] = (config >> 8) & 0xFF
|
|
181
|
+
self._configuration_command[38] = config & 0xFF
|
|
182
|
+
|
|
183
|
+
# Control Byte
|
|
184
|
+
self._configuration_command[39] = self._crc_check(
|
|
185
|
+
self._configuration_command, 39
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self._number_of_channels = (
|
|
189
|
+
len(self._grids) * self._grid_size
|
|
190
|
+
+ QUATTROCENTO_AUXILIARY_CHANNELS
|
|
191
|
+
+ QUATTROCENTO_SUPPLEMENTARY_CHANNELS
|
|
192
|
+
)
|
|
193
|
+
|
|
118
194
|
self._number_of_biosignal_channels = len(self._grids) * self._grid_size
|
|
119
195
|
self._biosignal_channel_indices = np.array(
|
|
120
196
|
[
|
|
@@ -123,64 +199,110 @@ class OTBQuattrocentoLight(BaseDevice):
|
|
|
123
199
|
for j in range(self._grid_size)
|
|
124
200
|
]
|
|
125
201
|
)
|
|
126
|
-
|
|
127
|
-
self._auxiliary_channel_indices = np.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
202
|
+
self._number_of_auxiliary_channels = QUATTROCENTO_AUXILIARY_CHANNELS
|
|
203
|
+
self._auxiliary_channel_indices = np.arange(
|
|
204
|
+
self._number_of_streamed_channels
|
|
205
|
+
- self._number_of_auxiliary_channels
|
|
206
|
+
- QUATTROCENTO_SUPPLEMENTARY_CHANNELS,
|
|
207
|
+
self._number_of_streamed_channels - QUATTROCENTO_SUPPLEMENTARY_CHANNELS,
|
|
132
208
|
)
|
|
133
209
|
|
|
134
|
-
self.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
]
|
|
210
|
+
self._samples_per_frame = QUATTROCENTO_SAMPLES_PER_FRAME
|
|
211
|
+
self._bytes_per_sample = QUATTROCENTO_BYTES_PER_SAMPLE
|
|
212
|
+
self._buffer_size = (
|
|
213
|
+
self._number_of_streamed_channels * self._bytes_per_sample
|
|
214
|
+
) * self._samples_per_frame
|
|
140
215
|
|
|
141
|
-
self.
|
|
216
|
+
self._send_configuration_to_device()
|
|
142
217
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
218
|
+
def _crc_check(self, command_bytes: bytearray, command_length: int) -> bytes:
|
|
219
|
+
"""
|
|
220
|
+
Performs the Cyclic Redundancy Check (CRC) of the transmitted bytes.
|
|
146
221
|
|
|
147
|
-
|
|
148
|
-
self.configure_toggled.emit(True)
|
|
222
|
+
Translated function from example code provided by OT Bioelettronica.
|
|
149
223
|
|
|
150
|
-
|
|
151
|
-
|
|
224
|
+
Args:
|
|
225
|
+
command_bytes (bytearray):
|
|
226
|
+
Bytearray of the transmitted bytes.
|
|
227
|
+
|
|
228
|
+
command_length (int):
|
|
229
|
+
Length of the transmitted bytes.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
bytes:
|
|
233
|
+
CRC of the transmitted bytes.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
crc = 0
|
|
237
|
+
j = 0
|
|
238
|
+
|
|
239
|
+
while command_length > 0:
|
|
240
|
+
extracted_byte = command_bytes[j]
|
|
241
|
+
for i in range(8, 0, -1):
|
|
242
|
+
sum = crc % 2 ^ extracted_byte % 2
|
|
243
|
+
crc = crc // 2
|
|
244
|
+
|
|
245
|
+
if sum > 0:
|
|
246
|
+
crc_bin = format(crc, "08b")
|
|
247
|
+
a_bin = format(140, "08b")
|
|
152
248
|
|
|
153
|
-
|
|
249
|
+
str_list = []
|
|
250
|
+
|
|
251
|
+
for k in range(8):
|
|
252
|
+
str_list.append("0" if crc_bin[k] == a_bin[k] else "1")
|
|
253
|
+
|
|
254
|
+
crc = int("".join(str_list), 2)
|
|
255
|
+
|
|
256
|
+
extracted_byte = extracted_byte // 2
|
|
257
|
+
|
|
258
|
+
command_length -= 1
|
|
259
|
+
j += 1
|
|
260
|
+
|
|
261
|
+
return crc
|
|
262
|
+
|
|
263
|
+
def _send_configuration_to_device(self) -> None:
|
|
264
|
+
success = self._interface.write(self._configuration_command)
|
|
265
|
+
|
|
266
|
+
if success == -1:
|
|
267
|
+
print("Error while sending configuration to device")
|
|
268
|
+
|
|
269
|
+
self._is_configured = True
|
|
270
|
+
self.configure_toggled.emit(self._is_configured)
|
|
154
271
|
|
|
155
272
|
def _stop_streaming(self) -> None:
|
|
156
273
|
super()._stop_streaming()
|
|
157
274
|
|
|
158
|
-
self.
|
|
159
|
-
self.
|
|
275
|
+
self._configuration_command[0] -= 1
|
|
276
|
+
self._configuration_command[39] = self._crc_check(
|
|
277
|
+
self._configuration_command, 39
|
|
278
|
+
)
|
|
279
|
+
self._send_configuration_to_device()
|
|
160
280
|
|
|
161
|
-
def
|
|
162
|
-
super().
|
|
281
|
+
def _start_streaming(self) -> None:
|
|
282
|
+
super()._start_streaming()
|
|
163
283
|
|
|
164
|
-
self.
|
|
284
|
+
self._configuration_command[0] += 1
|
|
285
|
+
self._configuration_command[39] = self._crc_check(
|
|
286
|
+
self._configuration_command, 39
|
|
287
|
+
)
|
|
288
|
+
self._send_configuration_to_device()
|
|
165
289
|
|
|
166
|
-
|
|
167
|
-
|
|
290
|
+
self._is_streaming = True
|
|
291
|
+
self.stream_toggled.emit(self._is_streaming)
|
|
168
292
|
|
|
169
|
-
|
|
170
|
-
if not
|
|
171
|
-
|
|
172
|
-
if self._interface.readAll() == CONNECTION_RESPONSE:
|
|
293
|
+
def clear_socket(self) -> None:
|
|
294
|
+
if self._interface is not None:
|
|
295
|
+
self._interface.readAll()
|
|
173
296
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return
|
|
297
|
+
def _read_data(self) -> None:
|
|
298
|
+
super()._read_data()
|
|
177
299
|
|
|
178
300
|
if not self._is_streaming:
|
|
179
301
|
self.clear_socket()
|
|
180
302
|
return
|
|
181
303
|
|
|
182
304
|
while self._interface.bytesAvailable() > self._buffer_size:
|
|
183
|
-
packet = self._interface.
|
|
305
|
+
packet = self._interface.readAll()
|
|
184
306
|
if not packet:
|
|
185
307
|
continue
|
|
186
308
|
|
|
@@ -191,45 +313,20 @@ class OTBQuattrocentoLight(BaseDevice):
|
|
|
191
313
|
self._process_data(data_to_process)
|
|
192
314
|
self._received_bytes = self._received_bytes[self._buffer_size :]
|
|
193
315
|
|
|
194
|
-
def _process_data(self,
|
|
195
|
-
super()._process_data(
|
|
316
|
+
def _process_data(self, input: bytearray) -> None:
|
|
317
|
+
super()._process_data(input)
|
|
196
318
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
# Reshape it to the correct format
|
|
201
|
-
processed_data = decoded_data.reshape(
|
|
202
|
-
self._number_of_channels, -1, order="F"
|
|
319
|
+
data = np.frombuffer(input, dtype="<i2")
|
|
320
|
+
reshaped_data = data.reshape(
|
|
321
|
+
(self._number_of_streamed_channels, -1), order="F"
|
|
203
322
|
).astype(np.float32)
|
|
204
323
|
|
|
205
|
-
|
|
206
|
-
self.
|
|
207
|
-
|
|
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
|
-
"""
|
|
324
|
+
biosignal_data = self._extract_biosignal_data(reshaped_data)
|
|
325
|
+
auxiliary_data = self._extract_auxiliary_data(reshaped_data)
|
|
326
|
+
supplementary_data = reshaped_data[-QUATTROCENTO_SUPPLEMENTARY_CHANNELS:]
|
|
220
327
|
|
|
221
|
-
|
|
222
|
-
self,
|
|
223
|
-
parent: Union[QMainWindow, QWidget] = None,
|
|
224
|
-
) -> None:
|
|
225
|
-
super().__init__(parent)
|
|
328
|
+
processed_data = np.vstack((biosignal_data, auxiliary_data, supplementary_data))
|
|
226
329
|
|
|
227
|
-
|
|
228
|
-
self.
|
|
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()
|
|
330
|
+
self.data_available.emit(processed_data)
|
|
331
|
+
self.biosignal_data_available.emit(biosignal_data)
|
|
332
|
+
self.auxiliary_data_available.emit(auxiliary_data)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quattrocento Light class for real-time interface to
|
|
3
|
+
Quattrocento using OT Biolab Light.
|
|
4
|
+
|
|
5
|
+
Developer: Dominik I. Braun
|
|
6
|
+
Contact: dome.braun@fau.de
|
|
7
|
+
Last Update: 2023-06-05
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Python Libraries
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from typing import TYPE_CHECKING, Union, Dict
|
|
13
|
+
from PySide6.QtNetwork import QTcpSocket, QHostAddress
|
|
14
|
+
from PySide6.QtCore import QIODevice
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from biosignal_device_interface.devices.core.base_device import BaseDevice
|
|
19
|
+
from biosignal_device_interface.constants.devices.core.base_device_constants import (
|
|
20
|
+
DeviceType,
|
|
21
|
+
)
|
|
22
|
+
from biosignal_device_interface.constants.devices.otb.otb_quattrocento_light_constants import (
|
|
23
|
+
COMMAND_START_STREAMING,
|
|
24
|
+
COMMAND_STOP_STREAMING,
|
|
25
|
+
CONNECTION_RESPONSE,
|
|
26
|
+
QUATTROCENTO_LIGHT_STREAMING_FREQUENCY_DICT,
|
|
27
|
+
QUATTROCENTO_SAMPLING_FREQUENCY_DICT,
|
|
28
|
+
QuattrocentoLightSamplingFrequency,
|
|
29
|
+
QuattrocentoLightStreamingFrequency,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
# Python Libraries
|
|
35
|
+
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
36
|
+
from aenum import Enum
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OTBQuattrocentoLight(BaseDevice):
|
|
40
|
+
"""
|
|
41
|
+
QuattrocentoLight device class derived from BaseDevice class.
|
|
42
|
+
The QuattrocentoLight is using a TCP/IP protocol to communicate with the device.
|
|
43
|
+
|
|
44
|
+
This class directly interfaces with the OT Biolab Light software from
|
|
45
|
+
OT Bioelettronica. The configured settings of the device have to
|
|
46
|
+
match the settings from the OT Biolab Light software!
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
parent: Union[QMainWindow, QWidget] = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
super().__init__(parent)
|
|
54
|
+
|
|
55
|
+
# Device Parameters
|
|
56
|
+
self._device_type: DeviceType = DeviceType.OTB_QUATTROCENTO_LIGHT
|
|
57
|
+
|
|
58
|
+
# Device Information
|
|
59
|
+
self._number_of_channels: int = 408 # Fix value
|
|
60
|
+
self._auxiliary_channel_start_index: int = 384 # Fix value
|
|
61
|
+
self._number_of_auxiliary_channels: int = 16 # Fix value
|
|
62
|
+
self._conversion_factor_biosignal: float = 5 / (2**16) / 150 * 1000 # in mV
|
|
63
|
+
self._conversion_factor_auxiliary: float = 5 / (2**16) / 0.5 # in mV
|
|
64
|
+
self._bytes_per_sample: int = 2 # Fix value
|
|
65
|
+
# Quattrocento unique parameters
|
|
66
|
+
self._streaming_frequency: int | None = None
|
|
67
|
+
|
|
68
|
+
# Connection Parameters
|
|
69
|
+
self._interface: QTcpSocket = QTcpSocket()
|
|
70
|
+
|
|
71
|
+
# Configuration Parameters
|
|
72
|
+
self._grids: list[int] | None = None
|
|
73
|
+
self._grid_size: int = 64 # TODO: This is only valid for the big electrodes
|
|
74
|
+
self._streaming_frequency_mode: QuattrocentoLightStreamingFrequency | None = (
|
|
75
|
+
None
|
|
76
|
+
)
|
|
77
|
+
self._sampling_frequency_mode: QuattrocentoLightSamplingFrequency | None = None
|
|
78
|
+
|
|
79
|
+
def _connect_to_device(self) -> bool:
|
|
80
|
+
super()._connect_to_device()
|
|
81
|
+
|
|
82
|
+
self._received_bytes: bytearray = bytearray()
|
|
83
|
+
return self._make_request()
|
|
84
|
+
|
|
85
|
+
def _make_request(self) -> bool:
|
|
86
|
+
super()._make_request()
|
|
87
|
+
# Signal self.connect_toggled is emitted in _read_data
|
|
88
|
+
self._interface.connectToHost(
|
|
89
|
+
QHostAddress(self._connection_settings[0]),
|
|
90
|
+
self._connection_settings[1],
|
|
91
|
+
QIODevice.ReadWrite,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not self._interface.waitForConnected(1000):
|
|
95
|
+
self._disconnect_from_device()
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
self._interface.readyRead.connect(self._read_data)
|
|
99
|
+
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def _disconnect_from_device(self) -> None:
|
|
103
|
+
super()._disconnect_from_device()
|
|
104
|
+
|
|
105
|
+
self._interface.disconnectFromHost()
|
|
106
|
+
self._interface.readyRead.disconnect(self._read_data)
|
|
107
|
+
self._interface.close()
|
|
108
|
+
|
|
109
|
+
def configure_device(
|
|
110
|
+
self, params: Dict[str, Union[Enum, Dict[str, Enum]]] # type: ignore
|
|
111
|
+
) -> None:
|
|
112
|
+
super().configure_device(params)
|
|
113
|
+
|
|
114
|
+
# Configure the device
|
|
115
|
+
self._number_of_biosignal_channels = len(self._grids) * self._grid_size
|
|
116
|
+
self._biosignal_channel_indices = np.array(
|
|
117
|
+
[
|
|
118
|
+
i * self._grid_size + j
|
|
119
|
+
for i in self._grids
|
|
120
|
+
for j in range(self._grid_size)
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self._auxiliary_channel_indices = np.array(
|
|
125
|
+
[
|
|
126
|
+
i + self._auxiliary_channel_start_index
|
|
127
|
+
for i in range(self._number_of_auxiliary_channels)
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self._streaming_frequency = QUATTROCENTO_LIGHT_STREAMING_FREQUENCY_DICT[
|
|
132
|
+
self._streaming_frequency_mode
|
|
133
|
+
]
|
|
134
|
+
self._sampling_frequency = QUATTROCENTO_SAMPLING_FREQUENCY_DICT[
|
|
135
|
+
self._sampling_frequency_mode
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
self._samples_per_frame = self._sampling_frequency // self._streaming_frequency
|
|
139
|
+
|
|
140
|
+
self._buffer_size = (
|
|
141
|
+
self._bytes_per_sample * self._number_of_channels * self._samples_per_frame
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
self._is_configured = True
|
|
145
|
+
self.configure_toggled.emit(True)
|
|
146
|
+
|
|
147
|
+
def _start_streaming(self) -> None:
|
|
148
|
+
super()._start_streaming()
|
|
149
|
+
|
|
150
|
+
self._interface.write(COMMAND_START_STREAMING)
|
|
151
|
+
|
|
152
|
+
def _stop_streaming(self) -> None:
|
|
153
|
+
super()._stop_streaming()
|
|
154
|
+
|
|
155
|
+
self._interface.write(COMMAND_STOP_STREAMING)
|
|
156
|
+
self._interface.waitForBytesWritten(1000)
|
|
157
|
+
|
|
158
|
+
def clear_socket(self) -> None:
|
|
159
|
+
super().clear_socket()
|
|
160
|
+
|
|
161
|
+
self._interface.readAll()
|
|
162
|
+
|
|
163
|
+
def _read_data(self) -> None:
|
|
164
|
+
super()._read_data()
|
|
165
|
+
|
|
166
|
+
# Wait for connection response
|
|
167
|
+
if not self.is_connected and (
|
|
168
|
+
self._interface.bytesAvailable() == len(CONNECTION_RESPONSE)
|
|
169
|
+
and self._interface.readAll() == CONNECTION_RESPONSE
|
|
170
|
+
):
|
|
171
|
+
self.is_connected = True
|
|
172
|
+
self.connect_toggled.emit(True)
|
|
173
|
+
return
|
|
174
|
+
if not self._is_streaming:
|
|
175
|
+
self.clear_socket()
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
while self._interface.bytesAvailable() > self._buffer_size:
|
|
179
|
+
packet = self._interface.read(self._buffer_size)
|
|
180
|
+
if not packet:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
self._received_bytes.extend(packet)
|
|
184
|
+
|
|
185
|
+
while len(self._received_bytes) >= self._buffer_size:
|
|
186
|
+
data_to_process = self._received_bytes[: self._buffer_size]
|
|
187
|
+
self._process_data(data_to_process)
|
|
188
|
+
self._received_bytes = self._received_bytes[self._buffer_size :]
|
|
189
|
+
|
|
190
|
+
def _process_data(self, input: bytearray) -> None:
|
|
191
|
+
super()._process_data(input)
|
|
192
|
+
|
|
193
|
+
# Decode the data
|
|
194
|
+
decoded_data = np.frombuffer(input, dtype=np.int16)
|
|
195
|
+
|
|
196
|
+
# Reshape it to the correct format
|
|
197
|
+
processed_data = decoded_data.reshape(
|
|
198
|
+
self._number_of_channels, -1, order="F"
|
|
199
|
+
).astype(np.float32)
|
|
200
|
+
|
|
201
|
+
# Emit the data
|
|
202
|
+
self.data_available.emit(processed_data)
|
|
203
|
+
|
|
204
|
+
biosignal_data = self._extract_biosignal_data(processed_data)
|
|
205
|
+
self.biosignal_data_available.emit(biosignal_data)
|
|
206
|
+
auxiliary_data = self._extract_auxiliary_data(processed_data)
|
|
207
|
+
self.auxiliary_data_available.emit(auxiliary_data)
|
|
208
|
+
|
|
209
|
+
def get_device_information(self) -> Dict[str, Enum | int | float | str]: # type: ignore
|
|
210
|
+
return super().get_device_information()
|