yostlabs 2025.1.10__py3-none-any.whl → 2025.2.10__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 CHANGED
@@ -5,10 +5,12 @@ from yostlabs.communication.serial import ThreespaceSerialComClass
5
5
  from enum import Enum
6
6
  from dataclasses import dataclass, field
7
7
  from typing import TypeVar, Generic
8
+ from collections.abc import Callable
8
9
  import struct
9
10
  import types
10
11
  import inspect
11
12
  import time
13
+ import math
12
14
 
13
15
 
14
16
  #For converting from internal format specifiers to struct module specifiers
@@ -61,10 +63,11 @@ class ThreespaceCommand:
61
63
  BINARY_START_BYTE = 0xf7
62
64
  BINARY_START_BYTE_HEADER = 0xf9
63
65
 
64
- def __init__(self, name: str, num: int, in_format: str, out_format: str):
66
+ def __init__(self, name: str, num: int, in_format: str, out_format: str, custom_func: Callable = None):
65
67
  self.info = ThreespaceCommandInfo(name, num, in_format, out_format)
66
68
  self.in_format = _3space_format_to_external(self.info.in_format)
67
69
  self.out_format = _3space_format_to_external(self.info.out_format)
70
+ self.custom_func = custom_func
68
71
 
69
72
  def format_cmd(self, *args, header_enabled=False):
70
73
  cmd_data = struct.pack("<B", self.info.num)
@@ -86,23 +89,29 @@ class ThreespaceCommand:
86
89
  def parse_response(self, response: bytes):
87
90
  if self.info.num_out_params == 0: return None
88
91
  output = []
89
- for c in self.out_format:
90
- if c != 's':
91
- format_str = f"<{c}"
92
- size = struct.calcsize(format_str)
93
- output.append(struct.unpack(format_str, response[:size])[0])
94
- response = response[size:]
95
- else: #Strings are special, find the null terminator
96
- str_len = response.index(0)
97
- output.append(struct.unpack(f"<{str_len}s", response[str_len])[0])
98
- response = response[str_len + 1:] #+1 to skip past the null terminator character too
92
+
93
+ if math.isnan(self.info.out_size): #Has strings in it, must slow parse
94
+ for c in self.out_format:
95
+ if c != 's':
96
+ format_str = f"<{c}"
97
+ size = struct.calcsize(format_str)
98
+ output.append(struct.unpack(format_str, response[:size])[0])
99
+ #TODO: Switch to using numpy views instead of slicing
100
+ response = response[size:]
101
+ else: #Strings are special, find the null terminator
102
+ str_len = response.index(0)
103
+ output.append(struct.unpack(f"<{str_len}s", response[str_len])[0])
104
+ response = response[str_len + 1:] #+1 to skip past the null terminator character too
105
+ else: #Fast parse because no strings
106
+ output.extend(struct.unpack(f"<{self.out_format}", response[:self.info.out_size]))
107
+
99
108
 
100
109
  if self.info.num_out_params == 1:
101
110
  return output[0]
102
111
  return output
103
112
 
104
113
  #Read the command dynamically from an input stream
105
- def read_command(self, com: ThreespaceInputStream):
114
+ def read_command(self, com: ThreespaceInputStream, verbose=False):
106
115
  raw = bytearray([])
107
116
  if self.info.num_out_params == 0: return None, raw
108
117
  output = []
@@ -113,14 +122,16 @@ class ThreespaceCommand:
113
122
  response = com.read(size)
114
123
  raw += response
115
124
  if len(response) != size:
116
- print(f"Failed to read {c} type. Aborting...")
125
+ if verbose:
126
+ print(f"Failed to read {c} type. Aborting...")
117
127
  return None
118
128
  output.append(struct.unpack(format_str, response)[0])
119
129
  else: #Strings are special, find the null terminator
120
130
  response = com.read(1)
121
131
  raw += response
122
132
  if len(response) != 1:
123
- print(f"Failed to read string. Aborting...")
133
+ if verbose:
134
+ print(f"Failed to read string. Aborting...")
124
135
  return None
125
136
  byte = chr(response[0])
126
137
  string = ""
@@ -130,7 +141,8 @@ class ThreespaceCommand:
130
141
  response = com.read(1)
131
142
  raw += response
132
143
  if len(response) != 1:
133
- print(f"Failed to read string. Aborting...")
144
+ if verbose:
145
+ print(f"Failed to read string. Aborting...")
134
146
  return None
135
147
  byte = chr(response[0])
136
148
  output.append(string)
@@ -150,6 +162,7 @@ class ThreespaceGetStreamingBatchCommand(ThreespaceCommand):
150
162
  def set_stream_slots(self, streaming_slots: list[ThreespaceCommand]):
151
163
  self.commands = streaming_slots
152
164
  self.out_format = ''.join(slot.out_format for slot in streaming_slots if slot is not None)
165
+ self.info.out_size = struct.calcsize(f"<{self.out_format}")
153
166
 
154
167
  def parse_response(self, response: bytes):
155
168
  data = []
@@ -161,7 +174,7 @@ class ThreespaceGetStreamingBatchCommand(ThreespaceCommand):
161
174
 
162
175
  return data
163
176
 
164
- def read_command(self, com: ThreespaceInputStream):
177
+ def read_command(self, com: ThreespaceInputStream, verbose=False):
165
178
  #Get the response to all the streaming commands
166
179
  response = []
167
180
  raw_response = bytearray([])
@@ -404,6 +417,7 @@ class StreamableCommands(Enum):
404
417
 
405
418
  THREESPACE_AWAIT_COMMAND_FOUND = 0
406
419
  THREESPACE_AWAIT_COMMAND_TIMEOUT = 1
420
+ THREESPACE_AWAIT_BOOTLOADER = 2
407
421
 
408
422
  T = TypeVar('T')
409
423
 
@@ -450,7 +464,7 @@ class ThreespaceBootloaderInfo:
450
464
  THREESPACE_REQUIRED_HEADER = THREESPACE_HEADER_ECHO_BIT | THREESPACE_HEADER_CHECKSUM_BIT | THREESPACE_HEADER_LENGTH_BIT
451
465
  class ThreespaceSensor:
452
466
 
453
- def __init__(self, com = None, timeout=2):
467
+ def __init__(self, com = None, timeout=2, verbose=False):
454
468
  if com is None: #Default to attempting to use the serial com class if none is provided
455
469
  com = ThreespaceSerialComClass
456
470
 
@@ -473,22 +487,15 @@ class ThreespaceSensor:
473
487
  except:
474
488
  raise ValueError("Failed to create default ThreespaceSerialComClass from parameter:", type(com), com)
475
489
 
476
- self.com.read_all() #Clear anything that may be there
477
-
478
- self.commands: list[ThreespaceCommand] = [None] * 256
479
- self.getStreamingBatchCommand: ThreespaceGetStreamingBatchCommand = None
480
- self.funcs = {}
481
- for command in _threespace_commands:
482
- #Some commands are special and need added specially
483
- if command.info.num == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM:
484
- self.getStreamingBatchCommand = ThreespaceGetStreamingBatchCommand([])
485
- command = self.getStreamingBatchCommand
486
-
487
- self.__add_command(command)
488
-
489
- self.immediate_debug = False
490
+ self.immediate_debug = True #Assume it is on from the start. May cause it to take slightly longer to initialize, but prevents breaking if it is on
491
+ #Callback gives the debug message and sensor object that caused it
492
+ self.__debug_cache: list[str] = [] #Used for storing startup debug messages until sensor state is confirmed
493
+
494
+ self.verbose = verbose
495
+ self.debug_callback: Callable[[str, ThreespaceSensor],None] = self.__default_debug_callback
490
496
  self.misaligned = False
491
497
  self.dirty_cache = False
498
+ self.header_info = ThreespaceHeaderInfo()
492
499
  self.header_enabled = True
493
500
 
494
501
  #All the different streaming options
@@ -499,12 +506,27 @@ class ThreespaceSensor:
499
506
 
500
507
  #Used to ensure connecting to the correct sensor when reconnecting
501
508
  self.serial_number = None
509
+ self.short_serial_number = None
510
+ self.sensor_family = None
511
+ self.firmware_version = None
512
+
513
+ self.commands: list[ThreespaceCommand] = [None] * 256
514
+ self.getStreamingBatchCommand: ThreespaceGetStreamingBatchCommand = None
515
+ self.funcs = {}
502
516
 
503
517
  self.__cached_in_bootloader = self.__check_bootloader_status()
504
518
  if not self.in_bootloader:
505
519
  self.__firmware_init()
506
520
  else:
507
- self.serial_number = self.bootloader_get_sn()
521
+ self.__cache_serial_number(self.bootloader_get_sn())
522
+ self.__empty_debug_cache()
523
+
524
+ #Just a helper for outputting information
525
+ def log(self, *args):
526
+ if self.verbose:
527
+ print(*args)
528
+
529
+ #-----------------------INITIALIZIATION & REINITIALIZATION-----------------------------------
508
530
 
509
531
  def __firmware_init(self):
510
532
  """
@@ -512,9 +534,13 @@ class ThreespaceSensor:
512
534
  Called for powerup events when booting into firmware
513
535
  """
514
536
  self.dirty_cache = False #No longer dirty cause initializing
515
-
516
- self.com.read_all() #Clear anything that may be there
517
537
 
538
+ #Only reinitialize settings if detected firmware version changed (Or on startup)
539
+ version = self.get_settings("version_firmware")
540
+ if version != self.firmware_version:
541
+ self.firmware_version = version
542
+ self.__initialize_commands()
543
+
518
544
  self.__reinit_firmware()
519
545
 
520
546
  self.valid_mags = self.__get_valid_components("valid_mags")
@@ -522,16 +548,10 @@ class ThreespaceSensor:
522
548
  self.valid_gyros = self.__get_valid_components("valid_gyros")
523
549
  self.valid_baros = self.__get_valid_components("valid_baros")
524
550
 
525
- def __get_valid_components(self, key: str):
526
- valid = self.get_settings(key)
527
- if len(valid) == 0: return []
528
- return [int(v) for v in valid.split(',')]
529
-
530
551
  def __reinit_firmware(self):
531
552
  """
532
553
  Called when settings may have changed but a full reboot did not occur
533
554
  """
534
- self.com.read_all() #Clear anything that may be there
535
555
  self.dirty_cache = False #No longer dirty cause initializing
536
556
 
537
557
  self.header_info = ThreespaceHeaderInfo()
@@ -543,26 +563,68 @@ class ThreespaceSensor:
543
563
  self.file_stream_length = 0
544
564
 
545
565
  self.streaming_packet_size = 0
546
- self.header_enabled = True
547
566
  self._force_stop_streaming()
548
567
 
549
568
  #Now reinitialize the cached settings
550
569
  self.__cache_header_settings()
551
- self.cache_streaming_settings()
570
+ self.__cache_streaming_settings()
552
571
 
553
- self.serial_number = int(self.get_settings("serial_number"), 16)
572
+ self.__cache_serial_number(int(self.get_settings("serial_number"), 16))
573
+ self.__empty_debug_cache()
554
574
  self.immediate_debug = int(self.get_settings("debug_mode")) == 1 #Needed for some startup processes when restarting
555
575
 
