dexcontrol 0.2.12__py3-none-any.whl → 0.3.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 (59) hide show
  1. dexcontrol/__init__.py +18 -8
  2. dexcontrol/apps/dualsense_teleop_base.py +1 -1
  3. dexcontrol/comm/__init__.py +51 -0
  4. dexcontrol/comm/base.py +421 -0
  5. dexcontrol/comm/rtc.py +400 -0
  6. dexcontrol/comm/subscribers.py +329 -0
  7. dexcontrol/config/core/chassis.py +9 -4
  8. dexcontrol/config/core/hand.py +1 -0
  9. dexcontrol/config/sensors/cameras/__init__.py +1 -2
  10. dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
  11. dexcontrol/config/sensors/vega_sensors.py +12 -18
  12. dexcontrol/config/vega.py +4 -1
  13. dexcontrol/core/arm.py +61 -37
  14. dexcontrol/core/chassis.py +141 -119
  15. dexcontrol/core/component.py +110 -59
  16. dexcontrol/core/hand.py +118 -85
  17. dexcontrol/core/head.py +18 -29
  18. dexcontrol/core/misc.py +327 -155
  19. dexcontrol/core/robot_query_interface.py +463 -0
  20. dexcontrol/core/torso.py +4 -8
  21. dexcontrol/proto/dexcontrol_msg_pb2.py +27 -39
  22. dexcontrol/proto/dexcontrol_msg_pb2.pyi +75 -118
  23. dexcontrol/proto/dexcontrol_query_pb2.py +39 -39
  24. dexcontrol/proto/dexcontrol_query_pb2.pyi +17 -4
  25. dexcontrol/robot.py +245 -574
  26. dexcontrol/sensors/__init__.py +1 -2
  27. dexcontrol/sensors/camera/__init__.py +0 -2
  28. dexcontrol/sensors/camera/base_camera.py +144 -0
  29. dexcontrol/sensors/camera/rgb_camera.py +67 -63
  30. dexcontrol/sensors/camera/zed_camera.py +89 -147
  31. dexcontrol/sensors/imu/chassis_imu.py +76 -56
  32. dexcontrol/sensors/imu/zed_imu.py +54 -43
  33. dexcontrol/sensors/lidar/rplidar.py +16 -20
  34. dexcontrol/sensors/manager.py +4 -11
  35. dexcontrol/sensors/ultrasonic.py +14 -27
  36. dexcontrol/utils/__init__.py +0 -11
  37. dexcontrol/utils/comm_helper.py +111 -0
  38. dexcontrol/utils/constants.py +1 -1
  39. dexcontrol/utils/os_utils.py +169 -1
  40. dexcontrol/utils/pb_utils.py +0 -22
  41. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/METADATA +13 -1
  42. dexcontrol-0.3.1.dist-info/RECORD +68 -0
  43. dexcontrol/config/sensors/cameras/luxonis_camera.py +0 -51
  44. dexcontrol/sensors/camera/luxonis_camera.py +0 -169
  45. dexcontrol/utils/rate_limiter.py +0 -172
  46. dexcontrol/utils/rtc_utils.py +0 -144
  47. dexcontrol/utils/subscribers/__init__.py +0 -52
  48. dexcontrol/utils/subscribers/base.py +0 -281
  49. dexcontrol/utils/subscribers/camera.py +0 -332
  50. dexcontrol/utils/subscribers/decoders.py +0 -88
  51. dexcontrol/utils/subscribers/generic.py +0 -110
  52. dexcontrol/utils/subscribers/imu.py +0 -175
  53. dexcontrol/utils/subscribers/lidar.py +0 -172
  54. dexcontrol/utils/subscribers/protobuf.py +0 -111
  55. dexcontrol/utils/subscribers/rtc.py +0 -316
  56. dexcontrol/utils/zenoh_utils.py +0 -122
  57. dexcontrol-0.2.12.dist-info/RECORD +0 -75
  58. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.dist-info}/WHEEL +0 -0
  59. {dexcontrol-0.2.12.dist-info → dexcontrol-0.3.1.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.utils import RateLimiter
21
+ from jaxtyping import Float
21
22
 
22
23
  from dexcontrol.config.core import ChassisConfig
23
24
  from dexcontrol.core.component import RobotJointComponent
24
25
  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=dexcontrol_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 = dexcontrol_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=dexcontrol_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 = dexcontrol_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,27 @@
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
- from typing import Any, Final, Mapping, TypeVar
19
+ from typing import Any, Final, Mapping, Optional, TypeVar
20
20
 
21
21
  import numpy as np
22
- import zenoh
22
+
23
+ # Use DexComm for all communication
24
+ from dexcomm import Publisher, Subscriber
25
+ from dexcomm.serialization import deserialize_protobuf, serialize_protobuf
23
26
  from google.protobuf.message import Message
24
27
  from jaxtyping import Float
25
28
  from loguru import logger
26
29
 
30
+ from dexcontrol.utils.comm_helper import get_zenoh_config_path
27
31
  from dexcontrol.utils.os_utils import resolve_key_name
28
- from dexcontrol.utils.subscribers import ProtobufZenohSubscriber
29
32
 
30
33
  # Type variable for Message subclasses
31
34
  M = TypeVar("M", bound=Message)
@@ -47,41 +50,55 @@ class RobotComponent:
47
50
  def __init__(
48
51
  self,
49
52
  state_sub_topic: str,
50
- zenoh_session: zenoh.Session,
51
53
  state_message_type: type[M],
52
54
  ) -> None:
53
55
  """Initializes RobotComponent.
54
56
 
55
57
  Args:
56
58
  state_sub_topic: Topic to subscribe to for state updates.
57
- zenoh_session: Active Zenoh session for communication.
58
59
  state_message_type: Protobuf message class for component state.
59
60
  """
60
61
  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)
