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,407 +1,407 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Device class for real-time interfacing the OTB Syncstation device.
|
|
3
|
-
Developer: Dominik I. Braun
|
|
4
|
-
Contact: dome.braun@fau.de
|
|
5
|
-
Last Update: 2025-01-09
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
from typing import TYPE_CHECKING, Union, Dict
|
|
10
|
-
from PySide6.QtNetwork import QTcpSocket, QHostAddress
|
|
11
|
-
from PySide6.QtCore import QIODevice
|
|
12
|
-
import numpy as np
|
|
13
|
-
|
|
14
|
-
# Local Libraries
|
|
15
|
-
from biosignal_device_interface.constants.devices.otb.otb_syncstation_constants import (
|
|
16
|
-
PROBE_CHARACTERISTICS_DICT,
|
|
17
|
-
SYNCSTATION_CHARACTERISTICS_DICT,
|
|
18
|
-
SYNCSTATION_CONVERSION_FACTOR_DICT,
|
|
19
|
-
SyncStationDetectionMode,
|
|
20
|
-
SyncStationProbeConfigMode,
|
|
21
|
-
SyncStationRecOnMode,
|
|
22
|
-
SyncStationWorkingMode,
|
|
23
|
-
)
|
|
24
|
-
from biosignal_device_interface.devices.core.base_device import BaseDevice
|
|
25
|
-
from biosignal_device_interface.constants.devices.core.base_device_constants import (
|
|
26
|
-
DeviceType,
|
|
27
|
-
DeviceChannelTypes,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if TYPE_CHECKING:
|
|
32
|
-
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
33
|
-
from aenum import Enum
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class OTBSyncStation(BaseDevice):
|
|
37
|
-
"""
|
|
38
|
-
Device class for real-time interfacing the OTB Syncstation device.
|
|
39
|
-
The SyncStation class is using a TCP/IP protocol to communicate with the device.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
def __init__(self, parent: Union[QMainWindow, QWidget] = None) -> None:
|
|
43
|
-
"""
|
|
44
|
-
Constructor of the OTBSyncStation class.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
parent (Union[QMainWindow, QWidget]): The parent object of the device. Defaults to None.
|
|
48
|
-
"""
|
|
49
|
-
super().__init__(parent)
|
|
50
|
-
|
|
51
|
-
# Device Parameters
|
|
52
|
-
self._device_type = DeviceType.OTB_SYNCSTATION
|
|
53
|
-
|
|
54
|
-
# Connection Parameters
|
|
55
|
-
self._interface: QTcpSocket = QTcpSocket()
|
|
56
|
-
|
|
57
|
-
# Configuration Parameters
|
|
58
|
-
self._configuration_command_A: bytearray = None
|
|
59
|
-
self._configuration_command_B: bytearray = None
|
|
60
|
-
|
|
61
|
-
# Configuration Parameters A
|
|
62
|
-
self._rec_on_mode: SyncStationRecOnMode = None
|
|
63
|
-
self._working_mode: SyncStationWorkingMode = None
|
|
64
|
-
self._number_of_probes: int = None
|
|
65
|
-
self._bytes_configuration_A: Dict[
|
|
66
|
-
SyncStationProbeConfigMode, Dict[str, SyncStationDetectionMode | bool]
|
|
67
|
-
] = None
|
|
68
|
-
|
|
69
|
-
# Configuration Parameters B
|
|
70
|
-
self._bytes_configuration_B: Dict[str, int] = None
|
|
71
|
-
|
|
72
|
-
def _connect_to_device(self) -> bool:
|
|
73
|
-
super()._connect_to_device()
|
|
74
|
-
|
|
75
|
-
self._received_bytes: bytearray = bytearray()
|
|
76
|
-
self._make_request()
|
|
77
|
-
|
|
78
|
-
def _make_request(self) -> bool:
|
|
79
|
-
super()._make_request()
|
|
80
|
-
|
|
81
|
-
self._interface.connectToHost(
|
|
82
|
-
QHostAddress(self._connection_settings[0]),
|
|
83
|
-
self._connection_settings[1],
|
|
84
|
-
QIODevice.ReadWrite,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
if not self._interface.waitForConnected(1000):
|
|
88
|
-
self._disconnect_from_device()
|
|
89
|
-
return False
|
|
90
|
-
|
|
91
|
-
self.is_connected = True
|
|
92
|
-
self.connect_toggled.emit(True)
|
|
93
|
-
|
|
94
|
-
self._interface.readyRead.connect(self._read_data)
|
|
95
|
-
return True
|
|
96
|
-
|
|
97
|
-
def _disconnect_from_device(self) -> None:
|
|
98
|
-
super()._disconnect_from_device()
|
|
99
|
-
|
|
100
|
-
self._interface.disconnectFromHost()
|
|
101
|
-
self._interface.readyRead.disconnect(self._read_data)
|
|
102
|
-
self._interface.close()
|
|
103
|
-
|
|
104
|
-
self.is_connected = False
|
|
105
|
-
self.connect_toggled.emit(False)
|
|
106
|
-
|
|
107
|
-
def configure_device(self, params) -> None:
|
|
108
|
-
super().configure_device(params)
|
|
109
|
-
|
|
110
|
-
success = self._configure_byte_sequence_A()
|
|
111
|
-
|
|
112
|
-
if not success:
|
|
113
|
-
print("Unable to configure device.")
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
self._send_configuration_to_device()
|
|
117
|
-
|
|
118
|
-
self._is_configured = True
|
|
119
|
-
self.configure_toggled.emit(True)
|
|
120
|
-
|
|
121
|
-
def _configure_byte_sequence_A(self) -> None:
|
|
122
|
-
start_byte = 0
|
|
123
|
-
start_byte += (self._rec_on_mode.value - 1) << 6
|
|
124
|
-
|
|
125
|
-
self._sampling_frequency = SYNCSTATION_CHARACTERISTICS_DICT[
|
|
126
|
-
"channel_information"
|
|
127
|
-
][self._working_mode]["sampling_frequency"]
|
|
128
|
-
self._bytes_per_sample = SYNCSTATION_CHARACTERISTICS_DICT[
|
|
129
|
-
"channel_information"
|
|
130
|
-
][self._working_mode]["bytes_per_sample"]
|
|
131
|
-
|
|
132
|
-
self._configuration_command_A = bytearray()
|
|
133
|
-
self._number_of_channels = 0
|
|
134
|
-
self._number_of_bytes = 0
|
|
135
|
-
|
|
136
|
-
self._number_of_biosignal_channels = 0
|
|
137
|
-
self._number_of_auxiliary_channels = 0
|
|
138
|
-
self._biosignal_channel_indices = []
|
|
139
|
-
self._auxiliary_channel_indices = []
|
|
140
|
-
|
|
141
|
-
for key, value in self._bytes_configuration_A.items():
|
|
142
|
-
probe_command = 0
|
|
143
|
-
probe_command += (key.value - 1) << 4
|
|
144
|
-
probe_command += (self._working_mode.value - 1) << 3
|
|
145
|
-
probe_command += (value["detection_mode"].value - 1) << 1
|
|
146
|
-
probe_command += int(value["probe_status"])
|
|
147
|
-
|
|
148
|
-
if value["probe_status"]:
|
|
149
|
-
self._configuration_command_A.append(probe_command)
|
|
150
|
-
channels = PROBE_CHARACTERISTICS_DICT[key][DeviceChannelTypes.ALL]
|
|
151
|
-
biosignal_channels = PROBE_CHARACTERISTICS_DICT[key][
|
|
152
|
-
DeviceChannelTypes.BIOSIGNAL
|
|
153
|
-
]
|
|
154
|
-
auxiliary_channels = PROBE_CHARACTERISTICS_DICT[key][
|
|
155
|
-
DeviceChannelTypes.AUXILIARY
|
|
156
|
-
]
|
|
157
|
-
|
|
158
|
-
self._biosignal_channel_indices.append(
|
|
159
|
-
np.arange(
|
|
160
|
-
self._number_of_channels,
|
|
161
|
-
self._number_of_channels + biosignal_channels,
|
|
162
|
-
)
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
self._auxiliary_channel_indices.append(
|
|
166
|
-
np.arange(
|
|
167
|
-
self._number_of_channels + biosignal_channels,
|
|
168
|
-
self._number_of_channels + channels,
|
|
169
|
-
)
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
self._number_of_channels += channels
|
|
173
|
-
self._number_of_biosignal_channels += biosignal_channels
|
|
174
|
-
self._number_of_auxiliary_channels += auxiliary_channels
|
|
175
|
-
|
|
176
|
-
self._conversion_factor_biosignal = SYNCSTATION_CONVERSION_FACTOR_DICT[
|
|
177
|
-
value["detection_mode"]
|
|
178
|
-
]
|
|
179
|
-
self._conversion_factor_auxiliary = self._conversion_factor_biosignal
|
|
180
|
-
|
|
181
|
-
self._biosignal_channel_indices = np.hstack(self._biosignal_channel_indices)
|
|
182
|
-
self._auxiliary_channel_indices = np.hstack(self._auxiliary_channel_indices)
|
|
183
|
-
self._number_of_bytes = self._number_of_channels * self._bytes_per_sample
|
|
184
|
-
|
|
185
|
-
# Add SyncStation Channels
|
|
186
|
-
self._number_of_channels += SYNCSTATION_CHARACTERISTICS_DICT[
|
|
187
|
-
DeviceChannelTypes.ALL
|
|
188
|
-
]
|
|
189
|
-
self._number_of_auxiliary_channels += SYNCSTATION_CHARACTERISTICS_DICT[
|
|
190
|
-
DeviceChannelTypes.ALL
|
|
191
|
-
]
|
|
192
|
-
|
|
193
|
-
self._number_of_bytes += (
|
|
194
|
-
SYNCSTATION_CHARACTERISTICS_DICT[DeviceChannelTypes.ALL]
|
|
195
|
-
* SYNCSTATION_CHARACTERISTICS_DICT["bytes_per_sample"]
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
self._samples_per_frame = int(
|
|
199
|
-
(1 / SYNCSTATION_CHARACTERISTICS_DICT["FPS"]) * self._sampling_frequency
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
self._buffer_size = int(self._number_of_bytes * self._samples_per_frame)
|
|
203
|
-
|
|
204
|
-
num_probes = len(self._configuration_command_A)
|
|
205
|
-
start_byte += num_probes << 1
|
|
206
|
-
self._configuration_command_A.insert(0, start_byte)
|
|
207
|
-
start_byte_ckc8 = self._crc_check(
|
|
208
|
-
self._configuration_command_A, len(self._configuration_command_A)
|
|
209
|
-
)
|
|
210
|
-
self._configuration_command_A.append(start_byte_ckc8)
|
|
211
|
-
|
|
212
|
-
return True
|
|
213
|
-
|
|
214
|
-
def _crc_check(self, command_bytes: bytearray, command_length: int) -> bytes:
|
|
215
|
-
"""
|
|
216
|
-
Performs the Cyclic Redundancy Check (CRC) of the transmitted bytes.
|
|
217
|
-
|
|
218
|
-
Translated function from example code provided by OT Bioelettronica.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
command_bytes (bytearray):
|
|
222
|
-
Bytearray of the transmitted bytes.
|
|
223
|
-
|
|
224
|
-
command_length (int):
|
|
225
|
-
Length of the transmitted bytes.
|
|
226
|
-
|
|
227
|
-
Returns:
|
|
228
|
-
bytes:
|
|
229
|
-
CRC of the transmitted bytes.
|
|
230
|
-
"""
|
|
231
|
-
|
|
232
|
-
crc = 0
|
|
233
|
-
j = 0
|
|
234
|
-
|
|
235
|
-
while command_length > 0:
|
|
236
|
-
extracted_byte = command_bytes[j]
|
|
237
|
-
for i in range(8, 0, -1):
|
|
238
|
-
sum = crc % 2 ^ extracted_byte % 2
|
|
239
|
-
crc = crc // 2
|
|
240
|
-
|
|
241
|
-
if sum > 0:
|
|
242
|
-
crc_bin = format(crc, "08b")
|
|
243
|
-
a_bin = format(140, "08b")
|
|
244
|
-
|
|
245
|
-
str_list = []
|
|
246
|
-
|
|
247
|
-
for k in range(8):
|
|
248
|
-
str_list.append("0" if crc_bin[k] == a_bin[k] else "1")
|
|
249
|
-
|
|
250
|
-
crc = int("".join(str_list), 2)
|
|
251
|
-
|
|
252
|
-
extracted_byte = extracted_byte // 2
|
|
253
|
-
|
|
254
|
-
command_length -= 1
|
|
255
|
-
j += 1
|
|
256
|
-
|
|
257
|
-
return crc
|
|
258
|
-
|
|
259
|
-
def _configure_byte_sequence_B(self) -> None:
|
|
260
|
-
# TODO: Implement this method
|
|
261
|
-
...
|
|
262
|
-
|
|
263
|
-
def _send_configuration_to_device(self) -> None:
|
|
264
|
-
print(
|
|
265
|
-
f"Device configuration sent: {[int.from_bytes(self._configuration_command_A[i : i + 1], 'big') for i in range(len(self._configuration_command_A))]}"
|
|
266
|
-
)
|
|
267
|
-
self._interface.write(self._configuration_command_A)
|
|
268
|
-
|
|
269
|
-
def _stop_streaming(self):
|
|
270
|
-
self._configuration_command_A[0] -= 1
|
|
271
|
-
self._configuration_command_A[-1] = self._crc_check(
|
|
272
|
-
self._configuration_command_A, len(self._configuration_command_A) - 1
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
self._send_configuration_to_device()
|
|
276
|
-
|
|
277
|
-
self._is_streaming = False
|
|
278
|
-
self.stream_toggled.emit(False)
|
|
279
|
-
|
|
280
|
-
def _start_streaming(self):
|
|
281
|
-
self._configuration_command_A[0] += 1
|
|
282
|
-
self._configuration_command_A[-1] = self._crc_check(
|
|
283
|
-
self._configuration_command_A, len(self._configuration_command_A) - 1
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
self._send_configuration_to_device()
|
|
287
|
-
|
|
288
|
-
self._is_streaming = True
|
|
289
|
-
self.stream_toggled.emit(True)
|
|
290
|
-
|
|
291
|
-
def _clear_socket(self) -> None:
|
|
292
|
-
"""
|
|
293
|
-
Clears the socket from any remaining data.
|
|
294
|
-
"""
|
|
295
|
-
self._interface.readAll()
|
|
296
|
-
self._received_bytes = bytearray()
|
|
297
|
-
|
|
298
|
-
def _read_data(self) -> None:
|
|
299
|
-
if not self._is_streaming:
|
|
300
|
-
packet = self._interface.readAll()
|
|
301
|
-
|
|
302
|
-
else:
|
|
303
|
-
if self._interface.bytesAvailable() > 0:
|
|
304
|
-
|
|
305
|
-
packet = self._interface.readAll()
|
|
306
|
-
packet_bytearray = bytearray(packet.data())
|
|
307
|
-
|
|
308
|
-
if not packet_bytearray:
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
self._received_bytes.extend(packet_bytearray)
|
|
312
|
-
|
|
313
|
-
while len(self._received_bytes) >= self._buffer_size:
|
|
314
|
-
self._process_data(
|
|
315
|
-
bytearray(self._received_bytes)[: self._buffer_size]
|
|
316
|
-
)
|
|
317
|
-
self._received_bytes = bytearray(self._received_bytes)[
|
|
318
|
-
self._buffer_size :
|
|
319
|
-
]
|
|
320
|
-
|
|
321
|
-
def _process_data(self, input: bytearray) -> None:
|
|
322
|
-
data: np.ndarray = np.frombuffer(input, dtype=np.uint8).astype(np.float32)
|
|
323
|
-
|
|
324
|
-
samples = self._samples_per_frame
|
|
325
|
-
data = np.reshape(data, (samples, self._number_of_bytes)).T
|
|
326
|
-
processed_data = self._bytes_to_integers(data)
|
|
327
|
-
|
|
328
|
-
# Emit the data
|
|
329
|
-
self.data_available.emit(processed_data)
|
|
330
|
-
self.biosignal_data_available.emit(self._extract_biosignal_data(processed_data))
|
|
331
|
-
self.auxiliary_data_available.emit(self._extract_auxiliary_data(processed_data))
|
|
332
|
-
|
|
333
|
-
def _integer_to_bytes(self, command: int) -> bytes:
|
|
334
|
-
return int(command).to_bytes(1, byteorder="big")
|
|
335
|
-
|
|
336
|
-
# Convert channels from bytes to integers
|
|
337
|
-
def _bytes_to_integers(
|
|
338
|
-
self,
|
|
339
|
-
data: np.ndarray,
|
|
340
|
-
) -> np.ndarray:
|
|
341
|
-
samples = self._samples_per_frame
|
|
342
|
-
frame_data = np.zeros((self._number_of_channels, samples), dtype=np.float32)
|
|
343
|
-
channels_to_read = 0
|
|
344
|
-
for device in list(SyncStationProbeConfigMode)[1:]:
|
|
345
|
-
if self._bytes_configuration_A[device]["probe_status"]:
|
|
346
|
-
channel_number = PROBE_CHARACTERISTICS_DICT[device][
|
|
347
|
-
DeviceChannelTypes.ALL
|
|
348
|
-
]
|
|
349
|
-
# Convert channel's byte value to integer
|
|
350
|
-
if self._working_mode == SyncStationWorkingMode.EMG:
|
|
351
|
-
channel_indices = (
|
|
352
|
-
np.arange(0, channel_number * 2, 2) + channels_to_read * 2
|
|
353
|
-
)
|
|
354
|
-
data_sub_matrix = self._decode_int16(data, channel_indices)
|
|
355
|
-
frame_data[
|
|
356
|
-
channels_to_read : channels_to_read + channel_number, :
|
|
357
|
-
] = data_sub_matrix
|
|
358
|
-
|
|
359
|
-
elif self._working_mode == SyncStationWorkingMode.EEG:
|
|
360
|
-
channel_indices = (
|
|
361
|
-
np.arange(0, channel_number * 3, 3) + channels_to_read * 2
|
|
362
|
-
)
|
|
363
|
-
data_sub_matrix = self._decode_int24(data, channel_indices)
|
|
364
|
-
frame_data[
|
|
365
|
-
channels_to_read : channels_to_read + channel_number, :
|
|
366
|
-
] = data_sub_matrix
|
|
367
|
-
|
|
368
|
-
channels_to_read += channel_number
|
|
369
|
-
del data_sub_matrix
|
|
370
|
-
del channel_indices
|
|
371
|
-
|
|
372
|
-
syncstation_aux_bytes_number = (
|
|
373
|
-
SYNCSTATION_CHARACTERISTICS_DICT[DeviceChannelTypes.ALL]
|
|
374
|
-
* SYNCSTATION_CHARACTERISTICS_DICT["bytes_per_sample"]
|
|
375
|
-
)
|
|
376
|
-
syncstation_aux_starting_byte = (
|
|
377
|
-
self._number_of_bytes - syncstation_aux_bytes_number
|
|
378
|
-
)
|
|
379
|
-
channel_indices = np.arange(
|
|
380
|
-
syncstation_aux_starting_byte,
|
|
381
|
-
syncstation_aux_starting_byte + syncstation_aux_bytes_number,
|
|
382
|
-
2,
|
|
383
|
-
)
|
|
384
|
-
data_sub_matrix = self._decode_int16(data, channel_indices)
|
|
385
|
-
frame_data[channels_to_read : channels_to_read + 6, :] = data_sub_matrix
|
|
386
|
-
return np.array(frame_data)
|
|
387
|
-
|
|
388
|
-
def _decode_int24(
|
|
389
|
-
self, data: np.ndarray, channel_indices: np.ndarray
|
|
390
|
-
) -> np.ndarray:
|
|
391
|
-
data_sub_matrix = (
|
|
392
|
-
data[channel_indices, :] * 2**16
|
|
393
|
-
+ data[channel_indices + 1, :] * 2**8
|
|
394
|
-
+ data[channel_indices + 2, :]
|
|
395
|
-
)
|
|
396
|
-
negative_indices = np.where(data_sub_matrix >= 2**23)
|
|
397
|
-
data_sub_matrix[negative_indices] -= 2**24
|
|
398
|
-
|
|
399
|
-
return data_sub_matrix
|
|
400
|
-
|
|
401
|
-
def _decode_int16(
|
|
402
|
-
self, data: np.ndarray, channel_indices: np.ndarray
|
|
403
|
-
) -> np.ndarray:
|
|
404
|
-
data_sub_matrix = data[channel_indices, :] * 2**8 + data[channel_indices + 1, :]
|
|
405
|
-
negative_indices = np.where(data_sub_matrix >= 2**15)
|
|
406
|
-
data_sub_matrix[negative_indices] -= 2**16
|
|
407
|
-
return data_sub_matrix
|
|
1
|
+
"""
|
|
2
|
+
Device class for real-time interfacing the OTB Syncstation device.
|
|
3
|
+
Developer: Dominik I. Braun
|
|
4
|
+
Contact: dome.braun@fau.de
|
|
5
|
+
Last Update: 2025-01-09
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import TYPE_CHECKING, Union, Dict
|
|
10
|
+
from PySide6.QtNetwork import QTcpSocket, QHostAddress
|
|
11
|
+
from PySide6.QtCore import QIODevice
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
# Local Libraries
|
|
15
|
+
from biosignal_device_interface.constants.devices.otb.otb_syncstation_constants import (
|
|
16
|
+
PROBE_CHARACTERISTICS_DICT,
|
|
17
|
+
SYNCSTATION_CHARACTERISTICS_DICT,
|
|
18
|
+
SYNCSTATION_CONVERSION_FACTOR_DICT,
|
|
19
|
+
SyncStationDetectionMode,
|
|
20
|
+
SyncStationProbeConfigMode,
|
|
21
|
+
SyncStationRecOnMode,
|
|
22
|
+
SyncStationWorkingMode,
|
|
23
|
+
)
|
|
24
|
+
from biosignal_device_interface.devices.core.base_device import BaseDevice
|
|
25
|
+
from biosignal_device_interface.constants.devices.core.base_device_constants import (
|
|
26
|
+
DeviceType,
|
|
27
|
+
DeviceChannelTypes,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from PySide6.QtWidgets import QMainWindow, QWidget
|
|
33
|
+
from aenum import Enum
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OTBSyncStation(BaseDevice):
|
|
37
|
+
"""
|
|
38
|
+
Device class for real-time interfacing the OTB Syncstation device.
|
|
39
|
+
The SyncStation class is using a TCP/IP protocol to communicate with the device.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, parent: Union[QMainWindow, QWidget] = None) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Constructor of the OTBSyncStation class.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
parent (Union[QMainWindow, QWidget]): The parent object of the device. Defaults to None.
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(parent)
|
|
50
|
+
|
|
51
|
+
# Device Parameters
|
|
52
|
+
self._device_type = DeviceType.OTB_SYNCSTATION
|
|
53
|
+
|
|
54
|
+
# Connection Parameters
|
|
55
|
+
self._interface: QTcpSocket = QTcpSocket()
|
|
56
|
+
|
|
57
|
+
# Configuration Parameters
|
|
58
|
+
self._configuration_command_A: bytearray = None
|
|
59
|
+
self._configuration_command_B: bytearray = None
|
|
60
|
+
|
|
61
|
+
# Configuration Parameters A
|
|
62
|
+
self._rec_on_mode: SyncStationRecOnMode = None
|
|
63
|
+
self._working_mode: SyncStationWorkingMode = None
|
|
64
|
+
self._number_of_probes: int = None
|
|
65
|
+
self._bytes_configuration_A: Dict[
|
|
66
|
+
SyncStationProbeConfigMode, Dict[str, SyncStationDetectionMode | bool]
|
|
67
|
+
] = None
|
|
68
|
+
|
|
69
|
+
# Configuration Parameters B
|
|
70
|
+
self._bytes_configuration_B: Dict[str, int] = None
|
|
71
|
+
|
|
72
|
+
def _connect_to_device(self) -> bool:
|
|
73
|
+
super()._connect_to_device()
|
|
74
|
+
|
|
75
|
+
self._received_bytes: bytearray = bytearray()
|
|
76
|
+
self._make_request()
|
|
77
|
+
|
|
78
|
+
def _make_request(self) -> bool:
|
|
79
|
+
super()._make_request()
|
|
80
|
+
|
|
81
|
+
self._interface.connectToHost(
|
|
82
|
+
QHostAddress(self._connection_settings[0]),
|
|
83
|
+
self._connection_settings[1],
|
|
84
|
+
QIODevice.ReadWrite,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if not self._interface.waitForConnected(1000):
|
|
88
|
+
self._disconnect_from_device()
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
self.is_connected = True
|
|
92
|
+
self.connect_toggled.emit(True)
|
|
93
|
+
|
|
94
|
+
self._interface.readyRead.connect(self._read_data)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def _disconnect_from_device(self) -> None:
|
|
98
|
+
super()._disconnect_from_device()
|
|
99
|
+
|
|
100
|
+
self._interface.disconnectFromHost()
|
|
101
|
+
self._interface.readyRead.disconnect(self._read_data)
|
|
102
|
+
self._interface.close()
|
|
103
|
+
|
|
104
|
+
self.is_connected = False
|
|
105
|
+
self.connect_toggled.emit(False)
|
|
106
|
+
|
|
107
|
+
def configure_device(self, params) -> None:
|
|
108
|
+
super().configure_device(params)
|
|
109
|
+
|
|
110
|
+
success = self._configure_byte_sequence_A()
|
|
111
|
+
|
|
112
|
+
if not success:
|
|
113
|
+
print("Unable to configure device.")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
self._send_configuration_to_device()
|
|
117
|
+
|
|
118
|
+
self._is_configured = True
|
|
119
|
+
self.configure_toggled.emit(True)
|
|
120
|
+
|
|
121
|
+
def _configure_byte_sequence_A(self) -> None:
|
|
122
|
+
start_byte = 0
|
|
123
|
+
start_byte += (self._rec_on_mode.value - 1) << 6
|
|
124
|
+
|
|
125
|
+
self._sampling_frequency = SYNCSTATION_CHARACTERISTICS_DICT[
|
|
126
|
+
"channel_information"
|
|
127
|
+
][self._working_mode]["sampling_frequency"]
|
|
128
|
+
self._bytes_per_sample = SYNCSTATION_CHARACTERISTICS_DICT[
|
|
129
|
+
"channel_information"
|
|
130
|
+
][self._working_mode]["bytes_per_sample"]
|
|
131
|
+
|
|
132
|
+
self._configuration_command_A = bytearray()
|
|
133
|
+
self._number_of_channels = 0
|
|
134
|
+
self._number_of_bytes = 0
|
|
135
|
+
|
|
136
|
+
self._number_of_biosignal_channels = 0
|
|
137
|
+
self._number_of_auxiliary_channels = 0
|
|
138
|
+
self._biosignal_channel_indices = []
|
|
139
|
+
self._auxiliary_channel_indices = []
|
|
140
|
+
|
|
141
|
+
for key, value in self._bytes_configuration_A.items():
|
|
142
|
+
probe_command = 0
|
|
143
|
+
probe_command += (key.value - 1) << 4
|
|
144
|
+
probe_command += (self._working_mode.value - 1) << 3
|
|
145
|
+
probe_command += (value["detection_mode"].value - 1) << 1
|
|
146
|
+
probe_command += int(value["probe_status"])
|
|
147
|
+
|
|
148
|
+
if value["probe_status"]:
|
|
149
|
+
self._configuration_command_A.append(probe_command)
|
|
150
|
+
channels = PROBE_CHARACTERISTICS_DICT[key][DeviceChannelTypes.ALL]
|
|
151
|
+
biosignal_channels = PROBE_CHARACTERISTICS_DICT[key][
|
|
152
|
+
DeviceChannelTypes.BIOSIGNAL
|
|
153
|
+
]
|
|
154
|
+
auxiliary_channels = PROBE_CHARACTERISTICS_DICT[key][
|
|
155
|
+
DeviceChannelTypes.AUXILIARY
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
self._biosignal_channel_indices.append(
|
|
159
|
+
np.arange(
|
|
160
|
+
self._number_of_channels,
|
|
161
|
+
self._number_of_channels + biosignal_channels,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self._auxiliary_channel_indices.append(
|
|
166
|
+
np.arange(
|
|
167
|
+
self._number_of_channels + biosignal_channels,
|
|
168
|
+
self._number_of_channels + channels,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self._number_of_channels += channels
|
|
173
|
+
self._number_of_biosignal_channels += biosignal_channels
|
|
174
|
+
self._number_of_auxiliary_channels += auxiliary_channels
|
|
175
|
+
|
|
176
|
+
self._conversion_factor_biosignal = SYNCSTATION_CONVERSION_FACTOR_DICT[
|
|
177
|
+
value["detection_mode"]
|
|
178
|
+
]
|
|
179
|
+
self._conversion_factor_auxiliary = self._conversion_factor_biosignal
|
|
180
|
+
|
|
181
|
+
self._biosignal_channel_indices = np.hstack(self._biosignal_channel_indices)
|
|
182
|
+
self._auxiliary_channel_indices = np.hstack(self._auxiliary_channel_indices)
|
|
183
|
+
self._number_of_bytes = self._number_of_channels * self._bytes_per_sample
|
|
184
|
+
|
|
185
|
+
# Add SyncStation Channels
|
|
186
|
+
self._number_of_channels += SYNCSTATION_CHARACTERISTICS_DICT[
|
|
187
|
+
DeviceChannelTypes.ALL
|
|
188
|
+
]
|
|
189
|
+
self._number_of_auxiliary_channels += SYNCSTATION_CHARACTERISTICS_DICT[
|
|
190
|
+
DeviceChannelTypes.ALL
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
self._number_of_bytes += (
|
|
194
|
+
SYNCSTATION_CHARACTERISTICS_DICT[DeviceChannelTypes.ALL]
|
|
195
|
+
* SYNCSTATION_CHARACTERISTICS_DICT["bytes_per_sample"]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
self._samples_per_frame = int(
|
|
199
|
+
(1 / SYNCSTATION_CHARACTERISTICS_DICT["FPS"]) * self._sampling_frequency
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
self._buffer_size = int(self._number_of_bytes * self._samples_per_frame)
|
|
203
|
+
|
|
204
|
+
num_probes = len(self._configuration_command_A)
|
|
205
|
+
start_byte += num_probes << 1
|
|
206
|
+
self._configuration_command_A.insert(0, start_byte)
|
|
207
|
+
start_byte_ckc8 = self._crc_check(
|
|
208
|
+
self._configuration_command_A, len(self._configuration_command_A)
|
|
209
|
+
)
|
|
210
|
+
self._configuration_command_A.append(start_byte_ckc8)
|
|
211
|
+
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
def _crc_check(self, command_bytes: bytearray, command_length: int) -> bytes:
|
|
215
|
+
"""
|
|
216
|
+
Performs the Cyclic Redundancy Check (CRC) of the transmitted bytes.
|
|
217
|
+
|
|
218
|
+
Translated function from example code provided by OT Bioelettronica.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
command_bytes (bytearray):
|
|
222
|
+
Bytearray of the transmitted bytes.
|
|
223
|
+
|
|
224
|
+
command_length (int):
|
|
225
|
+
Length of the transmitted bytes.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
bytes:
|
|
229
|
+
CRC of the transmitted bytes.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
crc = 0
|
|
233
|
+
j = 0
|
|
234
|
+
|
|
235
|
+
while command_length > 0:
|
|
236
|
+
extracted_byte = command_bytes[j]
|
|
237
|
+
for i in range(8, 0, -1):
|
|
238
|
+
sum = crc % 2 ^ extracted_byte % 2
|
|
239
|
+
crc = crc // 2
|
|
240
|
+
|
|
241
|
+
if sum > 0:
|
|
242
|
+
crc_bin = format(crc, "08b")
|
|
243
|
+
a_bin = format(140, "08b")
|
|
244
|
+
|
|
245
|
+
str_list = []
|
|
246
|
+
|
|
247
|
+
for k in range(8):
|
|
248
|
+
str_list.append("0" if crc_bin[k] == a_bin[k] else "1")
|
|
249
|
+
|
|
250
|
+
crc = int("".join(str_list), 2)
|
|
251
|
+
|
|
252
|
+
extracted_byte = extracted_byte // 2
|
|
253
|
+
|
|
254
|
+
command_length -= 1
|
|
255
|
+
j += 1
|
|
256
|
+
|
|
257
|
+
return crc
|
|
258
|
+
|
|
259
|
+
def _configure_byte_sequence_B(self) -> None:
|
|
260
|
+
# TODO: Implement this method
|
|
261
|
+
...
|
|
262
|
+
|
|
263
|
+
def _send_configuration_to_device(self) -> None:
|
|
264
|
+
print(
|
|
265
|
+
f"Device configuration sent: {[int.from_bytes(self._configuration_command_A[i : i + 1], 'big') for i in range(len(self._configuration_command_A))]}"
|
|
266
|
+
)
|
|
267
|
+
self._interface.write(self._configuration_command_A)
|
|
268
|
+
|
|
269
|
+
def _stop_streaming(self):
|
|
270
|
+
self._configuration_command_A[0] -= 1
|
|
271
|
+
self._configuration_command_A[-1] = self._crc_check(
|
|
272
|
+
self._configuration_command_A, len(self._configuration_command_A) - 1
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
self._send_configuration_to_device()
|
|
276
|
+
|
|
277
|
+
self._is_streaming = False
|
|
278
|
+
self.stream_toggled.emit(False)
|
|
279
|
+
|
|
280
|
+
def _start_streaming(self):
|
|
281
|
+
self._configuration_command_A[0] += 1
|
|
282
|
+
self._configuration_command_A[-1] = self._crc_check(
|
|
283
|
+
self._configuration_command_A, len(self._configuration_command_A) - 1
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
self._send_configuration_to_device()
|
|
287
|
+
|
|
288
|
+
self._is_streaming = True
|
|
289
|
+
self.stream_toggled.emit(True)
|
|
290
|
+
|
|
291
|
+
def _clear_socket(self) -> None:
|
|
292
|
+
"""
|
|
293
|
+
Clears the socket from any remaining data.
|
|
294
|
+
"""
|
|
295
|
+
self._interface.readAll()
|
|
296
|
+
self._received_bytes = bytearray()
|
|
297
|
+
|
|
298
|
+
def _read_data(self) -> None:
|
|
299
|
+
if not self._is_streaming:
|
|
300
|
+
packet = self._interface.readAll()
|
|
301
|
+
|
|
302
|
+
else:
|
|
303
|
+
if self._interface.bytesAvailable() > 0:
|
|
304
|
+
|
|
305
|
+
packet = self._interface.readAll()
|
|
306
|
+
packet_bytearray = bytearray(packet.data())
|
|
307
|
+
|
|
308
|
+
if not packet_bytearray:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
self._received_bytes.extend(packet_bytearray)
|
|
312
|
+
|
|
313
|
+
while len(self._received_bytes) >= self._buffer_size:
|
|
314
|
+
self._process_data(
|
|
315
|
+
bytearray(self._received_bytes)[: self._buffer_size]
|
|
316
|
+
)
|
|
317
|
+
self._received_bytes = bytearray(self._received_bytes)[
|
|
318
|
+
self._buffer_size :
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
def _process_data(self, input: bytearray) -> None:
|
|
322
|
+
data: np.ndarray = np.frombuffer(input, dtype=np.uint8).astype(np.float32)
|
|
323
|
+
|
|
324
|
+
samples = self._samples_per_frame
|
|
325
|
+
data = np.reshape(data, (samples, self._number_of_bytes)).T
|
|
326
|
+
processed_data = self._bytes_to_integers(data)
|
|
327
|
+
|
|
328
|
+
# Emit the data
|
|
329
|
+
self.data_available.emit(processed_data)
|
|
330
|
+
self.biosignal_data_available.emit(self._extract_biosignal_data(processed_data))
|
|
331
|
+
self.auxiliary_data_available.emit(self._extract_auxiliary_data(processed_data))
|
|
332
|
+
|
|
333
|
+
def _integer_to_bytes(self, command: int) -> bytes:
|
|
334
|
+
return int(command).to_bytes(1, byteorder="big")
|
|
335
|
+
|
|
336
|
+
# Convert channels from bytes to integers
|
|
337
|
+
def _bytes_to_integers(
|
|
338
|
+
self,
|
|
339
|
+
data: np.ndarray,
|
|
340
|
+
) -> np.ndarray:
|
|
341
|
+
samples = self._samples_per_frame
|
|
342
|
+
frame_data = np.zeros((self._number_of_channels, samples), dtype=np.float32)
|
|
343
|
+
channels_to_read = 0
|
|
344
|
+
for device in list(SyncStationProbeConfigMode)[1:]:
|
|
345
|
+
if self._bytes_configuration_A[device]["probe_status"]:
|
|
346
|
+
channel_number = PROBE_CHARACTERISTICS_DICT[device][
|
|
347
|
+
DeviceChannelTypes.ALL
|
|
348
|
+
]
|
|
349
|
+
# Convert channel's byte value to integer
|
|
350
|
+
if self._working_mode == SyncStationWorkingMode.EMG:
|
|
351
|
+
channel_indices = (
|
|
352
|
+
np.arange(0, channel_number * 2, 2) + channels_to_read * 2
|
|
353
|
+
)
|
|
354
|
+
data_sub_matrix = self._decode_int16(data, channel_indices)
|
|
355
|
+
frame_data[
|
|
356
|
+
channels_to_read : channels_to_read + channel_number, :
|
|
357
|
+
] = data_sub_matrix
|
|
358
|
+
|
|
359
|
+
elif self._working_mode == SyncStationWorkingMode.EEG:
|
|
360
|
+
channel_indices = (
|
|
361
|
+
np.arange(0, channel_number * 3, 3) + channels_to_read * 2
|
|
362
|
+
)
|
|
363
|
+
data_sub_matrix = self._decode_int24(data, channel_indices)
|
|
364
|
+
frame_data[
|
|
365
|
+
channels_to_read : channels_to_read + channel_number, :
|
|
366
|
+
] = data_sub_matrix
|
|
367
|
+
|
|
368
|
+
channels_to_read += channel_number
|
|
369
|
+
del data_sub_matrix
|
|
370
|
+
del channel_indices
|
|
371
|
+
|
|
372
|
+
syncstation_aux_bytes_number = (
|
|
373
|
+
SYNCSTATION_CHARACTERISTICS_DICT[DeviceChannelTypes.ALL]
|
|
374
|
+
* SYNCSTATION_CHARACTERISTICS_DICT["bytes_per_sample"]
|
|
375
|
+
)
|
|
376
|
+
syncstation_aux_starting_byte = (
|
|
377
|
+
self._number_of_bytes - syncstation_aux_bytes_number
|
|
378
|
+
)
|
|
379
|
+
channel_indices = np.arange(
|
|
380
|
+
syncstation_aux_starting_byte,
|
|
381
|
+
syncstation_aux_starting_byte + syncstation_aux_bytes_number,
|
|
382
|
+
2,
|
|
383
|
+
)
|
|
384
|
+
data_sub_matrix = self._decode_int16(data, channel_indices)
|
|
385
|
+
frame_data[channels_to_read : channels_to_read + 6, :] = data_sub_matrix
|
|
386
|
+
return np.array(frame_data)
|
|
387
|
+
|
|
388
|
+
def _decode_int24(
|
|
389
|
+
self, data: np.ndarray, channel_indices: np.ndarray
|
|
390
|
+
) -> np.ndarray:
|
|
391
|
+
data_sub_matrix = (
|
|
392
|
+
data[channel_indices, :] * 2**16
|
|
393
|
+
+ data[channel_indices + 1, :] * 2**8
|
|
394
|
+
+ data[channel_indices + 2, :]
|
|
395
|
+
)
|
|
396
|
+
negative_indices = np.where(data_sub_matrix >= 2**23)
|
|
397
|
+
data_sub_matrix[negative_indices] -= 2**24
|
|
398
|
+
|
|
399
|
+
return data_sub_matrix
|
|
400
|
+
|
|
401
|
+
def _decode_int16(
|
|
402
|
+
self, data: np.ndarray, channel_indices: np.ndarray
|
|
403
|
+
) -> np.ndarray:
|
|
404
|
+
data_sub_matrix = data[channel_indices, :] * 2**8 + data[channel_indices + 1, :]
|
|
405
|
+
negative_indices = np.where(data_sub_matrix >= 2**15)
|
|
406
|
+
data_sub_matrix[negative_indices] -= 2**16
|
|
407
|
+
return data_sub_matrix
|