yostlabs 2025.1.16__py3-none-any.whl → 2025.2.11__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,6 +5,7 @@ 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
@@ -62,10 +63,11 @@ class ThreespaceCommand:
62
63
  BINARY_START_BYTE = 0xf7
63
64
  BINARY_START_BYTE_HEADER = 0xf9
64
65
 
65
- 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):
66
67
  self.info = ThreespaceCommandInfo(name, num, in_format, out_format)
67
68
  self.in_format = _3space_format_to_external(self.info.in_format)
68
69
  self.out_format = _3space_format_to_external(self.info.out_format)
70
+ self.custom_func = custom_func
69
71
 
70
72
  def format_cmd(self, *args, header_enabled=False):
71
73
  cmd_data = struct.pack("<B", self.info.num)
@@ -109,7 +111,7 @@ class ThreespaceCommand:
109
111
  return output
110
112
 
111
113
  #Read the command dynamically from an input stream
112
- def read_command(self, com: ThreespaceInputStream):
114
+ def read_command(self, com: ThreespaceInputStream, verbose=False):
113
115
  raw = bytearray([])
114
116
  if self.info.num_out_params == 0: return None, raw
115
117
  output = []
@@ -120,14 +122,16 @@ class ThreespaceCommand:
120
122
  response = com.read(size)
121
123
  raw += response
122
124
  if len(response) != size:
123
- print(f"Failed to read {c} type. Aborting...")
125
+ if verbose:
126
+ print(f"Failed to read {c} type. Aborting...")
124
127
  return None
125
128
  output.append(struct.unpack(format_str, response)[0])
126
129
  else: #Strings are special, find the null terminator
127
130
  response = com.read(1)
128
131
  raw += response
129
132
  if len(response) != 1:
130
- print(f"Failed to read string. Aborting...")
133
+ if verbose:
134
+ print(f"Failed to read string. Aborting...")
131
135
  return None
132
136
  byte = chr(response[0])
133
137
  string = ""
@@ -137,7 +141,8 @@ class ThreespaceCommand:
137
141
  response = com.read(1)
138
142
  raw += response
139
143
  if len(response) != 1:
140
- print(f"Failed to read string. Aborting...")
144
+ if verbose:
145
+ print(f"Failed to read string. Aborting...")
141
146
  return None
142
147
  byte = chr(response[0])
143
148
  output.append(string)
@@ -157,6 +162,7 @@ class ThreespaceGetStreamingBatchCommand(ThreespaceCommand):
157
162
  def set_stream_slots(self, streaming_slots: list[ThreespaceCommand]):
158
163
  self.commands = streaming_slots
159
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}")
160
166
 
161
167
  def parse_response(self, response: bytes):
162
168
  data = []
@@ -168,7 +174,7 @@ class ThreespaceGetStreamingBatchCommand(ThreespaceCommand):
168
174
 
169
175
  return data
170
176
 
171
- def read_command(self, com: ThreespaceInputStream):
177
+ def read_command(self, com: ThreespaceInputStream, verbose=False):
172
178
  #Get the response to all the streaming commands
173
179
  response = []
174
180
  raw_response = bytearray([])
@@ -411,6 +417,7 @@ class StreamableCommands(Enum):
411
417
 
412
418
  THREESPACE_AWAIT_COMMAND_FOUND = 0
413
419
  THREESPACE_AWAIT_COMMAND_TIMEOUT = 1
420
+ THREESPACE_AWAIT_BOOTLOADER = 2
414
421
 
415
422
  T = TypeVar('T')
416
423
 
@@ -457,7 +464,7 @@ class ThreespaceBootloaderInfo:
457
464
  THREESPACE_REQUIRED_HEADER = THREESPACE_HEADER_ECHO_BIT | THREESPACE_HEADER_CHECKSUM_BIT | THREESPACE_HEADER_LENGTH_BIT
458
465
  class ThreespaceSensor:
459
466
 
460
- def __init__(self, com = None, timeout=2):
467
+ def __init__(self, com = None, timeout=2, verbose=False):
461
468
  if com is None: #Default to attempting to use the serial com class if none is provided
462
469
  com = ThreespaceSerialComClass
463
470
 
@@ -480,22 +487,15 @@ class ThreespaceSensor:
480
487
  except:
481
488
  raise ValueError("Failed to create default ThreespaceSerialComClass from parameter:", type(com), com)
482
489
 
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
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
497
496
  self.misaligned = False
498
497
  self.dirty_cache = False
498
+ self.header_info = ThreespaceHeaderInfo()
499
499
  self.header_enabled = True
500
500
 
501
501
  #All the different streaming options
@@ -506,12 +506,27 @@ class ThreespaceSensor:
506
506
 
507
507
  #Used to ensure connecting to the correct sensor when reconnecting
508
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 = {}
509
516
 
510
517
  self.__cached_in_bootloader = self.__check_bootloader_status()
