A Python client library for interacting with I2RT products β designed for learning-based robotics, teleoperation, and real-world deployment.
- Plug-and-play Python interface for YAM arms and Flow Base
- Real-time robot control via CAN bus (DM series motors)
- MuJoCo gravity compensation, simulation, and URDF/MJCF models
- Gripper force control and auto-calibration
- Bimanual teleoperation and trajectory record & replay
- Policy-deployment ready β works with standard robot learning pipelines
The YAM setup spans two machines on the same LAN:
- π€ robot β the YAM arms on CAN; runs the portal robot server. uv-managed, so
robot/yamusesuv runand there is nothing to activate. - π» workstation β RealSense cameras + LeRobot; connects to the robot over portal / plain TCP. Lives in a conda env (
yam_ws).
Two launchers pick the right env for you: robot/yam on the robot,
workstation/yam-data on the workstation.
One config.yaml at the repo root holds everything shared β robot host, camera serials,
control gains, recorder defaults. Every tool finds it on its own: no --config, no env var, from
any directory.
# π€ robot β create the uv env (optional; robot/yam auto-resolves it) + fix CAN names
bash robot/setup_robot_env.sh # optional: pre-create .venv + install i2rt
bash robot/setup_can_ids.sh # plug the 4 CAN adapters one-by-one -> persistent names (once)# π» workstation β conda env yam_ws + uv installs (you can pip-install other policy repos into it)
bash workstation/setup_workstation_env.sh # conda create yam_ws + uv pip install + RealSense udev
conda activate yam_ws
workstation/yam-data cams # list connected RealSense serials
workstation/yam-data tune # live sliders to match camera brightnessThen edit config.yaml at the repo root β at minimum the robot address and camera serials:
robot: { host: 192.168.0.42, port: 11331 }
cameras: { agentview: "<D455>", wrist_left: "<D405>", wrist_right: "<D405>" }# π€ robot
robot/yam canup # bring up the 4 CAN interfaces (after each boot)
robot/yam teleop
# lift both gellos to engage; bring both home to stop & auto-return.# π€ robot β teleop server (serves state / action / engage-gate over portal)
robot/yam canup
robot/yam teleop# π» workstation β recorder GUI (host + serials come from config.yaml)
workstation/yam-data recordThe recorder opens on a Setup page:
- Confirm
repo_id/root/taskand the source (teleop / dagger / eval). The dataset lands in<root>/<name>, where name is the last segment ofrepo_idβ~/lerobot_data+hello/pick_and_placeβ~/lerobot_data/pick_and_place. The status line shows which cameras were detected and whether that dataset already exists. - To add to an existing dataset, tick Continue collecting (resume/append). Otherwise START creates it fresh β and if the folder exists it asks twice before overwriting.
- START connects the robot, opens cameras + dataset, and (with
auto_arm: true) arms collection immediately.
Past demonstration overlay (after START) plays a recorded episode's three camera views
under the live ones, so you can match the scene before starting a take. Available in both
yam-data record and yam-data deploy.
| Control | What it does |
|---|---|
| Overlay dataset | any dataset folder under root, including one you are not recording into |
| Live camera opacity | blend live against the reference; 100% hides the reference |
| Resume reference | a selected episode starts paused on frame 1 and advances only after this |
- The first saved demonstration is selected automatically.
- Preview only β saved frames and policy inputs are always the unmodified camera images.
- Overlay-source datasets other than the one being recorded are read-only.
- Recording into an existing dataset still needs Continue collecting ticked before START; starting fresh deletes that dataset's episodes after the two overwrite confirmations.
Then teleoperate β lift both gellos to start recording, bring both home to end the episode:
review_before_save decides what happens at the end of each episode: true holds it for
Keep / Delete, false (what config.yaml ships) auto-saves.
Leader handle buttons
| Button | Action |
|---|---|
| left upper | toggle fine-grained control (2.5:1 with the checked-in fine_grained_scale: 0.4) |
| left lower | success |
| right lower | fail |
| right upper | discard β force-home, no save |
Leaving fine-grained mode pauses recording and teleoperation while the follower holds and the leader realigns.
Close the window when done: that calls finalize(), which completes the dataset. Cameras run on
their own capture thread, so the live view and saving never stall on a slow frame.
Dry run (no hardware):
workstation/yam-data record --mockexercises the whole pipeline with synthetic teleop + fake frames β no robot, cameras, or lerobot needed.
robot/yam deploy # π€ robot
python -m yam_policy.serve # π§ policy host (:8000)
workstation/yam-data deploy --repo-id user/yam_pick --prompt "pick up the cube" # π» workstation UI
# Left upper starts/stops rollout, or toggles fine control during intervention.
# Other handle buttons toggle intervention and keep/discard + home.
# Past demonstration overlay is inherited from the recorder UI and remains preview-only.openpi and LeRobot checkpoints both deploy through the same client. openpi's is the wire protocol; a LeRobot checkpoint runs through an adapter on our side, with no conversion step:
# π§ a LeRobot checkpoint (local dir or a Hub repo id)
python -m yam_policy.serve \
--policy yam_policy.policies.lerobot_policy:LeRobotPolicy \
--config pretrained_path=outputs/train/my_act/checkpoints/last/pretrained_model \
--config device=cuda- The client configures itself from the handshake: camera names, image size, chunk length.
- The deploy UI names what answered (
LeRobot Β· act,ACRFT Β· pi05_yam_lego_taxi) β they share a wire but not an observation format. - Before training on this recorder's data, drop
observation.leader, or the policy is handed the answer as an input. Details:policy_serving/README.md.
Replay is deployment with the actions read from a dataset, so it is not a separate tool β it
is the dataset mode of yam-data deploy (no wrapper server, no policy server, no separate
GUI):
robot/yam canup && robot/yam deploy # π€ robot β the deploy server, as usual
workstation/yam-data deploy # π» workstation β set mode = dataset, pick an episode- Set
mode = dataset, Start, then pick the episode on the run page's past-demonstration panel β the runner builds an in-processDatasetPolicy(reads the actions from the parquet) so takeover, e-stop, the follower smoother and the link-loss watchdog all apply. - The rollout button is Start Replay β Pause / Resume: pause is a send-gate (the robot holds, nothing on the robot side toggles, so the gripper is never snapped shut on resume).
- The past-demonstration overlay plays along with the rollout (at the arm's frame rate), freezes on pause, and rewinds to the first frame on stop/home. Watch-only; nothing is recorded.
speed/looplive onDatasetPolicy; seepolicy_serving/README.md.
Full hardware bring-up checklist:
docs/hardware-checklist.md.
Use jellyho/hf-utils β a local web app that
replaced the PyQt editor that used to live here. It works on any LeRobot v3.0 dataset, not
only YAM recordings, so it is a separate tool rather than part of this repo. Nothing here
imports it and nothing needs installing.
git clone https://github.com/jellyho/hf-utils && cd hf-utils && ./run.sh
# open http://127.0.0.1:8000 -> LeRobot tab -> point it at a dataset folderPer episode: scrub / play every camera in sync, plot action vs observation.state,
set the task, delete episodes (with re-indexing), split, merge, render to MP4/GIF,
and annotate subtasks. It reads this recorder's outcomes.jsonl when present, so
success / fail / discard still show up per episode and survive a delete. Everything
destructive backs up first.
Two things stayed here because they encode YAM specifics a general tool should not have:
homing annotation (below) and yam-data check-videos, which can re-encode an mp4 that
the recorder's encoder truncated β hf-utils detects that but only repairs the metadata side.
Homing annotation β every episode ends with the arms returning home and the
gripper closing; that tail isn't useful for training. New recordings are tagged live
(observation.control_mode = homing). For datasets collected earlier, auto-annotate
it from the gripper:
workstation/yam-data mark-homing --dataset user/yam_pick --dry-run # preview
workstation/yam-data mark-homing --dataset user/yam_pick # apply (or --all)It's non-destructive (only relabels the control_mode column β no frames removed, no
re-encode). At train time, drop the tail by filtering control_mode == homing.
If a GPU/streaming video encode dropped an episode's trailing frame (a camera's video ends up 1 frame shorter than the recorded length), LeRobot would otherwise refuse to delete a shared-file episode. The editor detects this and auto-repairs the metadata before retrying, and the recorder now runs the same check at
finalize()so freshly collected datasets stay consistent (with a warning if it recurs β considerrecorder.streaming_encoding: falseorvcodec: h264).
Everything below documents using i2rt as a Python library β driving motors over CAN, grippers, kinematics, the Flow Base, and the serving stack.
Running the YAM teleop / recording rig? The Quick Start is all you need β its setup scripts already create the envs and install everything (
robot/setup_robot_env.sh,workstation/setup_workstation_env.sh).The manual install below is only for using i2rt standalone as a library.
git clone https://github.com/i2rt-robotics/i2rt.git && cd i2rt
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
uv venv --python 3.11
source .venv/bin/activatesudo apt update
sudo apt install build-essential python3-dev linux-headers-$(uname -r)
uv pip install -e .# Check detected CAN devices
ls -l /sys/class/net/can*
# Bring up interface at 1 Mbit/s
sudo ip link set can0 up type can bitrate 1000000
# Auto-enable on boot
sudo sh devices/install_devices.sh
# Reset unresponsive adapter
bash robot/reset_all_can.shpython i2rt/robots/motor_chain_robot.py --channel can0 --gripper linear_4310from i2rt.robots.get_robot import get_yam_robot
from i2rt.robots.utils import GripperType
import numpy as np
robot = get_yam_robot(channel="can0", gripper_type=GripperType.LINEAR_4310)
# Read joint positions (radians)
q = robot.get_joint_pos() # shape: (6,)
# Command a target configuration
robot.command_joint_pos(np.zeros(6))# Follower arm
python examples/minimum_gello/minimum_gello.py --gripper linear_4310 --mode follower --can-channel can0 --bilateral-kp 0.2
# Leader arm (teaching handle)
python examples/minimum_gello/minimum_gello.py --gripper yam_teaching_handle --mode leader --can-channel can1 --bilateral-kp 0.2- Top button (press once): enable synchronisation β follower tracks leader
- Top button (press again): disable synchronisation
--bilateral-kpcontrols resistance felt on the leader (0.1β0.2 recommended)
To inspect leader arm output:
python robot/run_yam_leader.py --channel $CAN_CHANNELpython examples/minimum_gello/minimum_gello.py --mode visualizer_local| Gripper | Motor | Notes |
|---|---|---|
crank_4310 |
DM4310 | Zero-linkage crank β minimises gripper width |
linear_3507 |
DM3507 | Lightweight linear; start closed or run calibration |
linear_4310 |
DM4310 | Standard linear; slightly more force than 3507 |
yam_teaching_handle |
β | Leader arm handle with trigger + 2 buttons. |
The linear grippers require calibration because their motor travels more than 2Ο radians over the full stroke β either start with the gripper fully closed, or run the calibration routine.
# Joystick demo
python i2rt/flow_base/flow_base_controller.pyfrom i2rt.flow_base.flow_base_client import FlowBaseClient
client = FlowBaseClient(host="172.6.2.20")
client.set_target_velocity([0.1, 0.0, 0.0], frame="local")| Example | Location |
|---|---|
| Bimanual lead-follower | examples/bimanual_lead_follower/ |
| Record & replay trajectory | examples/record_replay_trajectory/ |
| Single motor PD control | examples/single_motor_position_pd_control/ |
| MuJoCo control interface | examples/control_with_mujoco/ |
The YAM rig is exposed to a workstation over portal (plain TCP); policy
inference is a separate websocket link (openpi-compatible). Bimanual by default
(2 leaders + 2 followers).
source .venv/bin/activate # robot env (uv; robot/setup_robot_env.sh)
python -m i2rt.serving.run_robot_server teleop --sim # auto home/engage teleop
python -m i2rt.serving.run_robot_server deploy --sim # HG-DAgger: policy + button takeover
python -m i2rt.serving.run_robot_server wrapper --sim # followers track an external command
# β¦or the shortcut launcher (activates the env for you):
robot/yam teleop --sim # also: dagger / wrapper / can / canupTargets are rate-limited and gravity compensation is always on, so policyβhuman takeovers ramp smoothly.
| Subsystem | Path | What it is |
|---|---|---|
| Robot serving (portal) | i2rt/serving/ |
teleop / DAgger / wrapper servers + RobotClient; snapshot contract; safety (e-stop, joint/effort limits, link-loss watchdog), EEF FK + safe resolved-rate OSC |
| Policy serving (websocket) | policy_serving/ |
openpi-compatible WebsocketPolicyServer/Client + serve.py; deploys openpi and LeRobot checkpoints through the same client |
| Workstation tools | workstation/lerobot_recorder/ |
LeRobot recorder (teleop/dagger/eval), replay+overlay, policy bridge; modern themed GUI with status banner, health, live stats, audio cues, success/fail/discard labeling |
Quick CLIs (workstation): workstation/yam-data {record\|replay\|bridge\|cams\|tune\|doctor}.
One config for everything: edit the tracked config.yaml at the
repo root β robot host/port, control gains/limits, camera serials, recorder
defaults, tasks, and the policy endpoint. Every tool auto-discovers <repo>/config.yaml
(no env var, no matter the directory); no --config needed. Precedence:
CLI flag > config.yaml > default.
Envs: the robot is uv-managed β robot/yam β¦ uses uv run, nothing to
activate. The workstation is a conda env (workstation/setup_workstation_env.sh)
with this repo installed via uv, so you can pip install other policy repos into the
same env. The policy server env is unconstrained (its own conda/uv).
Safety & ops highlights: network E-STOP, per-joint position + optional effort
(collision) limits, command-staleness watchdog (link loss β hold), async dataset
writer (collect while saving), camera-disconnect auto-reconnect, disk-space guard,
and a per-episode outcomes.jsonl (yam-data doctor summarizes success rates).
Verify on hardware: follow docs/hardware-checklist.md
β an ordered, runnable confirmation list for every feature.
The factory default is a 400 ms timeout β motors enter damping mode if no command is received within 400 ms.
# Disable timeout (advanced users only β run twice)
python i2rt/motor_config_tool/set_timeout.py --channel can0
python i2rt/motor_config_tool/set_timeout.py --channel can0
# Re-enable timeout
python i2rt/motor_config_tool/set_timeout.py --channel can0 --timeout
β οΈ Without the timeout, a failed gravity-compensation loop can produce uncontrolled torque. If you disable it, always initialise with a PD target:robot = get_yam_robot(channel="can0", zero_gravity_mode=False)
python i2rt/motor_config_tool/set_zero.py --channel can0 --motor_id 1Run for each motor ID (1β6 for a standard YAM).
Pull requests welcome. Open an issue to request examples or report bugs.
MIT License β see LICENSE.
- Email: support@i2rt.com
- Sales: sales@i2rt.com