62
+ self._latest_state: Optional[M] = None
63
+ self._is_active = False
64
+ self._init_subscriber(state_sub_topic, state_message_type)
63
65
 
64
66
  def _init_subscriber(
65
67
  self,
66
68
  state_sub_topic: str,
67
69
  state_message_type: type[M],
68
- zenoh_session: zenoh.Session,
69
70
  ) -> None:
70
- """Initialize the Zenoh subscriber for state updates.
71
+ """Initialize the subscriber for state updates using DexComm.
71
72
 
72
73
  Args:
73
74
  state_sub_topic: Topic to subscribe to for state updates.
74
75
  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,
76
+ """
77
+ # Resolve topic with robot namespace
78
+ full_topic = resolve_key_name(state_sub_topic)
79
+
80
+ # Create DexComm subscriber with protobuf deserialization
81
+ self._subscriber = Subscriber(
82
+ topic=full_topic,
83
+ callback=self._on_state_update,
84
+ deserializer=lambda data: deserialize_protobuf(data, state_message_type),
85
+ config=get_zenoh_config_path(),
86
+ )
87
+
88
+ logger.debug(
89
+ f"Created subscriber for {self.__class__.__name__} on {full_topic}"
83
90
  )
84
91
 
92
+ def _on_state_update(self, state: M) -> None:
93
+ """Handle incoming state updates.
94
+
95
+ Args:
96
+ state: Deserialized protobuf state message.
97
+ """
98
+ self._latest_state = state
99
+ if state:
100
+ self._is_active = True
101
+
85
102
  def _get_state(self) -> M:
86
103
  """Gets the current state of the component.
87
104
 
@@ -91,10 +108,9 @@ class RobotComponent:
91
108
  Raises:
92
109
  None: If no state data is available.
93
110
  """
94
- state = self._subscriber.get_latest_data()
95
- if state is None:
111
+ if self._latest_state is None:
96
112
  logger.error(f"No state data available for {self.__class__.__name__}")
97
- return state
113
+ return self._latest_state
98
114
 
99
115
  def wait_for_active(self, timeout: float = 5.0) -> bool:
100
116
  """Waits for the component to start receiving state updates.
@@ -105,7 +121,12 @@ class RobotComponent:
105
121
  Returns:
106
122
  True if component becomes active, False if timeout is reached.
107
123
  """
108
- return self._subscriber.wait_for_active(timeout)
124
+ start_time = time.time()
125
+ while time.time() - start_time < timeout:
126
+ if self._is_active:
127
+ return True
128
+ time.sleep(0.1)
129
+ return False
109
130
 
110
131
  def is_active(self) -> bool:
111
132
  """Check if component is receiving state updates.
@@ -113,7 +134,7 @@ class RobotComponent:
113
134
  Returns:
114
135
  True if component is active, False otherwise.
115
136
  """
116
- return self._subscriber.is_active()
137
+ return self._is_active
117
138
 
118
139
  def shutdown(self) -> None:
119
140
  """Cleans up Zenoh resources."""
@@ -149,7 +170,7 @@ class RobotJointComponent(RobotComponent):
149
170
  Extends RobotComponent to add APIs for interacting with joints.
150
171
 
151
172
  Attributes:
152
- _publisher: Zenoh publisher for control commands.
173
+ _publisher: Publisher for control commands (Zenoh or dexcomm).
153
174
  _joint_name: List of joint names for this component.
154
175
  _pose_pool: Dictionary of predefined poses for this component.
155
176
  """
