yostlabs 2025.10.6__tar.gz → 2025.10.29__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/PKG-INFO +2 -1
  2. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/pyproject.toml +3 -2
  3. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/communication/ble.py +117 -13
  4. yostlabs-2025.10.29/src/yostlabs/communication/bluetooth.py +203 -0
  5. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/communication/serial.py +40 -9
  6. yostlabs-2025.10.29/src/yostlabs/communication/socket.py +118 -0
  7. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/api.py +6 -1
  8. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/consts.py +3 -1
  9. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/.gitignore +0 -0
  10. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/embedded_2024_dec_20.xml +0 -0
  11. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_ble.py +0 -0
  12. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_commands.py +0 -0
  13. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_component_specific_settings_and_commands.py +0 -0
  14. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_firmware_upload.py +0 -0
  15. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_parsing_stored_binary.py +0 -0
  16. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_read_settings.py +0 -0
  17. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_streaming.py +0 -0
  18. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_streaming_manager.py +0 -0
  19. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/Examples/example_write_settings.py +0 -0
  20. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/LICENSE +0 -0
  21. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/README.md +0 -0
  22. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/__init__.py +0 -0
  23. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/communication/__init__.py +0 -0
  24. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/communication/base.py +0 -0
  25. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/math/__init__.py +0 -0
  26. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/math/quaternion.py +0 -0
  27. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/math/vector.py +0 -0
  28. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/__init__.py +0 -0
  29. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/eepts.py +0 -0
  30. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/utils/__init__.py +0 -0
  31. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/utils/calibration.py +0 -0
  32. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/utils/parser.py +0 -0
  33. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/utils/streaming.py +0 -0
  34. {yostlabs-2025.10.6 → yostlabs-2025.10.29}/src/yostlabs/tss3/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yostlabs
3
- Version: 2025.10.6
3
+ Version: 2025.10.29
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
@@ -16,6 +16,7 @@ Requires-Python: >=3.10
16
16
  Requires-Dist: async-timeout
17
17
  Requires-Dist: bleak
18
18
  Requires-Dist: numpy
19
+ Requires-Dist: pybluez2
19
20
  Requires-Dist: pyserial
20
21
  Description-Content-Type: text/markdown
21
22
 
