sensor-sdk 0.1.1__tar.gz → 0.1.4__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 (30) hide show
  1. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/PKG-INFO +2 -1
  2. sensor_sdk-0.1.4/sensor/__init__.py +20 -0
  3. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/bleak_host.py +87 -27
  4. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/bleak_process.py +55 -20
  5. sensor_sdk-0.1.4/sensor/fb/DataType.py +32 -0
  6. sensor_sdk-0.1.4/sensor/fb/Sample.py +49 -0
  7. sensor_sdk-0.1.4/sensor/fb/SensorData.py +296 -0
  8. sensor_sdk-0.1.4/sensor/fb/__init__.py +0 -0
  9. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/gforce.py +40 -19
  10. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/sdk_log.py +93 -5
  11. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/sensor_controller.py +3 -0
  12. sensor_sdk-0.1.4/sensor/sensor_data.py +241 -0
  13. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/sensor_data_context.py +296 -134
  14. sensor_sdk-0.1.4/sensor/sensor_data_pool.py +110 -0
  15. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/sensor_utils.py +20 -3
  16. sensor_sdk-0.1.4/sensor/winrt_high_throughput.py +104 -0
  17. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor_sdk.egg-info/PKG-INFO +2 -1
  18. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor_sdk.egg-info/SOURCES.txt +6 -0
  19. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor_sdk.egg-info/requires.txt +1 -0
  20. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/setup.py +2 -2
  21. sensor_sdk-0.1.1/sensor/__init__.py +0 -4
  22. sensor_sdk-0.1.1/sensor/sensor_data.py +0 -103
  23. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/LICENSE.txt +0 -0
  24. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/README.md +0 -0
  25. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/sensor_device.py +0 -0
  26. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor/sensor_profile.py +0 -0
  27. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor_sdk.egg-info/dependency_links.txt +0 -0
  28. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor_sdk.egg-info/top_level.txt +0 -0
  29. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/sensor_sdk.egg-info/zip-safe +0 -0
  30. {sensor_sdk-0.1.1 → sensor_sdk-0.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sensor-sdk
3
- Version: 0.1.1
3
+ Version: 0.1.4
4
4
  Summary: Python sdk for Synchroni
5
5
  Home-page: https://github.com/oymotion/SynchroniSDKPython
6
6
  Author: Martin Ye
@@ -11,6 +11,7 @@ License-File: LICENSE.txt
11
11
  Requires-Dist: numpy
12
12
  Requires-Dist: setuptools
13
13
  Requires-Dist: bleak>=3.0.2
14
+ Requires-Dist: flatbuffers>=25.0.0
14
15
  Dynamic: author
15
16
  Dynamic: author-email
16
17
  Dynamic: description
@@ -0,0 +1,20 @@
1
+ from sensor.sensor_controller import SensorController, SensorControllerInstance
2
+ from sensor.sensor_profile import SensorProfile
3
+ from sensor.sensor_device import BLEDevice, DeviceInfo, DeviceStateEx
4
+ from sensor.sensor_data import DataType, Sample, SensorData
5
+ from sensor.winrt_high_throughput import apply as _apply_winrt_high_throughput_patch
6
+
7
+ # Windows 下尝试给 bleak WinRT backend 打高吞吐率连接参数补丁
8
+ _apply_winrt_high_throughput_patch()
9
+
10
+ __all__ = [
11
+ "SensorController",
12
+ "SensorControllerInstance",
13
+ "SensorProfile",
14
+ "BLEDevice",
15
+ "DeviceInfo",
16
+ "DeviceStateEx",
17
+ "DataType",
18
+ "Sample",
19
+ "SensorData",
20
+ ]
@@ -17,7 +17,8 @@ class BleakHost:
17
17
  def __init__(self):
18
18
  self._cmd_queue = multiprocessing.Queue(maxsize=50)
19
19
  self._result_queue = multiprocessing.Queue(maxsize=sensor_utils.BLEAK_RESULT_QUEUE_MAXSIZE)
20
- self._bleak_process = BleakProcess(self._cmd_queue, self._result_queue, SdkLog.get_log_path())
20
+ self._data_queue = multiprocessing.Queue(maxsize=sensor_utils.BLEAK_DATA_QUEUE_MAXSIZE)
21
+ self._bleak_process = BleakProcess(self._cmd_queue, self._result_queue, self._data_queue, SdkLog.get_log_path())
21
22
  self._started = False
22
23
 
23
24
 
@@ -51,6 +52,7 @@ class BleakHost:
51
52
  def stop(self):
52
53
  if not self._started:
53
54
  return
55
+ self._should_exit = True
54
56
  try:
55
57
  self._cmd_queue.put({"type": "terminate"})
56
58
  except Exception as e:
@@ -60,9 +62,23 @@ class BleakHost:
60
62
  self._bleak_process.join(timeout=5)
61
63
  if self._bleak_process.is_alive():
62
64
  self._bleak_process.terminate()
65
+ self._bleak_process.join(timeout=2)
63
66
 
64
67
  self._stop_result_loop()
65
68
  self._stop_cmd_loop()
69
+
70
+ try:
71
+ self._cmd_queue.close()
72
+ self._result_queue.close()
73
+ self._data_queue.close()
74
+ except Exception as e:
75
+ SdkLog.exception(_TAG, "Error closing queues")
76
+
77
+ try:
78
+ self._bleak_process.close()
79
+ except Exception as e:
80
+ SdkLog.exception(_TAG, "Error closing bleak process")
81
+
66
82
  self._started = False
67
83
 
68
84
  # ------------------------------------------------------------------
@@ -77,9 +93,23 @@ class BleakHost:
77
93
  result_thread.start()
78
94
  self._result_loop = result_loop
79
95
  self._result_thread = result_thread
80
- asyncio.run_coroutine_threadsafe(self._consume_results(), result_loop)
96
+
97
+ def _create_consume_task():
98
+ self._consume_task = result_loop.create_task(self._consume_results())
99
+
100
+ result_loop.call_soon_threadsafe(_create_consume_task)
81
101
 
82
102
  def _stop_result_loop(self):
103
+ task = getattr(self, '_consume_task', None)
104
+ if task is not None:
105
+ try:
106
+ loop = task.get_loop()
107
+ if loop is not None and not loop.is_closed():
108
+ loop.call_soon_threadsafe(task.cancel)
109
+ except Exception as e:
110
+ SdkLog.exception(_TAG, "Unexpected error")
111
+ self._consume_task = None
112
+
83
113
  loop = getattr(self, '_result_loop', None)
84
114
  thread = getattr(self, '_result_thread', None)
85
115
  if loop is not None and not loop.is_closed():
@@ -97,38 +127,68 @@ class BleakHost:
97
127
 
98
128
  async def _consume_results(self):
99
129
 
100
- while True:
130
+ def _try_get(q, timeout):
131
+ try:
132
+ return q.get(True, timeout)
133
+ except queue.Empty:
134
+ return None
135
+
136
+ while not getattr(self, '_should_exit', False):
101
137
  try:
138
+ # Prioritize control/result messages; data messages are drained in batch below.
102
139
  msg = await asyncio.get_event_loop().run_in_executor(
103
- None, self._result_queue.get, True, 0.5
140
+ None, _try_get, self._result_queue, 0.02
104
141
  )
105
- except queue.Empty:
106
- continue
107
142
  except Exception:
143
+ msg = None
144
+
145
+ if msg is None:
146
+ try:
147
+ msg = await asyncio.get_event_loop().run_in_executor(
148
+ None, _try_get, self._data_queue, 0.02
149
+ )
150
+ except Exception:
151
+ msg = None
152
+
153
+ if msg is None:
154
+ await asyncio.sleep(0.001)
108
155
  continue
109
156
 
110
- msg_type = msg.get("type")
111
- if msg_type == "scan_once_result":
112
- if self.on_scan_once_result is not None:
113
- try:
114
- self.on_scan_once_result(msg)
115
- except Exception as e:
116
- SdkLog.exception(_TAG, "Unexpected error")
117
- elif msg_type == "devices":
118
- if self.on_scan_result is not None:
119
- try:
120
- self.on_scan_result(msg)
121
- except Exception as e:
122
- SdkLog.exception(_TAG, "Unexpected error")
123
- elif msg_type == "command_result":
124
- self._handle_command_result(msg)
125
- elif msg_type in ("state_changed", "power_changed", "sensor_data", "error"):
126
- if self.on_device_message is not None:
127
- device_mac = msg.get("device_mac")
157
+ self._dispatch_message(msg)
158
+
159
+ # Batch drain both queues to catch up quickly and reduce per-get overhead.
160
+ for q in (self._result_queue, self._data_queue):
161
+ while True:
128
162
  try:
129
- self.on_device_message(device_mac, msg)
130
- except Exception as e:
131
- SdkLog.exception(_TAG, "Unexpected error")
163
+ self._dispatch_message(q.get_nowait())
164
+ except queue.Empty:
165
+ break
166
+ except Exception:
167
+ break
168
+
169
+ def _dispatch_message(self, msg):
170
+ msg_type = msg.get("type")
171
+ if msg_type == "scan_once_result":
172
+ if self.on_scan_once_result is not None:
173
+ try:
174
+ self.on_scan_once_result(msg)
175
+ except Exception as e:
176
+ SdkLog.exception(_TAG, "Unexpected error")
177
+ elif msg_type == "devices":
178
+ if self.on_scan_result is not None:
179
+ try:
180
+ self.on_scan_result(msg)
181
+ except Exception as e:
182
+ SdkLog.exception(_TAG, "Unexpected error")
183
+ elif msg_type == "command_result":
184
+ self._handle_command_result(msg)
185
+ elif msg_type in ("state_changed", "power_changed", "sensor_data", "error"):
186
+ if self.on_device_message is not None:
187
+ device_mac = msg.get("device_mac")
188
+ try:
189
+ self.on_device_message(device_mac, msg)
190
+ except Exception as e:
191
+ SdkLog.exception(_TAG, "Unexpected error")
132
192
 
133
193
  def _handle_command_result(self, msg):
134
194
 
@@ -72,11 +72,13 @@ class BleakProcess(multiprocessing.Process):
72
72
  self,
73
73
  cmd_queue: multiprocessing.Queue,
74
74
  result_queue: multiprocessing.Queue,
75
+ data_queue: multiprocessing.Queue,
75
76
  log_path: str = None,
76
77
  ):
