yostlabs 2025.1.3.4__tar.gz → 2025.1.9__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 (31) hide show
  1. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_commands.py +2 -1
  2. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_component_specific_settings_and_commands.py +2 -1
  3. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_firmware_upload.py +3 -2
  4. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_parsing_stored_binary.py +3 -2
  5. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_read_settings.py +2 -1
  6. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_streaming.py +2 -1
  7. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_streaming_manager.py +3 -2
  8. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_write_settings.py +2 -1
  9. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/PKG-INFO +1 -1
  10. yostlabs-2025.1.9/install.bat +1 -0
  11. yostlabs-2025.1.9/install_test.bat +1 -0
  12. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/pyproject.toml +4 -2
  13. yostlabs-2025.1.9/src/yostlabs/communication/base.py +90 -0
  14. yostlabs-2025.1.9/src/yostlabs/communication/serial.py +141 -0
  15. yostlabs-2025.1.9/src/yostlabs/math/__init__.py +1 -0
  16. {yostlabs-2025.1.3.4/src/yostlabs/tss3 → yostlabs-2025.1.9/src/yostlabs/math}/quaternion.py +25 -42
  17. yostlabs-2025.1.9/src/yostlabs/math/vector.py +31 -0
  18. yostlabs-2025.1.9/src/yostlabs/tss3/__init__.py +0 -0
  19. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/src/yostlabs/tss3/api.py +14 -235
  20. yostlabs-2025.1.9/src/yostlabs/tss3/utils/__init__.py +0 -0
  21. yostlabs-2025.1.9/src/yostlabs/tss3/utils/calibration.py +153 -0
  22. {yostlabs-2025.1.3.4/src/yostlabs/tss3 → yostlabs-2025.1.9/src/yostlabs/tss3/utils}/parser.py +0 -2
  23. yostlabs-2025.1.3.4/src/yostlabs/tss3/utils.py → yostlabs-2025.1.9/src/yostlabs/tss3/utils/streaming.py +5 -241
  24. yostlabs-2025.1.9/src/yostlabs/tss3/utils/version.py +76 -0
  25. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/embedded_2024_dec_20.xml +0 -0
  26. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/LICENSE +0 -0
  27. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/README.md +0 -0
  28. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/src/yostlabs/__init__.py +0 -0
  29. {yostlabs-2025.1.3.4/src/yostlabs/tss3 → yostlabs-2025.1.9/src/yostlabs/communication}/__init__.py +0 -0
  30. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/src/yostlabs/tss3/consts.py +0 -0
  31. {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/src/yostlabs/tss3/eepts.py +0 -0
@@ -1,4 +1,5 @@
1
- from Threespace3 import ThreespaceSensor, ThreespaceSerialComClass
1
+ from yostlabs.tss3.api import ThreespaceSensor
2
+ from yostlabs.communication.serial import ThreespaceSerialComClass
2
3
 
3
4
  #Create a sensor by auto detecting a ThreespaceSerialComClass
4
5
  sensor = ThreespaceSensor(ThreespaceSerialComClass)
@@ -1,5 +1,6 @@
1
1
 
2
- from Threespace3 import ThreespaceSensor, ThreespaceSerialComClass
2
+ from yostlabs.tss3.api import ThreespaceSensor
3
+ from yostlabs.communication.serial import ThreespaceSerialComClass
3
4
 
4
5
  """
5
6
  It is possible for a board to have multiple types of the same component on it. For example there may be two accelerometers, such as a low-G and high-G component.
@@ -1,5 +1,6 @@
1
- from Threespace3 import ThreespaceSensor, ThreespaceSerialComClass
2
- from Threespace3_utils import ThreespaceFirmwareUploader
1
+ from yostlabs.tss3.api import ThreespaceSensor
2
+ from yostlabs.communication.serial import ThreespaceSerialComClass
3
+ from yostlabs.tss3.utils.version import ThreespaceFirmwareUploader
3
4
 
4
5
  #Create a sensor by auto detecting a ThreespaceSerialComClass
5
6
  sensor = ThreespaceSensor(ThreespaceSerialComClass)
@@ -1,5 +1,6 @@
1
- from Threespace3 import ThreespaceSensor, ThreespaceSerialComClass, StreamableCommands
2
- from ThreespaceParser import ThreespaceBinaryParser
1
+ from yostlabs.tss3.api import ThreespaceSensor, StreamableCommands
2
+ from yostlabs.communication.serial import ThreespaceSerialComClass
3
+ from yostlabs.tss3.utils.parser import ThreespaceBinaryParser
3
4
  import time
4
5
 
5
6
  #Create a sensor by auto detecting a ThreespaceSerialComClass
@@ -1,4 +1,5 @@
1
- from Threespace3 import ThreespaceSensor, ThreespaceSerialComClass
1
+ from yostlabs.tss3.api import ThreespaceSensor
2
+ from yostlabs.communication.serial import ThreespaceSerialComClass
2
3
 
3
4
  #Create a sensor by auto detecting a ThreespaceSerialComClass
4
5
  sensor = ThreespaceSensor(ThreespaceSerialComClass)
@@ -1,4 +1,5 @@
1
- from Threespace3 import ThreespaceSensor, ThreespaceSerialComClass, StreamableCommands
1
+ from yostlabs.tss3.api import ThreespaceSensor, StreamableCommands
2
+ from yostlabs.communication.serial import ThreespaceSerialComClass
2
3
  import time
3
4
 
4
5
  #Create a sensor by auto detecting a ThreespaceSerialComClass
@@ -1,5 +1,6 @@
1
- from Threespace3 import ThreespaceSensor, ThreespaceSerialComClass, StreamableCommands
2
- from Threespace3_utils import ThreespaceStreamingManager, ThreespaceStreamingStatus
1
+ from yostlabs.tss3.api import ThreespaceSensor, StreamableCommands
2
+ from yostlabs.communication.serial import ThreespaceSerialComClass
3
+ from yostlabs.tss3.utils.streaming import ThreespaceStreamingManager, ThreespaceStreamingStatus
3
4
  import time
4
5
 
5
6
  """
@@ -1,4 +1,5 @@
1
- from Threespace3 import ThreespaceSensor, ThreespaceSerialComClass
1
+ from yostlabs.tss3.api import ThreespaceSensor
2
+ from yostlabs.communication.serial import ThreespaceSerialComClass
2
3
  import time
3
4
 
4
5
  #Create a sensor by auto detecting a ThreespaceSerialComClass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yostlabs
3
- Version: 2025.1.3.4
3
+ Version: 2025.1.9
4
4
  Summary: Python resources and API for 3Space sensors from Yost Labs Inc.
5
5
  Project-URL: Homepage, https://yostlabs.com/
6
6
  Author-email: "Yost Labs Inc." <techsupport@yostlabs.com>, Andy Riedlinger <techsupport@yostlabs.com>
@@ -0,0 +1 @@
1
+ python -m pip install --upgrade yostlabs
@@ -0,0 +1 @@
1
+ python -m pip install --index-url https://test.pypi.org/simple/ --upgrade --pre --no-deps yostlabs
@@ -4,14 +4,16 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [tool.hatch.build.targets.sdist]
6
6
  exclude = [
7
- "build.bat"
7
+ "build.bat",
8
+ "build_test.bat",
9
+ "env"
8
10
  ]
9
11
 
10
12
 
11
13
  [project]
12
14
  name = "yostlabs"
13
15
  #If uploading again on the same day, add a fourth number
14
- version = "2025.01.03.4"
16
+ version = "2025.01.09"
15
17
  authors = [
16
18
  { name="Yost Labs Inc.", email="techsupport@yostlabs.com" },
17
19
  { name="Andy Riedlinger", email="techsupport@yostlabs.com" },
@@ -0,0 +1,90 @@
1
+ from typing import Generator
2
+
3
+ class ThreespaceInputStream:
4
+
5
+ """
6
+ Reads specified number of bytes.
7
+ If that many bytes are not available after timeout, less data will be returned
8
+ """
9
+ def read(self, num_bytes) -> bytes:
10
+ raise NotImplementedError()
11
+
12
+ def read_all(self):
13
+ return self.read(self.length)
14
+
15
+ def read_until(self, expected: bytes) -> bytes:
16
+ raise NotImplementedError()
17
+
18
+ """Allows reading without removing the data from the buffer"""
19
+ def peek(self, num_bytes) -> bytes:
20
+ raise NotImplementedError()
21
+
22
+ def peek_until(self, expected: bytes, max_length=None) -> bytes:
23
+ raise NotImplementedError()
24
+
25
+ def readline(self) -> bytes:
26
+ return self.read_until(b"\n")
27
+
28
+ def peekline(self, max_length=None) -> bytes:
29
+ return self.peek_until(b"\n", max_length=max_length)
30
+
31
+ @property
32
+ def length(self) -> int:
33
+ raise NotImplementedError()
34
+
35
+ @property
36
+ def timeout(self) -> float:
37
+ raise NotImplementedError()
38
+
39
+ @timeout.setter
40
+ def timeout(self, timeout: float):
41
+ raise NotImplementedError()
42
+
43
+ class ThreespaceOutputStream:
44
+
45
+ """Write the given bytes"""
46
+ def write(self, bytes):
47
+ raise NotImplementedError()
48
+
49
+ class ThreespaceComClass(ThreespaceInputStream, ThreespaceOutputStream):
50
+ """
51
+ Base class for a com class to use with the sensor object.
52
+ Com classes should be initialized without connection and require
53
+ there open called before use
54
+ """
55
+
56
+ def close(self):
57
+ raise NotImplementedError()
58
+
59
+ def open(self) -> bool:
60
+ """
61
+ Should return True on success, False on failure
62
+ If already open, should stay open
63
+ """
64
+ raise NotImplementedError()
65
+
66
+ def check_open(self) -> bool:
67
+ """
68
+ Should return True if the port is currently open, False otherwise.
69
+ Must give the current state, not a cached state
70
+ """
71
+ raise NotImplementedError()
72
+
73
+ @staticmethod
74
+ def auto_detect() -> Generator["ThreespaceComClass", None, None]:
75
+ """
76
+ Returns a list of com classes of the same type called on nearby
77
+ """
78
+ raise NotImplementedError()
79
+
80
+ @property
81
+ def reenumerates(self) -> bool:
82
+ """
83
+ If the device Re-Enumerates when going from bootloader to firmware or vice versa, this must return True.
84
+ This indicates to the API that it must search for the new com class representing the object when switching between bootloader and firmware
85
+ """
86
+ raise NotImplementedError
87
+
88
+ @property
89
+ def name(self) -> str:
90
+ raise NotImplementedError()
@@ -0,0 +1,141 @@
1
+ from yostlabs.communication.base import *
2
+ import serial
3
+ import serial.tools.list_ports
4
+
5
+
6
+ class ThreespaceSerialComClass(ThreespaceComClass):
7
+
8
+ PID_V3_MASK = 0x3000
9
+ PID_BOOTLOADER = 0x1000 #We should really change this to 0x3000
10
+
11
+ VID = 0x2476
12
+
13
+ DEFAULT_BAUDRATE = 115200
14
+ DEFAULT_TIMEOUT = 2
15
+
16
+ def __init__(self, ser: serial.Serial | str):
17
+ if isinstance(ser, serial.Serial):
18
+ self.ser = ser
19
+ elif isinstance(ser, str):
20
+ self.ser = serial.Serial(ser, baudrate=ThreespaceSerialComClass.DEFAULT_BAUDRATE, timeout=ThreespaceSerialComClass.DEFAULT_TIMEOUT)
21
+ else:
22
+ raise TypeError("Invalid type for creating a ThreespaceSerialComClass:", type(ser), ser)
23
+
24
+ self.ser = ser
25
+
26
+ self.peek_buffer = bytearray()
27
+ self.peek_length = 0
28
+
29
+ def write(self, bytes):
30
+ self.ser.write(bytes)
31
+
32
+ def read(self, num_bytes):
33
+ if self.peek_length >= num_bytes:
34
+ result = self.peek_buffer[:num_bytes]
35
+ self.peek_length -= num_bytes
36
+ del self.peek_buffer[:num_bytes]
37
+ else:
38
+ result = self.peek_buffer + self.ser.read(num_bytes - self.peek_length) #Must supply the amount desired to read instead of just buffering so the timeout works
39
+ self.peek_buffer.clear()
40
+ self.peek_length = 0
41
+ return result
42
+
43
+ def peek(self, num_bytes):
44
+ if self.peek_length >= num_bytes:
45
+ return self.peek_buffer[:num_bytes]
46
+ else:
47
+ self.peek_buffer += self.ser.read(num_bytes - self.peek_length) #Must supply the amount desired to read instead of just buffering so the timeout works
48
+ self.peek_length = len(self.peek_buffer) #The read may have timed out, so calculate new size
49
+ return self.peek_buffer.copy()
50
+
51
+ def read_until(self, expected: bytes) -> bytes:
52
+ if expected in self.peek_buffer:
53
+ length = self.peek_buffer.index(expected) + len(expected)
54
+ result = self.peek_buffer[:length]
55
+ self.peek_length -= length
56
+ del self.peek_buffer[:length]
57
+ return result
58
+ #Have to actually read from serial port until the data is available
59
+ result = self.peek_buffer + self.ser.read_until(expected)
60
+ self.peek_buffer.clear()
61
+ self.peek_length = 0
62
+ return result
63
+
64
+ def peek_until(self, expected: bytes, max_length=None) -> bytes:
65
+ if expected in self.peek_buffer:
66
+ length = self.peek_buffer.index(expected) + len(expected)
67
+ if max_length is not None and length > max_length:
68
+ length = max_length
69
+ result = self.peek_buffer[:length]
70
+ return result
71
+ if max_length is not None and self.peek_length >= max_length:
72
+ return self.peek_buffer[:max_length]
73
+
74
+ #Have to actually read from serial port until the data is available
75
+ if max_length is not None:
76
+ max_length = max(0, max_length - self.peek_length)
77
+ self.peek_buffer += self.ser.read_until(expected, size=max_length)
78
+ self.peek_length = len(self.peek_buffer)
79
+ return self.peek_buffer.copy()
80
+
81
+ def close(self):
82
+ self.ser.close()
83
+ self.peek_buffer.clear()
84
+ self.peek_length = 0
85
+
86
+ def open(self):
87
+ try:
88
+ self.ser.open()
89
+ except:
90
+ return False
91
+ return True
92
+
93
+ def check_open(self):
94
+ try:
95
+ self.ser.in_waiting
96
+ except:
97
+ return False
98
+ return True
99
+
100
+ @property
101
+ def length(self):
102
+ return self.peek_length + self.ser.in_waiting
103
+
104
+ @property
105
+ def timeout(self) -> float:
106
+ return self.ser.timeout
107
+
108
+ @timeout.setter
109
+ def timeout(self, timeout: float):
110
+ self.ser.timeout = timeout
111
+
112
+ @property
113
+ def reenumerates(self) -> bool:
114
+ return True
115
+
116
+ @property
117
+ def name(self) -> str:
118
+ return self.ser.port
119
+
120
+ #This is not part of the ThreespaceComClass interface, but is useful as a utility for those directly using the ThreespaceSerialComClass
121
+ @staticmethod
122
+ def enumerate_ports():
123
+ cls = ThreespaceSerialComClass
124
+ ports = serial.tools.list_ports.comports()
125
+ for port in ports:
126
+ if port.vid == cls.VID and (port.pid & cls.PID_V3_MASK == cls.PID_V3_MASK or port.pid == cls.PID_BOOTLOADER):
127
+ yield port
128
+
129
+ @staticmethod
130
+ def auto_detect(default_timeout=2, default_baudrate=115200) -> Generator["ThreespaceSerialComClass", None, None]:
131
+ """
132
+ Returns a list of com classes of the same type called on nearby.
133
+ These ports will start unopened. This allows the caller to get a list of ports without having to connect.
134
+ """
135
+ cls = ThreespaceSerialComClass
136
+ ports = serial.tools.list_ports.comports()
137
+ for port in ports:
138
+ if port.vid == cls.VID and (port.pid & cls.PID_V3_MASK == cls.PID_V3_MASK or port.pid == cls.PID_BOOTLOADER):
139
+ ser = serial.Serial(None, baudrate=default_baudrate, timeout=default_timeout) #By setting port as None, can create an object without immediately opening the port
140
+ ser.port = port.device #Now assign the port, allowing the serial object to exist without being opened yet
141
+ yield ThreespaceSerialComClass(ser)
@@ -0,0 +1 @@
1
+ __all__ = ["quaternion", "vector"]
@@ -1,23 +1,5 @@
1
1
  import math
2
-
3
- def vec_len(vector: list[float|int]):
4
- return sum(v ** 2 for v in vector) ** 0.5
5
-
6
- def vec_dot(a: list[float], b: list[float]):
7
- return sum(a[i] * b[i] for i in range(len(a)))
8
-
9
- def vec_cross(a: list[float], b: list[float]):
10
- cross = [0, 0, 0]
11
- cross[0] = a[1] * b[2] - a[2] * b[1]
12
- cross[1] = a[2] * b[0] - a[0] * b[2]
13
- cross[2] = a[0] * b[1] - a[1] * b[0]
14
- return cross
15
-
16
- def vec_normalize(vec: list[float]):
17
- l = vec_len(vec)
18
- if l == 0:
19
- return vec
20
- return [v / l for v in vec]
2
+ import yostlabs.math.vector as _vec
21
3
 
22
4
  def quat_mul(a: list[float], b: list[float]):
23
5
  out = [0, 0, 0, 0]
@@ -49,19 +31,6 @@ def quat_rotate_vec(quat: list[float], vec: list[float]):
49
31
  final = quat_mul(halfway, inv)
50
32
  return [final[0], final[1], final[2]]
51
33
 
52
- def is_right_handed(order: str, negations: list[bool]):
53
- right_handed = order.lower() in ("xzy", "yxz", "zyx")
54
- for i in range(3):
55
- if negations[i]:
56
- right_handed = not right_handed
57
- return right_handed
58
-
59
- def axis_to_unit_vector(axis: str):
60
- axis = axis.lower()
61
- if axis == 'x' or axis == 0: return [1, 0, 0]
62
- if axis == 'y' or axis == 1: return [0, 1, 0]
63
- if axis == 'z' or axis == 2: return [0, 0, 1]
64
-
65
34
  def angles_to_quaternion(angles: list[float], order: str, degrees=True):
66
35
  quat = [0, 0, 0, 1]
67
36
  for i in range(len(angles)):
@@ -69,7 +38,7 @@ def angles_to_quaternion(angles: list[float], order: str, degrees=True):
69
38
  angle = angles[i]
70
39
  if degrees:
71
40
  angle = math.radians(angle)
72
- unit_vec = axis_to_unit_vector(axis)
41
+ unit_vec = _vec.axis_to_unit_vector(axis)
73
42
  w = math.cos(angle / 2)
74
43
  imaginary = math.sin(angle / 2)
75
44
  unit_vec = [v * imaginary for v in unit_vec]
@@ -91,15 +60,15 @@ def quat_from_two_vectors(forward: list[float], down: list[float]):
91
60
  forward_reference = [0, 0, 1]
92
61
  down_reference = [0, -1, 0]
93
62
 
94
- forward = vec_normalize(forward)
95
- down = vec_normalize(down)
63
+ forward = _vec.vec_normalize(forward)
64
+ down = _vec.vec_normalize(down)
96
65
 
97
66
  #Create the first rotation to align the forward axis
98
- axis_of_rotation = vec_cross(forward_reference, forward)
99
- axis_of_rotation = vec_normalize(axis_of_rotation)
67
+ axis_of_rotation = _vec.vec_cross(forward_reference, forward)
68
+ axis_of_rotation = _vec.vec_normalize(axis_of_rotation)
100
69
  if not any(abs(v) > 0 for v in axis_of_rotation):
101
70
  axis_of_rotation = down_reference #This is just a direct 180 degree rotation around any orthogonal axis, so just use the down_ref
102
- dot = min(1, max(-1, vec_dot(forward_reference, forward)))
71
+ dot = min(1, max(-1, _vec.vec_dot(forward_reference, forward)))
103
72
  angle1 = math.acos(dot)
104
73
  imaginary = math.sin(angle1/2)
105
74
  quat = [v * imaginary for v in axis_of_rotation] #XYZ
@@ -109,11 +78,11 @@ def quat_from_two_vectors(forward: list[float], down: list[float]):
109
78
  down_reference = quat_rotate_vec(quat, down_reference)
110
79
 
111
80
  #find the rotation to make the remaining reference align with its given value
112
- axis_of_rotation = vec_cross(down_reference, down)
113
- axis_of_rotation = vec_normalize(axis_of_rotation)
81
+ axis_of_rotation = _vec.vec_cross(down_reference, down)
82
+ axis_of_rotation = _vec.vec_normalize(axis_of_rotation)
114
83
  if not any(abs(v) > 0 for v in axis_of_rotation):
115
84
  axis_of_rotation = forward #Rotate along the final forward vector until up is aligned
116
- dot = min(1, max(-1, vec_dot(down_reference, down)))
85
+ dot = min(1, max(-1, _vec.vec_dot(down_reference, down)))
117
86
  angle2 = math.acos(dot)
118
87
  imaginary = math.sin(angle2/2)
119
88
  rotation = [v * imaginary for v in axis_of_rotation] #XYZ
@@ -163,4 +132,18 @@ def quaternion_global_to_local(quat, vec):
163
132
  return quat_rotate_vec(inverse, vec)
164
133
 
165
134
  def quaternion_local_to_global(quat, vec):
166
- return quat_rotate_vec(quat, vec)
135
+ return quat_rotate_vec(quat, vec)
136
+
137
+ #https://splines.readthedocs.io/en/latest/rotation/slerp.html
138
+ def slerp(a, b, t):
139
+ dot = _vec.vec_dot(a, b)
140
+ if dot < 0: #To force it to be the shortest route
141
+ b = [-v for v in b]
142
+
143
+ theta = math.acos(dot)
144
+ sin_theta = math.sin(theta)
145
+ r1 = math.sin(1 - t) * theta / sin_theta
146
+ r2 = math.sin(t * theta) / sin_theta
147
+ a = [r1 * v for v in a]
148
+ b = [r2 * v for v in b]
149
+ return _vec.vec_normalize([v + w for v, w in zip(a, b)])
@@ -0,0 +1,31 @@
1
+ def vec_len(vector: list[float|int]):
2
+ return sum(v ** 2 for v in vector) ** 0.5
3
+
4
+ def vec_dot(a: list[float], b: list[float]):
5
+ return sum(a[i] * b[i] for i in range(len(a)))
6
+
7
+ def vec_cross(a: list[float], b: list[float]):
8
+ cross = [0, 0, 0]
9
+ cross[0] = a[1] * b[2] - a[2] * b[1]
10
+ cross[1] = a[2] * b[0] - a[0] * b[2]
11
+ cross[2] = a[0] * b[1] - a[1] * b[0]
12
+ return cross
13
+
14
+ def vec_normalize(vec: list[float]):
15
+ l = vec_len(vec)
16
+ if l == 0:
17
+ return vec
18
+ return [v / l for v in vec]
19
+
20
+ def vec_is_right_handed(order: str, negations: list[bool]):
21
+ right_handed = order.lower() in ("xzy", "yxz", "zyx")
22
+ for i in range(3):
23
+ if negations[i]:
24
+ right_handed = not right_handed
25
+ return right_handed
26
+
27
+ def axis_to_unit_vector(axis: str):
28
+ axis = axis.lower()
29
+ if axis == 'x' or axis == 0: return [1, 0, 0]
30
+ if axis == 'y' or axis == 1: return [0, 1, 0]
31
+ if axis == 'z' or axis == 2: return [0, 0, 1]
File without changes
@@ -1,233 +1,15 @@
1
1
  from yostlabs.tss3.consts import *
2
+ from yostlabs.communication.base import ThreespaceInputStream, ThreespaceOutputStream, ThreespaceComClass
3
+ from yostlabs.communication.serial import ThreespaceSerialComClass
2
4
 
3
- import serial
4
- import serial.tools.list_ports
5
5
  from enum import Enum
6
6
  from dataclasses import dataclass, field
7
- from typing import Any, TypeVar, Generic, Generator
7
+ from typing import TypeVar, Generic
8
8
  import struct
9
9
  import types
10
10
  import inspect
11
11
  import time
12
12
 
13
- class ThreespaceInputStream:
14
-
15
- """
16
- Reads specified number of bytes.
17
- If that many bytes are not available after timeout, less data will be returned
18
- """
19
- def read(self, num_bytes) -> bytes:
20
- raise NotImplementedError()
21
-
22
- def read_all(self):
23
- return self.read(self.length)
24
-
25
- def read_until(self, expected: bytes) -> bytes:
26
- raise NotImplementedError()
27
-
28
- """Allows reading without removing the data from the buffer"""
29
- def peek(self, num_bytes) -> bytes:
30
- raise NotImplementedError()
31
-
32
- def peek_until(self, expected: bytes, max_length=None) -> bytes:
33
- raise NotImplementedError()
34
-
35
- def readline(self) -> bytes:
36
- return self.read_until(b"\n")
37
-
38
- def peekline(self, max_length=None) -> bytes:
39
- return self.peek_until(b"\n", max_length=max_length)
40
-
41
- @property
42
- def length(self) -> int:
43
- raise NotImplementedError()
44
-
45
- @property
46
- def timeout(self) -> float:
47
- raise NotImplementedError()
48
-
49
- @timeout.setter
50
- def timeout(self, timeout: float):
51
- raise NotImplementedError()
52
-
53
- class ThreespaceOutputStream:
54
-
55
- """Write the given bytes"""
56
- def write(self, bytes):
57
- raise NotImplementedError()
58
-
59
- class ThreespaceComClass(ThreespaceInputStream, ThreespaceOutputStream):
60
- """
61
- Base class for a com class to use with the sensor object.
62
- Com classes should be initialized without connection and require
63
- there open called before use
64
- """
65
-
66
- def close(self):
67
- raise NotImplementedError()
68
-
69
- def open(self) -> bool:
70
- """
71
- Should return True on success, False on failure
72
- If already open, should stay open
73
- """
74
- raise NotImplementedError()
75
-
76
- def check_open(self) -> bool:
77
- """
78
- Should return True if the port is currently open, False otherwise.
79
- Must give the current state, not a cached state
80
- """
81
- raise NotImplementedError()
82
-
83
- @staticmethod
84
- def auto_detect() -> Generator["ThreespaceComClass", None, None]:
85
- """
86
- Returns a list of com classes of the same type called on nearby
87
- """
88
- raise NotImplementedError()
89
-
90
- @property
91
- def reenumerates(self) -> bool:
92
- """
93
- If the device Re-Enumerates when going from bootloader to firmware or vice versa, this must return True.
94
- This indicates to the API that it must search for the new com class representing the object when switching between bootloader and firmware
95
- """
96
- raise NotImplementedError
97
-
98
- @property
99
- def name(self) -> str:
100
- raise NotImplementedError()
101
-
102
- class ThreespaceSerialComClass(ThreespaceComClass):
103
-
104
- PID_V3_MASK = 0x3000
105
- PID_BOOTLOADER = 0x1000 #We should really change this to 0x3000
106
-
107
- VID = 0x2476
108
-
109
- def __init__(self, ser: serial.Serial):
110
- self.ser = ser
111
-
112
- self.peek_buffer = bytearray()
113
- self.peek_length = 0
114
-
115
- def write(self, bytes):
116
- self.ser.write(bytes)
117
-
118
- def read(self, num_bytes):
119
- if self.peek_length >= num_bytes:
120
- result = self.peek_buffer[:num_bytes]
121
- self.peek_length -= num_bytes
122
- del self.peek_buffer[:num_bytes]
123
- #self.peek_buffer = self.peek_buffer[num_bytes:]
124
- else:
125
- result = self.peek_buffer + self.ser.read(num_bytes - self.peek_length) #Must supply the amount desired to read instead of just buffering so the timeout works
126
- self.peek_buffer.clear()
127
- self.peek_length = 0
128
- return result
129
-
130
- def peek(self, num_bytes):
131
- if self.peek_length >= num_bytes:
132
- return self.peek_buffer[:num_bytes]
133
- else:
134
- self.peek_buffer += self.ser.read(num_bytes - self.peek_length) #Must supply the amount desired to read instead of just buffering so the timeout works
135
- self.peek_length = len(self.peek_buffer) #The read may have timed out, so calculate new size
136
- return self.peek_buffer.copy()
137
-
138
- def read_until(self, expected: bytes) -> bytes:
139
- if expected in self.peek_buffer:
140
- length = self.peek_buffer.index(expected) + len(expected)
141
- result = self.peek_buffer[:length]
142
- self.peek_length -= length
143
- del self.peek_buffer[:length]
144
- #self.peek_buffer = self.peek_buffer[length:]
145
- return result
146
- #Have to actually read from serial port until the data is available
147
- result = self.peek_buffer + self.ser.read_until(expected)
148
- self.peek_buffer.clear()
149
- self.peek_length = 0
150
- return result
151
-
152
- def peek_until(self, expected: bytes, max_length=None) -> bytes:
153
- if expected in self.peek_buffer:
154
- length = self.peek_buffer.index(expected) + len(expected)
155
- if max_length is not None and length > max_length:
156
- length = max_length
157
- result = self.peek_buffer[:length]
158
- return result
159
- if max_length is not None and self.peek_length >= max_length:
160
- return self.peek_buffer[:max_length]
161
-
162
- #Have to actually read from serial port until the data is available
163
- if max_length is not None:
164
- max_length = max(0, max_length - self.peek_length)
165
- self.peek_buffer += self.ser.read_until(expected, size=max_length)
166
- self.peek_length = len(self.peek_buffer)
167
- return self.peek_buffer.copy()
168
-
169
- def close(self):
170
- self.ser.close()
171
- self.peek_buffer.clear()
172
- self.peek_length = 0
173
-
174
- def open(self):
175
- try:
176
- self.ser.open()
177
- except:
178
- return False
179
- return True
180
-
181
- def check_open(self):
182
- try:
183
- self.ser.in_waiting
184
- except:
185
- return False
186
- return True
187
-
188
- @property
189
- def length(self):
190
- return self.peek_length + self.ser.in_waiting
191
-
192
- @property
193
- def timeout(self) -> float:
194
- return self.ser.timeout
195
-
196
- @timeout.setter
197
- def timeout(self, timeout: float):
198
- self.ser.timeout = timeout
199
-
200
- @property
201
- def reenumerates(self) -> bool:
202
- return True
203
-
204
- @property
205
- def name(self) -> str:
206
- return self.ser.port
207
-
208
- #This is not part of the ThreespaceComClass interface, but is useful as a utility for those directly using the ThreespaceSerialComClass
209
- @staticmethod
210
- def enumerate_ports():
211
- cls = ThreespaceSerialComClass
212
- ports = serial.tools.list_ports.comports()
213
- for port in ports:
214
- if port.vid == cls.VID and (port.pid & cls.PID_V3_MASK == cls.PID_V3_MASK or port.pid == cls.PID_BOOTLOADER):
215
- yield port
216
-
217
- @staticmethod
218
- def auto_detect(default_timeout=2, default_baudrate=115200) -> Generator["ThreespaceSerialComClass", None, None]:
219
- """
220
- Returns a list of com classes of the same type called on nearby.
221
- These ports will start unopened. This allows the caller to get a list of ports without having to connect.
222
- """
223
- cls = ThreespaceSerialComClass
224
- ports = serial.tools.list_ports.comports()
225
- for port in ports:
226
- if port.vid == cls.VID and (port.pid & cls.PID_V3_MASK == cls.PID_V3_MASK or port.pid == cls.PID_BOOTLOADER):
227
- ser = serial.Serial(None, baudrate=default_baudrate, timeout=default_timeout) #By setting port as None, can create an object without immediately opening the port
228
- ser.port = port.device #Now assign the port, allowing the serial object to exist without being opened yet
229
- yield ThreespaceSerialComClass(ser)
230
-
231
13
 
232
14
  #For converting from internal format specifiers to struct module specifiers
233
15
  __3space_format_conversion_dictionary = {
@@ -620,10 +402,6 @@ class StreamableCommands(Enum):
620
402
 
621
403
  GetButtonState = 250
622
404
 
623
- THREESPACE_STREAMING_STATUS_NORMAL = 0
624
- THREESPACE_STREAMING_STATUS_INTERCEPT = 1
625
- THREESPACE_STREAMING_STATUS_UNKNOWN_ECHO = 2
626
-
627
405
  THREESPACE_AWAIT_COMMAND_FOUND = 0
628
406
  THREESPACE_AWAIT_COMMAND_TIMEOUT = 1
629
407
 
@@ -673,10 +451,11 @@ THREESPACE_REQUIRED_HEADER = THREESPACE_HEADER_ECHO_BIT | THREESPACE_HEADER_CHEC
673
451
  class ThreespaceSensor:
674
452
 
675
453
  def __init__(self, com = None, timeout=2):
676
- if com is None: #Default to attempting to use the serial com class
454
+ if com is None: #Default to attempting to use the serial com class if none is provided
677
455
  com = ThreespaceSerialComClass
678
-
679
- if inspect.isclass(com) and issubclass(com, ThreespaceComClass): #Auto discover
456
+
457
+ #Auto discover using the supplied com class type
458
+ if inspect.isclass(com) and issubclass(com, ThreespaceComClass):
680
459
  new_com = None
681
460
  for serial_com in com.auto_detect():
682
461
  new_com = serial_com
@@ -685,14 +464,14 @@ class ThreespaceSensor:
685
464
  raise RuntimeError("Failed to auto discover com port")
686
465
  self.com = new_com
687
466
  self.com.open()
688
- elif inspect.isclass(type(com)) and issubclass(type(com), ThreespaceComClass): #Already a com class
467
+ #The supplied com already was a com class, nothing to do
468
+ elif inspect.isclass(type(com)) and issubclass(type(com), ThreespaceComClass):
689
469
  self.com = com
690
- elif isinstance(com, str): #Use string to open a serial port
691
- self.com = ThreespaceSerialComClass(serial.Serial(com, baudrate=115200, timeout=timeout))
692
- elif isinstance(com, serial.Serial):
693
- self.com = ThreespaceSerialComClass(com)
694
- else:
695
- raise ValueError("Unknown type", type(com))
470
+ else: #Unknown type, try making a ThreespaceSerialComClass out of this
471
+ try:
472
+ self.com = ThreespaceSerialComClass(com)
473
+ except:
474
+ raise ValueError("Failed to create default ThreespaceSerialComClass from parameter:", type(com), com)
696
475
 
697
476
  self.com.read_all() #Clear anything that may be there
698
477
 
File without changes
@@ -0,0 +1,153 @@
1
+ import yostlabs.math.quaternion as quat
2
+ import yostlabs.math.vector as vec
3
+
4
+ import numpy as np
5
+ from dataclasses import dataclass
6
+ import copy
7
+
8
+ class ThreespaceGradientDescentCalibration:
9
+
10
+ @dataclass
11
+ class StageInfo:
12
+ start_vector: int
13
+ end_vector: int
14
+ stage: int
15
+ scale: float
16
+
17
+ count: int = 0
18
+
19
+ MAX_SCALE = 1000000000
20
+ MIN_SCALE = 1
21
+ STAGES = [
22
+ StageInfo(0, 6, 0, MAX_SCALE),
23
+ StageInfo(0, 12, 1, MAX_SCALE),
24
+ StageInfo(0, 24, 2, MAX_SCALE)
25
+ ]
26
+
27
+ #Note that each entry has a positive and negative vector included in this list
28
+ CHANGE_VECTORS = [
29
+ np.array([0,0,0,0,0,0,0,0,0,.0001,0,0], dtype=np.float64),
30
+ np.array([0,0,0,0,0,0,0,0,0,-.0001,0,0], dtype=np.float64),
31
+ np.array([0,0,0,0,0,0,0,0,0,0,.0001,0], dtype=np.float64),
32
+ np.array([0,0,0,0,0,0,0,0,0,0,-.0001,0], dtype=np.float64),
33
+ np.array([0,0,0,0,0,0,0,0,0,0,0,.0001], dtype=np.float64),
34
+ np.array([0,0,0,0,0,0,0,0,0,0,0,-.0001], dtype=np.float64), #First 6 only try to change the bias
35
+ np.array([.001,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
36
+ np.array([-.001,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
37
+ np.array([0,0,0,0,.001,0,0,0,0,0,0,0], dtype=np.float64),
38
+ np.array([0,0,0,0,-.001,0,0,0,0,0,0,0], dtype=np.float64),
39
+ np.array([0,0,0,0,0,0,0,0,.001,0,0,0], dtype=np.float64),
40
+ np.array([0,0,0,0,0,0,0,0,-.001,0,0,0], dtype=np.float64), #Next 6 only try to change the scale
41
+ np.array([0,.0001,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
42
+ np.array([0,-.0001,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
43
+ np.array([0,0,.0001,0,0,0,0,0,0,0,0,0], dtype=np.float64),
44
+ np.array([0,0,-.0001,0,0,0,0,0,0,0,0,0], dtype=np.float64),
45
+ np.array([0,0,0,.0001,0,0,0,0,0,0,0,0], dtype=np.float64),
46
+ np.array([0,0,0,-.0001,0,0,0,0,0,0,0,0], dtype=np.float64),
47
+ np.array([0,0,0,0,0,.0001,0,0,0,0,0,0], dtype=np.float64),
48
+ np.array([0,0,0,0,0,-.0001,0,0,0,0,0,0], dtype=np.float64),
49
+ np.array([0,0,0,0,0,0,.0001,0,0,0,0,0], dtype=np.float64),
50
+ np.array([0,0,0,0,0,0,-.0001,0,0,0,0,0], dtype=np.float64),
51
+ np.array([0,0,0,0,0,0,0,.0001,0,0,0,0], dtype=np.float64),
52
+ np.array([0,0,0,0,0,0,0,-.0001,0,0,0,0], dtype=np.float64), #Next 12 only try to change the shear
53
+ ]
54
+
55
+ def __init__(self, relative_sensor_orients: list[np.ndarray[float]], no_inverse=False):
56
+ """
57
+ Params
58
+ ------
59
+ relative_sensor_orients : The orientation of the sensor during which each sample is taken if it was tared as if pointing into the screen.
60
+ The inverse of these will be used to calculate where the axes should be located relative to the sensor
61
+ no_inverse : The relative_sensor_orients will be treated as the sample_rotations
62
+ """
63
+ if no_inverse:
64
+ self.rotation_quats = relative_sensor_orients
65
+ else:
66
+ self.rotation_quats = [np.array(quat.quat_inverse(orient)) for orient in relative_sensor_orients]
67
+
68
+ def apply_parameters(self, sample: np.ndarray[float], params: np.ndarray[float]):
69
+ bias = params[9:]
70
+ scale = params[:9]
71
+ scale = scale.reshape((3, 3))
72
+ return scale @ (sample + bias)
73
+
74
+ def rate_parameters(self, params: np.ndarray[float], samples: list[np.ndarray[float]], targets: list[np.ndarray[float]]):
75
+ total_error = 0
76
+ for i in range(len(samples)):
77
+ sample = samples[i]
78
+ target = targets[i]
79
+
80
+ sample = self.apply_parameters(sample, params)
81
+
82
+ error = target - sample
83
+ total_error += vec.vec_len(error)
84
+ return total_error
85
+
86
+ def generate_target_list(self, origin: np.ndarray):
87
+ targets = []
88
+ for orient in self.rotation_quats:
89
+ new_vec = np.array(quat.quat_rotate_vec(orient, origin), dtype=np.float64)
90
+ targets.append(new_vec)
91
+ return targets
92
+
93
+ def __get_stage(self, stage_number: int):
94
+ if stage_number >= len(self.STAGES):
95
+ return None
96
+ #Always get a shallow copy of the stage so can modify without removing the initial values
97
+ return copy.copy(self.STAGES[stage_number])
98
+
99
+ def calculate(self, samples: list[np.ndarray[float]], origin: np.ndarray[float], verbose=False, max_cycles_per_stage=1000):
100
+ targets = self.generate_target_list(origin)
101
+ initial_params = np.array([1,0,0,0,1,0,0,0,1,0,0,0], dtype=np.float64)
102
+ stage = self.__get_stage(0)
103
+
104
+ best_params = initial_params
105
+ best_rating = self.rate_parameters(best_params, samples, targets)
106
+ count = 0
107
+ while True:
108
+ last_best_rating = best_rating
109
+ params = best_params
110
+
111
+ #Apply all the changes to see if any improve the result
112
+ for change_index in range(stage.start_vector, stage.end_vector):
113
+ change_vector = self.CHANGE_VECTORS[change_index]
114
+ new_params = params + (change_vector * stage.scale)
115
+ rating = self.rate_parameters(new_params, samples, targets)
116
+
117
+ #A better rating, store it
118
+ if rating < best_rating:
119
+ best_params = new_params
120
+ best_rating = rating
121
+
122
+ if verbose and count % 100 == 0:
123
+ print(f"Round {count}: {best_rating=} {stage=}")
124
+
125
+ #Decide if need to go to the next stage or not
126
+ count += 1
127
+ stage.count += 1
128
+ if stage.count >= max_cycles_per_stage:
129
+ stage = self.__get_stage(stage.stage + 1)
130
+ if stage is None:
131
+ if verbose: print("Done from reaching count limit")
132
+ break
133
+ if verbose: print("Going to next stage from count limit")
134
+
135
+ if best_rating == last_best_rating: #The rating did not improve
136
+ if stage.scale == self.MIN_SCALE: #Go to the next stage since can't get any better in this stage!
137
+ stage = self.__get_stage(stage.stage + 1)
138
+ if stage is None:
139
+ if verbose: print("Done from exhaustion")
140
+ break
141
+ if verbose: print("Going to next stage from exhaustion")
142
+ else: #Reduce the size of the changes to hopefully get more accurate tuning
143
+ stage.scale *= 0.1
144
+ if stage.scale < self.MIN_SCALE:
145
+ stage.scale = self.MIN_SCALE
146
+ else: #Rating got better! To help avoid falling in a local minimum, increase the size of the change to see if that could make it better
147
+ stage.scale *= 1.1
148
+
149
+ if verbose:
150
+ print(f"Final Rating: {best_rating}")
151
+ print(f"Final Params: {best_params}")
152
+
153
+ return best_params
@@ -15,7 +15,6 @@ class ThreespaceBufferInputStream(ThreespaceInputStream):
15
15
  num_bytes = min(len(self.buffer), num_bytes)
16
16
  result = self.buffer[:num_bytes]
17
17
  del self.buffer[:num_bytes]
18
- #self.buffer = self.buffer[num_bytes:]
19
18
  return result
20
19
 
21
20
  def read_all(self):
@@ -28,7 +27,6 @@ class ThreespaceBufferInputStream(ThreespaceInputStream):
28
27
  length = self.buffer.index(expected) + len(expected)
29
28
  result = self.buffer[:length]
30
29
  del self.buffer[:length]
31
- #self.buffer = self.buffer[length:]
32
30
  raise result
33
31
 
34
32
  """Allows reading without removing the data from the buffer"""
@@ -1,11 +1,8 @@
1
- from .api import ThreespaceSensor, ThreespaceCommand, StreamableCommands, ThreespaceCmdResult, threespaceGetHeaderLabels
2
- from dataclasses import dataclass, field
3
- import copy
4
- from typing import Any, Callable
5
- from enum import Enum
1
+ from yostlabs.tss3.api import ThreespaceSensor, StreamableCommands, ThreespaceCmdResult, threespaceGetHeaderLabels
6
2
 
7
- import numpy as np
8
- import yostlabs.tss3.quaternion as yl_math
3
+ from enum import Enum
4
+ from typing import Any, Callable
5
+ from dataclasses import dataclass, field
9
6
 
10
7
  class ThreespaceStreamingStatus(Enum):
11
8
  Data = 0 #Normal data update
@@ -435,237 +432,4 @@ class ThreespaceStreamingManager:
435
432
  else:
436
433
  slot_info.append(None)
437
434
 
438
- return slot_info
439
-
440
- class ThreespaceGradientDescentCalibration:
441
-
442
- @dataclass
443
- class StageInfo:
444
- start_vector: int
445
- end_vector: int
446
- stage: int
447
- scale: float
448
-
449
- count: int = 0
450
-
451
- MAX_SCALE = 1000000000
452
- MIN_SCALE = 1
453
- STAGES = [
454
- StageInfo(0, 6, 0, MAX_SCALE),
455
- StageInfo(0, 12, 1, MAX_SCALE),
456
- StageInfo(0, 24, 2, MAX_SCALE)
457
- ]
458
-
459
- #Note that each entry has a positive and negative vector included in this list
460
- CHANGE_VECTORS = [
461
- np.array([0,0,0,0,0,0,0,0,0,.0001,0,0], dtype=np.float64),
462
- np.array([0,0,0,0,0,0,0,0,0,-.0001,0,0], dtype=np.float64),
463
- np.array([0,0,0,0,0,0,0,0,0,0,.0001,0], dtype=np.float64),
464
- np.array([0,0,0,0,0,0,0,0,0,0,-.0001,0], dtype=np.float64),
465
- np.array([0,0,0,0,0,0,0,0,0,0,0,.0001], dtype=np.float64),
466
- np.array([0,0,0,0,0,0,0,0,0,0,0,-.0001], dtype=np.float64), #First 6 only try to change the bias
467
- np.array([.001,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
468
- np.array([-.001,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
469
- np.array([0,0,0,0,.001,0,0,0,0,0,0,0], dtype=np.float64),
470
- np.array([0,0,0,0,-.001,0,0,0,0,0,0,0], dtype=np.float64),
471
- np.array([0,0,0,0,0,0,0,0,.001,0,0,0], dtype=np.float64),
472
- np.array([0,0,0,0,0,0,0,0,-.001,0,0,0], dtype=np.float64), #Next 6 only try to change the scale
473
- np.array([0,.0001,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
474
- np.array([0,-.0001,0,0,0,0,0,0,0,0,0,0], dtype=np.float64),
475
- np.array([0,0,.0001,0,0,0,0,0,0,0,0,0], dtype=np.float64),
476
- np.array([0,0,-.0001,0,0,0,0,0,0,0,0,0], dtype=np.float64),
477
- np.array([0,0,0,.0001,0,0,0,0,0,0,0,0], dtype=np.float64),
478
- np.array([0,0,0,-.0001,0,0,0,0,0,0,0,0], dtype=np.float64),
479
- np.array([0,0,0,0,0,.0001,0,0,0,0,0,0], dtype=np.float64),
480
- np.array([0,0,0,0,0,-.0001,0,0,0,0,0,0], dtype=np.float64),
481
- np.array([0,0,0,0,0,0,.0001,0,0,0,0,0], dtype=np.float64),
482
- np.array([0,0,0,0,0,0,-.0001,0,0,0,0,0], dtype=np.float64),
483
- np.array([0,0,0,0,0,0,0,.0001,0,0,0,0], dtype=np.float64),
484
- np.array([0,0,0,0,0,0,0,-.0001,0,0,0,0], dtype=np.float64), #Next 12 only try to change the shear
485
- ]
486
-
487
- def __init__(self, relative_sensor_orients: list[np.ndarray[float]], no_inverse=False):
488
- """
489
- Params
490
- ------
491
- relative_sensor_orients : The orientation of the sensor during which each sample is taken if it was tared as if pointing into the screen.
492
- The inverse of these will be used to calculate where the axes should be located relative to the sensor
493
- no_inverse : The relative_sensor_orients will be treated as the sample_rotations
494
- """
495
- if no_inverse:
496
- self.rotation_quats = relative_sensor_orients
497
- else:
498
- self.rotation_quats = [np.array(yl_math.quat_inverse(orient)) for orient in relative_sensor_orients]
499
-
500
- def apply_parameters(self, sample: np.ndarray[float], params: np.ndarray[float]):
501
- bias = params[9:]
502
- scale = params[:9]
503
- scale = scale.reshape((3, 3))
504
- return scale @ (sample + bias)
505
-
506
- def rate_parameters(self, params: np.ndarray[float], samples: list[np.ndarray[float]], targets: list[np.ndarray[float]]):
507
- total_error = 0
508
- for i in range(len(samples)):
509
- sample = samples[i]
510
- target = targets[i]
511
-
512
- sample = self.apply_parameters(sample, params)
513
-
514
- error = target - sample
515
- total_error += yl_math.vec_len(error)
516
- return total_error
517
-
518
- def generate_target_list(self, origin: np.ndarray):
519
- targets = []
520
- for orient in self.rotation_quats:
521
- new_vec = np.array(yl_math.quat_rotate_vec(orient, origin), dtype=np.float64)
522
- targets.append(new_vec)
523
- return targets
524
-
525
- def __get_stage(self, stage_number: int):
526
- if stage_number >= len(self.STAGES):
527
- return None
528
- #Always get a shallow copy of the stage so can modify without removing the initial values
529
- return copy.copy(self.STAGES[stage_number])
530
-
531
- def calculate(self, samples: list[np.ndarray[float]], origin: np.ndarray[float], verbose=False, max_cycles_per_stage=1000):
532
- targets = self.generate_target_list(origin)
533
- initial_params = np.array([1,0,0,0,1,0,0,0,1,0,0,0], dtype=np.float64)
534
- stage = self.__get_stage(0)
535
-
536
- best_params = initial_params
537
- best_rating = self.rate_parameters(best_params, samples, targets)
538
- count = 0
539
- while True:
540
- last_best_rating = best_rating
541
- params = best_params
542
-
543
- #Apply all the changes to see if any improve the result
544
- for change_index in range(stage.start_vector, stage.end_vector):
545
- change_vector = self.CHANGE_VECTORS[change_index]
546
- new_params = params + (change_vector * stage.scale)
547
- rating = self.rate_parameters(new_params, samples, targets)
548
-
549
- #A better rating, store it
550
- if rating < best_rating:
551
- best_params = new_params
552
- best_rating = rating
553
-
554
- if verbose and count % 100 == 0:
555
- print(f"Round {count}: {best_rating=} {stage=}")
556
-
557
- #Decide if need to go to the next stage or not
558
- count += 1
559
- stage.count += 1
560
- if stage.count >= max_cycles_per_stage:
561
- stage = self.__get_stage(stage.stage + 1)
562
- if stage is None:
563
- if verbose: print("Done from reaching count limit")
564
- break
565
- if verbose: print("Going to next stage from count limit")
566
-
567
- if best_rating == last_best_rating: #The rating did not improve
568
- if stage.scale == self.MIN_SCALE: #Go to the next stage since can't get any better in this stage!
569
- stage = self.__get_stage(stage.stage + 1)
570
- if stage is None:
571
- if verbose: print("Done from exhaustion")
572
- break
573
- if verbose: print("Going to next stage from exhaustion")
574
- else: #Reduce the size of the changes to hopefully get more accurate tuning
575
- stage.scale *= 0.1
576
- if stage.scale < self.MIN_SCALE:
577
- stage.scale = self.MIN_SCALE
578
- else: #Rating got better! To help avoid falling in a local minimum, increase the size of the change to see if that could make it better
579
- stage.scale *= 1.1
580
-
581
- if verbose:
582
- print(f"Final Rating: {best_rating}")
583
- print(f"Final Params: {best_params}")
584
-
585
- return best_params
586
-
587
- from xml.dom import minidom, Node
588
- class ThreespaceFirmwareUploader:
589
-
590
- def __init__(self, sensor: ThreespaceSensor, file_path: str = None, percentage_callback: Callable[[int],None] = None, verbose: bool = False):
591
- self.sensor = sensor
592
- self.set_firmware_path(file_path)
593
- self.verbose = verbose
594
-
595
- self.percent_complete = 0
596
- self.callback = percentage_callback
597
-
598
- def set_firmware_path(self, file_path: str):
599
- if file_path is None:
600
- self.firmware = None
601
- return
602
- self.firmware = minidom.parse(file_path)
603
-
604
- def set_percent_callback(self, callback: Callable[[float],None]):
605
- self.callback = callback
606
-
607
- def set_verbose(self, verbose: bool):
608
- self.verbose = verbose
609
-
610
- def get_percent_done(self):
611
- return self.percent_complete
612
-
613
- def __set_percent_complete(self, percent: float):
614
- self.percent_complete = percent
615
- if self.callback:
616
- self.callback(percent)
617
-
618
- def log(self, *args):
619
- if not self.verbose: return
620
- print(*args)
621
-
622
- def upload_firmware(self):
623
- self.percent_complete = 0
624
- if not self.sensor.in_bootloader:
625
- self.sensor.enterBootloader()
626
- self.__set_percent_complete(5)
627
-
628
- boot_info = self.sensor.bootloader_get_info()
629
-
630
- root = self.firmware.firstChild
631
- for c in root.childNodes:
632
- if c.nodeType == Node.ELEMENT_NODE:
633
- name = c.nodeName
634
- if name == "SetAddr":
635
- self.log("Write S")
636
- error = self.sensor.bootloader_erase_firmware()
637
- if error:
638
- self.log("Failed to erase firmware:", error)
639
- else:
640
- self.log("Successfully erased firmware")
641
- self.__set_percent_complete(20)
642
- elif name == "MemProgC":
643
- mem = bytes.fromhex(c.firstChild.nodeValue)
644
- self.log("Attempting to program", len(mem), "bytes to the chip")
645
- cpos = 0
646
- while cpos < len(mem):
647
- memchunk = mem[cpos : min(len(mem), cpos + boot_info.pagesize)]
648
- error = self.sensor.bootloader_prog_mem(memchunk)
649
- if error:
650
- self.log("Failed upload:", error)
651
- else:
652
- self.log("Wrote", len(memchunk), "bytes successfully to offset", cpos)
653
- cpos += len(memchunk)
654
- self.__set_percent_complete(20 + cpos / len(mem) * 79)
655
- elif name == "Run":
656
- self.log("Resetting with new firmware.")
657
- self.sensor.bootloader_boot_firmware()
658
- self.__set_percent_complete(100)
659
-
660
- if __name__ == "__main__":
661
- firmware_file = "D:\\svn\\trunk\\3Space\\TSS_3.0_Firmware\\Nuvoton_BASE_Project\\build\\Application.xml"
662
- sensor = ThreespaceSensor()
663
- sensor.set_settings(debug_level=0)
664
- sensor.set_settings(cpu_speed=192000000, power_initial_hold_state=0)
665
- sensor.commitSettings()
666
-
667
- firmware_uploader = ThreespaceFirmwareUploader(firmware_file, verbose=True)
668
- firmware_uploader.upload_firmware(sensor)
669
-
670
- print(sensor.get_settings("version_firmware"))
671
-
435
+ return slot_info
@@ -0,0 +1,76 @@
1
+ from yostlabs.tss3.api import ThreespaceSensor
2
+ from xml.dom import minidom, Node
3
+
4
+ from typing import Callable
5
+
6
+ class ThreespaceFirmwareUploader:
7
+
8
+ def __init__(self, sensor: ThreespaceSensor, file_path: str = None, percentage_callback: Callable[[int],None] = None, verbose: bool = False):
9
+ self.sensor = sensor
10
+ self.set_firmware_path(file_path)
11
+ self.verbose = verbose
12
+
13
+ self.percent_complete = 0
14
+ self.callback = percentage_callback
15
+
16
+ def set_firmware_path(self, file_path: str):
17
+ if file_path is None:
18
+ self.firmware = None
19
+ return
20
+ self.firmware = minidom.parse(file_path)
21
+
22
+ def set_percent_callback(self, callback: Callable[[float],None]):
23
+ self.callback = callback
24
+
25
+ def set_verbose(self, verbose: bool):
26
+ self.verbose = verbose
27
+
28
+ def get_percent_done(self):
29
+ return self.percent_complete
30
+
31
+ def __set_percent_complete(self, percent: float):
32
+ self.percent_complete = percent
33
+ if self.callback:
34
+ self.callback(percent)
35
+
36
+ def log(self, *args):
37
+ if not self.verbose: return
38
+ print(*args)
39
+
40
+ def upload_firmware(self):
41
+ self.percent_complete = 0
42
+ if not self.sensor.in_bootloader:
43
+ self.sensor.enterBootloader()
44
+ self.__set_percent_complete(5)
45
+
46
+ boot_info = self.sensor.bootloader_get_info()
47
+
48
+ root = self.firmware.firstChild
49
+ for c in root.childNodes:
50
+ if c.nodeType == Node.ELEMENT_NODE:
51
+ name = c.nodeName
52
+ if name == "SetAddr":
53
+ self.log("Write S")
54
+ error = self.sensor.bootloader_erase_firmware()
55
+ if error:
56
+ self.log("Failed to erase firmware:", error)
57
+ else:
58
+ self.log("Successfully erased firmware")
59
+ self.__set_percent_complete(20)
60
+ elif name == "MemProgC":
61
+ mem = bytes.fromhex(c.firstChild.nodeValue)
62
+ self.log("Attempting to program", len(mem), "bytes to the chip")
63
+ cpos = 0
64
+ while cpos < len(mem):
65
+ memchunk = mem[cpos : min(len(mem), cpos + boot_info.pagesize)]
66
+ error = self.sensor.bootloader_prog_mem(memchunk)
67
+ if error:
68
+ self.log("Failed upload:", error)
69
+ else:
70
+ self.log("Wrote", len(memchunk), "bytes successfully to offset", cpos)
71
+ cpos += len(memchunk)
72
+ self.__set_percent_complete(20 + cpos / len(mem) * 79)
73
+ elif name == "Run":
74
+ self.log("Resetting with new firmware.")
75
+ self.sensor.bootloader_boot_firmware()
76
+ self.__set_percent_complete(100)
File without changes
File without changes