dexcontrol 0.2.12__py3-none-any.whl → 0.3.4__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.
Files changed (60) hide show
  1. dexcontrol/__init__.py +17 -8
  2. dexcontrol/apps/dualsense_teleop_base.py +1 -1
  3. dexcontrol/comm/__init__.py +51 -0
  4. dexcontrol/comm/rtc.py +401 -0
  5. dexcontrol/comm/subscribers.py +329 -0
  6. dexcontrol/config/core/chassis.py +9 -4
  7. dexcontrol/config/core/hand.py +1 -0
  8. dexcontrol/config/sensors/cameras/__init__.py +1 -2
  9. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  10. dexcontrol/config/sensors/vega_sensors.py +12 -18
  11. dexcontrol/config/vega.py +4 -1
  12. dexcontrol/core/arm.py +66 -42
  13. dexcontrol/core/chassis.py +142 -120
  14. dexcontrol/core/component.py +107 -58
  15. dexcontrol/core/hand.py +119 -86
  16. dexcontrol/core/head.py +22 -33
  17. dexcontrol/core/misc.py +331 -158
  18. dexcontrol/core/robot_query_interface.py +467 -0
  19. dexcontrol/core/torso.py +5 -9
  20. dexcontrol/robot.py +245 -574
  21. dexcontrol/sensors/__init__.py +1 -2
  22. dexcontrol/sensors/camera/__init__.py +0 -2
  23. dexcontrol/sensors/camera/base_camera.py +150 -0
  24. dexcontrol/sensors/camera/rgb_camera.py +68 -64
  25. dexcontrol/sensors/camera/zed_camera.py +140 -164
  26. dexcontrol/sensors/imu/chassis_imu.py +81 -62
  27. dexcontrol/sensors/imu/zed_imu.py +54 -43
  28. dexcontrol/sensors/lidar/rplidar.py +16 -20
  29. dexcontrol/sensors/manager.py +4 -14
  30. dexcontrol/sensors/ultrasonic.py +15 -28
  31. dexcontrol/utils/__init__.py +0 -11
  32. dexcontrol/utils/comm_helper.py +110 -0
  33. dexcontrol/utils/constants.py +1 -1
  34. dexcontrol/utils/error_code.py +2 -4
  35. dexcontrol/utils/os_utils.py +172 -4
  36. dexcontrol/utils/pb_utils.py +6 -28
  37. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/METADATA +16 -3
  38. dexcontrol-0.3.4.dist-info/RECORD +62 -0
  39. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/WHEEL +1 -1
  40. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  41. dexcontrol/proto/dexcontrol_msg_pb2.py +0 -73
  42. dexcontrol/proto/dexcontrol_msg_pb2.pyi +0 -220
  43. dexcontrol/proto/dexcontrol_query_pb2.py +0 -77
  44. dexcontrol/proto/dexcontrol_query_pb2.pyi +0 -162
  45. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  46. dexcontrol/utils/motion_utils.py +0 -199
  47. dexcontrol/utils/rate_limiter.py +0 -172
  48. dexcontrol/utils/rtc_utils.py +0 -144
  49. dexcontrol/utils/subscribers/__init__.py +0 -52
  50. dexcontrol/utils/subscribers/base.py +0 -281
  51. dexcontrol/utils/subscribers/camera.py +0 -332
  52. dexcontrol/utils/subscribers/decoders.py +0 -88
  53. dexcontrol/utils/subscribers/generic.py +0 -110
  54. dexcontrol/utils/subscribers/imu.py +0 -175
  55. dexcontrol/utils/subscribers/lidar.py +0 -172
  56. dexcontrol/utils/subscribers/protobuf.py +0 -111
  57. dexcontrol/utils/subscribers/rtc.py +0 -316
  58. dexcontrol/utils/zenoh_utils.py +0 -122
  59. dexcontrol-0.2.12.dist-info/RECORD +0 -75
  60. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.4.dist-info}/licenses/LICENSE +0 -0
@@ -17,15 +17,90 @@ Zenoh communication. It handles steering and wheel velocity control.
17
17
  import time
18
18
 
19
19
  import numpy as np
20
- import zenoh
20
+ from dexcomm.serialization.protobuf import control_msg_pb2
21
+ from dexcomm.utils import RateLimiter
22
+ from jaxtyping import Float
21
23
 
