dexcontrol 0.2.7__py3-none-any.whl → 0.2.12__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 (43) hide show
  1. dexcontrol/__init__.py +1 -0
  2. dexcontrol/config/core/arm.py +6 -0
  3. dexcontrol/config/core/hand.py +5 -16
  4. dexcontrol/config/core/head.py +7 -8
  5. dexcontrol/config/core/misc.py +14 -1
  6. dexcontrol/config/core/torso.py +8 -4
  7. dexcontrol/config/sensors/cameras/__init__.py +2 -1
  8. dexcontrol/config/sensors/cameras/luxonis_camera.py +51 -0
  9. dexcontrol/config/sensors/cameras/rgb_camera.py +1 -1
  10. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  11. dexcontrol/config/sensors/vega_sensors.py +9 -1
  12. dexcontrol/config/vega.py +30 -2
  13. dexcontrol/core/arm.py +95 -43
  14. dexcontrol/core/component.py +92 -6
  15. dexcontrol/core/hand.py +81 -13
  16. dexcontrol/core/head.py +55 -26
  17. dexcontrol/core/misc.py +94 -13
  18. dexcontrol/core/torso.py +44 -11
  19. dexcontrol/proto/dexcontrol_msg_pb2.py +38 -36
  20. dexcontrol/proto/dexcontrol_msg_pb2.pyi +48 -20
  21. dexcontrol/proto/dexcontrol_query_pb2.py +7 -3
  22. dexcontrol/proto/dexcontrol_query_pb2.pyi +24 -0
  23. dexcontrol/robot.py +319 -75
  24. dexcontrol/sensors/__init__.py +2 -1
  25. dexcontrol/sensors/camera/__init__.py +2 -0
  26. dexcontrol/sensors/camera/luxonis_camera.py +169 -0
  27. dexcontrol/sensors/camera/rgb_camera.py +36 -0
  28. dexcontrol/sensors/camera/zed_camera.py +48 -8
  29. dexcontrol/sensors/imu/chassis_imu.py +5 -1
  30. dexcontrol/sensors/imu/zed_imu.py +3 -2
  31. dexcontrol/sensors/lidar/rplidar.py +1 -0
  32. dexcontrol/sensors/manager.py +3 -0
  33. dexcontrol/utils/constants.py +3 -0
  34. dexcontrol/utils/error_code.py +236 -0
  35. dexcontrol/utils/subscribers/lidar.py +1 -0
  36. dexcontrol/utils/trajectory_utils.py +17 -5
  37. dexcontrol/utils/viz_utils.py +86 -11
  38. dexcontrol/utils/zenoh_utils.py +39 -0
  39. {dexcontrol-0.2.7.dist-info → dexcontrol-0.2.12.dist-info}/METADATA +6 -4
  40. dexcontrol-0.2.12.dist-info/RECORD +75 -0
  41. dexcontrol-0.2.7.dist-info/RECORD +0 -72
  42. {dexcontrol-0.2.7.dist-info → dexcontrol-0.2.12.dist-info}/WHEEL +0 -0
  43. {dexcontrol-0.2.7.dist-info → dexcontrol-0.2.12.dist-info}/licenses/LICENSE +0 -0
@@ -89,11 +89,11 @@ class RobotComponent:
89
89
  Parsed protobuf state message.
90
90
 
91
91
  Raises:
92
- RuntimeError: If no state data is available.
92
+ None: If no state data is available.
93
93
  """
94
94
  state = self._subscriber.get_latest_data()
95
95
  if state is None:
96
- raise RuntimeError("No state data available")
96
+ logger.error(f"No state data available for {self.__class__.__name__}")
97
97
  return state
98
98
 
99
99
  def wait_for_active(self, timeout: float = 5.0) -> bool:
@@ -133,6 +133,15 @@ class RobotComponent:
133
133
  if hasattr(self, "_subscriber") and self._subscriber:
134
134
  self._subscriber.shutdown()
135
135
 
136
+ def get_timestamp_ns(self) -> int:
137
+ """Get the current timestamp (in nanoseconds) of the most recent state update.
138
+
139
+ Returns:
140
+ The current timestamp in nanoseconds when driver updated the state.
141
+ We convert the time to client clock by adding the server time offset.
142
+ """
143
+ return self._get_state().timestamp_ns
144
+
136
145
 
137
146
  class RobotJointComponent(RobotComponent):
138
147
  """Base class for robot components with both state and control interfaces.
