sensor-sdk 0.1.4__tar.gz → 0.1.9__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.1.4/sensor_sdk.egg-info → sensor_sdk-0.1.9}/PKG-INFO +1 -1
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/__init__.py +6 -0
- sensor_sdk-0.1.9/sensor/bleak_no_ack_patch.py +75 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/bleak_process.py +24 -12
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_data_context.py +185 -122
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_utils.py +24 -23
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9/sensor_sdk.egg-info}/PKG-INFO +1 -1
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/SOURCES.txt +1 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/setup.py +1 -1
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/LICENSE.txt +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/README.md +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/bleak_host.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/fb/DataType.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/fb/Sample.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/fb/SensorData.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/fb/__init__.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/gforce.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sdk_log.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_controller.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_data.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_data_pool.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_device.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_profile.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/winrt_high_throughput.py +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/dependency_links.txt +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/requires.txt +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/top_level.txt +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/zip-safe +0 -0
- {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/setup.cfg +0 -0
|
@@ -3,10 +3,16 @@ from sensor.sensor_profile import SensorProfile
|
|
|
3
3
|
from sensor.sensor_device import BLEDevice, DeviceInfo, DeviceStateEx
|
|
4
4
|
from sensor.sensor_data import DataType, Sample, SensorData
|
|
5
5
|
from sensor.winrt_high_throughput import apply as _apply_winrt_high_throughput_patch
|
|
6
|
+
from sensor.bleak_no_ack_patch import apply as _apply_bleak_no_ack_patch
|
|
7
|
+
import os
|
|
6
8
|
|
|
7
9
|
# Windows 下尝试给 bleak WinRT backend 打高吞吐率连接参数补丁
|
|
8
10
|
_apply_winrt_high_throughput_patch()
|
|
9
11
|
|
|
12
|
+
# 如果设置了环境变量 SENSOR_SDK_FORCE_NO_ACK=1,则对命令特征强制 write-without-response
|
|
13
|
+
if os.environ.get("SENSOR_SDK_FORCE_NO_ACK", "0") == "1":
|
|
14
|
+
_apply_bleak_no_ack_patch()
|
|
15
|
+
|
|
10
16
|
__all__ = [
|
|
11
17
|
"SensorController",
|
|
12
18
|
"SensorControllerInstance",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""强制 bleak 对指定特征使用 write-without-response(no ACK)的补丁。
|
|
2
|
+
|
|
3
|
+
某些 Windows 主机上 write-with-response 会因为系统 ACK 等待导致命令超时,
|
|
4
|
+
把命令特征强制改成无响应写入可以显著提升命令通道的实时性。
|
|
5
|
+
|
|
6
|
+
默认只作用于 SDK 已知的两个 CMD 特征 UUID;传入空列表或 `None` 则全局生效。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from functools import wraps
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# SDK 默认使用的两条命令特征
|
|
15
|
+
DEFAULT_CMD_CHAR_UUIDS = {
|
|
16
|
+
"f000ffe1-0451-4000-b000-000000000000", # OYM CMD
|
|
17
|
+
"00000002-0000-1000-8000-00805f9b34fb", # RFSTAR CMD
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _char_uuid(char_specifier) -> str:
|
|
22
|
+
"""尽量把 char_specifier 转成小写 UUID 字符串。"""
|
|
23
|
+
if isinstance(char_specifier, str):
|
|
24
|
+
return char_specifier.lower()
|
|
25
|
+
try:
|
|
26
|
+
# bleak 的 BleakGATTCharacteristic 对象有 uuid 属性
|
|
27
|
+
return str(char_specifier.uuid).lower()
|
|
28
|
+
except Exception:
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def apply(cmd_char_uuids=DEFAULT_CMD_CHAR_UUIDS):
|
|
33
|
+
"""给 BleakClient.write_gatt_char 打补丁,强制指定特征使用 response=False。
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
cmd_char_uuids: 需要强制 no-ack 的特征 UUID 集合。传 None 表示所有写入都强制 no-ack。
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
from bleak import BleakClient
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.debug("bleak not available, skip no-ack patch: %s", e)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
if getattr(BleakClient, "_no_ack_patched", False):
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
orig_write = BleakClient.write_gatt_char
|
|
48
|
+
|
|
49
|
+
@wraps(orig_write)
|
|
50
|
+
async def patched_write(self, char_specifier, data, response=None):
|
|
51
|
+
if cmd_char_uuids is None or _char_uuid(char_specifier) in cmd_char_uuids:
|
|
52
|
+
response = False
|
|
53
|
+
return await orig_write(self, char_specifier, data, response=response)
|
|
54
|
+
|
|
55
|
+
BleakClient.write_gatt_char = patched_write
|
|
56
|
+
BleakClient._no_ack_patched = True # type: ignore[attr-defined]
|
|
57
|
+
logger.debug(
|
|
58
|
+
"Applied bleak write-without-response patch (targets: %s)",
|
|
59
|
+
"all chars" if cmd_char_uuids is None else cmd_char_uuids,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def reset():
|
|
64
|
+
"""移除补丁(主要用于测试)。"""
|
|
65
|
+
try:
|
|
66
|
+
from bleak import BleakClient
|
|
67
|
+
except Exception:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if not getattr(BleakClient, "_no_ack_patched", False):
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# 找到原始的未绑定方法比较麻烦,这里简单把标记清掉,
|
|
74
|
+
# 实际撤销需要保存原始方法;apply 没有保存,故 reset 仅作占位。
|
|
75
|
+
BleakClient._no_ack_patched = False # type: ignore[attr-defined]
|
|
@@ -839,14 +839,17 @@ class BleakProcess(multiprocessing.Process):
|
|
|
839
839
|
|
|
840
840
|
async def _battery_loop(self, device_mac: str):
|
|
841
841
|
|
|
842
|
-
|
|
843
|
-
|
|
842
|
+
while True:
|
|
843
|
+
interval = self._power_intervals.get(device_mac, 0)
|
|
844
|
+
if interval <= 0 or device_mac not in self._gforces:
|
|
845
|
+
break
|
|
844
846
|
await asyncio.sleep(interval / 1000)
|
|
845
847
|
try:
|
|
846
848
|
power = await self._gforces[device_mac].get_battery_level()
|
|
847
849
|
self._publish("power_changed", device_mac=device_mac, power=power)
|
|
848
850
|
except Exception:
|
|
849
|
-
|
|
851
|
+
# 单次刷新失败不中断循环,否则一次超时/丢包后就再也无法刷新电量
|
|
852
|
+
SdkLog.exception(_TAG, f"Battery refresh failed: {device_mac}")
|
|
850
853
|
|
|
851
854
|
async def _device_data_loop(self, device_mac: str):
|
|
852
855
|
|
|
@@ -864,15 +867,24 @@ class BleakProcess(multiprocessing.Process):
|
|
|
864
867
|
def on_error(message: str):
|
|
865
868
|
self._publish("error", device_mac=device_mac, message=message)
|
|
866
869
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
870
|
+
# 持续运行,异常后自动重启,避免单包错误导致整个解析线程退出
|
|
871
|
+
while not self._should_exit and device_mac in self._data_ctxs:
|
|
872
|
+
try:
|
|
873
|
+
if ctx.isUniversalStream:
|
|
874
|
+
await ctx._processUniversalData(local_buf, on_data, on_error)
|
|
875
|
+
else:
|
|
876
|
+
await ctx._process_data(local_buf, on_data, on_error)
|
|
877
|
+
# 正常返回说明 ctx 已关闭或停止
|
|
878
|
+
break
|
|
879
|
+
except asyncio.CancelledError:
|
|
880
|
+
SdkLog.i(_TAG, f"Data loop cancelled: {device_mac}")
|
|
881
|
+
break
|
|
882
|
+
except Exception as e:
|
|
883
|
+
SdkLog.exception(_TAG, f"Error in data loop, restarting: {device_mac}")
|
|
884
|
+
try:
|
|
885
|
+
await asyncio.sleep(0.5)
|
|
886
|
+
except asyncio.CancelledError:
|
|
887
|
+
break
|
|
876
888
|
|
|
877
889
|
async def _do_start_notification(self, cmd: dict):
|
|
878
890
|
|
|
@@ -544,7 +544,7 @@ class SensorProfileDataCtx:
|
|
|
544
544
|
async def initIMU(self, packageCount: int) -> int:
|
|
545
545
|
SdkLog.d(_TAG, "initIMU(...)")
|
|
546
546
|
IMU_TYPE_QAT6 = 0x0004
|
|
547
|
-
min_package_sample_count = 2
|
|
547
|
+
min_package_sample_count = 2 if self._chip_type == BLEChipType.OYM else 1
|
|
548
548
|
self.isContainQAT6 = False
|
|
549
549
|
|
|
550
550
|
if not self.hasIMU():
|
|
@@ -1099,39 +1099,44 @@ class SensorProfileDataCtx:
|
|
|
1099
1099
|
SdkLog.exception(_TAG, "Unexpected error")
|
|
1100
1100
|
|
|
1101
1101
|
if self.notifyDataFlag & DataSubscription.DNF_CONCAT_BLE != 0:
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1102
|
+
try:
|
|
1103
|
+
index = 0
|
|
1104
|
+
last_cut = -1
|
|
1105
|
+
data_size = len(self._concatDataBuffer)
|
|
1105
1106
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1107
|
+
while self._is_running:
|
|
1108
|
+
if index >= data_size:
|
|
1109
|
+
break
|
|
1109
1110
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1111
|
+
if self._concatDataBuffer[index] == 0x55:
|
|
1112
|
+
if (index + 1) >= data_size:
|
|
1113
|
+
index = data_size
|
|
1114
|
+
continue
|
|
1115
|
+
n = self._concatDataBuffer[index + 1]
|
|
1116
|
+
if n < 2 or (index + 1 + n + 1) >= data_size:
|
|
1117
|
+
index += 1
|
|
1118
|
+
continue
|
|
1119
|
+
crc8 = (self._concatDataBuffer[index + 1 + n + 1])
|
|
1120
|
+
calc_crc = sensor_utils.calc_crc8(self._concatDataBuffer[index + 2: index + 2 + n])
|
|
1121
|
+
if crc8 != calc_crc:
|
|
1122
|
+
index += 1
|
|
1123
|
+
continue
|
|
1124
|
+
if self._is_data_transfering:
|
|
1125
|
+
data_package = bytes(self._concatDataBuffer[index + 2: index + 2 + n])
|
|
1126
|
+
if self._processDataPackage(data_package, buf, on_error_callback):
|
|
1127
|
+
last_cut = index = index + 2 + n
|
|
1116
1128
|
index += 1
|
|
1117
|
-
|
|
1118
|
-
crc8 = (self._concatDataBuffer[index + 1 + n + 1])
|
|
1119
|
-
calc_crc = sensor_utils.calc_crc8(self._concatDataBuffer[index + 2: index + 2 + n])
|
|
1120
|
-
if crc8 != calc_crc:
|
|
1129
|
+
else:
|
|
1121
1130
|
index += 1
|
|
1122
|
-
continue
|
|
1123
|
-
if self._is_data_transfering:
|
|
1124
|
-
data_package = bytes(self._concatDataBuffer[index + 2: index + 2 + n])
|
|
1125
|
-
if self._processDataPackage(data_package, buf, on_error_callback):
|
|
1126
|
-
last_cut = index = index + 2 + n
|
|
1127
|
-
index += 1
|
|
1128
|
-
else:
|
|
1129
|
-
index += 1
|
|
1130
1131
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1132
|
+
if last_cut > 0:
|
|
1133
|
+
self._concatDataBuffer = self._concatDataBuffer[last_cut + 1:]
|
|
1134
|
+
last_cut = -1
|
|
1135
|
+
index = 0
|
|
1136
|
+
|
|
1137
|
+
self._last_progress_time = time.time()
|
|
1138
|
+
except Exception as e:
|
|
1139
|
+
SdkLog.exception(_TAG, "Unexpected error in concat data processing")
|
|
1135
1140
|
|
|
1136
1141
|
def _processDataPackage(self, data: bytes, buf: Queue[bytes], on_error_callback=None) -> bool:
|
|
1137
1142
|
if not data:
|
|
@@ -1240,7 +1245,11 @@ class SensorProfileDataCtx:
|
|
|
1240
1245
|
if on_error_callback:
|
|
1241
1246
|
on_error_callback("Incomplete Impedance packet received")
|
|
1242
1247
|
return False
|
|
1243
|
-
|
|
1248
|
+
|
|
1249
|
+
sensor_data = self.sensorDatas[SensorDataType.DATA_TYPE_IMPEDANCE]
|
|
1250
|
+
self.checkReadSamples(data, sensor_data, 0, -1, on_error_callback)
|
|
1251
|
+
sampleInterval = 1000.0 / sensor_data.sampleRate if sensor_data.sampleRate > 0 else 0
|
|
1252
|
+
|
|
1244
1253
|
for index in range(channelCount):
|
|
1245
1254
|
impedance = struct.unpack_from("<f", data, offset)[0]
|
|
1246
1255
|
offset += 4
|
|
@@ -1257,20 +1266,24 @@ class SensorProfileDataCtx:
|
|
|
1257
1266
|
|
|
1258
1267
|
self.impedanceData = impedanceData
|
|
1259
1268
|
self.saturationData = saturationData
|
|
1260
|
-
|
|
1261
|
-
sensor_data = self.sensorDatas[SensorDataType.DATA_TYPE_IMPEDANCE]
|
|
1262
|
-
self.checkReadSamples(data, sensor_data, 0, -1)
|
|
1263
|
-
sampleInterval = 1000.0 / sensor_data.sampleRate if sensor_data.sampleRate > 0 else 0
|
|
1264
1269
|
lastSampleIndex = sensor_data.lastPackageCounter * sensor_data.packageSampleCount
|
|
1265
1270
|
|
|
1266
1271
|
sensor_data.channelSamples = []
|
|
1267
1272
|
for index in range(channelCount):
|
|
1268
1273
|
samples = []
|
|
1269
1274
|
sample = self._object_pool.acquire_sample()
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1275
|
+
|
|
1276
|
+
impedanceValue = impedanceData[index]
|
|
1277
|
+
saturationValue = saturationData[index]
|
|
1278
|
+
if not math.isfinite(impedanceValue):
|
|
1279
|
+
impedanceValue = 0.0
|
|
1280
|
+
if not math.isfinite(saturationValue):
|
|
1281
|
+
saturationValue = 0.0
|
|
1282
|
+
|
|
1283
|
+
sample.rawData = int(saturationValue)
|
|
1284
|
+
sample.data = impedanceValue
|
|
1285
|
+
sample.impedance = impedanceValue
|
|
1286
|
+
sample.saturation = saturationValue
|
|
1274
1287
|
sample.sampleIndex = lastSampleIndex
|
|
1275
1288
|
sample.timeStampInMs = int(lastSampleIndex * sampleInterval)
|
|
1276
1289
|
sample.channelIndex = index
|
|
@@ -1291,7 +1304,8 @@ class SensorProfileDataCtx:
|
|
|
1291
1304
|
if sensor_data_quat is not None:
|
|
1292
1305
|
frameSize += 12
|
|
1293
1306
|
|
|
1294
|
-
|
|
1307
|
+
expected = dataOffset + sensor_data_ref.packageSampleCount * frameSize
|
|
1308
|
+
return len(data) == expected
|
|
1295
1309
|
|
|
1296
1310
|
def _process_imu_samples(self, data: bytes, buf: Queue[bytes], on_error_callback=None) -> bool:
|
|
1297
1311
|
sensor_data_acc = self.sensorDatas[SensorDataType.DATA_TYPE_ACC]
|
|
@@ -1398,6 +1412,47 @@ class SensorProfileDataCtx:
|
|
|
1398
1412
|
return ReadSamplesResult.Error
|
|
1399
1413
|
if sensorData is None or sensorData.packageSampleCount <= 0 or sensorData.channelCount <= 0 or sensorData.minPackageSampleCount <= 0 or sensorData.K <= 0:
|
|
1400
1414
|
return ReadSamplesResult.Error
|
|
1415
|
+
|
|
1416
|
+
def _type_name():
|
|
1417
|
+
try:
|
|
1418
|
+
return DataType(sensorData.dataType).name
|
|
1419
|
+
except Exception:
|
|
1420
|
+
return str(sensorData.dataType)
|
|
1421
|
+
|
|
1422
|
+
# 长度预检:在解析包序号/样本前就发现数据长度不足,并报告数据类型
|
|
1423
|
+
if dataGap >= 0 and sensorData.packageSampleCount > 0:
|
|
1424
|
+
if sensorData.resolutionBits in (7, 8):
|
|
1425
|
+
bytesPerChannel = 1
|
|
1426
|
+
elif sensorData.resolutionBits in (12, 16, 17, 0):
|
|
1427
|
+
bytesPerChannel = 2
|
|
1428
|
+
elif sensorData.resolutionBits == 24:
|
|
1429
|
+
bytesPerChannel = 3
|
|
1430
|
+
elif sensorData.resolutionBits in (31, 32, 33):
|
|
1431
|
+
bytesPerChannel = 4
|
|
1432
|
+
else:
|
|
1433
|
+
bytesPerChannel = 2
|
|
1434
|
+
|
|
1435
|
+
realChannelCount = 0
|
|
1436
|
+
for i in range(sensorData.channelCount):
|
|
1437
|
+
if (sensorData.channelMask & (1 << i)) != 0:
|
|
1438
|
+
realChannelCount += 1
|
|
1439
|
+
|
|
1440
|
+
expected = (
|
|
1441
|
+
dataOffset
|
|
1442
|
+
+ bytesPerChannel * realChannelCount * sensorData.packageSampleCount
|
|
1443
|
+
+ dataGap * (sensorData.packageSampleCount - 1)
|
|
1444
|
+
)
|
|
1445
|
+
if dataGap == 0:
|
|
1446
|
+
# 单一流数据包:要求长度完全一致
|
|
1447
|
+
if expected != len(data):
|
|
1448
|
+
SdkLog.i(_TAG, f"Invalid dataLength:{len(data)} (expected {expected}) for data type {_type_name()}")
|
|
1449
|
+
return ReadSamplesResult.Error
|
|
1450
|
+
else:
|
|
1451
|
+
# IMU 复合包:允许包含多个子流,只检查长度不足
|
|
1452
|
+
if expected > len(data):
|
|
1453
|
+
SdkLog.i(_TAG, f"Invalid dataLength:{len(data)} (expected at least {expected}) for data type {_type_name()}")
|
|
1454
|
+
return ReadSamplesResult.Error
|
|
1455
|
+
|
|
1401
1456
|
try:
|
|
1402
1457
|
packageIndex = 0
|
|
1403
1458
|
maxPackageIndex = 0
|
|
@@ -1416,39 +1471,58 @@ class SensorProfileDataCtx:
|
|
|
1416
1471
|
offset += sensorData.packageIndexLength
|
|
1417
1472
|
newPackageIndex = packageIndex
|
|
1418
1473
|
lastPackageIndex = sensorData.lastPackageIndex
|
|
1419
|
-
|
|
1420
|
-
|
|
1474
|
+
|
|
1475
|
+
# 首包:把上一包序号设为合法的前一个值,便于后续统一判断
|
|
1476
|
+
if sensorData.lastPackageCounter < 0:
|
|
1421
1477
|
sensorData.lastPackageCounter = 0
|
|
1478
|
+
if newPackageIndex > 0:
|
|
1479
|
+
lastPackageIndex = newPackageIndex - 1
|
|
1480
|
+
else:
|
|
1481
|
+
lastPackageIndex = maxPackageIndex
|
|
1482
|
+
sensorData.lastPackageIndex = lastPackageIndex
|
|
1422
1483
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1484
|
+
# 合法翻卷区间:last 在 [max-2, max] 且 new 在 [0, 2]
|
|
1485
|
+
if newPackageIndex <= 2 and lastPackageIndex >= maxPackageIndex - 2:
|
|
1486
|
+
packageIndex = maxPackageIndex + 1 + newPackageIndex
|
|
1487
|
+
elif newPackageIndex == lastPackageIndex:
|
|
1426
1488
|
return ReadSamplesResult.Repeated
|
|
1489
|
+
elif newPackageIndex < lastPackageIndex:
|
|
1490
|
+
SdkLog.i(_TAG, (
|
|
1491
|
+
"Illegal package index backward|MAC|" + str(sensorData.deviceMac)
|
|
1492
|
+
+ "|TYPE|" + str(sensorData.dataType)
|
|
1493
|
+
+ "|LAST_IDX|" + str(lastPackageIndex)
|
|
1494
|
+
+ "|CURR_IDX|" + str(newPackageIndex)
|
|
1495
|
+
+ "|DELTA|" + str(lastPackageIndex - newPackageIndex)
|
|
1496
|
+
))
|
|
1497
|
+
return ReadSamplesResult.Error
|
|
1427
1498
|
|
|
1428
1499
|
deltaPackageIndex = packageIndex - lastPackageIndex
|
|
1500
|
+
if deltaPackageIndex > 20:
|
|
1501
|
+
SdkLog.i(_TAG, (
|
|
1502
|
+
"Illegal package index jump|MAC|" + str(sensorData.deviceMac)
|
|
1503
|
+
+ "|TYPE|" + str(sensorData.dataType)
|
|
1504
|
+
+ "|LAST_IDX|" + str(lastPackageIndex)
|
|
1505
|
+
+ "|CURR_IDX|" + str(newPackageIndex)
|
|
1506
|
+
+ "|DELTA|" + str(deltaPackageIndex)
|
|
1507
|
+
))
|
|
1508
|
+
return ReadSamplesResult.Error
|
|
1509
|
+
|
|
1429
1510
|
lostPackageCounter = deltaPackageIndex - 1
|
|
1430
|
-
if lostPackageCounter > 65534:
|
|
1431
|
-
lostPackageCounter = 1
|
|
1432
|
-
elif lostPackageCounter > 50:
|
|
1433
|
-
lostPackageCounter = 50
|
|
1434
1511
|
sensorData.lostPackageCount = sensorData.lostPackageCount + lostPackageCounter
|
|
1435
1512
|
|
|
1436
1513
|
if deltaPackageIndex > 1:
|
|
1437
|
-
lostSampleCount = sensorData.packageSampleCount *
|
|
1514
|
+
lostSampleCount = sensorData.packageSampleCount * lostPackageCounter
|
|
1438
1515
|
SdkLog.i(_TAG, (
|
|
1439
1516
|
"MSG|LOST SAMPLE|MAC|" + str(sensorData.deviceMac)
|
|
1440
1517
|
+ "|TYPE|" + str(sensorData.dataType)
|
|
1441
1518
|
+ "|COUNT|" + str(lostSampleCount)
|
|
1442
1519
|
))
|
|
1443
1520
|
|
|
1444
|
-
if
|
|
1521
|
+
if lostPackageCounter < 20:
|
|
1445
1522
|
self.readSamples(data, sensorData, 0, dataGap, lostSampleCount)
|
|
1446
1523
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
else:
|
|
1450
|
-
sensorData.lastPackageIndex = newPackageIndex - 1
|
|
1451
|
-
sensorData.lastPackageCounter += deltaPackageIndex - 1
|
|
1524
|
+
sensorData.lastPackageIndex = newPackageIndex - 1
|
|
1525
|
+
sensorData.lastPackageCounter += lostPackageCounter
|
|
1452
1526
|
|
|
1453
1527
|
sensorData.lastPackageIndex = newPackageIndex
|
|
1454
1528
|
|
|
@@ -1476,31 +1550,15 @@ class SensorProfileDataCtx:
|
|
|
1476
1550
|
lostSampleCount: int,
|
|
1477
1551
|
):
|
|
1478
1552
|
sampleCount = sensorData.packageSampleCount
|
|
1553
|
+
def _type_name():
|
|
1554
|
+
try:
|
|
1555
|
+
return DataType(sensorData.dataType).name
|
|
1556
|
+
except Exception:
|
|
1557
|
+
return str(sensorData.dataType)
|
|
1558
|
+
|
|
1479
1559
|
if lostSampleCount <= 0:
|
|
1480
1560
|
if data is None or offset < 0 or offset > len(data):
|
|
1481
|
-
raise ValueError("Invalid data or offset")
|
|
1482
|
-
|
|
1483
|
-
if sensorData.resolutionBits in (7, 8):
|
|
1484
|
-
bytesPerChannel = 1
|
|
1485
|
-
elif sensorData.resolutionBits in (12, 16, 17, 0):
|
|
1486
|
-
bytesPerChannel = 2
|
|
1487
|
-
elif sensorData.resolutionBits == 24:
|
|
1488
|
-
bytesPerChannel = 3
|
|
1489
|
-
elif sensorData.resolutionBits in (31, 32, 33):
|
|
1490
|
-
bytesPerChannel = 4
|
|
1491
|
-
else:
|
|
1492
|
-
bytesPerChannel = 2
|
|
1493
|
-
|
|
1494
|
-
dataLength = len(data)
|
|
1495
|
-
|
|
1496
|
-
realChannelCount = 0
|
|
1497
|
-
for channelIndex, impedanceChannelIndex in enumerate(range(sensorData.channelCount)):
|
|
1498
|
-
if (sensorData.channelMask & (1 << channelIndex)) != 0:
|
|
1499
|
-
realChannelCount += 1
|
|
1500
|
-
|
|
1501
|
-
if offset + ((bytesPerChannel * realChannelCount * sampleCount) + (dataGap * (sampleCount - 1))) > dataLength:
|
|
1502
|
-
raise ValueError(f"Invalid dataLength:{dataLength}")
|
|
1503
|
-
|
|
1561
|
+
raise ValueError(f"Invalid data or offset for data type {_type_name()}")
|
|
1504
1562
|
|
|
1505
1563
|
sampleInterval = (
|
|
1506
1564
|
int(1000.0 / sensorData.sampleRate) if sensorData.sampleRate > 0 else 0
|
|
@@ -1742,55 +1800,60 @@ class SensorProfileDataCtx:
|
|
|
1742
1800
|
except Exception as e:
|
|
1743
1801
|
SdkLog.exception(_TAG, "Error reading raw data buffer")
|
|
1744
1802
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1803
|
+
try:
|
|
1804
|
+
index = 0
|
|
1805
|
+
last_cut = -1
|
|
1806
|
+
data_size = len(self._concatDataBuffer)
|
|
1748
1807
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1808
|
+
while self._is_running:
|
|
1809
|
+
if index >= data_size:
|
|
1810
|
+
break
|
|
1752
1811
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1812
|
+
if self._concatDataBuffer[index] == 0x55:
|
|
1813
|
+
if (index + 1) >= data_size:
|
|
1814
|
+
index = data_size
|
|
1815
|
+
continue
|
|
1816
|
+
n = self._concatDataBuffer[index + 1]
|
|
1817
|
+
if n < 2 or (index + 1 + n + 2) >= data_size:
|
|
1818
|
+
index += 1
|
|
1819
|
+
continue
|
|
1820
|
+
crc16 = (self._concatDataBuffer[index + 1 + n + 2] << 8) | self._concatDataBuffer[index + 1 + n + 1]
|
|
1821
|
+
calc_crc = sensor_utils.crc16_cal(self._concatDataBuffer[index + 2: index + 2 + n], n)
|
|
1822
|
+
if crc16 != calc_crc:
|
|
1823
|
+
index += 1
|
|
1824
|
+
continue
|
|
1825
|
+
if self._is_data_transfering:
|
|
1826
|
+
data_package = bytes(self._concatDataBuffer[index + 2: index + 2 + n])
|
|
1827
|
+
if self._processDataPackage(data_package, buf, on_error_callback):
|
|
1828
|
+
last_cut = index = index + 2 + n + 1
|
|
1764
1829
|
index += 1
|
|
1765
|
-
|
|
1766
|
-
|
|
1830
|
+
elif self._concatDataBuffer[index] == 0xAA:
|
|
1831
|
+
if (index + 1) >= data_size:
|
|
1832
|
+
index = data_size
|
|
1833
|
+
continue
|
|
1834
|
+
n = self._concatDataBuffer[index + 1]
|
|
1835
|
+
if n < 2 or (index + 1 + n + 2) >= data_size:
|
|
1836
|
+
index += 1
|
|
1837
|
+
continue
|
|
1838
|
+
crc16 = (self._concatDataBuffer[index + 1 + n + 2] << 8) | self._concatDataBuffer[index + 1 + n + 1]
|
|
1839
|
+
calc_crc = sensor_utils.crc16_cal(self._concatDataBuffer[index + 2: index + 2 + n], n)
|
|
1840
|
+
if crc16 != calc_crc:
|
|
1841
|
+
index += 1
|
|
1842
|
+
continue
|
|
1767
1843
|
data_package = bytes(self._concatDataBuffer[index + 2: index + 2 + n])
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
if (index + 1) >= data_size:
|
|
1773
|
-
index = data_size
|
|
1774
|
-
continue
|
|
1775
|
-
n = self._concatDataBuffer[index + 1]
|
|
1776
|
-
if n < 2 or (index + 1 + n + 2) >= data_size:
|
|
1844
|
+
|
|
1845
|
+
if not sensor_utils._terminated:
|
|
1846
|
+
await self.gForce.async_on_cmd_response(data_package)
|
|
1847
|
+
last_cut = index = index + 2 + n + 1
|
|
1777
1848
|
index += 1
|
|
1778
|
-
|
|
1779
|
-
crc16 = (self._concatDataBuffer[index + 1 + n + 2] << 8) | self._concatDataBuffer[index + 1 + n + 1]
|
|
1780
|
-
calc_crc = sensor_utils.crc16_cal(self._concatDataBuffer[index + 2: index + 2 + n], n)
|
|
1781
|
-
if crc16 != calc_crc:
|
|
1849
|
+
else:
|
|
1782
1850
|
index += 1
|
|
1783
|
-
continue
|
|
1784
|
-
data_package = bytes(self._concatDataBuffer[index + 2: index + 2 + n])
|
|
1785
1851
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
last_cut =
|
|
1789
|
-
index
|
|
1790
|
-
else:
|
|
1791
|
-
index += 1
|
|
1852
|
+
if last_cut > 0:
|
|
1853
|
+
self._concatDataBuffer = self._concatDataBuffer[last_cut + 1:]
|
|
1854
|
+
last_cut = -1
|
|
1855
|
+
index = 0
|
|
1792
1856
|
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
index = 0
|
|
1857
|
+
self._last_progress_time = time.time()
|
|
1858
|
+
except Exception as e:
|
|
1859
|
+
SdkLog.exception(_TAG, "Unexpected error in universal concat data processing")
|
|
@@ -38,33 +38,34 @@ def Terminate():
|
|
|
38
38
|
global _runloop, _needCloseRunloop, _event_thread, _terminated
|
|
39
39
|
_terminated = True
|
|
40
40
|
|
|
41
|
+
# 如果不是我们创建的 loop(例如用户自己的 qasync/Qt loop),不要动它
|
|
42
|
+
if not _needCloseRunloop or _runloop is None:
|
|
43
|
+
return
|
|
44
|
+
|
|
41
45
|
def _cancel_tasks():
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
try:
|
|
47
|
+
# 在 loop 线程内部调用,有运行中的事件循环
|
|
48
|
+
for task in asyncio.all_tasks():
|
|
49
|
+
if not task.done():
|
|
50
|
+
task.cancel()
|
|
51
|
+
except Exception:
|
|
52
|
+
# 取消任务失败时不阻塞终止流程
|
|
53
|
+
pass
|
|
44
54
|
|
|
45
55
|
try:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
_runloop.call_soon_threadsafe(_cancel_tasks)
|
|
52
|
-
time.sleep(0.1)
|
|
53
|
-
except Exception as e:
|
|
54
|
-
SdkLog.exception(_TAG, "Error cancelling tasks during terminate")
|
|
55
|
-
except Exception as e:
|
|
56
|
-
SdkLog.exception(_TAG, "Error cancelling tasks during terminate")
|
|
56
|
+
# 取消 / stop 都必须在 loop 所在线程执行,避免跨线程调用 asyncio API 抛异常
|
|
57
|
+
if _runloop.is_running():
|
|
58
|
+
_runloop.call_soon_threadsafe(_cancel_tasks)
|
|
59
|
+
time.sleep(0.2)
|
|
60
|
+
_runloop.call_soon_threadsafe(_runloop.stop)
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
_runloop.close()
|
|
66
|
-
except Exception as e:
|
|
67
|
-
SdkLog.exception(_TAG, "Error closing runloop during terminate")
|
|
62
|
+
if _event_thread is not None and _event_thread.is_alive():
|
|
63
|
+
_event_thread.join(timeout=3.0)
|
|
64
|
+
|
|
65
|
+
# 不再主动 close loop:Windows 的 ProactorEventLoop 在 still-running 时 close()
|
|
66
|
+
# 会抛 RuntimeError。守护线程会在 loop stop 后自然退出,进程退出时回收资源。
|
|
67
|
+
except Exception as e:
|
|
68
|
+
SdkLog.exception(_TAG, "Error during terminate")
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def async_exec(function, runloop=None):
|
|
@@ -8,7 +8,7 @@ with open(os.path.join(this_directory, "README.md"), "r", encoding="utf-8") as f
|
|
|
8
8
|
|
|
9
9
|
setup(
|
|
10
10
|
name="sensor-sdk",
|
|
11
|
-
version="0.1.
|
|
11
|
+
version="0.1.9",
|
|
12
12
|
description="Python sdk for Synchroni",
|
|
13
13
|
long_description=long_description,
|
|
14
14
|
long_description_content_type="text/markdown",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|