dexcontrol 0.3.0__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.

Files changed (51) hide show
  1. dexcontrol/__init__.py +16 -7
  2. dexcontrol/apps/dualsense_teleop_base.py +1 -1
  3. dexcontrol/comm/__init__.py +51 -0
  4. dexcontrol/comm/base.py +421 -0
  5. dexcontrol/comm/rtc.py +400 -0
  6. dexcontrol/comm/subscribers.py +329 -0
  7. dexcontrol/config/sensors/cameras/__init__.py +1 -2
  8. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  9. dexcontrol/config/sensors/vega_sensors.py +12 -18
  10. dexcontrol/core/arm.py +29 -25
  11. dexcontrol/core/chassis.py +3 -12
  12. dexcontrol/core/component.py +68 -43
  13. dexcontrol/core/hand.py +50 -52
  14. dexcontrol/core/head.py +14 -26
  15. dexcontrol/core/misc.py +188 -166
  16. dexcontrol/core/robot_query_interface.py +137 -114
  17. dexcontrol/core/torso.py +0 -4
  18. dexcontrol/robot.py +15 -37
  19. dexcontrol/sensors/__init__.py +1 -2
  20. dexcontrol/sensors/camera/__init__.py +0 -2
  21. dexcontrol/sensors/camera/base_camera.py +144 -0
  22. dexcontrol/sensors/camera/rgb_camera.py +67 -63
  23. dexcontrol/sensors/camera/zed_camera.py +89 -147
  24. dexcontrol/sensors/imu/chassis_imu.py +76 -56
  25. dexcontrol/sensors/imu/zed_imu.py +54 -43
  26. dexcontrol/sensors/lidar/rplidar.py +16 -20
  27. dexcontrol/sensors/manager.py +4 -11
  28. dexcontrol/sensors/ultrasonic.py +14 -27
  29. dexcontrol/utils/__init__.py +0 -11
  30. dexcontrol/utils/comm_helper.py +111 -0
  31. dexcontrol/utils/constants.py +1 -1
  32. dexcontrol/utils/os_utils.py +8 -22
  33. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.dist-info}/METADATA +2 -1
  34. dexcontrol-0.3.1.dist-info/RECORD +68 -0
  35. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  36. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  37. dexcontrol/utils/rate_limiter.py +0 -172
  38. dexcontrol/utils/rtc_utils.py +0 -144
  39. dexcontrol/utils/subscribers/__init__.py +0 -52
  40. dexcontrol/utils/subscribers/base.py +0 -281
  41. dexcontrol/utils/subscribers/camera.py +0 -332
  42. dexcontrol/utils/subscribers/decoders.py +0 -88
  43. dexcontrol/utils/subscribers/generic.py +0 -110
  44. dexcontrol/utils/subscribers/imu.py +0 -175
  45. dexcontrol/utils/subscribers/lidar.py +0 -172
  46. dexcontrol/utils/subscribers/protobuf.py +0 -111
  47. dexcontrol/utils/subscribers/rtc.py +0 -316
  48. dexcontrol/utils/zenoh_utils.py +0 -369
  49. dexcontrol-0.3.0.dist-info/RECORD +0 -76
  50. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.dist-info}/WHEEL +0 -0
  51. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.1.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)