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/__init__.py +4 -0
- sensor/gforce.py +864 -0
- sensor/sensor_controller.py +223 -0
- sensor/sensor_data.py +91 -0
- sensor/sensor_data_context.py +569 -0
- sensor/sensor_device.py +75 -0
- sensor/sensor_profile.py +449 -0
- sensor/utils.py +28 -0
- sensor_sdk-0.0.1.dist-info/LICENSE.txt +21 -0
- sensor_sdk-0.0.1.dist-info/METADATA +300 -0
- sensor_sdk-0.0.1.dist-info/RECORD +14 -0
- sensor_sdk-0.0.1.dist-info/WHEEL +5 -0
- sensor_sdk-0.0.1.dist-info/top_level.txt +1 -0
- sensor_sdk-0.0.1.dist-info/zip-safe +1 -0
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)
|