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/tss3/api.py ADDED
@@ -0,0 +1,1712 @@
1
+ from yostlabs.tss3.consts import *
2
+ from yostlabs.communication.base import ThreespaceInputStream, ThreespaceOutputStream, ThreespaceComClass
3
+ from yostlabs.communication.serial import ThreespaceSerialComClass
4
+
5
+ from enum import Enum
6
+ from dataclasses import dataclass, field
7
+ from typing import TypeVar, Generic
8
+ import struct
9
+ import types
10
+ import inspect
11
+ import time
12
+ import math
13
+
14
+
15
+ #For converting from internal format specifiers to struct module specifiers
16
+ __3space_format_conversion_dictionary = {
17
+ 'f': {"c": 'f', "size": 4},
18
+ 'd' : {"c": 'd', "size": 8},
19
+
20
+ 'b' : {"c": 'B', "size": 1},
21
+ 'B' : {"c": 'H', "size": 2},
22
+ "u" : {"c": 'L', "size": 4},
23
+ "U" : {"c": 'Q', "size": 8},
24
+
25
+ "i" : {"c": 'b', "size": 1},
26
+ "I" : {"c": 'h', "size": 2},
27
+ "l" : {"c": 'l', "size": 4},
28
+ "L" : {"c": 'q', "size": 8},
29
+
30
+ #Strings actually don't convert, they need handled special because
31
+ #struct unpack assumes static length strings, whereas the sensors
32
+ #use variable length null terminated strings
33
+ "s" : {"c": 's', "size": float('nan')},
34
+ "S" : {"c": 's', "size": float('nan')}
35
+ }
36
+
37
+ def _3space_format_get_size(format_str: str):
38
+ size = 0
39
+ for c in format_str:
40
+ size += __3space_format_conversion_dictionary[c]["size"]
41
+ return size
42
+
43
+ def _3space_format_to_external(format_str: str):
44
+ return ''.join(__3space_format_conversion_dictionary[c]['c'] for c in format_str)
45
+
46
+ @dataclass
47
+ class ThreespaceCommandInfo:
48
+ name: str
49
+ num: int
50
+ in_format: str
51
+ out_format: str
52
+
53
+ num_out_params: int = field(init=False)
54
+ out_size: int = field(init=False,)
55
+
56
+ def __post_init__(self):
57
+ self.num_out_params = len(self.out_format)
58
+ self.out_size = _3space_format_get_size(self.out_format)
59
+
60
+ class ThreespaceCommand:
61
+
62
+ BINARY_START_BYTE = 0xf7
63
+ BINARY_START_BYTE_HEADER = 0xf9
64
+
65
+ def __init__(self, name: str, num: int, in_format: str, out_format: str):
66
+ self.info = ThreespaceCommandInfo(name, num, in_format, out_format)
67
+ self.in_format = _3space_format_to_external(self.info.in_format)
68
+ self.out_format = _3space_format_to_external(self.info.out_format)
69
+
70
+ def format_cmd(self, *args, header_enabled=False):
71
+ cmd_data = struct.pack("<B", self.info.num)
72
+ for i, c in enumerate(self.in_format):
73
+ if c != 's':
74
+ cmd_data += struct.pack(f"<{c}", args[i])
75
+ else:
76
+ cmd_data += struct.pack(f"<{len(args[i])}sb", bytes(args[i], 'ascii'), 0)
77
+ checksum = sum(cmd_data) % 256
78
+ start_byte = ThreespaceCommand.BINARY_START_BYTE_HEADER if header_enabled else ThreespaceCommand.BINARY_START_BYTE
79
+ return struct.pack(f"<B{len(cmd_data)}sB", start_byte, cmd_data, checksum)
80
+
81
+ def send_command(self, com: ThreespaceOutputStream, *args, header_enabled = False):
82
+ cmd = self.format_cmd(*args, header_enabled=header_enabled)
83
+ com.write(cmd)
84
+
85
+ #Read the command result from an already read buffer. This will modify the given buffer to remove
86
+ #that data as well
87
+ def parse_response(self, response: bytes):
88
+ if self.info.num_out_params == 0: return None
89
+ output = []
90
+
91
+ if math.isnan(self.info.out_size): #Has strings in it, must slow parse
92
+ for c in self.out_format:
93
+ if c != 's':
94
+ format_str = f"<{c}"
95
+ size = struct.calcsize(format_str)
96
+ output.append(struct.unpack(format_str, response[:size])[0])
97
+ #TODO: Switch to using numpy views instead of slicing
98
+ response = response[size:]
99
+ else: #Strings are special, find the null terminator
100
+ str_len = response.index(0)
101
+ output.append(struct.unpack(f"<{str_len}s", response[str_len])[0])
102
+ response = response[str_len + 1:] #+1 to skip past the null terminator character too
103
+ else: #Fast parse because no strings
104
+ output.extend(struct.unpack(f"<{self.out_format}", response[:self.info.out_size]))
105
+
106
+
107
+ if self.info.num_out_params == 1:
108
+ return output[0]
109
+ return output
110
+
111
+ #Read the command dynamically from an input stream
112
+ def read_command(self, com: ThreespaceInputStream):
113
+ raw = bytearray([])
114
+ if self.info.num_out_params == 0: return None, raw
115
+ output = []
116
+ for c in self.out_format:
117
+ if c != 's':
118
+ format_str = f"<{c}"
119
+ size = struct.calcsize(format_str)
120
+ response = com.read(size)
121
+ raw += response
122
+ if len(response) != size:
123
+ print(f"Failed to read {c} type. Aborting...")
124
+ return None
125
+ output.append(struct.unpack(format_str, response)[0])
126
+ else: #Strings are special, find the null terminator
127
+ response = com.read(1)
128
+ raw += response
129
+ if len(response) != 1:
130
+ print(f"Failed to read string. Aborting...")
131
+ return None
132
+ byte = chr(response[0])
133
+ string = ""
134
+ while byte != '\0':
135
+ string += byte
136
+ #Get next byte
137
+ response = com.read(1)
138
+ raw += response
139
+ if len(response) != 1:
140
+ print(f"Failed to read string. Aborting...")
141
+ return None
142
+ byte = chr(response[0])
143
+ output.append(string)
144
+
145
+ if self.info.num_out_params == 1:
146
+ return output[0], raw
147
+ return output, raw
148
+
149
+ class ThreespaceGetStreamingBatchCommand(ThreespaceCommand):
150
+
151
+ def __init__(self, streaming_slots: list[ThreespaceCommand]):
152
+ self.commands = streaming_slots
153
+ combined_out_format = ''.join(slot.info.out_format for slot in streaming_slots if slot is not None)
154
+ super().__init__("getStreamingBatch", THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM, "", combined_out_format)
155
+ self.out_format = ''.join(slot.out_format for slot in streaming_slots if slot is not None)
156
+
157
+ def set_stream_slots(self, streaming_slots: list[ThreespaceCommand]):
158
+ self.commands = streaming_slots
159
+ self.out_format = ''.join(slot.out_format for slot in streaming_slots if slot is not None)
160
+
161
+ def parse_response(self, response: bytes):
162
+ data = []
163
+ for command in self.commands:
164
+ if command is None: continue
165
+ cmd_response_size = command.info.out_size
166
+ data.append(command.parse_response(response))
167
+ response = response[cmd_response_size:]
168
+
169
+ return data
170
+
171
+ def read_command(self, com: ThreespaceInputStream):
172
+ #Get the response to all the streaming commands
173
+ response = []
174
+ raw_response = bytearray([])
175
+ for command in self.commands:
176
+ if command is None: continue
177
+ binary = com.read(command.info.out_size)
178
+ raw_response += binary
179
+ out = command.parse_response(binary)
180
+ response.append(out)
181
+
182
+ return response, raw_response
183
+
184
+ THREESPACE_HEADER_FORMAT_CHARS = ['b', 'L', 'B', 'B', 'L', 'H']
185
+
186
+ @dataclass
187
+ class ThreespaceHeaderInfo:
188
+ __bitfield: int = 0
189
+ format: str = ""
190
+ size: int = 0
191
+
192
+ def get_start_byte(self, header_field: int):
193
+ """
194
+ Given a header field, give the initial byte offset for that field when
195
+ using binary mode
196
+ """
197
+ if not header_field & self.__bitfield: return None #The bit is not enabled, no start byte
198
+ #Get the index of the bit
199
+ bit_pos = 0
200
+ header_field >>= 1
201
+ while header_field > 0:
202
+ bit_pos += 1
203
+ header_field >>= 1
204
+
205
+ #Add up the size of everything before this field
206
+ start = 0
207
+ for i in range(bit_pos):
208
+ if (1 << i) & self.__bitfield:
209
+ start += struct.calcsize(THREESPACE_HEADER_FORMAT_CHARS[i])
210
+ return start
211
+
212
+ def get_index(self, header_field: int):
213
+ if not header_field & self.__bitfield: return None
214
+ index = 0
215
+ bit = 1
216
+ while bit < header_field:
217
+ if bit & self.__bitfield:
218
+ index += 1
219
+ bit <<= 1
220
+ return index
221
+
222
+ def __update(self):
223
+ self.format = "<"
224
+ for i in range(THREESPACE_HEADER_NUM_BITS):
225
+ if self.__bitfield & (1 << i):
226
+ self.format += THREESPACE_HEADER_FORMAT_CHARS[i]
227
+ self.size = struct.calcsize(self.format)
228
+
229
+ @property
230
+ def bitfield(self):
231
+ return self.__bitfield
232
+
233
+ @bitfield.setter
234
+ def bitfield(self, value):
235
+ self.__bitfield = value
236
+ self.__update()
237
+
238
+ @property
239
+ def status_enabled(self):
240
+ return bool(self.__bitfield & THREESPACE_HEADER_STATUS_BIT)
241
+
242
+ @status_enabled.setter
243
+ def status_enabled(self, value: bool):
244
+ if value: self.__bitfield |= THREESPACE_HEADER_STATUS_BIT
245
+ else: self.__bitfield &= ~THREESPACE_HEADER_STATUS_BIT
246
+ self.__update()
247
+
248
+ @property
249
+ def timestamp_enabled(self):
250
+ return bool(self.__bitfield & THREESPACE_HEADER_TIMESTAMP_BIT)
251
+
252
+ @timestamp_enabled.setter
253
+ def timestamp_enabled(self, value: bool):
254
+ if value: self.__bitfield |= THREESPACE_HEADER_TIMESTAMP_BIT
255
+ else: self.__bitfield &= ~THREESPACE_HEADER_TIMESTAMP_BIT
256
+ self.__update()
257
+
258
+ @property
259
+ def echo_enabled(self):
260
+ return bool(self.__bitfield & THREESPACE_HEADER_ECHO_BIT)
261
+
262
+ @echo_enabled.setter
263
+ def echo_enabled(self, value: bool):
264
+ if value: self.__bitfield |= THREESPACE_HEADER_ECHO_BIT
265
+ else: self.__bitfield &= ~THREESPACE_HEADER_ECHO_BIT
266
+ self.__update()
267
+
268
+ @property
269
+ def checksum_enabled(self):
270
+ return bool(self.__bitfield & THREESPACE_HEADER_CHECKSUM_BIT)
271
+
272
+ @checksum_enabled.setter
273
+ def checksum_enabled(self, value: bool):
274
+ if value: self.__bitfield |= THREESPACE_HEADER_CHECKSUM_BIT
275
+ else: self.__bitfield &= ~THREESPACE_HEADER_CHECKSUM_BIT
276
+ self.__update()
277
+
278
+ @property
279
+ def serial_enabled(self):
280
+ return bool(self.__bitfield & THREESPACE_HEADER_SERIAL_BIT)
281
+
282
+ @serial_enabled.setter
283
+ def serial_enabled(self, value: bool):
284
+ if value: self.__bitfield |= THREESPACE_HEADER_SERIAL_BIT
285
+ else: self.__bitfield &= ~THREESPACE_HEADER_SERIAL_BIT
286
+ self.__update()
287
+
288
+ @property
289
+ def length_enabled(self):
290
+ return bool(self.__bitfield & THREESPACE_HEADER_LENGTH_BIT)
291
+
292
+ @length_enabled.setter
293
+ def length_enabled(self, value: bool):
294
+ if value: self.__bitfield |= THREESPACE_HEADER_LENGTH_BIT
295
+ else: self.__bitfield &= ~THREESPACE_HEADER_LENGTH_BIT
296
+ self.__update()
297
+
298
+
299
+ @dataclass
300
+ class ThreespaceHeader:
301
+ raw: tuple = field(default=None, repr=False)
302
+
303
+ #Order here matters
304
+ status: int = None
305
+ timestamp: int = None
306
+ echo: int = None
307
+ checksum: int = None
308
+ serial: int = None
309
+ length: int = None
310
+
311
+ raw_binary: bytes = field(repr=False, default_factory=lambda: bytes([]))
312
+ info: ThreespaceHeaderInfo = field(default_factory=lambda: ThreespaceHeaderInfo(), repr=False)
313
+
314
+ @staticmethod
315
+ def from_tuple(data, info: ThreespaceHeaderInfo):
316
+ raw_expanded = []
317
+ cur_index = 0
318
+ for i in range(THREESPACE_HEADER_NUM_BITS):
319
+ if info.bitfield & (1 << i):
320
+ raw_expanded.append(data[cur_index])
321
+ cur_index += 1
322
+ else:
323
+ raw_expanded.append(None)
324
+ return ThreespaceHeader(data, *raw_expanded, info=info)
325
+
326
+ @staticmethod
327
+ def from_bytes(byte_data: bytes, info: ThreespaceHeaderInfo):
328
+ if info.size == 0: return ThreespaceHeader()
329
+ header = ThreespaceHeader.from_tuple(struct.unpack(info.format, byte_data[:info.size]), info)
330
+ header.raw_binary = byte_data
331
+ return header
332
+
333
+ def __getitem__(self, key):
334
+ return self.raw[key]
335
+
336
+ def __len__(self):
337
+ return len(self.raw)
338
+
339
+ def __iter__(self):
340
+ return iter(self.raw)
341
+
342
+ class StreamableCommands(Enum):
343
+ GetTaredOrientation = 0
344
+ GetTaredOrientationAsEuler = 1
345
+ GetTaredOrientationAsMatrix = 2
346
+ GetTaredOrientationAsAxisAngle = 3
347
+ GetTaredOrientationAsTwoVector = 4
348
+
349
+ GetDifferenceQuaternion = 5
350
+
351
+ GetUntaredOrientation = 6
352
+ GetUntaredOrientationAsEuler = 7
353
+ GetUntaredOrientationAsMatrix = 8
354
+ GetUntaredOrientationAsAxisAngle = 9
355
+ GetUntaredOrientationAsTwoVector = 10
356
+
357
+ GetTaredOrientationAsTwoVectorSensorFrame = 11
358
+ GetUntaredOrientationAsTwoVectorSensorFrame = 12
359
+
360
+ GetPrimaryBarometerPressure = 13
361
+ GetPrimaryBarometerAltitude = 14
362
+ GetBarometerAltitudeById = 15
363
+ GetBarometerPressureById = 16
364
+
365
+ GetAllPrimaryNormalizedData = 32
366
+ GetPrimaryNormalizedGyroRate = 33
367
+ GetPrimaryNormalizedAccelVec = 34
368
+ GetPrimaryNormalizedMagVec = 35
369
+
370
+ GetAllPrimaryCorrectedData = 37
371
+ GetPrimaryCorrectedGyroRate = 38
372
+ GetPrimaryCorrectedAccelVec = 39
373
+ GetPrimaryCorrectedMagVec = 40
374
+
375
+ GetPrimaryGlobalLinearAccel = 41
376
+ GetPrimaryLocalLinearAccel = 42
377
+
378
+ GetTemperatureCelsius = 43
379
+ GetTemperatureFahrenheit = 44
380
+ GetMotionlessConfidenceFactor = 45
381
+
382
+ GetNormalizedGyroRate = 51
383
+ GetNormalizedAccelVec = 52
384
+ GetNormalizedMagVec = 53
385
+
386
+ GetCorrectedGyroRate = 54
387
+ GetCorrectedAccelVec = 55
388
+ GetCorrectedMagVec = 56
389
+
390
+ GetRawGyroRate = 65
391
+ GetRawAccelVec = 66
392
+ GetRawMagVec = 67
393
+
394
+ GetEeptsOldestStep = 70
395
+ GetEeptsNewestStep = 71
396
+ GetEeptsNumStepsAvailable = 72
397
+
398
+ GetTimestamp = 94
399
+
400
+ GetBatteryVoltage = 201
401
+ GetBatteryPercent = 202
402
+ GetBatteryStatus = 203
403
+
404
+ GetGpsCoord = 215
405
+ GetGpsAltitude = 216
406
+ GetGpsFixState = 217
407
+ GetGpsHdop = 218
408
+ GetGpsSattelites = 219
409
+
410
+ GetButtonState = 250
411
+
412
+ THREESPACE_AWAIT_COMMAND_FOUND = 0
413
+ THREESPACE_AWAIT_COMMAND_TIMEOUT = 1
414
+
415
+ T = TypeVar('T')
416
+
417
+ @dataclass
418
+ class ThreespaceCmdResult(Generic[T]):
419
+ raw: tuple = field(default=None, repr=False)
420
+
421
+ header: ThreespaceHeader = None
422
+ data: T = None
423
+ raw_data: bytes = field(default=None, repr=False)
424
+
425
+ def __init__(self, data: T, header: ThreespaceHeader, data_raw_binary: bytes = None):
426
+ self.header = header
427
+ self.data = data
428
+ self.raw = (header.raw, data)
429
+ self.raw_data = data_raw_binary
430
+
431
+ def __getitem__(self, key):
432
+ return self.raw[key]
433
+
434
+ def __len__(self):
435
+ return len(self.raw)
436
+
437
+ def __iter__(self):
438
+ return iter(self.raw)
439
+
440
+ @property
441
+ def raw_binary(self):
442
+ bin = bytearray([])
443
+ if self.header is not None and self.header.raw_binary is not None:
444
+ bin += self.header.raw_binary
445
+ if self.raw_data is not None:
446
+ bin += self.raw_data
447
+ return bin
448
+
449
+ @dataclass
450
+ class ThreespaceBootloaderInfo:
451
+ memstart: int
452
+ memend: int
453
+ pagesize: int
454
+ bootversion: int
455
+
456
+ #Required for the API to work. The API will attempt to keep these enabled at all times.
457
+ THREESPACE_REQUIRED_HEADER = THREESPACE_HEADER_ECHO_BIT | THREESPACE_HEADER_CHECKSUM_BIT | THREESPACE_HEADER_LENGTH_BIT
458
+ class ThreespaceSensor:
459
+
460
+ def __init__(self, com = None, timeout=2):
461
+ if com is None: #Default to attempting to use the serial com class if none is provided
462
+ com = ThreespaceSerialComClass
463
+
464
+ #Auto discover using the supplied com class type
465
+ if inspect.isclass(com) and issubclass(com, ThreespaceComClass):
466
+ new_com = None
467
+ for serial_com in com.auto_detect():
468
+ new_com = serial_com
469
+ break #Exit after getting 1
470
+ if new_com is None:
471
+ raise RuntimeError("Failed to auto discover com port")
472
+ self.com = new_com
473
+ self.com.open()
474
+ #The supplied com already was a com class, nothing to do
475
+ elif inspect.isclass(type(com)) and issubclass(type(com), ThreespaceComClass):
476
+ self.com = com
477
+ else: #Unknown type, try making a ThreespaceSerialComClass out of this
478
+ try:
479
+ self.com = ThreespaceSerialComClass(com)
480
+ except:
481
+ raise ValueError("Failed to create default ThreespaceSerialComClass from parameter:", type(com), com)
482
+
483
+ self.com.read_all() #Clear anything that may be there
484
+
485
+ self.commands: list[ThreespaceCommand] = [None] * 256
486
+ self.getStreamingBatchCommand: ThreespaceGetStreamingBatchCommand = None
487
+ self.funcs = {}
488
+ for command in _threespace_commands:
489
+ #Some commands are special and need added specially
490
+ if command.info.num == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM:
491
+ self.getStreamingBatchCommand = ThreespaceGetStreamingBatchCommand([])
492
+ command = self.getStreamingBatchCommand
493
+
494
+ self.__add_command(command)
495
+
496
+ self.immediate_debug = False
497
+ self.misaligned = False
498
+ self.dirty_cache = False
499
+ self.header_enabled = True
500
+
501
+ #All the different streaming options
502
+ self.is_data_streaming = False
503
+ self.is_log_streaming = False
504
+ self.is_file_streaming = False
505
+ self._force_stop_streaming()
506
+
507
+ #Used to ensure connecting to the correct sensor when reconnecting
508
+ self.serial_number = None
509
+
510
+ self.__cached_in_bootloader = self.__check_bootloader_status()
511
+ if not self.in_bootloader:
512
+ self.__firmware_init()
513
+ else:
514
+ self.serial_number = self.bootloader_get_sn()
515
+
516
+ def __firmware_init(self):
517
+ """
518
+ Should only be called when not streaming and known in firmware.
519
+ Called for powerup events when booting into firmware
520
+ """
521
+ self.dirty_cache = False #No longer dirty cause initializing
522
+
523
+ self.com.read_all() #Clear anything that may be there
524
+
525
+ self.__reinit_firmware()
526
+
527
+ self.valid_mags = self.__get_valid_components("valid_mags")
528
+ self.valid_accels = self.__get_valid_components("valid_accels")
529
+ self.valid_gyros = self.__get_valid_components("valid_gyros")
530
+ self.valid_baros = self.__get_valid_components("valid_baros")
531
+
532
+ def __get_valid_components(self, key: str):
533
+ valid = self.get_settings(key)
534
+ if len(valid) == 0: return []
535
+ return [int(v) for v in valid.split(',')]
536
+
537
+ def __reinit_firmware(self):
538
+ """
539
+ Called when settings may have changed but a full reboot did not occur
540
+ """
541
+ self.com.read_all() #Clear anything that may be there
542
+ self.dirty_cache = False #No longer dirty cause initializing
543
+
544
+ self.header_info = ThreespaceHeaderInfo()
545
+ self.cmd_echo_byte_index = None
546
+ self.streaming_slots: list[ThreespaceCommand] = [None] * 16
547
+ self.streaming_packets: list[ThreespaceCmdResult[list]] = []
548
+
549
+ self.file_stream_data = bytearray([])
550
+ self.file_stream_length = 0
551
+
552
+ self.streaming_packet_size = 0
553
+ self.header_enabled = True
554
+ self._force_stop_streaming()
555
+
556
+ #Now reinitialize the cached settings
557
+ self.__cache_header_settings()
558
+ self.cache_streaming_settings()
559
+
560
+ self.serial_number = int(self.get_settings("serial_number"), 16)
561
+ self.immediate_debug = int(self.get_settings("debug_mode")) == 1 #Needed for some startup processes when restarting
562
+
563
+ def __add_command(self, command: ThreespaceCommand):
564
+ if self.commands[command.info.num] != None:
565
+ print(f"Registering duplicate command: {command.info.num} {self.commands[command.info.num].info.name} {command.info.name}")
566
+ self.commands[command.info.num] = command
567
+
568
+ #Build the actual method for executing the command
569
+ code = f"def {command.info.name}(self, *args):\n"
570
+ code += f" return self.execute_command(self.commands[{command.info.num}], *args)"
571
+ exec(code, globals(), self.funcs)
572
+ setattr(self, command.info.name, types.MethodType(self.funcs[command.info.name], self))
573
+
574
+ def __get_command(self, command_name: str):
575
+ for command in self.commands:
576
+ if command is None: continue
577
+ if command.info.name == command_name:
578
+ return command
579
+ return None
580
+
581
+ @property
582
+ def is_streaming(self):
583
+ return self.is_data_streaming or self.is_log_streaming or self.is_file_streaming
584
+
585
+ #Can't just do if "header" in string because log_header_enabled exists and doesn't actually require cacheing the header
586
+ HEADER_KEYS = ["header", "header_status", "header_timestamp", "header_echo", "header_checksum", "header_serial", "header_length"]
587
+ def set_settings(self, param_string: str = None, **kwargs):
588
+ self.check_dirty()
589
+ #Build cmd string
590
+ params = []
591
+ if param_string is not None:
592
+ params.append(param_string)
593
+
594
+ for key, value in kwargs.items():
595
+ if isinstance(value, list):
596
+ value = [str(v) for v in value]
597
+ value = ','.join(value)
598
+ elif isinstance(value, bool):
599
+ value = int(value)
600
+ params.append(f"{key}={value}")
601
+ cmd = f"!{';'.join(params)}\n"
602
+
603
+ #For dirty check
604
+ keys = cmd[1:-1].split(';')
605
+ keys = [v.split('=')[0] for v in keys]
606
+
607
+ #Send cmd
608
+ self.com.write(cmd.encode())
609
+
610
+ #Default values
611
+ err = 3
612
+ num_successes = 0
613
+
614
+ #Read response
615
+ if self.is_streaming: #Streaming have to read via peek and also validate it more
616
+ max_response_length = len("255,255\r\n")
617
+ found_response = False
618
+ start_time = time.time()
619
+ while not found_response: #Infinite loop to wait for the data to be available
620
+ if time.time() - start_time > self.com.timeout:
621
+ print("Timed out waiting for set_settings response")
622
+ return err, num_successes
623
+ line = ""
624
+ while True: #A loop used to allow breaking out of to be less wet.
625
+ line = self.com.peekline(max_length=max_response_length)
626
+ if b'\n' not in line:
627
+ break
628
+
629
+ try:
630
+ values = line.decode().strip()
631
+ values = values.split(',')
632
+ if len(values) != 2: break
633
+ err = int(values[0])
634
+ num_successes = int(values[1])
635
+ except: break
636
+ if err > 255 or num_successes > 255:
637
+ break
638
+
639
+ #Successfully got pass all the checks!
640
+ #Consume the buffer and continue
641
+ found_response = True
642
+ self.com.readline()
643
+ break
644
+ if found_response: break
645
+ while not self.updateStreaming(max_checks=1): pass #Wait for streaming to parse something!
646
+ else:
647
+ #When not streaming, way more straight forward
648
+ try:
649
+ response = self.com.readline()
650
+ response = response.decode().strip()
651
+ err, num_successes = response.split(',')
652
+ err = int(err)
653
+ num_successes = int(num_successes)
654
+ except:
655
+ print("Failed to parse set response:", response)
656
+ return err, num_successes
657
+
658
+ #Handle updating state variables based on settings
659
+ #If the user modified the header, need to cache the settings so the API knows how to interpret responses
660
+ if "header" in cmd.lower(): #First do a quick check
661
+ if any(v in keys for v in ThreespaceSensor.HEADER_KEYS): #Then do a longer check
662
+ self.__cache_header_settings()
663
+
664
+ if "stream_slots" in cmd.lower():
665
+ self.cache_streaming_settings()
666
+
667
+ if any(v in keys for v in ("default", "reboot")): #All the settings changed, just need to mark dirty
668
+ self.set_cached_settings_dirty()
669
+
670
+ if err:
671
+ print(f"Err setting {cmd}: {err=} {num_successes=}")
672
+ return err, num_successes
673
+
674
+ def get_settings(self, *args: str) -> dict[str, str] | str:
675
+ self.check_dirty()
676
+ #Build and send the cmd
677
+ params = list(args)
678
+ cmd = f"?{';'.join(params)}\n"
679
+ self.com.write(cmd.encode())
680
+
681
+ keys = cmd[1:-1].split(';')
682
+ error_response = "<KEY_ERROR>"
683
+
684
+ #Wait for the response to be available if streaming
685
+ #NOTE: THIS WILL NOT WORK WITH SETTINGS SUCH AS ?all ?settings or QUERY STRINGS
686
+ #THIS can be worked around by first getting a setting that does echo normally, as that will allow
687
+ #the sensor to determine where the ascii data actually starts.
688
+ #Ex: get_settings("header", "all") would work
689
+ if self.is_streaming:
690
+ first_key = bytes(keys[0] + "=", 'ascii') #Add on the equals sign to try and make this less likely to conflict with binary data
691
+ possible_outputs = [(len(error_response), bytes(error_response, 'ascii')), (len(first_key), first_key)]
692
+ possible_outputs.sort() #Must try the smallest one first because if streaming is slow, may take a while for the data to fill pass the largest possible value
693
+ start_time = time.time()
694
+ while True:
695
+ if time.time() - start_time > self.com.timeout:
696
+ print("Timeout parsing get response")
697
+ return {}
698
+ found_response = False
699
+ for length, key in possible_outputs:
700
+ possible_response = self.com.peek(length)
701
+ if possible_response == key: #This the response, so break and parse
702
+ found_response = True
703
+ break
704
+ if found_response: break
705
+ while not self.updateStreaming(max_checks=1): pass #Wait for streaming to process something. May just advance due to invalid
706
+
707
+ #Read the response
708
+ try:
709
+ response = self.com.readline()
710
+ if ord('\n') not in response:
711
+ print("Failed to get whole line")
712
+ response = response.decode().strip().split(';')
713
+ except:
714
+ print("Failed to parse get:", response)
715
+
716
+ #Build the response dict
717
+ response_dict = {}
718
+ for i, v in enumerate(response):
719
+ if v == error_response:
720
+ response_dict[keys[i]] = error_response
721
+ continue
722
+ try:
723
+ key, value = v.split('=')
724
+ response_dict[key] = value
725
+ except:
726
+ print("Failed to parse get:", response)
727
+
728
+ #Format response
729
+ if len(response_dict) == 1:
730
+ return list(response_dict.values())[0]
731
+ return response_dict
732
+
733
+ def execute_command(self, cmd: ThreespaceCommand, *args):
734
+ self.check_dirty()
735
+
736
+ retries = 0
737
+ MAX_RETRIES = 3
738
+
739
+ while retries < MAX_RETRIES:
740
+ cmd.send_command(self.com, *args, header_enabled=self.header_enabled)
741
+ result = self.__await_command(cmd)
742
+ if result == THREESPACE_AWAIT_COMMAND_FOUND:
743
+ break
744
+ retries += 1
745
+
746
+ if retries == MAX_RETRIES:
747
+ raise RuntimeError(f"Failed to get response to command {cmd.info.name}")
748
+
749
+ return self.read_and_parse_command(cmd)
750
+
751
+ def read_and_parse_command(self, cmd: ThreespaceCommand):
752
+ if self.header_enabled:
753
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
754
+ else:
755
+ header = ThreespaceHeader()
756
+ result, raw = cmd.read_command(self.com)
757
+ return ThreespaceCmdResult(result, header, data_raw_binary=raw)
758
+
759
+ def __peek_checksum(self, header: ThreespaceHeader):
760
+ header_len = len(header.raw_binary)
761
+ data = self.com.peek(header_len + header.length)[header_len:]
762
+ if len(data) != header.length: return False
763
+ checksum = sum(data) % 256
764
+ return checksum == header.checksum
765
+
766
+ def __await_command(self, cmd: ThreespaceCommand, timeout=2):
767
+ start_time = time.time()
768
+
769
+ #Update the streaming until the result for this command is next in the buffer
770
+ while True:
771
+ if time.time() - start_time > timeout:
772
+ return THREESPACE_AWAIT_COMMAND_TIMEOUT
773
+
774
+ #Get potential header
775
+ header = self.com.peek(self.header_info.size)
776
+ if len(header) != self.header_info.size: #Wait for more data
777
+ continue
778
+
779
+ #Check to see what this packet is a response to
780
+ header = ThreespaceHeader.from_bytes(header, self.header_info)
781
+ echo = header.echo
782
+
783
+ if echo == cmd.info.num: #Cmd matches
784
+ if self.__peek_checksum(header):
785
+ return THREESPACE_AWAIT_COMMAND_FOUND
786
+
787
+ #Error in packet, go start realigning
788
+ if not self.misaligned:
789
+ print(f"Checksum mismatch for command {cmd.info.num}")
790
+ self.misaligned = True
791
+ self.com.read(1)
792
+ else:
793
+ #It wasn't a response to the command, so may be a response to some internal system
794
+ self.__internal_update(header)
795
+
796
+ def __internal_update(self, header: ThreespaceHeader):
797
+ """
798
+ This should be called after a header is obtained via a command and it is determined that it can't
799
+ be in response to a synchronous command that got sent. This manages updating the streaming and realigning
800
+ the data buffer
801
+ """
802
+ checksum_match = False #Just for debugging
803
+
804
+ #NOTE: FOR THIS TO WORK IT IS REQUIRED THAT THE HEADER DOES NOT CHANGE WHILE STREAMING ANY FORM OF DATA.
805
+ #IT IS UP TO THE API TO ENFORCE NOT ALLOWING HEADER CHANGES WHILE ANY OF THOSE THINGS ARE HAPPENING
806
+ if self.is_data_streaming and header.echo == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM: #Its a streaming packet, so update streaming
807
+ if checksum_match := self.__peek_checksum(header):
808
+ self.__update_base_streaming()
809
+ return True
810
+ elif self.is_log_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
811
+ if checksum_match := self.__peek_checksum(header):
812
+ self.__update_log_streaming()
813
+ return True
814
+ elif self.is_file_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
815
+ if checksum_match := self.__peek_checksum(header):
816
+ self.__update_file_streaming()
817
+ return True
818
+
819
+ #The response didn't match any of the expected asynchronous streaming API responses, so assume a misalignment
820
+ #and start reading through the buffer
821
+ if not self.misaligned:
822
+ print(f"Possible Misalignment or corruption/debug message, header {header} raw {[hex(v) for v in header.raw_binary]}, Checksum match? {checksum_match}")
823
+ self.misaligned = True
824
+ self.com.read(1) #Because of expected misalignment, go through buffer 1 by 1 until realigned
825
+
826
+ def updateStreaming(self, max_checks=float('inf')):
827
+ """
828
+ Returns true if any amount of data was processed whether valid or not
829
+ """
830
+ if not self.is_streaming: return False
831
+
832
+ #I may need to make this have a max num bytes it will process before exiting to prevent locking up on slower machines
833
+ #due to streaming faster then the program runs
834
+ num_checks = 0
835
+ data_processed = False
836
+ while num_checks < max_checks:
837
+ if self.com.length < self.header_info.size:
838
+ return data_processed
839
+
840
+ #Get header
841
+ header = self.com.peek(self.header_info.size)
842
+
843
+ #Get the header and send it to the internal update
844
+ header = ThreespaceHeader.from_bytes(header, self.header_info)
845
+ self.__internal_update(header)
846
+ data_processed = True #Internal update always processes data. Either reads a streaming message, or advances buffer due to misalignment
847
+ num_checks += 1
848
+
849
+ return data_processed
850
+
851
+
852
+ def startStreaming(self):
853
+ if self.is_data_streaming: return
854
+ self.check_dirty()
855
+ self.streaming_packets.clear()
856
+
857
+ self.header_enabled = True
858
+
859
+ self.cache_streaming_settings()
860
+
861
+ cmd = self.commands[85]
862
+ cmd.send_command(self.com, header_enabled=self.header_enabled)
863
+ if self.header_enabled:
864
+ self.__await_command(cmd)
865
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
866
+ else:
867
+ header = ThreespaceHeader()
868
+ self.is_data_streaming = True
869
+ return ThreespaceCmdResult(None, header)
870
+
871
+ def _force_stop_streaming(self):
872
+ """
873
+ This function is used to stop streaming without validating it was streaming and ignoring any output of the
874
+ communication line. This is a destructive call that will lose data, but will gurantee stopping streaming
875
+ and leave the communication line in a clean state
876
+ """
877
+ cached_header_enabled = self.header_enabled
878
+ cahched_dirty = self.dirty_cache
879
+
880
+ #Must set these to gurantee it doesn't try and parse a response from anything
881
+ self.dirty_cache = False
882
+ self.header_enabled = False #Keep off for the attempt at stop streaming since if in an invalid state, won't be able to get response
883
+ self.stopStreaming() #Just in case was streaming
884
+ self.fileStopStream()
885
+
886
+ #TODO: Change this to pause the data logging instead, then check the state and update
887
+ self.stopDataLogging()
888
+
889
+ #Restore
890
+ self.header_enabled = cached_header_enabled
891
+ self.dirty_cache = cahched_dirty
892
+
893
+ def stopStreaming(self):
894
+ self.check_dirty()
895
+ cmd = self.commands[86]
896
+ cmd.send_command(self.com, header_enabled=self.header_enabled)
897
+ if self.header_enabled: #Header will be enabled while streaming, but this is useful for startup
898
+ self.__await_command(cmd)
899
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
900
+ else:
901
+ header = ThreespaceHeader()
902
+ time.sleep(0.05)
903
+ while self.com.length:
904
+ self.com.read_all()
905
+ self.is_data_streaming = False
906
+ return ThreespaceCmdResult(None, header)
907
+
908
+ def set_cached_settings_dirty(self):
909
+ """
910
+ Could be streaming settings, header settings...
911
+ Basically the sensor needs reinitialized
912
+ """
913
+ self.dirty_cache = True
914
+
915
+ def __attempt_rediscover_self(self):
916
+ """
917
+ Trys to change the com class currently being used to be a detected
918
+ com class with the same serial number. Useful for re-enumeration, such as when
919
+ entering bootloader and using USB
920
+ """
921
+ for potential_com in self.com.auto_detect():
922
+ potential_com.open()
923
+ sensor = ThreespaceSensor(potential_com)
924
+ if sensor.serial_number == self.serial_number:
925
+ self.com = potential_com
926
+ return True
927
+ sensor.cleanup() #Handles closing the potential_com
928
+ return False
929
+
930
+ def check_dirty(self):
931
+ if not self.dirty_cache: return
932
+ if self.com.reenumerates and not self.com.check_open(): #Must check this, as could have transitioned from bootloader to firmware or vice versa and just needs re-opened/detected
933
+ success = self.__attempt_rediscover_self()
934
+ if not success:
935
+ raise RuntimeError("Sensor connection lost")
936
+
937
+ self._force_stop_streaming() #Can't be streaming when checking the dirty cache. If you want to stream, don't do things that cause the object to go dirty.
938
+ was_in_bootloader = self.__cached_in_bootloader
939
+ self.__cached_in_bootloader = self.__check_bootloader_status()
940
+
941
+ if was_in_bootloader and not self.__cached_in_bootloader: #Just Exited bootloader, need to fully reinit
942
+ self.__firmware_init()
943
+ elif not self.__cached_in_bootloader: #Was already in firmware, so only need to partially reinit
944
+ self.__reinit_firmware() #Partially init when just naturally dirty
945
+ self.dirty_cache = False
946
+
947
+ def cache_streaming_settings(self):
948
+ cached_slots: list[ThreespaceCommand] = []
949
+ slots: str = self.get_settings("stream_slots")
950
+ slots = slots.split(',')
951
+ for slot in slots:
952
+ slot = int(slot.split(':')[0]) #Ignore parameters if any
953
+ if slot != 255:
954
+ cached_slots.append(self.commands[slot])
955
+ else:
956
+ cached_slots.append(None)
957
+ self.streaming_slots = cached_slots.copy()
958
+ self.getStreamingBatchCommand.set_stream_slots(self.streaming_slots)
959
+ self.streaming_packet_size = 0
960
+ for command in self.streaming_slots:
961
+ if command == None: continue
962
+ self.streaming_packet_size += command.info.out_size
963
+
964
+ def __cache_header_settings(self):
965
+ """
966
+ Should be called any time changes are made to the header. Will normally be called via the check_dirty/reinit
967
+ """
968
+ header = int(self.get_settings("header"))
969
+ #API requires these bits to be enabled, so don't let them be disabled
970
+ required_header = header | THREESPACE_REQUIRED_HEADER
971
+ if header == self.header_info.bitfield and header == required_header: return #Nothing to update
972
+
973
+ #Don't allow the header to change while streaming
974
+ #This is to prevent a situation where the header for streaming and commands are different
975
+ #since streaming caches the header. This would cause an issue where the echo byte could be in seperate
976
+ #positions, causing a situation where parsing a command and streaming at the same time breaks since it thinks both are valid cmd echoes.
977
+ if self.is_streaming:
978
+ print("PREVENTING HEADER CHANGE DUE TO CURRENTLY STREAMING")
979
+ self.set_settings(header=self.header_info.bitfield)
980
+ return
981
+
982
+ if required_header != header:
983
+ print(f"Forcing header checksum, echo, and length enabled")
984
+ self.set_settings(header=required_header)
985
+ return
986
+
987
+ #Current/New header is valid, so can cache it
988
+ self.header_info.bitfield = header
989
+ self.cmd_echo_byte_index = self.header_info.get_start_byte(THREESPACE_HEADER_ECHO_BIT) #Needed for cmd validation while streaming
990
+
991
+ def __update_base_streaming(self):
992
+ """
993
+ Should be called after the packet is validated
994
+ """
995
+ self.streaming_packets.append(self.read_and_parse_command(self.getStreamingBatchCommand))
996
+
997
+ def getOldestStreamingPacket(self):
998
+ if len(self.streaming_packets) == 0:
999
+ return None
1000
+ return self.streaming_packets.pop(0)
1001
+
1002
+ def getNewestStreamingPacket(self):
1003
+ if len(self.streaming_packets) == 0:
1004
+ return None
1005
+ return self.streaming_packets.pop()
1006
+
1007
+ def clearStreamingPackets(self):
1008
+ self.streaming_packets.clear()
1009
+
1010
+ def fileStartStream(self) -> ThreespaceCmdResult[int]:
1011
+ self.check_dirty()
1012
+ self.header_enabled = True
1013
+
1014
+ cmd = self.__get_command("__fileStartStream")
1015
+ cmd.send_command(self.com, header_enabled=self.header_enabled)
1016
+ self.__await_command(cmd)
1017
+
1018
+ if self.header_enabled:
1019
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1020
+ else:
1021
+ header = ThreespaceHeader()
1022
+
1023
+ result, raw = cmd.read_command(self.com)
1024
+ self.file_stream_length = result
1025
+ self.is_file_streaming = True
1026
+
1027
+ return ThreespaceCmdResult(result, header, data_raw_binary=raw)
1028
+
1029
+ def fileStopStream(self) -> ThreespaceCmdResult[None]:
1030
+ self.check_dirty()
1031
+
1032
+ cmd = self.__get_command("__fileStopStream")
1033
+ cmd.send_command(self.com, header_enabled=self.header_enabled)
1034
+
1035
+ if self.header_enabled: #Header will be enabled while streaming, but this is useful for startup
1036
+ self.__await_command(cmd)
1037
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1038
+ else:
1039
+ header = ThreespaceHeader()
1040
+
1041
+ #TODO: Remove me now that realignment exists and multiple things can be streaming at once
1042
+ time.sleep(0.05)
1043
+ while self.com.length:
1044
+ self.com.read_all()
1045
+
1046
+ self.is_file_streaming = False
1047
+ return ThreespaceCmdResult(None, header)
1048
+
1049
+ def getFileStreamData(self):
1050
+ to_return = self.file_stream_data.copy()
1051
+ self.file_stream_data.clear()
1052
+ return to_return
1053
+
1054
+ def clearFileStreamData(self):
1055
+ self.file_stream_data.clear()
1056
+
1057
+ def __update_file_streaming(self):
1058
+ """
1059
+ Should be called after the packet is validated
1060
+ """
1061
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1062
+ data = self.com.read(header.length)
1063
+ self.file_stream_data += data
1064
+ self.file_stream_length -= header.length
1065
+ if header.length < 512 or self.file_stream_length == 0: #File streaming sends in chunks of 512. If not 512, it must be the last packet
1066
+ self.is_file_streaming = False
1067
+ if self.file_stream_length != 0:
1068
+ print(f"File streaming stopped due to last packet. However still expected {self.file_stream_length} more bytes.")
1069
+
1070
+ def startDataLogging(self) -> ThreespaceCmdResult[None]:
1071
+ self.check_dirty()
1072
+
1073
+ self.header_enabled = True
1074
+ self.cache_streaming_settings()
1075
+
1076
+ #Must check whether streaming is being done alongside logging or not. Also configure required settings if it is
1077
+ streaming = bool(int(self.get_settings("log_immediate_output")))
1078
+ if streaming:
1079
+ self.set_settings(log_immediate_output_header_enabled=1,
1080
+ log_immediate_output_header_mode=THREESPACE_OUTPUT_MODE_BINARY) #Must have header enabled in the log messages for this to work and must use binary for the header
1081
+ cmd = self.__get_command("__startDataLogging")
1082
+ cmd.send_command(self.com, header_enabled=self.header_enabled)
1083
+ if self.header_enabled:
1084
+ self.__await_command(cmd)
1085
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1086
+ else:
1087
+ header = ThreespaceHeader()
1088
+
1089
+ self.is_log_streaming = streaming
1090
+ return ThreespaceCmdResult(None, header)
1091
+
1092
+ def stopDataLogging(self) -> ThreespaceCmdResult[None]:
1093
+ self.check_dirty()
1094
+
1095
+ cmd = self.__get_command("__stopDataLogging")
1096
+ cmd.send_command(self.com, header_enabled=self.header_enabled)
1097
+
1098
+ if self.header_enabled: #Header will be enabled while streaming, but this is useful for startup
1099
+ self.__await_command(cmd)
1100
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1101
+ else:
1102
+ header = ThreespaceHeader()
1103
+ #TODO: Remove me now that realignment exists and multiple things can be streaming at once
1104
+ if self.is_log_streaming:
1105
+ time.sleep(0.05)
1106
+ while self.com.length:
1107
+ self.com.read_all()
1108
+
1109
+ self.is_log_streaming = False
1110
+ return ThreespaceCmdResult(None, header)
1111
+
1112
+ def __update_log_streaming(self):
1113
+ """
1114
+ Should be called after the packet is validated
1115
+ Log streaming is essentially file streaming done as the file is recorded. So uses file
1116
+ streaming logistics. Will update this later to also parse the response maybe.
1117
+ """
1118
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1119
+ data = self.com.read(header.length)
1120
+ self.file_stream_data += data
1121
+
1122
+ def softwareReset(self):
1123
+ self.check_dirty()
1124
+ cmd = self.commands[226]
1125
+ cmd.send_command(self.com)
1126
+ self.com.close()
1127
+ time.sleep(0.5) #Give it time to restart
1128
+ self.com.open()
1129
+ if self.immediate_debug:
1130
+ time.sleep(2) #An additional 2 seconds to ensure can clear all debug messages
1131
+ self.com.read_all()
1132
+ self.__firmware_init()
1133
+
1134
+ def enterBootloader(self):
1135
+ if self.in_bootloader: return
1136
+
1137
+ cmd = self.commands[229]
1138
+ cmd.send_command(self.com)
1139
+ time.sleep(0.5) #Give it time to boot into bootloader
1140
+ if self.com.reenumerates:
1141
+ self.com.close()
1142
+ success = self.__attempt_rediscover_self()
1143
+ if not success:
1144
+ raise RuntimeError("Failed to reconnect to sensor in bootloader")
1145
+ in_bootloader = self.__check_bootloader_status()
1146
+ if not in_bootloader:
1147
+ raise RuntimeError("Failed to enter bootloader")
1148
+ self.__cached_in_bootloader = True
1149
+ self.com.read_all() #Just in case any garbage floating around
1150
+
1151
+
1152
+ @property
1153
+ def in_bootloader(self):
1154
+ #This function should not be used internally when solving dirty checks
1155
+ self.check_dirty() #If dirty, this we reobtain the value of __cached_in_bootloader.
1156
+ return self.__cached_in_bootloader
1157
+
1158
+ def __check_bootloader_status(self):
1159
+ """
1160
+ Checks if in the bootloader via command. If wanting via cache, just check .in_bootloader
1161
+ This function both updates .in_bootloader and returns the value
1162
+
1163
+ Must not call this function while streaming. It is only used internally and should be able to meet these conditions
1164
+ A user of this class should use .in_bootloader instead of this function .
1165
+
1166
+ To check, ? is sent, the bootloader will respond with OK. However, to avoid needing to wait
1167
+ for the timeout, we send a setting query at the same time. If the response is to the setting, in firmware,
1168
+ else if ok, in bootloader. If times out, something funky is happening.
1169
+ All bootloader commands are CAPITAL letters. Firmware commands are case insensitive. So as long as send no capitals, its fine.
1170
+ """
1171
+ #If sending commands over BT to the bootloader, it does an Auto Baudrate Detection
1172
+ #for the BT module that requires sending 3 U's. This will respond with 1-2 OK responses if in bootloader.
1173
+ #By then adding a ?UUU, that will trigger a <KEY_ERROR> if in firmware. So, can tell if in bootloader or firmware by checking for OK or <KEY_ERROR>
1174
+ bootloader = False
1175
+ self.com.write("UUU?UUU\n".encode())
1176
+ response = self.com.read(2)
1177
+ if len(response) == 0:
1178
+ raise RuntimeError("Failed to discover bootloader or firmware. Is the sensor a 3.0?")
1179
+ if response == b'OK':
1180
+ bootloader = True
1181
+ self.com.read_all() #Remove the rest of the OK responses or the rest of the <KEY_ERROR> response
1182
+ return bootloader
1183
+
1184
+ def bootloader_get_sn(self):
1185
+ self.com.write("Q".encode())
1186
+ result = self.com.read(9) #9 Because it includes a line feed for reasons
1187
+ if len(result) != 9:
1188
+ raise Exception()
1189
+ #Note bootloader uses big endian instead of little for reasons
1190
+ return struct.unpack(f">{_3space_format_to_external('U')}", result[:8])[0]
1191
+
1192
+ def bootloader_boot_firmware(self):
1193
+ if not self.in_bootloader: return
1194
+ self.com.write("B".encode())
1195
+ time.sleep(0.5) #Give time to boot into firmware
1196
+ if self.com.reenumerates:
1197
+ self.com.close()
1198
+ success = self.__attempt_rediscover_self()
1199
+ if not success:
1200
+ raise RuntimeError("Failed to reconnect to sensor in firmware")
1201
+ self.com.read_all() #If debug_mode=1, might be debug messages waiting
1202
+ if self.immediate_debug:
1203
+ print("Waiting longer before booting into firmware because immediate debug was enabled.")
1204
+ time.sleep(2)
1205
+ self.com.read_all()
1206
+ in_bootloader = self.__check_bootloader_status()
1207
+ if in_bootloader:
1208
+ raise RuntimeError("Failed to exit bootloader")
1209
+ self.__cached_in_bootloader = False
1210
+ self.__firmware_init()
1211
+
1212
+ def bootloader_erase_firmware(self, timeout=20):
1213
+ """
1214
+ This may take a long time
1215
+ """
1216
+ self.com.write('S'.encode())
1217
+ if timeout is not None:
1218
+ cached_timeout = self.com.timeout
1219
+ self.com.timeout = timeout
1220
+ response = self.com.read(1)[0]
1221
+ if timeout is not None:
1222
+ self.com.timeout = cached_timeout
1223
+ return response
1224
+
1225
+ def bootloader_get_info(self):
1226
+ self.com.write('I'.encode())
1227
+ memstart = struct.unpack(f">{_3space_format_to_external('l')}", self.com.read(4))[0]
1228
+ memend = struct.unpack(f">{_3space_format_to_external('l')}", self.com.read(4))[0]
1229
+ pagesize = struct.unpack(f">{_3space_format_to_external('I')}", self.com.read(2))[0]
1230
+ bootversion = struct.unpack(f">{_3space_format_to_external('I')}", self.com.read(2))[0]
1231
+ return ThreespaceBootloaderInfo(memstart, memend, pagesize, bootversion)
1232
+
1233
+ def bootloader_prog_mem(self, bytes: bytearray):
1234
+ memsize = len(bytes)
1235
+ checksum = sum(bytes)
1236
+ self.com.write('C'.encode())
1237
+ self.com.write(struct.pack(f">{_3space_format_to_external('I')}", memsize))
1238
+ self.com.write(bytes)
1239
+ self.com.write(struct.pack(f">{_3space_format_to_external('B')}", checksum & 0xFFFF))
1240
+ return self.com.read(1)[0]
1241
+
1242
+ def bootloader_get_state(self):
1243
+ self.com.write('OO'.encode()) #O is sent twice to compensate for a bug in some versions of the bootloader where the next character is ignored (except for R, do NOT send R after O, it will erase all settings)
1244
+ state = struct.unpack(f">{_3space_format_to_external('u')}", self.com.read(4))[0]
1245
+ self.com.read_all() #Once the bootloader is fixed, it will respond twice instead of once. So consume any remainder
1246
+ return state
1247
+
1248
+ def bootloader_restore_factory_settings(self):
1249
+ self.com.write("RR".encode())
1250
+
1251
+ def cleanup(self):
1252
+ if not self.in_bootloader:
1253
+ if self.is_data_streaming:
1254
+ self.stopStreaming()
1255
+ if self.is_file_streaming:
1256
+ self.fileStopStream()
1257
+ if self.is_log_streaming:
1258
+ self.stopDataLogging()
1259
+ #self.closeFile() #May not be opened, but also not cacheing that so just attempt to close. Currently commented out because breaks embedded
1260
+ self.com.close()
1261
+
1262
+ #-------------------------START ALL PROTOTYPES------------------------------------
1263
+
1264
+ def eeptsStart(self) -> ThreespaceCmdResult[None]:
1265
+ raise NotImplementedError("This method is not available.")
1266
+
1267
+ def eeptsStop(self) -> ThreespaceCmdResult[None]:
1268
+ raise NotImplementedError("This method is not available.")
1269
+
1270
+ def eeptsGetOldestStep(self) -> ThreespaceCmdResult[list]:
1271
+ raise NotImplementedError("This method is not available.")
1272
+
1273
+ def eeptsGetNewestStep(self) -> ThreespaceCmdResult[list]:
1274
+ raise NotImplementedError("This method is not available.")
1275
+
1276
+ def eeptsGetNumStepsAvailable(self) -> ThreespaceCmdResult[int]:
1277
+ raise NotImplementedError("This method is not available.")
1278
+
1279
+ def eeptsInsertGPS(self, latitude: float, longitude: float) -> ThreespaceCmdResult[None]:
1280
+ raise NotImplementedError("This method is not available.")
1281
+
1282
+ def eeptsAutoOffset(self) -> ThreespaceCmdResult[None]:
1283
+ raise NotImplementedError("This method is not available.")
1284
+
1285
+ def getRawGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]:
1286
+ raise NotImplementedError("This method is not available.")
1287
+
1288
+ def getRawAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1289
+ raise NotImplementedError("This method is not available.")
1290
+
1291
+ def getRawMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1292
+ raise NotImplementedError("This method is not available.")
1293
+
1294
+ def getTaredOrientation(self) -> ThreespaceCmdResult[list[float]]:
1295
+ raise NotImplementedError("This method is not available.")
1296
+
1297
+ def getTaredOrientationAsEulerAngles(self) -> ThreespaceCmdResult[list[float]]:
1298
+ raise NotImplementedError("This method is not available.")
1299
+
1300
+ def getTaredOrientationAsRotationMatrix(self) -> ThreespaceCmdResult[list[float]]:
1301
+ raise NotImplementedError("This method is not available.")
1302
+
1303
+ def getTaredOrientationAsAxisAngles(self) -> ThreespaceCmdResult[list[float]]:
1304
+ raise NotImplementedError("This method is not available.")
1305
+
1306
+ def getTaredOrientationAsTwoVector(self) -> ThreespaceCmdResult[list[float]]:
1307
+ raise NotImplementedError("This method is not available.")
1308
+
1309
+ def getDifferenceQuaternion(self) -> ThreespaceCmdResult[list[float]]:
1310
+ raise NotImplementedError("This method is not available.")
1311
+
1312
+ def getUntaredOrientation(self) -> ThreespaceCmdResult[list[float]]:
1313
+ raise NotImplementedError("This method is not available.")
1314
+
1315
+ def getUntaredOrientationAsEulerAngles(self) -> ThreespaceCmdResult[list[float]]:
1316
+ raise NotImplementedError("This method is not available.")
1317
+
1318
+ def getUntaredOrientationAsRotationMatrix(self) -> ThreespaceCmdResult[list[float]]:
1319
+ raise NotImplementedError("This method is not available.")
1320
+
1321
+ def getUntaredOrientationAsAxisAngles(self) -> ThreespaceCmdResult[list[float]]:
1322
+ raise NotImplementedError("This method is not available.")
1323
+
1324
+ def getUntaredOrientationAsTwoVector(self) -> ThreespaceCmdResult[list[float]]:
1325
+ raise NotImplementedError("This method is not available.")
1326
+
1327
+ def commitSettings(self) -> ThreespaceCmdResult[None]:
1328
+ raise NotImplementedError("This method is not available.")
1329
+
1330
+ def getMotionlessConfidenceFactor(self) -> ThreespaceCmdResult[float]:
1331
+ raise NotImplementedError("This method is not available.")
1332
+
1333
+ def enableMSC(self) -> ThreespaceCmdResult[None]:
1334
+ raise NotImplementedError("This method is not available.")
1335
+
1336
+ def disableMSC(self) -> ThreespaceCmdResult[None]:
1337
+ raise NotImplementedError("This method is not available.")
1338
+
1339
+ def getNextDirectoryItem(self) -> ThreespaceCmdResult[list[int,str,int]]:
1340
+ raise NotImplementedError("This method is not available.")
1341
+
1342
+ def changeDirectory(self, path: str) -> ThreespaceCmdResult[None]:
1343
+ raise NotImplementedError("This method is not available.")
1344
+
1345
+ def openFile(self, path: str) -> ThreespaceCmdResult[None]:
1346
+ raise NotImplementedError("This method is not available.")
1347
+
1348
+ def closeFile(self) -> ThreespaceCmdResult[None]:
1349
+ raise NotImplementedError("This method is not available.")
1350
+
1351
+ def fileGetRemainingSize(self) -> ThreespaceCmdResult[int]:
1352
+ raise NotImplementedError("This method is not available.")
1353
+
1354
+ def fileReadLine(self) -> ThreespaceCmdResult[str]:
1355
+ raise NotImplementedError("This method is not available.")
1356
+
1357
+ def fileReadBytes(self, num_bytes: int) -> ThreespaceCmdResult[bytes]:
1358
+ self.check_dirty()
1359
+ cmd = self.commands[THREESPACE_FILE_READ_BYTES_COMMAND_NUM]
1360
+ cmd.send_command(self.com, num_bytes, header_enabled=self.header_enabled)
1361
+ self.__await_command(cmd)
1362
+ if self.header_enabled:
1363
+ header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1364
+ else:
1365
+ header = ThreespaceHeader()
1366
+
1367
+ response = self.com.read(num_bytes)
1368
+ return ThreespaceCmdResult(response, header, data_raw_binary=response)
1369
+
1370
+ def deleteFile(self, path: str) -> ThreespaceCmdResult[None]:
1371
+ raise NotImplementedError("This method is not available.")
1372
+
1373
+ def getStreamingBatch(self):
1374
+ raise NotImplementedError("This method is not available.")
1375
+
1376
+ def setOffsetWithCurrentOrientation(self) -> ThreespaceCmdResult[None]:
1377
+ raise NotImplementedError("This method is not available.")
1378
+
1379
+ def resetBaseOffset(self) -> ThreespaceCmdResult[None]:
1380
+ raise NotImplementedError("This method is not available.")
1381
+
1382
+ def setBaseOffsetWithCurrentOrientation(self) -> ThreespaceCmdResult[None]:
1383
+ raise NotImplementedError("This method is not available.")
1384
+
1385
+ def getTaredTwoVectorInSensorFrame(self) -> ThreespaceCmdResult[list[float]]:
1386
+ raise NotImplementedError("This method is not available.")
1387
+
1388
+ def getUntaredTwoVectorInSensorFrame(self) -> ThreespaceCmdResult[list[float]]:
1389
+ raise NotImplementedError("This method is not available.")
1390
+
1391
+ def getPrimaryBarometerPressure(self) -> ThreespaceCmdResult[float]:
1392
+ raise NotImplementedError("This method is not available.")
1393
+
1394
+ def getPrimaryBarometerAltitude(self) -> ThreespaceCmdResult[float]:
1395
+ raise NotImplementedError("This method is not available.")
1396
+
1397
+ def getBarometerAltitude(self, id: int) -> ThreespaceCmdResult[float]:
1398
+ raise NotImplementedError("This method is not available.")
1399
+
1400
+ def getBarometerPressure(self, id: int) -> ThreespaceCmdResult[float]:
1401
+ raise NotImplementedError("This method is not available.")
1402
+
1403
+ def getAllPrimaryNormalizedData(self) -> ThreespaceCmdResult[list[float]]:
1404
+ raise NotImplementedError("This method is not available.")
1405
+
1406
+ def getPrimaryNormalizedGyroRate(self) -> ThreespaceCmdResult[list[float]]:
1407
+ raise NotImplementedError("This method is not available.")
1408
+
1409
+ def getPrimaryNormalizedAccelVec(self) -> ThreespaceCmdResult[list[float]]:
1410
+ raise NotImplementedError("This method is not available.")
1411
+
1412
+ def getPrimaryNormalizedMagVec(self) -> ThreespaceCmdResult[list[float]]:
1413
+ raise NotImplementedError("This method is not available.")
1414
+
1415
+ def getAllPrimaryCorrectedData(self) -> ThreespaceCmdResult[list[float]]:
1416
+ raise NotImplementedError("This method is not available.")
1417
+
1418
+ def getPrimaryCorrectedGyroRate(self) -> ThreespaceCmdResult[list[float]]:
1419
+ raise NotImplementedError("This method is not available.")
1420
+
1421
+ def getPrimaryCorrectedAccelVec(self) -> ThreespaceCmdResult[list[float]]:
1422
+ raise NotImplementedError("This method is not available.")
1423
+
1424
+ def getPrimaryCorrectedMagVec(self) -> ThreespaceCmdResult[list[float]]:
1425
+ raise NotImplementedError("This method is not available.")
1426
+
1427
+ def getPrimaryGlobalLinearAccel(self) -> ThreespaceCmdResult[list[float]]:
1428
+ raise NotImplementedError("This method is not available.")
1429
+
1430
+ def getPrimaryLocalLinearAccel(self) -> ThreespaceCmdResult[list[float]]:
1431
+ raise NotImplementedError("This method is not available.")
1432
+
1433
+ def getTemperatureCelsius(self) -> ThreespaceCmdResult[float]:
1434
+ raise NotImplementedError("This method is not available.")
1435
+
1436
+ def getTemperatureFahrenheit(self) -> ThreespaceCmdResult[float]:
1437
+ raise NotImplementedError("This method is not available.")
1438
+
1439
+ def getNormalizedGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]:
1440
+ raise NotImplementedError("This method is not available.")
1441
+
1442
+ def getNormalizedAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1443
+ raise NotImplementedError("This method is not available.")
1444
+
1445
+ def getNormalizedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1446
+ raise NotImplementedError("This method is not available.")
1447
+
1448
+ def getCorrectedGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]:
1449
+ raise NotImplementedError("This method is not available.")
1450
+
1451
+ def getCorrectedAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1452
+ raise NotImplementedError("This method is not available.")
1453
+
1454
+ def getCorrectedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1455
+ raise NotImplementedError("This method is not available.")
1456
+
1457
+ def enableMSC(self) -> ThreespaceCmdResult[None]:
1458
+ raise NotImplementedError("This method is not available.")
1459
+
1460
+ def disableMSC(self) -> ThreespaceCmdResult[None]:
1461
+ raise NotImplementedError("This method is not available.")
1462
+
1463
+ def getTimestamp(self) -> ThreespaceCmdResult[int]:
1464
+ raise NotImplementedError("This method is not available.")
1465
+
1466
+ def getBatteryVoltage(self) -> ThreespaceCmdResult[float]:
1467
+ raise NotImplementedError("This method is not available.")
1468
+
1469
+ def getBatteryPercent(self) -> ThreespaceCmdResult[int]:
1470
+ raise NotImplementedError("This method is not available.")
1471
+
1472
+ def getBatteryStatus(self) -> ThreespaceCmdResult[int]:
1473
+ raise NotImplementedError("This method is not available.")
1474
+
1475
+ def getGpsCoord(self) -> ThreespaceCmdResult[list[float]]:
1476
+ raise NotImplementedError("This method is not available.")
1477
+
1478
+ def getGpsAltitude(self) -> ThreespaceCmdResult[float]:
1479
+ raise NotImplementedError("This method is not available.")
1480
+
1481
+ def getGpsFixState(self) -> ThreespaceCmdResult[int]:
1482
+ raise NotImplementedError("This method is not available.")
1483
+
1484
+ def getGpsHdop(self) -> ThreespaceCmdResult[float]:
1485
+ raise NotImplementedError("This method is not available.")
1486
+
1487
+ def getGpsSatellites(self) -> ThreespaceCmdResult[int]:
1488
+ raise NotImplementedError("This method is not available.")
1489
+
1490
+ def getButtonState(self) -> ThreespaceCmdResult[int]:
1491
+ raise NotImplementedError("This method is not available.")
1492
+
1493
+ def correctRawGyroData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]:
1494
+ raise NotImplementedError("This method is not available.")
1495
+
1496
+ def correctRawAccelData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]:
1497
+ raise NotImplementedError("This method is not available.")
1498
+
1499
+ def correctRawMagData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]:
1500
+ raise NotImplementedError("This method is not available.")
1501
+
1502
+ def formatSd(self) -> ThreespaceCmdResult[None]:
1503
+ raise NotImplementedError("This method is not available.")
1504
+
1505
+ def setDateTime(self, year: int, month: int, day: int, hour: int, minute: int, second: int) -> ThreespaceCmdResult[None]:
1506
+ raise NotImplementedError("This method is not available.")
1507
+
1508
+ def getDateTime(self) -> ThreespaceCmdResult[list[int]]:
1509
+ raise NotImplementedError("This method is not available.")
1510
+
1511
+ def tareWithCurrentOrientation(self) -> ThreespaceCmdResult[None]:
1512
+ raise NotImplementedError("This method is not available.")
1513
+
1514
+ def setBaseTareWithCurrentOrientation(self) -> ThreespaceCmdResult[None]:
1515
+ raise NotImplementedError("This method is not available.")
1516
+
1517
+ def resetFilter(self) -> ThreespaceCmdResult[None]:
1518
+ raise NotImplementedError("This method is not available.")
1519
+
1520
+ def getNumDebugMessages(self) -> ThreespaceCmdResult[int]:
1521
+ raise NotImplementedError("This method is not available.")
1522
+
1523
+ def getOldestDebugMessage(self) -> ThreespaceCmdResult[str]:
1524
+ raise NotImplementedError("This method is not available.")
1525
+
1526
+ def beginPassiveAutoCalibration(self, enabled_bitfield: int) -> ThreespaceCmdResult[None]:
1527
+ raise NotImplementedError("This method is not available.")
1528
+
1529
+ def getActivePassiveAutoCalibration(self) -> ThreespaceCmdResult[int]:
1530
+ raise NotImplementedError("This method is not available.")
1531
+
1532
+ def beginActiveAutoCalibration(self) -> ThreespaceCmdResult[None]:
1533
+ raise NotImplementedError("This method is not available.")
1534
+
1535
+ def isActiveAutoCalibrationActive(self) -> ThreespaceCmdResult[int]:
1536
+ raise NotImplementedError("This method is not available.")
1537
+
1538
+ def getStreamingLabel(self, cmd_num: int) -> ThreespaceCmdResult[str]:
1539
+ raise NotImplementedError("This method is not available.")
1540
+
1541
+ def setCursor(self, cursor_index: int) -> ThreespaceCmdResult[None]:
1542
+ raise NotImplementedError("This method is not available.")
1543
+
1544
+ def getLastLogCursorInfo(self) -> ThreespaceCmdResult[tuple[int,str]]:
1545
+ raise NotImplementedError("This method is not available.")
1546
+
1547
+ def pauseLogStreaming(self, pause: bool) -> ThreespaceCmdResult[None]:
1548
+ raise NotImplementedError("This method is not available.")
1549
+
1550
+ THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM = 84
1551
+ THREESPACE_FILE_READ_BYTES_COMMAND_NUM = 177
1552
+
1553
+ #Acutal command definitions
1554
+ _threespace_commands: list[ThreespaceCommand] = [
1555
+ #Tared Orientation
1556
+ ThreespaceCommand("getTaredOrientation", 0, "", "ffff"),
1557
+ ThreespaceCommand("getTaredOrientationAsEulerAngles", 1, "", "fff"),
1558
+ ThreespaceCommand("getTaredOrientationAsRotationMatrix", 2, "", "fffffffff"),
1559
+ ThreespaceCommand("getTaredOrientationAsAxisAngles", 3, "", "ffff"),
1560
+ ThreespaceCommand("getTaredOrientationAsTwoVector", 4, "", "ffffff"),
1561
+
1562
+ #Weird
1563
+ ThreespaceCommand("getDifferenceQuaternion", 5, "", "ffff"),
1564
+
1565
+ #Untared Orientation
1566
+ ThreespaceCommand("getUntaredOrientation", 6, "", "ffff"),
1567
+ ThreespaceCommand("getUntaredOrientationAsEulerAngles", 7, "", "fff"),
1568
+ ThreespaceCommand("getUntaredOrientationAsRotationMatrix", 8, "", "fffffffff"),
1569
+ ThreespaceCommand("getUntaredOrientationAsAxisAngles", 9, "", "ffff"),
1570
+ ThreespaceCommand("getUntaredOrientationAsTwoVector", 10, "", "ffffff"),
1571
+
1572
+ #Late orientation additions
1573
+ ThreespaceCommand("getTaredTwoVectorInSensorFrame", 11, "", "ffffff"),
1574
+ ThreespaceCommand("getUntaredTwoVectorInSensorFrame", 12, "", "ffffff"),
1575
+
1576
+ ThreespaceCommand("getPrimaryBarometerPressure", 13, "", "f"),
1577
+ ThreespaceCommand("getPrimaryBarometerAltitude", 14, "", "f"),
1578
+ ThreespaceCommand("getBarometerAltitude", 15, "b", "f"),
1579
+ ThreespaceCommand("getBarometerPressure", 16, "b", "f"),
1580
+
1581
+ ThreespaceCommand("setOffsetWithCurrentOrientation", 19, "", ""),
1582
+ ThreespaceCommand("resetBaseOffset", 20, "", ""),
1583
+ ThreespaceCommand("setBaseOffsetWithCurrentOrientation", 22, "", ""),
1584
+
1585
+ ThreespaceCommand("getAllPrimaryNormalizedData", 32, "", "fffffffff"),
1586
+ ThreespaceCommand("getPrimaryNormalizedGyroRate", 33, "", "fff"),
1587
+ ThreespaceCommand("getPrimaryNormalizedAccelVec", 34, "", "fff"),
1588
+ ThreespaceCommand("getPrimaryNormalizedMagVec", 35, "", "fff"),
1589
+
1590
+ ThreespaceCommand("getAllPrimaryCorrectedData", 37, "", "fffffffff"),
1591
+ ThreespaceCommand("getPrimaryCorrectedGyroRate", 38, "", "fff"),
1592
+ ThreespaceCommand("getPrimaryCorrectedAccelVec", 39, "", "fff"),
1593
+ ThreespaceCommand("getPrimaryCorrectedMagVec", 40, "", "fff"),
1594
+
1595
+ ThreespaceCommand("getPrimaryGlobalLinearAccel", 41, "", "fff"),
1596
+ ThreespaceCommand("getPrimaryLocalLinearAccel", 42, "", "fff"),
1597
+
1598
+ ThreespaceCommand("getTemperatureCelsius", 43, "", "f"),
1599
+ ThreespaceCommand("getTemperatureFahrenheit", 44, "", "f"),
1600
+
1601
+ ThreespaceCommand("getMotionlessConfidenceFactor", 45, "", "f"),
1602
+
1603
+ ThreespaceCommand("correctRawGyroData", 48, "fffb", "fff"),
1604
+ ThreespaceCommand("correctRawAccelData", 49, "fffb", "fff"),
1605
+ ThreespaceCommand("correctRawMagData", 50, "fffb", "fff"),
1606
+
1607
+ ThreespaceCommand("getNormalizedGyroRate", 51, "b", "fff"),
1608
+ ThreespaceCommand("getNormalizedAccelVec", 52, "b", "fff"),
1609
+ ThreespaceCommand("getNormalizedMagVec", 53, "b", "fff"),
1610
+
1611
+ ThreespaceCommand("getCorrectedGyroRate", 54, "b", "fff"),
1612
+ ThreespaceCommand("getCorrectedAccelVec", 55, "b", "fff"),
1613
+ ThreespaceCommand("getCorrectedMagVec", 56, "b", "fff"),
1614
+
1615
+ ThreespaceCommand("enableMSC", 57, "", ""),
1616
+ ThreespaceCommand("disableMSC", 58, "", ""),
1617
+
1618
+ ThreespaceCommand("formatSd", 59, "", ""),
1619
+ ThreespaceCommand("__startDataLogging", 60, "", ""),
1620
+ ThreespaceCommand("__stopDataLogging", 61, "", ""),
1621
+
1622
+ ThreespaceCommand("setDateTime", 62, "Bbbbbb", ""),
1623
+ ThreespaceCommand("getDateTime", 63, "", "Bbbbbb"),
1624
+
1625
+ ThreespaceCommand("getRawGyroRate", 65, "b", "fff"),
1626
+ ThreespaceCommand("getRawAccelVec", 66, "b", "fff"),
1627
+ ThreespaceCommand("getRawMagVec", 67, "b", "fff"),
1628
+
1629
+ ThreespaceCommand("eeptsStart", 68, "", ""),
1630
+ ThreespaceCommand("eeptsStop", 69, "", ""),
1631
+ ThreespaceCommand("eeptsGetOldestStep", 70, "", "uuddffffffbbff"),
1632
+ ThreespaceCommand("eeptsGetNewestStep", 71, "", "uuddffffffbbff"),
1633
+ ThreespaceCommand("eeptsGetNumStepsAvailable", 72, "", "b"),
1634
+ ThreespaceCommand("eeptsInsertGPS", 73, "dd", ""),
1635
+ ThreespaceCommand("eeptsAutoOffset", 74, "", ""),
1636
+
1637
+ ThreespaceCommand("getStreamingLabel", 83, "b", "S"),
1638
+ ThreespaceCommand("__getStreamingBatch", THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM, "", "S"),
1639
+ ThreespaceCommand("__startStreaming", 85, "", ""),
1640
+ ThreespaceCommand("__stopStreaming", 86, "", ""),
1641
+ ThreespaceCommand("pauseLogStreaming", 87, "b", ""),
1642
+
1643
+ ThreespaceCommand("getTimestamp", 94, "", "U"),
1644
+
1645
+ ThreespaceCommand("tareWithCurrentOrientation", 96, "", ""),
1646
+ ThreespaceCommand("setBaseTareWithCurrentOrientation", 97, "", ""),
1647
+
1648
+ ThreespaceCommand("resetFilter", 120, "", ""),
1649
+ ThreespaceCommand("getNumDebugMessages", 126, "", "B"),
1650
+ ThreespaceCommand("getOldestDebugMessage", 127, "", "S"),
1651
+
1652
+ ThreespaceCommand("beginPassiveAutoCalibration", 165, "b", ""),
1653
+ ThreespaceCommand("getActivePassiveAutoCalibration", 166, "", "b"),
1654
+ ThreespaceCommand("beginActiveAutoCalibration", 167, "", ""),
1655
+ ThreespaceCommand("isActiveAutoCalibrationActive", 168, "", "b"),
1656
+
1657
+ ThreespaceCommand("getLastLogCursorInfo", 170, "", "US"),
1658
+ ThreespaceCommand("getNextDirectoryItem", 171, "", "bsU"),
1659
+ ThreespaceCommand("changeDirectory", 172, "S", ""),
1660
+ ThreespaceCommand("openFile", 173, "S", ""),
1661
+ ThreespaceCommand("closeFile", 174, "", ""),
1662
+ ThreespaceCommand("fileGetRemainingSize", 175, "", "U"),
1663
+ ThreespaceCommand("fileReadLine", 176, "", "S"),
1664
+ ThreespaceCommand("__fileReadBytes", THREESPACE_FILE_READ_BYTES_COMMAND_NUM, "B", "S"), #This has to be handled specially as the output is variable length BYTES not STRING
1665
+ ThreespaceCommand("deleteFile", 178, "S", ""),
1666
+ ThreespaceCommand("setCursor", 179, "U", ""),
1667
+ ThreespaceCommand("__fileStartStream", 180, "", "U"),
1668
+ ThreespaceCommand("__fileStopStream", 181, "", ""),
1669
+
1670
+ ThreespaceCommand("getBatteryVoltage", 201, "", "f"),
1671
+ ThreespaceCommand("getBatteryPercent", 202, "", "b"),
1672
+ ThreespaceCommand("getBatteryStatus", 203, "", "b"),
1673
+
1674
+ ThreespaceCommand("getGpsCoord", 215, "", "dd"),
1675
+ ThreespaceCommand("getGpsAltitude", 216, "", "f"),
1676
+ ThreespaceCommand("getGpsFixState", 217, "", "b"),
1677
+ ThreespaceCommand("getGpsHdop", 218, "", "f"),
1678
+ ThreespaceCommand("getGpsSatellites", 219, "", "b"),
1679
+
1680
+ ThreespaceCommand("commitSettings", 225, "", ""),
1681
+ ThreespaceCommand("__softwareReset", 226, "", ""),
1682
+ ThreespaceCommand("__enterBootloader", 229, "", ""),
1683
+
1684
+ ThreespaceCommand("getButtonState", 250, "", "b"),
1685
+ ]
1686
+
1687
+ def threespaceCommandGet(cmd_num: int):
1688
+ for command in _threespace_commands:
1689
+ if command.info.num == cmd_num:
1690
+ return command
1691
+ return None
1692
+
1693
+ def threespaceCommandGetInfo(cmd_num: int):
1694
+ command = threespaceCommandGet(cmd_num)
1695
+ if command is None: return None
1696
+ return command.info
1697
+
1698
+ def threespaceGetHeaderLabels(header_info: ThreespaceHeaderInfo):
1699
+ order = []
1700
+ if header_info.status_enabled:
1701
+ order.append("status")
1702
+ if header_info.timestamp_enabled:
1703
+ order.append("timestamp")
1704
+ if header_info.echo_enabled:
1705
+ order.append("echo")
1706
+ if header_info.checksum_enabled:
1707
+ order.append("checksum")
1708
+ if header_info.serial_enabled:
1709
+ order.append("serial#")
1710
+ if header_info.length_enabled:
1711
+ order.append("len")
1712
+ return order