dexcontrol 0.3.0__py3-none-any.whl → 0.3.2__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 +16 -7
- 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/sensors/cameras/__init__.py +1 -2
- dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
- dexcontrol/config/sensors/vega_sensors.py +12 -18
- dexcontrol/core/arm.py +29 -25
- dexcontrol/core/chassis.py +3 -12
- dexcontrol/core/component.py +68 -43
- dexcontrol/core/hand.py +50 -52
- dexcontrol/core/head.py +14 -26
- dexcontrol/core/misc.py +188 -166
- dexcontrol/core/robot_query_interface.py +140 -117
- dexcontrol/core/torso.py +0 -4
- dexcontrol/robot.py +15 -37
- 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 +8 -22
- {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/METADATA +2 -1
- dexcontrol-0.3.2.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 -369
- dexcontrol-0.3.0.dist-info/RECORD +0 -76
- {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/WHEEL +0 -0
- {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,281 +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
|
-
"""Base Zenoh subscriber utilities.
|
|
12
|
-
|
|
13
|
-
This module provides the abstract base class for all Zenoh subscribers
|
|
14
|
-
and common utilities used across different subscriber implementations.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
import threading
|
|
18
|
-
import time
|
|
19
|
-
from abc import ABC, abstractmethod
|
|
20
|
-
from collections.abc import Callable
|
|
21
|
-
from typing import Any, Final, TypeVar
|
|
22
|
-
|
|
23
|
-
import zenoh
|
|
24
|
-
from google.protobuf.message import Message
|
|
25
|
-
from loguru import logger
|
|
26
|
-
|
|
27
|
-
from dexcontrol.utils.os_utils import resolve_key_name
|
|
28
|
-
|
|
29
|
-
# Type variable for Message subclasses
|
|
30
|
-
M = TypeVar("M", bound=Message)
|
|
31
|
-
|
|
32
|
-
# Type alias for custom data handler functions
|
|
33
|
-
CustomDataHandler = Callable[[zenoh.Sample], None]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class BaseZenohSubscriber(ABC):
|
|
37
|
-
"""Base class for Zenoh subscribers with configurable data handling.
|
|
38
|
-
|
|
39
|
-
This class provides a common interface for subscribing to Zenoh topics
|
|
40
|
-
and processing incoming data through configurable decoder functions.
|
|
41
|
-
It handles the Zenoh communication setup and provides thread-safe access
|
|
42
|
-
to the latest data.
|
|
43
|
-
|
|
44
|
-
Attributes:
|
|
45
|
-
_active: Whether subscriber is receiving data updates.
|
|
46
|
-
_data_lock: Lock for thread-safe data access.
|
|
47
|
-
_zenoh_session: Active Zenoh session for communication.
|
|
48
|
-
_subscriber: Zenoh subscriber for data.
|
|
49
|
-
_topic: The resolved Zenoh topic name.
|
|
50
|
-
_enable_fps_tracking: Whether to track and log FPS metrics.
|
|
51
|
-
_frame_count: Counter for frames processed since last FPS calculation.
|
|
52
|
-
_fps: Most recently calculated frames per second value.
|
|
53
|
-
_last_fps_time: Timestamp of last FPS calculation.
|
|
54
|
-
_fps_log_interval: Number of frames between FPS calculations.
|
|
55
|
-
_name: Name for logging purposes.
|
|
56
|
-
_custom_data_handler: Optional custom data handler function.
|
|
57
|
-
_last_data_time: Timestamp of last data received.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
def __init__(
|
|
61
|
-
self,
|
|
62
|
-
topic: str,
|
|
63
|
-
zenoh_session: zenoh.Session,
|
|
64
|
-
name: str = "subscriber",
|
|
65
|
-
enable_fps_tracking: bool = False,
|
|
66
|
-
fps_log_interval: int = 100,
|
|
67
|
-
custom_data_handler: CustomDataHandler | None = None,
|
|
68
|
-
) -> None:
|
|
69
|
-
"""Initialize the base Zenoh subscriber.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
topic: Zenoh topic to subscribe to for data.
|
|
73
|
-
zenoh_session: Active Zenoh session for communication.
|
|
74
|
-
name: Name for logging purposes.
|
|
75
|
-
enable_fps_tracking: Whether to track and log FPS metrics.
|
|
76
|
-
fps_log_interval: Number of frames between FPS calculations.
|
|
77
|
-
custom_data_handler: Optional custom function to handle incoming data.
|
|
78
|
-
If provided, this will replace the default data
|
|
79
|
-
handling logic entirely.
|
|
80
|
-
"""
|
|
81
|
-
self._active: bool = False
|
|
82
|
-
self._data_lock: Final[threading.RLock] = threading.RLock()
|
|
83
|
-
self._zenoh_session: Final[zenoh.Session] = zenoh_session
|
|
84
|
-
self._name = name
|
|
85
|
-
self._custom_data_handler = custom_data_handler
|
|
86
|
-
|
|
87
|
-
# Data freshness tracking
|
|
88
|
-
self._last_data_time: float | None = None
|
|
89
|
-
|
|
90
|
-
# FPS tracking
|
|
91
|
-
self._enable_fps_tracking = enable_fps_tracking
|
|
92
|
-
self._fps_log_interval = fps_log_interval
|
|
93
|
-
self._frame_count = 0
|
|
94
|
-
self._fps = 0.0
|
|
95
|
-
self._last_fps_time = time.time()
|
|
96
|
-
|
|
97
|
-
# Setup Zenoh subscriber
|
|
98
|
-
self._topic: Final[str] = resolve_key_name(topic)
|
|
99
|
-
self._subscriber: Final[zenoh.Subscriber] = zenoh_session.declare_subscriber(
|
|
100
|
-
self._topic, self._data_handler_wrapper
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
logger.info(f"Initialized {self._name} subscriber for topic: {self._topic}")
|
|
104
|
-
|
|
105
|
-
def _data_handler_wrapper(self, sample: zenoh.Sample) -> None:
|
|
106
|
-
"""Wrapper for data handling that calls either custom or default handler.
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
sample: Zenoh sample containing data.
|
|
110
|
-
"""
|
|
111
|
-
# Update data freshness timestamp
|
|
112
|
-
with self._data_lock:
|
|
113
|
-
self._last_data_time = time.monotonic()
|
|
114
|
-
|
|
115
|
-
# Call custom data handler if provided, otherwise call default handler
|
|
116
|
-
if self._custom_data_handler is not None:
|
|
117
|
-
try:
|
|
118
|
-
self._custom_data_handler(sample)
|
|
119
|
-
except Exception as e:
|
|
120
|
-
logger.error(f"Custom data handler failed for {self._name}: {e}")
|
|
121
|
-
else:
|
|
122
|
-
# Call the default data handler
|
|
123
|
-
self._data_handler(sample)
|
|
124
|
-
|
|
125
|
-
@abstractmethod
|
|
126
|
-
def _data_handler(self, sample: zenoh.Sample) -> None:
|
|
127
|
-
"""Handle incoming data.
|
|
128
|
-
|
|
129
|
-
This method must be implemented by subclasses to process the specific
|
|
130
|
-
type of data they handle.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
sample: Zenoh sample containing data.
|
|
134
|
-
"""
|
|
135
|
-
pass
|
|
136
|
-
|
|
137
|
-
@abstractmethod
|
|
138
|
-
def get_latest_data(self) -> Any | None:
|
|
139
|
-
"""Get the latest data.
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
Latest data if available, None otherwise.
|
|
143
|
-
"""
|
|
144
|
-
pass
|
|
145
|
-
|
|
146
|
-
def _update_fps_metrics(self) -> None:
|
|
147
|
-
"""Update FPS tracking metrics.
|
|
148
|
-
|
|
149
|
-
Increments frame counter and recalculates FPS at specified intervals.
|
|
150
|
-
Only has an effect if fps_tracking was enabled during initialization.
|
|
151
|
-
"""
|
|
152
|
-
if not self._enable_fps_tracking:
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
self._frame_count += 1
|
|
156
|
-
if self._frame_count >= self._fps_log_interval:
|
|
157
|
-
current_time = time.time()
|
|
158
|
-
elapsed = current_time - self._last_fps_time
|
|
159
|
-
self._fps = self._frame_count / elapsed
|
|
160
|
-
logger.info(f"{self._name} {self._topic} frequency: {self._fps:.2f} Hz")
|
|
161
|
-
self._frame_count = 0
|
|
162
|
-
self._last_fps_time = current_time
|
|
163
|
-
|
|
164
|
-
def wait_for_active(self, timeout: float = 5.0) -> bool:
|
|
165
|
-
"""Wait for the subscriber to start receiving data.
|
|
166
|
-
|
|
167
|
-
Args:
|
|
168
|
-
timeout: Maximum time to wait in seconds.
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
True if subscriber becomes active, False if timeout is reached.
|
|
172
|
-
"""
|
|
173
|
-
start_time = time.monotonic()
|
|
174
|
-
check_interval = min(0.05, timeout / 10) # Check every 10ms or less
|
|
175
|
-
|
|
176
|
-
while True:
|
|
177
|
-
with self._data_lock:
|
|
178
|
-
if self._active:
|
|
179
|
-
return True
|
|
180
|
-
|
|
181
|
-
elapsed = time.monotonic() - start_time
|
|
182
|
-
if elapsed >= timeout:
|
|
183
|
-
logger.error(f"No data received from {self._topic} after {timeout}s")
|
|
184
|
-
return False
|
|
185
|
-
|
|
186
|
-
# Sleep for the shorter of: remaining time or check interval
|
|
187
|
-
sleep_time = min(check_interval, timeout - elapsed)
|
|
188
|
-
time.sleep(sleep_time)
|
|
189
|
-
|
|
190
|
-
def is_active(self) -> bool:
|
|
191
|
-
"""Check if the subscriber is actively receiving data.
|
|
192
|
-
|
|
193
|
-
Returns:
|
|
194
|
-
True if subscriber is active, False otherwise.
|
|
195
|
-
"""
|
|
196
|
-
with self._data_lock:
|
|
197
|
-
return self._active
|
|
198
|
-
|
|
199
|
-
def shutdown(self) -> None:
|
|
200
|
-
"""Stop the subscriber and release resources."""
|
|
201
|
-
# Mark as inactive first to prevent further data processing
|
|
202
|
-
with self._data_lock:
|
|
203
|
-
self._active = False
|
|
204
|
-
|
|
205
|
-
# Small delay to allow any ongoing data processing to complete
|
|
206
|
-
time.sleep(0.05)
|
|
207
|
-
|
|
208
|
-
try:
|
|
209
|
-
if hasattr(self, "_subscriber") and self._subscriber:
|
|
210
|
-
self._subscriber.undeclare()
|
|
211
|
-
except Exception as e:
|
|
212
|
-
# Don't log "Undeclared subscriber" errors as warnings - they're expected during shutdown
|
|
213
|
-
error_msg = str(e).lower()
|
|
214
|
-
if not ("undeclared" in error_msg or "closed" in error_msg):
|
|
215
|
-
logger.warning(f"Error undeclaring subscriber for {self._topic}: {e}")
|
|
216
|
-
|
|
217
|
-
# Additional delay to allow Zenoh to process the undeclare operation
|
|
218
|
-
time.sleep(0.02)
|
|
219
|
-
|
|
220
|
-
@property
|
|
221
|
-
def topic(self) -> str:
|
|
222
|
-
"""Get the Zenoh topic name.
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
The resolved Zenoh topic name.
|
|
226
|
-
"""
|
|
227
|
-
return self._topic
|
|
228
|
-
|
|
229
|
-
@property
|
|
230
|
-
def fps(self) -> float:
|
|
231
|
-
"""Get the current FPS measurement.
|
|
232
|
-
|
|
233
|
-
Returns:
|
|
234
|
-
Current frames per second measurement.
|
|
235
|
-
"""
|
|
236
|
-
return self._fps
|
|
237
|
-
|
|
238
|
-
@property
|
|
239
|
-
def name(self) -> str:
|
|
240
|
-
"""Get the subscriber name.
|
|
241
|
-
|
|
242
|
-
Returns:
|
|
243
|
-
The subscriber name.
|
|
244
|
-
"""
|
|
245
|
-
return self._name
|
|
246
|
-
|
|
247
|
-
def is_data_fresh(self, max_age_seconds: float) -> bool:
|
|
248
|
-
"""Check if the most recent data is fresh (received within the specified time).
|
|
249
|
-
|
|
250
|
-
This method checks if data has been received within the specified time window,
|
|
251
|
-
regardless of whether the data content has changed. This is useful for
|
|
252
|
-
detecting communication failures or stale data streams.
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
max_age_seconds: Maximum age in seconds for data to be considered fresh.
|
|
256
|
-
|
|
257
|
-
Returns:
|
|
258
|
-
True if fresh data was received within the time window, False otherwise.
|
|
259
|
-
Returns False if no data has ever been received.
|
|
260
|
-
"""
|
|
261
|
-
with self._data_lock:
|
|
262
|
-
if self._last_data_time is None:
|
|
263
|
-
return False
|
|
264
|
-
|
|
265
|
-
current_time = time.monotonic()
|
|
266
|
-
age = current_time - self._last_data_time
|
|
267
|
-
return age <= max_age_seconds
|
|
268
|
-
|
|
269
|
-
def get_time_since_last_data(self) -> float | None:
|
|
270
|
-
"""Get the time elapsed since the last data was received.
|
|
271
|
-
|
|
272
|
-
Returns:
|
|
273
|
-
Time in seconds since last data was received, or None if no data
|
|
274
|
-
has ever been received.
|
|
275
|
-
"""
|
|
276
|
-
with self._data_lock:
|
|
277
|
-
if self._last_data_time is None:
|
|
278
|
-
return None
|
|
279
|
-
|
|
280
|
-
current_time = time.monotonic()
|
|
281
|
-
return current_time - self._last_data_time
|
|
@@ -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)
|