576
+ def __initialize_commands(self):
577
+ self.commands: list[ThreespaceCommand] = [None] * 256
578
+ self.getStreamingBatchCommand: ThreespaceGetStreamingBatchCommand = None
579
+ self.funcs = {}
580
+
581
+ valid_commands = self.get_settings("valid_commands")
582
+ if valid_commands == THREESPACE_GET_SETTINGS_ERROR_RESPONSE:
583
+ #Treat all commands as valid because firmware is too old to have this setting
584
+ valid_commands = list(range(256))
585
+ self.log("Please update firmware to a version that contains ?valid_commands")
586
+ else:
587
+ valid_commands = list(int(v) for v in valid_commands.split(','))
588
+
589
+ for command in _threespace_commands:
590
+ #Skip commands that are not valid for this sensor
591
+ if command.info.num not in valid_commands:
592
+ #Register as invalid.
593
+ setattr(self, command.info.name, self.__invalid_command)
594
+ continue
595
+
596
+ #Some commands are special and need added specially
597
+ if command.info.num == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM:
598
+ self.getStreamingBatchCommand = ThreespaceGetStreamingBatchCommand([])
599
+ command = self.getStreamingBatchCommand
600
+
601
+ self.__add_command(command)
602
+
603
+ #------------------------------INITIALIZATION HELPERS--------------------------------------------
604
+
605
+ def __get_valid_components(self, key: str):
606
+ valid = self.get_settings(key)
607
+ if len(valid) == 0: return []
608
+ return [int(v) for v in valid.split(',')]
609
+
556
610
  def __add_command(self, command: ThreespaceCommand):
557
611
  if self.commands[command.info.num] != None:
558
- print(f"Registering duplicate command: {command.info.num} {self.commands[command.info.num].info.name} {command.info.name}")
612
+ self.log(f"Registering duplicate command: {command.info.num} {self.commands[command.info.num].info.name} {command.info.name}")
559
613
  self.commands[command.info.num] = command
560
614
 
561
- #Build the actual method for executing the command
562
- code = f"def {command.info.name}(self, *args):\n"
563
- code += f" return self.execute_command(self.commands[{command.info.num}], *args)"
564
- exec(code, globals(), self.funcs)
565
- setattr(self, command.info.name, types.MethodType(self.funcs[command.info.name], self))
615
+ #This command type has special logic that requires its own function.
616
+ #Make that function be called instead of using the generic execute that gets built
617
+ method = None
618
+ if command.custom_func is not None:
619
+ method = types.MethodType(command.custom_func, self)
620
+ else:
621
+ #Build the actual method for executing the command
622
+ code = f"def {command.info.name}(self, *args):\n"
623
+ code += f" return self.execute_command(self.commands[{command.info.num}], *args)"
624
+ exec(code, globals(), self.funcs)
625
+ method = types.MethodType(self.funcs[command.info.name], self)
626
+
627
+ setattr(self, command.info.name, method)
566
628
 
567
629
  def __get_command(self, command_name: str):
568
630
  for command in self.commands:
@@ -570,10 +632,107 @@ class ThreespaceSensor:
570
632
  if command.info.name == command_name:
571
633
  return command
572
634
  return None
635
+
636
+ def __attempt_rediscover_self(self):
637
+ """
638
+ Trys to change the com class currently being used to be a detected
639
+ com class with the same serial number. Useful for re-enumeration, such as when
640
+ entering bootloader and using USB.
641
+ """
642
+ for potential_com in self.com.auto_detect():
643
+ potential_com.open()
644
+ sensor = ThreespaceSensor(potential_com)
645
+ if sensor.serial_number == self.serial_number:
646
+ self.com = potential_com
647
+ return True
648
+ sensor.cleanup() #Handles closing the potential_com
649
+ return False
573
650
 
574
- @property
575
- def is_streaming(self):
576
- return self.is_data_streaming or self.is_log_streaming or self.is_file_streaming
651
+ def __cache_header_settings(self):
652
+ """
653
+ Should be called any time changes are made to the header. Will normally be called via the check_dirty/reinit
654
+ """
655
+ result = self.get_settings("header")
656
+ header = int(result)
657
+ #API requires these bits to be enabled, so don't let them be disabled
658
+ required_header = header | THREESPACE_REQUIRED_HEADER
659
+ if header == self.header_info.bitfield and header == required_header: return #Nothing to update
660
+
661
+ #Don't allow the header to change while streaming
662
+ #This is to prevent a situation where the header for streaming and commands are different
663
+ #since streaming caches the header. This would cause an issue where the echo byte could be in seperate
664
+ #positions, causing a situation where parsing a command and streaming at the same time breaks since it thinks both are valid cmd echoes.
665
+ if self.is_streaming:
666
+ self.log("Preventing header change due to currently streaming")
667
+ self.set_settings(header=self.header_info.bitfield)
668
+ return
669
+
670
+ if required_header != header:
671
+ self.log(f"Forcing header checksum, echo, and length enabled")
672
+ self.set_settings(header=required_header)
673
+ return
674
+
675
+ #Current/New header is valid, so can cache it
676
+ self.header_info.bitfield = header
677
+ self.cmd_echo_byte_index = self.header_info.get_start_byte(THREESPACE_HEADER_ECHO_BIT) #Needed for cmd validation while streaming
678
+
679
+ def __cache_serial_number(self, serial_number: int):
680
+ """
681
+ Doesn't actually retrieve the serial number, rather sets various properties based on the serial number
682
+ """
683
+ self.serial_number = serial_number
684
+
685
+ #Short SN is the 32 bit version of the u64 serial number
686
+ #It is defined as the FamilyVersion (byte) << 24 | Incrementor (24 bits)
687
+ family = (self.serial_number & THREESPACE_SN_FAMILY_MSK) >> THREESPACE_SN_FAMILY_POS
688
+ incrementor = (self.serial_number & THREESPACE_SN_INCREMENTOR_MSK) >> THREESPACE_SN_INCREMENTOR_POS
689
+ self.short_serial_number = family << 24 | incrementor
690
+ self.sensor_family = THREESPACE_SN_FAMILY_TO_NAME.get(family)
691
+ if self.sensor_family is None:
692
+ self.log(f"Unknown Sensor Family detected, {family}")
693
+
694
+
695
+ #--------------------------------REINIT/DIRTY Helpers-----------------------------------------------
696
+ def set_cached_settings_dirty(self):
697
+ """
698
+ Could be streaming settings, header settings...
699
+ Basically the sensor needs reinitialized
700
+ """
701
+ self.dirty_cache = True
702
+
703
+ def check_dirty(self):
704
+ if not self.dirty_cache: return
705
+ 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
706
+ success = self.__attempt_rediscover_self()
707
+ if not success:
708
+ raise RuntimeError("Sensor connection lost")
709
+
710
+ 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.
711
+ was_in_bootloader = self.__cached_in_bootloader
712
+ self.__cached_in_bootloader = self.__check_bootloader_status()
713
+
714
+ if was_in_bootloader and not self.__cached_in_bootloader: #Just Exited bootloader, need to fully reinit
715
+ self.__firmware_init()
716
+ elif not self.__cached_in_bootloader: #Was already in firmware, so only need to partially reinit
717
+ self.__reinit_firmware() #Partially init when just naturally dirty
718
+ self.dirty_cache = False
719
+
720
+ #-----------------------------------DEBUG COMMANDS---------------------------------------------------
721
+ def __default_debug_callback(self, msg: str, sensor: "ThreespaceSensor"):
722
+ if self.serial_number is None:
723
+ self.__debug_cache.append(msg.strip())
724
+ else:
725
+ print(f"DEBUG {hex(self.serial_number)}:", msg.strip())
726
+
727
+ def __empty_debug_cache(self):
728
+ for msg in self.__debug_cache:
729
+ print(f"DEBUG {hex(self.serial_number)}:", msg)
730
+ self.__debug_cache.clear()
731
+
732
+ def set_debug_callback(self, callback: Callable[[str, "ThreespaceSensor"], None]):
733
+ self.debug_callback = callback
734
+
735
+ #-----------------------------------------------BASE SETTINGS PROTOCOL------------------------------------------------
577
736
 
578
737
  #Can't just do if "header" in string because log_header_enabled exists and doesn't actually require cacheing the header
579
738
  HEADER_KEYS = ["header", "header_status", "header_timestamp", "header_echo", "header_checksum", "header_serial", "header_length"]
@@ -593,10 +752,18 @@ class ThreespaceSensor:
593
752
  params.append(f"{key}={value}")
594
753
  cmd = f"!{';'.join(params)}\n"
595
754
 
755
+ if len(cmd) > 2048:
756
+ self.log("Too many settings in one set_settings call. Max str length is 2048 but got", len(cmd))
757
+ return 0xFF, 0xFF
758
+
596
759
  #For dirty check
597
760
  keys = cmd[1:-1].split(';')
598
761
  keys = [v.split('=')[0] for v in keys]
599
762
 
763
+ #Must enable this before sending the set so can properly handle reading the response
764
+ if "debug_mode=1" in cmd:
765
+ self.immediate_debug = True
766
+
600
767
  #Send cmd
601
768
  self.com.write(cmd.encode())
602
769
 
@@ -604,50 +771,18 @@ class ThreespaceSensor:
604
771
  err = 3
605
772
  num_successes = 0
606
773
 
607
- #Read response
608
- if self.is_streaming: #Streaming have to read via peek and also validate it more
609
- max_response_length = len("255,255\r\n")
610
- found_response = False
611
- start_time = time.time()
612
- while not found_response: #Infinite loop to wait for the data to be available
613
- if time.time() - start_time > self.com.timeout:
614
- print("Timed out waiting for set_settings response")
615
- return err, num_successes
616
- line = ""
617
- while True: #A loop used to allow breaking out of to be less wet.
618
- line = self.com.peekline(max_length=max_response_length)
619
- if b'\n' not in line:
620
- break
621
-
622
- try:
623
- values = line.decode().strip()
624
- values = values.split(',')
625
- if len(values) != 2: break
626
- err = int(values[0])
627
- num_successes = int(values[1])
628
- except: break
629
- if err > 255 or num_successes > 255:
630
- break
631
-
632
- #Successfully got pass all the checks!
633
- #Consume the buffer and continue
634
- found_response = True
635
- self.com.readline()
636
- break
637
- if found_response: break
638
- while not self.updateStreaming(max_checks=1): pass #Wait for streaming to parse something!
639
- else:
640
- #When not streaming, way more straight forward
641
- try:
642
- response = self.com.readline()
643
- response = response.decode().strip()
644
- err, num_successes = response.split(',')
645
- err = int(err)
646
- num_successes = int(num_successes)
647
- except:
648
- print("Failed to parse set response:", response)
649
- return err, num_successes
650
-
774
+ response = self.__await_set_settings(self.com.timeout)
775
+ if response == THREESPACE_AWAIT_COMMAND_TIMEOUT:
776
+ self.log("Failed to get set_settings response")
777
+ return err, num_successes
778
+
779
+ #Decode response
780
+ response = self.com.readline()
781
+ response = response.decode().strip()
782
+ err, num_successes = response.split(',')
783
+ err = int(err)
784
+ num_successes = int(num_successes)
785
+
651
786
  #Handle updating state variables based on settings
652
787
  #If the user modified the header, need to cache the settings so the API knows how to interpret responses
653
788
  if "header" in cmd.lower(): #First do a quick check
@@ -655,13 +790,14 @@ class ThreespaceSensor:
655
790
  self.__cache_header_settings()
656
791
 
657
792
  if "stream_slots" in cmd.lower():
658
- self.cache_streaming_settings()
793
+ self.__cache_streaming_settings()
659
794
 