511
518
  if not self.in_bootloader:
512
519
  self.__firmware_init()
513
520
  else:
514
- 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-----------------------------------
515
530
 
516
531
  def __firmware_init(self):
517
532
  """
@@ -519,9 +534,13 @@ class ThreespaceSensor:
519
534
  Called for powerup events when booting into firmware
520
535
  """
521
536
  self.dirty_cache = False #No longer dirty cause initializing
522
-
523
- self.com.read_all() #Clear anything that may be there
524
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
+
525
544
  self.__reinit_firmware()
526
545
 
527
546
  self.valid_mags = self.__get_valid_components("valid_mags")
@@ -529,16 +548,10 @@ class ThreespaceSensor:
529
548
  self.valid_gyros = self.__get_valid_components("valid_gyros")
530
549
  self.valid_baros = self.__get_valid_components("valid_baros")
531
550
 
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
551
  def __reinit_firmware(self):
538
552
  """
539
553
  Called when settings may have changed but a full reboot did not occur
540
554
  """
541
- self.com.read_all() #Clear anything that may be there
542
555
  self.dirty_cache = False #No longer dirty cause initializing
543
556
 
544
557
  self.header_info = ThreespaceHeaderInfo()
@@ -550,26 +563,68 @@ class ThreespaceSensor:
550
563
  self.file_stream_length = 0
551
564
 
552
565
  self.streaming_packet_size = 0
553
- self.header_enabled = True
554
566
  self._force_stop_streaming()
555
567
 
556
568
  #Now reinitialize the cached settings
557
569
  self.__cache_header_settings()
558
- self.cache_streaming_settings()
570
+ self.__cache_streaming_settings()
559
571
 
560
- 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()
561
574
  self.immediate_debug = int(self.get_settings("debug_mode")) == 1 #Needed for some startup processes when restarting
562
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
+
563
610
  def __add_command(self, command: ThreespaceCommand):
564
611
  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}")
612
+ self.log(f"Registering duplicate command: {command.info.num} {self.commands[command.info.num].info.name} {command.info.name}")
566
613
  self.commands[command.info.num] = command
567
614
 
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))
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)
573
628
 
574
629
  def __get_command(self, command_name: str):
575
630
  for command in self.commands:
@@ -577,10 +632,107 @@ class ThreespaceSensor:
577
632
  if command.info.name == command_name:
578
633
  return command
579
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
580
650
 
581
- @property
582
- def is_streaming(self):
583
- 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------------------------------------------------
584
736
 
585
737
  #Can't just do if "header" in string because log_header_enabled exists and doesn't actually require cacheing the header
586
738
  HEADER_KEYS = ["header", "header_status", "header_timestamp", "header_echo", "header_checksum", "header_serial", "header_length"]
@@ -600,10 +752,18 @@ class ThreespaceSensor:
600
752
  params.append(f"{key}={value}")
601
753
  cmd = f"!{';'.join(params)}\n"
602
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
+
603
759
  #For dirty check
604
760
  keys = cmd[1:-1].split(';')
605
761
  keys = [v.split('=')[0] for v in keys]
606
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
+
607
767
  #Send cmd
608
768
  self.com.write(cmd.encode())
609
769
 
@@ -611,50 +771,18 @@ class ThreespaceSensor:
611
771
  err = 3
612
772
  num_successes = 0
613
773
 
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
-
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
+
658
786
  #Handle updating state variables based on settings
659
787
  #If the user modified the header, need to cache the settings so the API knows how to interpret responses
660
788
  if "header" in cmd.lower(): #First do a quick check
@@ -662,13 +790,14 @@ class ThreespaceSensor:
662
790
  self.__cache_header_settings()
663
791
 
664
792
  if "stream_slots" in cmd.lower():
665
- self.cache_streaming_settings()
793
+ self.__cache_streaming_settings()
666
794
 
667
- 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")):
668
797
  self.set_cached_settings_dirty()
669
798
 
670
799
  if err:
671
- print(f"Err setting {cmd}: {err=} {num_successes=}")
800
+ self.log(f"Err setting {cmd}: {err=} {num_successes=}")
672
801
  return err, num_successes
673
802
 
674
803
  def get_settings(self, *args: str) -> dict[str, str] | str:
@@ -679,91 +808,165 @@ class ThreespaceSensor:
679
808
  self.com.write(cmd.encode())
680
809
 
681
810
  keys = cmd[1:-1].split(';')
682
- error_response = "<KEY_ERROR>"
811
+ error_response_len = len(THREESPACE_GET_SETTINGS_ERROR_RESPONSE)
683
812
 
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
813
+ min_resp_length = 0
814
+ for key in keys:
815
+ min_resp_length += min(len(key) + 1, error_response_len)
706
816
 
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)
817
+
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(';')
715
826
 
716
827
  #Build the response dict
717
828
  response_dict = {}
