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
dexcontrol/__init__.py CHANGED
@@ -32,6 +32,7 @@ from dexcontrol.utils.constants import COMM_CFG_PATH_ENV_VAR
32
32
  # Package-level constants
33
33
  LIB_PATH: Final[Path] = Path(__file__).resolve().parent
34
34
  CFG_PATH: Final[Path] = LIB_PATH / "config"
35
+ MIN_SOC_SOFTWARE_VERSION: int = 233
35
36
 
36
37
  logger.configure(handlers=[{"sink": RichHandler(markup=True), "format": "{message}"}])
37
38
 
@@ -20,18 +20,24 @@ class ArmConfig:
20
20
  wrench_sub_topic: str = "state/wrench/right"
21
21
  control_pub_topic: str = "control/arm/right"
22
22
  set_mode_query: str = "mode/arm/right"
23
+ ee_pass_through_pub_topic: str = "control/ee_pass_through/right"
23
24
  dof: int = 7
25
+ default_control_hz: int = 100
24
26
  joint_name: list[str] = field(
25
27
  default_factory=lambda: [f"R_arm_j{i + 1}" for i in range(7)]
26
28
  )
27
29
  joint_limit: list[list[float]] = field(
28
30
  default_factory=lambda: [[-np.pi, np.pi] for _ in range(7)]
29
31
  )
32
+ joint_vel_limit: list[float] = field(
33
+ default_factory=lambda: [2.4, 2.4, 2.7, 2.7, 2.7, 2.7, 2.7]
34
+ )
30
35
  pose_pool: dict[str, list[float]] = field(
31
36
  default_factory=lambda: {
32
37
  "folded": [-1.57079, 0.0, 0, -3.1, 0, 0, 0.69813],
33
38
  "folded_closed_hand": [-1.57079, 0.0, 0, -3.1, 0, 0, 0.9],
34
39
  "L_shape": [-0.064, 0.3, 0.0, -1.556, -1.271, 0.0, 0.0],
40
+ "lift_up": [-0.064, 0.3, 0.0, -2.756, -1.271, 0.0, 0.0],
35
41
  "zero": [1.57079, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
36
42
  }
37
43
  )
@@ -28,20 +28,9 @@ class HandConfig:
28
28
  ]
29
29
  )
30
30
 
31
- # Not to modify the following varaibles unless you change a different hand
32
- control_joint_names: list[str] = field(
33
- default_factory=lambda: ["th_j1", "ff_j1", "mf_j1", "rf_j1", "lf_j1", "th_j0"]
34
- )
35
- joint_pos_open: list[float] = field(
36
- default_factory=lambda: [0.1834, 0.2891, 0.2801, 0.284, 0.2811, -0.0158]
37
- )
38
- joint_pos_close: list[float] = field(
39
- default_factory=lambda: [
40
- -0.3468,
41
- -1.0946,
42
- -1.0844,
43
- -1.0154,
44
- -1.0118,
45
- 1.6,
46
- ]
31
+ pose_pool: dict[str, list[float]] = field(
32
+ default_factory=lambda: {
33
+ "open": [0.1834, 0.2891, 0.2801, 0.284, 0.2811, -0.0158],
34
+ "close": [-0.2668, -1.0946, -1.0844, -1.0154, -1.0118, 1.6],
35
+ }
47
36
  )
@@ -21,15 +21,14 @@ class HeadConfig:
21
21
  default_factory=lambda: ["head_j1", "head_j2", "head_j3"]
22
22
  )
23
23
  dof: int = 3
