Skip to content

YAM Arm Series

✓ Python SDK✓ MuJoCo Sim✓ Gravity Compensation✓ Teleoperation

YAM (Yet Another Manipulator) is I2RT's flagship robotic arm — a 6-DOF, CAN bus–driven manipulator designed for real-world research and embodied AI data collection. The YAM family spans four tiers to match different reach, payload, and budget requirements.

Model Overview

ModelPriceNotesDetails
YAM Standard$2,999Standard research armSpec & API →
YAM Pro$3,499Enhanced actuators, same SDKSpec & API →
YAM Ultra$4,299Top spec; tighter joint 3 limitSpec & API →
BIG YAM$4,999Heavier payload; DM6248 shoulders; different gainsSpec & API →
YAM Leader$2,999Teaching handle for teleoperationSpec & API →

Choosing a variant

SDK-wise, YAM Standard / Pro / Ultra are interchangeable — only the arm_type argument differs and (for Ultra) the joint 3 limit. BIG YAM is the only variant with substantially different control gains, since it uses heavier DM6248 motors. See each variant page for the one-line API difference and per-variant config.

Specifications

ParameterValue
Degrees of Freedom6
CommunicationCAN bus (1 Mbit/s)
Motor SeriesDM series brushless
Control ModesJoint position PD · Gravity compensation · Zero-gravity
SimulationMuJoCo (MJCF + URDF provided)
Safety400 ms motor timeout (configurable)
MountingTable-top (standard)

Arm Hardware Variants

Arm TypeShoulder MotorsElbow/Wrist MotorsGravity FactorNotes
YAM3× DM43403× DM43101.3Standard arm
YAM_PRO3× DM43403× DM43101.3Same as YAM
YAM_ULTRA3× DM43403× DM43101.3Different joint 3 limit
BIG_YAM2× DM62482× DM4340 + 2× DM43101.0Heavier, reversed joint 2

3D Model

The YAM URDF and MuJoCo XML are included in the repository:

i2rt/robot_models/arm/yam/
├── yam.urdf
├── yam.xml
└── assets/          # STL meshes (visual + collision)

Videos

🎬
Video — Coming Soon
YAM arm performing a pick-and-place task on a cluttered tabletop. Close-up of gripper engagement. 30–60 seconds.

Hardware Setup

Get a single YAM arm out of the box, wired, powered, and ready to run a demo.

Prerequisite

Finish SW Setup first — Python SDK + CAN environment must be ready.

1. Unbox

  • [ ] Take the arm and base plate out of the foam
  • [ ] Verify accessories: CANable USB-CAN adapter, power supply, gripper
  • [ ] Inspect for shipping damage on the joint covers and cables

2. Mount on a stable surface

  • [ ] Bolt the base plate to a workbench (or place on a heavy table)
  • [ ] Keep at least 1 m clearance in all directions for the arm's reach
  • [ ] Route the CAN + power cables away from the workspace

3. Wire it up

  • [ ] Connect the CAN cable between the arm and your CANable adapter
  • [ ] Plug the CANable into a USB port on the host computer
  • [ ] Connect the 24 V power supply to the arm's power input

4. Power on

  • [ ] Flip the supply switch — joints should hum briefly as motors initialize
  • [ ] Verify the CAN device shows up:
    bash
    ls -l /sys/class/net/can*

5. Verify

bash
# Bring up CAN if not auto-enabled
sudo ip link set can0 up type can bitrate 1000000

# Quick zero-gravity test — the arm should float
python i2rt/robots/get_robot.py --channel can0 --gripper linear_4310

Push the arm gently — it should hold position when released.


Quick Start Demo

Run your first YAM arm demo in 5 minutes.

1. Test without hardware (sim mode)

You don't even need an arm. Launch the MuJoCo viewer:

bash
python examples/minimum_gello/minimum_gello.py --mode visualizer_local

A 3D window opens showing the YAM model. Use this to verify your install before plugging in hardware.

2. Zero-gravity mode (with hardware)

The arm enters a gravity-compensated floating state — push it freely, it stays where you leave it.

bash
python i2rt/robots/get_robot.py --channel can0 --gripper linear_4310

Press Ctrl+C to exit.

3. Python API — read state and move