718
829
  for i, v in enumerate(response):
719
- if v == error_response:
720
- response_dict[keys[i]] = error_response
830
+ if v == THREESPACE_GET_SETTINGS_ERROR_RESPONSE:
831
+ response_dict[keys[i]] = THREESPACE_GET_SETTINGS_ERROR_RESPONSE
721
832
  continue
722
833
  try:
723
834
  key, value = v.split('=')
724
835
  response_dict[key] = value
725
836
  except:
726
- print("Failed to parse get:", response)
837
+ self.log("Failed to parse get value:", i, v, len(v))
727
838
 
728
839
  #Format response
729
840
  if len(response_dict) == 1:
730
841
  return list(response_dict.values())[0]
731
842
  return response_dict
732
843
 
733
- def execute_command(self, cmd: ThreespaceCommand, *args):
734
- self.check_dirty()
844
+ #-----------Base Settings Parsing----------------
735
845
 
736
- retries = 0
737
- 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")
738
850
 
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}")
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
748
859
 
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)
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.
758
951
 
759
- 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
+ """
760
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
761
961
  data = self.com.peek(header_len + header.length)[header_len:]
762
962
  if len(data) != header.length: return False
763
963
  checksum = sum(data) % 256
764
964
  return checksum == header.checksum
765
965
 
766
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
+
767
970
  start_time = time.time()
768
971
 
769
972
  #Update the streaming until the result for this command is next in the buffer
@@ -772,179 +975,137 @@ class ThreespaceSensor:
772
975
  return THREESPACE_AWAIT_COMMAND_TIMEOUT
773
976
 
774
977
  #Get potential header
775
- header = self.com.peek(self.header_info.size)
776
- if len(header) != self.header_info.size: #Wait for more data
978
+ header = self.__try_peek_header()
979
+ if header is None:
777
980
  continue
778
981
 
779
- #Check to see what this packet is a response to
780
- header = ThreespaceHeader.from_bytes(header, self.header_info)
781
982
  echo = header.echo
782
983
 
783
984
  if echo == cmd.info.num: #Cmd matches
784
- if self.__peek_checksum(header):
985
+ if self.__peek_checksum(header, max_data_length=cmd.info.out_size):
986
+ self.misaligned = False
785
987
  return THREESPACE_AWAIT_COMMAND_FOUND
786
988
 
787
989
  #Error in packet, go start realigning
788
990
  if not self.misaligned:
789
- print(f"Checksum mismatch for command {cmd.info.num}")
991
+ self.log(f"Checksum mismatch for command {cmd.info.num}")
790
992
  self.misaligned = True
791
993
  self.com.read(1)
792
994
  else:
793
995
  #It wasn't a response to the command, so may be a response to some internal system
794
- self.__internal_update(header)
996
+ self.__internal_update(header)
795
997
 
796
- def __internal_update(self, header: ThreespaceHeader):
998
+ #------------------------------BASE INPUT PARSING--------------------------------------------
999
+
1000
+ def __internal_update(self, header: ThreespaceHeader = None):
797
1001
  """
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
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
801
1017
  """
802
1018
  checksum_match = False #Just for debugging
803
1019
 
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
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
818
1053
 
819
1054
  #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)
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
842
1064
 
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
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
850
1070
 
1071
+ #-----------------------------BASE COMMAND EXECUTION-------------------------------------
851
1072
 
852
- def startStreaming(self):
853
- if self.is_data_streaming: return
1073
+ def execute_command(self, cmd: ThreespaceCommand, *args):
854
1074
  self.check_dirty()
855
- self.streaming_packets.clear()
856
1075
 
857
- self.header_enabled = True
1076
+ retries = 0
1077
+ MAX_RETRIES = 3
1078
+
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}")
858
1088
 
859
- self.cache_streaming_settings()
1089
+ return self.read_and_parse_command(cmd)
1090
+
1091
+ def __invalid_command(self, *args):
1092
+ raise NotImplementedError("This method is not available.")
860
1093
 
861
- cmd = self.commands[85]
862
- cmd.send_command(self.com, header_enabled=self.header_enabled)
1094
+ def read_and_parse_command(self, cmd: ThreespaceCommand):
863
1095
  if self.header_enabled:
864
- self.__await_command(cmd)
865
1096
  header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
866
1097
  else:
867
1098
  header = ThreespaceHeader()
868
- self.is_data_streaming = True
869
- return ThreespaceCmdResult(None, header)
1099
+ result, raw = cmd.read_command(self.com, verbose=self.verbose)
1100
+ return ThreespaceCmdResult(result, header, data_raw_binary=raw)
870
1101
 
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
1102
+ #-----------------------------------BASE STREAMING COMMMANDS----------------------------------------------
914
1103
 
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
1104
+ @property
1105
+ def is_streaming(self):
1106
+ return self.is_data_streaming or self.is_log_streaming or self.is_file_streaming
946
1107
 
947
- def cache_streaming_settings(self):
1108
+ def __cache_streaming_settings(self):
948
1109
  cached_slots: list[ThreespaceCommand] = []
949
1110
  slots: str = self.get_settings("stream_slots")
950
1111
  slots = slots.split(',')
@@ -961,32 +1122,21 @@ class ThreespaceSensor:
961
1122
  if command == None: continue
962
1123
  self.streaming_packet_size += command.info.out_size
963
1124
 
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
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
990
1140
 
991
1141
  def __update_base_streaming(self):
992
1142
  """