24
- # Use the following varaibles if you are using vega-rc2
25
- joint_limit_lower: list[float] = field(
26
- default_factory=lambda: [-1.2217, -2.7925, -1.396]
24
+ joint_limit: list[list[float]] = field(
25
+ default_factory=lambda: [
26
+ [-1.2217, 1.2217],
27
+ [-2.7925, 2.7925],
28
+ [-1.396, 1.396],
29
+ ]
27
30
  )
28
- joint_limit_upper: list[float] = field(
29
- default_factory=lambda: [1.2217, 2.7925, 1.396]
30
- )
31
- default_vel: float = 1.57
32
- max_vel: float = 2.0
31
+ joint_vel_limit: list[float] = field(default_factory=lambda: [2.5, 2.5, 2.5])
33
32
  pose_pool: dict[str, list[float]] = field(
34
33
  default_factory=lambda: {
35
34
  "home": [0.0, 0.0, 0.0],
@@ -13,7 +13,10 @@
13
13
  import os
14
14
  from dataclasses import dataclass
15
15
 
16
- from dexcontrol.utils.constants import DISABLE_HEARTBEAT_ENV_VAR
16
+ from dexcontrol.utils.constants import (
17
+ DISABLE_ESTOP_CHECKING_ENV_VAR,
18
+ DISABLE_HEARTBEAT_ENV_VAR,
19
+ )
17
20
 
18
21
 
19
22
  @dataclass
@@ -27,6 +30,16 @@ class EStopConfig:
27
30
  _target_: str = "dexcontrol.core.misc.EStop"
28
31
  state_sub_topic: str = "state/estop"
29
32
  estop_query_name: str = "system/estop"
33
+ enabled: bool = True # Can be disabled via DEXCONTROL_DISABLE_ESTOP_CHECKING=1
34
+
35
+ def __post_init__(self):
36
+ """Check environment variable to disable estop checking."""
37
+ if os.getenv(DISABLE_ESTOP_CHECKING_ENV_VAR, "0").lower() in (
38
+ "1",
39
+ "true",
40
+ "yes",
41
+ ):
42
+ self.enabled = False
30
43
 
31
44
 
32
45
  @dataclass
@@ -20,10 +20,14 @@ class TorsoConfig:
20
20
  joint_name: list[str] = field(
21
21
  default_factory=lambda: ["torso_j1", "torso_j2", "torso_j3"]
22
22
  )
23
- default_vel: float = 0.4 # rad/s, will be used if joint_vel is not provided
24
- max_vel: float = 0.6 # max velocity of torso joint, will be used to clip
25
- # the joint_vel, Highly recommended to set this value
26
- # not higher than 0.6 rad/s
23
+ joint_limit: list[list[float]] = field(
24
+ default_factory=lambda: [
25
+ [0, 1.570],
26
+ [0, 3.141],
27
+ [-1.570, 1.570],
28
+ ]
29
+ )
30
+ joint_vel_limit: list[float] = field(default_factory=lambda: [0.6, 0.6, 0.6])
27
31
  pose_pool: dict[str, list[float]] = field(
28
32
  default_factory=lambda: {
29
33
  "home": [0.0, 0.0, 0.0],
@@ -8,7 +8,8 @@
8
8
  # 2. Commercial License
9
9
  # For commercial licensing terms, contact: contact@dexmate.ai
10
10
 
11
+ from .luxonis_camera import LuxonisCameraConfig
11
12
  from .rgb_camera import RGBCameraConfig
12
13
  from .zed_camera import ZedCameraConfig
13
14
 
14
- __all__ = ["RGBCameraConfig", "ZedCameraConfig"]
15
+ __all__ = ["RGBCameraConfig", "ZedCameraConfig", "LuxonisCameraConfig"]
@@ -0,0 +1,51 @@
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
+ from dataclasses import dataclass, field
12
+
13
+
14
+ @dataclass
15
+ class LuxonisCameraConfig:
16
+ """Configuration for LuxonisCameraSensor (wrist camera).
17
+
18
+ Attributes:
19
+ _target_: Target sensor class for Hydra instantiation.
20
+ name: Logical name of the camera.
21
+ enable: Whether this sensor is enabled.
22
+ enable_fps_tracking: Enable FPS tracking logs.
23
+ fps_log_interval: Frames between FPS logs.
24
+ subscriber_config: Topics for RGB and Depth Zenoh subscribers.
25
+ """
26
+
27
+ _target_: str = "dexcontrol.sensors.camera.luxonis_camera.LuxonisCameraSensor"
28
+ name: str = "wrist_camera"
29
+ enable: bool = False
30
+ enable_fps_tracking: bool = False
31
+ fps_log_interval: int = 30
32
+
33
+ # Note: Resolution is set by the publisher (dexsensor). The publisher now defaults
34
+ # to 720p to match common Luxonis sensor capabilities (e.g., OV9782 supports 720p/800p).
35
+ # These subscribers only define topics to consume.
36
+ subscriber_config: dict = field(
37
+ default_factory=lambda: {
38
+ "left_rgb": {
39
+ "enable": True,
40
+ "topic": "camera/wrist/left_rgb",
41
+ },
42
+ "right_rgb": {
43
+ "enable": True,
44
+ "topic": "camera/wrist/right_rgb",
45
+ },
46
+ "depth": {
47
+ "enable": False,
48
+ "topic": "camera/wrist/depth",
49
+ },
50
+ }
51
+ )
@@ -19,7 +19,7 @@ class RGBCameraConfig:
19
19
  _target_: The target class to instantiate.
20
20
  name: A unique name for the sensor instance.
21
21
  enable: Whether the sensor is enabled.
22
- use_rtc: If True, uses the high-performance RTCSubscriber. If False,
22
+ use_rtc: If True, uses the RTCSubscriber. If False,
23
23
  uses the standard RGBCameraSubscriber with a Zenoh topic.
24
24
  enable_fps_tracking: If True, tracks and logs the FPS of the stream.
25
25
  fps_log_interval: The interval in seconds for logging FPS.
@@ -19,7 +19,7 @@ class ZedCameraConfig:
19
19
  _target_: The target class to instantiate.
20
20
  name: A unique name for the sensor instance.
21
21
  enable: Whether the sensor is enabled.
22
- use_rtc: If True, RGB streams use the high-performance RTCSubscriber.
22
+ use_rtc: If True, RGB streams use the RTCSubscriber.
23
23
  If False, they use the standard RGBCameraSubscriber. Depth
24
24
  always uses a standard Zenoh subscriber.
25
25
  enable_fps_tracking: If True, tracks and logs the FPS of all streams.
@@ -31,7 +31,7 @@ class ZedCameraConfig:
31
31
  _target_: str = "dexcontrol.sensors.camera.zed_camera.ZedCameraSensor"
32
32
  name: str = "zed_camera"
33
33
  enable: bool = False
34
- use_rtc: bool = True
34
+ use_rtc: bool = False
35
35
  enable_fps_tracking: bool = False
36
36
  fps_log_interval: int = 30
37
37
 
@@ -18,7 +18,7 @@ LiDAR and ultrasonic sensors.
18
18
  from collections.abc import Callable
19
19
  from dataclasses import dataclass, field
20
20
 
21
- from .cameras import RGBCameraConfig, ZedCameraConfig
21
+ from .cameras import LuxonisCameraConfig, RGBCameraConfig, ZedCameraConfig
22
22
  from .imu import ChassisIMUConfig, ZedIMUConfig
23
23
  from .lidar import RPLidarConfig
24
24
  from .ultrasonic import UltraSonicConfig
@@ -58,6 +58,12 @@ class VegaSensorsConfig:
58
58
  """
59
59
 
60
60
  head_camera: ZedCameraConfig = field(default_factory=ZedCameraConfig)
61
+ left_wrist_camera: LuxonisCameraConfig = field(
62
+ default_factory=lambda: LuxonisCameraConfig(name="left_wrist_camera")
63
+ )
64
+ right_wrist_camera: LuxonisCameraConfig = field(
65
+ default_factory=lambda: LuxonisCameraConfig(name="right_wrist_camera")
66
+ )
61
67
  base_left_camera: RGBCameraConfig = field(default_factory=_make_rgb_camera("left"))
62
68
  base_right_camera: RGBCameraConfig = field(
63
69
  default_factory=_make_rgb_camera("right")
@@ -73,6 +79,8 @@ class VegaSensorsConfig:
73
79
 
74
80
  def __post_init__(self) -> None:
75
81
  self.head_camera.enable = False
82
+ self.left_wrist_camera.enable = False
83
+ self.right_wrist_camera.enable = False
76
84
  self.base_left_camera.enable = False
77
85
  self.base_right_camera.enable = False
78
86
  self.base_front_camera.enable = False
dexcontrol/config/vega.py CHANGED
@@ -52,6 +52,7 @@ class VegaConfig:
52
52
  "folded": [1.57079, 0.0, 0, -3.1, 0, 0, -0.69813],
53
53
  "folded_closed_hand": [1.57079, 0.0, 0, -3.1, 0, 0, -0.9],
54
54
  "L_shape": [0.064, -0.3, 0.0, -1.556, 1.271, 0.0, 0.0],
55
+ "lift_up": [0.064, -0.3, 0.0, -2.756, 1.271, 0.0, 0.0],
55
56
  "zero": [-1.57079, 0.0, 0, 0.0, 0, 0, 0.0],
56
57
  },
57
58
  )
@@ -67,6 +68,7 @@ class VegaConfig:
67
68
  "folded": [-1.57079, 0.0, 0, -3.1, 0, 0, 0.69813],
68
69
  "folded_closed_hand": [-1.57079, 0.0, 0, -3.1, 0, 0, 0.9],
69
70
  "L_shape": [-0.064, 0.3, 0.0, -1.556, -1.271, 0.0, 0.0],
71
+ "lift_up": [-0.064, 0.3, 0.0, -2.756, -1.271, 0.0, 0.0],
70
72
  "zero": [1.57079, 0.0, 0, 0.0, 0, 0, 0.0],
71
73
  },
72
74
  )
@@ -114,6 +116,7 @@ class VegaConfig:
114
116
  reboot_query_name: str = "system/reboot"
115
117
  clear_error_query_name: str = "system/clear_error"
116
118
  led_query_name: str = "system/led"
119
+ soc_ntp_query_name: str = "time/soc"
117
120
 
118
121
  @classmethod
119
122
  def register_variant(
@@ -138,11 +141,22 @@ class Vega1Config(VegaConfig):
138
141
  control_pub_topic="control/arm/left",
139
142
  set_mode_query="mode/arm/left",
140
143
  wrench_sub_topic="state/wrench/left",
144
+ ee_pass_through_pub_topic="control/ee_pass_through/left",
141
145
  joint_name=[f"L_arm_j{i + 1}" for i in range(7)],
146
+ joint_limit=[
147
+ [-3.071, 3.071],
148
+ [-0.453, 1.553],
149
+ [-3.071, 3.071],
150
+ [-3.071, 0.244],
151
+ [-3.071, 3.071],
152
+ [-1.396, 1.396],
153
+ [-1.378, 1.117],
154
+ ],
142
155
  pose_pool={
143
156
  "folded": [1.57079, 0.0, 0, -3.1, 0, 0, -0.69813],
144
157
  "folded_closed_hand": [1.57079, 0.0, 0, -3.1, 0, 0, -0.9],
145
158
  "L_shape": [0.064, 0.3, 0.0, -1.556, 1.271, 0.0, 0.0],
159
+ "lift_up": [0.064, 0.3, 0.0, -2.756, 1.271, 0.0, 0.0],
146
160
  "zero": [-1.57079, 0.0, 0, 0.0, 0, 0, 0.0],
147
161
  },
148
162
  )
@@ -153,11 +167,22 @@ class Vega1Config(VegaConfig):
153
167
  control_pub_topic="control/arm/right",
154
168
  set_mode_query="mode/arm/right",
155
169
  wrench_sub_topic="state/wrench/right",
170
+ ee_pass_through_pub_topic="control/ee_pass_through/right",
156
171
  joint_name=[f"R_arm_j{i + 1}" for i in range(7)],
172
+ joint_limit=[
173
+ [-3.071, 3.071],
174
+ [-1.553, 0.453],
175
+ [-3.071, 3.071],
176
+ [-3.071, 0.244],
177
+ [-3.071, 3.071],
178
+ [-1.396, 1.396],
179
+ [-1.117, 1.378],
180
+ ],
157
181
  pose_pool={
158
182
  "folded": [-1.57079, 0.0, 0, -3.1, 0, 0, 0.69813],
159
183
  "folded_closed_hand": [-1.57079, 0.0, 0, -3.1, 0, 0, 0.9],
160
184
  "L_shape": [-0.064, -0.3, 0.0, -1.556, -1.271, 0.0, 0.0],
185
+ "lift_up": [-0.064, -0.3, 0.0, -2.756, -1.271, 0.0, 0.0],
161
186
  "zero": [1.57079, 0.0, 0, 0.0, 0, 0, 0.0],
162
187
  },
163
188
  )
@@ -169,8 +194,11 @@ class Vega1Config(VegaConfig):
169
194
  )
170
195
  head: HeadConfig = field(
171
196
  default_factory=lambda: HeadConfig(
172
- joint_limit_lower=[-1.483, -2.792, -1.378],
173
- joint_limit_upper=[1.483, 2.792, 1.483],
197
+ joint_limit=[
198
+ [-1.483, 1.483],
199
+ [-2.792, 2.792],
200
+ [-1.378, 1.378],
201
+ ],
174
202
  )
175
203
  )
176
204
 
dexcontrol/core/arm.py CHANGED
@@ -15,7 +15,7 @@ communication and the ArmWrenchSensor class for reading wrench sensor data.
15
15
  """
16
16
 
17
17
  import time
18
- from typing import Final, Literal
18
+ from typing import Any, Final, Literal
19
19
 
20
20
  import numpy as np
21
21
  import zenoh
@@ -58,6 +58,12 @@ class Arm(RobotJointComponent):
58
58
  state_message_type=dexcontrol_msg_pb2.ArmState,
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
 
@@ -72,6 +78,22 @@ class Arm(RobotJointComponent):
72
78
  configs.wrench_sub_topic, zenoh_session
73
79
  )
74
80
 
81
+ # Initialize end effector pass through publisher
82
+ self._ee_pass_through_publisher: Final[zenoh.Publisher] = (
83
+ zenoh_session.declare_publisher(
84
+ resolve_key_name(configs.ee_pass_through_pub_topic)
85
+ )
86
+ )
87
+
88
+ self._default_control_hz = configs.default_control_hz
89
+ if self._joint_vel_limit is not None:
90
+ if np.any(self._joint_vel_limit > 2.8):
91
+ logger.warning(
92
+ "Joint velocity limit is greater than 2.8. This is not recommended."
93
+ )
94
+ self._joint_vel_limit = np.clip(self._joint_vel_limit, 0, 2.8)
95
+ logger.warning("Joint velocity limit is clamped to 2.8")
96
+
75
97
  def set_mode(self, mode: Literal["position", "disable"]) -> None:
76
98
  """Sets the operating mode of the arm.
77
99
 
@@ -112,6 +134,8 @@ class Arm(RobotJointComponent):
112
134
  relative: bool = False,
113
135
  wait_time: float = 0.0,
114
136
  wait_kwargs: dict[str, float] | None = None,
137
+ exit_on_reach: bool = False,
138
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
115
139
  ) -> None:
116
140
  """Controls the arm in joint position mode.
117
141
 
@@ -126,11 +150,19 @@ class Arm(RobotJointComponent):
126
150
  immediately. If wait_time is greater than 0, the joint positions will
127
151
  be interpolated between the current position and the target position,
128
152
  and the function will wait for the specified time between each movement.
153
+
154
+ **IMPORTANT: When wait_time is 0, you MUST call this function repeatedly
155
+ in a high-frequency loop (e.g., 100 Hz). DO NOT call it just once!**
156
+ The function is designed for continuous control when wait_time=0.
157
+ The highest frequency that the user can call this function is 500 Hz.
158
+
159
+
129
160
  wait_kwargs: Keyword arguments for the interpolation (only used if
130
161
  wait_time > 0). Supported keys:
131
162
  - control_hz: Control frequency in Hz (default: 100).
132
163
  - max_vel: Maximum velocity in rad/s (default: 0.5).
133
-
164
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
165
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
134
166
  Raises:
135
167
  ValueError: If joint_pos dictionary contains invalid joint names.
136
168
  """
@@ -140,53 +172,58 @@ class Arm(RobotJointComponent):
140
172
  resolved_joint_pos = (
141
173
  self._resolve_relative_joint_cmd(joint_pos) if relative else joint_pos
142
174
  )
175
+ resolved_joint_pos = self._convert_joint_cmd_to_array(resolved_joint_pos)
176
+ if self._joint_limit is not None:
177
+ resolved_joint_pos = np.clip(
178
+ resolved_joint_pos, self._joint_limit[:, 0], self._joint_limit[:, 1]
179
+ )
143
180
 
144
181
  if wait_time > 0.0:
145
- self._execute_trajectory_motion(resolved_joint_pos, wait_time, wait_kwargs)
182
+ self._execute_trajectory_motion(
183
+ resolved_joint_pos,
184
+ wait_time,
185
+ wait_kwargs,
186
+ exit_on_reach,
187
+ exit_on_reach_kwargs,
188
+ )
146
189
  else:
147
- # Convert to array format
148
- if isinstance(resolved_joint_pos, (list, dict)):
149
- resolved_joint_pos = self._convert_joint_cmd_to_array(
150
- resolved_joint_pos
151
- )
152
190
  self._send_position_command(resolved_joint_pos)
153
191
 
154
192
  def _execute_trajectory_motion(
155
193
  self,
156
- joint_pos: Float[np.ndarray, " N"] | list[float] | dict[str, float],
194
+ target_joint_pos: Float[np.ndarray, " N"],
157
195
  wait_time: float,
158
196
  wait_kwargs: dict[str, float],
197
+ exit_on_reach: bool = False,
198
+ exit_on_reach_kwargs: dict[str, Any] | None = None,
159
199
  ) -> None:
160
200
  """Execute trajectory-based motion to target position.
161
201
 
162
202
  Args:
163
- joint_pos: Target joint positions as list, numpy array, or dictionary.
203
+ target_joint_pos: Target joint positions as numpy array.
164
204
  wait_time: Total time for the motion.
165
205
  wait_kwargs: Parameters for trajectory generation.
206
+ exit_on_reach: If True, the function will exit when the joint positions are reached.
207
+ exit_on_reach_kwargs: Optional parameters for exit when the joint positions are reached.
166
208
  """
167
209
  # Set default parameters
168
- control_hz = wait_kwargs.get("control_hz", 100)
169
- max_vel = wait_kwargs.get("max_vel", 0.5)
210
+ control_hz = wait_kwargs.get("control_hz", self._default_control_hz)
211
+ max_vel = wait_kwargs.get("max_vel")
212
+ if max_vel is None:
213
+ max_vel = (
214
+ self._joint_vel_limit if self._joint_vel_limit is not None else 2.8
215
+ )
216
+ exit_on_reach_kwargs = exit_on_reach_kwargs or {}
217
+ exit_on_reach_kwargs.setdefault("tolerance", 0.05)
170
218
 
171
219
  # Create rate limiter and get current position
172
220
  rate_limiter = RateLimiter(control_hz)
173
221
  current_joint_pos = self.get_joint_pos().copy()
174
222
 
175
- # Convert input to numpy array for trajectory generation
176
- if isinstance(joint_pos, (list, dict)):
177
- target_pos = (
178
- self._convert_dict_to_array(joint_pos)
179
- if isinstance(joint_pos, dict)
180
- else np.array(joint_pos, dtype=np.float32)
181
- )
182
- else:
183
- target_pos = joint_pos
184
-
185
223
  # Generate trajectory using utility function
186
224
  trajectory, _ = generate_linear_trajectory(
187
- current_joint_pos, target_pos, max_vel, control_hz
225
+ current_joint_pos, target_joint_pos, max_vel, control_hz
188
226
  )
189
-
190
227
  # Execute trajectory with time limit
191
228
  start_time = time.time()
192
229
  for pos in trajectory:
@@ -197,8 +234,12 @@ class Arm(RobotJointComponent):
197
234
 
198
235
  # Hold final position for remaining time
199
236
  while time.time() - start_time < wait_time:
200
- self._send_position_command(target_pos)
237
+ self._send_position_command(target_joint_pos)
201
238
  rate_limiter.sleep()
239
+ if exit_on_reach and self.is_joint_pos_reached(
240
+ target_joint_pos, **exit_on_reach_kwargs
241
+ ):
242
+ break
202
243
 
203
244
  def set_joint_pos_vel(
204
245
  self,
@@ -208,6 +249,16 @@ class Arm(RobotJointComponent):
208
249
  ) -> None:
209
250
  """Controls the arm in joint position mode with a velocity feedforward term.
210
251
 
252
+ Warning:
253
+ The joint_vel parameter should be well-planned (such as from a trajectory planner).
254
+ Sending poorly planned or inappropriate joint velocity commands can cause the robot
255
+ to behave unexpectedly or potentially get damaged. Ensure velocity commands are
256
+ smooth, within safe limits, and properly coordinated across all joints.
257
+
258
+ Additionally, this command MUST be called at high frequency (e.g., 100Hz) to take
259
+ effect properly. DO NOT call this function just once or at low frequency, as this
260
+ can lead to unpredictable robot behavior.
261
+
211
262
  Args:
212
263
  joint_pos: Joint positions as either:
213
264
  - List of joint values [j1, j2, ..., j7]
@@ -217,6 +268,7 @@ class Arm(RobotJointComponent):
217
268
  - List of joint values [v1, v2, ..., v7]
218
269
  - Numpy array with shape (7,), in radians/sec
219
270
  - Dictionary of joint names and velocity values
271
+
220
272
  relative: If True, the joint positions are relative to the current position.
221
273
 
222
274
  Raises:
@@ -225,25 +277,15 @@ class Arm(RobotJointComponent):
225
277
  resolved_joint_pos = (
226
278
  self._resolve_relative_joint_cmd(joint_pos) if relative else joint_pos
227
279
  )
228
-
229
- # Convert inputs to numpy arrays
230
- if isinstance(resolved_joint_pos, (list, dict)):
231
- target_pos = (
232
- self._convert_dict_to_array(resolved_joint_pos)
233
- if isinstance(resolved_joint_pos, dict)
234
- else np.array(resolved_joint_pos, dtype=np.float32)
235
- )
236
- else:
237
- target_pos = resolved_joint_pos
238
-
239
- if isinstance(joint_vel, (list, dict)):
240
- target_vel = (
241
- self._convert_dict_to_array(joint_vel)
242
- if isinstance(joint_vel, dict)
243
- else np.array(joint_vel, dtype=np.float32)
280
+ resolved_joint_pos = self._convert_joint_cmd_to_array(resolved_joint_pos)
281
+ if self._joint_limit is not None:
282
+ resolved_joint_pos = np.clip(
283
+ resolved_joint_pos, self._joint_limit[:, 0], self._joint_limit[:, 1]
244
284
  )
245
- else:
246
- target_vel = joint_vel
285
+ target_pos = resolved_joint_pos
286
+ target_vel = self._convert_joint_cmd_to_array(
287
+ joint_vel, clip_value=self._joint_vel_limit
288
+ )
247
289
 
248
290
  control_msg = dexcontrol_msg_pb2.ArmCommand(
249
291
  command_type=dexcontrol_msg_pb2.ArmCommand.CommandType.VELOCITY_FEEDFORWARD,
@@ -269,6 +311,16 @@ class Arm(RobotJointComponent):
269
311
  if self.wrench_sensor:
270
312
  self.wrench_sensor.shutdown()
271
313
 
314
+ def send_ee_pass_through_message(self, message: bytes):
315
+ """Send an end effector pass through message to the robot arm.
316
+
317
+ Args:
318
+ message: The message to send to the robot arm.
319
+ """
320
+ control_msg = dexcontrol_msg_pb2.EndEffectorPassThroughCommand(data=message)
321
+ control_data = control_msg.SerializeToString()
322
+ self._ee_pass_through_publisher.put(control_data)
323
+
272
324
 
273
325
  class ArmWrenchSensor(RobotComponent):
274
326
  """Wrench sensor reader for the robot arm.