python
from i2rt.robots.get_robot import get_yam_robot
import numpy as np

# Connect (zero-gravity ON by default)
robot = get_yam_robot(channel="can0", zero_gravity_mode=True)

# Read current observations
obs = robot.get_observations()
print("Arm joints:", obs["joint_pos"])   # (6,) radians
print("Gripper:",    obs["gripper_pos"])  # (1,) normalized

# Command home position (6 arm joints + 1 gripper)
robot.command_joint_pos(np.zeros(7))

robot.close()

4. Different arm variants

python
from i2rt.robots.get_robot import get_yam_robot
from i2rt.robots.utils import ArmType

robot = get_yam_robot(arm_type=ArmType.BIG_YAM, channel="can0")
Arm typeConstant
StandardArmType.YAM
ProArmType.YAM_PRO
UltraArmType.YAM_ULTRA
BigArmType.BIG_YAM
Gripper-onlyArmType.NO_ARM

For the full SDK and advanced examples, see the API Reference section below.


Grippers

YAM supports six interchangeable end-effector options. Specify the gripper when creating the robot:

python
from i2rt.robots.get_robot import get_yam_robot
from i2rt.robots.utils import GripperType

robot = get_yam_robot(channel="can0", gripper_type=GripperType.LINEAR_4310)

crank_4310

Zero-linkage crank gripper powered by the DM4310 motor. The crank mechanism minimizes the total gripper width — ideal for reaching into tight spaces.

PropertyValue
MotorDM4310
MechanismZero-linkage crank
CalibrationNot required — fixed limits (0.0 to -2.7 rad)
PD Gainskp=20, kd=0.5
Best forNarrow workspace, minimizing sweep width

linear_3507

Lightweight linear gripper with a DM3507 motor. Smaller and lighter than the 4310 variant.

PropertyValue
MotorDM3507
MechanismLinear actuator
CalibrationRequired — auto-detected on startup
PD Gainskp=10, kd=0.3
Best forWeight-sensitive setups

Calibration required

The linear_3507 motor travels more than 2π radians over the full stroke, so the SDK needs to know its start position. On startup it auto-runs a calibration routine that moves the gripper in both directions to detect limits. Ensure the gripper can move freely during init.

linear_4310

Standard linear gripper with the heavier DM4310 motor. Slightly more gripping force than the 3507.

PropertyValue
MotorDM4310
MechanismLinear actuator
CalibrationRequired — same auto-calibration as linear_3507
PD Gainskp=20, kd=0.5
Best forGeneral-purpose tasks, higher force

flexible_4310

Linear gripper with flexible soft fingertips. Identical drivetrain to linear_4310 (DM4310 motor, same stroke), but the compliant tips conform to the workpiece — useful for grasping fragile or irregular objects without precise pose alignment.

PropertyValue
MotorDM4310
MechanismLinear actuator with flexible tips
CalibrationRequired — same auto-calibration as linear_4310
PD Gainskp=20, kd=0.5
Best forFragile or irregular objects, tolerant grasping

yam_teaching_handle

The leader arm handle — not a manipulation gripper, but a hand controller for teleoperation.

FeatureDescription
TriggerControls follower gripper open/close
Top buttonEnable/disable arm synchronization
Second buttonUser-programmable

For full usage — trigger reading, encoder calibration, and teleoperation setup — see the YAM Leader Arm page.

no_gripper

Arm-only configuration with no end effector. Use when the application doesn't require grasping.

python
robot = get_yam_robot(gripper_type=GripperType.NO_GRIPPER)
# robot.num_dofs() returns 6 (arm joints only)

Gripper Models (MuJoCo)

i2rt/robot_models/gripper/
├── crank_4310/        crank_4310.xml + assets/
├── linear_3507/       linear_3507.xml + assets/
├── linear_4310/       linear_4310.xml + assets/
├── flexible_4310/     flexible_4310.xml + flexible_4310.urdf + assets/
├── yam_teaching_handle/  yam_teaching_handle.xml
└── no_gripper/        no_gripper.xml

Gripper Force Limiting

The SDK includes automatic gripper force limiting when the gripper is clogged (stalled against an object). This is enabled by default with limit_gripper_force=50.0 N. The system monitors motor effort and speed to detect when the gripper has hit an object, then limits the applied torque accordingly.