@@ -1007,44 +1157,86 @@ class ThreespaceSensor:
1007
1157
  def clearStreamingPackets(self):
1008
1158
  self.streaming_packets.clear()
1009
1159
 
1010
- def fileStartStream(self) -> ThreespaceCmdResult[int]:
1011
- self.check_dirty()
1012
- 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
1013
1166
 
1014
- cmd = self.__get_command("__fileStartStream")
1015
- cmd.send_command(self.com, header_enabled=self.header_enabled)
1016
- 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
1017
1183
 
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()
1184
+ return data_processed
1022
1185
 
1023
- result, raw = cmd.read_command(self.com)
1024
- self.file_stream_length = result
1025
- 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.
1026
1193
 
1027
- return ThreespaceCmdResult(result, header, data_raw_binary=raw)
1028
-
1029
- def fileStopStream(self) -> ThreespaceCmdResult[None]:
1030
- 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
1031
1200
 
1032
- cmd = self.__get_command("__fileStopStream")
1033
- 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
1034
1204
 
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()
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
1040
1220
 
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()
1221
+ #Restore
1222
+ self.header_enabled = cached_header_enabled
1223
+ self.dirty_cache = cahched_dirty
1224
+
1225
+ #-------------------------------------FILE STREAMING----------------------------------------------
1045
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
+ if self.file_stream_length > 0:
1232
+ self.is_file_streaming = True
1233
+ return result
1234
+
1235
+ def fileStopStream(self) -> ThreespaceCmdResult[None]: ...
1236
+ def __fileStopStream(self) -> ThreespaceCmdResult[None]:
1237
+ result = self.execute_command(self.__get_command("fileStopStream"))
1046
1238
  self.is_file_streaming = False
1047
- return ThreespaceCmdResult(None, header)
1239
+ return result
1048
1240
 
1049
1241
  def getFileStreamData(self):
1050
1242
  to_return = self.file_stream_data.copy()
@@ -1062,52 +1254,32 @@ class ThreespaceSensor:
1062
1254
  data = self.com.read(header.length)
1063
1255
  self.file_stream_data += data
1064
1256
  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
1257
+ 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
1066
1258
  self.is_file_streaming = False
1067
1259
  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.")
1260
+ self.log(f"File streaming stopped due to last packet. However still expected {self.file_stream_length} more bytes.")
1069
1261
 
1070
- def startDataLogging(self) -> ThreespaceCmdResult[None]:
1071
- self.check_dirty()
1262
+ #----------------------------DATA LOGGING--------------------------------------
1072
1263
 
1073
- self.header_enabled = True
1074
- self.cache_streaming_settings()
1264
+ def startDataLogging(self) -> ThreespaceCmdResult[None]: ...
1265
+ def __startDataLogging(self) -> ThreespaceCmdResult[None]:
1266
+ self.__cache_streaming_settings()
1075
1267
 
1076
1268
  #Must check whether streaming is being done alongside logging or not. Also configure required settings if it is
1077
1269
  streaming = bool(int(self.get_settings("log_immediate_output")))
1078
1270
  if streaming:
