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.
Files changed (29) hide show
  1. {sensor_sdk-0.1.4/sensor_sdk.egg-info → sensor_sdk-0.1.9}/PKG-INFO +1 -1
  2. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/__init__.py +6 -0
  3. sensor_sdk-0.1.9/sensor/bleak_no_ack_patch.py +75 -0
  4. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/bleak_process.py +24 -12
  5. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_data_context.py +185 -122
  6. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_utils.py +24 -23
  7. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9/sensor_sdk.egg-info}/PKG-INFO +1 -1
  8. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/SOURCES.txt +1 -0
  9. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/setup.py +1 -1
  10. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/LICENSE.txt +0 -0
  11. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/README.md +0 -0
  12. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/bleak_host.py +0 -0
  13. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/fb/DataType.py +0 -0
  14. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/fb/Sample.py +0 -0
  15. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/fb/SensorData.py +0 -0
  16. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/fb/__init__.py +0 -0
  17. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/gforce.py +0 -0
  18. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sdk_log.py +0 -0
  19. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_controller.py +0 -0
  20. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_data.py +0 -0
  21. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_data_pool.py +0 -0
  22. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_device.py +0 -0
  23. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/sensor_profile.py +0 -0
  24. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor/winrt_high_throughput.py +0 -0
  25. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/dependency_links.txt +0 -0
  26. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/requires.txt +0 -0
  27. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/top_level.txt +0 -0
  28. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/sensor_sdk.egg-info/zip-safe +0 -0
  29. {sensor_sdk-0.1.4 → sensor_sdk-0.1.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sensor-sdk
3
- Version: 0.1.4
3
+ Version: 0.1.9
4
4
  Summary: Python sdk for Synchroni
5
5
  Home-page: https://github.com/oymotion/SynchroniSDKPython
6
6
  Author: Martin Ye
@@ -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
- interval = self._power_intervals.get(device_mac, 0)
843
- while interval > 0 and device_mac in self._gforces:
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
- break
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
- try:
868
- if ctx.isUniversalStream:
869
- await ctx._processUniversalData(local_buf, on_data, on_error)
870
- else:
871
- await ctx._process_data(local_buf, on_data, on_error)
872
- except asyncio.CancelledError as e:
873
- SdkLog.exception(_TAG, "Data loop cancelled")
874
- except Exception as e:
875
- SdkLog.exception(_TAG, "Error in data loop")
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
- index = 0
1103
- last_cut = -1
1104
- data_size = len(self._concatDataBuffer)
1102
+ try:
1103
+ index = 0
1104
+ last_cut = -1
1105
+ data_size = len(self._concatDataBuffer)
1105
1106
 
1106
- while self._is_running:
1107
- if index >= data_size:
1108
- break
1107
+ while self._is_running:
1108
+ if index >= data_size:
1109
+ break
1109
1110
 
1110
- if self._concatDataBuffer[index] == 0x55:
1111
- if (index + 1) >= data_size:
1112
- index = data_size
1113
- continue
1114
- n = self._concatDataBuffer[index + 1]
1115
- if n < 2 or (index + 1 + n + 1) >= data_size:
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
- continue
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
- if last_cut > 0:
1132
- self._concatDataBuffer = self._concatDataBuffer[last_cut + 1:]
1133
- last_cut = -1
1134
- index = 0
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
- sample.rawData = int(saturationData[index])
1271
- sample.data = impedanceData[index]
1272
- sample.impedance = impedanceData[index]
1273
- sample.saturation = saturationData[index]
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
- return len(data) >= dataOffset + sensor_data_ref.packageSampleCount * frameSize
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
- if sensorData.lastPackageCounter < 0 and newPackageIndex > 0:
1420
- sensorData.lastPackageIndex = lastPackageIndex = newPackageIndex - 1
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
- if packageIndex < lastPackageIndex:
1424
- packageIndex += (maxPackageIndex + 1)
1425
- elif packageIndex == lastPackageIndex:
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 * (deltaPackageIndex - 1)
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 lostSampleCount < 100:
1521
+ if lostPackageCounter < 20:
1445
1522
  self.readSamples(data, sensorData, 0, dataGap, lostSampleCount)
1446
1523
 
1447
- if newPackageIndex == 0:
1448
- sensorData.lastPackageIndex = maxPackageIndex
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
- index = 0
1746
- last_cut = -1
1747
- data_size = len(self._concatDataBuffer)
1803
+ try:
1804
+ index = 0
1805
+ last_cut = -1
1806
+ data_size = len(self._concatDataBuffer)
1748
1807
 
1749
- while self._is_running:
1750
- if index >= data_size:
1751
- break
1808
+ while self._is_running:
1809
+ if index >= data_size:
1810
+ break
1752
1811
 
1753
- if self._concatDataBuffer[index] == 0x55:
1754
- if (index + 1) >= data_size:
1755
- index = data_size
1756
- continue
1757
- n = self._concatDataBuffer[index + 1]
1758
- if n < 2 or (index + 1 + n + 2) >= data_size:
1759
- index += 1
1760
- continue
1761
- crc16 = (self._concatDataBuffer[index + 1 + n + 2] << 8) | self._concatDataBuffer[index + 1 + n + 1]
1762
- calc_crc = sensor_utils.crc16_cal(self._concatDataBuffer[index + 2: index + 2 + n], n)
1763
- if crc16 != calc_crc:
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
- continue
1766
- if self._is_data_transfering:
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
- if self._processDataPackage(data_package, buf, on_error_callback):
1769
- last_cut = index = index + 2 + n + 1
1770
- index += 1
1771
- elif self._concatDataBuffer[index] == 0xAA:
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
- continue
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
- if not sensor_utils._terminated:
1787
- await self.gForce.async_on_cmd_response(data_package)
1788
- last_cut = index = index + 2 + n + 1
1789
- index += 1
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
- if last_cut > 0:
1794
- self._concatDataBuffer = self._concatDataBuffer[last_cut + 1:]
1795
- last_cut = -1
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
- for task in asyncio.all_tasks():
43
- task.cancel()
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
- _cancel_tasks()
47
- except RuntimeError:
48
- # 当前线程没有运行的事件循环,尝试在管理的 runloop 上取消任务
49
- if _runloop is not None and _runloop.is_running():
50
- try:
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
- if _needCloseRunloop:
59
- try:
60
- if _runloop is not None and _runloop.is_running():
61
- _runloop.call_soon_threadsafe(_runloop.stop)
62
- if _event_thread is not None and _event_thread.is_alive():
63
- _event_thread.join(timeout=2.0)
64
- if _runloop is not None and not _runloop.is_running():
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sensor-sdk
3
- Version: 0.1.4
3
+ Version: 0.1.9
4
4
  Summary: Python sdk for Synchroni
5
5
  Home-page: https://github.com/oymotion/SynchroniSDKPython
6
6
  Author: Martin Ye
@@ -3,6 +3,7 @@ README.md
3
3
  setup.py
4
4
  sensor/__init__.py
5
5
  sensor/bleak_host.py
6
+ sensor/bleak_no_ack_patch.py
6
7
  sensor/bleak_process.py
7
8
  sensor/gforce.py
8
9
  sensor/sdk_log.py
@@ -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.4",
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