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.
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 +500 -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.2.dist-info}/METADATA +3 -2
  38. biosignal_device_interface-0.2.2.dist-info/RECORD +46 -0
  39. {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.2.dist-info}/WHEEL +1 -1
  40. {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.2.dist-info/licenses}/LICENSE +675 -675
  41. biosignal_device_interface-0.2.1a1.dist-info/RECORD +0 -46
@@ -1,412 +1,424 @@
1
- """
2
- Base Device class for real-time interfaces to hardware devices.
3
- Developer: Dominik I. Braun
4
- Contact: dome.braun@fau.de
5
- Last Update: 2024-06-05
6
- """
7
-
8
- # Python Libraries
9
- from __future__ import annotations
10
- from typing import TYPE_CHECKING, Union, Dict, Tuple
11
-
12
- from abc import abstractmethod
13
- import socket
14
- import psutil
15
- from PySide6.QtCore import QObject, Signal, QTimer
16
- import numpy as np
17
- import re
18
-
19
- # Local Libraries
20
- from biosignal_device_interface.constants.devices.core.base_device_constants import (
21
- DeviceType,
22
- DEVICE_NAME_DICT,
23
- )
24
-
25
- # Type Checking
26
- if TYPE_CHECKING:
27
- # Python Libraries
28
- from PySide6.QtWidgets import QMainWindow, QWidget
29
- from PySide6.QtNetwork import QTcpSocket, QUdpSocket, QTcpServer
30
- from PySide6.QtSerialPort import QSerialPort
31
- from aenum import Enum
32
-
33
-
34
- class BaseDevice(QObject):
35
- # Signals
36
- connect_toggled: Signal = Signal(bool)
37
- configure_toggled: Signal = Signal(bool)
38
- stream_toggled: Signal = Signal(bool)
39
- data_available: Signal = Signal(np.ndarray)
40
- biosignal_data_available: Signal = Signal(np.ndarray)
41
- auxiliary_data_available: Signal = Signal(np.ndarray)
42
-
43
- def __init__(self, parent: Union[QMainWindow, QWidget] = None, **kwargs) -> None:
44
- super().__init__(parent)
45
-
46
- # Device Parameters
47
- self._device_type: DeviceType = None
48
-
49
- # Device Information
50
- self._sampling_frequency: int | None = None
51
- self._number_of_channels: int | None = None
52
- self._number_of_biosignal_channels: int | None = None
53
- self._biosignal_channel_indices: list[int] | None = None
54
- self._number_of_auxiliary_channels: int | None = None
55
- self._auxiliary_channel_indices: list[int] | None = None
56
- self._samples_per_frame: int | None = None
57
- self._bytes_per_sample: int | None = None
58
-
59
- self._conversion_factor_biosignal: float = None # Returns mV
60
- self._conversion_factor_auxiliary: float = None # Returns mV
61
-
62
- # Connection Parameters
63
- self._interface: QTcpServer | QTcpSocket | QUdpSocket | QSerialPort | None = (
64
- None
65
- )
66
- self._connection_settings: Tuple[str, int] | None = None
67
- self._buffer_size: int | None = None
68
- self._received_bytes: bytearray | None = None
69
-
70
- self._connection_timeout_timer: QTimer = QTimer()
71
- self._connection_timeout_timer.setSingleShot(True)
72
- self._connection_timeout_timer.timeout.connect(self._disconnect_from_device)
73
- self._connection_timeout_timer.setInterval(1000)
74
-
75
- # Device Status
76
- self.is_connected: bool = False
77
- self._is_configured: bool = False
78
- self._is_streaming: bool = False
79
-
80
- @abstractmethod
81
- def _connect_to_device(self) -> bool:
82
- """
83
- Function to attempt a connection to the devices.
84
-
85
- Returns:
86
- bool:
87
- Success of the connection attempt.
88
- """
89
- pass
90
-
91
- @abstractmethod
92
- def _make_request(self) -> bool:
93
- """
94
- Requests a connection or checks if someone connected to the server.
95
- After connection is successful, the Signal connected_signal emits True
96
- and sets the current state is_connected to True.
97
-
98
- Returns:
99
- bool:
100
- Returns True if request was successfully. False if not.
101
- """
102
- pass
103
-
104
- @abstractmethod
105
- def _disconnect_from_device(self) -> bool:
106
- """
107
- Closes the connection to the device.
108
-
109
- self.interface closes and is set to None.
110
- Device state is_connected is set to False.
111
- Signal connected_signal emits False.
112
-
113
- Returns:
114
- bool:
115
- Success of the disconnection attempt.
116
- """
117
- self.is_connected = False
118
- self.connect_toggled.emit(False)
119
- self._is_configured = False
120
- self.configure_toggled.emit(False)
121
-
122
- @abstractmethod
123
- def configure_device(self, params: Dict[str, Union[Enum, Dict[str, Enum]]]) -> None: # type: ignore
124
- """
125
- Sends a configuration byte sequence based on selected params to the device.
126
- An overview of possible configurations can be seen in
127
- biosignal_device_interface/constants/{device}.py.
128
-
129
- E.g., enums/sessantaquattro.py
130
-
131
-
132
- Args:
133
- params (Dict[str, Union[Enum, Dict[str, Enum]]]):
134
- Dictionary that holds the configuration settings
135
- to which the device should be configured to.
136
-
137
- The first one should be the attributes (configuration mode) name,
138
- and the second its respective value. Orient yourself on the
139
- enums of the device to choose the correct configuration settings.
140
- """
141
- self._update_configuration_parameters(params)
142
-
143
- @abstractmethod
144
- def _start_streaming(self) -> None:
145
- """
146
- Sends the command to start the streaming to the device.
147
-
148
- if successful:
149
- Device state is_streaming is set to True.
150
- Signal streaming_signal emits True.
151
- """
152
- self._is_streaming = True
153
- self.stream_toggled.emit(True)
154
-
155
- @abstractmethod
156
- def _stop_streaming(self) -> None:
157
- """
158
- Sends the command to stop the streaing to the device
159
-
160
- if successful:
161
- Device state is_streaming is set to False.
162
- Signal streaming_signal emits False.
163
- """
164
- self._is_streaming = False
165
- self.stream_toggled.emit(False)
166
-
167
- @abstractmethod
168
- def clear_socket(self) -> None:
169
- """Reads all the bytes from the buffer."""
170
- pass
171
-
172
- @abstractmethod
173
- def _read_data(self) -> None:
174
- """
175
- This function is called when bytes are ready to be read in the buffer.
176
- After reading the bytes from the buffer, _process_data is called to
177
- decode and process the raw data.
178
- """
179
- pass
180
-
181
- @abstractmethod
182
- def _process_data(self, input: bytearray) -> None:
183
- """
184
- Decodes the transmitted bytes and convert them to respective
185
- output format (e.g., mV).
186
-
187
- Emits the processed data through the Signal data_available_signal
188
- which can be connected to a function using:
189
- {self.device}.data_available_signal.connect(your_custom_function).
190
-
191
- This works perfectly fine outside of this class.
192
-
193
- Your custom function your_custom_function needs to have a parameter
194
- "data" which is of type np.ndarray.
195
-
196
-
197
- In case that the current configuration of the device was requested,
198
- the configuration is provided through the Signal
199
- configuration_available_signal that emits the current parameters
200
- in a dictionary.
201
-
202
- Args:
203
- input (bytearray):
204
- Bytearray of the transmitted raw data.
205
- """
206
- pass
207
-
208
- def _update_configuration_parameters(
209
- self, params: Dict[str, Union[Enum, Dict[str, Enum]]]
210
- ) -> None:
211
- """
212
- Updates the device attributes with the new configuration parameters.
213
-
214
- Args:
215
- params (Dict[str, Union[Enum, Dict[str, Enum]]]):
216
- Dictionary that holds the configuration settings
217
- to which the device should be configured to.
218
-
219
- The first one should be the attributes (configuration mode) name,
220
- and the second its respective value. Orient yourself on the
221
- enums of the device to choose the correct configuration settings.
222
- """
223
- for key, value in params.items():
224
- key = "_" + key
225
- if hasattr(self, key):
226
- setattr(self, key, value)
227
- else:
228
- print(
229
- f"Attribute '{key}' not found in the class of {self._device_type.name}",
230
- )
231
-
232
- def _extract_biosignal_data(
233
- self, data: np.ndarray, milli_volts: bool = True
234
- ) -> np.ndarray:
235
- """
236
- Extracts the biosignals from the transmitted data.
237
-
238
- Args:
239
- data (np.ndarray):
240
- Raw data that got transmitted.
241
-
242
- milli_volts (bool, optional):
243
- If True, the biosignal data is converted to milli volts.
244
- Defaults to True.
245
-
246
- Returns:
247
- np.ndarray:
248
- Extracted biosignal channels.
249
- """
250
- biosignal_data = data[self._biosignal_channel_indices]
251
- if milli_volts:
252
- return biosignal_data * self._conversion_factor_biosignal
253
- return biosignal_data
254
-
255
- def _extract_auxiliary_data(
256
- self, data: np.ndarray, milli_volts: bool = True
257
- ) -> np.ndarray:
258
- """
259
- Extract auxiliary channels from the transmitted data.
260
-
261
- Args:
262
- data (np.ndarray):
263
- Raw data that got transmitted.
264
- milli_volts (bool, optional):
265
- If True, the auxiliary data is converted to milli volts.
266
- Defaults to True.
267
-
268
- Returns:
269
- np.ndarray:
270
- Extracted auxiliary channel data.
271
- """
272
-
273
- if milli_volts:
274
- return (
275
- data[self._auxiliary_channel_indices]
276
- * self._conversion_factor_auxiliary
277
- )
278
- return data[self._auxiliary_channel_indices]
279
-
280
- def toggle_connection(self, settings: Tuple[str, int] = None) -> bool:
281
- """
282
- Toggles the connection to the device.
283
-
284
- Args:
285
- settings (Tuple[str, int], optional):
286
- If CommunicationProtocol.TCPIP:
287
- Tuple[0] = IP -> string
288
- Tuple[1] = Port -> int
289
-
290
- If CommunicationProtocol.SERIAL pr CommunicationProtocol.USB:
291
- Tuple[0] = COM Port -> string
292
- Tuple[1] = Baudrate -> int
293
-
294
- Defaults to None.
295
-
296
- Returns:
297
- bool:
298
- True if connection attempt was successfully. False if not.
299
- """
300
- self._connection_settings = settings
301
-
302
- if self.is_connected:
303
- if self._is_streaming:
304
- self.toggle_streaming()
305
-
306
- success: bool = self._disconnect_from_device()
307
- else:
308
- success: bool = self._connect_to_device()
309
-
310
- return success
311
-
312
- def toggle_streaming(self) -> None:
313
- """
314
- Toggles the current state of the streaming.
315
- If device is streaming, the streaming is stopped and vice versa.
316
- """
317
- if self._is_streaming:
318
- self._stop_streaming()
319
- self.clear_socket()
320
- else:
321
- self._start_streaming()
322
-
323
- self.clear_socket()
324
-
325
- def get_device_information(self) -> Dict[str, Enum | int | float | str]:
326
- """
327
- Gets the current configuration of the device.
328
-
329
- Returns:
330
- Dict[str, Enum | int | float | str]:
331
- Dictionary that holds information about the
332
- current device configuration and status.
333
- """
334
- return {
335
- "name": DEVICE_NAME_DICT[self._device_type],
336
- "sampling_frequency": self._sampling_frequency,
337
- "number_of_channels": self._number_of_channels,
338
- "number_of_biosignal_channels": self._number_of_biosignal_channels,
339
- "number_of_auxiliary_channels": self._number_of_auxiliary_channels,
340
- "samples_per_frame": self._samples_per_frame,
341
- "conversion_factor_biosignal": self._conversion_factor_biosignal,
342
- "conversion_factor_auxiliary": self._conversion_factor_auxiliary,
343
- }
344
-
345
- def check_valid_ip(self, ip_address: str) -> bool:
346
- """
347
- Checks if the provided IP is valid.
348
-
349
- Args:
350
- ip (str): IP to be checked.
351
-
352
- Returns:
353
- bool: True if IP is valid. False if not.
354
- """
355
- ip_pattern = re.compile(
356
- r"^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
357
- r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
358
- r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
359
- r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
360
- )
361
-
362
- return bool(ip_pattern.match(ip_address))
363
-
364
- def check_valid_port(self, port: str) -> bool:
365
- """
366
- Checks if the provided port is valid.
367
-
368
- Args:
369
- port (str): Port to be checked.
370
-
371
- Returns:
372
- bool: True if port is valid. False if not.
373
- """
374
- try:
375
- port_num = int(port)
376
- return 0 <= port_num <= 65535
377
- except ValueError:
378
- return False
379
-
380
- def get_server_wifi_ip_address(self) -> list[str]:
381
- """
382
- Returns the IP address of the host server.
383
- """
384
- try:
385
- # Get all network interfaces
386
- interfaces = psutil.net_if_addrs()
387
-
388
- addresses_return = []
389
-
390
- # Iterate through interfaces to find the one associated with WiFi
391
- for interface, addresses in interfaces.items():
392
- if (
393
- "wlan" in interface.lower()
394
- or "wi-fi" in interface.lower()
395
- or "wifi" in interface.lower()
396
- or "wireless" in interface.lower()
397
- or "en0" in interface.lower()
398
- or "wlp" in interface.lower()
399
- or "wln" in interface.lower()
400
- ):
401
- for address in addresses:
402
- # Check if the address is an IPv4 address and not a loopback or virtual address
403
- if (
404
- address.family == socket.AF_INET
405
- and not address.address.startswith("127.")
406
- ):
407
- addresses_return.append(address.address)
408
-
409
- return addresses_return[::-1] if addresses_return else [""]
410
-
411
- except Exception:
412
- return [""]
1
+ """
2
+ Base Device class for real-time interfaces to hardware devices.
3
+ Developer: Dominik I. Braun
4
+ Contact: dome.braun@fau.de
5
+ Last Update: 2024-06-05
6
+ """
7
+
8
+ # Python Libraries
9
+ from __future__ import annotations
10
+ from typing import TYPE_CHECKING, Union, Dict, Tuple
11
+
12
+ from abc import abstractmethod
13
+ import socket
14
+ import psutil
15
+ from PySide6.QtCore import QObject, Signal, QTimer
16
+ import numpy as np
17
+ import re
18
+
19
+ # Local Libraries
20
+ from biosignal_device_interface.constants.devices.core.base_device_constants import (
21
+ DeviceType,
22
+ DEVICE_NAME_DICT,
23
+ )
24
+
25
+ # Type Checking
26
+ if TYPE_CHECKING:
27
+ # Python Libraries
28
+ from PySide6.QtWidgets import QMainWindow, QWidget
29
+ from PySide6.QtNetwork import QTcpSocket, QUdpSocket, QTcpServer
30
+ from PySide6.QtSerialPort import QSerialPort
31
+ from aenum import Enum
32
+
33
+ import warnings
34
+
35
+ # Set to keep track of seen error messages
36
+ _seen_error_messages = set()
37
+
38
+
39
+ def warn_once(e):
40
+ """
41
+ Issue a warning for the given error, but only once per unique message.
42
+
43
+ Args:
44
+ e: Exception instance whose message will be used to track uniqueness.
45
+ """
46
+ error_message = str(e)
47
+ if error_message not in _seen_error_messages:
48
+ warnings.warn(f"An error occurred: {error_message}", UserWarning)
49
+ _seen_error_messages.add(error_message)
50
+
51
+ class BaseDevice(QObject):
52
+ # Signals
53
+ connect_toggled: Signal = Signal(bool)
54
+ configure_toggled: Signal = Signal(bool)
55
+ stream_toggled: Signal = Signal(bool)
56
+ data_available: Signal = Signal(np.ndarray)
57
+ biosignal_data_available: Signal = Signal(np.ndarray)
58
+ auxiliary_data_available: Signal = Signal(np.ndarray)
59
+
60
+ def __init__(self, parent: Union[QMainWindow, QWidget] = None, **kwargs) -> None:
61
+ super().__init__(parent)
62
+
63
+ # Device Parameters
64
+ self._device_type: DeviceType = None
65
+
66
+ # Device Information
67
+ self._sampling_frequency: int | None = None
68
+ self._number_of_channels: int | None = None
69
+ self._number_of_biosignal_channels: int | None = None
70
+ self._biosignal_channel_indices: list[int] | None = None
71
+ self._number_of_auxiliary_channels: int | None = None
72
+ self._auxiliary_channel_indices: list[int] | None = None
73
+ self._samples_per_frame: int | None = None
74
+ self._bytes_per_sample: int | None = None
75
+
76
+ self._conversion_factor_biosignal: float = None # Returns mV
77
+ self._conversion_factor_auxiliary: float = None # Returns mV
78
+
79
+ # Connection Parameters
80
+ self._interface: QTcpServer | QTcpSocket | QUdpSocket | QSerialPort | None = (
81
+ None
82
+ )
83
+ self._connection_settings: Tuple[str, int] | None = None
84
+ self._buffer_size: int | None = None
85
+ self._received_bytes: bytearray | None = None
86
+
87
+ self._connection_timeout_timer: QTimer = QTimer()
88
+ self._connection_timeout_timer.setSingleShot(True)
89
+ self._connection_timeout_timer.timeout.connect(self._disconnect_from_device)
90
+ self._connection_timeout_timer.setInterval(1000)
91
+
92
+ # Device Status
93
+ self.is_connected: bool = False
94
+ self._is_configured: bool = False
95
+ self._is_streaming: bool = False
96
+
97
+ @abstractmethod
98
+ def _connect_to_device(self) -> bool:
99
+ """
100
+ Function to attempt a connection to the devices.
101
+
102
+ Returns:
103
+ bool:
104
+ Success of the connection attempt.
105
+ """
106
+ pass
107
+
108
+ @abstractmethod
109
+ def _make_request(self) -> bool:
110
+ """
111
+ Requests a connection or checks if someone connected to the server.
112
+ After connection is successful, the Signal connected_signal emits True
113
+ and sets the current state is_connected to True.
114
+
115
+ Returns:
116
+ bool:
117
+ Returns True if request was successfully. False if not.
118
+ """
119
+ pass
120
+
121
+ @abstractmethod
122
+ def _disconnect_from_device(self) -> bool:
123
+ """
124
+ Closes the connection to the device.
125
+
126
+ self.interface closes and is set to None.
127
+ Device state is_connected is set to False.
128
+ Signal connected_signal emits False.
129
+
130
+ Returns:
131
+ bool:
132
+ Success of the disconnection attempt.
133
+ """
134
+ self.is_connected = False
135
+ self.connect_toggled.emit(False)
136
+ self._is_configured = False
137
+ self.configure_toggled.emit(False)
138
+
139
+ @abstractmethod
140
+ def configure_device(self, params: Dict[str, Union[Enum, Dict[str, Enum]]]) -> None: # type: ignore
141
+ """
142
+ Sends a configuration byte sequence based on selected params to the device.
143
+ An overview of possible configurations can be seen in
144
+ biosignal_device_interface/constants/{device}.py.
145
+
146
+ E.g., enums/sessantaquattro.py
147
+
148
+
149
+ Args:
150
+ params (Dict[str, Union[Enum, Dict[str, Enum]]]):
151
+ Dictionary that holds the configuration settings
152
+ to which the device should be configured to.
153
+
154
+ The first one should be the attributes (configuration mode) name,
155
+ and the second its respective value. Orient yourself on the
156
+ enums of the device to choose the correct configuration settings.
157
+ """
158
+ self._update_configuration_parameters(params)
159
+
160
+ @abstractmethod
161
+ def _start_streaming(self) -> None:
162
+ """
163
+ Sends the command to start the streaming to the device.
164
+
165
+ if successful:
166
+ Device state is_streaming is set to True.
167
+ Signal streaming_signal emits True.
168
+ """
169
+ self._is_streaming = True
170
+ self.stream_toggled.emit(True)
171
+
172
+ @abstractmethod
173
+ def _stop_streaming(self) -> None:
174
+ """
175
+ Sends the command to stop the streaing to the device
176
+
177
+ if successful:
178
+ Device state is_streaming is set to False.
179
+ Signal streaming_signal emits False.
180
+ """
181
+ self._is_streaming = False
182
+ self.stream_toggled.emit(False)
183
+
184
+ @abstractmethod
185
+ def clear_socket(self) -> None:
186
+ """Reads all the bytes from the buffer."""
187
+ pass
188
+
189
+ @abstractmethod
190
+ def _read_data(self) -> None:
191
+ """
192
+ This function is called when bytes are ready to be read in the buffer.
193
+ After reading the bytes from the buffer, _process_data is called to
194
+ decode and process the raw data.
195
+ """
196
+ pass
197
+
198
+ @abstractmethod
199
+ def _process_data(self, input: bytearray) -> None:
200
+ """
201
+ Decodes the transmitted bytes and convert them to respective
202
+ output format (e.g., mV).
203
+
204
+ Emits the processed data through the Signal data_available_signal
205
+ which can be connected to a function using:
206
+ {self.device}.data_available_signal.connect(your_custom_function).
207
+
208
+ This works perfectly fine outside of this class.
209
+
210
+ Your custom function your_custom_function needs to have a parameter
211
+ "data" which is of type np.ndarray.
212
+
213
+
214
+ In case that the current configuration of the device was requested,
215
+ the configuration is provided through the Signal
216
+ configuration_available_signal that emits the current parameters
217
+ in a dictionary.
218
+
219
+ Args:
220
+ input (bytearray):
221
+ Bytearray of the transmitted raw data.
222
+ """
223
+ pass
224
+
225
+ def _update_configuration_parameters(
226
+ self, params: Dict[str, Union[Enum, Dict[str, Enum]]]
227
+ ) -> None:
228
+ """
229
+ Updates the device attributes with the new configuration parameters.
230
+
231
+ Args:
232
+ params (Dict[str, Union[Enum, Dict[str, Enum]]]):
233
+ Dictionary that holds the configuration settings
234
+ to which the device should be configured to.
235
+
236
+ The first one should be the attributes (configuration mode) name,
237
+ and the second its respective value. Orient yourself on the
238
+ enums of the device to choose the correct configuration settings.
239
+ """
240
+ for key, value in params.items():
241
+ key = "_" + key
242
+ if hasattr(self, key):
243
+ setattr(self, key, value)
244
+ else:
245
+ print(
246
+ f"Attribute '{key}' not found in the class of {self._device_type.name}",
247
+ )
248
+
249
+ def _extract_biosignal_data(
250
+ self, data: np.ndarray, milli_volts: bool = True
251
+ ) -> np.ndarray:
252
+ """
253
+ Extracts the biosignals from the transmitted data.
254
+
255
+ Args:
256
+ data (np.ndarray):
257
+ Raw data that got transmitted.
258
+
259
+ milli_volts (bool, optional):
260
+ If True, the biosignal data is converted to milli volts.
261
+ Defaults to True.
262
+
263
+ Returns:
264
+ np.ndarray:
265
+ Extracted biosignal channels.
266
+ """
267
+ biosignal_data = data[self._biosignal_channel_indices]
268
+ if milli_volts:
269
+ return biosignal_data * self._conversion_factor_biosignal
270
+ return biosignal_data
271
+
272
+ def _extract_auxiliary_data(
273
+ self, data: np.ndarray, milli_volts: bool = True
274
+ ) -> np.ndarray:
275
+ """
276
+ Extract auxiliary channels from the transmitted data.
277
+
278
+ Args:
279
+ data (np.ndarray):
280
+ Raw data that got transmitted.
281
+ milli_volts (bool, optional):
282
+ If True, the auxiliary data is converted to milli volts.
283
+ Defaults to True.
284
+
285
+ Returns:
286
+ np.ndarray:
287
+ Extracted auxiliary channel data.
288
+ """
289
+
290
+ if milli_volts:
291
+ return (
292
+ data[self._auxiliary_channel_indices]
293
+ * self._conversion_factor_auxiliary
294
+ )
295
+ return data[self._auxiliary_channel_indices]
296
+
297
+ def toggle_connection(self, settings: Tuple[str, int] = None) -> bool:
298
+ """
299
+ Toggles the connection to the device.
300
+
301
+ Args:
302
+ settings (Tuple[str, int], optional):
303
+ If CommunicationProtocol.TCPIP:
304
+ Tuple[0] = IP -> string
305
+ Tuple[1] = Port -> int
306
+
307
+ If CommunicationProtocol.SERIAL pr CommunicationProtocol.USB:
308
+ Tuple[0] = COM Port -> string
309
+ Tuple[1] = Baudrate -> int
310
+
311
+ Defaults to None.
312
+
313
+ Returns:
314
+ bool:
315
+ True if connection attempt was successfully. False if not.
316
+ """
317
+ self._connection_settings = settings
318
+
319
+ if self.is_connected:
320
+ if self._is_streaming:
321
+ self.toggle_streaming()
322
+
323
+ success: bool = self._disconnect_from_device()
324
+ else:
325
+ success: bool = self._connect_to_device()
326
+
327
+ return success
328
+
329
+ def toggle_streaming(self) -> None:
330
+ """
331
+ Toggles the current state of the streaming.
332
+ If device is streaming, the streaming is stopped and vice versa.
333
+ """
334
+ if self._is_streaming:
335
+ self._stop_streaming()
336
+ self.clear_socket()
337
+ else:
338
+ self._start_streaming()
339
+
340
+ self.clear_socket()
341
+
342
+ def get_device_information(self) -> Dict[str, Enum | int | float | str]:
343
+ """
344
+ Gets the current configuration of the device.
345
+
346
+ Returns:
347
+ Dict[str, Enum | int | float | str]:
348
+ Dictionary that holds information about the
349
+ current device configuration and status.
350
+ """
351
+ return {
352
+ "name": DEVICE_NAME_DICT[self._device_type],
353
+ "sampling_frequency": self._sampling_frequency,
354
+ "number_of_channels": self._number_of_channels,
355
+ "number_of_biosignal_channels": self._number_of_biosignal_channels,
356
+ "number_of_auxiliary_channels": self._number_of_auxiliary_channels,
357
+ "samples_per_frame": self._samples_per_frame,
358
+ "conversion_factor_biosignal": self._conversion_factor_biosignal,
359
+ "conversion_factor_auxiliary": self._conversion_factor_auxiliary,
360
+ }
361
+
362
+ def check_valid_ip(self, ip_address: str) -> bool:
363
+ """
364
+ Checks if the provided IP is valid.
365
+
366
+ Args:
367
+ ip (str): IP to be checked.
368
+
369
+ Returns:
370
+ bool: True if IP is valid. False if not.
371
+ """
372
+ ip_pattern = re.compile(
373
+ r"^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
374
+ r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
375
+ r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
376
+ r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
377
+ )
378
+
379
+ return bool(ip_pattern.match(ip_address))
380
+
381
+ def check_valid_port(self, port: str) -> bool:
382
+ """
383
+ Checks if the provided port is valid.
384
+
385
+ Args:
386
+ port (str): Port to be checked.
387
+
388
+ Returns:
389
+ bool: True if port is valid. False if not.
390
+ """
391
+ try:
392
+ port_num = int(port)
393
+ return 0 <= port_num <= 65535
394
+ except ValueError:
395
+ return False
396
+
397
+ def get_server_wifi_ip_address(self) -> list[str]:
398
+ """
399
+ Returns the IP address of the host server.
400
+ """
401
+ try:
402
+ # Get all network interfaces
403
+ interfaces = psutil.net_if_addrs()
404
+
405
+ addresses_return = []
406
+
407
+ # Iterate through interfaces to find the one associated with WiFi
408
+ for interface, addresses in interfaces.items():
409
+ if (
410
+ any(keyword in interface.lower() for keyword in ["wlan", "wi-fi", "wifi", "wireless", "en0", "wlp", "wln", "wl"])
411
+ ):
412
+ for address in addresses:
413
+ # Check if the address is an IPv4 address and not a loopback or virtual address
414
+ if (
415
+ address.family == socket.AF_INET
416
+ and not address.address.startswith("127.")
417
+ ):
418
+ addresses_return.append(address.address)
419
+
420
+ return addresses_return[::-1] if addresses_return else [""]
421
+
422
+ except Exception as e:
423
+ warn_once("Error occurred while getting server IP address: " + str(e))
424
+ return [""]