@@ -178,7 +199,6 @@ class RobotJointComponent(RobotComponent):
178
199
  state_sub_topic: str,
179
200
  control_pub_topic: str,
180
201
  state_message_type: type[M],
181
- zenoh_session: zenoh.Session,
182
202
  joint_name: list[str] | None = None,
183
203
  joint_limit: list[list[float]] | None = None,
184
204
  joint_vel_limit: list[float] | None = None,
@@ -190,16 +210,24 @@ class RobotJointComponent(RobotComponent):
190
210
  state_sub_topic: Topic to subscribe to for state updates.
191
211
  control_pub_topic: Topic to publish control commands.
192
212
  state_message_type: Protobuf message class for component state.
193
- zenoh_session: Active Zenoh session for communication.
194
213
  joint_name: List of joint names for this component.
214
+ joint_limit: Joint position limits.
215
+ joint_vel_limit: Joint velocity limits.
195
216
  pose_pool: Dictionary of predefined poses for this component.
196
217
  """
197
- super().__init__(state_sub_topic, zenoh_session, state_message_type)
218
+ super().__init__(state_sub_topic, state_message_type)
198
219
 
199
- resolved_topic: Final[str] = resolve_key_name(control_pub_topic)
200
- self._publisher: Final[zenoh.Publisher] = zenoh_session.declare_publisher(
201
- resolved_topic
220
+ # Resolve topic with robot namespace
221
+ full_topic = resolve_key_name(control_pub_topic)
222
+
223
+ # Create DexComm publisher with protobuf serialization
224
+ self._publisher: Final[Publisher] = Publisher(
225
+ topic=full_topic,
226
+ serializer=serialize_protobuf,
227
+ config=get_zenoh_config_path(),
202
228
  )
229
+
230
+ logger.debug(f"Created publisher for {self.__class__.__name__} on {full_topic}")
203
231
  self._joint_name: list[str] | None = joint_name
204
232
  self._joint_limit: np.ndarray | None = (
205
233
  np.array(joint_limit) if joint_limit else None
@@ -218,22 +246,19 @@ class RobotJointComponent(RobotComponent):
218
246
  Args:
219
247
  control_msg: Protobuf control message to publish.
220
248
  """
221
- msg_bytes = control_msg.SerializeToString()
222
- self._publisher.put(msg_bytes)
249
+ # DexComm publisher with protobuf serializer handles this
250
+ self._publisher.publish(control_msg)
223
251
 
224
252
  def shutdown(self) -> None:
225
- """Cleans up all Zenoh resources."""
253
+ """Cleans up all communication resources."""
226
254
  super().shutdown()
227
255
  try:
228
256
  if hasattr(self, "_publisher") and self._publisher:
229
- self._publisher.undeclare()
257
+ self._publisher.shutdown()
230
258
  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
- )
259
+ logger.warning(
260
+ f"Error shutting down publisher for {self.__class__.__name__}: {e}"
261
+ )
237
262
 
238
263
  @property
239
264
  def joint_name(self) -> list[str]:
@@ -311,9 +336,9 @@ class RobotJointComponent(RobotComponent):
311
336
  ValueError: If joint positions are not available for this component.
312
337
  """
313
338
  state = self._get_state()
314
- if not hasattr(state, "joint_pos"):
339
+ if not hasattr(state, "pos"):
315
340
  raise ValueError("Joint positions are not available for this component.")
316
- joint_pos = np.array(state.joint_pos, dtype=np.float32)
341
+ joint_pos = np.array(state.pos, dtype=np.float32)
317
342
  return self._extract_joint_info(joint_pos, joint_id=joint_id)
318
343
 
319
344
  def get_joint_pos_dict(
@@ -349,9 +374,9 @@ class RobotJointComponent(RobotComponent):
349
374
  ValueError: If joint velocities are not available for this component.
350
375
  """
351
376
  state = self._get_state()
352
- if not hasattr(state, "joint_vel"):
377
+ if not hasattr(state, "vel"):
353
378
  raise ValueError("Joint velocities are not available for this component.")
354
- joint_vel = np.array(state.joint_vel, dtype=np.float32)
379
+ joint_vel = np.array(state.vel, dtype=np.float32)
355
380
  return self._extract_joint_info(joint_vel, joint_id=joint_id)
356
381
 