22
24
  from dexcontrol.config.core import ChassisConfig
23
25
  from dexcontrol.core.component import RobotJointComponent
24
- from dexcontrol.proto import dexcontrol_msg_pb2
25
- from dexcontrol.utils.rate_limiter import RateLimiter
26
26
 
27
27
 
28
- class Chassis(RobotJointComponent):
28
+ class ChassisSteer(RobotJointComponent):
29
+ """Robot hand control class.
30
+
31
+ This class provides methods to control a robotic hand by publishing commands and
32
+ receiving state information through Zenoh communication.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ configs: ChassisConfig,
38
+ ) -> None:
39
+ """Initialize the hand controller.
40
+
41
+ Args:
42
+ configs: Hand configuration parameters containing communication topics
43
+ and predefined hand positions.
44
+ """
45
+ super().__init__(
46
+ state_sub_topic=configs.steer_state_sub_topic,
47
+ control_pub_topic=configs.steer_control_pub_topic,
48
+ state_message_type=control_msg_pb2.MotorStateWithCurrent,
49
+ joint_name=configs.steer_joint_name,
50
+ )
51
+
52
+ def _send_position_command(
53
+ self, joint_pos: Float[np.ndarray, " 2"] | list[float]
54
+ ) -> None:
55
+ """Send joint position control commands to the hand.
56
+
57
+ Args:
58
+ joint_pos: Joint positions as list or numpy array.
59
+ """
60
+ control_msg = control_msg_pb2.MotorPosVelCommand()
61
+ joint_pos_array = self._convert_joint_cmd_to_array(joint_pos)
62
+ control_msg.pos.extend(joint_pos_array.tolist())
63
+ self._publish_control(control_msg)
64
+
65
+
66
+ class ChassisDrive(RobotJointComponent):
67
+ """Robot base control class.
68
+
69
+ This class provides methods to control a robot's wheeled base by publishing commands
70
+ and receiving state information through Zenoh communication.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ configs: ChassisConfig,
76
+ ) -> None:
77
+ """Initialize the base controller.
78
+
79
+ Args:
80
+ configs: Base configuration parameters containing communication topics.
81
+ """
82
+ super().__init__(
83
+ state_sub_topic=configs.drive_state_sub_topic,
84
+ control_pub_topic=configs.drive_control_pub_topic,
85
+ state_message_type=control_msg_pb2.MotorStateWithCurrent,
86
+ joint_name=configs.drive_joint_name,
87
+ )
88
+
89
+ def _send_position_command(
90
+ self, joint_pos: Float[np.ndarray, " 2"] | list[float]
91
+ ) -> None:
92
+ raise NotImplementedError("ChassisDrive does not support position control.")
93
+
94
+ def _send_velocity_command(
95
+ self, joint_vel: Float[np.ndarray, " 2"] | list[float]
96
+ ) -> None:
97
+ control_msg = control_msg_pb2.MotorVelCommand()
98
+ joint_vel_array = self._convert_joint_cmd_to_array(joint_vel)
99
+ control_msg.vel.extend(joint_vel_array.tolist())
100
+ self._publish_control(control_msg)
101
+
102
+
103
+ class Chassis:
29
104
  """Robot base control class.
30
105
 
31
106
  This class provides methods to control a robot's wheeled base by publishing commands
@@ -44,21 +119,15 @@ class Chassis(RobotJointComponent):
44
119
  def __init__(
45
120
  self,
46
121
  configs: ChassisConfig,
47
- zenoh_session: zenoh.Session,
48
122
  ) -> None:
49
123
  """Initialize the base controller.
50
124
 
51
125
  Args:
52
126
  configs: Base configuration parameters containing communication topics.
53
- zenoh_session: Active Zenoh communication session for message passing.
54
127
  """
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
- )
128
+ self.chassis_steer = ChassisSteer(configs)
129
+ self.chassis_drive = ChassisDrive(configs)
130
+
62
131
  self.max_vel = configs.max_vel
63
132
  self._center_to_wheel_axis_dist = configs.center_to_wheel_axis_dist
64
133
  self._wheels_dist = configs.wheels_dist
@@ -96,7 +165,8 @@ class Chassis(RobotJointComponent):
96
165
  def shutdown(self) -> None:
97
166
  """Shutdown the base by stopping all motion."""
98
167
  self.stop()
99
- super().shutdown()
168
+ self.chassis_steer.shutdown()
169
+ self.chassis_drive.shutdown()
100
170
 
101
171
  @property
102
172
  def steering_angle(self) -> np.ndarray:
@@ -106,8 +176,7 @@ class Chassis(RobotJointComponent):
106
176
  Numpy array of shape (2,) containing current steering angles in radians.
107
177
  Index 0 is left wheel, index 1 is right wheel.
108
178
  """