@@ -171,6 +180,8 @@ class RobotJointComponent(RobotComponent):
171
180
  state_message_type: type[M],
172
181
  zenoh_session: zenoh.Session,
173
182
  joint_name: list[str] | None = None,
183
+ joint_limit: list[list[float]] | None = None,
184
+ joint_vel_limit: list[float] | None = None,
174
185
  pose_pool: Mapping[str, list[float] | np.ndarray] | None = None,
175
186
  ) -> None:
176
187
  """Initializes RobotJointComponent.
@@ -190,6 +201,13 @@ class RobotJointComponent(RobotComponent):
190
201
  resolved_topic
191
202
  )
192
203
  self._joint_name: list[str] | None = joint_name
204
+ self._joint_limit: np.ndarray | None = (
205
+ np.array(joint_limit) if joint_limit else None
206
+ )
207
+ self._joint_vel_limit: np.ndarray | None = (
208
+ np.array(joint_vel_limit) if joint_vel_limit else None
209
+ )
210
+
193
211
  self._pose_pool: dict[str, np.ndarray] | None = (
194
212
  self._convert_pose_pool_to_arrays(pose_pool)
195
213
  )
@@ -231,6 +249,18 @@ class RobotJointComponent(RobotComponent):
231
249
  raise ValueError("Joint names not available for this component")
232
250
  return self._joint_name.copy()
233
251
 
252
+ @property
253
+ def joint_limit(self) -> np.ndarray | None:
254
+ """Gets the joint limits of the component."""
255
+ return self._joint_limit.copy() if self._joint_limit is not None else None
256
+
257
+ @property
258
+ def joint_vel_limit(self) -> np.ndarray | None:
259
+ """Gets the joint velocity limits of the component."""
260
+ return (
261
+ self._joint_vel_limit.copy() if self._joint_vel_limit is not None else None
262
+ )
263
+
234
264
  def get_predefined_pose(self, pose_name: str) -> np.ndarray:
235
265
  """Gets a predefined pose from the pose pool.
236
266
 
@@ -466,7 +496,7 @@ class RobotJointComponent(RobotComponent):
466
496
  def _convert_joint_cmd_to_array(
467
497
  self,
468
498
  joint_cmd: Float[np.ndarray, " N"] | list[float] | dict[str, float],
469
- clip_value: float | None = None,
499
+ clip_value: float | np.ndarray | None = None,
470
500
  ) -> np.ndarray:
471
501
  """Convert joint command to numpy array format.
472
502
 
@@ -475,7 +505,9 @@ class RobotJointComponent(RobotComponent):
475
505
  - List of joint values [j1, j2, ..., jN]
476
506
  - Numpy array with shape (N,)
477
507
  - Dictionary mapping joint names to values
478
- clip_value: Optional value to clip the output array between [-clip_value, clip_value].
508
+ clip_value: Optional value to clip the output array. Can be:
509
+ - float: symmetric clipping between [-clip_value, clip_value]
510
+ - numpy array: element-wise clipping between [-clip_value, clip_value]
479
511
 
480
512
  Returns:
481
513
  Joint command as numpy array.
@@ -634,6 +666,8 @@ class RobotJointComponent(RobotComponent):
634
666
  relative: bool = False,
635
667
  wait_time: float = 0.0,
636
668
  wait_kwargs: dict[str, float] | None = None,
669
+ exit_on_reach: bool = False,
670
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
637
671
  ) -> None:
638
672
  """Send joint position control commands.
