yostlabs 2025.3.6__tar.gz → 2025.4.30__tar.gz

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.
Files changed (32) hide show
  1. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/PKG-INFO +1 -1
  2. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/pyproject.toml +1 -1
  3. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/communication/ble.py +15 -4
  4. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/communication/serial.py +5 -1
  5. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/math/quaternion.py +5 -7
  6. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/api.py +60 -23
  7. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/.gitignore +0 -0
  8. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/embedded_2024_dec_20.xml +0 -0
  9. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_ble.py +0 -0
  10. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_commands.py +0 -0
  11. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_component_specific_settings_and_commands.py +0 -0
  12. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_firmware_upload.py +0 -0
  13. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_parsing_stored_binary.py +0 -0
  14. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_read_settings.py +0 -0
  15. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_streaming.py +0 -0
  16. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_streaming_manager.py +0 -0
  17. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/Examples/example_write_settings.py +0 -0
  18. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/LICENSE +0 -0
  19. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/README.md +0 -0
  20. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/__init__.py +0 -0
  21. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/communication/__init__.py +0 -0
  22. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/communication/base.py +0 -0
  23. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/math/__init__.py +0 -0
  24. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/math/vector.py +0 -0
  25. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/__init__.py +0 -0
  26. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/consts.py +0 -0
  27. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/eepts.py +0 -0
  28. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/utils/__init__.py +0 -0
  29. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/utils/calibration.py +0 -0
  30. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/utils/parser.py +0 -0
  31. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/utils/streaming.py +0 -0
  32. {yostlabs-2025.3.6 → yostlabs-2025.4.30}/src/yostlabs/tss3/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yostlabs
3
- Version: 2025.3.6
3
+ Version: 2025.4.30
4
4
  Summary: Python resources and API for 3Space sensors from Yost Labs Inc.
5
5
  Project-URL: Homepage, https://yostlabs.com/
6
6
  Project-URL: Repository, https://github.com/YostLabs/3SpacePythonPackage/tree/main
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  name = "yostlabs"
7
7
  #If uploading again on the same day, add a fourth number