109
- state = self._get_state()
110
- return np.array([state.left.steering_pos, state.right.steering_pos])
179
+ return self.chassis_steer.get_joint_pos()
111
180
 
112
181
  @property
113
182
  def wheel_velocity(self) -> np.ndarray:
@@ -117,8 +186,17 @@ class Chassis(RobotJointComponent):
117
186
  Numpy array of shape (2,) containing current wheel velocities in m/s.
118
187
  Index 0 is left wheel, index 1 is right wheel.
119
188
  """
120
- state = self._get_state()
121
- return np.array([state.left.wheel_vel, state.right.wheel_vel])
189
+ return self.chassis_drive.get_joint_vel()
190
+
191
+ @property
192
+ def wheel_encoder_pos(self) -> np.ndarray:
193
+ """Get current wheel encoder positions.
194
+
195
+ Returns:
196
+ Numpy array of shape (2,) containing current wheel encoder positions in radians.
197
+ Index 0 is left wheel, index 1 is right wheel.
198
+ """
199
+ return self.chassis_drive.get_joint_pos()
122
200
 
123
201
  def set_velocity(
124
202
  self,
@@ -356,24 +434,23 @@ class Chassis(RobotJointComponent):
356
434
  Args:
357
435
  joint_id: Optional ID(s) of specific joints to query.
358
436
  0: left steering position
359
- 1: left wheel position
360
- 2: right steering position
437
+ 1: right steering position
438
+ 2: left wheel position
361
439
  3: right wheel position
362
440
 
363
441
  Returns:
364
442
  Array of joint positions in the order:
365
- [left_steering_pos, left_wheel_pos, right_steering_pos, right_wheel_pos]
443
+ [left_steering_pos, right_steering_pos, left_wheel_pos, right_wheel_pos]
366
444
  """
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)
445
+ steer_pos = self.chassis_steer.get_joint_pos()
446
+ drive_pos = self.chassis_drive.get_joint_pos()
447
+ joint_pos = np.concatenate([steer_pos, drive_pos])
448
+ return RobotJointComponent._extract_joint_info(joint_pos, joint_id=joint_id)
449
+
450
+ @property
451
+ def joint_name(self) -> list[str]:
452
+ """Get the joint names of the chassis."""
453
+ return self.chassis_steer.joint_name + self.chassis_drive.joint_name
377
454
 
378
455
  def get_joint_pos_dict(
379
456
  self, joint_id: list[int] | int | None = None
@@ -383,58 +460,22 @@ class Chassis(RobotJointComponent):
383
460
  Args:
384
461
  joint_id: Optional ID(s) of specific joints to query.
385
462
  0: left steering position
386
- 1: left wheel position
387
- 2: right steering position
463
+ 1: right steering position
464
+ 2: left wheel position
388
465
  3: right wheel position
389
466
 
390
467
  Returns:
391
468
  Dictionary mapping joint names to position values.
392
469
  """
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)
470
+ steer_pos = self.chassis_steer.get_joint_pos_dict()
471
+ drive_pos = self.chassis_drive.get_joint_pos_dict()
472
+ if joint_id is None:
473
+ return {**steer_pos, **drive_pos}
474
+ else:
475
+ joint_names = self.joint_name
476
+ if isinstance(joint_id, int):
477
+ joint_id = [joint_id]
478
+ return {joint_names[i]: steer_pos[joint_names[i]] for i in joint_id}
438
479
 
439
480
  def set_motion_state(
440
481
  self,
@@ -527,10 +568,19 @@ class Chassis(RobotJointComponent):
527
568
  # Set default control frequency if not provided
528
569
  control_hz = wait_kwargs.get("control_hz", 50)
529
570
  rate_limiter = RateLimiter(control_hz)
530
- start_time = time.time()
531
571
 
532
- while time.time() - start_time < wait_time:
572
+ now_sec = time.monotonic()
573
+ duration_sec = max(wait_time - 1.0, 0.0)
574
+ end_time_sec = now_sec + duration_sec
575
+
576
+ while True:
577
+ # Ensure command is sent at least once
533
578
  self._send_single_command(steering_angle, wheel_velocity)
579
+
580
+ # Exit if we've reached the allotted duration
581
+ if time.monotonic() >= end_time_sec:
582
+ break
583
+
534
584
  rate_limiter.sleep()
535
585
 
536
586
  def _send_single_command(
@@ -544,24 +594,16 @@ class Chassis(RobotJointComponent):
544
594
  steering_angle: Target steering angles.
545
595
  wheel_velocity: Target wheel velocities.
546
596
  """
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
- )
597
+ if isinstance(steering_angle, float):
598
+ steering_angle = np.array([steering_angle, steering_angle])
599
+ self.chassis_steer._send_position_command(steering_angle)
600
+ elif isinstance(steering_angle, np.ndarray):
601
+ self.chassis_steer._send_position_command(steering_angle)
563
602
 
