dexcontrol 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dexcontrol might be problematic. Click here for more details.

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 +140 -117
  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.2.dist-info}/METADATA +2 -1
  34. dexcontrol-0.3.2.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.2.dist-info}/WHEEL +0 -0
  51. {dexcontrol-0.3.0.dist-info → dexcontrol-0.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,144 @@
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 camera sensor class with common functionality for all camera sensors."""
12
+
13
+ from abc import ABC, abstractmethod
14
+ from typing import Any, Dict, Optional
15
+
16
+ from loguru import logger
17
+
18
+ from dexcontrol.utils.comm_helper import query_json_service
19
+
20
+
21
+ class BaseCameraSensor(ABC):
22
+ """Abstract base class for camera sensors.
23
+
24
+ Provides common functionality for querying camera info, extracting RTC URLs,
25
+ and managing camera metadata. Subclasses should implement the abstract methods
26
+ for their specific camera type.
27
+ """
28
+
29
+ def __init__(self, name: str):
30
+ """Initialize the base camera sensor.
31
+
32
+ Args:
33
+ name: Name of the camera sensor
34
+ """
35
+ self._name = name
36
+ self._camera_info: Optional[Dict[str, Any]] = None
37
+
38
+ @abstractmethod
39
+ def shutdown(self) -> None:
40
+ """Shutdown the camera sensor and release all resources."""
41
+ pass
42
+
43
+ @abstractmethod
44
+ def is_active(self) -> bool:
45
+ """Check if the camera sensor is actively receiving data."""
46
+ pass
47
+
48
+ @abstractmethod
49
+ def wait_for_active(self, timeout: float = 5.0) -> bool:
50
+ """Wait for the camera sensor to start receiving data."""
51
+ pass
52
+
53
+ def _query_camera_info(self, info_endpoint: Optional[str]) -> None:
54
+ """Query for camera metadata information.
55
+
56
+ Args:
57
+ info_endpoint: The endpoint to query for camera info
58
+ """
59
+ if not info_endpoint:
60
+ logger.debug(f"'{self._name}': No info endpoint provided for camera info query.")
61
+ return
62
+
63
+ try:
64
+ logger.debug(f"'{self._name}': Querying camera info from '{info_endpoint}'.")
65
+ self._camera_info = query_json_service(info_endpoint, timeout=2.0)
66
+
67
+ if self._camera_info:
68
+ logger.info(f"'{self._name}': Successfully retrieved camera info.")
69
+ else:
70
+ logger.debug(f"'{self._name}': No camera info available at '{info_endpoint}'.")
71
+
72
+ except Exception as e:
73
+ logger.debug(f"'{self._name}': Failed to query camera info: {e}")
74
+
75
+ def _get_rtc_signaling_url(self, stream_name: str) -> Optional[str]:
76
+ """Extract RTC signaling URL from camera_info for a specific stream.
77
+
78
+ Args:
79
+ stream_name: Name of the stream (e.g., 'left_rgb', 'right_rgb', 'rgb')
80
+
81
+ Returns:
82
+ Signaling URL if available, None otherwise
83
+ """
84
+ if not self._camera_info:
85
+ return None
86
+
87
+ rtc_info = self._camera_info.get("rtc", {})
88
+ streams = rtc_info.get("streams", {})
89
+ stream_info = streams.get(stream_name, {})
90
+ return stream_info.get("signaling_url")
91
+
92
+ def _derive_info_endpoint_from_topic(self, topic: str) -> Optional[str]:
93
+ """Derive camera info endpoint from a topic.
94
+
95
+ Args:
96
+ topic: The camera topic, e.g.:
97
+ - 'camera/head/left_rgb' -> 'camera/head/info'
98
+ - 'camera/head/right_rgb' -> 'camera/head/info'
99
+ - 'camera/head/depth' -> 'camera/head/info'
100
+ - 'camera/base_left/rgb' -> 'camera/base_left/info'
101
+
102
+ Returns:
103
+ Derived info endpoint or None
104
+ """
105
+ if not topic:
106
+ return None
107
+
108
+ parts = topic.split("/")
109
+ if len(parts) < 2:
110
+ return None
111
+
112
+ # Check if the last part is a stream identifier
113
+ last_part = parts[-1].lower()
114
+ stream_identifiers = ['left_rgb', 'right_rgb', 'depth', 'rgb', 'left', 'right']
115
+
116
+ if last_part in stream_identifiers:
117
+ # Remove the stream identifier and add 'info'
118
+ # e.g., camera/head/left_rgb -> camera/head/info
119
+ # e.g., camera/base_left/rgb -> camera/base_left/info
120
+ return "/".join(parts[:-1]) + "/info"
121
+ else:
122
+ # The last part is not a stream identifier, just append '/info'
123
+ # e.g., camera/head -> camera/head/info
124
+ return topic + "/info"
125
+
126
+ @property
127
+ def name(self) -> str:
128
+ """Get the sensor name.
129
+
130
+ Returns:
131
+ Sensor name string
132
+ """
133
+ return self._name
134
+
135
+ @property
136
+ def camera_info(self) -> Optional[Dict[str, Any]]:
137
+ """Get the camera metadata information.
138
+
139
+ Returns:
140
+ Camera info dictionary if available, None otherwise.
141
+ The dictionary typically contains intrinsic parameters,
142
+ resolution, and other camera metadata.
143
+ """
144
+ return self._camera_info
@@ -8,82 +8,101 @@
8
8
  # 2. Commercial License
9
9
  # For commercial licensing terms, contact: contact@dexmate.ai
10
10
 
11
- """RGB camera sensor implementation using RTC or Zenoh subscriber."""
11
+ """RGB camera sensor implementation using RTC or DexComm subscriber."""
12
12
 
13
- import logging
14
- from typing import Optional, Union
13
+ from typing import Optional
15
14
 
16
15
  import numpy as np
17
- import zenoh
16
+ from loguru import logger
18
17
 
18
+ from dexcontrol.comm import create_camera_subscriber, create_rtc_camera_subscriber
19
19
  from dexcontrol.config.sensors.cameras import RGBCameraConfig
20
- from dexcontrol.utils.rtc_utils import create_rtc_subscriber_with_config
21
- from dexcontrol.utils.subscribers.camera import RGBCameraSubscriber
22
- from dexcontrol.utils.subscribers.rtc import RTCSubscriber
20
+ from dexcontrol.sensors.camera.base_camera import BaseCameraSensor
23
21
 
24
- logger = logging.getLogger(__name__)
25
22
 
26
-
27
- class RGBCameraSensor:
28
- """RGB camera sensor that supports both RTC and standard Zenoh subscribers.
23
+ class RGBCameraSensor(BaseCameraSensor):
24
+ """RGB camera sensor that supports both RTC and standard DexComm subscribers.
29
25
 
30
26
  This sensor provides RGB image data from a camera. It can be configured to use
31
27
  either a high-performance RTC subscriber for real-time video streams or a
32
- standard Zenoh subscriber for raw image topics. The mode is controlled by the
28
+ standard DexComm subscriber for raw image topics. The mode is controlled by the
33
29
  `use_rtc` flag in the `RGBCameraConfig`.
30
+
31
+ Both subscriber types provide the same API, making them interchangeable.
34
32
  """
35
33
 
36
34
  def __init__(
37
35
  self,
38
36
  configs: RGBCameraConfig,
39
- zenoh_session: zenoh.Session,
40
37
  ) -> None:
41
38
  """Initialize the RGB camera sensor based on the provided configuration.
42
39
 
43
40
  Args:
44
41
  configs: Configuration object for the RGB camera sensor.
45
- zenoh_session: Active Zenoh session for communication.
46
42
  """
47
- self._name = configs.name
48
- self._subscriber: Optional[Union[RTCSubscriber, RGBCameraSubscriber]] = None
43
+ super().__init__(configs.name)
44
+ self._configs = configs
45
+ self._subscriber = None # Will be either RTCSubscriberWrapper or DexComm Subscriber
49
46
 
50
47
  subscriber_config = configs.subscriber_config.get("rgb", {})
51
48
  if not subscriber_config or not subscriber_config.get("enable", False):
52
49
  logger.info(f"RGBCameraSensor '{self._name}' is disabled in config.")
53
50
  return
54
51
 
55
- try:
56
- if configs.use_rtc:
57
- logger.info(f"'{self._name}': Using RTC subscriber.")
58
- self._subscriber = create_rtc_subscriber_with_config(
59
- zenoh_session=zenoh_session,
60
- config=subscriber_config,
61
- name=f"{self._name}_subscriber",
62
- enable_fps_tracking=configs.enable_fps_tracking,
63
- fps_log_interval=configs.fps_log_interval,
52
+ # Determine info endpoint and query camera info to potentially get RTC URLs
53
+ subscriber_config = configs.subscriber_config.get("rgb", {})
54
+ info_endpoint = self._determine_info_endpoint(subscriber_config)
55
+ self._query_camera_info(info_endpoint)
56
+
57
+ if configs.use_rtc:
58
+ logger.info(f"'{self._name}': Using RTC subscriber.")
59
+
60
+ # Try to get signaling URL from camera_info first
61
+ rtc_url = self._get_rtc_signaling_url("rgb")
62
+ if rtc_url:
63
+ logger.info(f"'{self._name}': Creating RTC subscriber with direct URL.")
64
+ self._subscriber = create_rtc_camera_subscriber(
65
+ signaling_url=rtc_url,
64
66
  )
65
67
  else:
66
- logger.info(f"'{self._name}': Using standard Zenoh subscriber.")
67
- topic = subscriber_config.get("topic")
68
- if topic:
69
- self._subscriber = RGBCameraSubscriber(
70
- topic=topic,
71
- zenoh_session=zenoh_session,
72
- name=f"{self._name}_subscriber",
73
- enable_fps_tracking=configs.enable_fps_tracking,
74
- fps_log_interval=configs.fps_log_interval,
68
+ # Fallback to using info_key
69
+ info_topic = subscriber_config.get("info_key") or subscriber_config.get("topic")
70
+ if info_topic:
71
+ logger.info(f"'{self._name}': Creating RTC subscriber with info_key.")
72
+ self._subscriber = create_rtc_camera_subscriber(
73
+ info_topic=info_topic,
75
74
  )
76
75
  else:
77
- logger.warning(
78
- f"No 'topic' specified for '{self._name}' in non-RTC mode."
79
- )
76
+ logger.warning(f"No RTC URL or info_key for '{self._name}' RTC mode.")
77
+ else:
78
+ logger.info(f"'{self._name}': Using standard subscriber.")
79
+ topic = subscriber_config.get("topic")
80
+ if topic:
81
+ self._subscriber = create_camera_subscriber(
82
+ topic=topic,
83
+ )
84
+ else:
85
+ logger.warning(
86
+ f"No 'topic' specified for '{self._name}' in non-RTC mode."
87
+ )
88
+
89
+ if self._subscriber is None:
90
+ logger.warning(f"Failed to create subscriber for '{self._name}'.")
80
91
 
81
- if self._subscriber is None:
82
- logger.warning(f"Failed to create subscriber for '{self._name}'.")
92
+ def _determine_info_endpoint(self, subscriber_config: dict) -> Optional[str]:
93
+ """Determine the info endpoint for querying camera metadata.
83
94
 
84
- except Exception as e:
85
- logger.error(f"Error creating subscriber for '{self._name}': {e}")
86
- self._subscriber = None
95
+ Args:
96
+ subscriber_config: Subscriber configuration dict
97
+
98
+ Returns:
99
+ Info endpoint or None
100
+ """
101
+ if self._configs.use_rtc:
102
+ return subscriber_config.get("info_key")
103
+ else:
104
+ topic = subscriber_config.get("topic")
105
+ return self._derive_info_endpoint_from_topic(topic) if topic else None
87
106
 
88
107
  def shutdown(self) -> None:
89
108
  """Shutdown the camera sensor and release all resources."""
@@ -97,7 +116,8 @@ class RGBCameraSensor:
97
116
  Returns:
98
117
  True if the subscriber exists and is receiving data, False otherwise.
99
118
  """
100
- return self._subscriber.is_active() if self._subscriber else False
119
+ data = self._subscriber.get_latest()
120
+ return data is not None
101
121
 
102
122
  def wait_for_active(self, timeout: float = 5.0) -> bool:
103
123
  """Wait for the camera sensor to start receiving data.
@@ -111,7 +131,8 @@ class RGBCameraSensor:
111
131
  if not self._subscriber:
112
132
  logger.warning(f"'{self._name}': Cannot wait, no subscriber initialized.")
113
133
  return False
114
- return self._subscriber.wait_for_active(timeout)
134
+ msg = self._subscriber.wait_for_message(timeout)
135
+ return msg is not None
115
136
 
116
137
  def get_obs(self) -> np.ndarray | None:
117
138
  """Get the latest observation (RGB image) from the sensor.
@@ -119,25 +140,8 @@ class RGBCameraSensor:
119
140
  Returns:
120
141
  The latest RGB image as a numpy array (HxWxC) if available, otherwise None.
121
142
  """
122
- return self._subscriber.get_latest_data() if self._subscriber else None
123
-
124
- @property
125
- def fps(self) -> float:
126
- """Get the current FPS measurement.
127
-
128
- Returns:
129
- Current frames per second measurement.
130
- """
131
- return self._subscriber.fps if self._subscriber else 0.0
132
-
133
- @property
134
- def name(self) -> str:
135
- """Get the sensor name.
136
-
137
- Returns:
138
- Sensor name string.
139
- """
140
- return self._name
143
+ data = self._subscriber.get_latest()
144
+ return data if data is not None else None
141
145
 
142
146
  @property
143
147
  def height(self) -> int: