dexcontrol 0.2.1__py3-none-any.whl → 0.2.3__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 (81) hide show
  1. dexcontrol/__init__.py +14 -3
  2. dexcontrol/apps/dualsense_teleop_base.py +16 -11
  3. dexcontrol/config/__init__.py +10 -5
  4. dexcontrol/config/core/__init__.py +8 -3
  5. dexcontrol/config/core/arm.py +8 -3
  6. dexcontrol/config/core/chassis.py +10 -5
  7. dexcontrol/config/core/hand.py +14 -9
  8. dexcontrol/config/core/head.py +8 -3
  9. dexcontrol/config/core/misc.py +8 -3
  10. dexcontrol/config/core/torso.py +8 -3
  11. dexcontrol/config/sensors/__init__.py +8 -3
  12. dexcontrol/config/sensors/cameras/__init__.py +9 -4
  13. dexcontrol/config/sensors/cameras/rgb_camera.py +18 -5
  14. dexcontrol/config/sensors/cameras/zed_camera.py +36 -0
  15. dexcontrol/config/sensors/imu/__init__.py +10 -5
  16. dexcontrol/config/sensors/imu/chassis_imu.py +21 -0
  17. dexcontrol/config/sensors/imu/zed_imu.py +21 -0
  18. dexcontrol/config/sensors/lidar/__init__.py +8 -3
  19. dexcontrol/config/sensors/lidar/rplidar.py +9 -3
  20. dexcontrol/config/sensors/ultrasonic/__init__.py +8 -3
  21. dexcontrol/config/sensors/ultrasonic/ultrasonic.py +9 -3
  22. dexcontrol/config/sensors/vega_sensors.py +34 -21
  23. dexcontrol/config/vega.py +14 -6
  24. dexcontrol/core/__init__.py +9 -0
  25. dexcontrol/core/arm.py +21 -6
  26. dexcontrol/core/chassis.py +8 -3
  27. dexcontrol/core/component.py +26 -6
  28. dexcontrol/core/hand.py +8 -3
  29. dexcontrol/core/head.py +18 -3
  30. dexcontrol/core/misc.py +94 -16
  31. dexcontrol/core/torso.py +8 -3
  32. dexcontrol/proto/dexcontrol_msg_pb2.py +17 -15
  33. dexcontrol/proto/dexcontrol_msg_pb2.pyi +24 -0
  34. dexcontrol/robot.py +82 -28
  35. dexcontrol/sensors/__init__.py +13 -8
  36. dexcontrol/sensors/camera/__init__.py +11 -6
  37. dexcontrol/sensors/camera/rgb_camera.py +33 -24
  38. dexcontrol/sensors/camera/zed_camera.py +364 -0
  39. dexcontrol/sensors/imu/__init__.py +13 -8
  40. dexcontrol/sensors/imu/chassis_imu.py +155 -0
  41. dexcontrol/sensors/imu/{nine_axis_imu.py → zed_imu.py} +41 -26
  42. dexcontrol/sensors/lidar/__init__.py +11 -1
  43. dexcontrol/sensors/lidar/rplidar.py +8 -3
  44. dexcontrol/sensors/manager.py +22 -9
  45. dexcontrol/sensors/ultrasonic.py +8 -3
  46. dexcontrol/utils/__init__.py +8 -3
  47. dexcontrol/utils/constants.py +10 -0
  48. dexcontrol/utils/io_utils.py +8 -3
  49. dexcontrol/utils/motion_utils.py +8 -3
  50. dexcontrol/utils/os_utils.py +23 -4
  51. dexcontrol/utils/pb_utils.py +8 -3
  52. dexcontrol/utils/rate_limiter.py +8 -3
  53. dexcontrol/utils/rtc_utils.py +144 -0
  54. dexcontrol/utils/subscribers/__init__.py +11 -3
  55. dexcontrol/utils/subscribers/base.py +26 -5
  56. dexcontrol/utils/subscribers/camera.py +10 -6
  57. dexcontrol/utils/subscribers/decoders.py +8 -3
  58. dexcontrol/utils/subscribers/generic.py +8 -3
  59. dexcontrol/utils/subscribers/imu.py +8 -3
  60. dexcontrol/utils/subscribers/lidar.py +8 -3
  61. dexcontrol/utils/subscribers/protobuf.py +8 -3
  62. dexcontrol/utils/subscribers/rtc.py +315 -0
  63. dexcontrol/utils/timer.py +8 -3
  64. dexcontrol/utils/trajectory_utils.py +8 -3
  65. dexcontrol/utils/viz_utils.py +8 -3
  66. dexcontrol/utils/zenoh_utils.py +83 -0
  67. dexcontrol-0.2.3.dist-info/METADATA +265 -0
  68. dexcontrol-0.2.3.dist-info/RECORD +72 -0
  69. {dexcontrol-0.2.1.dist-info → dexcontrol-0.2.3.dist-info}/WHEEL +1 -2
  70. dexcontrol-0.2.3.dist-info/licenses/LICENSE +184 -0
  71. dexcontrol/config/sensors/cameras/gemini_camera.py +0 -16
  72. dexcontrol/config/sensors/imu/gemini_imu.py +0 -15
  73. dexcontrol/config/sensors/imu/nine_axis_imu.py +0 -15
  74. dexcontrol/sensors/camera/gemini_camera.py +0 -139
  75. dexcontrol/sensors/imu/gemini_imu.py +0 -139
  76. dexcontrol/utils/reset_orbbec_camera_usb.py +0 -98
  77. dexcontrol-0.2.1.dist-info/METADATA +0 -369
  78. dexcontrol-0.2.1.dist-info/RECORD +0 -72
  79. dexcontrol-0.2.1.dist-info/licenses/LICENSE +0 -188
  80. dexcontrol-0.2.1.dist-info/licenses/NOTICE +0 -13
  81. dexcontrol-0.2.1.dist-info/top_level.txt +0 -1
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """Camera Zenoh subscribers for RGB and depth data.
7
12
 
@@ -167,8 +172,7 @@ class DepthCameraSubscriber(BaseZenohSubscriber):
167
172
  """Get the latest depth data.
