Skip to content

Commit 7ab32a5

Browse files
committed
Use NamedPoseArray for hand and ee pose topics
1 parent b114991 commit 7ab32a5

6 files changed

Lines changed: 208 additions & 195 deletions

File tree

examples/teleop_ros2/README.md

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,10 @@ Robot assets are never downloaded by `teleop_ros2_node.py` at runtime.
5858

5959
## Published Topics
6060

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`
63-
- `xr_teleop/ee_pose_left` (`geometry_msgs/PoseStamped`)
64-
- Left hand/controller EE pose; published when the left EE source is active
65-
- `xr_teleop/ee_pose_right` (`geometry_msgs/PoseStamped`)
66-
- Right hand/controller EE pose; published when the right EE source is active
61+
- `xr_teleop/hand` (`teleop_ros2_interfaces/msg/NamedPoseArray`)
62+
- Named OpenXR hand joint poses; always contains 25 `left_` entries followed by 25 `right_` entries, including `WRIST` and omitting `PALM`; missing or invalid joints use zero poses with `is_valid=false`
63+
- `xr_teleop/ee_pose` (`teleop_ros2_interfaces/msg/NamedPoseArray`)
64+
- Named hand/controller EE poses; always contains `left` and `right` entries, with zero poses and `is_valid=false` for invalid sides
6765
- `xr_teleop/root_twist` (`geometry_msgs/TwistStamped`)
6866
- `xr_teleop/root_pose` (`geometry_msgs/PoseStamped`)
6967
- `xr_teleop/head_pose` (`geometry_msgs/PoseStamped`)
@@ -121,24 +119,23 @@ docker run --rm --gpus all --net=host --ipc=host \
121119
teleop_ros2_ref --ros-args -p cloudxr_accept_eula:=true \
122120
-p world_frame:=odom -p rate_hz:=30.0 \
123121
-r xr_teleop/hand:=my_robot/hand \
124-
-r xr_teleop/ee_pose_left:=my_robot/ee_pose_left \
125-
-r xr_teleop/ee_pose_right:=my_robot/ee_pose_right
122+
-r xr_teleop/ee_pose:=my_robot/ee_pose
126123
```
127124

128125
Available parameters: `rate_hz`, `mode`, `hand_retargeter`, `config_asset_root`, `cloudxr_install_dir`, `cloudxr_env_config`, `cloudxr_accept_eula`, `cloudxr_setup_oob`, `cloudxr_usb_local`, `pedal_collection_id`, `world_frame`, `right_wrist_frame`, `left_wrist_frame`, `head_frame`, `left_finger_joint_names`, `right_finger_joint_names`. Use `ros2 param list /teleop_ros2_node` and `ros2 param describe /teleop_ros2_node <param>` (with the node running) for the full set.
129126

130127
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.
131128

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`.
129+
Available topics for remapping: `xr_teleop/hand`, `xr_teleop/ee_pose`, `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`.
133130

134131
### Mode
135132

136133
The `mode` parameter selects the teleoperation scenario and which topics are published:
137134

138135
| Mode | Topics published |
139136
|------|------------------|
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 |
137+
| `controller_teleop` (default) | `ee_pose` (from controller aim poses), `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 poses and head pose), and `hand` only for the explicit Sharpa retargeter path |
138+
| `hand_teleop` | `ee_pose` (from hand tracking wrists), `hand` (named left and right joint poses), `finger_joints` (finger joints in joint space), `root_twist`, `root_pose`, `head_pose`, `tf` (from hand tracking wrists and head pose); locomotion comes from the configured foot pedal collection |
142139
| `controller_raw` | `controller_data` only |
143140
| `full_body` | `full_body` and `controller_data` |
144141