8
- version = "2025.03.6"
8
+ version = "2025.04.30"
9
9
  authors = [
10
10
  { name="Yost Labs Inc.", email="techsupport@yostlabs.com" },
11
11
  { name="Andy Riedlinger", email="techsupport@yostlabs.com" },
@@ -39,14 +39,14 @@ class ThreespaceBLEComClass(ThreespaceComClass):
39
39
  error_on_disconnect : If trying to read while the sensor is disconnected, an exception will be generated. This may be undesirable \
40
40
  if it is expected that the sensor will frequently go in and out of range and the user wishes to preserve data (such as streaming)
41
41
  """
42
- self.event_loop = asyncio.new_event_loop()
43
42
  bleak_options = { "timeout": discovery_timeout, "disconnected_callback": self.__on_disconnect }
44
43
  if isinstance(ble, BleakClient): #Actual client
45
44
  self.client = ble
46
45
  self.__name = ble.address
47
46
  elif isinstance(ble, str):
48
47
  if discover_name: #Local Name stirng
49
- device = self.event_loop.run_until_complete(BleakScanner.find_device_by_name(ble, timeout=discovery_timeout))
48
+ self.__lazy_init_scanner()
49
+ device = ThreespaceBLEComClass.SCANNER_EVENT_LOOP.run_until_complete(BleakScanner.find_device_by_name(ble, timeout=discovery_timeout))
50
50
  if device is None:
51
51
  raise BleakDeviceNotFoundError(ble)
52
52
  self.client = BleakClient(device, **bleak_options)
@@ -63,7 +63,8 @@ class ThreespaceBLEComClass(ThreespaceComClass):
63
63
  self.__timeout = self.DEFAULT_TIMEOUT
64
64
 
65
65
  self.buffer = bytearray()
66
- self.data_read_event = asyncio.Event()
66
+ self.event_loop: asyncio.AbstractEventLoop = None
67
+ self.data_read_event: asyncio.Event = None
67
68
 
68
69
  #Default to 20, will update on open
69
70
  self.max_packet_size = 20
@@ -91,6 +92,8 @@ class ThreespaceBLEComClass(ThreespaceComClass):
91
92
  if not self.__connected and self.error_on_disconnect:
92
93
  self.close()
93
94
  return
95
+ self.event_loop = asyncio.new_event_loop()
96
+ self.data_read_event = asyncio.Event()
94
97
  self.event_loop.run_until_complete(self.__async_open())
95
98
  self.max_packet_size = self.client.mtu_size - 3 #-3 to account for the opcode and attribute handle stored in the data packet
96
99
  self.__opened = True
@@ -106,6 +109,9 @@ class ThreespaceBLEComClass(ThreespaceComClass):
106
109
  if not self.__opened: return
107
110
  self.event_loop.run_until_complete(self.__async_close())
108
111
  self.buffer.clear()
112
+ self.event_loop.close()
113
+ self.event_loop = None
114
+ self.data_read_event = None
109
115
  self.__opened = False
110
116
 
111
117
  def __on_disconnect(self, client: BleakClient):
@@ -138,6 +144,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
138
144
 
139
145
  def __read_all_data(self):
140
146
  self.event_loop.run_until_complete(self.__wait_for_callbacks_async())
147
+ self.data_read_event.clear()
141
148
  self.__assert_connected()
142
149
 
143
150
  def __on_data_received(self, sender: BleakGATTCharacteristic, data: bytearray):
@@ -153,10 +160,10 @@ class ThreespaceBLEComClass(ThreespaceComClass):
153
160
 
154
161
  async def __await_read(self, timeout_time: int):
155
162
  self.__assert_connected()
156
- self.data_read_event.clear()
157
163
  try:
158
164
  async with async_timeout.timeout_at(timeout_time):
159
165
  await self.data_read_event.wait()
166
+ self.data_read_event.clear()
160
167
  return True
161
168
  except:
162
169
  return False
@@ -231,6 +238,10 @@ class ThreespaceBLEComClass(ThreespaceComClass):
231
238
  @property
232
239
  def name(self) -> str:
233
240
  return self.__name
241
+
242
+ @property
243
+ def address(self) -> str:
244
+ return self.client.address
234
245
 
235
246
  SCANNER = None
236
247
  SCANNER_EVENT_LOOP = None
@@ -1,6 +1,7 @@
1
1
  from yostlabs.communication.base import *
2
2
  import serial
3
3
  import serial.tools.list_ports
4
+ import time
4
5
 
5
6
 
6
7
  class ThreespaceSerialComClass(ThreespaceComClass):
@@ -107,7 +108,10 @@ class ThreespaceSerialComClass(ThreespaceComClass):
107
108
 
108
109
  @timeout.setter
109
110
  def timeout(self, timeout: float):
110
- self.ser.timeout = timeout
111
+ self.ser.timeout = timeout
112
+ #There is a bug in Windows drivers that requires a delay after setting timeout
113
+ #When using certain Serial Interfaces
114
+ time.sleep(0.01)
111
115
 
112
116
  @property
113
117
  def reenumerates(self) -> bool:
@@ -12,16 +12,15 @@ def quat_mul(a: list[float], b: list[float]):
12
12
 
13
13
  #Rotates quaternion b by quaternion a, it does not combine them
14
14
  def quat_rotate(a: list[float], b: list[float]):
15
- inv = a.copy()
16
- inv[0] *= -1
17
- inv[1] *= -1
18
- inv[2] *= -1
15
+ inv = quat_inverse(a)
19
16
  axis = [b[0], b[1], b[2], 0]
20
17
  halfway = quat_mul(a, axis)
21
18
  final = quat_mul(halfway, inv)
22
19
  return [*final[:3], b[3]]
23
20
 
24
21
  def quat_inverse(quat: list[float]):
22
+ #Note: While technically negating just the W is rotationally equivalent, this is not a good idea
23
+ #as it will conflict with rotating vectors, which are not rotations, by quaternions
25
24
  return [-quat[0], -quat[1], -quat[2], quat[3]]
26
25
 
27
26
  def quat_rotate_vec(quat: list[float], vec: list[float]):
@@ -43,7 +42,6 @@ def angles_to_quaternion(angles: list[float], order: str, degrees=True):
43
42
  imaginary = math.sin(angle / 2)
44
43
  unit_vec = [v * imaginary for v in unit_vec]
45
44
  angle_quat = [*unit_vec, w]
46
- #print(f"{axis} {angle} {angle_quat}")
47
45
  quat = quat_mul(quat, angle_quat)
48
46
  return quat
49
47
 
@@ -160,8 +158,8 @@ def quaternion_swap_axes_fast(quat: list, old_parsed_order: list[list, list, boo
160
158
 
161
159
  if old_right_handed != new_right_handed:
162
160
  #Different handed systems rotate opposite directions. So to maintain the same rotation,
163
- #negate the rotation of the quaternion when swapping systems
164
- new_quat[-1] *= -1
161
+ #invert the quaternion
162
+ new_quat = quat_inverse(new_quat)
165
163
 
166
164
  return new_quat
167
165
 
@@ -471,16 +471,18 @@ class ThreespaceSensor:
471
471
  def __init__(self, com = None, timeout=2, verbose=False, initial_clear_timeout=None):
472
472
  if com is None: #Default to attempting to use the serial com class if none is provided
473
473
  com = ThreespaceSerialComClass
474
-
474
+ self.verbose = verbose
475
+
475
476
  manually_opened_com = False
476
477
  #Auto discover using the supplied com class type
477
478
  if inspect.isclass(com) and issubclass(com, ThreespaceComClass):
478
479
  new_com = None
480
+ self.log("Auto-Discovering Sensor")
479
481
  for serial_com in com.auto_detect():
480
482
  new_com = serial_com
481
483
  break #Exit after getting 1
482
484
  if new_com is None:
483
- raise RuntimeError("Failed to auto discover com port")
485
+ raise RuntimeError("Failed to auto discover com port")
484
486
  self.com = new_com
485
487
  manually_opened_com = True
486
488
  self.com.open()
@@ -496,11 +498,13 @@ class ThreespaceSensor:
496
498
  except:
497
499
  raise ValueError("Failed to create default ThreespaceSerialComClass from parameter:", type(com), com)
498
500
 
501
+ self.restart_delay = 0.5
502
+
503
+ self.log("Configuring sensor communication")
499
504
  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
500
505
  #Callback gives the debug message and sensor object that caused it
501
506
  self.__debug_cache: list[str] = [] #Used for storing startup debug messages until sensor state is confirmed
502
507
 
503
- self.verbose = verbose
504
508
  self.debug_callback: Callable[[str, ThreespaceSensor],None] = self.__default_debug_callback
505
509
  self.misaligned = False
506
510
  self.dirty_cache = False
@@ -511,11 +515,13 @@ class ThreespaceSensor:
511
515
  self.is_data_streaming = False
512
516
  self.is_log_streaming = False
513
517
  self.is_file_streaming = False
518
+ self.log("Stopping potential streaming")
514
519
  self._force_stop_streaming()
515
520
  #Clear out the buffer to allow faster initializing
516
521
  #Ex: If a large buffer build up due to streaming, especially if using a slower interface like BLE,
517
522
  #it may take a while before the entire garbage data can be parsed when checking for bootloader, causing a timeout
518
- #even though it would have eventually succeeded
523
+ #even though it would have eventually succeeded
524
+ self.log("Clearing com")
519
525
  self.__clear_com(initial_clear_timeout)
520
526
 
521
527
 
@@ -529,19 +535,24 @@ class ThreespaceSensor:
529
535
  self.getStreamingBatchCommand: ThreespaceGetStreamingBatchCommand = None
530
536
  self.funcs = {}
531
537
 
538
+ self.log("Checking firmware status")
532
539
  try:
533
540
  self.__cached_in_bootloader = self.__check_bootloader_status()
534
541
  if not self.in_bootloader:
542
+ self.log("Initializing firmware")
535
543
  self.__firmware_init()
536
544
  else:
545
+ self.log("Initializing bootloader")
537
546
  self.__cache_serial_number(self.bootloader_get_sn())
538
547
  self.__empty_debug_cache()
539
548
  #This is just to prevent a situation where instantiating the API creates and fails to release a com class on failure when user catches the exception
540
549
  #If user provides the com class, it is up to them to handle its state on error
541
550
  except Exception as e:
551
+ self.log("Failed to initialize sensor")
542
552
  if manually_opened_com:
543
553
  self.com.close()
544
554
  raise e
555
+ self.log("Successfully initialized sensor")
545
556
 
546
557
  #Just a helper for outputting information
547
558
  def log(self, *args):
@@ -554,6 +565,7 @@ class ThreespaceSensor:
554
565
  data = self.com.read_all()
555
566
  if refresh_timeout is None: return
556
567
  while len(data) > 0: #Continue until all data is cleared
568
+ self.log(f"Refresh clear Length: {len(data)}")
557
569
  start_time = time.time()
558
570
  while time.time() - start_time < refresh_timeout: #Wait up to refresh time for a new message
559
571
  data = self.com.read_all()
@@ -651,8 +663,8 @@ class ThreespaceSensor:
651
663
  method = types.MethodType(command.custom_func, self)
652
664
  else:
653
665
  #Build the actual method for executing the command
654
- code = f"def {command.info.name}(self, *args):\n"
655
- code += f" return self.execute_command(self.commands[{command.info.num}], *args)"
666
+ code = f"def {command.info.name}(self, *args, **kwargs):\n"
667
+ code += f" return self.execute_command(self.commands[{command.info.num}], *args, **kwargs)"
656
668
  exec(code, globals(), self.funcs)
657
669
  method = types.MethodType(self.funcs[command.info.name], self)
658
670
 
@@ -766,6 +778,17 @@ class ThreespaceSensor:
766
778
 
767
779
  #-----------------------------------------------BASE SETTINGS PROTOCOL------------------------------------------------
768
780
 
781
+ #Helper for converting python types to strings that set_settings can understand
782
+ def __internal_str(self, value):
783
+ if isinstance(value, float):
784
+ return f"{value:.10f}"
785
+ elif isinstance(value, bool):
786
+ return int(value)
787
+ elif isinstance(value, Enum):
788
+ return str(value.value)
789
+ else:
790
+ return str(value)
791
+
769
792
  #Can't just do if "header" in string because log_header_enabled exists and doesn't actually require cacheing the header
770
793
  HEADER_KEYS = ["header", "header_status", "header_timestamp", "header_echo", "header_checksum", "header_serial", "header_length"]
771
794
  def set_settings(self, param_string: str = None, **kwargs):
@@ -777,10 +800,10 @@ class ThreespaceSensor:
777
800
 
778
801
  for key, value in kwargs.items():
779
802
  if isinstance(value, list):
780
- value = [str(v) for v in value]
803
+ value = [self.__internal_str(v) for v in value]
781
804
  value = ','.join(value)
782
- elif isinstance(value, bool):
783
- value = int(value)
805
+ else:
806
+ value = self.__internal_str(value)
784
807
  params.append(f"{key}={value}")
785
808
  cmd = f"!{';'.join(params)}\n"
786
809
 
@@ -996,11 +1019,18 @@ class ThreespaceSensor:
996
1019
  """
997
1020
  header_len = len(header.raw_binary)
998
1021
  if header.length > max_data_length:
999
- self.log("DATA TOO BIG:", header.length)
1022
+ if not self.misaligned:
1023
+ self.log("DATA TOO BIG:", header.length)
1000
1024
  return False
1001
1025
  data = self.com.peek(header_len + header.length)[header_len:]
1002
- if len(data) != header.length: return False
1026
+ if len(data) != header.length:
1027
+ if not self.misaligned:
1028
+ self.log(f"Data Length Mismatch - Got: {len(data)} Expected: {header.length}")
1029
+ return False
1003
1030
  checksum = sum(data) % 256
1031
+ if checksum != header.checksum and not self.misaligned:
1032
+ self.log(f"Checksum Mismatch - Got: {checksum} Expected: {header.checksum}")
1033
+ self.log(f"Data: {data}")
1004
1034
  return checksum == header.checksum
1005
1035
 
1006
1036
  def __await_command(self, cmd: ThreespaceCommand, timeout=2):
@@ -1364,7 +1394,7 @@ class ThreespaceSensor:
1364
1394
  cmd.send_command(self.com)
1365
1395
  self.com.close()
1366
1396
  #TODO: Make this actually wait instead of an arbitrary sleep length
1367
- time.sleep(0.5) #Give it time to restart
1397
+ time.sleep(self.restart_delay) #Give it time to restart
1368
1398
  self.com.open()
1369
1399
  self.__firmware_init()
1370
1400
 
@@ -1375,7 +1405,7 @@ class ThreespaceSensor:
1375
1405
  cmd = self.commands[THREESPACE_ENTER_BOOTLOADER_COMMAND_NUM]
1376
1406
  cmd.send_command(self.com)
1377
1407
  #TODO: Make this actually wait instead of an arbitrary sleep length
1378
- time.sleep(0.5) #Give it time to boot into bootloader
1408
+ time.sleep(self.restart_delay) #Give it time to boot into bootloader
1379
1409
  if self.com.reenumerates:
1380
1410
  self.com.close()
1381
1411
  success = self.__attempt_rediscover_self()
@@ -1439,7 +1469,7 @@ class ThreespaceSensor:
1439
1469
  def bootloader_boot_firmware(self):
1440
1470
  if not self.in_bootloader: return
1441
1471
  self.com.write("B".encode())
1442
- time.sleep(0.5) #Give time to boot into firmware
1472
+ time.sleep(self.restart_delay) #Give time to boot into firmware
1443
1473
  if self.com.reenumerates:
1444
1474
  self.com.close()
1445
1475
  success = self.__attempt_rediscover_self()
@@ -1456,13 +1486,14 @@ class ThreespaceSensor:
1456
1486
  This may take a long time
1457
1487
  """
1458
1488
  self.com.write('S'.encode())
1459
- if timeout is not None:
1460
- cached_timeout = self.com.timeout
1461
- self.com.timeout = timeout
1462
- response = self.com.read(1)[0]
1463
- if timeout is not None:
1464
- self.com.timeout = cached_timeout
1465
- return response
1489
+
1490
+ start_time = time.perf_counter()
1491
+ response = []
1492
+ while len(response) == 0 and time.perf_counter() - start_time < timeout:
1493
+ response = self.com.read(1)
1494
+ if len(response) == 0:
1495
+ return -1
1496
+ return response[0]
1466
1497
 
1467
1498
  def bootloader_get_info(self):
1468
1499
  self.com.write('I'.encode())
@@ -1472,14 +1503,20 @@ class ThreespaceSensor:
1472
1503
  bootversion = struct.unpack(f">{_3space_format_to_external('I')}", self.com.read(2))[0]
1473
1504
  return ThreespaceBootloaderInfo(memstart, memend, pagesize, bootversion)
1474
1505
 
1475
- def bootloader_prog_mem(self, bytes: bytearray):
1506
+ def bootloader_prog_mem(self, bytes: bytearray, timeout=5):
1476
1507
  memsize = len(bytes)
1477
1508
  checksum = sum(bytes)
1478
1509
  self.com.write('C'.encode())
1479
1510
  self.com.write(struct.pack(f">{_3space_format_to_external('I')}", memsize))
1480
1511
  self.com.write(bytes)
1481
1512
  self.com.write(struct.pack(f">{_3space_format_to_external('B')}", checksum & 0xFFFF))
1482
- return self.com.read(1)[0]
1513
+ start_time = time.perf_counter()
1514
+ result = []
1515
+ while len(result) == 0 and time.perf_counter() - start_time < timeout:
1516
+ result = self.com.read(1)
1517
+ if len(result) > 0:
1518
+ return result[0]
1519
+ return -1
1483
1520
 
1484
1521
  def bootloader_get_state(self):
1485
1522
  self.com.write('OO'.encode()) #O is sent twice to compensate for a bug in some versions of the bootloader where the next character is ignored (except for R, do NOT send R after O, it will erase all settings)
File without changes
File without changes
File without changes