dexcontrol 0.2.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.

Files changed (72) hide show
  1. dexcontrol/__init__.py +45 -0
  2. dexcontrol/apps/dualsense_teleop_base.py +371 -0
  3. dexcontrol/config/__init__.py +14 -0
  4. dexcontrol/config/core/__init__.py +22 -0
  5. dexcontrol/config/core/arm.py +32 -0
  6. dexcontrol/config/core/chassis.py +22 -0
  7. dexcontrol/config/core/hand.py +42 -0
  8. dexcontrol/config/core/head.py +33 -0
  9. dexcontrol/config/core/misc.py +37 -0
  10. dexcontrol/config/core/torso.py +36 -0
  11. dexcontrol/config/sensors/__init__.py +4 -0
  12. dexcontrol/config/sensors/cameras/__init__.py +7 -0
  13. dexcontrol/config/sensors/cameras/gemini_camera.py +16 -0
  14. dexcontrol/config/sensors/cameras/rgb_camera.py +15 -0
  15. dexcontrol/config/sensors/imu/__init__.py +6 -0
  16. dexcontrol/config/sensors/imu/gemini_imu.py +15 -0
  17. dexcontrol/config/sensors/imu/nine_axis_imu.py +15 -0
  18. dexcontrol/config/sensors/lidar/__init__.py +6 -0
  19. dexcontrol/config/sensors/lidar/rplidar.py +15 -0
  20. dexcontrol/config/sensors/ultrasonic/__init__.py +6 -0
  21. dexcontrol/config/sensors/ultrasonic/ultrasonic.py +15 -0
  22. dexcontrol/config/sensors/vega_sensors.py +65 -0
  23. dexcontrol/config/vega.py +203 -0
  24. dexcontrol/core/__init__.py +0 -0
  25. dexcontrol/core/arm.py +324 -0
  26. dexcontrol/core/chassis.py +628 -0
  27. dexcontrol/core/component.py +834 -0
  28. dexcontrol/core/hand.py +170 -0
  29. dexcontrol/core/head.py +232 -0
  30. dexcontrol/core/misc.py +514 -0
  31. dexcontrol/core/torso.py +198 -0
  32. dexcontrol/proto/dexcontrol_msg_pb2.py +69 -0
  33. dexcontrol/proto/dexcontrol_msg_pb2.pyi +168 -0
  34. dexcontrol/proto/dexcontrol_query_pb2.py +73 -0
  35. dexcontrol/proto/dexcontrol_query_pb2.pyi +134 -0
  36. dexcontrol/robot.py +1091 -0
  37. dexcontrol/sensors/__init__.py +40 -0
  38. dexcontrol/sensors/camera/__init__.py +18 -0
  39. dexcontrol/sensors/camera/gemini_camera.py +139 -0
  40. dexcontrol/sensors/camera/rgb_camera.py +98 -0
  41. dexcontrol/sensors/imu/__init__.py +22 -0
  42. dexcontrol/sensors/imu/gemini_imu.py +139 -0
  43. dexcontrol/sensors/imu/nine_axis_imu.py +149 -0
  44. dexcontrol/sensors/lidar/__init__.py +3 -0
  45. dexcontrol/sensors/lidar/rplidar.py +164 -0
  46. dexcontrol/sensors/manager.py +185 -0
  47. dexcontrol/sensors/ultrasonic.py +110 -0
  48. dexcontrol/utils/__init__.py +15 -0
  49. dexcontrol/utils/constants.py +12 -0
  50. dexcontrol/utils/io_utils.py +26 -0
  51. dexcontrol/utils/motion_utils.py +194 -0
  52. dexcontrol/utils/os_utils.py +39 -0
  53. dexcontrol/utils/pb_utils.py +103 -0
  54. dexcontrol/utils/rate_limiter.py +167 -0
  55. dexcontrol/utils/reset_orbbec_camera_usb.py +98 -0
  56. dexcontrol/utils/subscribers/__init__.py +44 -0
  57. dexcontrol/utils/subscribers/base.py +260 -0
  58. dexcontrol/utils/subscribers/camera.py +328 -0
  59. dexcontrol/utils/subscribers/decoders.py +83 -0
  60. dexcontrol/utils/subscribers/generic.py +105 -0
  61. dexcontrol/utils/subscribers/imu.py +170 -0
  62. dexcontrol/utils/subscribers/lidar.py +195 -0
  63. dexcontrol/utils/subscribers/protobuf.py +106 -0
  64. dexcontrol/utils/timer.py +136 -0
  65. dexcontrol/utils/trajectory_utils.py +40 -0
  66. dexcontrol/utils/viz_utils.py +86 -0
  67. dexcontrol-0.2.1.dist-info/METADATA +369 -0
  68. dexcontrol-0.2.1.dist-info/RECORD +72 -0
  69. dexcontrol-0.2.1.dist-info/WHEEL +5 -0
  70. dexcontrol-0.2.1.dist-info/licenses/LICENSE +188 -0
  71. dexcontrol-0.2.1.dist-info/licenses/NOTICE +13 -0
  72. dexcontrol-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,260 @@
