yostlabs 2025.1.16__py3-none-any.whl
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/__init__.py +0 -0
- yostlabs/communication/__init__.py +0 -0
- yostlabs/communication/base.py +90 -0
- yostlabs/communication/serial.py +141 -0
- yostlabs/math/__init__.py +1 -0
- yostlabs/math/quaternion.py +149 -0
- yostlabs/math/vector.py +31 -0
- yostlabs/tss3/__init__.py +0 -0
- yostlabs/tss3/api.py +1712 -0
- yostlabs/tss3/consts.py +24 -0
- yostlabs/tss3/eepts.py +228 -0
- yostlabs/tss3/utils/__init__.py +0 -0
- yostlabs/tss3/utils/calibration.py +153 -0
- yostlabs/tss3/utils/parser.py +256 -0
- yostlabs/tss3/utils/streaming.py +435 -0
- yostlabs/tss3/utils/version.py +76 -0
- yostlabs-2025.1.16.dist-info/METADATA +50 -0
- yostlabs-2025.1.16.dist-info/RECORD +20 -0
- yostlabs-2025.1.16.dist-info/WHEEL +4 -0
- yostlabs-2025.1.16.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
from yostlabs.tss3.api import ThreespaceSensor, StreamableCommands, ThreespaceCmdResult, threespaceGetHeaderLabels
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
class ThreespaceStreamingStatus(Enum):
|
|
8
|
+
Data = 0 #Normal data update
|
|
9
|
+
DataEnd = 1 #This is the last packet being sent for this data update. This allows the user to more efficently handle their callback.
|
|
10
|
+
#For example, if you have an expensive computation that needs done over all the data, but only once per frame, it would
|
|
11
|
+
#be preferable to buffer the data received via the callback and only do the computation when DataEnd is received
|
|
12
|
+
Paused = 2 #Streaming has been paused
|
|
13
|
+
Resumed = 3 #Streaming has been resumed
|
|
14
|
+
|
|
15
|
+
#Streaming manager is resetting. It is required that the callback unregisters everything is has registered
|
|
16
|
+
#This option is intended for shutdown purposes or in complex applications where the user needs to completely
|
|
17
|
+
#disable the streaming manager for some reason
|
|
18
|
+
Reset = 4
|
|
19
|
+
|
|
20
|
+
from typing import NamedTuple
|
|
21
|
+
ThreespaceStreamingOption = NamedTuple("ThreespaceStreamingOption", [("cmd", StreamableCommands), ("param", int|None)])
|
|
22
|
+
class ThreespaceStreamingManager:
|
|
23
|
+
"""
|
|
24
|
+
A class that manages multiple clients wanting streamed data. Will update the streaming
|
|
25
|
+
slots and speed dynamically based on given requirements and allow those clients to
|
|
26
|
+
access that data without having to worry about which streaming slot is being used by what data.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Command:
|
|
31
|
+
cmd: StreamableCommands = None
|
|
32
|
+
param: int = None
|
|
33
|
+
|
|
34
|
+
slot: int = None
|
|
35
|
+
registrations: set = field(default_factory=set, init=False)
|
|
36
|
+
|
|
37
|
+
active: bool = False #If not active then it must have been queued for addition, so it will be set active immediately
|
|
38
|
+
|
|
39
|
+
labels: str = None
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Callback:
|
|
43
|
+
func: Callable[[ThreespaceStreamingStatus],None] = None
|
|
44
|
+
hz: int = None
|
|
45
|
+
|
|
46
|
+
only_newest: bool = False
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def interval(self):
|
|
50
|
+
if self.hz is None: return None
|
|
51
|
+
return 1000000 // self.hz
|
|
52
|
+
|
|
53
|
+
def __init__(self, sensor: ThreespaceSensor):
|
|
54
|
+
self.sensor = sensor
|
|
55
|
+
|
|
56
|
+
self.num_slots = len(self.get_slots_from_sensor()) #This is just so if number of available slots ever changes, this changes to match it
|
|
57
|
+
self.registered_commands: dict[tuple, ThreespaceStreamingManager.Command] = {}
|
|
58
|
+
self.slots: list[ThreespaceStreamingManager.Command] = [] #The same as self.registered_commands, but allow indexing based on slot instead
|
|
59
|
+
|
|
60
|
+
self.last_response: ThreespaceCmdResult|None = None
|
|
61
|
+
self.results: dict[tuple,Any] = {}
|
|
62
|
+
|
|
63
|
+
self.callbacks: dict[Callable, ThreespaceStreamingManager.Callback] = {}
|
|
64
|
+
|
|
65
|
+
#Objects currently pausing the streaming
|
|
66
|
+
self.pausers: set[object] = set()
|
|
67
|
+
self.lockers: set[object] = set()
|
|
68
|
+
|
|
69
|
+
#Keeps track of how many packets have been read. Useful for consumers to know if the values have been updated since they last read
|
|
70
|
+
self.sample_count = 0
|
|
71
|
+
|
|
72
|
+
self.enabled = False
|
|
73
|
+
self.is_streaming = False #Store this seperately to attempt to allow using both the regular streaming and streaming manager via pausing and such
|
|
74
|
+
|
|
75
|
+
#Set the initial streaming speed
|
|
76
|
+
self.interval = int(self.sensor.get_settings("stream_interval"))
|
|
77
|
+
|
|
78
|
+
self.dirty = False
|
|
79
|
+
self.validated = False
|
|
80
|
+
|
|
81
|
+
#Control variable to manually control when updating happens here
|
|
82
|
+
self.block_updates = False
|
|
83
|
+
|
|
84
|
+
#Using interval instead of HZ because more precise and the result of ?stream_hz may not be exactly equal to what is set
|
|
85
|
+
#However the functions for interfacing with these are still done in Hz
|
|
86
|
+
self.max_interval = 0xFFFFFFFF
|
|
87
|
+
self.min_interval = 1000000 / 2000
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def paused(self):
|
|
91
|
+
return len(self.pausers) > 0
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def locked(self):
|
|
95
|
+
return len(self.lockers) > 0
|
|
96
|
+
|
|
97
|
+
def pause(self, locker: object):
|
|
98
|
+
if locker in self.pausers: return True
|
|
99
|
+
if self.locked: return False
|
|
100
|
+
self.pausers.add(locker)
|
|
101
|
+
if len(self.pausers) == 1 and self.is_streaming:
|
|
102
|
+
self.__stop_streaming()
|
|
103
|
+
for callback in self.callbacks:
|
|
104
|
+
callback(ThreespaceStreamingStatus.Paused)
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def resume(self, locker: object):
|
|
108
|
+
try:
|
|
109
|
+
self.pausers.remove(locker)
|
|
110
|
+
except KeyError:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
#Attempt to start again
|
|
114
|
+
if len(self.pausers) == 0:
|
|
115
|
+
for callback in self.callbacks:
|
|
116
|
+
callback(ThreespaceStreamingStatus.Resumed)
|
|
117
|
+
self.__apply_streaming_settings_and_update_state()
|
|
118
|
+
|
|
119
|
+
def lock_modifications(self, locker: object):
|
|
120
|
+
"""
|
|
121
|
+
This still allows the streaming manager to operate and register new objects. However, registration
|
|
122
|
+
is limited to commands and speeds that are already operatable. Essentially, after this is called,
|
|
123
|
+
it is not possible to do actions that require updating the sensors onboard settings/state. This gurantees
|
|
124
|
+
streaming will not be stopped/restarted for time sensitive applications.
|
|
125
|
+
Note: This INCLUDES pausing/resuming, enabling/disabling...
|
|
126
|
+
If you need to lock modifications, then pause or resume. The locker should unlock modifications, call the necessary function, and then lock again
|
|
127
|
+
"""
|
|
128
|
+
self.lockers.add(locker)
|
|
129
|
+
|
|
130
|
+
def unlock_modifications(self, locker: object):
|
|
131
|
+
if not locker in self.lockers: return
|
|
132
|
+
self.lockers.remove(locker)
|
|
133
|
+
if not self.locked and self.dirty:
|
|
134
|
+
self.__apply_streaming_settings_and_update_state()
|
|
135
|
+
|
|
136
|
+
def reset(self):
|
|
137
|
+
#Prevent the callbacks unregistrations from instantly taking effect regardless of if they pass immediate_update or not
|
|
138
|
+
self.block_updates = True
|
|
139
|
+
values = list(self.callbacks.values()) #To prevent concurrent dict modification, cache this
|
|
140
|
+
for cb in values:
|
|
141
|
+
cb.func(ThreespaceStreamingStatus.Reset)
|
|
142
|
+
self.block_updates = False
|
|
143
|
+
self.lockers.clear()
|
|
144
|
+
self.pausers.clear()
|
|
145
|
+
self.__apply_streaming_settings_and_update_state()
|
|
146
|
+
if self.num_commands_registered != 0:
|
|
147
|
+
raise RuntimeError(f"Failed to reset streaming manager. {self.num_commands_registered} commands still registered.\n {self.registered_commands}")
|
|
148
|
+
if self.num_callbacks_registered != 0:
|
|
149
|
+
raise RuntimeError(f"Failed to reset streaming manager. {self.num_callbacks_registered} callbacks still registered.\n {self.callbacks}")
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
def update(self):
|
|
153
|
+
if self.paused or not self.enabled or not self.sensor.is_streaming: return
|
|
154
|
+
|
|
155
|
+
self.apply_updated_settings()
|
|
156
|
+
self.sensor.updateStreaming()
|
|
157
|
+
result = self.sensor.getOldestStreamingPacket()
|
|
158
|
+
if result is not None:
|
|
159
|
+
while result is not None:
|
|
160
|
+
self.sample_count += 1
|
|
161
|
+
self.last_response = result
|
|
162
|
+
slot_index = 0
|
|
163
|
+
for data in result.data:
|
|
164
|
+
while not self.slots[slot_index].active: slot_index += 1
|
|
165
|
+
cmd = self.slots[slot_index]
|
|
166
|
+
info = (cmd.cmd, cmd.param)
|
|
167
|
+
self.results[info] = data
|
|
168
|
+
slot_index += 1
|
|
169
|
+
|
|
170
|
+
#Let all the callbacks know the data was updated
|
|
171
|
+
for cb in self.callbacks.values():
|
|
172
|
+
if cb.only_newest: continue
|
|
173
|
+
cb.func(ThreespaceStreamingStatus.Data)
|
|
174
|
+
|
|
175
|
+
result = self.sensor.getOldestStreamingPacket()
|
|
176
|
+
|
|
177
|
+
for cb in self.callbacks.values():
|
|
178
|
+
if cb.only_newest:
|
|
179
|
+
cb.func(ThreespaceStreamingStatus.Data)
|
|
180
|
+
cb.func(ThreespaceStreamingStatus.DataEnd)
|
|
181
|
+
|
|
182
|
+
def register_callback(self, callback: Callable[[ThreespaceStreamingStatus],None], hz=None, only_newest=False):
|
|
183
|
+
if callback in self.callbacks: return
|
|
184
|
+
self.callbacks[callback] = ThreespaceStreamingManager.Callback(callback, hz, only_newest)
|
|
185
|
+
self.__update_streaming_speed()
|
|
186
|
+
|
|
187
|
+
def unregister_callback(self, callback: Callable[[ThreespaceStreamingStatus],None]):
|
|
188
|
+
if callback not in self.callbacks: return
|
|
189
|
+
del self.callbacks[callback]
|
|
190
|
+
self.__update_streaming_speed()
|
|
191
|
+
|
|
192
|
+
def register_command(self, owner: object, command: StreamableCommands|ThreespaceStreamingOption, param=None, immediate_update=True):
|
|
193
|
+
"""
|
|
194
|
+
Adds the given command to the streaming slots and starts streaming it
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----
|
|
198
|
+
owner : A reference to the object registering the command. A command is only unregistered after all its owners release it
|
|
199
|
+
command : The command to register
|
|
200
|
+
param : The parameter (if any) required for the command to be streamed. The command and param together identify a single slot
|
|
201
|
+
immediate_update : If true, the streaming manager will immediately change the streaming slots on the sensor. If doing bulk registers, it
|
|
202
|
+
is useful to set this as False until the last one for performance purposes.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
True : Successfully registered the command
|
|
207
|
+
False : Failed to register the command. Streaming slots are full
|
|
208
|
+
"""
|
|
209
|
+
if isinstance(command, tuple):
|
|
210
|
+
param = command[1]
|
|
211
|
+
command = command[0]
|
|
212
|
+
info = (command, param)
|
|
213
|
+
if info in self.registered_commands: #Already registered, just add this as an owner
|
|
214
|
+
cmd = self.registered_commands[info]
|
|
215
|
+
if len(cmd.registrations) == 0 and self.num_commands_registered >= self.num_slots: #No room to register
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
cmd.registrations.add(owner)
|
|
219
|
+
if immediate_update and self.dirty:
|
|
220
|
+
return self.__apply_streaming_settings_and_update_state()
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
if self.locked: #Wasn't already registered, so don't allow new registrations
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
#Make sure to only consider a command registered if it has registrations
|
|
227
|
+
num_commands_registered = self.num_commands_registered
|
|
228
|
+
if num_commands_registered >= self.num_slots: #No room to register
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
#Register the command and add it to the streaming
|
|
232
|
+
self.registered_commands[info] = ThreespaceStreamingManager.Command(command, param=param, slot=num_commands_registered)
|
|
233
|
+
self.registered_commands[info].labels = self.sensor.getStreamingLabel(command.value).data
|
|
234
|
+
self.registered_commands[info].registrations.add(owner)
|
|
235
|
+
self.dirty = True
|
|
236
|
+
if immediate_update:
|
|
237
|
+
return self.__apply_streaming_settings_and_update_state()
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
def unregister_command(self, owner: object, command: StreamableCommands|ThreespaceStreamingOption, param=None, immediate_update=True):
|
|
241
|
+
"""
|
|
242
|
+
Removes the given command to the streaming slots and starts streaming it
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
owner : A reference to the object unregistering the command. A command is only unregistered after all its owners release it
|
|
247
|
+
command : The command to unregister
|
|
248
|
+
param : The param (if any) required for the command
|
|
249
|
+
immediate_update : If true, the streaming manager will immediately change the streaming slots on the sensor. If doing bulk unregisters, it
|
|
250
|
+
is useful to set this as False until the last one for performance purposes.
|
|
251
|
+
"""
|
|
252
|
+
if isinstance(command, tuple):
|
|
253
|
+
param = command[1]
|
|
254
|
+
command = command[0]
|
|
255
|
+
info = (command, param)
|
|
256
|
+
if info not in self.registered_commands:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
self.registered_commands[info].registrations.remove(owner)
|
|
261
|
+
except KeyError:
|
|
262
|
+
return #This owner wasn't registered to begin with, just ignore
|
|
263
|
+
|
|
264
|
+
#Nothing else to do
|
|
265
|
+
if len(self.registered_commands[info].registrations) != 0:
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
#Remove the command from streaming since nothing owns it anymore
|
|
269
|
+
self.dirty = True
|
|
270
|
+
if immediate_update:
|
|
271
|
+
self.__apply_streaming_settings_and_update_state()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def __build_stream_slots_string(self):
|
|
275
|
+
cmd_strings = []
|
|
276
|
+
self.slots.clear()
|
|
277
|
+
if self.num_commands_registered == 0: return "255"
|
|
278
|
+
i = 0
|
|
279
|
+
for cmd_key in self.registered_commands:
|
|
280
|
+
cmd = self.registered_commands[cmd_key]
|
|
281
|
+
if not cmd.active: continue #Skip non active registrations
|
|
282
|
+
self.slots.append(cmd)
|
|
283
|
+
cmd.slot = i
|
|
284
|
+
if cmd.param == None:
|
|
285
|
+
cmd_strings.append(str(cmd.cmd.value))
|
|
286
|
+
else:
|
|
287
|
+
cmd_strings.append(f"{cmd.cmd.value}:{cmd.param}")
|
|
288
|
+
i += 1
|
|
289
|
+
return ','.join(cmd_strings)
|
|
290
|
+
|
|
291
|
+
#More user friendly version of __apply_streaming_settings_and_update_state that prevents the user from calling it when not needed.
|
|
292
|
+
def apply_updated_settings(self):
|
|
293
|
+
"""
|
|
294
|
+
This applys the current settings of the streaming manager and updates its state. This is normally done automatically, however
|
|
295
|
+
if the user is registering/unregistering with immediate_update turned off, this can be called to force the update.
|
|
296
|
+
"""
|
|
297
|
+
if not self.dirty: return self.validated
|
|
298
|
+
return self.__apply_streaming_settings_and_update_state()
|
|
299
|
+
|
|
300
|
+
def __apply_streaming_settings_and_update_state(self, ignore_lock=False):
|
|
301
|
+
"""
|
|
302
|
+
Used to apply the current configuration this manager represents to the streaming.
|
|
303
|
+
This involves disabling streaming if currently running
|
|
304
|
+
"""
|
|
305
|
+
if self.block_updates or (self.locked and not ignore_lock):
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
if self.sensor.is_streaming:
|
|
309
|
+
self.__stop_streaming()
|
|
310
|
+
|
|
311
|
+
#Clean up any registrations that need removed and activate any that need activated
|
|
312
|
+
if self.dirty:
|
|
313
|
+
to_remove = []
|
|
314
|
+
for k, v in self.registered_commands.items():
|
|
315
|
+
if len(v.registrations) == 0:
|
|
316
|
+
to_remove.append(k)
|
|
317
|
+
continue
|
|
318
|
+
v.active = True
|
|
319
|
+
for key in to_remove:
|
|
320
|
+
del self.registered_commands[key]
|
|
321
|
+
if key in self.results:
|
|
322
|
+
del self.results[key]
|
|
323
|
+
self.dirty = False
|
|
324
|
+
|
|
325
|
+
if self.num_commands_registered > 0:
|
|
326
|
+
slots_string = self.__build_stream_slots_string()
|
|
327
|
+
err, num_successes = self.sensor.set_settings(stream_slots=slots_string, stream_interval=self.interval)
|
|
328
|
+
if err:
|
|
329
|
+
self.validated = False
|
|
330
|
+
return False
|
|
331
|
+
if not self.paused and self.enabled:
|
|
332
|
+
self.__start_streaming() #Re-enable
|
|
333
|
+
|
|
334
|
+
self.validated = True
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
def __update_streaming_speed(self):
|
|
338
|
+
required_interval = None
|
|
339
|
+
for callback in self.callbacks.values():
|
|
340
|
+
if callback.interval is None: continue
|
|
341
|
+
if required_interval is None or callback.interval < required_interval:
|
|
342
|
+
required_interval = callback.interval
|
|
343
|
+
|
|
344
|
+
if required_interval is None: #Treat required as current to make sure the current interval is still valid
|
|
345
|
+
required_interval = self.interval
|
|
346
|
+
|
|
347
|
+
required_interval = min(self.max_interval, max(self.min_interval, required_interval))
|
|
348
|
+
if required_interval != self.interval:
|
|
349
|
+
print(f"Updating streaming speed from {1000000 / self.interval}hz to {1000000 / required_interval}hz")
|
|
350
|
+
self.interval = int(required_interval)
|
|
351
|
+
self.dirty = True
|
|
352
|
+
self.__apply_streaming_settings_and_update_state()
|
|
353
|
+
|
|
354
|
+
def __start_streaming(self):
|
|
355
|
+
self.sensor.startStreaming()
|
|
356
|
+
self.is_streaming = True
|
|
357
|
+
|
|
358
|
+
def __stop_streaming(self):
|
|
359
|
+
self.sensor.stopStreaming()
|
|
360
|
+
self.is_streaming = False
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def num_commands_registered(self):
|
|
364
|
+
return len([v for v in self.registered_commands.values() if len(v.registrations) != 0])
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def num_callbacks_registered(self):
|
|
368
|
+
return len(self.callbacks)
|
|
369
|
+
|
|
370
|
+
def get_value(self, command: StreamableCommands|ThreespaceStreamingOption, param=None):
|
|
371
|
+
if isinstance(command, tuple):
|
|
372
|
+
param = command[1]
|
|
373
|
+
command = command[0]
|
|
374
|
+
return self.results.get((command, param), None)
|
|
375
|
+
|
|
376
|
+
def get_last_response(self):
|
|
377
|
+
return self.last_response
|
|
378
|
+
|
|
379
|
+
def get_header(self):
|
|
380
|
+
return self.last_response.header
|
|
381
|
+
|
|
382
|
+
def get_cmd_labels(self):
|
|
383
|
+
return ','.join(cmd.labels for cmd in self.registered_commands.values())
|
|
384
|
+
|
|
385
|
+
def get_header_labels(self):
|
|
386
|
+
order = threespaceGetHeaderLabels(self.sensor.header_info)
|
|
387
|
+
return ','.join(order)
|
|
388
|
+
|
|
389
|
+
def get_response_labels(self):
|
|
390
|
+
return ','.join([self.get_header_labels(), self.get_cmd_labels()])
|
|
391
|
+
|
|
392
|
+
def enable(self):
|
|
393
|
+
if self.enabled:
|
|
394
|
+
return
|
|
395
|
+
self.enabled = True
|
|
396
|
+
self.__apply_streaming_settings_and_update_state()
|
|
397
|
+
|
|
398
|
+
def disable(self):
|
|
399
|
+
if not self.enabled:
|
|
400
|
+
return
|
|
401
|
+
if self.is_streaming:
|
|
402
|
+
self.__stop_streaming()
|
|
403
|
+
self.enabled = False
|
|
404
|
+
|
|
405
|
+
def set_max_hz(self, hz: float):
|
|
406
|
+
if hz <= 0 or hz > 2000:
|
|
407
|
+
raise ValueError(f"Invalid streaming Hz {hz}")
|
|
408
|
+
self.min_interval = 1000000 // hz
|
|
409
|
+
self.__update_streaming_speed()
|
|
410
|
+
|
|
411
|
+
def set_min_hz(self, hz: float):
|
|
412
|
+
if hz <= 0 or hz > 2000:
|
|
413
|
+
raise ValueError(f"Invalid streaming Hz {hz}")
|
|
414
|
+
self.max_interval = 1000000 // hz
|
|
415
|
+
self.__update_streaming_speed()
|
|
416
|
+
|
|
417
|
+
def get_slots_from_sensor(self):
|
|
418
|
+
"""
|
|
419
|
+
get a list containing the streaming information from the current sensor
|
|
420
|
+
"""
|
|
421
|
+
slot_setting: str = self.sensor.get_settings("stream_slots")
|
|
422
|
+
slots = slot_setting.split(',')
|
|
423
|
+
slot_info = []
|
|
424
|
+
for slot in slots:
|
|
425
|
+
info = slot.split(':')
|
|
426
|
+
slot = int(info[0]) #Ignore parameters if any
|
|
427
|
+
param = None
|
|
428
|
+
if len(info) > 1:
|
|
429
|
+
param = int(info[1])
|
|
430
|
+
if slot != 255:
|
|
431
|
+
slot_info.append((slot, param))
|
|
432
|
+
else:
|
|
433
|
+
slot_info.append(None)
|
|
434
|
+
|
|
435
|
+
return slot_info
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from yostlabs.tss3.api import ThreespaceSensor
|
|
2
|
+
from xml.dom import minidom, Node
|
|
3
|
+
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
class ThreespaceFirmwareUploader:
|
|
7
|
+
|
|
8
|
+
def __init__(self, sensor: ThreespaceSensor, file_path: str = None, percentage_callback: Callable[[int],None] = None, verbose: bool = False):
|
|
9
|
+
self.sensor = sensor
|
|
10
|
+
self.set_firmware_path(file_path)
|
|
11
|
+
self.verbose = verbose
|
|
12
|
+
|
|
13
|
+
self.percent_complete = 0
|
|
14
|
+
self.callback = percentage_callback
|
|
15
|
+
|
|
16
|
+
def set_firmware_path(self, file_path: str):
|
|
17
|
+
if file_path is None:
|
|
18
|
+
self.firmware = None
|
|
19
|
+
return
|
|
20
|
+
self.firmware = minidom.parse(file_path)
|
|
21
|
+
|
|
22
|
+
def set_percent_callback(self, callback: Callable[[float],None]):
|
|
23
|
+
self.callback = callback
|
|
24
|
+
|
|
25
|
+
def set_verbose(self, verbose: bool):
|
|
26
|
+
self.verbose = verbose
|
|
27
|
+
|
|
28
|
+
def get_percent_done(self):
|
|
29
|
+
return self.percent_complete
|
|
30
|
+
|
|
31
|
+
def __set_percent_complete(self, percent: float):
|
|
32
|
+
self.percent_complete = percent
|
|
33
|
+
if self.callback:
|
|
34
|
+
self.callback(percent)
|
|
35
|
+
|
|
36
|
+
def log(self, *args):
|
|
37
|
+
if not self.verbose: return
|
|
38
|
+
print(*args)
|
|
39
|
+
|
|
40
|
+
def upload_firmware(self):
|
|
41
|
+
self.percent_complete = 0
|
|
42
|
+
if not self.sensor.in_bootloader:
|
|
43
|
+
self.sensor.enterBootloader()
|
|
44
|
+
self.__set_percent_complete(5)
|
|
45
|
+
|
|
46
|
+
boot_info = self.sensor.bootloader_get_info()
|
|
47
|
+
|
|
48
|
+
root = self.firmware.firstChild
|
|
49
|
+
for c in root.childNodes:
|
|
50
|
+
if c.nodeType == Node.ELEMENT_NODE:
|
|
51
|
+
name = c.nodeName
|
|
52
|
+
if name == "SetAddr":
|
|
53
|
+
self.log("Write S")
|
|
54
|
+
error = self.sensor.bootloader_erase_firmware()
|
|
55
|
+
if error:
|
|
56
|
+
self.log("Failed to erase firmware:", error)
|
|
57
|
+
else:
|
|
58
|
+
self.log("Successfully erased firmware")
|
|
59
|
+
self.__set_percent_complete(20)
|
|
60
|
+
elif name == "MemProgC":
|
|
61
|
+
mem = bytes.fromhex(c.firstChild.nodeValue)
|
|
62
|
+
self.log("Attempting to program", len(mem), "bytes to the chip")
|
|
63
|
+
cpos = 0
|
|
64
|
+
while cpos < len(mem):
|
|
65
|
+
memchunk = mem[cpos : min(len(mem), cpos + boot_info.pagesize)]
|
|
66
|
+
error = self.sensor.bootloader_prog_mem(memchunk)
|
|
67
|
+
if error:
|
|
68
|
+
self.log("Failed upload:", error)
|
|
69
|
+
else:
|
|
70
|
+
self.log("Wrote", len(memchunk), "bytes successfully to offset", cpos)
|
|
71
|
+
cpos += len(memchunk)
|
|
72
|
+
self.__set_percent_complete(20 + cpos / len(mem) * 79)
|
|
73
|
+
elif name == "Run":
|
|
74
|
+
self.log("Resetting with new firmware.")
|
|
75
|
+
self.sensor.bootloader_boot_firmware()
|
|
76
|
+
self.__set_percent_complete(100)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yostlabs
|
|
3
|
+
Version: 2025.1.16
|
|
4
|
+
Summary: Python resources and API for 3Space sensors from Yost Labs Inc.
|
|
5
|
+
Project-URL: Homepage, https://yostlabs.com/
|
|
6
|
+
Project-URL: Repository, https://github.com/YostLabs/3SpacePythonPackage/tree/main
|
|
7
|
+
Project-URL: Issues, https://github.com/YostLabs/3SpacePythonPackage/issues
|
|
8
|
+
Author-email: "Yost Labs Inc." <techsupport@yostlabs.com>, Andy Riedlinger <techsupport@yostlabs.com>
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: 3space,threespace,yost
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: pyserial
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
<center><h4>API and Resources for Yost Labs 3.0 Threespace sensors.</h4></center>
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
`python -m pip install yostlabs`
|
|
25
|
+
|
|
26
|
+
## Basic Usage
|
|
27
|
+
|
|
28
|
+
```Python
|
|
29
|
+
from yostlabs.tss3.api import ThreespaceSensor
|
|
30
|
+
|
|
31
|
+
#Will auto detect a 3-Space sensor connected to the machine via a USB connection
|
|
32
|
+
sensor = ThreespaceSensor()
|
|
33
|
+
|
|
34
|
+
result = sensor.getPrimaryCorrectedAccelVec()
|
|
35
|
+
print(result)
|
|
36
|
+
|
|
37
|
+
sensor.cleanup()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Click [here](https://github.com/YostLabs/3SpacePythonPackage/tree/main/Examples) for more examples.
|
|
41
|
+
|
|
42
|
+
## Communication
|
|
43
|
+
|
|
44
|
+
The ThreespaceSensor class utilizes a ThreespaceComClass to define the hardware communication interface between the device utlizing this API and the Threespace Sensor. Currently only the ThreespaceSerialComClass is available for use with the API. New ComClasses for different interfaces will be added to the [communication package](https://github.com/YostLabs/3SpacePythonPackage/tree/main/src/yostlabs/communication) in the future.
|
|
45
|
+
|
|
46
|
+
To create your own ThreespaceComClass, take a look at the necessary interface definitions [here](https://github.com/YostLabs/3SpacePythonPackage/blob/main/src/yostlabs/communication/base.py) and the Serial implementation [here](https://github.com/YostLabs/3SpacePythonPackage/blob/main/src/yostlabs/communication/serial.py).
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
WIP. Please review the example scripts. For further assistance contact techsupport@yostlabs.com.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
yostlabs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
yostlabs/communication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
yostlabs/communication/base.py,sha256=i82t4Kq3B8CpPqGNpGc737rXFul4dTkutXt5N5OYuQg,2807
|
|
4
|
+
yostlabs/communication/serial.py,sha256=cOv3CxloQo7sCjdAEUzkfZuZieqy-JbsW2Zwavc9LT8,5514
|
|
5
|
+
yostlabs/math/__init__.py,sha256=JFzsPQ4AbsX1AH1brBpn1c_Pa_ItF43__D3mlPvA2a4,34
|
|
6
|
+
yostlabs/math/quaternion.py,sha256=YyvbSrTPXGS8BsQJCn2tjdzIZ9WeDzfUe7dIDKeWsAM,4989
|
|
7
|
+
yostlabs/math/vector.py,sha256=CPtIxJXelCidGxTBrz6vZwLv-qXlccpAlYODaKJnWNw,991
|
|
8
|
+
yostlabs/tss3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
yostlabs/tss3/api.py,sha256=75TQ13UzP1_u14NwcoJLhWsITXfGPYhzZM4HF05fEPU,74084
|
|
10
|
+
yostlabs/tss3/consts.py,sha256=YzCMHlsYc193H9Qml0Q0f6i68boZJM55m3loWYwr7LA,903
|
|
11
|
+
yostlabs/tss3/eepts.py,sha256=7A7sCyOfDiJgw5Y9pGneg-5YgNvcfKtqeS9FoVWfJO8,9540
|
|
12
|
+
yostlabs/tss3/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
yostlabs/tss3/utils/calibration.py,sha256=42jCEzfTXoHuPZ4e-30N1ijOhkz9ld4PQnhX6AhTrZE,7069
|
|
14
|
+
yostlabs/tss3/utils/parser.py,sha256=thM5s70CZvehM5qP3AGVgHs6woeQM-wmA7hIcRO3MlY,11332
|
|
15
|
+
yostlabs/tss3/utils/streaming.py,sha256=218U29LmsLel42kd6g63Hi9XnovRqFVMofO0GEOAAA0,18990
|
|
16
|
+
yostlabs/tss3/utils/version.py,sha256=NT2H9l-oIRCYhV_yjf5UjkadoJQ0IN4eLl8y__pyTPc,3001
|
|
17
|
+
yostlabs-2025.1.16.dist-info/METADATA,sha256=sF23w23gw_zH3Iw_P15waWE0ZQu8mHrA8eRgbmAfsfs,2184
|
|
18
|
+
yostlabs-2025.1.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
19
|
+
yostlabs-2025.1.16.dist-info/licenses/LICENSE,sha256=PtF8EXRlVhm1_ve52_3GHixSPwMn0tGajFxF3xKS-j0,1090
|
|
20
|
+
yostlabs-2025.1.16.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yost Labs Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|