sensor-sdk 0.1.1__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.1.1 → sensor_sdk-0.1.2}/PKG-INFO +1 -1
  2. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/bleak_host.py +31 -3
  3. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/bleak_process.py +25 -11
  4. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/gforce.py +29 -18
  5. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/sensor_controller.py +3 -0
  6. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/sensor_data_context.py +8 -2
  7. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/PKG-INFO +1 -1
  8. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/setup.py +1 -1
  9. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/LICENSE.txt +0 -0
  10. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/README.md +0 -0
  11. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/__init__.py +0 -0
  12. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/sdk_log.py +0 -0
  13. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/sensor_data.py +0 -0
  14. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/sensor_device.py +0 -0
  15. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/sensor_profile.py +0 -0
  16. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor/sensor_utils.py +0 -0
  17. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/SOURCES.txt +0 -0
  18. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/dependency_links.txt +0 -0
  19. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/requires.txt +0 -0
  20. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/top_level.txt +0 -0
  21. {sensor_sdk-0.1.1 → sensor_sdk-0.1.2}/sensor_sdk.egg-info/zip-safe +0 -0
  22. {sensor_sdk-0.1.1 → 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.1.1
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
@@ -51,6 +51,7 @@ class BleakHost:
51
51
  def stop(self):
52
52
  if not self._started:
53
53
  return
54
+ self._should_exit = True
54
55
  try:
55
56
  self._cmd_queue.put({"type": "terminate"})
56
57
  except Exception as e:
@@ -60,9 +61,22 @@ class BleakHost:
60
61
  self._bleak_process.join(timeout=5)
61
62
  if self._bleak_process.is_alive():
62
63
  self._bleak_process.terminate()
64
+ self._bleak_process.join(timeout=2)
63
65
 
64
66
  self._stop_result_loop()
65
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
+
66
80
  self._started = False
67
81
 
68
82
  # ------------------------------------------------------------------
@@ -77,9 +91,23 @@ class BleakHost:
77
91
  result_thread.start()
78
92
  self._result_loop = result_loop
79
93
  self._result_thread = result_thread
80
- 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)
81
99
 
82
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
+
83
111
  loop = getattr(self, '_result_loop', None)
84
112
  thread = getattr(self, '_result_thread', None)
85
113
  if loop is not None and not loop.is_closed():
@@ -97,10 +125,10 @@ class BleakHost:
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
@@ -261,6 +261,17 @@ class BleakProcess(multiprocessing.Process):
261
261
 
262
262
  def _stop_device_loops(self, device_mac: str):
263
263
 
264
+ # Cancel battery task if still running in event_loop
265
+ if device_mac in self._battery_tasks:
266
+ task = self._battery_tasks.pop(device_mac, None)
267
+ if task:
268
+ try:
269
+ loop = self._event_loops.get(device_mac)
270
+ if loop and not loop.is_closed():
271
+ loop.call_soon_threadsafe(task.cancel)
272
+ except Exception as e:
273
+ SdkLog.exception(_TAG, "Unexpected error")
274
+
264
275
  # Cancel data task first if still running in data_event_loop
265
276
  if device_mac in self._data_tasks:
266
277
  task = self._data_tasks.pop(device_mac, None)
@@ -684,7 +695,12 @@ class BleakProcess(multiprocessing.Process):
684
695
 
685
696
  # Cancel battery task
686
697
  if device_mac in self._battery_tasks:
687
- self._battery_tasks.pop(device_mac, None)
698
+ task = self._battery_tasks.pop(device_mac, None)
699
+ if task is not None:
700
+ try:
701
+ task.cancel()
702
+ except Exception as e:
703
+ SdkLog.exception(_TAG, "Unexpected error")
688
704
 
689
705
  # Stop streaming and optionally disconnect client
690
706
  if disconnect_client and device_mac in self._gforces:
@@ -1119,10 +1135,12 @@ class BleakProcess(multiprocessing.Process):
1119
1135
  if key == "NTF_PPG_RAW":
1120
1136
  map_key = "NTF_PPG"
1121
1137
 
1122
- # 老版本 EMG 设备上 Gesture 与 EMG 互斥,与 initGesture 逻辑保持一致
1138
+ # 老版本 EMG 设备上 Gesture 与 EMG 互斥,自动切换
1123
1139
  if not ctx.isNewEMG:
1124
1140
  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"
1141
+ ctx.notify_map["NTF_EMG"] = "OFF"
1142
+ ctx.notify_map[map_key] = value
1143
+ result = "OK"
1126
1144
  elif map_key == "NTF_EMG" and value == "ON" and ctx.notify_map.get("NTF_GEST") == "ON":
1127
1145
  ctx.notify_map["NTF_GEST"] = "OFF"
1128
1146
  ctx.notify_map[map_key] = value
@@ -1165,14 +1183,12 @@ class BleakProcess(multiprocessing.Process):
1165
1183
  try:
1166
1184
  if value == "False" or value == "":
1167
1185
  SdkLog.set_log_path("")
1168
- result = "OK"
1169
1186
  elif value == "True":
1170
1187
  path = SdkLog.get_default_log_path()
1171
1188
  SdkLog.set_log_path(path)
1172
- result = path
1173
1189
  else:
1174
1190
  SdkLog.set_log_path(value)
1175
- result = value
1191
+ result = "OK"
1176
1192
  except Exception as e:
1177
1193
  SdkLog.exception(_TAG, f"_do_set_param DEBUG_LOG_PATH failed: {e}")
1178
1194
  result = "ERROR: " + str(e)
@@ -1182,16 +1198,14 @@ class BleakProcess(multiprocessing.Process):
1182
1198
  if value == "False" or value == "":
1183
1199
  SdkLog.set_data_log_enabled(False)
1184
1200
  await ctx.setDebugCSV("")
1185
- result = "OK"
1186
1201
  elif value == "True":
1187
1202
  SdkLog.set_data_log_enabled(True)
1188
1203
  path = SdkLog.get_default_data_log_path()
1189
- csv_result = await ctx.setDebugCSV(path)
1190
- result = path if csv_result == "OK" else csv_result
1204
+ await ctx.setDebugCSV(path)
1191
1205
  else:
1192
1206
  SdkLog.set_data_log_enabled(True)
1193
- csv_result = await ctx.setDebugCSV(value)
1194
- result = value if csv_result == "OK" else csv_result
1207
+ await ctx.setDebugCSV(value)
1208
+ result = "OK"
1195
1209
  except Exception as e:
1196
1210
  SdkLog.exception(_TAG, f"_do_set_param setDebugCSV failed: {device_mac} path={value}")
1197
1211
  result = "ERROR: " + str(e)
@@ -764,6 +764,7 @@ class GForce:
764
764
  has_res=True,
765
765
  )
766
766
  )
767
+ return self._check_set_response(ret, "set_motor")
767
768
 
768
769
  async def set_led(self, switchStatus):
769
770
  body = [switchStatus == True]
@@ -775,6 +776,7 @@ class GForce:
775
776
  has_res=True,
776
777
  )
777
778
  )
779
+ return self._check_set_response(ret, "set_led")
778
780
 
779
781
  async def set_package_id(self, switchStatus):
780
782
  body = [switchStatus == True]
@@ -786,6 +788,7 @@ class GForce:
786
788
  has_res=True,
787
789
  )
788
790
  )
791
+ return self._check_set_response(ret, "set_package_id")
789
792
 
790
793
  async def set_log_level(self, logLevel):
791
794
  body = [0xFF & logLevel]
@@ -797,8 +800,9 @@ class GForce:
797
800
  has_res=True,
798
801
  )
799
802
  )
803
+ return self._check_set_response(ret, "set_log_level")
800
804
 