660
- if any(v in keys for v in ("default", "reboot")): #All the settings changed, just need to mark dirty
795
+ #All the settings changed, just need to mark dirty
796
+ if any(v in keys for v in ("default", "reboot")):
661
797
  self.set_cached_settings_dirty()
662
798
 
663
799
  if err:
664
- print(f"Err setting {cmd}: {err=} {num_successes=}")
800
+ self.log(f"Err setting {cmd}: {err=} {num_successes=}")
665
801
  return err, num_successes
666
802
 
667
803
  def get_settings(self, *args: str) -> dict[str, str] | str:
@@ -672,91 +808,165 @@ class ThreespaceSensor:
672
808
  self.com.write(cmd.encode())
673
809
 
674
810
  keys = cmd[1:-1].split(';')
675
- error_response = "<KEY_ERROR>"
811
+ error_response_len = len(THREESPACE_GET_SETTINGS_ERROR_RESPONSE)
676
812
 
677
- #Wait for the response to be available if streaming
678
- #NOTE: THIS WILL NOT WORK WITH SETTINGS SUCH AS ?all ?settings or QUERY STRINGS
679
- #THIS can be worked around by first getting a setting that does echo normally, as that will allow
680
- #the sensor to determine where the ascii data actually starts.
681
- #Ex: get_settings("header", "all") would work
682
- if self.is_streaming:
683
- first_key = bytes(keys[0] + "=", 'ascii') #Add on the equals sign to try and make this less likely to conflict with binary data
684
- possible_outputs = [(len(error_response), bytes(error_response, 'ascii')), (len(first_key), first_key)]
685
- 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
686
- start_time = time.time()
687
- while True:
688
- if time.time() - start_time > self.com.timeout:
689
- print("Timeout parsing get response")
690
- return {}
691
- found_response = False
692
- for length, key in possible_outputs:
693
- possible_response = self.com.peek(length)
694
- if possible_response == key: #This the response, so break and parse
695
- found_response = True
696
- break
697
- if found_response: break
698
- while not self.updateStreaming(max_checks=1): pass #Wait for streaming to process something. May just advance due to invalid
813
+ min_resp_length = 0
814
+ for key in keys:
815
+ min_resp_length += min(len(key) + 1, error_response_len)
816
+
699
817
 
700
- #Read the response
701
- try:
702
- response = self.com.readline()
703
- if ord('\n') not in response:
704
- print("Failed to get whole line")
705
- response = response.decode().strip().split(';')
706
- except:
707
- print("Failed to parse get:", response)
818
+ response = self.__await_get_settings(min_resp_length, timeout=self.com.timeout)
819
+ if response == THREESPACE_AWAIT_COMMAND_TIMEOUT:
820
+ self.log("Requested:", cmd)
821
+ self.log("Potential response:", self.com.peekline())
822
+ raise RuntimeError("Failed to receive get_settings response")
823
+
824
+ response = self.com.readline()
825
+ response = response.decode().strip().split(';')
708
826
 
709
827
  #Build the response dict
710
828
  response_dict = {}
711
829
  for i, v in enumerate(response):
712
- if v == error_response:
713
- response_dict[keys[i]] = error_response
830
+ if v == THREESPACE_GET_SETTINGS_ERROR_RESPONSE:
831
+ response_dict[keys[i]] = THREESPACE_GET_SETTINGS_ERROR_RESPONSE
714
832
  continue
715
833
  try:
716
834
  key, value = v.split('=')
717
835
  response_dict[key] = value
718
836
  except:
719
- print("Failed to parse get:", response)
837
+ self.log("Failed to parse get value:", i, v, len(v))
720
838
 
721
839
  #Format response
722
840
  if len(response_dict) == 1:
723
841
  return list(response_dict.values())[0]
724
842
  return response_dict
725
843
 
726
- def execute_command(self, cmd: ThreespaceCommand, *args):
727
- self.check_dirty()
844
+ #-----------Base Settings Parsing----------------
728
845
 
729
- retries = 0
730
- MAX_RETRIES = 3
846
+ def __await_set_settings(self, timeout=2):
847
+ start_time = time.time()
848
+ MINIMUM_LENGTH = len("0,0\r\n")
849
+ MAXIMUM_LENGTH = len("255,255\r\n")
731
850
 
732
- while retries < MAX_RETRIES:
733
- cmd.send_command(self.com, *args, header_enabled=self.header_enabled)
734
- result = self.__await_command(cmd)
735
- if result == THREESPACE_AWAIT_COMMAND_FOUND:
736
- break
737
- retries += 1
738
-
739
- if retries == MAX_RETRIES:
740
- raise RuntimeError(f"Failed to get response to command {cmd.info.name}")
851
+ while True:
852
+ remaining_time = timeout - (time.time() - start_time)
853
+ if remaining_time <= 0:
854
+ return THREESPACE_AWAIT_COMMAND_TIMEOUT
855
+ if self.com.length < MINIMUM_LENGTH: continue
856
+
857
+ possible_response = self.com.peekline()
858
+ if b'\r\n' not in possible_response: continue
741
859
 
742
- return self.read_and_parse_command(cmd)
743
-
744
- def read_and_parse_command(self, cmd: ThreespaceCommand):
745
- if self.header_enabled:
746
- header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
747
- else:
748
- header = ThreespaceHeader()
749
- result, raw = cmd.read_command(self.com)
750
- return ThreespaceCmdResult(result, header, data_raw_binary=raw)
860
+ if len(possible_response) < MINIMUM_LENGTH:
861
+ self.__internal_update(self.__try_peek_header())
862
+ continue
863
+
864
+ #Attempt to parse the line
865
+ values = possible_response.split(b',')
866
+ if len(values) != 2:
867
+ self.__internal_update(self.__try_peek_header())
868
+ continue
869
+
870
+ v1 = 0
871
+ v2 = 0
872
+ try:
873
+ v1 = int(values[0].decode())
874
+ v2 = int(values[0].decode())
875
+ except:
876
+ self.__internal_update(self.__try_peek_header())
877
+ continue
878
+
879
+ if v1 < 0 or v1 > 255 or v2 < 0 or v2 > 255:
880
+ self.__internal_update(self.__try_peek_header())
881
+ continue
882
+
883
+ self.misaligned = False
884
+ return THREESPACE_AWAIT_COMMAND_FOUND
885
+
886
+ def __await_get_settings(self, min_resp_length: int, timeout=2, check_bootloader=False):
887
+ start_time = time.time()
888
+
889
+ while True:
890
+ remaining_time = timeout - (time.time() - start_time)
891
+ if remaining_time <= 0:
892
+ return THREESPACE_AWAIT_COMMAND_TIMEOUT
893
+
894
+ if self.com.length < min_resp_length: continue
895
+ if check_bootloader and self.com.peek(2) == b'OK':
896
+ return THREESPACE_AWAIT_BOOTLOADER
897
+
898
+ possible_response = self.com.peekline()
899
+ if b'\r\n' not in possible_response: #failed to get newline
900
+ continue
901
+
902
+ if len(possible_response) < min_resp_length:
903
+ self.__internal_update(self.__try_peek_header())
904
+ continue
905
+
906
+ #Make sure the line is all ascii data
907
+ if not possible_response.isascii():
908
+ self.__internal_update(self.__try_peek_header())
909
+ continue
910
+
911
+ #Check to make sure each potential key conforms to the standard
912
+ key_value_pairs = possible_response.decode().split(';')
913
+ err = False
914
+ for kvp in key_value_pairs:
915
+ if kvp.strip() == THREESPACE_GET_SETTINGS_ERROR_RESPONSE: continue
916
+ split = kvp.split('=')
917
+ if len(split) != 2:
918
+ err = True
919
+ break
920
+ k, v = split
921
+ if any(c in k for c in THREESPACE_SETTING_KEY_INVALID_CHARS):
922
+ err = True
923
+ break
924
+ if err:
925
+ self.__internal_update(self.__try_peek_header())
926
+ continue
927
+
928
+ self.misaligned = False
929
+ return THREESPACE_AWAIT_COMMAND_FOUND
930
+
931
+ #---------------------------------BASE COMMAND PARSING--------------------------------------
932
+ def __try_peek_header(self):
933
+ """
934
+ Attempts to retrieve a header from the com class immediately.
935
+
936
+ Returns
937
+ -------
938
+ The header retrieved, or None
939
+ """
940
+ if not self.header_enabled: return None
941
+ if self.com.length < self.header_info.size: return None
942
+ header = self.com.peek(self.header_info.size)
943
+ if len(header) != self.header_info.size: return None
944
+ header = ThreespaceHeader.from_bytes(header, self.header_info)
945
+ return header
946
+
947
+ def __peek_checksum(self, header: ThreespaceHeader, max_data_length=4096):
948
+ """
949
+ Using a header that contains the checksum and data length, calculate the checksum of the expected
950
+ data and verify if with the checksum in the header.
751
951
 
752
- def __peek_checksum(self, header: ThreespaceHeader):
952
+ Params
953
+ ------
954
+ header : The header to verify
955
+ max_data_length : The maximum size to allow from header_length. This should be set to avoid a corrupted header with an extremely large length causing a lockup/timeout
956
+ """
753
957
  header_len = len(header.raw_binary)
958
+ if header.length > max_data_length:
959
+ self.log("DATA TOO BIG:", header.length)
960
+ return False
754
961
  data = self.com.peek(header_len + header.length)[header_len:]
755
962
  if len(data) != header.length: return False
756
963
  checksum = sum(data) % 256
757
964
  return checksum == header.checksum
758
965
 
759
966
  def __await_command(self, cmd: ThreespaceCommand, timeout=2):
967
+ #Header isn't enabled, nothing can do. Just pretend we found it
968
+ if not self.header_enabled: return THREESPACE_AWAIT_COMMAND_FOUND
969
+
760
970
  start_time = time.time()
761
971
 
762
972
  #Update the streaming until the result for this command is next in the buffer
@@ -765,179 +975,137 @@ class ThreespaceSensor:
765
975
  return THREESPACE_AWAIT_COMMAND_TIMEOUT
766
976
 
767
977
  #Get potential header
768
- header = self.com.peek(self.header_info.size)
769
- if len(header) != self.header_info.size: #Wait for more data
978
+ header = self.__try_peek_header()
979
+ if header is None:
770
980
  continue
771
981
 
772
- #Check to see what this packet is a response to
773
- header = ThreespaceHeader.from_bytes(header, self.header_info)
774
982
  echo = header.echo
775
983
 
776
984
  if echo == cmd.info.num: #Cmd matches
777
- if self.__peek_checksum(header):
985
+ if self.__peek_checksum(header, max_data_length=cmd.info.out_size):
986
+ self.misaligned = False
778
987
  return THREESPACE_AWAIT_COMMAND_FOUND
779
988
 
780
989
  #Error in packet, go start realigning
781
990
  if not self.misaligned:
782
- print(f"Checksum mismatch for command {cmd.info.num}")
991
+ self.log(f"Checksum mismatch for command {cmd.info.num}")
783
992
  self.misaligned = True
784
993
  self.com.read(1)
785
994
  else:
786
995
  #It wasn't a response to the command, so may be a response to some internal system
787
- self.__internal_update(header)
996
+ self.__internal_update(header)
997
+
998
+ #------------------------------BASE INPUT PARSING--------------------------------------------
788
999
 
