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
@@ -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])