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
|
@@ -0,0 +1,628 @@
|
|
|
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 base control module.
|
|
7
|
+
|
|
8
|
+
This module provides the Chassis class for controlling a robot's wheeled base through
|
|
9
|
+
Zenoh communication. It handles steering and wheel velocity control.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import zenoh
|
|
16
|
+
|
|
17
|
+
from dexcontrol.config.core import ChassisConfig
|
|
18
|
+
from dexcontrol.core.component import RobotJointComponent
|
|
19
|
+
from dexcontrol.proto import dexcontrol_msg_pb2
|
|
20
|
+
from dexcontrol.utils.rate_limiter import RateLimiter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Chassis(RobotJointComponent):
|
|
24
|
+
"""Robot base control class.
|
|
25
|
+
|
|
26
|
+
This class provides methods to control a robot's wheeled base by publishing commands
|
|
27
|
+
and receiving state information through Zenoh communication.
|
|
28
|
+
|
|
29
|
+
The chassis has 4 controllable joints:
|
|
30
|
+
- Left steering angle
|
|
31
|
+
- Left wheel velocity
|
|
32
|
+
- Right steering angle
|
|
33
|
+
- Right wheel velocity
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
max_vel: Maximum allowed wheel velocity in m/s.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
configs: ChassisConfig,
|
|
42
|
+
zenoh_session: zenoh.Session,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Initialize the base controller.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
configs: Base configuration parameters containing communication topics.
|
|
48
|
+
zenoh_session: Active Zenoh communication session for message passing.
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(
|
|
51
|
+
state_sub_topic=configs.state_sub_topic,
|
|
52
|
+
control_pub_topic=configs.control_pub_topic,
|
|
53
|
+
state_message_type=dexcontrol_msg_pb2.ChassisState,
|
|
54
|
+
zenoh_session=zenoh_session,
|
|
55
|
+
joint_name=configs.joint_name,
|
|
56
|
+
)
|
|
57
|
+
self.max_vel = configs.max_vel
|
|
58
|
+
self._center_to_wheel_axis_dist = configs.center_to_wheel_axis_dist
|
|
59
|
+
self._wheels_dist = configs.wheels_dist
|
|
60
|
+
self._half_wheels_dist = self._wheels_dist / 2
|
|
61
|
+
|
|
62
|
+
# Pre-compute geometry constants for efficiency
|
|
63
|
+
self._center_to_wheel_dist = np.sqrt(
|
|
64
|
+
self._center_to_wheel_axis_dist**2 + self._half_wheels_dist**2
|
|
65
|
+
)
|
|
66
|
+
self._left_wheel_ang_vel_vector = np.array(
|
|
67
|
+
[
|
|
68
|
+
-self._half_wheels_dist / self._center_to_wheel_dist,
|
|
69
|
+
self._center_to_wheel_axis_dist / self._center_to_wheel_dist,
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
self._right_wheel_ang_vel_vector = np.array(
|
|
73
|
+
[
|
|
74
|
+
self._half_wheels_dist / self._center_to_wheel_dist,
|
|
75
|
+
self._center_to_wheel_axis_dist / self._center_to_wheel_dist,
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Constants for steering angle constraints
|
|
80
|
+
self._max_steering_angle = np.deg2rad(135)
|
|
81
|
+
self._min_velocity_threshold = 1e-6
|
|
82
|
+
self.max_lin_vel = self.max_vel
|
|
83
|
+
self.max_ang_vel = self.max_vel / self._center_to_wheel_dist
|
|
84
|
+
|
|
85
|
+
def stop(self) -> None:
|
|
86
|
+
"""Stop the base by setting all wheel velocities and steering to zero."""
|
|
87
|
+
self.set_motion_state(
|
|
88
|
+
np.zeros(2, dtype=np.float32), np.zeros(2, dtype=np.float32)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def shutdown(self) -> None:
|
|
92
|
+
"""Shutdown the base by stopping all motion."""
|
|
93
|
+
self.stop()
|
|
94
|
+
super().shutdown()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def steering_angle(self) -> np.ndarray:
|
|
98
|
+
"""Get current steering angles.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Numpy array of shape (2,) containing current steering angles in radians.
|
|
102
|
+
Index 0 is left wheel, index 1 is right wheel.
|
|
103
|
+
"""
|
|
104
|
+
state = self._get_state()
|
|
105
|
+
return np.array([state.left.steering_pos, state.right.steering_pos])
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def wheel_velocity(self) -> np.ndarray:
|
|
109
|
+
"""Get current wheel velocities.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Numpy array of shape (2,) containing current wheel velocities in m/s.
|
|
113
|
+
Index 0 is left wheel, index 1 is right wheel.
|
|
114
|
+
"""
|
|
115
|
+
state = self._get_state()
|
|
116
|
+
return np.array([state.left.wheel_vel, state.right.wheel_vel])
|
|
117
|
+
|
|
118
|
+
def set_velocity(
|
|
119
|
+
self,
|
|
120
|
+
vx: float,
|
|
121
|
+
vy: float,
|
|
122
|
+
wz: float,
|
|
123
|
+
wait_time: float = 0.0,
|
|
124
|
+
wait_kwargs: dict[str, float] | None = None,
|
|
125
|
+
sequential_steering: bool = True,
|
|
126
|
+
steering_wait_time: float = 1.0,
|
|
127
|
+
steering_tolerance: float = 0.05,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Set the chassis velocity in the horizontal plane.
|
|
130
|
+
|
|
131
|
+
This method takes the desired translational velocity (vx, vy) and rotational
|
|
132
|
+
velocity (wz) of the chassis and computes the required steering angles
|
|
133
|
+
and wheel velocities for each wheel.
|
|
134
|
+
|
|
135
|
+
Coordinate System:
|
|
136
|
+
- X-axis: Points forward (front of chassis)
|
|
137
|
+
- Y-axis: Points left (left side of chassis)
|
|
138
|
+
- Z-axis: Points up (vertical)
|
|
139
|
+
- Rotation: Right-hand rule around Z-axis (positive = counter-clockwise)
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
vx: Translational velocity in x-direction in m/s.
|
|
143
|
+
Positive = forward, Negative = backward
|
|
144
|
+
vy: Translational velocity in y-direction in m/s.
|
|
145
|
+
Positive = left, Negative = right
|
|
146
|
+
wz: Rotational velocity around z-axis (yaw) in rad/s.
|
|
147
|
+
Positive = counter-clockwise (left turn)
|
|
148
|
+
Negative = clockwise (right turn)
|
|
149
|
+
wait_time: Time to maintain the command in seconds.
|
|
150
|
+
wait_kwargs: Additional parameters for wait behavior.
|
|
151
|
+
sequential_steering: If True, first adjust steering angles, then set wheel
|
|
152
|
+
velocities. If False, set both simultaneously.
|
|
153
|
+
steering_wait_time: Time to wait for steering adjustment when
|
|
154
|
+
sequential_steering=True.
|
|
155
|
+
steering_tolerance: Angular tolerance in radians to consider steering
|
|
156
|
+
"complete".
|
|
157
|
+
|
|
158
|
+
Note:
|
|
159
|
+
Steering angles are constrained to [-170°, 170°] ≈ [-2.967, 2.967] rad.
|
|
160
|
+
If the required steering angle exceeds this range, the wheel velocity
|
|
161
|
+
will be reversed and the steering angle adjusted accordingly.
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
# Move forward at 0.5 m/s
|
|
165
|
+
chassis.set_velocity(vx=0.5, vy=0.0, wz=0.0)
|
|
166
|
+
|
|
167
|
+
# Move left at 0.3 m/s
|
|
168
|
+
chassis.set_velocity(vx=0.0, vy=0.3, wz=0.0)
|
|
169
|
+
|
|
170
|
+
# Turn left (counter-clockwise) at 0.5 rad/s
|
|
171
|
+
chassis.set_velocity(vx=0.0, vy=0.0, wz=0.5)
|
|
172
|
+
|
|
173
|
+
# Combined: forward + left + turn left
|
|
174
|
+
chassis.set_velocity(vx=0.4, vy=0.2, wz=0.3)
|
|
175
|
+
"""
|
|
176
|
+
vx = np.clip(vx, -self.max_lin_vel, self.max_lin_vel)
|
|
177
|
+
vy = np.clip(vy, -self.max_lin_vel, self.max_lin_vel)
|
|
178
|
+
wz = np.clip(wz, -self.max_ang_vel, self.max_ang_vel)
|
|
179
|
+
wait_kwargs = wait_kwargs or {}
|
|
180
|
+
|
|
181
|
+
# Convert to numpy array for vectorized computation
|
|
182
|
+
linear_velocity = np.array([vx, vy])
|
|
183
|
+
|
|
184
|
+
# Compute velocity contribution from rotation for each wheel
|
|
185
|
+
left_wheel_rotation_vel = (
|
|
186
|
+
wz * self._center_to_wheel_dist * self._left_wheel_ang_vel_vector
|
|
187
|
+
)
|
|
188
|
+
right_wheel_rotation_vel = (
|
|
189
|
+
wz * self._center_to_wheel_dist * self._right_wheel_ang_vel_vector
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Total velocity for each wheel (translation + rotation)
|
|
193
|
+
left_wheel_velocity = linear_velocity + left_wheel_rotation_vel
|
|
194
|
+
right_wheel_velocity = linear_velocity + right_wheel_rotation_vel
|
|
195
|
+
|
|
196
|
+
# Get current steering angles
|
|
197
|
+
current_steering = self.steering_angle
|
|
198
|
+
|
|
199
|
+
# Compute steering angles and wheel speeds
|
|
200
|
+
left_steering_angle, left_wheel_speed = self._compute_wheel_control(
|
|
201
|
+
left_wheel_velocity, current_steering[0]
|
|
202
|
+
)
|
|
203
|
+
right_steering_angle, right_wheel_speed = self._compute_wheel_control(
|
|
204
|
+
right_wheel_velocity, current_steering[1]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
target_steering = np.array([left_steering_angle, right_steering_angle])
|
|
208
|
+
target_wheel_speeds = np.array([left_wheel_speed, right_wheel_speed])
|
|
209
|
+
|
|
210
|
+
if sequential_steering:
|
|
211
|
+
self._apply_sequential_steering(
|
|
212
|
+
target_steering,
|
|
213
|
+
target_wheel_speeds,
|
|
214
|
+
steering_tolerance,
|
|
215
|
+
steering_wait_time,
|
|
216
|
+
wait_time,
|
|
217
|
+
wait_kwargs,
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
# Apply both steering and velocity simultaneously
|
|
221
|
+
self.set_motion_state(
|
|
222
|
+
target_steering, target_wheel_speeds, wait_time, wait_kwargs
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def move_straight(
|
|
226
|
+
self,
|
|
227
|
+
speed: float,
|
|
228
|
+
wait_time: float = 0.0,
|
|
229
|
+
wait_kwargs: dict[str, float] | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Move the chassis straight (forward/backward).
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
speed: Straight movement speed in m/s.
|
|
235
|
+
Positive/negative values move in opposite directions.
|
|
236
|
+
wait_time: Time to maintain the movement in seconds.
|
|
237
|
+
wait_kwargs: Additional parameters for wait behavior.
|
|
238
|
+
"""
|
|
239
|
+
self.set_velocity(
|
|
240
|
+
vx=speed,
|
|
241
|
+
vy=0.0,
|
|
242
|
+
wz=0.0,
|
|
243
|
+
wait_time=wait_time,
|
|
244
|
+
wait_kwargs=wait_kwargs,
|
|
245
|
+
sequential_steering=True,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def move_sideways(
|
|
249
|
+
self,
|
|
250
|
+
speed: float,
|
|
251
|
+
wait_time: float = 0.0,
|
|
252
|
+
wait_kwargs: dict[str, float] | None = None,
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Move the chassis sideways (strafe left/right).
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
speed: Sideways movement speed in m/s.
|
|
258
|
+
Positive: left, Negative: right
|
|
259
|
+
wait_time: Time to maintain the movement in seconds.
|
|
260
|
+
wait_kwargs: Additional parameters for wait behavior.
|
|
261
|
+
"""
|
|
262
|
+
self.set_velocity(
|
|
263
|
+
vx=0.0,
|
|
264
|
+
vy=speed,
|
|
265
|
+
wz=0.0,
|
|
266
|
+
wait_time=wait_time,
|
|
267
|
+
wait_kwargs=wait_kwargs,
|
|
268
|
+
sequential_steering=True,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def turn(
|
|
272
|
+
self,
|
|
273
|
+
angular_speed: float,
|
|
274
|
+
wait_time: float = 0.0,
|
|
275
|
+
wait_kwargs: dict[str, float] | None = None,
|
|
276
|
+
center: str = "base_center",
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Turn the chassis in place.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
angular_speed: Turning speed in rad/s.
|
|
282
|
+
Positive: counter-clockwise, Negative: clockwise
|
|
283
|
+
wait_time: Time to maintain the turn in seconds.
|
|
284
|
+
wait_kwargs: Additional parameters for wait behavior.
|
|
285
|
+
center: Rotation center. Either "base_center" or "front_wheels_center".
|
|
286
|
+
|
|
287
|
+
Raises:
|
|
288
|
+
ValueError: If center is not a valid option.
|
|
289
|
+
"""
|
|
290
|
+
if center == "base_center":
|
|
291
|
+
self.set_velocity(
|
|
292
|
+
vx=0.0,
|
|
293
|
+
vy=0.0,
|
|
294
|
+
wz=angular_speed,
|
|
295
|
+
wait_time=wait_time,
|
|
296
|
+
wait_kwargs=wait_kwargs,
|
|
297
|
+
sequential_steering=True,
|
|
298
|
+
)
|
|
299
|
+
elif center == "front_wheels_center":
|
|
300
|
+
speed = angular_speed * self._half_wheels_dist
|
|
301
|
+
self.set_wheel_velocity(np.array([-speed, speed]), wait_time, wait_kwargs)
|
|
302
|
+
else:
|
|
303
|
+
raise ValueError(
|
|
304
|
+
f"Invalid center: {center}. Must be 'base_center' or 'front_wheels_center'"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def set_steering_angle(
|
|
308
|
+
self,
|
|
309
|
+
steering_angle: float | np.ndarray,
|
|
310
|
+
wait_time: float = 0.0,
|
|
311
|
+
wait_kwargs: dict[str, float] | None = None,
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Set the steering angles for both wheels.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
steering_angle: Target steering angle(s) in radians. If float, same angle
|
|
317
|
+
applied to both wheels. If array, [left_angle, right_angle].
|
|
318
|
+
wait_time: Time to maintain the command in seconds.
|
|
319
|
+
wait_kwargs: Additional parameters for wait behavior.
|
|
320
|
+
"""
|
|
321
|
+
wait_kwargs = wait_kwargs or {}
|
|
322
|
+
|
|
323
|
+
if isinstance(steering_angle, (int, float)):
|
|
324
|
+
steering_angle = np.array([steering_angle, steering_angle])
|
|
325
|
+
self.set_motion_state(steering_angle, np.zeros(2), wait_time, wait_kwargs)
|
|
326
|
+
|
|
327
|
+
def set_wheel_velocity(
|
|
328
|
+
self,
|
|
329
|
+
wheel_velocity: float | np.ndarray,
|
|
330
|
+
wait_time: float = 0.0,
|
|
331
|
+
wait_kwargs: dict[str, float] | None = None,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Set the wheel velocities while maintaining current steering angles.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
wheel_velocity: Target wheel velocity in m/s. If float, same velocity
|
|
337
|
+
applied to both wheels. If array, [left_vel, right_vel].
|
|
338
|
+
wait_time: Time to maintain the command in seconds.
|
|
339
|
+
wait_kwargs: Additional parameters for wait behavior.
|
|
340
|
+
"""
|
|
341
|
+
wait_kwargs = wait_kwargs or {}
|
|
342
|
+
|
|
343
|
+
if isinstance(wheel_velocity, (int, float)):
|
|
344
|
+
wheel_velocity = np.array([wheel_velocity, wheel_velocity])
|
|
345
|
+
steering_angle = self.steering_angle
|
|
346
|
+
self.set_motion_state(steering_angle, wheel_velocity, wait_time, wait_kwargs)
|
|
347
|
+
|
|
348
|
+
def get_joint_pos(self, joint_id: list[int] | int | None = None) -> np.ndarray:
|
|
349
|
+
"""Get the joint positions of the chassis.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
joint_id: Optional ID(s) of specific joints to query.
|
|
353
|
+
0: left steering position
|
|
354
|
+
1: left wheel position
|
|
355
|
+
2: right steering position
|
|
356
|
+
3: right wheel position
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Array of joint positions in the order:
|
|
360
|
+
[left_steering_pos, left_wheel_pos, right_steering_pos, right_wheel_pos]
|
|
361
|
+
"""
|
|
362
|
+
state = self._get_state()
|
|
363
|
+
joint_pos = np.array(
|
|
364
|
+
[
|
|
365
|
+
state.left.steering_pos,
|
|
366
|
+
state.left.wheel_pos,
|
|
367
|
+
state.right.steering_pos,
|
|
368
|
+
state.right.wheel_pos,
|
|
369
|
+
]
|
|
370
|
+
)
|
|
371
|
+
return self._extract_joint_info(joint_pos, joint_id=joint_id)
|
|
372
|
+
|
|
373
|
+
def get_joint_pos_dict(
|
|
374
|
+
self, joint_id: list[int] | int | None = None
|
|
375
|
+
) -> dict[str, float]:
|
|
376
|
+
"""Get the joint positions of the chassis as a dictionary.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
joint_id: Optional ID(s) of specific joints to query.
|
|
380
|
+
0: left steering position
|
|
381
|
+
1: left wheel position
|
|
382
|
+
2: right steering position
|
|
383
|
+
3: right wheel position
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Dictionary mapping joint names to position values.
|
|
387
|
+
"""
|
|
388
|
+
values = self.get_joint_pos(joint_id)
|
|
389
|
+
return self._convert_to_dict(values, joint_id)
|
|
390
|
+
|
|
391
|
+
def get_joint_state(self, joint_id: list[int] | int | None = None) -> np.ndarray:
|
|
392
|
+
"""Get current wheel states (left and right wheel).
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
joint_id: Optional ID(s) of specific joints to query.
|
|
396
|
+
0: left wheel
|
|
397
|
+
1: right wheel
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Numpy array containing:
|
|
401
|
+
[:, 0]: Current steering positions in radians
|
|
402
|
+
[:, 1]: Current wheel positions in radians
|
|
403
|
+
[:, 2]: Current wheel velocities in m/s
|
|
404
|
+
"""
|
|
405
|
+
state = self._get_state()
|
|
406
|
+
wheel_states = np.array(
|
|
407
|
+
[
|
|
408
|
+
[state.left.steering_pos, state.left.wheel_pos, state.left.wheel_vel],
|
|
409
|
+
[
|
|
410
|
+
state.right.steering_pos,
|
|
411
|
+
state.right.wheel_pos,
|
|
412
|
+
state.right.wheel_vel,
|
|
413
|
+
],
|
|
414
|
+
]
|
|
415
|
+
)
|
|
416
|
+
return self._extract_joint_info(wheel_states, joint_id=joint_id)
|
|
417
|
+
|
|
418
|
+
def get_joint_state_dict(
|
|
419
|
+
self, joint_id: list[int] | int | None = None
|
|
420
|
+
) -> dict[str, np.ndarray]:
|
|
421
|
+
"""Get current wheel states as a dictionary.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
joint_id: Optional ID(s) of specific joints to query.
|
|
425
|
+
0: left wheel
|
|
426
|
+
1: right wheel
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Dictionary mapping joint names to arrays of [steering_pos, wheel_pos, wheel_vel]
|
|
430
|
+
"""
|
|
431
|
+
values = self.get_joint_state(joint_id)
|
|
432
|
+
return self._convert_to_dict(values, joint_id)
|
|
433
|
+
|
|
434
|
+
def set_motion_state(
|
|
435
|
+
self,
|
|
436
|
+
steering_angle: float | np.ndarray,
|
|
437
|
+
wheel_velocity: float | np.ndarray,
|
|
438
|
+
wait_time: float = 0.0,
|
|
439
|
+
wait_kwargs: dict[str, float] | None = None,
|
|
440
|
+
) -> None:
|
|
441
|
+
"""Send control commands to the chassis wheels.
|
|
442
|
+
|
|
443
|
+
Sets steering positions and wheel velocities for both left and right wheels.
|
|
444
|
+
Values can be provided either as numpy arrays or dictionaries mapping joint
|
|
445
|
+
names to values. If using dictionaries, missing joints maintain current state.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
steering_angle: Steering angle in radians. As array: [left_angle, right_angle]
|
|
449
|
+
or dict mapping "L_wheel_j1"/"R_wheel_j1" to values.
|
|
450
|
+
wheel_velocity: Wheel velocities in m/s. As array: [left_vel, right_vel]
|
|
451
|
+
or dict mapping "L_wheel_j2"/"R_wheel_j2" to values.
|
|
452
|
+
wait_time: Time to maintain the command in seconds.
|
|
453
|
+
wait_kwargs: Additional parameters for wait behavior.
|
|
454
|
+
"""
|
|
455
|
+
wait_kwargs = wait_kwargs or {}
|
|
456
|
+
|
|
457
|
+
# Ensure wheel velocities are within limits
|
|
458
|
+
wheel_velocity = np.clip(wheel_velocity, -self.max_vel, self.max_vel)
|
|
459
|
+
|
|
460
|
+
if wait_time > 0.0:
|
|
461
|
+
self._execute_timed_command(
|
|
462
|
+
steering_angle, wheel_velocity, wait_time, wait_kwargs
|
|
463
|
+
)
|
|
464
|
+
else:
|
|
465
|
+
self._send_single_command(steering_angle, wheel_velocity)
|
|
466
|
+
|
|
467
|
+
def _apply_sequential_steering(
|
|
468
|
+
self,
|
|
469
|
+
target_steering: np.ndarray,
|
|
470
|
+
target_wheel_speeds: np.ndarray,
|
|
471
|
+
steering_tolerance: float,
|
|
472
|
+
steering_wait_time: float,
|
|
473
|
+
wait_time: float,
|
|
474
|
+
wait_kwargs: dict[str, float],
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Apply steering and velocity commands sequentially if needed.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
target_steering: Target steering angles for both wheels.
|
|
480
|
+
target_wheel_speeds: Target wheel speeds for both wheels.
|
|
481
|
+
steering_tolerance: Angular tolerance to consider steering complete.
|
|
482
|
+
steering_wait_time: Time to wait for steering adjustment.
|
|
483
|
+
wait_time: Time to maintain final command.
|
|
484
|
+
wait_kwargs: Additional parameters for wait behavior.
|
|
485
|
+
"""
|
|
486
|
+
current_steering = self.steering_angle
|
|
487
|
+
steering_error = np.linalg.norm(current_steering - target_steering)
|
|
488
|
+
if steering_error > steering_tolerance:
|
|
489
|
+
# Step 1: Adjust steering angles with zero wheel velocity
|
|
490
|
+
self.set_motion_state(
|
|
491
|
+
target_steering,
|
|
492
|
+
np.zeros(2),
|
|
493
|
+
wait_time=steering_wait_time,
|
|
494
|
+
wait_kwargs=wait_kwargs,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Step 2: Apply wheel velocities with the adjusted steering
|
|
498
|
+
self.set_motion_state(
|
|
499
|
+
target_steering, target_wheel_speeds, wait_time, wait_kwargs
|
|
500
|
+
)
|
|
501
|
+
else:
|
|
502
|
+
# Steering is already close enough, apply both simultaneously
|
|
503
|
+
self.set_motion_state(
|
|
504
|
+
target_steering, target_wheel_speeds, wait_time, wait_kwargs
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def _execute_timed_command(
|
|
508
|
+
self,
|
|
509
|
+
steering_angle: float | np.ndarray,
|
|
510
|
+
wheel_velocity: float | np.ndarray,
|
|
511
|
+
wait_time: float,
|
|
512
|
+
wait_kwargs: dict[str, float],
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Execute a command for a specified duration.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
steering_angle: Target steering angles.
|
|
518
|
+
wheel_velocity: Target wheel velocities.
|
|
519
|
+
wait_time: Duration to maintain the command.
|
|
520
|
+
wait_kwargs: Additional parameters including control frequency.
|
|
521
|
+
"""
|
|
522
|
+
# Set default control frequency if not provided
|
|
523
|
+
control_hz = wait_kwargs.get("control_hz", 50)
|
|
524
|
+
rate_limiter = RateLimiter(control_hz)
|
|
525
|
+
start_time = time.time()
|
|
526
|
+
|
|
527
|
+
while time.time() - start_time < wait_time:
|
|
528
|
+
self._send_single_command(steering_angle, wheel_velocity)
|
|
529
|
+
rate_limiter.sleep()
|
|
530
|
+
|
|
531
|
+
def _send_single_command(
|
|
532
|
+
self,
|
|
533
|
+
steering_angle: float | np.ndarray,
|
|
534
|
+
wheel_velocity: float | np.ndarray,
|
|
535
|
+
) -> None:
|
|
536
|
+
"""Send a single control command to the chassis.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
steering_angle: Target steering angles.
|
|
540
|
+
wheel_velocity: Target wheel velocities.
|
|
541
|
+
"""
|
|
542
|
+
control_msg = dexcontrol_msg_pb2.ChassisCommand()
|
|
543
|
+
state = self._get_state()
|
|
544
|
+
|
|
545
|
+
# Set steering positions and wheel velocities
|
|
546
|
+
control_msg.left.steering_pos = _get_value(
|
|
547
|
+
steering_angle, 0, "L_wheel_j1", state.left.steering_pos
|
|
548
|
+
)
|
|
549
|
+
control_msg.right.steering_pos = _get_value(
|
|
550
|
+
steering_angle, 1, "R_wheel_j1", state.right.steering_pos
|
|
551
|
+
)
|
|
552
|
+
control_msg.left.wheel_vel = _get_value(
|
|
553
|
+
wheel_velocity, 0, "L_wheel_j2", state.left.wheel_vel
|
|
554
|
+
)
|
|
555
|
+
control_msg.right.wheel_vel = _get_value(
|
|
556
|
+
wheel_velocity, 1, "R_wheel_j2", state.right.wheel_vel
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
self._publish_control(control_msg)
|
|
560
|
+
|
|
561
|
+
def _compute_wheel_control(
|
|
562
|
+
self, wheel_velocity: np.ndarray, current_angle: float
|
|
563
|
+
) -> tuple[float, float]:
|
|
564
|
+
"""Compute steering angle and wheel speed for a given wheel velocity vector.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
wheel_velocity: 2D velocity vector [vx, vy] for the wheel.
|
|
568
|
+
current_angle: Current steering angle of the wheel in radians.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Tuple of (steering_angle, wheel_speed) where:
|
|
572
|
+
- steering_angle is in radians, constrained to [-170°, 170°]
|
|
573
|
+
- wheel_speed is the magnitude of velocity (can be negative)
|
|
574
|
+
"""
|
|
575
|
+
# Compute speed and direction
|
|
576
|
+
wheel_speed = float(np.linalg.norm(wheel_velocity))
|
|
577
|
+
|
|
578
|
+
# Handle zero velocity case
|
|
579
|
+
if wheel_speed < self._min_velocity_threshold:
|
|
580
|
+
return 0.0, 0.0
|
|
581
|
+
|
|
582
|
+
# Compute base steering angle
|
|
583
|
+
base_angle = float(-np.arctan2(wheel_velocity[1], wheel_velocity[0]))
|
|
584
|
+
|
|
585
|
+
# Consider both possible solutions
|
|
586
|
+
sol1 = (base_angle, wheel_speed)
|
|
587
|
+
sol2 = (
|
|
588
|
+
base_angle + np.pi if base_angle < 0 else base_angle - np.pi,
|
|
589
|
+
-wheel_speed,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Choose solution with angle closer to current angle
|
|
593
|
+
angle1_diff = abs(sol1[0] - current_angle)
|
|
594
|
+
angle2_diff = abs(sol2[0] - current_angle)
|
|
595
|
+
|
|
596
|
+
# Select the better solution
|
|
597
|
+
steering_angle, wheel_speed = sol1 if angle1_diff < angle2_diff else sol2
|
|
598
|
+
|
|
599
|
+
# Ensure steering angle is within bounds
|
|
600
|
+
steering_angle = float(
|
|
601
|
+
np.clip(steering_angle, -self._max_steering_angle, self._max_steering_angle)
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
return steering_angle, wheel_speed
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _get_value(
|
|
608
|
+
data: float | np.ndarray | dict[str, float],
|
|
609
|
+
idx: int,
|
|
610
|
+
key: str,
|
|
611
|
+
fallback: float,
|
|
612
|
+
) -> float:
|
|
613
|
+
"""Helper function to extract values with fallback to current state.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
data: Input scalar, array or dictionary containing values.
|
|
617
|
+
idx: Index to access if data is array.
|
|
618
|
+
key: Key to access if data is dictionary.
|
|
619
|
+
fallback: Default value if key/index not found.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
Extracted float value.
|
|
623
|
+
"""
|
|
624
|
+
if isinstance(data, dict):
|
|
625
|
+
return float(data.get(key, fallback))
|
|
626
|
+
if isinstance(data, (int, float)):
|
|
627
|
+
return float(data)
|
|
628
|
+
return float(data[idx])
|