dexcontrol 0.2.10__py3-none-any.whl → 0.3.0__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 (47) hide show
  1. dexcontrol/__init__.py +2 -0
  2. dexcontrol/config/core/arm.py +5 -1
  3. dexcontrol/config/core/chassis.py +9 -4
  4. dexcontrol/config/core/hand.py +2 -1
  5. dexcontrol/config/core/head.py +7 -8
  6. dexcontrol/config/core/misc.py +14 -1
  7. dexcontrol/config/core/torso.py +8 -4
  8. dexcontrol/config/sensors/cameras/__init__.py +2 -1
  9. dexcontrol/config/sensors/cameras/luxonis_camera.py +51 -0
  10. dexcontrol/config/sensors/cameras/rgb_camera.py +1 -1
  11. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  12. dexcontrol/config/sensors/vega_sensors.py +9 -1
  13. dexcontrol/config/vega.py +34 -3
  14. dexcontrol/core/arm.py +103 -58
  15. dexcontrol/core/chassis.py +146 -115
  16. dexcontrol/core/component.py +83 -20
  17. dexcontrol/core/hand.py +74 -39
  18. dexcontrol/core/head.py +41 -28
  19. dexcontrol/core/misc.py +256 -25
  20. dexcontrol/core/robot_query_interface.py +440 -0
  21. dexcontrol/core/torso.py +28 -10
  22. dexcontrol/proto/dexcontrol_msg_pb2.py +27 -37
  23. dexcontrol/proto/dexcontrol_msg_pb2.pyi +111 -126
  24. dexcontrol/proto/dexcontrol_query_pb2.py +39 -35
  25. dexcontrol/proto/dexcontrol_query_pb2.pyi +41 -4
  26. dexcontrol/robot.py +266 -409
  27. dexcontrol/sensors/__init__.py +2 -1
  28. dexcontrol/sensors/camera/__init__.py +2 -0
  29. dexcontrol/sensors/camera/luxonis_camera.py +169 -0
  30. dexcontrol/sensors/camera/zed_camera.py +17 -8
  31. dexcontrol/sensors/imu/chassis_imu.py +5 -1
  32. dexcontrol/sensors/imu/zed_imu.py +3 -2
  33. dexcontrol/sensors/lidar/rplidar.py +1 -0
  34. dexcontrol/sensors/manager.py +3 -0
  35. dexcontrol/utils/constants.py +3 -0
  36. dexcontrol/utils/error_code.py +236 -0
  37. dexcontrol/utils/os_utils.py +183 -1
  38. dexcontrol/utils/pb_utils.py +0 -22
  39. dexcontrol/utils/subscribers/lidar.py +1 -0
  40. dexcontrol/utils/trajectory_utils.py +17 -5
  41. dexcontrol/utils/viz_utils.py +86 -11
  42. dexcontrol/utils/zenoh_utils.py +288 -2
  43. {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/METADATA +15 -2
  44. dexcontrol-0.3.0.dist-info/RECORD +76 -0
  45. dexcontrol-0.2.10.dist-info/RECORD +0 -72
  46. {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/WHEEL +0 -0
  47. {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,6 +18,7 @@ import time
18
18
 
19
19
  import numpy as np
20
20
  import zenoh
21
+ from jaxtyping import Float
21
22
 
22
23
  from dexcontrol.config.core import ChassisConfig
23
24
  from dexcontrol.core.component import RobotJointComponent
@@ -25,7 +26,88 @@ from dexcontrol.proto import dexcontrol_msg_pb2
25
26
  from dexcontrol.utils.rate_limiter import RateLimiter
26
27
 
27
28
 
28
- class Chassis(RobotJointComponent):
29
+ class ChassisSteer(RobotJointComponent):
30
+ """Robot hand control class.
31
+
32
+ This class provides methods to control a robotic hand by publishing commands and
33
+ receiving state information through Zenoh communication.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ configs: ChassisConfig,
39
+ zenoh_session: zenoh.Session,
40
+ ) -> None:
41
+ """Initialize the hand controller.
42
+
43
+ Args:
44
+ configs: Hand configuration parameters containing communication topics
45
+ and predefined hand positions.
46
+ zenoh_session: Active Zenoh communication session for message passing.
47
+ """
48
+ super().__init__(
49
+ state_sub_topic=configs.steer_state_sub_topic,
50
+ control_pub_topic=configs.steer_control_pub_topic,
51
+ state_message_type=dexcontrol_msg_pb2.MotorStateWithCurrent,
52
+ zenoh_session=zenoh_session,
53
+ joint_name=configs.steer_joint_name,
54
+ )
55
+
56
+ def _send_position_command(
57
+ self, joint_pos: Float[np.ndarray, " 2"] | list[float]
58
+ ) -> None:
59
+ """Send joint position control commands to the hand.
60
+
61
+ Args:
62
+ joint_pos: Joint positions as list or numpy array.
63
+ """
64
+ control_msg = dexcontrol_msg_pb2.MotorPosVelCommand()
65
+ joint_pos_array = self._convert_joint_cmd_to_array(joint_pos)
66
+ control_msg.pos.extend(joint_pos_array.tolist())
67
+ self._publish_control(control_msg)
68
+
69
+
70
+ class ChassisDrive(RobotJointComponent):
71
+ """Robot base control class.
72
+
73
+ This class provides methods to control a robot's wheeled base by publishing commands
74
+ and receiving state information through Zenoh communication.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ configs: ChassisConfig,
80
+ zenoh_session: zenoh.Session,
81
+ ) -> None:
82
+ """Initialize the base controller.
83
+
84
+ Args:
85
+ configs: Base configuration parameters containing communication topics.
86
+ zenoh_session: Active Zenoh communication session for message passing.
87
+ """
88
+ super().__init__(
89
+ state_sub_topic=configs.drive_state_sub_topic,
90
+ control_pub_topic=configs.drive_control_pub_topic,
91
+ state_message_type=dexcontrol_msg_pb2.MotorStateWithCurrent,
92
+ zenoh_session=zenoh_session,
93
+ joint_name=configs.drive_joint_name,
94
+ )
95
+
96
+ def _send_position_command(
97
+ self, joint_pos: Float[np.ndarray, " 2"] | list[float]
98
+ ) -> None:
99
+ raise NotImplementedError("ChassisDrive does not support position control.")
100
+
101
+ def _send_velocity_command(
102
+ self, joint_vel: Float[np.ndarray, " 2"] | list[float]
103
+ ) -> None:
104
+ control_msg = dexcontrol_msg_pb2.MotorVelCommand()
105
+ joint_vel_array = self._convert_joint_cmd_to_array(joint_vel)
106
+ control_msg.vel.extend(joint_vel_array.tolist())
107
+ self._publish_control(control_msg)
108
+
109
+
110
+ class Chassis:
29
111
  """Robot base control class.
30
112
 
31
113
  This class provides methods to control a robot's wheeled base by publishing commands
@@ -52,13 +134,9 @@ class Chassis(RobotJointComponent):
52
134
  configs: Base configuration parameters containing communication topics.
53
135
  zenoh_session: Active Zenoh communication session for message passing.
54
136
  """
55
- super().__init__(
56
- state_sub_topic=configs.state_sub_topic,
57
- control_pub_topic=configs.control_pub_topic,
58
- state_message_type=dexcontrol_msg_pb2.ChassisState,
59
- zenoh_session=zenoh_session,
60
- joint_name=configs.joint_name,
61
- )
137
+ self.chassis_steer = ChassisSteer(configs, zenoh_session)
138
+ self.chassis_drive = ChassisDrive(configs, zenoh_session)
139
+
62
140
  self.max_vel = configs.max_vel
63
141
  self._center_to_wheel_axis_dist = configs.center_to_wheel_axis_dist
64
142
  self._wheels_dist = configs.wheels_dist
@@ -96,7 +174,8 @@ class Chassis(RobotJointComponent):
96
174
  def shutdown(self) -> None:
97
175
  """Shutdown the base by stopping all motion."""
98
176
  self.stop()
99
- super().shutdown()
177
+ self.chassis_steer.shutdown()
178
+ self.chassis_drive.shutdown()
100
179
 
101
180
  @property
102
181
  def steering_angle(self) -> np.ndarray:
@@ -106,8 +185,7 @@ class Chassis(RobotJointComponent):
106
185
  Numpy array of shape (2,) containing current steering angles in radians.
107
186
  Index 0 is left wheel, index 1 is right wheel.
108
187
  """
109
- state = self._get_state()
110
- return np.array([state.left.steering_pos, state.right.steering_pos])
188
+ return self.chassis_steer.get_joint_pos()
111
189
 
112
190
  @property
113
191
  def wheel_velocity(self) -> np.ndarray:
@@ -117,8 +195,17 @@ class Chassis(RobotJointComponent):
117
195
  Numpy array of shape (2,) containing current wheel velocities in m/s.
118
196
  Index 0 is left wheel, index 1 is right wheel.
119
197
  """
120
- state = self._get_state()
121
- return np.array([state.left.wheel_vel, state.right.wheel_vel])
198
+ return self.chassis_drive.get_joint_vel()
199
+
200
+ @property
201
+ def wheel_encoder_pos(self) -> np.ndarray:
202
+ """Get current wheel encoder positions.
203
+
204
+ Returns:
205
+ Numpy array of shape (2,) containing current wheel encoder positions in radians.
206
+ Index 0 is left wheel, index 1 is right wheel.
207
+ """
208
+ return self.chassis_drive.get_joint_pos()
122
209
 
123
210
  def set_velocity(
124
211
  self,
@@ -356,24 +443,23 @@ class Chassis(RobotJointComponent):
356
443
  Args:
357
444
  joint_id: Optional ID(s) of specific joints to query.
358
445
  0: left steering position
359
- 1: left wheel position
360
- 2: right steering position
446
+ 1: right steering position
447
+ 2: left wheel position
361
448
  3: right wheel position
362
449
 
363
450
  Returns:
364
451
  Array of joint positions in the order:
365
- [left_steering_pos, left_wheel_pos, right_steering_pos, right_wheel_pos]
452
+ [left_steering_pos, right_steering_pos, left_wheel_pos, right_wheel_pos]
366
453
  """
367
- state = self._get_state()
368
- joint_pos = np.array(
369
- [
370
- state.left.steering_pos,
371
- state.left.wheel_pos,
372
- state.right.steering_pos,
373
- state.right.wheel_pos,
374
- ]
375
- )
376
- return self._extract_joint_info(joint_pos, joint_id=joint_id)
454
+ steer_pos = self.chassis_steer.get_joint_pos()
455
+ drive_pos = self.chassis_drive.get_joint_pos()
456
+ joint_pos = np.concatenate([steer_pos, drive_pos])
457
+ return RobotJointComponent._extract_joint_info(joint_pos, joint_id=joint_id)
458
+
459
+ @property
460
+ def joint_name(self) -> list[str]:
461
+ """Get the joint names of the chassis."""
462
+ return self.chassis_steer.joint_name + self.chassis_drive.joint_name
377
463
 
378
464
  def get_joint_pos_dict(
379
465
  self, joint_id: list[int] | int | None = None
@@ -383,58 +469,22 @@ class Chassis(RobotJointComponent):
383
469
  Args:
384
470
  joint_id: Optional ID(s) of specific joints to query.
385
471
  0: left steering position
386
- 1: left wheel position
387
- 2: right steering position
472
+ 1: right steering position
473
+ 2: left wheel position
388
474
  3: right wheel position
389
475
 
390
476
  Returns:
391
477
  Dictionary mapping joint names to position values.
392
478
  """
393
- values = self.get_joint_pos(joint_id)
394
- return self._convert_to_dict(values, joint_id)
395
-
396
- def get_joint_state(self, joint_id: list[int] | int | None = None) -> np.ndarray:
397
- """Get current wheel states (left and right wheel).
398
-
399
- Args:
400
- joint_id: Optional ID(s) of specific joints to query.
401
- 0: left wheel
402
- 1: right wheel
403
-
404
- Returns:
405
- Numpy array containing:
406
- [:, 0]: Current steering positions in radians
407
- [:, 1]: Current wheel positions in radians
408
- [:, 2]: Current wheel velocities in m/s
409
- """
410
- state = self._get_state()
411
- wheel_states = np.array(
412
- [
413
- [state.left.steering_pos, state.left.wheel_pos, state.left.wheel_vel],
414
- [
415
- state.right.steering_pos,
416
- state.right.wheel_pos,
417
- state.right.wheel_vel,
418
- ],
419
- ]
420
- )
421
- return self._extract_joint_info(wheel_states, joint_id=joint_id)
422
-
423
- def get_joint_state_dict(
424
- self, joint_id: list[int] | int | None = None
425
- ) -> dict[str, np.ndarray]:
426
- """Get current wheel states as a dictionary.
427
-
428
- Args:
429
- joint_id: Optional ID(s) of specific joints to query.
430
- 0: left wheel
431
- 1: right wheel
432
-
433
- Returns:
434
- Dictionary mapping joint names to arrays of [steering_pos, wheel_pos, wheel_vel]
435
- """
436
- values = self.get_joint_state(joint_id)
437
- return self._convert_to_dict(values, joint_id)
479
+ steer_pos = self.chassis_steer.get_joint_pos_dict()
480
+ drive_pos = self.chassis_drive.get_joint_pos_dict()
481
+ if joint_id is None:
482
+ return {**steer_pos, **drive_pos}
483
+ else:
484
+ joint_names = self.joint_name
485
+ if isinstance(joint_id, int):
486
+ joint_id = [joint_id]
487
+ return {joint_names[i]: steer_pos[joint_names[i]] for i in joint_id}
438
488
 
439
489
  def set_motion_state(
440
490
  self,
@@ -527,10 +577,19 @@ class Chassis(RobotJointComponent):
527
577
  # Set default control frequency if not provided
528
578
  control_hz = wait_kwargs.get("control_hz", 50)
529
579
  rate_limiter = RateLimiter(control_hz)
530
- start_time = time.time()
531
580
 
532
- while time.time() - start_time < wait_time:
581
+ now_sec = time.monotonic()
582
+ duration_sec = max(wait_time - 1.0, 0.0)
583
+ end_time_sec = now_sec + duration_sec
584
+
585
+ while True:
586
+ # Ensure command is sent at least once
533
587
  self._send_single_command(steering_angle, wheel_velocity)
588
+
589
+ # Exit if we've reached the allotted duration
590
+ if time.monotonic() >= end_time_sec:
591
+ break
592
+
534
593
  rate_limiter.sleep()
535
594
 
536
595
  def _send_single_command(
@@ -544,24 +603,16 @@ class Chassis(RobotJointComponent):
544
603
  steering_angle: Target steering angles.
545
604
  wheel_velocity: Target wheel velocities.
546
605
  """
547
- control_msg = dexcontrol_msg_pb2.ChassisCommand()
548
- state = self._get_state()
549
-
550
- # Set steering positions and wheel velocities
551
- control_msg.left.steering_pos = _get_value(
552
- steering_angle, 0, "L_wheel_j1", state.left.steering_pos
553
- )
554
- control_msg.right.steering_pos = _get_value(
555
- steering_angle, 1, "R_wheel_j1", state.right.steering_pos
556
- )
557
- control_msg.left.wheel_vel = _get_value(
558
- wheel_velocity, 0, "L_wheel_j2", state.left.wheel_vel
559
- )
560
- control_msg.right.wheel_vel = _get_value(
561
- wheel_velocity, 1, "R_wheel_j2", state.right.wheel_vel
562
- )
606
+ if isinstance(steering_angle, float):
607
+ steering_angle = np.array([steering_angle, steering_angle])
608
+ self.chassis_steer._send_position_command(steering_angle)
609
+ elif isinstance(steering_angle, np.ndarray):
610
+ self.chassis_steer._send_position_command(steering_angle)
563
611
 
564
- self._publish_control(control_msg)
612
+ if isinstance(wheel_velocity, float):
613
+ wheel_velocity = np.array([wheel_velocity, wheel_velocity])
614
+ elif isinstance(wheel_velocity, np.ndarray):
615
+ self.chassis_drive._send_velocity_command(wheel_velocity)
565
616
 
566
617
  def _compute_wheel_control(
567
618
  self, wheel_velocity: np.ndarray, current_angle: float
@@ -608,26 +659,6 @@ class Chassis(RobotJointComponent):
608
659
 
609
660
  return steering_angle, wheel_speed
610
661
 
611
-
612
- def _get_value(
613
- data: float | np.ndarray | dict[str, float],
614
- idx: int,
615
- key: str,
616
- fallback: float,
617
- ) -> float:
618
- """Helper function to extract values with fallback to current state.
619
-
620
- Args:
621
- data: Input scalar, array or dictionary containing values.
622
- idx: Index to access if data is array.
623
- key: Key to access if data is dictionary.
624
- fallback: Default value if key/index not found.
625
-
626
- Returns:
627
- Extracted float value.
628
- """
629
- if isinstance(data, dict):
630
- return float(data.get(key, fallback))
631
- if isinstance(data, (int, float)):
632
- return float(data)
633
- return float(data[idx])
662
+ def is_active(self) -> bool:
663
+ """Check if the chassis is active."""
664
+ return self.chassis_steer.is_active() and self.chassis_drive.is_active()
@@ -89,11 +89,11 @@ class RobotComponent:
89
89
  Parsed protobuf state message.
90
90
 
91
91
  Raises:
92
- RuntimeError: If no state data is available.
92
+ None: If no state data is available.
93
93
  """
94
94
  state = self._subscriber.get_latest_data()
95
95
  if state is None:
96
- raise RuntimeError("No state data available")
96
+ logger.error(f"No state data available for {self.__class__.__name__}")
97
97
  return state
98
98
 
99
99
  def wait_for_active(self, timeout: float = 5.0) -> bool:
@@ -133,6 +133,15 @@ class RobotComponent:
133
133
  if hasattr(self, "_subscriber") and self._subscriber:
134
134
  self._subscriber.shutdown()
135
135
 
136
+ def get_timestamp_ns(self) -> int:
137
+ """Get the current timestamp (in nanoseconds) of the most recent state update.
138
+
139
+ Returns:
140
+ The current timestamp in nanoseconds when driver updated the state.
141
+ We convert the time to client clock by adding the server time offset.
142
+ """
143
+ return self._get_state().timestamp_ns
144
+
136
145
 
137
146
  class RobotJointComponent(RobotComponent):
138
147
  """Base class for robot components with both state and control interfaces.
@@ -171,6 +180,8 @@ class RobotJointComponent(RobotComponent):
171
180
  state_message_type: type[M],
172
181
  zenoh_session: zenoh.Session,
173
182
  joint_name: list[str] | None = None,
183
+ joint_limit: list[list[float]] | None = None,
184
+ joint_vel_limit: list[float] | None = None,
174
185
  pose_pool: Mapping[str, list[float] | np.ndarray] | None = None,
175
186
  ) -> None:
176
187
  """Initializes RobotJointComponent.
@@ -190,6 +201,13 @@ class RobotJointComponent(RobotComponent):
190
201
  resolved_topic
191
202
  )
192
203
  self._joint_name: list[str] | None = joint_name
204
+ self._joint_limit: np.ndarray | None = (
205
+ np.array(joint_limit) if joint_limit else None
206
+ )
207
+ self._joint_vel_limit: np.ndarray | None = (
208
+ np.array(joint_vel_limit) if joint_vel_limit else None
209
+ )
210
+
193
211
  self._pose_pool: dict[str, np.ndarray] | None = (
194
212
  self._convert_pose_pool_to_arrays(pose_pool)
195
213
  )
@@ -231,6 +249,18 @@ class RobotJointComponent(RobotComponent):
231
249
  raise ValueError("Joint names not available for this component")
232
250
  return self._joint_name.copy()
233
251
 
252
+ @property
253
+ def joint_limit(self) -> np.ndarray | None:
254
+ """Gets the joint limits of the component."""
255
+ return self._joint_limit.copy() if self._joint_limit is not None else None
256
+
257
+ @property
258
+ def joint_vel_limit(self) -> np.ndarray | None:
259
+ """Gets the joint velocity limits of the component."""
260
+ return (
261
+ self._joint_vel_limit.copy() if self._joint_vel_limit is not None else None
262
+ )
263
+
234
264
  def get_predefined_pose(self, pose_name: str) -> np.ndarray:
235
265
  """Gets a predefined pose from the pose pool.
236
266
 
@@ -281,9 +311,9 @@ class RobotJointComponent(RobotComponent):
281
311
  ValueError: If joint positions are not available for this component.
282
312
  """
283
313
  state = self._get_state()
284
- if not hasattr(state, "joint_pos"):
314
+ if not hasattr(state, "pos"):
285
315
  raise ValueError("Joint positions are not available for this component.")
286
- joint_pos = np.array(state.joint_pos, dtype=np.float32)
316
+ joint_pos = np.array(state.pos, dtype=np.float32)
287
317
  return self._extract_joint_info(joint_pos, joint_id=joint_id)
288
318
 
289
319
  def get_joint_pos_dict(
@@ -319,9 +349,9 @@ class RobotJointComponent(RobotComponent):
319
349
  ValueError: If joint velocities are not available for this component.
320
350
  """
321
351
  state = self._get_state()
322
- if not hasattr(state, "joint_vel"):
352
+ if not hasattr(state, "vel"):
323
353
  raise ValueError("Joint velocities are not available for this component.")
324
- joint_vel = np.array(state.joint_vel, dtype=np.float32)
354
+ joint_vel = np.array(state.vel, dtype=np.float32)
325
355
  return self._extract_joint_info(joint_vel, joint_id=joint_id)
326
356
 
327
357
  def get_joint_vel_dict(
@@ -356,11 +386,31 @@ class RobotJointComponent(RobotComponent):
356
386
  ValueError: If joint currents are not available for this component.
357
387
  """
358
388
  state = self._get_state()
359
- if not hasattr(state, "joint_cur"):
389
+ if not hasattr(state, "cur"):
360
390
  raise ValueError("Joint currents are not available for this component.")
361
- joint_cur = np.array(state.joint_cur, dtype=np.float32)
391
+ joint_cur = np.array(state.cur, dtype=np.float32)
362
392
  return self._extract_joint_info(joint_cur, joint_id=joint_id)
363
393
 
394
+ def get_joint_torque(
395
+ self, joint_id: list[int] | int | None = None
396
+ ) -> Float[np.ndarray, " N"]:
397
+ """Gets the torque of all joints in the component.
398
+
399
+ Args:
400
+ joint_id: Optional ID(s) of specific joints to query.
401
+
402
+ Returns:
403
+ Array of joint torques in component-specific units (Nm).
404
+
405
+ Raises:
406
+ ValueError: If joint torques are not available for this component.
407
+ """
408
+ state = self._get_state()
409
+ if not hasattr(state, "torque"):
410
+ raise ValueError("Joint torques are not available for this component.")
411
+ joint_torque = np.array(state.torque, dtype=np.float32)
412
+ return self._extract_joint_info(joint_torque, joint_id=joint_id)
413
+
364
414
  def get_joint_current_dict(
365
415
  self, joint_id: list[int] | int | None = None
366
416
  ) -> dict[str, float]:
@@ -391,9 +441,9 @@ class RobotJointComponent(RobotComponent):
391
441
  ValueError: If joint error codes are not available for this component.
392
442
  """
393
443
  state = self._get_state()
394
- if not hasattr(state, "joint_err"):
444
+ if not hasattr(state, "error"):
395
445
  raise ValueError("Joint error codes are not available for this component.")
396
- joint_err = np.array(state.joint_err, dtype=np.uint32)
446
+ joint_err = np.array(state.error, dtype=np.uint32)
397
447
  return self._extract_joint_info(joint_err, joint_id=joint_id)
398
448
 
399
449
  def get_joint_err_dict(
@@ -427,22 +477,27 @@ class RobotJointComponent(RobotComponent):
427
477
  ValueError: If joint positions or velocities are not available.
428
478
  """
429
479
  state = self._get_state()
430
- if not hasattr(state, "joint_pos") or not hasattr(state, "joint_vel"):
480
+ if not hasattr(state, "pos") or not hasattr(state, "vel"):
431
481
  raise ValueError(
432
482
  "Joint positions or velocities are not available for this component."
433
483
  )
434
484
 
435
485
  # Create initial state array with positions and velocities
436
- joint_pos = np.array(state.joint_pos, dtype=np.float32)
437
- joint_vel = np.array(state.joint_vel, dtype=np.float32)
486
+ joint_pos = np.array(state.pos, dtype=np.float32)
487
+ joint_vel = np.array(state.vel, dtype=np.float32)
438
488
 
439
- if hasattr(state, "joint_cur"):
489
+ if hasattr(state, "cur"):
440
490
  # If currents are available, include them
441
- joint_cur = np.array(state.joint_cur, dtype=np.float32)
491
+ joint_cur = np.array(state.cur, dtype=np.float32)
442
492
  joint_state = np.stack([joint_pos, joint_vel, joint_cur], axis=1)
493
+ elif hasattr(state, "torque"):
494
+ # If torques are available, include them
495
+ joint_torque = np.array(state.torque, dtype=np.float32)
496
+ joint_state = np.stack([joint_pos, joint_vel, joint_torque], axis=1)
443
497
  else:
444
- # Otherwise just include positions and velocities
445
- joint_state = np.stack([joint_pos, joint_vel], axis=1)
498
+ raise ValueError(
499
+ f"Either current or torque should be available for this {self.__class__.__name__}."
500
+ )
446
501
 
447
502
  return self._extract_joint_info(joint_state, joint_id=joint_id)
448
503
 
@@ -466,7 +521,7 @@ class RobotJointComponent(RobotComponent):
466
521
  def _convert_joint_cmd_to_array(
467
522
  self,
468
523
  joint_cmd: Float[np.ndarray, " N"] | list[float] | dict[str, float],
469
- clip_value: float | None = None,
524
+ clip_value: float | np.ndarray | None = None,
470
525
  ) -> np.ndarray:
471
526
  """Convert joint command to numpy array format.
472
527
 
@@ -475,7 +530,9 @@ class RobotJointComponent(RobotComponent):
475
530
  - List of joint values [j1, j2, ..., jN]
476
531
  - Numpy array with shape (N,)
477
532
  - Dictionary mapping joint names to values
478
- clip_value: Optional value to clip the output array between [-clip_value, clip_value].
533
+ clip_value: Optional value to clip the output array. Can be:
534
+ - float: symmetric clipping between [-clip_value, clip_value]
535
+ - numpy array: element-wise clipping between [-clip_value, clip_value]
479
536
 
480
537
  Returns:
481
538
  Joint command as numpy array.
@@ -511,8 +568,9 @@ class RobotJointComponent(RobotComponent):
511
568
  joint_cmd = self._convert_joint_cmd_to_array(joint_cmd)
512
569
  return self.get_joint_pos() + joint_cmd
513
570
 
571
+ @staticmethod
514
572
  def _extract_joint_info(
515
- self, joint_info: np.ndarray, joint_id: list[int] | int | None = None
573
+ joint_info: np.ndarray, joint_id: list[int] | int | None = None
516
574
  ) -> np.ndarray:
517
575
  """Extract the joint information of the component as a numpy array.
518
576
 
@@ -660,6 +718,11 @@ class RobotJointComponent(RobotComponent):
660
718
  if isinstance(joint_pos, (list, dict)):
661
719
  joint_pos = self._convert_joint_cmd_to_array(joint_pos)
662
720
 
721
+ if self._joint_limit is not None:
722
+ joint_pos = np.clip(
723
+ joint_pos, self._joint_limit[:, 0], self._joint_limit[:, 1]
724
+ )
725
+
663
726
  self._send_position_command(joint_pos)
664
727
 
665
728
  if wait_time > 0.0: