yostlabs 2025.4.30__tar.gz → 2025.9.17__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.
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/PKG-INFO +1 -1
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/pyproject.toml +1 -1
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/communication/ble.py +81 -69
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/api.py +55 -40
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/utils/parser.py +1 -2
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/utils/streaming.py +82 -27
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/.gitignore +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/embedded_2024_dec_20.xml +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_ble.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_commands.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_component_specific_settings_and_commands.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_firmware_upload.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_parsing_stored_binary.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_read_settings.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_streaming.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_streaming_manager.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/Examples/example_write_settings.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/LICENSE +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/README.md +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/__init__.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/communication/__init__.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/communication/base.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/communication/serial.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/math/__init__.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/math/quaternion.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/math/vector.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/__init__.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/consts.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/eepts.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/utils/__init__.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/utils/calibration.py +0 -0
- {yostlabs-2025.4.30 → yostlabs-2025.9.17}/src/yostlabs/tss3/utils/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yostlabs
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.9.17
|
|
4
4
|
Summary: Python resources and API for 3Space sensors from Yost Labs Inc.
|
|
5
5
|
Project-URL: Homepage, https://yostlabs.com/
|
|
6
6
|
Project-URL: Repository, https://github.com/YostLabs/3SpacePythonPackage/tree/main
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yostlabs"
|
|
7
7
|
#If uploading again on the same day, add a fourth number
|
|
8
|
-
version = "2025.
|
|
8
|
+
version = "2025.09.17"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Yost Labs Inc.", email="techsupport@yostlabs.com" },
|
|
11
11
|
{ name="Andy Riedlinger", email="techsupport@yostlabs.com" },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import async_timeout
|
|
3
|
+
import threading
|
|
3
4
|
import time
|
|
4
5
|
from bleak import BleakScanner, BleakClient
|
|
5
6
|
from bleak.backends.device import BLEDevice
|
|
@@ -24,10 +25,23 @@ MANUFACTURER_NAME_STRING_UUID = "00002a29-0000-1000-8000-00805f9b34fb"
|
|
|
24
25
|
|
|
25
26
|
class TssBLENoConnectionError(Exception): ...
|
|
26
27
|
|
|
28
|
+
def ylBleEventLoopThread(loop: asyncio.AbstractEventLoop):
|
|
29
|
+
asyncio.set_event_loop(loop)
|
|
30
|
+
loop.run_forever()
|
|
31
|
+
|
|
27
32
|
from yostlabs.communication.base import *
|
|
28
33
|
class ThreespaceBLEComClass(ThreespaceComClass):
|
|
29
34
|
|
|
30
35
|
DEFAULT_TIMEOUT = 2
|
|
36
|
+
EVENT_LOOP = None
|
|
37
|
+
EVENT_LOOP_THREAD = None
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def __lazy_event_loop_init(cls):
|
|
41
|
+
if cls.EVENT_LOOP is None:
|
|
42
|
+
cls.EVENT_LOOP = asyncio.new_event_loop()
|
|
43
|
+
cls.EVENT_LOOP_THREAD = threading.Thread(target=ylBleEventLoopThread, args=(cls.EVENT_LOOP,), daemon=True)
|
|
44
|
+
cls.EVENT_LOOP_THREAD.start()
|
|
31
45
|
|
|
32
46
|
def __init__(self, ble: BleakClient | BLEDevice | str, discover_name: bool = True, discovery_timeout=5, error_on_disconnect=True):
|
|
33
47
|
"""
|
|
@@ -39,6 +53,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
39
53
|
error_on_disconnect : If trying to read while the sensor is disconnected, an exception will be generated. This may be undesirable \
|
|
40
54
|
if it is expected that the sensor will frequently go in and out of range and the user wishes to preserve data (such as streaming)
|
|
41
55
|
"""
|
|
56
|
+
self.__lazy_event_loop_init()
|
|
42
57
|
bleak_options = { "timeout": discovery_timeout, "disconnected_callback": self.__on_disconnect }
|
|
43
58
|
if isinstance(ble, BleakClient): #Actual client
|
|
44
59
|
self.client = ble
|
|
@@ -46,7 +61,8 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
46
61
|
elif isinstance(ble, str):
|
|
47
62
|
if discover_name: #Local Name stirng
|
|
48
63
|
self.__lazy_init_scanner()
|
|
49
|
-
|
|
64
|
+
future = asyncio.run_coroutine_threadsafe(BleakScanner.find_device_by_name(ble, timeout=discovery_timeout), self.EVENT_LOOP)
|
|
65
|
+
device = future.result()
|
|
50
66
|
if device is None:
|
|
51
67
|
raise BleakDeviceNotFoundError(ble)
|
|
52
68
|
self.client = BleakClient(device, **bleak_options)
|
|
@@ -63,26 +79,27 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
63
79
|
self.__timeout = self.DEFAULT_TIMEOUT
|
|
64
80
|
|
|
65
81
|
self.buffer = bytearray()
|
|
66
|
-
self.event_loop: asyncio.AbstractEventLoop = None
|
|
67
82
|
self.data_read_event: asyncio.Event = None
|
|
68
83
|
|
|
69
84
|
#Default to 20, will update on open
|
|
70
85
|
self.max_packet_size = 20
|
|
71
86
|
|
|
72
87
|
self.error_on_disconnect = error_on_disconnect
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#This behavior might be specific to Windows.
|
|
88
|
+
|
|
89
|
+
#is_connected is different from open. Open is the cached idea of whether the BLE connection should be active,
|
|
90
|
+
#while is_connected is the actual state of the connection. Basically, opened means a connection is desired, while
|
|
91
|
+
#is_connected means it is actually connected.
|
|
78
92
|
self.__opened = False
|
|
93
|
+
|
|
79
94
|
#client.is_connected is really slow (noticeable when called in bulk, which happens do to the assert_connected)...
|
|
80
95
|
#So instead using the disconnected callback and this variable to manage tracking the state without the delay
|
|
81
96
|
self.__connected = False
|
|
97
|
+
|
|
82
98
|
#Writing functions will naturally throw an exception if disconnected. Reading ones don't because they use notifications rather
|
|
83
99
|
#then direct reads. This means reading functions will need to assert the connection status but writing does not.
|
|
84
100
|
|
|
85
101
|
async def __async_open(self):
|
|
102
|
+
self.data_read_event = asyncio.Event()
|
|
86
103
|
await self.client.connect()
|
|
87
104
|
await self.client.start_notify(NORDIC_UART_TX_UUID, self.__on_data_received)
|
|
88
105
|
|
|
@@ -92,9 +109,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
92
109
|
if not self.__connected and self.error_on_disconnect:
|
|
93
110
|
self.close()
|
|
94
111
|
return
|
|
95
|
-
self.
|
|
96
|
-
self.data_read_event = asyncio.Event()
|
|
97
|
-
self.event_loop.run_until_complete(self.__async_open())
|
|
112
|
+
asyncio.run_coroutine_threadsafe(self.__async_open(), self.EVENT_LOOP).result()
|
|
98
113
|
self.max_packet_size = self.client.mtu_size - 3 #-3 to account for the opcode and attribute handle stored in the data packet
|
|
99
114
|
self.__opened = True
|
|
100
115
|
self.__connected = True
|
|
@@ -104,14 +119,12 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
104
119
|
#the disconnect call will hang on Windows. It seems similar to this issue: https://github.com/hbldh/bleak/issues/1359
|
|
105
120
|
await asyncio.sleep(0.5)
|
|
106
121
|
await self.client.disconnect()
|
|
122
|
+
self.data_read_event = None
|
|
123
|
+
self.buffer.clear()
|
|
107
124
|
|
|
108
125
|
def close(self):
|
|
109
126
|
if not self.__opened: return
|
|
110
|
-
|
|
111
|
-
self.buffer.clear()
|
|
112
|
-
self.event_loop.close()
|
|
113
|
-
self.event_loop = None
|
|
114
|
-
self.data_read_event = None
|
|
127
|
+
asyncio.run_coroutine_threadsafe(self.__async_close(), self.EVENT_LOOP).result()
|
|
115
128
|
self.__opened = False
|
|
116
129
|
|
|
117
130
|
def __on_disconnect(self, client: BleakClient):
|
|
@@ -125,28 +138,10 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
125
138
|
raise TssBLENoConnectionError(f"{self.name} is not connected")
|
|
126
139
|
|
|
127
140
|
def check_open(self):
|
|
128
|
-
#Checking this, while slow, isn't much difference in speed as allowing the disconnect callback to update via
|
|
129
|
-
#running the empty async function. So just going to use this here. Repeated calls to check_open are not a good
|
|
130
|
-
#idea from a speed perspective until a fix is found. We will probably make a version of this BLEComClass that uses
|
|
131
|
-
#a background thread for asyncio to allow for speed increases.
|
|
132
|
-
self.__connected = self.client.is_connected
|
|
133
141
|
if not self.__connected and self.__opened and self.error_on_disconnect:
|
|
134
142
|
self.close()
|
|
135
143
|
return self.__connected
|
|
136
144
|
|
|
137
|
-
#Bleak does run a thread to read data on notification after calling start_notify, however on notification
|
|
138
|
-
#it schedules a callback using loop.call_soon_threadsafe() so the actual notification can't happen unless we
|
|
139
|
-
#run the event loop. Therefore, this async function that does nothing is used just to trigger an event loop updated
|
|
140
|
-
#so the read callbacks __on_data_received can occur
|
|
141
|
-
@staticmethod
|
|
142
|
-
async def __wait_for_callbacks_async():
|
|
143
|
-
pass
|
|
144
|
-
|
|
145
|
-
def __read_all_data(self):
|
|
146
|
-
self.event_loop.run_until_complete(self.__wait_for_callbacks_async())
|
|
147
|
-
self.data_read_event.clear()
|
|
148
|
-
self.__assert_connected()
|
|
149
|
-
|
|
150
145
|
def __on_data_received(self, sender: BleakGATTCharacteristic, data: bytearray):
|
|
151
146
|
self.buffer += data
|
|
152
147
|
self.data_read_event.set()
|
|
@@ -155,7 +150,9 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
155
150
|
start_index = 0
|
|
156
151
|
while start_index < len(bytes):
|
|
157
152
|
end_index = min(len(bytes), start_index + self.max_packet_size) #Can only send max_packet_size data per call to write_gatt_char
|
|
158
|
-
|
|
153
|
+
asyncio.run_coroutine_threadsafe(
|
|
154
|
+
self.client.write_gatt_char(NORDIC_UART_RX_UUID, bytes[start_index:end_index], response=False),
|
|
155
|
+
self.EVENT_LOOP).result()
|
|
159
156
|
start_index = end_index
|
|
160
157
|
|
|
161
158
|
async def __await_read(self, timeout_time: int):
|
|
@@ -169,19 +166,19 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
169
166
|
return False
|
|
170
167
|
|
|
171
168
|
async def __await_num_bytes(self, num_bytes: int):
|
|
172
|
-
start_time = self.
|
|
173
|
-
while len(self.buffer) < num_bytes and self.
|
|
169
|
+
start_time = self.EVENT_LOOP.time()
|
|
170
|
+
while len(self.buffer) < num_bytes and self.EVENT_LOOP.time() - start_time < self.timeout:
|
|
174
171
|
await self.__await_read(start_time + self.timeout)
|
|
175
172
|
|
|
176
173
|
def read(self, num_bytes: int):
|
|
177
|
-
|
|
174
|
+
asyncio.run_coroutine_threadsafe(self.__await_num_bytes(num_bytes), self.EVENT_LOOP).result()
|
|
178
175
|
num_bytes = min(num_bytes, len(self.buffer))
|
|
179
176
|
data = self.buffer[:num_bytes]
|
|
180
177
|
del self.buffer[:num_bytes]
|
|
181
178
|
return data
|
|
182
179
|
|
|
183
180
|
def peek(self, num_bytes: int):
|
|
184
|
-
|
|
181
|
+
asyncio.run_coroutine_threadsafe(self.__await_num_bytes(num_bytes), self.EVENT_LOOP).result()
|
|
185
182
|
num_bytes = min(num_bytes, len(self.buffer))
|
|
186
183
|
data = self.buffer[:num_bytes]
|
|
187
184
|
return data
|
|
@@ -189,13 +186,13 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
189
186
|
#Reads until the pattern is received, max_length is exceeded, or timeout occurs
|
|
190
187
|
async def __await_pattern(self, pattern: bytes, max_length: int = None):
|
|
191
188
|
if max_length is None: max_length = float('inf')
|
|
192
|
-
start_time = self.
|
|
193
|
-
while pattern not in self.buffer and self.
|
|
189
|
+
start_time = self.EVENT_LOOP.time()
|
|
190
|
+
while pattern not in self.buffer and self.EVENT_LOOP.time() - start_time < self.timeout and len(self.buffer) < max_length:
|
|
194
191
|
await self.__await_read(start_time + self.timeout)
|
|
195
192
|
return pattern in self.buffer
|
|
196
193
|
|
|
197
194
|
def read_until(self, expected: bytes) -> bytes:
|
|
198
|
-
|
|
195
|
+
asyncio.run_coroutine_threadsafe(self.__await_pattern(expected), self.EVENT_LOOP).result()
|
|
199
196
|
if expected in self.buffer: #Found the pattern
|
|
200
197
|
length = self.buffer.index(expected) + len(expected)
|
|
201
198
|
result = self.buffer[:length]
|
|
@@ -207,7 +204,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
207
204
|
return result
|
|
208
205
|
|
|
209
206
|
def peek_until(self, expected: bytes, max_length: int = None) -> bytes:
|
|
210
|
-
|
|
207
|
+
asyncio.run_coroutine_threadsafe(self.__await_pattern(expected, max_length=max_length), self.EVENT_LOOP).result()
|
|
211
208
|
if expected in self.buffer:
|
|
212
209
|
length = self.buffer.index(expected) + len(expected)
|
|
213
210
|
else:
|
|
@@ -220,7 +217,6 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
220
217
|
|
|
221
218
|
@property
|
|
222
219
|
def length(self):
|
|
223
|
-
self.__read_all_data() #Gotta update the data before knowing the length
|
|
224
220
|
return len(self.buffer)
|
|
225
221
|
|
|
226
222
|
@property
|
|
@@ -236,7 +232,12 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
236
232
|
return False
|
|
237
233
|
|
|
238
234
|
@property
|
|
239
|
-
def name(self) -> str:
|
|
235
|
+
def name(self) -> str | None:
|
|
236
|
+
"""
|
|
237
|
+
The name of the device. This may be the Address or the Local Name of the device
|
|
238
|
+
depending on how discovery was done.
|
|
239
|
+
May also be None
|
|
240
|
+
"""
|
|
240
241
|
return self.__name
|
|
241
242
|
|
|
242
243
|
@property
|
|
@@ -244,7 +245,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
244
245
|
return self.client.address
|
|
245
246
|
|
|
246
247
|
SCANNER = None
|
|
247
|
-
|
|
248
|
+
SCANNER_LOCK = None
|
|
248
249
|
|
|
249
250
|
SCANNER_CONTINOUS = False #Controls if scanning will continously run
|
|
250
251
|
SCANNER_TIMEOUT = 5 #Controls the scanners timeout
|
|
@@ -256,13 +257,20 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
256
257
|
|
|
257
258
|
@classmethod
|
|
258
259
|
def __lazy_init_scanner(cls):
|
|
260
|
+
cls.__lazy_event_loop_init()
|
|
259
261
|
if cls.SCANNER is None:
|
|
260
|
-
cls.
|
|
261
|
-
|
|
262
|
+
cls.SCANNER_LOCK = threading.Lock()
|
|
263
|
+
#Scanner should be created inside of the Async Context that will use it
|
|
264
|
+
async def create_scanner():
|
|
265
|
+
cls.SCANNER = BleakScanner(detection_callback=cls.__detection_callback, service_uuids=[NORDIC_UART_SERVICE_UUID])
|
|
266
|
+
asyncio.run_coroutine_threadsafe(create_scanner(), cls.EVENT_LOOP).result()
|
|
267
|
+
|
|
268
|
+
|
|
262
269
|
|
|
263
270
|
@classmethod
|
|
264
271
|
def __detection_callback(cls, device: BLEDevice, adv: AdvertisementData):
|
|
265
|
-
cls.
|
|
272
|
+
with cls.SCANNER_LOCK:
|
|
273
|
+
cls.discovered_devices[device.address] = {"device": device, "adv": adv, "last_found": time.time()}
|
|
266
274
|
|
|
267
275
|
@classmethod
|
|
268
276
|
def set_scanner_continous(cls, continous: bool):
|
|
@@ -276,8 +284,10 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
276
284
|
"""
|
|
277
285
|
cls.__lazy_init_scanner()
|
|
278
286
|
cls.SCANNER_CONTINOUS = continous
|
|
279
|
-
if continous:
|
|
280
|
-
|
|
287
|
+
if continous:
|
|
288
|
+
asyncio.run_coroutine_threadsafe(cls.SCANNER.start(), cls.EVENT_LOOP).result()
|
|
289
|
+
else:
|
|
290
|
+
asyncio.run_coroutine_threadsafe(cls.SCANNER.stop(), cls.EVENT_LOOP).result()
|
|
281
291
|
|
|
282
292
|
@classmethod
|
|
283
293
|
def update_nearby_devices(cls):
|
|
@@ -286,36 +296,37 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
286
296
|
"""
|
|
287
297
|
cls.__lazy_init_scanner()
|
|
288
298
|
if cls.SCANNER_CONTINOUS:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
299
|
+
with cls.SCANNER_LOCK:
|
|
300
|
+
#Remove expired devices
|
|
301
|
+
cur_time = time.time()
|
|
302
|
+
to_remove = [] #Avoiding concurrent list modification
|
|
303
|
+
for device in cls.discovered_devices:
|
|
304
|
+
if cur_time - cls.discovered_devices[device]["last_found"] > cls.SCANNER_EXPIRATION_TIME:
|
|
305
|
+
to_remove.append(device)
|
|
306
|
+
for device in to_remove:
|
|
307
|
+
del cls.discovered_devices[device]
|
|
308
|
+
discovered = cls.discovered_devices.copy()
|
|
300
309
|
else:
|
|
301
310
|
#Mark all devices as invalid before searching for nearby devices
|
|
302
311
|
cls.discovered_devices.clear()
|
|
303
312
|
start_time = time.time()
|
|
304
313
|
end_time = cls.SCANNER_TIMEOUT or float('inf')
|
|
305
314
|
end_count = cls.SCANNER_FIND_COUNT or float('inf')
|
|
306
|
-
|
|
315
|
+
asyncio.run_coroutine_threadsafe(cls.SCANNER.start(), cls.EVENT_LOOP).result()
|
|
307
316
|
while time.time() - start_time < end_time and len(cls.discovered_devices) < end_count:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return
|
|
317
|
+
time.sleep(0)
|
|
318
|
+
asyncio.run_coroutine_threadsafe(cls.SCANNER.stop(), cls.EVENT_LOOP).result()
|
|
319
|
+
discovered = cls.discovered_devices.copy()
|
|
320
|
+
return discovered
|
|
312
321
|
|
|
313
322
|
@classmethod
|
|
314
323
|
def get_discovered_nearby_devices(cls):
|
|
315
324
|
"""
|
|
316
325
|
A helper to get a copy of the discovered devices
|
|
317
326
|
"""
|
|
318
|
-
|
|
327
|
+
with cls.SCANNER_LOCK:
|
|
328
|
+
discovered = cls.discovered_devices.copy()
|
|
329
|
+
return discovered
|
|
319
330
|
|
|
320
331
|
@staticmethod
|
|
321
332
|
def auto_detect() -> Generator["ThreespaceBLEComClass", None, None]:
|
|
@@ -325,5 +336,6 @@ class ThreespaceBLEComClass(ThreespaceComClass):
|
|
|
325
336
|
"""
|
|
326
337
|
cls = ThreespaceBLEComClass
|
|
327
338
|
cls.update_nearby_devices()
|
|
328
|
-
|
|
329
|
-
|
|
339
|
+
with cls.SCANNER_LOCK:
|
|
340
|
+
for device_info in cls.discovered_devices.values():
|
|
341
|
+
yield(ThreespaceBLEComClass(device_info["device"]))
|
|
@@ -100,7 +100,7 @@ class ThreespaceCommand:
|
|
|
100
100
|
response = response[size:]
|
|
101
101
|
else: #Strings are special, find the null terminator
|
|
102
102
|
str_len = response.index(0)
|
|
103
|
-
output.append(struct.unpack(f"<{str_len}s", response[str_len])[0])
|
|
103
|
+
output.append(struct.unpack(f"<{str_len}s", response[:str_len])[0])
|
|
104
104
|
response = response[str_len + 1:] #+1 to skip past the null terminator character too
|
|
105
105
|
else: #Fast parse because no strings
|
|
106
106
|
output.extend(struct.unpack(f"<{self.out_format}", response[:self.info.out_size]))
|
|
@@ -115,37 +115,42 @@ class ThreespaceCommand:
|
|
|
115
115
|
raw = bytearray([])
|
|
116
116
|
if self.info.num_out_params == 0: return None, raw
|
|
117
117
|
output = []
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
while byte != '\0':
|
|
139
|
-
string += byte
|
|
140
|
-
#Get next byte
|
|
141
|
-
response = com.read(1)
|
|
118
|
+
|
|
119
|
+
if not math.isnan(self.info.out_size):
|
|
120
|
+
#Fast read and parse
|
|
121
|
+
response = com.read(self.info.out_size)
|
|
122
|
+
raw += response
|
|
123
|
+
if len(response) != self.info.out_size:
|
|
124
|
+
if verbose:
|
|
125
|
+
print(f"Failed to read {self.info.name} {len(response)} / {self.info.out_size}. Aborting...")
|
|
126
|
+
output.extend(struct.unpack(f"<{self.out_format}", response))
|
|
127
|
+
else:
|
|
128
|
+
#There is a string, so go element by element
|
|
129
|
+
i = 0
|
|
130
|
+
while i < len(self.out_format):
|
|
131
|
+
c = self.out_format[i]
|
|
132
|
+
if c != 's':
|
|
133
|
+
end_index = self.out_format.find('s', i)
|
|
134
|
+
if end_index == -1: end_index = len(self.out_format)
|
|
135
|
+
format_str = f"<{self.out_format[i:end_index]}"
|
|
136
|
+
size = struct.calcsize(format_str)
|
|
137
|
+
response = com.read(size)
|
|
142
138
|
raw += response
|
|
143
|
-
if len(response) !=
|
|
139
|
+
if len(response) != size:
|
|
140
|
+
if verbose:
|
|
141
|
+
print(f"Failed to read {c} type. Aborting...")
|
|
142
|
+
return None
|
|
143
|
+
output.append(struct.unpack(format_str, response)[0])
|
|
144
|
+
i = end_index
|
|
145
|
+
else:
|
|
146
|
+
response = com.read_until(b'\0')
|
|
147
|
+
raw += response
|
|
148
|
+
if response[-1] != 0:
|
|
144
149
|
if verbose:
|
|
145
150
|
print(f"Failed to read string. Aborting...")
|
|
146
151
|
return None
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
output.append(response[:-1].decode())
|
|
153
|
+
i += 1
|
|
149
154
|
|
|
150
155
|
if self.info.num_out_params == 1:
|
|
151
156
|
return output[0], raw
|
|
@@ -162,7 +167,7 @@ class ThreespaceGetStreamingBatchCommand(ThreespaceCommand):
|
|
|
162
167
|
def set_stream_slots(self, streaming_slots: list[ThreespaceCommand]):
|
|
163
168
|
self.commands = streaming_slots
|
|
164
169
|
self.out_format = ''.join(slot.out_format for slot in streaming_slots if slot is not None)
|
|
165
|
-
self.info.out_size =
|
|
170
|
+
self.info.out_size = sum(slot.info.out_size for slot in streaming_slots if slot is not None)
|
|
166
171
|
|
|
167
172
|
def parse_response(self, response: bytes):
|
|
168
173
|
data = []
|
|
@@ -180,9 +185,8 @@ class ThreespaceGetStreamingBatchCommand(ThreespaceCommand):
|
|
|
180
185
|
raw_response = bytearray([])
|
|
181
186
|
for command in self.commands:
|
|
182
187
|
if command is None: continue
|
|
183
|
-
|
|
184
|
-
raw_response +=
|
|
185
|
-
out = command.parse_response(binary)
|
|
188
|
+
out, raw = command.read_command(com, verbose=verbose)
|
|
189
|
+
raw_response += raw
|
|
186
190
|
response.append(out)
|
|
187
191
|
|
|
188
192
|
return response, raw_response
|
|
@@ -393,6 +397,8 @@ class StreamableCommands(Enum):
|
|
|
393
397
|
GetCorrectedAccelVec = 55
|
|
394
398
|
GetCorrectedMagVec = 56
|
|
395
399
|
|
|
400
|
+
GetDateTime = 63
|
|
401
|
+
|
|
396
402
|
GetRawGyroRate = 65
|
|
397
403
|
GetRawAccelVec = 66
|
|
398
404
|
GetRawMagVec = 67
|
|
@@ -401,8 +407,10 @@ class StreamableCommands(Enum):
|
|
|
401
407
|
GetEeptsNewestStep = 71
|
|
402
408
|
GetEeptsNumStepsAvailable = 72
|
|
403
409
|
|
|
410
|
+
GetDateTimeString = 93
|
|
404
411
|
GetTimestamp = 94
|
|
405
412
|
|
|
413
|
+
GetBatteryCurrent = 200
|
|
406
414
|
GetBatteryVoltage = 201
|
|
407
415
|
GetBatteryPercent = 202
|
|
408
416
|
GetBatteryStatus = 203
|
|
@@ -609,14 +617,14 @@ class ThreespaceSensor:
|
|
|
609
617
|
self.streaming_packet_size = 0
|
|
610
618
|
self._force_stop_streaming()
|
|
611
619
|
|
|
612
|
-
#Now reinitialize the cached settings
|
|
613
|
-
self.__cache_header_settings()
|
|
614
|
-
self.__cache_streaming_settings()
|
|
615
|
-
|
|
616
620
|
self.__cache_serial_number(int(self.get_settings("serial_number"), 16))
|
|
617
621
|
self.__empty_debug_cache()
|
|
618
622
|
self.immediate_debug = int(self.get_settings("debug_mode")) == 1 #Needed for some startup processes when restarting
|
|
619
623
|
|
|
624
|
+
#Now reinitialize the cached settings
|
|
625
|
+
self.__cache_header_settings()
|
|
626
|
+
self.__cache_streaming_settings()
|
|
627
|
+
|
|
620
628
|
def __initialize_commands(self):
|
|
621
629
|
self.commands: list[ThreespaceCommand] = [None] * 256
|
|
622
630
|
self.getStreamingBatchCommand: ThreespaceGetStreamingBatchCommand = None
|
|
@@ -670,6 +678,9 @@ class ThreespaceSensor:
|
|
|
670
678
|
|
|
671
679
|
setattr(self, command.info.name, method)
|
|
672
680
|
|
|
681
|
+
def has_command(self, command: ThreespaceCommand):
|
|
682
|
+
return self.commands[command.info.num] is not None
|
|
683
|
+
|
|
673
684
|
def __get_command(self, command_name: str):
|
|
674
685
|
for command in self.commands:
|
|
675
686
|
if command is None: continue
|
|
@@ -1007,6 +1018,7 @@ class ThreespaceSensor:
|
|
|
1007
1018
|
header = ThreespaceHeader.from_bytes(header, self.header_info)
|
|
1008
1019
|
return header
|
|
1009
1020
|
|
|
1021
|
+
#TODO: max_data_length is not sufficient. Need MINIMUM length as well. Can have a situation where len = 0 and checksum = 0
|
|
1010
1022
|
def __peek_checksum(self, header: ThreespaceHeader, max_data_length=4096):
|
|
1011
1023
|
"""
|
|
1012
1024
|
Using a header that contains the checksum and data length, calculate the checksum of the expected
|
|
@@ -1100,7 +1112,7 @@ class ThreespaceSensor:
|
|
|
1100
1112
|
self.misaligned = False
|
|
1101
1113
|
return THREESPACE_UPDATE_COMMAND_PARSED
|
|
1102
1114
|
elif self.is_log_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
|
|
1103
|
-
if not blocking:
|
|
1115
|
+
if not blocking:
|
|
1104
1116
|
expected_output_size = len(header.raw_binary) + min(header.length, THREESPACE_LIVE_LOG_STREAM_MAX_PACKET_SIZE)
|
|
1105
1117
|
if self.com.length < expected_output_size: return THREESPACE_UPDATE_COMMAND_NOT_ENOUGH_DATA
|
|
1106
1118
|
if checksum_match := self.__peek_checksum(header, max_data_length=THREESPACE_LIVE_LOG_STREAM_MAX_PACKET_SIZE):
|
|
@@ -1204,9 +1216,9 @@ class ThreespaceSensor:
|
|
|
1204
1216
|
|
|
1205
1217
|
def startStreaming(self) -> ThreespaceCmdResult[None]: ...
|
|
1206
1218
|
def __startStreaming(self) -> ThreespaceCmdResult[None]:
|
|
1207
|
-
if self.is_data_streaming:
|
|
1208
|
-
|
|
1209
|
-
|
|
1219
|
+
if not self.is_data_streaming:
|
|
1220
|
+
self.streaming_packets.clear()
|
|
1221
|
+
self.__cache_streaming_settings()
|
|
1210
1222
|
|
|
1211
1223
|
result = self.execute_command(self.commands[THREESPACE_START_STREAMING_COMMAND_NUM])
|
|
1212
1224
|
self.is_data_streaming = True
|
|
@@ -1632,6 +1644,7 @@ class ThreespaceSensor:
|
|
|
1632
1644
|
def getCorrectedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
|
|
1633
1645
|
def enableMSC(self) -> ThreespaceCmdResult[None]: ...
|
|
1634
1646
|
def disableMSC(self) -> ThreespaceCmdResult[None]: ...
|
|
1647
|
+
def getDateTimeString(self) -> ThreespaceCmdResult[str]: ...
|
|
1635
1648
|
def getTimestamp(self) -> ThreespaceCmdResult[int]: ...
|
|
1636
1649
|
def getBatteryVoltage(self) -> ThreespaceCmdResult[float]: ...
|
|
1637
1650
|
def getBatteryPercent(self) -> ThreespaceCmdResult[int]: ...
|
|
@@ -1760,6 +1773,7 @@ _threespace_commands: list[ThreespaceCommand] = [
|
|
|
1760
1773
|
ThreespaceCommand("stopStreaming", THREESPACE_STOP_STREAMING_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__stopStreaming),
|
|
1761
1774
|
ThreespaceCommand("pauseLogStreaming", 87, "b", ""),
|
|
1762
1775
|
|
|
1776
|
+
ThreespaceCommand("getDateTimeString", 93, "", "S"),
|
|
1763
1777
|
ThreespaceCommand("getTimestamp", 94, "", "U"),
|
|
1764
1778
|
|
|
1765
1779
|
ThreespaceCommand("tareWithCurrentOrientation", 96, "", ""),
|
|
@@ -1788,6 +1802,7 @@ _threespace_commands: list[ThreespaceCommand] = [
|
|
|
1788
1802
|
ThreespaceCommand("fileStartStream", 180, "", "U", custom_func=ThreespaceSensor._ThreespaceSensor__fileStartStream),
|
|
1789
1803
|
ThreespaceCommand("fileStopStream", 181, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__fileStopStream),
|
|
1790
1804
|
|
|
1805
|
+
ThreespaceCommand("getBatteryCurrent", 200, "", "I"),
|
|
1791
1806
|
ThreespaceCommand("getBatteryVoltage", 201, "", "f"),
|
|
1792
1807
|
ThreespaceCommand("getBatteryPercent", 202, "", "b"),
|
|
1793
1808
|
ThreespaceCommand("getBatteryStatus", 203, "", "b"),
|
|
@@ -137,7 +137,7 @@ class ThreespaceBinaryParser:
|
|
|
137
137
|
if "read_size" not in kwargs:
|
|
138
138
|
raise ValueError("Missing arguement 'read_size' when registering the fileReadBytes command with the binary parser")
|
|
139
139
|
raise NotImplementedError("The fileReadBytes command has yet to be implemented for the ThreespaceBinaryParser")
|
|
140
|
-
elif cmd.info.num == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM:
|
|
140
|
+
elif cmd.info.num == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM and not isinstance(cmd, ThreespaceGetStreamingBatchCommand):
|
|
141
141
|
if "stream_slots" not in kwargs:
|
|
142
142
|
raise ValueError("Missing arguement 'stream_slots' when registering the getStreamingBatch command with the binary parser")
|
|
143
143
|
cmd = ThreespaceGetStreamingBatchCommand(kwargs['stream_slots'])
|
|
@@ -221,7 +221,6 @@ class ThreespaceBinaryParser:
|
|
|
221
221
|
return None
|
|
222
222
|
|
|
223
223
|
if self.header_info.checksum_enabled and not math.isnan(self.__parsing_msg_length): #Can validate checksum before parsing
|
|
224
|
-
print("Pre validating checksum")
|
|
225
224
|
if not self.__peek_checksum():
|
|
226
225
|
#Data corruption/Misalignment error
|
|
227
226
|
if self.verbose and not self.misaligned:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from yostlabs.tss3.api import ThreespaceSensor, StreamableCommands, ThreespaceCmdResult, threespaceGetHeaderLabels
|
|
1
|
+
from yostlabs.tss3.api import ThreespaceSensor, StreamableCommands, ThreespaceCmdResult, threespaceGetHeaderLabels, \
|
|
2
|
+
ThreespaceGetStreamingBatchCommand, threespaceCommandGet
|
|
2
3
|
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from typing import Any, Callable
|
|
@@ -17,6 +18,17 @@ class ThreespaceStreamingStatus(Enum):
|
|
|
17
18
|
#disable the streaming manager for some reason
|
|
18
19
|
Reset = 4
|
|
19
20
|
|
|
21
|
+
#Used for when the callback signature changed to allow user data, but maintain backwards compatability
|
|
22
|
+
def _api_compatible_callback(func: Callable):
|
|
23
|
+
arg_count = func.__code__.co_argcount
|
|
24
|
+
if hasattr(func, "__self__"): arg_count -= 1 #Do NOT count self
|
|
25
|
+
|
|
26
|
+
if arg_count == 1:
|
|
27
|
+
def without_userdata(status: ThreespaceStreamingStatus, user_data: Any):
|
|
28
|
+
return func(status)
|
|
29
|
+
return without_userdata
|
|
30
|
+
return func
|
|
31
|
+
|
|
20
32
|
from typing import NamedTuple
|
|
21
33
|
ThreespaceStreamingOption = NamedTuple("ThreespaceStreamingOption", [("cmd", StreamableCommands), ("param", int|None)])
|
|
22
34
|
class ThreespaceStreamingManager:
|
|
@@ -44,6 +56,7 @@ class ThreespaceStreamingManager:
|
|
|
44
56
|
hz: int = None
|
|
45
57
|
|
|
46
58
|
only_newest: bool = False
|
|
59
|
+
user_data: Any = None
|
|
47
60
|
|
|
48
61
|
@property
|
|
49
62
|
def interval(self):
|
|
@@ -73,9 +86,12 @@ class ThreespaceStreamingManager:
|
|
|
73
86
|
self.is_streaming = False #Store this seperately to attempt to allow using both the regular streaming and streaming manager via pausing and such
|
|
74
87
|
|
|
75
88
|
#Set the initial streaming speed
|
|
76
|
-
self.interval = int(self.sensor.get_settings("stream_interval"))
|
|
89
|
+
self.interval = int(self.sensor.get_settings("stream_interval"))
|
|
90
|
+
self.desired_interval = self.interval
|
|
77
91
|
|
|
78
|
-
|
|
92
|
+
#Registrations and update rate dirt are handled separately for some situations.
|
|
93
|
+
#Primarily, we want registration to succeed even if the update can't update the rate.
|
|
94
|
+
self.dirty_registrations = False
|
|
79
95
|
self.validated = False
|
|
80
96
|
|
|
81
97
|
#Control variable to manually control when updating happens here
|
|
@@ -86,6 +102,14 @@ class ThreespaceStreamingManager:
|
|
|
86
102
|
self.max_interval = 0xFFFFFFFF
|
|
87
103
|
self.min_interval = 1000000 / 2000
|
|
88
104
|
|
|
105
|
+
@property
|
|
106
|
+
def dirty(self):
|
|
107
|
+
return self.dirty_registrations or self.dirty_rate
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def dirty_rate(self):
|
|
111
|
+
return self.desired_interval != self.interval
|
|
112
|
+
|
|
89
113
|
@property
|
|
90
114
|
def paused(self):
|
|
91
115
|
return len(self.pausers) > 0
|
|
@@ -100,8 +124,8 @@ class ThreespaceStreamingManager:
|
|
|
100
124
|
self.pausers.add(locker)
|
|
101
125
|
if len(self.pausers) == 1 and self.is_streaming:
|
|
102
126
|
self.__stop_streaming()
|
|
103
|
-
for callback in self.callbacks:
|
|
104
|
-
callback(ThreespaceStreamingStatus.Paused)
|
|
127
|
+
for callback in self.callbacks.values():
|
|
128
|
+
callback.func(ThreespaceStreamingStatus.Paused, callback.user_data)
|
|
105
129
|
return True
|
|
106
130
|
|
|
107
131
|
def resume(self, locker: object):
|
|
@@ -112,8 +136,8 @@ class ThreespaceStreamingManager:
|
|
|
112
136
|
|
|
113
137
|
#Attempt to start again
|
|
114
138
|
if len(self.pausers) == 0:
|
|
115
|
-
for callback in self.callbacks:
|
|
116
|
-
callback(ThreespaceStreamingStatus.Resumed)
|
|
139
|
+
for callback in self.callbacks.values():
|
|
140
|
+
callback.func(ThreespaceStreamingStatus.Resumed, callback.user_data)
|
|
117
141
|
self.__apply_streaming_settings_and_update_state()
|
|
118
142
|
|
|
119
143
|
def lock_modifications(self, locker: object):
|
|
@@ -138,7 +162,7 @@ class ThreespaceStreamingManager:
|
|
|
138
162
|
self.block_updates = True
|
|
139
163
|
values = list(self.callbacks.values()) #To prevent concurrent dict modification, cache this
|
|
140
164
|
for cb in values:
|
|
141
|
-
cb.func(ThreespaceStreamingStatus.Reset)
|
|
165
|
+
cb.func(ThreespaceStreamingStatus.Reset, cb.user_data)
|
|
142
166
|
self.block_updates = False
|
|
143
167
|
self.lockers.clear()
|
|
144
168
|
self.pausers.clear()
|
|
@@ -170,18 +194,18 @@ class ThreespaceStreamingManager:
|
|
|
170
194
|
#Let all the callbacks know the data was updated
|
|
171
195
|
for cb in self.callbacks.values():
|
|
172
196
|
if cb.only_newest: continue
|
|
173
|
-
cb.func(ThreespaceStreamingStatus.Data)
|
|
197
|
+
cb.func(ThreespaceStreamingStatus.Data, cb.user_data)
|
|
174
198
|
|
|
175
199
|
result = self.sensor.getOldestStreamingPacket()
|
|
176
200
|
|
|
177
201
|
for cb in self.callbacks.values():
|
|
178
202
|
if cb.only_newest:
|
|
179
|
-
cb.func(ThreespaceStreamingStatus.Data)
|
|
180
|
-
cb.func(ThreespaceStreamingStatus.DataEnd)
|
|
203
|
+
cb.func(ThreespaceStreamingStatus.Data, cb.user_data)
|
|
204
|
+
cb.func(ThreespaceStreamingStatus.DataEnd, cb.user_data)
|
|
181
205
|
|
|
182
|
-
def register_callback(self, callback: Callable[[ThreespaceStreamingStatus],None], hz=None, only_newest=False):
|
|
206
|
+
def register_callback(self, callback: Callable[[ThreespaceStreamingStatus,Any],None], hz=None, only_newest=False, user_data=None):
|
|
183
207
|
if callback in self.callbacks: return
|
|
184
|
-
self.callbacks[callback] = ThreespaceStreamingManager.Callback(callback, hz, only_newest)
|
|
208
|
+
self.callbacks[callback] = ThreespaceStreamingManager.Callback(_api_compatible_callback(callback), hz, only_newest, user_data=user_data)
|
|
185
209
|
self.__update_streaming_speed()
|
|
186
210
|
|
|
187
211
|
def unregister_callback(self, callback: Callable[[ThreespaceStreamingStatus],None]):
|
|
@@ -217,7 +241,11 @@ class ThreespaceStreamingManager:
|
|
|
217
241
|
|
|
218
242
|
cmd.registrations.add(owner)
|
|
219
243
|
if immediate_update and self.dirty:
|
|
220
|
-
|
|
244
|
+
updated = self.__apply_streaming_settings_and_update_state()
|
|
245
|
+
if self.dirty_registrations:
|
|
246
|
+
return updated
|
|
247
|
+
else: #The rate was dirty, that does not affect the registration process
|
|
248
|
+
return True
|
|
221
249
|
return True
|
|
222
250
|
|
|
223
251
|
if self.locked: #Wasn't already registered, so don't allow new registrations
|
|
@@ -232,7 +260,7 @@ class ThreespaceStreamingManager:
|
|
|
232
260
|
self.registered_commands[info] = ThreespaceStreamingManager.Command(command, param=param, slot=num_commands_registered)
|
|
233
261
|
self.registered_commands[info].labels = self.sensor.getStreamingLabel(command.value).data
|
|
234
262
|
self.registered_commands[info].registrations.add(owner)
|
|
235
|
-
self.
|
|
263
|
+
self.dirty_registrations = True
|
|
236
264
|
if immediate_update:
|
|
237
265
|
return self.__apply_streaming_settings_and_update_state()
|
|
238
266
|
return True
|
|
@@ -266,7 +294,7 @@ class ThreespaceStreamingManager:
|
|
|
266
294
|
return
|
|
267
295
|
|
|
268
296
|
#Remove the command from streaming since nothing owns it anymore
|
|
269
|
-
self.
|
|
297
|
+
self.dirty_registrations = True
|
|
270
298
|
if immediate_update:
|
|
271
299
|
self.__apply_streaming_settings_and_update_state()
|
|
272
300
|
|
|
@@ -284,7 +312,7 @@ class ThreespaceStreamingManager:
|
|
|
284
312
|
if owner in registered_command.registrations:
|
|
285
313
|
registered_command.registrations.remove(owner)
|
|
286
314
|
if len(registered_command.registrations) == 0:
|
|
287
|
-
self.
|
|
315
|
+
self.dirty_registrations = True
|
|
288
316
|
|
|
289
317
|
if self.dirty and immediate_update:
|
|
290
318
|
self.__apply_streaming_settings_and_update_state()
|
|
@@ -327,7 +355,7 @@ class ThreespaceStreamingManager:
|
|
|
327
355
|
self.__stop_streaming()
|
|
328
356
|
|
|
329
357
|
#Clean up any registrations that need removed and activate any that need activated
|
|
330
|
-
if self.
|
|
358
|
+
if self.dirty_registrations:
|
|
331
359
|
to_remove = []
|
|
332
360
|
for k, v in self.registered_commands.items():
|
|
333
361
|
if len(v.registrations) == 0:
|
|
@@ -338,21 +366,24 @@ class ThreespaceStreamingManager:
|
|
|
338
366
|
del self.registered_commands[key]
|
|
339
367
|
if key in self.results:
|
|
340
368
|
del self.results[key]
|
|
341
|
-
self.
|
|
369
|
+
self.dirty_registrations = False
|
|
342
370
|
|
|
343
371
|
if self.num_commands_registered > 0:
|
|
344
372
|
slots_string = self.__build_stream_slots_string()
|
|
345
|
-
err, num_successes = self.sensor.set_settings(stream_slots=slots_string, stream_interval=self.
|
|
373
|
+
err, num_successes = self.sensor.set_settings(stream_slots=slots_string, stream_interval=self.desired_interval)
|
|
346
374
|
if err:
|
|
347
375
|
self.validated = False
|
|
348
376
|
return False
|
|
377
|
+
self.interval = self.desired_interval
|
|
349
378
|
if not self.paused and self.enabled:
|
|
350
379
|
self.__start_streaming() #Re-enable
|
|
351
380
|
|
|
352
381
|
self.validated = True
|
|
353
382
|
return True
|
|
354
383
|
|
|
355
|
-
def __update_streaming_speed(self):
|
|
384
|
+
def __update_streaming_speed(self):
|
|
385
|
+
if self.locked: return #Don't update the desired speed if modifications are locked
|
|
386
|
+
|
|
356
387
|
required_interval = None
|
|
357
388
|
for callback in self.callbacks.values():
|
|
358
389
|
if callback.interval is None: continue
|
|
@@ -360,13 +391,12 @@ class ThreespaceStreamingManager:
|
|
|
360
391
|
required_interval = callback.interval
|
|
361
392
|
|
|
362
393
|
if required_interval is None: #Treat required as current to make sure the current interval is still valid
|
|
363
|
-
required_interval = self.
|
|
394
|
+
required_interval = self.desired_interval
|
|
364
395
|
|
|
365
396
|
required_interval = min(self.max_interval, max(self.min_interval, required_interval))
|
|
366
|
-
if required_interval != self.
|
|
367
|
-
print(f"Updating
|
|
368
|
-
self.
|
|
369
|
-
self.dirty = True
|
|
397
|
+
if required_interval != self.desired_interval:
|
|
398
|
+
print(f"Updating desired speed from {1000000 / self.desired_interval}hz to {1000000 / required_interval}hz")
|
|
399
|
+
self.desired_interval = int(required_interval)
|
|
370
400
|
self.__apply_streaming_settings_and_update_state()
|
|
371
401
|
|
|
372
402
|
def __start_streaming(self):
|
|
@@ -450,4 +480,29 @@ class ThreespaceStreamingManager:
|
|
|
450
480
|
else:
|
|
451
481
|
slot_info.append(None)
|
|
452
482
|
|
|
453
|
-
return slot_info
|
|
483
|
+
return slot_info
|
|
484
|
+
|
|
485
|
+
#Utility functions
|
|
486
|
+
def get_stream_options_from_str(string: str):
|
|
487
|
+
options = []
|
|
488
|
+
slots = string.split(',')
|
|
489
|
+
for slot in slots:
|
|
490
|
+
slot = slot.split(':')
|
|
491
|
+
cmd = int(slot[0])
|
|
492
|
+
if cmd == 255: continue #Ignore 255
|
|
493
|
+
|
|
494
|
+
#Get the param if any
|
|
495
|
+
param = None
|
|
496
|
+
if len(slot) > 2:
|
|
497
|
+
raise Exception()
|
|
498
|
+
if len(slot) == 2:
|
|
499
|
+
param = int(slot[1])
|
|
500
|
+
|
|
501
|
+
stream_option = ThreespaceStreamingOption(StreamableCommands(cmd), param)
|
|
502
|
+
options.append(stream_option)
|
|
503
|
+
|
|
504
|
+
return options
|
|
505
|
+
|
|
506
|
+
def stream_options_to_command(options: list[ThreespaceStreamingOption]):
|
|
507
|
+
commands = [threespaceCommandGet(v.cmd.value) for v in options]
|
|
508
|
+
return ThreespaceGetStreamingBatchCommand(commands)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|