789
- def __internal_update(self, header: ThreespaceHeader):
1000
+ def __internal_update(self, header: ThreespaceHeader = None):
790
1001
  """
791
- This should be called after a header is obtained via a command and it is determined that it can't
792
- be in response to a synchronous command that got sent. This manages updating the streaming and realigning
793
- the data buffer
1002
+ Manages checking the datastream for asynchronous responses (Streaming, Immediate Debug Messages).
1003
+ If no data is found to match these responses, the data buffer will be considered corrupted/misaligned
1004
+ and start advancing 1 byte at a time until a message is retrieved.
1005
+ For this reason, if waiting for a synchronous command response, this should be only checked after confirming the data
1006
+ is not in response to any synchronously queued commands to avoid removing actual data bytes from the com class.
1007
+
1008
+ Parameters
1009
+ ----------
1010
+ header : ThreespaceHeader
1011
+ The header to use for checking if streaming results exist. Can optionally leave None if don't want to check streaming responses.
1012
+
1013
+ Returns
1014
+ --------
1015
+ False : Misalignment
1016
+ True : Internal Data Found/Parsed
794
1017
  """
795
1018
  checksum_match = False #Just for debugging
796
1019
 
797
- #NOTE: FOR THIS TO WORK IT IS REQUIRED THAT THE HEADER DOES NOT CHANGE WHILE STREAMING ANY FORM OF DATA.
798
- #IT IS UP TO THE API TO ENFORCE NOT ALLOWING HEADER CHANGES WHILE ANY OF THOSE THINGS ARE HAPPENING
799
- if self.is_data_streaming and header.echo == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM: #Its a streaming packet, so update streaming
800
- if checksum_match := self.__peek_checksum(header):
801
- self.__update_base_streaming()
802
- return True
803
- elif self.is_log_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
804
- if checksum_match := self.__peek_checksum(header):
805
- self.__update_log_streaming()
806
- return True
807
- elif self.is_file_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
808
- if checksum_match := self.__peek_checksum(header):
809
- self.__update_file_streaming()
810
- return True
1020
+ if header is not None:
1021
+ #NOTE: FOR THIS TO WORK IT IS REQUIRED THAT THE HEADER DOES NOT CHANGE WHILE STREAMING ANY FORM OF DATA.
1022
+ #IT IS UP TO THE API TO ENFORCE NOT ALLOWING HEADER CHANGES WHILE ANY OF THOSE THINGS ARE HAPPENING
1023
+ if self.is_data_streaming and header.echo == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM: #Its a streaming packet, so update streaming
1024
+ if checksum_match := self.__peek_checksum(header, max_data_length=self.getStreamingBatchCommand.info.out_size):
1025
+ self.__update_base_streaming()
1026
+ self.misaligned = False
1027
+ return True
1028
+ elif self.is_log_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
1029
+ if checksum_match := self.__peek_checksum(header, max_data_length=2560): #TODO: Confirm this number can be 2048 and then pound define it. Currently set to 2560 because I know that is big enough, but not optimal
1030
+ self.__update_log_streaming()
1031
+ self.misaligned = False
1032
+ return True
1033
+ elif self.is_file_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
1034
+ if checksum_match := self.__peek_checksum(header, max_data_length=THREESPACE_FILE_STREAMING_MAX_PACKET_SIZE):
1035
+ self.__update_file_streaming()
1036
+ self.misaligned = False
1037
+ return True
1038
+
1039
+ #Debug messages are possible and there is enough data to potentially be a debug message
1040
+ #NOTE: Firmware should avoid putting more then one \r\n in a debug message as they will be treated as unprocessed/misaligned characters
1041
+ if self.immediate_debug and self.com.length >= 7:
1042
+ #This peek can't be blocking so peekline can't be used
1043
+ potential_message = self.com.peek(min(self.com.length, 27)) #27 is 20 digit timestamp + " Level:"
1044
+ if b"Level:" in potential_message: #There is a debug message somewhere in the data, must validate it is the next item
1045
+ level_index = potential_message.index(b" Level:")
1046
+ partial = potential_message[:level_index]
1047
+ #There should not be a newline until the end of the message, so it shouldn't be in partial
1048
+ if partial.isascii() and partial.decode('ascii').isnumeric() and b'\r\n' not in partial:
1049
+ message = self.com.readline() #Read out the whole message!
1050
+ self.debug_callback(message.decode('ascii'), self)
1051
+ self.misaligned = False
1052
+ return True
811
1053
 
812
1054
  #The response didn't match any of the expected asynchronous streaming API responses, so assume a misalignment
813
- #and start reading through the buffer
814
- if not self.misaligned:
815
- print(f"Possible Misalignment or corruption/debug message, header {header} raw {[hex(v) for v in header.raw_binary]}, Checksum match? {checksum_match}")
816
- self.misaligned = True
817
- self.com.read(1) #Because of expected misalignment, go through buffer 1 by 1 until realigned
818
-
819
- def updateStreaming(self, max_checks=float('inf')):
820
- """
821
- Returns true if any amount of data was processed whether valid or not
822
- """
823
- if not self.is_streaming: return False
824
-
825
- #I may need to make this have a max num bytes it will process before exiting to prevent locking up on slower machines
826
- #due to streaming faster then the program runs
827
- num_checks = 0
828
- data_processed = False
829
- while num_checks < max_checks:
830
- if self.com.length < self.header_info.size:
831
- return data_processed
832
-
833
- #Get header
834
- header = self.com.peek(self.header_info.size)
1055
+ if header is not None:
1056
+ msg = f"Possible Misalignment or corruption/debug message, header {header} raw {header.raw_binary} {[hex(v) for v in header.raw_binary]}" \
1057
+ f" Checksum match? {checksum_match}"
1058
+ #f"{self.com.peek(min(self.com.length, 10))}"
1059
+ else:
1060
+ msg = "Possible Misalignment or corruption/debug message"
1061
+ #self.log("Misaligned:", self.com.peek(1))
1062
+ self.__handle_misalignment(msg)
1063
+ return False
835
1064
 
836
- #Get the header and send it to the internal update
837
- header = ThreespaceHeader.from_bytes(header, self.header_info)
838
- self.__internal_update(header)
839
- data_processed = True #Internal update always processes data. Either reads a streaming message, or advances buffer due to misalignment
840
- num_checks += 1
841
-
842
- return data_processed
1065
+ def __handle_misalignment(self, message: str = None):
1066
+ if not self.misaligned and message is not None:
1067
+ self.log(message)
1068
+ self.misaligned = True
1069
+ self.com.read(1) #Because of expected misalignment, go through buffer 1 by 1 until realigned
843
1070
 
1071
+ #-----------------------------BASE COMMAND EXECUTION-------------------------------------
844
1072
 
845
- def startStreaming(self):
846
- if self.is_data_streaming: return
1073
+ def execute_command(self, cmd: ThreespaceCommand, *args):
847
1074
  self.check_dirty()
848
- self.streaming_packets.clear()
849
1075
 
850
- self.header_enabled = True
1076
+ retries = 0
1077
+ MAX_RETRIES = 3
851
1078
 
852
- self.cache_streaming_settings()
1079
+ while retries < MAX_RETRIES:
1080
+ cmd.send_command(self.com, *args, header_enabled=self.header_enabled)
1081
+ result = self.__await_command(cmd)
1082
+ if result == THREESPACE_AWAIT_COMMAND_FOUND:
1083
+ break
1084
+ retries += 1
1085
+
1086
+ if retries == MAX_RETRIES:
1087
+ raise RuntimeError(f"Failed to get response to command {cmd.info.name}")
853
1088
 
854
- cmd = self.commands[85]
855
- cmd.send_command(self.com, header_enabled=self.header_enabled)
856
- if self.header_enabled:
857
- self.__await_command(cmd)
858
- header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
859
- else:
860
- header = ThreespaceHeader()
861
- self.is_data_streaming = True
862
- return ThreespaceCmdResult(None, header)
863
-
864
- def _force_stop_streaming(self):
865
- """
866
- This function is used to stop streaming without validating it was streaming and ignoring any output of the
867
- communication line. This is a destructive call that will lose data, but will gurantee stopping streaming
868
- and leave the communication line in a clean state
869
- """
870
- cached_header_enabled = self.header_enabled
871
- cahched_dirty = self.dirty_cache
872
-
873
- #Must set these to gurantee it doesn't try and parse a response from anything
874
- self.dirty_cache = False
875
- 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
876
- self.stopStreaming() #Just in case was streaming
877
- self.fileStopStream()
878
-
879
- #TODO: Change this to pause the data logging instead, then check the state and update
880
- self.stopDataLogging()
881
-
882
- #Restore
883
- self.header_enabled = cached_header_enabled
884
- self.dirty_cache = cahched_dirty
1089
+ return self.read_and_parse_command(cmd)
1090
+
1091
+ def __invalid_command(self, *args):
1092
+ raise NotImplementedError("This method is not available.")
885
1093
 
886
- def stopStreaming(self):
887
- self.check_dirty()
888
- cmd = self.commands[86]
889
- cmd.send_command(self.com, header_enabled=self.header_enabled)
890
- if self.header_enabled: #Header will be enabled while streaming, but this is useful for startup
891
- self.__await_command(cmd)
1094
+ def read_and_parse_command(self, cmd: ThreespaceCommand):
1095
+ if self.header_enabled:
892
1096
  header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
893
1097
  else:
894
1098
  header = ThreespaceHeader()
895
- time.sleep(0.05)
896
- while self.com.length:
897
- self.com.read_all()
898
- self.is_data_streaming = False
899
- return ThreespaceCmdResult(None, header)
900
-
901
- def set_cached_settings_dirty(self):
902
- """
903
- Could be streaming settings, header settings...
904
- Basically the sensor needs reinitialized
905
- """
906
- self.dirty_cache = True
1099
+ result, raw = cmd.read_command(self.com, verbose=self.verbose)
1100
+ return ThreespaceCmdResult(result, header, data_raw_binary=raw)
907
1101
 
908
- def __attempt_rediscover_self(self):
909
- """
910
- Trys to change the com class currently being used to be a detected
911
- com class with the same serial number. Useful for re-enumeration, such as when
912
- entering bootloader and using USB
913
- """
914
- for potential_com in self.com.auto_detect():
915
- potential_com.open()
916
- sensor = ThreespaceSensor(potential_com)
917
- if sensor.serial_number == self.serial_number:
918
- self.com = potential_com
919
- return True
920
- sensor.cleanup() #Handles closing the potential_com
921
- return False
1102
+ #-----------------------------------BASE STREAMING COMMMANDS----------------------------------------------
922
1103
 
923
- def check_dirty(self):
924
- if not self.dirty_cache: return
925
- 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
926
- success = self.__attempt_rediscover_self()
927
- if not success:
928
- raise RuntimeError("Sensor connection lost")
929
-
930
- 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.
931
- was_in_bootloader = self.__cached_in_bootloader
932
- self.__cached_in_bootloader = self.__check_bootloader_status()
933
-
934
- if was_in_bootloader and not self.__cached_in_bootloader: #Just Exited bootloader, need to fully reinit
935
- self.__firmware_init()
936
- elif not self.__cached_in_bootloader: #Was already in firmware, so only need to partially reinit
937
- self.__reinit_firmware() #Partially init when just naturally dirty
938
- self.dirty_cache = False
1104
+ @property
1105
+ def is_streaming(self):
1106
+ return self.is_data_streaming or self.is_log_streaming or self.is_file_streaming
939
1107
 
940
- def cache_streaming_settings(self):
1108
+ def __cache_streaming_settings(self):
941
1109
  cached_slots: list[ThreespaceCommand] = []
