Skip to content

Commit b114991

Browse files
committed
Consolidate left and right hand topics into a single topic
1 parent abd3a40 commit b114991

4 files changed

Lines changed: 46 additions & 72 deletions

File tree

examples/teleop_ros2/README.md

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ parameter:
4444
In `controller_teleop`, explicitly setting `hand_retargeter:=dexpilot` or
4545
`hand_retargeter:=pink_ik` keeps XR controllers responsible for EE poses, wrist
4646
TFs, locomotion, and `controller_data`, while Manus/OpenXR hand data drives
47-
`xr_teleop/hand_left`, `xr_teleop/hand_right`, and Sharpa
48-
`xr_teleop/finger_joints`.
47+
`xr_teleop/hand` and Sharpa `xr_teleop/finger_joints`.
4948

5049
The Docker build fetches the pinned official Sharpa Wave URDFs and installs them
5150
at `/opt/isaacteleop/install/examples/teleop_ros2/assets/urdf/sharpa_standalone/`.
@@ -59,10 +58,8 @@ Robot assets are never downloaded by `teleop_ros2_node.py` at runtime.
5958

6059
## Published Topics
6160

62-
- `xr_teleop/hand_left` (`teleop_ros2_interfaces/msg/HandJointPoses`)
63-
- Named left OpenXR hand joint poses; `name`, `pose`, and `is_valid` each have 25 index-aligned entries; includes `WRIST`, omits `PALM`
64-
- `xr_teleop/hand_right` (`teleop_ros2_interfaces/msg/HandJointPoses`)
65-
- Named right OpenXR hand joint poses; `name`, `pose`, and `is_valid` each have 25 index-aligned entries; includes `WRIST`, omits `PALM`
61+
- `xr_teleop/hand` (`teleop_ros2_interfaces/msg/HandJointPoses`)
62+
- Named OpenXR hand joint poses; `name`, `pose`, and `is_valid` are index-aligned, names use `left_`/`right_` prefixes, and each available hand contributes 25 entries including `WRIST` and omitting `PALM`
6663
- `xr_teleop/ee_pose_left` (`geometry_msgs/PoseStamped`)
6764
- Left hand/controller EE pose; published when the left EE source is active
6865
- `xr_teleop/ee_pose_right` (`geometry_msgs/PoseStamped`)
@@ -123,8 +120,7 @@ docker run --rm --gpus all --net=host --ipc=host \
123120
--name teleop_ros2_ref \
124121
teleop_ros2_ref --ros-args -p cloudxr_accept_eula:=true \
125122
-p world_frame:=odom -p rate_hz:=30.0 \
126-
-r xr_teleop/hand_left:=my_robot/hand_left \
127-
-r xr_teleop/hand_right:=my_robot/hand_right \
123+
-r xr_teleop/hand:=my_robot/hand \
128124
-r xr_teleop/ee_pose_left:=my_robot/ee_pose_left \
129125
-r xr_teleop/ee_pose_right:=my_robot/ee_pose_right
130126
```
@@ -133,16 +129,16 @@ Available parameters: `rate_hz`, `mode`, `hand_retargeter`, `config_asset_root`,
133129

134130
By default, `left_finger_joint_names` and `right_finger_joint_names` use the selected mode's retargeter joint names. They can be overridden to publish robot-specific names on `xr_teleop/finger_joints`, but each override must provide the same number of names as the joints emitted by that mode's retargeter.
135131

136-
Available topics for remapping: `xr_teleop/hand_left`, `xr_teleop/hand_right`, `xr_teleop/ee_pose_left`, `xr_teleop/ee_pose_right`, `xr_teleop/root_twist`, `xr_teleop/root_pose`, `xr_teleop/head_pose`, `xr_teleop/controller_data`, `xr_teleop/full_body`, `xr_teleop/finger_joints`. Active remaps can be inspected with `ros2 node info /teleop_ros2_node`.
132+
Available topics for remapping: `xr_teleop/hand`, `xr_teleop/ee_pose_left`, `xr_teleop/ee_pose_right`, `xr_teleop/root_twist`, `xr_teleop/root_pose`, `xr_teleop/head_pose`, `xr_teleop/controller_data`, `xr_teleop/full_body`, `xr_teleop/finger_joints`. Active remaps can be inspected with `ros2 node info /teleop_ros2_node`.
137133

138134
### Mode
139135

140136
The `mode` parameter selects the teleoperation scenario and which topics are published:
141137

142138
| Mode | Topics published |
143139
|------|------------------|
144-
| `controller_teleop` (default) | `ee_pose_left`, `ee_pose_right` (from controller aim pose), `root_twist`, `root_pose`, `head_pose`, `finger_joints` (TriHand by default; Sharpa from Manus/OpenXR hands when `hand_retargeter:=dexpilot` or `hand_retargeter:=pink_ik`), `controller_data`, `tf` (from controller aim pose and head pose), and `hand_left`/`hand_right` only for the explicit Sharpa retargeter path |
145-
| `hand_teleop` | `ee_pose_left`, `ee_pose_right` (from hand tracking wrist), `hand_left`/`hand_right` (named per-hand joint poses), `finger_joints` (finger joints in joint space), `root_twist`, `root_pose`, `head_pose`, `tf` (from hand tracking wrist and head pose); locomotion comes from the configured foot pedal collection |
140+
| `controller_teleop` (default) | `ee_pose_left`, `ee_pose_right` (from controller aim pose), `root_twist`, `root_pose`, `head_pose`, `finger_joints` (TriHand by default; Sharpa from Manus/OpenXR hands when `hand_retargeter:=dexpilot` or `hand_retargeter:=pink_ik`), `controller_data`, `tf` (from controller aim pose and head pose), and `hand` only for the explicit Sharpa retargeter path |
141+
| `hand_teleop` | `ee_pose_left`, `ee_pose_right` (from hand tracking wrist), `hand` (named left and right joint poses), `finger_joints` (finger joints in joint space), `root_twist`, `root_pose`, `head_pose`, `tf` (from hand tracking wrist and head pose); locomotion comes from the configured foot pedal collection |
146142
| `controller_raw` | `controller_data` only |
147143
| `full_body` | `full_body` and `controller_data` |
148144

@@ -197,8 +193,7 @@ coverage.
197193
```bash
198194
docker exec -it teleop_ros2_ref /bin/bash
199195