77
78
  super().__init__(daemon=True)
78
79
  self.cmd_queue = cmd_queue
79
80
  self.result_queue = result_queue
81
+ self.data_queue = data_queue
80
82
  self._log_path = log_path
81
83
  self._scanner = None
82
84
  self._is_scanning = False
@@ -141,15 +143,23 @@ class BleakProcess(multiprocessing.Process):
141
143
 
142
144
  def _flush_msg_queue(self):
143
145
 
146
+ def _route_and_put(msg):
147
+ msg_type = msg.get("type")
148
+ if msg_type == "sensor_data":
149
+ try:
150
+ self.data_queue.put_nowait(msg)
151
+ except queue.Full:
152
+ pass
153
+ elif msg_type in self._DROPABLE_MSG_TYPES:
154
+ self.result_queue.put_nowait(msg)
155
+ else:
156
+ # Use a short timeout during shutdown so the child process can exit cleanly.
157
+ self.result_queue.put(msg, timeout=2.0)
158
+
144
159
  while True:
145
160
  try:
146
161
  msg = self._msg_queue.get_nowait()
147
- msg_type = msg.get("type")
148
- if msg_type in self._DROPABLE_MSG_TYPES:
149
- self.result_queue.put_nowait(msg)
150
- else:
151
- # Use a short timeout during shutdown so the child process can exit cleanly.
152
- self.result_queue.put(msg, timeout=2.0)
162
+ _route_and_put(msg)
153
163
  except queue.Empty:
154
164
  break
155
165
  except queue.Full:
@@ -170,6 +180,7 @@ class BleakProcess(multiprocessing.Process):
170
180
  async def _publisher_task(self):
171
181
  # Throttle queue-full logs to avoid blocking stdout on Windows.
172
182
  _last_queue_full_log = 0.0
183
+ _last_data_queue_full_log = 0.0
173
184
 
174
185
  while True:
175
186
  msg = None
@@ -177,7 +188,17 @@ class BleakProcess(multiprocessing.Process):
177
188
  try:
178
189
  msg = self._msg_queue.get_nowait()
179
190
  msg_type = msg.get("type")
180
- if msg_type in self._DROPABLE_MSG_TYPES:
191
+ if msg_type == "sensor_data":
192
+ # High-frequency data: use a dedicated drop-capable queue so it cannot
193
+ # block control/result messages in the main result queue.
194
+ try:
195
+ self.data_queue.put_nowait(msg)
196
+ except queue.Full:
197
+ now = time.time()
198
+ if now - _last_data_queue_full_log >= 2.0:
199
+ _last_data_queue_full_log = now
200
+ SdkLog.w(_TAG, "Data queue is full, dropping sensor_data")
201
+ elif msg_type in self._DROPABLE_MSG_TYPES:
181
202
  self.result_queue.put_nowait(msg)
182
203
  else:
183
204
  # Use a short timeout for important messages so the publisher does not stall.
@@ -261,6 +282,17 @@ class BleakProcess(multiprocessing.Process):
261
282
 
262
283
  def _stop_device_loops(self, device_mac: str):
263
284
 