1079
1271
  self.set_settings(log_immediate_output_header_enabled=1,
1080
1272
  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
-
1273
+
1274
+ result = self.execute_command(self.__get_command("startDataLogging"))
1089
1275
  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()
1276
+ return result
1108
1277
 
1278
+ def stopDataLogging(self) -> ThreespaceCmdResult[None]: ...
1279
+ def __stopDataLogging(self) -> ThreespaceCmdResult[None]:
1280
+ result = self.execute_command(self.__get_command("stopDataLogging"))
1109
1281
  self.is_log_streaming = False
1110
- return ThreespaceCmdResult(None, header)
1282
+ return result
1111
1283
 
1112
1284
  def __update_log_streaming(self):
1113
1285
  """
@@ -1119,23 +1291,26 @@ class ThreespaceSensor:
1119
1291
  data = self.com.read(header.length)
1120
1292
  self.file_stream_data += data
1121
1293
 
1122
- def softwareReset(self):
1294
+ #---------------------------------POWER STATE CHANGING COMMANDS & BOOTLOADER------------------------------------
1295
+
1296
+ def softwareReset(self): ...
1297
+ def __softwareReset(self):
1123
1298
  self.check_dirty()
1124
- cmd = self.commands[226]
1299
+ cmd = self.commands[THREESPACE_SOFTWARE_RESET_COMMAND_NUM]
1125
1300
  cmd.send_command(self.com)
1126
1301
  self.com.close()
1302
+ #TODO: Make this actually wait instead of an arbitrary sleep length
1127
1303
  time.sleep(0.5) #Give it time to restart
1128
1304
  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
1305
  self.__firmware_init()
1133
1306
 
1134
- def enterBootloader(self):
1307
+ def enterBootloader(self): ...
1308
+ def __enterBootloader(self):
1135
1309
  if self.in_bootloader: return
1136
1310
 
1137
- cmd = self.commands[229]
1311
+ cmd = self.commands[THREESPACE_ENTER_BOOTLOADER_COMMAND_NUM]
1138
1312
  cmd.send_command(self.com)
1313
+ #TODO: Make this actually wait instead of an arbitrary sleep length
1139
1314
  time.sleep(0.5) #Give it time to boot into bootloader
1140
1315
  if self.com.reenumerates:
1141
1316
  self.com.close()
@@ -1173,12 +1348,20 @@ class ThreespaceSensor:
1173
1348
  #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
1349
  bootloader = False
1175
1350
  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':
1351
+ response = self.__await_get_settings(2, check_bootloader=True)
1352
+ if response == THREESPACE_AWAIT_COMMAND_TIMEOUT:
1353
+ self.log("Requested Bootloader, Got:")
1354
+ self.log(self.com.peek(self.com.length))
1355
+ raise RuntimeError("Failed to discover bootloader or firmware.")
1356
+ if response == THREESPACE_AWAIT_BOOTLOADER:
1180
1357
  bootloader = True
1181
- self.com.read_all() #Remove the rest of the OK responses or the rest of the <KEY_ERROR> response
1358
+ time.sleep(0.1) #Give time for all the OK responses to come in
1359
+ self.com.read_all() #Remove the rest of the OK responses or the rest of the <KEY_ERROR> response
1360
+ elif response == THREESPACE_AWAIT_COMMAND_FOUND:
1361
+ bootloader = False
1362
+ self.com.readline() #Clear the setting, no need to parse
1363
+ else:
1364
+ raise Exception("Failed to detect if in bootloader or firmware")
1182
1365
  return bootloader
1183
1366
 
1184
1367
  def bootloader_get_sn(self):
@@ -1198,11 +1381,6 @@ class ThreespaceSensor:
1198
1381
  success = self.__attempt_rediscover_self()
1199
1382
  if not success:
1200
1383
  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
1384
  in_bootloader = self.__check_bootloader_status()
1207
1385
  if in_bootloader:
1208
1386
  raise RuntimeError("Failed to exit bootloader")
@@ -1256,299 +1434,134 @@ class ThreespaceSensor:
1256
1434
  self.fileStopStream()
1257
1435
  if self.is_log_streaming:
1258
1436
  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
1437
+
1438
+ #The sensor may or may not have this command registered. So just try it
1439
+ try:
1440
+ #May not be opened, but also not cacheing that so just attempt to close.
1441
+ self.closeFile()
1442
+ except: pass
1260
1443
  self.com.close()
1261
1444
 
1262
1445
  #-------------------------START ALL PROTOTYPES------------------------------------
1263
1446
 
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]:
1447
+ #To actually see how commands work, look at __initialize_commands and __add_command
1448
+ #But basically, these are all just prototypes. Information about the commands is in the table
1449
+ #beneath here, and the API simply calls its execute_command function on the Command information objects defined.
1450
+
1451
+ def eeptsStart(self) -> ThreespaceCmdResult[None]: ...
1452
+ def eeptsStop(self) -> ThreespaceCmdResult[None]: ...
1453
+ def eeptsGetOldestStep(self) -> ThreespaceCmdResult[list]: ...
1454
+ def eeptsGetNewestStep(self) -> ThreespaceCmdResult[list]: ...
1455
+ def eeptsGetNumStepsAvailable(self) -> ThreespaceCmdResult[int]: ...
1456
+ def eeptsInsertGPS(self, latitude: float, longitude: float) -> ThreespaceCmdResult[None]: ...
1457
+ def eeptsAutoOffset(self) -> ThreespaceCmdResult[None]: ...
1458
+ def getRawGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1459
+ def getRawAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1460
+ def getRawMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1461
+ def getTaredOrientation(self) -> ThreespaceCmdResult[list[float]]: ...
1462
+ def getTaredOrientationAsEulerAngles(self) -> ThreespaceCmdResult[list[float]]: ...
1463
+ def getTaredOrientationAsRotationMatrix(self) -> ThreespaceCmdResult[list[float]]: ...
1464
+ def getTaredOrientationAsAxisAngles(self) -> ThreespaceCmdResult[list[float]]: ...
1465
+ def getTaredOrientationAsTwoVector(self) -> ThreespaceCmdResult[list[float]]: ...
1466
+ def getDifferenceQuaternion(self) -> ThreespaceCmdResult[list[float]]: ...
1467
+ def getUntaredOrientation(self) -> ThreespaceCmdResult[list[float]]: ...
1468
+ def getUntaredOrientationAsEulerAngles(self) -> ThreespaceCmdResult[list[float]]: ...
1469
+ def getUntaredOrientationAsRotationMatrix(self) -> ThreespaceCmdResult[list[float]]: ...
1470
+ def getUntaredOrientationAsAxisAngles(self) -> ThreespaceCmdResult[list[float]]: ...
1471
+ def getUntaredOrientationAsTwoVector(self) -> ThreespaceCmdResult[list[float]]: ...
1472
+ def commitSettings(self) -> ThreespaceCmdResult[None]: ...
1473
+ def getMotionlessConfidenceFactor(self) -> ThreespaceCmdResult[float]: ...
1474
+ def enableMSC(self) -> ThreespaceCmdResult[None]: ...
1475
+ def disableMSC(self) -> ThreespaceCmdResult[None]: ...
1476
+ def getNextDirectoryItem(self) -> ThreespaceCmdResult[list[int,str,int]]: ...
1477
+ def changeDirectory(self, path: str) -> ThreespaceCmdResult[None]: ...
1478
+ def openFile(self, path: str) -> ThreespaceCmdResult[None]: ...
1479
+ def closeFile(self) -> ThreespaceCmdResult[None]: ...
1480
+ def fileGetRemainingSize(self) -> ThreespaceCmdResult[int]: ...
1481
+ def fileReadLine(self) -> ThreespaceCmdResult[str]: ...
1482
+ def fileReadBytes(self, num_bytes: int) -> ThreespaceCmdResult[bytes]: ...
1483
+ def __fileReadBytes(self, num_bytes: int) -> ThreespaceCmdResult[bytes]:
1358
1484
  self.check_dirty()
1359
1485
  cmd = self.commands[THREESPACE_FILE_READ_BYTES_COMMAND_NUM]
1360
1486
  cmd.send_command(self.com, num_bytes, header_enabled=self.header_enabled)
1361
1487
  self.__await_command(cmd)
1362
1488
  if self.header_enabled:
1363
1489
  header = ThreespaceHeader.from_bytes(self.com.read(self.header_info.size), self.header_info)
1490
+ num_bytes = min(num_bytes, header.length) #Its possible for less bytes to be returned when an error occurs (EX: Reading from unopened file)
1364
1491
  else:
1365
1492
  header = ThreespaceHeader()
1366
1493
 
1367
1494
  response = self.com.read(num_bytes)
1368
1495
  return ThreespaceCmdResult(response, header, data_raw_binary=response)
1369
1496
 
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.")
1497
+ def deleteFile(self, path: str) -> ThreespaceCmdResult[None]: ...
1498
+ def getStreamingBatch(self) -> ThreespaceCmdResult[list]: ...
1499
+ def setOffsetWithCurrentOrientation(self) -> ThreespaceCmdResult[None]: ...
1500
+ def resetBaseOffset(self) -> ThreespaceCmdResult[None]: ...
1501
+ def setBaseOffsetWithCurrentOrientation(self) -> ThreespaceCmdResult[None]: ...
1502
+ def getTaredTwoVectorInSensorFrame(self) -> ThreespaceCmdResult[list[float]]: ...
1503
+ def getUntaredTwoVectorInSensorFrame(self) -> ThreespaceCmdResult[list[float]]: ...
1504
+ def getPrimaryBarometerPressure(self) -> ThreespaceCmdResult[float]: ...
1505
+ def getPrimaryBarometerAltitude(self) -> ThreespaceCmdResult[float]: ...
1506
+ def getBarometerAltitude(self, id: int) -> ThreespaceCmdResult[float]: ...
1507
+ def getBarometerPressure(self, id: int) -> ThreespaceCmdResult[float]: ...
1508
+ def getAllPrimaryNormalizedData(self) -> ThreespaceCmdResult[list[float]]: ...
1509
+ def getPrimaryNormalizedGyroRate(self) -> ThreespaceCmdResult[list[float]]: ...
1510
+ def getPrimaryNormalizedAccelVec(self) -> ThreespaceCmdResult[list[float]]: ...
1511
+ def getPrimaryNormalizedMagVec(self) -> ThreespaceCmdResult[list[float]]: ...
1512
+ def getAllPrimaryCorrectedData(self) -> ThreespaceCmdResult[list[float]]: ...
1513
+ def getPrimaryCorrectedGyroRate(self) -> ThreespaceCmdResult[list[float]]: ...
1514
+ def getPrimaryCorrectedAccelVec(self) -> ThreespaceCmdResult[list[float]]: ...
1515
+ def getPrimaryCorrectedMagVec(self) -> ThreespaceCmdResult[list[float]]: ...
1516
+ def getPrimaryGlobalLinearAccel(self) -> ThreespaceCmdResult[list[float]]: ...
1517
+ def getPrimaryLocalLinearAccel(self) -> ThreespaceCmdResult[list[float]]: ...
1518
+ def getTemperatureCelsius(self) -> ThreespaceCmdResult[float]: ...
1519
+ def getTemperatureFahrenheit(self) -> ThreespaceCmdResult[float]: ...
1520
+ def getNormalizedGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1521
+ def getNormalizedAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1522
+ def getNormalizedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1523
+ def getCorrectedGyroRate(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1524
+ def getCorrectedAccelVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1525
+ def getCorrectedMagVec(self, id: int) -> ThreespaceCmdResult[list[float]]: ...
1526
+ def enableMSC(self) -> ThreespaceCmdResult[None]: ...
1527
+ def disableMSC(self) -> ThreespaceCmdResult[None]: ...
1528
+ def getTimestamp(self) -> ThreespaceCmdResult[int]: ...
1529
+ def getBatteryVoltage(self) -> ThreespaceCmdResult[float]: ...
1530
+ def getBatteryPercent(self) -> ThreespaceCmdResult[int]: ...
1531
+ def getBatteryStatus(self) -> ThreespaceCmdResult[int]: ...
1532
+ def getGpsCoord(self) -> ThreespaceCmdResult[list[float]]: ...
1533
+ def getGpsAltitude(self) -> ThreespaceCmdResult[float]: ...
1534
+ def getGpsFixState(self) -> ThreespaceCmdResult[int]: ...
1535
+ def getGpsHdop(self) -> ThreespaceCmdResult[float]: ...
1536
+ def getGpsSatellites(self) -> ThreespaceCmdResult[int]: ...
1537
+ def getButtonState(self) -> ThreespaceCmdResult[int]: ...
1538
+ def correctRawGyroData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]: ...
1539
+ def correctRawAccelData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]: ...
1540
+ def correctRawMagData(self, x: float, y: float, z: float, id: int) -> ThreespaceCmdResult[list[float]]: ...
1541
+ def formatSd(self) -> ThreespaceCmdResult[None]: ...
1542
+ def setDateTime(self, year: int, month: int, day: int, hour: int, minute: int, second: int) -> ThreespaceCmdResult[None]: ...
1543
+ def getDateTime(self) -> ThreespaceCmdResult[list[int]]: ...
1544
+ def tareWithCurrentOrientation(self) -> ThreespaceCmdResult[None]: ...
1545
+ def setBaseTareWithCurrentOrientation(self) -> ThreespaceCmdResult[None]: ...
1546
+ def resetFilter(self) -> ThreespaceCmdResult[None]: ...
1547
+ def getNumDebugMessages(self) -> ThreespaceCmdResult[int]: ...
1548
+ def getOldestDebugMessage(self) -> ThreespaceCmdResult[str]: ...
1549
+ def selfTest(self) -> ThreespaceCmdResult[int]: ...
1550
+ def beginPassiveAutoCalibration(self, enabled_bitfield: int) -> ThreespaceCmdResult[None]: ...
1551
+ def getActivePassiveAutoCalibration(self) -> ThreespaceCmdResult[int]: ...
1552
+ def beginActiveAutoCalibration(self) -> ThreespaceCmdResult[None]: ...
1553
+ def isActiveAutoCalibrationActive(self) -> ThreespaceCmdResult[int]: ...
1554
+ def getStreamingLabel(self, cmd_num: int) -> ThreespaceCmdResult[str]: ...
1555
+ def setCursor(self, cursor_index: int) -> ThreespaceCmdResult[None]: ...
1556
+ def getLastLogCursorInfo(self) -> ThreespaceCmdResult[tuple[int,str]]: ...
1557
+ def pauseLogStreaming(self, pause: bool) -> ThreespaceCmdResult[None]: ...
1549
1558
 
1550
1559
  THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM = 84
1560
+ THREESPACE_START_STREAMING_COMMAND_NUM = 85
1561
+ THREESPACE_STOP_STREAMING_COMMAND_NUM = 86
1551
1562
  THREESPACE_FILE_READ_BYTES_COMMAND_NUM = 177
1563
+ THREESPACE_SOFTWARE_RESET_COMMAND_NUM = 226
1564
+ THREESPACE_ENTER_BOOTLOADER_COMMAND_NUM = 229
1552
1565
 
1553
1566
  #Acutal command definitions
1554
1567
  _threespace_commands: list[ThreespaceCommand] = [
@@ -1616,8 +1629,8 @@ _threespace_commands: list[ThreespaceCommand] = [
1616
1629
  ThreespaceCommand("disableMSC", 58, "", ""),
1617
1630
 
1618
1631
  ThreespaceCommand("formatSd", 59, "", ""),
1619
- ThreespaceCommand("__startDataLogging", 60, "", ""),
1620
- ThreespaceCommand("__stopDataLogging", 61, "", ""),
1632
+ ThreespaceCommand("startDataLogging", 60, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__startDataLogging),
1633
+ ThreespaceCommand("stopDataLogging", 61, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__stopDataLogging),
1621
1634
 
1622
1635
  ThreespaceCommand("setDateTime", 62, "Bbbbbb", ""),
1623
1636
  ThreespaceCommand("getDateTime", 63, "", "Bbbbbb"),
@@ -1635,9 +1648,9 @@ _threespace_commands: list[ThreespaceCommand] = [
1635
1648
  ThreespaceCommand("eeptsAutoOffset", 74, "", ""),
1636
1649
 
1637
1650
  ThreespaceCommand("getStreamingLabel", 83, "b", "S"),
1638
- ThreespaceCommand("__getStreamingBatch", THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM, "", "S"),
1639
- ThreespaceCommand("__startStreaming", 85, "", ""),
1640
- ThreespaceCommand("__stopStreaming", 86, "", ""),
1651
+ ThreespaceCommand("getStreamingBatch", THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM, "", "S"),
1652
+ ThreespaceCommand("startStreaming", THREESPACE_START_STREAMING_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__startStreaming),
1653
+ ThreespaceCommand("stopStreaming", THREESPACE_STOP_STREAMING_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__stopStreaming),
1641
1654
  ThreespaceCommand("pauseLogStreaming", 87, "b", ""),
1642
1655
 
1643
1656
  ThreespaceCommand("getTimestamp", 94, "", "U"),
@@ -1648,6 +1661,7 @@ _threespace_commands: list[ThreespaceCommand] = [
1648
1661
  ThreespaceCommand("resetFilter", 120, "", ""),
1649
1662
  ThreespaceCommand("getNumDebugMessages", 126, "", "B"),
1650
1663
  ThreespaceCommand("getOldestDebugMessage", 127, "", "S"),
1664
+ ThreespaceCommand("selfTest", 128, "", "u"),
1651
1665
 
1652
1666
  ThreespaceCommand("beginPassiveAutoCalibration", 165, "b", ""),
1653
1667
  ThreespaceCommand("getActivePassiveAutoCalibration", 166, "", "b"),
@@ -1661,11 +1675,11 @@ _threespace_commands: list[ThreespaceCommand] = [
1661
1675
  ThreespaceCommand("closeFile", 174, "", ""),
1662
1676
  ThreespaceCommand("fileGetRemainingSize", 175, "", "U"),
1663
1677
  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
1678
+ 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
1665
1679
  ThreespaceCommand("deleteFile", 178, "S", ""),
1666
1680
  ThreespaceCommand("setCursor", 179, "U", ""),
1667
- ThreespaceCommand("__fileStartStream", 180, "", "U"),
1668
- ThreespaceCommand("__fileStopStream", 181, "", ""),
1681
+ ThreespaceCommand("fileStartStream", 180, "", "U", custom_func=ThreespaceSensor._ThreespaceSensor__fileStartStream),
1682
+ ThreespaceCommand("fileStopStream", 181, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__fileStopStream),
1669
1683
 
1670
1684
  ThreespaceCommand("getBatteryVoltage", 201, "", "f"),
1671
1685
  ThreespaceCommand("getBatteryPercent", 202, "", "b"),
@@ -1678,8 +1692,8 @@ _threespace_commands: list[ThreespaceCommand] = [
1678
1692
  ThreespaceCommand("getGpsSatellites", 219, "", "b"),
1679
1693
 
1680
1694
  ThreespaceCommand("commitSettings", 225, "", ""),
1681
- ThreespaceCommand("__softwareReset", 226, "", ""),
1682
- ThreespaceCommand("__enterBootloader", 229, "", ""),
1695
+ ThreespaceCommand("softwareReset", THREESPACE_SOFTWARE_RESET_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__softwareReset),
1696
+ ThreespaceCommand("enterBootloader", THREESPACE_ENTER_BOOTLOADER_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__enterBootloader),
1683
1697
 
1684
1698
  ThreespaceCommand("getButtonState", 250, "", "b"),
1685
1699
  ]
@@ -1690,6 +1704,12 @@ def threespaceCommandGet(cmd_num: int):
1690
1704
  return command
1691
1705
  return None
1692
1706
 
1707
+ def threespaceCommandGetByName(name: str):
1708
+ for command in _threespace_commands:
1709
+ if command.info.name == name:
1710
+ return command
1711
+ return None
1712
+
1693
1713
  def threespaceCommandGetInfo(cmd_num: int):
1694
1714
  command = threespaceCommandGet(cmd_num)
1695
1715
  if command is None: return None