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
@@ -8,24 +8,27 @@
8
8
  # 2. Commercial License
9
9
  # For commercial licensing terms, contact: contact@dexmate.ai
10
10
 
11
- """Base module for robot components with Zenoh communication.
11
+ """Base module for robot components using DexComm communication.
12
12
 
13
- This module provides base classes for robot components that communicate via Zenoh.
14
- It includes RobotComponent for state-only components and RobotJointComponent for
15
- components that also support control commands.
13
+ This module provides base classes for robot components that use DexComm's
14
+ Raw API for communication. It includes RobotComponent for state-only components
15
+ and RobotJointComponent for components that also support control commands.
16
16
  """
17
17
 
18
18
  import time
19
- from typing import Any, Final, Mapping, TypeVar
19
+ from typing import Any, Final, Mapping, Optional, TypeVar
20
20
 
21
21
  import numpy as np
22
- import zenoh
22
+
23
+ # Use DexComm for all communication
24
+ from dexcomm import Publisher, Subscriber
25
+ from dexcomm.serialization import deserialize_protobuf, serialize_protobuf
23
26
  from google.protobuf.message import Message
24
27
  from jaxtyping import Float
25
28
  from loguru import logger
26
29
 
30
+ from dexcontrol.utils.comm_helper import get_zenoh_config_path
27
31
  from dexcontrol.utils.os_utils import resolve_key_name
28
- from dexcontrol.utils.subscribers import ProtobufZenohSubscriber
29
32
 
30
33
  # Type variable for Message subclasses
31
34
  M = TypeVar("M", bound=Message)
@@ -47,41 +50,55 @@ class RobotComponent:
47
50
  def __init__(
48
51
  self,
49
52
  state_sub_topic: str,
50
- zenoh_session: zenoh.Session,
51
53
  state_message_type: type[M],
52
54
  ) -> None:
53
55
  """Initializes RobotComponent.
54
56
 
55
57
  Args:
56
58
  state_sub_topic: Topic to subscribe to for state updates.
57
- zenoh_session: Active Zenoh session for communication.
58
59
  state_message_type: Protobuf message class for component state.