285
+ # Cancel battery task if still running in event_loop
286
+ if device_mac in self._battery_tasks:
287
+ task = self._battery_tasks.pop(device_mac, None)
288
+ if task:
289
+ try:
290
+ loop = self._event_loops.get(device_mac)
291
+ if loop and not loop.is_closed():
292
+ loop.call_soon_threadsafe(task.cancel)
293
+ except Exception as e:
294
+ SdkLog.exception(_TAG, "Unexpected error")
295
+
264
296
  # Cancel data task first if still running in data_event_loop
265
297
  if device_mac in self._data_tasks:
266
298
  task = self._data_tasks.pop(device_mac, None)
@@ -684,7 +716,12 @@ class BleakProcess(multiprocessing.Process):
684
716
 
685
717
  # Cancel battery task
686
718
  if device_mac in self._battery_tasks:
687
- self._battery_tasks.pop(device_mac, None)
719
+ task = self._battery_tasks.pop(device_mac, None)
720
+ if task is not None:
721
+ try:
722
+ task.cancel()
723
+ except Exception as e:
724
+ SdkLog.exception(_TAG, "Unexpected error")
688
725
 
689
726
  # Stop streaming and optionally disconnect client
690
727
  if disconnect_client and device_mac in self._gforces:
@@ -829,9 +866,9 @@ class BleakProcess(multiprocessing.Process):
829
866
 
830
867
  try:
831
868
  if ctx.isUniversalStream:
832
- await ctx.processUniversalData(local_buf, on_data, on_error)
869
+ await ctx._processUniversalData(local_buf, on_data, on_error)
833
870
  else:
834
- await ctx.process_data(local_buf, on_data, on_error)
871
+ await ctx._process_data(local_buf, on_data, on_error)
835
872
  except asyncio.CancelledError as e:
836
873
  SdkLog.exception(_TAG, "Data loop cancelled")
837
874
  except Exception as e:
@@ -1119,10 +1156,12 @@ class BleakProcess(multiprocessing.Process):
1119
1156
  if key == "NTF_PPG_RAW":
1120
1157
  map_key = "NTF_PPG"
1121
1158
 
1122
- # 老版本 EMG 设备上 Gesture 与 EMG 互斥,与 initGesture 逻辑保持一致
1159
+ # 老版本 EMG 设备上 Gesture 与 EMG 互斥,自动切换
1123
1160
  if not ctx.isNewEMG:
1124
1161
  if map_key == "NTF_GEST" and value == "ON" and ctx.notify_map.get("NTF_EMG") == "ON":
1125
- result = "Error: NTF_GEST conflicts with NTF_EMG on legacy EMG device"
1162
+ ctx.notify_map["NTF_EMG"] = "OFF"
1163
+ ctx.notify_map[map_key] = value
1164
+ result = "OK"
1126
1165
  elif map_key == "NTF_EMG" and value == "ON" and ctx.notify_map.get("NTF_GEST") == "ON":
1127
1166
  ctx.notify_map["NTF_GEST"] = "OFF"
1128
1167
  ctx.notify_map[map_key] = value
@@ -1165,14 +1204,12 @@ class BleakProcess(multiprocessing.Process):
1165
1204
  try:
1166
1205
  if value == "False" or value == "":
1167
1206
  SdkLog.set_log_path("")
1168
- result = "OK"
1169
1207
  elif value == "True":
1170
1208
  path = SdkLog.get_default_log_path()
1171
1209
  SdkLog.set_log_path(path)
1172
- result = path
1173
1210
  else:
1174
1211
  SdkLog.set_log_path(value)
1175
- result = value
1212
+ result = "OK"
1176
1213
  except Exception as e:
1177
1214
  SdkLog.exception(_TAG, f"_do_set_param DEBUG_LOG_PATH failed: {e}")
1178
1215
  result = "ERROR: " + str(e)
@@ -1182,16 +1219,14 @@ class BleakProcess(multiprocessing.Process):
1182
1219
  if value == "False" or value == "":
1183
1220
  SdkLog.set_data_log_enabled(False)
1184
1221
  await ctx.setDebugCSV("")
1185
- result = "OK"
1186
1222
  elif value == "True":
1187
1223
  SdkLog.set_data_log_enabled(True)
1188
1224
  path = SdkLog.get_default_data_log_path()
1189
- csv_result = await ctx.setDebugCSV(path)
1190
- result = path if csv_result == "OK" else csv_result
1225
+ await ctx.setDebugCSV(path)
1191
1226
  else:
1192
1227
  SdkLog.set_data_log_enabled(True)