API Reference

Import

python
from i2rt.robots.get_robot import get_yam_robot
from i2rt.robots.utils import ArmType, GripperType

get_yam_robot()

Factory function — the recommended way to create a robot instance.

python
robot = get_yam_robot(
    channel="can0",
    arm_type=ArmType.YAM,
    gripper_type=GripperType.LINEAR_4310,
    zero_gravity_mode=True,
    ee_mass=None,
    ee_inertia=None,
    sim=False,
)
ParameterTypeDefaultDescription
channelstr"can0"CAN interface name (e.g. can0, can_follower_l). Ignored in sim mode.
arm_typeArmTypeArmType.YAMArm variant: YAM, YAM_PRO, YAM_ULTRA, BIG_YAM, or NO_ARM (gripper-only)
gripper_typeGripperTypeGripperType.LINEAR_4310Gripper type. See Grippers
zero_gravity_modeboolTrueEnable gravity compensation on init
ee_massfloat | NoneNoneEnd-effector mass override (kg) for MuJoCo inertial
ee_inertianp.ndarray | NoneNoneEnd-effector inertia override — 10-element array [ipos(3), quat(4), diaginertia(3)]
gravity_comp_factornp.ndarray | NoneNonePer-joint gravity-compensation factor (6 elements). Overrides the arm-type default.
gripper_limits_overridenp.ndarray | NoneNone[closed, open] limits in radians. When set, the gripper skips auto-calibration.
gripper_kpfloat | NoneNoneOverride the gripper's default kp (per-call).
gripper_kdfloat | NoneNoneOverride the gripper's default kd (per-call).
simboolFalseIf True, return a SimRobot (no hardware needed)

Returns: MotorChainRobot instance (or SimRobot when sim=True).

Arm types

Since v1.0, all arm variants use the same factory function. Pass arm_type=ArmType.BIG_YAM instead of the removed get_big_yam_robot(). Pass arm_type=ArmType.NO_ARM for gripper-only setups.

Zero-gravity vs PD mode

With zero_gravity_mode=True the arm floats — great for teleoperation. With False, the arm holds its current joint positions as PD targets. Use False when operating without the motor safety timeout.

MotorChainRobot

num_dofs() → int

Returns the number of controllable degrees of freedom (arm joints + gripper if present).

python
n = robot.num_dofs()
# 7 (6 arm joints + 1 gripper), or 6 if no gripper

get_joint_pos() → np.ndarray

Returns the current joint positions as a NumPy array in radians. Includes all DOFs (arm + gripper).

python
q = robot.get_joint_pos()
# shape: (7,) for 6-joint arm + gripper
# shape: (6,) for arm-only (no_gripper)

command_joint_pos(joint_pos: np.ndarray) → None

Commands all joints to move to joint_pos (radians). The controller uses PD tracking. For robots with a gripper, the last element controls the gripper position (normalized 0–1 for linear grippers).

python
import numpy as np
robot.command_joint_pos(np.zeros(7))  # move arm to home, close gripper

Joint limits

Joint limits are defined in the MuJoCo XML model (e.g. i2rt/robot_models/arm/yam/yam.xml). A 0.15 rad safety buffer is applied automatically. Exceeding limits can cause motor errors.

command_joint_state(joint_state: dict) → None

Commands joints with position, velocity, and optional PD gains.

python
robot.command_joint_state({
    "pos": target_positions,    # np.ndarray
    "vel": target_velocities,   # np.ndarray
    "kp": custom_kp,            # optional, overrides defaults
    "kd": custom_kd,            # optional, overrides defaults
})

get_observations() → dict

Returns a dictionary of all robot observations. This is the primary method for reading robot state.

python
obs = robot.get_observations()
# Without gripper:
#   obs["joint_pos"]  — (6,) joint positions
#   obs["joint_vel"]  — (6,) joint velocities
#   obs["joint_eff"]  — (6,) joint efforts (torques)
#
# With gripper:
#   obs["joint_pos"]  — (6,) arm joint positions only
#   obs["joint_vel"]  — (6,) arm joint velocities
#   obs["joint_eff"]  — (6,) arm joint efforts
#   obs["gripper_pos"] — (1,) gripper position
#   obs["gripper_vel"] — (1,) gripper velocity
#   obs["gripper_eff"] — (1,) gripper effort
#
# With temp_record_flag=True (passed to get_yam_robot):
#   obs["temp_mos"]   — (N,) MOS temperature per motor (°C)
#   obs["temp_rotor"] — (N,) rotor temperature per motor (°C)

