dexcontrol 0.2.12__py3-none-any.whl → 0.3.1__py3-none-any.whl
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.
Potentially problematic release.
This version of dexcontrol might be problematic. Click here for more details.
- dexcontrol/__init__.py +18 -8
- dexcontrol/apps/dualsense_teleop_base.py +1 -1
- dexcontrol/comm/__init__.py +51 -0
- dexcontrol/comm/base.py +421 -0
- dexcontrol/comm/rtc.py +400 -0
- dexcontrol/comm/subscribers.py +329 -0
- dexcontrol/config/core/chassis.py +9 -4
- dexcontrol/config/core/hand.py +1 -0
- dexcontrol/config/sensors/cameras/__init__.py +1 -2
- dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
- dexcontrol/config/sensors/vega_sensors.py +12 -18
- dexcontrol/config/vega.py +4 -1
- dexcontrol/core/arm.py +61 -37
- dexcontrol/core/chassis.py +141 -119
- dexcontrol/core/component.py +110 -59
- dexcontrol/core/hand.py +118 -85
- dexcontrol/core/head.py +18 -29
- dexcontrol/core/misc.py +327 -155
- dexcontrol/core/robot_query_interface.py +463 -0
- dexcontrol/core/torso.py +4 -8
- dexcontrol/proto/dexcontrol_msg_pb2.py +27 -39
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +75 -118
- dexcontrol/proto/dexcontrol_query_pb2.py +39 -39
- dexcontrol/proto/dexcontrol_query_pb2.pyi +17 -4
- dexcontrol/robot.py +245 -574
- dexcontrol/sensors/__init__.py +1 -2
- dexcontrol/sensors/camera/__init__.py +0 -2
- dexcontrol/sensors/camera/base_camera.py +144 -0
- dexcontrol/sensors/camera/rgb_camera.py +67 -63
- dexcontrol/sensors/camera/zed_camera.py +89 -147
- dexcontrol/sensors/imu/chassis_imu.py +76 -56
- dexcontrol/sensors/imu/zed_imu.py +54 -43
- dexcontrol/sensors/lidar/rplidar.py +16 -20
- dexcontrol/sensors/manager.py +4 -11
- dexcontrol/sensors/ultrasonic.py +14 -27
- dexcontrol/utils/__init__.py +0 -11
- dexcontrol/utils/comm_helper.py +111 -0
- dexcontrol/utils/constants.py +1 -1
- dexcontrol/utils/os_utils.py +169 -1
- dexcontrol/utils/pb_utils.py +0 -22
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/METADATA +13 -1
- dexcontrol-0.3.1.dist-info/RECORD +68 -0
- dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
- dexcontrol/sensors/camera/luxonis_camera.py +0 -169
- dexcontrol/utils/rate_limiter.py +0 -172
- dexcontrol/utils/rtc_utils.py +0 -144
- dexcontrol/utils/subscribers/__init__.py +0 -52
- dexcontrol/utils/subscribers/base.py +0 -281
- dexcontrol/utils/subscribers/camera.py +0 -332
- dexcontrol/utils/subscribers/decoders.py +0 -88
- dexcontrol/utils/subscribers/generic.py +0 -110
- dexcontrol/utils/subscribers/imu.py +0 -175
- dexcontrol/utils/subscribers/lidar.py +0 -172
- dexcontrol/utils/subscribers/protobuf.py +0 -111
- dexcontrol/utils/subscribers/rtc.py +0 -316
- dexcontrol/utils/zenoh_utils.py +0 -122
- dexcontrol-0.2.12.dist-info/RECORD +0 -75
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/WHEEL +0 -0
- {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2025 Dexmate Inc.
|
|
2
|
-
#
|
|
3
|
-
# This software is dual-licensed:
|
|
4
|
-
#
|
|
5
|
-
# 1. GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
6
|
-
# See LICENSE-AGPL for details
|
|
7
|
-
#
|
|
8
|
-
# 2. Commercial License
|
|
9
|
-
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
|
-
|
|
11
|
-
"""Camera Zenoh subscribers for RGB and depth data.
|
|
12
|
-
|
|
13
|
-
This module provides specialized subscribers for camera data including RGB images
|
|
14
|
-
and depth images, using the serialization formats from dexsensor.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import numpy as np
|
|
18
|
-
import zenoh
|
|
19
|
-
from loguru import logger
|
|
20
|
-
|
|
21
|
-
from .base import BaseZenohSubscriber, CustomDataHandler
|
|
22
|
-
|
|
23
|
-
# Import camera serialization functions from dexsensor
|
|
24
|
-
try:
|
|
25
|
-
from dexsensor.serialization.camera import decode_depth, decode_image
|
|
26
|
-
except ImportError:
|
|
27
|
-
logger.error(
|
|
28
|
-
"Failed to import dexsensor camera serialization functions. Please install dexsensor."
|
|
29
|
-
)
|
|
30
|
-
decode_image = None
|
|
31
|
-
decode_depth = None
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class RGBCameraSubscriber(BaseZenohSubscriber):
|
|
35
|
-
"""Zenoh subscriber for RGB camera data.
|
|
36
|
-
|
|
37
|
-
This subscriber handles RGB image data encoded using the dexsensor
|
|
38
|
-
camera serialization format with JPEG compression.
|
|
39
|
-
Uses lazy decoding - data is only decoded when requested.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
def __init__(
|
|
43
|
-
self,
|
|
44
|
-
topic: str,
|
|
45
|
-
zenoh_session: zenoh.Session,
|
|
46
|
-
name: str = "rgb_camera_subscriber",
|
|
47
|
-
enable_fps_tracking: bool = True,
|
|
48
|
-
fps_log_interval: int = 30,
|
|
49
|
-
custom_data_handler: CustomDataHandler | None = None,
|
|
50
|
-
) -> None:
|
|
51
|
-
"""Initialize the RGB camera subscriber.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
topic: Zenoh topic to subscribe to for RGB data.
|
|
55
|
-
zenoh_session: Active Zenoh session for communication.
|
|
56
|
-
name: Name for logging purposes.
|
|
57
|
-
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
58
|
-
fps_log_interval: Number of frames between FPS calculations.
|
|
59
|
-
custom_data_handler: Optional custom function to handle incoming data.
|
|
60
|
-
If provided, this will replace the default data
|
|
61
|
-
handling logic entirely.
|
|
62
|
-
"""
|
|
63
|
-
super().__init__(
|
|
64
|
-
topic,
|
|
65
|
-
zenoh_session,
|
|
66
|
-
name,
|
|
67
|
-
enable_fps_tracking,
|
|
68
|
-
fps_log_interval,
|
|
69
|
-
custom_data_handler,
|
|
70
|
-
)
|
|
71
|
-
self._latest_raw_data: bytes | None = None
|
|
72
|
-
|
|
73
|
-
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
74
|
-
"""Handle incoming RGB image data.
|
|
75
|
-
|
|
76
|
-
Args:
|
|
77
|
-
sample: Zenoh sample containing encoded RGB image data.
|
|
78
|
-
"""
|
|
79
|
-
with self._data_lock:
|
|
80
|
-
self._latest_raw_data = sample.payload.to_bytes()
|
|
81
|
-
self._active = True
|
|
82
|
-
|
|
83
|
-
self._update_fps_metrics()
|
|
84
|
-
|
|
85
|
-
def get_latest_data(self) -> np.ndarray | None:
|
|
86
|
-
"""Get the latest RGB image.
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
Latest RGB image as numpy array (HxWxC) if available, None otherwise.
|
|
90
|
-
"""
|
|
91
|
-
with self._data_lock:
|
|
92
|
-
if self._latest_raw_data is None:
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
if decode_image is None:
|
|
96
|
-
logger.error(
|
|
97
|
-
f"Cannot decode RGB image for {self._name}: dexsensor not available"
|
|
98
|
-
)
|
|
99
|
-
return None
|
|
100
|
-
|
|
101
|
-
try:
|
|
102
|
-
# Decode the image, which is typically in BGR format
|
|
103
|
-
image = decode_image(self._latest_raw_data)
|
|
104
|
-
return image
|
|
105
|
-
except Exception as e:
|
|
106
|
-
logger.error(f"Failed to decode RGB image for {self._name}: {e}")
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
def get_latest_image(self) -> np.ndarray | None:
|
|
110
|
-
"""Get the latest RGB image.
|
|
111
|
-
|
|
112
|
-
Alias for get_latest_data() for clarity.
|
|
113
|
-
|
|
114
|
-
Returns:
|
|
115
|
-
Latest RGB image as numpy array (HxWxC) if available, None otherwise.
|
|
116
|
-
"""
|
|
117
|
-
return self.get_latest_data()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class DepthCameraSubscriber(BaseZenohSubscriber):
|
|
121
|
-
"""Zenoh subscriber for depth camera data.
|
|
122
|
-
|
|
123
|
-
This subscriber handles depth image data encoded using the dexsensor
|
|
124
|
-
camera serialization format with compression.
|
|
125
|
-
Uses lazy decoding - data is only decoded when requested.
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
def __init__(
|
|
129
|
-
self,
|
|
130
|
-
topic: str,
|
|
131
|
-
zenoh_session: zenoh.Session,
|
|
132
|
-
name: str = "depth_camera_subscriber",
|
|
133
|
-
enable_fps_tracking: bool = True,
|
|
134
|
-
fps_log_interval: int = 30,
|
|
135
|
-
custom_data_handler: CustomDataHandler | None = None,
|
|
136
|
-
) -> None:
|
|
137
|
-
"""Initialize the depth camera subscriber.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
topic: Zenoh topic to subscribe to for depth data.
|
|
141
|
-
zenoh_session: Active Zenoh session for communication.
|
|
142
|
-
name: Name for logging purposes.
|
|
143
|
-
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
144
|
-
fps_log_interval: Number of frames between FPS calculations.
|
|
145
|
-
custom_data_handler: Optional custom function to handle incoming data.
|
|
146
|
-
If provided, this will replace the default data
|
|
147
|
-
handling logic entirely.
|
|
148
|
-
"""
|
|
149
|
-
super().__init__(
|
|
150
|
-
topic,
|
|
151
|
-
zenoh_session,
|
|
152
|
-
name,
|
|
153
|
-
enable_fps_tracking,
|
|
154
|
-
fps_log_interval,
|
|
155
|
-
custom_data_handler,
|
|
156
|
-
)
|
|
157
|
-
self._latest_raw_data: bytes | None = None
|
|
158
|
-
|
|
159
|
-
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
160
|
-
"""Handle incoming depth image data.
|
|
161
|
-
|
|
162
|
-
Args:
|
|
163
|
-
sample: Zenoh sample containing encoded depth image data.
|
|
164
|
-
"""
|
|
165
|
-
with self._data_lock:
|
|
166
|
-
self._latest_raw_data = sample.payload.to_bytes()
|
|
167
|
-
self._active = True
|
|
168
|
-
|
|
169
|
-
self._update_fps_metrics()
|
|
170
|
-
|
|
171
|
-
def get_latest_data(self) -> np.ndarray | None:
|
|
172
|
-
"""Get the latest depth data.
|
|
173
|
-
|
|
174
|
-
Returns:
|
|
175
|
-
depth_image if available, None otherwise. Depth values in meters as numpy array (HxW)
|
|
176
|
-
"""
|
|
177
|
-
with self._data_lock:
|
|
178
|
-
if self._latest_raw_data is None:
|
|
179
|
-
return None
|
|
180
|
-
|
|
181
|
-
if decode_depth is None:
|
|
182
|
-
logger.error(
|
|
183
|
-
f"Cannot decode depth image for {self._name}: dexsensor not available"
|
|
184
|
-
)
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
try:
|
|
188
|
-
# Decode the depth image
|
|
189
|
-
depth = decode_depth(self._latest_raw_data)
|
|
190
|
-
return depth
|
|
191
|
-
except Exception as e:
|
|
192
|
-
logger.error(f"Failed to decode depth image for {self._name}: {e}")
|
|
193
|
-
return None
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class RGBDCameraSubscriber(BaseZenohSubscriber):
|
|
197
|
-
"""Zenoh subscriber for RGBD camera data.
|
|
198
|
-
|
|
199
|
-
This subscriber handles both RGB and depth data from an RGBD camera,
|
|
200
|
-
subscribing to separate topics for RGB and depth streams.
|
|
201
|
-
"""
|
|
202
|
-
|
|
203
|
-
def __init__(
|
|
204
|
-
self,
|
|
205
|
-
rgb_topic: str,
|
|
206
|
-
depth_topic: str,
|
|
207
|
-
zenoh_session: zenoh.Session,
|
|
208
|
-
name: str = "rgbd_camera_subscriber",
|
|
209
|
-
enable_fps_tracking: bool = True,
|
|
210
|
-
fps_log_interval: int = 30,
|
|
211
|
-
custom_data_handler: CustomDataHandler | None = None,
|
|
212
|
-
) -> None:
|
|
213
|
-
"""Initialize the RGBD camera subscriber.
|
|
214
|
-
|
|
215
|
-
Args:
|
|
216
|
-
rgb_topic: Zenoh topic for RGB data.
|
|
217
|
-
depth_topic: Zenoh topic for depth data.
|
|
218
|
-
zenoh_session: Active Zenoh session for communication.
|
|
219
|
-
name: Name for logging purposes.
|
|
220
|
-
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
221
|
-
fps_log_interval: Number of frames between FPS calculations.
|
|
222
|
-
custom_data_handler: Optional custom function to handle incoming data.
|
|
223
|
-
If provided, this will replace the default data
|
|
224
|
-
handling logic entirely.
|
|
225
|
-
"""
|
|
226
|
-
# Initialize with RGB topic as primary
|
|
227
|
-
super().__init__(
|
|
228
|
-
rgb_topic,
|
|
229
|
-
zenoh_session,
|
|
230
|
-
name,
|
|
231
|
-
enable_fps_tracking,
|
|
232
|
-
fps_log_interval,
|
|
233
|
-
custom_data_handler,
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
# Create separate subscribers for RGB and depth
|
|
237
|
-
self._rgb_subscriber = RGBCameraSubscriber(
|
|
238
|
-
rgb_topic,
|
|
239
|
-
zenoh_session,
|
|
240
|
-
f"{name}_rgb",
|
|
241
|
-
enable_fps_tracking,
|
|
242
|
-
fps_log_interval,
|
|
243
|
-
custom_data_handler,
|
|
244
|
-
)
|
|
245
|
-
self._depth_subscriber = DepthCameraSubscriber(
|
|
246
|
-
depth_topic,
|
|
247
|
-
zenoh_session,
|
|
248
|
-
f"{name}_depth",
|
|
249
|
-
enable_fps_tracking,
|
|
250
|
-
fps_log_interval,
|
|
251
|
-
custom_data_handler,
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
255
|
-
"""Handle incoming data - not used as we use separate subscribers."""
|
|
256
|
-
pass
|
|
257
|
-
|
|
258
|
-
def get_latest_data(self) -> tuple[np.ndarray, np.ndarray] | None:
|
|
259
|
-
"""Get the latest RGBD data.
|
|
260
|
-
|
|
261
|
-
Returns:
|
|
262
|
-
Tuple of (rgb_image, depth_image, depth_min, depth_max) if both available, None otherwise.
|
|
263
|
-
rgb_image: RGB image as numpy array (HxWxC)
|
|
264
|
-
depth_image: Depth values in meters as numpy array (HxW)
|
|
265
|
-
"""
|
|
266
|
-
rgb_image = self._rgb_subscriber.get_latest_data()
|
|
267
|
-
depth_image = self._depth_subscriber.get_latest_data()
|
|
268
|
-
|
|
269
|
-
if rgb_image is not None and depth_image is not None:
|
|
270
|
-
return rgb_image, depth_image
|
|
271
|
-
return None
|
|
272
|
-
|
|
273
|
-
def get_latest_rgb(self) -> np.ndarray | None:
|
|
274
|
-
"""Get the latest RGB image.
|
|
275
|
-
|
|
276
|
-
Returns:
|
|
277
|
-
Latest RGB image as numpy array (HxWxC) if available, None otherwise.
|
|
278
|
-
"""
|
|
279
|
-
return self._rgb_subscriber.get_latest_image()
|
|
280
|
-
|
|
281
|
-
def get_latest_depth(self) -> np.ndarray | None:
|
|
282
|
-
"""Get the latest depth image.
|
|
283
|
-
|
|
284
|
-
Returns:
|
|
285
|
-
Latest depth image as numpy array (HxW) with values in meters if available, None otherwise.
|
|
286
|
-
"""
|
|
287
|
-
return self._depth_subscriber.get_latest_data()
|
|
288
|
-
|
|
289
|
-
def wait_for_active(self, timeout: float = 5.0) -> bool:
|
|
290
|
-
"""Wait for both RGB and depth subscribers to start receiving data.
|
|
291
|
-
|
|
292
|
-
Args:
|
|
293
|
-
timeout: Maximum time to wait in seconds.
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
True if both subscribers become active, False if timeout is reached.
|
|
297
|
-
"""
|
|
298
|
-
rgb_active = self._rgb_subscriber.wait_for_active(timeout)
|
|
299
|
-
depth_active = self._depth_subscriber.wait_for_active(timeout)
|
|
300
|
-
return rgb_active and depth_active
|
|
301
|
-
|
|
302
|
-
def is_active(self) -> bool:
|
|
303
|
-
"""Check if both RGB and depth subscribers are actively receiving data.
|
|
304
|
-
|
|
305
|
-
Returns:
|
|
306
|
-
True if both subscribers are active, False otherwise.
|
|
307
|
-
"""
|
|
308
|
-
return self._rgb_subscriber.is_active() and self._depth_subscriber.is_active()
|
|
309
|
-
|
|
310
|
-
def shutdown(self) -> None:
|
|
311
|
-
"""Stop both subscribers and release resources."""
|
|
312
|
-
self._rgb_subscriber.shutdown()
|
|
313
|
-
self._depth_subscriber.shutdown()
|
|
314
|
-
super().shutdown()
|
|
315
|
-
|
|
316
|
-
@property
|
|
317
|
-
def rgb_fps(self) -> float:
|
|
318
|
-
"""Get the RGB stream FPS measurement.
|
|
319
|
-
|
|
320
|
-
Returns:
|
|
321
|
-
Current RGB frames per second measurement.
|
|
322
|
-
"""
|
|
323
|
-
return self._rgb_subscriber.fps
|
|
324
|
-
|
|
325
|
-
@property
|
|
326
|
-
def depth_fps(self) -> float:
|
|
327
|
-
"""Get the depth stream FPS measurement.
|
|
328
|
-
|
|
329
|
-
Returns:
|
|
330
|
-
Current depth frames per second measurement.
|
|
331
|
-
"""
|
|
332
|
-
return self._depth_subscriber.fps
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2025 Dexmate Inc.
|
|
2
|
-
#
|
|
3
|
-
# This software is dual-licensed:
|
|
4
|
-
#
|
|
5
|
-
# 1. GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
6
|
-
# See LICENSE-AGPL for details
|
|
7
|
-
#
|
|
8
|
-
# 2. Commercial License
|
|
9
|
-
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
|
-
|
|
11
|
-
"""Decoder functions for Zenoh subscribers.
|
|
12
|
-
|
|
13
|
-
This module provides common decoder functions that can be used with the
|
|
14
|
-
GenericZenohSubscriber to transform raw Zenoh bytes into specific data formats.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
from collections.abc import Callable
|
|
19
|
-
from typing import Any, Type, TypeVar
|
|
20
|
-
|
|
21
|
-
import zenoh
|
|
22
|
-
from google.protobuf.message import Message
|
|
23
|
-
|
|
24
|
-
M = TypeVar("M", bound=Message)
|
|
25
|
-
|
|
26
|
-
# Type alias for decoder functions
|
|
27
|
-
DecoderFunction = Callable[[zenoh.ZBytes], Any]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def protobuf_decoder(message_type: Type[M]) -> DecoderFunction:
|
|
31
|
-
"""Create a decoder function for a specific protobuf message type.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
message_type: Protobuf message class to decode to.
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
Decoder function that parses bytes into the specified protobuf message.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
def decode(data: zenoh.ZBytes) -> M:
|
|
41
|
-
message = message_type()
|
|
42
|
-
message.ParseFromString(data.to_bytes())
|
|
43
|
-
return message
|
|
44
|
-
|
|
45
|
-
return decode
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def raw_bytes_decoder(data: zenoh.ZBytes) -> bytes:
|
|
49
|
-
"""Decoder that returns raw bytes.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
data: Zenoh bytes data.
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Raw bytes data.
|
|
56
|
-
"""
|
|
57
|
-
return data.to_bytes()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def json_decoder(data: zenoh.ZBytes) -> Any:
|
|
61
|
-
"""Decoder that parses JSON from bytes.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
data: Zenoh bytes data containing JSON.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
Parsed JSON data.
|
|
68
|
-
|
|
69
|
-
Raises:
|
|
70
|
-
json.JSONDecodeError: If the data is not valid JSON.
|
|
71
|
-
"""
|
|
72
|
-
return json.loads(data.to_bytes().decode("utf-8"))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def string_decoder(data: zenoh.ZBytes, encoding: str = "utf-8") -> str:
|
|
76
|
-
"""Decoder that converts bytes to string.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
data: Zenoh bytes data.
|
|
80
|
-
encoding: Text encoding to use for decoding.
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
Decoded string.
|
|
84
|
-
|
|
85
|
-
Raises:
|
|
86
|
-
UnicodeDecodeError: If the data cannot be decoded with the specified encoding.
|
|
87
|
-
"""
|
|
88
|
-
return data.to_bytes().decode(encoding)
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2025 Dexmate Inc.
|
|
2
|
-
#
|
|
3
|
-
# This software is dual-licensed:
|
|
4
|
-
#
|
|
5
|
-
# 1. GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
6
|
-
# See LICENSE-AGPL for details
|
|
7
|
-
#
|
|
8
|
-
# 2. Commercial License
|
|
9
|
-
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
|
-
|
|
11
|
-
"""Generic Zenoh subscriber with configurable decoder functions.
|
|
12
|
-
|
|
13
|
-
This module provides a flexible subscriber that can handle any type of data
|
|
14
|
-
by accepting decoder functions that transform raw Zenoh bytes into the desired format.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from typing import Any
|
|
18
|
-
|
|
19
|
-
import zenoh
|
|
20
|
-
from loguru import logger
|
|
21
|
-
|
|
22
|
-
from .base import BaseZenohSubscriber, CustomDataHandler
|
|
23
|
-
from .decoders import DecoderFunction
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class GenericZenohSubscriber(BaseZenohSubscriber):
|
|
27
|
-
"""Generic Zenoh subscriber with configurable decoder function.
|
|
28
|
-
|
|
29
|
-
This subscriber can handle any type of data by accepting a decoder function
|
|
30
|
-
that transforms the raw Zenoh bytes into the desired data format.
|
|
31
|
-
Uses lazy decoding - data is only decoded when requested.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
def __init__(
|
|
35
|
-
self,
|
|
36
|
-
topic: str,
|
|
37
|
-
zenoh_session: zenoh.Session,
|
|
38
|
-
decoder: DecoderFunction | None = None,
|
|
39
|
-
name: str = "generic_subscriber",
|
|
40
|
-
enable_fps_tracking: bool = False,
|
|
41
|
-
fps_log_interval: int = 100,
|
|
42
|
-
custom_data_handler: CustomDataHandler | None = None,
|
|
43
|
-
) -> None:
|
|
44
|
-
"""Initialize the generic Zenoh subscriber.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
topic: Zenoh topic to subscribe to for data.
|
|
48
|
-
zenoh_session: Active Zenoh session for communication.
|
|
49
|
-
decoder: Optional function to decode raw bytes into desired format.
|
|
50
|
-
If None, raw bytes are returned.
|
|
51
|
-
name: Name for logging purposes.
|
|
52
|
-
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
53
|
-
fps_log_interval: Number of frames between FPS calculations.
|
|
54
|
-
custom_data_handler: Optional custom function to handle incoming data.
|
|
55
|
-
If provided, this will replace the default data
|
|
56
|
-
handling logic entirely.
|
|
57
|
-
"""
|
|
58
|
-
super().__init__(
|
|
59
|
-
topic,
|
|
60
|
-
zenoh_session,
|
|
61
|
-
name,
|
|
62
|
-
enable_fps_tracking,
|
|
63
|
-
fps_log_interval,
|
|
64
|
-
custom_data_handler,
|
|
65
|
-
)
|
|
66
|
-
self._decoder = decoder
|
|
67
|
-
self._latest_raw_data: zenoh.ZBytes = zenoh.ZBytes("")
|
|
68
|
-
|
|
69
|
-
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
70
|
-
"""Handle incoming data.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
sample: Zenoh sample containing data.
|
|
74
|
-
"""
|
|
75
|
-
with self._data_lock:
|
|
76
|
-
self._latest_raw_data = sample.payload
|
|
77
|
-
self._active = True
|
|
78
|
-
|
|
79
|
-
self._update_fps_metrics()
|
|
80
|
-
|
|
81
|
-
def get_latest_data(self) -> Any | None:
|
|
82
|
-
"""Get the latest data.
|
|
83
|
-
|
|
84
|
-
Returns:
|
|
85
|
-
Latest decoded data if decoder is provided and decoding succeeded,
|
|
86
|
-
otherwise raw bytes. None if no data received.
|
|
87
|
-
"""
|
|
88
|
-
with self._data_lock:
|
|
89
|
-
if not self._active:
|
|
90
|
-
return None
|
|
91
|
-
|
|
92
|
-
if self._decoder is not None:
|
|
93
|
-
try:
|
|
94
|
-
return self._decoder(self._latest_raw_data)
|
|
95
|
-
except Exception as e:
|
|
96
|
-
logger.error(f"Failed to decode data for {self._name}: {e}")
|
|
97
|
-
return None
|
|
98
|
-
else:
|
|
99
|
-
return self._latest_raw_data.to_bytes()
|
|
100
|
-
|
|
101
|
-
def get_latest_raw_data(self) -> bytes | None:
|
|
102
|
-
"""Get the latest raw data bytes.
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
Latest raw data bytes if available, None otherwise.
|
|
106
|
-
"""
|
|
107
|
-
with self._data_lock:
|
|
108
|
-
if not self._active:
|
|
109
|
-
return None
|
|
110
|
-
return self._latest_raw_data.to_bytes()
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
# Copyright (C) 2025 Dexmate Inc.
|
|
2
|
-
#
|
|
3
|
-
# This software is dual-licensed:
|
|
4
|
-
#
|
|
5
|
-
# 1. GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
6
|
-
# See LICENSE-AGPL for details
|
|
7
|
-
#
|
|
8
|
-
# 2. Commercial License
|
|
9
|
-
# For commercial licensing terms, contact: contact@dexmate.ai
|
|
10
|
-
|
|
11
|
-
"""IMU Zenoh subscriber for inertial measurement data.
|
|
12
|
-
|
|
13
|
-
This module provides a specialized subscriber for IMU data,
|
|
14
|
-
using the serialization format from dexsensor.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from typing import Any
|
|
18
|
-
|
|
19
|
-
import numpy as np
|
|
20
|
-
import zenoh
|
|
21
|
-
from loguru import logger
|
|
22
|
-
|
|
23
|
-
from .base import BaseZenohSubscriber, CustomDataHandler
|
|
24
|
-
|
|
25
|
-
# Import IMU serialization functions from dexsensor
|
|
26
|
-
try:
|
|
27
|
-
from dexsensor.serialization.imu import decode_imu_data
|
|
28
|
-
except ImportError:
|
|
29
|
-
logger.error(
|
|
30
|
-
"Failed to import dexsensor IMU serialization functions. Please install dexsensor."
|
|
31
|
-
)
|
|
32
|
-
decode_imu_data = None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class IMUSubscriber(BaseZenohSubscriber):
|
|
36
|
-
"""Zenoh subscriber for IMU data.
|
|
37
|
-
|
|
38
|
-
This subscriber handles IMU data encoded using the dexsensor
|
|
39
|
-
IMU serialization format with compression.
|
|
40
|
-
Uses lazy decoding - data is only decoded when requested.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
topic: str,
|
|
46
|
-
zenoh_session: zenoh.Session,
|
|
47
|
-
name: str = "imu_subscriber",
|
|
48
|
-
enable_fps_tracking: bool = True,
|
|
49
|
-
fps_log_interval: int = 50,
|
|
50
|
-
custom_data_handler: CustomDataHandler | None = None,
|
|
51
|
-
) -> None:
|
|
52
|
-
"""Initialize the IMU subscriber.
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
topic: Zenoh topic to subscribe to for IMU data.
|
|
56
|
-
zenoh_session: Active Zenoh session for communication.
|
|
57
|
-
name: Name for logging purposes.
|
|
58
|
-
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
59
|
-
fps_log_interval: Number of frames between FPS calculations.
|
|
60
|
-
custom_data_handler: Optional custom function to handle incoming data.
|
|
61
|
-
If provided, this will replace the default data
|
|
62
|
-
handling logic entirely.
|
|
63
|
-
"""
|
|
64
|
-
super().__init__(
|
|
65
|
-
topic,
|
|
66
|
-
zenoh_session,
|
|
67
|
-
name,
|
|
68
|
-
enable_fps_tracking,
|
|
69
|
-
fps_log_interval,
|
|
70
|
-
custom_data_handler,
|
|
71
|
-
)
|
|
72
|
-
self._latest_raw_data: bytes | None = None
|
|
73
|
-
|
|
74
|
-
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
75
|
-
"""Handle incoming IMU data.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
sample: Zenoh sample containing encoded IMU data.
|
|
79
|
-
"""
|
|
80
|
-
with self._data_lock:
|
|
81
|
-
self._latest_raw_data = sample.payload.to_bytes()
|
|
82
|
-
self._active = True
|
|
83
|
-
|
|
84
|
-
self._update_fps_metrics()
|
|
85
|
-
|
|
86
|
-
def get_latest_data(self) -> dict[str, Any] | None:
|
|
87
|
-
"""Get the latest IMU data.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
Latest IMU data dictionary if available, None otherwise.
|
|
91
|
-
Dictionary contains:
|
|
92
|
-
- acceleration: Linear acceleration [x, y, z] in m/s²
|
|
93
|
-
- angular_velocity: Angular velocity [x, y, z] in rad/s
|
|
94
|
-
- orientation: Orientation quaternion [x, y, z, w]
|
|
95
|
-
- magnetometer: Magnetic field [x, y, z] in µT (if available)
|
|
96
|
-
- timestamp: Timestamp of the measurement
|
|
97
|
-
"""
|
|
98
|
-
with self._data_lock:
|
|
99
|
-
if self._latest_raw_data is None:
|
|
100
|
-
return None
|
|
101
|
-
|
|
102
|
-
if decode_imu_data is None:
|
|
103
|
-
logger.error(
|
|
104
|
-
f"Cannot decode IMU data for {self._name}: dexsensor not available"
|
|
105
|
-
)
|
|
106
|
-
return None
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
# Decode the IMU data
|
|
110
|
-
imu_data = decode_imu_data(self._latest_raw_data)
|
|
111
|
-
# Return a copy to avoid external modifications
|
|
112
|
-
return {
|
|
113
|
-
key: value.copy() if isinstance(value, np.ndarray) else value
|
|
114
|
-
for key, value in imu_data.items()
|
|
115
|
-
}
|
|
116
|
-
except Exception as e:
|
|
117
|
-
logger.error(f"Failed to decode IMU data for {self._name}: {e}")
|
|
118
|
-
return None
|
|
119
|
-
|
|
120
|
-
def get_acceleration(self) -> np.ndarray | None:
|
|
121
|
-
"""Get the latest linear acceleration.
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
Linear acceleration [x, y, z] in m/s² if available, None otherwise.
|
|
125
|
-
"""
|
|
126
|
-
imu_data = self.get_latest_data()
|
|
127
|
-
if imu_data is not None:
|
|
128
|
-
return imu_data["acceleration"]
|
|
129
|
-
return None
|
|
130
|
-
|
|
131
|
-
def get_angular_velocity(self) -> np.ndarray | None:
|
|
132
|
-
"""Get the latest angular velocity.
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
Angular velocity [x, y, z] in rad/s if available, None otherwise.
|
|
136
|
-
"""
|
|
137
|
-
imu_data = self.get_latest_data()
|
|
138
|
-
if imu_data is not None:
|
|
139
|
-
return imu_data["angular_velocity"]
|
|
140
|
-
return None
|
|
141
|
-
|
|
142
|
-
def get_orientation(self) -> np.ndarray | None:
|
|
143
|
-
"""Get the latest orientation quaternion.
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
Orientation quaternion [x, y, z, w] if available, None otherwise.
|
|
147
|
-
"""
|
|
148
|
-
imu_data = self.get_latest_data()
|
|
149
|
-
if imu_data is not None:
|
|
150
|
-
return imu_data["orientation"]
|
|
151
|
-
return None
|
|
152
|
-
|
|
153
|
-
def get_magnetometer(self) -> np.ndarray | None:
|
|
154
|
-
"""Get the latest magnetometer reading.
|
|
155
|
-
|
|
156
|
-
Returns:
|
|
157
|
-
Magnetic field [x, y, z] in µT if available, None otherwise.
|
|
158
|
-
"""
|
|
159
|
-
imu_data = self.get_latest_data()
|
|
160
|
-
if imu_data is not None and "magnetometer" in imu_data:
|
|
161
|
-
magnetometer = imu_data["magnetometer"]
|
|
162
|
-
return magnetometer if magnetometer is not None else None
|
|
163
|
-
return None
|
|
164
|
-
|
|
165
|
-
def has_magnetometer(self) -> bool:
|
|
166
|
-
"""Check if the latest IMU data includes magnetometer information.
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
True if magnetometer data is available, False otherwise.
|
|
170
|
-
"""
|
|
171
|
-
imu_data = self.get_latest_data()
|
|
172
|
-
if imu_data is not None:
|
|
173
|
-
magnetometer = imu_data.get("magnetometer")
|
|
174
|
-
return magnetometer is not None and len(magnetometer) > 0
|
|
175
|
-
return False
|