168
173
 
169
174
  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)
175
+ depth_image if available, None otherwise. Depth values in meters as numpy array (HxW)
172
176
  """
173
177
  with self._data_lock:
174
178
  if self._latest_raw_data is None:
@@ -182,7 +186,7 @@ class DepthCameraSubscriber(BaseZenohSubscriber):
182
186
 
183
187
  try:
184
188
  # Decode the depth image
185
- depth, depth_min, depth_max = decode_depth(self._latest_raw_data)
189
+ depth = decode_depth(self._latest_raw_data)
186
190
  return depth
187
191
  except Exception as e:
188
192
  logger.error(f"Failed to decode depth image for {self._name}: {e}")
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """Decoder functions for Zenoh subscribers.
7
12
 
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """Generic Zenoh subscriber with configurable decoder functions.
7
12
 
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """IMU Zenoh subscriber for inertial measurement data.
7
12
 
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """LIDAR Zenoh subscriber for scan data.
7
12
 
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """Protobuf-specific Zenoh subscriber.
7
12
 
@@ -0,0 +1,315 @@
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
+ import asyncio
12
+ import json
13
+ import threading
14
+ import time
15
+
16
+ import numpy as np
17
+ import websockets
18
+ from aiortc import RTCPeerConnection, RTCSessionDescription
19
+ from loguru import logger
20
+
21
+ # Try to import uvloop for better performance
22
+ try:
23
+ import uvloop # type: ignore[import-untyped]
24
+
25
+ UVLOOP_AVAILABLE = True
26
+ except ImportError:
27
+ uvloop = None # type: ignore[assignment]
28
+ UVLOOP_AVAILABLE = False
29
+
30
+
31
+ class RTCSubscriber:
32
+ """
33
+ Subscriber for receiving video data via RTC.
34
+
35
+ This class connects to a RTC peer through a signaling server,
36
+ receives a video stream, and makes the latest frame available.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ url: str,
42
+ name: str = "rtc_subscriber",
43
+ enable_fps_tracking: bool = True,
44
+ fps_log_interval: int = 100,
45
+ ):
46
+ """
47
+ Initialize the RTC subscriber.
48
+
49
+ Args:
50
+ url: WebSocket URL of the signaling server.
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
+ """
55
+ self._url = url
56
+ self._name = name
57
+ self._pc = RTCPeerConnection()
58
+ self._latest_frame: np.ndarray | None = None
59
+ self._active = False
60
+ self._data_lock = threading.Lock()
61
+ self._stop_event = (
62
+ threading.Event()
63
+ ) # Use threading.Event for cross-thread communication
64
+ self._async_stop_event = None # Will be created in the async context
65
+ self._websocket = None # Store websocket reference for clean shutdown
66
+
67
+ # FPS tracking
68
+ self._enable_fps_tracking = enable_fps_tracking
69
+ self._fps_log_interval = fps_log_interval
70
+ self._frame_count = 0
71
+ self._fps = 0.0
72
+ self._last_fps_time = time.time()
73
+
74
+ self._thread = threading.Thread(target=self._run_event_loop, daemon=True)
75
+ self._thread.start()
76
+
77
+ def _run_event_loop(self):
78
+ """Run the asyncio event loop in a separate thread."""
79
+ try:
80
+ # Use uvloop if available for better performance
81
+ if UVLOOP_AVAILABLE and uvloop is not None:
82
+ # Create a new uvloop event loop for this thread
83
+ loop = uvloop.new_event_loop()
84
+ asyncio.set_event_loop(loop)
85
+ logger.debug(f"Using uvloop for {self._name}")
86
+
87
+ try:
88
+ loop.run_until_complete(self._run())
89
+ finally:
90
+ loop.close()
91
+ else:
92
+ # Use default asyncio event loop
93
+ asyncio.run(self._run())
94
+
95
+ except Exception as e:
96
+ logger.error(f"Event loop error for {self._name}: {e}")
97
+ finally:
98
+ with self._data_lock:
99
+ self._active = False
100
+
101
+ async def _run(self):
102
+ """
103
+ Connects to a RTC peer, receives video, and saves frames to disk.
104
+ """
105
+ # Create async stop event in the async context
106
+ self._async_stop_event = asyncio.Event()
107
+
108
+ # Start a task to monitor the threading stop event
109
+ monitor_task = asyncio.create_task(self._monitor_stop_event())
110
+
111
+ @self._pc.on("track")
112
+ async def on_track(track):
113
+ if track.kind == "video":
114
+ while (
115
+ self._async_stop_event is not None
116
+ and not self._async_stop_event.is_set()
117
+ ):
118
+ try:
119
+ frame = await asyncio.wait_for(
120
+ track.recv(), timeout=1.0
121
+ ) # Reduced timeout for faster shutdown response
122
+ img = frame.to_ndarray(format="rgb24")
123
+ with self._data_lock:
124
+ self._latest_frame = img
125
+ if not self._active:
126
+ self._active = True
127
+ self._update_fps_metrics()
128
+ except asyncio.TimeoutError:
129
+ # Check if we should stop before logging error
130
+ if (
131
+ self._async_stop_event is not None
132
+ and not self._async_stop_event.is_set()
133
+ ):
134
+ logger.warning(
135
+ f"Timeout: No frame received in 1 second from {self._url}"
136
+ )
137
+ continue
138
+ except Exception as e:
139
+ if (
140
+ self._async_stop_event is not None
141
+ and not self._async_stop_event.is_set()
142
+ ):
143
+ logger.error(f"Error receiving frame from {self._url}: {e}")
144
+ break
145
+
146
+ @self._pc.on("connectionstatechange")
147
+ async def on_connectionstatechange():
148
+ if self._pc.connectionState == "failed":
149
+ logger.warning(f"RTC connection failed for {self._url}")
150
+ await self._pc.close()
151
+ if self._async_stop_event is not None:
152
+ self._async_stop_event.set()
153
+
154
+ try:
155
+ async with websockets.connect(self._url) as websocket:
156
+ self._websocket = websocket
157
+
158
+ # Create an offer
159
+ self._pc.addTransceiver("video", direction="recvonly")
160
+ offer = await self._pc.createOffer()
161
+ await self._pc.setLocalDescription(offer)
162
+
163
+ # Send the offer to the server
164
+ await websocket.send(
165
+ json.dumps(
166
+ {
167
+ "sdp": self._pc.localDescription.sdp,
168
+ "type": self._pc.localDescription.type,
169
+ }
170
+ )
171
+ )
172
+
173
+ # Wait for the answer
174
+ response = json.loads(await websocket.recv())
175
+ if response["type"] == "answer":
176
+ await self._pc.setRemoteDescription(
177
+ RTCSessionDescription(
178
+ sdp=response["sdp"], type=response["type"]
179
+ )
180
+ )
181
+ else:
182
+ logger.error(
183
+ f"Received unexpected message type: {response['type']} from {self._url}"
184
+ )
185
+ if self._async_stop_event is not None:
186
+ self._async_stop_event.set()
187
+
188
+ # Wait until the stop event is set
189
+ if self._async_stop_event is not None:
190
+ await self._async_stop_event.wait()
191
+
192
+ except websockets.exceptions.ConnectionClosed:
193
+ logger.info(f"WebSocket connection closed for {self._url}")
194
+ except Exception as e:
195
+ if not self._async_stop_event.is_set():
196
+ logger.error(f"Operation failed for {self._url}: {e}")
197
+ finally:
198
+ # Cancel the monitor task
199
+ monitor_task.cancel()
200
+ try:
201
+ await monitor_task
202
+ except asyncio.CancelledError:
203
+ pass
204
+
205
+ # Close websocket if still open
206
+ if self._websocket:
207
+ try:
208
+ await self._websocket.close()
209
+ except Exception as e:
210
+ logger.debug(f"Error closing websocket for {self._url}: {e}")
211
+
212
+ # Close peer connection if not already closed
213
+ if self._pc.connectionState != "closed":
214
+ try:
215
+ await self._pc.close()
216
+ except Exception as e:
217
+ logger.debug(f"Error closing peer connection for {self._url}: {e}")
218
+
219
+ with self._data_lock:
220
+ self._active = False
221
+
222
+ async def _monitor_stop_event(self):
223
+ """Monitor the threading stop event and set the async stop event when needed."""
224
+ while not self._stop_event.is_set():
225
+ await asyncio.sleep(0.1) # Check every 100ms
226
+ if self._async_stop_event is not None:
227
+ self._async_stop_event.set()
228
+
229
+ def _update_fps_metrics(self) -> None:
230
+ """Update FPS tracking metrics.
231
+
232
+ Increments frame counter and recalculates FPS at specified intervals.
233
+ Only has an effect if fps_tracking was enabled during initialization.
234
+ """
235
+ if not self._enable_fps_tracking:
236
+ return
237
+
238
+ self._frame_count += 1
239
+ if self._frame_count >= self._fps_log_interval:
240
+ current_time = time.time()
241
+ elapsed = current_time - self._last_fps_time
242
+ self._fps = self._frame_count / elapsed
243
+ logger.info(f"{self._name} frequency: {self._fps:.2f} Hz")
244
+ self._frame_count = 0
245
+ self._last_fps_time = current_time
246
+
247
+ def get_latest_data(self) -> np.ndarray | None:
248
+ """
249
+ Get the latest video frame.
250
+
251
+ Returns:
252
+ Latest video frame as a numpy array (HxWxC RGB) if available, None otherwise.
253
+ """
254
+ with self._data_lock:
255
+ return self._latest_frame.copy() if self._latest_frame is not None else None
256
+
257
+ def is_active(self) -> bool:
258
+ """Check if the subscriber is actively receiving data."""
259
+ with self._data_lock:
260
+ return self._active
261
+
262
+ def wait_for_active(self, timeout: float = 5.0) -> bool:
263
+ """
264
+ Wait for the subscriber to start receiving data.
265
+
266
+ Args:
267
+ timeout: Maximum time to wait in seconds.
268
+
269
+ Returns:
270
+ True if subscriber becomes active, False if timeout is reached.
271
+ """
272
+ start_time = time.time()
273
+ while not self.is_active():
274
+ if time.time() - start_time > timeout:
275
+ logger.error(
276
+ f"No data received from {self._name} at {self._url} after {timeout}s"
277
+ )
278
+ return False
279
+ time.sleep(0.1)
280
+ return True
281
+
282
+ def shutdown(self):
283
+ """Stop the subscriber and release resources."""
284
+
285
+ # Signal the async loop to stop
286
+ self._stop_event.set()
287
+
288
+ # Wait for the thread to finish with a reasonable timeout
289
+ if self._thread.is_alive():
290
+ self._thread.join(
291
+ timeout=10.0
292
+ ) # Increased timeout for more graceful shutdown
293
+
294
+ if self._thread.is_alive():
295
+ logger.warning(
296
+ f"{self._name} thread did not shut down gracefully within timeout."
297
+ )
298
+
299
+ # Ensure active state is set to False
300
+ with self._data_lock:
301
+ self._active = False
302
+
303
+ @property
304
+ def name(self) -> str:
305
+ """Get the subscriber name."""
306
+ return self._name
307
+
308
+ @property
309
+ def fps(self) -> float:
310
+ """Get the current FPS measurement.
311
+
312
+ Returns:
313
+ Current frames per second measurement.
314
+ """
315
+ return self._fps
dexcontrol/utils/timer.py CHANGED
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """Utility for measuring code execution time."""
7
12
 
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """Trajectory utility functions for smooth motion generation."""
7
12
 
@@ -1,7 +1,12 @@
1
- # Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
1
+ # Copyright (C) 2025 Dexmate Inc.
2
2
  #
3
- # Licensed under the Apache License, Version 2.0 with Commons Clause License
4
- # Condition v1.0 [see LICENSE for details].
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
5
10
 
6
11
  """Utility functions for displaying information in a Rich table format."""
7
12
 
@@ -0,0 +1,83 @@
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
+ """Zenoh utilities for dexcontrol.
12
+
13
+ This module provides general utility functions for working with Zenoh
14
+ communication framework.
15
+ """
16
+
17
+ import json
18
+ import time
19
+
20
+ import zenoh
21
+ from loguru import logger
22
+
23
+ from dexcontrol.utils.os_utils import resolve_key_name
24
+
25
+
26
+ def query_zenoh_json(
27
+ zenoh_session: zenoh.Session,
28
+ topic: str,
29
+ timeout: float = 2.0,
30
+ max_retries: int = 1,
31
+ retry_delay: float = 0.5,
32
+ ) -> dict | None:
33
+ """Query Zenoh for JSON information with retry logic.
34
+
35
+ Args:
36
+ zenoh_session: Active Zenoh session for communication.
37
+ topic: Zenoh topic to query.
38
+ timeout: Maximum time to wait for a response in seconds.
39
+ max_retries: Maximum number of retry attempts.
40
+ retry_delay: Initial delay between retries (doubles each retry).
41
+
42
+ Returns:
43
+ Dictionary containing the parsed JSON response if successful, None otherwise.
44
+ """
45
+ resolved_topic = resolve_key_name(topic)
46
+ logger.debug(f"Querying Zenoh topic: {resolved_topic}")
47
+
48
+ for attempt in range(max_retries + 1):
49
+ try:
50
+ # Add delay before retry (except first attempt)
51
+ if attempt > 0:
52
+ delay = retry_delay * (2 ** (attempt - 1)) # Exponential backoff
53
+ logger.debug(f"Retry {attempt}/{max_retries} after {delay}s delay...")
54
+ time.sleep(delay)
55
+
56
+ # Try to get the info
57
+ for reply in zenoh_session.get(resolved_topic, timeout=timeout):
58
+ if reply.ok:
59
+ response = json.loads(reply.ok.payload.to_bytes())
60
+ return response
61
+ else:
62
+ # No valid reply received
63
+ if attempt < max_retries:
64
+ logger.debug(f"No reply on attempt {attempt + 1}, will retry...")
65
+ else:
66
+ logger.error(
67
+ f"No valid reply received on topic '{resolved_topic}' after {max_retries + 1} attempts."
68
+ )
69
+
70
+ except StopIteration:
71
+ if attempt < max_retries:
72
+ logger.debug(f"Query timed out on attempt {attempt + 1}, will retry...")
73
+ else:
74
+ logger.error(f"Query timed out after {max_retries + 1} attempts.")
75
+ except Exception as e:
76
+ if attempt < max_retries:
77
+ logger.debug(
78
+ f"Query failed on attempt {attempt + 1}: {e}, will retry..."
79
+ )
80
+ else:
81
+ logger.error(f"Query failed after {max_retries + 1} attempts: {e}")
82
+
83
+ return None