357
382
  def get_joint_vel_dict(
@@ -386,11 +411,31 @@ class RobotJointComponent(RobotComponent):
386
411
  ValueError: If joint currents are not available for this component.
387
412
  """
388
413
  state = self._get_state()
389
- if not hasattr(state, "joint_cur"):
414
+ if not hasattr(state, "cur"):
390
415
  raise ValueError("Joint currents are not available for this component.")
391
- joint_cur = np.array(state.joint_cur, dtype=np.float32)
416
+ joint_cur = np.array(state.cur, dtype=np.float32)
392
417
  return self._extract_joint_info(joint_cur, joint_id=joint_id)
393
418
 
419
+ def get_joint_torque(
420
+ self, joint_id: list[int] | int | None = None
421
+ ) -> Float[np.ndarray, " N"]:
422
+ """Gets the torque of all joints in the component.
423
+
424
+ Args:
425
+ joint_id: Optional ID(s) of specific joints to query.
426
+
427
+ Returns:
428
+ Array of joint torques in component-specific units (Nm).
429
+
430
+ Raises:
431
+ ValueError: If joint torques are not available for this component.
432
+ """
433
+ state = self._get_state()
434
+ if not hasattr(state, "torque"):
435
+ raise ValueError("Joint torques are not available for this component.")
436
+ joint_torque = np.array(state.torque, dtype=np.float32)
437
+ return self._extract_joint_info(joint_torque, joint_id=joint_id)
438
+
394
439
  def get_joint_current_dict(
395
440
  self, joint_id: list[int] | int | None = None
396
441
  ) -> dict[str, float]:
@@ -421,9 +466,9 @@ class RobotJointComponent(RobotComponent):
421
466
  ValueError: If joint error codes are not available for this component.
422
467
  """
423
468
  state = self._get_state()
424
- if not hasattr(state, "joint_err"):
469
+ if not hasattr(state, "error"):
425
470
  raise ValueError("Joint error codes are not available for this component.")
426
- joint_err = np.array(state.joint_err, dtype=np.uint32)
471
+ joint_err = np.array(state.error, dtype=np.uint32)
427
472
  return self._extract_joint_info(joint_err, joint_id=joint_id)
428
473
 
429
474
  def get_joint_err_dict(
@@ -457,22 +502,27 @@ class RobotJointComponent(RobotComponent):
457
502
  ValueError: If joint positions or velocities are not available.
458
503
  """
459
504
  state = self._get_state()
460
- if not hasattr(state, "joint_pos") or not hasattr(state, "joint_vel"):
505
+ if not hasattr(state, "pos") or not hasattr(state, "vel"):
461
506
  raise ValueError(
462
507
  "Joint positions or velocities are not available for this component."
463
508
  )
464
509
 
465
510
  # 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)
511
+ joint_pos = np.array(state.pos, dtype=np.float32)
512
+ joint_vel = np.array(state.vel, dtype=np.float32)
468
513
 
469
- if hasattr(state, "joint_cur"):
514
+ if hasattr(state, "cur"):
470
515
  # If currents are available, include them
471
- joint_cur = np.array(state.joint_cur, dtype=np.float32)
516
+ joint_cur = np.array(state.cur, dtype=np.float32)
472
517
  joint_state = np.stack([joint_pos, joint_vel, joint_cur], axis=1)
518
+ elif hasattr(state, "torque"):
519
+ # If torques are available, include them
520
+ joint_torque = np.array(state.torque, dtype=np.float32)
521
+ joint_state = np.stack([joint_pos, joint_vel, joint_torque], axis=1)
473
522
  else:
474
- # Otherwise just include positions and velocities
475
- joint_state = np.stack([joint_pos, joint_vel], axis=1)
523
+ raise ValueError(
524
+ f"Either current or torque should be available for this {self.__class__.__name__}."
525
+ )
476
526
 
477
527
  return self._extract_joint_info(joint_state, joint_id=joint_id)
478
528
 
@@ -543,8 +593,9 @@ class RobotJointComponent(RobotComponent):
543
593
  joint_cmd = self._convert_joint_cmd_to_array(joint_cmd)
544
594
  return self.get_joint_pos() + joint_cmd
545
595
 
596
+ @staticmethod
546
597
  def _extract_joint_info(
547
- self, joint_info: np.ndarray, joint_id: list[int] | int | None = None
598
+ joint_info: np.ndarray, joint_id: list[int] | int | None = None
548
599
  ) -> np.ndarray:
549
600
  """Extract the joint information of the component as a numpy array.
550
601