yostlabs 2025.2.17__tar.gz → 2025.3.6__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.3.6/Examples/example_ble.py +23 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/PKG-INFO +22 -1
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/README.md +20 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/pyproject.toml +3 -2
- yostlabs-2025.3.6/src/yostlabs/communication/ble.py +318 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/math/quaternion.py +31 -0
- yostlabs-2025.3.6/src/yostlabs/math/vector.py +81 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/api.py +114 -40
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/consts.py +1 -0
- yostlabs-2025.2.17/src/yostlabs/math/vector.py +0 -31
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/.gitignore +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/embedded_2024_dec_20.xml +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/example_commands.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/example_component_specific_settings_and_commands.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/example_firmware_upload.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/example_parsing_stored_binary.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/example_read_settings.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/example_streaming.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/example_streaming_manager.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/Examples/example_write_settings.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/LICENSE +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/__init__.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/communication/__init__.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/communication/base.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/communication/serial.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/math/__init__.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/__init__.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/eepts.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/utils/__init__.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/utils/calibration.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/utils/parser.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/utils/streaming.py +0 -0
- {yostlabs-2025.2.17 → yostlabs-2025.3.6}/src/yostlabs/tss3/utils/version.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from yostlabs.tss3.api import ThreespaceSensor
|
|
2
|
+
from yostlabs.communication.ble import ThreespaceBLEComClass
|
|
3
|
+
|
|
4
|
+
auto_detect = False
|
|
5
|
+
|
|
6
|
+
if auto_detect:
|
|
7
|
+
#Create a sensor by auto detecting a ThreespaceBLEComClass.
|
|
8
|
+
#It does this by attempting to connect to a device with the Nordic Uart Service
|
|
9
|
+
sensor = ThreespaceSensor(ThreespaceBLEComClass)
|
|
10
|
+
else:
|
|
11
|
+
#PUT YOUR SENSORS BLE_NAME HERE
|
|
12
|
+
ble_name = "YL-TSS-####" #Defaults to the lowest 4 hex digits of the sensors serial number
|
|
13
|
+
print("Attempting to discover and connect to a sensor with the name:", ble_name)
|
|
14
|
+
com_class = ThreespaceBLEComClass(ble_name)
|
|
15
|
+
sensor = ThreespaceSensor(com_class)
|
|
16
|
+
|
|
17
|
+
ble_name = sensor.get_settings("ble_name")
|
|
18
|
+
print("Connected to:", ble_name)
|
|
19
|
+
|
|
20
|
+
result = sensor.getPrimaryCorrectedAccelVec()
|
|
21
|
+
print(result)
|
|
22
|
+
|
|
23
|
+
sensor.cleanup()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yostlabs
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.3.6
|
|
4
4
|
Summary: Python resources and API for 3Space sensors from Yost Labs Inc.
|
|
5
5
|
Project-URL: Homepage, https://yostlabs.com/
|
|
6
6
|
Project-URL: Repository, https://github.com/YostLabs/3SpacePythonPackage/tree/main
|
|
@@ -13,6 +13,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: bleak
|
|
16
17
|
Requires-Dist: numpy
|
|
17
18
|
Requires-Dist: pyserial
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
@@ -25,6 +26,8 @@ Description-Content-Type: text/markdown
|
|
|
25
26
|
|
|
26
27
|
## Basic Usage
|
|
27
28
|
|
|
29
|
+
#### USB
|
|
30
|
+
|
|
28
31
|
```Python
|
|
29
32
|
from yostlabs.tss3.api import ThreespaceSensor
|
|
30
33
|
|
|
@@ -37,6 +40,24 @@ print(result)
|
|
|
37
40
|
sensor.cleanup()
|
|
38
41
|
```
|
|
39
42
|
|
|
43
|
+
#### BLE
|
|
44
|
+
|
|
45
|
+
```Python
|
|
46
|
+
from yostlabs.tss3.api import ThreespaceSensor
|
|
47
|
+
from yostlabs.communication.ble import ThreespaceBLEComClass
|
|
48
|
+
|
|
49
|
+
#PUT YOUR SENSORS BLE_NAME HERE
|
|
50
|
+
ble_name = "YL-TSS-####" #Defaults to the lowest 4 hex digits of the sensors serial number
|
|
51
|
+
com_class = ThreespaceBLEComClass(ble_name)
|
|
52
|
+
sensor = ThreespaceSensor(com_class)
|
|
53
|
+
#sensor = ThreespaceSensor(ThreespaceBLEComClass) #Attempt to auto discover nearby sensor
|
|
54
|
+
|
|
55
|
+
result = sensor.getPrimaryCorrectedAccelVec()
|
|
56
|
+
print(result)
|
|
57
|
+
|
|
58
|
+
sensor.cleanup()
|
|
59
|
+
```
|
|
60
|
+
|
|
40
61
|
Click [here](https://github.com/YostLabs/3SpacePythonPackage/tree/main/Examples) for more examples.
|
|
41
62
|
|
|
42
63
|
## Communication
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
## Basic Usage
|
|
8
8
|
|
|
9
|
+
#### USB
|
|
10
|
+
|
|
9
11
|
```Python
|
|
10
12
|
from yostlabs.tss3.api import ThreespaceSensor
|
|
11
13
|
|
|
@@ -18,6 +20,24 @@ print(result)
|
|
|
18
20
|
sensor.cleanup()
|
|
19
21
|
```
|
|
20
22
|
|
|
23
|
+
#### BLE
|
|
24
|
+
|
|
25
|
+
```Python
|
|
26
|
+
from yostlabs.tss3.api import ThreespaceSensor
|
|
27
|
+
from yostlabs.communication.ble import ThreespaceBLEComClass
|
|
28
|
+
|
|
29
|
+
#PUT YOUR SENSORS BLE_NAME HERE
|
|
30
|
+
ble_name = "YL-TSS-####" #Defaults to the lowest 4 hex digits of the sensors serial number
|
|
31
|
+
com_class = ThreespaceBLEComClass(ble_name)
|
|
32
|
+
sensor = ThreespaceSensor(com_class)
|
|
33
|
+
#sensor = ThreespaceSensor(ThreespaceBLEComClass) #Attempt to auto discover nearby sensor
|
|
34
|
+
|
|
35
|
+
result = sensor.getPrimaryCorrectedAccelVec()
|
|
36
|
+
print(result)
|
|
37
|
+
|
|
38
|
+
sensor.cleanup()
|
|
39
|
+
```
|
|
40
|
+
|
|
21
41
|
Click [here](https://github.com/YostLabs/3SpacePythonPackage/tree/main/Examples) for more examples.
|
|
22
42
|
|
|
23
43
|
## Communication
|
|
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "yostlabs"
|
|
7
7
|
#If uploading again on the same day, add a fourth number
|
|
8
|
-
version = "2025.
|
|
8
|
+
version = "2025.03.6"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name="Yost Labs Inc.", email="techsupport@yostlabs.com" },
|
|
11
11
|
{ name="Andy Riedlinger", email="techsupport@yostlabs.com" },
|
|
@@ -22,7 +22,8 @@ classifiers = [
|
|
|
22
22
|
keywords = ["3space", "threespace", "yost"]
|
|
23
23
|
dependencies = [
|
|
24
24
|
"pyserial",
|
|
25
|
-
"numpy"
|
|
25
|
+
"numpy",
|
|
26
|
+
"bleak"
|
|
26
27
|
]
|
|
27
28
|
|
|
28
29
|
[project.urls]
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import async_timeout
|
|
3
|
+
import time
|
|
4
|
+
from bleak import BleakScanner, BleakClient
|
|
5
|
+
from bleak.backends.device import BLEDevice
|
|
6
|
+
from bleak.backends.scanner import AdvertisementData
|
|
7
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
|
8
|
+
from bleak.exc import BleakDeviceNotFoundError
|
|
9
|
+
|
|
10
|
+
#Services
|
|
11
|
+
NORDIC_UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
|
|
12
|
+
|
|
13
|
+
#Characteristics
|
|
14
|
+
NORDIC_UART_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
|
|
15
|
+
NORDIC_UART_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
|
|
16
|
+
|
|
17
|
+
DEVICE_NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb"
|
|
18
|
+
APPEARANCE_UUID = "00002a01-0000-1000-8000-00805f9b34fb"
|
|
19
|
+
|
|
20
|
+
FIRMWARE_REVISION_STRING_UUID = "00002a26-0000-1000-8000-00805f9b34fb"
|
|
21
|
+
HARDWARE_REVISION_STRING_UUID = "00002a27-0000-1000-8000-00805f9b34fb"
|
|
22
|
+
SERIAL_NUMBER_STRING_UUID = "00002a25-0000-1000-8000-00805f9b34fb"
|
|
23
|
+
MANUFACTURER_NAME_STRING_UUID = "00002a29-0000-1000-8000-00805f9b34fb"
|
|
24
|
+
|
|
25
|
+
class TssBLENoConnectionError(Exception): ...
|
|
26
|
+
|
|
27
|
+
from yostlabs.communication.base import *
|
|
28
|
+
class ThreespaceBLEComClass(ThreespaceComClass):
|
|
29
|
+
|
|
30
|
+
DEFAULT_TIMEOUT = 2
|
|
31
|
+
|
|
32
|
+
def __init__(self, ble: BleakClient | BLEDevice | str, discover_name: bool = True, discovery_timeout=5, error_on_disconnect=True):
|
|
33
|
+
"""
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
ble : Can be either a BleakClient, BleakDevice, MacAddress String, or localName string
|
|
37
|
+
discover_name : If true, a string ble parameter is interpreted as a localName, else as a MacAddress
|
|
38
|
+
discovery_timeout : Max amount of time in seconds to discover the BLE device for the corresponding MacAddress/localName
|
|
39
|
+
error_on_disconnect : If trying to read while the sensor is disconnected, an exception will be generated. This may be undesirable \
|
|
40
|
+
if it is expected that the sensor will frequently go in and out of range and the user wishes to preserve data (such as streaming)
|
|
41
|
+
"""
|
|
42
|
+
self.event_loop = asyncio.new_event_loop()
|
|
43
|
+
bleak_options = { "timeout": discovery_timeout, "disconnected_callback": self.__on_disconnect }
|
|
44
|
+
if isinstance(ble, BleakClient): #Actual client
|
|
45
|
+
self.client = ble
|
|
46
|
+
self.__name = ble.address
|
|
47
|
+
elif isinstance(ble, str):
|
|
48
|
+
if discover_name: #Local Name stirng
|
|
49
|
+
device = self.event_loop.run_until_complete(BleakScanner.find_device_by_name(ble, timeout=discovery_timeout))
|
|
50
|
+
if device is None:
|
|
51
|
+
raise BleakDeviceNotFoundError(ble)
|
|
52
|
+
self.client = BleakClient(device, **bleak_options)
|
|
53
|
+
self.__name = ble
|
|
54
|
+
else: #Address string
|
|
55
|
+
self.client = BleakClient(ble, **bleak_options)
|
|
56
|
+
self.__name = self.client.address
|
|
57
|
+
elif isinstance(ble, BLEDevice):
|
|
58
|
+
self.client = BleakClient(ble, **bleak_options)
|
|
59
|
+
self.__name = ble.name #Use the local name instead of the address
|
|
60
|
+
else:
|
|
61
|
+
raise TypeError("Invalid type for creating a ThreespaceBLEComClass:", type(ble), ble)
|
|
62
|
+
|
|
63
|
+
self.__timeout = self.DEFAULT_TIMEOUT
|
|
64
|
+
|
|
65
|
+
self.buffer = bytearray()
|
|
66
|
+
self.data_read_event = asyncio.Event()
|
|
67
|
+
|
|
68
|
+
#Default to 20, will update on open
|
|
69
|
+
self.max_packet_size = 20
|
|
70
|
+
|
|
71
|
+
self.error_on_disconnect = error_on_disconnect
|
|
72
|
+
#is_connected is different from open.
|
|
73
|
+
#check_open() should return is_connected as that is what the user likely wants.
|
|
74
|
+
#open is whether or not the client will auto connect to the device when rediscovered.
|
|
75
|
+
#This file is set up to automatically close the connection if a method is called and is_connected is False
|
|
76
|
+
#This behavior might be specific to Windows.
|
|
77
|
+
self.__opened = False
|
|
78
|
+
#client.is_connected is really slow (noticeable when called in bulk, which happens do to the assert_connected)...
|
|
79
|
+
#So instead using the disconnected callback and this variable to manage tracking the state without the delay
|
|
80
|
+
self.__connected = False
|
|
81
|
+
#Writing functions will naturally throw an exception if disconnected. Reading ones don't because they use notifications rather
|
|
82
|
+
#then direct reads. This means reading functions will need to assert the connection status but writing does not.
|
|
83
|
+
|
|
84
|
+
async def __async_open(self):
|
|
85
|
+
await self.client.connect()
|
|
86
|
+
await self.client.start_notify(NORDIC_UART_TX_UUID, self.__on_data_received)
|
|
87
|
+
|
|
88
|
+
def open(self):
|
|
89
|
+
#If trying to open while already open, this infinitely loops
|
|
90
|
+
if self.__opened:
|
|
91
|
+
if not self.__connected and self.error_on_disconnect:
|
|
92
|
+
self.close()
|
|
93
|
+
return
|
|
94
|
+
self.event_loop.run_until_complete(self.__async_open())
|
|
95
|
+
self.max_packet_size = self.client.mtu_size - 3 #-3 to account for the opcode and attribute handle stored in the data packet
|
|
96
|
+
self.__opened = True
|
|
97
|
+
self.__connected = True
|
|
98
|
+
|
|
99
|
+
async def __async_close(self):
|
|
100
|
+
#There appears to be a bug where if you call close too soon after is_connected returns false,
|
|
101
|
+
#the disconnect call will hang on Windows. It seems similar to this issue: https://github.com/hbldh/bleak/issues/1359
|
|
102
|
+
await asyncio.sleep(0.5)
|
|
103
|
+
await self.client.disconnect()
|
|
104
|
+
|
|
105
|
+
def close(self):
|
|
106
|
+
if not self.__opened: return
|
|
107
|
+
self.event_loop.run_until_complete(self.__async_close())
|
|
108
|
+
self.buffer.clear()
|
|
109
|
+
self.__opened = False
|
|
110
|
+
|
|
111
|
+
def __on_disconnect(self, client: BleakClient):
|
|
112
|
+
self.__connected = False
|
|
113
|
+
|
|
114
|
+
#Goal is that this is always called after something that would have already performed an async callback
|
|
115
|
+
#to prevent needing to run the event loop. Running the event loop frequently is slow. Which is also why this
|
|
116
|
+
#comclass will eventually have a threaded asyncio version.
|
|
117
|
+
def __assert_connected(self):
|
|
118
|
+
if not self.__connected and self.error_on_disconnect:
|
|
119
|
+
raise TssBLENoConnectionError(f"{self.name} is not connected")
|
|
120
|
+
|
|
121
|
+
def check_open(self):
|
|
122
|
+
#Checking this, while slow, isn't much difference in speed as allowing the disconnect callback to update via
|
|
123
|
+
#running the empty async function. So just going to use this here. Repeated calls to check_open are not a good
|
|
124
|
+
#idea from a speed perspective until a fix is found. We will probably make a version of this BLEComClass that uses
|
|
125
|
+
#a background thread for asyncio to allow for speed increases.
|
|
126
|
+
self.__connected = self.client.is_connected
|
|
127
|
+
if not self.__connected and self.__opened and self.error_on_disconnect:
|
|
128
|
+
self.close()
|
|
129
|
+
return self.__connected
|
|
130
|
+
|
|
131
|
+
#Bleak does run a thread to read data on notification after calling start_notify, however on notification
|
|
132
|
+
#it schedules a callback using loop.call_soon_threadsafe() so the actual notification can't happen unless we
|
|
133
|
+
#run the event loop. Therefore, this async function that does nothing is used just to trigger an event loop updated
|
|
134
|
+
#so the read callbacks __on_data_received can occur
|
|
135
|
+
@staticmethod
|
|
136
|
+
async def __wait_for_callbacks_async():
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
def __read_all_data(self):
|
|
140
|
+
self.event_loop.run_until_complete(self.__wait_for_callbacks_async())
|
|
141
|
+
self.__assert_connected()
|
|
142
|
+
|
|
143
|
+
def __on_data_received(self, sender: BleakGATTCharacteristic, data: bytearray):
|
|
144
|
+
self.buffer += data
|
|
145
|
+
self.data_read_event.set()
|
|
146
|
+
|
|
147
|
+
def write(self, bytes: bytes):
|
|
148
|
+
start_index = 0
|
|
149
|
+
while start_index < len(bytes):
|
|
150
|
+
end_index = min(len(bytes), start_index + self.max_packet_size) #Can only send max_packet_size data per call to write_gatt_char
|
|
151
|
+
self.event_loop.run_until_complete(self.client.write_gatt_char(NORDIC_UART_RX_UUID, bytes[start_index:end_index], response=False))
|
|
152
|
+
start_index = end_index
|
|
153
|
+
|
|
154
|
+
async def __await_read(self, timeout_time: int):
|
|
155
|
+
self.__assert_connected()
|
|
156
|
+
self.data_read_event.clear()
|
|
157
|
+
try:
|
|
158
|
+
async with async_timeout.timeout_at(timeout_time):
|
|
159
|
+
await self.data_read_event.wait()
|
|
160
|
+
return True
|
|
161
|
+
except:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
async def __await_num_bytes(self, num_bytes: int):
|
|
165
|
+
start_time = self.event_loop.time()
|
|
166
|
+
while len(self.buffer) < num_bytes and self.event_loop.time() - start_time < self.timeout:
|
|
167
|
+
await self.__await_read(start_time + self.timeout)
|
|
168
|
+
|
|
169
|
+
def read(self, num_bytes: int):
|
|
170
|
+
self.event_loop.run_until_complete(self.__await_num_bytes(num_bytes))
|
|
171
|
+
num_bytes = min(num_bytes, len(self.buffer))
|
|
172
|
+
data = self.buffer[:num_bytes]
|
|
173
|
+
del self.buffer[:num_bytes]
|
|
174
|
+
return data
|
|
175
|
+
|
|
176
|
+
def peek(self, num_bytes: int):
|
|
177
|
+
self.event_loop.run_until_complete(self.__await_num_bytes(num_bytes))
|
|
178
|
+
num_bytes = min(num_bytes, len(self.buffer))
|
|
179
|
+
data = self.buffer[:num_bytes]
|
|
180
|
+
return data
|
|
181
|
+
|
|
182
|
+
#Reads until the pattern is received, max_length is exceeded, or timeout occurs
|
|
183
|
+
async def __await_pattern(self, pattern: bytes, max_length: int = None):
|
|
184
|
+
if max_length is None: max_length = float('inf')
|
|
185
|
+
start_time = self.event_loop.time()
|
|
186
|
+
while pattern not in self.buffer and self.event_loop.time() - start_time < self.timeout and len(self.buffer) < max_length:
|
|
187
|
+
await self.__await_read(start_time + self.timeout)
|
|
188
|
+
return pattern in self.buffer
|
|
189
|
+
|
|
190
|
+
def read_until(self, expected: bytes) -> bytes:
|
|
191
|
+
self.event_loop.run_until_complete(self.__await_pattern(expected))
|
|
192
|
+
if expected in self.buffer: #Found the pattern
|
|
193
|
+
length = self.buffer.index(expected) + len(expected)
|
|
194
|
+
result = self.buffer[:length]
|
|
195
|
+
del self.buffer[:length]
|
|
196
|
+
return result
|
|
197
|
+
#Failed to find the pattern, just return whatever is there
|
|
198
|
+
result = self.buffer.copy()
|
|
199
|
+
self.buffer.clear()
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def peek_until(self, expected: bytes, max_length: int = None) -> bytes:
|
|
203
|
+
self.event_loop.run_until_complete(self.__await_pattern(expected, max_length=max_length))
|
|
204
|
+
if expected in self.buffer:
|
|
205
|
+
length = self.buffer.index(expected) + len(expected)
|
|
206
|
+
else:
|
|
207
|
+
length = len(self.buffer)
|
|
208
|
+
|
|
209
|
+
if max_length is not None and length > max_length:
|
|
210
|
+
length = max_length
|
|
211
|
+
|
|
212
|
+
return self.buffer[:length]
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def length(self):
|
|
216
|
+
self.__read_all_data() #Gotta update the data before knowing the length
|
|
217
|
+
return len(self.buffer)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def timeout(self) -> float:
|
|
221
|
+
return self.__timeout
|
|
222
|
+
|
|
223
|
+
@timeout.setter
|
|
224
|
+
def timeout(self, timeout: float):
|
|
225
|
+
self.__timeout = timeout
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def reenumerates(self) -> bool:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def name(self) -> str:
|
|
233
|
+
return self.__name
|
|
234
|
+
|
|
235
|
+
SCANNER = None
|
|
236
|
+
SCANNER_EVENT_LOOP = None
|
|
237
|
+
|
|
238
|
+
SCANNER_CONTINOUS = False #Controls if scanning will continously run
|
|
239
|
+
SCANNER_TIMEOUT = 5 #Controls the scanners timeout
|
|
240
|
+
SCANNER_FIND_COUNT = 1 #When continous=False, will stop scanning after at least this many devices are found. Set to None to search the entire timeout.
|
|
241
|
+
SCANNER_EXPIRATION_TIME = 5 #Controls the timeout for detected BLE sensors. If a sensor hasn't been detected again in this amount of time, its removed from discovered devices
|
|
242
|
+
|
|
243
|
+
#Format: Address - dict = { device: ..., adv: ..., last_found: ... }
|
|
244
|
+
discovered_devices: dict[str,dict] = {}
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def __lazy_init_scanner(cls):
|
|
248
|
+
if cls.SCANNER is None:
|
|
249
|
+
cls.SCANNER = BleakScanner(detection_callback=cls.__detection_callback, service_uuids=[NORDIC_UART_SERVICE_UUID])
|
|
250
|
+
cls.SCANNER_EVENT_LOOP = asyncio.new_event_loop()
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def __detection_callback(cls, device: BLEDevice, adv: AdvertisementData):
|
|
254
|
+
cls.discovered_devices[device.address] = {"device": device, "adv": adv, "last_found": time.time()}
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def set_scanner_continous(cls, continous: bool):
|
|
258
|
+
"""
|
|
259
|
+
If not using continous mode, functions like update_nearby_devices and auto_detect are blocking with the following rules:
|
|
260
|
+
- Will search for at most SCANNER_TIMEOUT time
|
|
261
|
+
- Will stop searching immediately once SCANNER_FIND_COUNT is reached
|
|
262
|
+
|
|
263
|
+
If using continous mode, no scanning functions are blocking. However, the user must continously call
|
|
264
|
+
update_nearby_devices to ensure up to date information.
|
|
265
|
+
"""
|
|
266
|
+
cls.__lazy_init_scanner()
|
|
267
|
+
cls.SCANNER_CONTINOUS = continous
|
|
268
|
+
if continous: cls.SCANNER_EVENT_LOOP.run_until_complete(cls.SCANNER.start())
|
|
269
|
+
else: cls.SCANNER_EVENT_LOOP.run_until_complete(cls.SCANNER.stop())
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def update_nearby_devices(cls):
|
|
273
|
+
"""
|
|
274
|
+
Updates ThreespaceBLEComClass.discovered_devices using the current configuration.
|
|
275
|
+
"""
|
|
276
|
+
cls.__lazy_init_scanner()
|
|
277
|
+
if cls.SCANNER_CONTINOUS:
|
|
278
|
+
#Allow the callbacks for nearby devices to trigger
|
|
279
|
+
cls.SCANNER_EVENT_LOOP.run_until_complete(cls.__wait_for_callbacks_async())
|
|
280
|
+
#Remove expired devices
|
|
281
|
+
cur_time = time.time()
|
|
282
|
+
to_remove = [] #Avoiding concurrent list modification
|
|
283
|
+
for device in cls.discovered_devices:
|
|
284
|
+
if cur_time - cls.discovered_devices[device]["last_found"] > cls.SCANNER_EXPIRATION_TIME:
|
|
285
|
+
to_remove.append(device)
|
|
286
|
+
for device in to_remove:
|
|
287
|
+
del cls.discovered_devices[device]
|
|
288
|
+
|
|
289
|
+
else:
|
|
290
|
+
#Mark all devices as invalid before searching for nearby devices
|
|
291
|
+
cls.discovered_devices.clear()
|
|
292
|
+
start_time = time.time()
|
|
293
|
+
end_time = cls.SCANNER_TIMEOUT or float('inf')
|
|
294
|
+
end_count = cls.SCANNER_FIND_COUNT or float('inf')
|
|
295
|
+
cls.SCANNER_EVENT_LOOP.run_until_complete(cls.SCANNER.start())
|
|
296
|
+
while time.time() - start_time < end_time and len(cls.discovered_devices) < end_count:
|
|
297
|
+
cls.SCANNER_EVENT_LOOP.run_until_complete(cls.__wait_for_callbacks_async())
|
|
298
|
+
cls.SCANNER_EVENT_LOOP.run_until_complete(cls.SCANNER.stop())
|
|
299
|
+
|
|
300
|
+
return cls.discovered_devices
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def get_discovered_nearby_devices(cls):
|
|
304
|
+
"""
|
|
305
|
+
A helper to get a copy of the discovered devices
|
|
306
|
+
"""
|
|
307
|
+
return cls.discovered_devices.copy()
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def auto_detect() -> Generator["ThreespaceBLEComClass", None, None]:
|
|
311
|
+
"""
|
|
312
|
+
Returns a list of com classes of the same type called on nearby.
|
|
313
|
+
These ports will start unopened. This allows the caller to get a list of ports without having to connect.
|
|
314
|
+
"""
|
|
315
|
+
cls = ThreespaceBLEComClass
|
|
316
|
+
cls.update_nearby_devices()
|
|
317
|
+
for device_info in cls.discovered_devices.values():
|
|
318
|
+
yield(ThreespaceBLEComClass(device_info["device"]))
|
|
@@ -134,6 +134,37 @@ def quaternion_global_to_local(quat, vec):
|
|
|
134
134
|
def quaternion_local_to_global(quat, vec):
|
|
135
135
|
return quat_rotate_vec(quat, vec)
|
|
136
136
|
|
|
137
|
+
def quaternion_swap_axes(quat: list, old_order: str, new_order: str):
|
|
138
|
+
return quaternion_swap_axes_fast(quat, _vec.parse_axis_string_info(old_order), _vec.parse_axis_string_info(new_order))
|
|
139
|
+
|
|
140
|
+
def quaternion_swap_axes_fast(quat: list, old_parsed_order: list[list, list, bool], new_parsed_order: list[list, list, bool]):
|
|
141
|
+
"""
|
|
142
|
+
Like quaternion_swap_axes but uses the inputs of parsing the axis strings to avoid having to recompute
|
|
143
|
+
the storage types.
|
|
144
|
+
|
|
145
|
+
each order should be a sequence of [order, mults, right_handed]
|
|
146
|
+
"""
|
|
147
|
+
old_order, old_mults, old_right_handed = old_parsed_order
|
|
148
|
+
new_order, new_mults, new_right_handed = new_parsed_order
|
|
149
|
+
|
|
150
|
+
#Undo the old negations
|
|
151
|
+
base_quat = quat.copy()
|
|
152
|
+
for i, mult in enumerate(old_mults):
|
|
153
|
+
base_quat[i] *= mult
|
|
154
|
+
|
|
155
|
+
#Now swap the positions and apply new multipliers
|
|
156
|
+
new_quat = base_quat.copy()
|
|
157
|
+
for i in range(3):
|
|
158
|
+
new_quat[i] = base_quat[old_order.index(new_order[i])]
|
|
159
|
+
new_quat[i] *= new_mults[i]
|
|
160
|
+
|
|
161
|
+
if old_right_handed != new_right_handed:
|
|
162
|
+
#Different handed systems rotate opposite directions. So to maintain the same rotation,
|
|
163
|
+
#negate the rotation of the quaternion when swapping systems
|
|
164
|
+
new_quat[-1] *= -1
|
|
165
|
+
|
|
166
|
+
return new_quat
|
|
167
|
+
|
|
137
168
|
#https://splines.readthedocs.io/en/latest/rotation/slerp.html
|
|
138
169
|
def slerp(a, b, t):
|
|
139
170
|
dot = _vec.vec_dot(a, b)
|
|
@@ -0,0 +1,81 @@
|
|
|
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] = None):
|
|
21
|
+
order = order.lower()
|
|
22
|
+
if negations is None: #Must build the negation list
|
|
23
|
+
base_order = order.replace('-', '')
|
|
24
|
+
num_negations = order.count('-')
|
|
25
|
+
else:
|
|
26
|
+
base_order = order
|
|
27
|
+
num_negations = sum(negations)
|
|
28
|
+
|
|
29
|
+
right_handed = base_order in ("xzy", "yxz", "zyx")
|
|
30
|
+
if num_negations & 1: #Odd number of negations causes handedness to swap
|
|
31
|
+
right_handed = not right_handed
|
|
32
|
+
|
|
33
|
+
return right_handed
|
|
34
|
+
|
|
35
|
+
def axis_to_unit_vector(axis: str):
|
|
36
|
+
axis = axis.lower()
|
|
37
|
+
if axis == 'x' or axis == 0: return [1, 0, 0]
|
|
38
|
+
if axis == 'y' or axis == 1: return [0, 1, 0]
|
|
39
|
+
if axis == 'z' or axis == 2: return [0, 0, 1]
|
|
40
|
+
|
|
41
|
+
def parse_axis_string(axis: str):
|
|
42
|
+
"""
|
|
43
|
+
Given an axis order string, convert it to an array representing the axis order and negations/multipliers
|
|
44
|
+
"""
|
|
45
|
+
axis = axis.lower()
|
|
46
|
+
order = [0, 1, 2]
|
|
47
|
+
multipliers = [1, 1, 1]
|
|
48
|
+
if 'x' in axis: #Using XYZ notation
|
|
49
|
+
index = 0
|
|
50
|
+
for c in axis:
|
|
51
|
+
if c == '-':
|
|
52
|
+
multipliers[index] = -1
|
|
53
|
+
continue
|
|
54
|
+
order[index] = ord(c) - ord('x')
|
|
55
|
+
index += 1
|
|
56
|
+
else: #Using N/S E/W U/D notation
|
|
57
|
+
axis_lookup = {'e': 0, 'w': 0, 'u': 1, 'd': 1, 'n': 2, 's': 2}
|
|
58
|
+
negative_axes = "wds"
|
|
59
|
+
for i, c in enumerate(axis):
|
|
60
|
+
order[i] = axis_lookup[c]
|
|
61
|
+
if c in negative_axes:
|
|
62
|
+
multipliers[i] = -1
|
|
63
|
+
|
|
64
|
+
return order, multipliers
|
|
65
|
+
|
|
66
|
+
def parse_axis_string_info(axis: str):
|
|
67
|
+
order, mult = parse_axis_string(axis)
|
|
68
|
+
right_handed = axis_is_righthanded(order, mult)
|
|
69
|
+
return [order, mult, right_handed]
|
|
70
|
+
|
|
71
|
+
def axis_is_righthanded(order: list[int], negations: list[int]):
|
|
72
|
+
num_swaps = 0
|
|
73
|
+
for i in range(3):
|
|
74
|
+
if i != order[i]:
|
|
75
|
+
num_swaps += 1
|
|
76
|
+
|
|
77
|
+
right_handed = num_swaps == 2 #Defaults to left handed, but if a singular swap occured, the handedness swaps
|
|
78
|
+
if negations.count(-1) & 1: #Odd number of negations causes handedness to swap
|
|
79
|
+
right_handed = not right_handed
|
|
80
|
+
|
|
81
|
+
return right_handed
|
|
@@ -419,6 +419,10 @@ THREESPACE_AWAIT_COMMAND_FOUND = 0
|
|
|
419
419
|
THREESPACE_AWAIT_COMMAND_TIMEOUT = 1
|
|
420
420
|
THREESPACE_AWAIT_BOOTLOADER = 2
|
|
421
421
|
|
|
422
|
+
THREESPACE_UPDATE_COMMAND_PARSED = 0
|
|
423
|
+
THREESPACE_UPDATE_COMMAND_NOT_ENOUGH_DATA = 1
|
|
424
|
+
THREESPACE_UPDATE_COMMAND_MISALIGNED = 2
|
|
425
|
+
|
|
422
426
|
T = TypeVar('T')
|
|
423
427
|
|
|
424
428
|
@dataclass
|
|
@@ -464,10 +468,11 @@ class ThreespaceBootloaderInfo:
|
|
|
464
468
|
THREESPACE_REQUIRED_HEADER = THREESPACE_HEADER_ECHO_BIT | THREESPACE_HEADER_CHECKSUM_BIT | THREESPACE_HEADER_LENGTH_BIT
|
|
465
469
|
class ThreespaceSensor:
|
|
466
470
|
|
|
467
|
-
def __init__(self, com = None, timeout=2, verbose=False):
|
|
471
|
+
def __init__(self, com = None, timeout=2, verbose=False, initial_clear_timeout=None):
|
|
468
472
|
if com is None: #Default to attempting to use the serial com class if none is provided
|
|
469
473
|
com = ThreespaceSerialComClass
|
|
470
474
|
|
|
475
|
+
manually_opened_com = False
|
|
471
476
|
#Auto discover using the supplied com class type
|
|
472
477
|
if inspect.isclass(com) and issubclass(com, ThreespaceComClass):
|
|
473
478
|
new_com = None
|
|
@@ -477,10 +482,14 @@ class ThreespaceSensor:
|
|
|
477
482
|
if new_com is None:
|
|
478
483
|
raise RuntimeError("Failed to auto discover com port")
|
|
479
484
|
self.com = new_com
|
|
485
|
+
manually_opened_com = True
|
|
480
486
|
self.com.open()
|
|
481
487
|
#The supplied com already was a com class, nothing to do
|
|
482
488
|
elif inspect.isclass(type(com)) and issubclass(type(com), ThreespaceComClass):
|
|
483
489
|
self.com = com
|
|
490
|
+
if not self.com.check_open():
|
|
491
|
+
self.com.open()
|
|
492
|
+
manually_opened_com = True
|
|
484
493
|
else: #Unknown type, try making a ThreespaceSerialComClass out of this
|
|
485
494
|
try:
|
|
486
495
|
self.com = ThreespaceSerialComClass(com)
|
|
@@ -503,6 +512,12 @@ class ThreespaceSensor:
|
|
|
503
512
|
self.is_log_streaming = False
|
|
504
513
|
self.is_file_streaming = False
|
|
505
514
|
self._force_stop_streaming()
|
|
515
|
+
#Clear out the buffer to allow faster initializing
|
|
516
|
+
#Ex: If a large buffer build up due to streaming, especially if using a slower interface like BLE,
|
|
517
|
+
#it may take a while before the entire garbage data can be parsed when checking for bootloader, causing a timeout
|
|
518
|
+
#even though it would have eventually succeeded
|
|
519
|
+
self.__clear_com(initial_clear_timeout)
|
|
520
|
+
|
|
506
521
|
|
|
507
522
|
#Used to ensure connecting to the correct sensor when reconnecting
|
|
508
523
|
self.serial_number = None
|
|
@@ -514,12 +529,19 @@ class ThreespaceSensor:
|
|
|
514
529
|
self.getStreamingBatchCommand: ThreespaceGetStreamingBatchCommand = None
|
|
515
530
|
self.funcs = {}
|
|
516
531
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
self.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
532
|
+
try:
|
|
533
|
+
self.__cached_in_bootloader = self.__check_bootloader_status()
|
|
534
|
+
if not self.in_bootloader:
|
|
535
|
+
self.__firmware_init()
|
|
536
|
+
else:
|
|
537
|
+
self.__cache_serial_number(self.bootloader_get_sn())
|
|
538
|
+
self.__empty_debug_cache()
|
|
539
|
+
#This is just to prevent a situation where instantiating the API creates and fails to release a com class on failure when user catches the exception
|
|
540
|
+
#If user provides the com class, it is up to them to handle its state on error
|
|
541
|
+
except Exception as e:
|
|
542
|
+
if manually_opened_com:
|
|
543
|
+
self.com.close()
|
|
544
|
+
raise e
|
|
523
545
|
|
|
524
546
|
#Just a helper for outputting information
|
|
525
547
|
def log(self, *args):
|
|
@@ -528,6 +550,16 @@ class ThreespaceSensor:
|
|
|
528
550
|
|
|
529
551
|
#-----------------------INITIALIZIATION & REINITIALIZATION-----------------------------------
|
|
530
552
|
|
|
553
|
+
def __clear_com(self, refresh_timeout=None):
|
|
554
|
+
data = self.com.read_all()
|
|
555
|
+
if refresh_timeout is None: return
|
|
556
|
+
while len(data) > 0: #Continue until all data is cleared
|
|
557
|
+
start_time = time.time()
|
|
558
|
+
while time.time() - start_time < refresh_timeout: #Wait up to refresh time for a new message
|
|
559
|
+
data = self.com.read_all()
|
|
560
|
+
if len(data) > 0:
|
|
561
|
+
break #Refresh the start time and wait for more data
|
|
562
|
+
|
|
531
563
|
def __firmware_init(self):
|
|
532
564
|
"""
|
|
533
565
|
Should only be called when not streaming and known in firmware.
|
|
@@ -757,8 +789,7 @@ class ThreespaceSensor:
|
|
|
757
789
|
return 0xFF, 0xFF
|
|
758
790
|
|
|
759
791
|
#For dirty check
|
|
760
|
-
|
|
761
|
-
keys = [v.split('=')[0] for v in keys]
|
|
792
|
+
param_dict = threespaceSetSettingsStringToDict(cmd[1:-1])
|
|
762
793
|
|
|
763
794
|
#Must enable this before sending the set so can properly handle reading the response
|
|
764
795
|
if "debug_mode=1" in cmd:
|
|
@@ -786,14 +817,14 @@ class ThreespaceSensor:
|
|
|
786
817
|
#Handle updating state variables based on settings
|
|
787
818
|
#If the user modified the header, need to cache the settings so the API knows how to interpret responses
|
|
788
819
|
if "header" in cmd.lower(): #First do a quick check
|
|
789
|
-
if any(v in keys for v in ThreespaceSensor.HEADER_KEYS): #Then do a longer check
|
|
820
|
+
if any(v in param_dict.keys() for v in ThreespaceSensor.HEADER_KEYS): #Then do a longer check
|
|
790
821
|
self.__cache_header_settings()
|
|
791
822
|
|
|
792
823
|
if "stream_slots" in cmd.lower():
|
|
793
824
|
self.__cache_streaming_settings()
|
|
794
825
|
|
|
795
826
|
#All the settings changed, just need to mark dirty
|
|
796
|
-
if any(v in keys for v in ("default", "reboot")):
|
|
827
|
+
if any(v in param_dict.keys() for v in ("default", "reboot")):
|
|
797
828
|
self.set_cached_settings_dirty()
|
|
798
829
|
|
|
799
830
|
if err:
|
|
@@ -1006,7 +1037,7 @@ class ThreespaceSensor:
|
|
|
1006
1037
|
|
|
1007
1038
|
#------------------------------BASE INPUT PARSING--------------------------------------------
|
|
1008
1039
|
|
|
1009
|
-
def __internal_update(self, header: ThreespaceHeader = None):
|
|
1040
|
+
def __internal_update(self, header: ThreespaceHeader = None, blocking=True):
|
|
1010
1041
|
"""
|
|
1011
1042
|
Manages checking the datastream for asynchronous responses (Streaming, Immediate Debug Messages).
|
|
1012
1043
|
If no data is found to match these responses, the data buffer will be considered corrupted/misaligned
|
|
@@ -1021,29 +1052,39 @@ class ThreespaceSensor:
|
|
|
1021
1052
|
|
|
1022
1053
|
Returns
|
|
1023
1054
|
--------
|
|
1024
|
-
|
|
1025
|
-
|
|
1055
|
+
0 : Internal Data Found/Parsed
|
|
1056
|
+
1 : Not enough data (Only possible when blocking == False)
|
|
1057
|
+
2 : Misalignment
|
|
1026
1058
|
"""
|
|
1027
1059
|
checksum_match = False #Just for debugging
|
|
1028
1060
|
|
|
1029
1061
|
if header is not None:
|
|
1030
1062
|
#NOTE: FOR THIS TO WORK IT IS REQUIRED THAT THE HEADER DOES NOT CHANGE WHILE STREAMING ANY FORM OF DATA.
|
|
1031
1063
|
#IT IS UP TO THE API TO ENFORCE NOT ALLOWING HEADER CHANGES WHILE ANY OF THOSE THINGS ARE HAPPENING
|
|
1032
|
-
if self.is_data_streaming and header.echo == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM:
|
|
1064
|
+
if self.is_data_streaming and header.echo == THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM:
|
|
1065
|
+
if not blocking:
|
|
1066
|
+
expected_output_size = len(header.raw_binary) + self.getStreamingBatchCommand.info.out_size
|
|
1067
|
+
if self.com.length < expected_output_size: return THREESPACE_UPDATE_COMMAND_NOT_ENOUGH_DATA
|
|
1033
1068
|
if checksum_match := self.__peek_checksum(header, max_data_length=self.getStreamingBatchCommand.info.out_size):
|
|
1034
1069
|
self.__update_base_streaming()
|
|
1035
1070
|
self.misaligned = False
|
|
1036
|
-
return
|
|
1071
|
+
return THREESPACE_UPDATE_COMMAND_PARSED
|
|
1037
1072
|
elif self.is_log_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
|
|
1038
|
-
if
|
|
1073
|
+
if not blocking:
|
|
1074
|
+
expected_output_size = len(header.raw_binary) + min(header.length, THREESPACE_LIVE_LOG_STREAM_MAX_PACKET_SIZE)
|
|
1075
|
+
if self.com.length < expected_output_size: return THREESPACE_UPDATE_COMMAND_NOT_ENOUGH_DATA
|
|
1076
|
+
if checksum_match := self.__peek_checksum(header, max_data_length=THREESPACE_LIVE_LOG_STREAM_MAX_PACKET_SIZE):
|
|
1039
1077
|
self.__update_log_streaming()
|
|
1040
1078
|
self.misaligned = False
|
|
1041
|
-
return
|
|
1079
|
+
return THREESPACE_UPDATE_COMMAND_PARSED
|
|
1042
1080
|
elif self.is_file_streaming and header.echo == THREESPACE_FILE_READ_BYTES_COMMAND_NUM:
|
|
1081
|
+
if not blocking:
|
|
1082
|
+
expected_output_size = len(header.raw_binary) + min(header.length, THREESPACE_FILE_STREAMING_MAX_PACKET_SIZE)
|
|
1083
|
+
if self.com.length < expected_output_size: return THREESPACE_UPDATE_COMMAND_NOT_ENOUGH_DATA
|
|
1043
1084
|
if checksum_match := self.__peek_checksum(header, max_data_length=THREESPACE_FILE_STREAMING_MAX_PACKET_SIZE):
|
|
1044
1085
|
self.__update_file_streaming()
|
|
1045
1086
|
self.misaligned = False
|
|
1046
|
-
return
|
|
1087
|
+
return THREESPACE_UPDATE_COMMAND_PARSED
|
|
1047
1088
|
|
|
1048
1089
|
#Debug messages are possible and there is enough data to potentially be a debug message
|
|
1049
1090
|
#NOTE: Firmware should avoid putting more then one \r\n in a debug message as they will be treated as unprocessed/misaligned characters
|
|
@@ -1058,7 +1099,7 @@ class ThreespaceSensor:
|
|
|
1058
1099
|
message = self.com.readline() #Read out the whole message!
|
|
1059
1100
|
self.debug_callback(message.decode('ascii'), self)
|
|
1060
1101
|
self.misaligned = False
|
|
1061
|
-
return
|
|
1102
|
+
return THREESPACE_UPDATE_COMMAND_PARSED
|
|
1062
1103
|
|
|
1063
1104
|
#The response didn't match any of the expected asynchronous streaming API responses, so assume a misalignment
|
|
1064
1105
|
if header is not None:
|
|
@@ -1069,7 +1110,7 @@ class ThreespaceSensor:
|
|
|
1069
1110
|
msg = "Possible Misalignment or corruption/debug message"
|
|
1070
1111
|
#self.log("Misaligned:", self.com.peek(1))
|
|
1071
1112
|
self.__handle_misalignment(msg)
|
|
1072
|
-
return
|
|
1113
|
+
return THREESPACE_UPDATE_COMMAND_MISALIGNED
|
|
1073
1114
|
|
|
1074
1115
|
def __handle_misalignment(self, message: str = None):
|
|
1075
1116
|
if not self.misaligned and message is not None:
|
|
@@ -1167,12 +1208,21 @@ class ThreespaceSensor:
|
|
|
1167
1208
|
self.streaming_packets.clear()
|
|
1168
1209
|
|
|
1169
1210
|
#This is called for all streaming types
|
|
1170
|
-
def updateStreaming(self, max_checks=float('inf')):
|
|
1211
|
+
def updateStreaming(self, max_checks=float('inf'), timeout=None, blocking=False):
|
|
1171
1212
|
"""
|
|
1172
1213
|
Returns true if any amount of data was processed whether valid or not. This is called for all streaming types.
|
|
1214
|
+
|
|
1215
|
+
Parameters
|
|
1216
|
+
----------
|
|
1217
|
+
max_checks : Will only attempt to read up to max_checks packets
|
|
1218
|
+
timeout : Will only attempt to read packets for this duration. It is possible for this function to take longer then this timeout \
|
|
1219
|
+
if blocking = True, in which case it could take up to timeout + com.timeout
|
|
1220
|
+
blocking : If False, will immediately stop when not enough data is available. If true, will immediately stop if not enough data \
|
|
1221
|
+
for a header, but will block when trying to retrieve the data associated with that header. For most com classes, this does not matter. \
|
|
1222
|
+
But for communication such as BLE where the header and data may be split between different packets, this will have a clear effect.
|
|
1173
1223
|
"""
|
|
1174
1224
|
if not self.is_streaming: return False
|
|
1175
|
-
|
|
1225
|
+
if timeout is None: timeout = float('inf')
|
|
1176
1226
|
#I may need to make this have a max num bytes it will process before exiting to prevent locking up on slower machines
|
|
1177
1227
|
#due to streaming faster then the program runs
|
|
1178
1228
|
num_checks = 0
|
|
@@ -1182,12 +1232,17 @@ class ThreespaceSensor:
|
|
|
1182
1232
|
return data_processed
|
|
1183
1233
|
|
|
1184
1234
|
#Get header
|
|
1235
|
+
|
|
1185
1236
|
header = self.com.peek(self.header_info.size)
|
|
1186
1237
|
|
|
1187
1238
|
#Get the header and send it to the internal update
|
|
1188
1239
|
header = ThreespaceHeader.from_bytes(header, self.header_info)
|
|
1189
|
-
self.__internal_update(header)
|
|
1190
|
-
|
|
1240
|
+
result = self.__internal_update(header, blocking=blocking)
|
|
1241
|
+
if result == THREESPACE_UPDATE_COMMAND_PARSED:
|
|
1242
|
+
data_processed = True
|
|
1243
|
+
elif result == THREESPACE_UPDATE_COMMAND_NOT_ENOUGH_DATA:
|
|
1244
|
+
return data_processed
|
|
1245
|
+
|
|
1191
1246
|
num_checks += 1
|
|
1192
1247
|
|
|
1193
1248
|
return data_processed
|
|
@@ -1436,20 +1491,26 @@ class ThreespaceSensor:
|
|
|
1436
1491
|
self.com.write("RR".encode())
|
|
1437
1492
|
|
|
1438
1493
|
def cleanup(self):
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
#
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1494
|
+
error = None
|
|
1495
|
+
try:
|
|
1496
|
+
if not self.in_bootloader:
|
|
1497
|
+
if self.is_data_streaming:
|
|
1498
|
+
self.stopStreaming()
|
|
1499
|
+
if self.is_file_streaming:
|
|
1500
|
+
self.fileStopStream()
|
|
1501
|
+
if self.is_log_streaming:
|
|
1502
|
+
self.stopDataLogging()
|
|
1503
|
+
|
|
1504
|
+
#The sensor may or may not have this command registered. So just try it
|
|
1505
|
+
try:
|
|
1506
|
+
#May not be opened, but also not cacheing that so just attempt to close.
|
|
1507
|
+
self.closeFile()
|
|
1508
|
+
except: pass
|
|
1509
|
+
except Exception as e:
|
|
1510
|
+
error = e
|
|
1511
|
+
self.com.close() #Ensuring the close gets called, that way com ports can't get stuck open. Also makes calling cleanup() "safe" even after disconnect
|
|
1512
|
+
if error:
|
|
1513
|
+
raise error
|
|
1453
1514
|
|
|
1454
1515
|
#-------------------------START ALL PROTOTYPES------------------------------------
|
|
1455
1516
|
|
|
@@ -1738,4 +1799,17 @@ def threespaceGetHeaderLabels(header_info: ThreespaceHeaderInfo):
|
|
|
1738
1799
|
order.append("serial#")
|
|
1739
1800
|
if header_info.length_enabled:
|
|
1740
1801
|
order.append("len")
|
|
1741
|
-
return order
|
|
1802
|
+
return order
|
|
1803
|
+
|
|
1804
|
+
def threespaceSetSettingsStringToDict(setting_string: str):
|
|
1805
|
+
d = {}
|
|
1806
|
+
for item in setting_string.split(';'):
|
|
1807
|
+
result = item.split('=')
|
|
1808
|
+
key = result[0]
|
|
1809
|
+
if len(result) == 1:
|
|
1810
|
+
value = None
|
|
1811
|
+
else:
|
|
1812
|
+
value = '='.join(result[1:]) #In case = was part of the value, do a join
|
|
1813
|
+
|
|
1814
|
+
d[key] = value
|
|
1815
|
+
return d
|
|
@@ -29,6 +29,7 @@ THREESPACE_GET_SETTINGS_ERROR_RESPONSE = "<KEY_ERROR>"
|
|
|
29
29
|
THREESPACE_SETTING_KEY_INVALID_CHARS = " :;"
|
|
30
30
|
|
|
31
31
|
THREESPACE_FILE_STREAMING_MAX_PACKET_SIZE = 512
|
|
32
|
+
THREESPACE_LIVE_LOG_STREAM_MAX_PACKET_SIZE = 2048
|
|
32
33
|
|
|
33
34
|
THREESPACE_SN_FAMILY_POS = 14 * 4
|
|
34
35
|
THREESPACE_SN_VARIATION_POS = 11 * 4
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|