1193
- csv_result = await ctx.setDebugCSV(value)
1194
- result = value if csv_result == "OK" else csv_result
1228
+ await ctx.setDebugCSV(value)
1229
+ result = "OK"
1195
1230
  except Exception as e:
1196
1231
  SdkLog.exception(_TAG, f"_do_set_param setDebugCSV failed: {device_mac} path={value}")
1197
1232
  result = "ERROR: " + str(e)
@@ -0,0 +1,32 @@
1
+ # automatically generated by the FlatBuffers compiler, do not modify
2
+
3
+ # namespace: fb
4
+
5
+ # BLE 数据通知类型,与 SensorSDKCXX src/SensorProfileImpl.h 中的 NotifyDataType 保持一致
6
+ class DataType(object):
7
+ NONE = 0
8
+ NTF_ACC_DATA = 1
9
+ NTF_GYO_DATA = 2
10
+ NTF_MAG_DATA = 3
11
+ NTF_EULER_DATA = 4
12
+ NTF_QUAT_FLOAT_DATA = 5
13
+ NTF_ROTA_DATA = 6
14
+ NTF_EMG_GEST_DATA = 7
15
+ NTF_EMG_ADC_DATA = 8
16
+ NTF_HID_MOUSE = 9
17
+ NTF_HID_JOYSTICK = 10
18
+ NTF_DEV_STATUS = 11
19
+ NTF_LOG_DATA = 12
20
+ NTF_MAG_ANGLE_DATA = 13
21
+ NTF_MOT_CURRENT_DATA = 14
22
+ NTF_NEUCIR_STATUS = 15
23
+ NTF_EEG = 16
24
+ NTF_ECG = 17
25
+ NTF_IMPEDANCE = 18
26
+ NTF_IMU = 19
27
+ NTF_ADS = 20
28
+ NTF_BRTH = 21
29
+ NTF_IMPEDANCE_EXT = 22
30
+ NTF_SPO2 = 23
31
+ NTF_PPG = 24
32
+ NTF_PARTIAL_DATA = 255
@@ -0,0 +1,49 @@
1
+ # automatically generated by the FlatBuffers compiler, do not modify
2
+
3
+ # namespace: fb
4
+
5
+ import flatbuffers
6
+ from flatbuffers.compat import import_numpy
7
+ np = import_numpy()
8
+
9
+ # 单个采样点,内存紧凑的 struct
10
+ class Sample(object):
11
+ __slots__ = ['_tab']
12
+
13
+ @classmethod
14
+ def SizeOf(cls):
15
+ return 32
16
+
17
+ # Sample
18
+ def Init(self, buf, pos):
19
+ self._tab = flatbuffers.table.Table(buf, pos)
20
+
21
+ # Sample
22
+ def TimeStampInMs(self): return self._tab.Get(flatbuffers.number_types.Int32Flags, self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(0))
23
+ # Sample
24
+ def ChannelIndex(self): return self._tab.Get(flatbuffers.number_types.Int32Flags, self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(4))
25
+ # Sample
26
+ def SampleIndex(self): return self._tab.Get(flatbuffers.number_types.Int32Flags, self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(8))
27
+ # Sample
28
+ def RawData(self): return self._tab.Get(flatbuffers.number_types.Int32Flags, self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(12))
29
+ # Sample
30
+ def Data(self): return self._tab.Get(flatbuffers.number_types.Float32Flags, self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(16))
31
+ # Sample
32
+ def Impedance(self): return self._tab.Get(flatbuffers.number_types.Float32Flags, self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(20))
33
+ # Sample
34
+ def Saturation(self): return self._tab.Get(flatbuffers.number_types.Float32Flags, self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(24))
35
+ # Sample
36
+ def IsLost(self): return self._tab.Get(flatbuffers.number_types.BoolFlags, self._tab.Pos + flatbuffers.number_types.UOffsetTFlags.py_type(28))
37
+
38
+ def CreateSample(builder, timeStampInMs, channelIndex, sampleIndex, rawData, data, impedance, saturation, isLost):
39
+ builder.Prep(4, 32)
40
+ builder.Pad(3)
41
+ builder.PrependBool(isLost)
42
+ builder.PrependFloat32(saturation)
43
+ builder.PrependFloat32(impedance)
44
+ builder.PrependFloat32(data)
45
+ builder.PrependInt32(rawData)
46
+ builder.PrependInt32(sampleIndex)
47
+ builder.PrependInt32(channelIndex)
48
+ builder.PrependInt32(timeStampInMs)
49
+ return builder.Offset()