639
673
 
@@ -645,6 +679,8 @@ class RobotJointComponent(RobotComponent):
645
679
  relative: If True, the joint positions are relative to the current position.
646
680
  wait_time: Time to wait after sending command in seconds.
647
681
  wait_kwargs: Optional parameters for trajectory generation.
682
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
683
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
648
684
 
649
685
  Raises:
650
686
  ValueError: If joint_pos dictionary contains invalid joint names.
@@ -656,9 +692,49 @@ class RobotJointComponent(RobotComponent):
656
692
  if isinstance(joint_pos, (list, dict)):
657
693
  joint_pos = self._convert_joint_cmd_to_array(joint_pos)
658
694
 
695
+ if self._joint_limit is not None:
696
+ joint_pos = np.clip(
697
+ joint_pos, self._joint_limit[:, 0], self._joint_limit[:, 1]
698
+ )
699
+
659
700
  self._send_position_command(joint_pos)
660
701
 
661
702
  if wait_time > 0.0:
703
+ self._wait_for_position(
704
+ joint_pos, wait_time, exit_on_reach, exit_on_reach_kwargs
705
+ )
706
+
707
+ def _wait_for_position(
708
+ self,
709
+ joint_pos: Float[np.ndarray, " N"] | list[float] | dict[str, float],
710
+ wait_time: float,
711
+ exit_on_reach: bool = False,
712
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
713
+ ) -> None:
714
+ """Wait for a specified time with optional early exit when position is reached.
715
+
716
+ Args:
717
+ joint_pos: Target joint positions to check against.
718
+ wait_time: Maximum time to wait in seconds.
719
+ exit_on_reach: If True, exit early when joint positions are reached.
720
+ exit_on_reach_kwargs: Optional parameters for position checking.
721
+ """
722
+ if exit_on_reach:
723
+ # Set default tolerance if not provided
724
+ exit_on_reach_kwargs = exit_on_reach_kwargs or {}
725
+ exit_on_reach_kwargs.setdefault("tolerance", 0.05)
726
+
727
+ # Convert to expected format for is_joint_pos_reached
728
+ if isinstance(joint_pos, list):
729
+ joint_pos = np.array(joint_pos, dtype=np.float32)
730
+
731
+ # Wait until position is reached or timeout
732
+ start_time = time.time()
733
+ while time.time() - start_time < wait_time:
734
+ if self.is_joint_pos_reached(joint_pos, **exit_on_reach_kwargs):
735
+ break
736
+ time.sleep(0.01)
737
+ else:
662
738
  time.sleep(wait_time)
663
739
 
664
740
  def _send_position_command(self, joint_pos: Float[np.ndarray, " N"]) -> None:
@@ -679,12 +755,16 @@ class RobotJointComponent(RobotComponent):
679
755
  self,
680
756
  pose_name: str,
681
757
  wait_time: float = 3.0,
758
+ exit_on_reach: bool = False,
759
+ exit_on_reach_kwargs: dict[str, float] | None = None,
682
760
  ) -> None:
683
761
  """Move the component to a predefined pose.
684
762
 
685
763
  Args:
686
764
  pose_name: Name of the pose to move to.
687
765
  wait_time: Time to wait for the component to reach the pose.
766
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
767
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
688
768
 
689
769
  Raises:
690
770
  ValueError: If pose pool is not available or if an invalid pose name is provided.
@@ -696,7 +776,12 @@ class RobotJointComponent(RobotComponent):
696
776
  f"Invalid pose name: {pose_name}. Available poses: {list(self._pose_pool.keys())}"
697
777
  )
698
778
  pose = self._pose_pool[pose_name]
699
- self.set_joint_pos(pose, wait_time=wait_time)
779
+ self.set_joint_pos(
780
+ pose,
781
+ wait_time=wait_time,
782
+ exit_on_reach=exit_on_reach,
783
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
784
+ )
700
785
 
701
786
  def is_joint_pos_reached(
702
787
  self,
@@ -819,11 +904,12 @@ class RobotJointComponent(RobotComponent):
819
904
  else:
820
905
  # Check all joints, ensuring arrays are same length
821
906
  min_len = min(len(current_pos), len(target_pos))
822
- return bool(
907
+ is_reached = bool(
823
908
  np.all(
824
909
  np.abs(current_pos[:min_len] - target_pos[:min_len]) <= tolerance
825
910
  )
826
911
  )
912
+ return is_reached
827
913
 
828
914
  def is_pose_reached(
829
915
  self,
dexcontrol/core/hand.py CHANGED
@@ -14,6 +14,8 @@ This module provides the Hand class for controlling a robotic hand through Zenoh
14
14
  communication. It handles joint position control and state monitoring.
15
15
  """
16
16
 
17
+ from typing import Any
18
+
17
19
  import numpy as np
18
20
  import zenoh
19
21
  from jaxtyping import Float
@@ -51,8 +53,8 @@ class Hand(RobotJointComponent):
51
53
  )
52
54
 
53
55
  # Store predefined hand positions as private attributes
54
- self._joint_pos_open = np.array(configs.joint_pos_open, dtype=np.float32)
55
- self._joint_pos_close = np.array(configs.joint_pos_close, dtype=np.float32)
56
+ self._joint_pos_open = np.array(configs.pose_pool["open"], dtype=np.float32)
57
+ self._joint_pos_close = np.array(configs.pose_pool["close"], dtype=np.float32)
56
58
 
57
59
  def _send_position_command(
58
60
  self, joint_pos: Float[np.ndarray, " N"] | list[float]
@@ -73,6 +75,8 @@ class Hand(RobotJointComponent):
73
75
  relative: bool = False,
74
76
  wait_time: float = 0.0,
75
77
  wait_kwargs: dict[str, float] | None = None,
78
+ exit_on_reach: bool = False,
79
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
76
80
  ) -> None:
77
81
  """Send joint position control commands to the hand.
78
82
 
@@ -84,27 +88,61 @@ class Hand(RobotJointComponent):
84
88
  relative: If True, the joint positions are relative to the current position.
85
89
  wait_time: Time to wait after setting the joint positions.
86
90
  wait_kwargs: Optional parameters for trajectory generation (not used in Hand).
91
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
92
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
87
93
 
88
94
  Raises:
89
95
  ValueError: If joint_pos dictionary contains invalid joint names.
90
96
  """
91
- super().set_joint_pos(joint_pos, relative, wait_time, wait_kwargs)
97
+ super().set_joint_pos(
98
+ joint_pos,
99
+ relative,
100
+ wait_time,
101
+ wait_kwargs,
102
+ exit_on_reach,
103
+ exit_on_reach_kwargs,
104
+ )
92
105
 
93
- def open_hand(self, wait_time: float = 0.0) -> None:
106
+ def open_hand(
107
+ self,
108
+ wait_time: float = 0.0,
109
+ exit_on_reach: bool = False,
110
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
111
+ ) -> None:
94
112
  """Open the hand to the predefined open position.
95
113
 
96
114
  Args:
97
115
  wait_time: Time to wait after opening the hand.
116
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
117
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
98
118
  """
99
- self.set_joint_pos(self._joint_pos_open, wait_time=wait_time)
119
+ self.set_joint_pos(
120
+ self._joint_pos_open,
121
+ wait_time=wait_time,
122
+ exit_on_reach=exit_on_reach,
123
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
124
+ )
100
125
 
101
- def close_hand(self, wait_time: float = 0.0) -> None:
126
+ def close_hand(
127
+ self,
128
+ wait_time: float = 0.0,
129
+ exit_on_reach: bool = False,
130
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
131
+ ) -> None:
102
132
  """Close the hand to the predefined closed position.
103
133
 
104
134
  Args:
105
135
  wait_time: Time to wait after closing the hand.
136
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
137
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
138
+
106
139
  """
