yostlabs 2025.1.3.4__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/tss3/utils.py ADDED
@@ -0,0 +1,671 @@
1
+ from .api import ThreespaceSensor, ThreespaceCommand, StreamableCommands, ThreespaceCmdResult, threespaceGetHeaderLabels
2
+ from dataclasses import dataclass, field
3
+ import copy
4
+ from typing import Any, Callable
5
+ from enum import Enum
6
+
7
+ import numpy as np
8
+ import yostlabs.tss3.quaternion as yl_math
9
+
10
+ class ThreespaceStreamingStatus(Enum):
11
+ Data = 0 #Normal data update
12
+ DataEnd = 1 #This is the last packet being sent for this data update. This allows the user to more efficently handle their callback.
13
+ #For example, if you have an expensive computation that needs done over all the data, but only once per frame, it would
14
+ #be preferable to buffer the data received via the callback and only do the computation when DataEnd is received
15
+ Paused = 2 #Streaming has been paused
16
+ Resumed = 3 #Streaming has been resumed
17
+
18
+ #Streaming manager is resetting. It is required that the callback unregisters everything is has registered
19
+ #This option is intended for shutdown purposes or in complex applications where the user needs to completely
20
+ #disable the streaming manager for some reason
21
+ Reset = 4
22
+
23
+ from typing import NamedTuple
24
+ ThreespaceStreamingOption = NamedTuple("ThreespaceStreamingOption", [("cmd", StreamableCommands), ("param", int|None)])
25
+ class ThreespaceStreamingManager:
26
+ """
27
+ A class that manages multiple clients wanting streamed data. Will update the streaming
28
+ slots and speed dynamically based on given requirements and allow those clients to
29
+ access that data without having to worry about which streaming slot is being used by what data.
30
+ """
31
+
32
+ @dataclass
33
+ class Command:
34
+ cmd: StreamableCommands = None
35
+ param: int = None
36
+
37
+ slot: int = None
38
+ registrations: set = field(default_factory=set, init=False)
39
+
40
+ active: bool = False #If not active then it must have been queued for addition, so it will be set active immediately
41
+
42
+ labels: str = None
43
+
44
+ @dataclass
45
+ class Callback:
46
+ func: Callable[[ThreespaceStreamingStatus],None] = None
47
+ hz: int = None
48
+
49
+ only_newest: bool = False
50
+
51
+ @property
52
+ def interval(self):
53
+ if self.hz is None: return None
54
+ return 1000000 // self.hz
55
+
56
+ def __init__(self, sensor: ThreespaceSensor):
57
+ self.sensor = sensor
58
+
59
+ 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
60
+ self.registered_commands: dict[tuple, ThreespaceStreamingManager.Command] = {}
61
+ self.slots: list[ThreespaceStreamingManager.Command] = [] #The same as self.registered_commands, but allow indexing based on slot instead
62
+
63
+ self.last_response: ThreespaceCmdResult|None = None
64
+ self.results: dict[tuple,Any] = {}
65
+
66
+ self.callbacks: dict[Callable, ThreespaceStreamingManager.Callback] = {}
67
+
68
+ #Objects currently pausing the streaming
69
+ self.pausers: set[object] = set()
70
+ self.lockers: set[object] = set()
71
+
72
+ #Keeps track of how many packets have been read. Useful for consumers to know if the values have been updated since they last read
73
+ self.sample_count = 0
74
+
75
+ self.enabled = False
76
+ self.is_streaming = False #Store this seperately to attempt to allow using both the regular streaming and streaming manager via pausing and such
77
+
78
+ #Set the initial streaming speed
79
+ self.interval = int(self.sensor.get_settings("stream_interval"))
80
+
81
+ self.dirty = False
82
+ self.validated = False
83
+
84
+ #Control variable to manually control when updating happens here
85
+ self.block_updates = False
86
+
87
+ #Using interval instead of HZ because more precise and the result of ?stream_hz may not be exactly equal to what is set
88
+ #However the functions for interfacing with these are still done in Hz
89
+ self.max_interval = 0xFFFFFFFF
90
+ self.min_interval = 1000000 / 2000
91
+
92
+ @property
93
+ def paused(self):
94
+ return len(self.pausers) > 0
95
+
96
+ @property
97
+ def locked(self):
98
+ return len(self.lockers) > 0
99
+
100
+ def pause(self, locker: object):
101
+ if locker in self.pausers: return True
102
+ if self.locked: return False
103
+ self.pausers.add(locker)
104
+ if len(self.pausers) == 1 and self.is_streaming:
105
+ self.__stop_streaming()
106
+ for callback in self.callbacks:
107
+ callback(ThreespaceStreamingStatus.Paused)
108
+ return True
109
+
110
+ def resume(self, locker: object):
111
+ try:
112
+ self.pausers.remove(locker)
113
+ except KeyError:
114
+ return
115
+
116
+ #Attempt to start again
117
+ if len(self.pausers) == 0:
118
+ for callback in self.callbacks:
119
+ callback(ThreespaceStreamingStatus.Resumed)
120
+ self.__apply_streaming_settings_and_update_state()
121
+
122
+ def lock_modifications(self, locker: object):
123
+ """
124
+ This still allows the streaming manager to operate and register new objects. However, registration
125
+ is limited to commands and speeds that are already operatable. Essentially, after this is called,
126
+ it is not possible to do actions that require updating the sensors onboard settings/state. This gurantees
127
+ streaming will not be stopped/restarted for time sensitive applications.
128
+ Note: This INCLUDES pausing/resuming, enabling/disabling...
129
+ If you need to lock modifications, then pause or resume. The locker should unlock modifications, call the necessary function, and then lock again
130
+ """
131
+ self.lockers.add(locker)
132
+
133
+ def unlock_modifications(self, locker: object):
134
+ if not locker in self.lockers: return
135
+ self.lockers.remove(locker)
136
+ if not self.locked and self.dirty:
137
+ self.__apply_streaming_settings_and_update_state()
138
+
139
+ def reset(self):
140
+ #Prevent the callbacks unregistrations from instantly taking effect regardless of if they pass immediate_update or not
141
+ self.block_updates = True
142
+ values = list(self.callbacks.values()) #To prevent concurrent dict modification, cache this
143
+ for cb in values:
144
+ cb.func(ThreespaceStreamingStatus.Reset)
145
+ self.block_updates = False
146
+ self.lockers.clear()
147
+ self.pausers.clear()
148
+ self.__apply_streaming_settings_and_update_state()
149
+ if self.num_commands_registered != 0:
150
+ raise RuntimeError(f"Failed to reset streaming manager. {self.num_commands_registered} commands still registered.\n {self.registered_commands}")
151
+ if self.num_callbacks_registered != 0:
152
+ raise RuntimeError(f"Failed to reset streaming manager. {self.num_callbacks_registered} callbacks still registered.\n {self.callbacks}")
153
+ return True
154
+
155
+ def update(self):
156
+ if self.paused or not self.enabled or not self.sensor.is_streaming: return
157
+
158
+ self.apply_updated_settings()
159
+ self.sensor.updateStreaming()
160
+ result = self.sensor.getOldestStreamingPacket()
161
+ if result is not None:
162
+ while result is not None:
163
+ self.sample_count += 1
164
+ self.last_response = result
165
+ slot_index = 0
166
+ for data in result.data:
167
+ while not self.slots[slot_index].active: slot_index += 1
168
+ cmd = self.slots[slot_index]
169
+ info = (cmd.cmd, cmd.param)
170
+ self.results[info] = data
171
+ slot_index += 1
172
+
173
+ #Let all the callbacks know the data was updated
174
+ for cb in self.callbacks.values():
175
+ if cb.only_newest: continue
176
+ cb.func(ThreespaceStreamingStatus.Data)
177
+
178
+ result = self.sensor.getOldestStreamingPacket()
179
+
180
+ for cb in self.callbacks.values():
181
+ if cb.only_newest:
182
+ cb.func(ThreespaceStreamingStatus.Data)
183
+ cb.func(ThreespaceStreamingStatus.DataEnd)
184
+
185
+ def register_callback(self, callback: Callable[[ThreespaceStreamingStatus],None], hz=None, only_newest=False):
186
+ if callback in self.callbacks: return
187
+ self.callbacks[callback] = ThreespaceStreamingManager.Callback(callback, hz, only_newest)
188
+ self.__update_streaming_speed()
189
+
190
+ def unregister_callback(self, callback: Callable[[ThreespaceStreamingStatus],None]):
191
+ if callback not in self.callbacks: return
192
+ del self.callbacks[callback]
193
+ self.__update_streaming_speed()
194
+
195
+ def register_command(self, owner: object, command: StreamableCommands|ThreespaceStreamingOption, param=None, immediate_update=True):
196
+ """
197
+ Adds the given command to the streaming slots and starts streaming it
198
+
199
+ Parameters
200
+ ----
201
+ owner : A reference to the object registering the command. A command is only unregistered after all its owners release it
202
+ command : The command to register
203
+ param : The parameter (if any) required for the command to be streamed. The command and param together identify a single slot
204
+ immediate_update : If true, the streaming manager will immediately change the streaming slots on the sensor. If doing bulk registers, it
205
+ is useful to set this as False until the last one for performance purposes.
206
+
207
+ Returns
208
+ -------
209
+ True : Successfully registered the command
210
+ False : Failed to register the command. Streaming slots are full
211
+ """
212
+ if isinstance(command, tuple):
213
+ param = command[1]
214
+ command = command[0]
215
+ info = (command, param)
216
+ if info in self.registered_commands: #Already registered, just add this as an owner
217
+ cmd = self.registered_commands[info]
218
+ if len(cmd.registrations) == 0 and self.num_commands_registered >= self.num_slots: #No room to register
219
+ return False
220
+
221
+ cmd.registrations.add(owner)
222
+ if immediate_update and self.dirty:
223
+ return self.__apply_streaming_settings_and_update_state()
224
+ return True
225
+
226
+ if self.locked: #Wasn't already registered, so don't allow new registrations
227
+ return False
228
+
229
+ #Make sure to only consider a command registered if it has registrations
230
+ num_commands_registered = self.num_commands_registered
231
+ if num_commands_registered >= self.num_slots: #No room to register
232
+ return False
233
+
234
+ #Register the command and add it to the streaming
235
+ self.registered_commands[info] = ThreespaceStreamingManager.Command(command, param=param, slot=num_commands_registered)
236
+ self.registered_commands[info].labels = self.sensor.getStreamingLabel(command.value).data
237
+ self.registered_commands[info].registrations.add(owner)
238
+ self.dirty = True
239
+ if immediate_update:
240
+ return self.__apply_streaming_settings_and_update_state()
241
+ return True
242
+
243
+ def unregister_command(self, owner: object, command: StreamableCommands|ThreespaceStreamingOption, param=None, immediate_update=True):
244
+ """
245
+ Removes the given command to the streaming slots and starts streaming it
246
+
247
+ Parameters
248
+ ----------
249
+ owner : A reference to the object unregistering the command. A command is only unregistered after all its owners release it
250
+ command : The command to unregister
251
+ param : The param (if any) required for the command
252
+ immediate_update : If true, the streaming manager will immediately change the streaming slots on the sensor. If doing bulk unregisters, it
253
+ is useful to set this as False until the last one for performance purposes.
254
+ """
255
+ if isinstance(command, tuple):
256
+ param = command[1]
257
+ command = command[0]
258
+ info = (command, param)
259
+ if info not in self.registered_commands:
260
+ return
261
+
262
+ try:
263
+ self.registered_commands[info].registrations.remove(owner)
264
+ except KeyError:
265
+ return #This owner wasn't registered to begin with, just ignore
266
+
267
+ #Nothing else to do
268
+ if len(self.registered_commands[info].registrations) != 0:
269
+ return
270
+
271
+ #Remove the command from streaming since nothing owns it anymore
272
+ self.dirty = True
273
+ if immediate_update:
274
+ self.__apply_streaming_settings_and_update_state()
275
+
276
+
277
+ def __build_stream_slots_string(self):
278
+ cmd_strings = []
279
+ self.slots.clear()
280
+ if self.num_commands_registered == 0: return "255"
281
+ i = 0
282
+ for cmd_key in self.registered_commands:
283
+ cmd = self.registered_commands[cmd_key]
284
+ if not cmd.active: continue #Skip non active registrations
285
+ self.slots.append(cmd)
286
+ cmd.slot = i
287
+ if cmd.param == None:
288
+ cmd_strings.append(str(cmd.cmd.value))
289
+ else:
290
+ cmd_strings.append(f"{cmd.cmd.value}:{cmd.param}")
291
+ i += 1
292
+ return ','.join(cmd_strings)
293
+
294
+ #More user friendly version of __apply_streaming_settings_and_update_state that prevents the user from calling it when not needed.
295
+ def apply_updated_settings(self):
296
+ """
297
+ This applys the current settings of the streaming manager and updates its state. This is normally done automatically, however
298
+ if the user is registering/unregistering with immediate_update turned off, this can be called to force the update.
299
+ """
300
+ if not self.dirty: return self.validated
301
+ return self.__apply_streaming_settings_and_update_state()
302
+
303
+ def __apply_streaming_settings_and_update_state(self, ignore_lock=False):
304
+ """
305
+ Used to apply the current configuration this manager represents to the streaming.
306
+ This involves disabling streaming if currently running
307
+ """
308
+ if self.block_updates or (self.locked and not ignore_lock):
309
+ return False
310
+
311
+ if self.sensor.is_streaming:
312
+ self.__stop_streaming()
313
+
314
+ #Clean up any registrations that need removed and activate any that need activated
315
+ if self.dirty:
316
+ to_remove = []
317
+ for k, v in self.registered_commands.items():
318
+ if len(v.registrations) == 0:
319
+ to_remove.append(k)
320
+ continue
321
+ v.active = True
322
+ for key in to_remove:
323
+ del self.registered_commands[key]
324
+ if key in self.results:
325
+ del self.results[key]
326
+ self.dirty = False
327
+
328
+ if self.num_commands_registered > 0:
329
+ slots_string = self.__build_stream_slots_string()
330
+ err, num_successes = self.sensor.set_settings(stream_slots=slots_string, stream_interval=self.interval)
331
+ if err:
332
+ self.validated = False
333
+ return False
334
+ if not self.paused and self.enabled:
335
+ self.__start_streaming() #Re-enable
336
+
337
+ self.validated = True
338
+ return True
339
+
340
+ def __update_streaming_speed(self):
341
+ required_interval = None
342
+ for callback in self.callbacks.values():
343
+ if callback.interval is None: continue
344
+ if required_interval is None or callback.interval < required_interval:
345
+ required_interval = callback.interval
346
+
347
+ if required_interval is None: #Treat required as current to make sure the current interval is still valid
348
+ required_interval = self.interval
349
+
350
+ required_interval = min(self.max_interval, max(self.min_interval, required_interval))
351
+ if required_interval != self.interval:
352
+ print(f"Updating streaming speed from {1000000 / self.interval}hz to {1000000 / required_interval}hz")
353
+ self.interval = int(required_interval)
354
+ self.dirty = True
355
+ self.__apply_streaming_settings_and_update_state()
356
+
357
+ def __start_streaming(self):
358
+ self.sensor.startStreaming()
359
+ self.is_streaming = True
360
+
361
+ def __stop_streaming(self):
362
+ self.sensor.stopStreaming()
363
+ self.is_streaming = False
364
+
365
+ @property
366
+ def num_commands_registered(self):
367
+ return len([v for v in self.registered_commands.values() if len(v.registrations) != 0])
368
+
369
+ @property
370
+ def num_callbacks_registered(self):
371
+ return len(self.callbacks)
372
+
373
+ def get_value(self, command: StreamableCommands|ThreespaceStreamingOption, param=None):
374
+ if isinstance(command, tuple):
375
+ param = command[1]
376
+ command = command[0]
377
+ return self.results.get((command, param), None)
378
+
379
+ def get_last_response(self):
380
+ return self.last_response
381
+
382
+ def get_header(self):
383
+ return self.last_response.header
384
+
385
+ def get_cmd_labels(self):
386
+ return ','.join(cmd.labels for cmd in self.registered_commands.values())
387
+
388
+ def get_header_labels(self):
389
+ order = threespaceGetHeaderLabels(self.sensor.header_info)
390
+ return ','.join(order)
391
+
392
+ def get_response_labels(self):
393
+ return ','.join([self.get_header_labels(), self.get_cmd_labels()])
394
+
395
+ def enable(self):
396
+ if self.enabled:
397
+ return
398
+ self.enabled = True
399
+ self.__apply_streaming_settings_and_update_state()
400
+
401
+ def disable(self):
402
+ if not self.enabled:
403
+ return
404
+ if self.is_streaming:
405
+ self.__stop_streaming()
406
+ self.enabled = False
407
+
408
+ def set_max_hz(self, hz: float):
409
+ if hz <= 0 or hz > 2000:
410
+ raise ValueError(f"Invalid streaming Hz {hz}")
411
+ self.min_interval = 1000000 // hz
412
+ self.__update_streaming_speed()
413
+
414
+ def set_min_hz(self, hz: float):
415
+ if hz <= 0 or hz > 2000:
416
+ raise ValueError(f"Invalid streaming Hz {hz}")
417
+ self.max_interval = 1000000 // hz
418
+ self.__update_streaming_speed()
419
+
420
+ def get_slots_from_sensor(self):
421
+ """
422
+ get a list containing the streaming information from the current sensor
423
+ """
424
+ slot_setting: str = self.sensor.get_settings("stream_slots")
425
+ slots = slot_setting.split(',')
426
+ slot_info = []
427
+ for slot in slots:
428
+ info = slot.split(':')
429
+ slot = int(info[0]) #Ignore parameters if any
430
+ param = None
431
+ if len(info) > 1:
432
+ param = int(info[1])
433
+ if slot != 255:
434
+ slot_info.append((slot, param))
435
+ else:
436
+ slot_info.append(None)
437
+
438
+ return slot_info
439
+
440
+ class ThreespaceGradientDescentCalibration:
441
+
442
+ @dataclass
443
+ class StageInfo:
444
+ start_vector: int
445
+ end_vector: int
446
+ stage: int
447
+ scale: float
448
+
449
+ count: int = 0
450
+
451
+ MAX_SCALE = 1000000000
452
+ MIN_SCALE = 1
453
+ STAGES = [
454
+ StageInfo(0, 6, 0, MAX_SCALE),
455
+ StageInfo(0, 12, 1, MAX_SCALE),
456
+ StageInfo(0, 24, 2, MAX_SCALE)
457
+ ]
458
+
459
+ #Note that each entry has a positive and negative vector included in this list
460
+ CHANGE_VECTORS = [
461
+ np.array([0,0,0,0,0,0,0,0,0,.0001,0,0], dtype=np.float64),
462
+ np.array([0,0,0,0,0,0,0,0,0,-.0001,0,0], dtype=np.float64),
463
+ np.array([0,0,0,0,0,0,0,0,0,0,.0001,0], dtype=np.float64),
464
+ np.array([0,0,0,0,0,0,0,0,0,0,-.0001,0], dtype=np.float64),
465
+ np.array([0,0,0,0,0,0,0,0,0,0,0,.0001], dtype=np.float64),
466
+ np.array([0,0,0,0,0,0,0,0,0,0,0,-.0001], dtype=np.float64), #First 6 only try to change the bias
467
+ np.array([.001,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
468
+ np.array([-.001,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
469
+ np.array([0,0,0,0,.001,0,0,0,0,0,0,0], dtype=np.float64),
470
+ np.array([0,0,0,0,-.001,0,0,0,0,0,0,0], dtype=np.float64),
471
+ np.array([0,0,0,0,0,0,0,0,.001,0,0,0], dtype=np.float64),
472
+ np.array([0,0,0,0,0,0,0,0,-.001,0,0,0], dtype=np.float64), #Next 6 only try to change the scale
473
+ np.array([0,.0001,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
474
+ np.array([0,-.0001,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
475
+ np.array([0,0,.0001,0,0,0,0,0,0,0,0,0], dtype=np.float64),
476
+ np.array([0,0,-.0001,0,0,0,0,0,0,0,0,0], dtype=np.float64),
477
+ np.array([0,0,0,.0001,0,0,0,0,0,0,0,0], dtype=np.float64),
478
+ np.array([0,0,0,-.0001,0,0,0,0,0,0,0,0], dtype=np.float64),
479
+ np.array([0,0,0,0,0,.0001,0,0,0,0,0,0], dtype=np.float64),
480
+ np.array([0,0,0,0,0,-.0001,0,0,0,0,0,0], dtype=np.float64),
481
+ np.array([0,0,0,0,0,0,.0001,0,0,0,0,0], dtype=np.float64),
482
+ np.array([0,0,0,0,0,0,-.0001,0,0,0,0,0], dtype=np.float64),
483
+ np.array([0,0,0,0,0,0,0,.0001,0,0,0,0], dtype=np.float64),
484
+ np.array([0,0,0,0,0,0,0,-.0001,0,0,0,0], dtype=np.float64), #Next 12 only try to change the shear
485
+ ]
486
+
487
+ def __init__(self, relative_sensor_orients: list[np.ndarray[float]], no_inverse=False):
488
+ """
489
+ Params
490
+ ------
491
+ relative_sensor_orients : The orientation of the sensor during which each sample is taken if it was tared as if pointing into the screen.
492
+ The inverse of these will be used to calculate where the axes should be located relative to the sensor
493
+ no_inverse : The relative_sensor_orients will be treated as the sample_rotations
494
+ """
495
+ if no_inverse:
496
+ self.rotation_quats = relative_sensor_orients
497
+ else:
498
+ self.rotation_quats = [np.array(yl_math.quat_inverse(orient)) for orient in relative_sensor_orients]
499
+
500
+ def apply_parameters(self, sample: np.ndarray[float], params: np.ndarray[float]):
501
+ bias = params[9:]
502
+ scale = params[:9]
503
+ scale = scale.reshape((3, 3))
504
+ return scale @ (sample + bias)
505
+
506
+ def rate_parameters(self, params: np.ndarray[float], samples: list[np.ndarray[float]], targets: list[np.ndarray[float]]):
507
+ total_error = 0
508
+ for i in range(len(samples)):
509
+ sample = samples[i]
510
+ target = targets[i]
511
+
512
+ sample = self.apply_parameters(sample, params)
513
+
514
+ error = target - sample
515
+ total_error += yl_math.vec_len(error)
516
+ return total_error
517
+
518
+ def generate_target_list(self, origin: np.ndarray):
519
+ targets = []
520
+ for orient in self.rotation_quats:
521
+ new_vec = np.array(yl_math.quat_rotate_vec(orient, origin), dtype=np.float64)
522
+ targets.append(new_vec)
523
+ return targets
524
+
525
+ def __get_stage(self, stage_number: int):
526
+ if stage_number >= len(self.STAGES):
527
+ return None
528
+ #Always get a shallow copy of the stage so can modify without removing the initial values
529
+ return copy.copy(self.STAGES[stage_number])
530
+
531
+ def calculate(self, samples: list[np.ndarray[float]], origin: np.ndarray[float], verbose=False, max_cycles_per_stage=1000):
532
+ targets = self.generate_target_list(origin)
533
+ initial_params = np.array([1,0,0,0,1,0,0,0,1,0,0,0], dtype=np.float64)
534
+ stage = self.__get_stage(0)
535
+
536
+ best_params = initial_params
537
+ best_rating = self.rate_parameters(best_params, samples, targets)
538
+ count = 0
539
+ while True:
540
+ last_best_rating = best_rating
541
+ params = best_params
542
+
543
+ #Apply all the changes to see if any improve the result
544
+ for change_index in range(stage.start_vector, stage.end_vector):
545
+ change_vector = self.CHANGE_VECTORS[change_index]
546
+ new_params = params + (change_vector * stage.scale)
547
+ rating = self.rate_parameters(new_params, samples, targets)
548
+
549
+ #A better rating, store it
550
+ if rating < best_rating:
551
+ best_params = new_params
552
+ best_rating = rating
553
+
554
+ if verbose and count % 100 == 0:
555
+ print(f"Round {count}: {best_rating=} {stage=}")
556
+
557
+ #Decide if need to go to the next stage or not
558
+ count += 1
559
+ stage.count += 1
560
+ if stage.count >= max_cycles_per_stage:
561
+ stage = self.__get_stage(stage.stage + 1)
562
+ if stage is None:
563
+ if verbose: print("Done from reaching count limit")
564
+ break
565
+ if verbose: print("Going to next stage from count limit")
566
+
567
+ if best_rating == last_best_rating: #The rating did not improve
568
+ if stage.scale == self.MIN_SCALE: #Go to the next stage since can't get any better in this stage!
569
+ stage = self.__get_stage(stage.stage + 1)
570
+ if stage is None:
571
+ if verbose: print("Done from exhaustion")
572
+ break
573
+ if verbose: print("Going to next stage from exhaustion")
574
+ else: #Reduce the size of the changes to hopefully get more accurate tuning
575
+ stage.scale *= 0.1
576
+ if stage.scale < self.MIN_SCALE:
577
+ stage.scale = self.MIN_SCALE
578
+ else: #Rating got better! To help avoid falling in a local minimum, increase the size of the change to see if that could make it better
579
+ stage.scale *= 1.1
580
+
581
+ if verbose:
582
+ print(f"Final Rating: {best_rating}")
583
+ print(f"Final Params: {best_params}")
584
+
585
+ return best_params
586
+
587
+ from xml.dom import minidom, Node
588
+ class ThreespaceFirmwareUploader:
589
+
590
+ def __init__(self, sensor: ThreespaceSensor, file_path: str = None, percentage_callback: Callable[[int],None] = None, verbose: bool = False):
591
+ self.sensor = sensor
592
+ self.set_firmware_path(file_path)
593
+ self.verbose = verbose
594
+
595
+ self.percent_complete = 0
596
+ self.callback = percentage_callback
597
+
598
+ def set_firmware_path(self, file_path: str):
599
+ if file_path is None:
600
+ self.firmware = None
601
+ return
602
+ self.firmware = minidom.parse(file_path)
603
+
604
+ def set_percent_callback(self, callback: Callable[[float],None]):
605
+ self.callback = callback
606
+
607
+ def set_verbose(self, verbose: bool):
608
+ self.verbose = verbose
609
+
610
+ def get_percent_done(self):
611
+ return self.percent_complete
612
+
613
+ def __set_percent_complete(self, percent: float):
614
+ self.percent_complete = percent
615
+ if self.callback:
616
+ self.callback(percent)
617
+
618
+ def log(self, *args):
619
+ if not self.verbose: return
620
+ print(*args)
621
+
622
+ def upload_firmware(self):
623
+ self.percent_complete = 0
624
+ if not self.sensor.in_bootloader:
625
+ self.sensor.enterBootloader()
626
+ self.__set_percent_complete(5)
627
+
628
+ boot_info = self.sensor.bootloader_get_info()
629
+
630
+ root = self.firmware.firstChild
631
+ for c in root.childNodes:
632
+ if c.nodeType == Node.ELEMENT_NODE:
633
+ name = c.nodeName
634
+ if name == "SetAddr":
635
+ self.log("Write S")
636
+ error = self.sensor.bootloader_erase_firmware()
637
+ if error:
638
+ self.log("Failed to erase firmware:", error)
639
+ else:
640
+ self.log("Successfully erased firmware")
641
+ self.__set_percent_complete(20)
642
+ elif name == "MemProgC":
643
+ mem = bytes.fromhex(c.firstChild.nodeValue)
644
+ self.log("Attempting to program", len(mem), "bytes to the chip")
645
+ cpos = 0
646
+ while cpos < len(mem):
647
+ memchunk = mem[cpos : min(len(mem), cpos + boot_info.pagesize)]
648
+ error = self.sensor.bootloader_prog_mem(memchunk)
649
+ if error:
650
+ self.log("Failed upload:", error)
651
+ else:
652
+ self.log("Wrote", len(memchunk), "bytes successfully to offset", cpos)
653
+ cpos += len(memchunk)
654
+ self.__set_percent_complete(20 + cpos / len(mem) * 79)
655
+ elif name == "Run":
656
+ self.log("Resetting with new firmware.")
657
+ self.sensor.bootloader_boot_firmware()
658
+ self.__set_percent_complete(100)
659
+
660
+ if __name__ == "__main__":
661
+ firmware_file = "D:\\svn\\trunk\\3Space\\TSS_3.0_Firmware\\Nuvoton_BASE_Project\\build\\Application.xml"
662
+ sensor = ThreespaceSensor()
663
+ sensor.set_settings(debug_level=0)
664
+ sensor.set_settings(cpu_speed=192000000, power_initial_hold_state=0)
665
+ sensor.commitSettings()
666
+
667
+ firmware_uploader = ThreespaceFirmwareUploader(firmware_file, verbose=True)
668
+ firmware_uploader.upload_firmware(sensor)
669
+
670
+ print(sensor.get_settings("version_firmware"))
671
+
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: yostlabs
3
+ Version: 2025.1.3.4
4
+ Summary: Python resources and API for 3Space sensors from Yost Labs Inc.
5
+ Project-URL: Homepage, https://yostlabs.com/
6
+ Author-email: "Yost Labs Inc." <techsupport@yostlabs.com>, Andy Riedlinger <techsupport@yostlabs.com>
7
+ License-File: LICENSE
8
+ Keywords: 3space,threespace,yost
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: numpy
15
+ Requires-Dist: pyserial
16
+ Description-Content-Type: text/markdown
17
+
18
+ Work In Progress