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