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.
@@ -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))