59
60
  """
60
61
  self._state_message_type = state_message_type
61
- self._zenoh_session: Final[zenoh.Session] = zenoh_session
62
- self._init_subscriber(state_sub_topic, state_message_type, zenoh_session)
62
+ self._latest_state: Optional[M] = None
63
+ self._is_active = False
64
+ self._init_subscriber(state_sub_topic, state_message_type)
63
65
 
64
66
  def _init_subscriber(
65
67
  self,
66
68
  state_sub_topic: str,
67
69
  state_message_type: type[M],
68
- zenoh_session: zenoh.Session,
69
70
  ) -> None:
70
- """Initialize the Zenoh subscriber for state updates.
71
+ """Initialize the subscriber for state updates using DexComm.
71
72
 
72
73
  Args:
73
74
  state_sub_topic: Topic to subscribe to for state updates.
74
75
  state_message_type: Protobuf message class for component state.
75
- zenoh_session: Active Zenoh session for communication.
76
- """
77
- self._subscriber = ProtobufZenohSubscriber(
78
- topic=state_sub_topic,
79
- zenoh_session=zenoh_session,
80
- message_type=state_message_type,
81
- name=f"{self.__class__.__name__}",
82
- enable_fps_tracking=False,
76
+ """
77
+ # Resolve topic with robot namespace
78
+ full_topic = resolve_key_name(state_sub_topic)
79
+
80
+ # Create DexComm subscriber with protobuf deserialization
81
+ self._subscriber = Subscriber(
82
+ topic=full_topic,
83
+ callback=self._on_state_update,
84
+ deserializer=lambda data: deserialize_protobuf(data, state_message_type),
85
+ config=get_zenoh_config_path(),
83
86
  )
84
87
 
88
+ logger.debug(
89
+ f"Created subscriber for {self.__class__.__name__} on {full_topic}"
90
+ )
91
+
92
+ def _on_state_update(self, state: M) -> None:
93
+ """Handle incoming state updates.
94
+
95
+ Args:
96
+ state: Deserialized protobuf state message.
97
+ """
98
+ self._latest_state = state
99
+ if state:
100
+ self._is_active = True
101
+
85
102
  def _get_state(self) -> M:
86
103
  """Gets the current state of the component.
87
104
 
@@ -91,10 +108,9 @@ class RobotComponent:
91
108
  Raises:
92
109
  None: If no state data is available.
93
110
  """
94
- state = self._subscriber.get_latest_data()
95
- if state is None:
111
+ if self._latest_state is None:
96
112
  logger.error(f"No state data available for {self.__class__.__name__}")
97
- return state
113
+ return self._latest_state
98
114
 
99
115
  def wait_for_active(self, timeout: float = 5.0) -> bool:
100
116
  """Waits for the component to start receiving state updates.
@@ -105,7 +121,12 @@ class RobotComponent:
105
121
  Returns:
106
122
  True if component becomes active, False if timeout is reached.
107
123
  """
108
- return self._subscriber.wait_for_active(timeout)
124
+ start_time = time.time()
125
+ while time.time() - start_time < timeout:
126
+ if self._is_active:
127
+ return True
128
+ time.sleep(0.1)
129
+ return False
109
130
 
110
131
  def is_active(self) -> bool:
111
132
  """Check if component is receiving state updates.
@@ -113,7 +134,7 @@ class RobotComponent:
113
134
  Returns:
114
135
  True if component is active, False otherwise.
115
136
  """
116
- return self._subscriber.is_active()
137
+ return self._is_active
117
138
 
118
139
  def shutdown(self) -> None:
119
140
  """Cleans up Zenoh resources."""
@@ -149,7 +170,7 @@ class RobotJointComponent(RobotComponent):
149
170
  Extends RobotComponent to add APIs for interacting with joints.
150
171
 
151
172
  Attributes:
152
- _publisher: Zenoh publisher for control commands.
173
+ _publisher: Publisher for control commands (Zenoh or dexcomm).
153
174
  _joint_name: List of joint names for this component.
154
175
  _pose_pool: Dictionary of predefined poses for this component.
155
176
  """
@@ -178,7 +199,6 @@ class RobotJointComponent(RobotComponent):
178
199
  state_sub_topic: str,
179
200
  control_pub_topic: str,
180
201
  state_message_type: type[M],
181
- zenoh_session: zenoh.Session,
182
202
  joint_name: list[str] | None = None,
183
203
  joint_limit: list[list[float]] | None = None,
184
204
  joint_vel_limit: list[float] | None = None,
@@ -190,16 +210,24 @@ class RobotJointComponent(RobotComponent):
190
210
  state_sub_topic: Topic to subscribe to for state updates.
191
211
  control_pub_topic: Topic to publish control commands.
192
212
  state_message_type: Protobuf message class for component state.
193
- zenoh_session: Active Zenoh session for communication.
194
213
  joint_name: List of joint names for this component.
214
+ joint_limit: Joint position limits.
215
+ joint_vel_limit: Joint velocity limits.
195
216
  pose_pool: Dictionary of predefined poses for this component.
196
217
  """
197
- super().__init__(state_sub_topic, zenoh_session, state_message_type)
218
+ super().__init__(state_sub_topic, state_message_type)
219
+
220
+ # Resolve topic with robot namespace
221
+ full_topic = resolve_key_name(control_pub_topic)
198
222
 
199
- resolved_topic: Final[str] = resolve_key_name(control_pub_topic)
200
- self._publisher: Final[zenoh.Publisher] = zenoh_session.declare_publisher(
201
- resolved_topic
223
+ # Create DexComm publisher with protobuf serialization
224
+ self._publisher: Final[Publisher] = Publisher(
225
+ topic=full_topic,
226
+ serializer=serialize_protobuf,
227
+ config=get_zenoh_config_path(),
202
228
  )
229
+
230
+ logger.debug(f"Created publisher for {self.__class__.__name__} on {full_topic}")
203
231
  self._joint_name: list[str] | None = joint_name
204
232
  self._joint_limit: np.ndarray | None = (
205
233
  np.array(joint_limit) if joint_limit else None
@@ -218,22 +246,19 @@ class RobotJointComponent(RobotComponent):
218
246
  Args:
219
247
  control_msg: Protobuf control message to publish.
220
248
  """
221
- msg_bytes = control_msg.SerializeToString()
222
- self._publisher.put(msg_bytes)
249
+ # DexComm publisher with protobuf serializer handles this
250
+ self._publisher.publish(control_msg)
223
251
 
224
252
  def shutdown(self) -> None:
225
- """Cleans up all Zenoh resources."""
253
+ """Cleans up all communication resources."""
226
254
  super().shutdown()
227
255
  try:
228
256
  if hasattr(self, "_publisher") and self._publisher:
229
- self._publisher.undeclare()
257
+ self._publisher.shutdown()
230
258
  except Exception as e:
231
- # Don't log "Undeclared publisher" errors as warnings - they're expected during shutdown
232
- error_msg = str(e).lower()
233
- if not ("undeclared" in error_msg or "closed" in error_msg):
234
- logger.warning(
235
- f"Error undeclaring publisher for {self.__class__.__name__}: {e}"
236
- )
259
+ logger.warning(
260
+ f"Error shutting down publisher for {self.__class__.__name__}: {e}"
261
+ )
237
262
 
238
263
  @property
239
264
  def joint_name(self) -> list[str]:
dexcontrol/core/hand.py CHANGED
@@ -18,8 +18,8 @@ from enum import Enum
18
18
  from typing import Any, cast
19
19
 
20
20
  import numpy as np
21
- import zenoh
22
21
  from jaxtyping import Float
22
+ from loguru import logger
23
23
 
24
24
  from dexcontrol.config.core.hand import HandConfig
25
25
  from dexcontrol.core.component import RobotComponent, RobotJointComponent
@@ -42,7 +42,6 @@ class Hand(RobotJointComponent):
42
42
  def __init__(
43
43
  self,
44
44
  configs: HandConfig,
45
- zenoh_session: zenoh.Session,
46
45
  hand_type: HandType = HandType.HandF5D6_V1,
47
46
  ) -> None:
48
47
  """Initialize the hand controller.
@@ -50,13 +49,11 @@ class Hand(RobotJointComponent):
50
49
  Args:
51
50
  configs: Hand configuration parameters containing communication topics
52
51
  and predefined hand positions.
53
- zenoh_session: Active Zenoh communication session for message passing.
54
52
  """
55
53
  super().__init__(
56
54
  state_sub_topic=configs.state_sub_topic,
57
55
  control_pub_topic=configs.control_pub_topic,
58
56
  state_message_type=dexcontrol_msg_pb2.MotorStateWithCurrent,
59
- zenoh_session=zenoh_session,
60
57
  joint_name=configs.joint_name,
61
58
  )
62
59
 
@@ -129,17 +126,14 @@ class HandF5D6(Hand):
129
126
  def __init__(
130
127
  self,
131
128
  configs: HandConfig,
132
- zenoh_session: zenoh.Session,
133
129
  hand_type: HandType = HandType.HandF5D6_V1,
134
130
  ) -> None:
135
- super().__init__(configs, zenoh_session)
131
+ super().__init__(configs)
136
132
 
137
133
  # Initialize touch sensor for F5D6_V2 hands
138
134
  self._hand_type = hand_type
139
135
  if self._hand_type == HandType.HandF5D6_V2:
140
- self._touch_sensor = HandF5D6TouchSensor(
141
- configs.touch_sensor_sub_topic, zenoh_session
142
- )
136
+ self._touch_sensor = HandF5D6TouchSensor(configs.touch_sensor_sub_topic)
143
137
  elif self._hand_type == HandType.HandF5D6_V1:
144
138
  self._touch_sensor = None
145
139
  else:
@@ -164,27 +158,30 @@ class HandF5D6(Hand):
164
158
  exit_on_reach_kwargs: dict[str, Any] | None = None,
165
159
  ) -> None:
166
160
  """Close the hand fully using a two-step approach for better control."""
167
- if self.is_joint_pos_reached(self._joint_pos_close, tolerance=0.1):
168
- return
169
-
170
- # First step: Move to intermediate position
171
- intermediate_pos = self._get_intermediate_close_position()
172
- first_step_wait_time = 0.8
173
- self.set_joint_pos(
174
- intermediate_pos,
175
- wait_time=first_step_wait_time,
176
- exit_on_reach=exit_on_reach,
177
- exit_on_reach_kwargs=exit_on_reach_kwargs,
178
- )
161
+ try:
162
+ if self.is_joint_pos_reached(self._joint_pos_close, tolerance=0.1):
163
+ return
164
+
165
+ # First step: Move to intermediate position
166
+ intermediate_pos = self._get_intermediate_close_position()
167
+ first_step_wait_time = 0.8
168
+ self.set_joint_pos(
169
+ intermediate_pos,
170
+ wait_time=first_step_wait_time,
171
+ exit_on_reach=exit_on_reach,
172
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
173
+ )
179
174
 
180
- # Second step: Move to final closed position
181
- remaining_wait_time = max(0.0, wait_time - first_step_wait_time)
182
- self.set_joint_pos(
183
- self._joint_pos_close,
184
- wait_time=remaining_wait_time,
185
- exit_on_reach=exit_on_reach,
186
- exit_on_reach_kwargs=exit_on_reach_kwargs,
187
- )
175
+ # Second step: Move to final closed position
176
+ remaining_wait_time = max(0.0, wait_time - first_step_wait_time)
177
+ self.set_joint_pos(
178
+ self._joint_pos_close,
179
+ wait_time=remaining_wait_time,
180
+ exit_on_reach=exit_on_reach,
181
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
182
+ )
183
+ except Exception as e:
184
+ logger.warning(f"Failed to close hand: {e}")
188
185
 