107
- self.set_joint_pos(self._joint_pos_close, wait_time=wait_time)
140
+ self.set_joint_pos(
141
+ self._joint_pos_close,
142
+ wait_time=wait_time,
143
+ exit_on_reach=exit_on_reach,
144
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
145
+ )
108
146
 
109
147
 
110
148
  class HandF5D6(Hand):
@@ -114,7 +152,12 @@ class HandF5D6(Hand):
114
152
  the F5D6 hand model.
115
153
  """
116
154
 
117
- def close_hand(self, wait_time: float = 0.0) -> None:
155
+ def close_hand(
156
+ self,
157
+ wait_time: float = 0.0,
158
+ exit_on_reach: bool = False,
159
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
160
+ ) -> None:
118
161
  """Close the hand fully using a two-step approach for better control."""
119
162
  if self.is_joint_pos_reached(self._joint_pos_close, tolerance=0.1):
120
163
  return
@@ -122,13 +165,28 @@ class HandF5D6(Hand):
122
165
  # First step: Move to intermediate position
123
166
  intermediate_pos = self._get_intermediate_close_position()
124
167
  first_step_wait_time = 0.8
125
- self.set_joint_pos(intermediate_pos, wait_time=first_step_wait_time)
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
+ )
126
174
 
127
175
  # Second step: Move to final closed position
128
176
  remaining_wait_time = max(0.0, wait_time - first_step_wait_time)
129
- self.set_joint_pos(self._joint_pos_close, wait_time=remaining_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
+ )
130
183
 
131
- def open_hand(self, wait_time: float = 0.0) -> None:
184
+ def open_hand(
185
+ self,
186
+ wait_time: float = 0.0,
187
+ exit_on_reach: bool = False,
188
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
189
+ ) -> None:
132
190
  """Open the hand fully using a two-step approach for better control."""
133
191
  if self.is_joint_pos_reached(self._joint_pos_open, tolerance=0.1):
134
192
  return
@@ -136,11 +194,21 @@ class HandF5D6(Hand):
136
194
  # First step: Move to intermediate position
137
195
  intermediate_pos = self._get_intermediate_open_position()
138
196
  first_step_wait_time = 0.3
139
- self.set_joint_pos(intermediate_pos, wait_time=first_step_wait_time)
197
+ self.set_joint_pos(
198
+ intermediate_pos,
199
+ wait_time=first_step_wait_time,
200
+ exit_on_reach=exit_on_reach,
201
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
202
+ )
140
203
 
141
204
  # Second step: Move to final open position
142
205
  remaining_wait_time = max(0.0, wait_time - first_step_wait_time)
143
- self.set_joint_pos(self._joint_pos_open, wait_time=remaining_wait_time)
206
+ self.set_joint_pos(
207
+ self._joint_pos_open,
208
+ wait_time=remaining_wait_time,
209
+ exit_on_reach=exit_on_reach,
210
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
211
+ )
144
212
 
145
213
  def _get_intermediate_close_position(self) -> np.ndarray:
146
214
  """Get intermediate position for closing hand.
dexcontrol/core/head.py CHANGED
@@ -58,74 +58,86 @@ class Head(RobotJointComponent):
58
58
  state_message_type=dexcontrol_msg_pb2.HeadState,
59
59
  zenoh_session=zenoh_session,
60
60
  joint_name=configs.joint_name,
61
+ joint_limit=configs.joint_limit
62
+ if hasattr(configs, "joint_limit")
63
+ else None,
64
+ joint_vel_limit=configs.joint_vel_limit
65
+ if hasattr(configs, "joint_vel_limit")
66
+ else None,
61
67
  pose_pool=configs.pose_pool,
62
68
  )
63
69
 
64
70
  self.mode_querier: Final[zenoh.Querier] = zenoh_session.declare_querier(
65
71
  resolve_key_name(configs.set_mode_query), timeout=2.0
66
72
  )