564
- self._publish_control(control_msg)
603
+ if isinstance(wheel_velocity, float):
604
+ wheel_velocity = np.array([wheel_velocity, wheel_velocity])
605
+ elif isinstance(wheel_velocity, np.ndarray):
606
+ self.chassis_drive._send_velocity_command(wheel_velocity)
565
607
 
566
608
  def _compute_wheel_control(
567
609
  self, wheel_velocity: np.ndarray, current_angle: float
@@ -608,26 +650,6 @@ class Chassis(RobotJointComponent):
608
650
 
609
651
  return steering_angle, wheel_speed
610
652
 
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])
653
+ def is_active(self) -> bool:
654
+ """Check if the chassis is active."""
655
+ return self.chassis_steer.is_active() and self.chassis_drive.is_active()
@@ -8,24 +8,25 @@
8
8
  # 2. Commercial License
9
9
  # For commercial licensing terms, contact: contact@dexmate.ai
10
10
 
11
- """Base module for robot components with Zenoh communication.
11
+ """Base module for robot components using DexComm communication.
12
12
 
13
- This module provides base classes for robot components that communicate via Zenoh.
14
- It includes RobotComponent for state-only components and RobotJointComponent for
15
- components that also support control commands.
13
+ This module provides base classes for robot components that use DexComm's
14
+ Raw API for communication. It includes RobotComponent for state-only components
15
+ and RobotJointComponent for components that also support control commands.
16
16
  """
17
17
 
18
18
  import time
19
19
  from typing import Any, Final, Mapping, TypeVar
20
20
 
21
21
  import numpy as np
22
- import zenoh
22
+ from dexcomm import Publisher, Subscriber
23
+ from dexcomm.serialization import deserialize_protobuf, serialize_protobuf
23
24
  from google.protobuf.message import Message
24
25
  from jaxtyping import Float
25
26
  from loguru import logger
26
27
 
28
+ from dexcontrol.utils.comm_helper import get_zenoh_config_path
27
29
  from dexcontrol.utils.os_utils import resolve_key_name
28
- from dexcontrol.utils.subscribers import ProtobufZenohSubscriber
29
30
 
30
31
  # Type variable for Message subclasses
31
32
  M = TypeVar("M", bound=Message)
@@ -47,41 +48,55 @@ class RobotComponent:
47
48
  def __init__(
48
49
  self,
49
50
  state_sub_topic: str,
50
- zenoh_session: zenoh.Session,
51
51
  state_message_type: type[M],
52
52
  ) -> None:
53
53
  """Initializes RobotComponent.
54
54
 
55
55
  Args:
56
56
  state_sub_topic: Topic to subscribe to for state updates.
57
- zenoh_session: Active Zenoh session for communication.
58
57
  state_message_type: Protobuf message class for component state.
