sensor-sdk 0.0.43__tar.gz → 0.0.45__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 (23) hide show
  1. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/PKG-INFO +2 -2
  2. sensor_sdk-0.0.45/sensor/bleak_host.py +279 -0
  3. sensor_sdk-0.0.45/sensor/bleak_process.py +1001 -0
  4. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor/gforce.py +24 -4
  5. sensor_sdk-0.0.45/sensor/sensor_controller.py +255 -0
  6. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor/sensor_data.py +24 -24
  7. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor/sensor_data_context.py +107 -223
  8. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor/sensor_device.py +23 -28
  9. sensor_sdk-0.0.45/sensor/sensor_profile.py +499 -0
  10. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor/sensor_utils.py +1 -0
  11. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor_sdk.egg-info/PKG-INFO +2 -2
  12. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor_sdk.egg-info/SOURCES.txt +2 -0
  13. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/setup.py +2 -2
  14. sensor_sdk-0.0.43/sensor/sensor_controller.py +0 -276
  15. sensor_sdk-0.0.43/sensor/sensor_profile.py +0 -686
  16. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/LICENSE.txt +0 -0
  17. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/README.md +0 -0
  18. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor/__init__.py +0 -0
  19. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor_sdk.egg-info/dependency_links.txt +0 -0
  20. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor_sdk.egg-info/requires.txt +0 -0
  21. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor_sdk.egg-info/top_level.txt +0 -0
  22. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/sensor_sdk.egg-info/zip-safe +0 -0
  23. {sensor_sdk-0.0.43 → sensor_sdk-0.0.45}/setup.cfg +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sensor-sdk
3
- Version: 0.0.43
3
+ Version: 0.0.45
4
4
  Summary: Python sdk for Synchroni
5
5
  Home-page: https://github.com/oymotion/SynchroniSDKPython
6
6
  Author: Martin Ye
7
7
  Author-email: yecq_82@hotmail.com
8
- Requires-Python: >=3.9.0
8
+ Requires-Python: >=3.10.0
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.txt
11
11
  Requires-Dist: numpy
@@ -0,0 +1,279 @@
1
+ import asyncio
2
+ import multiprocessing
3
+ import queue
4
+ import threading
5
+ import uuid
6
+
7
+ from sensor.bleak_process import BleakProcess
8
+ from sensor import sensor_utils
9
+
10
+
11
+ class BleakHost:
12
+
13
+
14
+ def __init__(self):
15
+ self._cmd_queue = multiprocessing.Queue(maxsize=50)
16
+ self._result_queue = multiprocessing.Queue(maxsize=sensor_utils.BLEAK_RESULT_QUEUE_MAXSIZE)
17
+ self._bleak_process = BleakProcess(self._cmd_queue, self._result_queue)
18
+ self._started = False
19
+
20
+
21
+ self._result_loop = None
22
+ self._result_thread = None
23
+
24
+
25
+ self._cmd_loop = None
26
+ self._cmd_thread = None
27
+
28
+ # Pending commands: cmd_id -> (waiter_type, waiter, container, loop)
29
+ self._pending_lock = threading.Lock()
30
+ self._pending = {}
31
+
32
+
33
+ self.on_scan_once_result = None # callable(msg)
34
+ self.on_scan_result = None # callable(msg)
35
+ self.on_device_message = None # callable(device_mac, msg)
36
+
37
+ # ------------------------------------------------------------------
38
+ # Lifecycle
39
+ # ------------------------------------------------------------------
40
+ def start(self):
41
+ if self._started:
42
+ return
43
+ self._bleak_process.start()
44
+ self._started = True
45
+ self._start_result_loop()
46
+ self._start_cmd_loop()
47
+
48
+ def stop(self):
49
+ if not self._started:
50
+ return
51
+ try:
52
+ self._cmd_queue.put({"type": "terminate"})
53
+ except Exception:
54
+ pass
55
+
56
+ if self._bleak_process.is_alive():
57
+ self._bleak_process.join(timeout=5)
58
+ if self._bleak_process.is_alive():
59
+ self._bleak_process.terminate()
60
+
61
+ self._stop_result_loop()
62
+ self._stop_cmd_loop()
63
+ self._started = False
64
+
65
+ # ------------------------------------------------------------------
66
+ # Result loop
67
+ # ------------------------------------------------------------------
68
+ def _start_result_loop(self):
69
+ from sensor import sensor_utils
70
+ result_loop = asyncio.new_event_loop()
71
+ result_thread = threading.Thread(target=sensor_utils.start_loop, args=(result_loop,))
72
+ result_thread.daemon = True
73
+ result_thread.name = "BleakHost result"
74
+ result_thread.start()
75
+ self._result_loop = result_loop
76
+ self._result_thread = result_thread
77
+ asyncio.run_coroutine_threadsafe(self._consume_results(), result_loop)
78
+
79
+ def _stop_result_loop(self):
80
+ loop = getattr(self, '_result_loop', None)
81
+ thread = getattr(self, '_result_thread', None)
82
+ if loop is not None and not loop.is_closed():
83
+ try:
84
+ loop.call_soon_threadsafe(loop.stop)
85
+ except Exception:
86
+ pass
87
+ if thread is not None and thread.is_alive():
88
+ try:
89
+ thread.join(timeout=2)
90
+ except Exception:
91
+ pass
92
+ self._result_loop = None
93
+ self._result_thread = None
94
+
95
+ async def _consume_results(self):
96
+
97
+ while True:
98
+ try:
99
+ msg = await asyncio.get_event_loop().run_in_executor(
100
+ None, self._result_queue.get, True, 0.5
101
+ )
102
+ except queue.Empty:
103
+ continue
104
+ except Exception:
105
+ continue
106
+
107
+ msg_type = msg.get("type")
108
+ if msg_type == "scan_once_result":
109
+ if self.on_scan_once_result is not None:
110
+ try:
111
+ self.on_scan_once_result(msg)
112
+ except Exception:
113
+ pass
114
+ elif msg_type == "devices":
115
+ if self.on_scan_result is not None:
116
+ try:
117
+ self.on_scan_result(msg)
118
+ except Exception:
119
+ pass
120
+ elif msg_type == "command_result":
121
+ self._handle_command_result(msg)
122
+ elif msg_type in ("state_changed", "power_changed", "sensor_data", "error"):
123
+ if self.on_device_message is not None:
124
+ device_mac = msg.get("device_mac")
125
+ try:
126
+ self.on_device_message(device_mac, msg)
127
+ except Exception:
128
+ pass
129
+
130
+ def _handle_command_result(self, msg):
131
+
132
+ cmd_id = msg.get("cmd_id")
133
+ with self._pending_lock:
134
+ pending = self._pending.pop(cmd_id, None)
135
+ if pending is None:
136
+ return
137
+ waiter_type, waiter, container, loop = pending
138
+ if waiter_type == "async_event":
139
+ container["result"] = msg
140
+ if loop is not None and not loop.is_closed():
141
+ loop.call_soon_threadsafe(waiter.set)
142
+ elif waiter_type == "future":
143
+ if loop is not None and not loop.is_closed() and not waiter.done():
144
+ loop.call_soon_threadsafe(waiter.set_result, msg)
145
+
146
+ # ------------------------------------------------------------------
147
+ # Command loop
148
+ # ------------------------------------------------------------------
149
+ def _start_cmd_loop(self):
150
+ from sensor import sensor_utils
151
+ cmd_loop = asyncio.new_event_loop()
152
+ cmd_thread = threading.Thread(target=sensor_utils.start_loop, args=(cmd_loop,))
153
+ cmd_thread.daemon = True
154
+ cmd_thread.name = "BleakHost cmd"
155
+ cmd_thread.start()
156
+ self._cmd_loop = cmd_loop
157
+ self._cmd_thread = cmd_thread
158
+
159
+ def _stop_cmd_loop(self):
160
+ loop = getattr(self, '_cmd_loop', None)
161
+ thread = getattr(self, '_cmd_thread', None)
162
+ if loop is not None and not loop.is_closed():
163
+ try:
164
+ loop.call_soon_threadsafe(loop.stop)
165
+ except Exception:
166
+ pass
167
+ if thread is not None and thread.is_alive():
168
+ try:
169
+ thread.join(timeout=2)
170
+ except Exception:
171
+ pass
172
+ self._cmd_loop = None
173
+ self._cmd_thread = None
174
+
175
+ def _ensure_cmd_loop(self):
176
+ if self._cmd_loop is None or self._cmd_loop.is_closed():
177
+ self._start_cmd_loop()
178
+
179
+ # ------------------------------------------------------------------
180
+ # Public APIs
181
+ # ------------------------------------------------------------------
182
+ def scan_once(self, period):
183
+
184
+ try:
185
+ self._cmd_queue.put_nowait({"type": "scan_once", "period": period})
186
+ except queue.Full:
187
+ pass
188
+
189
+ def start_scan(self, period):
190
+
191
+ try:
192
+ self._cmd_queue.put_nowait({"type": "start_scan", "period": period})
193
+ except queue.Full:
194
+ pass
195
+
196
+ def stop_scan(self):
197
+
198
+ try:
199
+ self._cmd_queue.put_nowait({"type": "stop_scan"})
200
+ except queue.Full:
201
+ pass
202
+
203
+ def send_command(self, cmd: dict):
204
+
205
+ try:
206
+ self._cmd_queue.put_nowait(cmd)
207
+ except queue.Full:
208
+ pass
209
+
210
+ def send_command_sync(self, cmd: dict, timeout: float = 10.0) -> dict:
211
+
212
+ if not self._started:
213
+ return {}
214
+ self._ensure_cmd_loop()
215
+
216
+ async def _do_send():
217
+ cmd_id = str(uuid.uuid4())
218
+ cmd["cmd_id"] = cmd_id
219
+ event = asyncio.Event()
220
+ container = {}
221
+ with self._pending_lock:
222
+ self._pending[cmd_id] = ("async_event", event, container, asyncio.get_event_loop())
223
+ try:
224
+ self._cmd_queue.put(cmd, timeout=1.0)
225
+ except queue.Full:
226
+ with self._pending_lock:
227
+ self._pending.pop(cmd_id, None)
228
+ return {}
229
+ try:
230
+ await asyncio.wait_for(event.wait(), timeout=timeout)
231
+ except asyncio.TimeoutError:
232
+ with self._pending_lock:
233
+ self._pending.pop(cmd_id, None)
234
+ return {}
235
+ with self._pending_lock:
236
+ self._pending.pop(cmd_id, None)
237
+ return container.get("result", {})
238
+
239
+ future = asyncio.run_coroutine_threadsafe(_do_send(), self._cmd_loop)
240
+ try:
241
+ return future.result(timeout=timeout)
242
+ except Exception:
243
+ return {}
244
+
245
+ async def send_command_async(self, cmd: dict, timeout: float = 10.0) -> dict:
246
+
247
+ if not self._started:
248
+ return {}
249
+ self._ensure_cmd_loop()
250
+
251
+ async def _do_send():
252
+ cmd_id = str(uuid.uuid4())
253
+ cmd["cmd_id"] = cmd_id
254
+ future = asyncio.get_running_loop().create_future()
255
+ with self._pending_lock:
256
+ self._pending[cmd_id] = ("future", future, None, asyncio.get_running_loop())
257
+ try:
258
+ self._cmd_queue.put(cmd, timeout=1.0)
259
+ except queue.Full:
260
+ with self._pending_lock:
261
+ self._pending.pop(cmd_id, None)
262
+ return {}
263
+ try:
264
+ return await asyncio.wait_for(future, timeout=timeout)
265
+ except asyncio.TimeoutError:
266
+ with self._pending_lock:
267
+ self._pending.pop(cmd_id, None)
268
+ return {}
269
+
270
+ try:
271
+ caller_loop = asyncio.get_running_loop()
272
+ except RuntimeError:
273
+ caller_loop = None
274
+
275
+ future = asyncio.run_coroutine_threadsafe(_do_send(), self._cmd_loop)
276
+ if caller_loop is not None:
277
+ return await asyncio.wrap_future(future, loop=caller_loop)
278
+ else:
279
+ return future.result()