Temperature fields are only included when the robot is constructed with temp_record_flag=True.

get_robot_info() → dict

Returns robot configuration — useful for programmatic introspection (used internally by the Viser and MuJoCo control interfaces).

python
info = robot.get_robot_info()
# {
#   "arm_type":           ArmType,
#   "gripper_type":       GripperType,
#   "kp":                 np.ndarray,
#   "kd":                 np.ndarray,
#   "grav_comp_kd":       np.ndarray,
#   "coulomb_friction":   np.ndarray,
#   "joint_limits":       np.ndarray,   # (2, N) [lower, upper]
#   "gripper_limits":     np.ndarray,
#   "gravity_comp_factor":np.ndarray,
#   "gripper_index":      int | None,
#   "limit_gripper_effort": float,
# }

get_motor_torques() → np.ndarray | None

Returns the last computed motor torques (gravity compensation + PD command torques) sent to hardware. Returns None before the first control loop iteration.

python
torques = robot.get_motor_torques()  # (N,) Nm

move_joints(target, time_interval_s=2.0) → None

Smoothly interpolates from the current position to target over time_interval_s seconds (blocking).

python
robot.move_joints(np.zeros(7), time_interval_s=3.0)

zero_torque_mode() → None

Enters zero-torque mode — all PD gains are set to zero, motors produce no active torque.

update_kp_kd(kp, kd) → None

Update the PD gains at runtime.

python
robot.update_kp_kd(kp=new_kp_array, kd=new_kd_array)

enter_gravity_comp_idle() → None