@@ -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.10.06"
8
+ version = "2025.10.29"
9
9
  authors = [
10
10
  { name="Yost Labs Inc.", email="techsupport@yostlabs.com" },
11
11
  { name="Andy Riedlinger", email="techsupport@yostlabs.com" },
@@ -24,7 +24,8 @@ dependencies = [
24
24
  "pyserial",
25
25
  "numpy",
26
26
  "bleak",
27
- "async-timeout"
27
+ "async-timeout",
28
+ "pybluez2"
28
29
  ]
29
30
 
30
31
  [project.urls]
@@ -7,6 +7,7 @@ from bleak.backends.device import BLEDevice
7
7
  from bleak.backends.scanner import AdvertisementData
8
8
  from bleak.backends.characteristic import BleakGATTCharacteristic
9
9
  from bleak.exc import BleakDeviceNotFoundError
10
+ from dataclasses import dataclass
10
11
 
11
12
  #Services
12
13
  NORDIC_UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
@@ -23,6 +24,12 @@ HARDWARE_REVISION_STRING_UUID = "00002a27-0000-1000-8000-00805f9b34fb"
23
24
  SERIAL_NUMBER_STRING_UUID = "00002a25-0000-1000-8000-00805f9b34fb"
24
25
  MANUFACTURER_NAME_STRING_UUID = "00002a29-0000-1000-8000-00805f9b34fb"
25
26
 
27
+ @dataclass
28
+ class ThreespaceBLENordicUartProfile:
29
+ SERVICE_UUID: str
30
+ RX_UUID: str
31
+ TX_UUID: str
32
+
26
33
  class TssBLENoConnectionError(Exception): ...
27
34
 
28
35
  def ylBleEventLoopThread(loop: asyncio.AbstractEventLoop):
@@ -36,6 +43,9 @@ class ThreespaceBLEComClass(ThreespaceComClass):
36
43
  EVENT_LOOP = None
37
44
  EVENT_LOOP_THREAD = None
38
45
 
46
+ DEFAULT_PROFILE = ThreespaceBLENordicUartProfile(NORDIC_UART_SERVICE_UUID, NORDIC_UART_RX_UUID, NORDIC_UART_TX_UUID)
47
+ REGISTERED_PROFILES: list[ThreespaceBLENordicUartProfile] = [DEFAULT_PROFILE]
48
+
39
49
  @classmethod
40
50
  def __lazy_event_loop_init(cls):
41
51
  if cls.EVENT_LOOP is None:
@@ -43,7 +53,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
43
53
  cls.EVENT_LOOP_THREAD = threading.Thread(target=ylBleEventLoopThread, args=(cls.EVENT_LOOP,), daemon=True)
44
54
  cls.EVENT_LOOP_THREAD.start()
45
55
 
46
- def __init__(self, ble: BleakClient | BLEDevice | str, discover_name: bool = True, discovery_timeout=5, error_on_disconnect=True):
56
+ def __init__(self, ble: BleakClient | BLEDevice | str, discover_name: bool = True, discovery_timeout=5, error_on_disconnect=True, adv: AdvertisementData = None, profile: ThreespaceBLENordicUartProfile=None):
47
57
  """
48
58
  Parameters
49
59
  ----------
@@ -54,6 +64,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
54
64
  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)
55
65
  """
56
66
  self.__lazy_event_loop_init()
67
+ self.adv = adv
57
68
  bleak_options = { "timeout": discovery_timeout, "disconnected_callback": self.__on_disconnect }
58
69
  if isinstance(ble, BleakClient): #Actual client
59
70
  self.client = ble
@@ -76,6 +87,20 @@ class ThreespaceBLEComClass(ThreespaceComClass):
76
87
  else:
77
88
  raise TypeError("Invalid type for creating a ThreespaceBLEComClass:", type(ble), ble)
78
89
 
90
+ #Select the profile
91
+ self.profile = profile
92
+ if self.profile is None:
93
+ if self.adv is not None and len(self.adv.service_uuids) > 0:
94
+ for service_uuid in self.adv.service_uuids:
95
+ self.profile = self.get_profile(service_uuid)
96
+ if self.profile is not None:
97
+ break
98
+ if self.profile is None:
99
+ self.profile = ThreespaceBLEComClass.DEFAULT_PROFILE
100
+ raise Exception(f"Unknown Service UUIDS: {self.adv.service_uuids}")
101
+ else:
102
+ self.profile = ThreespaceBLEComClass.DEFAULT_PROFILE
103
+
79
104
  self.__timeout = self.DEFAULT_TIMEOUT
80
105
 
81
106
  self.buffer = bytearray()
@@ -101,7 +126,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
101
126
  async def __async_open(self):
102
127
  self.data_read_event = asyncio.Event()
103
128
  await self.client.connect()
104
- await self.client.start_notify(NORDIC_UART_TX_UUID, self.__on_data_received)
129
+ await self.client.start_notify(self.profile.TX_UUID, self.__on_data_received)
105
130
 
106
131
  def open(self):
107
132
  #If trying to open while already open, this infinitely loops
@@ -151,7 +176,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
151
176
  while start_index < len(bytes):
152
177
  end_index = min(len(bytes), start_index + self.max_packet_size) #Can only send max_packet_size data per call to write_gatt_char
153
178
  asyncio.run_coroutine_threadsafe(
154
- self.client.write_gatt_char(NORDIC_UART_RX_UUID, bytes[start_index:end_index], response=False),
179
+ self.client.write_gatt_char(self.profile.RX_UUID, bytes[start_index:end_index], response=False),
155
180
  self.EVENT_LOOP).result()
156
181
  start_index = end_index
157
182
 
@@ -244,8 +269,16 @@ class ThreespaceBLEComClass(ThreespaceComClass):
244
269
  def address(self) -> str:
245
270
  return self.client.address
246
271
 
247
- SCANNER = None
272
+ @classmethod
273
+ def get_profile(cls, service_uuid: str):
274
+ for profile in cls.REGISTERED_PROFILES:
275
+ if profile.SERVICE_UUID == service_uuid:
276
+ return profile
277
+ return None
278
+
279
+ SCANNER: BleakScanner = None
248
280
  SCANNER_LOCK = None
281
+ SCANNER_RUNNING = False
249
282
 
250
283
  SCANNER_CONTINOUS = False #Controls if scanning will continously run
251
284
  SCANNER_TIMEOUT = 5 #Controls the scanners timeout
@@ -255,23 +288,94 @@ class ThreespaceBLEComClass(ThreespaceComClass):
255
288
  #Format: Address - dict = { device: ..., adv: ..., last_found: ... }
256
289
  discovered_devices: dict[str,dict] = {}
257
290
 
291
+ @classmethod
292
+ def set_profiles(cls, profiles: list[ThreespaceBLENordicUartProfile]):
293
+ cls.REGISTERED_PROFILES = profiles
294
+ if cls.SCANNER is not None:
295
+ asyncio.run_coroutine_threadsafe(cls.create_scanner(), cls.EVENT_LOOP).result()
296
+ cls.__remove_unused_profiles()
297
+
298
+ @classmethod
299
+ def register_profile(cls, profile: ThreespaceBLENordicUartProfile):
300
+ if any(v.SERVICE_UUID == profile.SERVICE_UUID for v in cls.REGISTERED_PROFILES): return
301
+ cls.REGISTERED_PROFILES.append(profile)
302
+ if cls.SCANNER is not None:
303
+ asyncio.run_coroutine_threadsafe(cls.create_scanner(), cls.EVENT_LOOP).result()
304
+
305
+ @classmethod
306
+ def unregister_profile(cls, service_uuid: str|ThreespaceBLENordicUartProfile):
307
+ if isinstance(service_uuid, ThreespaceBLENordicUartProfile):
308
+ service_uuid = service_uuid.SERVICE_UUID
309
+ index = None
310
+ for i in range(len(cls.REGISTERED_PROFILES)):
311
+ if cls.REGISTERED_PROFILES[i].SERVICE_UUID == service_uuid:
312
+ index = i
313
+ break
314
+ del cls.REGISTERED_PROFILES[index]
315
+ if cls.SCANNER is not None:
316
+ asyncio.run_coroutine_threadsafe(cls.create_scanner(), cls.EVENT_LOOP).result()
317
+ cls.__remove_unused_profiles()
318
+
319
+ @classmethod
320
+ def __remove_unused_profiles(cls):
321
+ if cls.SCANNER is None: return
322
+ to_remove = []
323
+ valid_service_uuids = [v.SERVICE_UUID for v in cls.REGISTERED_PROFILES]
324
+ with cls.SCANNER_LOCK:
325
+ for address in cls.discovered_devices:
326
+ adv: AdvertisementData = cls.discovered_devices[address]["adv"]
327
+ if not any(uuid in valid_service_uuids for uuid in adv.service_uuids):
328
+ to_remove.append(address)
329
+ for address in to_remove:
330
+ del cls.discovered_devices[address]
331
+
332
+ #Scanner should be created inside of the Async Context that will use it
333
+ @classmethod
334
+ async def create_scanner(cls):
335
+ uuids = [v.SERVICE_UUID for v in cls.REGISTERED_PROFILES]
336
+ restart_scanner = cls.SCANNER_RUNNING
337
+ with cls.SCANNER_LOCK:
338
+ if restart_scanner:
339
+ await cls.__async_stop_scanner()
340
+ cls.SCANNER = BleakScanner(detection_callback=cls.__detection_callback, service_uuids=uuids)
341
+ if restart_scanner:
342
+ await cls.__async_start_scanner()
343
+
258
344
  @classmethod
259
345
  def __lazy_init_scanner(cls):
260
346
  cls.__lazy_event_loop_init()
261
347
  if cls.SCANNER is None:
262
348
  cls.SCANNER_LOCK = threading.Lock()
263
- #Scanner should be created inside of the Async Context that will use it
264
- async def create_scanner():
265
- cls.SCANNER = BleakScanner(detection_callback=cls.__detection_callback, service_uuids=[NORDIC_UART_SERVICE_UUID])
266
- asyncio.run_coroutine_threadsafe(create_scanner(), cls.EVENT_LOOP).result()
267
-
268
-
349
+ asyncio.run_coroutine_threadsafe(cls.create_scanner(), cls.EVENT_LOOP).result()
269
350
 
270
351
  @classmethod
271
352
  def __detection_callback(cls, device: BLEDevice, adv: AdvertisementData):
272
353
  with cls.SCANNER_LOCK:
273
354
  cls.discovered_devices[device.address] = {"device": device, "adv": adv, "last_found": time.time()}
274
355
 
356
+ @classmethod
357
+ async def __async_start_scanner(cls):
358
+ if cls.SCANNER_RUNNING: return
359
+ await cls.SCANNER.start()
360
+ cls.SCANNER_RUNNING = True
361
+
362
+ @classmethod
363
+ async def __async_stop_scanner(cls):
364
+ if not cls.SCANNER_RUNNING: return
365
+ await cls.SCANNER.stop()
366
+ cls.SCANNER_RUNNING = False
367
+
368
+ @classmethod
369
+ def __start_scanner(cls):
370
+ if cls.SCANNER_RUNNING: return
371
+ asyncio.run_coroutine_threadsafe(cls.__async_start_scanner(), cls.EVENT_LOOP).result()
372
+
373
+ @classmethod
374
+ def __stop_scanner(cls):
375
+ if not cls.SCANNER_RUNNING: return
376
+ asyncio.run_coroutine_threadsafe(cls.__async_stop_scanner(), cls.EVENT_LOOP).result()
377
+ cls.__stop_scanner()
378
+
275
379
  @classmethod
276
380
  def set_scanner_continous(cls, continous: bool):
277
381
  """
@@ -285,9 +389,9 @@ class ThreespaceBLEComClass(ThreespaceComClass):
285
389
  cls.__lazy_init_scanner()
286
390
  cls.SCANNER_CONTINOUS = continous
287
391
  if continous:
288
- asyncio.run_coroutine_threadsafe(cls.SCANNER.start(), cls.EVENT_LOOP).result()
392
+ cls.__start_scanner()
289
393
  else:
290
- asyncio.run_coroutine_threadsafe(cls.SCANNER.stop(), cls.EVENT_LOOP).result()
394
+ cls.__stop_scanner()
291
395
 
292
396
  @classmethod
293
397
  def update_nearby_devices(cls):
@@ -338,4 +442,4 @@ class ThreespaceBLEComClass(ThreespaceComClass):
338
442
  cls.update_nearby_devices()
339
443
  with cls.SCANNER_LOCK:
340
444
  for device_info in cls.discovered_devices.values():
341
- yield(ThreespaceBLEComClass(device_info["device"]))
445
+ yield(ThreespaceBLEComClass(device_info["device"], adv=device_info["adv"]))
@@ -0,0 +1,203 @@
1
+ from yostlabs.communication.socket import ThreespaceSocketComClass
2
+ import bluetooth
3
+ import time
4
+ import threading
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Callable, Generator
8
+
9
+ @dataclass
10
+ class COD:
11
+ raw: int
12
+ services: list[str]
13
+ major_class: str
14
+ minor_class: str
15
+
16
+ def decode_class_of_device(cod: int) -> dict:
17
+ """Decode a Bluetooth Class of Device (CoD) integer into its components."""
18
+
19
+ # Service Class bit masks (11 bits total)
20
+ services = {
21
+ 0x002000: "Limited Discoverable Mode",
22
+ 0x004000: "Positioning (Location Identification)",
23
+ 0x008000: "Networking",
24
+ 0x010000: "Rendering",
25
+ 0x020000: "Capturing",
26
+ 0x040000: "Object Transfer",
27
+ 0x080000: "Audio",
28
+ 0x100000: "Telephony",
29
+ 0x200000: "Information",
30
+ }
31
+
32
+ # Major Device Classes (5 bits)
33
+ major_classes = {
34
+ 0x00: "Miscellaneous",
35
+ 0x01: "Computer",
36
+ 0x02: "Phone",
37
+ 0x03: "LAN/Network Access Point",
38
+ 0x04: "Audio/Video",
39
+ 0x05: "Peripheral",
40
+ 0x06: "Imaging",
41
+ 0x07: "Wearable",
42
+ 0x08: "Toy",
43
+ 0x09: "Health",
44
+ 0x1F: "Uncategorized",
45
+ }
46
+
47
+ # Minor Class mappings for some major classes (for simplicity)
48
+ minor_classes = {
49
+ 0x01: {
50
+ 0x00: "Uncategorized",
51
+ 0x01: "Desktop workstation",
52
+ 0x02: "Server-class computer",
53
+ 0x03: "Laptop",
54
+ 0x04: "Handheld PC/PDA",
55
+ 0x05: "Palm-size PC/PDA",
56
+ },
57
+ 0x02: {
58
+ 0x00: "Uncategorized",
59
+ 0x01: "Cellular",
60
+ 0x02: "Cordless",
61
+ 0x03: "Smartphone",
62
+ 0x04: "Wired modem or voice gateway",
63
+ 0x05: "Common ISDN access",
64
+ },
65
+ 0x05: {
66
+ 0x00: "Uncategorized",
67
+ 0x01: "Keyboard",
68
+ 0x02: "Pointing device",
69
+ 0x03: "Combo keyboard/pointing device",
70
+ },
71
+ }
72
+
73
+ # Extract fields
74
+ service_bits = cod & 0xFFE000 # top 11 bits
75
+ major_class = (cod >> 8) & 0x1F
76
+ minor_class = (cod >> 2) & 0x3F
77
+
78
+ # Decode services
79
+ decoded_services = [name for bit, name in services.items() if service_bits & bit]
80
+
81
+ # Decode major class
82
+ major_name = major_classes.get(major_class, "Unknown")
83
+
84
+ # Decode minor class (context-dependent)
85
+ minor_name = minor_classes.get(major_class, {}).get(minor_class, f"Minor code {minor_class}")
86
+
87
+ return COD(cod, decoded_services or ["None"], major_name, minor_name)
88
+
89
+ @dataclass
90
+ class ScannerResult:
91
+ address: str
92
+ name: str
93
+ class_of_device: COD
94
+
95
+ class Scanner:
96
+
97
+ def __init__(self, interval=5):
98
+ self.enabled = False
99
+ self.done = True
100
+
101
+ self.nearby = None
102
+ self.thread = None
103
+ self.updated = False
104
+ self.duration = int(interval / 1.2)
105
+
106
+ def start(self):
107
+ if not self.done: return
108
+ self.thread = threading.Thread(target=self.process, daemon=True)
109
+ self.enabled = True
110
+ self.done = False
111
+ self.updated = False
112
+ self.thread.start()
113
+
114
+ def stop(self):
115
+ self.enabled = False
116
+
117
+ def get_most_recent(self):
118
+ if not self.updated: return None
119
+ self.updated = False
120
+ return self.nearby
121
+
122
+ @property
123
+ def is_running(self):
124
+ return self.done
125
+
126
+ def process(self):
127
+ while self.enabled:
128
+ nearby = bluetooth.discover_devices(duration=self.duration, lookup_names=True, lookup_class=True)
129
+ self.nearby = [ScannerResult(addr, name, decode_class_of_device(cod)) for addr, name, cod in nearby]
130
+ self.updated = True
131
+ self.done = True
132
+
133
+ class ThreespaceBluetoothComClass(ThreespaceSocketComClass):
134
+
135
+ SCANNER = None
136
+
137
+ def __init__(self, addr: str, name: str = None, connection_timeout=None):
138
+ super().__init__(bluetooth.BluetoothSocket(bluetooth.Protocols.RFCOMM), (addr, 1), connection_timeout=connection_timeout)
139
+ self.address = addr
140
+ self.__name = name or addr
141
+
142
+ @property
143
+ def name(self) -> str:
144
+ return self.__name
145
+
146
+ @classmethod
147
+ def __lazy_init_scanner(cls):
148
+ if cls.SCANNER is None:
149
+ cls.SCANNER = Scanner()
150
+ cls.SCANNER.start()
151
+
152
+ @staticmethod
153
+ def __default_filter(result: ScannerResult):
154
+ return result.class_of_device.major_class == "Wearable" and result.class_of_device.minor_class == "Minor code 0"
155
+
156
+ @staticmethod
157
+ def auto_detect(wait_for_update=True, filter: Callable[[ScannerResult],bool] = None) -> Generator["ThreespaceBluetoothComClass", None, None]:
158
+ """
159
+ Returns a list of com classes of the same type called on nearby
160
+ """
161
+ cls = ThreespaceBluetoothComClass
162
+ cls.__lazy_init_scanner()
163
+ if filter is None:
164
+ filter = cls.__default_filter
165
+ if wait_for_update:
166
+ cls.SCANNER.updated = False
167
+ while not cls.SCANNER.updated: time.sleep(0.1)
168
+ if cls.SCANNER.nearby is None: return
169
+ for device_info in cls.SCANNER.nearby:
170
+ if not filter(device_info): continue
171
+ name = device_info.name or None
172
+ yield ThreespaceBluetoothComClass(device_info.address, name)
173
+
174
+
175
+ if __name__ == "__main__":
176
+ from yostlabs.tss3.api import ThreespaceSensor
177
+
178
+ com = None
179
+ for device in ThreespaceBluetoothComClass.auto_detect():
180
+ com = device
181
+
182
+ if com is None:
183
+ print("Failed to detect a bluetooth sensor")
184
+ exit()
185
+ print("Connecting to:", com.name)
186
+
187
+ sensor = ThreespaceSensor(com, verbose=True)
188
+
189
+ sensor.set_settings(stream_slots=0)
190
+ print(sensor.get_settings("stream_slots", "stream_hz"))
191
+ print(sensor.getTaredOrientation())
192
+
193
+ sensor.startStreaming()
194
+
195
+ start_time = time.perf_counter()
196
+ while time.perf_counter() - start_time < 5:
197
+ sensor.updateStreaming()
198
+ packet = sensor.getOldestStreamingPacket()
199
+ while packet is not None:
200
+ print(packet)
201
+ packet = sensor.getOldestStreamingPacket()
202
+ sensor.stopStreaming()
203
+ sensor.cleanup()
@@ -1,16 +1,27 @@
1
1
  from yostlabs.communication.base import *
2
2
  import serial
3
3
  import serial.tools.list_ports
4
+ from serial.tools.list_ports_common import ListPortInfo
4
5
  import time
5
6
 
6
-
7
7
  class ThreespaceSerialComClass(ThreespaceComClass):
8
-
9
8
  PID_V3_MASK = 0x3000
10
- PID_BOOTLOADER = 0x1000
11
9
 
12
10
  VID = 0x2476
13
11
 
12
+ PID_BOOTLOADER = 0x1000
13
+ PID_EMBED = 0x3040
14
+ PID_DL = 0x3050
15
+ PID_LX = 0x3090
16
+
17
+ PID_TO_STR_DICT = {
18
+ PID_EMBED: "EM",
19
+ PID_DL: "DL",
20
+ PID_BOOTLOADER: "BOOT",
21
+ PID_LX: "LX"
22
+ }
23
+
24
+
14
25
  DEFAULT_BAUDRATE = 115200
15
26
  DEFAULT_TIMEOUT = 2
16
27
 
@@ -18,12 +29,11 @@ class ThreespaceSerialComClass(ThreespaceComClass):
18
29
  if isinstance(ser, serial.Serial):
19
30
  self.ser = ser
20
31
  elif isinstance(ser, str):
21
- self.ser = serial.Serial(ser, baudrate=ThreespaceSerialComClass.DEFAULT_BAUDRATE, timeout=ThreespaceSerialComClass.DEFAULT_TIMEOUT)
32
+ self.ser = serial.Serial(None, baudrate=ThreespaceSerialComClass.DEFAULT_BAUDRATE, timeout=ThreespaceSerialComClass.DEFAULT_TIMEOUT)
33
+ self.ser.port = ser
22
34
  else:
23
35
  raise TypeError("Invalid type for creating a ThreespaceSerialComClass:", type(ser), ser)
24
36
 
25
- self.ser = ser
26
-
27
37
  self.peek_buffer = bytearray()
28
38
  self.peek_length = 0
29
39
 
@@ -120,6 +130,22 @@ class ThreespaceSerialComClass(ThreespaceComClass):
120
130
  @property
121
131
  def name(self) -> str:
122
132
  return self.ser.port
133
+
134
+ @property
135
+ def suffix(self) -> str:
136
+ return self.pid_to_str(self.get_port_info().pid)
137
+
138
+ def get_port_info(self):
139
+ ports = serial.tools.list_ports.comports()
140
+ for port in ports:
141
+ if port.device == self.ser.port:
142
+ return port
143
+ return None
144
+
145
+ @staticmethod
146
+ def is_threespace_port(port: ListPortInfo):
147
+ cls = ThreespaceSerialComClass
148
+ return port.vid == cls.VID and (port.pid & cls.PID_V3_MASK == cls.PID_V3_MASK or port.pid == cls.PID_BOOTLOADER)
123
149
 
124
150
  #This is not part of the ThreespaceComClass interface, but is useful as a utility for those directly using the ThreespaceSerialComClass
125
151
  @staticmethod
@@ -127,7 +153,7 @@ class ThreespaceSerialComClass(ThreespaceComClass):
127
153
  cls = ThreespaceSerialComClass
128
154
  ports = serial.tools.list_ports.comports()
129
155
  for port in ports:
130
- if port.vid == cls.VID and (port.pid & cls.PID_V3_MASK == cls.PID_V3_MASK or port.pid == cls.PID_BOOTLOADER):
156
+ if cls.is_threespace_port(port):
131
157
  yield port
132
158
 
133
159
  @staticmethod
@@ -139,7 +165,12 @@ class ThreespaceSerialComClass(ThreespaceComClass):
139
165
  cls = ThreespaceSerialComClass
140
166
  ports = serial.tools.list_ports.comports()
141
167
  for port in ports:
142
- if port.vid == cls.VID and (port.pid & cls.PID_V3_MASK == cls.PID_V3_MASK or port.pid == cls.PID_BOOTLOADER):
168
+ if cls.is_threespace_port(port):
143
169
  ser = serial.Serial(None, baudrate=default_baudrate, timeout=default_timeout) #By setting port as None, can create an object without immediately opening the port
144
170
  ser.port = port.device #Now assign the port, allowing the serial object to exist without being opened yet
145
- yield ThreespaceSerialComClass(ser)
171
+ yield ThreespaceSerialComClass(ser)
172
+
173
+ @classmethod
174
+ def pid_to_str(cls, pid):
175
+ if pid not in cls.PID_TO_STR_DICT: return "Unknown"
176
+ return cls.PID_TO_STR_DICT[pid]
@@ -0,0 +1,118 @@
1
+ from yostlabs.communication.base import *
2
+ import socket
3
+ import time
4
+ from typing import Callable
5
+
6
+ class ThreespaceSocketComClass(ThreespaceComClass):
7
+
8
+ """
9
+ Inheriting classes must implement auto_detect() functionality.
10
+ Should also implement 'name' functionality
11
+ """
12
+
13
+ def __init__(self, sock: socket.socket, *connection_params, connection_timeout=None):
14
+ self.socket = sock
15
+ self._opened = False
16
+
17
+ self.buffer = bytearray()
18
+ self.__timeout = 2
19
+ self.__connection_timeout = connection_timeout
20
+ self.__connection_params = connection_params
21
+
22
+ @property
23
+ def timeout(self) -> float:
24
+ return self.__timeout
25
+
26
+ @timeout.setter
27
+ def timeout(self, timeout: float):
28
+ self.__timeout = timeout
29
+
30
+ def open(self):
31
+ if self._opened: return True
32
+ try:
33
+ if self.__connection_timeout is None:
34
+ self.socket.setblocking(True)
35
+ else:
36
+ self.socket.settimeout(self.__connection_timeout)
37
+ self.socket.connect(*self.__connection_params)
38
+ except Exception as e:
39
+ try:
40
+ self.socket.close()
41
+ except: pass
42
+ print(e)
43
+ return False
44
+ self._opened = True
45
+ return True
46
+
47
+ def close(self):
48
+ if not self._opened: return
49
+ self.socket.close()
50
+ self._opened = False
51
+
52
+ def check_open(self):
53
+ return self._opened
54
+
55
+ def write(self, bytes: bytes):
56
+ self.socket.send(bytes)
57
+
58
+ def read(self, num_bytes: int):
59
+ self.__update_while(lambda: len(self.buffer) < num_bytes)
60
+ amount = min(len(self.buffer), num_bytes)
61
+ result = self.buffer[:amount]
62
+ del self.buffer[:amount]
63
+ return result
64
+
65
+ def peek(self, num_bytes: int):
66
+ self.__update_while(lambda: len(self.buffer) < num_bytes)
67
+ amount = min(len(self.buffer), num_bytes)
68
+ return self.buffer[:amount]
69
+
70
+ def read_until(self, expected: bytes):
71
+ self.__update_while(lambda: expected not in self.buffer)
72
+ if expected in self.buffer:
73
+ length = self.buffer.index(expected) + len(expected)
74
+ result = self.buffer[:length]
75
+ del self.buffer[:length]
76
+ else:
77
+ result = self.buffer.copy()
78
+ self.buffer.clear()
79
+ return result
80
+
81
+ def peek_until(self, expected: bytes, max_length: int = None):
82
+ self.__update_while(lambda: expected not in self.buffer and (max_length is None or len(self.buffer) < max_length))
83
+ if expected in self.buffer:
84
+ length = self.buffer.index(expected) + len(expected)
85
+ if max_length is not None:
86
+ length = min(length, max_length)
87
+ result = self.buffer[:length]
88
+ else:
89
+ length = len(self.buffer)
90
+ if max_length is not None:
91
+ length = min(length, max_length)
92
+ result = self.buffer[length]
93
+ return result
94
+
95
+ def __update_while(self, condition: Callable):
96
+ start_time = time.perf_counter()
97
+ while condition():
98
+ self.__update_buffer()
99
+ if time.perf_counter() - start_time >= self.timeout:
100
+ return
101
+
102
+ def __update_buffer(self, max_bytes: int = 1000):
103
+ self.socket.setblocking(False)
104
+ try:
105
+ self.buffer += self.socket.recv(max_bytes)
106
+ except BlockingIOError:
107
+ return False
108
+ self.socket.settimeout(self.timeout)
109
+ return True
110
+
111
+ @property
112
+ def length(self):
113
+ while self.__update_buffer(): pass
114
+ return len(self.buffer)
115
+
116
+ @property
117
+ def reenumerates(self) -> bool:
118
+ return False
@@ -421,6 +421,8 @@ class StreamableCommands(Enum):
421
421
  GetGpsHdop = 218
422
422
  GetGpsSattelites = 219
423
423
 
424
+ GetLedColor = 238
425
+
424
426
  GetButtonState = 250
425
427
 
426
428
  THREESPACE_AWAIT_COMMAND_FOUND = 0
@@ -1674,7 +1676,8 @@ class ThreespaceSensor:
1674
1676
  def getStreamingLabel(self, cmd_num: int) -> ThreespaceCmdResult[str]: ...
1675
1677
  def setCursor(self, cursor_index: int) -> ThreespaceCmdResult[None]: ...
1676
1678
  def getLastLogCursorInfo(self) -> ThreespaceCmdResult[tuple[int,str]]: ...
1677
- def pauseLogStreaming(self, pause: bool) -> ThreespaceCmdResult[None]: ...
1679
+ def pauseLogStreaming(self, pause: bool) -> ThreespaceCmdResult[None]: ...
1680
+ def getLedColor(self) -> ThreespaceCmdResult[list[float]]: ...
1678
1681
 
1679
1682
  THREESPACE_GET_STREAMING_BATCH_COMMAND_NUM = 84
1680
1683
  THREESPACE_START_STREAMING_COMMAND_NUM = 85
@@ -1817,6 +1820,8 @@ _threespace_commands: list[ThreespaceCommand] = [
1817
1820
  ThreespaceCommand("softwareReset", THREESPACE_SOFTWARE_RESET_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__softwareReset),
1818
1821
  ThreespaceCommand("enterBootloader", THREESPACE_ENTER_BOOTLOADER_COMMAND_NUM, "", "", custom_func=ThreespaceSensor._ThreespaceSensor__enterBootloader),
1819
1822
 
1823
+ ThreespaceCommand("getLedColor", 238, "", "fff"),
1824
+
1820
1825
  ThreespaceCommand("getButtonState", 250, "", "b"),
1821
1826
  ]
1822
1827
 
@@ -61,4 +61,6 @@ THREESPACE_SN_FAMILY_TO_NAME = {
61
61
  0x15 : THREESPACE_FAMILY_BLUETOOTH,
62
62
  0x16 : THREESPACE_FAMILY_DATA_LOGGER,
63
63
  0x17 : THREESPACE_FAMILY_MICRO_USB
64
- }
64
+ }
65
+
66
+ THREESPACE_ERR_COMMIT_FS_LOCKED=8
File without changes
File without changes
File without changes