59
58
  """
60
59
  self._state_message_type = state_message_type
61
- self._zenoh_session: Final[zenoh.Session] = zenoh_session
62
- self._init_subscriber(state_sub_topic, state_message_type, zenoh_session)
60
+ self._latest_state: M | None = None
61
+ self._is_active = False
62
+ self._init_subscriber(state_sub_topic, state_message_type)
63
63
 
64
64
  def _init_subscriber(
65
65
  self,
66
66
  state_sub_topic: str,
67
67
  state_message_type: type[M],
68
- zenoh_session: zenoh.Session,
69
68
  ) -> None:
70
- """Initialize the Zenoh subscriber for state updates.
69
+ """Initialize the subscriber for state updates using DexComm.
71
70
 
72
71
  Args:
73
72
  state_sub_topic: Topic to subscribe to for state updates.
74
73
  state_message_type: Protobuf message class for component state.
75
- zenoh_session: Active Zenoh session for communication.
76
- """
77
- self._subscriber = ProtobufZenohSubscriber(
78
- topic=state_sub_topic,
79
- zenoh_session=zenoh_session,
80
- message_type=state_message_type,
81
- name=f"{self.__class__.__name__}",
82
- enable_fps_tracking=False,
74
+ """
75
+ # Resolve topic with robot namespace
76
+ full_topic = resolve_key_name(state_sub_topic)
77
+
78
+ # Create DexComm subscriber with protobuf deserialization
79
+ self._subscriber = Subscriber(
80
+ topic=full_topic,
81
+ callback=self._on_state_update,
82
+ deserializer=lambda data: deserialize_protobuf(data, state_message_type),
83
+ config=get_zenoh_config_path(),
84
+ )
85
+
86
+ logger.debug(
87
+ f"Created subscriber for {self.__class__.__name__} on {full_topic}"
83
88
  )
84
89
 
90
+ def _on_state_update(self, state: M) -> None:
91
+ """Handle incoming state updates.
92
+
93
+ Args:
94
+ state: Deserialized protobuf state message.
95
+ """
96
+ self._latest_state = state
97
+ if state:
98
+ self._is_active = True
99
+
85
100
  def _get_state(self) -> M:
86
101
  """Gets the current state of the component.
87
102
 
@@ -91,10 +106,9 @@ class RobotComponent:
91
106
  Raises:
92
107
  None: If no state data is available.
93
108
  """
94
- state = self._subscriber.get_latest_data()
95
- if state is None:
109
+ if self._latest_state is None:
96
110
  logger.error(f"No state data available for {self.__class__.__name__}")
97
- return state
111
+ return self._latest_state
98
112
 
99
113
  def wait_for_active(self, timeout: float = 5.0) -> bool:
100
114
  """Waits for the component to start receiving state updates.
@@ -105,7 +119,12 @@ class RobotComponent:
105
119
  Returns:
106
120
  True if component becomes active, False if timeout is reached.
107
121
  """
108
- return self._subscriber.wait_for_active(timeout)
122
+ start_time = time.time()
123
+ while time.time() - start_time < timeout:
124
+ if self._is_active:
125
+ return True
126
+ time.sleep(0.1)
127
+ return False
109
128
 
110
129
  def is_active(self) -> bool:
111
130
  """Check if component is receiving state updates.
@@ -113,7 +132,7 @@ class RobotComponent:
113
132
  Returns:
114
133
  True if component is active, False otherwise.
115
134
  """
116
- return self._subscriber.is_active()
135
+ return self._is_active
117
136
 
118
137
  def shutdown(self) -> None:
119
138
  """Cleans up Zenoh resources."""
@@ -149,7 +168,7 @@ class RobotJointComponent(RobotComponent):
149
168
  Extends RobotComponent to add APIs for interacting with joints.
150
169
 
151
170
  Attributes:
152
- _publisher: Zenoh publisher for control commands.
171
+ _publisher: Publisher for control commands (Zenoh or dexcomm).
153
172
  _joint_name: List of joint names for this component.
154
173
  _pose_pool: Dictionary of predefined poses for this component.
155
174
  """
@@ -178,7 +197,6 @@ class RobotJointComponent(RobotComponent):
178
197
  state_sub_topic: str,
179
198
  control_pub_topic: str,
180
199
  state_message_type: type[M],
181
- zenoh_session: zenoh.Session,
182
200
  joint_name: list[str] | None = None,
183
201
  joint_limit: list[list[float]] | None = None,
184
202
  joint_vel_limit: list[float] | None = None,
@@ -190,16 +208,24 @@ class RobotJointComponent(RobotComponent):
190
208
  state_sub_topic: Topic to subscribe to for state updates.
191
209
  control_pub_topic: Topic to publish control commands.
192
210
  state_message_type: Protobuf message class for component state.
193
- zenoh_session: Active Zenoh session for communication.
194
211
  joint_name: List of joint names for this component.
212
+ joint_limit: Joint position limits.
213
+ joint_vel_limit: Joint velocity limits.
195
214
  pose_pool: Dictionary of predefined poses for this component.
196
215
  """
197
- super().__init__(state_sub_topic, zenoh_session, state_message_type)
216
+ super().__init__(state_sub_topic, state_message_type)
198
217
 
199
- resolved_topic: Final[str] = resolve_key_name(control_pub_topic)
200
- self._publisher: Final[zenoh.Publisher] = zenoh_session.declare_publisher(
201
- resolved_topic
218
+ # Resolve topic with robot namespace
219
+ full_topic = resolve_key_name(control_pub_topic)
220
+
221
+ # Create DexComm publisher with protobuf serialization
222
+ self._publisher: Final[Publisher] = Publisher(
223
+ topic=full_topic,
224
+ serializer=serialize_protobuf,
225
+ config=get_zenoh_config_path(),
202
226
  )
227
+
228
+ logger.debug(f"Created publisher for {self.__class__.__name__} on {full_topic}")
203
229
  self._joint_name: list[str] | None = joint_name
204
230
  self._joint_limit: np.ndarray | None = (
205
231
  np.array(joint_limit) if joint_limit else None
@@ -218,22 +244,19 @@ class RobotJointComponent(RobotComponent):
218
244
  Args:
219
245
  control_msg: Protobuf control message to publish.
220
246
  """
221
- msg_bytes = control_msg.SerializeToString()
222
- self._publisher.put(msg_bytes)
247
+ # DexComm publisher with protobuf serializer handles this
248
+ self._publisher.publish(control_msg)
223
249
 
224
250
  def shutdown(self) -> None:
225
- """Cleans up all Zenoh resources."""
251
+ """Cleans up all communication resources."""
226
252
  super().shutdown()
227
253
  try:
228
254
  if hasattr(self, "_publisher") and self._publisher:
229
- self._publisher.undeclare()
255
+ self._publisher.shutdown()
230
256
  except Exception as e:
231
- # Don't log "Undeclared publisher" errors as warnings - they're expected during shutdown
232
- error_msg = str(e).lower()
233
- if not ("undeclared" in error_msg or "closed" in error_msg):
234
- logger.warning(
235
- f"Error undeclaring publisher for {self.__class__.__name__}: {e}"
236
- )
257
+ logger.warning(
258
+ f"Error shutting down publisher for {self.__class__.__name__}: {e}"
259
+ )
237
260
 
238
261
  @property
239
262
  def joint_name(self) -> list[str]:
@@ -311,9 +334,9 @@ class RobotJointComponent(RobotComponent):
311
334
  ValueError: If joint positions are not available for this component.
312
335
  """
313
336
  state = self._get_state()
314
- if not hasattr(state, "joint_pos"):
337
+ if not hasattr(state, "pos"):
315
338
  raise ValueError("Joint positions are not available for this component.")
316
- joint_pos = np.array(state.joint_pos, dtype=np.float32)
339
+ joint_pos = np.array(state.pos, dtype=np.float32)
317
340
  return self._extract_joint_info(joint_pos, joint_id=joint_id)
318
341
 
319
342
  def get_joint_pos_dict(
@@ -349,9 +372,9 @@ class RobotJointComponent(RobotComponent):
349
372
  ValueError: If joint velocities are not available for this component.
350
373
  """
351
374
  state = self._get_state()
352
- if not hasattr(state, "joint_vel"):
375
+ if not hasattr(state, "vel"):
353
376
  raise ValueError("Joint velocities are not available for this component.")
354
- joint_vel = np.array(state.joint_vel, dtype=np.float32)
377
+ joint_vel = np.array(state.vel, dtype=np.float32)
355
378
  return self._extract_joint_info(joint_vel, joint_id=joint_id)
356
379
 
357
380
  def get_joint_vel_dict(
@@ -386,11 +409,31 @@ class RobotJointComponent(RobotComponent):
386
409
  ValueError: If joint currents are not available for this component.
387
410
  """
388
411
  state = self._get_state()
389
- if not hasattr(state, "joint_cur"):
412
+ if not hasattr(state, "cur"):
390
413
  raise ValueError("Joint currents are not available for this component.")
391
- joint_cur = np.array(state.joint_cur, dtype=np.float32)
414
+ joint_cur = np.array(state.cur, dtype=np.float32)
392
415
  return self._extract_joint_info(joint_cur, joint_id=joint_id)
393
416
 
417
+ def get_joint_torque(
418
+ self, joint_id: list[int] | int | None = None
419
+ ) -> Float[np.ndarray, " N"]:
420
+ """Gets the torque of all joints in the component.
421
+
422
+ Args:
423
+ joint_id: Optional ID(s) of specific joints to query.
424
+
425
+ Returns:
426
+ Array of joint torques in component-specific units (Nm).
427
+
428
+ Raises:
429
+ ValueError: If joint torques are not available for this component.
430
+ """
431
+ state = self._get_state()
432
+ if not hasattr(state, "torque"):
433
+ raise ValueError("Joint torques are not available for this component.")
434
+ joint_torque = np.array(state.torque, dtype=np.float32)
435
+ return self._extract_joint_info(joint_torque, joint_id=joint_id)
436
+
394
437
  def get_joint_current_dict(
395
438
  self, joint_id: list[int] | int | None = None
396
439
  ) -> dict[str, float]:
@@ -421,9 +464,9 @@ class RobotJointComponent(RobotComponent):
421
464
  ValueError: If joint error codes are not available for this component.
422
465
  """
423
466
  state = self._get_state()
424
- if not hasattr(state, "joint_err"):
467
+ if not hasattr(state, "error"):
425
468
  raise ValueError("Joint error codes are not available for this component.")
426
- joint_err = np.array(state.joint_err, dtype=np.uint32)
469
+ joint_err = np.array(state.error, dtype=np.uint32)
427
470
  return self._extract_joint_info(joint_err, joint_id=joint_id)
428
471
 
429
472
  def get_joint_err_dict(
@@ -457,22 +500,27 @@ class RobotJointComponent(RobotComponent):
457
500
  ValueError: If joint positions or velocities are not available.
458
501
  """
459
502
  state = self._get_state()
460
- if not hasattr(state, "joint_pos") or not hasattr(state, "joint_vel"):
503
+ if not hasattr(state, "pos") or not hasattr(state, "vel"):
461
504
  raise ValueError(
462
505
  "Joint positions or velocities are not available for this component."
463
506
  )
464
507
 
465
508
  # Create initial state array with positions and velocities
466
- joint_pos = np.array(state.joint_pos, dtype=np.float32)
467
- joint_vel = np.array(state.joint_vel, dtype=np.float32)
509
+ joint_pos = np.array(state.pos, dtype=np.float32)
510
+ joint_vel = np.array(state.vel, dtype=np.float32)
468
511
 
469
- if hasattr(state, "joint_cur"):
512
+ if hasattr(state, "cur"):
470
513
  # If currents are available, include them
471
- joint_cur = np.array(state.joint_cur, dtype=np.float32)
514
+ joint_cur = np.array(state.cur, dtype=np.float32)
472
515
  joint_state = np.stack([joint_pos, joint_vel, joint_cur], axis=1)
516
+ elif hasattr(state, "torque"):
517
+ # If torques are available, include them
518
+ joint_torque = np.array(state.torque, dtype=np.float32)
519
+ joint_state = np.stack([joint_pos, joint_vel, joint_torque], axis=1)
473
520
  else:
474
- # Otherwise just include positions and velocities
475
- joint_state = np.stack([joint_pos, joint_vel], axis=1)
521
+ raise ValueError(
522
+ f"Either current or torque should be available for this {self.__class__.__name__}."
523
+ )
476
524
 
477
525
  return self._extract_joint_info(joint_state, joint_id=joint_id)
478
526
 
@@ -543,8 +591,9 @@ class RobotJointComponent(RobotComponent):
543
591
  joint_cmd = self._convert_joint_cmd_to_array(joint_cmd)
544
592
  return self.get_joint_pos() + joint_cmd
545
593
 
594
+ @staticmethod
546
595
  def _extract_joint_info(
547
- self, joint_info: np.ndarray, joint_id: list[int] | int | None = None
596
+ joint_info: np.ndarray, joint_id: list[int] | int | None = None
548
597
  ) -> np.ndarray:
549
598
  """Extract the joint information of the component as a numpy array.
550
599