biosignal-device-interface 0.1.0__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 (35) hide show
  1. biosignal_device_interface/__init__.py +4 -0
  2. biosignal_device_interface/constants/__init__.py +0 -0
  3. biosignal_device_interface/constants/devices/__init__.py +0 -0
  4. biosignal_device_interface/constants/devices/core/base_device_constants.py +51 -0
  5. biosignal_device_interface/constants/devices/otb/otb_muovi_constants.py +129 -0
  6. biosignal_device_interface/constants/devices/otb/otb_quattrocento_constants.py +59 -0
  7. biosignal_device_interface/constants/plots/color_palette.py +59 -0
  8. biosignal_device_interface/devices/__init__.py +9 -0
  9. biosignal_device_interface/devices/core/__init__.py +0 -0
  10. biosignal_device_interface/devices/core/base_device.py +413 -0
  11. biosignal_device_interface/devices/otb/__init__.py +9 -0
  12. biosignal_device_interface/devices/otb/otb_muovi.py +291 -0
  13. biosignal_device_interface/devices/otb/otb_quattrocento.py +235 -0
  14. biosignal_device_interface/gui/device_template_widgets/__init__.py +6 -0
  15. biosignal_device_interface/gui/device_template_widgets/all_devices_widget.py +39 -0
  16. biosignal_device_interface/gui/device_template_widgets/core/base_device_widget.py +121 -0
  17. biosignal_device_interface/gui/device_template_widgets/core/base_multiple_devices_widget.py +105 -0
  18. biosignal_device_interface/gui/device_template_widgets/otb/__init__.py +10 -0
  19. biosignal_device_interface/gui/device_template_widgets/otb/otb_devices_widget.py +32 -0
  20. biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_plus_widget.py +158 -0
  21. biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_widget.py +158 -0
  22. biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_light_widget.py +170 -0
  23. biosignal_device_interface/gui/plot_widgets/biosignal_plot_widget.py +496 -0
  24. biosignal_device_interface/gui/ui/devices_template_widget.ui +38 -0
  25. biosignal_device_interface/gui/ui/otb_muovi_plus_template_widget.ui +171 -0
  26. biosignal_device_interface/gui/ui/otb_muovi_template_widget.ui +171 -0
  27. biosignal_device_interface/gui/ui/otb_quattrocento_light_template_widget.ui +266 -0
  28. biosignal_device_interface/gui/ui_compiled/devices_template_widget.py +56 -0
  29. biosignal_device_interface/gui/ui_compiled/otb_muovi_plus_template_widget.py +153 -0
  30. biosignal_device_interface/gui/ui_compiled/otb_muovi_template_widget.py +153 -0
  31. biosignal_device_interface/gui/ui_compiled/otb_quattrocento_light_template_widget.py +217 -0
  32. biosignal_device_interface-0.1.0.dist-info/LICENSE +395 -0
  33. biosignal_device_interface-0.1.0.dist-info/METADATA +138 -0
  34. biosignal_device_interface-0.1.0.dist-info/RECORD +35 -0
  35. biosignal_device_interface-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,413 @@
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
+
251
+ if milli_volts:
252
+ return (
253
+ data[self._biosignal_channel_indices]
254
+ * self._conversion_factor_biosignal
255
+ )
256
+ return data[self._biosignal_channel_indices]
257
+
258
+ def _extract_auxiliary_data(
259
+ self, data: np.ndarray, milli_volts: bool = True
260
+ ) -> np.ndarray:
261
+ """
262
+ Extract auxiliary channels from the transmitted data.
263
+
264
+ Args:
265
+ data (np.ndarray):
266
+ Raw data that got transmitted.
267
+ milli_volts (bool, optional):
268
+ If True, the auxiliary data is converted to milli volts.
269
+ Defaults to True.
270
+
271
+ Returns:
272
+ np.ndarray:
273
+ Extracted auxiliary channel data.
274
+ """
275
+
276
+ if milli_volts:
277
+ return (
278
+ data[self._auxiliary_channel_indices]
279
+ * self._conversion_factor_auxiliary
280
+ )
281
+ return data[self._auxiliary_channel_indices]
282
+
283
+ def toggle_connection(self, settings: Tuple[str, int] = None) -> bool:
284
+ """
285
+ Toggles the connection to the device.
286
+
287
+ Args:
288
+ settings (Tuple[str, int], optional):
289
+ If CommunicationProtocol.TCPIP:
290
+ Tuple[0] = IP -> string
291
+ Tuple[1] = Port -> int
292
+
293
+ If CommunicationProtocol.SERIAL pr CommunicationProtocol.USB:
294
+ Tuple[0] = COM Port -> string
295
+ Tuple[1] = Baudrate -> int
296
+
297
+ Defaults to None.
298
+
299
+ Returns:
300
+ bool:
301
+ True if connection attempt was successfully. False if not.
302
+ """
303
+ self._connection_settings = settings
304
+
305
+ if self._is_connected:
306
+ if self._is_streaming:
307
+ self.toggle_streaming()
308
+
309
+ success: bool = self._disconnect_from_device()
310
+ else:
311
+ success: bool = self._connect_to_device()
312
+
313
+ return success
314
+
315
+ def toggle_streaming(self) -> None:
316
+ """
317
+ Toggles the current state of the streaming.
318
+ If device is streaming, the streaming is stopped and vice versa.
319
+ """
320
+ if self._is_streaming:
321
+ self._stop_streaming()
322
+ self.clear_socket()
323
+ else:
324
+ self._start_streaming()
325
+
326
+ self.clear_socket()
327
+
328
+ def get_device_information(self) -> Dict[str, Enum | int | float | str]:
329
+ """
330
+ Gets the current configuration of the device.
331
+
332
+ Returns:
333
+ Dict[str, Enum | int | float | str]:
334
+ Dictionary that holds information about the
335
+ current device configuration and status.
336
+ """
337
+ return {
338
+ "name": DEVICE_NAME_DICT[self._device_type],
339
+ "sampling_frequency": self._sampling_frequency,
340
+ "number_of_channels": self._number_of_channels,
341
+ "number_of_biosignal_channels": self._number_of_biosignal_channels,
342
+ "number_of_auxiliary_channels": self._number_of_auxiliary_channels,
343
+ "samples_per_frame": self._samples_per_frame,
344
+ "conversion_factor_biosignal": self._conversion_factor_biosignal,
345
+ "conversion_factor_auxiliary": self._conversion_factor_auxiliary,
346
+ }
347
+
348
+ def check_valid_ip(self, ip_address: str) -> bool:
349
+ """
350
+ Checks if the provided IP is valid.
351
+
352
+ Args:
353
+ ip (str): IP to be checked.
354
+
355
+ Returns:
356
+ bool: True if IP is valid. False if not.
357
+ """
358
+ ip_pattern = re.compile(
359
+ r"^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
360
+ r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
361
+ r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
362
+ r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
363
+ )
364
+
365
+ return bool(ip_pattern.match(ip_address))
366
+
367
+ def check_valid_port(self, port: str) -> bool:
368
+ """
369
+ Checks if the provided port is valid.
370
+
371
+ Args:
372
+ port (str): Port to be checked.
373
+
374
+ Returns:
375
+ bool: True if port is valid. False if not.
376
+ """
377
+ try:
378
+ port_num = int(port)
379
+ return 0 <= port_num <= 65535
380
+ except ValueError:
381
+ return False
382
+
383
+ def get_server_wifi_ip_address(self) -> list[str]:
384
+ """
385
+ Returns the IP address of the host server.
386
+ """
387
+ try:
388
+ # Get all network interfaces
389
+ interfaces = psutil.net_if_addrs()
390
+
391
+ addresses_return = []
392
+
393
+ # Iterate through interfaces to find the one associated with WiFi
394
+ for interface, addresses in interfaces.items():
395
+ if (
396
+ "wlan" in interface.lower()
397
+ or "wi-fi" in interface.lower()
398
+ or "wifi" in interface.lower()
399
+ or "wireless" in interface.lower()
400
+ or "en0" in interface.lower()
401
+ ):
402
+ for address in addresses:
403
+ # Check if the address is an IPv4 address and not a loopback or virtual address
404
+ if (
405
+ address.family == socket.AF_INET
406
+ and not address.address.startswith("127.")
407
+ ):
408
+ addresses_return.append(address.address)
409
+
410
+ return addresses_return[::-1] if addresses_return else [""]
411
+
412
+ except Exception:
413
+ return [""]
@@ -0,0 +1,9 @@
1
+ # Import OTB Devices to be used from biosignal_device_interface.devices
2
+ # import Muovi, Quattrocento, QuattrocentoLight, ...
3
+
4
+ # OTB Devices
5
+ from biosignal_device_interface.devices.otb.otb_quattrocento import (
6
+ OTBQuattrocento as Quattrocento,
7
+ OTBQuattrocentoLight as QuattrocentoLight,
8
+ )
9
+ from biosignal_device_interface.devices.otb.otb_muovi import OTBMuovi as Muovi