-
Notifications
You must be signed in to change notification settings - Fork 35
feat: add generic joint-space teleop device (SO-101 leader) #660
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rwiltz
wants to merge
5
commits into
main
Choose a base branch
from
rwiltz/so101-joint-space-device
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d838f56
feat: add generic joint-space teleop device (SO-101 leader)
rwiltz 5f9b6ab
feat: add FEETECH serial backend to SO-101 leader plugin
rwiltz cba3197
feat: SO-101 leader sync-read + calibrate mode
rwiltz 0a0172b
feat: range-of-motion sweep in SO-101 leader calibrate
rwiltz daa450f
feat: LeRobot calibration interop for SO-101 leader
rwiltz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| .. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| .. SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| Generic Joint-Space Device | ||
| ========================== | ||
|
|
||
| A reusable device path for any **joint-encoder source** -- leader arms, exoskeletons, haptic | ||
| gloves, or other articulated input devices. A device streams a name-keyed ``JointStateOutput`` | ||
| FlatBuffer over the OpenXR tensor transport; one schema, one tracker, one source, and one | ||
| retargeter serve them all, so adding a new joint-space device is just a new **plugin** plus a | ||
| small **config**. | ||
|
|
||
| The **SO-101 leader arm** (`TheRobotStudio SO-ARM100 <https://github.com/TheRobotStudio/SO-ARM100>`_, | ||
| 6 Feetech STS3215 bus servos) is the reference instance. | ||
|
|
||
| At a glance | ||
| ----------- | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
| :widths: 18 82 | ||
|
|
||
| * - Layer | ||
| - Component | ||
| * - Schema | ||
| - :code-file:`src/core/schema/fbs/joint_state.fbs` -- ``JointState`` (name + position + | ||
| optional velocity/effort) and ``JointStateOutput`` (a vector of joints + ``device_id``). | ||
| * - Plugin | ||
| - :code-dir:`src/plugins/so101_leader` -- pushes ``JointStateOutput`` via ``SchemaPusher``. | ||
| Reads the FEETECH STS3215 servos over serial (``FeetechBus``); synthetic fallback when no | ||
| device path is given. | ||
| * - Tracker | ||
| - ``JointStateTracker`` (facade) with live (``LiveJointStateTrackerImpl``) and MCAP-replay | ||
| (``ReplayJointStateTrackerImpl``) backends, registered in the live/replay factories. | ||
| * - Source | ||
| - ``JointStateSource`` (``IDeviceIOSource``) -- converts the FlatBuffer into a name-keyed | ||
| group of joint positions for the retargeting graph. | ||
| * - Retargeter | ||
| - ``JointStateRetargeter`` -- ``joint`` (mirror) or ``ee_pose`` (URDF FK) mode. See | ||
| :doc:`/references/retargeting/joint_space`. | ||
|
|
||
| Data schema | ||
| ----------- | ||
|
|
||
| Joints are modeled as **name -> value** records so consumers read them by name, independent of | ||
| wire order: | ||
|
|
||
| .. code-block:: idl | ||
| :class: code-100col | ||
|
|
||
| table JointState { | ||
| name: string (id: 0, key); // e.g. "shoulder_pan", "gripper" | ||
| position: float (id: 1); // [rad] revolute, [m] prismatic | ||
| velocity: float (id: 2); // optional (JointStateOutput.has_velocity) | ||
| effort: float (id: 3); // optional (JointStateOutput.has_effort) | ||
| valid: bool = true (id: 4); | ||
| } | ||
|
|
||
| table JointStateOutput { | ||
| joints: [JointState] (id: 0); | ||
| device_id: string (id: 1); | ||
| has_velocity: bool (id: 2); | ||
| has_effort: bool (id: 3); | ||
| ee_pose: Pose (id: 4); // RESERVED: device-side FK; not consumed yet | ||
| ee_pose_valid: bool (id: 5); | ||
| } | ||
|
|
||
| The gripper is just another named DOF (conventionally ``"gripper"``). ``velocity``, ``effort``, | ||
| and ``ee_pose`` are optional/reserved: the reference plugin and ``JointStateSource`` populate and | ||
| surface joint **positions** only. | ||
|
|
||
| The SO-101 leader plugin | ||
| ------------------------ | ||
|
|
||
| ``so101_leader`` reads the six SO-101 servos (``shoulder_pan, shoulder_lift, elbow_flex, | ||
| wrist_flex, wrist_roll, gripper``) and pushes them to a tensor collection. With a serial device | ||
| path it talks to the FEETECH STS3215 bus servos directly via ``FeetechBus`` -- the same SMS/STS | ||
| wire protocol the FEETECH SCServo SDK / LeRobot's ``FeetechMotorsBus`` use, with no SDK dependency: | ||
| it disables torque (so the leader can be back-driven) and reads ``Present_Position`` each frame, | ||
| converting ticks to radians with per-joint calibration. With no device path it falls back to a | ||
| **synthetic** trajectory so the pipeline runs hardware-free (CI and the headless example). | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| # Synthetic backend (no hardware), default collection id "so101_leader": | ||
| ./install/plugins/so101_leader/so101_leader_plugin | ||
|
|
||
| # Real SO-101 leader on a serial port (Linux), optional calibration file: | ||
| ./install/plugins/so101_leader/so101_leader_plugin /dev/ttyACM0 so101_leader so101_leader.calib | ||
|
|
||
| See the :code-file:`plugin README <src/plugins/so101_leader/README.md>` for hardware setup | ||
| (unique servo ids, gear removal, back-driving) and the calibration file format. | ||
|
|
||
| The consumer side creates a ``JointStateSource(name=..., collection_id="so101_leader", | ||
| joint_names=[...])`` on the same ``collection_id``; ``TeleopSession`` discovers and polls the | ||
| ``JointStateTracker`` each frame. | ||
|
|
||
| Record and replay | ||
| ----------------- | ||
|
|
||
| The live tracker records to MCAP, and ``ReplayJointStateTrackerImpl`` replays it back with no | ||
| OpenXR runtime, so a recorded session drives the retargeting graph headlessly: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from isaacteleop.deviceio import McapRecordingConfig, McapReplayConfig | ||
| from isaacteleop.teleop_session_manager import SessionMode, TeleopSession, TeleopSessionConfig | ||
|
|
||
| # Record (live): TeleopSessionConfig(..., mcap_config=McapRecordingConfig("leader.mcap")) | ||
| # Replay (headless): TeleopSessionConfig(..., mode=SessionMode.REPLAY, | ||
| # mcap_config=McapReplayConfig("leader.mcap")) | ||
|
|
||
| Add another joint-space device | ||
| ------------------------------ | ||
|
|
||
| Reuse everything above by writing only: | ||
|
|
||
| #. A **plugin** that reads your hardware and fills ``JointStateOutput`` (positions; optionally | ||
| velocity/effort), modeled on :code-dir:`src/plugins/so101_leader`. | ||
| #. A **config**: a ``collection_id``, the device joint names, and -- for ``ee_pose`` mode -- a URDF | ||
| and end-effector link. | ||
|
|
||
| The schema, ``JointStateTracker``, ``JointStateSource``, and ``JointStateRetargeter`` are unchanged. | ||
|
|
||
| .. seealso:: | ||
|
|
||
| :doc:`add_device` -- the general four-step device-plugin recipe (foot-pedal reference). | ||
|
|
||
| :doc:`/references/retargeting/joint_space` -- the ``JointStateRetargeter`` (joint / EE modes), | ||
| the end-to-end example, and validation. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| .. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| .. SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| Retargeter: Joint-Space Device | ||
| ============================== | ||
|
|
||
| ``JointStateRetargeter`` maps a name-keyed joint-state input (from | ||
| :doc:`/device/joint_space`'s ``JointStateSource``) onto an Isaac Lab action, in one of two modes. | ||
| It is the generic retargeter for leader arms, exoskeletons, and other joint-encoder devices; the | ||
| SO-101 leader arm is the reference instance. | ||
|
|
||
| At a glance | ||
| ----------- | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
| :widths: 16 30 54 | ||
|
|
||
| * - Mode | ||
| - Output | ||
| - Use | ||
| * - ``joint`` | ||
| - one float per target joint (``joint_targets``) | ||
| - Lossless leader -> follower mirror for same-kinematics teleoperation. Name remap + optional | ||
| per-joint affine. No extra dependencies. | ||
| * - ``ee_pose`` | ||
| - 7-D ``ee_pose`` ``[x,y,z,qx,qy,qz,qw]`` + ``gripper_command`` | ||
| - Task-space / cross-embodiment teleoperation via URDF forward kinematics. Requires | ||
| ``pinocchio`` (the ``[retargeters]`` extra). | ||
|
|
||
| ``joint`` mode | ||
| -------------- | ||
|
|
||
| Each target joint is filled from a device joint (by name) with an optional affine | ||
| ``offset + sign * scale * value``. Defaults are an identity mirror. ``JointStateRetargeterConfig``: | ||
|
|
||
| * ``device_joints`` -- ordered device DOF names (must match the source's ``joint_names`` order). | ||
| * ``target_joints`` -- robot joint names to emit (defaults to ``device_joints``). | ||
| * ``joint_map`` -- ``{device_name: target_name}`` overrides; ``scale`` / ``offset`` / ``sign`` -- | ||
| per-target affine. | ||
|
|
||
| ``ee_pose`` mode | ||
| ---------------- | ||
|
|
||
| Forward-kinematics the device joints through a URDF and emit the end-effector pose plus a gripper | ||
| command. Config: ``urdf_path``, ``ee_link``, ``gripper_joint`` (and optional ``gripper_open`` / | ||
| ``gripper_close`` to emit normalized closedness in ``[0, 1]`` instead of the raw value). | ||
|
|
||
| * FK uses ``pinocchio`` (imported lazily; ``joint`` mode never needs it). Install via | ||
| ``pip install 'isaacteleop[retargeters]'``. | ||
| * Assumes a fixed-base model of single-DOF joints (the common leader-arm / exoskeleton case). | ||
| * The schema's device ``ee_pose`` field is **not** consumed yet -- FK is always computed from the | ||
| joint positions. | ||
| * ``clutch=True`` rebases the EE around an origin captured on the first ``RUNNING`` frame so | ||
| engaging teleop does not jump the robot; when the optional ``robot_ee_pos`` input (the live | ||
| ``world_T_ee``) is connected, the latched home is the robot's current end-effector. | ||
|
|
||
| .. note:: | ||
|
|
||
| The ``joints`` input is read positionally in ``device_joints`` order, so the upstream source's | ||
| ``joint_names`` must list the same names in the same order. A name mismatch is rejected by the | ||
| graph's type check at ``connect`` time. | ||
|
|
||
| Use it from Python | ||
| ------------------ | ||
|
|
||
| A pipeline builder returns an ``OutputCombiner`` with a single ``"action"`` key (the layout your | ||
| environment's action space expects): | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from isaacteleop.retargeting_engine.deviceio_source_nodes import JointStateSource | ||
| from isaacteleop.retargeting_engine.interface import OutputCombiner | ||
| from isaacteleop.retargeters import ( | ||
| JointStateRetargeter, | ||
| JointStateRetargeterConfig, | ||
| TensorReorderer, | ||
| ) | ||
|
|
||
| SO101_JOINTS = ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper"] | ||
|
|
||
| def build_so101_joint_pipeline(): | ||
| source = JointStateSource(name="leader", collection_id="so101_leader", joint_names=SO101_JOINTS) | ||
| retargeter = JointStateRetargeter( | ||
| name="leader", | ||
| mode="joint", | ||
| config=JointStateRetargeterConfig(device_joints=SO101_JOINTS, target_joints=SO101_JOINTS), | ||
| ) | ||
| head = retargeter.connect({JointStateRetargeter.JOINTS: source.output(JointStateSource.JOINTS)}) | ||
| reorderer = TensorReorderer( | ||
| input_config={"joint_targets": SO101_JOINTS}, | ||
| output_order=SO101_JOINTS, | ||
| name="action_reorderer", | ||
| input_types={"joint_targets": "scalar"}, | ||
| ) | ||
| connected = reorderer.connect({"joint_targets": head.output("joint_targets")}) | ||
| return OutputCombiner({"action": connected.output("output")}) | ||
|
|
||
| For ``ee_pose`` mode, build the retargeter with ``mode="ee_pose"`` + a ``urdf_path`` / ``ee_link`` | ||
| and flatten ``ee_pose`` + ``gripper_command`` into the env's task-space action layout. | ||
|
|
||
| Run the example | ||
| --------------- | ||
|
|
||
| The repo ships ``examples/teleop/python/joint_space_device_example.py``: | ||
|
|
||
| .. code-block:: console | ||
|
|
||
| # Consumes the so101_leader plugin over OpenXR (source cloudxr.env first): | ||
| $ python joint_space_device_example.py --launch-plugin --mode joint --frames 8 | ||
| $ python joint_space_device_example.py --launch-plugin --mode ee --urdf so101_new_calib.urdf | ||
|
|
||
| Validate | ||
| -------- | ||
|
|
||
| Sim-free unit tests cover both modes (joint affine/remap/hold/reset, EE forward kinematics, clutch | ||
| rebasing, and the flattened action width/order): | ||
|
|
||
| .. code-block:: console | ||
|
|
||
| $ ctest --test-dir build -R 'retargeting_test_joint_state' --output-on-failure | ||
|
|
||
| .. seealso:: | ||
|
|
||
| :doc:`/device/joint_space` -- the schema, ``JointStateTracker``, ``JointStateSource``, the | ||
| SO-101 plugin, and MCAP record/replay. | ||
|
|
||
| :doc:`index` -- the broader retargeting interface and pipeline-builder pattern. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a easier to follow term for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't we call it leader follower arms?