189
186
  def open_hand(
190
187
  self,
@@ -193,27 +190,30 @@ class HandF5D6(Hand):
193
190
  exit_on_reach_kwargs: dict[str, Any] | None = None,
194
191
  ) -> None:
195
192
  """Open the hand fully using a two-step approach for better control."""
196
- if self.is_joint_pos_reached(self._joint_pos_open, tolerance=0.1):
197
- return
198
-
199
- # First step: Move to intermediate position
200
- intermediate_pos = self._get_intermediate_open_position()
201
- first_step_wait_time = 0.3
202
- self.set_joint_pos(
203
- intermediate_pos,
204
- wait_time=first_step_wait_time,
205
- exit_on_reach=exit_on_reach,
206
- exit_on_reach_kwargs=exit_on_reach_kwargs,
207
- )
193
+ try:
194
+ if self.is_joint_pos_reached(self._joint_pos_open, tolerance=0.1):
195
+ return
196
+
197
+ # First step: Move to intermediate position
198
+ intermediate_pos = self._get_intermediate_open_position()
199
+ first_step_wait_time = 0.3
200
+ self.set_joint_pos(
201
+ intermediate_pos,
202
+ wait_time=first_step_wait_time,
203
+ exit_on_reach=exit_on_reach,
204
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
205
+ )
208
206
 
209
- # Second step: Move to final open position
210
- remaining_wait_time = max(0.0, wait_time - first_step_wait_time)
211
- self.set_joint_pos(
212
- self._joint_pos_open,
213
- wait_time=remaining_wait_time,
214
- exit_on_reach=exit_on_reach,
215
- exit_on_reach_kwargs=exit_on_reach_kwargs,
216
- )
207
+ # Second step: Move to final open position
208
+ remaining_wait_time = max(0.0, wait_time - first_step_wait_time)
209
+ self.set_joint_pos(
210
+ self._joint_pos_open,
211
+ wait_time=remaining_wait_time,
212
+ exit_on_reach=exit_on_reach,
213
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
214
+ )
215
+ except Exception as e:
216
+ logger.warning(f"Failed to open hand: {e}")
217
217
 
218
218
  def _get_intermediate_close_position(self) -> np.ndarray:
219
219
  """Get intermediate position for closing hand.
@@ -254,16 +254,14 @@ class HandF5D6TouchSensor(RobotComponent):
254
254
  This class provides methods to read wrench sensor data through Zenoh communication.
255
255
  """
256
256
 
257
- def __init__(self, state_sub_topic: str, zenoh_session: zenoh.Session) -> None:
257
+ def __init__(self, state_sub_topic: str) -> None:
258
258
  """Initialize the wrench sensor reader.
259
259
 
260
260
  Args:
261
261
  state_sub_topic: Topic to subscribe to for wrench sensor data.
262
- zenoh_session: Active Zenoh communication session for message passing.
263
262
  """
264
263
  super().__init__(
265
264
  state_sub_topic=state_sub_topic,
266
- zenoh_session=zenoh_session,
267
265
  state_message_type=dexcontrol_msg_pb2.HandTouchSensorState,
268
266
  )
