sensor-sdk 0.0.71__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {sensor_sdk-0.0.71/sensor_sdk.egg-info → sensor_sdk-0.1.2}/PKG-INFO +41 -11
  2. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/README.md +40 -10
  3. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/bleak_host.py +61 -28
  4. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/bleak_process.py +1242 -1088
  5. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/gforce.py +1098 -1077
  6. sensor_sdk-0.1.2/sensor/sdk_log.py +259 -0
  7. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/sensor_controller.py +43 -14
  8. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/sensor_data.py +103 -103
  9. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/sensor_data_context.py +1640 -1488
  10. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/sensor_device.py +4 -0
  11. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/sensor_profile.py +630 -540
  12. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/sensor_utils.py +908 -906
  13. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2/sensor_sdk.egg-info}/PKG-INFO +41 -11
  14. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/SOURCES.txt +1 -0
  15. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/setup.py +1 -1
  16. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/LICENSE.txt +0 -0
  17. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor/__init__.py +0 -0
  18. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/dependency_links.txt +0 -0
  19. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/requires.txt +0 -0
  20. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/top_level.txt +0 -0
  21. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/zip-safe +0 -0
  22. {sensor_sdk-0.0.71 → sensor_sdk-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sensor-sdk
3
- Version: 0.0.71
3
+ Version: 0.1.2
4
4
  Summary: Python sdk for Synchroni
5
5
  Home-page: https://github.com/oymotion/SynchroniSDKPython
6
6
  Author: Martin Ye
@@ -208,6 +208,8 @@ success = sensorProfile.connect()
208
208
 
209
209
  Use `def disconnect() -> bool` to disconnect.
210
210
 
211
+ If data notification is currently active, `disconnect()` will automatically stop it first before closing the BLE connection.
212
+
211
213
  ```python
212
214
  success = sensorProfile.disconnect()
213
215
  ```
@@ -369,24 +371,31 @@ Please check async_console.py in examples directory
369
371
 
370
372
  Use `def setParam(self, key: str, value: str) -> str` to set parameter of sensor profile. Please call after device in 'Ready' state.
371
373
 
374
+ The asynchronous variant is `asyncSetParam(self, key: str, value: str) -> str`.
375
+
376
+ If the device is already streaming when you change an `NTF_*` or `FILTER_*` key, the SDK will stop and restart the data notification so the new setting takes effect immediately.
377
+
372
378
  Below is available key and value:
373
379
 
374
380
  ```python
381
+ # Data stream toggles
382
+ result = sensorProfile.setParam("NTF_GEST", "ON")
375
383
  result = sensorProfile.setParam("NTF_EMG", "ON")
376
- # set EMG data to ON or OFF, result is "OK" if succeed
377
-
378
384
  result = sensorProfile.setParam("NTF_EEG", "ON")
379
- # set EEG data to ON or OFF, result is "OK" if succeed
380
-
381
385
  result = sensorProfile.setParam("NTF_ECG", "ON")
382
- # set ECG data to ON or OFF, result is "OK" if succeed
383
-
384
386
  result = sensorProfile.setParam("NTF_IMU", "ON")
385
- # set IMU data to ON or OFF, result is "OK" if succeed
386
-
387
387
  result = sensorProfile.setParam("NTF_BRTH", "ON")
388
- # set BRTH data to ON or OFF, result is "OK" if succeed
389
-
388
+ result = sensorProfile.setParam("NTF_IMPEDANCE", "ON")
389
+ result = sensorProfile.setParam("NTF_MAG_ANGLE", "ON")
390
+ result = sensorProfile.setParam("NTF_PPG_RAW", "ON")
391
+ result = sensorProfile.setParam("NTF_GFORCE_EULER", "ON")
392
+ result = sensorProfile.setParam("NTF_GFORCE_QUAT", "ON")
393
+ result = sensorProfile.setParam("NTF_GFORCE_ACC", "ON")
394
+ result = sensorProfile.setParam("NTF_GFORCE_GYRO", "ON")
395
+ # set data stream to ON or OFF, result is "OK" if succeed
396
+ # Note: on legacy (non-new) EMG devices, NTF_GEST and NTF_EMG are mutually exclusive.
397
+
398
+ # Firmware filter toggles
390
399
  result = sensorProfile.setParam("FILTER_50HZ", "ON")
391
400
  # set 50Hz notch filter to ON or OFF, result is "OK" if succeed
392
401
 
@@ -403,3 +412,24 @@ result = sensorProfile.setParam("DEBUG_BLE_DATA_PATH", "d:/temp/test.csv")
403
412
  # set debug ble data path, result is "OK" if succeed
404
413
  # please give an absolute path and make sure it is valid and writeable by yourself
405
414
  ```
415
+
416
+
417
+ ### getParam method
418
+
419
+ Use `def getParam(self, key: str) -> str` to query the current parameter state of a sensor profile. Please call after the device reaches the 'Ready' state.
420
+
421
+ The asynchronous variant is `asyncGetParam(self, key: str) -> str`.
422
+
423
+ Supported aggregate query keys:
424
+
425
+ ```python
426
+ result = sensorProfile.getParam("FILTER")
427
+ # Returns a pipe-separated string of all filter states, e.g.:
428
+ # "FILTER_50HZ|ON|FILTER_60HZ|ON|FILTER_HPF|ON|FILTER_LPF|ON"
429
+
430
+ result = sensorProfile.getParam("NTF")
431
+ # Returns a pipe-separated string of all notification states, e.g.:
432
+ # "NTF_BRTH|ON|NTF_ECG|ON|NTF_EEG|ON|NTF_EMG|ON|..."
433
+ ```
434
+
435
+ If the key is not supported, the result starts with `"Error"`.
@@ -185,6 +185,8 @@ success = sensorProfile.connect()
185
185
 
186
186
  Use `def disconnect() -> bool` to disconnect.
187
187
 
188
+ If data notification is currently active, `disconnect()` will automatically stop it first before closing the BLE connection.
189
+
188
190
  ```python
189
191
  success = sensorProfile.disconnect()
190
192
  ```
@@ -346,24 +348,31 @@ Please check async_console.py in examples directory
346
348
 
347
349
  Use `def setParam(self, key: str, value: str) -> str` to set parameter of sensor profile. Please call after device in 'Ready' state.
348
350
 
351
+ The asynchronous variant is `asyncSetParam(self, key: str, value: str) -> str`.
352
+
353
+ If the device is already streaming when you change an `NTF_*` or `FILTER_*` key, the SDK will stop and restart the data notification so the new setting takes effect immediately.
354
+
349
355
  Below is available key and value:
350
356
 
351
357
  ```python
358
+ # Data stream toggles
359
+ result = sensorProfile.setParam("NTF_GEST", "ON")
352
360
  result = sensorProfile.setParam("NTF_EMG", "ON")
353
- # set EMG data to ON or OFF, result is "OK" if succeed
354
-
355
361
  result = sensorProfile.setParam("NTF_EEG", "ON")
356
- # set EEG data to ON or OFF, result is "OK" if succeed
357
-
358
362
  result = sensorProfile.setParam("NTF_ECG", "ON")
359
- # set ECG data to ON or OFF, result is "OK" if succeed
360
-
361
363
  result = sensorProfile.setParam("NTF_IMU", "ON")
362
- # set IMU data to ON or OFF, result is "OK" if succeed
363
-
364
364
  result = sensorProfile.setParam("NTF_BRTH", "ON")
365
- # set BRTH data to ON or OFF, result is "OK" if succeed
366
-
365
+ result = sensorProfile.setParam("NTF_IMPEDANCE", "ON")
366
+ result = sensorProfile.setParam("NTF_MAG_ANGLE", "ON")
367
+ result = sensorProfile.setParam("NTF_PPG_RAW", "ON")
368
+ result = sensorProfile.setParam("NTF_GFORCE_EULER", "ON")
369
+ result = sensorProfile.setParam("NTF_GFORCE_QUAT", "ON")
370
+ result = sensorProfile.setParam("NTF_GFORCE_ACC", "ON")
371
+ result = sensorProfile.setParam("NTF_GFORCE_GYRO", "ON")
372
+ # set data stream to ON or OFF, result is "OK" if succeed
373
+ # Note: on legacy (non-new) EMG devices, NTF_GEST and NTF_EMG are mutually exclusive.
374
+
375
+ # Firmware filter toggles
367
376
  result = sensorProfile.setParam("FILTER_50HZ", "ON")
368
377
  # set 50Hz notch filter to ON or OFF, result is "OK" if succeed
369
378
 
@@ -380,3 +389,24 @@ result = sensorProfile.setParam("DEBUG_BLE_DATA_PATH", "d:/temp/test.csv")
380
389
  # set debug ble data path, result is "OK" if succeed
381
390
  # please give an absolute path and make sure it is valid and writeable by yourself
382
391
  ```
392
+
393
+
394
+ ### getParam method
395
+
396
+ Use `def getParam(self, key: str) -> str` to query the current parameter state of a sensor profile. Please call after the device reaches the 'Ready' state.
397
+
398
+ The asynchronous variant is `asyncGetParam(self, key: str) -> str`.
399
+
400
+ Supported aggregate query keys:
401
+
402
+ ```python
403
+ result = sensorProfile.getParam("FILTER")
404
+ # Returns a pipe-separated string of all filter states, e.g.:
405
+ # "FILTER_50HZ|ON|FILTER_60HZ|ON|FILTER_HPF|ON|FILTER_LPF|ON"
406
+
407
+ result = sensorProfile.getParam("NTF")
408
+ # Returns a pipe-separated string of all notification states, e.g.:
409
+ # "NTF_BRTH|ON|NTF_ECG|ON|NTF_EEG|ON|NTF_EMG|ON|..."
410
+ ```
411
+
412
+ If the key is not supported, the result starts with `"Error"`.
@@ -6,6 +6,9 @@ import uuid
6
6
 
7
7
  from sensor.bleak_process import BleakProcess
8
8
  from sensor import sensor_utils
9
+ from sensor.sdk_log import SdkLog
10
+
11
+ _TAG = "BleakHost"
9
12
 
10
13
 
11
14
  class BleakHost:
@@ -14,7 +17,7 @@ class BleakHost:
14
17
  def __init__(self):
15
18
  self._cmd_queue = multiprocessing.Queue(maxsize=50)
16
19
  self._result_queue = multiprocessing.Queue(maxsize=sensor_utils.BLEAK_RESULT_QUEUE_MAXSIZE)
17
- self._bleak_process = BleakProcess(self._cmd_queue, self._result_queue)
20
+ self._bleak_process = BleakProcess(self._cmd_queue, self._result_queue, SdkLog.get_log_path())
18
21
  self._started = False
19
22
 
20
23
 
@@ -48,19 +51,32 @@ class BleakHost:
48
51
  def stop(self):
49
52
  if not self._started:
50
53
  return
54
+ self._should_exit = True
51
55
  try:
52
56
  self._cmd_queue.put({"type": "terminate"})
53
57
  except Exception as e:
54
- import traceback
55
- traceback.print_exc()
58
+ SdkLog.exception(_TAG, "Unexpected error")
56
59
 
57
60
  if self._bleak_process.is_alive():
58
61
  self._bleak_process.join(timeout=5)
59
62
  if self._bleak_process.is_alive():
60
63
  self._bleak_process.terminate()
64
+ self._bleak_process.join(timeout=2)
61
65
 
62
66
  self._stop_result_loop()
63
67
  self._stop_cmd_loop()
68
+
69
+ try:
70
+ self._cmd_queue.close()
71
+ self._result_queue.close()
72
+ except Exception as e:
73
+ SdkLog.exception(_TAG, "Error closing queues")
74
+
75
+ try:
76
+ self._bleak_process.close()
77
+ except Exception as e:
78
+ SdkLog.exception(_TAG, "Error closing bleak process")
79
+
64
80
  self._started = False
65
81
 
66
82
  # ------------------------------------------------------------------
@@ -75,32 +91,44 @@ class BleakHost:
75
91
  result_thread.start()
76
92
  self._result_loop = result_loop
77
93
  self._result_thread = result_thread
78
- asyncio.run_coroutine_threadsafe(self._consume_results(), result_loop)
94
+
95
+ def _create_consume_task():
96
+ self._consume_task = result_loop.create_task(self._consume_results())
97
+
98
+ result_loop.call_soon_threadsafe(_create_consume_task)
79
99
 
80
100
  def _stop_result_loop(self):
101
+ task = getattr(self, '_consume_task', None)
102
+ if task is not None:
103
+ try:
104
+ loop = task.get_loop()
105
+ if loop is not None and not loop.is_closed():
106
+ loop.call_soon_threadsafe(task.cancel)
107
+ except Exception as e:
108
+ SdkLog.exception(_TAG, "Unexpected error")
109
+ self._consume_task = None
110
+
81
111
  loop = getattr(self, '_result_loop', None)
82
112
  thread = getattr(self, '_result_thread', None)
83
113
  if loop is not None and not loop.is_closed():
84
114
  try:
85
115
  loop.call_soon_threadsafe(loop.stop)
86
116
  except Exception as e:
87
- import traceback
88
- traceback.print_exc()
117
+ SdkLog.exception(_TAG, "Unexpected error")
89
118
  if thread is not None and thread.is_alive():
90
119
  try:
91
120
  thread.join(timeout=2)
92
121
  except Exception as e:
93
- import traceback
94
- traceback.print_exc()
122
+ SdkLog.exception(_TAG, "Unexpected error")
95
123
  self._result_loop = None
96
124
  self._result_thread = None
97
125
 
98
126
  async def _consume_results(self):
99
127
 
100
- while True:
128
+ while not getattr(self, '_should_exit', False):
101
129
  try:
102
130
  msg = await asyncio.get_event_loop().run_in_executor(
103
- None, self._result_queue.get, True, 0.5
131
+ None, self._result_queue.get, True, 0.1
104
132
  )
105
133
  except queue.Empty:
106
134
  continue
@@ -113,15 +141,13 @@ class BleakHost:
113
141
  try:
114
142
  self.on_scan_once_result(msg)
115
143
  except Exception as e:
116
- import traceback
117
- traceback.print_exc()
144
+ SdkLog.exception(_TAG, "Unexpected error")
118
145
  elif msg_type == "devices":
119
146
  if self.on_scan_result is not None:
120
147
  try:
121
148
  self.on_scan_result(msg)
122
149
  except Exception as e:
123
- import traceback
124
- traceback.print_exc()
150
+ SdkLog.exception(_TAG, "Unexpected error")
125
151
  elif msg_type == "command_result":
126
152
  self._handle_command_result(msg)
127
153
  elif msg_type in ("state_changed", "power_changed", "sensor_data", "error"):
@@ -130,8 +156,7 @@ class BleakHost:
130
156
  try:
131
157
  self.on_device_message(device_mac, msg)
132
158
  except Exception as e:
133
- import traceback
134
- traceback.print_exc()
159
+ SdkLog.exception(_TAG, "Unexpected error")
135
160
 
136
161
  def _handle_command_result(self, msg):
137
162
 
@@ -149,6 +174,15 @@ class BleakHost:
149
174
  if loop is not None and not loop.is_closed() and not waiter.done():
150
175
  loop.call_soon_threadsafe(waiter.set_result, msg)
151
176
 
177
+ # 将 connect 返回的 chip_type 转发给对应 SensorProfile
178
+ chip_type = msg.get("chip_type")
179
+ device_mac = msg.get("device_mac")
180
+ if chip_type is not None and self.on_device_message is not None and device_mac is not None:
181
+ try:
182
+ self.on_device_message(device_mac, {"type": "chip_type", "chip_type": chip_type})
183
+ except Exception as e:
184
+ SdkLog.exception(_TAG, "Error forwarding chip_type")
185
+
152
186
  # ------------------------------------------------------------------
153
187
  # Command loop
154
188
  # ------------------------------------------------------------------
@@ -169,14 +203,12 @@ class BleakHost:
169
203
  try:
170
204
  loop.call_soon_threadsafe(loop.stop)
171
205
  except Exception as e:
172
- import traceback
173
- traceback.print_exc()
206
+ SdkLog.exception(_TAG, "Unexpected error")
174
207
  if thread is not None and thread.is_alive():
175
208
  try:
176
209
  thread.join(timeout=2)
177
210
  except Exception as e:
178
- import traceback
179
- traceback.print_exc()
211
+ SdkLog.exception(_TAG, "Unexpected error")
180
212
  self._cmd_loop = None
181
213
  self._cmd_thread = None
182
214
 
@@ -192,32 +224,28 @@ class BleakHost:
192
224
  try:
193
225
  self._cmd_queue.put_nowait({"type": "scan_once", "period": period})
194
226
  except queue.Full as e:
195
- import traceback
196
- traceback.print_exc()
227
+ SdkLog.exception(_TAG, "Unexpected error")
197
228
 
198
229
  def start_scan(self, period):
199
230
 
200
231
  try:
201
232
  self._cmd_queue.put_nowait({"type": "start_scan", "period": period})
202
233
  except queue.Full as e:
203
- import traceback
204
- traceback.print_exc()
234
+ SdkLog.exception(_TAG, "Unexpected error")
205
235
 
206
236
  def stop_scan(self):
207
237
 
208
238
  try:
209
239
  self._cmd_queue.put_nowait({"type": "stop_scan"})
210
240
  except queue.Full as e:
211
- import traceback
212
- traceback.print_exc()
241
+ SdkLog.exception(_TAG, "Unexpected error")
213
242
 
214
243
  def send_command(self, cmd: dict):
215
244
 
216
245
  try:
217
246
  self._cmd_queue.put_nowait(cmd)
218
247
  except queue.Full as e:
219
- import traceback
220
- traceback.print_exc()
248
+ SdkLog.exception(_TAG, "Command queue full")
221
249
 
222
250
  def send_command_sync(self, cmd: dict, timeout: float = 10.0) -> dict:
223
251
 
@@ -235,12 +263,14 @@ class BleakHost:
235
263
  try:
236
264
  self._cmd_queue.put(cmd, timeout=1.0)
237
265
  except queue.Full:
266
+ SdkLog.e(_TAG, f"send_command_sync queue full: {cmd.get('type')}")
238
267
  with self._pending_lock:
239
268
  self._pending.pop(cmd_id, None)
240
269
  return {}
241
270
  try:
242
271
  await asyncio.wait_for(event.wait(), timeout=timeout)
243
272
  except asyncio.TimeoutError:
273
+ SdkLog.e(_TAG, f"send_command_sync timeout: {cmd.get('type')}")
244
274
  with self._pending_lock:
245
275
  self._pending.pop(cmd_id, None)
246
276
  return {}
@@ -252,6 +282,7 @@ class BleakHost:
252
282
  try:
253
283
  return future.result(timeout=timeout)
254
284
  except Exception:
285
+ SdkLog.exception(_TAG, "send_command_sync future failed")
255
286
  return {}
256
287
 
257
288
  async def send_command_async(self, cmd: dict, timeout: float = 10.0) -> dict:
@@ -269,12 +300,14 @@ class BleakHost:
269
300
  try:
270
301
  self._cmd_queue.put(cmd, timeout=1.0)
271
302
  except queue.Full:
303
+ SdkLog.e(_TAG, f"send_command_async queue full: {cmd.get('type')}")
272
304
  with self._pending_lock:
273
305
  self._pending.pop(cmd_id, None)
274
306
  return {}
275
307
  try:
276
308
  return await asyncio.wait_for(future, timeout=timeout)
277
309
  except asyncio.TimeoutError:
310
+ SdkLog.e(_TAG, f"send_command_async timeout: {cmd.get('type')}")
278
311
  with self._pending_lock:
279
312
  self._pending.pop(cmd_id, None)
280
313
  return {}