sensor-sdk 0.0.1__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.

Potentially problematic release.


This version of sensor-sdk might be problematic. Click here for more details.

sensor/gforce.py ADDED
@@ -0,0 +1,864 @@
1
+ import asyncio
2
+ import queue
3
+ import struct
4
+ from asyncio import Queue
5
+ from contextlib import suppress
6
+ from dataclasses import dataclass
7
+ from enum import IntEnum
8
+ from typing import Optional, Dict, List
9
+
10
+ import numpy as np
11
+ from bleak import (
12
+ BleakScanner,
13
+ BLEDevice,
14
+ AdvertisementData,
15
+ BleakClient,
16
+ BleakGATTCharacteristic,
17
+ )
18
+
19
+
20
+ @dataclass
21
+ class Characteristic:
22
+ uuid: str
23
+ service_uuid: str
24
+ descriptor_uuids: List[str]
25
+
26
+
27
+ class Command(IntEnum):
28
+ GET_PROTOCOL_VERSION = (0x00,)
29
+ GET_FEATURE_MAP = (0x01,)
30
+ GET_DEVICE_NAME = (0x02,)
31
+ GET_MODEL_NUMBER = (0x03,)
32
+ GET_SERIAL_NUMBER = (0x04,)
33
+ GET_HW_REVISION = (0x05,)
34
+ GET_FW_REVISION = (0x06,)
35
+ GET_MANUFACTURER_NAME = (0x07,)
36
+ GET_BOOTLOADER_VERSION = (0x0A,)
37
+
38
+ GET_BATTERY_LEVEL = (0x08,)
39
+ GET_TEMPERATURE = (0x09,)
40
+
41
+ POWEROFF = (0x1D,)
42
+ SWITCH_TO_OAD = (0x1E,)
43
+ SYSTEM_RESET = (0x1F,)
44
+ SWITCH_SERVICE = (0x20,)
45
+
46
+ SET_LOG_LEVEL = (0x21,)
47
+ SET_LOG_MODULE = (0x22,)
48
+ PRINT_KERNEL_MSG = (0x23,)
49
+ MOTOR_CONTROL = (0x24,)
50
+ LED_CONTROL_TEST = (0x25,)
51
+ PACKAGE_ID_CONTROL = (0x26,)
52
+
53
+ GET_ACCELERATE_CAP = (0x30,)
54
+ SET_ACCELERATE_CONFIG = (0x31,)
55
+
56
+ GET_GYROSCOPE_CAP = (0x32,)
57
+ SET_GYROSCOPE_CONFIG = (0x33,)
58
+
59
+ GET_MAGNETOMETER_CAP = (0x34,)
60
+ SET_MAGNETOMETER_CONFIG = (0x35,)
61
+
62
+ GET_EULER_ANGLE_CAP = (0x36,)
63
+ SET_EULER_ANGLE_CONFIG = (0x37,)
64
+
65
+ QUATERNION_CAP = (0x38,)
66
+ QUATERNION_CONFIG = (0x39,)
67
+
68
+ GET_ROTATION_MATRIX_CAP = (0x3A,)
69
+ SET_ROTATION_MATRIX_CONFIG = (0x3B,)
70
+
71
+ GET_GESTURE_CAP = (0x3C,)
72
+ SET_GESTURE_CONFIG = (0x3D,)
73
+
74
+ GET_EMG_RAWDATA_CAP = (0x3E,)
75
+ SET_EMG_RAWDATA_CONFIG = (0x3F,)
76
+
77
+ GET_MOUSE_DATA_CAP = (0x40,)
78
+ SET_MOUSE_DATA_CONFIG = (0x41,)
79
+
80
+ GET_JOYSTICK_DATA_CAP = (0x42,)
81
+ SET_JOYSTICK_DATA_CONFIG = (0x43,)
82
+
83
+ GET_DEVICE_STATUS_CAP = (0x44,)
84
+ SET_DEVICE_STATUS_CONFIG = (0x45,)
85
+
86
+ GET_EMG_RAWDATA_CONFIG = (0x46,)
87
+
88
+ SET_DATA_NOTIF_SWITCH = (0x4F,)
89
+
90
+ CMD_GET_EEG_CONFIG = (0xA0,)
91
+ CMD_SET_EEG_CONFIG = (0xA1,)
92
+ CMD_GET_ECG_CONFIG = (0xA2,)
93
+ CMD_SET_ECG_CONFIG = (0xA3,)
94
+ CMD_GET_IMPEDANCE_CONFIG = (0xA4,)
95
+ CMD_SET_IMPEDANCE_CONFIG = (0xA5,)
96
+ CMD_GET_EEG_CAP = (0xA6,)
97
+ CMD_GET_ECG_CAP = (0xA7,)
98
+ CMD_GET_IMPEDANCE_CAP = (0xA8,)
99
+ CMD_GET_IMU_CONFIG = (0xAC,)
100
+ CMD_SET_IMU_CONFIG = (0xAD,)
101
+ CMD_GET_BLE_MTU_INFO = (0xAE,)
102
+ CMD_GET_BRT_CONFIG = (0xB3,)
103
+
104
+ # Partial command packet, format: [CMD_PARTIAL_DATA, packet number in reverse order, packet content]
105
+ MD_PARTIAL_DATA = 0xFF
106
+
107
+
108
+ class DataSubscription(IntEnum):
109
+ # Data Notify All Off
110
+ OFF = (0x00000000,)
111
+
112
+ # Accelerate On(C.7)
113
+ ACCELERATE = (0x00000001,)
114
+
115
+ # Gyroscope On(C.8)
116
+ GYROSCOPE = (0x00000002,)
117
+
118
+ # Magnetometer On(C.9)
119
+ MAGNETOMETER = (0x00000004,)
120
+
121
+ # Euler Angle On(C.10)
122
+ EULERANGLE = (0x00000008,)
123
+
124
+ # Quaternion On(C.11)
125
+ QUATERNION = (0x00000010,)
126
+
127
+ # Rotation Matrix On(C.12)
128
+ ROTATIONMATRIX = (0x00000020,)
129
+
130
+ # EMG Gesture On(C.13)
131
+ EMG_GESTURE = (0x00000040,)
132
+
133
+ # EMG Raw Data On(C.14)
134
+ EMG_RAW = (0x00000080,)
135
+
136
+ # HID Mouse On(C.15)
137
+ HID_MOUSE = (0x00000100,)
138
+
139
+ # HID Joystick On(C.16)
140
+ HID_JOYSTICK = (0x00000200,)
141
+
142
+ # Device Status On(C.17)
143
+ DEVICE_STATUS = (0x00000400,)
144
+
145
+ # Device Log On
146
+ LOG = (0x00000800,)
147
+
148
+ DNF_EEG = (0x00010000,)
149
+
150
+ DNF_ECG = (0x00020000,)
151
+
152
+ DNF_IMPEDANCE = (0x00040000,)
153
+
154
+ DNF_IMU = (0x00080000,)
155
+
156
+ DNF_ADS = (0x00100000,)
157
+
158
+ DNF_BRTH = (0x00200000,)
159
+
160
+ DNF_CONCAT_BLE = (0x80000000,)
161
+ # Data Notify All On
162
+ ALL = 0xFFFFFFFF
163
+
164
+
165
+ class DataType(IntEnum):
166
+ ACC = (0x01,)
167
+ GYO = (0x02,)
168
+ MAG = (0x03,)
169
+ EULER = (0x04,)
170
+ QUAT = (0x05,)
171
+ ROTA = (0x06,)
172
+ EMG_GEST = (0x07,)
173
+ EMG_ADC = (0x08,)
174
+ HID_MOUSE = (0x09,)
175
+ HID_JOYSTICK = (0x0A,)
176
+ DEV_STATUS = (0x0B,)
177
+ LOG = (0x0C,)
178
+
179
+ PARTIAL = 0xFF
180
+
181
+
182
+ class SampleResolution(IntEnum):
183
+ BITS_8 = (8,)
184
+ BITS_12 = 12,
185
+ BITS_16 = 16,
186
+ BITS_24 = 24
187
+
188
+
189
+ class SamplingRate(IntEnum):
190
+ HZ_250 = (250,)
191
+ HZ_500 = (500,)
192
+ HZ_650 = (650,)
193
+
194
+
195
+ @dataclass
196
+ class EmgRawDataConfig:
197
+ fs: SamplingRate = SamplingRate.HZ_500
198
+ channel_mask: int = 0xFF
199
+ batch_len: int = 16
200
+ resolution: SampleResolution = SampleResolution.BITS_8
201
+
202
+ def to_bytes(self) -> bytes:
203
+ body = b""
204
+ body += struct.pack("<H", self.fs)
205
+ body += struct.pack("<H", self.channel_mask)
206
+ body += struct.pack("<B", self.batch_len)
207
+ body += struct.pack("<B", self.resolution)
208
+ return body
209
+
210
+ @classmethod
211
+ def from_bytes(cls, data: bytes):
212
+ fs, channel_mask, batch_len, resolution = struct.unpack(
213
+ "<HHBB",
214
+ data,
215
+ )
216
+ return cls(fs, channel_mask, batch_len, resolution)
217
+
218
+ @dataclass
219
+ class EegRawDataConfig:
220
+ fs: SamplingRate = 0
221
+ channel_mask: int = 0
222
+ batch_len: int = 0
223
+ resolution: SampleResolution = 0
224
+ K: float = 0
225
+
226
+ def to_bytes(self) -> bytes:
227
+ body = b""
228
+ body += struct.pack("<H", self.fs)
229
+ body += struct.pack("<Q", self.channel_mask)
230
+ body += struct.pack("<B", self.batch_len)
231
+ body += struct.pack("<B", self.resolution)
232
+ body += struct.pack("<d", self.K)
233
+ return body
234
+
235
+ @classmethod
236
+ def from_bytes(cls, data: bytes):
237
+ fs, channel_mask, batch_len, resolution, K = struct.unpack(
238
+ "<HQBBd",
239
+ data,
240
+ )
241
+ return cls(fs, channel_mask, batch_len, resolution, K)
242
+
243
+ @dataclass
244
+ class EegRawDataCap:
245
+ fs: SamplingRate = 0
246
+ channel_count: int = 0
247
+ batch_len: int = 0
248
+ resolution: SampleResolution = 0
249
+
250
+ def to_bytes(self) -> bytes:
251
+ body = b""
252
+ body += struct.pack("<B", self.fs)
253
+ body += struct.pack("<B", self.channel_count)
254
+ body += struct.pack("<B", self.batch_len)
255
+ body += struct.pack("<B", self.resolution)
256
+ return body
257
+
258
+ @classmethod
259
+ def from_bytes(cls, data: bytes):
260
+ fs, channel_count, batch_len, resolution = struct.unpack(
261
+ "<BBBB",
262
+ data,
263
+ )
264
+ return cls(fs, channel_count, batch_len, resolution)
265
+
266
+ @dataclass
267
+ class EcgRawDataConfig:
268
+ fs: SamplingRate = SamplingRate.HZ_250
269
+ channel_mask: int = 0
270
+ batch_len: int = 16
271
+ resolution: SampleResolution = SampleResolution.BITS_24
272
+ K: float = 0
273
+
274
+ def to_bytes(self) -> bytes:
275
+ body = b""
276
+ body += struct.pack("<H", self.fs)
277
+ body += struct.pack("<H", self.channel_mask)
278
+ body += struct.pack("<B", self.batch_len)
279
+ body += struct.pack("<B", self.resolution)
280
+ body += struct.pack("<d", self.K)
281
+ return body
282
+
283
+ @classmethod
284
+ def from_bytes(cls, data: bytes):
285
+ fs, channel_mask, batch_len, resolution, K = struct.unpack(
286
+ "<HHBBd",
287
+ data,
288
+ )
289
+ return cls(fs, channel_mask, batch_len, resolution, K)
290
+
291
+ @dataclass
292
+ class ImuRawDataConfig:
293
+ channel_count: int = 0
294
+ fs: SamplingRate = 0
295
+ batch_len: int = 0
296
+ accK: float = 0
297
+ gyroK: float = 0
298
+ def to_bytes(self) -> bytes:
299
+ body = b""
300
+ body += struct.pack("<i", self.channel_count)
301
+ body += struct.pack("<H", self.fs)
302
+ body += struct.pack("<B", self.batch_len)
303
+ body += struct.pack("<d", self.accK)
304
+ body += struct.pack("<d", self.gyroK)
305
+ return body
306
+
307
+ @classmethod
308
+ def from_bytes(cls, data: bytes):
309
+ channel_count, fs, batch_len, accK, gyroK = struct.unpack(
310
+ "<iHBdd",
311
+ data,
312
+ )
313
+ return cls(channel_count, fs, batch_len, accK, gyroK)
314
+
315
+ @dataclass
316
+ class BrthRawDataConfig:
317
+ fs: SamplingRate = 0
318
+ channel_mask: int = 0
319
+ batch_len: int = 0
320
+ resolution: SampleResolution = 0
321
+ K: float = 0
322
+
323
+ def to_bytes(self) -> bytes:
324
+ body = b""
325
+ body += struct.pack("<H", self.fs)
326
+ body += struct.pack("<H", self.channel_mask)
327
+ body += struct.pack("<B", self.batch_len)
328
+ body += struct.pack("<B", self.resolution)
329
+ body += struct.pack("<d", self.K)
330
+ return body
331
+
332
+ @classmethod
333
+ def from_bytes(cls, data: bytes):
334
+ fs, channel_mask, batch_len, resolution, K = struct.unpack(
335
+ "<HHBBd",
336
+ data,
337
+ )
338
+ return cls(fs, channel_mask, batch_len, resolution, K)
339
+
340
+ @dataclass
341
+ class Request:
342
+ cmd: Command
343
+ has_res: bool
344
+ body: Optional[bytes] = None
345
+
346
+
347
+ class ResponseCode(IntEnum):
348
+ SUCCESS = (0x00,)
349
+ NOT_SUPPORT = (0x01,)
350
+ BAD_PARAM = (0x02,)
351
+ FAILED = (0x03,)
352
+ TIMEOUT = (0x04,)
353
+ PARTIAL_PACKET = 0xFF
354
+
355
+
356
+ @dataclass
357
+ class Response:
358
+ code: ResponseCode
359
+ cmd: Command
360
+ data: bytes
361
+
362
+
363
+ class GForce:
364
+ def __init__(self, device: BLEDevice, cmd_char: str, data_char: str, isUniversalStream: bool):
365
+ self.device_name = ""
366
+ self.client = None
367
+ self.cmd_char = cmd_char
368
+ self.data_char = data_char
369
+ self.responses: Dict[Command, Queue] = {}
370
+ self.resolution = SampleResolution.BITS_8
371
+ self._num_channels = 8
372
+ self._device = device
373
+ self._is_universal_stream = isUniversalStream
374
+ self._raw_data_buf:queue.Queue[bytes] = None
375
+ self.packet_id = 0
376
+ self.data_packet = []
377
+
378
+ async def connect(self, disconnect_cb, buf: queue.Queue[bytes]):
379
+ client = BleakClient(self._device, disconnected_callback=disconnect_cb)
380
+ await client.connect()
381
+
382
+ self.client = client
383
+ self.device_name = self._device.name
384
+ self._raw_data_buf = buf
385
+ if (not self._is_universal_stream):
386
+ await client.start_notify(self.cmd_char,self._on_cmd_response)
387
+ else:
388
+ await client.start_notify(self.data_char,self._on_universal_response)
389
+
390
+ def _on_data_response(self, q: Queue[bytes], bs: bytearray):
391
+ bs = bytes(bs)
392
+ full_packet = []
393
+
394
+ is_partial_data = bs[0] == ResponseCode.PARTIAL_PACKET
395
+ if is_partial_data:
396
+ packet_id = bs[1]
397
+ if self.packet_id != 0 and self.packet_id != packet_id + 1:
398
+ raise Exception(
399
+ "Unexpected packet id: expected {} got {}".format(
400
+ self.packet_id + 1,
401
+ packet_id,
402
+ )
403
+ )
404
+ elif self.packet_id == 0 or self.packet_id > packet_id:
405
+ self.packet_id = packet_id
406
+ self.data_packet += bs[2:]
407
+
408
+ if self.packet_id == 0:
409
+ full_packet = self.data_packet
410
+ self.data_packet = []
411
+ else:
412
+ full_packet = bs
413
+
414
+ if len(full_packet) == 0:
415
+ return
416
+
417
+ q.put_nowait(bytes(full_packet))
418
+ # data = None
419
+ # data_type = DataType(full_packet[0])
420
+ # packet = full_packet[1:]
421
+ # match data_type:
422
+ # case DataType.EMG_ADC:
423
+ # data = self._convert_emg_to_raw(packet)
424
+
425
+ # case DataType.ACC:
426
+ # data = self._convert_acceleration_to_g(packet)
427
+
428
+ # case DataType.GYO:
429
+ # data = self._convert_gyro_to_dps(packet)
430
+
431
+ # case DataType.MAG:
432
+ # data = self._convert_magnetometer_to_ut(packet)
433
+
434
+ # case DataType.EULER:
435
+ # data = self._convert_euler(packet)
436
+
437
+ # case DataType.QUAT:
438
+ # data = self._convert_quaternion(packet)
439
+
440
+ # case DataType.ROTA:
441
+ # data = self._convert_rotation_matrix(packet)
442
+
443
+ # case DataType.EMG_GEST: # It is not supported by the device (?)
444
+ # data = self._convert_emg_gesture(packet)
445
+
446
+ # case DataType.HID_MOUSE: # It is not supported by the device
447
+ # pass
448
+
449
+ # case DataType.HID_JOYSTICK: # It is not supported by the device
450
+ # pass
451
+
452
+ # case DataType.PARTIAL:
453
+ # pass
454
+ # case _:
455
+ # raise Exception(
456
+ # f"Unknown data type {data_type}, full packet: {full_packet}"
457
+ # )
458
+
459
+ # q.put_nowait(data)
460
+
461
+ # def _convert_emg_to_raw(self, data: bytes) -> np.ndarray[np.integer]:
462
+ # match self.resolution:
463
+ # case SampleResolution.BITS_8:
464
+ # dtype = np.uint8
465
+
466
+ # case SampleResolution.BITS_12:
467
+ # dtype = np.uint16
468
+
469
+ # case _:
470
+ # raise Exception(f"Unsupported resolution {self.resolution}")
471
+
472
+ # emg_data = np.frombuffer(data, dtype=dtype)
473
+
474
+ # return emg_data.reshape(-1, self._num_channels)
475
+
476
+ @staticmethod
477
+ def _convert_acceleration_to_g(data: bytes) -> np.ndarray[np.float32]:
478
+ normalizing_factor = 65536.0
479
+
480
+ acceleration_data = (
481
+ np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
482
+ )
483
+ num_channels = 3
484
+
485
+ return acceleration_data.reshape(-1, num_channels)
486
+
487
+ @staticmethod
488
+ def _convert_gyro_to_dps(data: bytes) -> np.ndarray[np.float32]:
489
+ normalizing_factor = 65536.0
490
+
491
+ gyro_data = (
492
+ np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
493
+ )
494
+ num_channels = 3
495
+
496
+ return gyro_data.reshape(-1, num_channels)
497
+
498
+ @staticmethod
499
+ def _convert_magnetometer_to_ut(data: bytes) -> np.ndarray[np.float32]:
500
+ normalizing_factor = 65536.0
501
+
502
+ magnetometer_data = (
503
+ np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
504
+ )
505
+ num_channels = 3
506
+
507
+ return magnetometer_data.reshape(-1, num_channels)
508
+
509
+ @staticmethod
510
+ def _convert_euler(data: bytes) -> np.ndarray[np.float32]:
511
+
512
+ euler_data = np.frombuffer(data, dtype=np.float32).astype(np.float32)
513
+ num_channels = 3
514
+
515
+ return euler_data.reshape(-1, num_channels)
516
+
517
+ @staticmethod
518
+ def _convert_quaternion(data: bytes) -> np.ndarray[np.float32]:
519
+
520
+ quaternion_data = np.frombuffer(data, dtype=np.float32).astype(np.float32)
521
+ num_channels = 4
522
+
523
+ return quaternion_data.reshape(-1, num_channels)
524
+
525
+ @staticmethod
526
+ def _convert_rotation_matrix(data: bytes) -> np.ndarray[np.float32]:
527
+
528
+ rotation_matrix_data = np.frombuffer(data, dtype=np.int32).astype(np.float32)
529
+ num_channels = 9
530
+
531
+ return rotation_matrix_data.reshape(-1, num_channels)
532
+
533
+ @staticmethod
534
+ def _convert_emg_gesture(data: bytes) -> np.ndarray[np.float16]:
535
+
536
+ emg_gesture_data = np.frombuffer(data, dtype=np.int16).astype(np.float16)
537
+ num_channels = 6
538
+
539
+ return emg_gesture_data.reshape(-1, num_channels)
540
+ def _on_universal_response(self, _: BleakGATTCharacteristic, bs: bytearray):
541
+ self._raw_data_buf.put_nowait(bytes(bs))
542
+
543
+ def _on_cmd_response(self, _: BleakGATTCharacteristic, bs: bytearray):
544
+ try:
545
+ response = self._parse_response(bytes(bs))
546
+ if response.cmd in self.responses:
547
+ self.responses[response.cmd].put_nowait(
548
+ response.data,
549
+ )
550
+ except Exception as e:
551
+ raise Exception("Failed to parse response: %s" % e)
552
+
553
+ @staticmethod
554
+ def _parse_response(res: bytes) -> Response:
555
+ code = int.from_bytes(res[:1], byteorder="big")
556
+ code = ResponseCode(code)
557
+
558
+ cmd = int.from_bytes(res[1:2], byteorder="big")
559
+ cmd = Command(cmd)
560
+
561
+ data = res[2:]
562
+
563
+ return Response(
564
+ code=code,
565
+ cmd=cmd,
566
+ data=data,
567
+ )
568
+
569
+ async def get_protocol_version(self) -> str:
570
+ buf = await self._send_request(
571
+ Request(
572
+ cmd=Command.GET_PROTOCOL_VERSION,
573
+ has_res=True,
574
+ )
575
+ )
576
+ return buf.decode("utf-8")
577
+
578
+ async def get_feature_map(self) -> int:
579
+ buf = await self._send_request(
580
+ Request(
581
+ cmd=Command.GET_FEATURE_MAP,
582
+ has_res=True,
583
+ )
584
+ )
585
+ return int.from_bytes(buf, byteorder="little") # TODO: check if this is correct
586
+
587
+ async def get_device_name(self) -> str:
588
+ buf = await self._send_request(
589
+ Request(
590
+ cmd=Command.GET_DEVICE_NAME,
591
+ has_res=True,
592
+ )
593
+ )
594
+ return buf.decode("utf-8")
595
+
596
+ async def get_firmware_revision(self) -> str:
597
+ buf = await self._send_request(
598
+ Request(
599
+ cmd=Command.GET_FW_REVISION,
600
+ has_res=True,
601
+ )
602
+ )
603
+ return buf.decode("utf-8")
604
+
605
+ async def get_hardware_revision(self) -> str:
606
+ buf = await self._send_request(
607
+ Request(
608
+ cmd=Command.GET_HW_REVISION,
609
+ has_res=True,
610
+ )
611
+ )
612
+ return buf.decode("utf-8")
613
+
614
+ async def get_model_number(self) -> str:
615
+ buf = await self._send_request(
616
+ Request(
617
+ cmd=Command.GET_MODEL_NUMBER,
618
+ has_res=True,
619
+ )
620
+ )
621
+ return buf.decode("utf-8")
622
+
623
+ async def get_serial_number(self) -> str:
624
+ buf = await self._send_request(
625
+ Request(
626
+ cmd=Command.GET_SERIAL_NUMBER,
627
+ has_res=True,
628
+ )
629
+ )
630
+ return buf.decode("utf-8")
631
+
632
+ async def get_manufacturer_name(self) -> str:
633
+ buf = await self._send_request(
634
+ Request(
635
+ cmd=Command.GET_MANUFACTURER_NAME,
636
+ has_res=True,
637
+ )
638
+ )
639
+
640
+ return buf.decode("utf-8")
641
+
642
+ async def get_bootloader_version(self) -> str:
643
+ buf = await self._send_request(
644
+ Request(
645
+ cmd=Command.GET_BOOTLOADER_VERSION,
646
+ has_res=True,
647
+ )
648
+ )
649
+
650
+ return buf.decode("utf-8")
651
+
652
+ async def get_battery_level(self) -> int:
653
+ buf = await self._send_request(
654
+ Request(
655
+ cmd=Command.GET_BATTERY_LEVEL,
656
+ has_res=True,
657
+ )
658
+ )
659
+ return int.from_bytes(buf, byteorder="big")
660
+
661
+ async def get_temperature(self) -> int:
662
+ buf = await self._send_request(
663
+ Request(
664
+ cmd=Command.GET_TEMPERATURE,
665
+ has_res=True,
666
+ )
667
+ )
668
+ return int.from_bytes(buf, byteorder="big")
669
+
670
+ async def power_off(self) -> None:
671
+ await self._send_request(
672
+ Request(
673
+ cmd=Command.POWEROFF,
674
+ has_res=False,
675
+ )
676
+ )
677
+
678
+
679
+ async def system_reset(self):
680
+ await self._send_request(
681
+ Request(
682
+ cmd=Command.SYSTEM_RESET,
683
+ has_res=False,
684
+ )
685
+ )
686
+
687
+
688
+ async def set_motor(self, switchStatus):
689
+ body = [
690
+ switchStatus == True
691
+ ]
692
+ body = bytes(body)
693
+ ret = await self._send_request(
694
+ Request(
695
+ cmd=Command.MOTOR_CONTROL,
696
+ body=body,
697
+ has_res=True,
698
+ )
699
+ )
700
+
701
+ async def set_led(self, switchStatus):
702
+ body = [
703
+ switchStatus == True
704
+ ]
705
+ body = bytes(body)
706
+ ret = await self._send_request(
707
+ Request(
708
+ cmd=Command.LED_CONTROL_TEST,
709
+ body=body,
710
+ has_res=True,
711
+ )
712
+ )
713
+
714
+ async def set_log_level(self, logLevel):
715
+ body = [
716
+ 0xFF & logLevel
717
+ ]
718
+ body = bytes(body)
719
+ ret = await self._send_request(
720
+ Request(
721
+ cmd=Command.SET_LOG_LEVEL,
722
+ body=body,
723
+ has_res=True,
724
+ )
725
+ )
726
+
727
+
728
+ async def set_emg_raw_data_config(self, cfg=EmgRawDataConfig()):
729
+ body = cfg.to_bytes()
730
+ await self._send_request(
731
+ Request(
732
+ cmd=Command.SET_EMG_RAWDATA_CONFIG,
733
+ body=body,
734
+ has_res=True,
735
+ )
736
+ )
737
+
738
+ # print('_send_request returned:', ret)
739
+
740
+ self.resolution = cfg.resolution
741
+
742
+ num_channels = 0
743
+ ch_mask = cfg.channel_mask
744
+
745
+ while ch_mask != 0:
746
+ if ch_mask & 0x01 != 0:
747
+ num_channels += 1
748
+ ch_mask >>= 1
749
+
750
+ self.__num_channels = num_channels
751
+
752
+ async def get_emg_raw_data_config(self) -> EmgRawDataConfig:
753
+ buf = await self._send_request(
754
+ Request(
755
+ cmd=Command.GET_EMG_RAWDATA_CONFIG,
756
+ has_res=True,
757
+ )
758
+ )
759
+ return EmgRawDataConfig.from_bytes(buf)
760
+
761
+ async def get_eeg_raw_data_config(self) -> EegRawDataConfig:
762
+ buf = await self._send_request(
763
+ Request(
764
+ cmd=Command.CMD_GET_EEG_CONFIG,
765
+ has_res=True,
766
+ )
767
+ )
768
+ return EegRawDataConfig.from_bytes(buf)
769
+
770
+ async def get_eeg_raw_data_cap(self) -> EegRawDataCap:
771
+ buf = await self._send_request(
772
+ Request(
773
+ cmd=Command.CMD_GET_EEG_CAP,
774
+ has_res=True,
775
+ )
776
+ )
777
+ return EegRawDataCap.from_bytes(buf)
778
+
779
+ async def get_ecg_raw_data_config(self) -> EcgRawDataConfig:
780
+ buf = await self._send_request(
781
+ Request(
782
+ cmd=Command.CMD_GET_ECG_CONFIG,
783
+ has_res=True,
784
+ )
785
+ )
786
+ return EcgRawDataConfig.from_bytes(buf)
787
+
788
+ async def get_imu_raw_data_config(self) -> ImuRawDataConfig:
789
+ buf = await self._send_request(
790
+ Request(
791
+ cmd=Command.CMD_GET_IMU_CONFIG,
792
+ has_res=True,
793
+ )
794
+ )
795
+ return ImuRawDataConfig.from_bytes(buf)
796
+
797
+ async def get_brth_raw_data_config(self) -> BrthRawDataConfig:
798
+ buf = await self._send_request(
799
+ Request(
800
+ cmd=Command.CMD_GET_BRT_CONFIG,
801
+ has_res=True,
802
+ )
803
+ )
804
+ return BrthRawDataConfig.from_bytes(buf)
805
+
806
+ async def set_subscription(self, subscription: DataSubscription):
807
+ body = [
808
+ 0xFF & subscription,
809
+ 0xFF & (subscription >> 8),
810
+ 0xFF & (subscription >> 16),
811
+ 0xFF & (subscription >> 24),
812
+ ]
813
+ body = bytes(body)
814
+ await self._send_request(
815
+ Request(
816
+ cmd=Command.SET_DATA_NOTIF_SWITCH,
817
+ body=body,
818
+ has_res=True,
819
+ )
820
+ )
821
+
822
+ async def start_streaming(self, q:queue.Queue):
823
+ await self.client.start_notify(
824
+ self.data_char,
825
+ lambda _, data: self._on_data_response(q, data),
826
+ )
827
+
828
+ async def stop_streaming(self):
829
+ exceptions = []
830
+ try:
831
+ await self.set_subscription(DataSubscription.OFF)
832
+ except Exception as e:
833
+ exceptions.append(e)
834
+ try:
835
+ await self.client.stop_notify(self.data_char)
836
+ except Exception as e:
837
+ exceptions.append(e)
838
+
839
+ if len(exceptions) > 0:
840
+ raise Exception("Failed to stop streaming: %s" % exceptions)
841
+
842
+ async def disconnect(self):
843
+ with suppress(asyncio.CancelledError):
844
+ await self.client.disconnect()
845
+
846
+ def _get_response_channel(self, cmd: Command) -> Queue:
847
+ q = Queue()
848
+ self.responses[cmd] = q
849
+ return q
850
+
851
+ async def _send_request(self, req: Request) -> Optional[bytes]:
852
+ q = None
853
+ if req.has_res:
854
+ q = self._get_response_channel(req.cmd)
855
+
856
+ bs = bytes([req.cmd])
857
+ if req.body is not None:
858
+ bs += req.body
859
+ await self.client.write_gatt_char(self.cmd_char, bs)
860
+
861
+ if not req.has_res:
862
+ return None
863
+
864
+ return await asyncio.wait_for(q.get(), 3)