269
267
 
dexcontrol/core/head.py CHANGED
@@ -15,17 +15,16 @@ communication. It handles joint position and velocity control, mode setting, and
15
15
  state monitoring.
16
16
  """
17
17
 
18
- import time
19
- from typing import Final, Literal
18
+ from typing import Literal
20
19
 
21
20
  import numpy as np
22
- import zenoh
21
+ from dexcomm import call_service
23
22
  from jaxtyping import Float
24
- from loguru import logger
25
23
 
26
24
  from dexcontrol.config.core import HeadConfig
27
25
  from dexcontrol.core.component import RobotJointComponent
28
26
  from dexcontrol.proto import dexcontrol_msg_pb2, dexcontrol_query_pb2
27
+ from dexcontrol.utils.comm_helper import get_zenoh_config_path
29
28
  from dexcontrol.utils.os_utils import resolve_key_name
30
29
 
31
30
 
@@ -44,19 +43,16 @@ class Head(RobotJointComponent):
44
43
  def __init__(
45
44
  self,
46
45
  configs: HeadConfig,
47
- zenoh_session: zenoh.Session,
48
46
  ) -> None:
49
47
  """Initialize the head controller.
50
48
 
51
49
  Args:
52
50
  configs: Configuration parameters for the head including communication topics.
53
- zenoh_session: Active Zenoh communication session for message passing.
54
51
  """
55
52
  super().__init__(
56
53
  state_sub_topic=configs.state_sub_topic,
57
54
  control_pub_topic=configs.control_pub_topic,
58
55
  state_message_type=dexcontrol_msg_pb2.MotorStateWithTorque,
59
- zenoh_session=zenoh_session,
60
56
  joint_name=configs.joint_name,
61
57
  joint_limit=configs.joint_limit
62
58
  if hasattr(configs, "joint_limit")
@@ -67,9 +63,8 @@ class Head(RobotJointComponent):
67
63
  pose_pool=configs.pose_pool,
68
64
  )
69
65
 
70
- self.mode_querier: Final[zenoh.Querier] = zenoh_session.declare_querier(
71
- resolve_key_name(configs.set_mode_query), timeout=2.0
72
- )
66
+ # Store the query topic for later use with DexComm
67
+ self._mode_query_topic = resolve_key_name(configs.set_mode_query)
73
68
  assert self._joint_vel_limit is not None, "joint_vel_limit is not set"
74
69
 
75
70
  def set_joint_pos(
@@ -198,13 +193,15 @@ class Head(RobotJointComponent):
198
193
 
199
194
  query_msg = dexcontrol_query_pb2.SetHeadMode()
200
195
  query_msg.mode = mode_map[mode]
201
- replies = self.mode_querier.get(payload=query_msg.SerializeToString())
202
196
 
203
- for reply in replies:
204
- if reply.ok is not None and reply.ok.payload is not None:
205
- # TODO: handle the reply message of head mode
206
- pass
207
- time.sleep(0.5)
197
+ call_service(
198
+ self._mode_query_topic,
199
+ request=query_msg,
200
+ timeout=1.0,
201
+ config=get_zenoh_config_path(),
202
+ request_serializer=lambda x: x.SerializeToString(),
203
+ response_deserializer=None,
204
+ )
208
205
 
209
206
  def get_joint_limit(self) -> Float[np.ndarray, "3 2"] | None:
210
207
  """Get the joint limits of the head.
@@ -225,16 +222,7 @@ class Head(RobotJointComponent):
225
222
  """Clean up Zenoh resources for the head component."""
226
223
  self.stop()
227
224
  super().shutdown()
228
- try:
229
- if hasattr(self, "mode_querier") and self.mode_querier:
230
- self.mode_querier.undeclare()
231
- except Exception as e:
232
- # Don't log "Undeclared querier" errors as warnings - they're expected during shutdown
233
- error_msg = str(e).lower()
234
- if not ("undeclared" in error_msg or "closed" in error_msg):
235
- logger.warning(
236
- f"Error undeclaring mode querier for {self.__class__.__name__}: {e}"
237
- )
225
+ # No need to undeclare queriers when using DexComm
238
226
 
239
227
  def _process_joint_velocities(
240
228
  self,