801
- async def set_function_switch(self, funcSwitch)-> bool:
805
+ async def set_function_switch(self, funcSwitch):
802
806
  body = [0xFF & funcSwitch]
803
807
  body = bytes(body)
804
808
  ret = await self._send_request(
@@ -808,11 +812,9 @@ class GForce:
808
812
  has_res=True,
809
813
  )
810
814
  )
811
- if (len(ret) > 0 and ret[0] == 0):
812
- return True
813
- return False
815
+ return self._check_set_response(ret, "set_function_switch")
814
816
 
815
- async def set_neucir_app_control(self, open, close, stop)-> bool:
817
+ async def set_neucir_app_control(self, open, close, stop):
816
818
  if stop:
817
819
  body = [4]
818
820
  elif open:
@@ -828,11 +830,9 @@ class GForce:
828
830
  has_res=True,
829
831
  )
830
832
  )
831
- if (len(ret) > 0 and ret[0] == 0):
832
- return True
833
- return False
833
+ return self._check_set_response(ret, "set_neucir_app_control")
834
834
 
835
- async def set_neucir_mode(self, mode)-> bool:
835
+ async def set_neucir_mode(self, mode):
836
836
  body = [0x90]
837
837
 
838
838
  body = bytes(body)
@@ -843,14 +843,13 @@ class GForce:
843
843
  has_res=True,
844
844
  )
845
845
  )
846
- if (len(ret) > 0 and ret[0] == 0):
847
- return True
848
- return False
846
+ return self._check_set_response(ret, "set_neucir_mode")
849
847
 
850
848
  async def set_firmware_filter_switch(self, switchStatus: int):
851
849
  body = [0xFF & switchStatus]
852
850
  body = bytes(body)
853
- await self._send_request(Request(cmd=Command.CMD_SET_FRIMWARE_FILTER_SWITCH, body=body, has_res=True))
851
+ ret = await self._send_request(Request(cmd=Command.CMD_SET_FRIMWARE_FILTER_SWITCH, body=body, has_res=True))
852
+ return self._check_set_response(ret, "set_firmware_filter_switch")
854
853
 
855
854
  async def get_firmware_filter_switch(self):
856
855
  buf = await self._send_request(Request(cmd=Command.CMD_GET_FRIMWARE_FILTER_SWITCH, has_res=True))
@@ -867,6 +866,7 @@ class GForce:
867
866
  )
868
867
 
869
868
  # print('set_emg_raw_data_config returned:', ret)
869
+ return self._check_set_response(ret, "set_emg_raw_data_config")
870
870
 
871
871
  self.resolution = cfg.resolution
872
872
 
@@ -925,7 +925,7 @@ class GForce:
925
925
  )
926
926
  return PpgRawDataConfig.from_bytes(buf)
927
927
 
928
- async def set_ppg_raw_data_config(self, cfg: PpgRawDataConfig) -> bool:
928
+ async def set_ppg_raw_data_config(self, cfg: PpgRawDataConfig):
929
929
  body = cfg.to_bytes()
930
930
  ret = await self._send_request(
931
931
  Request(
@@ -934,7 +934,7 @@ class GForce:
934
934
  has_res=True,
935
935
  )
936
936
  )
937
- return ret is not None and len(ret) > 0 and ret[0] == 0
937
+ return self._check_set_response(ret, "set_ppg_raw_data_config")
938
938
 
939
939
  async def get_imu_raw_data_config(self) -> ImuRawDataConfig:
940
940
  buf = await self._send_request(
@@ -945,7 +945,7 @@ class GForce:
945
945
  )
946
946
  return ImuRawDataConfig.from_bytes(buf)
947
947
 
948
- async def set_imu_raw_data_config(self, cfg: ImuRawDataConfig) -> bool:
948
+ async def set_imu_raw_data_config(self, cfg: ImuRawDataConfig):
949
949
  body = cfg.to_bytes()