67
- self.default_vel: Final[float] = configs.default_vel
68
- self.max_vel: Final[float] = configs.max_vel
69
- assert self.max_vel > self.default_vel, (
70
- "max_vel must be greater than default_vel"
71
- )
72
- self._joint_limit: Float[np.ndarray, "3 2"] = np.stack(
73
- [configs.joint_limit_lower, configs.joint_limit_upper], axis=1
74
- )
73
+ assert self._joint_vel_limit is not None, "joint_vel_limit is not set"
75
74
 
76
75
  def set_joint_pos(
77
76
  self,
78
- joint_pos: Float[np.ndarray, "2"] | list[float] | dict[str, float],
77
+ joint_pos: Float[np.ndarray, "3"] | list[float] | dict[str, float],
79
78
  relative: bool = False,
80
79
  wait_time: float = 0.0,
81
80
  wait_kwargs: dict[str, float] | None = None,
81
+ exit_on_reach: bool = False,
82
+ exit_on_reach_kwargs: dict[str, float] | None = None,
82
83
  ) -> None:
83
84
  """Send joint position control commands to the head.
84
85
 
85
86
  Args:
86
87
  joint_pos: Joint positions as either:
87
88
  - List of joint values [j1, j2]
88
- - Numpy array with shape (2,), in radians
89
+ - Numpy array with shape (3,), in radians
89
90
  - Dictionary mapping joint names to position values
90
91
  relative: If True, the joint positions are relative to the current position.
91
92
  wait_time: Time to wait after sending command in seconds. If 0, returns
92
93
  immediately after sending command.
93
94
  wait_kwargs: Optional parameters for trajectory generation (not used in Head).
95
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
96
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
94
97
 
95
98
  Raises:
96
99
  ValueError: If joint_pos dictionary contains invalid joint names.
97
100
  """
98
101
  self.set_joint_pos_vel(
99
- joint_pos, joint_vel=None, relative=relative, wait_time=wait_time
102
+ joint_pos,
103
+ joint_vel=None,
104
+ relative=relative,
105
+ wait_time=wait_time,
106
+ exit_on_reach=exit_on_reach,
107
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
100
108
  )
101
109
 
102
110
  def set_joint_pos_vel(
103
111
  self,
104
- joint_pos: Float[np.ndarray, "2"] | list[float] | dict[str, float],
105
- joint_vel: Float[np.ndarray, "2"]
112
+ joint_pos: Float[np.ndarray, "3"] | list[float] | dict[str, float],
113
+ joint_vel: Float[np.ndarray, "3"]
106
114
  | list[float]
107
115
  | dict[str, float]
108
116
  | float
109
117
  | None = None,
110
118
  relative: bool = False,
111
119
  wait_time: float = 0.0,
120
+ exit_on_reach: bool = False,
121
+ exit_on_reach_kwargs: dict[str, float] | None = None,
112
122
  ) -> None:
