dexcontrol 0.2.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.
- dexcontrol/__init__.py +45 -0
- dexcontrol/apps/dualsense_teleop_base.py +371 -0
- dexcontrol/config/__init__.py +14 -0
- dexcontrol/config/core/__init__.py +22 -0
- dexcontrol/config/core/arm.py +32 -0
- dexcontrol/config/core/chassis.py +22 -0
- dexcontrol/config/core/hand.py +42 -0
- dexcontrol/config/core/head.py +33 -0
- dexcontrol/config/core/misc.py +37 -0
- dexcontrol/config/core/torso.py +36 -0
- dexcontrol/config/sensors/__init__.py +4 -0
- dexcontrol/config/sensors/cameras/__init__.py +7 -0
- dexcontrol/config/sensors/cameras/gemini_camera.py +16 -0
- dexcontrol/config/sensors/cameras/rgb_camera.py +15 -0
- dexcontrol/config/sensors/imu/__init__.py +6 -0
- dexcontrol/config/sensors/imu/gemini_imu.py +15 -0
- dexcontrol/config/sensors/imu/nine_axis_imu.py +15 -0
- dexcontrol/config/sensors/lidar/__init__.py +6 -0
- dexcontrol/config/sensors/lidar/rplidar.py +15 -0
- dexcontrol/config/sensors/ultrasonic/__init__.py +6 -0
- dexcontrol/config/sensors/ultrasonic/ultrasonic.py +15 -0
- dexcontrol/config/sensors/vega_sensors.py +65 -0
- dexcontrol/config/vega.py +203 -0
- dexcontrol/core/__init__.py +0 -0
- dexcontrol/core/arm.py +324 -0
- dexcontrol/core/chassis.py +628 -0
- dexcontrol/core/component.py +834 -0
- dexcontrol/core/hand.py +170 -0
- dexcontrol/core/head.py +232 -0
- dexcontrol/core/misc.py +514 -0
- dexcontrol/core/torso.py +198 -0
- dexcontrol/proto/dexcontrol_msg_pb2.py +69 -0
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +168 -0
- dexcontrol/proto/dexcontrol_query_pb2.py +73 -0
- dexcontrol/proto/dexcontrol_query_pb2.pyi +134 -0
- dexcontrol/robot.py +1091 -0
- dexcontrol/sensors/__init__.py +40 -0
- dexcontrol/sensors/camera/__init__.py +18 -0
- dexcontrol/sensors/camera/gemini_camera.py +139 -0
- dexcontrol/sensors/camera/rgb_camera.py +98 -0
- dexcontrol/sensors/imu/__init__.py +22 -0
- dexcontrol/sensors/imu/gemini_imu.py +139 -0
- dexcontrol/sensors/imu/nine_axis_imu.py +149 -0
- dexcontrol/sensors/lidar/__init__.py +3 -0
- dexcontrol/sensors/lidar/rplidar.py +164 -0
- dexcontrol/sensors/manager.py +185 -0
- dexcontrol/sensors/ultrasonic.py +110 -0
- dexcontrol/utils/__init__.py +15 -0
- dexcontrol/utils/constants.py +12 -0
- dexcontrol/utils/io_utils.py +26 -0
- dexcontrol/utils/motion_utils.py +194 -0
- dexcontrol/utils/os_utils.py +39 -0
- dexcontrol/utils/pb_utils.py +103 -0
- dexcontrol/utils/rate_limiter.py +167 -0
- dexcontrol/utils/reset_orbbec_camera_usb.py +98 -0
- dexcontrol/utils/subscribers/__init__.py +44 -0
- dexcontrol/utils/subscribers/base.py +260 -0
- dexcontrol/utils/subscribers/camera.py +328 -0
- dexcontrol/utils/subscribers/decoders.py +83 -0
- dexcontrol/utils/subscribers/generic.py +105 -0
- dexcontrol/utils/subscribers/imu.py +170 -0
- dexcontrol/utils/subscribers/lidar.py +195 -0
- dexcontrol/utils/subscribers/protobuf.py +106 -0
- dexcontrol/utils/timer.py +136 -0
- dexcontrol/utils/trajectory_utils.py +40 -0
- dexcontrol/utils/viz_utils.py +86 -0
- dexcontrol-0.2.1.dist-info/METADATA +369 -0
- dexcontrol-0.2.1.dist-info/RECORD +72 -0
- dexcontrol-0.2.1.dist-info/WHEEL +5 -0
- dexcontrol-0.2.1.dist-info/licenses/LICENSE +188 -0
- dexcontrol-0.2.1.dist-info/licenses/NOTICE +13 -0
- dexcontrol-0.2.1.dist-info/top_level.txt +1 -0
dexcontrol/core/arm.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Copyright (c) 2025 Dexmate CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 with Commons Clause License
|
|
4
|
+
# Condition v1.0 [see LICENSE for details].
|
|
5
|
+
|
|
6
|
+
"""Robot arm control module.
|
|
7
|
+
|
|
8
|
+
This module provides the Arm class for controlling a robot arm through Zenoh
|
|
9
|
+
communication and the ArmWrenchSensor class for reading wrench sensor data.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import Final, Literal
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import zenoh
|
|
17
|
+
from jaxtyping import Float
|
|
18
|
+
|
|
19
|
+
from dexcontrol.config.core.arm import ArmConfig
|
|
20
|
+
from dexcontrol.core.component import RobotComponent, RobotJointComponent
|
|
21
|
+
from dexcontrol.proto import dexcontrol_msg_pb2, dexcontrol_query_pb2
|
|
22
|
+
from dexcontrol.utils.os_utils import resolve_key_name
|
|
23
|
+
from dexcontrol.utils.rate_limiter import RateLimiter
|
|
24
|
+
from dexcontrol.utils.trajectory_utils import generate_linear_trajectory
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Arm(RobotJointComponent):
|
|
28
|
+
"""Robot arm control class.
|
|
29
|
+
|
|
30
|
+
This class provides methods to control a robot arm by publishing commands and
|
|
31
|
+
receiving state information through Zenoh communication.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
mode_querier: Zenoh querier for setting arm mode.
|
|
35
|
+
wrench_sensor: Optional ArmWrenchSensor instance for wrench sensor data.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
configs: ArmConfig,
|
|
41
|
+
zenoh_session: zenoh.Session,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Initialize the arm controller.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
configs: Configuration parameters for the arm including communication topics.
|
|
47
|
+
zenoh_session: Active Zenoh communication session for message passing.
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(
|
|
50
|
+
state_sub_topic=configs.state_sub_topic,
|
|
51
|
+
control_pub_topic=configs.control_pub_topic,
|
|
52
|
+
state_message_type=dexcontrol_msg_pb2.ArmState,
|
|
53
|
+
zenoh_session=zenoh_session,
|
|
54
|
+
joint_name=configs.joint_name,
|
|
55
|
+
pose_pool=configs.pose_pool,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.mode_querier: Final[zenoh.Querier] = zenoh_session.declare_querier(
|
|
59
|
+
resolve_key_name(configs.set_mode_query), timeout=2.0
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Initialize wrench sensor if configured
|
|
63
|
+
self.wrench_sensor: ArmWrenchSensor | None = None
|
|
64
|
+
if configs.wrench_sub_topic:
|
|
65
|
+
self.wrench_sensor = ArmWrenchSensor(
|
|
66
|
+
configs.wrench_sub_topic, zenoh_session
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def set_mode(self, mode: Literal["position", "disable"]) -> None:
|
|
70
|
+
"""Sets the operating mode of the arm.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
mode: Operating mode for the arm. Must be either "position" or "disable".
|
|
74
|
+
"position": Enable position control
|
|
75
|
+
"disable": Disable control
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: If an invalid mode is specified.
|
|
79
|
+
"""
|
|
80
|
+
mode_map = {
|
|
81
|
+
"position": dexcontrol_query_pb2.SetArmMode.Mode.POSITION,
|
|
82
|
+
"disable": dexcontrol_query_pb2.SetArmMode.Mode.DISABLE,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if mode not in mode_map:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Invalid mode: {mode}. Must be one of {list(mode_map.keys())}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
query_msg = dexcontrol_query_pb2.SetArmMode(mode=mode_map[mode])
|
|
91
|
+
self.mode_querier.get(payload=query_msg.SerializeToString())
|
|
92
|
+
|
|
93
|
+
def _send_position_command(self, joint_pos: np.ndarray) -> None:
|
|
94
|
+
"""Send joint position command.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
joint_pos: Joint positions as numpy array.
|
|
98
|
+
"""
|
|
99
|
+
control_msg = dexcontrol_msg_pb2.ArmCommand()
|
|
100
|
+
control_msg.joint_pos.extend(joint_pos.astype(np.float32).tolist())
|
|
101
|
+
self._publish_control(control_msg)
|
|
102
|
+
|
|
103
|
+
def set_joint_pos(
|
|
104
|
+
self,
|
|
105
|
+
joint_pos: Float[np.ndarray, " N"] | list[float] | dict[str, float],
|
|
106
|
+
relative: bool = False,
|
|
107
|
+
wait_time: float = 0.0,
|
|
108
|
+
wait_kwargs: dict[str, float] | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Controls the arm in joint position mode.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
joint_pos: Joint positions as either:
|
|
114
|
+
- List of joint values [j1, j2, ..., j7]
|
|
115
|
+
- Numpy array with shape (7,), in radians
|
|
116
|
+
- Dictionary mapping joint names to position values
|
|
117
|
+
relative: If True, the joint positions are relative to the current position.
|
|
118
|
+
wait_time: Time to wait between movements in seconds. If wait_time is 0,
|
|
119
|
+
the joint positions will be sent, and the function call will return
|
|
120
|
+
immediately. If wait_time is greater than 0, the joint positions will
|
|
121
|
+
be interpolated between the current position and the target position,
|
|
122
|
+
and the function will wait for the specified time between each movement.
|
|
123
|
+
wait_kwargs: Keyword arguments for the interpolation (only used if
|
|
124
|
+
wait_time > 0). Supported keys:
|
|
125
|
+
- control_hz: Control frequency in Hz (default: 100).
|
|
126
|
+
- max_vel: Maximum velocity in rad/s (default: 0.5).
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If joint_pos dictionary contains invalid joint names.
|
|
130
|
+
"""
|
|
131
|
+
if wait_kwargs is None:
|
|
132
|
+
wait_kwargs = {}
|
|
133
|
+
|
|
134
|
+
resolved_joint_pos = (
|
|
135
|
+
self._resolve_relative_joint_cmd(joint_pos) if relative else joint_pos
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if wait_time > 0.0:
|
|
139
|
+
self._execute_trajectory_motion(resolved_joint_pos, wait_time, wait_kwargs)
|
|
140
|
+
else:
|
|
141
|
+
# Convert to array format
|
|
142
|
+
if isinstance(resolved_joint_pos, (list, dict)):
|
|
143
|
+
resolved_joint_pos = self._convert_joint_cmd_to_array(
|
|
144
|
+
resolved_joint_pos
|
|
145
|
+
)
|
|
146
|
+
self._send_position_command(resolved_joint_pos)
|
|
147
|
+
|
|
148
|
+
def _execute_trajectory_motion(
|
|
149
|
+
self,
|
|
150
|
+
joint_pos: Float[np.ndarray, " N"] | list[float] | dict[str, float],
|
|
151
|
+
wait_time: float,
|
|
152
|
+
wait_kwargs: dict[str, float],
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Execute trajectory-based motion to target position.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
joint_pos: Target joint positions as list, numpy array, or dictionary.
|
|
158
|
+
wait_time: Total time for the motion.
|
|
159
|
+
wait_kwargs: Parameters for trajectory generation.
|
|
160
|
+
"""
|
|
161
|
+
# Set default parameters
|
|
162
|
+
control_hz = wait_kwargs.get("control_hz", 100)
|
|
163
|
+
max_vel = wait_kwargs.get("max_vel", 0.5)
|
|
164
|
+
|
|
165
|
+
# Create rate limiter and get current position
|
|
166
|
+
rate_limiter = RateLimiter(control_hz)
|
|
167
|
+
current_joint_pos = self.get_joint_pos().copy()
|
|
168
|
+
|
|
169
|
+
# Convert input to numpy array for trajectory generation
|
|
170
|
+
if isinstance(joint_pos, (list, dict)):
|
|
171
|
+
target_pos = (
|
|
172
|
+
self._convert_dict_to_array(joint_pos)
|
|
173
|
+
if isinstance(joint_pos, dict)
|
|
174
|
+
else np.array(joint_pos, dtype=np.float32)
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
target_pos = joint_pos
|
|
178
|
+
|
|
179
|
+
# Generate trajectory using utility function
|
|
180
|
+
trajectory, _ = generate_linear_trajectory(
|
|
181
|
+
current_joint_pos, target_pos, max_vel, control_hz
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Execute trajectory with time limit
|
|
185
|
+
start_time = time.time()
|
|
186
|
+
for pos in trajectory:
|
|
187
|
+
if time.time() - start_time > wait_time:
|
|
188
|
+
break
|
|
189
|
+
self._send_position_command(pos)
|
|
190
|
+
rate_limiter.sleep()
|
|
191
|
+
|
|
192
|
+
# Hold final position for remaining time
|
|
193
|
+
while time.time() - start_time < wait_time:
|
|
194
|
+
self._send_position_command(target_pos)
|
|
195
|
+
rate_limiter.sleep()
|
|
196
|
+
|
|
197
|
+
def set_joint_pos_vel(
|
|
198
|
+
self,
|
|
199
|
+
joint_pos: Float[np.ndarray, " N"] | list[float] | dict[str, float],
|
|
200
|
+
joint_vel: Float[np.ndarray, " N"] | list[float] | dict[str, float],
|
|
201
|
+
relative: bool = False,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Controls the arm in joint position mode with a velocity feedforward term.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
joint_pos: Joint positions as either:
|
|
207
|
+
- List of joint values [j1, j2, ..., j7]
|
|
208
|
+
- Numpy array with shape (7,), in radians
|
|
209
|
+
- Dictionary of joint names and position values
|
|
210
|
+
joint_vel: Joint velocities as either:
|
|
211
|
+
- List of joint values [v1, v2, ..., v7]
|
|
212
|
+
- Numpy array with shape (7,), in radians/sec
|
|
213
|
+
- Dictionary of joint names and velocity values
|
|
214
|
+
relative: If True, the joint positions are relative to the current position.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
ValueError: If joint_pos dictionary contains invalid joint names.
|
|
218
|
+
"""
|
|
219
|
+
resolved_joint_pos = (
|
|
220
|
+
self._resolve_relative_joint_cmd(joint_pos) if relative else joint_pos
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Convert inputs to numpy arrays
|
|
224
|
+
if isinstance(resolved_joint_pos, (list, dict)):
|
|
225
|
+
target_pos = (
|
|
226
|
+
self._convert_dict_to_array(resolved_joint_pos)
|
|
227
|
+
if isinstance(resolved_joint_pos, dict)
|
|
228
|
+
else np.array(resolved_joint_pos, dtype=np.float32)
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
target_pos = resolved_joint_pos
|
|
232
|
+
|
|
233
|
+
if isinstance(joint_vel, (list, dict)):
|
|
234
|
+
target_vel = (
|
|
235
|
+
self._convert_dict_to_array(joint_vel)
|
|
236
|
+
if isinstance(joint_vel, dict)
|
|
237
|
+
else np.array(joint_vel, dtype=np.float32)
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
target_vel = joint_vel
|
|
241
|
+
|
|
242
|
+
control_msg = dexcontrol_msg_pb2.ArmCommand(
|
|
243
|
+
command_type=dexcontrol_msg_pb2.ArmCommand.CommandType.VELOCITY_FEEDFORWARD,
|
|
244
|
+
joint_pos=list(target_pos),
|
|
245
|
+
joint_vel=list(target_vel),
|
|
246
|
+
)
|
|
247
|
+
self._publish_control(control_msg)
|
|
248
|
+
|
|
249
|
+
def shutdown(self) -> None:
|
|
250
|
+
"""Cleans up all Zenoh resources."""
|
|
251
|
+
super().shutdown()
|
|
252
|
+
if hasattr(self, "mode_querier") and self.mode_querier:
|
|
253
|
+
self.mode_querier.undeclare()
|
|
254
|
+
if self.wrench_sensor:
|
|
255
|
+
self.wrench_sensor.shutdown()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class ArmWrenchSensor(RobotComponent):
|
|
259
|
+
"""Wrench sensor reader for the robot arm.
|
|
260
|
+
|
|
261
|
+
This class provides methods to read wrench sensor data through Zenoh communication.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def __init__(self, state_sub_topic: str, zenoh_session: zenoh.Session) -> None:
|
|
265
|
+
"""Initialize the wrench sensor reader.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
state_sub_topic: Topic to subscribe to for wrench sensor data.
|
|
269
|
+
zenoh_session: Active Zenoh communication session for message passing.
|
|
270
|
+
"""
|
|
271
|
+
super().__init__(
|
|
272
|
+
state_sub_topic=state_sub_topic,
|
|
273
|
+
zenoh_session=zenoh_session,
|
|
274
|
+
state_message_type=dexcontrol_msg_pb2.WrenchState,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def get_wrench_state(self) -> Float[np.ndarray, "6"]:
|
|
278
|
+
"""Get the current wrench sensor reading.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Array of wrench values [fx, fy, fz, tx, ty, tz].
|
|
282
|
+
"""
|
|
283
|
+
state = self._get_state()
|
|
284
|
+
return np.array(state.wrench, dtype=np.float32)
|
|
285
|
+
|
|
286
|
+
def get_button_state(self) -> tuple[bool, bool]:
|
|
287
|
+
"""Get the state of the wrench sensor buttons.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Tuple of (blue_button_state, green_button_state).
|
|
291
|
+
"""
|
|
292
|
+
state = self._get_state()
|
|
293
|
+
return state.blue_button, state.green_button
|
|
294
|
+
|
|
295
|
+
def get_state(self) -> dict[str, Float[np.ndarray, "6"] | bool]:
|
|
296
|
+
"""Get the complete wrench sensor state.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Dictionary containing wrench values and button states.
|
|
300
|
+
"""
|
|
301
|
+
state = self._get_state()
|
|
302
|
+
return {
|
|
303
|
+
"wrench": np.array(state.wrench, dtype=np.float32),
|
|
304
|
+
"blue_button": state.blue_button,
|
|
305
|
+
"green_button": state.green_button,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
def get_blue_button_state(self) -> bool:
|
|
309
|
+
"""Get the state of the blue button.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
True if the blue button is pressed, False otherwise.
|
|
313
|
+
"""
|
|
314
|
+
state = self._get_state()
|
|
315
|
+
return state.blue_button
|
|
316
|
+
|
|
317
|
+
def get_green_button_state(self) -> bool:
|
|
318
|
+
"""Get the state of the green button.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if the green button is pressed, False otherwise.
|
|
322
|
+
"""
|
|
323
|
+
state = self._get_state()
|
|
324
|
+
return state.green_button
|