942
1110
  slots: str = self.get_settings("stream_slots")
943
1111
  slots = slots.split(',')
@@ -954,32 +1122,21 @@ class ThreespaceSensor:
954
1122
  if command == None: continue
955
1123
  self.streaming_packet_size += command.info.out_size
956
1124
 
957
- def __cache_header_settings(self):
958
- """
959
- Should be called any time changes are made to the header. Will normally be called via the check_dirty/reinit
960
- """
961
- header = int(self.get_settings("header"))
962
- #API requires these bits to be enabled, so don't let them be disabled
963
- required_header = header | THREESPACE_REQUIRED_HEADER
964
- if header == self.header_info.bitfield and header == required_header: return #Nothing to update
965
-
966
- #Don't allow the header to change while streaming
967
- #This is to prevent a situation where the header for streaming and commands are different
968
- #since streaming caches the header. This would cause an issue where the echo byte could be in seperate
969
- #positions, causing a situation where parsing a command and streaming at the same time breaks since it thinks both are valid cmd echoes.
970
- if self.is_streaming:
971
- print("PREVENTING HEADER CHANGE DUE TO CURRENTLY STREAMING")
972
- self.set_settings(header=self.header_info.bitfield)
973
- return
974
-
975
- if required_header != header:
976
- print(f"Forcing header checksum, echo, and length enabled")
977
- self.set_settings(header=required_header)
978
- return
979
-
980
- #Current/New header is valid, so can cache it
981
- self.header_info.bitfield = header
982
- self.cmd_echo_byte_index = self.header_info.get_start_byte(THREESPACE_HEADER_ECHO_BIT) #Needed for cmd validation while streaming
1125
+ def startStreaming(self) -> ThreespaceCmdResult[None]: ...
1126
+ def __startStreaming(self) -> ThreespaceCmdResult[None]:
1127
+ if self.is_data_streaming: return
1128
+ self.streaming_packets.clear()
1129
+ self.__cache_streaming_settings()
1130
+
1131
+ result = self.execute_command(self.commands[THREESPACE_START_STREAMING_COMMAND_NUM])
1132
+ self.is_data_streaming = True
1133
+ return result
1134
+
1135
+ def stopStreaming(self) -> ThreespaceCmdResult[None]: ...
1136
+ def __stopStreaming(self) -> ThreespaceCmdResult[None]:
1137
+ result = self.execute_command(self.commands[THREESPACE_STOP_STREAMING_COMMAND_NUM])
1138
+ self.is_data_streaming = False
1139
+ return result
983
1140
 
984
1141
  def __update_base_streaming(self):
985
1142
  """
@@ -1000,44 +1157,85 @@ class ThreespaceSensor:
1000
1157
  def clearStreamingPackets(self):
1001
1158
  self.streaming_packets.clear()
1002
1159
 
1003
- def fileStartStream(self) -> ThreespaceCmdResult[int]:
1004
- self.check_dirty()
1005
- self.header_enabled = True
1160
+ #This is called for all streaming types
1161
+ def updateStreaming(self, max_checks=float('inf')):
1162
+ """
1163
+ Returns true if any amount of data was processed whether valid or not. This is called for all streaming types.
1164
+ """
1165
+ if not self.is_streaming: return False
1006
1166
 
1007
- cmd = self.__get_command("__fileStartStream")
1008
- cmd.send_command(self.com, header_enabled=self.header_enabled)
1009
- self.__await_command(cmd)
1167
+ #I may need to make this have a max num bytes it will process before exiting to prevent locking up on slower machines
1168
+ #due to streaming faster then the program runs
1169
+ num_checks = 0
1170
+ data_processed = False
1171
+ while num_checks < max_checks:
1172
+ if self.com.length < self.header_info.size:
1173
+ return data_processed
1174
+
1175
+ #Get header
1176
+ header = self.com.peek(self.header_info.size)
1177
+
1178
+ #Get the header and send it to the internal update
1179
+ header = ThreespaceHeader.from_bytes(header, self.header_info)
1180
+ self.__internal_update(header)
1181
+ data_processed = True #Internal update always processes data. Either reads a streaming message, or advances buffer due to misalignment
1182
+ num_checks += 1
1010
1183
 
1011
- if self.header_enabled:
1012
- header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1013
- else:
1014
- header = ThreespaceHeader()
1184
+ return data_processed
1015
1185
 
1016
- result, raw = cmd.read_command(self.com)
1017
- self.file_stream_length = result
1018
- self.is_file_streaming = True
1186
+ #This is more so used for initialization. Its a way of stopping streaming without having to worry about parsing.
1187
+ #That way it can clean up the data stream that won't match the expected state if not already configured.
1188
+ def _force_stop_streaming(self):
1189
+ """
1190
+ This function attempts to stop all possible streaming without knowing anything about the state of the sensor.
1191
+ This includes trying to stop before any commands are even registered as valid. This is to ensure the sensor can properly
1192
+ start and recover from error conditions.
1019
1193
 
1020
- return ThreespaceCmdResult(result, header, data_raw_binary=raw)
1021
-
1022
- def fileStopStream(self) -> ThreespaceCmdResult[None]:
1023
- self.check_dirty()
1194
+ This will stop streaming without validating it was streaming and ignoring any output of the
1195
+ communication line. This is a destructive call that will lose data, but will gurantee stopping streaming
1196
+ and leave the communication line in a clean state.
1197
+ """
1198
+ cached_header_enabled = self.header_enabled
1199
+ cahched_dirty = self.dirty_cache
1024
1200
 
1025
- cmd = self.__get_command("__fileStopStream")
1026
- cmd.send_command(self.com, header_enabled=self.header_enabled)
1201
+ #Must set these to gurantee it doesn't try and parse a response from anything since don't know the state of header
1202
+ self.dirty_cache = False
1203
+ 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
1027
1204
 
1028
- if self.header_enabled: #Header will be enabled while streaming, but this is useful for startup
1029
- self.__await_command(cmd)
1030
- header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1031
- else:
1032
- header = ThreespaceHeader()
1205
+ #NOTE that commands are accessed directly from the global table instead of commands registered to this sensor object
1206
+ #since this sensor object may have yet to register these commands when calling force_stop_streaming
1207
+
1208
+ #Stop base Streaming
1209
+ self.execute_command(threespaceCommandGetByName("stopStreaming"))
1210
+ self.is_data_streaming = False
1211
+
1212
+ #Stop file streaming
1213
+ self.execute_command(threespaceCommandGetByName("fileStopStream"))
1214
+ self.is_file_streaming = False
1215
+
1216
+ #Stop logging streaming
1217
+ # #TODO: Change this to pause the data logging instead, then check the state and update
1218
+ self.execute_command(threespaceCommandGetByName("stopDataLogging"))
1219
+ self.is_log_streaming = False
1033
1220
 
1034
- #TODO: Remove me now that realignment exists and multiple things can be streaming at once
1035
- time.sleep(0.05)
1036
- while self.com.length:
1037
- self.com.read_all()
1221
+ #Restore
1222
+ self.header_enabled = cached_header_enabled
1223
+ self.dirty_cache = cahched_dirty
1224
+
1225
+ #-------------------------------------FILE STREAMING----------------------------------------------
1038
1226
 
1227
+ def fileStartStream(self) -> ThreespaceCmdResult[int]: ...
1228
+ def __fileStartStream(self) -> ThreespaceCmdResult[int]:
1229
+ result = self.execute_command(self.__get_command("fileStartStream"))
1230
+ self.file_stream_length = result.data
1231
+ self.is_file_streaming = True
1232
+ return result
1233
+
1234
+ def fileStopStream(self) -> ThreespaceCmdResult[None]: ...
1235
+ def __fileStopStream(self) -> ThreespaceCmdResult[None]:
1236
+ result = self.execute_command(self.__get_command("fileStopStream"))
1039
1237
  self.is_file_streaming = False
1040
- return ThreespaceCmdResult(None, header)
1238
+ return result
1041
1239
 
1042
1240
  def getFileStreamData(self):
1043
1241
  to_return = self.file_stream_data.copy()
@@ -1055,52 +1253,32 @@ class ThreespaceSensor:
1055
1253
  data = self.com.read(header.length)
1056
1254
  self.file_stream_data += data
1057
1255
  self.file_stream_length -= header.length
1058
- 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
1256
+ if header.length < THREESPACE_FILE_STREAMING_MAX_PACKET_SIZE or self.file_stream_length == 0: #File streaming sends in chunks of 512. If not 512, it must be the last packet
1059
1257
  self.is_file_streaming = False
1060
1258
  if self.file_stream_length != 0:
1061
- print(f"File streaming stopped due to last packet. However still expected {self.file_stream_length} more bytes.")
1259
+ self.log(f"File streaming stopped due to last packet. However still expected {self.file_stream_length} more bytes.")
1062
1260
 
1063
- def startDataLogging(self) -> ThreespaceCmdResult[None]:
1064
- self.check_dirty()
1261
+ #----------------------------DATA LOGGING--------------------------------------
1065
1262
 
1066
- self.header_enabled = True
1067
- self.cache_streaming_settings()
1263
+ def startDataLogging(self) -> ThreespaceCmdResult[None]: ...
1264
+ def __startDataLogging(self) -> ThreespaceCmdResult[None]:
1265
+ self.__cache_streaming_settings()
1068
1266
 
1069
1267
  #Must check whether streaming is being done alongside logging or not. Also configure required settings if it is
1070
1268
  streaming = bool(int(self.get_settings("log_immediate_output")))
1071
1269
  if streaming:
1072
1270
  self.set_settings(log_immediate_output_header_enabled=1,
1073
1271
  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
1074
- cmd = self.__get_command("__startDataLogging")
1075
- cmd.send_command(self.com, header_enabled=self.header_enabled)
1076
- if self.header_enabled:
1077
- self.__await_command(cmd)
1078
- header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1079
- else:
1080
- header = ThreespaceHeader()
1081
-
1272
+
1273
+ result = self.execute_command(self.__get_command("startDataLogging"))
1082
1274
  self.is_log_streaming = streaming
1083
- return ThreespaceCmdResult(None, header)
1084
-
1085
- def stopDataLogging(self) -> ThreespaceCmdResult[None]:
1086
- self.check_dirty()
1087
-
1088
- cmd = self.__get_command("__stopDataLogging")
1089
- cmd.send_command(self.com, header_enabled=self.header_enabled)
1090
-
1091
- if self.header_enabled: #Header will be enabled while streaming, but this is useful for startup
1092
- self.__await_command(cmd)
1093
- header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1094
- else:
1095
- header = ThreespaceHeader()
1096
- #TODO: Remove me now that realignment exists and multiple things can be streaming at once
1097
- if self.is_log_streaming:
1098
- time.sleep(0.05)
1099
- while self.com.length:
1100
- self.com.read_all()
1275
+ return result
1101
1276
 
1277
+ def stopDataLogging(self) -> ThreespaceCmdResult[None]: ...
1278
+ def __stopDataLogging(self) -> ThreespaceCmdResult[None]:
1279
+ result = self.execute_command(self.__get_command("stopDataLogging"))
1102
1280
  self.is_log_streaming = False
1103
- return ThreespaceCmdResult(None, header)
1281
+ return result
1104
1282
 
1105
1283
  def __update_log_streaming(self):
1106
1284
  """
@@ -1112,23 +1290,26 @@ class ThreespaceSensor:
1112
1290
  data = self.com.read(header.length)
1113
1291
  self.file_stream_data += data