Reset the command buffer to zeros with kd = grav_comp_kd (from the arm's YAML), returning the arm to gravity-comp idle. self._kp and self._kd are left untouched, so a later command_joint_pos keeps its tracking gains.

python
robot.command_joint_pos(target)
# ... do work ...
robot.enter_gravity_comp_idle()  # arm floats again, with motor-side idle damping

The MuJoCo viewer calls this automatically on every CONTROL → VIS toggle. See Gravity & Friction Compensation for details.

start_recording(save_dir: str) → bool

Starts asynchronous joint state recording to disk. Requires the robot to have been constructed with a joint_state_saver_factory. Raises RuntimeError if no saver factory was provided.

python
robot.start_recording("/tmp/session_001")

stop_recording(prefix: str = "") → tuple[bool, str]

Stops an active recording and returns (success, message).

python
ok, msg = robot.stop_recording(prefix="take_1")

close() → None

Safely shuts down the robot: stops the control thread and closes the CAN interface.

python
robot.close()

Command-line: Zero-gravity test

bash
# Arm enters gravity-compensated floating mode
python i2rt/robots/get_robot.py --channel can0 --gripper linear_4310

# Test with BIG YAM
python i2rt/robots/get_robot.py --arm big_yam --channel can0

# Simulation mode (no hardware needed)
python i2rt/robots/get_robot.py --sim

Motor Configuration Utilities

bash
# Disable the 400 ms safety timeout (run twice)
python i2rt/motor_config_tool/set_timeout.py --channel can0
python i2rt/motor_config_tool/set_timeout.py --channel can0

# Re-enable
python i2rt/motor_config_tool/set_timeout.py --channel can0 --timeout

# Zero motor ID 1 (run for each motor 1–6 as needed)
python i2rt/motor_config_tool/set_zero.py --channel can0 --motor_id 1

MuJoCo Models

The SDK uses MuJoCo for gravity computation, simulation, and visualization. Arm and gripper models are combined at runtime via combine_arm_and_gripper_xml().

i2rt/robot_models/
├── arm/
│   ├── yam/yam.xml
│   ├── yam_pro/yam_pro.xml
│   ├── yam_ultra/yam_ultra.xml
│   └── big_yam/big_yam.xml
└── gripper/
    ├── crank_4310/crank_4310.xml
    ├── linear_3507/linear_3507.xml
    ├── linear_4310/linear_4310.xml
    ├── flexible_4310/flexible_4310.xml
    ├── yam_teaching_handle/yam_teaching_handle.xml
    └── no_gripper/no_gripper.xml

Launch the visualizer without hardware:

bash
python examples/minimum_gello/minimum_gello.py --mode visualizer_local

Per-Arm YAML Configuration

Hardware tuning lives in i2rt/robots/config/<arm>.yml — one file per arm variant.

FieldTypePurpose
kp / kd6-element listPer-joint PD gains used by command_joint_pos / command_joint_state.
gravity_comp_factor6-element listPer-joint multiplier on the model gravity torque. Overridable via get_yam_robot(gravity_comp_factor=...).
grav_comp_kd6-element listMotor-side MIT-mode kd applied only in gravity-comp idle. Tunes how stiff the arm feels when released. YAML-only.
coulomb_friction6-element list, NmMagnitude of friction · sign(q_dot) injected alongside gravity comp. Cancels static stiction. YAML-only.

The gripper slot is automatically padded with 0.0 for grav_comp_kd and coulomb_friction — gripper joints don't need passive compensation.

See Gravity & Friction Compensation below for the full physics and tuning workflow.


Gravity & Friction Compensation

YAM arms compute gravity torques from their MuJoCo model and add them to every motor command, so the arm "floats" against gravity. From v1.1.1 the same control law also includes per-joint Coulomb friction compensation and per-joint motor-side idle damping (grav_comp_kd). Together they make the arm easy to backdrive while keeping idle behaviour stable.

How the torque command is built

In gravity-comp idle (zero_gravity_mode=True, no active control command), each motor receives:

τ = g(q) · gravity_comp_factor  +  coulomb_friction · sign(q_dot)

plus motor-side MIT-mode kd damping equal to grav_comp_kd[i] running at kHz on the motor's onboard loop.

Under an active PD command (command_joint_pos / command_joint_state):

τ = τ_PD  +  g(q) · gravity_comp_factor  +  coulomb_friction · sign(q_dot)

grav_comp_kd is not applied during PD control — self._kd is used instead, so PD tracking gains are unchanged. The relevant code lives in i2rt/robots/motor_chain_robot.py.

YAML defaults

yaml
gravity_comp_factor: [1.0, 1.1, 1.1, 1.2, 1.0, 1.0]
grav_comp_kd:        [0.1, 0.1, 0.1, 0.3, 0.05, 0.05]
coulomb_friction:    [0.3, 0.3, 0.3, 0.06, 0.06, 0.06]

Per-arm defaults

yam_pro.yml, yam_ultra.yml, and big_yam.yml ship sensible starting values, but the wrist joints in particular vary with payload — expect to retune grav_comp_kd[3] (J4) and coulomb_friction[3] on a custom build.

Runtime override

Only gravity_comp_factor can be overridden per-call:

python
import numpy as np
from i2rt.robots.get_robot import get_yam_robot

robot = get_yam_robot(
    channel="can0",
    gravity_comp_factor=np.array([1.0, 1.15, 1.10, 1.25, 1.0, 1.0]),
)

grav_comp_kd and coulomb_friction are YAML-only — they're hardware-tuning constants tied to the specific arm build.

Mode transitions: enter_gravity_comp_idle()

After issuing PD commands you can return the arm to a clean gravity-comp idle:

python
robot.command_joint_pos(target)   # active PD
# ... do work ...
robot.enter_gravity_comp_idle()   # back to floating, with grav_comp_kd damping

Why this matters

Before v1.1.1 the viewer used update_kp_kd(zeros, zeros) on toggle. That only rewrote member caches and left the stale PD target in _commands, so motors kept holding the last CONTROL pose — felt as per-joint friction after returning to VIS. If you call update_kp_kd directly, follow it with enter_gravity_comp_idle() to clear the stale target.

Simulation parity

SimRobot runs a daemon physics thread that applies model-based gravity comp at the same schedule as hardware. The MuJoCo viewer toggles it via:

python
robot.enable_gravity_comp()    # VIS mode
robot.disable_gravity_comp()   # CONTROL mode (teleport on; physics paused)

get_yam_robot(sim=True) passes a unit factor (np.ones(...)) so sim uses the unscaled MuJoCo gravity. coulomb_friction and grav_comp_kd flow through but have no observable effect in sim (the MuJoCo model has no Coulomb friction and no MIT-mode motor model).

Tuning workflow

Start from the per-arm defaults and adjust in this order:

  1. Set gravity_comp_factor. Lift the arm to a horizontal pose and release. Increase gravity_comp_factor[i] on whichever joint sags first; decrease on whichever drifts up. Tune one joint at a time, shoulder-to-wrist.
  2. Set coulomb_friction. Once the arm holds pose, give each joint a small push. If a joint resists at first then "snaps free", raise its coulomb_friction slightly. Stop just before joints start drifting on their own.
  3. Set grav_comp_kd only if needed. If a joint chatters or limit-cycles after release, raise its grav_comp_kd. Most joints don't need it (defaults near 0.05–0.1).

Quick test loop

bash
# Headless: brings the arm up in grav-comp idle (add --log for live joint/torque table)
python i2rt/robots/motor_chain_robot.py --arm yam --gripper linear_4310 --channel can0

# Browser viewer (Viser) — mirror, IK drag, per-joint sliders
python examples/control_with_viser/control_with_viser.py --arm yam --gripper linear_4310 --channel can0

# MuJoCo viewer — VIS/CONTROL toggle, auto-calls enter_gravity_comp_idle() on toggle
python examples/control_with_mujoco/control_with_mujoco.py --arm yam --gripper linear_4310 --channel can0

Edit the YAML, re-run, repeat. No code changes needed.

Troubleshooting

SymptomLikely causeFix
Joint sags slowly under gravitygravity_comp_factor too lowRaise the per-joint factor by 0.05–0.10 increments
Joint drifts up against gravitygravity_comp_factor too highLower the per-joint factor
Joint chatters / limit-cycles when releasedStiction interacting with under-compensation; or grav_comp_kd too lowFirst, recheck gravity_comp_factor; if clean, raise grav_comp_kd
Joint feels "sticky" — needs a push to start, then moves freelycoulomb_friction too lowRaise the per-joint friction value
Joint walks on its own after releasecoulomb_friction too highLower the per-joint friction value
Arm "holds" the previous PD target after CONTROL → VISenter_gravity_comp_idle() was not calledUse the v1.1.1 viewer (auto-calls), or call it yourself after command_joint_pos
Sim arm flops to the floorenable_gravity_comp() was not called or start_server() not invokedUse mujoco_control_interface (calls both for you)

MuJoCo Control Interface

Location: examples/control_with_mujoco/

Interactive MuJoCo viewer for i2rt robots. Visualises the robot in real time and lets you move it by dragging a target marker via inverse kinematics — no hardware required in simulation mode.

The sliders/mocap → IK → self-collision → command_joint_pos pipeline runs on a daemon control thread at control_dt = 5 ms (200 Hz), guarded by short viewer.lock() sections so viewer rendering never stalls commands. Commands that would self-collide are blocked.

Modes

The interface has two modes toggled with SPACE:

VIS mode (default):
  Robot joint states  ──►  MuJoCo viewer
  (real hw: gravity comp idle    sim: physics thread on, grav-comp active)

CONTROL mode (press SPACE):
  Drag target marker  ──►  IK solver  ──►  Command arm
  (real hw: PD tracks target      sim: physics paused; arm teleports)

In --sim, SPACE additionally calls SimRobot.enable_gravity_comp() / disable_gravity_comp() so the simulated arm settles under gravity in VIS and follows the IK target instantly in CONTROL — matching the hardware feel.

Running

bash
# Simulation (no hardware)
python examples/control_with_mujoco/control_with_mujoco.py --sim
python examples/control_with_mujoco/control_with_mujoco.py --arm big_yam --sim
python examples/control_with_mujoco/control_with_mujoco.py --arm no_arm --gripper flexible_4310 --sim

# Real hardware
python examples/control_with_mujoco/control_with_mujoco.py --channel can0
python examples/control_with_mujoco/control_with_mujoco.py --arm big_yam --channel can0

Arguments

ArgumentDefaultDescription
--armyamArm type: yam, yam_pro, yam_ultra, big_yam, no_arm
--gripperlinear_4310Gripper type
--channelcan0CAN interface name (real hardware only)
--simoffUse simulation instead of real hardware
--dt0.02Control loop timestep
--siteautoMuJoCo site name as end-effector
--logoffLog joint state and torques each loop iteration

Viewer Controls

  1. Press SPACE to enter CONTROL mode (marker turns red)
  2. Double-click the red target sphere to select it
  3. Ctrl + right-drag — translate the target
  4. Ctrl + left-drag — rotate the target
  5. Press SPACE again to return to VIS mode

Viser Control Interface

Location: examples/control_with_viser/

Browser-based 3-D viewer and teleop interface, powered by viser. Open the viewer in any browser — no native viewer required, no display attached to the robot.

The interface starts in read-only mode. The robot accepts no commands until the operator visually confirms alignment and clicks Enable Robot. Once enabled, three control modes are available:

VIS mode (default):
  Robot joint states  ──►  Browser viewer  (read-only mirror)

IK mode:
  Drag transform gizmo  ──►  mink IK solver  ──►  Command arm

Joint-slider mode:
  Per-joint sliders (deg)  ──►  Command arm directly

A gripper slider (normalised 0–1) is always available, independent of the arm mode.

Running

bash
# Simulation
python examples/control_with_viser/control_with_viser.py --sim
python examples/control_with_viser/control_with_viser.py --arm big_yam --gripper flexible_4310 --sim

# Custom port (multiple instances on one machine)
python examples/control_with_viser/control_with_viser.py --sim --port 8090

# Real hardware
python examples/control_with_viser/control_with_viser.py --channel can0

Open http://localhost:8080 (or whichever --port you chose) in your browser.

Enabling the Robot

  1. Open the viewer URL printed on launch.
  2. Verify the rendered pose matches the physical robot.
  3. Tick Alignment Confirmed.
  4. Click Enable Robot.

The robot only starts responding to slider / IK input after step 4.

Teaching-Handle Indicators

When running with --gripper yam_teaching_handle, two on-screen LEDs mirror the leader-arm buttons:

  • SYNC — top button, latches arm-sync on/off.
  • RECORD — second button, user-programmable (commonly bound to recording).

MuJoCo vs Viser

FeatureMuJoCoViser
DisplayNative windowBrowser (any client)
Multi-userLocal onlyMultiple browsers can connect to one server
IK targetDrag in 3-D sceneDrag transform gizmo
Per-joint slidersYes (CONTROL mode)Yes (Joint-slider mode)
Safety gateNoneRead-only until "Enable Robot"
Best forLocal debuggingRemote / headless robot, browser-only setups

Record & Replay Trajectory

Location: examples/record_replay_trajectory/

Record a manipulation trajectory through teleoperation (or gravity-comp hand-guiding) and replay it exactly — useful for dataset collection and validating robot configurations.

Record phase:
  Arm in gravity-comp mode  ──►  Guide by hand  ──►  Save joint trajectory

Replay phase:
  Load trajectory  ──►  Command arm via PD control  ──►  Arm replays motion

Running

bash
python examples/record_replay_trajectory/record_replay_trajectory.py --channel can0 --gripper linear_4310
KeyAction
rStart / stop recording
pPlay back recorded motion
sSave trajectory to file
lLoad trajectory from file
qQuit

Options:

bash
--channel can0          # CAN bus channel
--gripper linear_4310   # Gripper type (for gravity compensation)
--output file.npy       # Output filename
--load file.npy         # Load and replay a trajectory at startup

Output Format

Trajectories are saved as a NumPy pickled dictionary (not a plain array):

python
import numpy as np

data = np.load("trajectory.npy", allow_pickle=True).item()

trajectory = data["trajectory"]   # np.ndarray, shape (T, 7) — T timesteps × joints
timestamps = data["timestamps"]   # np.ndarray, shape (T,)   — seconds since epoch
frequency  = data["frequency"]    # float — target control frequency in Hz

allow_pickle=True required

The file is saved with np.save(path, dict) which uses Python pickling. Loading with plain np.load() will raise a ValueError. Always pass allow_pickle=True and call .item() to extract the dict.


Where to Buy

Visit i2rt.com or contact sales@i2rt.com.

Released under the MIT License. · Questions? support@i2rt.com