dexcontrol 0.2.12__py3-none-any.whl → 0.3.4__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.
Files changed (60) hide show
  1. dexcontrol/__init__.py +17 -8
  2. dexcontrol/apps/dualsense_teleop_base.py +1 -1
  3. dexcontrol/comm/__init__.py +51 -0
  4. dexcontrol/comm/rtc.py +401 -0
  5. dexcontrol/comm/subscribers.py +329 -0
  6. dexcontrol/config/core/chassis.py +9 -4
  7. dexcontrol/config/core/hand.py +1 -0
  8. dexcontrol/config/sensors/cameras/__init__.py +1 -2
  9. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  10. dexcontrol/config/sensors/vega_sensors.py +12 -18
  11. dexcontrol/config/vega.py +4 -1
  12. dexcontrol/core/arm.py +66 -42
  13. dexcontrol/core/chassis.py +142 -120
  14. dexcontrol/core/component.py +107 -58
  15. dexcontrol/core/hand.py +119 -86
  16. dexcontrol/core/head.py +22 -33
  17. dexcontrol/core/misc.py +331 -158
  18. dexcontrol/core/robot_query_interface.py +467 -0
  19. dexcontrol/core/torso.py +5 -9
  20. dexcontrol/robot.py +245 -574
  21. dexcontrol/sensors/__init__.py +1 -2
  22. dexcontrol/sensors/camera/__init__.py +0 -2
  23. dexcontrol/sensors/camera/base_camera.py +150 -0
  24. dexcontrol/sensors/camera/rgb_camera.py +68 -64
  25. dexcontrol/sensors/camera/zed_camera.py +140 -164
  26. dexcontrol/sensors/imu/chassis_imu.py +81 -62
  27. dexcontrol/sensors/imu/zed_imu.py +54 -43
  28. dexcontrol/sensors/lidar/rplidar.py +16 -20
  29. dexcontrol/sensors/manager.py +4 -14
  30. dexcontrol/sensors/ultrasonic.py +15 -28
  31. dexcontrol/utils/__init__.py +0 -11
  32. dexcontrol/utils/comm_helper.py +110 -0
  33. dexcontrol/utils/constants.py +1 -1
  34. dexcontrol/utils/error_code.py +2 -4
  35. dexcontrol/utils/os_utils.py +172 -4
  36. dexcontrol/utils/pb_utils.py +6 -28
  37. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/METADATA +16 -3
  38. dexcontrol-0.3.4.dist-info/RECORD +62 -0
  39. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/WHEEL +1 -1
  40. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  41. dexcontrol/proto/dexcontrol_msg_pb2.py +0 -73
  42. dexcontrol/proto/dexcontrol_msg_pb2.pyi +0 -220
  43. dexcontrol/proto/dexcontrol_query_pb2.py +0 -77
  44. dexcontrol/proto/dexcontrol_query_pb2.pyi +0 -162
  45. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  46. dexcontrol/utils/motion_utils.py +0 -199
  47. dexcontrol/utils/rate_limiter.py +0 -172
  48. dexcontrol/utils/rtc_utils.py +0 -144
  49. dexcontrol/utils/subscribers/__init__.py +0 -52
  50. dexcontrol/utils/subscribers/base.py +0 -281
  51. dexcontrol/utils/subscribers/camera.py +0 -332
  52. dexcontrol/utils/subscribers/decoders.py +0 -88
  53. dexcontrol/utils/subscribers/generic.py +0 -110
  54. dexcontrol/utils/subscribers/imu.py +0 -175
  55. dexcontrol/utils/subscribers/lidar.py +0 -172
  56. dexcontrol/utils/subscribers/protobuf.py +0 -111
  57. dexcontrol/utils/subscribers/rtc.py +0 -316
  58. dexcontrol/utils/zenoh_utils.py +0 -122
  59. dexcontrol-0.2.12.dist-info/RECORD +0 -75
  60. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/licenses/LICENSE +0 -0
@@ -8,71 +8,52 @@
8
8
  # 2. Commercial License
9
9
  # For commercial licensing terms, contact: contact@dexmate.ai
10
10
 