950
950
  ret = await self._send_request(
951
951
  Request(
@@ -954,7 +954,7 @@ class GForce:
954
954
  has_res=True,
955
955
  )
956
956
  )
957
- return ret is not None and len(ret) > 0 and ret[0] == 0
957
+ return self._check_set_response(ret, "set_imu_raw_data_config")
958
958
 
959
959
  async def get_imu_cap_data_config(self) -> Optional[tuple]:
960
960
  """
@@ -999,13 +999,24 @@ class GForce:
999
999
  0xFF & (subscription >> 24),
1000
1000
  ]
1001
1001
  body = bytes(body)
1002
- await self._send_request(
1002
+ ret = await self._send_request(
1003
1003
  Request(
1004
1004
  cmd=Command.SET_DATA_NOTIF_SWITCH,
1005
1005
  body=body,
1006
1006
  has_res=True,
1007
1007
  )
1008
1008
  )
1009
+ return self._check_set_response(ret, "set_subscription")
1010
+
1011
+ def _check_set_response(self, ret: Optional[bytes], name: str) -> bytes:
1012
+ # """检查 set_xxxx 命令的响应,失败时抛出 RuntimeError。"""
1013
+ # if ret is None:
1014
+ # raise RuntimeError(f"{name} failed: no response")
1015
+ # if len(ret) == 0:
1016
+ # raise RuntimeError(f"{name} failed: empty response")
1017
+ # if ret[0] != 0:
1018
+ # raise RuntimeError(f"{name} failed: error code {ret[0]}")
1019
+ return ret
1009
1020
 
1010
1021
  async def start_streaming(self, q: queue.Queue):
1011
1022
  return await self._run_in_gforce_loop(self._do_start_streaming(q))
@@ -69,6 +69,9 @@ class SensorController:
69
69
  pass
70
70
 
71
71
  def terminate(self):
72
+ if getattr(self, '_terminated', False):
73
+ return
74
+ self._terminated = True
72
75
  SdkLog.controller(_TAG, "terminate called")
73
76
  sensor_utils._terminated = True
74
77
 
@@ -244,7 +244,7 @@ class SensorProfileDataCtx:
244
244
  data.channelCount = 8
245
245
  data.channelMask = config.channel_mask
246
246
  data.minPackageSampleCount = packageCount
247
- data.packageSampleCount = 8
247
+ data.packageSampleCount = config.batch_len
248
248
 
249
249
  data.clear()
250
250
  isNewEMG = True
@@ -297,6 +297,10 @@ class SensorProfileDataCtx:
297
297
  self.sensorDatas[SensorDataType.DATA_TYPE_EMG] = data
298
298
  self.isNewEMG = isNewEMG
299
299
 
300
+ # 老 EMG 设备不支持 FILTER
301
+ if not isNewEMG:
302
+ self.filter_map.clear()
303
+
300
304
  return data.channelCount
301
305
 
302
306
 
@@ -433,7 +437,7 @@ class SensorProfileDataCtx:
433
437
  async def initIMU(self, packageCount: int) -> int:
434
438
  SdkLog.d(_TAG, "initIMU(...)")
435
439
  IMU_TYPE_QAT6 = 0x0004
436
- min_package_sample_count = 2
440
+ min_package_sample_count = 1
437
441
  self.isContainQAT6 = False
438
442
 
439
443
  if not self.hasIMU():
@@ -797,6 +801,8 @@ class SensorProfileDataCtx:
797
801
  return True
798
802
 
799
803
  async def setFilter(self, filter: str, value: str) -> str:
804
+ if not self.filter_map:
805
+ return "ERROR: Filter not supported on this device"
800
806
  self.filter_map[filter] = value
801
807
  switch = 0
802
808
  for filter in self.filter_map.keys():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sensor-sdk
3
- Version: 0.1.1
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
@@ -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.1.01",
11
+ version="0.1.2",
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