@@ -193,9 +190,8 @@ coverage.
193190
```bash
194191
docker exec -it teleop_ros2_ref /bin/bash
195192

196-
ros2 topic echo /xr_teleop/hand teleop_ros2_interfaces/msg/HandJointPoses
197-
ros2 topic echo /xr_teleop/ee_pose_left geometry_msgs/msg/PoseStamped
198-
ros2 topic echo /xr_teleop/ee_pose_right geometry_msgs/msg/PoseStamped
193+
ros2 topic echo /xr_teleop/hand teleop_ros2_interfaces/msg/NamedPoseArray
194+
ros2 topic echo /xr_teleop/ee_pose teleop_ros2_interfaces/msg/NamedPoseArray
199195
ros2 topic echo /xr_teleop/root_twist geometry_msgs/msg/TwistStamped
200196
ros2 topic echo /xr_teleop/root_pose geometry_msgs/msg/PoseStamped
201197
ros2 topic echo /xr_teleop/head_pose geometry_msgs/msg/PoseStamped

examples/teleop_ros2/python/integration_tests/teleop_ros2_topic_verifier.py

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from rclpy.node import Node
2121
from sensor_msgs.msg import JointState
2222
from std_msgs.msg import ByteMultiArray
23-
from teleop_ros2_interfaces.msg import HandJointPoses
23+
from teleop_ros2_interfaces.msg import NamedPoseArray
2424
from tf2_msgs.msg import TFMessage
2525

2626

@@ -55,20 +55,17 @@ def _unpack_msgpack(msg: ByteMultiArray) -> dict:
5555
)
5656

5757

58-
def _assert_hand_joint_poses(msg: HandJointPoses) -> None:
58+
def _assert_named_pose_array(msg: NamedPoseArray, expected_names: list[str]) -> None:
5959
if msg.header.frame_id != "world":
6060
raise ValueError(f"unexpected frame_id {msg.header.frame_id!r}")
6161
names = list(msg.name)
62-
if names != _EXPECTED_HAND_POSE_NAMES:
63-
raise ValueError("hand joint pose names do not match the expected order")
62+
if names != expected_names:
63+
raise ValueError("named pose entries do not match the expected order")
6464
if len(set(names)) != len(names):
65-
raise ValueError("hand joint pose names are not unique")
65+
raise ValueError("named pose entries are not unique")
6666
if len(msg.pose) != len(names) or len(msg.is_valid) != len(names):
67-
raise ValueError("hand joint pose arrays differ in length")
68-
if not any(bool(is_valid) for is_valid in msg.is_valid):
69-
raise ValueError("all hand joint poses are invalid")
67+
raise ValueError("named pose arrays differ in length")
7068

71-
positions = []
7269
for pose in msg.pose:
7370
position = (pose.position.x, pose.position.y, pose.position.z)
7471
orientation = (
@@ -78,12 +75,32 @@ def _assert_hand_joint_poses(msg: HandJointPoses) -> None:
7875
pose.orientation.w,
7976
)
8077
if not _is_finite_sequence(position):
81-
raise ValueError("hand joint pose position contains non-finite values")
78+
raise ValueError("named pose position contains non-finite values")
8279
if not _is_finite_sequence(orientation):
83-
raise ValueError("hand joint pose orientation contains non-finite values")
84-
positions.extend(position)
85-
if not any(abs(value) > 1e-6 for value in positions):
86-
raise ValueError("hand joint pose positions are all zero")
80+
raise ValueError("named pose orientation contains non-finite values")
81+
82+
83+
def _assert_nonzero_pose_positions(msg: NamedPoseArray, label: str) -> None:
84+
if not any(
85+
abs(value) > 1e-6
86+
for pose in msg.pose
87+
for value in (pose.position.x, pose.position.y, pose.position.z)
88+
):
89+
raise ValueError(f"{label} positions are all zero")
90+
91+
92+
def _assert_ee_pose_array(msg: NamedPoseArray) -> None:
93+
_assert_named_pose_array(msg, ["left", "right"])
94+
if not all(bool(is_valid) for is_valid in msg.is_valid):
95+
raise ValueError("EE pose array contains an invalid entry")
96+
_assert_nonzero_pose_positions(msg, "EE pose")
97+
98+
99+
def _assert_hand_pose_array(msg: NamedPoseArray) -> None:
100+
_assert_named_pose_array(msg, _EXPECTED_HAND_POSE_NAMES)
101+
if not any(bool(is_valid) for is_valid in msg.is_valid):
102+
raise ValueError("all hand joint poses are invalid")
103+
_assert_nonzero_pose_positions(msg, "hand joint pose")
87104

88105

89106
def _assert_pose_stamped(
@@ -246,20 +263,10 @@ def _expected_subscriptions(
246263
if mode == "controller_teleop":
247264
return [
248265
(
249-
"ee_pose_left",
250-
"xr_teleop/ee_pose_left",
251-
PoseStamped,
252-
lambda msg: _assert_pose_stamped(
253-
msg, require_nonzero_position=True
254-
),
255-
),
256-
(
257-
"ee_pose_right",
258-
"xr_teleop/ee_pose_right",
259-
PoseStamped,
260-
lambda msg: _assert_pose_stamped(
261-
msg, require_nonzero_position=True
262-
),
266+
"ee_pose",
267+
"xr_teleop/ee_pose",
268+
NamedPoseArray,
269+
_assert_ee_pose_array,
263270
),
264271
("root_twist", "xr_teleop/root_twist", TwistStamped, _assert_twist),
265272
("root_pose", "xr_teleop/root_pose", PoseStamped, _assert_pose_stamped),
@@ -282,24 +289,14 @@ def _expected_subscriptions(
282289
(
283290
"hand",
284291
"xr_teleop/hand",
285-
HandJointPoses,
286-
_assert_hand_joint_poses,
287-
),
288-
(
289-
"ee_pose_left",
290-
"xr_teleop/ee_pose_left",
291-
PoseStamped,
292-
lambda msg: _assert_pose_stamped(
293-
msg, require_nonzero_position=True
294-
),
292+
NamedPoseArray,
293+
_assert_hand_pose_array,
295294
),
296295
(
297-
"ee_pose_right",
298-
"xr_teleop/ee_pose_right",
299-
PoseStamped,
300-
lambda msg: _assert_pose_stamped(
301-
msg, require_nonzero_position=True
302-
),
296+
"ee_pose",
297+
"xr_teleop/ee_pose",
298+
NamedPoseArray,
299+
_assert_ee_pose_array,
303300
),
304301
("root_twist", "xr_teleop/root_twist", TwistStamped, _assert_twist),
305302
("root_pose", "xr_teleop/root_pose", PoseStamped, _assert_pose_stamped),

examples/teleop_ros2/python/messages.py

Lines changed: 108 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from geometry_msgs.msg import Pose, PoseStamped
1212
from scipy.spatial.transform import Rotation
1313
from sensor_msgs.msg import JointState
14-
from teleop_ros2_interfaces.msg import HandJointPoses
14+
from teleop_ros2_interfaces.msg import NamedPoseArray
1515

1616
from isaacteleop.retargeting_engine.interface import OptionalTensorGroup
1717
from isaacteleop.retargeting_engine.tensor_types.indices import (
@@ -36,6 +36,70 @@
3636
)
3737

3838

39+
def _compose_ee_msg(
40+
left_pose: Pose | None,
41+
right_pose: Pose | None,
42+
now,
43+
frame_id: str,
44+
) -> NamedPoseArray:
45+
msg = NamedPoseArray()
46+
msg.header.stamp = now
47+
msg.header.frame_id = frame_id
48+
for side, pose in (("left", left_pose), ("right", right_pose)):
49+
# Set the pose and validity together so the parallel fields cannot diverge.
50+
if pose is None:
51+
pose = to_pose([0.0, 0.0, 0.0])
52+
pose_is_valid = False
53+
else:
54+
pose_is_valid = True
55+
msg.name.append(side)
56+
msg.pose.append(pose)
57+
msg.is_valid.append(pose_is_valid)
58+
return msg
59+
60+
61+
def _compute_ee_pose_from_controller(
62+
ctrl: OptionalTensorGroup,
63+
side: str,
64+
transform_rot: Rotation | None = None,
65+
transform_trans: Sequence[float] | None = None,
66+
controller_uses_hands_source: bool = False,
67+
) -> Pose | None:
68+
if not controller_aim_is_valid(ctrl):
69+
return None
70+
71+
pos = [float(x) for x in ctrl[ControllerInputIndex.AIM_POSITION]]
72+
ori = [float(x) for x in ctrl[ControllerInputIndex.AIM_ORIENTATION]]
73+
pose = to_pose(pos, ori)
74+
75+
if transform_rot is not None or transform_trans is not None:
76+
pose = apply_transform_to_pose(pose, transform_rot, transform_trans)
77+
78+
if controller_uses_hands_source:
79+
pose = apply_manus_controller_to_hand_pose(pose, side)
80+
81+
return pose
82+
83+
84+
def _compute_ee_pose_from_hand(
85+
hand: OptionalTensorGroup,
86+
transform_rot: Rotation | None = None,
87+
transform_trans: Sequence[float] | None = None,
88+
) -> Pose | None:
89+
if not hand_wrist_is_valid(hand):
90+
return None
91+
92+
positions = np.asarray(hand[HandInputIndex.JOINT_POSITIONS])
93+
orientations = np.asarray(hand[HandInputIndex.JOINT_ORIENTATIONS])
94+
pose = to_pose(
95+
positions[HandJointIndex.WRIST],
96+
orientations[HandJointIndex.WRIST],
97+
)
98+
if transform_rot is not None or transform_trans is not None:
99+
pose = apply_transform_to_pose(pose, transform_rot, transform_trans)
100+
return pose
101+
102+
39103
def _to_pose_stamped(pose: Pose, now, frame_id: str) -> PoseStamped:
40104
msg = PoseStamped()
41105
msg.header.stamp = now
@@ -119,50 +183,53 @@ def _as_float(ctrl, index):
119183
}
120184

121185

122-
def build_ee_msg_from_controller(
123-
ctrl: OptionalTensorGroup,
124-
side: str,
186+
def build_ee_msg_from_controllers(
187+
left_ctrl: OptionalTensorGroup,
188+
right_ctrl: OptionalTensorGroup,
125189
now,
126190
frame_id: str,
127191
transform_rot: Rotation | None = None,
128192
transform_trans: Sequence[float] | None = None,
129193
controller_uses_hands_source: bool = False,
130-
) -> PoseStamped | None:
131-
if not controller_aim_is_valid(ctrl):
132-
return None
133-
134-
pos = [float(x) for x in ctrl[ControllerInputIndex.AIM_POSITION]]
135-
ori = [float(x) for x in ctrl[ControllerInputIndex.AIM_ORIENTATION]]
136-
pose = to_pose(pos, ori)
137-
138-
if transform_rot is not None or transform_trans is not None:
139-
pose = apply_transform_to_pose(pose, transform_rot, transform_trans)
140-
141-
if controller_uses_hands_source:
142-
pose = apply_manus_controller_to_hand_pose(pose, side)
143-
144-
return _to_pose_stamped(pose, now, frame_id)
194+
) -> NamedPoseArray:
195+
"""Build fixed left/right EE entries from controller aim poses."""
196+
left_pose = _compute_ee_pose_from_controller(
197+
left_ctrl,
198+
"left",
199+
transform_rot,
200+
transform_trans,
201+
controller_uses_hands_source,
202+
)
203+
right_pose = _compute_ee_pose_from_controller(
204+
right_ctrl,
205+
"right",
206+
transform_rot,
207+
transform_trans,
208+
controller_uses_hands_source,
209+
)
210+
return _compose_ee_msg(left_pose, right_pose, now, frame_id)
145211

146212

147-
def build_ee_msg_from_hand(
148-
hand: OptionalTensorGroup,
213+
def build_ee_msg_from_hands(
214+
left_hand: OptionalTensorGroup,
215+
right_hand: OptionalTensorGroup,
149216
now,
150217
frame_id: str,
151218
transform_rot: Rotation | None = None,
152219
transform_trans: Sequence[float] | None = None,
153-
) -> PoseStamped | None:
154-
if not hand_wrist_is_valid(hand):
155-
return None
156-
157-
positions = np.asarray(hand[HandInputIndex.JOINT_POSITIONS])
158-
orientations = np.asarray(hand[HandInputIndex.JOINT_ORIENTATIONS])
159-
pose = to_pose(
160-
positions[HandJointIndex.WRIST],
161-
orientations[HandJointIndex.WRIST],
220+
) -> NamedPoseArray:
221+
"""Build fixed left/right EE entries from hand wrist poses."""
222+
left_pose = _compute_ee_pose_from_hand(
223+
left_hand,
224+
transform_rot,
225+
transform_trans,
162226
)
163-
if transform_rot is not None or transform_trans is not None:
164-
pose = apply_transform_to_pose(pose, transform_rot, transform_trans)
165-
return _to_pose_stamped(pose, now, frame_id)
227+
right_pose = _compute_ee_pose_from_hand(
228+
right_hand,
229+
transform_rot,
230+
transform_trans,
231+
)
232+
return _compose_ee_msg(left_pose, right_pose, now, frame_id)
166233

167234

168235
def build_finger_joints_msg(
@@ -214,24 +281,25 @@ def build_full_body_payload(full_body: OptionalTensorGroup) -> Dict:
214281
}
215282

216283

217-
def build_hand_msg_from_hands(
284+
def build_hand_msg(
218285
left_hand: OptionalTensorGroup,
219286
right_hand: OptionalTensorGroup,
220287
now,
221288
frame_id: str,
222289
transform_rot: Rotation | None = None,
223290
transform_trans: Sequence[float] | None = None,
224-
) -> HandJointPoses | None:
225-
"""Build named hand joint poses for available hands, left then right."""
226-
if left_hand.is_none and right_hand.is_none:
227-
return None
228-
229-
msg = HandJointPoses()
291+
) -> NamedPoseArray:
292+
"""Build fixed left/right hand joint entries with invalid placeholders."""
293+
msg = NamedPoseArray()
230294
msg.header.stamp = now
231295
msg.header.frame_id = frame_id
232296

233297
for side, hand in (("left", left_hand), ("right", right_hand)):
234298
if hand.is_none:
299+
for joint_name in HAND_POSE_NAMES:
300+
msg.name.append(f"{side}_{joint_name}")
301+
msg.pose.append(to_pose([0.0, 0.0, 0.0]))
302+
msg.is_valid.append(False)
235303
continue
236304
positions = np.asarray(hand[HandInputIndex.JOINT_POSITIONS])
237305
orientations = np.asarray(hand[HandInputIndex.JOINT_ORIENTATIONS])

0 commit comments

Comments
 (0)