1114
1292
 
1115
- def softwareReset(self):
1293
+ #---------------------------------POWER STATE CHANGING COMMANDS & BOOTLOADER------------------------------------
1294
+
1295
+ def softwareReset(self): ...
1296
+ def __softwareReset(self):
1116
1297
  self.check_dirty()
1117
- cmd = self.commands[226]
1298
+ cmd = self.commands[THREESPACE_SOFTWARE_RESET_COMMAND_NUM]
1118
1299
  cmd.send_command(self.com)
1119
1300
  self.com.close()
1301
+ #TODO: Make this actually wait instead of an arbitrary sleep length
1120
1302
  time.sleep(0.5) #Give it time to restart
1121
1303
  self.com.open()
1122
- if self.immediate_debug:
1123
- time.sleep(2) #An additional 2 seconds to ensure can clear all debug messages
1124
- self.com.read_all()
1125
1304
  self.__firmware_init()
1126
1305
 
1127
- def enterBootloader(self):
1306
+ def enterBootloader(self): ...
1307
+ def __enterBootloader(self):
1128
1308
  if self.in_bootloader: return
1129
1309
 
1130
- cmd = self.commands[229]
1310
+ cmd = self.commands[THREESPACE_ENTER_BOOTLOADER_COMMAND_NUM]
1131
1311
  cmd.send_command(self.com)
1312
+ #TODO: Make this actually wait instead of an arbitrary sleep length
1132
1313
  time.sleep(0.5) #Give it time to boot into bootloader
1133
1314
  if self.com.reenumerates:
1134
1315
  self.com.close()
@@ -1166,12 +1347,20 @@ class ThreespaceSensor:
1166
1347
  #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>
1167
1348
  bootloader = False
1168
1349
  self.com.write("UUU?UUU\n".encode())
1169
- response = self.com.read(2)
1170
- if len(response) == 0:
1171
- raise RuntimeError("Failed to discover bootloader or firmware. Is the sensor a 3.0?")
1172
- if response == b'OK':
1350
+ response = self.__await_get_settings(2, check_bootloader=True)
1351
+ if response == THREESPACE_AWAIT_COMMAND_TIMEOUT:
1352
+ self.log("Requested Bootloader, Got:")
1353
+ self.log(self.com.peek(self.com.length))
1354
+ raise RuntimeError("Failed to discover bootloader or firmware.")
1355
+ if response == THREESPACE_AWAIT_BOOTLOADER:
1173
1356
  bootloader = True
1174
- self.com.read_all() #Remove the rest of the OK responses or the rest of the <KEY_ERROR> response
1357
+ time.sleep(0.1) #Give time for all the OK responses to come in
1358
+ self.com.read_all() #Remove the rest of the OK responses or the rest of the <KEY_ERROR> response
1359
+ elif response == THREESPACE_AWAIT_COMMAND_FOUND:
1360
+ bootloader = False
1361
+ self.com.readline() #Clear the setting, no need to parse
1362
+ else:
1363
+ raise Exception("Failed to detect if in bootloader or firmware")
1175
1364
  return bootloader
1176
1365
 
1177
1366
  def bootloader_get_sn(self):
@@ -1191,11 +1380,6 @@ class ThreespaceSensor:
1191
1380
  success = self.__attempt_rediscover_self()
1192
1381
  if not success:
1193
1382
  raise RuntimeError("Failed to reconnect to sensor in firmware")
1194
- self.com.read_all() #If debug_mode=1, might be debug messages waiting
1195
- if self.immediate_debug:
1196
- print("Waiting longer before booting into firmware because immediate debug was enabled.")
1197
- time.sleep(2)
1198
- self.com.read_all()
1199
1383
  in_bootloader = self.__check_bootloader_status()
1200
1384
  if in_bootloader:
1201
1385
  raise RuntimeError("Failed to exit bootloader")
@@ -1249,299 +1433,134 @@ class ThreespaceSensor:
1249
1433
  self.fileStopStream()
1250
1434
  if self.is_log_streaming:
1251
1435
  self.stopDataLogging()
1252
- #self.closeFile() #May not be opened, but also not cacheing that so just attempt to close. Currently commented out because breaks embedded
1436
+
1437
+ #The sensor may or may not have this command registered. So just try it
1438
+ try:
1439
+ #May not be opened, but also not cacheing that so just attempt to close.
1440
+ self.closeFile()
1441
+ except: pass
1253
1442
  self.com.close()
1254
1443
 
1255
1444
  #-------------------------START ALL PROTOTYPES------------------------------------
1256
1445
 
1257
- def eeptsStart(self) -> ThreespaceCmdResult[None]:
1258
- raise NotImplementedError("This method is not available.")
1259
-
1260
- def eeptsStop(self) -> ThreespaceCmdResult[None]:
1261
- raise NotImplementedError("This method is not available.")
1262
-
1263
- def eeptsGetOldestStep(self) -> ThreespaceCmdResult[list]:
1264
- raise NotImplementedError("This method is not available.")
1265
-
1266
- def eeptsGetNewestStep(self) -> ThreespaceCmdResult[list]:
1267
- raise NotImplementedError("This method is not available.")
1268
-
1269
- def eeptsGetNumStepsAvailable(self) -> ThreespaceCmdResult[int]:
1270
- raise NotImplementedError("This method is not available.")
1271
-
1272
- def eeptsInsertGPS(self, latitude: float, longitude: float) -> ThreespaceCmdResult[None]:
1273
- raise NotImplementedError("This method is not available.")
1274
-
1275
- def eeptsAutoOffset(self) -> ThreespaceCmdResult[None]:
1276
- raise NotImplementedError("This method is not available.")
1277
-
1278
- def getRawGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]:
1279
- raise NotImplementedError("This method is not available.")
1280
-
1281
- def getRawAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1282
- raise NotImplementedError("This method is not available.")
1283
-
1284
- def getRawMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1285
- raise NotImplementedError("This method is not available.")
1286
-
1287
- def getTaredOrientation(self) -> ThreespaceCmdResult[list[float]]:
1288
- raise NotImplementedError("This method is not available.")
1289
-
1290
- def getTaredOrientationAsEulerAngles(self) -> ThreespaceCmdResult[list[float]]:
1291
- raise NotImplementedError("This method is not available.")
1292
-
1293
- def getTaredOrientationAsRotationMatrix(self) -> ThreespaceCmdResult[list[float]]:
1294
- raise NotImplementedError("This method is not available.")
1295
-
1296
- def getTaredOrientationAsAxisAngles(self) -> ThreespaceCmdResult[list[float]]:
1297
- raise NotImplementedError("This method is not available.")
1298
-
1299
- def getTaredOrientationAsTwoVector(self) -> ThreespaceCmdResult[list[float]]:
1300
- raise NotImplementedError("This method is not available.")
1301
-
1302
- def getDifferenceQuaternion(self) -> ThreespaceCmdResult[list[float]]:
1303
- raise NotImplementedError("This method is not available.")
1304
-
1305
- def getUntaredOrientation(self) -> ThreespaceCmdResult[list[float]]:
1306
- raise NotImplementedError("This method is not available.")
1307
-
1308
- def getUntaredOrientationAsEulerAngles(self) -> ThreespaceCmdResult[list[float]]:
1309
- raise NotImplementedError("This method is not available.")
1310
-
1311
- def getUntaredOrientationAsRotationMatrix(self) -> ThreespaceCmdResult[list[float]]:
1312
- raise NotImplementedError("This method is not available.")
1313
-
1314
- def getUntaredOrientationAsAxisAngles(self) -> ThreespaceCmdResult[list[float]]:
1315
- raise NotImplementedError("This method is not available.")
1316
-
1317
- def getUntaredOrientationAsTwoVector(self) -> ThreespaceCmdResult[list[float]]:
1318
- raise NotImplementedError("This method is not available.")
1319
-
1320
- def commitSettings(self) -> ThreespaceCmdResult[None]:
1321
- raise NotImplementedError("This method is not available.")
1322
-
1323
- def getMotionlessConfidenceFactor(self) -> ThreespaceCmdResult[float]:
1324
- raise NotImplementedError("This method is not available.")
1325
-
1326
- def enableMSC(self) -> ThreespaceCmdResult[None]:
1327
- raise NotImplementedError("This method is not available.")
1328
-
1329
- def disableMSC(self) -> ThreespaceCmdResult[None]:
1330
- raise NotImplementedError("This method is not available.")
1331
-
1332
- def getNextDirectoryItem(self) -> ThreespaceCmdResult[list[int,str,int]]:
1333
- raise NotImplementedError("This method is not available.")
1334
-
1335
- def changeDirectory(self, path: str) -> ThreespaceCmdResult[None]:
1336
- raise NotImplementedError("This method is not available.")
1337
-
1338
- def openFile(self, path: str) -> ThreespaceCmdResult[None]:
1339
- raise NotImplementedError("This method is not available.")
1340
-
1341
- def closeFile(self) -> ThreespaceCmdResult[None]:
1342
- raise NotImplementedError("This method is not available.")
1343
-
1344
- def fileGetRemainingSize(self) -> ThreespaceCmdResult[int]:
1345
- raise NotImplementedError("This method is not available.")
1346
-
1347
- def fileReadLine(self) -> ThreespaceCmdResult[str]:
1348
- raise NotImplementedError("This method is not available.")
1349
-
1350
- def fileReadBytes(self, num_bytes: int) -> ThreespaceCmdResult[bytes]:
1446
+ #To actually see how commands work, look at __initialize_commands and __add_command
1447
+ #But basically, these are all just prototypes. Information about the commands is in the table
1448
+ #beneath here, and the API simply calls its execute_command function on the Command information objects defined.
1449
+
1450
+ def eeptsStart(self) -> ThreespaceCmdResult[None]: ...
1451
+ def eeptsStop(self) -> ThreespaceCmdResult[None]: ...
1452
+ def eeptsGetOldestStep(self) -> ThreespaceCmdResult[list]: ...
1453
+ def eeptsGetNewestStep(self) -> ThreespaceCmdResult[list]: ...
1454
+ def eeptsGetNumStepsAvailable(self) -> ThreespaceCmdResult[int]: ...
1455
+ def eeptsInsertGPS(self, latitude: float, longitude: float) -> ThreespaceCmdResult[None]: ...
1456
+ def eeptsAutoOffset(self) -> ThreespaceCmdResult[None]: ...
1457
+ def getRawGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1458
+ def getRawAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1459
+ def getRawMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1460
+ def getTaredOrientation(self) -> ThreespaceCmdResult[list[float]]: ...
1461
+ def getTaredOrientationAsEulerAngles(self) -> ThreespaceCmdResult[list[float]]: ...
1462
+ def getTaredOrientationAsRotationMatrix(self) -> ThreespaceCmdResult[list[float]]: ...
1463
+ def getTaredOrientationAsAxisAngles(self) -> ThreespaceCmdResult[list[float]]: ...
1464
+ def getTaredOrientationAsTwoVector(self) -> ThreespaceCmdResult[list[float]]: ...
1465
+ def getDifferenceQuaternion(self) -> ThreespaceCmdResult[list[float]]: ...
1466
+ def getUntaredOrientation(self) -> ThreespaceCmdResult[list[float]]: ...
1467
+ def getUntaredOrientationAsEulerAngles(self) -> ThreespaceCmdResult[list[float]]: ...
1468
+ def getUntaredOrientationAsRotationMatrix(self) -> ThreespaceCmdResult[list[float]]: ...
1469
+ def getUntaredOrientationAsAxisAngles(self) -> ThreespaceCmdResult[list[float]]: ...
1470
+ def getUntaredOrientationAsTwoVector(self) -> ThreespaceCmdResult[list[float]]: ...
1471
+ def commitSettings(self) -> ThreespaceCmdResult[None]: ...
1472
+ def getMotionlessConfidenceFactor(self) -> ThreespaceCmdResult[float]: ...
1473
+ def enableMSC(self) -> ThreespaceCmdResult[None]: ...
1474
+ def disableMSC(self) -> ThreespaceCmdResult[None]: ...
1475
+ def getNextDirectoryItem(self) -> ThreespaceCmdResult[list[int,str,int]]: ...
1476
+ def changeDirectory(self, path: str) -> ThreespaceCmdResult[None]: ...
1477
+ def openFile(self, path: str) -> ThreespaceCmdResult[None]: ...
1478
+ def closeFile(self) -> ThreespaceCmdResult[None]: ...
1479
+ def fileGetRemainingSize(self) -> ThreespaceCmdResult[int]: ...
1480
+ def fileReadLine(self) -> ThreespaceCmdResult[str]: ...
1481
+ def fileReadBytes(self, num_bytes: int) -> ThreespaceCmdResult[bytes]: ...
1482
+ def __fileReadBytes(self, num_bytes: int) -> ThreespaceCmdResult[bytes]:
1351
1483
  self.check_dirty()