1
+ # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
+ # Condition v1.0 [see LICENSE for details].
5
+
6
+ """Base Zenoh subscriber utilities.
7
+
8
+ This module provides the abstract base class for all Zenoh subscribers
9
+ and common utilities used across different subscriber implementations.
10
+ """
11
+
12
+ import threading
13
+ import time
14
+ from abc import ABC, abstractmethod
15
+ from collections.abc import Callable
16
+ from typing import Any, Final, TypeVar
17
+
18
+ import zenoh
19
+ from google.protobuf.message import Message
20
+ from loguru import logger
21
+
22
+ from dexcontrol.utils.os_utils import resolve_key_name
23
+
24
+ # Type variable for Message subclasses
25
+ M = TypeVar("M", bound=Message)
26
+
27
+ # Type alias for custom data handler functions
28
+ CustomDataHandler = Callable[[zenoh.Sample], None]
29
+
30
+
31
+ class BaseZenohSubscriber(ABC):
32
+ """Base class for Zenoh subscribers with configurable data handling.
33
+
34
+ This class provides a common interface for subscribing to Zenoh topics
35
+ and processing incoming data through configurable decoder functions.
36
+ It handles the Zenoh communication setup and provides thread-safe access
37
+ to the latest data.
38
+
39
+ Attributes:
40
+ _active: Whether subscriber is receiving data updates.
41
+ _data_lock: Lock for thread-safe data access.
42
+ _zenoh_session: Active Zenoh session for communication.
43
+ _subscriber: Zenoh subscriber for data.
44
+ _topic: The resolved Zenoh topic name.
45
+ _enable_fps_tracking: Whether to track and log FPS metrics.
46
+ _frame_count: Counter for frames processed since last FPS calculation.
47
+ _fps: Most recently calculated frames per second value.
48
+ _last_fps_time: Timestamp of last FPS calculation.
49
+ _fps_log_interval: Number of frames between FPS calculations.
50
+ _name: Name for logging purposes.
51
+ _custom_data_handler: Optional custom data handler function.
52
+ _last_data_time: Timestamp of last data received.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ topic: str,
58
+ zenoh_session: zenoh.Session,
59
+ name: str = "subscriber",
60
+ enable_fps_tracking: bool = False,
61
+ fps_log_interval: int = 100,
62
+ custom_data_handler: CustomDataHandler | None = None,
63
+ ) -> None:
64
+ """Initialize the base Zenoh subscriber.
65
+
66
+ Args:
67
+ topic: Zenoh topic to subscribe to for data.
68
+ zenoh_session: Active Zenoh session for communication.
69
+ name: Name for logging purposes.
70
+ enable_fps_tracking: Whether to track and log FPS metrics.
71
+ fps_log_interval: Number of frames between FPS calculations.
72
+ custom_data_handler: Optional custom function to handle incoming data.
73
+ If provided, this will replace the default data
74
+ handling logic entirely.
75
+ """
76
+ self._active: bool = False
77
+ self._data_lock: Final[threading.RLock] = threading.RLock()
78
+ self._zenoh_session: Final[zenoh.Session] = zenoh_session
79
+ self._name = name
80
+ self._custom_data_handler = custom_data_handler
81
+
82
+ # Data freshness tracking
83
+ self._last_data_time: float | None = None
84
+
85
+ # FPS tracking
86
+ self._enable_fps_tracking = enable_fps_tracking
87
+ self._fps_log_interval = fps_log_interval
88
+ self._frame_count = 0
89
+ self._fps = 0.0
90
+ self._last_fps_time = time.time()
91
+
92
+ # Setup Zenoh subscriber
93
+ self._topic: Final[str] = resolve_key_name(topic)
94
+ self._subscriber: Final[zenoh.Subscriber] = zenoh_session.declare_subscriber(
95
+ self._topic, self._data_handler_wrapper
96
+ )
97
+
98
+ logger.info(f"Initialized {self._name} subscriber for topic: {self._topic}")
99
+
100
+ def _data_handler_wrapper(self, sample: zenoh.Sample) -> None:
101
+ """Wrapper for data handling that calls either custom or default handler.
102
+
103
+ Args:
104
+ sample: Zenoh sample containing data.
105
+ """
106
+ # Update data freshness timestamp
107
+ with self._data_lock:
108
+ self._last_data_time = time.monotonic()
109
+
110
+ # Call custom data handler if provided, otherwise call default handler
111
+ if self._custom_data_handler is not None:
112
+ try:
113
+ self._custom_data_handler(sample)
114
+ except Exception as e:
115
+ logger.error(f"Custom data handler failed for {self._name}: {e}")
116
+ else:
117
+ # Call the default data handler
118
+ self._data_handler(sample)
119
+
120
+ @abstractmethod
121
+ def _data_handler(self, sample: zenoh.Sample) -> None:
122
+ """Handle incoming data.
123
+
124
+ This method must be implemented by subclasses to process the specific
125
+ type of data they handle.
126
+
127
+ Args:
128
+ sample: Zenoh sample containing data.
129
+ """
130
+ pass
131
+
132
+ @abstractmethod
133
+ def get_latest_data(self) -> Any | None:
134
+ """Get the latest data.
135
+
136
+ Returns:
137
+ Latest data if available, None otherwise.
138
+ """
139
+ pass
140
+
141
+ def _update_fps_metrics(self) -> None:
142
+ """Update FPS tracking metrics.
143
+
144
+ Increments frame counter and recalculates FPS at specified intervals.
145
+ Only has an effect if fps_tracking was enabled during initialization.
146
+ """
147
+ if not self._enable_fps_tracking:
148
+ return
149
+
150
+ self._frame_count += 1
151
+ if self._frame_count >= self._fps_log_interval:
152
+ current_time = time.time()
153
+ elapsed = current_time - self._last_fps_time
154
+ self._fps = self._frame_count / elapsed
155
+ logger.info(f"{self._name} {self._topic} frequency: {self._fps:.2f} Hz")
156
+ self._frame_count = 0
157
+ self._last_fps_time = current_time
158
+
159
+ def wait_for_active(self, timeout: float = 5.0) -> bool:
160
+ """Wait for the subscriber to start receiving data.
161
+
162
+ Args:
163
+ timeout: Maximum time to wait in seconds.
164
+
165
+ Returns:
166
+ True if subscriber becomes active, False if timeout is reached.
167
+ """
168
+ start_time = time.monotonic()
169
+ check_interval = min(0.05, timeout / 10) # Check every 10ms or less
170
+
171
+ while True:
172
+ with self._data_lock:
173
+ if self._active:
174
+ return True
175
+
176
+ elapsed = time.monotonic() - start_time
177
+ if elapsed >= timeout:
178
+ logger.error(f"No data received from {self._topic} after {timeout}s")
179
+ return False
180
+
181
+ # Sleep for the shorter of: remaining time or check interval
182
+ sleep_time = min(check_interval, timeout - elapsed)
183
+ time.sleep(sleep_time)
184
+
185
+ def is_active(self) -> bool:
186
+ """Check if the subscriber is actively receiving data.
187
+
188
+ Returns:
189
+ True if subscriber is active, False otherwise.
190
+ """
191
+ with self._data_lock:
192
+ return self._active
193
+
194
+ def shutdown(self) -> None:
195
+ """Stop the subscriber and release resources."""
196
+ if hasattr(self, "_subscriber") and self._subscriber:
197
+ self._subscriber.undeclare()
198
+
199
+ @property
200
+ def topic(self) -> str:
201
+ """Get the Zenoh topic name.
202
+
203
+ Returns:
204
+ The resolved Zenoh topic name.
205
+ """
206
+ return self._topic
207
+
208
+ @property
209
+ def fps(self) -> float:
210
+ """Get the current FPS measurement.
211
+
212
+ Returns:
213
+ Current frames per second measurement.
214
+ """
215
+ return self._fps
216
+
217
+ @property
218
+ def name(self) -> str:
219
+ """Get the subscriber name.
220
+
221
+ Returns:
222
+ The subscriber name.
223
+ """
224
+ return self._name
225
+
226
+ def is_data_fresh(self, max_age_seconds: float) -> bool:
227
+ """Check if the most recent data is fresh (received within the specified time).
228
+
229
+ This method checks if data has been received within the specified time window,
230
+ regardless of whether the data content has changed. This is useful for
231
+ detecting communication failures or stale data streams.
232
+
233
+ Args:
234
+ max_age_seconds: Maximum age in seconds for data to be considered fresh.
235
+
236
+ Returns:
237
+ True if fresh data was received within the time window, False otherwise.
238
+ Returns False if no data has ever been received.
239
+ """
240
+ with self._data_lock:
241
+ if self._last_data_time is None:
242
+ return False
243
+
244
+ current_time = time.monotonic()
245
+ age = current_time - self._last_data_time
246
+ return age <= max_age_seconds
247
+
248
+ def get_time_since_last_data(self) -> float | None:
249
+ """Get the time elapsed since the last data was received.
250
+
251
+ Returns:
252
+ Time in seconds since last data was received, or None if no data
253
+ has ever been received.
254
+ """
255
+ with self._data_lock:
256
+ if self._last_data_time is None:
257
+ return None
258
+
259
+ current_time = time.monotonic()
260
+ return current_time - self._last_data_time
@@ -0,0 +1,328 @@
1
+ # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
+ # Condition v1.0 [see LICENSE for details].
5
+
6
+ """Camera Zenoh subscribers for RGB and depth data.
7
+
8
+ This module provides specialized subscribers for camera data including RGB images
9
+ and depth images, using the serialization formats from dexsensor.
10
+ """
11
+
12
+ import numpy as np
13
+ import zenoh
14
+ from loguru import logger
15
+
16
+ from .base import BaseZenohSubscriber, CustomDataHandler
17
+
18
+ # Import camera serialization functions from dexsensor
19
+ try:
20
+ from dexsensor.serialization.camera import decode_depth, decode_image
21
+ except ImportError:
22
+ logger.error(
23
+ "Failed to import dexsensor camera serialization functions. Please install dexsensor."
24
+ )
25
+ decode_image = None
26
+ decode_depth = None
27
+
28
+
29
+ class RGBCameraSubscriber(BaseZenohSubscriber):
30
+ """Zenoh subscriber for RGB camera data.
31
+
32
+ This subscriber handles RGB image data encoded using the dexsensor
33
+ camera serialization format with JPEG compression.
34
+ Uses lazy decoding - data is only decoded when requested.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ topic: str,
40
+ zenoh_session: zenoh.Session,
41
+ name: str = "rgb_camera_subscriber",
42
+ enable_fps_tracking: bool = True,
43
+ fps_log_interval: int = 30,
44
+ custom_data_handler: CustomDataHandler | None = None,
45
+ ) -> None:
46
+ """Initialize the RGB camera subscriber.
47
+
48
+ Args:
49
+ topic: Zenoh topic to subscribe to for RGB data.
50
+ zenoh_session: Active Zenoh session for communication.
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._latest_raw_data: bytes | None = None
67
+
68
+ def _data_handler(self, sample: zenoh.Sample) -> None:
69
+ """Handle incoming RGB image data.
70
+
71
+ Args:
72
+ sample: Zenoh sample containing encoded RGB image data.
73
+ """
74
+ with self._data_lock:
75
+ self._latest_raw_data = sample.payload.to_bytes()
76
+ self._active = True
77
+
78
+ self._update_fps_metrics()
79
+
80
+ def get_latest_data(self) -> np.ndarray | None:
81
+ """Get the latest RGB image.
82
+
83
+ Returns:
84
+ Latest RGB image as numpy array (HxWxC) if available, None otherwise.
85
+ """
86
+ with self._data_lock:
87
+ if self._latest_raw_data is None:
88
+ return None
89
+
90
+ if decode_image is None:
91
+ logger.error(
92
+ f"Cannot decode RGB image for {self._name}: dexsensor not available"
93
+ )
94
+ return None
95
+
96
+ try:
97
+ # Decode the RGB image
98
+ image = decode_image(self._latest_raw_data)
99
+ return image
100
+ except Exception as e:
101
+ logger.error(f"Failed to decode RGB image for {self._name}: {e}")
102
+ return None
103
+
104
+ def get_latest_image(self) -> np.ndarray | None:
105
+ """Get the latest RGB image.
106
+
107
+ Alias for get_latest_data() for clarity.
108
+
109
+ Returns:
110
+ Latest RGB image as numpy array (HxWxC) if available, None otherwise.
111
+ """
112
+ return self.get_latest_data()
113
+
114
+
115
+ class DepthCameraSubscriber(BaseZenohSubscriber):
116
+ """Zenoh subscriber for depth camera data.
117
+
118
+ This subscriber handles depth image data encoded using the dexsensor
119
+ camera serialization format with compression.
120
+ Uses lazy decoding - data is only decoded when requested.
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ topic: str,
126
+ zenoh_session: zenoh.Session,
127
+ name: str = "depth_camera_subscriber",
128
+ enable_fps_tracking: bool = True,
129
+ fps_log_interval: int = 30,
130
+ custom_data_handler: CustomDataHandler | None = None,
131
+ ) -> None:
132
+ """Initialize the depth camera subscriber.
133
+
134
+ Args:
135
+ topic: Zenoh topic to subscribe to for depth data.
136
+ zenoh_session: Active Zenoh session for communication.
137
+ name: Name for logging purposes.
138
+ enable_fps_tracking: Whether to track and log FPS metrics.
139
+ fps_log_interval: Number of frames between FPS calculations.
140
+ custom_data_handler: Optional custom function to handle incoming data.
141
+ If provided, this will replace the default data
142
+ handling logic entirely.
143
+ """
144
+ super().__init__(
145
+ topic,
146
+ zenoh_session,
147
+ name,
148
+ enable_fps_tracking,
149
+ fps_log_interval,
150
+ custom_data_handler,
151
+ )
152
+ self._latest_raw_data: bytes | None = None
153
+
154
+ def _data_handler(self, sample: zenoh.Sample) -> None:
155
+ """Handle incoming depth image data.
156
+
157
+ Args:
158
+ sample: Zenoh sample containing encoded depth image data.
159
+ """
160
+ with self._data_lock:
161
+ self._latest_raw_data = sample.payload.to_bytes()
162
+ self._active = True
163
+
164
+ self._update_fps_metrics()
165
+
166
+ def get_latest_data(self) -> np.ndarray | None:
167
+ """Get the latest depth data.
168
+
169
+ Returns:
170
+ Tuple of (depth_image, depth_min, depth_max) if available, None otherwise.
171
+ depth_image: Depth values in meters as numpy array (HxW)
172
+ """
173
+ with self._data_lock:
174
+ if self._latest_raw_data is None:
175
+ return None
176
+
177
+ if decode_depth is None:
178
+ logger.error(
179
+ f"Cannot decode depth image for {self._name}: dexsensor not available"
180
+ )
181
+ return None
182
+
183
+ try:
184
+ # Decode the depth image
185
+ depth, depth_min, depth_max = decode_depth(self._latest_raw_data)
186
+ return depth
187
+ except Exception as e:
188
+ logger.error(f"Failed to decode depth image for {self._name}: {e}")
189
+ return None
190
+
191
+
192
+ class RGBDCameraSubscriber(BaseZenohSubscriber):
193
+ """Zenoh subscriber for RGBD camera data.
194
+
195
+ This subscriber handles both RGB and depth data from an RGBD camera,
196
+ subscribing to separate topics for RGB and depth streams.
197
+ """
198
+
199
+ def __init__(
200
+ self,
201
+ rgb_topic: str,
202
+ depth_topic: str,
203
+ zenoh_session: zenoh.Session,
204
+ name: str = "rgbd_camera_subscriber",
205
+ enable_fps_tracking: bool = True,
206
+ fps_log_interval: int = 30,
207
+ custom_data_handler: CustomDataHandler | None = None,
208
+ ) -> None:
209
+ """Initialize the RGBD camera subscriber.
210
+
211
+ Args:
212
+ rgb_topic: Zenoh topic for RGB data.
213
+ depth_topic: Zenoh topic for depth data.
214
+ zenoh_session: Active Zenoh session for communication.
215
+ name: Name for logging purposes.
216
+ enable_fps_tracking: Whether to track and log FPS metrics.
217
+ fps_log_interval: Number of frames between FPS calculations.
218
+ custom_data_handler: Optional custom function to handle incoming data.
219
+ If provided, this will replace the default data
220
+ handling logic entirely.
221
+ """
222
+ # Initialize with RGB topic as primary
223
+ super().__init__(
224
+ rgb_topic,
225
+ zenoh_session,
226
+ name,
227
+ enable_fps_tracking,
228
+ fps_log_interval,
229
+ custom_data_handler,
230
+ )
231
+
232
+ # Create separate subscribers for RGB and depth
233
+ self._rgb_subscriber = RGBCameraSubscriber(
234
+ rgb_topic,
235
+ zenoh_session,
236
+ f"{name}_rgb",
237
+ enable_fps_tracking,
238
+ fps_log_interval,
239
+ custom_data_handler,
240
+ )
241
+ self._depth_subscriber = DepthCameraSubscriber(
242
+ depth_topic,
243
+ zenoh_session,
244
+ f"{name}_depth",
245
+ enable_fps_tracking,
246
+ fps_log_interval,
247
+ custom_data_handler,
248
+ )
249
+
250
+ def _data_handler(self, sample: zenoh.Sample) -> None:
251
+ """Handle incoming data - not used as we use separate subscribers."""
252
+ pass
253
+
254
+ def get_latest_data(self) -> tuple[np.ndarray, np.ndarray] | None:
255
+ """Get the latest RGBD data.
256
+
257
+ Returns:
258
+ Tuple of (rgb_image, depth_image, depth_min, depth_max) if both available, None otherwise.
259
+ rgb_image: RGB image as numpy array (HxWxC)
260
+ depth_image: Depth values in meters as numpy array (HxW)
261
+ """
262
+ rgb_image = self._rgb_subscriber.get_latest_data()
263
+ depth_image = self._depth_subscriber.get_latest_data()
264
+
265
+ if rgb_image is not None and depth_image is not None:
266
+ return rgb_image, depth_image
267
+ return None
268
+
269
+ def get_latest_rgb(self) -> np.ndarray | None:
270
+ """Get the latest RGB image.
271
+
272
+ Returns:
273
+ Latest RGB image as numpy array (HxWxC) if available, None otherwise.
274
+ """
275
+ return self._rgb_subscriber.get_latest_image()
276
+
277
+ def get_latest_depth(self) -> np.ndarray | None:
278
+ """Get the latest depth image.
279
+
280
+ Returns:
281
+ Latest depth image as numpy array (HxW) with values in meters if available, None otherwise.
282
+ """
283
+ return self._depth_subscriber.get_latest_data()
284
+
285
+ def wait_for_active(self, timeout: float = 5.0) -> bool:
286
+ """Wait for both RGB and depth subscribers to start receiving data.
287
+
288
+ Args:
289
+ timeout: Maximum time to wait in seconds.
290
+
291
+ Returns:
292
+ True if both subscribers become active, False if timeout is reached.
293
+ """
294
+ rgb_active = self._rgb_subscriber.wait_for_active(timeout)
295
+ depth_active = self._depth_subscriber.wait_for_active(timeout)
296
+ return rgb_active and depth_active
297
+
298
+ def is_active(self) -> bool:
299
+ """Check if both RGB and depth subscribers are actively receiving data.
300
+
301
+ Returns:
302
+ True if both subscribers are active, False otherwise.
303
+ """
304
+ return self._rgb_subscriber.is_active() and self._depth_subscriber.is_active()
305
+
306
+ def shutdown(self) -> None:
307
+ """Stop both subscribers and release resources."""
308
+ self._rgb_subscriber.shutdown()
309
+ self._depth_subscriber.shutdown()
310
+ super().shutdown()
311
+
312
+ @property
313
+ def rgb_fps(self) -> float:
314
+ """Get the RGB stream FPS measurement.
315
+
316
+ Returns:
317
+ Current RGB frames per second measurement.
318
+ """
319
+ return self._rgb_subscriber.fps
320
+
321
+ @property
322
+ def depth_fps(self) -> float:
323
+ """Get the depth stream FPS measurement.
324
+
325
+ Returns:
326
+ Current depth frames per second measurement.
327
+ """
328
+ return self._depth_subscriber.fps
@@ -0,0 +1,83 @@
1
+ # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
+ # Condition v1.0 [see LICENSE for details].
5
+
6
+ """Decoder functions for Zenoh subscribers.
7
+
8
+ This module provides common decoder functions that can be used with the
9
+ GenericZenohSubscriber to transform raw Zenoh bytes into specific data formats.
10
+ """
11
+
12
+ import json
13
+ from collections.abc import Callable
14
+ from typing import Any, Type, TypeVar
15
+
16
+ import zenoh
17
+ from google.protobuf.message import Message
18
+
19
+ M = TypeVar("M", bound=Message)
20
+
21
+ # Type alias for decoder functions
22
+ DecoderFunction = Callable[[zenoh.ZBytes], Any]
23
+
24
+
25
+ def protobuf_decoder(message_type: Type[M]) -> DecoderFunction:
26
+ """Create a decoder function for a specific protobuf message type.
27
+
28
+ Args:
29
+ message_type: Protobuf message class to decode to.
30
+
31
+ Returns:
32
+ Decoder function that parses bytes into the specified protobuf message.
33
+ """
34
+
35
+ def decode(data: zenoh.ZBytes) -> M:
36
+ message = message_type()
37
+ message.ParseFromString(data.to_bytes())
38
+ return message
39
+
40
+ return decode
41
+
42
+
43
+ def raw_bytes_decoder(data: zenoh.ZBytes) -> bytes:
44
+ """Decoder that returns raw bytes.
45
+
46
+ Args:
47
+ data: Zenoh bytes data.
48
+
49
+ Returns:
50
+ Raw bytes data.
51
+ """
52
+ return data.to_bytes()
53
+
54
+
55
+ def json_decoder(data: zenoh.ZBytes) -> Any:
56
+ """Decoder that parses JSON from bytes.
57
+
58
+ Args:
59
+ data: Zenoh bytes data containing JSON.
60
+
61
+ Returns:
62
+ Parsed JSON data.
63
+
64
+ Raises:
65
+ json.JSONDecodeError: If the data is not valid JSON.
66
+ """
67
+ return json.loads(data.to_bytes().decode("utf-8"))
68
+
69
+
70
+ def string_decoder(data: zenoh.ZBytes, encoding: str = "utf-8") -> str:
71
+ """Decoder that converts bytes to string.
72
+
73
+ Args:
74
+ data: Zenoh bytes data.
75
+ encoding: Text encoding to use for decoding.
76
+
77
+ Returns:
78
+ Decoded string.
79
+
80
+ Raises:
81
+ UnicodeDecodeError: If the data cannot be decoded with the specified encoding.
82
+ """
83
+ return data.to_bytes().decode(encoding)