sensor-sdk 0.1.9__tar.gz → 0.2.1__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.9/sensor_sdk.egg-info → sensor_sdk-0.2.1}/PKG-INFO +1 -1
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/bleak_no_ack_patch.py +6 -3
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/bleak_process.py +146 -7
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/gforce.py +5 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/sensor_data_context.py +100 -6
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1/sensor_sdk.egg-info}/PKG-INFO +1 -1
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/setup.py +1 -1
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/LICENSE.txt +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/README.md +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/__init__.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/bleak_host.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/fb/DataType.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/fb/Sample.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/fb/SensorData.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/fb/__init__.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/sdk_log.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/sensor_controller.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/sensor_data.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/sensor_data_pool.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/sensor_device.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/sensor_profile.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/sensor_utils.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor/winrt_high_throughput.py +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/SOURCES.txt +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/dependency_links.txt +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/requires.txt +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/top_level.txt +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/zip-safe +0 -0
- {sensor_sdk-0.1.9 → sensor_sdk-0.2.1}/setup.cfg +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
某些 Windows 主机上 write-with-response 会因为系统 ACK 等待导致命令超时,
|
|
4
4
|
把命令特征强制改成无响应写入可以显著提升命令通道的实时性。
|
|
5
5
|
|
|
6
|
-
默认只作用于 SDK 已知的两个 CMD 特征 UUID
|
|
6
|
+
默认只作用于 SDK 已知的两个 CMD 特征 UUID;传 `None` 则全局生效,空列表表示不匹配任何特征。
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import logging
|
|
@@ -53,6 +53,7 @@ def apply(cmd_char_uuids=DEFAULT_CMD_CHAR_UUIDS):
|
|
|
53
53
|
return await orig_write(self, char_specifier, data, response=response)
|
|
54
54
|
|
|
55
55
|
BleakClient.write_gatt_char = patched_write
|
|
56
|
+
BleakClient._orig_write_gatt_char = orig_write # type: ignore[attr-defined]
|
|
56
57
|
BleakClient._no_ack_patched = True # type: ignore[attr-defined]
|
|
57
58
|
logger.debug(
|
|
58
59
|
"Applied bleak write-without-response patch (targets: %s)",
|
|
@@ -70,6 +71,8 @@ def reset():
|
|
|
70
71
|
if not getattr(BleakClient, "_no_ack_patched", False):
|
|
71
72
|
return
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
orig_write = getattr(BleakClient, "_orig_write_gatt_char", None)
|
|
75
|
+
if orig_write is not None:
|
|
76
|
+
BleakClient.write_gatt_char = orig_write
|
|
77
|
+
del BleakClient._orig_write_gatt_char # type: ignore[attr-defined]
|
|
75
78
|
BleakClient._no_ack_patched = False # type: ignore[attr-defined]
|
|
@@ -25,6 +25,10 @@ OYM_DATA_NOTIFY_CHAR_UUID = "f000ffe2-0451-4000-b000-000000000000"
|
|
|
25
25
|
RFSTAR_CMD_UUID = "00000002-0000-1000-8000-00805f9b34fb"
|
|
26
26
|
RFSTAR_DATA_UUID = "00000003-0000-1000-8000-00805f9b34fb"
|
|
27
27
|
|
|
28
|
+
# 异常断开后自动重连参数
|
|
29
|
+
_MAX_RECONNECT_ATTEMPTS = 5
|
|
30
|
+
_RECONNECT_DELAY_SECONDS = 1.0
|
|
31
|
+
|
|
28
32
|
|
|
29
33
|
def _extract_mac(_device: bleak.BLEDevice, adv: AdvertisementData) -> str:
|
|
30
34
|
|
|
@@ -93,6 +97,8 @@ class BleakProcess(multiprocessing.Process):
|
|
|
93
97
|
self._power_intervals: dict = {} # mac -> int (ms)
|
|
94
98
|
self._data_tasks: dict = {} # mac -> asyncio.Task
|
|
95
99
|
self._battery_tasks: dict = {} # mac -> asyncio.Task
|
|
100
|
+
self._reconnect_info: dict = {} # mac -> {cmd, attempts, task, normal_disconnect}
|
|
101
|
+
self._main_event_loop = None # BleakProcess 主事件循环
|
|
96
102
|
|
|
97
103
|
# Per-device event loops and threads
|
|
98
104
|
self._event_loops: dict = {} # mac -> asyncio.AbstractEventLoop
|
|
@@ -344,6 +350,7 @@ class BleakProcess(multiprocessing.Process):
|
|
|
344
350
|
self._msg_queue = queue.Queue()
|
|
345
351
|
|
|
346
352
|
async def _run_main():
|
|
353
|
+
self._main_event_loop = asyncio.get_running_loop()
|
|
347
354
|
publisher_task = asyncio.create_task(self._publisher_task())
|
|
348
355
|
try:
|
|
349
356
|
await self._main_loop()
|
|
@@ -581,6 +588,14 @@ class BleakProcess(multiprocessing.Process):
|
|
|
581
588
|
name = cmd.get("name", "")
|
|
582
589
|
service_data = cmd.get("service_data", {})
|
|
583
590
|
|
|
591
|
+
# 保存连接信息,用于异常断开后自动重连
|
|
592
|
+
self._reconnect_info[device_mac] = {
|
|
593
|
+
"cmd": cmd,
|
|
594
|
+
"attempts": 0,
|
|
595
|
+
"task": None,
|
|
596
|
+
"normal_disconnect": False,
|
|
597
|
+
}
|
|
598
|
+
|
|
584
599
|
event_loop = self._event_loops.get(device_mac)
|
|
585
600
|
gforce_event_loop = self._gforce_event_loop
|
|
586
601
|
data_event_loop = self._data_event_loops.get(device_mac)
|
|
@@ -676,8 +691,18 @@ class BleakProcess(multiprocessing.Process):
|
|
|
676
691
|
self._raw_bufs[device_mac] = raw_buf
|
|
677
692
|
self._device_states[device_mac] = "Connected"
|
|
678
693
|
|
|
694
|
+
# 重连成功,重置重连计数
|
|
695
|
+
reconnect_info = self._reconnect_info.get(device_mac)
|
|
696
|
+
if reconnect_info is not None:
|
|
697
|
+
reconnect_info["attempts"] = 0
|
|
698
|
+
|
|
679
699
|
# Create data context
|
|
680
|
-
data_ctx = SensorProfileDataCtx(
|
|
700
|
+
data_ctx = SensorProfileDataCtx(
|
|
701
|
+
gforce,
|
|
702
|
+
device_mac,
|
|
703
|
+
raw_buf,
|
|
704
|
+
on_reconnect_request=lambda: self._schedule_reconnect(device_mac),
|
|
705
|
+
)
|
|
681
706
|
self._data_ctxs[device_mac] = data_ctx
|
|
682
707
|
|
|
683
708
|
# Start data processing loop in data_event_loop
|
|
@@ -705,6 +730,19 @@ class BleakProcess(multiprocessing.Process):
|
|
|
705
730
|
if device_mac not in self._gforces and not disconnect_client:
|
|
706
731
|
return # Already cleaned
|
|
707
732
|
|
|
733
|
+
# 用户主动断开时标记为正常断开,并取消正在进行的自动重连
|
|
734
|
+
if disconnect_client:
|
|
735
|
+
info = self._reconnect_info.get(device_mac)
|
|
736
|
+
if info is not None:
|
|
737
|
+
info["normal_disconnect"] = True
|
|
738
|
+
task = info.get("task")
|
|
739
|
+
if task is not None:
|
|
740
|
+
try:
|
|
741
|
+
task.cancel()
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
744
|
+
info["task"] = None
|
|
745
|
+
|
|
708
746
|
# Cancel data task (running in data_event_loop)
|
|
709
747
|
if device_mac in self._data_tasks:
|
|
710
748
|
task = self._data_tasks.pop(device_mac, None)
|
|
@@ -750,8 +788,63 @@ class BleakProcess(multiprocessing.Process):
|
|
|
750
788
|
self._raw_bufs.pop(device_mac, None)
|
|
751
789
|
self._device_states[device_mac] = "Disconnected"
|
|
752
790
|
|
|
791
|
+
# 正常断开后清理重连信息;异常断开保留以继续自动重连
|
|
792
|
+
if disconnect_client:
|
|
793
|
+
self._reconnect_info.pop(device_mac, None)
|
|
794
|
+
|
|
753
795
|
self._publish("state_changed", device_mac=device_mac, state="Disconnected")
|
|
754
796
|
|
|
797
|
+
def _schedule_reconnect(self, device_mac: str):
|
|
798
|
+
"""从 watchdog 线程调用,调度一次自动重连。"""
|
|
799
|
+
info = self._reconnect_info.get(device_mac)
|
|
800
|
+
if info is None or info.get("normal_disconnect"):
|
|
801
|
+
return
|
|
802
|
+
if info.get("task") is not None:
|
|
803
|
+
return # 已有重连任务在进行
|
|
804
|
+
if info["attempts"] >= _MAX_RECONNECT_ATTEMPTS:
|
|
805
|
+
SdkLog.e(_TAG, f"Max reconnect attempts reached for {device_mac}")
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
info["attempts"] += 1
|
|
809
|
+
SdkLog.i(_TAG, f"Scheduling reconnect attempt {info['attempts']} for {device_mac}")
|
|
810
|
+
|
|
811
|
+
loop = self._main_event_loop
|
|
812
|
+
if loop is None or loop.is_closed():
|
|
813
|
+
SdkLog.e(_TAG, f"Main event loop not available, cannot reconnect {device_mac}")
|
|
814
|
+
return
|
|
815
|
+
|
|
816
|
+
async def _delayed_reconnect():
|
|
817
|
+
await asyncio.sleep(_RECONNECT_DELAY_SECONDS)
|
|
818
|
+
await self._do_reconnect(device_mac)
|
|
819
|
+
|
|
820
|
+
try:
|
|
821
|
+
info["task"] = asyncio.run_coroutine_threadsafe(_delayed_reconnect(), loop)
|
|
822
|
+
except Exception as e:
|
|
823
|
+
SdkLog.exception(_TAG, f"Failed to schedule reconnect for {device_mac}: {e}")
|
|
824
|
+
info["task"] = None
|
|
825
|
+
|
|
826
|
+
async def _do_reconnect(self, device_mac: str):
|
|
827
|
+
"""执行一次自动重连。"""
|
|
828
|
+
info = self._reconnect_info.get(device_mac)
|
|
829
|
+
if info is None or info.get("normal_disconnect"):
|
|
830
|
+
return
|
|
831
|
+
|
|
832
|
+
# 已经连接则无需重连
|
|
833
|
+
if device_mac in self._gforces:
|
|
834
|
+
info["task"] = None
|
|
835
|
+
return
|
|
836
|
+
|
|
837
|
+
SdkLog.i(_TAG, f"Reconnecting {device_mac}, attempt {info['attempts']}")
|
|
838
|
+
try:
|
|
839
|
+
await self._do_connect(info["cmd"])
|
|
840
|
+
except Exception as e:
|
|
841
|
+
SdkLog.exception(_TAG, f"Reconnect attempt failed for {device_mac}: {e}")
|
|
842
|
+
# 失败后会再次由 _schedule_reconnect 决定是否继续
|
|
843
|
+
self._schedule_reconnect(device_mac)
|
|
844
|
+
finally:
|
|
845
|
+
if info is not None:
|
|
846
|
+
info["task"] = None
|
|
847
|
+
|
|
755
848
|
async def _do_disconnect(self, cmd: dict):
|
|
756
849
|
|
|
757
850
|
device_mac = cmd["device_mac"]
|
|
@@ -1111,6 +1204,12 @@ class BleakProcess(multiprocessing.Process):
|
|
|
1111
1204
|
sorted_keys = sorted(ctx.notify_map.keys())
|
|
1112
1205
|
result = "|".join(f"{k}|{ctx.notify_map[k]}" for k in sorted_keys)
|
|
1113
1206
|
|
|
1207
|
+
if key == "NTF_IMU":
|
|
1208
|
+
imu_sub_keys = ["NTF_GFORCE_ACC", "NTF_GFORCE_GYRO", "NTF_GFORCE_QUAT", "NTF_GFORCE_EULER"]
|
|
1209
|
+
result = "ON" if all(ctx.notify_map.get(k) == "ON" for k in imu_sub_keys) else "OFF"
|
|
1210
|
+
elif key in ctx.notify_map:
|
|
1211
|
+
result = ctx.notify_map[key]
|
|
1212
|
+
|
|
1114
1213
|
if key == "DEBUG_LOG_PATH":
|
|
1115
1214
|
result = SdkLog.get_log_path() or ""
|
|
1116
1215
|
|
|
@@ -1154,6 +1253,8 @@ class BleakProcess(multiprocessing.Process):
|
|
|
1154
1253
|
|
|
1155
1254
|
result = "Error: Not supported"
|
|
1156
1255
|
needs_restart = False
|
|
1256
|
+
oym_needs_subscribe = False
|
|
1257
|
+
was_streaming = ctx.isDataTransfering
|
|
1157
1258
|
|
|
1158
1259
|
ntf_keys = [
|
|
1159
1260
|
"NTF_GEST", "NTF_EMG", "NTF_EEG", "NTF_ECG", "NTF_IMU", "NTF_BRTH", "NTF_IMPEDANCE",
|
|
@@ -1168,6 +1269,12 @@ class BleakProcess(multiprocessing.Process):
|
|
|
1168
1269
|
if key == "NTF_PPG_RAW":
|
|
1169
1270
|
map_key = "NTF_PPG"
|
|
1170
1271
|
|
|
1272
|
+
# IMU 总开关同时控制 ACC/GYRO/QUAT/EULER
|
|
1273
|
+
imu_sub_keys = ["NTF_GFORCE_ACC", "NTF_GFORCE_GYRO", "NTF_GFORCE_QUAT", "NTF_GFORCE_EULER"]
|
|
1274
|
+
if map_key == "NTF_IMU":
|
|
1275
|
+
for sub in imu_sub_keys:
|
|
1276
|
+
ctx.notify_map[sub] = value
|
|
1277
|
+
|
|
1171
1278
|
# 老版本 EMG 设备上 Gesture 与 EMG 互斥,自动切换
|
|
1172
1279
|
if not ctx.isNewEMG:
|
|
1173
1280
|
if map_key == "NTF_GEST" and value == "ON" and ctx.notify_map.get("NTF_EMG") == "ON":
|
|
@@ -1182,12 +1289,40 @@ class BleakProcess(multiprocessing.Process):
|
|
|
1182
1289
|
ctx.notify_map[map_key] = value
|
|
1183
1290
|
result = "OK"
|
|
1184
1291
|
else:
|
|
1292
|
+
# 新 EMG:Gesture 依赖 EMG 数据,关闭 EMG 时同步关闭 Gesture;打开 Gesture 时自动打开 EMG
|
|
1293
|
+
if map_key == "NTF_EMG" and value == "OFF":
|
|
1294
|
+
ctx.notify_map["NTF_GEST"] = "OFF"
|
|
1295
|
+
elif map_key == "NTF_GEST" and value == "ON" and ctx.notify_map.get("NTF_EMG") != "ON":
|
|
1296
|
+
ctx.notify_map["NTF_EMG"] = "ON"
|
|
1185
1297
|
ctx.notify_map[map_key] = value
|
|
1186
1298
|
result = "OK"
|
|
1187
|
-
|
|
1299
|
+
|
|
1300
|
+
# 单个 IMU 子开关变化时,同步更新 NTF_IMU 总开关的聚合状态
|
|
1301
|
+
if map_key in imu_sub_keys:
|
|
1302
|
+
ctx.notify_map["NTF_IMU"] = "ON" if all(ctx.notify_map.get(k) == "ON" for k in imu_sub_keys) else "OFF"
|
|
1303
|
+
|
|
1304
|
+
if result == "OK" and ctx.hasInit():
|
|
1188
1305
|
ctx._buildNotifyDataFlag()
|
|
1189
|
-
|
|
1190
|
-
|
|
1306
|
+
|
|
1307
|
+
# 新 EMG 设备通过 function switch 控制 EMG/Gesture 输出,bit0=gesture, bit1=emg
|
|
1308
|
+
if ctx.isNewEMG and map_key in ("NTF_EMG", "NTF_GEST"):
|
|
1309
|
+
emg_bit = 1 if ctx.notify_map.get("NTF_EMG") == "ON" else 0
|
|
1310
|
+
gest_bit = 1 if (emg_bit and ctx.notify_map.get("NTF_GEST") == "ON") else 0
|
|
1311
|
+
func_switch = (emg_bit << 1) | gest_bit
|
|
1312
|
+
try:
|
|
1313
|
+
await ctx.gForce.set_function_switch(func_switch)
|
|
1314
|
+
await asyncio.sleep(0.5)
|
|
1315
|
+
except Exception as e:
|
|
1316
|
+
SdkLog.exception(_TAG, f"_do_set_param set_function_switch failed: {device_mac}")
|
|
1317
|
+
result = "ERROR: set_function_switch fail: " + str(e)
|
|
1318
|
+
if ctx.getChipType() == BLEChipType.OYM:
|
|
1319
|
+
oym_needs_subscribe = True
|
|
1320
|
+
elif was_streaming:
|
|
1321
|
+
needs_restart = True
|
|
1322
|
+
if ctx.getChipType() == BLEChipType.OYM:
|
|
1323
|
+
oym_needs_subscribe = True
|
|
1324
|
+
elif ctx.getChipType() == BLEChipType.OYM:
|
|
1325
|
+
# 未在传输时,直接更新 OYM 订阅掩码
|
|
1191
1326
|
try:
|
|
1192
1327
|
await ctx.gForce.set_subscription(ctx.notifyDataFlag)
|
|
1193
1328
|
except Exception as e:
|
|
@@ -1204,10 +1339,14 @@ class BleakProcess(multiprocessing.Process):
|
|
|
1204
1339
|
SdkLog.exception(_TAG, f"_do_set_param setFilter failed: {device_mac} {key}={value}")
|
|
1205
1340
|
result = "ERROR: " + str(e)
|
|
1206
1341
|
|
|
1207
|
-
if needs_restart:
|
|
1342
|
+
if needs_restart or oym_needs_subscribe:
|
|
1208
1343
|
try:
|
|
1209
|
-
|
|
1210
|
-
|
|
1344
|
+
if was_streaming:
|
|
1345
|
+
await ctx.stop_streaming()
|
|
1346
|
+
if oym_needs_subscribe and ctx.getChipType() == BLEChipType.OYM:
|
|
1347
|
+
await ctx.gForce.set_subscription(ctx.notifyDataFlag)
|
|
1348
|
+
if was_streaming:
|
|
1349
|
+
await ctx.start_streaming()
|
|
1211
1350
|
except Exception as e:
|
|
1212
1351
|
SdkLog.exception(_TAG, f"_do_set_param restart stream failed: {device_mac}")
|
|
1213
1352
|
result = "ERROR: restart stream fail: " + str(e)
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import queue
|
|
3
3
|
import struct
|
|
4
4
|
import platform
|
|
5
|
+
import time
|
|
5
6
|
from contextlib import suppress
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from dataclasses import dataclass
|
|
@@ -453,6 +454,8 @@ class GForce:
|
|
|
453
454
|
self.cmd_char = cmd_char
|
|
454
455
|
self.data_char = data_char
|
|
455
456
|
self.responses: Dict[Command, queue.Queue] = {}
|
|
457
|
+
self.last_command_failure_time: Optional[float] = None
|
|
458
|
+
self.last_command_failure_cmd: Optional[int] = None
|
|
456
459
|
self.resolution = SampleResolution.BITS_8
|
|
457
460
|
self._num_channels = 8
|
|
458
461
|
self._device = device
|
|
@@ -1091,6 +1094,8 @@ class GForce:
|
|
|
1091
1094
|
timeout=2
|
|
1092
1095
|
)
|
|
1093
1096
|
except Exception as e:
|
|
1097
|
+
self.last_command_failure_time = time.time()
|
|
1098
|
+
self.last_command_failure_cmd = req.cmd
|
|
1094
1099
|
SdkLog.exception(_TAG, f"_send_request write_gatt_char failed: {req.cmd}")
|
|
1095
1100
|
if req.has_res:
|
|
1096
1101
|
self.responses[req.cmd] = None
|
|
@@ -9,7 +9,7 @@ import struct
|
|
|
9
9
|
import math
|
|
10
10
|
import threading
|
|
11
11
|
import time
|
|
12
|
-
from typing import Deque, List
|
|
12
|
+
from typing import Deque, List, Optional
|
|
13
13
|
from concurrent.futures import ThreadPoolExecutor
|
|
14
14
|
import csv
|
|
15
15
|
from sensor import sensor_utils
|
|
@@ -30,6 +30,8 @@ QUAT_SCALE = 1 / 1073741824.0 # 2^30
|
|
|
30
30
|
_MAX_CONCAT_BUFFER_SIZE = 64 * 1024 # 64KB
|
|
31
31
|
# data 日志队列最大长度,写盘跟不上时丢弃日志而不是阻塞解析线程
|
|
32
32
|
_DATA_LOG_QUEUE_MAXSIZE = 1000
|
|
33
|
+
# 包序号最大允许跳变值,超过视为非法跳变/丢包
|
|
34
|
+
_MAX_ALLOWED_PACKAGE_INDEX_DELTA = 100
|
|
33
35
|
|
|
34
36
|
class SensorDataType(IntEnum):
|
|
35
37
|
DATA_TYPE_EEG = 0
|
|
@@ -83,7 +85,13 @@ class ReadSamplesResult(IntEnum):
|
|
|
83
85
|
|
|
84
86
|
|
|
85
87
|
class SensorProfileDataCtx:
|
|
86
|
-
def __init__(
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
gForce: GForce,
|
|
91
|
+
deviceMac: str,
|
|
92
|
+
buf: Queue[bytes],
|
|
93
|
+
on_reconnect_request: Optional[callable] = None,
|
|
94
|
+
):
|
|
87
95
|
self.featureMap = 0
|
|
88
96
|
self.notifyDataFlag: DataSubscription = 0
|
|
89
97
|
|
|
@@ -150,6 +158,15 @@ class SensorProfileDataCtx:
|
|
|
150
158
|
self._watchdog_thread.daemon = True
|
|
151
159
|
self._watchdog_thread.start()
|
|
152
160
|
|
|
161
|
+
# 非法跳变监控:持续超过阈值则重启数据通知
|
|
162
|
+
self._illegal_jump_start_time: Optional[float] = None
|
|
163
|
+
self._is_restarting_stream = False
|
|
164
|
+
|
|
165
|
+
# 命令失败导致异常断开后自动重连
|
|
166
|
+
self._on_reconnect_request = on_reconnect_request
|
|
167
|
+
self._disconnected_start_time: Optional[float] = None
|
|
168
|
+
self._reconnect_requested = False
|
|
169
|
+
|
|
153
170
|
def close(self):
|
|
154
171
|
self._is_running = False
|
|
155
172
|
if self.debugCSVWriter != None:
|
|
@@ -188,10 +205,28 @@ class SensorProfileDataCtx:
|
|
|
188
205
|
pass
|
|
189
206
|
self._watchdog_thread = None
|
|
190
207
|
|
|
208
|
+
async def _restart_streaming(self):
|
|
209
|
+
"""非法跳变持续时重启数据通知。"""
|
|
210
|
+
if self._is_restarting_stream:
|
|
211
|
+
return
|
|
212
|
+
self._is_restarting_stream = True
|
|
213
|
+
try:
|
|
214
|
+
SdkLog.w(_TAG, f"Illegal package index jump persisted > 5s, restarting data notification for {self.deviceMac}")
|
|
215
|
+
await self.stop_streaming()
|
|
216
|
+
await self.start_streaming(self._rawDataBuffer)
|
|
217
|
+
self._illegal_jump_start_time = None
|
|
218
|
+
except Exception as e:
|
|
219
|
+
SdkLog.exception(_TAG, f"Restart streaming failed for {self.deviceMac}: {e}")
|
|
220
|
+
finally:
|
|
221
|
+
self._is_restarting_stream = False
|
|
222
|
+
|
|
191
223
|
def _watchdog_loop(self):
|
|
192
224
|
"""守护线程:监控解析线程进度,防止卡死后内存无限增长。"""
|
|
193
225
|
WATCHDOG_INTERVAL = 1.0 # 检查间隔
|
|
194
226
|
WATCHDOG_TIMEOUT = 5.0 # 无进度报警阈值
|
|
227
|
+
ILLEGAL_JUMP_TIMEOUT = 5.0 # 非法跳变持续阈值
|
|
228
|
+
RECONNECT_AFTER_DISCONNECT = 3.0 # 断开后等待多久尝试重连
|
|
229
|
+
RECENT_CMD_FAILURE_WINDOW = 30.0 # 命令失败多久内视为相关
|
|
195
230
|
RAW_BUF_CLEAR_THRESHOLD = 1500 # 超过此值且无进度时清空
|
|
196
231
|
|
|
197
232
|
while not self._watchdog_stop_event.is_set():
|
|
@@ -229,6 +264,46 @@ class SensorProfileDataCtx:
|
|
|
229
264
|
self._concatDataBuffer.clear()
|
|
230
265
|
# 清空后更新时间戳,避免持续报警
|
|
231
266
|
self._last_progress_time = time.time()
|
|
267
|
+
|
|
268
|
+
# 非法跳变持续超过阈值则重启数据通知
|
|
269
|
+
if (self._illegal_jump_start_time is not None
|
|
270
|
+
and not self._is_restarting_stream
|
|
271
|
+
and self.isDataTransfering):
|
|
272
|
+
jump_elapsed = time.time() - self._illegal_jump_start_time
|
|
273
|
+
if jump_elapsed > ILLEGAL_JUMP_TIMEOUT:
|
|
274
|
+
SdkLog.w(_TAG, f"Illegal jump persisted for {jump_elapsed:.1f}s, restarting stream for {self.deviceMac}")
|
|
275
|
+
try:
|
|
276
|
+
asyncio.run_coroutine_threadsafe(
|
|
277
|
+
self._restart_streaming(), self.gForce.gforce_event_loop
|
|
278
|
+
)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
SdkLog.exception(_TAG, f"Failed to schedule stream restart for {self.deviceMac}: {e}")
|
|
281
|
+
|
|
282
|
+
# 命令失败后设备异常断开,触发自动重连
|
|
283
|
+
if self._on_reconnect_request is not None:
|
|
284
|
+
client = getattr(self.gForce, "client", None)
|
|
285
|
+
is_connected = getattr(client, "is_connected", False) if client is not None else False
|
|
286
|
+
if is_connected:
|
|
287
|
+
self._disconnected_start_time = None
|
|
288
|
+
self._reconnect_requested = False
|
|
289
|
+
else:
|
|
290
|
+
if self._disconnected_start_time is None:
|
|
291
|
+
self._disconnected_start_time = time.time()
|
|
292
|
+
disconnected_elapsed = time.time() - self._disconnected_start_time
|
|
293
|
+
cmd_fail_time = getattr(self.gForce, "last_command_failure_time", None)
|
|
294
|
+
recent_cmd_failure = (
|
|
295
|
+
cmd_fail_time is not None
|
|
296
|
+
and (time.time() - cmd_fail_time) < RECENT_CMD_FAILURE_WINDOW
|
|
297
|
+
)
|
|
298
|
+
if (disconnected_elapsed > RECONNECT_AFTER_DISCONNECT
|
|
299
|
+
and recent_cmd_failure
|
|
300
|
+
and not self._reconnect_requested):
|
|
301
|
+
SdkLog.w(_TAG, f"Device disconnected after command failure, scheduling reconnect for {self.deviceMac}")
|
|
302
|
+
self._reconnect_requested = True
|
|
303
|
+
try:
|
|
304
|
+
self._on_reconnect_request()
|
|
305
|
+
except Exception as e:
|
|
306
|
+
SdkLog.exception(_TAG, f"Reconnect request failed for {self.deviceMac}: {e}")
|
|
232
307
|
except Exception as e:
|
|
233
308
|
SdkLog.exception(_TAG, f"Watchdog error: {e}")
|
|
234
309
|
|
|
@@ -390,8 +465,12 @@ class SensorProfileDataCtx:
|
|
|
390
465
|
config.batch_len = 128
|
|
391
466
|
|
|
392
467
|
if isNewEMG:
|
|
393
|
-
|
|
394
|
-
|
|
468
|
+
# 新版 EMG:bit0=gesture,bit1=emg;Gesture 依赖 EMG,EMG 关闭时 Gesture 同步关闭
|
|
469
|
+
emg_bit = 1 if self.notify_map.get("NTF_EMG") == "ON" else 0
|
|
470
|
+
gest_bit = 1 if (emg_bit and self.notify_map.get("NTF_GEST") == "ON") else 0
|
|
471
|
+
await self.gForce.set_function_switch((emg_bit << 1) | gest_bit)
|
|
472
|
+
await asyncio.sleep(0.5)
|
|
473
|
+
|
|
395
474
|
await self.gForce.set_emg_raw_data_config(config)
|
|
396
475
|
await self.gForce.set_package_id(True)
|
|
397
476
|
|
|
@@ -404,6 +483,15 @@ class SensorProfileDataCtx:
|
|
|
404
483
|
self.sensorDatas[SensorDataType.DATA_TYPE_EMG] = data
|
|
405
484
|
self.isNewEMG = isNewEMG
|
|
406
485
|
|
|
486
|
+
# 新 EMG + OYM 芯片:默认只开 EMG,关闭 Gesture 和 IMU
|
|
487
|
+
if isNewEMG and self._chip_type == BLEChipType.OYM:
|
|
488
|
+
self.notify_map["NTF_GEST"] = "OFF"
|
|
489
|
+
self.notify_map["NTF_IMU"] = "OFF"
|
|
490
|
+
self.notify_map["NTF_GFORCE_ACC"] = "OFF"
|
|
491
|
+
self.notify_map["NTF_GFORCE_GYRO"] = "OFF"
|
|
492
|
+
self.notify_map["NTF_GFORCE_QUAT"] = "OFF"
|
|
493
|
+
self.notify_map["NTF_GFORCE_EULER"] = "OFF"
|
|
494
|
+
|
|
407
495
|
# 老 EMG 设备不支持 FILTER
|
|
408
496
|
if not isNewEMG:
|
|
409
497
|
self.filter_map.clear()
|
|
@@ -1485,6 +1573,7 @@ class SensorProfileDataCtx:
|
|
|
1485
1573
|
if newPackageIndex <= 2 and lastPackageIndex >= maxPackageIndex - 2:
|
|
1486
1574
|
packageIndex = maxPackageIndex + 1 + newPackageIndex
|
|
1487
1575
|
elif newPackageIndex == lastPackageIndex:
|
|
1576
|
+
self._illegal_jump_start_time = None
|
|
1488
1577
|
return ReadSamplesResult.Repeated
|
|
1489
1578
|
elif newPackageIndex < lastPackageIndex:
|
|
1490
1579
|
SdkLog.i(_TAG, (
|
|
@@ -1494,10 +1583,12 @@ class SensorProfileDataCtx:
|
|
|
1494
1583
|
+ "|CURR_IDX|" + str(newPackageIndex)
|
|
1495
1584
|
+ "|DELTA|" + str(lastPackageIndex - newPackageIndex)
|
|
1496
1585
|
))
|
|
1586
|
+
if self._illegal_jump_start_time is None:
|
|
1587
|
+
self._illegal_jump_start_time = time.time()
|
|
1497
1588
|
return ReadSamplesResult.Error
|
|
1498
1589
|
|
|
1499
1590
|
deltaPackageIndex = packageIndex - lastPackageIndex
|
|
1500
|
-
if deltaPackageIndex >
|
|
1591
|
+
if deltaPackageIndex > _MAX_ALLOWED_PACKAGE_INDEX_DELTA:
|
|
1501
1592
|
SdkLog.i(_TAG, (
|
|
1502
1593
|
"Illegal package index jump|MAC|" + str(sensorData.deviceMac)
|
|
1503
1594
|
+ "|TYPE|" + str(sensorData.dataType)
|
|
@@ -1505,6 +1596,8 @@ class SensorProfileDataCtx:
|
|
|
1505
1596
|
+ "|CURR_IDX|" + str(newPackageIndex)
|
|
1506
1597
|
+ "|DELTA|" + str(deltaPackageIndex)
|
|
1507
1598
|
))
|
|
1599
|
+
if self._illegal_jump_start_time is None:
|
|
1600
|
+
self._illegal_jump_start_time = time.time()
|
|
1508
1601
|
return ReadSamplesResult.Error
|
|
1509
1602
|
|
|
1510
1603
|
lostPackageCounter = deltaPackageIndex - 1
|
|
@@ -1518,7 +1611,7 @@ class SensorProfileDataCtx:
|
|
|
1518
1611
|
+ "|COUNT|" + str(lostSampleCount)
|
|
1519
1612
|
))
|
|
1520
1613
|
|
|
1521
|
-
if lostPackageCounter <
|
|
1614
|
+
if lostPackageCounter < _MAX_ALLOWED_PACKAGE_INDEX_DELTA:
|
|
1522
1615
|
self.readSamples(data, sensorData, 0, dataGap, lostSampleCount)
|
|
1523
1616
|
|
|
1524
1617
|
sensorData.lastPackageIndex = newPackageIndex - 1
|
|
@@ -1530,6 +1623,7 @@ class SensorProfileDataCtx:
|
|
|
1530
1623
|
self.readSamples(data, sensorData, dataOffset, dataGap, 0)
|
|
1531
1624
|
|
|
1532
1625
|
sensorData.lastPackageCounter += 1
|
|
1626
|
+
self._illegal_jump_start_time = None
|
|
1533
1627
|
except Exception as e:
|
|
1534
1628
|
SdkLog.exception(_TAG, "Unexpected error")
|
|
1535
1629
|
return ReadSamplesResult.Error
|
|
@@ -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.2.1",
|
|
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
|
|
File without changes
|
|
File without changes
|