1352
1484
  cmd = self.commands[THREESPACE_FILE_READ_BYTES_COMMAND_NUM]
1353
1485
  cmd.send_command(self.com, num_bytes, header_enabled=self.header_enabled)
1354
1486
  self.__await_command(cmd)
1355
1487
  if self.header_enabled:
1356
1488
  header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1489
+ num_bytes = min(num_bytes, header.length) #Its possible for less bytes to be returned when an error occurs (EX: Reading from unopened file)
1357
1490
  else:
1358
1491
  header = ThreespaceHeader()
1359
1492
 
1360
1493
  response = self.com.read(num_bytes)
1361
1494
  return ThreespaceCmdResult(response, header, data_raw_binary=response)
1362
1495
 
1363
- def deleteFile(self, path: str) -> ThreespaceCmdResult[None]:
1364
- raise NotImplementedError("This method is not available.")
1365
-
1366
- def getStreamingBatch(self):
1367
- raise NotImplementedError("This method is not available.")
1368
-
1369
- def setOffsetWithCurrentOrientation(self) -> ThreespaceCmdResult[None]:
1370
- raise NotImplementedError("This method is not available.")
1371
-
1372
- def resetBaseOffset(self) -> ThreespaceCmdResult[None]:
1373
- raise NotImplementedError("This method is not available.")
1374
-
1375
- def setBaseOffsetWithCurrentOrientation(self) -> ThreespaceCmdResult[None]:
1376
- raise NotImplementedError("This method is not available.")
1377
-
1378
- def getTaredTwoVectorInSensorFrame(self) -> ThreespaceCmdResult[list[float]]:
1379
- raise NotImplementedError("This method is not available.")
1380
-
1381
- def getUntaredTwoVectorInSensorFrame(self) -> ThreespaceCmdResult[list[float]]:
1382
- raise NotImplementedError("This method is not available.")
1383
-
1384
- def getPrimaryBarometerPressure(self) -> ThreespaceCmdResult[float]:
1385
- raise NotImplementedError("This method is not available.")
1386
-
1387
- def getPrimaryBarometerAltitude(self) -> ThreespaceCmdResult[float]:
1388
- raise NotImplementedError("This method is not available.")
1389
-
1390
- def getBarometerAltitude(self, id: int) -> ThreespaceCmdResult[float]:
1391
- raise NotImplementedError("This method is not available.")
1392
-
1393
- def getBarometerPressure(self, id: int) -> ThreespaceCmdResult[float]:
1394
- raise NotImplementedError("This method is not available.")
1395
-
1396
- def getAllPrimaryNormalizedData(self) -> ThreespaceCmdResult[list[float]]:
1397
- raise NotImplementedError("This method is not available.")
1398
-
1399
- def getPrimaryNormalizedGyroRate(self) -> ThreespaceCmdResult[list[float]]:
1400
- raise NotImplementedError("This method is not available.")
1401
-
1402
- def getPrimaryNormalizedAccelVec(self) -> ThreespaceCmdResult[list[float]]:
1403
- raise NotImplementedError("This method is not available.")
1404
-
1405
- def getPrimaryNormalizedMagVec(self) -> ThreespaceCmdResult[list[float]]:
1406
- raise NotImplementedError("This method is not available.")
1407
-
1408
- def getAllPrimaryCorrectedData(self) -> ThreespaceCmdResult[list[float]]:
1409
- raise NotImplementedError("This method is not available.")
1410
-
1411
- def getPrimaryCorrectedGyroRate(self) -> ThreespaceCmdResult[list[float]]:
1412
- raise NotImplementedError("This method is not available.")
1413
-
1414
- def getPrimaryCorrectedAccelVec(self) -> ThreespaceCmdResult[list[float]]:
1415
- raise NotImplementedError("This method is not available.")
1416
-
1417
- def getPrimaryCorrectedMagVec(self) -> ThreespaceCmdResult[list[float]]:
1418
- raise NotImplementedError("This method is not available.")
1419
-
1420
- def getPrimaryGlobalLinearAccel(self) -> ThreespaceCmdResult[list[float]]:
1421
- raise NotImplementedError("This method is not available.")
1422
-
1423
- def getPrimaryLocalLinearAccel(self) -> ThreespaceCmdResult[list[float]]:
1424
- raise NotImplementedError("This method is not available.")
1425
-
1426
- def getTemperatureCelsius(self) -> ThreespaceCmdResult[float]:
1427
- raise NotImplementedError("This method is not available.")
1428
-
1429
- def getTemperatureFahrenheit(self) -> ThreespaceCmdResult[float]:
1430
- raise NotImplementedError("This method is not available.")
1431
-
1432
- def getNormalizedGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]:
1433
- raise NotImplementedError("This method is not available.")
1434
-
1435
- def getNormalizedAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1436
- raise NotImplementedError("This method is not available.")
1437
-
1438
- def getNormalizedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1439
- raise NotImplementedError("This method is not available.")
1440
-
1441
- def getCorrectedGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]:
1442
- raise NotImplementedError("This method is not available.")
1443
-
1444
- def getCorrectedAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1445
- raise NotImplementedError("This method is not available.")
1446
-
1447
- def getCorrectedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]:
1448
- raise NotImplementedError("This method is not available.")
1449
-
1450
- def enableMSC(self) -> ThreespaceCmdResult[None]:
1451
- raise NotImplementedError("This method is not available.")
1452
-
1453
- def disableMSC(self) -> ThreespaceCmdResult[None]:
1454
- raise NotImplementedError("This method is not available.")
1455
-
1456
- def getTimestamp(self) -> ThreespaceCmdResult[int]:
1457
- raise NotImplementedError("This method is not available.")
1458
-
1459
- def getBatteryVoltage(self) -> ThreespaceCmdResult[float]:
1460
- raise NotImplementedError("This method is not available.")
1461
-
1462
- def getBatteryPercent(self) -> ThreespaceCmdResult[int]:
1463
- raise NotImplementedError("This method is not available.")
1464
-
1465
- def getBatteryStatus(self) -> ThreespaceCmdResult[int]:
1466
- raise NotImplementedError("This method is not available.")
1467
-
1468
- def getGpsCoord(self) -> ThreespaceCmdResult[list[float]]:
1469
- raise NotImplementedError("This method is not available.")
1470
-
1471
- def getGpsAltitude(self) -> ThreespaceCmdResult[float]:
1472
- raise NotImplementedError("This method is not available.")
1473
-
1474
- def getGpsFixState(self) -> ThreespaceCmdResult[int]:
1475
- raise NotImplementedError("This method is not available.")
1476
-
1477
- def getGpsHdop(self) -> ThreespaceCmdResult[float]:
1478
- raise NotImplementedError("This method is not available.")
1479
-
1480
- def getGpsSatellites(self) -> ThreespaceCmdResult[int]:
1481
- raise NotImplementedError("This method is not available.")
1482
-
1483
- def getButtonState(self) -> ThreespaceCmdResult[int]:
1484
- raise NotImplementedError("This method is not available.")
1485
-
1486
- def correctRawGyroData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]:
1487
- raise NotImplementedError("This method is not available.")
1488
-
1489
- def correctRawAccelData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]:
1490
- raise NotImplementedError("This method is not available.")
1491
-
1492
- def correctRawMagData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]:
1493
- raise NotImplementedError("This method is not available.")
1494
-
1495
- def formatSd(self) -> ThreespaceCmdResult[None]:
1496
- raise NotImplementedError("This method is not available.")
1497
-
1498
- def setDateTime(self, year: int, month: int, day: int, hour: int, minute: int, second: int) -> ThreespaceCmdResult[None]:
1499
- raise NotImplementedError("This method is not available.")
1500
-
1501
- def getDateTime(self) -> ThreespaceCmdResult[list[int]]:
1502
- raise NotImplementedError("This method is not available.")
1503
-
1504
- def tareWithCurrentOrientation(self) -> ThreespaceCmdResult[None]:
1505
- raise NotImplementedError("This method is not available.")
1506
-
1507
- def setBaseTareWithCurrentOrientation(self) -> ThreespaceCmdResult[None]:
1508
- raise NotImplementedError("This method is not available.")
1509
-
1510
- def resetFilter(self) -> ThreespaceCmdResult[None]:
1511
- raise NotImplementedError("This method is not available.")
1512
-
1513
- def getNumDebugMessages(self) -> ThreespaceCmdResult[int]:
1514
- raise NotImplementedError("This method is not available.")
1515
-
1516
- def getOldestDebugMessage(self) -> ThreespaceCmdResult[str]:
1517
- raise NotImplementedError("This method is not available.")
1518
-
1519
- def beginPassiveAutoCalibration(self, enabled_bitfield: int) -> ThreespaceCmdResult[None]:
1520
- raise NotImplementedError("This method is not available.")
1521
-
1522
- def getActivePassiveAutoCalibration(self) -> ThreespaceCmdResult[int]:
1523
- raise NotImplementedError("This method is not available.")
1524
-
1525
- def beginActiveAutoCalibration(self) -> ThreespaceCmdResult[None]:
1526
- raise NotImplementedError("This method is not available.")
1527
-
1528
- def isActiveAutoCalibrationActive(self) -> ThreespaceCmdResult[int]:
1529
- raise NotImplementedError("This method is not available.")
1530
-
1531
- def getStreamingLabel(self, cmd_num: int) -> ThreespaceCmdResult[str]:
1532
- raise NotImplementedError("This method is not available.")
1533
-
1534
- def setCursor(self, cursor_index: int) -> ThreespaceCmdResult[None]:
1535
- raise NotImplementedError("This method is not available.")
1536
-
1537
- def getLastLogCursorInfo(self) -> ThreespaceCmdResult[tuple[int,str]]:
1538
- raise NotImplementedError("This method is not available.")
1539
-
1540
- def pauseLogStreaming(self, pause: bool) -> ThreespaceCmdResult[None]:
1541
- raise NotImplementedError("This method is not available.")
1496
+ def deleteFile(self, path: str) -> ThreespaceCmdResult[None]: ...
1497
+ def getStreamingBatch(self) -> ThreespaceCmdResult[list]: ...
1498
+ def setOffsetWithCurrentOrientation(self) -> ThreespaceCmdResult[None]: ...
1499
+ def resetBaseOffset(self) -> ThreespaceCmdResult[None]: ...
1500
+ def setBaseOffsetWithCurrentOrientation(self) -> ThreespaceCmdResult[None]: ...
1501
+ def getTaredTwoVectorInSensorFrame(self) -> ThreespaceCmdResult[list[float]]: ...
1502
+ def getUntaredTwoVectorInSensorFrame(self) -> ThreespaceCmdResult[list[float]]: ...
1503
+ def getPrimaryBarometerPressure(self) -> ThreespaceCmdResult[float]: ...
1504
+ def getPrimaryBarometerAltitude(self) -> ThreespaceCmdResult[float]: ...
1505
+ def getBarometerAltitude(self, id: int) -> ThreespaceCmdResult[float]: ...
1506
+ def getBarometerPressure(self, id: int) -> ThreespaceCmdResult[float]: ...
1507
+ def getAllPrimaryNormalizedData(self) -> ThreespaceCmdResult[list[float]]: ...
1508
+ def getPrimaryNormalizedGyroRate(self) -> ThreespaceCmdResult[list[float]]: ...
1509
+ def getPrimaryNormalizedAccelVec(self) -> ThreespaceCmdResult[list[float]]: ...
1510
+ def getPrimaryNormalizedMagVec(self) -> ThreespaceCmdResult[list[float]]: ...
1511
+ def getAllPrimaryCorrectedData(self) -> ThreespaceCmdResult[list[float]]: ...
1512
+ def getPrimaryCorrectedGyroRate(self) -> ThreespaceCmdResult[list[float]]: ...
1513
+ def getPrimaryCorrectedAccelVec(self) -> ThreespaceCmdResult[list[float]]: ...
1514
+ def getPrimaryCorrectedMagVec(self) -> ThreespaceCmdResult[list[float]]: ...
1515
+ def getPrimaryGlobalLinearAccel(self) -> ThreespaceCmdResult[list[float]]: ...
1516
+ def getPrimaryLocalLinearAccel(self) -> ThreespaceCmdResult[list[float]]: ...
1517
+ def getTemperatureCelsius(self) -> ThreespaceCmdResult[float]: ...
1518
+ def getTemperatureFahrenheit(self) -> ThreespaceCmdResult[float]: ...
1519
+ def getNormalizedGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1520
+ def getNormalizedAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1521
+ def getNormalizedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1522
+ def getCorrectedGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1523
+ def getCorrectedAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1524
+ def getCorrectedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1525
+ def enableMSC(self) -> ThreespaceCmdResult[None]: ...
1526
+ def disableMSC(self) -> ThreespaceCmdResult[None]: ...
1527
+ def getTimestamp(self) -> ThreespaceCmdResult[int]: ...
1528
+ def getBatteryVoltage(self) -> ThreespaceCmdResult[float]: ...
1529
+ def getBatteryPercent(self) -> ThreespaceCmdResult[int]: ...
1530
+ def getBatteryStatus(self) -> ThreespaceCmdResult[int]: ...
1531
+ def getGpsCoord(self) -> ThreespaceCmdResult[list[float]]: ...
1532
+ def getGpsAltitude(self) -> ThreespaceCmdResult[float]: ...
1533
+ def getGpsFixState(self) -> ThreespaceCmdResult[int]: ...
1534
+ def getGpsHdop(self) -> ThreespaceCmdResult[float]: ...
1535
+ def getGpsSatellites(self) -> ThreespaceCmdResult[int]: ...
1536
+ def getButtonState(self) -> ThreespaceCmdResult[int]: ...
1537
+ def correctRawGyroData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]: ...
1538
+ def correctRawAccelData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]: ...
1539
+ def correctRawMagData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]: ...
1540
+ def formatSd(self) -> ThreespaceCmdResult[None]: ...
1541
+ def setDateTime(self, year: int, month: int, day: int, hour: int, minute: int, second: int) -> ThreespaceCmdResult[None]: ...
1542
+ def getDateTime(self) -> ThreespaceCmdResult[list[int]]: ...
1543
+ def tareWithCurrentOrientation(self) -> ThreespaceCmdResult[None]: ...
1544
+ def setBaseTareWithCurrentOrientation(self) -> ThreespaceCmdResult[None]: ...
1545
+ def resetFilter(self) -> ThreespaceCmdResult[None]: ...
1546
+ def getNumDebugMessages(self) -> ThreespaceCmdResult[int]: ...
1547
+ def getOldestDebugMessage(self) -> ThreespaceCmdResult[str]: ...
1548
+ def selfTest(self) -> ThreespaceCmdResult[int]: ...
1549
+ def beginPassiveAutoCalibration(self, enabled_bitfield: int) -> ThreespaceCmdResult[None]: ...
1550
+ def getActivePassiveAutoCalibration(self) -> ThreespaceCmdResult[int]: ...
1551
+ def beginActiveAutoCalibration(self) -> ThreespaceCmdResult[None]: ...
1552
+ def isActiveAutoCalibrationActive(self) -> ThreespaceCmdResult[int]: ...
1553
+ def getStreamingLabel(self, cmd_num: int) -> ThreespaceCmdResult[str]: ...
1554
+ def setCursor(self, cursor_index: int) -> ThreespaceCmdResult[None]: ...
1555
+ def getLastLogCursorInfo(self) -> ThreespaceCmdResult[tuple[int,str]]: ...
1556
+ def pauseLogStreaming(self, pause: bool) -> ThreespaceCmdResult[None]: ...
1542
1557
 
