> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroruntime.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Common errors, what they mean, and what to change.

## Nothing moves

The follower applies nothing until a leader holds control. Check, in order:

1. `claim_control()` returned `true`. It returns `false` on a denial and on a
   timeout.
2. The deadman is held.
3. The follower is `ACTIVE`, not `HOLDING`. Watch `on_safe_state`.

If it stays in `HOLDING` while commands arrive, `resume_requires_reclaim` is set
and something tripped a failsafe. Call `Follower.arm()` to clear it.

## Control is denied

`claim_control()` returned `false` and `on_control_denied` fired with a message
like this:

```text theme={null}
schema mismatch: follower a1b2c3d4, we derived e5f6a7b8
```

The follower granted the lease, the leader compared the two schemas, saw they
differed and handed it straight back. Both machines have to name the same
joints, in the same order, with the same units. On the LeRobot adapters that
means the same joint names and the same `units=` on each side.

## The session joins, then silence

Both ends joined and neither sees the other. Almost always two different rooms.

Set `ZERORUNTIME_MEETING_ID` on both machines and confirm they match. Without it
each side creates its own room, and joining an empty room succeeds.

## Holding, active, holding

Commands stopped arriving for longer than `watchdog_timeout_s`. In order of
likelihood: a slow tick on the leader, network jitter, or an overrunning tick on
the follower. Check `stats()` for overruns.

## Calibration

**The arm moves, but wrongly.** Almost always the wrong calibration file. The two
arms are identical hardware, so loading the leader's file against the follower's
bus gives plausible numbers and wrong motion. The schema check does not catch
it, because both arms have the same joint names.

Run `so101_calibration.verify_against_bus()` and check the `id=` you passed
LeRobot.

**Both arms move, on different scales.** The two machines are running
different `use_degrees` values. Nothing raises, because each side declares the
same joints either way. Print the first `read_joints()` on both and compare the
magnitudes.

**`no calibration for robot_id ...`** means the file is not where the lookup
expects it. Calibration lives under `~/.cache/huggingface/lerobot/calibration/`,
with robots and teleoperators in separate directories, and each host reads only
its own arm's file. The error lists every path it tried, so read that first.

Two causes beyond a missing file:

* The id does not match. `robot_id` is the `id=` you passed `lerobot-calibrate`,
  and followers and teleoperators are filed separately, so the same name in both
  is two different files.
* **LeRobot's cache is somewhere else.** The lookup uses the default path
  directly. LeRobot resolves its own in three steps, and any of them moves the
  file out of reach:

  | Set | Calibration lives in |
  | - | - |
  | `HF_LEROBOT_CALIBRATION` | that path |
  | `HF_LEROBOT_HOME` | `$HF_LEROBOT_HOME/calibration` |
  | `HF_HOME` | `$HF_HOME/lerobot/calibration` |
  | none of them | `~/.cache/huggingface/lerobot/calibration`, which is where this looks |

  `HF_HOME` is the one that catches people out. It is set routinely in Docker
  images, in CI and on shared machines, often by someone who has never heard of
  LeRobot, and it moves the calibration cache with it.

  Work out the same path LeRobot uses, and pass the result:

  ```python theme={null}
  import os
  from pathlib import Path

  from zeroruntime.teleops.adapters import so101_calibration as calib


  def calibration_root() -> Path:
      if path := os.environ.get("HF_LEROBOT_CALIBRATION"):
          return Path(path).expanduser()
      if home := os.environ.get("HF_LEROBOT_HOME"):
          return Path(home).expanduser() / "calibration"
      if hf := os.environ.get("HF_HOME"):
          return Path(hf).expanduser() / "lerobot" / "calibration"
      return calib.DEFAULT_ROOT


  cal = calib.load_for("my_follower", root=calibration_root())
  ```

  With none of the three set, or one set to an empty value, this returns the
  default, so it works whether or not the cache was moved. `load_for()` searches
  both directory layouts, so there is no path to spell out.

  On ROS 2, set `calibration_file` instead of `robot_id`, and keep
  `calibration_profile: lerobot_so101_rad`.

**`'LEROBOT_HOME' is deprecated, please use 'HF_LEROBOT_HOME' instead`** is a
`ValueError` raised by LeRobot while it imports, before any Torq code runs.
Unset `LEROBOT_HOME` and set `HF_LEROBOT_HOME` in its place.

**Gain feels wrong near the end stops.** Each host normalises against its own
arm's travel. Copying `joint_min` and `joint_max` from one machine to the other
produces an arm that tracks but with the wrong gain at the extremes.

## Errors

| Error | Fix |
| - | - |
| `pass token= or set ZERORUNTIME_AUTH_TOKEN` | export `ZERORUNTIME_AUTH_TOKEN`, or pass `token=` |
| `RoomError` | create a room through the API. Invented names are rejected |
| `TimeoutError` from the transport | check the token has not expired and signaling is reachable |
| `cameras did not open` | the device is busy or the path is wrong. Rerun `python -m zeroruntime.teleops.devices` |
| `ImportError` on `adapters.lerobot` | `pip install "zeroruntime-teleops[lerobot]"` |
| `call start() before tick()` | call `start()`, or use `run()` which does both |
| `AttributeError` inside the base constructor | move `super().__init__()` to the end of your `__init__` |
| `sets on_starvation=TORQUE_OFF but does not override torque_enable()` | implement `torque_enable()`, or use the default `HOLD` |
| `NotAligned: cannot arm` | move the leader to match the follower, or use `arm_when_aligned()` |
| `CalibrationError: no calibration for robot_id ...` | run `lerobot-calibrate` on that machine, or pass an explicit path. See [Calibration](#calibration) |
| `CalibrationError: ... is not valid JSON` | the calibration file is corrupt. Re-run `lerobot-calibrate` |

## Related

<CardGroup cols={2}>
  <Card title="Safety" icon="shield-halved" href="/robotics/concepts/safety">
    What to get right before a session touches hardware.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/robotics/api-reference/python/errors">
    Every exception, what raises it, and what to catch.
  </Card>
</CardGroup>

## Still stuck?

<CardGroup cols={3}>
  <Card title="Community" icon="discord" href="https://discord.gg/f2WsNDN9S5">
    Ask other developers, and us.
  </Card>

  <Card title="GitHub" icon="github" href="https://github.com/ZeroRuntimeAI">
    Report a bug or open an issue.
  </Card>

  <Card title="Talk to an Expert" icon="headset" href="https://zeroruntime.ai/talk-to-expert">
    Contact support or speak with our team.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.