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.
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_commands.py +2 -1
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_component_specific_settings_and_commands.py +2 -1
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_firmware_upload.py +3 -2
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_parsing_stored_binary.py +3 -2
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_read_settings.py +2 -1
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_streaming.py +2 -1
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_streaming_manager.py +3 -2
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/example_write_settings.py +2 -1
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/PKG-INFO +1 -1
- yostlabs-2025.1.9/install.bat +1 -0
- yostlabs-2025.1.9/install_test.bat +1 -0
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/pyproject.toml +4 -2
- yostlabs-2025.1.9/src/yostlabs/communication/base.py +90 -0
- yostlabs-2025.1.9/src/yostlabs/communication/serial.py +141 -0
- yostlabs-2025.1.9/src/yostlabs/math/__init__.py +1 -0
- {yostlabs-2025.1.3.4/src/yostlabs/tss3 → yostlabs-2025.1.9/src/yostlabs/math}/quaternion.py +25 -42
- yostlabs-2025.1.9/src/yostlabs/math/vector.py +31 -0
- yostlabs-2025.1.9/src/yostlabs/tss3/__init__.py +0 -0
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/src/yostlabs/tss3/api.py +14 -235
- yostlabs-2025.1.9/src/yostlabs/tss3/utils/__init__.py +0 -0
- yostlabs-2025.1.9/src/yostlabs/tss3/utils/calibration.py +153 -0
- {yostlabs-2025.1.3.4/src/yostlabs/tss3 → yostlabs-2025.1.9/src/yostlabs/tss3/utils}/parser.py +0 -2
- yostlabs-2025.1.3.4/src/yostlabs/tss3/utils.py → yostlabs-2025.1.9/src/yostlabs/tss3/utils/streaming.py +5 -241
- yostlabs-2025.1.9/src/yostlabs/tss3/utils/version.py +76 -0
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/Examples/embedded_2024_dec_20.xml +0 -0
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/LICENSE +0 -0
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/README.md +0 -0
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/src/yostlabs/__init__.py +0 -0
- {yostlabs-2025.1.3.4/src/yostlabs/tss3 → yostlabs-2025.1.9/src/yostlabs/communication}/__init__.py +0 -0
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/src/yostlabs/tss3/consts.py +0 -0
- {yostlabs-2025.1.3.4 → yostlabs-2025.1.9}/src/yostlabs/tss3/eepts.py +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
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
|
|
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
|
|
2
|
-
from
|
|
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
|
|
2
|
-
from
|
|
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
|
|
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
|
|
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
|
|
2
|
-
from
|
|
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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yostlabs
|
|
3
|
-
Version: 2025.1.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
{yostlabs-2025.1.3.4/src/yostlabs/tss3 → yostlabs-2025.1.9/src/yostlabs/tss3/utils}/parser.py
RENAMED
|
@@ -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,
|
|
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
|
-
|
|
8
|
-
import
|
|
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
|
|
File without changes
|
|
File without changes
|
{yostlabs-2025.1.3.4/src/yostlabs/tss3 → yostlabs-2025.1.9/src/yostlabs/communication}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|