200-
ros2 topic echo /xr_teleop/hand_left teleop_ros2_interfaces/msg/HandJointPoses
201-
ros2 topic echo /xr_teleop/hand_right teleop_ros2_interfaces/msg/HandJointPoses
196+
ros2 topic echo /xr_teleop/hand teleop_ros2_interfaces/msg/HandJointPoses
202197
ros2 topic echo /xr_teleop/ee_pose_left geometry_msgs/msg/PoseStamped
203198
ros2 topic echo /xr_teleop/ee_pose_right geometry_msgs/msg/PoseStamped
204199
ros2 topic echo /xr_teleop/root_twist geometry_msgs/msg/TwistStamped

examples/teleop_ros2/python/integration_tests/teleop_ros2_topic_verifier.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626

2727
_MODES = ("controller_teleop", "hand_teleop", "controller_raw", "full_body")
2828
_EXPECTED_TF_FRAMES = {"right_wrist", "left_wrist", "head"}
29-
_EXPECTED_HAND_POSE_NAMES = [
29+
_HAND_POSE_NAMES = [
3030
HandJointIndex(i).name
3131
for i in range(HandJointIndex.WRIST, HandJointIndex.LITTLE_TIP + 1)
3232
]
33+
_EXPECTED_HAND_POSE_NAMES = [
34+
f"{side}_{name}" for side in ("left", "right") for name in _HAND_POSE_NAMES
35+
]
3336

3437

3538
def _is_finite_sequence(values) -> bool:
@@ -277,14 +280,8 @@ def _expected_subscriptions(
277280
if mode == "hand_teleop":
278281
return [
279282
(
280-
"hand_left",
281-
"xr_teleop/hand_left",
282-
HandJointPoses,
283-
_assert_hand_joint_poses,
284-
),
285-
(
286-
"hand_right",
287-
"xr_teleop/hand_right",
283+
"hand",
284+
"xr_teleop/hand",
288285
HandJointPoses,
289286
_assert_hand_joint_poses,
290287
),

examples/teleop_ros2/python/messages.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -214,35 +214,39 @@ def build_full_body_payload(full_body: OptionalTensorGroup) -> Dict:
214214
}
215215

216216

217-
def build_hand_msg_from_hand(
218-
hand: OptionalTensorGroup,
217+
def build_hand_msg_from_hands(
218+
left_hand: OptionalTensorGroup,
219+
right_hand: OptionalTensorGroup,
219220
now,
220221
frame_id: str,
221222
transform_rot: Rotation | None = None,
222223
transform_trans: Sequence[float] | None = None,
223224
) -> HandJointPoses | None:
224-
"""Build named hand joint poses for one hand."""
225-
if hand.is_none:
225+
"""Build named hand joint poses for available hands, left then right."""
226+
if left_hand.is_none and right_hand.is_none:
226227
return None
227228

228229
msg = HandJointPoses()
229230
msg.header.stamp = now
230231
msg.header.frame_id = frame_id
231232

232-
positions = np.asarray(hand[HandInputIndex.JOINT_POSITIONS])
233-
orientations = np.asarray(hand[HandInputIndex.JOINT_ORIENTATIONS])
234-
joint_valid = np.asarray(hand[HandInputIndex.JOINT_VALID])
235-
for joint_idx, joint_name in zip(HAND_POSE_JOINT_INDICES, HAND_POSE_NAMES):
236-
joint_is_valid = bool(joint_valid[joint_idx])
237-
if joint_is_valid:
238-
pose = to_pose(positions[joint_idx], orientations[joint_idx])
239-
if transform_rot is not None or transform_trans is not None:
240-
pose = apply_transform_to_pose(pose, transform_rot, transform_trans)
241-
else:
242-
pose = to_pose([0.0, 0.0, 0.0])
243-
msg.name.append(joint_name)
244-
msg.pose.append(pose)
245-
msg.is_valid.append(joint_is_valid)
233+
for side, hand in (("left", left_hand), ("right", right_hand)):
234+
if hand.is_none:
235+
continue
236+
positions = np.asarray(hand[HandInputIndex.JOINT_POSITIONS])
237+
orientations = np.asarray(hand[HandInputIndex.JOINT_ORIENTATIONS])
238+
joint_valid = np.asarray(hand[HandInputIndex.JOINT_VALID])
239+
for joint_idx, joint_name in zip(HAND_POSE_JOINT_INDICES, HAND_POSE_NAMES):
240+
joint_is_valid = bool(joint_valid[joint_idx])
241+
if joint_is_valid:
242+
pose = to_pose(positions[joint_idx], orientations[joint_idx])
243+
if transform_rot is not None or transform_trans is not None:
244+
pose = apply_transform_to_pose(pose, transform_rot, transform_trans)
245+
else:
246+
pose = to_pose([0.0, 0.0, 0.0])
247+
msg.name.append(f"{side}_{joint_name}")
248+
msg.pose.append(pose)
249+
msg.is_valid.append(joint_is_valid)
246250
return msg
247251

248252

examples/teleop_ros2/python/teleop_ros2_node.py

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515
(retargeted TriHand angles), controller_data, head_pose,
1616
and TF transforms for left/right wrists and head
1717
- hand_teleop: ee_pose_left/ee_pose_right (from hand tracking wrist),
18-
hand_left/hand_right (named per-hand joint poses),
18+
hand (named left and right joint poses),
1919
finger_joints (retargeted Sharpa joint angles),
2020
root_twist/root_pose (from foot pedal locomotion), head_pose,
2121
and TF transforms for left/right wrists and head
2222
- controller_raw: controller_data only
2323
- full_body: full_body and controller_data
2424
2525
Topic names (remappable via ROS 2 remapping):
26-
- xr_teleop/hand_left (HandJointPoses): named left hand joint poses
27-
- xr_teleop/hand_right (HandJointPoses): named right hand joint poses
26+
- xr_teleop/hand (HandJointPoses): named left and right hand joint poses
2827
- xr_teleop/ee_pose_left (PoseStamped): left hand/controller EE pose
2928
- xr_teleop/ee_pose_right (PoseStamped): right hand/controller EE pose
3029
- xr_teleop/root_twist (TwistStamped): root velocity command
@@ -68,7 +67,7 @@
6867
build_ee_msg_from_hand,
6968
build_finger_joints_msg,
7069
build_full_body_payload,
71-
build_hand_msg_from_hand,
70+
build_hand_msg_from_hands,
7271
build_head_msg,
7372
)
7473
from node_parameters import (
@@ -134,12 +133,7 @@ def _get_orientation(pose: Pose) -> List[float]:
134133
return tfs
135134

136135
def _create_publishers(self) -> None:
137-
self._pub_hand_left = self.create_publisher(
138-
HandJointPoses, "xr_teleop/hand_left", 10
139-
)
140-
self._pub_hand_right = self.create_publisher(
141-
HandJointPoses, "xr_teleop/hand_right", 10
142-
)
136+
self._pub_hand = self.create_publisher(HandJointPoses, "xr_teleop/hand", 10)
143137
self._pub_ee_pose_left = self.create_publisher(
144138
PoseStamped, "xr_teleop/ee_pose_left", 10
145139
)
@@ -196,24 +190,16 @@ def _publish_controller_outputs(self, result: dict, now) -> None:
196190
if wrist_tfs:
197191
self._tf_broadcaster.sendTransform(wrist_tfs)
198192
if self._params.controller_uses_hands_source:
199-
left_hand_msg = build_hand_msg_from_hand(
193+
hand_msg = build_hand_msg_from_hands(
200194
result["hand_left"],
201-
now,
202-
self._params.world_frame,
203-
self._params.transform_rotation,
204-
self._params.transform_translation,
205-
)
206-
right_hand_msg = build_hand_msg_from_hand(
207195
result["hand_right"],
208196
now,
209197
self._params.world_frame,
210198
self._params.transform_rotation,
211199
self._params.transform_translation,
212200
)
213-
if left_hand_msg is not None:
214-
self._pub_hand_left.publish(left_hand_msg)
215-
if right_hand_msg is not None:
216-
self._pub_hand_right.publish(right_hand_msg)
201+
if hand_msg is not None:
202+
self._pub_hand.publish(hand_msg)
217203

218204
def _publish_controller_payload(self, result: dict) -> None:
219205
if self._params.mode not in (
@@ -264,24 +250,16 @@ def _publish_full_body_payload(self, result: dict) -> None:
264250
def _publish_hand_tracking_outputs(self, result: dict, now) -> None:
265251
left_hand = result["hand_left"]
266252
right_hand = result["hand_right"]
267-
left_hand_msg = build_hand_msg_from_hand(
253+
hand_msg = build_hand_msg_from_hands(
268254
left_hand,
269-
now,
270-
self._params.world_frame,
271-
self._params.transform_rotation,
272-
self._params.transform_translation,
273-
)
274-
right_hand_msg = build_hand_msg_from_hand(
275255
right_hand,
276256
now,
277257
self._params.world_frame,
278258
self._params.transform_rotation,
279259
self._params.transform_translation,
280260
)
281-
if left_hand_msg is not None:
282-
self._pub_hand_left.publish(left_hand_msg)
283-
if right_hand_msg is not None:
284-
self._pub_hand_right.publish(right_hand_msg)
261+
if hand_msg is not None:
262+
self._pub_hand.publish(hand_msg)
285263

286264
left_ee_msg = build_ee_msg_from_hand(
287265
left_hand,

0 commit comments

Comments
 (0)