sensor-sdk 0.2.0__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.2.0/sensor_sdk.egg-info → sensor_sdk-0.2.1}/PKG-INFO +1 -1
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/bleak_no_ack_patch.py +6 -3
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/bleak_process.py +114 -10
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/gforce.py +5 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/sensor_data_context.py +94 -4
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1/sensor_sdk.egg-info}/PKG-INFO +1 -1
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/setup.py +1 -1
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/LICENSE.txt +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/README.md +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/__init__.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/bleak_host.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/fb/DataType.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/fb/Sample.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/fb/SensorData.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/fb/__init__.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/sdk_log.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/sensor_controller.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/sensor_data.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/sensor_data_pool.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/sensor_device.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/sensor_profile.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/sensor_utils.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor/winrt_high_throughput.py +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/SOURCES.txt +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/dependency_links.txt +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/requires.txt +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/top_level.txt +0 -0
- {sensor_sdk-0.2.0 → sensor_sdk-0.2.1}/sensor_sdk.egg-info/zip-safe +0 -0
- {sensor_sdk-0.2.0 → 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"]
|
|
@@ -1160,6 +1253,8 @@ class BleakProcess(multiprocessing.Process):
|
|
|
1160
1253
|
|
|
1161
1254
|
result = "Error: Not supported"
|
|
1162
1255
|
needs_restart = False
|
|
1256
|
+
oym_needs_subscribe = False
|
|
1257
|
+
was_streaming = ctx.isDataTransfering
|
|
1163
1258
|
|
|
1164
1259
|
ntf_keys = [
|
|
1165
1260
|
"NTF_GEST", "NTF_EMG", "NTF_EEG", "NTF_ECG", "NTF_IMU", "NTF_BRTH", "NTF_IMPEDANCE",
|
|
@@ -1220,14 +1315,19 @@ class BleakProcess(multiprocessing.Process):
|
|
|
1220
1315
|
except Exception as e:
|
|
1221
1316
|
SdkLog.exception(_TAG, f"_do_set_param set_function_switch failed: {device_mac}")
|
|
1222
1317
|
result = "ERROR: set_function_switch fail: " + str(e)
|
|
1223
|
-
|
|
1318
|
+
if ctx.getChipType() == BLEChipType.OYM:
|
|
1319
|
+
oym_needs_subscribe = True
|
|
1320
|
+
elif was_streaming:
|
|
1224
1321
|
needs_restart = True
|
|
1225
1322
|
if ctx.getChipType() == BLEChipType.OYM:
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1323
|
+
oym_needs_subscribe = True
|
|
1324
|
+
elif ctx.getChipType() == BLEChipType.OYM:
|
|
1325
|
+
# 未在传输时,直接更新 OYM 订阅掩码
|
|
1326
|
+
try:
|
|
1327
|
+
await ctx.gForce.set_subscription(ctx.notifyDataFlag)
|
|
1328
|
+
except Exception as e:
|
|
1329
|
+
SdkLog.exception(_TAG, f"_do_set_param set_subscription failed: {device_mac}")
|
|
1330
|
+
result = "ERROR: set_subscription fail: " + str(e)
|
|
1231
1331
|
|
|
1232
1332
|
if key in ["FILTER_50HZ", "FILTER_60HZ", "FILTER_HPF", "FILTER_LPF"]:
|
|
1233
1333
|
if value in ["ON", "OFF"]:
|
|
@@ -1239,10 +1339,14 @@ class BleakProcess(multiprocessing.Process):
|
|
|
1239
1339
|
SdkLog.exception(_TAG, f"_do_set_param setFilter failed: {device_mac} {key}={value}")
|
|
1240
1340
|
result = "ERROR: " + str(e)
|
|
1241
1341
|
|
|
1242
|
-
if needs_restart:
|
|
1342
|
+
if needs_restart or oym_needs_subscribe:
|
|
1243
1343
|
try:
|
|
1244
|
-
|
|
1245
|
-
|
|
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()
|
|
1246
1350
|
except Exception as e:
|
|
1247
1351
|
SdkLog.exception(_TAG, f"_do_set_param restart stream failed: {device_mac}")
|
|
1248
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
|
|
|
@@ -408,6 +483,15 @@ class SensorProfileDataCtx:
|
|
|
408
483
|
self.sensorDatas[SensorDataType.DATA_TYPE_EMG] = data
|
|
409
484
|
self.isNewEMG = isNewEMG
|
|
410
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
|
+
|
|
411
495
|
# 老 EMG 设备不支持 FILTER
|
|
412
496
|
if not isNewEMG:
|
|
413
497
|
self.filter_map.clear()
|
|
@@ -1489,6 +1573,7 @@ class SensorProfileDataCtx:
|
|
|
1489
1573
|
if newPackageIndex <= 2 and lastPackageIndex >= maxPackageIndex - 2:
|
|
1490
1574
|
packageIndex = maxPackageIndex + 1 + newPackageIndex
|
|
1491
1575
|
elif newPackageIndex == lastPackageIndex:
|
|
1576
|
+
self._illegal_jump_start_time = None
|
|
1492
1577
|
return ReadSamplesResult.Repeated
|
|
1493
1578
|
elif newPackageIndex < lastPackageIndex:
|
|
1494
1579
|
SdkLog.i(_TAG, (
|
|
@@ -1498,10 +1583,12 @@ class SensorProfileDataCtx:
|
|
|
1498
1583
|
+ "|CURR_IDX|" + str(newPackageIndex)
|
|
1499
1584
|
+ "|DELTA|" + str(lastPackageIndex - newPackageIndex)
|
|
1500
1585
|
))
|
|
1586
|
+
if self._illegal_jump_start_time is None:
|
|
1587
|
+
self._illegal_jump_start_time = time.time()
|
|
1501
1588
|
return ReadSamplesResult.Error
|
|
1502
1589
|
|
|
1503
1590
|
deltaPackageIndex = packageIndex - lastPackageIndex
|
|
1504
|
-
if deltaPackageIndex >
|
|
1591
|
+
if deltaPackageIndex > _MAX_ALLOWED_PACKAGE_INDEX_DELTA:
|
|
1505
1592
|
SdkLog.i(_TAG, (
|
|
1506
1593
|
"Illegal package index jump|MAC|" + str(sensorData.deviceMac)
|
|
1507
1594
|
+ "|TYPE|" + str(sensorData.dataType)
|
|
@@ -1509,6 +1596,8 @@ class SensorProfileDataCtx:
|
|
|
1509
1596
|
+ "|CURR_IDX|" + str(newPackageIndex)
|
|
1510
1597
|
+ "|DELTA|" + str(deltaPackageIndex)
|
|
1511
1598
|
))
|
|
1599
|
+
if self._illegal_jump_start_time is None:
|
|
1600
|
+
self._illegal_jump_start_time = time.time()
|
|
1512
1601
|
return ReadSamplesResult.Error
|
|
1513
1602
|
|
|
1514
1603
|
lostPackageCounter = deltaPackageIndex - 1
|
|
@@ -1522,7 +1611,7 @@ class SensorProfileDataCtx:
|
|
|
1522
1611
|
+ "|COUNT|" + str(lostSampleCount)
|
|
1523
1612
|
))
|
|
1524
1613
|
|
|
1525
|
-
if lostPackageCounter <
|
|
1614
|
+
if lostPackageCounter < _MAX_ALLOWED_PACKAGE_INDEX_DELTA:
|
|
1526
1615
|
self.readSamples(data, sensorData, 0, dataGap, lostSampleCount)
|
|
1527
1616
|
|
|
1528
1617
|
sensorData.lastPackageIndex = newPackageIndex - 1
|
|
@@ -1534,6 +1623,7 @@ class SensorProfileDataCtx:
|
|
|
1534
1623
|
self.readSamples(data, sensorData, dataOffset, dataGap, 0)
|
|
1535
1624
|
|
|
1536
1625
|
sensorData.lastPackageCounter += 1
|
|
1626
|
+
self._illegal_jump_start_time = None
|
|
1537
1627
|
except Exception as e:
|
|
1538
1628
|
SdkLog.exception(_TAG, "Unexpected error")
|
|
1539
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.2.
|
|
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
|