yostlabs 2025.10.24__tar.gz → 2025.10.29.1__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.24 → yostlabs-2025.10.29.1}/PKG-INFO +2 -1
  2. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/pyproject.toml +3 -2
  3. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/communication/ble.py +12 -11
  4. yostlabs-2025.10.29.1/src/yostlabs/communication/bluetooth.py +203 -0
  5. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/communication/serial.py +3 -1
  6. yostlabs-2025.10.29.1/src/yostlabs/communication/socket.py +118 -0
  7. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/tss3/consts.py +3 -1
  8. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/.gitignore +0 -0
  9. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/embedded_2024_dec_20.xml +0 -0
  10. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_ble.py +0 -0
  11. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_commands.py +0 -0
  12. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_component_specific_settings_and_commands.py +0 -0
  13. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_firmware_upload.py +0 -0
  14. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_parsing_stored_binary.py +0 -0
  15. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_read_settings.py +0 -0
  16. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_streaming.py +0 -0
  17. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_streaming_manager.py +0 -0
  18. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/Examples/example_write_settings.py +0 -0
  19. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/LICENSE +0 -0
  20. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/README.md +0 -0
  21. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/__init__.py +0 -0
  22. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/communication/__init__.py +0 -0
  23. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/communication/base.py +0 -0
  24. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/math/__init__.py +0 -0
  25. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/math/quaternion.py +0 -0
  26. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/math/vector.py +0 -0
  27. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/tss3/__init__.py +0 -0
  28. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/tss3/api.py +0 -0
  29. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/tss3/eepts.py +0 -0
  30. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/tss3/utils/__init__.py +0 -0
  31. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/tss3/utils/calibration.py +0 -0
  32. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/tss3/utils/parser.py +0 -0
  33. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/src/yostlabs/tss3/utils/streaming.py +0 -0
  34. {yostlabs-2025.10.24 → yostlabs-2025.10.29.1}/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.24
3
+ Version: 2025.10.29.1
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.24"
8
+ version = "2025.10.29.1"
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]
@@ -53,7 +53,7 @@ class ThreespaceBLEComClass(ThreespaceComClass):
53
53
  cls.EVENT_LOOP_THREAD = threading.Thread(target=ylBleEventLoopThread, args=(cls.EVENT_LOOP,), daemon=True)
54
54
  cls.EVENT_LOOP_THREAD.start()
55
55
 
56
- def __init__(self, ble: BleakClient | BLEDevice | str, discover_name: bool = True, discovery_timeout=5, error_on_disconnect=True, adv: AdvertisementData = None):
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):
57
57
  """
58
58
  Parameters
59
59
  ----------
@@ -88,17 +88,18 @@ class ThreespaceBLEComClass(ThreespaceComClass):
88
88
  raise TypeError("Invalid type for creating a ThreespaceBLEComClass:", type(ble), ble)
89
89
 
90
90
  #Select the profile
91
- self.profile = None
92
- if self.adv is not None and len(self.adv.service_uuids) > 0:
93
- for service_uuid in self.adv.service_uuids:
94
- self.profile = self.get_profile(service_uuid)
95
- if self.profile is not None:
96
- break
97
- if self.profile is None:
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:
98
102
  self.profile = ThreespaceBLEComClass.DEFAULT_PROFILE
99
- raise Exception(f"Unknown Service UUIDS: {self.adv.service_uuids}")
100
- else:
101
- self.profile = ThreespaceBLEComClass.DEFAULT_PROFILE
102
103
 
103
104
  self.__timeout = self.DEFAULT_TIMEOUT
104
105
 
@@ -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()
@@ -12,11 +12,13 @@ class ThreespaceSerialComClass(ThreespaceComClass):
12
12
  PID_BOOTLOADER = 0x1000
13
13
  PID_EMBED = 0x3040
14
14
  PID_DL = 0x3050
15
+ PID_LX = 0x3090
15
16
 
16
17
  PID_TO_STR_DICT = {
17
18
  PID_EMBED: "EM",
18
19
  PID_DL: "DL",
19
- PID_BOOTLOADER: "BOOT"
20
+ PID_BOOTLOADER: "BOOT",
21
+ PID_LX: "LX"
20
22
  }
21
23
 
22
24
 
@@ -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
@@ -52,6 +52,7 @@ THREESPACE_FAMILY_EMBEDDED = "EM"
52
52
  THREESPACE_FAMILY_BLUETOOTH = "BT"
53
53
  THREESPACE_FAMILY_DATA_LOGGER = "DL"
54
54
  THREESPACE_FAMILY_MICRO_USB = "MUSB"
55
+ THREESPACE_FAMILY_LX = "LX"
55
56
 
56
57
  THREESPACE_SN_FAMILY_TO_NAME = {
57
58
  0x00 : THREESPACE_FAMILY_DEV,
@@ -60,7 +61,8 @@ THREESPACE_SN_FAMILY_TO_NAME = {
60
61
  0x14 : THREESPACE_FAMILY_EMBEDDED,
61
62
  0x15 : THREESPACE_FAMILY_BLUETOOTH,
62
63
  0x16 : THREESPACE_FAMILY_DATA_LOGGER,
63
- 0x17 : THREESPACE_FAMILY_MICRO_USB
64
+ 0x17 : THREESPACE_FAMILY_MICRO_USB,
65
+ 0x21: THREESPACE_FAMILY_LX
64
66
  }
65
67
 
66
68
  THREESPACE_ERR_COMMIT_FS_LOCKED=8
File without changes
File without changes