Skip to content

Commit 35449a5

Browse files
authored
ROS: Split TeleopSessionConfigs by mode (#395)
1 parent f91a320 commit 35449a5

2 files changed

Lines changed: 177 additions & 32 deletions

File tree

examples/teleop_ros2/README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ NV_CXR_ENABLE_PUSH_DEVICES=0
2323
./scripts/run_cloudxr_via_docker.sh
2424
```
2525

26+
## Prerequisite: Foot Pedal For `hand_teleop`
27+
28+
`hand_teleop` uses `Generic3AxisPedalSource` + `FootPedalRootCmdRetargeter` for
29+
`xr_teleop/root_twist` and `xr_teleop/root_pose`. Start `foot_pedal_reader`,
30+
`pedal_pusher`, or a compatible pedal plugin so pedal data is available through
31+
the matching collection ID.
32+
33+
The default collection ID is `generic_3axis_pedal`. Override it with
34+
`--ros-args -p pedal_collection_id:=<your_collection_id>` when your pedal
35+
publisher uses a different ID.
36+
2637
## Published Topics
2738

2839
- `xr_teleop/hand` (`geometry_msgs/PoseArray`)
@@ -86,7 +97,7 @@ docker run --rm --net=host --ipc=host \
8697
-r xr_teleop/hand:=my_robot/hand -r xr_teleop/ee_poses:=my_robot/ee_poses
8798
```
8899

89-
Available parameters: `rate_hz`, `mode`, `world_frame`, `right_wrist_frame`, `left_wrist_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.
100+
Available parameters: `rate_hz`, `mode`, `pedal_collection_id`, `world_frame`, `right_wrist_frame`, `left_wrist_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.
90101

91102
By default, `left_finger_joint_names` and `right_finger_joint_names` use the prefixed TriHand names. If overridden, each parameter must provide 7 names in TriHand order (`thumb_rotation`, `thumb_proximal`, `thumb_distal`, `index_proximal`, `index_distal`, `middle_proximal`, `middle_distal`). Those names are threaded into the retargeter output and published on `xr_teleop/finger_joints`.
92103

@@ -98,10 +109,10 @@ The `mode` parameter selects the teleoperation scenario and which topics are pub
98109

99110
| Mode | Topics published |
100111
|------|------------------|
101-
| `controller_teleop` (default) | `ee_poses` (from controller aim pose), `root_twist`, `root_pose`, `finger_joints` (finger joints in joint space), `tf` (from controller aim pose) |
102-
| `hand_teleop` | `ee_poses` (from hand tracking wrist), `hand` (finger joints only in pose space), `root_twist`, `root_pose`, `tf` (from hand tracking wrist) |
112+
| `controller_teleop` (default) | `ee_poses` (from controller aim pose), `root_twist`, `root_pose`, `finger_joints` (finger joints in joint space), `controller_data`, `tf` (from controller aim pose) |
113+
| `hand_teleop` | `ee_poses` (from hand tracking wrist), `hand` (finger joints only in pose space), `root_twist`, `root_pose`, `tf` (from hand tracking wrist); locomotion comes from the configured foot pedal collection |
103114
| `controller_raw` | `controller_data` only |
104-
| `full_body` | `full_body` only |
115+
| `full_body` | `full_body` and `controller_data` |
105116

106117
Example: `--ros-args -p mode:=controller_raw`
107118

examples/teleop_ros2/python/teleop_ros2_node.py

Lines changed: 162 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
The `mode` parameter selects the teleoperation scenario and which topics are
1111
published:
1212
13-
- controller_teleop (default): ee_poses (from controller aim pose), root_twist, root_pose,
14-
finger_joints (retargeted TriHand angles), and TF transforms for
15-
left/right wrists
13+
- controller_teleop (default): ee_poses (from controller aim pose), root_twist,
14+
root_pose, finger_joints (retargeted TriHand angles),
15+
controller_data, and TF transforms for left/right wrists
1616
- hand_teleop: ee_poses (from hand tracking wrist), hand (finger joint poses),
17-
root_twist, root_pose, and TF transforms for left/right wrists
17+
root_twist/root_pose (from foot pedal locomotion), and TF
18+
transforms for left/right wrists
1819
- controller_raw: controller_data only
1920
- full_body: full_body and controller_data
2021
@@ -59,10 +60,16 @@
5960
from isaacteleop.retargeting_engine.deviceio_source_nodes import (
6061
ControllersSource,
6162
FullBodySource,
63+
Generic3AxisPedalSource,
6264
HandsSource,
6365
)
66+
from isaacteleop.retargeting_engine.deviceio_source_nodes.pedals_source import (
67+
DEFAULT_PEDAL_COLLECTION_ID,
68+
)
6469
from isaacteleop.retargeting_engine.interface import OptionalTensorGroup, OutputCombiner
6570
from isaacteleop.retargeters import (
71+
FootPedalRootCmdRetargeter,
72+
FootPedalRootCmdRetargeterConfig,
6673
LocomotionRootCmdRetargeter,
6774
LocomotionRootCmdRetargeterConfig,
6875
TriHandMotionControllerRetargeter,
@@ -161,6 +168,30 @@ def _apply_transform_to_pose(
161168
return result
162169

163170

171+
def _build_plugins(
172+
use_mock_operators: bool,
173+
*,
174+
include_synthetic_hands: bool,
175+
search_start: Path,
176+
) -> List[PluginConfig]:
177+
if not use_mock_operators or not include_synthetic_hands:
178+
return []
179+
180+
plugin_paths = []
181+
env_paths = os.environ.get("ISAAC_TELEOP_PLUGIN_PATH")
182+
if env_paths:
183+
plugin_paths.extend([Path(p) for p in env_paths.split(os.pathsep) if p])
184+
plugin_paths.extend(_find_plugins_dirs(search_start))
185+
186+
return [
187+
PluginConfig(
188+
plugin_name="controller_synthetic_hands",
189+
plugin_root_id="synthetic_hands",
190+
search_paths=plugin_paths,
191+
)
192+
]
193+
194+
164195
def _find_plugins_dirs(start: Path) -> List[Path]:
165196
candidates: List[Path] = []
166197
for parent in [start, *start.parents]:
@@ -446,6 +477,16 @@ def __init__(self) -> None:
446477
self.declare_parameter("mode", "controller_teleop")
447478
self.declare_parameter("rate_hz", 60.0)
448479
self.declare_parameter("use_mock_operators", value=False)
480+
self.declare_parameter(
481+
"pedal_collection_id",
482+
DEFAULT_PEDAL_COLLECTION_ID,
483+
ParameterDescriptor(
484+
description=(
485+
"Tensor collection ID used for hand_teleop foot pedal locomotion. "
486+
"Must match the pedal pusher or reader collection_id."
487+
)
488+
),
489+
)
449490

450491
self.declare_parameter(
451492
"transform_translation",
@@ -534,6 +575,12 @@ def __init__(self) -> None:
534575
self.get_logger().info(f"Mode: {mode}")
535576
self._mode: str = mode
536577

578+
self._pedal_collection_id: str = (
579+
self.get_parameter("pedal_collection_id").get_parameter_value().string_value
580+
)
581+
if not self._pedal_collection_id:
582+
raise ValueError("Parameter 'pedal_collection_id' must not be empty")
583+
537584
self._world_frame: str = (
538585
self.get_parameter("world_frame").get_parameter_value().string_value
539586
)
@@ -631,9 +678,37 @@ def __init__(self) -> None:
631678
JointState, "xr_teleop/finger_joints", 10
632679
)
633680

634-
hands = HandsSource(name="hands")
681+
self._config = self._build_session_config(
682+
self._mode,
683+
left_finger_joint_names,
684+
right_finger_joint_names,
685+
)
686+
687+
def _build_controller_raw_config(self) -> TeleopSessionConfig:
688+
controllers = ControllersSource(name="controllers")
689+
pipeline = OutputCombiner(
690+
{
691+
"controller_left": controllers.output(ControllersSource.LEFT),
692+
"controller_right": controllers.output(ControllersSource.RIGHT),
693+
}
694+
)
695+
696+
return TeleopSessionConfig(
697+
app_name="TeleopRos2Publisher",
698+
pipeline=pipeline,
699+
plugins=_build_plugins(
700+
self._use_mock_operators,
701+
include_synthetic_hands=False,
702+
search_start=Path(__file__).resolve(),
703+
),
704+
)
705+
706+
def _build_controller_teleop_config(
707+
self,
708+
left_finger_joint_names: Sequence[str],
709+
right_finger_joint_names: Sequence[str],
710+
) -> TeleopSessionConfig:
635711
controllers = ControllersSource(name="controllers")
636-
full_body = FullBodySource(name="full_body")
637712
locomotion = LocomotionRootCmdRetargeter(
638713
LocomotionRootCmdRetargeterConfig(), name="locomotion"
639714
)
@@ -665,37 +740,92 @@ def __init__(self) -> None:
665740

666741
pipeline = OutputCombiner(
667742
{
668-
"hand_left": hands.output(HandsSource.LEFT),
669-
"hand_right": hands.output(HandsSource.RIGHT),
670743
"controller_left": controllers.output(ControllersSource.LEFT),
671744
"controller_right": controllers.output(ControllersSource.RIGHT),
672745
"root_command": locomotion_connected.output("root_command"),
673-
"full_body": full_body.output(FullBodySource.FULL_BODY),
674746
"finger_joints_left": left_hand_connected.output("hand_joints"),
675747
"finger_joints_right": right_hand_connected.output("hand_joints"),
676748
}
677749
)
678750

679-
plugins: List[PluginConfig] = []
680-
if self._use_mock_operators:
681-
plugin_paths = []
682-
env_paths = os.environ.get("ISAAC_TELEOP_PLUGIN_PATH")
683-
if env_paths:
684-
plugin_paths.extend([Path(p) for p in env_paths.split(os.pathsep) if p])
685-
plugin_paths.extend(_find_plugins_dirs(Path(__file__).resolve()))
686-
plugins.append(
687-
PluginConfig(
688-
plugin_name="controller_synthetic_hands",
689-
plugin_root_id="synthetic_hands",
690-
search_paths=plugin_paths,
691-
)
692-
)
751+
return TeleopSessionConfig(
752+
app_name="TeleopRos2Publisher",
753+
pipeline=pipeline,
754+
plugins=_build_plugins(
755+
self._use_mock_operators,
756+
include_synthetic_hands=False,
757+
search_start=Path(__file__).resolve(),
758+
),
759+
)
760+
761+
def _build_full_body_config(self) -> TeleopSessionConfig:
762+
controllers = ControllersSource(name="controllers")
763+
full_body = FullBodySource(name="full_body")
764+
pipeline = OutputCombiner(
765+
{
766+
"controller_left": controllers.output(ControllersSource.LEFT),
767+
"controller_right": controllers.output(ControllersSource.RIGHT),
768+
"full_body": full_body.output(FullBodySource.FULL_BODY),
769+
}
770+
)
693771

694-
self._config = TeleopSessionConfig(
772+
return TeleopSessionConfig(
695773
app_name="TeleopRos2Publisher",
696774
pipeline=pipeline,
697-
plugins=plugins,
775+
plugins=_build_plugins(
776+
self._use_mock_operators,
777+
include_synthetic_hands=False,
778+
search_start=Path(__file__).resolve(),
779+
),
780+
)
781+
782+
def _build_hand_teleop_config(self) -> TeleopSessionConfig:
783+
hands = HandsSource(name="hands")
784+
pedals = Generic3AxisPedalSource(
785+
name="pedals", collection_id=self._pedal_collection_id
698786
)
787+
locomotion = FootPedalRootCmdRetargeter(
788+
FootPedalRootCmdRetargeterConfig(),
789+
name="foot_pedal",
790+
)
791+
locomotion_connected = locomotion.connect({"pedals": pedals.output("pedals")})
792+
793+
pipeline = OutputCombiner(
794+
{
795+
"hand_left": hands.output(HandsSource.LEFT),
796+
"hand_right": hands.output(HandsSource.RIGHT),
797+
"root_command": locomotion_connected.output("root_command"),
798+
}
799+
)
800+
801+
return TeleopSessionConfig(
802+
app_name="TeleopRos2Publisher",
803+
pipeline=pipeline,
804+
plugins=_build_plugins(
805+
self._use_mock_operators,
806+
include_synthetic_hands=True,
807+
search_start=Path(__file__).resolve(),
808+
),
809+
)
810+
811+
def _build_session_config(
812+
self,
813+
mode: str,
814+
left_finger_joint_names: Sequence[str],
815+
right_finger_joint_names: Sequence[str],
816+
) -> TeleopSessionConfig:
817+
if mode == "controller_teleop":
818+
return self._build_controller_teleop_config(
819+
left_finger_joint_names,
820+
right_finger_joint_names,
821+
)
822+
if mode == "hand_teleop":
823+
return self._build_hand_teleop_config()
824+
if mode == "controller_raw":
825+
return self._build_controller_raw_config()
826+
if mode == "full_body":
827+
return self._build_full_body_config()
828+
raise ValueError(f"Unsupported mode {mode!r}")
699829

700830
def _build_wrist_tfs(
701831
self,
@@ -749,8 +879,6 @@ def run(self) -> int:
749879
result = session.step()
750880

751881
now = self.get_clock().now().to_msg()
752-
left_ctrl = result["controller_left"]
753-
right_ctrl = result["controller_right"]
754882

755883
if self._mode == "hand_teleop":
756884
left_hand = result["hand_left"]
@@ -786,6 +914,8 @@ def run(self) -> int:
786914
if wrist_tfs:
787915
self._tf_broadcaster.sendTransform(wrist_tfs)
788916
elif self._mode == "controller_teleop":
917+
left_ctrl = result["controller_left"]
918+
right_ctrl = result["controller_right"]
789919
# Build EE poses from controllers
790920
ee_msg = _build_ee_msg_from_controllers(
791921
left_ctrl,
@@ -807,7 +937,7 @@ def run(self) -> int:
807937
self._tf_broadcaster.sendTransform(wrist_tfs)
808938

809939
if self._mode in ("hand_teleop", "controller_teleop"):
810-
root_command = result.get("root_command")
940+
root_command = result["root_command"]
811941
if not root_command.is_none:
812942
cmd = np.asarray(root_command[0])
813943
twist_msg = TwistStamped()
@@ -827,6 +957,8 @@ def run(self) -> int:
827957
self._pub_root_pose.publish(pose_msg)
828958

829959
if self._mode == "controller_teleop":
960+
left_ctrl = result["controller_left"]
961+
right_ctrl = result["controller_right"]
830962
left_joints = result["finger_joints_left"]
831963
right_joints = result["finger_joints_right"]
832964
if not left_joints.is_none or not right_joints.is_none:
@@ -862,6 +994,8 @@ def run(self) -> int:
862994
"controller_teleop",
863995
"full_body",
864996
):
997+
left_ctrl = result["controller_left"]
998+
right_ctrl = result["controller_right"]
865999
if not left_ctrl.is_none or not right_ctrl.is_none:
8661000
controller_payload = _build_controller_payload(
8671001
left_ctrl, right_ctrl

0 commit comments

Comments
 (0)