yostlabs 2025.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yostlabs/__init__.py +0 -0
- yostlabs/communication/__init__.py +0 -0
- yostlabs/communication/base.py +90 -0
- yostlabs/communication/serial.py +141 -0
- yostlabs/math/__init__.py +1 -0
- yostlabs/math/quaternion.py +149 -0
- yostlabs/math/vector.py +31 -0
- yostlabs/tss3/__init__.py +0 -0
- yostlabs/tss3/api.py +1712 -0
- yostlabs/tss3/consts.py +24 -0
- yostlabs/tss3/eepts.py +228 -0
- yostlabs/tss3/utils/__init__.py +0 -0
- yostlabs/tss3/utils/calibration.py +153 -0
- yostlabs/tss3/utils/parser.py +256 -0
- yostlabs/tss3/utils/streaming.py +435 -0
- yostlabs/tss3/utils/version.py +76 -0
- yostlabs-2025.1.16.dist-info/METADATA +50 -0
- yostlabs-2025.1.16.dist-info/RECORD +20 -0
- yostlabs-2025.1.16.dist-info/WHEEL +4 -0
- yostlabs-2025.1.16.dist-info/licenses/LICENSE +21 -0
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
|