11
- """ZED camera sensor implementation using RTC subscribers for RGB and Zenoh subscriber for depth."""
11
+ """ZED camera sensor implementation using RTC or DexComm subscribers for RGB and depth."""
12
12
 
13
- import logging
14
13
  import time
15
- from typing import Any, Dict, Optional, Union
14
+ from typing import Any
16
15
 
17
16
  import numpy as np
18
- import zenoh
17
+ from loguru import logger
19
18
 
20
- from dexcontrol.config.sensors.cameras import ZedCameraConfig
21
- from dexcontrol.utils.os_utils import resolve_key_name
22
- from dexcontrol.utils.rtc_utils import create_rtc_subscriber_from_zenoh
23
- from dexcontrol.utils.subscribers.camera import (
24
- DepthCameraSubscriber,
25
- RGBCameraSubscriber,
19
+ from dexcontrol.comm import (
20
+ create_camera_subscriber,
21
+ create_depth_subscriber,
22
+ create_rtc_camera_subscriber,
26
23
  )
27
- from dexcontrol.utils.subscribers.rtc import RTCSubscriber
28
- from dexcontrol.utils.zenoh_utils import query_zenoh_json
29
-
30
- logger = logging.getLogger(__name__)
31
-
32
- # Optional import for depth processing
33
- try:
34
- from dexsensor.serialization.camera import decode_depth
35
- DEXSENSOR_AVAILABLE = True
36
- except ImportError:
37
- logger.warning("dexsensor not available. Depth data will be returned without decoding.")
38
- decode_depth = None
39
- DEXSENSOR_AVAILABLE = False
24
+ from dexcontrol.config.sensors.cameras import ZedCameraConfig
25
+ from dexcontrol.sensors.camera.base_camera import BaseCameraSensor
40
26
 
41
27
 
42
- class ZedCameraSensor:
28
+ class ZedCameraSensor(BaseCameraSensor):
43
29
  """ZED camera sensor for multi-stream (RGB, Depth) data acquisition.
44
30
 
45
31
  This sensor manages left RGB, right RGB, and depth data streams from a ZED
46
32
  camera. It can be configured to use high-performance RTC subscribers for RGB
47
- streams (`use_rtc=True`) or fall back to standard Zenoh subscribers
48
- (`use_rtc=False`). The depth stream always uses a standard Zenoh subscriber.
33
+ streams (`use_rtc=True`) or standard DexComm subscribers. Both types provide
34
+ the same API interface, making them interchangeable.
49
35
  """
50
36
 
51
- SubscriberType = Union[RTCSubscriber, DepthCameraSubscriber, RGBCameraSubscriber]
52
-
53
37
  def __init__(
54
38
  self,
55
39
  configs: ZedCameraConfig,
56
- zenoh_session: zenoh.Session,
57
40
  ) -> None:
58
41
  """Initialize the ZED camera sensor and its subscribers.
59
42
 
60
43
  Args:
61
44
  configs: Configuration object for the ZED camera.
62
- zenoh_session: Active Zenoh session for communication.
63
45
  """
64
- self._name = configs.name
65
- self._zenoh_session = zenoh_session
46
+ super().__init__(configs.name)
66
47
  self._configs = configs
67
- self._subscribers: Dict[str, Optional[ZedCameraSensor.SubscriberType]] = {}
68
- self._camera_info: Optional[Dict[str, Any]] = None
48
+ self._subscribers: dict[
49
+ str, Any | None
50
+ ] = {} # Will hold either RTCSubscriberWrapper or Subscriber
69
51
 
70
52
  self._create_subscribers()
71
- self._query_camera_info()
72
53
 
73
54
  def _create_subscriber(
74
- self, stream_name: str, stream_config: Dict[str, Any]
75
- ) -> Optional[SubscriberType]:
55
+ self, stream_name: str, stream_config: dict[str, Any]
56
+ ) -> Any | None:
76
57
  """Factory method to create a subscriber based on stream type and config."""
77
58
  try:
78
59
  if not stream_config.get("enable", False):
@@ -85,50 +66,68 @@ class ZedCameraSensor:
85
66
  if not topic:
86
67
  logger.warning(f"'{self._name}': No 'topic' for depth stream.")
87
68
  return None
88
- logger.info(f"'{self._name}': Creating Zenoh depth subscriber.")
89
- return DepthCameraSubscriber(
69
+ logger.info(f"'{self._name}': Creating depth subscriber.")
70
+ # Use new DexComm integration
71
+ return create_depth_subscriber(
90
72
  topic=topic,
91
- zenoh_session=self._zenoh_session,
92
- name=f"{self._name}_{stream_name}_subscriber",
93
- enable_fps_tracking=self._configs.enable_fps_tracking,
94
- fps_log_interval=self._configs.fps_log_interval,
95
73
  )
96
74
 
97
- # Create RGB subscriber (RTC or Zenoh)
75
+ # Create RGB subscriber (RTC or DexComm)
98
76
  if self._configs.use_rtc:
99
- info_key = stream_config.get("info_key")
100
- if not info_key:
101
- logger.warning(f"'{self._name}': No 'info_key' for RTC stream '{stream_name}'.")
102
- return None
103
- logger.info(f"'{self._name}': Creating RTC subscriber for '{stream_name}'.")
104
- return create_rtc_subscriber_from_zenoh(
105
- zenoh_session=self._zenoh_session,
106
- info_topic=info_key,
107
- name=f"{self._name}_{stream_name}_subscriber",
108
- enable_fps_tracking=self._configs.enable_fps_tracking,
109
- fps_log_interval=self._configs.fps_log_interval,
110
- )
77
+ # Check if we have RTC info from camera_info
78
+ rtc_url = self._get_rtc_signaling_url(stream_name)
79
+ if rtc_url:
80
+ logger.info(
81
+ f"'{self._name}': Creating RTC subscriber for '{stream_name}' with direct URL."
82
+ )
83
+ return create_rtc_camera_subscriber(
84
+ signaling_url=rtc_url,
85
+ )
86
+ else:
87
+ # Fallback to querying via info_key
88
+ info_key = stream_config.get("info_key")
89
+ if not info_key:
90
+ logger.warning(
91
+ f"'{self._name}': No RTC URL or info_key for stream '{stream_name}'."
92
+ )
93
+ return None
94
+ logger.info(
95
+ f"'{self._name}': Creating RTC subscriber for '{stream_name}' with info_key."
96
+ )
97
+ return create_rtc_camera_subscriber(
98
+ info_topic=info_key,
99
+ )
111
100
  else:
112
101
  topic = stream_config.get("topic")
113
102
  if not topic:
114
- logger.warning(f"'{self._name}': No 'topic' for Zenoh stream '{stream_name}'.")
103
+ logger.warning(
104
+ f"'{self._name}': No 'topic' for Zenoh stream '{stream_name}'."
105
+ )
115
106
  return None
116
- logger.info(f"'{self._name}': Creating Zenoh RGB subscriber for '{stream_name}'.")
117
- return RGBCameraSubscriber(
107
+ logger.info(
108
+ f"'{self._name}': Creating RGB subscriber for '{stream_name}'."
109
+ )
110
+ # Use new DexComm integration
111
+ return create_camera_subscriber(
118
112
  topic=topic,
119
- zenoh_session=self._zenoh_session,
120
- name=f"{self._name}_{stream_name}_subscriber",
121
- enable_fps_tracking=self._configs.enable_fps_tracking,
122
- fps_log_interval=self._configs.fps_log_interval,
123
113
  )
124
114
 
125
115
  except Exception as e:
126
- logger.error(f"Error creating subscriber for '{self._name}/{stream_name}': {e}")
116
+ logger.error(
117
+ f"Error creating subscriber for '{self._name}/{stream_name}': {e}"
118
+ )
127
119
  return None
128
120
 
129
121
  def _create_subscribers(self) -> None:
130
122
  """Create subscribers for all configured camera streams."""
131
123
  subscriber_config = self._configs.subscriber_config
124
+
125
+ # Determine info endpoint for camera metadata
126
+ info_endpoint = self._determine_info_endpoint(subscriber_config)
127
+
128
+ # Query camera info first to potentially get RTC URLs
129
+ self._query_camera_info(info_endpoint)
130
+
132
131
  stream_definitions = {
133
132
  "left_rgb": subscriber_config.get("left_rgb", {}),
134
133
  "right_rgb": subscriber_config.get("right_rgb", {}),
@@ -138,42 +137,28 @@ class ZedCameraSensor:
138
137
  for name, config in stream_definitions.items():
139
138
  self._subscribers[name] = self._create_subscriber(name, config)
140
139
 
141
- def _query_camera_info(self) -> None:
142
- """Query Zenoh for camera metadata if using RTC."""
140
+ def _determine_info_endpoint(self, subscriber_config: dict) -> str | None:
141
+ """Determine the info endpoint for querying camera metadata.
143
142
 
144
- enabled_rgb_streams = [
145
- s
146
- for s_name, s in self._subscribers.items()
147
- if "rgb" in s_name and s is not None
148
- ]
149
-
150
- if not enabled_rgb_streams:
151
- logger.warning(f"'{self._name}': No enabled RGB streams to query for camera info.")
152
- return
153
-
154
- # Use the info_key from the first available RGB subscriber's config
155
- first_stream_name = "left_rgb" if self._subscribers.get("left_rgb") else "right_rgb"
156
- stream_config = self._configs.subscriber_config.get(first_stream_name, {})
157
- info_key = stream_config.get("info_key")
158
-
159
- if not info_key:
160
- logger.warning(f"'{self._name}': Could not find info_key for camera info query.")
161
- return
143
+ Args:
144
+ subscriber_config: Subscriber configuration dict
162
145
 
163
- try:
164
- # Construct the root info key (e.g., 'camera/head/info')
165
- resolved_key = resolve_key_name(info_key).rstrip("/")
166
- info_key_root = "/".join(resolved_key.split("/")[:-2])
167
- final_info_key = f"{info_key_root}/info"
168
-
169
- logger.info(f"'{self._name}': Querying for camera info at '{final_info_key}'.")
170
- self._camera_info = query_zenoh_json(self._zenoh_session, final_info_key)
171
- if self._camera_info:
172
- logger.info(f"'{self._name}': Successfully received camera info.")
173
- else:
174
- logger.warning(f"'{self._name}': No camera info found at '{final_info_key}'.")
175
- except Exception as e:
176
- logger.error(f"'{self._name}': Failed to query camera info: {e}")
146
+ Returns:
147
+ Info endpoint or None
148
+ """
149
+ # Try to find the first enabled RGB stream to get the base info endpoint
150
+ for stream_name in ["left_rgb", "right_rgb"]:
151
+ stream_config = subscriber_config.get(stream_name, {})
152
+ if stream_config.get("enable", False):
153
+ if self._configs.use_rtc:
154
+ info_key = stream_config.get("info_key")
155
+ if info_key:
156
+ return info_key
157
+ else:
158
+ topic = stream_config.get("topic")
159
+ if topic:
160
+ return self._derive_info_endpoint_from_topic(topic)
161
+ return None
177
162
 
178
163
  def shutdown(self) -> None:
179
164
  """Shutdown all active subscribers for the camera sensor."""
@@ -182,7 +167,9 @@ class ZedCameraSensor:
182
167
  if subscriber:
183
168
  try:
184
169
  subscriber.shutdown()
185
- logger.debug(f"'{self._name}': Subscriber '{stream_name}' shut down.")
170
+ logger.debug(
171
+ f"'{self._name}': Subscriber '{stream_name}' shut down."
172
+ )
186
173
  except Exception as e:
187
174
  logger.error(
188
175
  f"Error shutting down '{stream_name}' subscriber for '{self._name}': {e}"
@@ -196,7 +183,9 @@ class ZedCameraSensor:
196
183
  True if at least one subscriber is active, False otherwise.
197
184
  """
198
185
  return any(
199
- sub.is_active() for sub in self._subscribers.values() if sub is not None
186
+ sub.get_latest() is not None
187
+ for sub in self._subscribers.values()
188
+ if sub is not None
200
189
  )
201
190
 
202
191
  def is_stream_active(self, stream_name: str) -> bool:
@@ -209,7 +198,7 @@ class ZedCameraSensor:
209
198
  True if the specified stream's subscriber is active, False otherwise.
210
199
  """
211
200
  subscriber = self._subscribers.get(stream_name)
212
- return subscriber.is_active() if subscriber else False
201
+ return subscriber.get_latest() is not None if subscriber else False
213
202
 
214
203
  def wait_for_active(self, timeout: float = 5.0, require_all: bool = False) -> bool:
215
204
  """Wait for camera streams to become active.
@@ -229,8 +218,10 @@ class ZedCameraSensor:
229
218
 
230
219
  if require_all:
231
220
  for sub in enabled_subscribers:
232
- if not sub.wait_for_active(timeout):
233
- logger.warning(f"'{self._name}': Timed out waiting for subscriber '{sub.name}'.")
221
+ if not sub.wait_for_message(timeout):
222
+ logger.warning(
223
+ f"'{self._name}': Timed out waiting for subscriber '{sub.name}'."
224
+ )
234
225
  return False
235
226
  logger.info(f"'{self._name}': All enabled streams are active.")
236
227
  return True
@@ -241,13 +232,14 @@ class ZedCameraSensor:
241
232
  logger.info(f"'{self._name}': At least one stream is active.")
242
233
  return True
243
234
  time.sleep(0.1)
244
- logger.warning(f"'{self._name}': Timed out waiting for any stream to become active.")
235
+ logger.warning(
236
+ f"'{self._name}': Timed out waiting for any stream to become active."
237
+ )
245
238
  return False
246
239
 
247
240
  def get_obs(
248
- self, obs_keys: Optional[list[str]] = None,
249
- include_timestamp: bool = False
250
- ) -> Dict[str, Optional[np.ndarray]]:
241
+ self, obs_keys: list[str] | None = None, include_timestamp: bool = False
242
+ ) -> dict[str, np.ndarray | None]:
251
243
  """Get the latest observation data from specified camera streams.
252
244
 
253
245
  Args:
@@ -267,37 +259,49 @@ class ZedCameraSensor:
267
259
  obs_out = {}
268
260
  for key in keys_to_fetch:
269
261
  subscriber = self._subscribers.get(key)
270
- data = subscriber.get_latest_data() if subscriber else None
262
+ data = subscriber.get_latest() if subscriber else None
271
263
 
272
- is_tuple_or_list = isinstance(data, (tuple, list))
264
+ # DexComm returns dict with 'data' and 'timestamp' keys when timestamp is present
265
+ has_timestamp = isinstance(data, dict) and "timestamp" in data
273
266
 
274
267
  if include_timestamp:
275
- if not is_tuple_or_list:
276
- logger.warning(f"Timestamp is not available yet for {key} stream.")
277
- obs_out[key] = data
268
+ # Always return a consistent structure
269
+ if has_timestamp:
270
+ obs_out[key] = {
271
+ "data": data.get("data") if isinstance(data, dict) else None,
272
+ "timestamp": data.get("timestamp")
273
+ if isinstance(data, dict)
274
+ else None,
275
+ }
276
+ else:
277
+ obs_out[key] = {"data": data, "timestamp": None}
278
278
  else:
279
- obs_out[key] = data[0] if is_tuple_or_list else data
279
+ if has_timestamp:
280
+ # Extract payload when timestamp wrapper is present
281
+ obs_out[key] = data.get("data") if isinstance(data, dict) else None
282
+ else:
283
+ obs_out[key] = data
280
284
  return obs_out
281
285
 
282
- def get_left_rgb(self) -> Optional[np.ndarray]:
286
+ def get_left_rgb(self) -> np.ndarray | None:
283
287
  """Get the latest image from the left RGB stream.
284
288
 
285
289
  Returns:
286
290
  The latest left RGB image as a numpy array, or None if not available.
287
291
  """
288
292
  subscriber = self._subscribers.get("left_rgb")
289
- return subscriber.get_latest_data() if subscriber else None
293
+ return subscriber.get_latest() if subscriber else None
290
294
 
291
- def get_right_rgb(self) -> Optional[np.ndarray]:
295
+ def get_right_rgb(self) -> np.ndarray | None:
292
296
  """Get the latest image from the right RGB stream.
293
297
 
294
298
  Returns:
295
299
  The latest right RGB image as a numpy array, or None if not available.
296
300
  """
297
301
  subscriber = self._subscribers.get("right_rgb")
298
- return subscriber.get_latest_data() if subscriber else None
302
+ return subscriber.get_latest() if subscriber else None
299
303
 
300
- def get_depth(self) -> Optional[np.ndarray]:
304
+ def get_depth(self) -> np.ndarray | None:
301
305
  """Get the latest image from the depth stream.
302
306
 
303
307
  The depth data is returned as a numpy array with values in meters.
@@ -306,29 +310,7 @@ class ZedCameraSensor:
306
310
  The latest depth image as a numpy array, or None if not available.
307
311
  """
308
312
  subscriber = self._subscribers.get("depth")
309
- return subscriber.get_latest_data() if subscriber else None
310
-
311
- @property
312
- def fps(self) -> Dict[str, float]:
313
- """Get the current FPS measurement for each active stream.
314
-
315
- Returns:
316
- A dictionary mapping stream names to their FPS measurements.
317
- """
318
- return {
319
- name: sub.fps
320
- for name, sub in self._subscribers.items()
321
- if sub is not None
322
- }
323
-
324
- @property
325
- def name(self) -> str:
326
- """Get the sensor name.
327
-
328
- Returns:
329
- Sensor name string.
330
- """
331
- return self._name
313
+ return subscriber.get_latest() if subscriber else None
332
314
 
333
315
  @property
334
316
  def available_streams(self) -> list:
@@ -346,25 +328,11 @@ class ZedCameraSensor:
346
328
  Returns:
347
329
  List of stream names that are currently receiving data.
348
330
  """
349
- return [name for name, sub in self._subscribers.items() if sub and sub.is_active()]
350
-
351
- @property
352
- def dexsensor_available(self) -> bool:
353
- """Check if dexsensor is available for depth decoding.
354
-
355
- Returns:
356
- True if dexsensor is available, False otherwise.
357
- """
358
- return DEXSENSOR_AVAILABLE
359
-
360
- @property
361
- def camera_info(self) -> dict | None:
362
- """Get the camera info.
363
-
364
- Returns:
365
- Camera info dictionary if available, None otherwise.
366
- """
367
- return self._camera_info
331
+ return [
332
+ name
333
+ for name, sub in self._subscribers.items()
334
+ if sub and sub.get_latest() is not None
335
+ ]
368
336
 
369
337
  @property
370
338
  def height(self) -> dict[str, int]:
@@ -374,7 +342,10 @@ class ZedCameraSensor:
374
342
  Height of the camera image.
375
343
  """
376
344
  images = self.get_obs()
377
- return {name: image.shape[0] if image is not None else 0 for name, image in images.items()}
345
+ return {
346
+ name: image.shape[0] if image is not None else 0
347
+ for name, image in images.items()
348
+ }
378
349
 
379
350
  @property
380
351
  def width(self) -> dict[str, int]:
@@ -384,8 +355,10 @@ class ZedCameraSensor:
384
355
  Width of the camera image.
385
356
  """
386
357
  images = self.get_obs()
387
- return {name: image.shape[1] if image is not None else 0 for name, image in images.items()}
388
-
358
+ return {
359
+ name: image.shape[1] if image is not None else 0
360
+ for name, image in images.items()
361
+ }
389
362
 
390
363
  @property
391
364
  def resolution(self) -> dict[str, tuple[int, int]]:
@@ -395,4 +368,7 @@ class ZedCameraSensor:
395
368
  Resolution of the camera image.
396
369
  """
397
370
  images = self.get_obs()
398
- return {name: (image.shape[0], image.shape[1]) if image is not None else (0, 0) for name, image in images.items()}
371
+ return {
372
+ name: (image.shape[0], image.shape[1]) if image is not None else (0, 0)
373
+ for name, image in images.items()
374
+ }