sensor-sdk 0.0.6__tar.gz → 0.0.33__tar.gz
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.
- {sensor-sdk-0.0.6/sensor_sdk.egg-info → sensor_sdk-0.0.33}/PKG-INFO +57 -6
- sensor-sdk-0.0.6/PKG-INFO → sensor_sdk-0.0.33/README.md +42 -14
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor/gforce.py +127 -102
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor/sensor_controller.py +28 -30
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor/sensor_data.py +7 -3
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor/sensor_data_context.py +295 -342
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor/sensor_device.py +6 -0
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor/sensor_profile.py +209 -100
- sensor_sdk-0.0.33/sensor/sensor_utils.py +906 -0
- sensor-sdk-0.0.6/README.md → sensor_sdk-0.0.33/sensor_sdk.egg-info/PKG-INFO +405 -343
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor_sdk.egg-info/SOURCES.txt +1 -1
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/setup.py +23 -23
- sensor-sdk-0.0.6/sensor/utils.py +0 -73
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/LICENSE.txt +0 -0
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor/__init__.py +0 -0
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor_sdk.egg-info/dependency_links.txt +0 -0
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor_sdk.egg-info/requires.txt +0 -0
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor_sdk.egg-info/top_level.txt +0 -0
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/sensor_sdk.egg-info/zip-safe +0 -0
- {sensor-sdk-0.0.6 → sensor_sdk-0.0.33}/setup.cfg +0 -0
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sensor-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.33
|
|
4
4
|
Summary: Python sdk for Synchroni
|
|
5
5
|
Home-page: https://github.com/oymotion/SynchroniSDKPython
|
|
6
6
|
Author: Martin Ye
|
|
7
7
|
Author-email: yecq_82@hotmail.com
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.9.0
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE.txt
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: setuptools
|
|
13
|
+
Requires-Dist: bleak
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: author-email
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
11
23
|
|
|
12
24
|
# sensor-sdk
|
|
13
25
|
|
|
@@ -92,10 +104,10 @@ isScanning = SensorControllerInstance.isScanning
|
|
|
92
104
|
|
|
93
105
|
### 5. Check if bluetooth is enabled
|
|
94
106
|
|
|
95
|
-
Use `property
|
|
107
|
+
Use `property isEnable: bool` to check if bluetooth is enable
|
|
96
108
|
|
|
97
109
|
```python
|
|
98
|
-
|
|
110
|
+
isEnable = SensorControllerInstance.isEnable
|
|
99
111
|
```
|
|
100
112
|
|
|
101
113
|
### 6. Create SensorProfile
|
|
@@ -347,8 +359,47 @@ batteryPower = sensorProfile.getBatteryLevel()
|
|
|
347
359
|
|
|
348
360
|
Please check console.py in examples directory
|
|
349
361
|
|
|
350
|
-
|
|
362
|
+
### Async methods
|
|
351
363
|
|
|
352
364
|
all methods start with async is async methods, they has same params and return result as sync methods.
|
|
353
365
|
|
|
354
366
|
Please check async_console.py in examples directory
|
|
367
|
+
|
|
368
|
+
### setParam method
|
|
369
|
+
|
|
370
|
+
Use `def setParam(self, key: str, value: str) -> str` to set parameter of sensor profile. Please call after device in 'Ready' state.
|
|
371
|
+
|
|
372
|
+
Below is available key and value:
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
result = sensorProfile.setParam("NTF_EMG", "ON")
|
|
376
|
+
# set EMG data to ON or OFF, result is "OK" if succeed
|
|
377
|
+
|
|
378
|
+
result = sensorProfile.setParam("NTF_EEG", "ON")
|
|
379
|
+
# set EEG data to ON or OFF, result is "OK" if succeed
|
|
380
|
+
|
|
381
|
+
result = sensorProfile.setParam("NTF_ECG", "ON")
|
|
382
|
+
# set ECG data to ON or OFF, result is "OK" if succeed
|
|
383
|
+
|
|
384
|
+
result = sensorProfile.setParam("NTF_IMU", "ON")
|
|
385
|
+
# set IMU data to ON or OFF, result is "OK" if succeed
|
|
386
|
+
|
|
387
|
+
result = sensorProfile.setParam("NTF_BRTH", "ON")
|
|
388
|
+
# set BRTH data to ON or OFF, result is "OK" if succeed
|
|
389
|
+
|
|
390
|
+
result = sensorProfile.setParam("FILTER_50HZ", "ON")
|
|
391
|
+
# set 50Hz notch filter to ON or OFF, result is "OK" if succeed
|
|
392
|
+
|
|
393
|
+
result = sensorProfile.setParam("FILTER_60HZ", "ON")
|
|
394
|
+
# set 60Hz notch filter to ON or OFF, result is "OK" if succeed
|
|
395
|
+
|
|
396
|
+
result = sensorProfile.setParam("FILTER_HPF", "ON")
|
|
397
|
+
# set 0.5Hz hpf filter to ON or OFF, result is "OK" if succeed
|
|
398
|
+
|
|
399
|
+
result = sensorProfile.setParam("FILTER_LPF", "ON")
|
|
400
|
+
# set 80Hz lpf filter to ON or OFF, result is "OK" if succeed
|
|
401
|
+
|
|
402
|
+
result = sensorProfile.setParam("DEBUG_BLE_DATA_PATH", "d:/temp/test.csv")
|
|
403
|
+
# set debug ble data path, result is "OK" if succeed
|
|
404
|
+
# please give an absolute path and make sure it is valid and writeable by yourself
|
|
405
|
+
```
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: sensor-sdk
|
|
3
|
-
Version: 0.0.6
|
|
4
|
-
Summary: Python sdk for Synchroni
|
|
5
|
-
Home-page: https://github.com/oymotion/SynchroniSDKPython
|
|
6
|
-
Author: Martin Ye
|
|
7
|
-
Author-email: yecq_82@hotmail.com
|
|
8
|
-
Requires-Python: >=3.8.0
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE.txt
|
|
11
|
-
|
|
12
1
|
# sensor-sdk
|
|
13
2
|
|
|
14
3
|
Synchroni sdk for Python
|
|
@@ -92,10 +81,10 @@ isScanning = SensorControllerInstance.isScanning
|
|
|
92
81
|
|
|
93
82
|
### 5. Check if bluetooth is enabled
|
|
94
83
|
|
|
95
|
-
Use `property
|
|
84
|
+
Use `property isEnable: bool` to check if bluetooth is enable
|
|
96
85
|
|
|
97
86
|
```python
|
|
98
|
-
|
|
87
|
+
isEnable = SensorControllerInstance.isEnable
|
|
99
88
|
```
|
|
100
89
|
|
|
101
90
|
### 6. Create SensorProfile
|
|
@@ -347,8 +336,47 @@ batteryPower = sensorProfile.getBatteryLevel()
|
|
|
347
336
|
|
|
348
337
|
Please check console.py in examples directory
|
|
349
338
|
|
|
350
|
-
|
|
339
|
+
### Async methods
|
|
351
340
|
|
|
352
341
|
all methods start with async is async methods, they has same params and return result as sync methods.
|
|
353
342
|
|
|
354
343
|
Please check async_console.py in examples directory
|
|
344
|
+
|
|
345
|
+
### setParam method
|
|
346
|
+
|
|
347
|
+
Use `def setParam(self, key: str, value: str) -> str` to set parameter of sensor profile. Please call after device in 'Ready' state.
|
|
348
|
+
|
|
349
|
+
Below is available key and value:
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
result = sensorProfile.setParam("NTF_EMG", "ON")
|
|
353
|
+
# set EMG data to ON or OFF, result is "OK" if succeed
|
|
354
|
+
|
|
355
|
+
result = sensorProfile.setParam("NTF_EEG", "ON")
|
|
356
|
+
# set EEG data to ON or OFF, result is "OK" if succeed
|
|
357
|
+
|
|
358
|
+
result = sensorProfile.setParam("NTF_ECG", "ON")
|
|
359
|
+
# set ECG data to ON or OFF, result is "OK" if succeed
|
|
360
|
+
|
|
361
|
+
result = sensorProfile.setParam("NTF_IMU", "ON")
|
|
362
|
+
# set IMU data to ON or OFF, result is "OK" if succeed
|
|
363
|
+
|
|
364
|
+
result = sensorProfile.setParam("NTF_BRTH", "ON")
|
|
365
|
+
# set BRTH data to ON or OFF, result is "OK" if succeed
|
|
366
|
+
|
|
367
|
+
result = sensorProfile.setParam("FILTER_50HZ", "ON")
|
|
368
|
+
# set 50Hz notch filter to ON or OFF, result is "OK" if succeed
|
|
369
|
+
|
|
370
|
+
result = sensorProfile.setParam("FILTER_60HZ", "ON")
|
|
371
|
+
# set 60Hz notch filter to ON or OFF, result is "OK" if succeed
|
|
372
|
+
|
|
373
|
+
result = sensorProfile.setParam("FILTER_HPF", "ON")
|
|
374
|
+
# set 0.5Hz hpf filter to ON or OFF, result is "OK" if succeed
|
|
375
|
+
|
|
376
|
+
result = sensorProfile.setParam("FILTER_LPF", "ON")
|
|
377
|
+
# set 80Hz lpf filter to ON or OFF, result is "OK" if succeed
|
|
378
|
+
|
|
379
|
+
result = sensorProfile.setParam("DEBUG_BLE_DATA_PATH", "d:/temp/test.csv")
|
|
380
|
+
# set debug ble data path, result is "OK" if succeed
|
|
381
|
+
# please give an absolute path and make sure it is valid and writeable by yourself
|
|
382
|
+
```
|
|
@@ -3,6 +3,7 @@ import queue
|
|
|
3
3
|
import struct
|
|
4
4
|
from asyncio import Queue
|
|
5
5
|
from contextlib import suppress
|
|
6
|
+
from datetime import datetime
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from enum import IntEnum
|
|
8
9
|
from typing import Optional, Dict, List
|
|
@@ -16,7 +17,7 @@ from bleak import (
|
|
|
16
17
|
BleakGATTCharacteristic,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
|
-
from sensor import
|
|
20
|
+
from sensor import sensor_utils
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
@dataclass
|
|
@@ -88,6 +89,7 @@ class Command(IntEnum):
|
|
|
88
89
|
GET_EMG_RAWDATA_CONFIG = (0x46,)
|
|
89
90
|
|
|
90
91
|
SET_DATA_NOTIF_SWITCH = (0x4F,)
|
|
92
|
+
SET_FUNCTION_SWITCH = (0x85,)
|
|
91
93
|
|
|
92
94
|
CMD_GET_EEG_CONFIG = (0xA0,)
|
|
93
95
|
CMD_SET_EEG_CONFIG = (0xA1,)
|
|
@@ -103,6 +105,8 @@ class Command(IntEnum):
|
|
|
103
105
|
CMD_GET_BLE_MTU_INFO = (0xAE,)
|
|
104
106
|
CMD_GET_BRT_CONFIG = (0xB3,)
|
|
105
107
|
|
|
108
|
+
CMD_SET_FRIMWARE_FILTER_SWITCH = (0xAA,)
|
|
109
|
+
CMD_GET_FRIMWARE_FILTER_SWITCH = (0xA9,)
|
|
106
110
|
# Partial command packet, format: [CMD_PARTIAL_DATA, packet number in reverse order, packet content]
|
|
107
111
|
MD_PARTIAL_DATA = 0xFF
|
|
108
112
|
|
|
@@ -371,10 +375,18 @@ class Response:
|
|
|
371
375
|
|
|
372
376
|
class GForce:
|
|
373
377
|
def __init__(
|
|
374
|
-
self,
|
|
378
|
+
self,
|
|
379
|
+
device: BLEDevice,
|
|
380
|
+
cmd_char: str,
|
|
381
|
+
data_char: str,
|
|
382
|
+
isUniversalStream: bool,
|
|
383
|
+
event_loop: asyncio.AbstractEventLoop,
|
|
384
|
+
gforce_event_loop: asyncio.AbstractEventLoop,
|
|
375
385
|
):
|
|
376
386
|
self.device_name = ""
|
|
377
387
|
self.client = None
|
|
388
|
+
self.event_loop = event_loop
|
|
389
|
+
self.gforce_event_loop = gforce_event_loop
|
|
378
390
|
self.cmd_char = cmd_char
|
|
379
391
|
self.data_char = data_char
|
|
380
392
|
self.responses: Dict[Command, Queue] = {}
|
|
@@ -393,25 +405,33 @@ class GForce:
|
|
|
393
405
|
self._raw_data_buf = buf
|
|
394
406
|
|
|
395
407
|
try:
|
|
396
|
-
await
|
|
408
|
+
await sensor_utils.async_call(client.connect(), sensor_utils._TIMEOUT, self.gforce_event_loop)
|
|
397
409
|
except Exception as e:
|
|
398
410
|
return
|
|
399
411
|
|
|
400
412
|
if not client.is_connected:
|
|
401
413
|
return
|
|
402
|
-
if not self._is_universal_stream:
|
|
403
|
-
await asyncio.wait_for(
|
|
404
|
-
client.start_notify(self.cmd_char, self._on_cmd_response),
|
|
405
|
-
utils._TIMEOUT,
|
|
406
|
-
)
|
|
407
|
-
else:
|
|
408
|
-
await asyncio.wait_for(
|
|
409
|
-
client.start_notify(self.data_char, self._on_universal_response),
|
|
410
|
-
utils._TIMEOUT,
|
|
411
|
-
)
|
|
412
414
|
|
|
413
|
-
|
|
415
|
+
try:
|
|
416
|
+
if not self._is_universal_stream:
|
|
417
|
+
await sensor_utils.async_call(
|
|
418
|
+
client.start_notify(self.cmd_char, self._on_cmd_response),
|
|
419
|
+
sensor_utils._TIMEOUT,
|
|
420
|
+
self.gforce_event_loop,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
else:
|
|
424
|
+
await sensor_utils.async_call(
|
|
425
|
+
client.start_notify(self.data_char, self._on_universal_response),
|
|
426
|
+
sensor_utils._TIMEOUT,
|
|
427
|
+
self.gforce_event_loop,
|
|
428
|
+
)
|
|
429
|
+
except Exception as e:
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
def _on_data_response(self, q: queue.Queue[bytes], bs: bytearray):
|
|
414
433
|
bs = bytes(bs)
|
|
434
|
+
|
|
415
435
|
full_packet = []
|
|
416
436
|
|
|
417
437
|
is_partial_data = bs[0] == ResponseCode.PARTIAL_PACKET
|
|
@@ -438,71 +458,12 @@ class GForce:
|
|
|
438
458
|
return
|
|
439
459
|
|
|
440
460
|
q.put_nowait(bytes(full_packet))
|
|
441
|
-
# data = None
|
|
442
|
-
# data_type = DataType(full_packet[0])
|
|
443
|
-
# packet = full_packet[1:]
|
|
444
|
-
# match data_type:
|
|
445
|
-
# case DataType.EMG_ADC:
|
|
446
|
-
# data = self._convert_emg_to_raw(packet)
|
|
447
|
-
|
|
448
|
-
# case DataType.ACC:
|
|
449
|
-
# data = self._convert_acceleration_to_g(packet)
|
|
450
|
-
|
|
451
|
-
# case DataType.GYO:
|
|
452
|
-
# data = self._convert_gyro_to_dps(packet)
|
|
453
|
-
|
|
454
|
-
# case DataType.MAG:
|
|
455
|
-
# data = self._convert_magnetometer_to_ut(packet)
|
|
456
|
-
|
|
457
|
-
# case DataType.EULER:
|
|
458
|
-
# data = self._convert_euler(packet)
|
|
459
|
-
|
|
460
|
-
# case DataType.QUAT:
|
|
461
|
-
# data = self._convert_quaternion(packet)
|
|
462
|
-
|
|
463
|
-
# case DataType.ROTA:
|
|
464
|
-
# data = self._convert_rotation_matrix(packet)
|
|
465
|
-
|
|
466
|
-
# case DataType.EMG_GEST: # It is not supported by the device (?)
|
|
467
|
-
# data = self._convert_emg_gesture(packet)
|
|
468
|
-
|
|
469
|
-
# case DataType.HID_MOUSE: # It is not supported by the device
|
|
470
|
-
# pass
|
|
471
|
-
|
|
472
|
-
# case DataType.HID_JOYSTICK: # It is not supported by the device
|
|
473
|
-
# pass
|
|
474
|
-
|
|
475
|
-
# case DataType.PARTIAL:
|
|
476
|
-
# pass
|
|
477
|
-
# case _:
|
|
478
|
-
# raise Exception(
|
|
479
|
-
# f"Unknown data type {data_type}, full packet: {full_packet}"
|
|
480
|
-
# )
|
|
481
|
-
|
|
482
|
-
# q.put_nowait(data)
|
|
483
|
-
|
|
484
|
-
# def _convert_emg_to_raw(self, data: bytes) -> np.ndarray[np.integer]:
|
|
485
|
-
# match self.resolution:
|
|
486
|
-
# case SampleResolution.BITS_8:
|
|
487
|
-
# dtype = np.uint8
|
|
488
|
-
|
|
489
|
-
# case SampleResolution.BITS_12:
|
|
490
|
-
# dtype = np.uint16
|
|
491
|
-
|
|
492
|
-
# case _:
|
|
493
|
-
# raise Exception(f"Unsupported resolution {self.resolution}")
|
|
494
|
-
|
|
495
|
-
# emg_data = np.frombuffer(data, dtype=dtype)
|
|
496
|
-
|
|
497
|
-
# return emg_data.reshape(-1, self._num_channels)
|
|
498
461
|
|
|
499
462
|
@staticmethod
|
|
500
463
|
def _convert_acceleration_to_g(data: bytes) -> np.ndarray[np.float32]:
|
|
501
464
|
normalizing_factor = 65536.0
|
|
502
465
|
|
|
503
|
-
acceleration_data = (
|
|
504
|
-
np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
|
|
505
|
-
)
|
|
466
|
+
acceleration_data = np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
|
|
506
467
|
num_channels = 3
|
|
507
468
|
|
|
508
469
|
return acceleration_data.reshape(-1, num_channels)
|
|
@@ -511,9 +472,7 @@ class GForce:
|
|
|
511
472
|
def _convert_gyro_to_dps(data: bytes) -> np.ndarray[np.float32]:
|
|
512
473
|
normalizing_factor = 65536.0
|
|
513
474
|
|
|
514
|
-
gyro_data = (
|
|
515
|
-
np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
|
|
516
|
-
)
|
|
475
|
+
gyro_data = np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
|
|
517
476
|
num_channels = 3
|
|
518
477
|
|
|
519
478
|
return gyro_data.reshape(-1, num_channels)
|
|
@@ -522,9 +481,7 @@ class GForce:
|
|
|
522
481
|
def _convert_magnetometer_to_ut(data: bytes) -> np.ndarray[np.float32]:
|
|
523
482
|
normalizing_factor = 65536.0
|
|
524
483
|
|
|
525
|
-
magnetometer_data = (
|
|
526
|
-
np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
|
|
527
|
-
)
|
|
484
|
+
magnetometer_data = np.frombuffer(data, dtype=np.int32).astype(np.float32) / normalizing_factor
|
|
528
485
|
num_channels = 3
|
|
529
486
|
|
|
530
487
|
return magnetometer_data.reshape(-1, num_channels)
|
|
@@ -565,12 +522,18 @@ class GForce:
|
|
|
565
522
|
self._raw_data_buf.put_nowait(bytes(bs))
|
|
566
523
|
|
|
567
524
|
def _on_cmd_response(self, _: BleakGATTCharacteristic, bs: bytearray):
|
|
525
|
+
sensor_utils.async_exec(self.async_on_cmd_response(bs), self.event_loop)
|
|
526
|
+
|
|
527
|
+
async def async_on_cmd_response(self, bs: bytearray):
|
|
568
528
|
try:
|
|
529
|
+
# print(bytes(bs))
|
|
569
530
|
response = self._parse_response(bytes(bs))
|
|
570
|
-
if response.cmd
|
|
531
|
+
if self.responses.get(response.cmd) != None:
|
|
571
532
|
self.responses[response.cmd].put_nowait(
|
|
572
533
|
response.data,
|
|
573
534
|
)
|
|
535
|
+
else:
|
|
536
|
+
print("invalid response:" + bytes(bs))
|
|
574
537
|
except Exception as e:
|
|
575
538
|
raise Exception("Failed to parse response: %s" % e)
|
|
576
539
|
|
|
@@ -729,6 +692,17 @@ class GForce:
|
|
|
729
692
|
)
|
|
730
693
|
)
|
|
731
694
|
|
|
695
|
+
async def set_package_id(self, switchStatus):
|
|
696
|
+
body = [switchStatus == True]
|
|
697
|
+
body = bytes(body)
|
|
698
|
+
ret = await self._send_request(
|
|
699
|
+
Request(
|
|
700
|
+
cmd=Command.PACKAGE_ID_CONTROL,
|
|
701
|
+
body=body,
|
|
702
|
+
has_res=True,
|
|
703
|
+
)
|
|
704
|
+
)
|
|
705
|
+
|
|
732
706
|
async def set_log_level(self, logLevel):
|
|
733
707
|
body = [0xFF & logLevel]
|
|
734
708
|
body = bytes(body)
|
|
@@ -740,9 +714,32 @@ class GForce:
|
|
|
740
714
|
)
|
|
741
715
|
)
|
|
742
716
|
|
|
717
|
+
async def set_function_switch(self, funcSwitch)-> bool:
|
|
718
|
+
body = [0xFF & funcSwitch]
|
|
719
|
+
body = bytes(body)
|
|
720
|
+
ret = await self._send_request(
|
|
721
|
+
Request(
|
|
722
|
+
cmd=Command.SET_FUNCTION_SWITCH,
|
|
723
|
+
body=body,
|
|
724
|
+
has_res=True,
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
if (len(ret) > 0 and ret[0] == 0):
|
|
728
|
+
return True
|
|
729
|
+
return False
|
|
730
|
+
|
|
731
|
+
async def set_firmware_filter_switch(self, switchStatus: int):
|
|
732
|
+
body = [0xFF & switchStatus]
|
|
733
|
+
body = bytes(body)
|
|
734
|
+
await self._send_request(Request(cmd=Command.CMD_SET_FRIMWARE_FILTER_SWITCH, body=body, has_res=True))
|
|
735
|
+
|
|
736
|
+
async def get_firmware_filter_switch(self):
|
|
737
|
+
buf = await self._send_request(Request(cmd=Command.CMD_GET_FRIMWARE_FILTER_SWITCH, has_res=True))
|
|
738
|
+
return buf[0]
|
|
739
|
+
|
|
743
740
|
async def set_emg_raw_data_config(self, cfg=EmgRawDataConfig()):
|
|
744
741
|
body = cfg.to_bytes()
|
|
745
|
-
await self._send_request(
|
|
742
|
+
ret = await self._send_request(
|
|
746
743
|
Request(
|
|
747
744
|
cmd=Command.SET_EMG_RAWDATA_CONFIG,
|
|
748
745
|
body=body,
|
|
@@ -750,7 +747,7 @@ class GForce:
|
|
|
750
747
|
)
|
|
751
748
|
)
|
|
752
749
|
|
|
753
|
-
# print('
|
|
750
|
+
# print('set_emg_raw_data_config returned:', ret)
|
|
754
751
|
|
|
755
752
|
self.resolution = cfg.resolution
|
|
756
753
|
|
|
@@ -835,26 +832,20 @@ class GForce:
|
|
|
835
832
|
)
|
|
836
833
|
|
|
837
834
|
async def start_streaming(self, q: queue.Queue):
|
|
838
|
-
await
|
|
835
|
+
await sensor_utils.async_call(
|
|
839
836
|
self.client.start_notify(
|
|
840
837
|
self.data_char,
|
|
841
838
|
lambda _, data: self._on_data_response(q, data),
|
|
842
839
|
),
|
|
843
|
-
|
|
840
|
+
sensor_utils._TIMEOUT,
|
|
841
|
+
self.gforce_event_loop,
|
|
844
842
|
)
|
|
845
843
|
|
|
846
844
|
async def stop_streaming(self):
|
|
847
845
|
exceptions = []
|
|
846
|
+
|
|
848
847
|
try:
|
|
849
|
-
await
|
|
850
|
-
self.set_subscription(DataSubscription.OFF), utils._TIMEOUT
|
|
851
|
-
)
|
|
852
|
-
except Exception as e:
|
|
853
|
-
exceptions.append(e)
|
|
854
|
-
try:
|
|
855
|
-
await asyncio.wait_for(
|
|
856
|
-
self.client.stop_notify(self.data_char), utils._TIMEOUT
|
|
857
|
-
)
|
|
848
|
+
await sensor_utils.async_call(self.client.stop_notify(self.data_char), sensor_utils._TIMEOUT, self.gforce_event_loop)
|
|
858
849
|
except Exception as e:
|
|
859
850
|
exceptions.append(e)
|
|
860
851
|
|
|
@@ -864,28 +855,62 @@ class GForce:
|
|
|
864
855
|
async def disconnect(self):
|
|
865
856
|
with suppress(asyncio.CancelledError):
|
|
866
857
|
try:
|
|
867
|
-
await
|
|
858
|
+
await sensor_utils.async_call(self.client.disconnect(), sensor_utils._TIMEOUT, self.gforce_event_loop)
|
|
868
859
|
except Exception as e:
|
|
869
860
|
pass
|
|
870
861
|
|
|
871
862
|
def _get_response_channel(self, cmd: Command) -> Queue:
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
863
|
+
if self.responses.get(cmd) != None:
|
|
864
|
+
return self.responses[cmd]
|
|
865
|
+
else:
|
|
866
|
+
q = Queue()
|
|
867
|
+
self.responses[cmd] = q
|
|
868
|
+
return q
|
|
875
869
|
|
|
876
870
|
async def _send_request(self, req: Request) -> Optional[bytes]:
|
|
871
|
+
return await sensor_utils.async_call(self._send_request_internal(req=req), runloop=self.event_loop)
|
|
872
|
+
|
|
873
|
+
async def _send_request_internal(self, req: Request) -> Optional[bytes]:
|
|
877
874
|
q = None
|
|
878
875
|
if req.has_res:
|
|
879
876
|
q = self._get_response_channel(req.cmd)
|
|
880
877
|
|
|
878
|
+
timeStamp_old = -1
|
|
879
|
+
while not q.empty():
|
|
880
|
+
timeStamp_old = q.get_nowait()
|
|
881
|
+
|
|
882
|
+
now = datetime.now()
|
|
883
|
+
timestamp_now = now.timestamp()
|
|
884
|
+
if (timeStamp_old > -1) and ((timestamp_now - timeStamp_old) < 3):
|
|
885
|
+
print("send request too fast")
|
|
886
|
+
q.put_nowait(timeStamp_old)
|
|
887
|
+
return None
|
|
888
|
+
|
|
881
889
|
bs = bytes([req.cmd])
|
|
882
890
|
if req.body is not None:
|
|
883
891
|
bs += req.body
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
892
|
+
|
|
893
|
+
# print(str(req.cmd) + str(req.body))
|
|
894
|
+
try:
|
|
895
|
+
await sensor_utils.async_call(
|
|
896
|
+
self.client.write_gatt_char(self.cmd_char, bs),
|
|
897
|
+
1,
|
|
898
|
+
runloop=self.gforce_event_loop,
|
|
899
|
+
)
|
|
900
|
+
except Exception as e:
|
|
901
|
+
self.responses[req.cmd] = None
|
|
902
|
+
return None
|
|
887
903
|
|
|
888
904
|
if not req.has_res:
|
|
905
|
+
self.responses[req.cmd] = None
|
|
889
906
|
return None
|
|
890
907
|
|
|
891
|
-
|
|
908
|
+
try:
|
|
909
|
+
ret = await asyncio.wait_for(q.get(), 2)
|
|
910
|
+
now = datetime.now()
|
|
911
|
+
timestamp_now = now.timestamp()
|
|
912
|
+
q.put_nowait(timestamp_now)
|
|
913
|
+
return ret
|
|
914
|
+
except Exception as e:
|
|
915
|
+
self.responses[req.cmd] = None
|
|
916
|
+
return None
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from concurrent.futures import ThreadPoolExecutor
|
|
2
3
|
import threading
|
|
3
4
|
from typing import Callable, Dict, List, Optional, Tuple
|
|
@@ -5,11 +6,10 @@ from typing import Callable, Dict, List, Optional, Tuple
|
|
|
5
6
|
import bleak
|
|
6
7
|
|
|
7
8
|
from sensor import sensor_profile
|
|
8
|
-
from sensor import
|
|
9
|
+
from sensor import sensor_utils
|
|
9
10
|
from sensor.sensor_profile import DeviceStateEx, SensorProfile
|
|
10
|
-
import asyncio
|
|
11
11
|
|
|
12
|
-
from sensor.
|
|
12
|
+
from sensor.sensor_utils import async_call, sync_call, async_exec
|
|
13
13
|
from bleak import (
|
|
14
14
|
BleakScanner,
|
|
15
15
|
AdvertisementData,
|
|
@@ -27,6 +27,7 @@ class SensorController:
|
|
|
27
27
|
with SensorController._instance_lock:
|
|
28
28
|
if not hasattr(SensorController, "_instance"):
|
|
29
29
|
SensorController._instance = object.__new__(cls)
|
|
30
|
+
|
|
30
31
|
return SensorController._instance
|
|
31
32
|
|
|
32
33
|
"""
|
|
@@ -37,21 +38,8 @@ class SensorController:
|
|
|
37
38
|
"""
|
|
38
39
|
初始化 SensorController 实例。
|
|
39
40
|
"""
|
|
40
|
-
self._event_loop = asyncio.new_event_loop()
|
|
41
|
-
self._event_thread = threading.Thread(target=start_loop, args=(self._event_loop,))
|
|
42
|
-
self._event_thread.daemon = True
|
|
43
|
-
self._event_thread.name = "SensorController event"
|
|
44
|
-
self._event_thread.start()
|
|
45
|
-
self._gforce_event_loop = asyncio.new_event_loop()
|
|
46
|
-
self._gforce_event_thread = threading.Thread(target=start_loop, args=(self._gforce_event_loop,))
|
|
47
|
-
self._gforce_event_thread.daemon = True
|
|
48
|
-
self._gforce_event_thread.name = "BLE operation"
|
|
49
|
-
self._gforce_event_thread.start()
|
|
50
|
-
self._scanner = BleakScanner(
|
|
51
|
-
detection_callback=self._match_device,
|
|
52
|
-
service_uuids=[SERVICE_GUID, RFSTAR_SERVICE_GUID],
|
|
53
|
-
)
|
|
54
41
|
self._is_scanning = False
|
|
42
|
+
self._scanner: BleakScanner = None
|
|
55
43
|
self._device_callback: Callable[[List[sensor_profile.BLEDevice]], None] = None
|
|
56
44
|
self._device_callback_period = 0
|
|
57
45
|
self._enable_callback: Callable[[bool], None] = None
|
|
@@ -62,15 +50,16 @@ class SensorController:
|
|
|
62
50
|
反初始化 SensorController 类的实例。
|
|
63
51
|
|
|
64
52
|
"""
|
|
65
|
-
self._event_loop.close()
|
|
66
|
-
self._gforce_event_loop.close()
|
|
67
53
|
|
|
68
54
|
def terminate(self):
|
|
69
|
-
|
|
55
|
+
sensor_utils._terminated = True
|
|
56
|
+
|
|
70
57
|
for sensor in self._sensor_profiles.values():
|
|
71
58
|
if sensor.deviceState == DeviceStateEx.Connected or sensor.deviceState == DeviceStateEx.Ready:
|
|
72
59
|
sensor._destroy()
|
|
73
60
|
|
|
61
|
+
sensor_utils.Terminate()
|
|
62
|
+
|
|
74
63
|
def _match_device(self, _device: bleak.BLEDevice, adv: AdvertisementData):
|
|
75
64
|
if _device.name == None:
|
|
76
65
|
return False
|
|
@@ -82,7 +71,7 @@ class SensorController:
|
|
|
82
71
|
return False
|
|
83
72
|
|
|
84
73
|
@property
|
|
85
|
-
def
|
|
74
|
+
def isScanning(self) -> bool:
|
|
86
75
|
"""
|
|
87
76
|
检查是否正在扫描。
|
|
88
77
|
|
|
@@ -150,15 +139,23 @@ class SensorController:
|
|
|
150
139
|
if deviceMap.get(mac) != None:
|
|
151
140
|
devices.append(self._sensor_profiles[mac].BLEDevice)
|
|
152
141
|
else:
|
|
153
|
-
newSensor = SensorProfile(device, adv, mac
|
|
142
|
+
newSensor = SensorProfile(device, adv, mac)
|
|
154
143
|
deviceMap[mac] = newSensor
|
|
155
144
|
devices.append(newSensor.BLEDevice)
|
|
156
145
|
|
|
157
146
|
self._sensor_profiles = deviceMap
|
|
158
147
|
return devices
|
|
159
148
|
|
|
149
|
+
def _init_scan(self):
|
|
150
|
+
if self._scanner == None:
|
|
151
|
+
self._scanner = BleakScanner(
|
|
152
|
+
detection_callback=self._match_device,
|
|
153
|
+
service_uuids=[SERVICE_GUID, RFSTAR_SERVICE_GUID],
|
|
154
|
+
)
|
|
155
|
+
|
|
160
156
|
async def _async_scan(self, period):
|
|
161
157
|
self._is_scanning = True
|
|
158
|
+
self._init_scan()
|
|
162
159
|
found_devices = await self._scanner.discover(timeout=period / 1000, return_adv=True)
|
|
163
160
|
self._is_scanning = False
|
|
164
161
|
return self._process_ble_devices(found_devices)
|
|
@@ -171,7 +168,7 @@ class SensorController:
|
|
|
171
168
|
|
|
172
169
|
:return: List[sensor_profile.BLEDevice]: BLEDevice列表
|
|
173
170
|
"""
|
|
174
|
-
return sync_call(self.
|
|
171
|
+
return sync_call(self._async_scan(period))
|
|
175
172
|
|
|
176
173
|
async def asyncScan(self, period) -> List[sensor_profile.BLEDevice]:
|
|
177
174
|
"""
|
|
@@ -181,22 +178,23 @@ class SensorController:
|
|
|
181
178
|
|
|
182
179
|
:return: List[sensor_profile.BLEDevice]: BLEDevice列表
|
|
183
180
|
"""
|
|
184
|
-
return await async_call(self.
|
|
181
|
+
return await async_call(self._async_scan(period))
|
|
185
182
|
|
|
186
183
|
async def _device_scan_callback(self, devices: List[sensor_profile.BLEDevice]):
|
|
187
|
-
if self._device_callback:
|
|
184
|
+
if not sensor_utils._terminated and self._device_callback:
|
|
188
185
|
try:
|
|
189
|
-
self._device_callback
|
|
186
|
+
asyncio.get_event_loop().run_in_executor(None, self._device_callback, devices)
|
|
190
187
|
except Exception as e:
|
|
191
188
|
print(e)
|
|
192
189
|
|
|
193
|
-
if self._is_scanning:
|
|
194
|
-
async_exec(self.
|
|
190
|
+
if not sensor_utils._terminated and self._is_scanning:
|
|
191
|
+
async_exec(self._startScan())
|
|
195
192
|
|
|
196
193
|
async def _startScan(self) -> bool:
|
|
194
|
+
self._init_scan()
|
|
197
195
|
found_devices = await self._scanner.discover(timeout=self._device_callback_period / 1000, return_adv=True)
|
|
198
196
|
devices = self._process_ble_devices(found_devices)
|
|
199
|
-
async_exec(self.
|
|
197
|
+
async_exec(self._device_scan_callback(devices))
|
|
200
198
|
|
|
201
199
|
def startScan(self, periodInMs: int) -> bool:
|
|
202
200
|
"""
|
|
@@ -212,7 +210,7 @@ class SensorController:
|
|
|
212
210
|
self._is_scanning = True
|
|
213
211
|
self._device_callback_period = periodInMs
|
|
214
212
|
|
|
215
|
-
async_exec(self.
|
|
213
|
+
async_exec(self._startScan())
|
|
216
214
|
return True
|
|
217
215
|
|
|
218
216
|
def stopScan(self) -> None:
|