113
123
  """Send control commands to the head.
114
124
 
115
125
  Args:
116
126
  joint_pos: Joint positions as either:
117
127
  - List of joint values [j1, j2]
118
- - Numpy array with shape (2,), in radians
128
+ - Numpy array with shape (3,), in radians
119
129
  - Dictionary mapping joint names to position values
120
130
  joint_vel: Optional joint velocities as either:
121
131
  - List of joint values [v1, v2]
122
- - Numpy array with shape (2,), in rad/s
132
+ - Numpy array with shape (3,), in rad/s
123
133
  - Dictionary mapping joint names to velocity values
124
134
  - Single float value to be applied to all joints
125
135
  If None, velocities are calculated based on default velocity setting.
126
136
  relative: If True, the joint positions are relative to the current position.
127
137
  wait_time: Time to wait after sending command in seconds. If 0, returns
128
138
  immediately after sending command.
139
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
140
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
129
141
 
130
142
  Raises:
131
143
  ValueError: If wait_time is negative or joint_pos dictionary contains
@@ -142,6 +154,15 @@ class Head(RobotJointComponent):
142
154
  joint_pos = self._convert_joint_cmd_to_array(joint_pos)
143
155
  joint_vel = self._process_joint_velocities(joint_vel, joint_pos)
144
156
 
157
+ if self._joint_limit is not None:
158
+ joint_pos = np.clip(
159
+ joint_pos, self._joint_limit[:, 0], self._joint_limit[:, 1]
160
+ )
161
+ if self._joint_vel_limit is not None:
162
+ joint_vel = np.clip(
163
+ joint_vel, -self._joint_vel_limit, self._joint_vel_limit
164
+ )
165
+
145
166
  # Create and send control message
146
167
  control_msg = dexcontrol_msg_pb2.HeadCommand()
147
168
  control_msg.joint_pos.extend(joint_pos.tolist())
@@ -149,8 +170,12 @@ class Head(RobotJointComponent):
149
170
  self._publish_control(control_msg)
150
171
 
151
172
  # Wait if specified
152
- if wait_time > 0.0:
153
- time.sleep(wait_time)
173
+ self._wait_for_position(
174
+ joint_pos=joint_pos,
175
+ wait_time=wait_time,
176
+ exit_on_reach=exit_on_reach,
177
+ exit_on_reach_kwargs=exit_on_reach_kwargs,
178
+ )
154
179
 
155
180
  def set_mode(self, mode: Literal["enable", "disable"]) -> None:
156
181
  """Set the operating mode of the head.
@@ -180,12 +205,12 @@ class Head(RobotJointComponent):
180
205
  logger.info(reply.ok.payload.to_string())
181
206
  time.sleep(0.5)
182
207
 
183
- def get_joint_limit(self) -> Float[np.ndarray, "3 2"]:
208
+ def get_joint_limit(self) -> Float[np.ndarray, "3 2"] | None:
184
209
  """Get the joint limits of the head.
185
210
 
186
211
  Returns:
187
212
  Array of joint limits with shape (3, 2), where the first column contains
188
- lower limits and the second column contains upper limits.
213
+ lower limits and the second column contains upper limits, or None if not configured.
189
214
  """
190
215
  return self._joint_limit
191
216
 
@@ -212,7 +237,7 @@ class Head(RobotJointComponent):
212
237
 
213
238
  def _process_joint_velocities(
214
239
  self,
215
- joint_vel: Float[np.ndarray, "2"]
240
+ joint_vel: Float[np.ndarray, "3"]
216
241
  | list[float]
217
242
  | dict[str, float]
218
243
  | float
@@ -234,14 +259,18 @@ class Head(RobotJointComponent):
234
259
  motion_norm = np.linalg.norm(joint_motion)
235
260
 
236
261
  if motion_norm < 1e-6: # Avoid division by zero
237
- return np.zeros(2, dtype=np.float32)
262
+ return np.zeros(3, dtype=np.float32)
238
263
 
239
- # Scale velocities by default velocity
240
- return (joint_motion / motion_norm) * self.default_vel
264
+ default_vel = (
265
+ 2.5 if self._joint_vel_limit is None else np.min(self._joint_vel_limit)
266
+ )
267
+ return (joint_motion / motion_norm) * default_vel
241
268
 
242
269
  if isinstance(joint_vel, (int, float)):
243
270
  # Single value - apply to all joints
244
- return np.full(2, joint_vel, dtype=np.float32)
271
+ return np.full(3, joint_vel, dtype=np.float32)
245
272
 
246
- # Convert to array and clip to max velocity
247
- return self._convert_joint_cmd_to_array(joint_vel, clip_value=self.max_vel)
273
+ # Convert to array and clip to velocity limits
274
+ return self._convert_joint_cmd_to_array(
275
+ joint_vel, clip_value=self._joint_vel_limit
276
+ )