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.
Files changed (41) hide show
  1. biosignal_device_interface/constants/devices/__init__.py +3 -3
  2. biosignal_device_interface/constants/devices/core/base_device_constants.py +61 -61
  3. biosignal_device_interface/constants/devices/otb/otb_muovi_constants.py +129 -129
  4. biosignal_device_interface/constants/devices/otb/otb_quattrocento_constants.py +313 -313
  5. biosignal_device_interface/constants/devices/otb/otb_quattrocento_light_constants.py +59 -59
  6. biosignal_device_interface/constants/devices/otb/otb_syncstation_constants.py +233 -233
  7. biosignal_device_interface/constants/plots/color_palette.py +59 -59
  8. biosignal_device_interface/devices/__init__.py +17 -17
  9. biosignal_device_interface/devices/core/base_device.py +424 -412
  10. biosignal_device_interface/devices/otb/__init__.py +29 -29
  11. biosignal_device_interface/devices/otb/otb_muovi.py +290 -290
  12. biosignal_device_interface/devices/otb/otb_quattrocento.py +332 -332
  13. biosignal_device_interface/devices/otb/otb_quattrocento_light.py +210 -210
  14. biosignal_device_interface/devices/otb/otb_syncstation.py +407 -407
  15. biosignal_device_interface/gui/device_template_widgets/all_devices_widget.py +51 -51
  16. biosignal_device_interface/gui/device_template_widgets/core/base_device_widget.py +130 -130
  17. biosignal_device_interface/gui/device_template_widgets/core/base_multiple_devices_widget.py +108 -108
  18. biosignal_device_interface/gui/device_template_widgets/otb/otb_devices_widget.py +44 -44
  19. biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_plus_widget.py +158 -158
  20. biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_widget.py +158 -158
  21. biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_light_widget.py +174 -174
  22. biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_widget.py +260 -260
  23. biosignal_device_interface/gui/device_template_widgets/otb/otb_syncstation_widget.py +262 -262
  24. biosignal_device_interface/gui/plot_widgets/biosignal_plot_widget.py +501 -501
  25. biosignal_device_interface/gui/ui/devices_template_widget.ui +38 -38
  26. biosignal_device_interface/gui/ui/otb_muovi_plus_template_widget.ui +171 -171
  27. biosignal_device_interface/gui/ui/otb_muovi_template_widget.ui +171 -171
  28. biosignal_device_interface/gui/ui/otb_quattrocento_light_template_widget.ui +266 -266
  29. biosignal_device_interface/gui/ui/otb_quattrocento_template_widget.ui +415 -415
  30. biosignal_device_interface/gui/ui/otb_syncstation_template_widget.ui +732 -732
  31. biosignal_device_interface/gui/ui_compiled/devices_template_widget.py +56 -56
  32. biosignal_device_interface/gui/ui_compiled/otb_muovi_plus_template_widget.py +153 -153
  33. biosignal_device_interface/gui/ui_compiled/otb_muovi_template_widget.py +153 -153
  34. biosignal_device_interface/gui/ui_compiled/otb_quattrocento_light_template_widget.py +217 -217
  35. biosignal_device_interface/gui/ui_compiled/otb_quattrocento_template_widget.py +318 -318
  36. biosignal_device_interface/gui/ui_compiled/otb_syncstation_template_widget.py +495 -495
  37. {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.1a2.dist-info}/LICENSE +675 -675
  38. {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.1a2.dist-info}/METADATA +2 -2
  39. biosignal_device_interface-0.2.1a2.dist-info/RECORD +46 -0
  40. {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.1a2.dist-info}/WHEEL +1 -1
  41. 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