1543
1558
  THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM = 84
1559
+ THREESPACE_START_STREAMING_COMMAND_NUM = 85
1560
+ THREESPACE_STOP_STREAMING_COMMAND_NUM = 86
1544
1561
  THREESPACE_FILE_READ_BYTES_COMMAND_NUM = 177
1562
+ THREESPACE_SOFTWARE_RESET_COMMAND_NUM = 226
1563
+ THREESPACE_ENTER_BOOTLOADER_COMMAND_NUM = 229
1545
1564
 
1546
1565
  #Acutal command definitions
1547
1566
  _threespace_commands: list[ThreespaceCommand] = [
@@ -1609,8 +1628,8 @@ _threespace_commands: list[ThreespaceCommand] = [
1609
1628
  ThreespaceCommand("disableMSC", 58, "", ""),
1610
1629
 
1611
1630
  ThreespaceCommand("formatSd", 59, "", ""),
1612
- ThreespaceCommand("__startDataLogging", 60, "", ""),
1613
- ThreespaceCommand("__stopDataLogging", 61, "", ""),
1631
+ ThreespaceCommand("startDataLogging", 60, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__startDataLogging),
1632
+ ThreespaceCommand("stopDataLogging", 61, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__stopDataLogging),
1614
1633
 
1615
1634
  ThreespaceCommand("setDateTime", 62, "Bbbbbb", ""),
1616
1635
  ThreespaceCommand("getDateTime", 63, "", "Bbbbbb"),
@@ -1628,9 +1647,9 @@ _threespace_commands: list[ThreespaceCommand] = [
1628
1647
  ThreespaceCommand("eeptsAutoOffset", 74, "", ""),
1629
1648
 
1630
1649
  ThreespaceCommand("getStreamingLabel", 83, "b", "S"),
1631
- ThreespaceCommand("__getStreamingBatch", THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM, "", "S"),
1632
- ThreespaceCommand("__startStreaming", 85, "", ""),
1633
- ThreespaceCommand("__stopStreaming", 86, "", ""),
1650
+ ThreespaceCommand("getStreamingBatch", THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM, "", "S"),
1651
+ ThreespaceCommand("startStreaming", THREESPACE_START_STREAMING_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__startStreaming),
1652
+ ThreespaceCommand("stopStreaming", THREESPACE_STOP_STREAMING_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__stopStreaming),
1634
1653
  ThreespaceCommand("pauseLogStreaming", 87, "b", ""),
1635
1654
 
1636
1655
  ThreespaceCommand("getTimestamp", 94, "", "U"),
@@ -1641,6 +1660,7 @@ _threespace_commands: list[ThreespaceCommand] = [
1641
1660
  ThreespaceCommand("resetFilter", 120, "", ""),
1642
1661
  ThreespaceCommand("getNumDebugMessages", 126, "", "B"),
1643
1662
  ThreespaceCommand("getOldestDebugMessage", 127, "", "S"),
1663
+ ThreespaceCommand("selfTest", 128, "", "u"),
1644
1664
 
1645
1665
  ThreespaceCommand("beginPassiveAutoCalibration", 165, "b", ""),
1646
1666
  ThreespaceCommand("getActivePassiveAutoCalibration", 166, "", "b"),
@@ -1654,11 +1674,11 @@ _threespace_commands: list[ThreespaceCommand] = [
1654
1674
  ThreespaceCommand("closeFile", 174, "", ""),
1655
1675
  ThreespaceCommand("fileGetRemainingSize", 175, "", "U"),
1656
1676
  ThreespaceCommand("fileReadLine", 176, "", "S"),
1657
- 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
1677
+ ThreespaceCommand("fileReadBytes", THREESPACE_FILE_READ_BYTES_COMMAND_NUM, "B", "S", custom_func=ThreespaceSensor._ThreespaceSensor__fileReadBytes), #This has to be handled specially as the output is variable length BYTES not STRING
1658
1678
  ThreespaceCommand("deleteFile", 178, "S", ""),
1659
1679
  ThreespaceCommand("setCursor", 179, "U", ""),
1660
- ThreespaceCommand("__fileStartStream", 180, "", "U"),
1661
- ThreespaceCommand("__fileStopStream", 181, "", ""),
1680
+ ThreespaceCommand("fileStartStream", 180, "", "U", custom_func=ThreespaceSensor._ThreespaceSensor__fileStartStream),
1681
+ ThreespaceCommand("fileStopStream", 181, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__fileStopStream),
1662
1682
 
1663
1683
  ThreespaceCommand("getBatteryVoltage", 201, "", "f"),
1664
1684
  ThreespaceCommand("getBatteryPercent", 202, "", "b"),
@@ -1671,8 +1691,8 @@ _threespace_commands: list[ThreespaceCommand] = [
1671
1691
  ThreespaceCommand("getGpsSatellites", 219, "", "b"),
1672
1692
 
1673
1693
  ThreespaceCommand("commitSettings", 225, "", ""),
1674
- ThreespaceCommand("__softwareReset", 226, "", ""),
1675
- ThreespaceCommand("__enterBootloader", 229, "", ""),
1694
+ ThreespaceCommand("softwareReset", THREESPACE_SOFTWARE_RESET_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__softwareReset),
1695
+ ThreespaceCommand("enterBootloader", THREESPACE_ENTER_BOOTLOADER_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__enterBootloader),
1676
1696
 
1677
1697
  ThreespaceCommand("getButtonState", 250, "", "b"),
1678
1698
  ]
@@ -1683,6 +1703,12 @@ def threespaceCommandGet(cmd_num: int):
1683
1703
  return command
1684
1704
  return None
1685
1705
 
1706
+ def threespaceCommandGetByName(name: str):
1707
+ for command in _threespace_commands:
1708
+ if command.info.name == name:
1709
+ return command
1710
+ return None
1711
+
1686
1712
  def threespaceCommandGetInfo(cmd_num: int):
1687
1713
  command = threespaceCommandGet(cmd_num)
1688
1714
  if command is None: return None