# Operator Training and Certification

> How a drill scenario is defined, how the safety core grades a run, and how curriculum completion produces a certification that gates dispatch.

Teleoperation is the one modality where a bad operator breaks hardware.
ClutchCall therefore does not let an operator onto a live robot class
until the safety core itself has signed off on them. The training screen
is that exam: the operator drives a **simulated** edge through the **real**
safety core, and the core grades the run.

Passing every drill in a skill/level curriculum makes a certification
claimable. The certification is what the dispatcher checks before it hands
that operator a live session on that robot class.

## The drill rig: safety core against a sim edge

A drill run builds the same rig as the cockpit. The only additions are the
scenario target overlay and the live scorer.

| Piece | What it is |
| ----- | ---------- |
| `BrowserOperator` | The operator side of the control link, created with a 32-byte shared key and `periodUs: 40_000` (a 40 ms control period) and an `epoch` of the run's start time. |
| `SimEdge` | A simulated robot edge on the other end of the same key and period, with `trackingDps: 26` — the rate at which the simulated arm chases the commanded pose. |
| `LoopbackPair` | An in-process transport between the two, configured with a delay and a drop rate taken from the scenario (see below). |
| `Scorer` | Fed one sample per tick; produces the result when the run terminates. |

The safety core running in the browser is the same WASM build that runs on a
live link, so the state machine the operator is graded against is
bit-identical to the one that governs real hardware. States surface as
`BOOT`, `SAFE`, `ARMED`, `DIVERGED`, and `PROTECTIVE_STOP`.

The run loop ticks every 40 ms:

1. Integrate the joystick velocities into the commanded target pose —
   `55 °/s` per axis, clamped to `±165°` per joint.
2. `op.step(target)` then `edge.step()`.
3. Read `op.view()` for the ghost pose, divergence, robot state, and fault.
4. Push that sample into the scorer with `dt = 0.04`.

Aborting, resetting, switching drills, or leaving the screen tears the rig
down: the interval is cleared, and both the operator and the sim edge are
closed.

## Anatomy of a scenario

A scenario is a plain object. These are the fields the drill screen reads:

| Field | Meaning |
| ----- | ------- |
| `id` | Scenario id. This is what `recordPass` records. |
| `name` | Display name of the drill. |
| `skill` | The skill family the drill belongs to (e.g. the robot class skill). |
| `level` | The level within that skill. `skill` + `level` together identify a curriculum and a certification. |
| `targetDeg` | Six joint angles. The pose the operator must reach. Rendered as `J1…J6` chips. |
| `toleranceDeg` | The pose is "reached" when the **worst** joint error is within this many degrees. |
| `holdS` | How long the pose must be held inside tolerance. |
| `timeLimitS` | The wall-clock budget for the run. |
| `link` | `poor` degrades the transport. Anything else is a clean link. |
| `obstacleAtS` | If set, an obstruction fires at this elapsed time. If null, no obstruction. |

Pose error shown in the HUD is the max-norm:

```
err = max over joints i of | targetDeg[i] - ghost[i] |
```

The progress meter is labelled **reach-and-hold** — reaching tolerance is not
the end of the drill, holding there for `holdS` is.

## Difficulty: link quality and obstructions

Two scenario fields make a drill hard. Both are visible to the operator as
warning chips before they start.

**Link quality** configures the loopback transport:

| `link` | Loopback delay | Drop rate |
| ------ | -------------- | --------- |
| `poor` | 95 ms | 6% |
| (anything else) | 40 ms | 0 |

A poor link adds delay and loss on top of a 40 ms control period, which puts
the watchdog at risk. The operator has to drive in a way that tolerates the
link, not just a way that reaches the pose.

**Obstruction** fires once, the first tick where elapsed time reaches
`obstacleAtS`. It calls `edge.setTracking(0.5)` — the simulated arm's
tracking collapses, so it stalls behind the commanded ghost. Divergence then
grows for as long as the operator keeps pushing. The correct response is to
ease off; the wrong response trips the core.

## How the scorer grades a run

Each tick the scorer receives the ghost pose, the divergence in degrees, the
robot state, and the fault, along with the 40 ms delta. From that stream it
maintains three live values that the HUD renders:

- `progress` — reach-and-hold completion, 0 to 1.
- `elapsedS` — run time, checked against `timeLimitS`.
- `faultEvents` — the count of safety-core trips: watchdog, divergence, and
  E-STOP.

When the run terminates the scorer exposes a `result`, the rig is torn down,
and the result panel renders. A drill **passes only on a clean run**: the
pose reached and held, *and* zero safety-core trips. The core is the
examiner — there is no way to argue with it, and there is no partial credit
for a run that tripped it.

## The score breakdown

The result carries a `score` out of 100 and a four-part `breakdown`:

| Component | Max | Reported alongside |
| --------- | --- | ------------------ |
| **Reach** | 40 | Whether the target pose was reached and held. |
| **Speed** | 20 | `timeS` — the run's total elapsed time. |
| **Safety** | 20 | `faultEvents` — the number of safety-core trips. |
| **Precision** | 20 | `peakDivergenceDeg` — the worst divergence seen during the run. |

The panel headline reflects one of three outcomes:

| Outcome | Condition |
| ------- | --------- |
| `PASSED` | `result.passed` |
| `FAILED — safety violation` | The pose was reached (`result.reached`) but the run did not pass. |
| `FAILED — timed out` | The pose was never reached. |

## Driving the twin

Two on-screen joysticks, or a gamepad if one is connected:

| Control | Axis |
| ------- | ---- |
| Left stick, horizontal | J1 |
| Left stick, vertical | J2 |
| Right stick, vertical | J3 |
| Right stick, horizontal | Grip |

Stick deflection is a velocity, not a position: it is integrated into the
commanded pose at `55 °/s` per tick and clamped to `±165°`. Releasing the
stick holds the current commanded pose, which is what lets an operator settle
inside tolerance for the hold window. Aborting or resetting zeroes all six
velocities.

## Curriculum and certification

A curriculum is the set of drills for one `skill` + `level` pair. The screen
loads it on mount and whenever the selected drill changes:

```ts
const curr = await api.curriculum(scenario.skill, scenario.level);
// → { drills: Scenario[], passedIds: string[], complete: boolean }
```

The curriculum card lists every drill in the set, ticks the ones whose id is
in `passedIds`, tags drills that have a poor link or an obstruction as
`hard`, and lets the operator jump straight to any of them.

The loop is:

1. Operator passes a drill.
2. The screen calls `api.recordPass(scenario.id)` and reloads the curriculum.
3. When every drill in the set has been passed, the reloaded curriculum comes
   back with `complete: true` and a **curriculum complete — cert available**
   chip appears.
4. The operator claims it: `api.grantCert(scenario.skill, scenario.level)`.

Until the curriculum is complete, a passing run shows progress toward the
certification (`passedIds.length` of `drills.length`) rather than a claim
button. A single passed drill is never a certification.

## How a certification gates dispatch

The certification is keyed by `skill` and `level`, which is how it maps onto
a robot class. Once granted, the automatic call distributor's
`claim_lowest_rtt` path is permitted to dispatch that operator to sessions on
that robot class. An operator without the certification for a class is not a
dispatch candidate for it.

This is why the drill rig runs the production safety core rather than a
lookalike: the certification asserts that this operator drove that class to a
target pose without tripping the exact state machine that will be watching
them on live hardware.

## Related

- [Telemetry](/platform/telemetry) — metrics and traces emitted by the gateway
- [Authentication](/concepts/authentication) — how session credentials are minted
