biosignal-device-interface 0.2.1a1__py3-none-any.whl → 0.2.2__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 +500 -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.2.dist-info}/METADATA +3 -2
- biosignal_device_interface-0.2.2.dist-info/RECORD +46 -0
- {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.2.dist-info}/WHEEL +1 -1
- {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.2.dist-info/licenses}/LICENSE +675 -675
- biosignal_device_interface-0.2.1a1.dist-info/RECORD +0 -46
|
@@ -1,332 +1,332 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Quattrocento class for real-time direct interface to
|
|
3
|
-
Quattrocento.
|
|
4
|
-
|
|
5
|
-
Developer: Dominik I. Braun
|
|
6
|
-
Contact: dome.braun@fau.de
|
|
7
|
-
Last Update: 2025-01-14
|
|
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_constants import (
|
|
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,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
if TYPE_CHECKING:
|
|
38
|
-
# Python Libraries
|
|
39
|
-
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
40
|
-
from aenum import Enum
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class OTBQuattrocento(BaseDevice):
|
|
44
|
-
"""
|
|
45
|
-
Quattrocento device class derived from BaseDevice class.
|
|
46
|
-
The Quattrocento is using a TCP/IP protocol to communicate with the device.
|
|
47
|
-
|
|
48
|
-
This class directly interfaces with the Quattrocento from
|
|
49
|
-
OT Bioelettronica.
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
def __init__(self, parent: Union[QMainWindow, QWidget] = None):
|
|
53
|
-
super().__init__(parent)
|
|
54
|
-
|
|
55
|
-
# Device Paramters
|
|
56
|
-
self._device_type: DeviceType = DeviceType.OTB_QUATTROCENTO
|
|
57
|
-
|
|
58
|
-
# Device Information
|
|
59
|
-
self._conversion_factor_biosignal: float = 5 / (2**16) / 150 * 1000 # in mV
|
|
60
|
-
self._conversion_factor_auxiliary: float = 5 / (2**16) / 0.5 # in mV
|
|
61
|
-
self._number_of_streamed_channels: int = None
|
|
62
|
-
|
|
63
|
-
# Connection Parameters
|
|
64
|
-
self._interface: QTcpSocket = QTcpSocket()
|
|
65
|
-
|
|
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()
|
|
78
|
-
)
|
|
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)
|
|
92
|
-
|
|
93
|
-
def _connect_to_device(self) -> bool:
|
|
94
|
-
super()._connect_to_device()
|
|
95
|
-
|
|
96
|
-
self._received_bytes: bytearray = bytearray()
|
|
97
|
-
return self._make_request()
|
|
98
|
-
|
|
99
|
-
def _make_request(self) -> bool:
|
|
100
|
-
super()._make_request()
|
|
101
|
-
|
|
102
|
-
self._interface.connectToHost(
|
|
103
|
-
QHostAddress(self._connection_settings[0]), self._connection_settings[1]
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
if not self._interface.waitForConnected(1000):
|
|
107
|
-
self._disconnect_from_device()
|
|
108
|
-
return False
|
|
109
|
-
|
|
110
|
-
self._interface.readyRead.connect(self._read_data)
|
|
111
|
-
|
|
112
|
-
self.is_connected = True
|
|
113
|
-
self.connect_toggled.emit(self.is_connected)
|
|
114
|
-
|
|
115
|
-
return True
|
|
116
|
-
|
|
117
|
-
def _disconnect_from_device(self) -> None:
|
|
118
|
-
super()._disconnect_from_device()
|
|
119
|
-
|
|
120
|
-
self._interface.disconnectFromHost()
|
|
121
|
-
self._interface.readyRead.disconnect(self._read_data)
|
|
122
|
-
self._interface.close()
|
|
123
|
-
|
|
124
|
-
def 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
|
-
)
|
|
133
|
-
|
|
134
|
-
self._configuration_command = bytearray(40)
|
|
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
|
-
|
|
194
|
-
self._number_of_biosignal_channels = len(self._grids) * self._grid_size
|
|
195
|
-
self._biosignal_channel_indices = np.array(
|
|
196
|
-
[
|
|
197
|
-
i * self._grid_size + j
|
|
198
|
-
for i in self._grids
|
|
199
|
-
for j in range(self._grid_size)
|
|
200
|
-
]
|
|
201
|
-
)
|
|
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,
|
|
208
|
-
)
|
|
209
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
self._send_configuration_to_device()
|
|
217
|
-
|
|
218
|
-
def _crc_check(self, command_bytes: bytearray, command_length: int) -> bytes:
|
|
219
|
-
"""
|
|
220
|
-
Performs the Cyclic Redundancy Check (CRC) of the transmitted bytes.
|
|
221
|
-
|
|
222
|
-
Translated function from example code provided by OT Bioelettronica.
|
|
223
|
-
|
|
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")
|
|
248
|
-
|
|
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)
|
|
271
|
-
|
|
272
|
-
def _stop_streaming(self) -> None:
|
|
273
|
-
super()._stop_streaming()
|
|
274
|
-
|
|
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()
|
|
280
|
-
|
|
281
|
-
def _start_streaming(self) -> None:
|
|
282
|
-
super()._start_streaming()
|
|
283
|
-
|
|
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()
|
|
289
|
-
|
|
290
|
-
self._is_streaming = True
|
|
291
|
-
self.stream_toggled.emit(self._is_streaming)
|
|
292
|
-
|
|
293
|
-
def clear_socket(self) -> None:
|
|
294
|
-
if self._interface is not None:
|
|
295
|
-
self._interface.readAll()
|
|
296
|
-
|
|
297
|
-
def _read_data(self) -> None:
|
|
298
|
-
super()._read_data()
|
|
299
|
-
|
|
300
|
-
if not self._is_streaming:
|
|
301
|
-
self.clear_socket()
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
while self._interface.bytesAvailable() > self._buffer_size:
|
|
305
|
-
packet = self._interface.readAll()
|
|
306
|
-
if not packet:
|
|
307
|
-
continue
|
|
308
|
-
|
|
309
|
-
self._received_bytes.extend(packet)
|
|
310
|
-
|
|
311
|
-
while len(self._received_bytes) >= self._buffer_size:
|
|
312
|
-
data_to_process = self._received_bytes[: self._buffer_size]
|
|
313
|
-
self._process_data(data_to_process)
|
|
314
|
-
self._received_bytes = self._received_bytes[self._buffer_size :]
|
|
315
|
-
|
|
316
|
-
def _process_data(self, input: bytearray) -> None:
|
|
317
|
-
super()._process_data(input)
|
|
318
|
-
|
|
319
|
-
data = np.frombuffer(input, dtype="<i2")
|
|
320
|
-
reshaped_data = data.reshape(
|
|
321
|
-
(self._number_of_streamed_channels, -1), order="F"
|
|
322
|
-
).astype(np.float32)
|
|
323
|
-
|
|
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:]
|
|
327
|
-
|
|
328
|
-
processed_data = np.vstack((biosignal_data, auxiliary_data, supplementary_data))
|
|
329
|
-
|
|
330
|
-
self.data_available.emit(processed_data)
|
|
331
|
-
self.biosignal_data_available.emit(biosignal_data)
|
|
332
|
-
self.auxiliary_data_available.emit(auxiliary_data)
|
|
1
|
+
"""
|
|
2
|
+
Quattrocento class for real-time direct interface to
|
|
3
|
+
Quattrocento.
|
|
4
|
+
|
|
5
|
+
Developer: Dominik I. Braun
|
|
6
|
+
Contact: dome.braun@fau.de
|
|
7
|
+
Last Update: 2025-01-14
|
|
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_constants import (
|
|
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,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
# Python Libraries
|
|
39
|
+
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
40
|
+
from aenum import Enum
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OTBQuattrocento(BaseDevice):
|
|
44
|
+
"""
|
|
45
|
+
Quattrocento device class derived from BaseDevice class.
|
|
46
|
+
The Quattrocento is using a TCP/IP protocol to communicate with the device.
|
|
47
|
+
|
|
48
|
+
This class directly interfaces with the Quattrocento from
|
|
49
|
+
OT Bioelettronica.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, parent: Union[QMainWindow, QWidget] = None):
|
|
53
|
+
super().__init__(parent)
|
|
54
|
+
|
|
55
|
+
# Device Paramters
|
|
56
|
+
self._device_type: DeviceType = DeviceType.OTB_QUATTROCENTO
|
|
57
|
+
|
|
58
|
+
# Device Information
|
|
59
|
+
self._conversion_factor_biosignal: float = 5 / (2**16) / 150 * 1000 # in mV
|
|
60
|
+
self._conversion_factor_auxiliary: float = 5 / (2**16) / 0.5 # in mV
|
|
61
|
+
self._number_of_streamed_channels: int = None
|
|
62
|
+
|
|
63
|
+
# Connection Parameters
|
|
64
|
+
self._interface: QTcpSocket = QTcpSocket()
|
|
65
|
+
|
|
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()
|
|
78
|
+
)
|
|
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)
|
|
92
|
+
|
|
93
|
+
def _connect_to_device(self) -> bool:
|
|
94
|
+
super()._connect_to_device()
|
|
95
|
+
|
|
96
|
+
self._received_bytes: bytearray = bytearray()
|
|
97
|
+
return self._make_request()
|
|
98
|
+
|
|
99
|
+
def _make_request(self) -> bool:
|
|
100
|
+
super()._make_request()
|
|
101
|
+
|
|
102
|
+
self._interface.connectToHost(
|
|
103
|
+
QHostAddress(self._connection_settings[0]), self._connection_settings[1]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if not self._interface.waitForConnected(1000):
|
|
107
|
+
self._disconnect_from_device()
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
self._interface.readyRead.connect(self._read_data)
|
|
111
|
+
|
|
112
|
+
self.is_connected = True
|
|
113
|
+
self.connect_toggled.emit(self.is_connected)
|
|
114
|
+
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
def _disconnect_from_device(self) -> None:
|
|
118
|
+
super()._disconnect_from_device()
|
|
119
|
+
|
|
120
|
+
self._interface.disconnectFromHost()
|
|
121
|
+
self._interface.readyRead.disconnect(self._read_data)
|
|
122
|
+
self._interface.close()
|
|
123
|
+
|
|
124
|
+
def 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
|
+
)
|
|
133
|
+
|
|
134
|
+
self._configuration_command = bytearray(40)
|
|
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
|
+
|
|
194
|
+
self._number_of_biosignal_channels = len(self._grids) * self._grid_size
|
|
195
|
+
self._biosignal_channel_indices = np.array(
|
|
196
|
+
[
|
|
197
|
+
i * self._grid_size + j
|
|
198
|
+
for i in self._grids
|
|
199
|
+
for j in range(self._grid_size)
|
|
200
|
+
]
|
|
201
|
+
)
|
|
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,
|
|
208
|
+
)
|
|
209
|
+
|
|
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
|
|
215
|
+
|
|
216
|
+
self._send_configuration_to_device()
|
|
217
|
+
|
|
218
|
+
def _crc_check(self, command_bytes: bytearray, command_length: int) -> bytes:
|
|
219
|
+
"""
|
|
220
|
+
Performs the Cyclic Redundancy Check (CRC) of the transmitted bytes.
|
|
221
|
+
|
|
222
|
+
Translated function from example code provided by OT Bioelettronica.
|
|
223
|
+
|
|
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")
|
|
248
|
+
|
|
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)
|
|
271
|
+
|
|
272
|
+
def _stop_streaming(self) -> None:
|
|
273
|
+
super()._stop_streaming()
|
|
274
|
+
|
|
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()
|
|
280
|
+
|
|
281
|
+
def _start_streaming(self) -> None:
|
|
282
|
+
super()._start_streaming()
|
|
283
|
+
|
|
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()
|
|
289
|
+
|
|
290
|
+
self._is_streaming = True
|
|
291
|
+
self.stream_toggled.emit(self._is_streaming)
|
|
292
|
+
|
|
293
|
+
def clear_socket(self) -> None:
|
|
294
|
+
if self._interface is not None:
|
|
295
|
+
self._interface.readAll()
|
|
296
|
+
|
|
297
|
+
def _read_data(self) -> None:
|
|
298
|
+
super()._read_data()
|
|
299
|
+
|
|
300
|
+
if not self._is_streaming:
|
|
301
|
+
self.clear_socket()
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
while self._interface.bytesAvailable() > self._buffer_size:
|
|
305
|
+
packet = self._interface.readAll()
|
|
306
|
+
if not packet:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
self._received_bytes.extend(packet)
|
|
310
|
+
|
|
311
|
+
while len(self._received_bytes) >= self._buffer_size:
|
|
312
|
+
data_to_process = self._received_bytes[: self._buffer_size]
|
|
313
|
+
self._process_data(data_to_process)
|
|
314
|
+
self._received_bytes = self._received_bytes[self._buffer_size :]
|
|
315
|
+
|
|
316
|
+
def _process_data(self, input: bytearray) -> None:
|
|
317
|
+
super()._process_data(input)
|
|
318
|
+
|
|
319
|
+
data = np.frombuffer(input, dtype="<i2")
|
|
320
|
+
reshaped_data = data.reshape(
|
|
321
|
+
(self._number_of_streamed_channels, -1), order="F"
|
|
322
|
+
).astype(np.float32)
|
|
323
|
+
|
|
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:]
|
|
327
|
+
|
|
328
|
+
processed_data = np.vstack((biosignal_data, auxiliary_data, supplementary_data))
|
|
329
|
+
|
|
330
|
+
self.data_available.emit(processed_data)
|
|
331
|
+
self.biosignal_data_available.emit(biosignal_data)
|
|
332
|
+
self.auxiliary_data_available.emit(auxiliary_data)
|