sensor-sdk 1.3.3__py3-none-win_amd64.whl
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/__init__.py +34 -0
- sensor/_dll.py +381 -0
- sensor/_sync.py +85 -0
- sensor/bin/sensor.dll +0 -0
- sensor/bin/sensor_ble_helper.exe +0 -0
- sensor/pyinstaller_hooks/__init__.py +4 -0
- sensor/pyinstaller_hooks/hook-sensor.py +3 -0
- sensor/sensor_controller.py +666 -0
- sensor/sensor_data.py +294 -0
- sensor/sensor_device.py +120 -0
- sensor/sensor_profile.py +674 -0
- sensor_sdk-1.3.3.dist-info/METADATA +23 -0
- sensor_sdk-1.3.3.dist-info/RECORD +16 -0
- sensor_sdk-1.3.3.dist-info/WHEEL +5 -0
- sensor_sdk-1.3.3.dist-info/entry_points.txt +2 -0
- sensor_sdk-1.3.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import ctypes
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
8
|
+
from typing import Callable, List, Optional
|
|
9
|
+
|
|
10
|
+
from . import _dll
|
|
11
|
+
from . import _sync
|
|
12
|
+
from .sensor_device import BLEDevice
|
|
13
|
+
from .sensor_profile import SensorProfile, _decode_str, _resolve_future, _raise_on_sdk_thread, _mark_sdk_thread
|
|
14
|
+
|
|
15
|
+
_MAC_PATTERN = re.compile(r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$")
|
|
16
|
+
|
|
17
|
+
def _validate_scan_period(periodInMs):
|
|
18
|
+
if not isinstance(periodInMs, int) or periodInMs <= 0:
|
|
19
|
+
raise ValueError("invalid scan period: %r" % (periodInMs,))
|
|
20
|
+
|
|
21
|
+
async def _async_wait_replay_done(profile, timeout):
|
|
22
|
+
loop = asyncio.get_running_loop()
|
|
23
|
+
fut = loop.create_future()
|
|
24
|
+
prev = profile._on_data_transfer_state_change
|
|
25
|
+
|
|
26
|
+
def _on_state(p, transferring):
|
|
27
|
+
if not transferring:
|
|
28
|
+
_resolve_future(loop, fut, True)
|
|
29
|
+
if prev is not None:
|
|
30
|
+
try:
|
|
31
|
+
prev(p, transferring)
|
|
32
|
+
except Exception:
|
|
33
|
+
traceback.print_exc()
|
|
34
|
+
|
|
35
|
+
profile.on_data_transfer_state_change = _on_state
|
|
36
|
+
try:
|
|
37
|
+
if not profile.isDataTransfering:
|
|
38
|
+
return
|
|
39
|
+
try:
|
|
40
|
+
await asyncio.wait_for(fut, timeout)
|
|
41
|
+
except asyncio.TimeoutError:
|
|
42
|
+
pass
|
|
43
|
+
finally:
|
|
44
|
+
if profile._on_data_transfer_state_change is _on_state:
|
|
45
|
+
profile.on_data_transfer_state_change = prev
|
|
46
|
+
|
|
47
|
+
class SensorController:
|
|
48
|
+
_instance_lock = threading.Lock()
|
|
49
|
+
|
|
50
|
+
def __new__(cls, *args, **kwargs):
|
|
51
|
+
if not hasattr(SensorController, "_instance"):
|
|
52
|
+
with SensorController._instance_lock:
|
|
53
|
+
if not hasattr(SensorController, "_instance"):
|
|
54
|
+
SensorController._instance = object.__new__(cls)
|
|
55
|
+
return SensorController._instance
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
if getattr(self, "_inited", False):
|
|
59
|
+
return
|
|
60
|
+
self._inited = True
|
|
61
|
+
|
|
62
|
+
self._handle = _dll.load_dll().sen_controller_create()
|
|
63
|
+
if not self._handle:
|
|
64
|
+
raise RuntimeError("sen_controller_create failed")
|
|
65
|
+
|
|
66
|
+
self._on_sensor_scan_result: Optional[Callable[[List[BLEDevice]], None]] = None
|
|
67
|
+
self._on_sensor_controller_enable_changed: Optional[Callable[[bool], None]] = None
|
|
68
|
+
|
|
69
|
+
self._profiles = {}
|
|
70
|
+
self._profiles_lock = threading.Lock()
|
|
71
|
+
|
|
72
|
+
self._pending_cbs = []
|
|
73
|
+
|
|
74
|
+
self._ctx = ctypes.py_object(self)
|
|
75
|
+
self._ctx_ptr = ctypes.cast(ctypes.pointer(self._ctx), ctypes.c_void_p)
|
|
76
|
+
self._c_on_scan = _dll._sen_scan_result_cb(SensorController._c_scan_result)
|
|
77
|
+
self._c_on_enable = _dll._sen_enable_changed_cb(SensorController._c_enable_changed)
|
|
78
|
+
self._cbs = _dll.sen_controller_cbs_t()
|
|
79
|
+
self._cbs.structSize = ctypes.sizeof(self._cbs)
|
|
80
|
+
self._cbs.onScanResult = self._c_on_scan
|
|
81
|
+
self._cbs.onEnableChanged = self._c_on_enable
|
|
82
|
+
_dll.load_dll().sen_controller_set_callbacks(
|
|
83
|
+
self._handle, ctypes.byref(self._cbs), self._ctx_ptr)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _c_scan_result(ctx, devices, count):
|
|
87
|
+
_mark_sdk_thread()
|
|
88
|
+
try:
|
|
89
|
+
self = ctypes.cast(ctx, ctypes.POINTER(ctypes.py_object)).contents.value
|
|
90
|
+
cb = self._on_sensor_scan_result
|
|
91
|
+
if cb is None:
|
|
92
|
+
return
|
|
93
|
+
device_list = [BLEDevice._from_c(devices[i]) for i in range(count)]
|
|
94
|
+
cb(device_list)
|
|
95
|
+
except Exception:
|
|
96
|
+
traceback.print_exc()
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _c_enable_changed(ctx, enabled):
|
|
100
|
+
_mark_sdk_thread()
|
|
101
|
+
try:
|
|
102
|
+
self = ctypes.cast(ctx, ctypes.POINTER(ctypes.py_object)).contents.value
|
|
103
|
+
cb = self._on_sensor_controller_enable_changed
|
|
104
|
+
if cb is not None:
|
|
105
|
+
cb(enabled != 0)
|
|
106
|
+
except Exception:
|
|
107
|
+
traceback.print_exc()
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def on_sensor_scan_result(self):
|
|
111
|
+
return self._on_sensor_scan_result
|
|
112
|
+
|
|
113
|
+
@on_sensor_scan_result.setter
|
|
114
|
+
def on_sensor_scan_result(self, callback):
|
|
115
|
+
self._on_sensor_scan_result = callback
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def on_sensor_controller_enable_changed(self):
|
|
119
|
+
return self._on_sensor_controller_enable_changed
|
|
120
|
+
|
|
121
|
+
@on_sensor_controller_enable_changed.setter
|
|
122
|
+
def on_sensor_controller_enable_changed(self, callback):
|
|
123
|
+
self._on_sensor_controller_enable_changed = callback
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def onDeviceFoundCallback(self):
|
|
127
|
+
return self._on_sensor_scan_result
|
|
128
|
+
|
|
129
|
+
@onDeviceFoundCallback.setter
|
|
130
|
+
def onDeviceFoundCallback(self, callback):
|
|
131
|
+
self._on_sensor_scan_result = callback
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def onEnableCallback(self):
|
|
135
|
+
return self._on_sensor_controller_enable_changed
|
|
136
|
+
|
|
137
|
+
@onEnableCallback.setter
|
|
138
|
+
def onEnableCallback(self, callback):
|
|
139
|
+
self._on_sensor_controller_enable_changed = callback
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def hasDeviceFoundCallback(self) -> bool:
|
|
143
|
+
return self._on_sensor_scan_result is not None
|
|
144
|
+
|
|
145
|
+
def terminate(self):
|
|
146
|
+
handle = getattr(self, "_handle", None)
|
|
147
|
+
if handle:
|
|
148
|
+
self._handle = None
|
|
149
|
+
_dll.load_dll().sen_controller_destroy(handle)
|
|
150
|
+
_dll.load_dll().sen_terminate()
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def isScanning(self) -> bool:
|
|
154
|
+
return _dll.load_dll().sen_controller_is_scanning(self._handle) != 0
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def isEnable(self) -> bool:
|
|
158
|
+
return _dll.load_dll().sen_controller_is_enable(self._handle) != 0
|
|
159
|
+
|
|
160
|
+
def startScan(self, periodInMs: int) -> bool:
|
|
161
|
+
_validate_scan_period(periodInMs)
|
|
162
|
+
return _dll.load_dll().sen_controller_start_scan(self._handle, periodInMs) != 0
|
|
163
|
+
|
|
164
|
+
def stopScan(self) -> bool:
|
|
165
|
+
return _dll.load_dll().sen_controller_stop_scan(self._handle) != 0
|
|
166
|
+
|
|
167
|
+
def _wrap_profile(self, handle) -> Optional[SensorProfile]:
|
|
168
|
+
if not handle:
|
|
169
|
+
return None
|
|
170
|
+
with self._profiles_lock:
|
|
171
|
+
profile = self._profiles.get(handle)
|
|
172
|
+
if profile is None:
|
|
173
|
+
profile = SensorProfile(self, handle)
|
|
174
|
+
self._profiles[handle] = profile
|
|
175
|
+
return profile
|
|
176
|
+
|
|
177
|
+
def requireSensor(self, device: BLEDevice) -> Optional[SensorProfile]:
|
|
178
|
+
address = getattr(device, "Address", None)
|
|
179
|
+
if not isinstance(address, str) or _MAC_PATTERN.match(address) is None:
|
|
180
|
+
return None
|
|
181
|
+
handle = _dll.load_dll().sen_controller_require_sensor(
|
|
182
|
+
self._handle, address.encode())
|
|
183
|
+
return self._wrap_profile(handle)
|
|
184
|
+
|
|
185
|
+
def getSensor(self, deviceMac: str) -> Optional[SensorProfile]:
|
|
186
|
+
handle = _dll.load_dll().sen_controller_get_sensor(
|
|
187
|
+
self._handle, deviceMac.encode())
|
|
188
|
+
return self._wrap_profile(handle)
|
|
189
|
+
|
|
190
|
+
def getSensors(self) -> List[SensorProfile]:
|
|
191
|
+
dll = _dll.load_dll()
|
|
192
|
+
count = dll.sen_controller_get_sensors(self._handle, None, 0)
|
|
193
|
+
if count == 0:
|
|
194
|
+
return []
|
|
195
|
+
arr = (ctypes.c_void_p * count)()
|
|
196
|
+
dll.sen_controller_get_sensors(self._handle, arr, count)
|
|
197
|
+
return [p for p in (self._wrap_profile(arr[i]) for i in range(count)) if p]
|
|
198
|
+
|
|
199
|
+
def getConnectedSensors(self) -> List[SensorProfile]:
|
|
200
|
+
dll = _dll.load_dll()
|
|
201
|
+
count = dll.sen_controller_get_connected_sensors(self._handle, None, 0)
|
|
202
|
+
if count == 0:
|
|
203
|
+
return []
|
|
204
|
+
arr = (ctypes.c_void_p * count)()
|
|
205
|
+
dll.sen_controller_get_connected_sensors(self._handle, arr, count)
|
|
206
|
+
return [p for p in (self._wrap_profile(arr[i]) for i in range(count)) if p]
|
|
207
|
+
|
|
208
|
+
def getConnectedDevices(self) -> List[BLEDevice]:
|
|
209
|
+
return [p.BLEDevice for p in self.getConnectedSensors()]
|
|
210
|
+
|
|
211
|
+
def _track(self, cb_obj):
|
|
212
|
+
self._pending_cbs.append(cb_obj)
|
|
213
|
+
return cb_obj
|
|
214
|
+
|
|
215
|
+
def _discard(self, cb_obj):
|
|
216
|
+
try:
|
|
217
|
+
self._pending_cbs.remove(cb_obj)
|
|
218
|
+
except ValueError:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
def _multiStartDataNotification(self, sensors: List[SensorProfile],
|
|
222
|
+
timeoutInMs: int = 0,
|
|
223
|
+
maxDelayDispersionMs: int = 5,
|
|
224
|
+
maxAttempts: int = 3,
|
|
225
|
+
cb: Optional[Callable[[dict, str], None]] = None):
|
|
226
|
+
@_dll._sen_multi_result_cb
|
|
227
|
+
def _cb(ctx, macs, oks, errors, count):
|
|
228
|
+
_mark_sdk_thread()
|
|
229
|
+
try:
|
|
230
|
+
if cb is not None:
|
|
231
|
+
results = {_decode_str(macs[i]): oks[i] != 0 for i in range(count)}
|
|
232
|
+
cb(results, "")
|
|
233
|
+
finally:
|
|
234
|
+
self._discard(_cb)
|
|
235
|
+
self._track(_cb)
|
|
236
|
+
arr = (ctypes.c_void_p * len(sensors))(*[s._handle for s in sensors])
|
|
237
|
+
_dll.load_dll().sen_controller_multi_start_data(
|
|
238
|
+
self._handle, arr, len(sensors), timeoutInMs,
|
|
239
|
+
maxDelayDispersionMs, maxAttempts, _cb, None)
|
|
240
|
+
|
|
241
|
+
def _multiStopDataNotification(self, sensors: List[SensorProfile],
|
|
242
|
+
timeoutInMs: int = 0,
|
|
243
|
+
cb: Optional[Callable[[dict, str], None]] = None):
|
|
244
|
+
@_dll._sen_multi_result_cb
|
|
245
|
+
def _cb(ctx, macs, oks, errors, count):
|
|
246
|
+
_mark_sdk_thread()
|
|
247
|
+
try:
|
|
248
|
+
if cb is not None:
|
|
249
|
+
results = {_decode_str(macs[i]): oks[i] != 0 for i in range(count)}
|
|
250
|
+
cb(results, "")
|
|
251
|
+
finally:
|
|
252
|
+
self._discard(_cb)
|
|
253
|
+
self._track(_cb)
|
|
254
|
+
arr = (ctypes.c_void_p * len(sensors))(*[s._handle for s in sensors])
|
|
255
|
+
_dll.load_dll().sen_controller_multi_stop_data(
|
|
256
|
+
self._handle, arr, len(sensors), timeoutInMs, _cb, None)
|
|
257
|
+
|
|
258
|
+
def multiStartDataNotification(self, sensors: List[SensorProfile],
|
|
259
|
+
timeout: float = 0.0,
|
|
260
|
+
maxDelayDispersionMs: int = 5,
|
|
261
|
+
maxAttempts: int = 3) -> dict:
|
|
262
|
+
_raise_on_sdk_thread()
|
|
263
|
+
try:
|
|
264
|
+
return _sync.run_sync(
|
|
265
|
+
self.asyncMultiStartDataNotification(
|
|
266
|
+
sensors, timeout, maxDelayDispersionMs, maxAttempts),
|
|
267
|
+
max(timeout, 60.0) + 45.0)
|
|
268
|
+
except _sync.FuturesTimeoutError:
|
|
269
|
+
return {}
|
|
270
|
+
|
|
271
|
+
def multiStopDataNotification(self, sensors: List[SensorProfile],
|
|
272
|
+
timeout: float = 0.0) -> dict:
|
|
273
|
+
_raise_on_sdk_thread()
|
|
274
|
+
try:
|
|
275
|
+
return _sync.run_sync(
|
|
276
|
+
self.asyncMultiStopDataNotification(sensors, timeout),
|
|
277
|
+
timeout + 45.0)
|
|
278
|
+
except _sync.FuturesTimeoutError:
|
|
279
|
+
return {}
|
|
280
|
+
|
|
281
|
+
def log(self, message: str, level: str = "I"):
|
|
282
|
+
try:
|
|
283
|
+
_dll.load_dll().sen_controller_log(
|
|
284
|
+
self._handle, str(message).encode(), str(level).encode())
|
|
285
|
+
except Exception:
|
|
286
|
+
traceback.print_exc()
|
|
287
|
+
|
|
288
|
+
def onSuspend(self):
|
|
289
|
+
try:
|
|
290
|
+
_dll.load_dll().sen_controller_on_suspend(self._handle)
|
|
291
|
+
except Exception:
|
|
292
|
+
traceback.print_exc()
|
|
293
|
+
|
|
294
|
+
_BIN_INFO_PREFIXES_V2 = (
|
|
295
|
+
("Ppg", "Ppg"), ("Spo2", "Spo2"), ("Impe", "Impe"),
|
|
296
|
+
("Emg", "EMG"), ("Eeg", "EEG"), ("Ecg", "ECG"),
|
|
297
|
+
("Acc", "Acc"), ("Gyro", "Gyro"), ("Brth", "BRTH"),
|
|
298
|
+
("MagAngle", "MagAngle"), ("Euler", "Euler"), ("Quat", "Quat"),
|
|
299
|
+
)
|
|
300
|
+
_BIN_INFO_PREFIXES_V3 = _BIN_INFO_PREFIXES_V2 + (("Imu", "Imu"),)
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _bin_device_info(d, version):
|
|
304
|
+
def _cstr(raw):
|
|
305
|
+
return bytes(raw).split(b"\0", 1)[0].decode("utf-8", errors="replace")
|
|
306
|
+
out = {
|
|
307
|
+
"DeviceName": _cstr(d.deviceName),
|
|
308
|
+
"ModelName": _cstr(d.modelName),
|
|
309
|
+
"HardwareVersion": _cstr(d.hardwareVersion),
|
|
310
|
+
"FirmwareVersion": _cstr(d.firmwareVersion),
|
|
311
|
+
"MTUSize": d.MTUSize,
|
|
312
|
+
}
|
|
313
|
+
prefixes = (SensorController._BIN_INFO_PREFIXES_V2 if version < 3
|
|
314
|
+
else SensorController._BIN_INFO_PREFIXES_V3)
|
|
315
|
+
for key_prefix, field_prefix in prefixes:
|
|
316
|
+
out[key_prefix + "ChannelCount"] = getattr(d, field_prefix + "ChannelCount")
|
|
317
|
+
out[key_prefix + "SampleRate"] = getattr(d, field_prefix + "SampleRate")
|
|
318
|
+
return out
|
|
319
|
+
|
|
320
|
+
def getBinFileInfo(self, file_path: str) -> Optional[dict]:
|
|
321
|
+
info = _dll.sen_bin_file_info_t()
|
|
322
|
+
info.structSize = ctypes.sizeof(info)
|
|
323
|
+
valid = _dll.load_dll().sen_controller_get_bin_file_info(
|
|
324
|
+
self._handle, file_path.encode(), ctypes.byref(info))
|
|
325
|
+
if not valid:
|
|
326
|
+
return None
|
|
327
|
+
version = info.configVersion
|
|
328
|
+
sensor_datas = []
|
|
329
|
+
count = min(info.sensorDataCount, _dll.SEN_BIN_MAX_SENSOR_DATAS)
|
|
330
|
+
for i in range(count):
|
|
331
|
+
s = info.sensorDatas[i]
|
|
332
|
+
if s.packageSampleCount <= 0 or s.channelCount <= 0:
|
|
333
|
+
continue
|
|
334
|
+
sensor_datas.append({
|
|
335
|
+
"type_index": s.typeIndex,
|
|
336
|
+
"data_type": s.dataType,
|
|
337
|
+
"sample_rate": s.sampleRate,
|
|
338
|
+
"channel_count": s.channelCount,
|
|
339
|
+
"package_sample_count": s.packageSampleCount,
|
|
340
|
+
"min_package_sample_count": s.minPackageSampleCount,
|
|
341
|
+
"package_index_length": s.packageIndexLength,
|
|
342
|
+
"resolution_bits": s.resolutionBits,
|
|
343
|
+
"resolution_signed": s.resolutionSigned,
|
|
344
|
+
"channel_mask": s.channelMask,
|
|
345
|
+
"k": s.k,
|
|
346
|
+
"raw_data_bias": s.rawDataBias,
|
|
347
|
+
})
|
|
348
|
+
return {
|
|
349
|
+
"version": version,
|
|
350
|
+
"first_data_ts_ms": info.firstDataTsMs,
|
|
351
|
+
"last_data_ts_ms": info.lastDataTsMs,
|
|
352
|
+
"device_mac": bytes(info.mac).split(b"\0", 1)[0].decode("utf-8", errors="replace"),
|
|
353
|
+
"device_name": bytes(info.deviceName).split(b"\0", 1)[0].decode("utf-8", errors="replace"),
|
|
354
|
+
"chip_type": info.chipType,
|
|
355
|
+
"is_universal_stream": bool(info.isUniversalStream),
|
|
356
|
+
"feature_map": info.featureMap,
|
|
357
|
+
"notify_data_flag": info.notifyDataFlag,
|
|
358
|
+
"is_new_emg": bool(info.isNewEmg),
|
|
359
|
+
"is_contain_qat6": bool(info.isContainQat6),
|
|
360
|
+
"ppg_model": info.ppgModel,
|
|
361
|
+
"device_info": SensorController._bin_device_info(info.deviceInfo, version),
|
|
362
|
+
"sensor_datas": sensor_datas,
|
|
363
|
+
"replay_duration": info.durationSec,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
def _start_replay(self, file_path: str, sensor: Optional[SensorProfile],
|
|
367
|
+
realtime: bool, timeoutMs: int) -> Optional[SensorProfile]:
|
|
368
|
+
if sensor is None:
|
|
369
|
+
info = self.getBinFileInfo(file_path)
|
|
370
|
+
if info is None or not info["device_mac"]:
|
|
371
|
+
return None
|
|
372
|
+
sensor = self.requireSensor(
|
|
373
|
+
BLEDevice(info["device_name"] or "", info["device_mac"], 0))
|
|
374
|
+
if sensor is None:
|
|
375
|
+
return None
|
|
376
|
+
mac = sensor if isinstance(sensor, str) else sensor.BLEDevice.Address
|
|
377
|
+
handle = _dll.load_dll().sen_controller_replay_bin_file(
|
|
378
|
+
self._handle, file_path.encode(), mac.encode(), 1 if realtime else 0, timeoutMs)
|
|
379
|
+
return self._wrap_profile(handle)
|
|
380
|
+
|
|
381
|
+
def replayBinFile(self, file_path: str, sensor: Optional[SensorProfile] = None,
|
|
382
|
+
realtime: bool = True, timeout: Optional[float] = None,
|
|
383
|
+
block: bool = True, timeoutMs: int = 0
|
|
384
|
+
) -> Optional[SensorProfile]:
|
|
385
|
+
profile = self._start_replay(file_path, sensor, realtime, timeoutMs)
|
|
386
|
+
if profile is None or not block:
|
|
387
|
+
return profile
|
|
388
|
+
_raise_on_sdk_thread()
|
|
389
|
+
if timeout is None:
|
|
390
|
+
info = self.getBinFileInfo(file_path)
|
|
391
|
+
duration = info["replay_duration"] if info else 0
|
|
392
|
+
timeout = max(60.0, duration + 30.0) if realtime else 600.0
|
|
393
|
+
try:
|
|
394
|
+
_sync.run_sync(_async_wait_replay_done(profile, timeout), timeout + 15.0)
|
|
395
|
+
except _sync.FuturesTimeoutError:
|
|
396
|
+
pass
|
|
397
|
+
return profile
|
|
398
|
+
|
|
399
|
+
def multiReplayBinFile(self, file_paths, sensors, realtime: bool = True,
|
|
400
|
+
timeout: Optional[float] = None, block: bool = True,
|
|
401
|
+
timeoutMs: int = 0) -> list:
|
|
402
|
+
dll = _dll.load_dll()
|
|
403
|
+
count = len(file_paths)
|
|
404
|
+
if len(sensors) != count:
|
|
405
|
+
raise ValueError("sensors must align with file_paths")
|
|
406
|
+
macs = []
|
|
407
|
+
for s in sensors:
|
|
408
|
+
if s is None:
|
|
409
|
+
raise ValueError("sensors must not contain None entries")
|
|
410
|
+
macs.append(s if isinstance(s, str) else s.BLEDevice.Address)
|
|
411
|
+
path_arr = (ctypes.c_char_p * count)(*[p.encode() for p in file_paths])
|
|
412
|
+
mac_arr = (ctypes.c_char_p * count)(*[m.encode() for m in macs])
|
|
413
|
+
out_arr = (ctypes.c_void_p * count)()
|
|
414
|
+
dll.sen_controller_multi_replay_bin_file(
|
|
415
|
+
self._handle, path_arr, mac_arr, count,
|
|
416
|
+
1 if realtime else 0, timeoutMs, out_arr)
|
|
417
|
+
profiles = [self._wrap_profile(h) for h in out_arr]
|
|
418
|
+
if not block:
|
|
419
|
+
return profiles
|
|
420
|
+
_raise_on_sdk_thread()
|
|
421
|
+
if timeout is None:
|
|
422
|
+
durations = []
|
|
423
|
+
for p in file_paths:
|
|
424
|
+
info = self.getBinFileInfo(p)
|
|
425
|
+
durations.append(info["replay_duration"] if info else 0)
|
|
426
|
+
timeout = max(60.0, max(durations + [0]) + 30.0) if realtime else 600.0
|
|
427
|
+
|
|
428
|
+
async def _wait_all():
|
|
429
|
+
deadline = time.monotonic() + timeout
|
|
430
|
+
for profile in profiles:
|
|
431
|
+
if profile is not None:
|
|
432
|
+
await _async_wait_replay_done(
|
|
433
|
+
profile, max(0.0, deadline - time.monotonic()))
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
_sync.run_sync(_wait_all(), timeout + 15.0)
|
|
437
|
+
except _sync.FuturesTimeoutError:
|
|
438
|
+
pass
|
|
439
|
+
return profiles
|
|
440
|
+
|
|
441
|
+
def _bin_replay_cmd(self, fn, sensor: Optional[SensorProfile]) -> str:
|
|
442
|
+
if sensor is None:
|
|
443
|
+
return "Error: no sensor"
|
|
444
|
+
mac = sensor if isinstance(sensor, str) else sensor.BLEDevice.Address
|
|
445
|
+
buf = ctypes.create_string_buffer(256)
|
|
446
|
+
fn(self._handle, mac.encode(), buf, len(buf))
|
|
447
|
+
return buf.value.decode("utf-8", errors="replace")
|
|
448
|
+
|
|
449
|
+
def pauseBinReplay(self, sensor: Optional[SensorProfile]) -> str:
|
|
450
|
+
return self._bin_replay_cmd(_dll.load_dll().sen_controller_pause_bin_replay, sensor)
|
|
451
|
+
|
|
452
|
+
def resumeBinReplay(self, sensor: Optional[SensorProfile]) -> str:
|
|
453
|
+
return self._bin_replay_cmd(_dll.load_dll().sen_controller_resume_bin_replay, sensor)
|
|
454
|
+
|
|
455
|
+
def stopBinReplay(self, sensor: Optional[SensorProfile]) -> str:
|
|
456
|
+
return self._bin_replay_cmd(_dll.load_dll().sen_controller_stop_bin_replay, sensor)
|
|
457
|
+
|
|
458
|
+
def _parseBinToCsv(self, bin_path: str, csv_path: Optional[str] = None,
|
|
459
|
+
cb: Optional[Callable[[str, str], None]] = None):
|
|
460
|
+
if not csv_path:
|
|
461
|
+
root, _ = os.path.splitext(bin_path)
|
|
462
|
+
csv_path = root + ".csv"
|
|
463
|
+
@_dll._sen_param_cb
|
|
464
|
+
def _cb(ctx, result, errorMsg):
|
|
465
|
+
_mark_sdk_thread()
|
|
466
|
+
try:
|
|
467
|
+
if cb is not None:
|
|
468
|
+
cb(_decode_str(result), _decode_str(errorMsg))
|
|
469
|
+
finally:
|
|
470
|
+
self._discard(_cb)
|
|
471
|
+
self._track(_cb)
|
|
472
|
+
_dll.load_dll().sen_controller_parse_bin_to_csv(
|
|
473
|
+
self._handle, bin_path.encode(), csv_path.encode(), _cb, None)
|
|
474
|
+
|
|
475
|
+
def parseBinToCsv(self, bin_path: str, csv_path: Optional[str] = None) -> str:
|
|
476
|
+
if not os.path.isfile(bin_path):
|
|
477
|
+
raise FileNotFoundError(bin_path)
|
|
478
|
+
_raise_on_sdk_thread()
|
|
479
|
+
try:
|
|
480
|
+
return _sync.run_sync(self.asyncParseBinToCsv(bin_path, csv_path), 320.0)
|
|
481
|
+
except _sync.FuturesTimeoutError:
|
|
482
|
+
return "Error: Timeout"
|
|
483
|
+
|
|
484
|
+
def getVersion(self) -> str:
|
|
485
|
+
buf = ctypes.create_string_buffer(256)
|
|
486
|
+
_dll.load_dll().sen_controller_get_version(self._handle, buf, len(buf))
|
|
487
|
+
return buf.value.decode("utf-8", errors="replace")
|
|
488
|
+
|
|
489
|
+
def _checkSetupDongle(self, cb: Optional[Callable[[str, str], None]] = None):
|
|
490
|
+
@_dll._sen_param_cb
|
|
491
|
+
def _cb(ctx, result, errorMsg):
|
|
492
|
+
_mark_sdk_thread()
|
|
493
|
+
try:
|
|
494
|
+
if cb is not None:
|
|
495
|
+
cb(_decode_str(result), _decode_str(errorMsg))
|
|
496
|
+
finally:
|
|
497
|
+
self._discard(_cb)
|
|
498
|
+
self._track(_cb)
|
|
499
|
+
_dll.load_dll().sen_controller_check_setup_dongle(self._handle, _cb, None)
|
|
500
|
+
|
|
501
|
+
def checkSetupDongle(self) -> str:
|
|
502
|
+
_raise_on_sdk_thread()
|
|
503
|
+
try:
|
|
504
|
+
return _sync.run_sync(self.asyncCheckSetupDongle(), 320.0)
|
|
505
|
+
except _sync.FuturesTimeoutError:
|
|
506
|
+
return "Error: Timeout"
|
|
507
|
+
|
|
508
|
+
def _getParam(self, key: str, cb: Optional[Callable[[str, str], None]] = None):
|
|
509
|
+
@_dll._sen_param_cb
|
|
510
|
+
def _cb(ctx, result, errorMsg):
|
|
511
|
+
_mark_sdk_thread()
|
|
512
|
+
try:
|
|
513
|
+
if cb is not None:
|
|
514
|
+
cb(_decode_str(result), _decode_str(errorMsg))
|
|
515
|
+
finally:
|
|
516
|
+
self._discard(_cb)
|
|
517
|
+
self._track(_cb)
|
|
518
|
+
_dll.load_dll().sen_controller_get_param(self._handle, key.encode(), _cb, None)
|
|
519
|
+
|
|
520
|
+
def _setParam(self, key: str, value: str, cb: Optional[Callable[[str, str], None]] = None):
|
|
521
|
+
@_dll._sen_param_cb
|
|
522
|
+
def _cb(ctx, result, errorMsg):
|
|
523
|
+
_mark_sdk_thread()
|
|
524
|
+
try:
|
|
525
|
+
if cb is not None:
|
|
526
|
+
cb(_decode_str(result), _decode_str(errorMsg))
|
|
527
|
+
finally:
|
|
528
|
+
self._discard(_cb)
|
|
529
|
+
self._track(_cb)
|
|
530
|
+
_dll.load_dll().sen_controller_set_param(
|
|
531
|
+
self._handle, key.encode(), value.encode(), _cb, None)
|
|
532
|
+
|
|
533
|
+
def getParam(self, key: str) -> str:
|
|
534
|
+
_raise_on_sdk_thread()
|
|
535
|
+
try:
|
|
536
|
+
return _sync.run_sync(self.asyncGetParam(key), 20.0)
|
|
537
|
+
except _sync.FuturesTimeoutError:
|
|
538
|
+
return "Error: Timeout"
|
|
539
|
+
|
|
540
|
+
def setParam(self, key: str, value: str) -> str:
|
|
541
|
+
_raise_on_sdk_thread()
|
|
542
|
+
try:
|
|
543
|
+
return _sync.run_sync(self.asyncSetParam(key, value), 20.0)
|
|
544
|
+
except _sync.FuturesTimeoutError:
|
|
545
|
+
return "Error: Timeout"
|
|
546
|
+
|
|
547
|
+
def setDebugEnabled(self, enabled: bool):
|
|
548
|
+
self.setParam("DEBUG_ENABLED", "True" if enabled else "False")
|
|
549
|
+
|
|
550
|
+
def setLogPath(self, enabled: bool = True, path: str = ""):
|
|
551
|
+
if not enabled:
|
|
552
|
+
self.setParam("LOG_PATH", "False")
|
|
553
|
+
elif path:
|
|
554
|
+
self.setParam("LOG_PATH", path)
|
|
555
|
+
else:
|
|
556
|
+
self.setParam("LOG_PATH", "True")
|
|
557
|
+
|
|
558
|
+
def getBLEBackendName(self) -> str:
|
|
559
|
+
return self.getParam("BACK_END")
|
|
560
|
+
|
|
561
|
+
def scan(self, period) -> List[BLEDevice]:
|
|
562
|
+
_raise_on_sdk_thread()
|
|
563
|
+
_validate_scan_period(period)
|
|
564
|
+
try:
|
|
565
|
+
return _sync.run_sync(self.asyncScan(period), period / 1000.0 + 15.0)
|
|
566
|
+
except _sync.FuturesTimeoutError:
|
|
567
|
+
return []
|
|
568
|
+
|
|
569
|
+
async def asyncScan(self, period) -> List[BLEDevice]:
|
|
570
|
+
loop = asyncio.get_running_loop()
|
|
571
|
+
fut = loop.create_future()
|
|
572
|
+
|
|
573
|
+
@_dll._sen_scan_result_cb
|
|
574
|
+
def _cb(ctx, devices, count):
|
|
575
|
+
_mark_sdk_thread()
|
|
576
|
+
try:
|
|
577
|
+
device_list = [BLEDevice._from_c(devices[i]) for i in range(count)]
|
|
578
|
+
_resolve_future(loop, fut, device_list)
|
|
579
|
+
finally:
|
|
580
|
+
self._discard(_cb)
|
|
581
|
+
|
|
582
|
+
self._track(_cb)
|
|
583
|
+
_dll.load_dll().sen_controller_scan_once(self._handle, int(period), _cb, None)
|
|
584
|
+
try:
|
|
585
|
+
return await asyncio.wait_for(fut, period / 1000.0 + 15.0)
|
|
586
|
+
except asyncio.TimeoutError:
|
|
587
|
+
return []
|
|
588
|
+
|
|
589
|
+
async def asyncMultiStartDataNotification(self, sensors: List[SensorProfile],
|
|
590
|
+
timeout: float = 0.0,
|
|
591
|
+
maxDelayDispersionMs: int = 5,
|
|
592
|
+
maxAttempts: int = 3) -> dict:
|
|
593
|
+
loop = asyncio.get_running_loop()
|
|
594
|
+
fut = loop.create_future()
|
|
595
|
+
self._multiStartDataNotification(
|
|
596
|
+
sensors, int(timeout * 1000), maxDelayDispersionMs, maxAttempts,
|
|
597
|
+
cb=lambda res, err: _resolve_future(loop, fut, res))
|
|
598
|
+
try:
|
|
599
|
+
return await asyncio.wait_for(fut, max(timeout, 60.0) + 30.0)
|
|
600
|
+
except asyncio.TimeoutError:
|
|
601
|
+
return {}
|
|
602
|
+
|
|
603
|
+
async def asyncMultiStopDataNotification(self, sensors: List[SensorProfile],
|
|
604
|
+
timeout: float = 0.0) -> dict:
|
|
605
|
+
loop = asyncio.get_running_loop()
|
|
606
|
+
fut = loop.create_future()
|
|
607
|
+
self._multiStopDataNotification(
|
|
608
|
+
sensors, int(timeout * 1000),
|
|
609
|
+
cb=lambda res, err: _resolve_future(loop, fut, res))
|
|
610
|
+
try:
|
|
611
|
+
return await asyncio.wait_for(fut, timeout + 30.0)
|
|
612
|
+
except asyncio.TimeoutError:
|
|
613
|
+
return {}
|
|
614
|
+
|
|
615
|
+
async def asyncParseBinToCsv(self, bin_path: str, csv_path: Optional[str] = None) -> str:
|
|
616
|
+
loop = asyncio.get_running_loop()
|
|
617
|
+
fut = loop.create_future()
|
|
618
|
+
self._parseBinToCsv(bin_path, csv_path,
|
|
619
|
+
cb=lambda res, err: _resolve_future(loop, fut, err if err and res in ('', 'Error') else res))
|
|
620
|
+
try:
|
|
621
|
+
return await asyncio.wait_for(fut, 300.0)
|
|
622
|
+
except asyncio.TimeoutError:
|
|
623
|
+
return "Error: Timeout"
|
|
624
|
+
|
|
625
|
+
async def asyncCheckSetupDongle(self) -> str:
|
|
626
|
+
loop = asyncio.get_running_loop()
|
|
627
|
+
fut = loop.create_future()
|
|
628
|
+
self._checkSetupDongle(cb=lambda res, err: _resolve_future(loop, fut, err if err and res in ('', 'Error') else res))
|
|
629
|
+
try:
|
|
630
|
+
return await asyncio.wait_for(fut, 300.0)
|
|
631
|
+
except asyncio.TimeoutError:
|
|
632
|
+
return "Error: Timeout"
|
|
633
|
+
|
|
634
|
+
async def asyncGetParam(self, key: str) -> str:
|
|
635
|
+
loop = asyncio.get_running_loop()
|
|
636
|
+
fut = loop.create_future()
|
|
637
|
+
self._getParam(key, cb=lambda res, err: _resolve_future(loop, fut, err if err and res in ('', 'Error') else res))
|
|
638
|
+
try:
|
|
639
|
+
return await asyncio.wait_for(fut, 12.0)
|
|
640
|
+
except asyncio.TimeoutError:
|
|
641
|
+
return "Error: Timeout"
|
|
642
|
+
|
|
643
|
+
async def asyncSetParam(self, key: str, value: str) -> str:
|
|
644
|
+
loop = asyncio.get_running_loop()
|
|
645
|
+
fut = loop.create_future()
|
|
646
|
+
self._setParam(key, str(value), cb=lambda res, err: _resolve_future(loop, fut, err if err and res in ('', 'Error') else res))
|
|
647
|
+
try:
|
|
648
|
+
result = await asyncio.wait_for(fut, 12.0)
|
|
649
|
+
except asyncio.TimeoutError:
|
|
650
|
+
return "Error: Timeout"
|
|
651
|
+
if key == "CMD_TIMEOUT_MS" and result.startswith("OK"):
|
|
652
|
+
_sync.note_cmd_timeout_ms(value)
|
|
653
|
+
return result
|
|
654
|
+
|
|
655
|
+
_instance = None
|
|
656
|
+
_instance_create_lock = threading.Lock()
|
|
657
|
+
|
|
658
|
+
def __getattr__(name):
|
|
659
|
+
if name == "SensorControllerInstance":
|
|
660
|
+
global _instance
|
|
661
|
+
if _instance is None:
|
|
662
|
+
with _instance_create_lock:
|
|
663
|
+
if _instance is None:
|
|
664
|
+
_instance = SensorController()
|
|
665
|
+
return _instance
|
|
666
|
+
raise AttributeError("module %r has no attribute %r" % (__name__, name))
|