# YAM — robot control

How we command the YAMs from Python: chiral protocol, joint-position
commands, the obs stream, and teleop.

## chiral — websocket RPC to `rd serve`

Server runs on russet at `ws://10.110.22.11:8765`:

```bash
ssh robot-lab  # on russet
cd ~/cameron && rd serve --action-type joint --resize-images 1080x1920
```

`--resize-images 1080x1920` keeps the scene cam at HD1080; the wrist cams
default to HD720 (see the name-conditional patch in
`raiden_fork/raiden/server.py:1213`). `rd serve` prints "Raiden policy
server ready" when up; deploy/record connect via:

```python
from chiral.client import PolicyClient
env = PolicyClient("ws://10.110.22.11:8765")
env.connect()
```

The client serializes every send+recv pair behind a single
`asyncio.Lock`. **All RPCs are single-flighted.** Lock contention is the
root cause of most "slow" symptoms — see `known-bugs.md`.

## Commanding joints

### `env.smooth_move_to(joints, max_joint_vel_rad_s, min_move_s)` — blocking

The recommended RPC for sequencing chunked policy actions. The server
runs `smooth_move_joints` in parallel threads with `thread.join`, then
replies with `{"ok": True/False, "delta_rad": float, "duration_s": float}`.

Velocity-paced safe pattern (default for record + deploy):

```python
for idx, cmd in enumerate(joint_cmds):
    vel = first_move_vel if idx == 0 else max_joint_vel
    info = env.smooth_move_to(
        cmd.astype(np.float32),
        max_joint_vel_rad_s=vel,
        min_move_s=0.1,
    )
    if not info.get("ok"):
        break
```

The first pose of a chunk uses a slower velocity (typical `0.2 rad/s`)
because it's likely a larger jump from current; subsequent poses are
short deltas at the full velocity (typical `0.3 rad/s` for safe motion).

See `feedback_yam_control_stack.md` in auto-memory.

### `env.put_action(action)` — non-blocking (action_dispatch coroutine)

Queues an action for the server's high-rate action dispatcher. Used for
the bimanual control side; less common for chunked single-arm policy
deploy.

### `env.reset()` — blocking

Sends both arms to home positions. Server prints "Moving all arms to
home positions..." → "All arms at home positions". **Caveat**: when the
deploy then disconnects (e.g. quits), the server interprets the
disconnect as estop and shuts itself down. Restart `rd serve` between
sessions.

## Obs stream

`env.start_obs_stream(hz=30)` spawns a background coroutine that polls
the server for the latest scene+wrist frames + state at `hz`.
`env.latest_obs` returns the most recent decoded snapshot or `None`
before the first frame arrives.

```python
env.start_obs_stream(hz=30)
while env.latest_obs is None:
    time.sleep(0.05)
obs = env.latest_obs            # ← use this in inference
scene = obs[args.scene_cam_name]
wrist = obs["right_wrist_camera"]
joints = obs.joints              # bimanual state
```

**Do not** read `obs.timestamp` for dedupe — it's always `0.0` for the
scene cam. Use `id(scene.image)` instead (the underlying numpy buffer
changes per-frame).

**Do not** call `env.stop_obs_stream()` followed by an immediate RPC.
The cancel happens at the next await; if the obs coroutine is between
`send` and `recv` under the lock, the orphan obs frame on the wire is
consumed by the next RPC's `recv`. Symptoms: `smooth_move_to` returns
`{}`, then the next obs decode raises `KeyError('cameras')`. See
`known-bugs.md`.

## Teleop — yellow leader buttons

The follower arms are commanded via leader-arm teleop (gello-style).
Buttons on the leader top + bottom drive the recording state machine.
See `memory.md` for the full mapping table. Quick reference:

| Button | Action |
|---|---|
| Left pedal / top leader | Start / stop recording |
| Middle pedal / top leader | Mark episode **Success** |
| Right pedal / bottom leader | Mark episode **Failure** |

Calibration scripts (`rd calibrate-wrist`, `rd exo-calibrate`) bind
yellow #1-5 to a freeze/unfreeze state machine — see `calibration.md`.

## Joint convention

7-DoF per arm: 6 revolute + 1 prismatic (linear_4310 gripper).
Both arms in single concatenated `proprio14` for the model. Right-arm
indices `[7:14]`, left-arm `[0:7]`. For right-arm-only training:

```python
joints_r = obs.joints[7:14]            # right arm state
joint_cmds_r = ee_actions_to_joint_cmds(...)
```

`obs.joints` is from the leader arm in teleop; from the follower during
deploy. Same shape either way.

## Safety

- Lower `--max_joint_vel_rad_s` whenever something is unsure (Cameron's
  default at deploy: `0.3`).
- `--first_move_vel_rad_s` always slower (default `0.2`) — the first
  chunk pose is the riskiest jump.
- `min_move_s` (typical `0.1`) guarantees a minimum smoothing duration
  even for tiny deltas, preventing jerks.
- If the server doesn't return within a few seconds of a chunk-step, do
  not retry blindly — it's likely the obs-stream lock contention. Drop
  to single-cam mode or pause the stream **at the start of a session**,
  not mid-chunk.
