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.

Files changed (72) hide show
  1. dexcontrol/__init__.py +45 -0
  2. dexcontrol/apps/dualsense_teleop_base.py +371 -0
  3. dexcontrol/config/__init__.py +14 -0
  4. dexcontrol/config/core/__init__.py +22 -0
  5. dexcontrol/config/core/arm.py +32 -0
  6. dexcontrol/config/core/chassis.py +22 -0
  7. dexcontrol/config/core/hand.py +42 -0
  8. dexcontrol/config/core/head.py +33 -0
  9. dexcontrol/config/core/misc.py +37 -0
  10. dexcontrol/config/core/torso.py +36 -0
  11. dexcontrol/config/sensors/__init__.py +4 -0
  12. dexcontrol/config/sensors/cameras/__init__.py +7 -0
  13. dexcontrol/config/sensors/cameras/gemini_camera.py +16 -0
  14. dexcontrol/config/sensors/cameras/rgb_camera.py +15 -0
  15. dexcontrol/config/sensors/imu/__init__.py +6 -0
  16. dexcontrol/config/sensors/imu/gemini_imu.py +15 -0
  17. dexcontrol/config/sensors/imu/nine_axis_imu.py +15 -0
  18. dexcontrol/config/sensors/lidar/__init__.py +6 -0
  19. dexcontrol/config/sensors/lidar/rplidar.py +15 -0
  20. dexcontrol/config/sensors/ultrasonic/__init__.py +6 -0
  21. dexcontrol/config/sensors/ultrasonic/ultrasonic.py +15 -0
  22. dexcontrol/config/sensors/vega_sensors.py +65 -0
  23. dexcontrol/config/vega.py +203 -0
  24. dexcontrol/core/__init__.py +0 -0
  25. dexcontrol/core/arm.py +324 -0
  26. dexcontrol/core/chassis.py +628 -0
  27. dexcontrol/core/component.py +834 -0
  28. dexcontrol/core/hand.py +170 -0
  29. dexcontrol/core/head.py +232 -0
  30. dexcontrol/core/misc.py +514 -0
  31. dexcontrol/core/torso.py +198 -0
  32. dexcontrol/proto/dexcontrol_msg_pb2.py +69 -0
  33. dexcontrol/proto/dexcontrol_msg_pb2.pyi +168 -0
  34. dexcontrol/proto/dexcontrol_query_pb2.py +73 -0
  35. dexcontrol/proto/dexcontrol_query_pb2.pyi +134 -0
  36. dexcontrol/robot.py +1091 -0
  37. dexcontrol/sensors/__init__.py +40 -0
  38. dexcontrol/sensors/camera/__init__.py +18 -0
  39. dexcontrol/sensors/camera/gemini_camera.py +139 -0
  40. dexcontrol/sensors/camera/rgb_camera.py +98 -0
  41. dexcontrol/sensors/imu/__init__.py +22 -0
  42. dexcontrol/sensors/imu/gemini_imu.py +139 -0
  43. dexcontrol/sensors/imu/nine_axis_imu.py +149 -0
  44. dexcontrol/sensors/lidar/__init__.py +3 -0
  45. dexcontrol/sensors/lidar/rplidar.py +164 -0
  46. dexcontrol/sensors/manager.py +185 -0
  47. dexcontrol/sensors/ultrasonic.py +110 -0
  48. dexcontrol/utils/__init__.py +15 -0
  49. dexcontrol/utils/constants.py +12 -0
  50. dexcontrol/utils/io_utils.py +26 -0
  51. dexcontrol/utils/motion_utils.py +194 -0
  52. dexcontrol/utils/os_utils.py +39 -0
  53. dexcontrol/utils/pb_utils.py +103 -0
  54. dexcontrol/utils/rate_limiter.py +167 -0
  55. dexcontrol/utils/reset_orbbec_camera_usb.py +98 -0
  56. dexcontrol/utils/subscribers/__init__.py +44 -0
  57. dexcontrol/utils/subscribers/base.py +260 -0
  58. dexcontrol/utils/subscribers/camera.py +328 -0
  59. dexcontrol/utils/subscribers/decoders.py +83 -0
  60. dexcontrol/utils/subscribers/generic.py +105 -0
  61. dexcontrol/utils/subscribers/imu.py +170 -0
  62. dexcontrol/utils/subscribers/lidar.py +195 -0
  63. dexcontrol/utils/subscribers/protobuf.py +106 -0
  64. dexcontrol/utils/timer.py +136 -0
  65. dexcontrol/utils/trajectory_utils.py +40 -0
  66. dexcontrol/utils/viz_utils.py +86 -0
  67. dexcontrol-0.2.1.dist-info/METADATA +369 -0
  68. dexcontrol-0.2.1.dist-info/RECORD +72 -0
  69. dexcontrol-0.2.1.dist-info/WHEEL +5 -0
  70. dexcontrol-0.2.1.dist-info/licenses/LICENSE +188 -0
  71. dexcontrol-0.2.1.dist-info/licenses/NOTICE +13 -0
  72. 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