# anvil

> The Python SDK for an Anvil workcell.

The Python SDK for an Anvil workcell, its guides and its reference.

# Tutorials

# Quickstart

Bring an arm up in the sim, move it home, and read back where its gripper is. Nothing here needs a robot: the sim stands in for the drives, so the same script runs against a real arm by swapping one line.

## Install

The SDK is the `anvil` package, which PyPI holds as `anvil-robotics`; its `sim` extra brings the sim:

```
pip install 'anvil-robotics[sim]'
```

The script below then runs as it is.

## The script

```
"""Brings an arm up in the sim, homes it, and says where its gripper ended up."""

import anvil.control
import anvil.hw
import anvil.sim
import anvil.workcell

# The single-arm yam, as Anvil ships it.
workcell = anvil.workcell.WorkcellConfig.OPENYAM_V1_SINGLE
robot = workcell.robot

# The sim stands in for the bench: a world with the arm bolted onto a lift, its drives enabled as real ones would be.
with anvil.sim.World(robot=robot, scene="lift") as world:
    hw = anvil.hw.SimHw(world=world)
    hw.enable()

    # The control loop, holding the one controller this script needs, ticking at 500 Hz on a thread of its own.
    trajectory_controller = anvil.control.JointTrajectoryController(robot=robot)
    with anvil.control.Runtime(hw=hw, controllers=[trajectory_controller], rate_hz=500) as runtime:
        runtime.start()
        handle = runtime.controller(kind=anvil.control.JointTrajectoryController)
        runtime.activate(controller=handle)

        # Home the arm from wherever it stands, and wait for the move to finish.
        home = robot.home_trajectory(from_q=runtime.latest().q)
        handle.play(trajectory=home, timeout_s=30.0)

        arm = anvil.control.control_arms(robot=robot, kind=anvil.control.JointTrajectoryController)[0]
        tip = arm.tip_pose(q=runtime.latest().q)
        print(f"homed: the gripper is at {[round(x, 3) for x in tip.position]} m")
```

Run it with `python quickstart.py`. It prints where the gripper ended up, in meters, in the world frame:

```
homed: the gripper is at [0.28, 0.0, 0.299] m
```

## What it did

1. **Picked a workcell.** `anvil.workcell.WorkcellConfig.OPENYAM_V1_SINGLE` is the single-arm yam as Anvil ships it: the arm, its gripper, its tuning and where it stands.
1. **Stood it in the sim.** `anvil.sim.World` starts a sim and stands the arm in it, on a lift, its drives off; `anvil.hw.SimHw` takes the arm over and drives it the way the real drives are driven. `enable()` switches the drives on.
1. **Started the control loop.** `anvil.control.Runtime` runs the loop on a thread of its own at 500 Hz, ticking the active controller each time. Here it holds one, the `anvil.control.JointTrajectoryController`, which plays timed joint trajectories.
1. **Moved the arm.** `anvil.workcell.RobotConfig.home_trajectory` plans the homing run the arm's description states, from where the arm stands, and `play` runs it, returning once the arm is home.
1. **Read where it is.** `tip_pose` turns the measured joint positions into the gripper's pose.

Leaving the `with` blocks stops the loop and closes the sim.

## On a real arm

The same script drives real drives with [`anvil.hw.DamiaoHw`](reference/hw/#anvil.hw.DamiaoHw) in place of the sim, once the bench passes its readiness check. [Run on real arms](how-to/run-on-real-arms/index.md) takes it there, and says how to take the drives down without dropping an arm: leaving the `with` blocks disables them.

## Next

- [`anvil.control.Runtime`](reference/control/#anvil.control.Runtime): the loop, and switching controllers.
- [`anvil.control`](reference/control/index.md): the other controllers, which follow streamed joint targets or end-effector poses.
# How-to

# Record episodes and export a dataset

Record what a running workcell does into episodes, then turn finished episodes into a LeRobot dataset to train a policy on. This runs in the sim as it is; on a bench, the runtime and the cameras are the ones your script already has.

```
"""Records two episodes of an arm in the sim, with its wrist camera, and exports them as one LeRobot dataset."""

import tempfile
from pathlib import Path

import anvil.camera
import anvil.control
import anvil.hw
import anvil.recorder
import anvil.sim
import anvil.workcell

# The single-arm yam with the camera on its wrist, as a bench that has one would save it.
workcell = anvil.workcell.WorkcellConfig.OPENYAM_V1_SINGLE
workcell.cameras.append(
    anvil.workcell.CameraConfig(
        name="wrist",
        device="",
        mode=anvil.camera.Mode(width_px=640, height_px=480, fps=30),
        link="wrist_cam",
    )
)
robot = workcell.robot
# Where the episodes go: a scratch directory here, wherever your data lives on a bench.
data_root = Path(tempfile.mkdtemp())

with (
    anvil.sim.World(robot=robot, scene="table") as world,
    anvil.camera.Cameras(workcell=workcell, world=world) as cameras,
):
    hw = anvil.hw.SimHw(world=world)
    hw.enable()
    trajectory_controller = anvil.control.JointTrajectoryController(robot=robot)
    with anvil.control.Runtime(hw=hw, controllers=[trajectory_controller]) as runtime:
        runtime.start()
        handle = runtime.controller(kind=anvil.control.JointTrajectoryController)
        runtime.activate(controller=handle)

        session = anvil.recorder.Session(root=data_root, task="home the arm and fold it back")
        recorder = anvil.recorder.Recorder(workcell=workcell, runtime=runtime, cameras=cameras)
        for _ in range(2):
            recorder.start(session=session)
            handle.play(trajectory=robot.home_trajectory(from_q=runtime.latest().q), timeout_s=30.0)
            handle.play(trajectory=robot.dehome_trajectory(from_q=runtime.latest().q), timeout_s=30.0)
            stats = recorder.stop(outcome=anvil.recorder.Outcome.SUCCESS)
            print(f"recorded an episode: {stats.frames_received['wrist']} frames, {stats.ticks_missed} ticks missed")

# Encoding the video is the slow part; H.264 is several times faster than LeRobot's default AV1.
episodes = sorted(session.path.glob("*.mcap"))
export = anvil.recorder.DatasetExport(
    episodes=episodes, out_dir=data_root / "dataset", codec=anvil.recorder.VideoCodec.H264
)
dataset = export.run()
print(f"exported {dataset.num_episodes} episodes, {dataset.num_frames} frames at {dataset.fps} fps to {dataset.path}")
```

## Record

1. **Open a session.** A Session is a directory under a data root you choose, under a name you give or one generated for it. Every episode recorded into it is its next file, `0001.mcap`, `0002.mcap`, each with the task written into it.
1. **Build a recorder** over the workcell, the running runtime and the cameras to record, as Cameras opens them. On real arms, pass the Report the bench came up on as `readiness`, so each episode says which physical drives it was recorded on.
1. **Start, do the task, stop.** start returns at once and records on threads of its own; the loop never waits on it. stop finishes the file and returns what it counted. Say how it went as you stop it, or label it later; a failed episode is kept, labeled, not deleted. Session.discard_last_episode deletes one you do not want.

By default each source is recorded at its own rate, the loop at 100 Hz and each camera at its frame rate; RecorderConfig says otherwise. An episode opens in Foxglove as it is, with the layout written beside it.

## Export

DatasetExport writes the episodes, in the order given, into a new dataset directory that LeRobot opens with `LeRobotDataset(root=...)`. The episodes must be of one workcell, with the same cameras at the same rates. The dataset runs at the anchor camera's rate, the first camera by name unless you name one: each frame is the anchor's, carrying each slot's measured position as `observation.state` and its commanded position as `action`. Frames where the loop commanded no position, as while no controller is active, are left out.

Encoding the video is the slow part, several core-seconds per second of video per camera, and needs `ffmpeg` on `PATH`. It runs at the lowest priority; on the robot, call cancel from another thread the moment the robot needs the CPU, or export on another machine.

# Replay an episode

Play a recorded episode on the robot again: the arms ease from where they stand to where the episode starts, play it at its recorded pace or slower, and hold where it ended. This runs in the sim as it is, recording the episode it replays first.

```
"""Records an arm homing in the sim, then plays the episode back on it from where it stands."""

import tempfile
import time
from pathlib import Path

import anvil.apps
import anvil.control
import anvil.hw
import anvil.recorder
import anvil.sim
import anvil.workcell

workcell = anvil.workcell.WorkcellConfig.OPENYAM_V1_SINGLE
robot = workcell.robot
data_root = Path(tempfile.mkdtemp())

with anvil.sim.World(robot=robot, scene="lift") as world:
    hw = anvil.hw.SimHw(world=world)
    hw.enable()
    # A replay eases the arms in under the trajectory controller and plays joint angles through the joint controller.
    controllers = [
        anvil.control.JointTrajectoryController(robot=robot),
        anvil.control.JointController(robot=robot),
    ]
    with anvil.control.Runtime(hw=hw, controllers=controllers) as runtime:
        runtime.start()
        handle = runtime.controller(kind=anvil.control.JointTrajectoryController)
        runtime.activate(controller=handle)

        # The episode to replay: the arm homing.
        recorder = anvil.recorder.Recorder(workcell=workcell, runtime=runtime)
        recorder.start(session=anvil.recorder.Session(root=data_root))
        episode = recorder.path
        handle.play(trajectory=robot.home_trajectory(from_q=runtime.latest().q), timeout_s=30.0)
        recorder.stop()
        handle.play(trajectory=robot.dehome_trajectory(from_q=runtime.latest().q), timeout_s=30.0)

        # Poll the replay at a steady pace: make the controller it names active, and send what it says is due.
        replay = anvil.apps.EpisodeReplay(path=episode, robot=robot, mode=anvil.apps.ReplayMode.JOINT_ANGLES)
        print(f"replaying {replay.duration_s:.1f} s of {episode.name}")
        started_ns = time.monotonic_ns()
        active_kind = anvil.control.JointTrajectoryController
        while True:
            polled = replay.poll(elapsed_ns=time.monotonic_ns() - started_ns, q=runtime.latest().q)
            if polled.phase == anvil.apps.ReplayPhase.HOLDING:
                break
            if polled.controller is not active_kind:
                active_kind = polled.controller
                runtime.activate(controller=runtime.controller(kind=active_kind))
            if polled.command is not None:
                runtime.send_command(command=polled.command)
            time.sleep(0.005)
        print("the replay ended where the episode did")
```

1. **Open the episode** with EpisodeReplay, for the robot that is running. It refuses an episode recorded on other arms, variants or grippers. Choose what to play back with ReplayMode: the joint angles the arms measured, the commands each controller was sent, or what the drives were told. `speed` below 1 plays it slower.
1. **Hold the controllers it names** in the runtime: the joint trajectory controller for the approach, and the joint controller for the rest, or, replaying commands, every controller the episode was driven by.
1. **Poll it at a steady pace**, with the time since the first poll and the measured joints. poll says which controller should be active and what command is due now: activate that controller when it changes, and send the command when there is one. Stop polling once the phase is HOLDING; the arms hold where the episode ended.

An episode recorded on real drives carries which ones in EpisodeReplay.hardware; compare it with the bench's Report.hardware to know whether this is the same physical robot. Commands replayed run under the settings in effect now, so other gains or IK parameters change what the arms do.

# Run on real arms

Take a script that works in the sim, as the [quickstart](tutorials/quickstart/index.md) does, to the arms on your bench: check the bench, bring the drives up, and take them down again without dropping an arm.

```
"""Checks the bench, brings its arm up on the real drives, homes it, and parks it before the drives are disabled."""

import sys

import anvil.control
import anvil.hw
import anvil.workcell

# The preset that is the bench: the arms, their variant and how they stand.
workcell = anvil.workcell.WorkcellConfig.OPENYAM_V2_SINGLE
robot = workcell.robot

# Read-only: brings the CAN links up, asks every drive and camera what it is, and moves nothing.
report = anvil.hw.check_readiness(workcell=workcell)
print(report.render())
if not report.ok:
    sys.exit("the bench is not ready: fix each failed line above and run again")

hw = anvil.hw.DamiaoHw(buses=anvil.hw.buses(robot=robot))
hw.enable()

stop = anvil.control.CollisionStop(workcell=workcell)
trajectory_controller = anvil.control.JointTrajectoryController(robot=robot, collision_stop=stop)
with anvil.control.Runtime(hw=hw, controllers=[trajectory_controller]) as runtime:
    runtime.start()
    if runtime.realtime_refusal is not None:
        print(f"the loop is not real-time: {runtime.realtime_refusal}")
    handle = runtime.controller(kind=anvil.control.JointTrajectoryController)
    runtime.activate(controller=handle)
    try:
        handle.play(trajectory=robot.home_trajectory(from_q=runtime.latest().q), timeout_s=30.0)
        print(f"homed: {robot.slot_summary}")
        # The work goes here.
    finally:
        # Leaving the with block disables the drives, so park the arm where it can rest limp first. A faulted loop
        # moves nothing more: support the arm by hand before the block ends.
        if runtime.fault() is None:
            runtime.activate(controller=handle)
            handle.play(trajectory=robot.dehome_trajectory(from_q=runtime.latest().q), timeout_s=30.0)
```

## 1. Pick the workcell that is your bench

Each shipped bench is a class attribute of WorkcellConfig, named for the arms' variant and how many stand there: `OPENYAM_V2_SINGLE`, `OPENARM_V2_BIMANUAL`, `OPENYAM_V2_LEADER_FOLLOWER`. WorkcellConfig.preset_shapes() lists them all with their variant and shape. If you do not know which your bench is, ask it: anvil.hw.Survey().detect() reads every drive on the machine, moving nothing, and names the presets the drives could be.

A bench with cameras or a table is the preset with those added: append a CameraConfig per camera and a KeepOutBox per obstacle, save it, and load it in every script after.

## 2. Check the bench

check_readiness brings every CAN link up, then asks each drive and camera what it is. It writes nothing and moves nothing, and takes about half a second. Bringing the links up runs `sudo ip link set`, which `anvil-robotics setup` lets run without a password: run it once on a Linux machine that drives the arms, and it also grants the control loop its real-time priority and installs a real-time kernel where the machine takes one. An Anvil box arrives set up.

Print render(): one line per finding, each `pass`, `warn`, `missing` or `fail`. Go on only when Report.ok is true:

- **fail**: the bench is not this workcell: a drive is another motor model than the description says, or not on Anvil firmware, a motor answers where the arm has none, an arm's drives say it is the other side. The line says what was found, where, and what to do.
- **missing**: a drive, bus or camera did not answer. By default that holds the run up too. To run without it, check with `strict=False` and enable with `hw.enable(require_enabled=False)`; the arm comes up without that part.
- **warn**: worth reading, and does not hold the run up.

It does not matter what the CAN links are called. buses finds each arm on the link its drives answer on, by the identity record each drive carries.

## 3. Bring the drives up

DamiaoHw opens each arm's bus and enable switches the drives on. From there, build the controllers and the Runtime as in the sim. Hand each controller the one CollisionStop: a controller built without one sends its targets into a collision. [Collision stop and speed limits](explanation/collision-stop-and-speed-limits/index.md) says what it does and does not catch.

If Runtime.realtime_refusal is not `None`, the loop thread is not real-time and may run late; the message says what would fix it.

## 4. Park the arms before the drives go off

Leaving the runtime's `with` block disables every drive, however the block is left: at its end, on an exception, on Ctrl-C. A disabled arm is limp and falls under gravity. Bring the arms somewhere they can rest limp before the block ends, in a `finally` so an exception cannot skip it. RobotConfig.dehome_trajectory runs the homing run backwards, ending at the row the homing starts from, the pose the arm's description gives a limp arm to settle in.

To stop the arms in place instead, Runtime.freeze holds each joint where it is, with torque on, until a controller is activated again.

## 5. When a drive drops out

A drive that loses power or faults stops reading `Enabled`, and one that goes unanswered for five ticks in a row reads `CommunicationLoss` instead. By default the loop drives on: every other drive keeps following its controller, and the dropped drive's slot says what it reads in each snapshot's `status_names` and `status_faults`, so a gripper that browns out leaves both arms working and says so. Its joint's position is the last one it reported, and a controller goes on using it.

To stop instead, enable with `hw.enable(stop_on_dropout=True)`. Then the loop faults on the tick that finds a dropped drive and writes nothing more: every drive still answering holds its last command, torque on. From then on, every runtime call that drives the arms or reads their state, such as `latest`, `activate`, `play` and `freeze`, raises HwFaultError, its message naming the drive and what it reads. Runtime.fault() returns the same message without raising, and Runtime.fault_snapshot() what the loop read on that tick, whose `status_faults` say which drive dropped out.

That fault lasts the runtime's life, and nothing moves the arms again. Support the arm by hand, then let the script leave the `with` block, which disables the drives that are still holding. Fix what dropped out, then run the script again: a new DamiaoHw and a new runtime.
# Explanation

# Collision stop and speed limits

Between what a controller asks for and what reaches the drives sit two safeguards: the speed limits, which every controller applies, and the collision stop, which the controllers you hand one to apply. Both judge the targets a controller sends, not the arm the drives measure. Most of what they do not catch follows from that.

## Speed limits

Each controller moves each arm's chain targets no faster than the arm's tuning for that controller allows: a top speed per joint and one for the tool frame. They come from the arm's description, and no joint's target ever moves faster than the top speed the description states for the joint itself. A running controller is retuned through ArmTuning.speed and its `configure`.

Every tick, a step of the targets that would outrun a limit is cut short, every joint's share of it by the same factor, so the arm keeps the path it was asked along and only takes longer over it. A velocity target that would outrun a limit is slowed the same way. The step is measured from the target last sent, never from where the arm measures, so an arm lagging behind its target does not earn a bigger step to catch up.

The limits bound the targets, not the arm. The drives chase each target with the gains they are given, so an arm pushed by hand, falling under gains too soft to hold it, or catching up with a target it lagged behind moves at whatever speed that gives. The gripper is not a chain joint, and these limits do not pace it.

## The collision stop

A CollisionStop guards one robot. It is built from the workcell and handed to every controller of the robot; a controller built without one sends its targets wherever they go. It guards pairs: two of the robot's links, except the pairs an arm's description excludes because their meshes always overlap, and each link against each KeepOutBox of the workcell. Links are measured by the convex hulls of their collision pieces, a box as a box square to the world frame.

Measuring takes longer than a tick, so the stop measures on a thread of its own, around the targets the controllers send, and hands each tick every pair standing within its margin and a few centimeters past it, with how fast each joint opens or closes it. Each tick, of the step a controller is about to send, the stop takes out only what closes on one of those gaps too fast: a pair may close by a share of what is left of its gap past the margin, so it slows as it nears the margin and stops there, within about 150 milliseconds. The rest of the step goes through, so an arm driven sideways into something slides along it, and an arm driven away from it leaves at once, with no hold to wait out. A pair already inside its margin, as the arms start up or as a box goes up around them, may only open: an arm may leave a box, never go deeper in.

The step is cut joint by joint: it keeps as much of each joint's motion as it can. A hand driven by pose, as the end-effector pose controller does, slides along a surface toward where the hand is asked to be. An arm driven by joint targets that run on through what it would hit, a joint stream or a leader, comes to rest at the point nearest its target joint by joint, which may be not far past where it met the surface.

What a cut step does to a run depends on the controller. A trajectory ends where the stop first slowed it, and play raises StoppedShortError. The end-effector pose controller walks on from where the stop sent the arm, at the pace of the hand driving it. A follower the stop leaves more than a few degrees from its leader engages again, at the engage speed, rather than snapping to its leader once it is let go; one left nearer goes on mirroring. The joint controller goes on to its command. CollisionStop.last_stop says which links were closing on what.

## What neither catches

- **The arm itself.** Both judge targets. An arm too soft to hold its own weight, under a low gain override, sags past where the stop sends it, and an arm knocked or pushed goes where it is pushed.
- **What is not modeled.** The stop knows the robot's links and the keep-out boxes you declared, nothing else: not a person, not an object on the table unless a box stands around it, not what the gripper holds.
- **What a straight line misses.** The stop steps along each gap as a straight line from where it last measured it, so a fast arm, or one turning a link's corner toward something, can come inside the margin by what that misses. The margin is the knob for that: the faster the arms move, the wider the margin they need. A stop holds every pair at DEFAULT_MARGIN_M unless `within_arm_margin_m`, `between_arms_margin_m` or `keep_out_margins_m` says otherwise.
- **Everything else.** Neither is a safety-rated stop. To stop the arms where they are, call Runtime.freeze; to take the torque off, cut the drives' power.

In the sim, World.contacts says what touched during a whole run, which is how to find out whether a margin was wide enough before trying it on the arms.

# The sim

The sim is MuJoCo standing where the drives stand, and `pip install 'anvil-robotics[sim]'` brings it. The same script runs against it and against a real arm: anvil.hw.SimHw takes the place of anvil.hw.DamiaoHw, and nothing above the hw interface can tell which it got.

## It is a robot, not a step function

The world comes first, as the workcell does on the bench: anvil.sim.World starts a sim, or joins one already running, and stands your robot in a scene with its drives switched off, on the clock you choose. `SimHw` then takes the robot over, the way `DamiaoHw` opens its CAN links:

```
world = anvil.sim.World(robot=robot, scene="table")
hw = anvil.hw.SimHw(world=world)              # on metal: anvil.hw.DamiaoHw(buses=anvil.hw.buses(robot=robot))
```

Left without a `scene`, a world stands the robot on what its workcell stands on, RobotConfig.stand: a shipped workcell without leaders on the lift, a yam pair on the bracket it carries, whose pole camera, `pole_cam`, the sim renders as any other.

A world holds one robot; another robot is another world. The sim behaves as the drives do, so code that only works because a sim is forgiving fails here first:

- the drives come up disabled and have to be enabled;
- each drive runs its own impedance law on the last setpoint it was sent, and holds it if nothing new arrives;
- a setpoint past what a drive's frame can carry arrives clipped, and a torque past what its motor delivers arrives at that maximum;
- the robot collides, with itself and with the scene it stands in.

A loop that stops leaves the drives disabled and the robot where it falls, as an arm does when its loop stops; the world goes on. The next `SimHw` takes it over as it stands.

The sim models no friction and no rotor inertia yet, so it is optimistic about back-driving an arm and about starting one from rest.

## Two clocks

The world's clock is chosen when it is set up:

- **Free-running**, the default and what the hardware keeps: the world steps against wall time whatever the loop is doing, so a loop that goes quiet is a robot still being commanded.
- **Lockstep**, `World(lockstep_hz=...)`: the world steps once per command and not otherwise, so the same commands give the same run twice, whatever else the machine is doing. A test wants this one.

A runtime over a lockstep world still ticks against the wall clock, at the world's rate, unless it is built stepped: then it ticks only when advance asks. A call that waits on the loop runs no ticks of its own, so on a stepped runtime play times out unless another thread advances the loop meanwhile.

## Cameras

Every camera link of the robot's URDF is a camera in the sim, looking out of the lens the meshes draw, watching from the moment the world is set up. anvil.camera.SimCamera streams one as JPEG frames, as a USB camera would. A frame falls on a grid of the world's time and is stamped with the tick it shows, so a recording places it beside that tick. Free-running, a frame the sim has no time to render is dropped, and the gap in the frame's sequence number says so; lockstep renders every one, so a run sends the same frames twice.

Rendering is offscreen, through EGL, which needs no display and no GPU. On a machine with neither, install Mesa's:

```
sudo apt install libegl1 libegl-mesa0 libgl1-mesa-dri libopengl0
```

## Watching the world

MuJoCo's own window shows the whole world, the robot and the scene it stands in: orbit, pan and zoom with the mouse, and pick a body with a double-click to push it with Ctrl and a drag. `World(viewer=True)` opens it as the world is set up, and World.show_window opens it on a world already running, or again after it was closed. It opens on the machine the sim server runs on, so that machine needs a desktop session; without one the call says so and the world runs on.

## A sim on another machine

The sim stands in for metal, so the machine that would drive the metal need not also simulate it, and the window is nicer on the machine you are sitting at. Start a sim there:

```
anvil-robotics sim --listen 0.0.0.0:7447 --viewer
```

and give its address to `World` on the machine running the loop, with the same release of `anvil` on both:

```
world = anvil.sim.World(robot=robot, address="10.0.0.4:7447", scene="table")
```

## What the world answers

The world answers questions beside whatever drives it, never costing the loop a tick: World.contacts says what is touching now and everything that has touched since the world was set up, looked at every physics step, so "nothing collided during the whole sweep" is one question asked at the end; World.place puts a loose object of the scene, a block on the table, where a run wants it; and World.hold holds joints of the robot still where a run says, a hand on an arm nothing drives, which is how a run moves a leader the operator would move by hand.
# Reference

# Reference

Every public module, class and call of the `anvil` package, generated from its docstrings.

## anvil

The Anvil SDK: a workcell's arms, grippers, cameras and control loop, programmed from Python.

Import the module you use and name things through it: `import anvil.control`, then `anvil.control.Runtime`; or `from anvil.control import Runtime`. Each module loads only what it needs on import, and `import anvil` on its own loads nothing but the exceptions below.

- `anvil.workcell`: what is bolted down — robots, arms, grippers, cameras, keep-out boxes — and the shipped presets.
- `anvil.kinematics`: chains, forward kinematics, IK and poses.
- `anvil.control`: the controllers, their commands and tunings, and the `anvil.control.Runtime` that runs them.
- `anvil.hw`: the drives and the sim's, and the readiness check.
- `anvil.sim`: the sim's world, a robot standing in a scene with MuJoCo standing in for its drives.
- `anvil.camera`, `anvil.recorder`: cameras, and episodes with their compression and export.
- `anvil.apps`: the teleop apps and episode replay.

Every exception the SDK raises is an `anvil.Error`, and also the builtin a Python caller would expect: a refused value is an `anvil.InvalidArgumentError`, a `ValueError`; a call that could not be carried out an `anvil.OperationError`, a `RuntimeError`; a bug in the SDK itself an `anvil.PanicError`. Where a caller can act on why a call failed, the `anvil.OperationError` is a subclass naming it: `anvil.NotFoundError`, a `FileNotFoundError`; `anvil.BusyError`; `anvil.TimedOutError`, a `TimeoutError`; `anvil.UnreachableError`, a `ConnectionError`. These live here because any module's calls raise them; a failure only one module's calls meet is named in that module, like `anvil.control.HwFaultError`. The one other exception is Python's own `TypeError`, for an argument of the wrong type, such as a string where an `anvil.kinematics.Pose` goes: it names the parameter and the type it takes.

What the SDK logs goes to Python's `logging` at info and above, on loggers under `anvil`, each named for the part of the stack that logged, so `logging.getLogger("anvil")` reaches them all. A record carries the thread, file and line that logged it, and is handled on that thread, never inside a tick of the control loop, so a slow handler cannot delay one. The SDK sets up no handler: an application's logging decides where records go, and one that sets none sees warnings and errors on stderr.

The SDK reports to Anvil: each `anvil.PanicError` as a crash report, and how the SDK is used, a robot started and stopped or an episode recorded, as events naming nothing in the episode, under the serial of a machine Anvil provisioned or an ID made at random for any other. `ANVIL_TELEMETRY=0` in the environment turns both off; set it before importing any `anvil` module, as each reads it once, when it loads.

Classes:

| Name                   | Description                                                                                                       |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `BusyError`            | Raised when what the call needs is held by another program or another call: a bus, a camera, an episode.          |
| `Error`                | Raised by any SDK call that fails: the base of every exception the SDK raises, so catching it catches them all.   |
| `InvalidArgumentError` | Raised when a call refuses a value it was given: out of range, the wrong length, or a file not of the right form. |
| `NotFoundError`        | Raised when a file, directory or device the call names is not there: a path, a camera, a CAN interface.           |
| `OperationError`       | Raised when a call took its arguments but could not carry them out, for a reason no subclass names.               |
| `PanicError`           | Raised when the SDK hit a bug of its own, in this call or in an earlier one on the same object.                   |
| `TimedOutError`        | Raised when what the call waited on did not happen in the time it was given.                                      |
| `UnreachableError`     | Raised when a peer the call needs did not answer: a motor silent on its bus, no sim server at the address.        |

Attributes:

| Name          | Type | Description                                                                                          |
| ------------- | ---- | ---------------------------------------------------------------------------------------------------- |
| `__all__`     |      |                                                                                                      |
| `__version__` |      | The release this SDK is, e.g. 2.0.0-a5: its libraries' version, as the installed package carries it. |

### __all__

```
__all__ = [
    "BusyError",
    "Error",
    "InvalidArgumentError",
    "NotFoundError",
    "OperationError",
    "PanicError",
    "TimedOutError",
    "UnreachableError",
]
```

### __version__

```
__version__ = '2.0.0-b2'
```

The release this SDK is, e.g. `2.0.0-a5`: its libraries' version, as the installed package carries it.

### BusyError

Bases: `OperationError`

Raised when what the call needs is held by another program or another call: a bus, a camera, an episode.

Nothing was taken. Call again once the holder lets go: close the object that holds it, or stop the program that does; an episode is held until its recording stops.

### Error

Bases: `Exception`

Raised by any SDK call that fails: the base of every exception the SDK raises, so catching it catches them all.

Each subclass is also the builtin a Python caller expects for its kind, so `except ValueError` catches a refused value and `except FileNotFoundError` a missing file, as anywhere else. Its message, `str(error)`, says what failed and the value it saw; like any Python exception it has no `message` attribute.

### InvalidArgumentError

Bases: `Error`, `ValueError`

Raised when a call refuses a value it was given: out of range, the wrong length, or a file not of the right form.

The call stopped before acting on it, so the object it was made on is as it was: correct the value and call again. It is a `ValueError`.

### NotFoundError

```
NotFoundError(message: str)
```

Bases: `OperationError`, `FileNotFoundError`

Raised when a file, directory or device the call names is not there: a path, a camera, a CAN interface.

Nothing was opened: fix the path, or plug the device in, and call again. It is a `FileNotFoundError`, its `errno` `errno.ENOENT` and its `filename` `None`: the path is in the message.

Takes `message` as its text, `str(error)`, and sets the errno a `FileNotFoundError` the OS raised would.

### OperationError

Bases: `Error`, `RuntimeError`

Raised when a call took its arguments but could not carry them out, for a reason no subclass names.

The subclasses name the reasons a caller can act on: `anvil.NotFoundError`, `anvil.BusyError`, `anvil.TimedOutError`, `anvil.UnreachableError`, `anvil.control.HwFaultError`, `anvil.control.StoppedShortError` and `anvil.control.PreemptedError`. This class itself is also what any call on an object raises once it was closed, or once an `anvil.control.Runtime` took it over; make a new one to go on. It is a `RuntimeError`.

### PanicError

Bases: `Error`, `RuntimeError`

Raised when the SDK hit a bug of its own, in this call or in an earlier one on the same object.

The object the call was made on is good only for closing: close it, make a new one, and send Anvil the message. A crash report goes to Anvil as well, unless `ANVIL_TELEMETRY=0`. Any call can raise it, so no call lists it. It is a `RuntimeError`.

### TimedOutError

```
TimedOutError(message: str)
```

Bases: `OperationError`, `TimeoutError`

Raised when what the call waited on did not happen in the time it was given.

Only the wait ended: what it waited on may still be under way, as a trajectory goes on playing past its `timeout_s`. It is a `TimeoutError`, its `errno` `errno.ETIMEDOUT`.

Takes `message` as its text, `str(error)`, and sets the errno a `TimeoutError` the OS raised would.

### UnreachableError

Bases: `OperationError`, `ConnectionError`

Raised when a peer the call needs did not answer: a motor silent on its bus, no sim server at the address.

A call that was opening something opened nothing; on an object already open, such as a `anvil.hw.SimHw` whose sim went away, the object answers nothing more, so build a new one. Check that it is powered, wired and running, for a sim that `anvil-robotics sim` is up at that address, and call again. It is a `ConnectionError`, its `errno` `None`, since no one errno covers a silent motor and a refused connection alike.

# anvil.apps

## anvil.apps

The apps: what turns an operator's intent, or a recorded episode, into commands for a controller.

`QuestTeleop` folds a Meta Quest headset's controller frames into end-effector targets, and `QuestDevice` provisions the headset over USB and watches it. `KeyboardTeleop` does the same from the keys held and the spots clicked on a page. `EpisodeReplay` turns a recorded episode into the commands that play it on the robot again. `GloveTeleop` turns UDCap gloves into the six angles of the RealHand L6 on each side, which the caller sends with `anvil.hw.RealHand`.

None of them runs a loop or touches hardware: the caller polls the app at its own pace, sends what it hands back to the active controller with `anvil.control.Runtime.send_command`, and polls again. Each app is released when its object is collected.

Classes:

| Name                | Description                                                                                                     |
| ------------------- | --------------------------------------------------------------------------------------------------------------- |
| `QuestHand`         | One of the two physical Quest controllers, as it drives an arm or plays a cue; both arms may name the same.     |
| `QuestCue`          | Something the Quest controllers are made to feel, named as its QuestHaptics field is.                           |
| `QuestMotionFrame`  | The frame the Quest controllers' motion is read in.                                                             |
| `QuestArmState`     | What one arm is doing under the Quest app.                                                                      |
| `QuestAction`       | What a Quest button does when it fires, as QuestConfig.bind takes it.                                           |
| `QuestBoundAction`  | One of the six Quest actions that carry an argument, built with it, as QuestConfig.bind takes it.               |
| `QuestHaptic`       | What one Quest controller feels for a cue that happens once: a burst of count pulses.                           |
| `QuestHeldHaptic`   | What one Quest controller feels for a cue that lasts: a burst when it starts, repeated while it lasts.          |
| `QuestHands`        | One value for each Quest controller.                                                                            |
| `QuestContactRange` | How far from its path an arm must stand for QuestCue.CONTACT to be felt, and to be felt in full.                |
| `QuestHaptics`      | What the Quest controllers are made to feel, each QuestCue set apart for each controller.                       |
| `QuestVelocity`     | A Quest controller's velocity in the headset's stage frame, as the headset estimates it from its own sensors.   |
| `QuestConfig`       | Everything the Quest app is told, which QuestTeleop takes on construction and through QuestTeleop.config.       |
| `QuestFired`        | One action a Quest button asked for since the last poll, as QuestAppOutput.fired lists it.                      |
| `QuestCheckOutcome` | How one headset check came out.                                                                                 |
| `QuestCheckStatus`  | What one headset check last reported, as QuestDevice.status lists it.                                           |
| `QuestBatteries`    | Each Quest battery's charge as last read, from QuestDevice.batteries.                                           |
| `QuestAbout`        | What the headset says it is, from QuestDevice.about.                                                            |
| `QuestDeviceConfig` | How QuestDevice reaches the headset over adb and sets it up.                                                    |
| `QuestAppOutput`    | What one QuestTeleop.poll returns: the command to send, what each arm is doing, and the headset's newest frame. |
| `KeyboardAction`    | What a key does under KeyboardTeleop.                                                                           |
| `KeyboardConfig`    | Everything the keyboard app is told, which KeyboardTeleop takes on construction and through its config.         |
| `KeyboardAppOutput` | What one KeyboardTeleop.poll returns: the command to send, the arm the keys drive, and what each arm is doing.  |
| `QuestTeleop`       | The Quest teleop app: a Meta Quest headset's controller frames turned into end-effector targets for ee_pose.    |
| `QuestDevice`       | The Quest headset's lifecycle over USB, watched on a thread of its own.                                         |
| `KeyboardTeleop`    | The keyboard app: keys held and spots clicked, driving a robot's arms one at a time through ee_pose.            |
| `ReplayMode`        | What of an episode EpisodeReplay plays back.                                                                    |
| `ReplayOverrides`   | What a ReplayMode.COMMANDS replay does with each command's tuning overrides and IK parameters.                  |
| `ReplayPhase`       | Where along its timeline an EpisodeReplay is.                                                                   |
| `ReplayPoll`        | What one EpisodeReplay.poll returns: the controller to have active, and the command due now.                    |
| `EpisodeReplay`     | A recorded episode played on the robot again, polled for the controller to have active and the command due.     |
| `GlovePose`         | One pose the wearer holds while a glove is calibrated.                                                          |
| `GloveProcedure`    | How the gloves are paired and calibrated, as the SDK runs them. Get one from glove_procedure.                   |
| `GloveCalibration`  | One glove's calibration: each of its twelve bend sensors' raw reading with the hand flat, and bent.             |
| `GloveSlot`         | One slot of the glove receiver, and the glove paired into it. Get them from GloveTeleop.slots.                  |
| `GloveHandPoll`     | One hand's part of a GloveTeleop.poll: what its glove sent since the last poll, and what that made of it.       |
| `GloveTeleop`       | UDCap gloves, read over their USB receiver, to the six angles of the RealHand L6 on each side.                  |

Functions:

| Name              | Description                                                                                               |
| ----------------- | --------------------------------------------------------------------------------------------------------- |
| `quest_streamer`  | Returns the headset streamer app this package ships, as QuestDeviceConfig.apk takes it.                   |
| `glove_procedure` | Returns how the gloves are paired and calibrated: the poses, how long each is held, and the pairing wait. |

Attributes:

| Name            | Type              | Description                                                                                           |
| --------------- | ----------------- | ----------------------------------------------------------------------------------------------------- |
| `QUEST_BUTTONS` |                   | The seven Quest buttons a binding may name, by the names QuestConfig.bind takes.                      |
| `KEYBOARD_KEYS` | `tuple[str, ...]` | Every key a keyboard binding may name, by the names KeyboardConfig.bind and KeyboardTeleop.hold take. |

### QUEST_BUTTONS

```
QUEST_BUTTONS = (
    "left_x",
    "left_y",
    "left_menu",
    "left_thumbstick",
    "right_a",
    "right_b",
    "right_thumbstick",
)
```

The seven Quest buttons a binding may name, by the names `QuestConfig.bind` takes.

They are `left_x`, `left_y`, `left_menu`, `left_thumbstick`, `right_a`, `right_b` and `right_thumbstick`. The headset's system menu takes `left_menu` on some headset builds; bind what you need to another.

### KEYBOARD_KEYS

```
KEYBOARD_KEYS: tuple[str, ...] = (
    "a",
    "b",
    "c",
    "d",
    "e",
    "f",
    "g",
    "h",
    "i",
    "j",
    "k",
    "l",
    "m",
    "n",
    "o",
    "p",
    "q",
    "r",
    "s",
    "t",
    "u",
    "v",
    "w",
    "x",
    "y",
    "z",
    "0",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    "up",
    "down",
    "left",
    "right",
    "shift",
    "space",
    "tab",
    "escape",
    "enter",
    "backspace",
    "[",
    "]",
    ";",
    "'",
    ",",
    ".",
    "/",
    "-",
    "=",
    "`",
    "\\",
)
```

Every key a keyboard binding may name, by the names `KeyboardConfig.bind` and `KeyboardTeleop.hold` take.

They are `a` to `z`, `0` to `9`, `up`, `down`, `left`, `right`, `shift`, `space`, `tab`, `escape`, `enter`, `backspace`, and the punctuation keys by their character.

### QuestHand

Bases: `StrEnum`

One of the two physical Quest controllers, as it drives an arm or plays a cue; both arms may name the same.

Attributes:

| Name    | Type | Description           |
| ------- | ---- | --------------------- |
| `LEFT`  |      | The left controller.  |
| `RIGHT` |      | The right controller. |

#### LEFT

```
LEFT
```

The left controller.

#### RIGHT

```
RIGHT
```

The right controller.

### QuestCue

Bases: `StrEnum`

Something the Quest controllers are made to feel, named as its `QuestHaptics` field is.

`TRACKING_LOST`, `LIMIT` and `CONTACT` last, and the app plays them itself as they happen. The caller plays the other four with `QuestTeleop.play` when it sees them happen.

Attributes:

| Name                | Type | Description                                                                                          |
| ------------------- | ---- | ---------------------------------------------------------------------------------------------------- |
| `TRACKING_LOST`     |      | The headset lost sight of the controller; lasts while it is lost.                                    |
| `LIMIT`             |      | An arm the controller drives stopped short of the hand, at a joint's limit or the edge of its reach. |
| `CONTACT`           |      | Something stopped an arm the controller drives; lasts while it does.                                 |
| `RECORDING_START`   |      | An episode started recording.                                                                        |
| `RECORDING_STOP`    |      | An episode stopped recording.                                                                        |
| `COLLISION_WARNING` |      | The arms came within a collision rule's warning margin.                                              |
| `COLLISION_STOP`    |      | The collision stop held the arms.                                                                    |

#### TRACKING_LOST

```
TRACKING_LOST
```

The headset lost sight of the controller; lasts while it is lost.

#### LIMIT

```
LIMIT
```

An arm the controller drives stopped short of the hand, at a joint's limit or the edge of its reach.

#### CONTACT

```
CONTACT
```

Something stopped an arm the controller drives; lasts while it does.

#### RECORDING_START

```
RECORDING_START
```

An episode started recording.

#### RECORDING_STOP

```
RECORDING_STOP
```

An episode stopped recording.

#### COLLISION_WARNING

```
COLLISION_WARNING
```

The arms came within a collision rule's warning margin.

#### COLLISION_STOP

```
COLLISION_STOP
```

The collision stop held the arms.

### QuestMotionFrame

Bases: `StrEnum`

The frame the Quest controllers' motion is read in.

Attributes:

| Name      | Type | Description                                                                                       |
| --------- | ---- | ------------------------------------------------------------------------------------------------- |
| `GRIPPER` |      | A motion along the controller's own axes moves the gripper along its own axes.                    |
| `WORLD`   |      | Stage up, forward and right move the robot along its world axes, whichever way the gripper faces. |

#### GRIPPER

```
GRIPPER
```

A motion along the controller's own axes moves the gripper along its own axes.

Once the two have turned apart across a declutch, "up" lands wherever the gripper's matching axis points.

#### WORLD

```
WORLD
```

Stage up, forward and right move the robot along its world axes, whichever way the gripper faces.

Which way is forward is set by `QuestTeleop.recentre`.

### QuestArmState

Bases: `StrEnum`

What one arm is doing under the Quest app.

Attributes:

| Name        | Type | Description                                                                      |
| ----------- | ---- | -------------------------------------------------------------------------------- |
| `IDLE`      |      | The clutch is open, and the arm holds its last target.                           |
| `ANCHORING` |      | The poll the clutch closed on: the chain re-anchored to the arm's measured pose. |
| `DRIVING`   |      | Following the controller.                                                        |
| `RECLUTCH`  |      | Latched after a tracking loss or QuestTeleop.reset, refusing to move.            |

#### IDLE

```
IDLE
```

The clutch is open, and the arm holds its last target.

#### ANCHORING

```
ANCHORING
```

The poll the clutch closed on: the chain re-anchored to the arm's measured pose.

#### DRIVING

```
DRIVING
```

Following the controller.

#### RECLUTCH

```
RECLUTCH
```

Latched after a tracking loss or `QuestTeleop.reset`, refusing to move.

Nothing moves until the clutch is released and the controller has been tracked without a break for `QuestConfig.tracking_settle_s`.

### QuestAction

Bases: `StrEnum`

What a Quest button does when it fires, as `QuestConfig.bind` takes it.

The app does `recentre`, `toggle_motion_frame`, `scale`, `set_scale`, `toggle_velocity`, `toggle_rigid` and `toggle_axis_lock` itself. `rehome`, `record_start`, `record_stop`, `mark_success`, `mark_failure`, `annotate` and `switch_profile` it only reports, in `QuestAppOutput.fired`, for the caller to act on: no app re-homes a robot, records, labels or annotates an episode or retunes a controller.

The six that carry an argument are built by the static methods of the same name. The other eight are the members themselves, and each has a static method too, so a binding table reads the same either way.

Example

```
config = QuestConfig()
config.bind(button="left_x", action=QuestAction.recentre(hand="left"))
config.bind(button="left_y", action=QuestAction.rehome(), held=True)
```

Methods:

| Name                  | Description                                                                                             |
| --------------------- | ------------------------------------------------------------------------------------------------------- |
| `recentre`            | Builds the action that takes hand's controller heading as the robot's forward.                          |
| `toggle_motion_frame` | Returns TOGGLE_MOTION_FRAME, which switches between the frames the controllers' motion is read in.      |
| `scale`               | Builds the action that multiplies the reach, QuestConfig.position_scale, by multiply_by.                |
| `set_scale`           | Builds the action that sets the reach, QuestConfig.position_scale, to to.                               |
| `toggle_velocity`     | Returns TOGGLE_VELOCITY, which switches QuestConfig.velocity_ff on or off.                              |
| `toggle_rigid`        | Returns TOGGLE_RIGID, which locks the two arms together at the transform between them, or lets them go. |
| `toggle_axis_lock`    | Builds the action that puts lock on arms, or takes it off them when every one already holds it.         |
| `rehome`              | Returns REHOME, reported for the caller to bring the arms home.                                         |
| `record_start`        | Returns RECORD_START, reported for the caller to start recording.                                       |
| `record_stop`         | Returns RECORD_STOP, reported for the caller to stop recording.                                         |
| `mark_success`        | Returns MARK_SUCCESS, reported for the caller to say an episode succeeded.                              |
| `mark_failure`        | Returns MARK_FAILURE, reported for the caller to say an episode failed.                                 |
| `annotate`            | Builds the action reported for the caller to pin the annotation it numbers annotation to now.           |
| `switch_profile`      | Builds the action reported for the caller to switch to the tuning profile it numbers profile.           |

Attributes:

| Name                  | Type  | Description                                                                                                  |
| --------------------- | ----- | ------------------------------------------------------------------------------------------------------------ |
| `RECENTRE`            |       | A controller's heading taken as the robot's forward; built with the controller by recentre.                  |
| `TOGGLE_MOTION_FRAME` |       | A switch between the two frames the controllers' motion is read in.                                          |
| `SCALE`               |       | The reach multiplied by a factor; built with the factor by scale.                                            |
| `SET_SCALE`           |       | The reach set to a preset; built with the preset by set_scale.                                               |
| `TOGGLE_VELOCITY`     |       | QuestConfig.velocity_ff switched on or off.                                                                  |
| `TOGGLE_RIGID`        |       | QuestConfig.rigid switched on or off.                                                                        |
| `TOGGLE_AXIS_LOCK`    |       | An axis lock put on arms or taken off them; built with the arms and the lock by toggle_axis_lock.            |
| `REHOME`              |       | Reported for the caller to act on: bring the arms home.                                                      |
| `RECORD_START`        |       | Reported for the caller to act on: start recording.                                                          |
| `RECORD_STOP`         |       | Reported for the caller to act on: stop recording.                                                           |
| `MARK_SUCCESS`        |       | Reported for the caller to act on: say the episode went as its task asks.                                    |
| `MARK_FAILURE`        |       | Reported for the caller to act on: say the episode did not go as its task asks.                              |
| `ANNOTATE`            |       | Reported for the caller to act on: pin an annotation to the episode being recorded; built with its number by |
| `SWITCH_PROFILE`      |       | Reported for the caller to act on: switch to a tuning profile; built with its number by switch_profile.      |
| `name`                | `str` | The action's name as a binding table and a log spell it, such as recentre or toggle_motion_frame.            |

#### RECENTRE

```
RECENTRE
```

A controller's heading taken as the robot's forward; built with the controller by `recentre`.

#### TOGGLE_MOTION_FRAME

```
TOGGLE_MOTION_FRAME
```

A switch between the two frames the controllers' motion is read in.

#### SCALE

```
SCALE
```

The reach multiplied by a factor; built with the factor by `scale`.

#### SET_SCALE

```
SET_SCALE
```

The reach set to a preset; built with the preset by `set_scale`.

#### TOGGLE_VELOCITY

```
TOGGLE_VELOCITY
```

`QuestConfig.velocity_ff` switched on or off.

#### TOGGLE_RIGID

```
TOGGLE_RIGID
```

`QuestConfig.rigid` switched on or off.

#### TOGGLE_AXIS_LOCK

```
TOGGLE_AXIS_LOCK
```

An axis lock put on arms or taken off them; built with the arms and the lock by `toggle_axis_lock`.

#### REHOME

```
REHOME
```

Reported for the caller to act on: bring the arms home.

#### RECORD_START

```
RECORD_START
```

Reported for the caller to act on: start recording.

#### RECORD_STOP

```
RECORD_STOP
```

Reported for the caller to act on: stop recording.

#### MARK_SUCCESS

```
MARK_SUCCESS
```

Reported for the caller to act on: say the episode went as its task asks.

#### MARK_FAILURE

```
MARK_FAILURE
```

Reported for the caller to act on: say the episode did not go as its task asks.

#### ANNOTATE

```
ANNOTATE
```

Reported for the caller to act on: pin an annotation to the episode being recorded; built with its number by `annotate`.

#### SWITCH_PROFILE

```
SWITCH_PROFILE
```

Reported for the caller to act on: switch to a tuning profile; built with its number by `switch_profile`.

#### name

```
name: str
```

The action's name as a binding table and a log spell it, such as `recentre` or `toggle_motion_frame`.

This is the member's value, not its Python name: `QuestAction.REHOME.name` is `rehome`.

#### recentre

```
recentre(hand: QuestHand | str) -> QuestBoundAction
```

Builds the action that takes `hand`'s controller heading as the robot's forward.

The heading is the way that controller points, flattened to the floor; it need not be the controller whose button fired. Only `QuestMotionFrame.WORLD` reads it. `QuestTeleop.recentre` does the same on a call.

Parameters:

| Name   | Type               | Description                            | Default    |
| ------ | ------------------ | -------------------------------------- | ---------- |
| `hand` | `QuestHand \| str` | The controller whose heading is taken. | *required* |

Raises:

| Type         | Description                           |
| ------------ | ------------------------------------- |
| `ValueError` | hand is not a QuestHand or its value. |

#### toggle_motion_frame

```
toggle_motion_frame() -> QuestAction
```

Returns `TOGGLE_MOTION_FRAME`, which switches between the frames the controllers' motion is read in.

#### scale

```
scale(multiply_by: float) -> QuestBoundAction
```

Builds the action that multiplies the reach, `QuestConfig.position_scale`, by `multiply_by`.

The product is held inside the range `position_scale` allows, 0.1 to 5. 1.25 and 0.8 are one step either way and undo each other exactly.

Parameters:

| Name          | Type    | Description                                                                                                                | Default    |
| ------------- | ------- | -------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `multiply_by` | `float` | The factor. One that is not positive and finite is refused when the configuration is validated or handed to a QuestTeleop. | *required* |

#### set_scale

```
set_scale(to: float) -> QuestBoundAction
```

Builds the action that sets the reach, `QuestConfig.position_scale`, to `to`.

A preset one press returns to, wherever `scale` steps have taken the reach: bind 1, 2 and 3 to three buttons to switch between them.

Parameters:

| Name | Type    | Description                                                                                                                                  | Default    |
| ---- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `to` | `float` | The reach. One outside the range position_scale allows, 0.1 to 5, is refused when the configuration is validated or handed to a QuestTeleop. | *required* |

#### toggle_velocity

```
toggle_velocity() -> QuestAction
```

Returns `TOGGLE_VELOCITY`, which switches `QuestConfig.velocity_ff` on or off.

#### toggle_rigid

```
toggle_rigid() -> QuestAction
```

Returns `TOGGLE_RIGID`, which locks the two arms together at the transform between them, or lets them go.

#### toggle_axis_lock

```
toggle_axis_lock(arms: str, lock: str) -> QuestBoundAction
```

Builds the action that puts `lock` on `arms`, or takes it off them when every one already holds it.

Neither is checked here: an arm or a lock nothing is named is refused when the configuration is validated or handed to a `QuestTeleop`.

Parameters:

| Name   | Type  | Description                                                                                           | Default    |
| ------ | ----- | ----------------------------------------------------------------------------------------------------- | ---------- |
| `arms` | `str` | left, right or both.                                                                                  | *required* |
| `lock` | `str` | The axes the arms may move along, spelled as QuestConfig.axis_locks spells a lock, such as gripper:z. | *required* |

#### rehome

```
rehome() -> QuestAction
```

Returns `REHOME`, reported for the caller to bring the arms home.

#### record_start

```
record_start() -> QuestAction
```

Returns `RECORD_START`, reported for the caller to start recording.

#### record_stop

```
record_stop() -> QuestAction
```

Returns `RECORD_STOP`, reported for the caller to stop recording.

#### mark_success

```
mark_success() -> QuestAction
```

Returns `MARK_SUCCESS`, reported for the caller to say an episode succeeded.

#### mark_failure

```
mark_failure() -> QuestAction
```

Returns `MARK_FAILURE`, reported for the caller to say an episode failed.

#### annotate

```
annotate(annotation: int) -> QuestBoundAction
```

Builds the action reported for the caller to pin the annotation it numbers `annotation` to now.

The caller decides what each number says, a key and a value, and pins it with `anvil.recorder.Recorder.annotate`; the app only reports it.

Parameters:

| Name         | Type  | Description                                                                                                        | Default    |
| ------------ | ----- | ------------------------------------------------------------------------------------------------------------------ | ---------- |
| `annotation` | `int` | The annotation's number. A negative one is refused when the configuration is validated or handed to a QuestTeleop. | *required* |

#### switch_profile

```
switch_profile(profile: int) -> QuestBoundAction
```

Builds the action reported for the caller to switch to the tuning profile it numbers `profile`.

The caller decides what each number means; the app only reports it.

Parameters:

| Name      | Type  | Description                                                                                                     | Default    |
| --------- | ----- | --------------------------------------------------------------------------------------------------------------- | ---------- |
| `profile` | `int` | The profile's number. A negative one is refused when the configuration is validated or handed to a QuestTeleop. | *required* |

### QuestBoundAction

```
QuestBoundAction(
    action: QuestAction,
    argument: QuestHand | float | int | str,
)
```

One of the six Quest actions that carry an argument, built with it, as `QuestConfig.bind` takes it.

`QuestAction.recentre`, `QuestAction.scale`, `QuestAction.set_scale`, `QuestAction.toggle_axis_lock`, `QuestAction.annotate` and `QuestAction.switch_profile` return one, and `QuestConfig.bindings`, `QuestConfig.holds` and `QuestFired.action` hold one where such an action is bound. Its `name` and `str` are the action's own, the way the eight that carry nothing read, and the argument goes out beside it in the binding table: `recentre:left`, `scale:1.25`, `set_scale:2.0`, `annotate:2`, `switch_profile:3`, `toggle_axis_lock:left:gripper:z`.

Attributes:

| Name       | Type                               | Description                                                                                   |
| ---------- | ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `action`   | `QuestAction`                      | The action, one of RECENTRE, SCALE, SET_SCALE, TOGGLE_AXIS_LOCK, ANNOTATE and SWITCH_PROFILE. |
| `argument` | `QuestHand \| float \| int \| str` | recentre's hand, scale's factor, set_scale's reach, annotate's or switch_profile's number, or |
| `name`     | `str`                              | The action's name as a binding table and a log spell it, such as recentre or scale.           |

#### action

```
action: QuestAction
```

The action, one of `RECENTRE`, `SCALE`, `SET_SCALE`, `TOGGLE_AXIS_LOCK`, `ANNOTATE` and `SWITCH_PROFILE`.

#### argument

```
argument: QuestHand | float | int | str
```

`recentre`'s hand, `scale`'s factor, `set_scale`'s reach, `annotate`'s or `switch_profile`'s number, or `toggle_axis_lock`'s arms and lock.

#### name

```
name: str
```

The action's name as a binding table and a log spell it, such as `recentre` or `scale`.

### QuestHaptic

```
QuestHaptic(
    enabled: bool,
    amplitude: float,
    duration_s: float,
    count: int,
    gap_s: float,
)
```

What one Quest controller feels for a cue that happens once: a burst of `count` pulses.

Its numbers are checked when the configuration is validated or handed to a `QuestTeleop`, enabled or not.

Attributes:

| Name         | Type    | Description                                                           |
| ------------ | ------- | --------------------------------------------------------------------- |
| `enabled`    | `bool`  | Whether it is felt; off keeps the rest as set, for switching back on. |
| `amplitude`  | `float` | How hard each pulse is, 0 to 1.                                       |
| `duration_s` | `float` | How long each pulse lasts, in seconds; more than zero.                |
| `count`      | `int`   | How many pulses in the burst, at least one.                           |
| `gap_s`      | `float` | From one pulse ending to the next starting, in seconds.               |

#### enabled

```
enabled: bool
```

Whether it is felt; off keeps the rest as set, for switching back on.

#### amplitude

```
amplitude: float
```

How hard each pulse is, 0 to 1.

#### duration_s

```
duration_s: float
```

How long each pulse lasts, in seconds; more than zero.

#### count

```
count: int
```

How many pulses in the burst, at least one.

#### gap_s

```
gap_s: float
```

From one pulse ending to the next starting, in seconds.

### QuestHeldHaptic

```
QuestHeldHaptic(
    enabled: bool,
    amplitude: float,
    duration_s: float,
    count: int,
    gap_s: float,
    repeat_s: float | None,
)
```

What one Quest controller feels for a cue that lasts: a burst when it starts, repeated while it lasts.

A single pulse longer than its repeat is one unbroken rumble. Its numbers are checked when the configuration is validated or handed to a `QuestTeleop`, enabled or not.

Attributes:

| Name         | Type            | Description                                                                                                |
| ------------ | --------------- | ---------------------------------------------------------------------------------------------------------- |
| `enabled`    | `bool`          | Whether it is felt; off keeps the rest as set, for switching back on.                                      |
| `amplitude`  | `float`         | How hard each pulse is, 0 to 1.                                                                            |
| `duration_s` | `float`         | How long each pulse lasts, in seconds; more than zero.                                                     |
| `count`      | `int`           | How many pulses in a burst, at least one.                                                                  |
| `gap_s`      | `float`         | From one pulse ending to the next starting, in seconds.                                                    |
| `repeat_s`   | `float \| None` | From one burst's start to the next while the cue lasts, in seconds and more than zero; None plays it once. |

#### enabled

```
enabled: bool
```

Whether it is felt; off keeps the rest as set, for switching back on.

#### amplitude

```
amplitude: float
```

How hard each pulse is, 0 to 1.

#### duration_s

```
duration_s: float
```

How long each pulse lasts, in seconds; more than zero.

#### count

```
count: int
```

How many pulses in a burst, at least one.

#### gap_s

```
gap_s: float
```

From one pulse ending to the next starting, in seconds.

#### repeat_s

```
repeat_s: float | None
```

From one burst's start to the next while the cue lasts, in seconds and more than zero; `None` plays it once.

### QuestHands

```
QuestHands(left: T, right: T)
```

One value for each Quest controller.

Attributes:

| Name    | Type | Description             |
| ------- | ---- | ----------------------- |
| `left`  | `T`  | The left controller's.  |
| `right` | `T`  | The right controller's. |

#### left

```
left: T
```

The left controller's.

#### right

```
right: T
```

The right controller's.

### QuestContactRange

```
QuestContactRange(deadband_m: float, full_m: float)
```

How far from its path an arm must stand for `QuestCue.CONTACT` to be felt, and to be felt in full.

The distance is between a driven arm that something stops and the path it was told along over the last moments. An arm anywhere on that path is trailing its target, which is lag, not contact.

Attributes:

| Name         | Type    | Description                                                              |
| ------------ | ------- | ------------------------------------------------------------------------ |
| `deadband_m` | `float` | Nothing is felt this close, in meters; zero or more.                     |
| `full_m`     | `float` | Full strength from this far and beyond, in meters; more than deadband_m. |

#### deadband_m

```
deadband_m: float
```

Nothing is felt this close, in meters; zero or more.

#### full_m

```
full_m: float
```

Full strength from this far and beyond, in meters; more than `deadband_m`.

### QuestHaptics

```
QuestHaptics(
    tracking_lost: QuestHands[QuestHeldHaptic],
    limit: QuestHands[QuestHeldHaptic],
    contact: QuestHands[QuestHeldHaptic],
    contact_range: QuestContactRange,
    recording_start: QuestHands[QuestHaptic],
    recording_stop: QuestHands[QuestHaptic],
    collision_warning: QuestHands[QuestHaptic],
    collision_stop: QuestHands[QuestHaptic],
)
```

What the Quest controllers are made to feel, each `QuestCue` set apart for each controller.

Felt only through a headset streamer of 1.3.0 or newer.

Attributes:

| Name                | Type                          | Description                                                                                              |
| ------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------- |
| `tracking_lost`     | `QuestHands[QuestHeldHaptic]` | While the headset has lost sight of the controller.                                                      |
| `limit`             | `QuestHands[QuestHeldHaptic]` | While an arm the controller drives stops short of the hand, at a joint's limit or the edge of its reach. |
| `contact`           | `QuestHands[QuestHeldHaptic]` | While something stops an arm the controller drives; the amplitude is the strength at full contact.       |
| `contact_range`     | `QuestContactRange`           | How far from its path a stopped arm must stand for contact to be felt, and to be felt in full.           |
| `recording_start`   | `QuestHands[QuestHaptic]`     | When an episode starts recording; the caller plays it with QuestTeleop.play.                             |
| `recording_stop`    | `QuestHands[QuestHaptic]`     | When an episode stops recording; the caller plays it with QuestTeleop.play.                              |
| `collision_warning` | `QuestHands[QuestHaptic]`     | When the arms come within a collision rule's warning margin; the caller plays it with QuestTeleop.play.  |
| `collision_stop`    | `QuestHands[QuestHaptic]`     | When the collision stop holds the arms; the caller plays it with QuestTeleop.play.                       |

#### tracking_lost

```
tracking_lost: QuestHands[QuestHeldHaptic]
```

While the headset has lost sight of the controller.

#### limit

```
limit: QuestHands[QuestHeldHaptic]
```

While an arm the controller drives stops short of the hand, at a joint's limit or the edge of its reach.

#### contact

```
contact: QuestHands[QuestHeldHaptic]
```

While something stops an arm the controller drives; the amplitude is the strength at full contact.

#### contact_range

```
contact_range: QuestContactRange
```

How far from its path a stopped arm must stand for `contact` to be felt, and to be felt in full.

#### recording_start

```
recording_start: QuestHands[QuestHaptic]
```

When an episode starts recording; the caller plays it with `QuestTeleop.play`.

#### recording_stop

```
recording_stop: QuestHands[QuestHaptic]
```

When an episode stops recording; the caller plays it with `QuestTeleop.play`.

#### collision_warning

```
collision_warning: QuestHands[QuestHaptic]
```

When the arms come within a collision rule's warning margin; the caller plays it with `QuestTeleop.play`.

#### collision_stop

```
collision_stop: QuestHands[QuestHaptic]
```

When the collision stop holds the arms; the caller plays it with `QuestTeleop.play`.

### QuestVelocity

```
QuestVelocity(
    linear_mps: list[float], angular_radps: list[float]
)
```

A Quest controller's velocity in the headset's stage frame, as the headset estimates it from its own sensors.

Attributes:

| Name            | Type          | Description                                                                |
| --------------- | ------------- | -------------------------------------------------------------------------- |
| `linear_mps`    | `list[float]` | Linear velocity along the stage's x, y and z axes, in meters per second.   |
| `angular_radps` | `list[float]` | Angular velocity about the stage's x, y and z axes, in radians per second. |

#### linear_mps

```
linear_mps: list[float]
```

Linear velocity along the stage's x, y and z axes, in meters per second.

#### angular_radps

```
angular_radps: list[float]
```

Angular velocity about the stage's x, y and z axes, in radians per second.

### QuestConfig

```
QuestConfig(address: str = '127.0.0.1:8081', left_arm: QuestHand = QuestHand.LEFT, right_arm: QuestHand = QuestHand.RIGHT, engage_above: float = 0.75, disengage_below: float = 0.25, tracking_settle_s: float = 0.5, link_timeout_s: float = 0.5, pivot_m: float = -0.05, gripper_from_controller: list[list[float]] = [[0.0, -1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, -1.0]], world_from_stage: list[list[float]] = [[0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], grip_open_m: float = 0.1, grip_closed_m: float = 0.0, grip_trigger_exponent: float = 0.75, motion_frame: QuestMotionFrame = QuestMotionFrame.GRIPPER, position_scale: float = 1.0, velocity_ff: bool = False, rigid: bool = False, axis_locks: list[str | None] = [None, None], bindings: dict[str, _Bound] = {'left_menu': <QuestAction.TOGGLE_MOTION_FRAME: 'toggle_motion_frame'>, 'left_thumbstick': QuestBoundAction(action=<QuestAction.SCALE: 'scale'>, argument=0.8), 'left_x': QuestBoundAction(action=<QuestAction.RECENTRE: 'recentre'>, argument=<QuestHand.LEFT: 'left'>), 'left_y': <QuestAction.REHOME: 'rehome'>, 'right_a': <QuestAction.RECORD_START: 'record_start'>, 'right_b': <QuestAction.RECORD_STOP: 'record_stop'>, 'right_thumbstick': QuestBoundAction(action=<QuestAction.SCALE: 'scale'>, argument=1.25)}, holds: dict[str, _Bound] = {}, hold_s: float = 0.5, haptics: QuestHaptics = QuestHaptics(tracking_lost=QuestHands(left=QuestHeldHaptic(enabled=True, amplitude=0.5, duration_s=0.15, count=1, gap_s=0.0, repeat_s=None), right=QuestHeldHaptic(enabled=True, amplitude=0.5, duration_s=0.15, count=1, gap_s=0.0, repeat_s=None)), limit=QuestHands(left=QuestHeldHaptic(enabled=False, amplitude=1.0, duration_s=0.04, count=1, gap_s=0.0, repeat_s=None), right=QuestHeldHaptic(enabled=False, amplitude=1.0, duration_s=0.04, count=1, gap_s=0.0, repeat_s=None)), contact=QuestHands(left=QuestHeldHaptic(enabled=False, amplitude=1.0, duration_s=0.1, count=1, gap_s=0.0, repeat_s=0.05), right=QuestHeldHaptic(enabled=False, amplitude=1.0, duration_s=0.1, count=1, gap_s=0.0, repeat_s=0.05)), contact_range=QuestContactRange(deadband_m=0.015, full_m=0.06), recording_start=QuestHands(left=QuestHaptic(enabled=False, amplitude=0.8, duration_s=0.06, count=2, gap_s=0.09), right=QuestHaptic(enabled=False, amplitude=0.8, duration_s=0.06, count=2, gap_s=0.09)), recording_stop=QuestHands(left=QuestHaptic(enabled=False, amplitude=0.8, duration_s=0.06, count=2, gap_s=0.09), right=QuestHaptic(enabled=False, amplitude=0.8, duration_s=0.06, count=2, gap_s=0.09)), collision_warning=QuestHands(left=QuestHaptic(enabled=False, amplitude=1.0, duration_s=0.3, count=1, gap_s=0.0), right=QuestHaptic(enabled=False, amplitude=1.0, duration_s=0.3, count=1, gap_s=0.0)), collision_stop=QuestHands(left=QuestHaptic(enabled=False, amplitude=1.0, duration_s=0.3, count=1, gap_s=0.0), right=QuestHaptic(enabled=False, amplitude=1.0, duration_s=0.3, count=1, gap_s=0.0))))
```

Everything the Quest app is told, which `QuestTeleop` takes on construction and through `QuestTeleop.config`.

It says where the headset connects, how the clutch and the gripper read, the frame motion is read in, how far a motion carries, what every button does, and what the controllers feel. `QuestConfig()` is the OpenArm setup and `shipped` picks one by arm family; assign the fields that differ.

`validate` checks a configuration alone. Constructing a `QuestTeleop` or assigning its `config` checks it again, with what only an app can judge, and the buttons write the same fields while the app runs.

Methods:

| Name        | Description                                                                           |
| ----------- | ------------------------------------------------------------------------------------- |
| `shipped`   | Returns the Quest setup shipped for an arm family.                                    |
| `validate`  | Checks this configuration on its own, raising on what QuestTeleop would refuse in it. |
| `bind`      | Binds button to action, or to nothing when action is None.                            |
| `to_json`   | Returns this configuration as JSON, the form from_json reads back.                    |
| `from_json` | Builds a configuration from the JSON to_json wrote.                                   |

Attributes:

| Name                      | Type                | Description                                                                                                   |
| ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
| `address`                 | `str`               | Where the headset connects, host:port.                                                                        |
| `left_arm`                | `QuestHand`         | The controller that drives the left arm of a two-arm cell; a one-arm cell is driven by whichever clutches in. |
| `right_arm`               | `QuestHand`         | The controller that drives the right arm of a two-arm cell.                                                   |
| `engage_above`            | `float`             | The side trigger's reading, 0 to 1, above which the clutch closes.                                            |
| `disengage_below`         | `float`             | The side trigger's reading, 0 to 1, below which the clutch opens; between the two it keeps its state.         |
| `tracking_settle_s`       | `float`             | How long a controller must be tracked without a break before a latched arm may clutch in again, in seconds.   |
| `link_timeout_s`          | `float`             | How long without a frame from the headset before every arm latches, in seconds.                               |
| `pivot_m`                 | `float`             | Distance along the controller's z axis to the point its motion pivots about, in meters.                       |
| `gripper_from_controller` | `list[list[float]]` | Controller axes to gripper axes, a 3 by 3 rotation as a list of its rows.                                     |
| `world_from_stage`        | `list[list[float]]` | Headset stage axes to the robot's world axes, a 3 by 3 rotation as a list of its rows.                        |
| `grip_open_m`             | `float`             | Gripper opening commanded with the index trigger released, in meters.                                         |
| `grip_closed_m`           | `float`             | Gripper opening commanded with the index trigger fully pulled, in meters.                                     |
| `grip_trigger_exponent`   | `float`             | The power the index trigger's reading, 0 to 1, is raised to before it maps to an opening.                     |
| `motion_frame`            | `QuestMotionFrame`  | The frame the controllers' motion is read in.                                                                 |
| `position_scale`          | `float`             | Multiplier on the controller's translation, 0.1 to 5; rotation is never scaled.                               |
| `velocity_ff`             | `bool`              | Whether each target carries the rate it is moving at as a velocity, so damping does not brake the move.       |
| `rigid`                   | `bool`              | Whether the two arms move as one body; refused on a one-arm cell.                                             |
| `axis_locks`              | `list[str \| None]` | The axes each arm may move along, the left arm's first, None for every axis; a one-arm cell's is the first.   |
| `bindings`                | `dict[str, _Bound]` | What each button does when pressed, keyed by its name in QUEST_BUTTONS; a button left out does nothing.       |
| `holds`                   | `dict[str, _Bound]` | What each button does once held for hold_s, on the same terms as bindings.                                    |
| `hold_s`                  | `float`             | How long a button is held before its hold fires, in seconds; more than zero.                                  |
| `haptics`                 | `QuestHaptics`      | What the controllers are made to feel.                                                                        |

#### address

```
address: str = '127.0.0.1:8081'
```

Where the headset connects, `host:port`.

The headset's own port is tunneled to this machine's loopback over USB, so the host is loopback and the port is `QuestDeviceConfig.port`. Port 0 binds one the machine picks, which `QuestTeleop.config` reads back.

#### left_arm

```
left_arm: QuestHand = QuestHand.LEFT
```

The controller that drives the left arm of a two-arm cell; a one-arm cell is driven by whichever clutches in.

#### right_arm

```
right_arm: QuestHand = QuestHand.RIGHT
```

The controller that drives the right arm of a two-arm cell.

#### engage_above

```
engage_above: float = 0.75
```

The side trigger's reading, 0 to 1, above which the clutch closes.

#### disengage_below

```
disengage_below: float = 0.25
```

The side trigger's reading, 0 to 1, below which the clutch opens; between the two it keeps its state.

#### tracking_settle_s

```
tracking_settle_s: float = 0.5
```

How long a controller must be tracked without a break before a latched arm may clutch in again, in seconds.

#### link_timeout_s

```
link_timeout_s: float = 0.5
```

How long without a frame from the headset before every arm latches, in seconds.

#### pivot_m

```
pivot_m: float = -0.05
```

Distance along the controller's z axis to the point its motion pivots about, in meters.

#### gripper_from_controller

```
gripper_from_controller: list[list[float]] = [
    [0.0, -1.0, 0.0],
    [-1.0, 0.0, 0.0],
    [0.0, 0.0, -1.0],
]
```

Controller axes to gripper axes, a 3 by 3 rotation as a list of its rows.

#### world_from_stage

```
world_from_stage: list[list[float]] = [
    [0.0, 0.0, -1.0],
    [-1.0, 0.0, 0.0],
    [0.0, 1.0, 0.0],
]
```

Headset stage axes to the robot's world axes, a 3 by 3 rotation as a list of its rows.

#### grip_open_m

```
grip_open_m: float = 0.1
```

Gripper opening commanded with the index trigger released, in meters.

#### grip_closed_m

```
grip_closed_m: float = 0.0
```

Gripper opening commanded with the index trigger fully pulled, in meters.

#### grip_trigger_exponent

```
grip_trigger_exponent: float = 0.75
```

The power the index trigger's reading, 0 to 1, is raised to before it maps to an opening.

Below 1, the first part of the pull closes the gripper faster than the last.

#### motion_frame

```
motion_frame: QuestMotionFrame = QuestMotionFrame.GRIPPER
```

The frame the controllers' motion is read in.

#### position_scale

```
position_scale: float = 1.0
```

Multiplier on the controller's translation, 0.1 to 5; rotation is never scaled.

#### velocity_ff

```
velocity_ff: bool = False
```

Whether each target carries the rate it is moving at as a velocity, so damping does not brake the move.

#### rigid

```
rigid: bool = False
```

Whether the two arms move as one body; refused on a one-arm cell.

The hand on the clutch drives the arm it always drives, and the other follows at the transform the two had when the lock was taken.

#### axis_locks

```
axis_locks: list[str | None] = [None, None]
```

The axes each arm may move along, the left arm's first, `None` for every axis; a one-arm cell's is the first.

A lock is its frame and the axes it lets through, `gripper:z` or `world:x,y,rz`: `x`, `y` and `z` translate along the frame's axes, `rx`, `ry` and `rz` turn about them, and the gripper's approach is its `z`. A lock that lets no axis through is refused.

#### bindings

```
bindings: dict[str, _Bound] = {'left_menu': <QuestAction.TOGGLE_MOTION_FRAME: 'toggle_motion_frame'>, 'left_thumbstick': QuestBoundAction(action=<QuestAction.SCALE: 'scale'>, argument=0.8), 'left_x': QuestBoundAction(action=<QuestAction.RECENTRE: 'recentre'>, argument=<QuestHand.LEFT: 'left'>), 'left_y': <QuestAction.REHOME: 'rehome'>, 'right_a': <QuestAction.RECORD_START: 'record_start'>, 'right_b': <QuestAction.RECORD_STOP: 'record_stop'>, 'right_thumbstick': QuestBoundAction(action=<QuestAction.SCALE: 'scale'>, argument=1.25)}
```

What each button does when pressed, keyed by its name in `QUEST_BUTTONS`; a button left out does nothing.

#### holds

```
holds: dict[str, _Bound] = {}
```

What each button does once held for `hold_s`, on the same terms as `bindings`.

A button with a hold fires its press on release rather than as it goes down.

#### hold_s

```
hold_s: float = 0.5
```

How long a button is held before its hold fires, in seconds; more than zero.

#### haptics

```
haptics: QuestHaptics = QuestHaptics(
    tracking_lost=QuestHands(
        left=QuestHeldHaptic(
            enabled=True,
            amplitude=0.5,
            duration_s=0.15,
            count=1,
            gap_s=0.0,
            repeat_s=None,
        ),
        right=QuestHeldHaptic(
            enabled=True,
            amplitude=0.5,
            duration_s=0.15,
            count=1,
            gap_s=0.0,
            repeat_s=None,
        ),
    ),
    limit=QuestHands(
        left=QuestHeldHaptic(
            enabled=False,
            amplitude=1.0,
            duration_s=0.04,
            count=1,
            gap_s=0.0,
            repeat_s=None,
        ),
        right=QuestHeldHaptic(
            enabled=False,
            amplitude=1.0,
            duration_s=0.04,
            count=1,
            gap_s=0.0,
            repeat_s=None,
        ),
    ),
    contact=QuestHands(
        left=QuestHeldHaptic(
            enabled=False,
            amplitude=1.0,
            duration_s=0.1,
            count=1,
            gap_s=0.0,
            repeat_s=0.05,
        ),
        right=QuestHeldHaptic(
            enabled=False,
            amplitude=1.0,
            duration_s=0.1,
            count=1,
            gap_s=0.0,
            repeat_s=0.05,
        ),
    ),
    contact_range=QuestContactRange(
        deadband_m=0.015, full_m=0.06
    ),
    recording_start=QuestHands(
        left=QuestHaptic(
            enabled=False,
            amplitude=0.8,
            duration_s=0.06,
            count=2,
            gap_s=0.09,
        ),
        right=QuestHaptic(
            enabled=False,
            amplitude=0.8,
            duration_s=0.06,
            count=2,
            gap_s=0.09,
        ),
    ),
    recording_stop=QuestHands(
        left=QuestHaptic(
            enabled=False,
            amplitude=0.8,
            duration_s=0.06,
            count=2,
            gap_s=0.09,
        ),
        right=QuestHaptic(
            enabled=False,
            amplitude=0.8,
            duration_s=0.06,
            count=2,
            gap_s=0.09,
        ),
    ),
    collision_warning=QuestHands(
        left=QuestHaptic(
            enabled=False,
            amplitude=1.0,
            duration_s=0.3,
            count=1,
            gap_s=0.0,
        ),
        right=QuestHaptic(
            enabled=False,
            amplitude=1.0,
            duration_s=0.3,
            count=1,
            gap_s=0.0,
        ),
    ),
    collision_stop=QuestHands(
        left=QuestHaptic(
            enabled=False,
            amplitude=1.0,
            duration_s=0.3,
            count=1,
            gap_s=0.0,
        ),
        right=QuestHaptic(
            enabled=False,
            amplitude=1.0,
            duration_s=0.3,
            count=1,
            gap_s=0.0,
        ),
    ),
)
```

What the controllers are made to feel.

#### shipped

```
shipped(family: str) -> Self
```

Returns the Quest setup shipped for an arm family.

`openarm` is `QuestConfig()`; `openyam` differs from it only in `pivot_m`.

Parameters:

| Name     | Type  | Description                         | Default    |
| -------- | ----- | ----------------------------------- | ---------- |
| `family` | `str` | The arm family, openarm or openyam. | *required* |

Raises:

| Type                   | Description               |
| ---------------------- | ------------------------- |
| `InvalidArgumentError` | Nothing ships for family. |

#### validate

```
validate() -> None
```

Checks this configuration on its own, raising on what `QuestTeleop` would refuse in it.

What only an app can judge is left to it: whether `address` is the one it bound, a pair lock or a second arm's axis lock on a one-arm cell, and whether the two frames are rotations.

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | engage_above is not under 1, or disengage_below not over 0 and no higher than it; grip_trigger_exponent is not above 0; link_timeout_s is zero; position_scale is outside 0.1 to 5; a bound scale factor is not positive and finite, or a bound profile is negative; an axis lock lets no axis through, or names a frame or an axis that is not one; hold_s is zero; a haptic is one no controller can make; a duration is negative or not finite; a binding names a button that is not one of QUEST_BUTTONS; or address is not host:port, or names a host that resolves to nothing. |

#### bind

```
bind(
    button: str,
    action: _Bound | str | None = None,
    held: bool = False,
) -> None
```

Binds `button` to `action`, or to nothing when `action` is `None`.

Parameters:

| Name     | Type                    | Description                                                                                                                                                   | Default    |
| -------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `button` | `str`                   | The button's name, one of QUEST_BUTTONS.                                                                                                                      | *required* |
| `action` | `_Bound \| str \| None` | A QuestAction or its value, or a QuestBoundAction; None leaves the button doing nothing.                                                                      | `None`     |
| `held`   | `bool`                  | Whether to bind what the button does once held for hold_s rather than its press. A button with a hold fires its press on release rather than as it goes down. | `False`    |

Raises:

| Type                   | Description                            |
| ---------------------- | -------------------------------------- |
| `InvalidArgumentError` | button is not one of QUEST_BUTTONS.    |
| `ValueError`           | action is a string no QuestAction has. |

#### to_json

```
to_json() -> str
```

Returns this configuration as JSON, the form `from_json` reads back.

Raises:

| Type                   | Description                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A binding names a button that is not one of QUEST_BUTTONS, or a duration is negative or not finite. |

#### from_json

```
from_json(wire: str) -> Self
```

Builds a configuration from the JSON `to_json` wrote.

The values are not checked here but when the configuration is validated or handed to a `QuestTeleop`. Text saved before the haptics or the axis locks were settings reads with the shipped ones.

Parameters:

| Name   | Type  | Description                      | Default    |
| ------ | ----- | -------------------------------- | ---------- |
| `wire` | `str` | JSON text, as to_json writes it. | *required* |

Raises:

| Type                   | Description                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------- |
| `ValueError`           | wire is not JSON, or a hand, a motion frame or an action is a string that names none. |
| `InvalidArgumentError` | wire is missing a field to_json writes.                                               |

### QuestFired

```
QuestFired(action: _Bound, held: bool, refused: str | None)
```

One action a Quest button asked for since the last poll, as `QuestAppOutput.fired` lists it.

An action the caller owns, `rehome`, `record_start`, `record_stop`, `mark_success`, `mark_failure`, `annotate` or `switch_profile`, is never refused: the app reports it rather than attempting it.

Attributes:

| Name      | Type          | Description                                                                                          |
| --------- | ------------- | ---------------------------------------------------------------------------------------------------- |
| `action`  | `_Bound`      | What the button was bound to: a QuestAction, or a QuestBoundAction for one that carries an argument. |
| `held`    | `bool`        | Whether it was the button's hold that fired, rather than its press.                                  |
| `refused` | `str \| None` | Why the app did not do it, or None when it did, or when the action is the caller's.                  |

#### action

```
action: _Bound
```

What the button was bound to: a `QuestAction`, or a `QuestBoundAction` for one that carries an argument.

#### held

```
held: bool
```

Whether it was the button's hold that fired, rather than its press.

#### refused

```
refused: str | None
```

Why the app did not do it, or `None` when it did, or when the action is the caller's.

### QuestCheckOutcome

Bases: `StrEnum`

How one headset check came out.

Attributes:

| Name          | Type | Description                                                              |
| ------------- | ---- | ------------------------------------------------------------------------ |
| `UNTESTED`    |      | Not run yet.                                                             |
| `IN_PROGRESS` |      | Still running a slow step its message names, such as installing the app. |
| `PASSED`      |      | Passed.                                                                  |
| `WARNED`      |      | Passed with something to see to, such as a battery under 20 percent.     |
| `FAILED`      |      | Failed; its message says what was found.                                 |

#### UNTESTED

```
UNTESTED
```

Not run yet.

#### IN_PROGRESS

```
IN_PROGRESS
```

Still running a slow step its message names, such as installing the app.

#### PASSED

```
PASSED
```

Passed.

#### WARNED

```
WARNED
```

Passed with something to see to, such as a battery under 20 percent.

#### FAILED

```
FAILED
```

Failed; its message says what was found.

### QuestCheckStatus

```
QuestCheckStatus(
    name: str,
    stage: str,
    outcome: QuestCheckOutcome,
    message: str,
)
```

What one headset check last reported, as `QuestDevice.status` lists it.

Attributes:

| Name      | Type                | Description                                                                       |
| --------- | ------------------- | --------------------------------------------------------------------------------- |
| `name`    | `str`               | How the check reads on an operator's screen, such as Quest USB connected.         |
| `stage`   | `str`               | The stage of provisioning it belongs to: Connected, Set up, Running or Batteries. |
| `outcome` | `QuestCheckOutcome` | How it came out.                                                                  |
| `message` | `str`               | What it found, or what to do about it.                                            |

#### name

```
name: str
```

How the check reads on an operator's screen, such as `Quest USB connected`.

#### stage

```
stage: str
```

The stage of provisioning it belongs to: `Connected`, `Set up`, `Running` or `Batteries`.

#### outcome

```
outcome: QuestCheckOutcome
```

How it came out.

#### message

```
message: str
```

What it found, or what to do about it.

### QuestBatteries

```
QuestBatteries(
    headset_pct: int | None,
    left_pct: int | None,
    right_pct: int | None,
)
```

Each Quest battery's charge as last read, from `QuestDevice.batteries`.

Attributes:

| Name          | Type          | Description                                                                                 |
| ------------- | ------------- | ------------------------------------------------------------------------------------------- |
| `headset_pct` | `int \| None` | The headset's charge, in percent; None before a read, or when the last one failed.          |
| `left_pct`    | `int \| None` | The left controller's charge, in percent; None before a read, or when the last one failed.  |
| `right_pct`   | `int \| None` | The right controller's charge, in percent; None before a read, or when the last one failed. |

#### headset_pct

```
headset_pct: int | None
```

The headset's charge, in percent; `None` before a read, or when the last one failed.

#### left_pct

```
left_pct: int | None
```

The left controller's charge, in percent; `None` before a read, or when the last one failed.

#### right_pct

```
right_pct: int | None
```

The right controller's charge, in percent; `None` before a read, or when the last one failed.

### QuestAbout

```
QuestAbout(
    model: str | None,
    serial: str | None,
    os_build: str | None,
    app_version: str | None,
)
```

What the headset says it is, from `QuestDevice.about`.

Attributes:

| Name          | Type          | Description                                                                          |
| ------------- | ------------- | ------------------------------------------------------------------------------------ |
| `model`       | `str \| None` | The headset's model; None before it is read, or when the read failed.                |
| `serial`      | `str \| None` | The headset's serial number; None before it is read, or when the read failed.        |
| `os_build`    | `str \| None` | The headset's OS build; None before it is read, or when the read failed.             |
| `app_version` | `str \| None` | The teleop app's installed version; None before it is read, or when the read failed. |

#### model

```
model: str | None
```

The headset's model; `None` before it is read, or when the read failed.

#### serial

```
serial: str | None
```

The headset's serial number; `None` before it is read, or when the read failed.

#### os_build

```
os_build: str | None
```

The headset's OS build; `None` before it is read, or when the read failed.

#### app_version

```
app_version: str | None
```

The teleop app's installed version; `None` before it is read, or when the read failed.

### QuestDeviceConfig

```
QuestDeviceConfig(
    adb: str = "adb",
    package: str = "bot.anvil.AnvilQuestTeleop",
    activity: str = "android.app.NativeActivity",
    port: int = 8081,
    apk: tuple[str, str] | None = None,
    screen_off_ms: int = 14400000,
    brightness: int = 5,
    retry_s: float = 5.0,
)
```

How `QuestDevice` reaches the headset over adb and sets it up.

`QuestDeviceConfig()` is the shipped setup: `adb` on the path, the teleop app's package and the default tunneled port, with no app to install. Assign the fields that differ, `apk` from `anvil.apps.quest_streamer` among them.

Attributes:

| Name            | Type                      | Description                                                                                             |
| --------------- | ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `adb`           | `str`                     | The adb binary to run: a path, or a name on the PATH.                                                   |
| `package`       | `str`                     | The teleop app's Android package.                                                                       |
| `activity`      | `str`                     | The activity the teleop app is started with.                                                            |
| `port`          | `int`                     | The port adb reverse tunnels from the headset; it must match the app's own and QuestConfig.address's.   |
| `apk`           | `tuple[str, str] \| None` | The app to keep installed, as its .apk path and the versionName it carries; None leaves it to the user. |
| `screen_off_ms` | `int`                     | How long the headset's screen stays on, in milliseconds.                                                |
| `brightness`    | `int`                     | The headset's screen brightness, 0 to 255.                                                              |
| `retry_s`       | `float`                   | How long to wait before retrying a startup check that failed, in seconds.                               |

#### adb

```
adb: str = 'adb'
```

The `adb` binary to run: a path, or a name on the `PATH`.

#### package

```
package: str = 'bot.anvil.AnvilQuestTeleop'
```

The teleop app's Android package.

#### activity

```
activity: str = 'android.app.NativeActivity'
```

The activity the teleop app is started with.

#### port

```
port: int = 8081
```

The port `adb reverse` tunnels from the headset; it must match the app's own and `QuestConfig.address`'s.

#### apk

```
apk: tuple[str, str] | None = None
```

The app to keep installed, as its `.apk` path and the `versionName` it carries; `None` leaves it to the user.

An installed version other than this one is replaced.

#### screen_off_ms

```
screen_off_ms: int = 14400000
```

How long the headset's screen stays on, in milliseconds.

#### brightness

```
brightness: int = 5
```

The headset's screen brightness, 0 to 255.

#### retry_s

```
retry_s: float = 5.0
```

How long to wait before retrying a startup check that failed, in seconds.

### QuestAppOutput

```
QuestAppOutput()
```

Bases: `Struct`

What one `QuestTeleop.poll` returns: the command to send, what each arm is doing, and the headset's newest frame.

Read-only. The fields a frame carries, the controllers' poses, triggers, grips, thumbsticks, buttons and tracking, are `None` until the first frame arrives, which is how a poll before the headset has connected reads. The head pose and the velocities are `None` besides from a streamer older than 1.3.0, which sends neither. A per-arm list holds one entry per arm, in the cell's order.

Get one from `QuestTeleop.poll`, never by calling the class.

Methods:

| Name    | Description                                                                               |
| ------- | ----------------------------------------------------------------------------------------- |
| `frame` | Returns the newest frame as plain data, what a recording logs as the headset's raw input. |

Attributes:

| Name               | Type                      | Description                                                                                               |
| ------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------- |
| `command`          | `EePoseCommand`           | The command to send this poll: every arm's held target and gripper opening, driving or not.               |
| `states`           | `list[QuestArmState]`     | What each arm is doing, in the cell's order.                                                              |
| `drivers`          | `list[QuestHand \| None]` | The controller driving each arm, in the cell's order; None while its clutch is open.                      |
| `measured`         | `list[Pose]`              | Each arm's tip pose at the q the poll was given, in the cell's order.                                     |
| `connected`        | `bool`                    | Whether the headset is connected to the app's socket.                                                     |
| `frame_hz`         | `float`                   | Frames received per second over the last second, in hertz.                                                |
| `left_controller`  | `Pose \| None`            | The left controller's raw aim pose in the headset's stage frame; None before the first frame.             |
| `right_controller` | `Pose \| None`            | The right controller's raw aim pose in the headset's stage frame; None before the first frame.            |
| `left_trigger`     | `float \| None`           | The left index trigger, 0 released to 1 pulled; None before the first frame.                              |
| `right_trigger`    | `float \| None`           | The right index trigger, 0 released to 1 pulled; None before the first frame.                             |
| `left_grip`        | `float \| None`           | The left side trigger, the clutch, 0 released to 1 squeezed; None before the first frame.                 |
| `right_grip`       | `float \| None`           | The right side trigger, the clutch, 0 released to 1 squeezed; None before the first frame.                |
| `left_thumbstick`  | `list[float] \| None`     | The left thumbstick's x and y, each -1 to 1; None before the first frame.                                 |
| `right_thumbstick` | `list[float] \| None`     | The right thumbstick's x and y, each -1 to 1; None before the first frame.                                |
| `left_buttons`     | `list[str] \| None`       | The buttons held on the left controller, by their names in QUEST_BUTTONS; None before a frame.            |
| `right_buttons`    | `list[str] \| None`       | The buttons held on the right controller, by their names in QUEST_BUTTONS; None before a frame.           |
| `left_tracked`     | `bool \| None`            | Whether the headset tracked the left controller in the newest frame; None before the first frame.         |
| `right_tracked`    | `bool \| None`            | Whether the headset tracked the right controller in the newest frame; None before the first frame.        |
| `headset_us`       | `int \| None`             | The headset's own clock at the newest frame, in microseconds since its boot; None before the first frame. |
| `protocol`         | `int \| None`             | The protocol the headset's streamer speaks; None before the first frame.                                  |
| `head`             | `Pose \| None`            | The headset's own pose in its stage frame; None until a frame carries one.                                |
| `head_tracked`     | `bool \| None`            | Whether the headset tracked its own pose; None while head is.                                             |
| `left_velocity`    | `QuestVelocity \| None`   | The left controller's velocity in the newest frame; None until a frame carries one.                       |
| `right_velocity`   | `QuestVelocity \| None`   | The right controller's velocity in the newest frame; None until a frame carries one.                      |
| `fired`            | `list[QuestFired]`        | What the buttons asked for since the last poll, at most one per button, in the order of QUEST_BUTTONS.    |

#### command

```
command: EePoseCommand
```

The command to send this poll: every arm's held target and gripper opening, driving or not.

Send every poll's with `anvil.control.Runtime.send_command`, which stamps it as the loop picks it up. The poll a clutch closes on asks IK to reseed, and a skipped one loses that.

#### states

```
states: list[QuestArmState]
```

What each arm is doing, in the cell's order.

#### drivers

```
drivers: list[QuestHand | None]
```

The controller driving each arm, in the cell's order; `None` while its clutch is open.

#### measured

```
measured: list[Pose]
```

Each arm's tip pose at the `q` the poll was given, in the cell's order.

It is what a clutch-in anchors to, and what a panel compares the target against.

#### connected

```
connected: bool
```

Whether the headset is connected to the app's socket.

#### frame_hz

```
frame_hz: float
```

Frames received per second over the last second, in hertz.

The headset sends one per rendered frame and never asks for a refresh rate, so this reads whatever display mode the headset is in: 90 Hz on a Quest 3S.

#### left_controller

```
left_controller: Pose | None
```

The left controller's raw aim pose in the headset's stage frame; `None` before the first frame.

The stage frame has +y up and -z forward, in meters.

#### right_controller

```
right_controller: Pose | None
```

The right controller's raw aim pose in the headset's stage frame; `None` before the first frame.

#### left_trigger

```
left_trigger: float | None
```

The left index trigger, 0 released to 1 pulled; `None` before the first frame.

#### right_trigger

```
right_trigger: float | None
```

The right index trigger, 0 released to 1 pulled; `None` before the first frame.

#### left_grip

```
left_grip: float | None
```

The left side trigger, the clutch, 0 released to 1 squeezed; `None` before the first frame.

#### right_grip

```
right_grip: float | None
```

The right side trigger, the clutch, 0 released to 1 squeezed; `None` before the first frame.

#### left_thumbstick

```
left_thumbstick: list[float] | None
```

The left thumbstick's x and y, each -1 to 1; `None` before the first frame.

#### right_thumbstick

```
right_thumbstick: list[float] | None
```

The right thumbstick's x and y, each -1 to 1; `None` before the first frame.

#### left_buttons

```
left_buttons: list[str] | None
```

The buttons held on the left controller, by their names in `QUEST_BUTTONS`; `None` before a frame.

Held, not pressed: what a press asked for is in `fired`.

#### right_buttons

```
right_buttons: list[str] | None
```

The buttons held on the right controller, by their names in `QUEST_BUTTONS`; `None` before a frame.

#### left_tracked

```
left_tracked: bool | None
```

Whether the headset tracked the left controller in the newest frame; `None` before the first frame.

While it is false, `left_controller` means nothing.

#### right_tracked

```
right_tracked: bool | None
```

Whether the headset tracked the right controller in the newest frame; `None` before the first frame.

#### headset_us

```
headset_us: int | None
```

The headset's own clock at the newest frame, in microseconds since its boot; `None` before the first frame.

An ordering key, never comparable with a clock on this machine.

#### protocol

```
protocol: int | None
```

The protocol the headset's streamer speaks; `None` before the first frame.

It is 1 for a streamer older than 1.3.0, which takes no haptics and sends no head pose or velocity, else 2.

#### head

```
head: Pose | None
```

The headset's own pose in its stage frame; `None` until a frame carries one.

It is the eyes when worn, and the chest when hung around the operator's neck.

#### head_tracked

```
head_tracked: bool | None
```

Whether the headset tracked its own pose; `None` while `head` is.

#### left_velocity

```
left_velocity: QuestVelocity | None
```

The left controller's velocity in the newest frame; `None` until a frame carries one.

#### right_velocity

```
right_velocity: QuestVelocity | None
```

The right controller's velocity in the newest frame; `None` until a frame carries one.

#### fired

```
fired: list[QuestFired]
```

What the buttons asked for since the last poll, at most one per button, in the order of `QUEST_BUTTONS`.

The app has already done the ones it owns, or said why not in `QuestFired.refused`; `rehome`, `record_start`, `record_stop`, `mark_success`, `mark_failure`, `annotate` and `switch_profile` are for the caller to act on.

#### frame

```
frame() -> dict[str, Any] | None
```

Returns the newest frame as plain data, what a recording logs as the headset's raw input.

Returns:

| Type                     | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `dict[str, Any] \| None` | None before the first frame, else a dict of the properties of the same names: headset_us, protocol, and head, None or a dict of pose and tracked; then left and right, each a dict of the controller's pose, velocity, trigger, grip, thumbstick, buttons and tracked; and arms, a list of one dict per arm in the cell's order, its state and the hand that is its driver, as strings, driver None while the clutch is open. A pose is a dict of position and orientation, and a velocity None or a dict of linear_mps and angular_radps. |

### KeyboardAction

Bases: `StrEnum`

What a key does under `KeyboardTeleop`.

The move, tip, roll, twist, open, close and fast actions act while the key is held, in the frame of the operator's view; the rest act once per press.

Attributes:

| Name           | Type | Description                                                                           |
| -------------- | ---- | ------------------------------------------------------------------------------------- |
| `MOVE_FORWARD` |      | Motion of the gripper the way the view faces, while held.                             |
| `MOVE_BACK`    |      | Motion of the gripper back toward the viewer, while held.                             |
| `MOVE_LEFT`    |      | Motion of the gripper to the view's left, while held.                                 |
| `MOVE_RIGHT`   |      | Motion of the gripper to the view's right, while held.                                |
| `MOVE_UP`      |      | Motion of the gripper up, while held.                                                 |
| `MOVE_DOWN`    |      | Motion of the gripper down, while held.                                               |
| `TIP_AWAY`     |      | A downward gripper's tip swung away from the viewer, while held.                      |
| `TIP_TOWARD`   |      | A downward gripper's tip swung toward the viewer, while held.                         |
| `ROLL_LEFT`    |      | A downward gripper's tip swung to the view's left, while held.                        |
| `ROLL_RIGHT`   |      | A downward gripper's tip swung to the view's right, while held.                       |
| `TWIST_LEFT`   |      | The gripper twisted counterclockwise seen from above, while held.                     |
| `TWIST_RIGHT`  |      | The gripper twisted clockwise seen from above, while held.                            |
| `OPEN`         |      | The jaws opened a little at a time, at KeyboardConfig.grip_creep_mps, while held.     |
| `CLOSE`        |      | The jaws closed a little at a time, at KeyboardConfig.grip_creep_mps, while held.     |
| `FAST`         |      | Every speed multiplied by KeyboardConfig.fast_scale, while held.                      |
| `TOGGLE_GRIP`  |      | A closed gripper opened, or an open one closed, once per press.                       |
| `NEXT_ARM`     |      | The next arm driven by the keys, once per press.                                      |
| `ARM_1`        |      | The first arm driven by the keys, once per press; passed over in a cell without one.  |
| `ARM_2`        |      | The second arm driven by the keys, once per press; passed over in a cell without one. |
| `ARM_3`        |      | The third arm driven by the keys, once per press; passed over in a cell without one.  |
| `ARM_4`        |      | The fourth arm driven by the keys, once per press; passed over in a cell without one. |
| `STOP`         |      | Every arm stopped where it is aimed and every goal dropped, once per press.           |

#### MOVE_FORWARD

```
MOVE_FORWARD
```

Motion of the gripper the way the view faces, while held.

#### MOVE_BACK

```
MOVE_BACK
```

Motion of the gripper back toward the viewer, while held.

#### MOVE_LEFT

```
MOVE_LEFT
```

Motion of the gripper to the view's left, while held.

#### MOVE_RIGHT

```
MOVE_RIGHT
```

Motion of the gripper to the view's right, while held.

#### MOVE_UP

```
MOVE_UP
```

Motion of the gripper up, while held.

#### MOVE_DOWN

```
MOVE_DOWN
```

Motion of the gripper down, while held.

#### TIP_AWAY

```
TIP_AWAY
```

A downward gripper's tip swung away from the viewer, while held.

#### TIP_TOWARD

```
TIP_TOWARD
```

A downward gripper's tip swung toward the viewer, while held.

#### ROLL_LEFT

```
ROLL_LEFT
```

A downward gripper's tip swung to the view's left, while held.

#### ROLL_RIGHT

```
ROLL_RIGHT
```

A downward gripper's tip swung to the view's right, while held.

#### TWIST_LEFT

```
TWIST_LEFT
```

The gripper twisted counterclockwise seen from above, while held.

#### TWIST_RIGHT

```
TWIST_RIGHT
```

The gripper twisted clockwise seen from above, while held.

#### OPEN

```
OPEN
```

The jaws opened a little at a time, at `KeyboardConfig.grip_creep_mps`, while held.

#### CLOSE

```
CLOSE
```

The jaws closed a little at a time, at `KeyboardConfig.grip_creep_mps`, while held.

#### FAST

```
FAST
```

Every speed multiplied by `KeyboardConfig.fast_scale`, while held.

#### TOGGLE_GRIP

```
TOGGLE_GRIP
```

A closed gripper opened, or an open one closed, once per press.

#### NEXT_ARM

```
NEXT_ARM
```

The next arm driven by the keys, once per press.

#### ARM_1

```
ARM_1
```

The first arm driven by the keys, once per press; passed over in a cell without one.

#### ARM_2

```
ARM_2
```

The second arm driven by the keys, once per press; passed over in a cell without one.

#### ARM_3

```
ARM_3
```

The third arm driven by the keys, once per press; passed over in a cell without one.

#### ARM_4

```
ARM_4
```

The fourth arm driven by the keys, once per press; passed over in a cell without one.

#### STOP

```
STOP
```

Every arm stopped where it is aimed and every goal dropped, once per press.

### KeyboardConfig

```
KeyboardConfig(speed_mps: float = 0.08, turn_radps: float = 0.9, fast_scale: float = 2.5, accel_mps2: float = 0.8, turn_accel_radps2: float = 8.0, goal_speed_mps: float = 0.15, grip_mps: float = 0.2, grip_creep_mps: float = 0.05, lead_m: float = 0.05, lead_rad: float = 0.35, bindings: dict[str, KeyboardAction] = {'1': <KeyboardAction.ARM_1: 'arm_1'>, '2': <KeyboardAction.ARM_2: 'arm_2'>, '3': <KeyboardAction.ARM_3: 'arm_3'>, '4': <KeyboardAction.ARM_4: 'arm_4'>, 'a': <KeyboardAction.MOVE_LEFT: 'move_left'>, 'c': <KeyboardAction.ROLL_RIGHT: 'roll_right'>, 'd': <KeyboardAction.MOVE_RIGHT: 'move_right'>, 'down': <KeyboardAction.TIP_TOWARD: 'tip_toward'>, 'e': <KeyboardAction.MOVE_UP: 'move_up'>, 'escape': <KeyboardAction.STOP: 'stop'>, 'f': <KeyboardAction.CLOSE: 'close'>, 'left': <KeyboardAction.TWIST_LEFT: 'twist_left'>, 'q': <KeyboardAction.MOVE_DOWN: 'move_down'>, 'r': <KeyboardAction.OPEN: 'open'>, 'right': <KeyboardAction.TWIST_RIGHT: 'twist_right'>, 's': <KeyboardAction.MOVE_BACK: 'move_back'>, 'shift': <KeyboardAction.FAST: 'fast'>, 'space': <KeyboardAction.TOGGLE_GRIP: 'toggle_grip'>, 'tab': <KeyboardAction.NEXT_ARM: 'next_arm'>, 'up': <KeyboardAction.TIP_AWAY: 'tip_away'>, 'w': <KeyboardAction.MOVE_FORWARD: 'move_forward'>, 'z': <KeyboardAction.ROLL_LEFT: 'roll_left'>})
```

Everything the keyboard app is told, which `KeyboardTeleop` takes on construction and through its `config`.

It says how fast the keys fly the gripper and how quickly they get there, how fast a clicked goal is glided to, the jaws' speeds, how far the target may lead the arm, and what every key does. `KeyboardConfig()` is the shipped setup; assign the fields that differ. Each speed, rate and lead must lie in the range its field states, above its floor and up to its ceiling, which `validate`, the constructor and assigning `KeyboardTeleop.config` check.

Methods:

| Name        | Description                                                                   |
| ----------- | ----------------------------------------------------------------------------- |
| `validate`  | Checks this configuration, raising on what KeyboardTeleop would refuse in it. |
| `bind`      | Binds key to action, or to nothing when action is None.                       |
| `to_json`   | Returns this configuration as JSON, the form from_json reads back.            |
| `from_json` | Builds a configuration from the JSON to_json wrote.                           |

Attributes:

| Name                | Type                        | Description                                                                                         |
| ------------------- | --------------------------- | --------------------------------------------------------------------------------------------------- |
| `speed_mps`         | `float`                     | How fast a move key flies the gripper, in meters per second; above 0, up to 0.5.                    |
| `turn_radps`        | `float`                     | How fast a tip, roll or twist key turns it, in radians per second; above 0, up to 3.                |
| `fast_scale`        | `float`                     | What KeyboardAction.FAST multiplies both speeds by; 1 to 5.                                         |
| `accel_mps2`        | `float`                     | How quickly the gripper gets up to speed and stops, in meters per second squared; above 0, up to 5. |
| `turn_accel_radps2` | `float`                     | How quickly a turn gets up to speed and stops, in radians per second squared; above 0, up to 50.    |
| `goal_speed_mps`    | `float`                     | How fast the gripper glides to a clicked goal, in meters per second; above 0, up to 0.5.            |
| `grip_mps`          | `float`                     | How fast the jaws travel on KeyboardAction.TOGGLE_GRIP, in meters per second; above 0, up to 1.     |
| `grip_creep_mps`    | `float`                     | How fast the open and close keys creep the jaws, in meters per second; above 0, up to 0.5.          |
| `lead_m`            | `float`                     | How far the target may lead the arm's measured tip, in meters; above 0, up to 0.2.                  |
| `lead_rad`          | `float`                     | How far the target's orientation may turn ahead of the arm's, in radians; above 0, up to 1.         |
| `bindings`          | `dict[str, KeyboardAction]` | What each key does, keyed by its name in KEYBOARD_KEYS; a key left out does nothing.                |

#### speed_mps

```
speed_mps: float = 0.08
```

How fast a move key flies the gripper, in meters per second; above 0, up to 0.5.

#### turn_radps

```
turn_radps: float = 0.9
```

How fast a tip, roll or twist key turns it, in radians per second; above 0, up to 3.

#### fast_scale

```
fast_scale: float = 2.5
```

What `KeyboardAction.FAST` multiplies both speeds by; 1 to 5.

#### accel_mps2

```
accel_mps2: float = 0.8
```

How quickly the gripper gets up to speed and stops, in meters per second squared; above 0, up to 5.

#### turn_accel_radps2

```
turn_accel_radps2: float = 8.0
```

How quickly a turn gets up to speed and stops, in radians per second squared; above 0, up to 50.

#### goal_speed_mps

```
goal_speed_mps: float = 0.15
```

How fast the gripper glides to a clicked goal, in meters per second; above 0, up to 0.5.

#### grip_mps

```
grip_mps: float = 0.2
```

How fast the jaws travel on `KeyboardAction.TOGGLE_GRIP`, in meters per second; above 0, up to 1.

#### grip_creep_mps

```
grip_creep_mps: float = 0.05
```

How fast the open and close keys creep the jaws, in meters per second; above 0, up to 0.5.

#### lead_m

```
lead_m: float = 0.05
```

How far the target may lead the arm's measured tip, in meters; above 0, up to 0.2.

#### lead_rad

```
lead_rad: float = 0.35
```

How far the target's orientation may turn ahead of the arm's, in radians; above 0, up to 1.

#### bindings

```
bindings: dict[str, KeyboardAction] = {'1': <KeyboardAction.ARM_1: 'arm_1'>, '2': <KeyboardAction.ARM_2: 'arm_2'>, '3': <KeyboardAction.ARM_3: 'arm_3'>, '4': <KeyboardAction.ARM_4: 'arm_4'>, 'a': <KeyboardAction.MOVE_LEFT: 'move_left'>, 'c': <KeyboardAction.ROLL_RIGHT: 'roll_right'>, 'd': <KeyboardAction.MOVE_RIGHT: 'move_right'>, 'down': <KeyboardAction.TIP_TOWARD: 'tip_toward'>, 'e': <KeyboardAction.MOVE_UP: 'move_up'>, 'escape': <KeyboardAction.STOP: 'stop'>, 'f': <KeyboardAction.CLOSE: 'close'>, 'left': <KeyboardAction.TWIST_LEFT: 'twist_left'>, 'q': <KeyboardAction.MOVE_DOWN: 'move_down'>, 'r': <KeyboardAction.OPEN: 'open'>, 'right': <KeyboardAction.TWIST_RIGHT: 'twist_right'>, 's': <KeyboardAction.MOVE_BACK: 'move_back'>, 'shift': <KeyboardAction.FAST: 'fast'>, 'space': <KeyboardAction.TOGGLE_GRIP: 'toggle_grip'>, 'tab': <KeyboardAction.NEXT_ARM: 'next_arm'>, 'up': <KeyboardAction.TIP_AWAY: 'tip_away'>, 'w': <KeyboardAction.MOVE_FORWARD: 'move_forward'>, 'z': <KeyboardAction.ROLL_LEFT: 'roll_left'>}
```

What each key does, keyed by its name in `KEYBOARD_KEYS`; a key left out does nothing.

#### validate

```
validate() -> None
```

Checks this configuration, raising on what `KeyboardTeleop` would refuse in it.

Raises:

| Type                   | Description                                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A speed, rate or lead is outside its field's range; a binding names a key that is not one of KEYBOARD_KEYS. |

#### bind

```
bind(
    key: str, action: KeyboardAction | str | None = None
) -> None
```

Binds `key` to `action`, or to nothing when `action` is `None`.

Parameters:

| Name     | Type                            | Description                                                       | Default    |
| -------- | ------------------------------- | ----------------------------------------------------------------- | ---------- |
| `key`    | `str`                           | The key's name, one of KEYBOARD_KEYS.                             | *required* |
| `action` | `KeyboardAction \| str \| None` | A KeyboardAction or its value; None leaves the key doing nothing. | `None`     |

Raises:

| Type                   | Description                               |
| ---------------------- | ----------------------------------------- |
| `InvalidArgumentError` | key is not one of KEYBOARD_KEYS.          |
| `ValueError`           | action is a string no KeyboardAction has. |

#### to_json

```
to_json() -> str
```

Returns this configuration as JSON, the form `from_json` reads back.

#### from_json

```
from_json(wire: str) -> Self
```

Builds a configuration from the JSON `to_json` wrote.

The values are not checked here but when the configuration is validated or handed to a `KeyboardTeleop`.

Parameters:

| Name   | Type  | Description                      | Default    |
| ------ | ----- | -------------------------------- | ---------- |
| `wire` | `str` | JSON text, as to_json writes it. | *required* |

Raises:

| Type                   | Description                                                           |
| ---------------------- | --------------------------------------------------------------------- |
| `ValueError`           | wire is not JSON, or a binding names an action no KeyboardAction has. |
| `InvalidArgumentError` | wire is missing a field to_json writes.                               |

### KeyboardAppOutput

```
KeyboardAppOutput()
```

Bases: `Struct`

What one `KeyboardTeleop.poll` returns: the command to send, the arm the keys drive, and what each arm is doing.

Read-only. A per-arm list holds one entry per arm, in the cell's order.

Get one from `KeyboardTeleop.poll`, never by calling the class.

Attributes:

| Name         | Type                        | Description                                                                                              |
| ------------ | --------------------------- | -------------------------------------------------------------------------------------------------------- |
| `command`    | `EePoseCommand`             | The command to send this poll: every arm's target and gripper opening.                                   |
| `driven`     | `int`                       | The arm the keys drive, as an index into the cell's arms.                                                |
| `measured`   | `list[Pose]`                | Each arm's tip pose at the q the poll was given, in the cell's order: what its target's lead is from.    |
| `goals`      | `list[list[float] \| None]` | Where a click sent each arm's gripper, x y z in the world frame in meters; None for an arm with no goal. |
| `blocked`    | `list[bool]`                | Whether each arm's target is held back by the lead, waiting for the arm.                                 |
| `speeds_mps` | `list[float]`               | How fast each arm's target is moving, in meters per second.                                              |

#### command

```
command: EePoseCommand
```

The command to send this poll: every arm's target and gripper opening.

Send every poll's with `anvil.control.Runtime.send_command`, which stamps it as the loop picks it up: the controller holds an arm whose command has gone stale.

#### driven

```
driven: int
```

The arm the keys drive, as an index into the cell's arms.

#### measured

```
measured: list[Pose]
```

Each arm's tip pose at the `q` the poll was given, in the cell's order: what its target's lead is from.

#### goals

```
goals: list[list[float] | None]
```

Where a click sent each arm's gripper, x y z in the world frame in meters; `None` for an arm with no goal.

A goal stays until the gripper gets there or gives up.

#### blocked

```
blocked: list[bool]
```

Whether each arm's target is held back by the lead, waiting for the arm.

An arm holds its target back at the edge of its reach, or against a joint's limit.

#### speeds_mps

```
speeds_mps: list[float]
```

How fast each arm's target is moving, in meters per second.

### QuestTeleop

```
QuestTeleop(
    config: QuestConfig,
    robot: RobotConfig,
    q: Sequence[float],
)
```

The Quest teleop app: a Meta Quest headset's controller frames turned into end-effector targets for `ee_pose`.

Constructing it binds the socket the headset connects back to, which is closed when this object is collected. Poll it at the caller's loop rate and send each poll's command to the `ee_pose` controller. It drives follower arms, one or two; its methods may be called from any thread.

Binds the headset's socket and starts capture, holding `robot`'s arms where they are at `q`.

Each arm's target starts at its tip pose and gripper opening at `q`, so the first commands change nothing whether or not the headset ever connects. Two arms are driven by the controllers `config` names for them; one arm by whichever controller clutches in first.

Parameters:

| Name     | Type              | Description                                                                                                                           | Default    |
| -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `config` | `QuestConfig`     | What the app is told. Its address is bound here and nowhere else, so port 0 reads back through config as the port the machine picked. | *required* |
| `robot`  | `RobotConfig`     | The robot to drive: one or two follower arms.                                                                                         | *required* |
| `q`      | `Sequence[float]` | The measured joint positions, one per slot, as anvil.control.Snapshot.q holds them.                                                   | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                 |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | robot is not an anvil.workcell.RobotConfig, or q is not a flat sequence of numbers.                                                                                                                                         |
| `InvalidArgumentError` | q does not cover the arms' slots; robot has a leader arm or more than two arms; config is one QuestConfig.validate refuses; config.address is not host:port or resolves to nothing; or a frame in config is not a rotation. |
| `BusyError`            | Another program holds config.address, such as another QuestTeleop.                                                                                                                                                          |
| `OperationError`       | config.address cannot be bound for another reason, such as an address that is not this machine's.                                                                                                                           |

Methods:

| Name          | Description                                                                                      |
| ------------- | ------------------------------------------------------------------------------------------------ |
| `poll`        | Folds every headset frame received since the last poll into the targets and returns the result.  |
| `reset`       | Latches every arm so nothing moves until the operator releases the clutch and closes it again.   |
| `play`        | Plays cue on hand's controller, or on both for None, as config.haptics says, from the next poll. |
| `pulse`       | Vibrates hand's controller now, at amplitude for duration_s, replacing any pulse still running.  |
| `recentre`    | Takes the heading of hand's controller as the robot's forward, for QuestMotionFrame.WORLD.       |
| `set_enabled` | Lets hand's controller drive, or stops it, from the next poll.                                   |
| `enabled`     | Returns whether hand's controller may drive, as set_enabled left it.                             |
| `targets`     | Returns the targets every arm holds now, in the cell's order.                                    |

Attributes:

| Name     | Type          | Description                                                                                |
| -------- | ------------- | ------------------------------------------------------------------------------------------ |
| `config` | `QuestConfig` | Everything the app is told, as it stands; the buttons write the same fields while it runs. |

#### config

```
config: QuestConfig
```

Everything the app is told, as it stands; the buttons write the same fields while it runs.

Read it, edit it and assign it back: everything but `address` is in force from the next poll, and `address` reads back as the socket that was bound. Nothing is taken when the assignment is refused.

Raises:

| Type                   | Description                                                                                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | On assignment, what QuestConfig.validate refuses; a frame that is not a rotation; rigid or a second arm's axis lock on a one-arm cell; or an address other than the bound one. |

#### poll

```
poll(
    q: Sequence[float], q_commanded: Sequence[float]
) -> QuestAppOutput
```

Folds every headset frame received since the last poll into the targets and returns the result.

It never blocks. Send every poll's `QuestAppOutput.command`, and act on what its `fired` reports for the caller.

Parameters:

| Name          | Type              | Description                                                                                                                                                                                                                                                             | Default    |
| ------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `q`           | `Sequence[float]` | The measured joint positions, one per slot, anvil.control.Snapshot.q: each arm's tip pose at it anchors the chain at every clutch-in.                                                                                                                                   | *required* |
| `q_commanded` | `Sequence[float]` | The positions the loop last wrote, one per slot, anvil.control.Snapshot.q_commanded: where the controller holds each arm, and where letting go, or losing the controller mid-drive, freezes the target, so the arm stops where it stands instead of finishing the move. | *required* |

Raises:

| Type                   | Description                                         |
| ---------------------- | --------------------------------------------------- |
| `TypeError`            | q or q_commanded is not a flat sequence of numbers. |
| `InvalidArgumentError` | q or q_commanded does not cover the arms' slots.    |

#### reset

```
reset() -> None
```

Latches every arm so nothing moves until the operator releases the clutch and closes it again.

Call it when something outside the app moved the arms, such as a re-home or a controller swap, and when `QuestDevice.take_reset` returns true. Each arm re-anchors to its measured pose at the next clutch-in.

#### play

```
play(
    cue: QuestCue | str, hand: QuestHand | str | None = None
) -> None
```

Plays `cue` on `hand`'s controller, or on both for `None`, as `config.haptics` says, from the next poll.

Call it on both for the cues the caller sees happen: `QuestCue.RECORDING_START` and `QuestCue.RECORDING_STOP`, whatever started or stopped the episode, and `QuestCue.COLLISION_WARNING` and `QuestCue.COLLISION_STOP`. A cue that lasts is played as if it lasted a second, which is how a settings page lets one be felt. A controller whose haptic for the cue is off feels nothing.

Parameters:

| Name   | Type                       | Description                                  | Default    |
| ------ | -------------------------- | -------------------------------------------- | ---------- |
| `cue`  | `QuestCue \| str`          | What to play.                                | *required* |
| `hand` | `QuestHand \| str \| None` | The controller to play it on; None for both. | `None`     |

Raises:

| Type                   | Description                                                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `ValueError`           | cue or hand is a string that names none.                                                     |
| `InvalidArgumentError` | No headset that takes haptics is connected: none, or one whose streamer is older than 1.3.0. |

#### pulse

```
pulse(
    hand: QuestHand | str,
    amplitude: float,
    duration_s: float,
) -> None
```

Vibrates `hand`'s controller now, at `amplitude` for `duration_s`, replacing any pulse still running.

One pulse every poll at a changing amplitude is continuous feedback.

Parameters:

| Name         | Type               | Description                                                                | Default    |
| ------------ | ------------------ | -------------------------------------------------------------------------- | ---------- |
| `hand`       | `QuestHand \| str` | The controller to vibrate.                                                 | *required* |
| `amplitude`  | `float`            | How hard, 0 to 1; a value outside is clamped to it.                        | *required* |
| `duration_s` | `float`            | How long, in seconds; the headset clamps it to what the controller can do. | *required* |

Raises:

| Type                   | Description                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `ValueError`           | hand is a string no QuestHand has.                                                                                                    |
| `InvalidArgumentError` | duration_s is negative or not finite, or no headset that takes haptics is connected: none, or one whose streamer is older than 1.3.0. |

#### recentre

```
recentre(hand: QuestHand | str) -> None
```

Takes the heading of `hand`'s controller as the robot's forward, for `QuestMotionFrame.WORLD`.

The heading is the way the controller points, flattened to the floor. Point it the way the arms face and call this once, and again after the headset redraws its boundary. `QuestMotionFrame.GRIPPER` reads nothing from it. A button bound to `QuestAction.recentre` does the same.

Parameters:

| Name   | Type               | Description                            | Default    |
| ------ | ------------------ | -------------------------------------- | ---------- |
| `hand` | `QuestHand \| str` | The controller whose heading is taken. | *required* |

Raises:

| Type                   | Description                                                                                                    |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| `ValueError`           | hand is a string no QuestHand has.                                                                             |
| `InvalidArgumentError` | No frame has arrived yet, the controller is untracked, or it points within ten degrees of straight up or down. |

#### set_enabled

```
set_enabled(hand: QuestHand | str, enabled: bool) -> None
```

Lets `hand`'s controller drive, or stops it, from the next poll.

A disabled controller's side trigger reads as released however far it is squeezed: an arm it drives is let go of where it stands, and enabling it with the trigger squeezed is a clutch-in, anchored to the arm's measured pose like any other. Its buttons still fire. This is for a controller whose side trigger is held down for it, one strapped to a glove say. Every controller starts enabled, and `reset` leaves this alone.

Parameters:

| Name      | Type               | Description                          | Default    |
| --------- | ------------------ | ------------------------------------ | ---------- |
| `hand`    | `QuestHand \| str` | The controller to enable or disable. | *required* |
| `enabled` | `bool`             | Whether it may drive.                | *required* |

Raises:

| Type         | Description                        |
| ------------ | ---------------------------------- |
| `ValueError` | hand is a string no QuestHand has. |

#### enabled

```
enabled(hand: QuestHand | str) -> bool
```

Returns whether `hand`'s controller may drive, as `set_enabled` left it.

Parameters:

| Name   | Type               | Description                 | Default    |
| ------ | ------------------ | --------------------------- | ---------- |
| `hand` | `QuestHand \| str` | The controller asked about. | *required* |

Raises:

| Type         | Description                        |
| ------------ | ---------------------------------- |
| `ValueError` | hand is a string no QuestHand has. |

#### targets

```
targets() -> EePoseCommand
```

Returns the targets every arm holds now, in the cell's order.

### QuestDevice

```
QuestDevice(config: QuestDeviceConfig | None = None)
```

The Quest headset's lifecycle over USB, watched on a thread of its own.

Constructing it starts provisioning the headset over adb: setting it up, installing the teleop app when `QuestDeviceConfig.apk` names one, starting it and tunneling its port. Each check then re-runs as it comes due. A critical check failing, the USB link, adb or the headset being awake, means the connection itself is gone: the headset is provisioned again from the top, and `take_reset` says so. The thread stops when this object is collected. Its methods return what the watcher last found, and may be called from any thread.

Starts provisioning the headset and watching it, returning at once.

Parameters:

| Name     | Type                        | Description                                                        | Default |
| -------- | --------------------------- | ------------------------------------------------------------------ | ------- |
| `config` | `QuestDeviceConfig \| None` | How to reach and set up the headset; None for QuestDeviceConfig(). | `None`  |

Raises:

| Type                   | Description                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | config.retry_s is negative or not finite, or a field is outside what it holds: port 0 to 65535, brightness 0 to 255, screen_off_ms zero or more. |
| `OperationError`       | The watcher thread could not be started.                                                                                                         |

Methods:

| Name         | Description                                                                                               |
| ------------ | --------------------------------------------------------------------------------------------------------- |
| `status`     | Returns what every check last reported, in the order provisioning runs them.                              |
| `batteries`  | Returns each battery's charge as last read; the batteries are read every two minutes.                     |
| `about`      | Returns what the headset says it is, as last read.                                                        |
| `ready`      | Returns whether provisioning has finished: every startup check passed, the teleop app running among them. |
| `take_reset` | Returns whether a critical check has failed since the last call, clearing the flag.                       |

#### status

```
status() -> list[QuestCheckStatus]
```

Returns what every check last reported, in the order provisioning runs them.

#### batteries

```
batteries() -> QuestBatteries
```

Returns each battery's charge as last read; the batteries are read every two minutes.

#### about

```
about() -> QuestAbout
```

Returns what the headset says it is, as last read.

#### ready

```
ready() -> bool
```

Returns whether provisioning has finished: every startup check passed, the teleop app running among them.

#### take_reset

```
take_reset() -> bool
```

Returns whether a critical check has failed since the last call, clearing the flag.

A critical check failing means the USB link, adb or the headset being awake was lost, and whatever the operator was doing is over. Answer true with `QuestTeleop.reset`, so nothing moves until they clutch in again.

### KeyboardTeleop

```
KeyboardTeleop(
    config: KeyboardConfig,
    robot: RobotConfig,
    q: Sequence[float],
)
```

The keyboard app: keys held and spots clicked, driving a robot's arms one at a time through `ee_pose`.

Keys held fly the driven arm's gripper the way the operator's view faces; a spot clicked is a goal its gripper glides to. Neither may lead the measured arm far, so holding a key into the edge of the reach winds nothing up.

It reads no keyboard and no clock: the caller says which keys are held and when, on the clock it polls on. Keys held are a lease that runs out 0.3 seconds after the last `hold`, so a page that falls silent stops the arm. It is released when this object is collected; its methods may be called from any thread.

Builds the app over `robot`, holding its arms where they are at `q` and driving the first.

Each arm's target starts at its tip pose and gripper opening at `q`, so the first commands change nothing.

Parameters:

| Name     | Type              | Description                                                                         | Default    |
| -------- | ----------------- | ----------------------------------------------------------------------------------- | ---------- |
| `config` | `KeyboardConfig`  | What the app is told.                                                               | *required* |
| `robot`  | `RobotConfig`     | The robot to drive.                                                                 | *required* |
| `q`      | `Sequence[float]` | The measured joint positions, one per slot, as anvil.control.Snapshot.q holds them. | *required* |

Raises:

| Type                   | Description                                                                         |
| ---------------------- | ----------------------------------------------------------------------------------- |
| `TypeError`            | robot is not an anvil.workcell.RobotConfig, or q is not a flat sequence of numbers. |
| `InvalidArgumentError` | What KeyboardConfig.validate refuses, or q does not cover the arms' slots.          |

Methods:

| Name    | Description                                                                                             |
| ------- | ------------------------------------------------------------------------------------------------------- |
| `poll`  | Moves every target on by the time since the last poll and returns the result, never blocking.           |
| `hold`  | Takes keys and the sticks as held at t_ns, until the lease runs out 0.3 seconds later or the next call. |
| `press` | Does what one press of key asks, where the configuration binds it to an action done once per press.     |
| `drive` | Drives arm with the keys from here on.                                                                  |
| `go_to` | Sends arm's gripper gliding to position_m, its orientation kept, and drives that arm with the keys.     |
| `reset` | Snaps every target to where the arms are at q, holding, with every goal, speed and held key dropped.    |

Attributes:

| Name     | Type             | Description                               |
| -------- | ---------------- | ----------------------------------------- |
| `config` | `KeyboardConfig` | Everything the app is told, as it stands. |

#### config

```
config: KeyboardConfig
```

Everything the app is told, as it stands.

Read it, edit it and assign it back: it is in force from the next poll, and keys held now keep doing what they did until they are said again. Nothing is taken when the assignment is refused.

Raises:

| Type                   | Description                                          |
| ---------------------- | ---------------------------------------------------- |
| `InvalidArgumentError` | On assignment, what KeyboardConfig.validate refuses. |

#### poll

```
poll(q: Sequence[float], t_ns: int) -> KeyboardAppOutput
```

Moves every target on by the time since the last poll and returns the result, never blocking.

The first poll moves nothing, and a gap of more than 0.1 seconds since the last moves the targets no further than 0.1 seconds would. Send every poll's `KeyboardAppOutput.command`.

Parameters:

| Name   | Type              | Description                                                                                                  | Default    |
| ------ | ----------------- | ------------------------------------------------------------------------------------------------------------ | ---------- |
| `q`    | `Sequence[float]` | The measured joint positions, one per slot, anvil.control.Snapshot.q: what each arm's lead is measured from. | *required* |
| `t_ns` | `int`             | Now, in nanoseconds, on the clock hold is given.                                                             | *required* |

Raises:

| Type                   | Description                                            |
| ---------------------- | ------------------------------------------------------ |
| `TypeError`            | q is not a flat sequence of numbers.                   |
| `InvalidArgumentError` | t_ns is negative, or q does not cover the arms' slots. |

#### hold

```
hold(
    keys: Sequence[str],
    heading_rad: float,
    t_ns: int,
    move_view: Sequence[float] = (0.0, 0.0, 0.0),
    turn_view: Sequence[float] = (0.0, 0.0, 0.0),
) -> None
```

Takes `keys` and the sticks as held at `t_ns`, until the lease runs out 0.3 seconds later or the next call.

Each key does what the configuration binds it to while held. A name that is no key, or a key bound to nothing or to an action done once per press, is passed over. The sticks, a touch screen's or a gamepad's, move and turn as the keys do, each as far as it is pushed: 1 is a key's whole speed, and a stick held with a key adds to it. Moving the driven arm drops the goal a click gave it. After a stop, nothing held does anything until every key and stick is let go, so one still held when the stop came does not start the arm again.

Parameters:

| Name          | Type              | Description                                                                                                           | Default           |
| ------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `keys`        | `Sequence[str]`   | The names of the keys held, from KEYBOARD_KEYS; empty for none.                                                       | *required*        |
| `heading_rad` | `float`           | The way the operator's view faces, in radians about world +z from +x: what KeyboardAction.MOVE_FORWARD drives toward. | *required*        |
| `t_ns`        | `int`             | When the keys were held, in nanoseconds, on the clock poll is given.                                                  | *required*        |
| `move_view`   | `Sequence[float]` | How far the sticks move the gripper along the view's forward, left and up, each from -1 to 1.                         | `(0.0, 0.0, 0.0)` |
| `turn_view`   | `Sequence[float]` | How far they turn it about the view's forward, left and up, right-handed, each from -1 to 1.                          | `(0.0, 0.0, 0.0)` |

Raises:

| Type                   | Description                                                                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | keys is one string rather than a sequence of them.                                                                                                |
| `InvalidArgumentError` | t_ns is negative, heading_rad is not finite, keys holds something other than strings, or move_view or turn_view is not three values from -1 to 1. |

#### press

```
press(key: str) -> None
```

Does what one press of `key` asks, where the configuration binds it to an action done once per press.

That is driving the next arm or a numbered one, opening or closing the driven gripper, or stopping every arm and dropping every goal, the keys held then doing nothing until let go. A name that is no key, a key bound to nothing or to an action done while held, or an arm the cell does not have, is passed over.

Parameters:

| Name  | Type  | Description                         | Default    |
| ----- | ----- | ----------------------------------- | ---------- |
| `key` | `str` | The key's name, from KEYBOARD_KEYS. | *required* |

Raises:

| Type        | Description          |
| ----------- | -------------------- |
| `TypeError` | key is not a string. |

#### drive

```
drive(arm: int) -> None
```

Drives `arm` with the keys from here on.

Parameters:

| Name  | Type  | Description                            | Default    |
| ----- | ----- | -------------------------------------- | ---------- |
| `arm` | `int` | An index into the cell's arms, from 0. | *required* |

Raises:

| Type                   | Description              |
| ---------------------- | ------------------------ |
| `InvalidArgumentError` | The cell has no arm arm. |

#### go_to

```
go_to(arm: int, position_m: Sequence[float]) -> None
```

Sends `arm`'s gripper gliding to `position_m`, its orientation kept, and drives that arm with the keys.

It glides at `KeyboardConfig.goal_speed_mps`. A held move key drops the goal, and one out of reach is given up a second after the target stops getting closer. The way there is checked first against the robot's collision pieces, solved a centimeter at a time with the other arms where the last poll measured them, and a goal whose way takes two links of the robot nearer than `anvil.control.CollisionStop.DEFAULT_MARGIN_M` that stood further apart where it starts is refused: a spot behind the robot would otherwise send the gripper through it.

Parameters:

| Name         | Type              | Description                                    | Default    |
| ------------ | ----------------- | ---------------------------------------------- | ---------- |
| `arm`        | `int`             | An index into the cell's arms, from 0.         | *required* |
| `position_m` | `Sequence[float]` | Where to, x y z in the world frame, in meters. | *required* |

Raises:

| Type                   | Description                                                                                                                                                         |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | position_m does not hold three values, or one is not finite; the cell has no arm arm; or the way there takes the robot into itself, naming the two links and where. |

#### reset

```
reset(q: Sequence[float]) -> None
```

Snaps every target to where the arms are at `q`, holding, with every goal, speed and held key dropped.

Call it after something other than this app moved the arms.

Parameters:

| Name | Type              | Description                                                           | Default    |
| ---- | ----------------- | --------------------------------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | The measured joint positions, one per slot, anvil.control.Snapshot.q. | *required* |

Raises:

| Type                   | Description                          |
| ---------------------- | ------------------------------------ |
| `TypeError`            | q is not a flat sequence of numbers. |
| `InvalidArgumentError` | q does not cover the arms' slots.    |

### ReplayMode

Bases: `StrEnum`

What of an episode `EpisodeReplay` plays back.

Attributes:

| Name             | Type | Description                                                                                      |
| ---------------- | ---- | ------------------------------------------------------------------------------------------------ |
| `COMMANDS`       |      | Every command a controller was sent, resent to it at the time the loop picked it up.             |
| `JOINT_ANGLES`   |      | Every slot's measured position, tracked through the joint controller: what the arms did.         |
| `DRIVE_COMMANDS` |      | What the drives were told, each slot's position, velocity, gains and feedforward, through joint. |

#### COMMANDS

```
COMMANDS
```

Every command a controller was sent, resent to it at the time the loop picked it up.

They run under the settings in effect now, so other gains, IK or tuning change what the arms do.

#### JOINT_ANGLES

```
JOINT_ANGLES
```

Every slot's measured position, tracked through the `joint` controller: what the arms did.

The grippers are played by motor angle, and positions are linear between samples.

#### DRIVE_COMMANDS

```
DRIVE_COMMANDS
```

What the drives were told, each slot's position, velocity, gains and feedforward, through `joint`.

The loop's guards and limits still apply.

### ReplayOverrides

Bases: `StrEnum`

What a `ReplayMode.COMMANDS` replay does with each command's tuning overrides and IK parameters.

Attributes:

| Name     | Type | Description                                                        |
| -------- | ---- | ------------------------------------------------------------------ |
| `APPLY`  |      | Resends them as recorded.                                          |
| `IGNORE` |      | Strips them, so the settings in effect now apply to every command. |

#### APPLY

```
APPLY
```

Resends them as recorded.

#### IGNORE

```
IGNORE
```

Strips them, so the settings in effect now apply to every command.

### ReplayPhase

Bases: `StrEnum`

Where along its timeline an `EpisodeReplay` is.

Attributes:

| Name          | Type | Description                                                                                |
| ------------- | ---- | ------------------------------------------------------------------------------------------ |
| `APPROACHING` |      | Easing the arms from where they stood to the episode's first pose, under joint_trajectory. |
| `PLAYING`     |      | Playing the episode at its recorded timing, divided by the replay's speed.                 |
| `HOLDING`     |      | Holding where the episode ended, under joint.                                              |

#### APPROACHING

```
APPROACHING
```

Easing the arms from where they stood to the episode's first pose, under `joint_trajectory`.

#### PLAYING

```
PLAYING
```

Playing the episode at its recorded timing, divided by the replay's speed.

#### HOLDING

```
HOLDING
```

Holding where the episode ended, under `joint`.

### ReplayPoll

```
ReplayPoll()
```

Bases: `Struct`

What one `EpisodeReplay.poll` returns: the controller to have active, and the command due now.

Read-only. Make `controller` active, then send `command` with `anvil.control.Runtime.send_command` where there is one.

Get one from `EpisodeReplay.poll`, never by calling the class.

Attributes:

| Name         | Type                                                              | Description                                                                                               |
| ------------ | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `phase`      | `ReplayPhase`                                                     | Where along its timeline the replay is.                                                                   |
| `episode_ns` | `int`                                                             | How far into the episode, in nanoseconds of its own time: 0 while approaching, its duration once holding. |
| `controller` | `type`                                                            | The class of the controller that should be active now, as anvil.control.Runtime.controller takes it.      |
| `command`    | `JointCommand \| JointTrajectoryCommand \| EePoseCommand \| None` | The command due now, for controller; None when nothing new is due.                                        |

#### phase

```
phase: ReplayPhase
```

Where along its timeline the replay is.

#### episode_ns

```
episode_ns: int
```

How far into the episode, in nanoseconds of its own time: 0 while approaching, its duration once holding.

#### controller

```
controller: type
```

The class of the controller that should be active now, as `anvil.control.Runtime.controller` takes it.

It is `anvil.control.JointTrajectoryController` while approaching, the controller each recorded command was for while playing a `ReplayMode.COMMANDS` replay, and `anvil.control.JointController` otherwise.

#### command

```
command: (
    JointCommand
    | JointTrajectoryCommand
    | EePoseCommand
    | None
)
```

The command due now, for `controller`; `None` when nothing new is due.

### EpisodeReplay

```
EpisodeReplay(
    path: Path,
    robot: RobotConfig,
    mode: ReplayMode | str,
    speed: float = 1.0,
    overrides: ReplayOverrides | str = APPLY,
)
```

A recorded episode played on the robot again, polled for the controller to have active and the command due.

Its timeline eases the arms from where they stand to the episode's first pose under `joint_trajectory`, plays the episode at its recorded timing divided by its speed, and holds where it ended under `joint`. Poll it with the time since the first poll and the measured joints, make the controller each poll names active, and send its command. The episode is closed when this object is collected.

Opens the episode at `path` to replay on `robot`, checking it was recorded on the same robot.

Parameters:

| Name        | Type                     | Description                                                                                                              | Default    |
| ----------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `path`      | `Path`                   | The episode, a file the recorder finished.                                                                               | *required* |
| `robot`     | `RobotConfig`            | The running robot, which must have the arms, variants and grippers the episode was recorded on.                          | *required* |
| `mode`      | `ReplayMode \| str`      | What of the episode to play back.                                                                                        | *required* |
| `speed`     | `float`                  | The fraction of the recorded pace to play at, above 0 and up to 1, the episode's own.                                    | `1.0`      |
| `overrides` | `ReplayOverrides \| str` | What a ReplayMode.COMMANDS replay does with each command's tuning overrides and IK parameters; read for that mode alone. | `APPLY`    |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | robot is not an anvil.workcell.RobotConfig.                                                                                                                                                                                                                                                                                                                                                                          |
| `ValueError`           | mode or overrides is a string that names none.                                                                                                                                                                                                                                                                                                                                                                       |
| `NotFoundError`        | Nothing is at path.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `InvalidArgumentError` | The file is not a finished episode, or its workcell does not read in this version; it was recorded on other arms, variants or grippers than robot's; it holds no motion; a ReplayMode.COMMANDS replay of an episode with no controller commands, which one the leader_follower controller drove has none of; a ReplayMode.DRIVE_COMMANDS replay of one that recorded no drive commands; or speed is outside (0, 1\]. |
| `OperationError`       | The file could not be read.                                                                                                                                                                                                                                                                                                                                                                                          |

Methods:

| Name   | Description                                                                  |
| ------ | ---------------------------------------------------------------------------- |
| `poll` | Returns what is due elapsed_ns after the first poll, the arms standing at q. |

Attributes:

| Name         | Type                           | Description                                                                                            |
| ------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `duration_s` | `float`                        | The episode's length, from its first state to its last, in seconds of its own time.                    |
| `hardware`   | `list[dict[str, Any]] \| None` | The physical robot the episode was recorded on, as anvil.hw.Report.hardware has one; None without one. |

#### duration_s

```
duration_s: float
```

The episode's length, from its first state to its last, in seconds of its own time.

It plays over this divided by the replay's speed.

#### hardware

```
hardware: list[dict[str, Any]] | None
```

The physical robot the episode was recorded on, as `anvil.hw.Report.hardware` has one; `None` without one.

It holds per arm its bus, the interface it was placed on and every motor that answered there, with its identity record's `uuid`. An episode recorded without a readiness check, as one in the sim is, has none.

#### poll

```
poll(elapsed_ns: int, q: Sequence[float]) -> ReplayPoll
```

Returns what is due `elapsed_ns` after the first poll, the arms standing at `q`.

The first poll plans the approach from `q`; later polls read `q` only to check it covers the robot.

Parameters:

| Name         | Type              | Description                                                           | Default    |
| ------------ | ----------------- | --------------------------------------------------------------------- | ---------- |
| `elapsed_ns` | `int`             | Time since the first poll, in nanoseconds.                            | *required* |
| `q`          | `Sequence[float]` | The measured joint positions, one per slot, anvil.control.Snapshot.q. | *required* |

Raises:

| Type                   | Description                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------- |
| `TypeError`            | q is not a flat sequence of numbers.                                                            |
| `InvalidArgumentError` | elapsed_ns is negative, or q does not cover the robot's slots.                                  |
| `OperationError`       | The episode's file stopped reading partway, as one changed or damaged since it was opened does. |

### GlovePose

Bases: `StrEnum`

One pose the wearer holds while a glove is calibrated.

Attributes:

| Name     | Type | Description                                |
| -------- | ---- | ------------------------------------------ |
| `FIST`   |      | Fists.                                     |
| `FLAT`   |      | Hands flat, fingers straight and together. |
| `SPREAD` |      | Fingers spread wide.                       |

#### FIST

```
FIST
```

Fists.

#### FLAT

```
FLAT
```

Hands flat, fingers straight and together.

#### SPREAD

```
SPREAD
```

Fingers spread wide.

### GloveProcedure

```
GloveProcedure(
    poses: list[GlovePose], hold_s: float, pairing_s: float
)
```

How the gloves are paired and calibrated, as the SDK runs them. Get one from `glove_procedure`.

Attributes:

| Name        | Type              | Description                                                                                |
| ----------- | ----------------- | ------------------------------------------------------------------------------------------ |
| `poses`     | `list[GlovePose]` | The poses a calibration holds, in the order they are held.                                 |
| `hold_s`    | `float`           | How long each pose is held, in seconds; its reading is the last one before the time is up. |
| `pairing_s` | `float`           | How long GloveTeleop.pair waits for a glove to pair, in seconds.                           |

#### poses

```
poses: list[GlovePose]
```

The poses a calibration holds, in the order they are held.

#### hold_s

```
hold_s: float
```

How long each pose is held, in seconds; its reading is the last one before the time is up.

#### pairing_s

```
pairing_s: float
```

How long `GloveTeleop.pair` waits for a glove to pair, in seconds.

### GloveCalibration

```
GloveCalibration(flat: list[float], bent: list[float])
```

One glove's calibration: each of its twelve bend sensors' raw reading with the hand flat, and bent.

Build one with `from_poses` from the readings at the end of each pose of `glove_procedure`, and keep it by the glove's name: it holds for that glove until it is calibrated again.

Methods:

| Name         | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `from_poses` | Builds a calibration from a glove's twelve sensors at the end of each pose. |

Attributes:

| Name   | Type          | Description                                                                                         |
| ------ | ------------- | --------------------------------------------------------------------------------------------------- |
| `flat` | `list[float]` | Each sensor with the hand flat and the fingers straight and together.                               |
| `bent` | `list[float]` | Each sensor bent as far as it goes: in a fist, or spread for the three sensors between the fingers. |

#### flat

```
flat: list[float]
```

Each sensor with the hand flat and the fingers straight and together.

#### bent

```
bent: list[float]
```

Each sensor bent as far as it goes: in a fist, or spread for the three sensors between the fingers.

#### from_poses

```
from_poses(
    fist: Sequence[float],
    flat: Sequence[float],
    spread: Sequence[float],
) -> Self
```

Builds a calibration from a glove's twelve sensors at the end of each pose.

Parameters:

| Name     | Type              | Description                                                | Default    |
| -------- | ----------------- | ---------------------------------------------------------- | ---------- |
| `fist`   | `Sequence[float]` | The sensors after holding fists, GloveHandPoll.sensors.    | *required* |
| `flat`   | `Sequence[float]` | The sensors after holding the hand flat, fingers together. | *required* |
| `spread` | `Sequence[float]` | The sensors after holding the fingers spread.              | *required* |

Raises:

| Type                   | Description                                                                                                                                    |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | A reading is not a flat sequence of numbers.                                                                                                   |
| `InvalidArgumentError` | A reading is not the glove's twelve sensors; or no sensor moved more than a few counts between the poses: the glove was not worn through them. |

### GloveSlot

```
GloveSlot(side: Side, glove: str | None)
```

One slot of the glove receiver, and the glove paired into it. Get them from `GloveTeleop.slots`.

Attributes:

| Name    | Type          | Description                                                             |
| ------- | ------------- | ----------------------------------------------------------------------- |
| `side`  | `Side`        | Which hand the slot is for.                                             |
| `glove` | `str \| None` | The paired glove's name, e.g. UDXST4810L; None while the slot is empty. |

#### side

```
side: Side
```

Which hand the slot is for.

#### glove

```
glove: str | None
```

The paired glove's name, e.g. `UDXST4810L`; `None` while the slot is empty.

### GloveHandPoll

```
GloveHandPoll(
    side: Side,
    readings: int,
    sensors: list[float],
    calibrated: bool,
    angles: list[float] | None,
    locked: bool,
    a_pressed: bool,
)
```

One hand's part of a `GloveTeleop.poll`: what its glove sent since the last poll, and what that made of it.

Attributes:

| Name         | Type                  | Description                                                                                                     |
| ------------ | --------------------- | --------------------------------------------------------------------------------------------------------------- |
| `side`       | `Side`                | Which hand.                                                                                                     |
| `readings`   | `int`                 | How many readings its glove sent since the last poll; none leaves the rest as the last poll had them.           |
| `sensors`    | `list[float]`         | The glove's twelve bend sensors, raw, as last read: what GloveCalibration.from_poses takes.                     |
| `calibrated` | `bool`                | Whether the hand has a calibration to be driven through, from GloveTeleop.set_calibration.                      |
| `angles`     | `list[float] \| None` | The six angles to send the hand, from 0 closed to 100 open: thumb flexion, thumb abduction, index, middle,      |
| `locked`     | `bool`                | Whether the B button's grasp lock is on: the thumb parks and pinches with the index, and middle, ring and pinky |
| `a_pressed`  | `bool`                | Whether A was pressed since the last poll.                                                                      |

#### side

```
side: Side
```

Which hand.

#### readings

```
readings: int
```

How many readings its glove sent since the last poll; none leaves the rest as the last poll had them.

#### sensors

```
sensors: list[float]
```

The glove's twelve bend sensors, raw, as last read: what `GloveCalibration.from_poses` takes.

#### calibrated

```
calibrated: bool
```

Whether the hand has a calibration to be driven through, from `GloveTeleop.set_calibration`.

#### angles

```
angles: list[float] | None
```

The six angles to send the hand, from 0 closed to 100 open: thumb flexion, thumb abduction, index, middle, ring, pinky; `None` until a calibrated glove has sent a reading that could be used.

#### locked

```
locked: bool
```

Whether the B button's grasp lock is on: the thumb parks and pinches with the index, and middle, ring and pinky close together.

#### a_pressed

```
a_pressed: bool
```

Whether A was pressed since the last poll.

### GloveTeleop

```
GloveTeleop()
```

UDCap gloves, read over their USB receiver, to the six angles of the RealHand L6 on each side.

Constructing it opens the receiver, which has a slot for each hand, and starts the glove paired into each slot streaming. A hand is driven once it has a calibration; then `poll` turns every reading its glove sent into the angles to send it with `anvil.hw.RealHand.send`, at whatever pace the caller polls. B toggles a hand's grasp lock; A is reported for the caller to act on. Nothing moves a hand here. The receiver is closed when this object is collected, and its methods may be called from any thread, one at a time.

The receiver's ports belong to the `dialout` group, which the user has to be in.

Opens the receiver and names the glove in each of its slots, returning once both have answered.

Raises:

| Type             | Description                                                                                                                                                                                                   |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OperationError` | No receiver is plugged in; a port cannot be opened, as one the user may not open or HandDriver holds cannot; the receiver does not answer; or a glove is a hardware revision the SDK has no sensor model for. |

Methods:

| Name              | Description                                                                                             |
| ----------------- | ------------------------------------------------------------------------------------------------------- |
| `slots`           | Returns each slot of the receiver, left first, and the glove paired into it.                            |
| `pair`            | Pairs a glove into side's slot, blocking until one has or GloveProcedure.pairing_s has passed.          |
| `unpair`          | Clears the receiver's pairings, both slots at once, and both hands' calibrations with them.             |
| `set_calibration` | Drives side's hand through calibration of its glove from the next reading on.                           |
| `poll`            | Returns each hand, left then right, after every reading that arrived since the last poll; never blocks. |

#### slots

```
slots() -> list[GloveSlot]
```

Returns each slot of the receiver, left first, and the glove paired into it.

#### pair

```
pair(side: Side) -> None
```

Pairs a glove into `side`'s slot, blocking until one has or `GloveProcedure.pairing_s` has passed.

The slot is put in pairing mode, and the wearer turns the glove on and holds its power button about three seconds, until its lights flash green. A slot that already has a glove must be emptied with `unpair` first. The side's hand loses its calibration, since the glove may be another. No `poll` runs while this blocks.

Parameters:

| Name   | Type   | Description              | Default    |
| ------ | ------ | ------------------------ | ---------- |
| `side` | `Side` | Which slot to pair into. | *required* |

Raises:

| Type             | Description                                                                                                                                                 |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ValueError`     | side is not a side.                                                                                                                                         |
| `OperationError` | No glove paired in time; the receiver said the pairing failed; or the glove that paired is the other hand's, or a revision the SDK has no sensor model for. |

#### unpair

```
unpair() -> None
```

Clears the receiver's pairings, both slots at once, and both hands' calibrations with them.

Raises:

| Type             | Description                                                             |
| ---------------- | ----------------------------------------------------------------------- |
| `OperationError` | The receiver did not confirm, or a slot still reported its glove after. |

#### set_calibration

```
set_calibration(
    side: Side, calibration: GloveCalibration | None
) -> None
```

Drives `side`'s hand through `calibration` of its glove from the next reading on.

Parameters:

| Name          | Type                       | Description                                                                     | Default    |
| ------------- | -------------------------- | ------------------------------------------------------------------------------- | ---------- |
| `side`        | `Side`                     | Which hand.                                                                     | *required* |
| `calibration` | `GloveCalibration \| None` | The calibration of the glove in that side's slot; None drives the hand no more. | *required* |

Raises:

| Type                   | Description                                           |
| ---------------------- | ----------------------------------------------------- |
| `ValueError`           | side is not a side.                                   |
| `InvalidArgumentError` | calibration does not hold twelve values in each half. |

#### poll

```
poll() -> list[GloveHandPoll]
```

Returns each hand, left then right, after every reading that arrived since the last poll; never blocks.

### quest_streamer

```
quest_streamer() -> tuple[str, str] | None
```

Returns the headset streamer app this package ships, as `QuestDeviceConfig.apk` takes it.

Assigned to `QuestDeviceConfig.apk`, it has `QuestDevice` install this version over any other on the headset.

Returns:

| Type                      | Description                                                                                                                                            |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tuple[str, str] \| None` | The .apk's path and its versionName, or None when this package carries none, in which case the app on the headset is left as it was installed by hand. |

### glove_procedure

```
glove_procedure() -> GloveProcedure
```

Returns how the gloves are paired and calibrated: the poses, how long each is held, and the pairing wait.

# anvil.camera

## anvil.camera

The cameras: a UVC camera streaming one mode, or one of the robot's cameras in a sim.

A `Camera` opens a device at one of the modes `modes()` lists and keeps its newest frame on a thread of its own, which any number of `FrameReader`s each read at their own pace. A `SimCamera` is the same over one of the robot's camera links in a sim, rendered at the mode it is asked for. `Cameras` opens every camera a workcell states, once, keyed by its name, from the devices or from a sim. `devices()`, `controls()` and `set_control()` find a host's cameras and read and write their settings.

A frame is a JPEG, the camera's own or, for a camera that sends only YUYV such as a RealSense D405, its frame encoded on the capture thread, with every stamp it was given: the driver's, which is its `t_ns`, and the capture thread's and the reader's, all in nanoseconds on `CLOCK_MONOTONIC`, what `time.monotonic_ns()` reads and the runtime stamps its ticks on, except where `FrameStamps.driver_clock` says the driver's is not.

Classes:

| Name            | Description                                                                                             |
| --------------- | ------------------------------------------------------------------------------------------------------- |
| `DriverClock`   | The clock a driver stamped a frame on, which says what its FrameStamps.driver_ns can be compared with.  |
| `StampSource`   | The moment of a frame a driver says its stamp marks.                                                    |
| `FrameStamps`   | Every stamp a frame has, each kept on its own, all in nanoseconds.                                      |
| `Frame`         | One frame of a camera: its own JPEG and every stamp it was given, as FrameReader.frame returns it.      |
| `FrameCounts`   | What a camera has delivered and dropped since it opened, as Camera.counts returns it.                   |
| `Mode`          | One mode a camera streams: a frame size and a frame rate, as modes() lists a device's.                  |
| `VideoDevice`   | One video node on this host that streams video in, as devices() lists it.                               |
| `ControlKind`   | How a camera control is set.                                                                            |
| `MenuChoice`    | One value a menu control takes, and what the driver calls it.                                           |
| `CameraControl` | One setting a camera offers, with what it holds now, as controls() lists it.                            |
| `Camera`        | A UVC camera streaming one mode, on a thread of its own.                                                |
| `SimCamera`     | One of the robot's cameras in a sim, read as any Camera is.                                             |
| `FrameReader`   | One reader of a camera's frames, with its own place in them, from Camera.reader.                        |
| `Cameras`       | Every camera a workcell states, each opened once and keyed by its name: cameras["head"] is that Camera. |

Functions:

| Name          | Description                                                                                                |
| ------------- | ---------------------------------------------------------------------------------------------------------- |
| `devices`     | Returns every video node this user can open that streams video in as MJPEG or YUYV, sorted by path.        |
| `modes`       | Returns every mode device offers, in MJPEG where it has it, else YUYV, in the order its driver lists them. |
| `controls`    | Returns every setting device offers, in the order its driver lists them, each with what it holds now.      |
| `set_control` | Sets the setting key of device to value now, whether or not the camera streams.                            |

### DriverClock

Bases: `StrEnum`

The clock a driver stamped a frame on, which says what its `FrameStamps.driver_ns` can be compared with.

Attributes:

| Name        | Type | Description                                                                         |
| ----------- | ---- | ----------------------------------------------------------------------------------- |
| `MONOTONIC` |      | CLOCK_MONOTONIC, the clock the runtime stamps its ticks on: every UVC camera's.     |
| `SIM`       |      | Simulated time, the clock the sim stamps its state on.                              |
| `OTHER`     |      | Unknown, or one the driver does not name: not comparable with the runtime's stamps. |

#### MONOTONIC

```
MONOTONIC
```

`CLOCK_MONOTONIC`, the clock the runtime stamps its ticks on: every UVC camera's.

#### SIM

```
SIM
```

Simulated time, the clock the sim stamps its state on.

A sim camera's frame carries the stamp of the tick it shows.

#### OTHER

```
OTHER
```

Unknown, or one the driver does not name: not comparable with the runtime's stamps.

### StampSource

Bases: `StrEnum`

The moment of a frame a driver says its stamp marks.

Attributes:

| Name                | Type | Description                        |
| ------------------- | ---- | ---------------------------------- |
| `END_OF_FRAME`      |      | The end of the frame.              |
| `START_OF_EXPOSURE` |      | The start of the exposure.         |
| `OTHER`             |      | A source the driver does not name. |

#### END_OF_FRAME

```
END_OF_FRAME
```

The end of the frame.

#### START_OF_EXPOSURE

```
START_OF_EXPOSURE
```

The start of the exposure.

uvcvideo claims this while stamping the frame's first USB packet, which is after it; a sim camera's render is the world at one instant, so its exposure starts and ends there.

#### OTHER

```
OTHER
```

A source the driver does not name.

### FrameStamps

```
FrameStamps(
    driver_ns: int,
    driver_clock: DriverClock,
    driver_source: StampSource,
    queued_ns: int,
    dequeued_ns: int,
    published_ns: int,
    read_ns: int,
)
```

Every stamp a frame has, each kept on its own, all in nanoseconds.

`driver_ns` is on the clock `driver_clock` names; the rest are on `CLOCK_MONOTONIC`, what `time.monotonic_ns()` reads.

Attributes:

| Name            | Type          | Description                                                       |
| --------------- | ------------- | ----------------------------------------------------------------- |
| `driver_ns`     | `int`         | The driver's stamp on the frame, on the clock driver_clock names. |
| `driver_clock`  | `DriverClock` | Which clock driver_ns is on.                                      |
| `driver_source` | `StampSource` | Which moment of the frame the driver says driver_ns marks.        |
| `queued_ns`     | `int`         | When the buffer went back to the driver to be filled.             |
| `dequeued_ns`   | `int`         | When the capture thread took the filled buffer from the driver.   |
| `published_ns`  | `int`         | When the capture thread made it the newest frame for readers.     |
| `read_ns`       | `int`         | When the reader that returned it took it.                         |

#### driver_ns

```
driver_ns: int
```

The driver's stamp on the frame, on the clock `driver_clock` names.

A UVC camera's is taken when the frame's first USB packet arrives, after the exposure, whatever `driver_source` says.

#### driver_clock

```
driver_clock: DriverClock
```

Which clock `driver_ns` is on.

#### driver_source

```
driver_source: StampSource
```

Which moment of the frame the driver says `driver_ns` marks.

#### queued_ns

```
queued_ns: int
```

When the buffer went back to the driver to be filled.

The nearest thing to a request, since a UVC camera pushes frames rather than being asked for them.

#### dequeued_ns

```
dequeued_ns: int
```

When the capture thread took the filled buffer from the driver.

#### published_ns

```
published_ns: int
```

When the capture thread made it the newest frame for readers.

#### read_ns

```
read_ns: int
```

When the reader that returned it took it.

### Frame

```
Frame(
    t_ns: int,
    stamps: FrameStamps,
    sequence: int,
    width_px: int,
    height_px: int,
    jpeg: bytes,
)
```

One frame of a camera: its own JPEG and every stamp it was given, as `FrameReader.frame` returns it.

Attributes:

| Name        | Type          | Description                                                                                                    |
| ----------- | ------------- | -------------------------------------------------------------------------------------------------------------- |
| `t_ns`      | `int`         | The frame's timestamp: the driver's, stamps.driver_ns, on the clock stamps.driver_clock names.                 |
| `stamps`    | `FrameStamps` | Every stamp the frame has, t_ns among them.                                                                    |
| `sequence`  | `int`         | The driver's frame counter.                                                                                    |
| `width_px`  | `int`         | Width of the frame, in pixels.                                                                                 |
| `height_px` | `int`         | Height of the frame, in pixels.                                                                                |
| `jpeg`      | `bytes`       | The JPEG: exactly as the camera produced it, or for a camera that sends YUYV, its frame encoded on the capture |

#### t_ns

```
t_ns: int
```

The frame's timestamp: the driver's, `stamps.driver_ns`, on the clock `stamps.driver_clock` names.

#### stamps

```
stamps: FrameStamps
```

Every stamp the frame has, `t_ns` among them.

#### sequence

```
sequence: int
```

The driver's frame counter.

Between two frames one reader returned, a gap is frames the reader skipped or the driver dropped; `Camera.counts` tells the two apart.

#### width_px

```
width_px: int
```

Width of the frame, in pixels.

#### height_px

```
height_px: int
```

Height of the frame, in pixels.

#### jpeg

```
jpeg: bytes
```

The JPEG: exactly as the camera produced it, or for a camera that sends YUYV, its frame encoded on the capture thread.

### FrameCounts

```
FrameCounts(frames: int, dropped: int)
```

What a camera has delivered and dropped since it opened, as `Camera.counts` returns it.

Attributes:

| Name      | Type  | Description                                                               |
| --------- | ----- | ------------------------------------------------------------------------- |
| `frames`  | `int` | Frames delivered, counted as each arrives whether or not anyone reads it. |
| `dropped` | `int` | Frames the driver dropped: the gaps in its Frame.sequence.                |

#### frames

```
frames: int
```

Frames delivered, counted as each arrives whether or not anyone reads it.

#### dropped

```
dropped: int
```

Frames the driver dropped: the gaps in its `Frame.sequence`.

### Mode

```
Mode(width_px: int, height_px: int, fps: int)
```

One mode a camera streams: a frame size and a frame rate, as `modes()` lists a device's.

Attributes:

| Name        | Type  | Description                   |
| ----------- | ----- | ----------------------------- |
| `width_px`  | `int` | Width of a frame, in pixels.  |
| `height_px` | `int` | Height of a frame, in pixels. |
| `fps`       | `int` | Frames per second.            |

#### width_px

```
width_px: int
```

Width of a frame, in pixels.

#### height_px

```
height_px: int
```

Height of a frame, in pixels.

#### fps

```
fps: int
```

Frames per second.

### VideoDevice

```
VideoDevice(device: str, name: str, aliases: list[str])
```

One video node on this host that streams video in, as `devices()` lists it.

Attributes:

| Name      | Type        | Description                                                                      |
| --------- | ----------- | -------------------------------------------------------------------------------- |
| `device`  | `str`       | Where it is: its /dev/v4l/by-path/ link, else the node itself, e.g. /dev/video0. |
| `name`    | `str`       | What the driver calls the camera behind it.                                      |
| `aliases` | `list[str]` | Every other path that reaches the same node right now.                           |

#### device

```
device: str
```

Where it is: its `/dev/v4l/by-path/` link, else the node itself, e.g. `/dev/video0`.

The `by-path` link names the USB port it is plugged into, and so stays put across reboots and replugs into that port. Not its `/dev/v4l/by-id/` link: two cameras of one model often report one serial, and udev then gives the link to whichever was plugged in last.

#### name

```
name: str
```

What the driver calls the camera behind it.

#### aliases

```
aliases: list[str]
```

Every other path that reaches the same node right now.

The node itself and its other `by-path` and `by-id` links, so a camera saved under any of them is still recognized.

### ControlKind

Bases: `StrEnum`

How a camera control is set.

Attributes:

| Name      | Type | Description                                                                           |
| --------- | ---- | ------------------------------------------------------------------------------------- |
| `INTEGER` |      | A number from CameraControl.min to CameraControl.max, in steps of CameraControl.step. |
| `BOOLEAN` |      | On (1) or off (0).                                                                    |
| `MENU`    |      | One of the values in CameraControl.menu.                                              |

#### INTEGER

```
INTEGER
```

A number from `CameraControl.min` to `CameraControl.max`, in steps of `CameraControl.step`.

#### BOOLEAN

```
BOOLEAN
```

On (1) or off (0).

#### MENU

```
MENU
```

One of the values in `CameraControl.menu`.

### MenuChoice

```
MenuChoice(value: int, label: str)
```

One value a menu control takes, and what the driver calls it.

Attributes:

| Name    | Type  | Description                                              |
| ------- | ----- | -------------------------------------------------------- |
| `value` | `int` | What is written to set it.                               |
| `label` | `str` | What the driver calls it, e.g. "Aperture Priority Mode". |

#### value

```
value: int
```

What is written to set it.

#### label

```
label: str
```

What the driver calls it, e.g. `"Aperture Priority Mode"`.

### CameraControl

```
CameraControl(
    key: str,
    name: str,
    kind: ControlKind,
    min: int,
    max: int,
    step: int,
    default: int,
    value: int | None,
    menu: list[MenuChoice],
    inactive: bool,
    read_only: bool,
)
```

One setting a camera offers, with what it holds now, as `controls()` lists it.

The settings are those `v4l2-ctl --list-ctrls-menus` prints, keyed as it keys them.

Attributes:

| Name        | Type               | Description                                                                             |
| ----------- | ------------------ | --------------------------------------------------------------------------------------- |
| `key`       | `str`              | Its name in snake case, as v4l2-ctl keys it, e.g. "white_balance_temperature".          |
| `name`      | `str`              | The driver's name for it, e.g. "White Balance Temperature".                             |
| `kind`      | `ControlKind`      | How it is set.                                                                          |
| `min`       | `int`              | The least value it takes.                                                               |
| `max`       | `int`              | The greatest value it takes.                                                            |
| `step`      | `int`              | The step between the values it takes.                                                   |
| `default`   | `int`              | The value the driver starts it at.                                                      |
| `value`     | `int \| None`      | What it holds now; None where the driver will not say, as for a write-only control.     |
| `menu`      | `list[MenuChoice]` | A menu control's choices, in the order the driver lists them; empty for any other kind. |
| `inactive`  | `bool`             | Whether another control decides it now, as auto exposure decides the exposure time.     |
| `read_only` | `bool`             | Whether it can only be read.                                                            |

#### key

```
key: str
```

Its name in snake case, as `v4l2-ctl` keys it, e.g. `"white_balance_temperature"`.

What `set_control` takes and a workcell saves it under.

#### name

```
name: str
```

The driver's name for it, e.g. `"White Balance Temperature"`.

#### kind

```
kind: ControlKind
```

How it is set.

#### min

```
min: int
```

The least value it takes.

#### max

```
max: int
```

The greatest value it takes.

#### step

```
step: int
```

The step between the values it takes.

#### default

```
default: int
```

The value the driver starts it at.

#### value

```
value: int | None
```

What it holds now; `None` where the driver will not say, as for a write-only control.

#### menu

```
menu: list[MenuChoice]
```

A menu control's choices, in the order the driver lists them; empty for any other kind.

#### inactive

```
inactive: bool
```

Whether another control decides it now, as auto exposure decides the exposure time.

Setting it then may be refused or have no effect.

#### read_only

```
read_only: bool
```

Whether it can only be read.

### Camera

```
Camera(
    device: str,
    mode: Mode,
    controls: Mapping[str, int] | None = None,
)
```

Bases: `Handle`

A UVC camera streaming one mode, on a thread of its own.

It streams MJPEG where the device offers it, passing the camera's own JPEG on, else YUYV, each frame of which the thread encodes to a JPEG: a RealSense D405's color stream is one such.

The thread keeps the newest frame whether or not anyone reads it; read frames through `reader()`, one reader for each consumer that wants every frame. A camera that delivers no frame for five seconds, or whose device is unplugged or fails, stops for good: `error()` says why, and every reader raises it. `close()` stops the stream and releases the device, as garbage collection does, and `with Camera(device=..., mode=...) as camera:` closes it on the way out. Every method but `close` raises `anvil.OperationError` once it is closed.

Opens `device` at `mode` and starts it streaming, returning once its first frame has arrived.

A UVC camera takes about a second to send its first frame, and one that sends none in five seconds is refused. Opening also turns off the camera's `exposure_dynamic_framerate`, wherever it has one, whatever `controls` says: auto exposure would otherwise lengthen the exposure in dim light and drop the frame rate below `mode`'s.

Parameters:

| Name       | Type                        | Description                                                                                                                                                                                                                                                                                                | Default    |
| ---------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `device`   | `str`                       | The video node to open, e.g. /dev/video0 or a VideoDevice.device.                                                                                                                                                                                                                                          | *required* |
| `mode`     | `Mode`                      | The mode to stream, one of those modes(device) lists; anything else is refused.                                                                                                                                                                                                                            | *required* |
| `controls` | `Mapping[str, int] \| None` | Settings to write before it streams, each a CameraControl.key and its value; a setting not named keeps what the camera holds. None or empty writes none. A setting another one decides, such as the exposure time under auto exposure, is written again once the rest are, so their order does not matter. | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A field of mode is negative or over 2\*\*32 - 1; a value in controls is not an integer.                                                                                                                                                                   |
| `NotFoundError`        | Nothing is at device: the camera is unplugged, or was saved on another USB port.                                                                                                                                                                          |
| `BusyError`            | Another program has the camera open.                                                                                                                                                                                                                      |
| `TimedOutError`        | It started streaming but sent no first frame in five seconds.                                                                                                                                                                                             |
| `OperationError`       | device cannot be opened by this user or is not a streaming camera, it does not offer mode (the message lists the modes it does), a setting in controls is one it has not got or will not take, or its settings cannot be listed while controls names any. |

Methods:

| Name     | Description                                                                                             |
| -------- | ------------------------------------------------------------------------------------------------------- |
| `counts` | Returns what this camera has delivered and dropped since it opened: a rate without reading every frame. |
| `error`  | Returns why this camera stopped delivering for good, or None while it streams.                          |
| `reader` | Returns a new FrameReader of this camera.                                                               |
| `close`  | Stops the stream and releases the device; closing twice does nothing.                                   |

Attributes:

| Name     | Type          | Description                                                      |
| -------- | ------------- | ---------------------------------------------------------------- |
| `device` | `str \| None` | The video node it was opened at, as given; None for a SimCamera. |

#### device

```
device: str | None
```

The video node it was opened at, as given; `None` for a `SimCamera`.

#### counts

```
counts() -> FrameCounts
```

Returns what this camera has delivered and dropped since it opened: a rate without reading every frame.

#### error

```
error() -> str | None
```

Returns why this camera stopped delivering for good, or `None` while it streams.

It stops when its device is unplugged or fails, or delivers no frame for five seconds; a `SimCamera` stops when its sim goes. The fix is a new camera once the device is back.

#### reader

```
reader() -> FrameReader
```

Returns a new `FrameReader` of this camera.

#### close

```
close() -> None
```

Stops the stream and releases the device; closing twice does nothing.

Every reader's `frame` then raises `anvil.OperationError` saying the camera is closed.

### SimCamera

```
SimCamera(world: World, name: str, mode: Mode, lens: Lens)
```

Bases: `Camera`

One of the robot's cameras in a sim, read as any `Camera` is.

It renders one of the camera links of the robot in an `anvil.sim.World` at the mode it is asked for. Each frame shows one tick and carries that tick's stamp, on `DriverClock.SIM`. The sim renders a pinhole, so a fisheye lens is a very wide rectilinear view. Its frames stop, and `error()` says why, when the robot it watches is replaced by another or the world goes.

Opens the camera `name` of the robot in `world`, returning once its first frame is in.

Parameters:

| Name    | Type    | Description                                                                                       | Default    |
| ------- | ------- | ------------------------------------------------------------------------------------------------- | ---------- |
| `world` | `World` | The world whose robot it watches.                                                                 | *required* |
| `name`  | `str`   | The robot's camera link, e.g. "left_wrist_cam", one of anvil.workcell.RobotConfig.camera_links(). | *required* |
| `mode`  | `Mode`  | The size and rate to render at; each must be positive.                                            | *required* |
| `lens`  | `Lens`  | The lens to render through, which sets the horizontal field of view.                              | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | A field of mode is negative or over 2\*\*32 - 1.                                                                                                                                           |
| `OperationError`       | No sim answers at world's address, the robot has no camera name (the message names those it has), mode has a zero, the sim is from another release, or no first frame came in six seconds. |

Methods:

| Name     | Description                                                                                             |
| -------- | ------------------------------------------------------------------------------------------------------- |
| `close`  | Stops the stream and releases the device; closing twice does nothing.                                   |
| `counts` | Returns what this camera has delivered and dropped since it opened: a rate without reading every frame. |
| `error`  | Returns why this camera stopped delivering for good, or None while it streams.                          |
| `reader` | Returns a new FrameReader of this camera.                                                               |

Attributes:

| Name   | Type | Description                         |
| ------ | ---- | ----------------------------------- |
| `name` |      | The robot's camera link it renders. |

#### name

```
name
```

The robot's camera link it renders.

#### close

```
close() -> None
```

Stops the stream and releases the device; closing twice does nothing.

Every reader's `frame` then raises `anvil.OperationError` saying the camera is closed.

#### counts

```
counts() -> FrameCounts
```

Returns what this camera has delivered and dropped since it opened: a rate without reading every frame.

#### error

```
error() -> str | None
```

Returns why this camera stopped delivering for good, or `None` while it streams.

It stops when its device is unplugged or fails, or delivers no frame for five seconds; a `SimCamera` stops when its sim goes. The fix is a new camera once the device is back.

#### reader

```
reader() -> FrameReader
```

Returns a new `FrameReader` of this camera.

### FrameReader

```
FrameReader(camera: Camera)
```

Bases: `Handle`

One reader of a camera's frames, with its own place in them, from `Camera.reader`.

`frame()` returns the newest frame newer than the last this reader returned, whatever other readers took, so each consumer that wants every frame takes a reader of its own. A reader slower than the camera skips, and what it missed shows as a gap in `Frame.sequence`. A reader copies each frame into a buffer of its own, so only one thread reads through it; another thread takes another reader. It keeps its camera from being collected, but not from being closed. Every method but `close` raises `anvil.OperationError` once it is closed.

Makes a reader of `camera` that has had no frame yet, so its first is the newest.

Parameters:

| Name     | Type     | Description         | Default    |
| -------- | -------- | ------------------- | ---------- |
| `camera` | `Camera` | The camera to read. | *required* |

Raises:

| Type             | Description       |
| ---------------- | ----------------- |
| `OperationError` | camera is closed. |

Methods:

| Name    | Description                                                                              |
| ------- | ---------------------------------------------------------------------------------------- |
| `frame` | Returns the newest frame this reader has not had, waiting up to timeout_s for one.       |
| `close` | Frees the reader while the camera streams on for its others; closing twice does nothing. |

Attributes:

| Name     | Type | Description          |
| -------- | ---- | -------------------- |
| `camera` |      | The camera it reads. |

#### camera

```
camera
```

The camera it reads.

#### frame

```
frame(timeout_s: float = 1.0) -> Frame | None
```

Returns the newest frame this reader has not had, waiting up to `timeout_s` for one.

Parameters:

| Name        | Type    | Description                                                                                       | Default |
| ----------- | ------- | ------------------------------------------------------------------------------------------------- | ------- |
| `timeout_s` | `float` | How long to wait for a frame, in seconds, to the millisecond; 0 returns only one already waiting. | `1.0`   |

Returns:

| Type            | Description                                                                     |
| --------------- | ------------------------------------------------------------------------------- |
| `Frame \| None` | The frame, stamped read_ns as it is taken, or None when none came in timeout_s. |

Raises:

| Type                   | Description                                                                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | timeout_s is negative.                                                                                                                    |
| `OperationError`       | The camera has stopped, with why: most often its device went away, and the fix is a new Camera once it is back; or the camera was closed. |

#### close

```
close() -> None
```

Frees the reader while the camera streams on for its others; closing twice does nothing.

### Cameras

```
Cameras(
    workcell: WorkcellConfig, world: World | None = None
)
```

Bases: `Mapping[str, Camera]`

Every camera a workcell states, each opened once and keyed by its name: `cameras["head"]` is that `Camera`.

Whatever reads one, a recorder or a viewer, takes a `Camera.reader()` of its own, so each gets every frame. With a sim, each is instead the `SimCamera` of the robot link its `anvil.workcell.CameraConfig.link` names, at its mode, so a workcell a bench runs on metal runs in the sim as it is. `close()` closes them all, as `with anvil.camera.Cameras(workcell=...) as cameras:` does on the way out.

Opens every camera `workcell` states, in turn, each waiting for its first frame.

A camera that will not open, or an interrupt while one opens, closes the ones opened before it; a camera's `anvil.OperationError` is raised again as the same class, naming the camera.

Parameters:

| Name       | Type             | Description                                                                                                     | Default    |
| ---------- | ---------------- | --------------------------------------------------------------------------------------------------------------- | ---------- |
| `workcell` | `WorkcellConfig` | The workcell whose cameras to open.                                                                             | *required* |
| `world`    | `World \| None`  | The sim world to render them in instead of opening their devices, which are left alone; None opens the devices. | `None`     |

Raises:

| Type                   | Description                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | workcell fails its checks, or, with world, a camera has no link, since the sim has no view from it. |
| `NotFoundError`        | A camera's device is not plugged in.                                                                |
| `BusyError`            | Another program has a camera's device open.                                                         |
| `TimedOutError`        | A camera's device sent no first frame in time.                                                      |
| `OperationError`       | A camera did not open, for any reason Camera or SimCamera gives.                                    |

Methods:

| Name    | Description                                                                                      |
| ------- | ------------------------------------------------------------------------------------------------ |
| `close` | Closes every camera, after which each one's readers raise saying so; closing twice does nothing. |

#### close

```
close() -> None
```

Closes every camera, after which each one's readers raise saying so; closing twice does nothing.

### devices

```
devices() -> list[VideoDevice]
```

Returns every video node this user can open that streams video in as MJPEG or YUYV, sorted by path.

A UVC camera's second node, which carries metadata rather than frames, is not one, nor a depth camera's depth and infrared nodes: a RealSense D405 is listed once, by its color node. A node this user may not open is left out, and a warning on the `anvil.camera` logger names it and how to grant access.

### modes

```
modes(device: str) -> list[Mode]
```

Returns every mode `device` offers, in MJPEG where it has it, else YUYV, in the order its driver lists them.

Asks without streaming, so it answers while the camera is open elsewhere.

Parameters:

| Name     | Type  | Description                                               | Default    |
| -------- | ----- | --------------------------------------------------------- | ---------- |
| `device` | `str` | The video node, e.g. /dev/video0 or a VideoDevice.device. | *required* |

Returns:

| Type         | Description                                                                                                                               |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `list[Mode]` | The modes; empty for a node that streams neither MJPEG nor YUYV, such as a depth camera's depth node, or one that is not a camera at all. |

Raises:

| Type                   | Description                                                           |
| ---------------------- | --------------------------------------------------------------------- |
| `InvalidArgumentError` | device is empty.                                                      |
| `NotFoundError`        | Nothing is at device.                                                 |
| `OperationError`       | device cannot be opened by this user, or does not answer the listing. |

### controls

```
controls(device: str) -> list[CameraControl]
```

Returns every setting `device` offers, in the order its driver lists them, each with what it holds now.

Settings are what a number sets, such as brightness, exposure, white balance and focus; buttons, the headings that group the rest, text settings and those the driver has disabled are left out. Reads only, and works while the camera streams.

Parameters:

| Name     | Type  | Description                                               | Default    |
| -------- | ----- | --------------------------------------------------------- | ---------- |
| `device` | `str` | The video node, e.g. /dev/video0 or a VideoDevice.device. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                          |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | device is empty.                                                                                                                                                                                                     |
| `NotFoundError`        | Nothing is at device.                                                                                                                                                                                                |
| `OperationError`       | device cannot be opened by this user, or does not list its settings. Some newer webcams list a setting of a kind the SDK cannot read, such as a region of interest, and none of their settings can be listed or set. |

### set_control

```
set_control(device: str, key: str, value: int) -> None
```

Sets the setting `key` of `device` to `value` now, whether or not the camera streams.

It holds until the camera is unplugged or set again; save it in the workcell's `anvil.workcell.CameraConfig.controls` to have it written every time the camera opens.

Parameters:

| Name     | Type  | Description                                                              | Default    |
| -------- | ----- | ------------------------------------------------------------------------ | ---------- |
| `device` | `str` | The video node, e.g. /dev/video0 or a VideoDevice.device.                | *required* |
| `key`    | `str` | The setting's CameraControl.key, e.g. "brightness".                      | *required* |
| `value`  | `int` | The value to write, from its CameraControl.min to its CameraControl.max. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                 |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | device is empty, or has no setting key (the message lists those it has); value is outside the setting's range; or the driver refused value as invalid. Nothing was written. |
| `NotFoundError`        | Nothing is at device.                                                                                                                                                       |
| `BusyError`            | The driver will not change the setting now, as for one it locks while the camera streams.                                                                                   |
| `OperationError`       | device cannot be opened by this user, or its settings cannot be listed; or the driver refused the write for another reason, as it may for an inactive setting.              |

# anvil.control

## anvil.control

The controllers, what they are commanded with, what they report, how they are tuned, and the loop that runs them.

There are four controllers: `JointController` tracks streamed joint targets, `JointTrajectoryController` plays timed waypoints, `EePoseController` tracks end-effector poses through IK, and `LeaderFollowerController` mirrors leader arms on followers moved by hand. Each is built for an `anvil.workcell.RobotConfig` and is usually handed to a `Runtime`, which ticks it at a fixed rate against a hw interface; one can also be ticked by hand with `activate` and `tick`.

Every per-slot list is indexed by slot: each arm's chain joints in chain order, then its gripper, arm after arm in the order the robot declares them. `MAX_JOINTS`, `MAX_ARMS` and `MAX_WAYPOINTS` bound what one state, command or trajectory carries.

Classes:

| Name                                | Description                                                                                                     |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `JointState`                        | Measured state of every slot for one tick, as the hw interface reads it.                                        |
| `MotorCommand`                      | One slot's command for one tick: the targets and gains the drive's own loop runs on.                            |
| `RobotCommand`                      | The active controller's output for one tick: one MotorCommand per slot, indexed as a JointState is.             |
| `CorrectionOverride`                | What a command replaces of one feedforward term's correction while that command is tracked.                     |
| `JointOverride`                     | What a command replaces of one chain joint's tuning while that command is tracked.                              |
| `GripperOverride`                   | What a command replaces of an arm's gripper tuning while that command is tracked.                               |
| `ArmOverride`                       | What a command replaces of one arm's tuning: its chain joints in chain order, then its gripper.                 |
| `TuningOverride`                    | What a command replaces of its arms' tunings, for as long as that command is the one tracked.                   |
| `GripperTarget`                     | The gripper motor's own target: an angle, and the rate it is to be turning at as it passes it.                  |
| `GripperCommand`                    | What a command asks of one arm's gripper: a jaw opening and the rate it opens at, or the motor's own target.    |
| `JointCommand`                      | The JointController's command: a position and velocity target per slot, and one GripperCommand per arm.         |
| `Provenance`                        | Where each slot's gains and feedforward came from on one tick, as every trace carries it.                       |
| `JointTrace`                        | The JointController's trace of one tick: the command it tracked, and why each slot was sent what it was.        |
| `LeaderFollowerTrace`               | The LeaderFollowerController's trace of one tick: what each follower was sent from, and why.                    |
| `Waypoint`                          | One point of a JointTrajectoryCommand: where every slot is at one time, and how fast it moves through there.    |
| `JointTrajectoryCommand`            | The JointTrajectoryController's command: timed waypoints for every slot, played once from when it is taken.     |
| `JointTrajectoryTrace`              | The JointTrajectoryController's trace of one tick: its progress along the trajectory and the tracking error.    |
| `ArmTarget`                         | One arm's target for the EePoseController: a world-frame pose for its tool frame, and what to ask its gripper.  |
| `EePoseCommand`                     | The EePoseController's command: one ArmTarget per arm, in the robot's arm order.                                |
| `EePoseTrace`                       | The EePoseController's trace of one tick: the command it tracked, and the IK behind its targets.                |
| `GripperMap`                        | A gripper's linear map from jaw opening to motor angle, as its description states it.                           |
| `GripperTuning`                     | The gripper motor's tuning: its gains, the grip assist, and the map from a jaw opening to its motor angle.      |
| `LimitGuards`                       | Which of the limits an arm's description states the tick holds the arm's chain targets to.                      |
| `SpeedLimits`                       | How fast a controller moves an arm's chain targets: each joint's, and the tool frame's.                         |
| `JointGuards`                       | What the tick clamps an arm to, whatever asked for it: the arm's own tuning, or a command's override.           |
| `ArmTuning`                         | One arm's tuning: gains and gravity correction per chain joint, the gripper's tuning, speed limits and guards.  |
| `JointControllerSettings`           | What the JointController is configured with: one ArmTuning per arm.                                             |
| `JointTrajectoryControllerSettings` | What the JointTrajectoryController is configured with: one ArmTuning per arm.                                   |
| `ForceFeedback`                     | How hard each leader is pulled toward its follower, so the operator's hand feels what holds the follower back.  |
| `LeaderFollowerControllerSettings`  | What the LeaderFollowerController is configured with: a tuning per arm, the force feedback, and the engagement. |
| `EePoseControllerSettings`          | What the EePoseController is configured with: one ArmTuning and one set of IK parameters per arm.               |
| `ControlArm`                        | One arm as a controller drives it: the slots it owns, its chain, and the tuning its description states.         |
| `CollisionClosing`                  | The link whose closing the collision stop held the arms for, what on, and how near, from CollisionHold.         |
| `CollisionHold`                     | Why the collision stop held the arms, as CollisionStop.last_stop reports it.                                    |
| `CollisionStop`                     | A guard that holds the arms short of hitting themselves or a keep-out box, whichever controller drives them.    |
| `Controller`                        | What the four controllers share: built for a robot, warm-started, ticked and retuned the same way.              |
| `JointController`                   | The controller that tracks streamed joint targets, one JointCommand at a time, for every arm of a robot.        |
| `JointTrajectoryController`         | The controller that plays a JointTrajectoryCommand's timed waypoints and holds the last one.                    |
| `EePoseController`                  | The controller that tracks streamed end-effector poses, one EePoseCommand at a time, solving each with IK.      |
| `LeaderFollowerController`          | The controller that mirrors each leader arm, moved by hand, on the follower on its side, every tick.            |
| `ControllerHandle`                  | A controller a Runtime holds, reached through that runtime.                                                     |
| `HwFaultError`                      | Raised by an anvil.control.Runtime call that would drive the arms or read their state, once the loop faulted.   |
| `JointTrajectoryHandle`             | The handle to a runtime's JointTrajectoryController, which can also play a trajectory through to its end.       |
| `LinkCounts`                        | Frames the hw interface sent each drive, the replies that came back and the sends the kernel refused.           |
| `LoopTiming`                        | Where one tick of the loop's time went, as marks and the spans between them.                                    |
| `PreemptedError`                    | Raised by anvil.control.JointTrajectoryHandle.play when something took over before the trajectory played out.   |
| `Runtime`                           | The control loop, running on a thread of its own at a fixed rate and holding every controller it was given.     |
| `Snapshot`                          | A copy of what the loop last measured, wrote and did, taken when Runtime.latest was called.                     |
| `StoppedShortError`                 | Raised by anvil.control.JointTrajectoryHandle.play when the collision stop slowed the trajectory, ending it.    |

Functions:

| Name           | Description                                                                        |
| -------------- | ---------------------------------------------------------------------------------- |
| `control_arms` | Returns the arms of robot as the controller class kind drives them, in slot order. |

Attributes:

| Name            | Type  | Description                                                                                   |
| --------------- | ----- | --------------------------------------------------------------------------------------------- |
| `MAX_JOINTS`    | `int` | Most slots a state, a command or a trace carries: every arm's chain joints, then its gripper. |
| `MAX_ARMS`      | `int` | Most arms one robot drives, and so the most entries a per-arm list takes.                     |
| `MAX_WAYPOINTS` | `int` | Most waypoints one JointTrajectoryCommand carries.                                            |

### MAX_JOINTS

```
MAX_JOINTS: int = 32
```

Most slots a state, a command or a trace carries: every arm's chain joints, then its gripper.

### MAX_ARMS

```
MAX_ARMS: int = 4
```

Most arms one robot drives, and so the most entries a per-arm list takes.

### MAX_WAYPOINTS

```
MAX_WAYPOINTS: int = 16
```

Most waypoints one `JointTrajectoryCommand` carries.

### ControllerSettings

```
ControllerSettings = (
    JointControllerSettings
    | JointTrajectoryControllerSettings
    | EePoseControllerSettings
    | LeaderFollowerControllerSettings
)
```

Any of the four controllers' settings: what a controller's `settings()` returns and its `configure` takes.

### JointState

```
JointState(
    q: Sequence[float],
    qd: Sequence[float] = (),
    tau: Sequence[float] = (),
    num_joints: int | None = None,
)
```

Bases: `Struct`

Measured state of every slot for one tick, as the hw interface reads it.

Each list is one entry per slot, `num_joints` of them, in slot order. One comes from a `Snapshot`, or is built here to tick a controller by hand.

Builds a state to tick a controller with by hand; its status codes and temperatures read zero.

Parameters:

| Name         | Type              | Description                                                              | Default    |
| ------------ | ----------------- | ------------------------------------------------------------------------ | ---------- |
| `q`          | `Sequence[float]` | Position per slot, in radians.                                           | *required* |
| `qd`         | `Sequence[float]` | Velocity per slot, in radians per second; zero for a slot past the list. | `()`       |
| `tau`        | `Sequence[float]` | Torque per slot, in newton meters; zero for a slot past the list.        | `()`       |
| `num_joints` | `int \| None`     | Slots the state counts, 0 to MAX_JOINTS; None for len(q).                | `None`     |

Raises:

| Type                   | Description                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------- |
| `TypeError`            | A list holds something other than numbers.                                             |
| `InvalidArgumentError` | A list has more than MAX_JOINTS entries, or num_joints is negative or past MAX_JOINTS. |

Attributes:

| Name            | Type          | Description                                                                                            |
| --------------- | ------------- | ------------------------------------------------------------------------------------------------------ |
| `num_joints`    | `int`         | Slots the state counts.                                                                                |
| `q`             | `list[float]` | Position per slot, in radians.                                                                         |
| `qd`            | `list[float]` | Velocity per slot, in radians per second.                                                              |
| `tau`           | `list[float]` | Torque per slot as the drive estimates it, in newton meters.                                           |
| `status`        | `list[int]`   | Drive status code per slot, verbatim from the drive; status_names decodes them.                        |
| `status_names`  | `list[str]`   | Drive status per slot by name: Enabled, Disabled, CommunicationLoss, or a fault such as Overcurrent.   |
| `status_faults` | `list[bool]`  | Whether each slot's drive reports a fault: any status but Enabled or Disabled, unknown codes included. |
| `temp_mos_c`    | `list[int]`   | Driver-stage temperature per slot, in degrees Celsius.                                                 |
| `temp_rotor_c`  | `list[int]`   | Winding temperature per slot, in degrees Celsius.                                                      |

#### num_joints

```
num_joints: int
```

Slots the state counts.

#### q

```
q: list[float]
```

Position per slot, in radians.

#### qd

```
qd: list[float]
```

Velocity per slot, in radians per second.

#### tau

```
tau: list[float]
```

Torque per slot as the drive estimates it, in newton meters.

#### status

```
status: list[int]
```

Drive status code per slot, verbatim from the drive; `status_names` decodes them.

#### status_names

```
status_names: list[str]
```

Drive status per slot by name: `Enabled`, `Disabled`, `CommunicationLoss`, or a fault such as `Overcurrent`.

A code the drive family does not define reads as hex.

#### status_faults

```
status_faults: list[bool]
```

Whether each slot's drive reports a fault: any status but `Enabled` or `Disabled`, unknown codes included.

#### temp_mos_c

```
temp_mos_c: list[int]
```

Driver-stage temperature per slot, in degrees Celsius.

#### temp_rotor_c

```
temp_rotor_c: list[int]
```

Winding temperature per slot, in degrees Celsius.

### MotorCommand

```
MotorCommand(
    position: float = 0.0,
    velocity: float = 0.0,
    kp: float = 0.0,
    kd: float = 0.0,
    feedforward_torque: float = 0.0,
)
```

Bases: `Struct`

One slot's command for one tick: the targets and gains the drive's own loop runs on.

The drive applies `kp * (position - measured) + kd * (velocity - measured) + feedforward_torque`. One read from a `RobotCommand`'s `joints` is that command's own entry, so assigning a field changes what the slot is sent.

Builds one slot's command; every field left out is zero, which leaves the drive limp.

Parameters:

| Name                 | Type    | Description                                               | Default |
| -------------------- | ------- | --------------------------------------------------------- | ------- |
| `position`           | `float` | Position target, in radians.                              | `0.0`   |
| `velocity`           | `float` | Velocity target, in radians per second.                   | `0.0`   |
| `kp`                 | `float` | Position gain, in newton meters per radian.               | `0.0`   |
| `kd`                 | `float` | Velocity gain, in newton meters per radian per second.    | `0.0`   |
| `feedforward_torque` | `float` | Torque added on top of the gains' pull, in newton meters. | `0.0`   |

Attributes:

| Name                 | Type    | Description                                               |
| -------------------- | ------- | --------------------------------------------------------- |
| `position`           | `float` | Position target, in radians.                              |
| `velocity`           | `float` | Velocity target, in radians per second.                   |
| `kp`                 | `float` | Position gain, in newton meters per radian.               |
| `kd`                 | `float` | Velocity gain, in newton meters per radian per second.    |
| `feedforward_torque` | `float` | Torque added on top of the gains' pull, in newton meters. |

#### position

```
position: float = 0.0
```

Position target, in radians.

#### velocity

```
velocity: float = 0.0
```

Velocity target, in radians per second.

#### kp

```
kp: float = 0.0
```

Position gain, in newton meters per radian.

#### kd

```
kd: float = 0.0
```

Velocity gain, in newton meters per radian per second.

#### feedforward_torque

```
feedforward_torque: float = 0.0
```

Torque added on top of the gains' pull, in newton meters.

### RobotCommand

```
RobotCommand(
    num_joints: int, joints: Sequence[MotorCommand] = ()
)
```

Bases: `Struct`

The active controller's output for one tick: one `MotorCommand` per slot, indexed as a `JointState` is.

One comes from a controller's `tick` or a `Snapshot`'s `robot_command`, or is built here to hand `activate` what was last sent. `joints` reads as the command's own entries, so `robot_cmd.joints[3].kp = 20.0` changes what that slot is sent; assigning a shorter list back leaves the slots past it limp.

Builds a command for `num_joints` slots.

Parameters:

| Name         | Type                     | Description                                                                              | Default    |
| ------------ | ------------------------ | ---------------------------------------------------------------------------------------- | ---------- |
| `num_joints` | `int`                    | Slots commanded, 0 to MAX_JOINTS.                                                        | *required* |
| `joints`     | `Sequence[MotorCommand]` | One MotorCommand per slot; a slot past the list is limp: zero gains, targets and torque. | `()`       |

Raises:

| Type                   | Description                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | num_joints is negative or past MAX_JOINTS, or joints has more than MAX_JOINTS entries. |

Attributes:

| Name         | Type                 | Description                                                                                            |
| ------------ | -------------------- | ------------------------------------------------------------------------------------------------------ |
| `num_joints` | `int`                | Slots commanded, 0 to MAX_JOINTS.                                                                      |
| `joints`     | `list[MotorCommand]` | One MotorCommand per slot, the command's own: assigning a field of one changes what that slot is sent. |

#### num_joints

```
num_joints: int
```

Slots commanded, 0 to `MAX_JOINTS`.

Raises:

| Type                   | Description                                                 |
| ---------------------- | ----------------------------------------------------------- |
| `InvalidArgumentError` | On assignment, a count that is negative or past MAX_JOINTS. |

#### joints

```
joints: list[MotorCommand]
```

One `MotorCommand` per slot, the command's own: assigning a field of one changes what that slot is sent.

Raises:

| Type                   | Description                                            |
| ---------------------- | ------------------------------------------------------ |
| `InvalidArgumentError` | On assignment, a list of more than MAX_JOINTS entries. |

### CorrectionOverride

```
CorrectionOverride(
    scale: float | None = None,
    offset_nm: float | None = None,
)
```

What a command replaces of one feedforward term's correction while that command is tracked.

The term is sent as `scale` times the term as computed, plus `offset_nm`; a field left `None` keeps the tuning's. A command refuses a scale that is negative or not finite, and an offset that is not finite, when it is built.

Attributes:

| Name        | Type            | Description                                                                                    |
| ----------- | --------------- | ---------------------------------------------------------------------------------------------- |
| `scale`     | `float \| None` | What the term is multiplied by: 1 sends it as computed, 0 none of it; None keeps the tuning's. |
| `offset_nm` | `float \| None` | What is added to the term after, in newton meters; None keeps the tuning's.                    |

#### scale

```
scale: float | None = None
```

What the term is multiplied by: 1 sends it as computed, 0 none of it; `None` keeps the tuning's.

#### offset_nm

```
offset_nm: float | None = None
```

What is added to the term after, in newton meters; `None` keeps the tuning's.

### JointOverride

```
JointOverride(
    kp: float | None = None,
    kd: float | None = None,
    gravity: CorrectionOverride = CorrectionOverride(
        scale=None, offset_nm=None
    ),
    tau_add_nm: float | None = None,
)
```

What a command replaces of one chain joint's tuning while that command is tracked.

A field left `None` keeps the tuning's. A command refuses a gain that is negative or not finite, and a torque that is not finite, when it is built.

Attributes:

| Name         | Type                 | Description                                                                                                    |
| ------------ | -------------------- | -------------------------------------------------------------------------------------------------------------- |
| `kp`         | `float \| None`      | Position gain in place of the tuning's, stepped to at once rather than slewed, and still capped by the guards. |
| `kd`         | `float \| None`      | Velocity gain in place of the tuning's, as kp is.                                                              |
| `gravity`    | `CorrectionOverride` | What replaces the fields of the joint's gravity correction it sets.                                            |
| `tau_add_nm` | `float \| None`      | Torque added to the feedforward after every term, in newton meters: it rides on top of the system's own.       |

#### kp

```
kp: float | None = None
```

Position gain in place of the tuning's, stepped to at once rather than slewed, and still capped by the guards.

#### kd

```
kd: float | None = None
```

Velocity gain in place of the tuning's, as `kp` is.

#### gravity

```
gravity: CorrectionOverride = CorrectionOverride(
    scale=None, offset_nm=None
)
```

What replaces the fields of the joint's gravity correction it sets.

#### tau_add_nm

```
tau_add_nm: float | None = None
```

Torque added to the feedforward after every term, in newton meters: it rides on top of the system's own.

### GripperOverride

```
GripperOverride(
    kp: float | None = None,
    kd: float | None = None,
    assist: CorrectionOverride = CorrectionOverride(
        scale=None, offset_nm=None
    ),
    tau_add_nm: float | None = None,
)
```

What a command replaces of an arm's gripper tuning while that command is tracked.

A field left `None` keeps the tuning's. A command refuses a gain that is negative or not finite, and a torque that is not finite, when it is built.

Attributes:

| Name         | Type                 | Description                                                                                             |
| ------------ | -------------------- | ------------------------------------------------------------------------------------------------------- |
| `kp`         | `float \| None`      | Gripper position gain in place of the tuning's.                                                         |
| `kd`         | `float \| None`      | Gripper velocity gain in place of the tuning's.                                                         |
| `assist`     | `CorrectionOverride` | A correction of the grip assist's torque, which the tuning sends as computed: scale = 0.0 is no assist. |
| `tau_add_nm` | `float \| None`      | Torque added to the gripper's feedforward after the assist, in newton meters.                           |

#### kp

```
kp: float | None = None
```

Gripper position gain in place of the tuning's.

#### kd

```
kd: float | None = None
```

Gripper velocity gain in place of the tuning's.

#### assist

```
assist: CorrectionOverride = CorrectionOverride(
    scale=None, offset_nm=None
)
```

A correction of the grip assist's torque, which the tuning sends as computed: `scale = 0.0` is no assist.

#### tau_add_nm

```
tau_add_nm: float | None = None
```

Torque added to the gripper's feedforward after the assist, in newton meters.

### ArmOverride

```
ArmOverride(
    name: str,
    side: str | None,
    role: str,
    joints: tuple[JointOverride, ...],
    gripper: GripperOverride = GripperOverride(
        kp=None,
        kd=None,
        assist=CorrectionOverride(
            scale=None, offset_nm=None
        ),
        tau_add_nm=None,
    ),
)
```

What a command replaces of one arm's tuning: its chain joints in chain order, then its gripper.

One comes from a `TuningOverride`, which builds one for every arm of a robot.

Attributes:

| Name      | Type                        | Description                                                                              |
| --------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| `name`    | `str`                       | The arm's name, as its anvil.workcell.ArmConfig states it.                               |
| `side`    | `str \| None`               | The arm's side, as its anvil.workcell.ArmConfig states it; None for an arm with no side. |
| `role`    | `str`                       | The arm's role, as its anvil.workcell.ArmConfig states it.                               |
| `joints`  | `tuple[JointOverride, ...]` | One JointOverride per chain joint, in chain order.                                       |
| `gripper` | `GripperOverride`           | What the command replaces of the gripper's tuning.                                       |

#### name

```
name: str
```

The arm's name, as its `anvil.workcell.ArmConfig` states it.

#### side

```
side: str | None
```

The arm's side, as its `anvil.workcell.ArmConfig` states it; `None` for an arm with no side.

#### role

```
role: str
```

The arm's role, as its `anvil.workcell.ArmConfig` states it.

#### joints

```
joints: tuple[JointOverride, ...]
```

One `JointOverride` per chain joint, in chain order.

#### gripper

```
gripper: GripperOverride = GripperOverride(
    kp=None,
    kd=None,
    assist=CorrectionOverride(scale=None, offset_nm=None),
    tau_add_nm=None,
)
```

What the command replaces of the gripper's tuning.

### TuningOverride

```
TuningOverride(robot: RobotConfig)
```

What a command replaces of its arms' tunings, for as long as that command is the one tracked.

Built for a robot, it is shaped like it and addressed the way its configuration is: `arms` in slot order, `arms_where` and `arm` picking arms as `anvil.workcell.RobotConfig` does, and each arm's `joints` in chain order. Every field starts `None`, which keeps the tuning's; a joint the arm does not have is an `IndexError` where it is asked for. A `JointCommand` and an `EePoseCommand` carry one; a `JointTrajectoryCommand` does not.

It lasts while the command carrying it is tracked, so every command carries it: one without hands the slots straight back to their tunings, and a stream that goes stale eases them back over the tuning's slew.

Example

```
overrides = anvil.control.TuningOverride(robot=robot)
overrides.arms[0].joints[2].kp = 2.0
overrides.arm(name="left").gripper.assist.scale = 0.0
runtime.send_command(command=anvil.control.EePoseCommand(targets=arm_targets, overrides=overrides))
```

Builds an override that replaces nothing, for every arm of `robot` and every chain joint each has.

Parameters:

| Name    | Type          | Description                                  | Default    |
| ------- | ------------- | -------------------------------------------- | ---------- |
| `robot` | `RobotConfig` | The robot whose arms the override addresses. | *required* |

Methods:

| Name         | Description                                                           |
| ------------ | --------------------------------------------------------------------- |
| `arms_where` | Returns the arms of the side and the role asked for, in slot order.   |
| `arm`        | Returns the arm named name, or None for a name the robot has none of. |

Attributes:

| Name   | Type | Description                             |
| ------ | ---- | --------------------------------------- |
| `arms` |      | One ArmOverride per arm, in slot order. |

#### arms

```
arms
```

One `ArmOverride` per arm, in slot order.

#### arms_where

```
arms_where(
    side: str | None = None, role: str | None = None
) -> list[ArmOverride]
```

Returns the arms of the side and the role asked for, in slot order.

Picks them as `anvil.workcell.RobotConfig.arms_where` does.

Parameters:

| Name   | Type          | Description                     | Default |
| ------ | ------------- | ------------------------------- | ------- |
| `side` | `str \| None` | The side to pick; None for any. | `None`  |
| `role` | `str \| None` | The role to pick; None for any. | `None`  |

Returns:

| Type                | Description                                         |
| ------------------- | --------------------------------------------------- |
| `list[ArmOverride]` | The arms that match; an empty list where none does. |

#### arm

```
arm(name: str) -> ArmOverride | None
```

Returns the arm named `name`, or `None` for a name the robot has none of.

Parameters:

| Name   | Type  | Description                                                | Default    |
| ------ | ----- | ---------------------------------------------------------- | ---------- |
| `name` | `str` | The arm's name, as its anvil.workcell.ArmConfig states it. | *required* |

### GripperTarget

```
GripperTarget(
    position_rad: float, velocity_radps: float = 0.0
)
```

Bases: `Struct`

The gripper motor's own target: an angle, and the rate it is to be turning at as it passes it.

A `GripperCommand` carries one for an application that would rather drive the motor than ask for a jaw opening. An angle past the gripper's travel is held at the end of it. Read-only once built.

Builds a motor target; a value that is not finite is refused when a command carrying it is built.

Parameters:

| Name             | Type    | Description                                                                                                                                                                 | Default    |
| ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `position_rad`   | `float` | The motor angle asked for, in radians.                                                                                                                                      | *required* |
| `velocity_radps` | `float` | The rate the motor is to be turning at as it passes the angle, in radians per second, sent to the drive beside it; zero asks it to arrive at rest, not to hold where it is. | `0.0`      |

Attributes:

| Name             | Type    | Description                                                                                              |
| ---------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `position_rad`   | `float` | The motor angle asked for, in radians.                                                                   |
| `velocity_radps` | `float` | The rate the motor is to be turning at as it passes the angle, in radians per second; 0 arrives at rest. |

#### position_rad

```
position_rad: float
```

The motor angle asked for, in radians.

#### velocity_radps

```
velocity_radps: float
```

The rate the motor is to be turning at as it passes the angle, in radians per second; 0 arrives at rest.

### GripperCommand

```
GripperCommand(
    opening_m: float = 0.0,
    speed_mps: float = 0.0,
    motor: GripperTarget | None = None,
)
```

Bases: `Struct`

What a command asks of one arm's gripper: a jaw opening and the rate it opens at, or the motor's own target.

Every command that drives a gripper carries the same ask, so a gripper is asked for the same way everywhere: an `ArmTarget` holds one, and a `JointCommand` one per arm. An opening is the width between the jaws, which the gripper's map turns into a motor angle; one past the gripper's travel is held at the end of it. Read-only once built.

Builds the ask; a value that is read and not finite is refused when a command carrying it is built.

Parameters:

| Name        | Type                    | Description                                                                                                                                                                                                   | Default |
| ----------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `opening_m` | `float`                 | The jaw opening asked for, in meters; not read when motor is given.                                                                                                                                           | `0.0`   |
| `speed_mps` | `float`                 | The rate the jaws are to be opening at as they pass opening_m, in meters per second, sent to the drive beside it; zero asks them to arrive at rest, not to hold where they are. Not read when motor is given. | `0.0`   |
| `motor`     | `GripperTarget \| None` | The motor's own angle and rate, driven instead of the opening; None drives the opening.                                                                                                                       | `None`  |

Attributes:

| Name        | Type                    | Description                                                                                             |
| ----------- | ----------------------- | ------------------------------------------------------------------------------------------------------- |
| `opening_m` | `float`                 | The jaw opening asked for, in meters.                                                                   |
| `speed_mps` | `float`                 | The rate the jaws are to be opening at as they pass opening_m, in meters per second; 0 arrives at rest. |
| `motor`     | `GripperTarget \| None` | The motor target driven instead of the opening; None where the opening is what was asked.               |

#### opening_m

```
opening_m: float = 0.0
```

The jaw opening asked for, in meters.

#### speed_mps

```
speed_mps: float = 0.0
```

The rate the jaws are to be opening at as they pass `opening_m`, in meters per second; 0 arrives at rest.

#### motor

```
motor: GripperTarget | None = None
```

The motor target driven instead of the opening; `None` where the opening is what was asked.

### JointCommand

```
JointCommand(
    q: Sequence[float],
    qd: Sequence[float],
    grips: Sequence[GripperCommand],
    overrides: TuningOverride | None = None,
)
```

Bases: `Struct`

The `JointController`'s command: a position and velocity target per slot, and one `GripperCommand` per arm.

The per-slot lists are indexed as a `JointState` is. A gripper's slot in `q` and `qd` is not read: the gripper is asked for by its arm's `GripperCommand`, since what an application has is a jaw opening, not a motor angle. A position past a joint's limit is commanded at the limit, at rest, unless the arm's `LimitGuards` let it past. Read-only once built.

The controller takes a command only when it covers every slot and every arm of the robot; `Runtime.send_command` refuses one that does not, and a controller ticked by hand holds its last target instead.

Builds a command, checking that a drive can be sent it.

Parameters:

| Name        | Type                       | Description                                                                                                                                                          | Default    |
| ----------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `q`         | `Sequence[float]`          | Position target per slot, in radians; its length is the slot count, num_joints.                                                                                      | *required* |
| `qd`        | `Sequence[float]`          | Velocity target per slot, in radians per second, one per entry of q.                                                                                                 | *required* |
| `grips`     | `Sequence[GripperCommand]` | One GripperCommand per arm, in the robot's arm order. It must speak for every arm, or the controller does not take the command: an arm left out would be asked shut. | *required* |
| `overrides` | `TuningOverride \| None`   | What this command replaces of the arms' tunings while it is tracked; None replaces nothing.                                                                          | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `TypeError`            | grips is one GripperCommand rather than a list, or q or qd holds something other than numbers.                                                                                                                                                                                                   |
| `InvalidArgumentError` | q is empty or longer than MAX_JOINTS, qd is not one entry per entry of q, an entry is not finite, grips has more than MAX_ARMS entries or one whose value that is read is not finite, or overrides sets a gain or scale that is negative or not finite or a torque or offset that is not finite. |

Attributes:

| Name         | Type                   | Description                                                             |
| ------------ | ---------------------- | ----------------------------------------------------------------------- |
| `num_joints` | `int`                  | Slots commanded: len(q) at construction.                                |
| `num_arms`   | `int`                  | Arms whose grippers the command speaks for: len(grips) at construction. |
| `q`          | `list[float]`          | Position target per slot, in radians.                                   |
| `qd`         | `list[float]`          | Velocity target per slot, in radians per second.                        |
| `grips`      | `list[GripperCommand]` | What each arm's gripper is asked, in the robot's arm order.             |

#### num_joints

```
num_joints: int
```

Slots commanded: `len(q)` at construction.

#### num_arms

```
num_arms: int
```

Arms whose grippers the command speaks for: `len(grips)` at construction.

#### q

```
q: list[float]
```

Position target per slot, in radians.

#### qd

```
qd: list[float]
```

Velocity target per slot, in radians per second.

#### grips

```
grips: list[GripperCommand]
```

What each arm's gripper is asked, in the robot's arm order.

### Provenance

```
Provenance()
```

Bases: `Counted`

Where each slot's gains and feedforward came from on one tick, as every trace carries it.

Each feedforward term as computed and as sent, what a command added after them, and the gains the tuning's slew asked for before a command's override or a guard. What was sent in the end is the tick's `RobotCommand`; between the two, a recording says which layer moved a joint. Each list is one entry per slot, and a slot the controller does not drive reads zero. One comes from a trace's `provenance`.

Get one from a trace's `provenance`, never by calling the class.

Attributes:

| Name               | Type          | Description                                                                                        |
| ------------------ | ------------- | -------------------------------------------------------------------------------------------------- |
| `tau_gravity`      | `list[float]` | Chain gravity per slot at the measured configuration, uncorrected, in newton meters.               |
| `tau_gravity_sent` | `list[float]` | The gravity term per slot as sent, after the tuning's correction or a command's, in newton meters. |
| `tau_assist`       | `list[float]` | The grip assist on a gripper's slot as its law computes it, in newton meters; zero on other slots. |
| `tau_assist_sent`  | `list[float]` | The grip assist on a gripper's slot as sent, after a command's correction, in newton meters.       |
| `tau_added`        | `list[float]` | What a command added to each slot's feedforward after every term, in newton meters.                |
| `kp_tuned`         | `list[float]` | The position gain per slot the tuning's slew asked for, before any override or guard.              |
| `kd_tuned`         | `list[float]` | The velocity gain per slot the tuning's slew asked for, before any override or guard.              |

#### tau_gravity

```
tau_gravity: list[float]
```

Chain gravity per slot at the measured configuration, uncorrected, in newton meters.

#### tau_gravity_sent

```
tau_gravity_sent: list[float]
```

The gravity term per slot as sent, after the tuning's correction or a command's, in newton meters.

#### tau_assist

```
tau_assist: list[float]
```

The grip assist on a gripper's slot as its law computes it, in newton meters; zero on other slots.

#### tau_assist_sent

```
tau_assist_sent: list[float]
```

The grip assist on a gripper's slot as sent, after a command's correction, in newton meters.

#### tau_added

```
tau_added: list[float]
```

What a command added to each slot's feedforward after every term, in newton meters.

#### kp_tuned

```
kp_tuned: list[float]
```

The position gain per slot the tuning's slew asked for, before any override or guard.

#### kd_tuned

```
kd_tuned: list[float]
```

The velocity gain per slot the tuning's slew asked for, before any override or guard.

### JointTrace

```
JointTrace()
```

Bases: `Counted`

The `JointController`'s trace of one tick: the command it tracked, and why each slot was sent what it was.

One comes from `JointController.tick` or a `Snapshot`'s trace; each per-slot list is one entry per slot. Read-only.

Get one from `JointController.tick` or a `Snapshot`'s trace, never by calling the class.

Attributes:

| Name                  | Type         | Description                                                                                             |
| --------------------- | ------------ | ------------------------------------------------------------------------------------------------------- |
| `command_seq`         | `int`        | The sequence number of the command tracked this tick; 0 until the first command is taken.               |
| `command_age_ns`      | `int`        | Age of the command tracked at this tick, in nanoseconds, on the state's clock.                          |
| `provenance`          | `Provenance` | Where each slot's gains and feedforward came from this tick.                                            |
| `gain_ramp`           | `float`      | How far the gains have slewed toward what the tunings ask for: 0 at the request, 1 once they are there. |
| `guards_fired`        | `int`        | The slots a guard clamped this tick, as a bit mask: bit n is slot n.                                    |
| `guards_fired_slots`  | `list[int]`  | The slots a guard clamped this tick, lowest first; the robot's layout().slot_names names them.          |
| `limit_clamped`       | `int`        | The slots held at a limit this tick because the command asked past it, as a bit mask: bit n is slot n.  |
| `limit_clamped_slots` | `list[int]`  | The slots held at a limit this tick, lowest first; the robot's layout().slot_names names them.          |

#### command_seq

```
command_seq: int
```

The sequence number of the command tracked this tick; 0 until the first command is taken.

#### command_age_ns

```
command_age_ns: int
```

Age of the command tracked at this tick, in nanoseconds, on the state's clock.

Keeps growing while the stream is stale and the controller holds; before the first command, the time since `activate`.

#### provenance

```
provenance: Provenance
```

Where each slot's gains and feedforward came from this tick.

#### gain_ramp

```
gain_ramp: float
```

How far the gains have slewed toward what the tunings ask for: 0 at the request, 1 once they are there.

#### guards_fired

```
guards_fired: int
```

The slots a guard clamped this tick, as a bit mask: bit `n` is slot `n`.

#### guards_fired_slots

```
guards_fired_slots: list[int]
```

The slots a guard clamped this tick, lowest first; the robot's `layout().slot_names` names them.

#### limit_clamped

```
limit_clamped: int
```

The slots held at a limit this tick because the command asked past it, as a bit mask: bit `n` is slot `n`.

A limit is a chain joint's, or the gripper's travel.

#### limit_clamped_slots

```
limit_clamped_slots: list[int]
```

The slots held at a limit this tick, lowest first; the robot's `layout().slot_names` names them.

### LeaderFollowerTrace

```
LeaderFollowerTrace()
```

Bases: `Counted`

The `LeaderFollowerController`'s trace of one tick: what each follower was sent from, and why.

One comes from `LeaderFollowerController.tick` or a `Snapshot`'s trace; each per-slot list is one entry per slot. Read-only.

Get one from `LeaderFollowerController.tick` or a `Snapshot`'s trace, never by calling the class.

Attributes:

| Name                  | Type          | Description                                                                                             |
| --------------------- | ------------- | ------------------------------------------------------------------------------------------------------- |
| `q_leader`            | `list[float]` | By follower slot, the position measured this tick on the leader slot it tracks, in radians.             |
| `provenance`          | `Provenance`  | Where each slot's gains and feedforward came from this tick.                                            |
| `gain_ramp`           | `float`       | How far the gains have slewed toward what the tunings ask for: 0 at the request, 1 once they are there. |
| `guards_fired`        | `int`         | The slots a guard clamped this tick, as a bit mask: bit n is slot n.                                    |
| `guards_fired_slots`  | `list[int]`   | The slots a guard clamped this tick, lowest first; the robot's layout().slot_names names them.          |
| `limit_clamped`       | `int`         | The slots held at a limit this tick because the leader stood past it, as a bit mask: bit n is slot n.   |
| `limit_clamped_slots` | `list[int]`   | The slots held at a limit this tick, lowest first; the robot's layout().slot_names names them.          |

#### q_leader

```
q_leader: list[float]
```

By follower slot, the position measured this tick on the leader slot it tracks, in radians.

On a gripper's slot it is the leader handle's motor angle, whose share of its travel the follower's gripper opens to; zero on a leader's slots. While engaged, the follower's target is this clamped to its limits.

#### provenance

```
provenance: Provenance
```

Where each slot's gains and feedforward came from this tick.

#### gain_ramp

```
gain_ramp: float
```

How far the gains have slewed toward what the tunings ask for: 0 at the request, 1 once they are there.

#### guards_fired

```
guards_fired: int
```

The slots a guard clamped this tick, as a bit mask: bit `n` is slot `n`.

#### guards_fired_slots

```
guards_fired_slots: list[int]
```

The slots a guard clamped this tick, lowest first; the robot's `layout().slot_names` names them.

#### limit_clamped

```
limit_clamped: int
```

The slots held at a limit this tick because the leader stood past it, as a bit mask: bit `n` is slot `n`.

A limit is a chain joint's, or the gripper's travel.

#### limit_clamped_slots

```
limit_clamped_slots: list[int]
```

The slots held at a limit this tick, lowest first; the robot's `layout().slot_names` names them.

### Waypoint

```
Waypoint(
    time_from_start_s: float,
    q: Sequence[float],
    qd: Sequence[float] | None = None,
)
```

Bases: `Struct`

One point of a `JointTrajectoryCommand`: where every slot is at one time, and how fast it moves through there.

Indexed by slot as a `JointState` is; a gripper's slot is its motor angle. Read-only once built.

Builds a waypoint.

Parameters:

| Name                | Type                      | Description                                                                                                                                                       | Default    |
| ------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `time_from_start_s` | `float`                   | When the trajectory reaches it, in seconds after the trajectory starts, rounded to the nanosecond. A first waypoint at 0 is refused when the trajectory is built. | *required* |
| `q`                 | `Sequence[float]`         | Position per slot, in radians.                                                                                                                                    | *required* |
| `qd`                | `Sequence[float] \| None` | Velocity per slot as the trajectory passes through, in radians per second, one per entry of q; None passes it at rest, stopping there before moving on.           | `None`     |

Raises:

| Type                   | Description                                                                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | q or qd holds something other than numbers.                                                                                                         |
| `InvalidArgumentError` | time_from_start_s is negative or not a number, q is empty or longer than MAX_JOINTS, qd is not one entry per entry of q, or an entry is not finite. |

Attributes:

| Name                | Type          | Description                                                                |
| ------------------- | ------------- | -------------------------------------------------------------------------- |
| `time_from_start_s` | `float`       | When the trajectory reaches the waypoint, in seconds after it starts.      |
| `num_joints`        | `int`         | Slots the waypoint was written for: len(q) at construction.                |
| `q`                 | `list[float]` | Position per slot, in radians.                                             |
| `qd`                | `list[float]` | Velocity per slot as the trajectory passes through, in radians per second. |

#### time_from_start_s

```
time_from_start_s: float
```

When the trajectory reaches the waypoint, in seconds after it starts.

#### num_joints

```
num_joints: int
```

Slots the waypoint was written for: `len(q)` at construction.

#### q

```
q: list[float]
```

Position per slot, in radians.

#### qd

```
qd: list[float]
```

Velocity per slot as the trajectory passes through, in radians per second.

### JointTrajectoryCommand

```
JointTrajectoryCommand(
    num_joints: int, points: Sequence[Waypoint]
)
```

Bases: `Struct`

The `JointTrajectoryController`'s command: timed waypoints for every slot, played once from when it is taken.

The controller starts it on the tick it takes the command, from the targets it was sending then, so a trajectory never steps, even one replacing another mid-run. Between waypoints each slot follows a cubic through both ends at their stated velocities; past the last, the controller holds it. A position past a joint's limit or the gripper's travel is commanded at the limit, at rest. It carries no `TuningOverride`: it plays under the tuning the controller is configured with. Read-only once built.

Builds a trajectory, checking that it can be played.

Parameters:

| Name         | Type                 | Description                                                                              | Default    |
| ------------ | -------------------- | ---------------------------------------------------------------------------------------- | ---------- |
| `num_joints` | `int`                | Slots the trajectory drives: the robot's slot count, or the controller does not take it. | *required* |
| `points`     | `Sequence[Waypoint]` | The waypoints, in time order, each written for num_joints slots.                         | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | num_joints is 0 or past MAX_JOINTS; there is no waypoint, or more than MAX_WAYPOINTS; a waypoint was written for another slot count; the first waypoint is at time 0, or one is not later than the one before it; or the last is not at rest. |

Methods:

| Name             | Description                                                                               |
| ---------------- | ----------------------------------------------------------------------------------------- |
| `from_waypoints` | Builds the trajectory that plays waypoints, a run a robot planned, waypoint for waypoint. |

Attributes:

| Name         | Type             | Description                                           |
| ------------ | ---------------- | ----------------------------------------------------- |
| `num_joints` | `int`            | Slots the trajectory drives.                          |
| `points`     | `list[Waypoint]` | The waypoints, in time order.                         |
| `duration_s` | `float`          | Time from the start to the last waypoint, in seconds. |

#### num_joints

```
num_joints: int
```

Slots the trajectory drives.

#### points

```
points: list[Waypoint]
```

The waypoints, in time order.

#### duration_s

```
duration_s: float
```

Time from the start to the last waypoint, in seconds.

#### from_waypoints

```
from_waypoints(waypoints: TimedWaypoints) -> Self
```

Builds the trajectory that plays `waypoints`, a run a robot planned, waypoint for waypoint.

Parameters:

| Name        | Type             | Description                                                    | Default    |
| ----------- | ---------------- | -------------------------------------------------------------- | ---------- |
| `waypoints` | `TimedWaypoints` | The run, such as anvil.workcell.RobotConfig.home_trajectory's. | *required* |

Raises:

| Type                   | Description                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
| `TypeError`            | A waypoint's q or qd holds something other than numbers.                                            |
| `InvalidArgumentError` | As the constructor and Waypoint refuse them, a run of more than MAX_WAYPOINTS waypoints among them. |

### JointTrajectoryTrace

```
JointTrajectoryTrace()
```

Bases: `Counted`

The `JointTrajectoryController`'s trace of one tick: its progress along the trajectory and the tracking error.

One comes from `JointTrajectoryController.tick` or a `Snapshot`'s trace; each per-slot list is one entry per slot. Read-only.

Get one from `JointTrajectoryController.tick` or a `Snapshot`'s trace, never by calling the class.

Attributes:

| Name                  | Type          | Description                                                                                              |
| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------- |
| `command_seq`         | `int`         | The sequence number of the trajectory tracked this tick; 0 until the first is taken.                     |
| `elapsed_ns`          | `int`         | Time into the tracked trajectory, in nanoseconds; 0 until the first is taken.                            |
| `segment`             | `int`         | The index of the waypoint being approached; the waypoint count once the last is reached.                 |
| `done`                | `bool`        | Whether the last waypoint's time has passed and the targets sent are there, so the controller holds it.  |
| `stopped_short`       | `bool`        | Whether the collision stop slowed a step of the trajectory, which ended it.                              |
| `q_error`             | `list[float]` | Sampled position target minus measured position per slot, in radians.                                    |
| `provenance`          | `Provenance`  | Where each slot's gains and feedforward came from this tick.                                             |
| `gain_ramp`           | `float`       | How far the gains have slewed toward what the tunings ask for: 0 at the request, 1 once they are there.  |
| `guards_fired`        | `int`         | The slots a guard clamped this tick, as a bit mask: bit n is slot n.                                     |
| `guards_fired_slots`  | `list[int]`   | The slots a guard clamped this tick, lowest first; the robot's layout().slot_names names them.           |
| `limit_clamped`       | `int`         | The slots held at a limit this tick because the trajectory runs past it, as a bit mask: bit n is slot n. |
| `limit_clamped_slots` | `list[int]`   | The slots held at a limit this tick, lowest first; the robot's layout().slot_names names them.           |

#### command_seq

```
command_seq: int
```

The sequence number of the trajectory tracked this tick; 0 until the first is taken.

#### elapsed_ns

```
elapsed_ns: int
```

Time into the tracked trajectory, in nanoseconds; 0 until the first is taken.

Keeps growing while its last waypoint is held.

#### segment

```
segment: int
```

The index of the waypoint being approached; the waypoint count once the last is reached.

#### done

```
done: bool
```

Whether the last waypoint's time has passed and the targets sent are there, so the controller holds it.

#### stopped_short

```
stopped_short: bool
```

Whether the collision stop slowed a step of the trajectory, which ended it.

The controller holds where the stop sent the arms until it takes a new trajectory.

#### q_error

```
q_error: list[float]
```

Sampled position target minus measured position per slot, in radians.

#### provenance

```
provenance: Provenance
```

Where each slot's gains and feedforward came from this tick.

#### gain_ramp

```
gain_ramp: float
```

How far the gains have slewed toward what the tunings ask for: 0 at the request, 1 once they are there.

#### guards_fired

```
guards_fired: int
```

The slots a guard clamped this tick, as a bit mask: bit `n` is slot `n`.

#### guards_fired_slots

```
guards_fired_slots: list[int]
```

The slots a guard clamped this tick, lowest first; the robot's `layout().slot_names` names them.

#### limit_clamped

```
limit_clamped: int
```

The slots held at a limit this tick because the trajectory runs past it, as a bit mask: bit `n` is slot `n`.

A limit is a chain joint's, or the gripper's travel.

#### limit_clamped_slots

```
limit_clamped_slots: list[int]
```

The slots held at a limit this tick, lowest first; the robot's `layout().slot_names` names them.

### ArmTarget

```
ArmTarget(
    pose: Pose,
    grip_m: float,
    reseed_ik: bool = False,
    hand_speed_mps: float = 0.0,
    hold: bool = False,
    vel_ff: float = 0.0,
    grip_rad: float | None = None,
)
```

Bases: `Struct`

One arm's target for the `EePoseController`: a world-frame pose for its tool frame, and what to ask its gripper.

Unless it holds, the controller solves the pose with IK and walks the arm toward the solution, its links moving no faster than the hand driving the target plus a floor, so a still hand has them settle rather than snap. A target that holds stops the arm where the controller has it and leaves the pose untracked: an arm nobody is driving. Read-only once built.

Builds a target; what is wrong with it is refused when an `EePoseCommand` carrying it is built.

Parameters:

| Name             | Type            | Description                                                                                                                                                                                                                                                   | Default    |
| ---------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `pose`           | `Pose`          | Where the arm's tool frame goes, in the world frame.                                                                                                                                                                                                          | *required* |
| `grip_m`         | `float`         | The jaw opening to ask for, in meters, at rest; not read when grip_rad is given.                                                                                                                                                                              | *required* |
| `reseed_ik`      | `bool`          | Whether the controller solves this target from the measured joints instead of its previous solution, as when a clutch closes.                                                                                                                                 | `False`    |
| `hand_speed_mps` | `float`         | How fast the hand driving the target is translating, in meters per second: the controller lets the arm's links move that fast, plus a floor. Zero for a hand that is still or only turning.                                                                   | `0.0`      |
| `hold`           | `bool`          | Whether the arm holds where the controller has it, the pose untracked; reseed_ik, hand_speed_mps and vel_ff are then not read.                                                                                                                                | `False`    |
| `vel_ff`         | `float`         | The share of the rate the arm's own position target moves at that goes out as its velocity target: 0 none, 1 all of it. At 0, kd damps measured velocity and brakes a move that was asked for; at 1 it damps only what the arm does that its target does not. | `0.0`      |
| `grip_rad`       | `float \| None` | The gripper motor's own angle to drive instead of grip_m, in radians; None drives the opening.                                                                                                                                                                | `None`     |

Attributes:

| Name             | Type             | Description                                                                                            |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------ |
| `pose`           | `Pose`           | Where the arm's tool frame goes, in the world frame.                                                   |
| `grip`           | `GripperCommand` | What the gripper is asked: the same GripperCommand a JointCommand carries per arm.                     |
| `grip_m`         | `float`          | The jaw opening asked for, in meters.                                                                  |
| `grip_rad`       | `float \| None`  | The motor angle driven instead of the opening, in radians; None where the target asks for the opening. |
| `reseed_ik`      | `bool`           | Whether the controller solves this target from the measured joints instead of its previous solution.   |
| `hold`           | `bool`           | Whether the arm holds where the controller has it, the pose untracked.                                 |
| `hand_speed_mps` | `float`          | How fast the hand driving the target is translating, in meters per second.                             |
| `vel_ff`         | `float`          | The share of the position target's own rate sent as the velocity target: 0 none, 1 all of it.          |

#### pose

```
pose: Pose
```

Where the arm's tool frame goes, in the world frame.

#### grip

```
grip: GripperCommand
```

What the gripper is asked: the same `GripperCommand` a `JointCommand` carries per arm.

#### grip_m

```
grip_m: float
```

The jaw opening asked for, in meters.

#### grip_rad

```
grip_rad: float | None
```

The motor angle driven instead of the opening, in radians; `None` where the target asks for the opening.

#### reseed_ik

```
reseed_ik: bool
```

Whether the controller solves this target from the measured joints instead of its previous solution.

#### hold

```
hold: bool
```

Whether the arm holds where the controller has it, the pose untracked.

#### hand_speed_mps

```
hand_speed_mps: float
```

How fast the hand driving the target is translating, in meters per second.

#### vel_ff

```
vel_ff: float
```

The share of the position target's own rate sent as the velocity target: 0 none, 1 all of it.

### EePoseCommand

```
EePoseCommand(
    targets: Sequence[ArmTarget],
    overrides: TuningOverride | None = None,
    ik: Sequence[IkParams | None] | None = None,
)
```

Bases: `Struct`

The `EePoseController`'s command: one `ArmTarget` per arm, in the robot's arm order.

Entry 0 targets the arm the workcell declares first, so a command never names a side. It may also carry what it replaces of the arms' tunings and the IK parameters each arm solves with, both for as long as it is tracked. The velocity target is the controller's to derive, from each target's `vel_ff`. The controller takes a command only when it targets every arm of the robot; `Runtime.send_command` refuses one that does not. Read-only once built.

Builds a command, checking that a drive can be sent it.

Parameters:

| Name        | Type                                 | Description                                                                                                                                                                                                                        | Default    |
| ----------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `targets`   | `Sequence[ArmTarget]`                | One ArmTarget per arm, in the robot's arm order.                                                                                                                                                                                   | *required* |
| `overrides` | `TuningOverride \| None`             | What this command replaces of the arms' tunings while it is tracked; None replaces nothing.                                                                                                                                        | `None`     |
| `ik`        | `Sequence[IkParams \| None] \| None` | One anvil.kinematics.IkParams or None per arm; a set one replaces the controller's configured parameters for that arm while this command is tracked. None, or a list shorter than targets, leaves the rest on the configured ones. | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | targets is one ArmTarget rather than a list.                                                                                                                                                                                                                                                                                                                                                                  |
| `InvalidArgumentError` | targets is empty or longer than MAX_ARMS; a target's pose is not finite or its orientation not a unit quaternion; a grip value that is read is not finite; a tracking target's hand_speed_mps or vel_ff is negative or not finite; overrides sets a gain or scale that is negative or not finite or a torque or offset that is not finite; or ik is longer than targets or holds parameters that are refused. |

Attributes:

| Name       | Type                     | Description                                                                               |
| ---------- | ------------------------ | ----------------------------------------------------------------------------------------- |
| `num_arms` | `int`                    | Arms targeted: len(targets) at construction.                                              |
| `arms`     | `list[ArmTarget]`        | The targets, in the robot's arm order.                                                    |
| `ik`       | `list[IkParams \| None]` | The IK parameters per arm; None for an arm solving with the controller's configured ones. |

#### num_arms

```
num_arms: int
```

Arms targeted: `len(targets)` at construction.

#### arms

```
arms: list[ArmTarget]
```

The targets, in the robot's arm order.

#### ik

```
ik: list[IkParams | None]
```

The IK parameters per arm; `None` for an arm solving with the controller's configured ones.

### EePoseTrace

```
EePoseTrace()
```

Bases: `Counted`

The `EePoseController`'s trace of one tick: the command it tracked, and the IK behind its targets.

One comes from `EePoseController.tick` or a `Snapshot`'s trace; each per-slot list is one entry per slot, and each per-arm list `MAX_ARMS` entries. Read-only.

Get one from `EePoseController.tick` or a `Snapshot`'s trace, never by calling the class.

Attributes:

| Name                  | Type          | Description                                                                                             |
| --------------------- | ------------- | ------------------------------------------------------------------------------------------------------- |
| `command_seq`         | `int`         | The sequence number of the command tracked this tick; 0 until the first command is taken.               |
| `command_age_ns`      | `int`         | Age of the command tracked at this tick, in nanoseconds, on the state's clock.                          |
| `q_ik`                | `list[float]` | The IK solution for the commanded poses per slot, in radians.                                           |
| `provenance`          | `Provenance`  | Where each slot's gains and feedforward came from this tick.                                            |
| `gain_ramp`           | `float`       | How far the gains have slewed toward what the tunings ask for: 0 at the request, 1 once they are there. |
| `guards_fired`        | `int`         | The slots a guard clamped this tick, as a bit mask: bit n is slot n.                                    |
| `guards_fired_slots`  | `list[int]`   | The slots a guard clamped this tick, lowest first; the robot's layout().slot_names names them.          |
| `limit_clamped`       | `int`         | The slots held at a limit this tick because the command asked past it, as a bit mask: bit n is slot n.  |
| `limit_clamped_slots` | `list[int]`   | The slots held at a limit this tick, lowest first; the robot's layout().slot_names names them.          |
| `ik_solved`           | `int`         | The arms IK ran for this tick, on a fresh target, as a bit mask: bit n is arm n.                        |
| `ik_converged`        | `int`         | The arms whose latest IK solution converged, as a bit mask; 0 before the first solve.                   |
| `ik_iterations`       | `list[int]`   | Steps the latest IK solve took per arm; 0 before the first.                                             |
| `ik_residual_m`       | `list[float]` | Tip position error of the latest IK solution per arm, in meters; 0 before the first.                    |

#### command_seq

```
command_seq: int
```

The sequence number of the command tracked this tick; 0 until the first command is taken.

#### command_age_ns

```
command_age_ns: int
```

Age of the command tracked at this tick, in nanoseconds, on the state's clock.

Keeps growing while the stream is stale and the controller holds; before the first command, the time since `activate`.

#### q_ik

```
q_ik: list[float]
```

The IK solution for the commanded poses per slot, in radians.

#### provenance

```
provenance: Provenance
```

Where each slot's gains and feedforward came from this tick.

#### gain_ramp

```
gain_ramp: float
```

How far the gains have slewed toward what the tunings ask for: 0 at the request, 1 once they are there.

#### guards_fired

```
guards_fired: int
```

The slots a guard clamped this tick, as a bit mask: bit `n` is slot `n`.

#### guards_fired_slots

```
guards_fired_slots: list[int]
```

The slots a guard clamped this tick, lowest first; the robot's `layout().slot_names` names them.

#### limit_clamped

```
limit_clamped: int
```

The slots held at a limit this tick because the command asked past it, as a bit mask: bit `n` is slot `n`.

A limit is a chain joint's, or the gripper's travel.

#### limit_clamped_slots

```
limit_clamped_slots: list[int]
```

The slots held at a limit this tick, lowest first; the robot's `layout().slot_names` names them.

#### ik_solved

```
ik_solved: int
```

The arms IK ran for this tick, on a fresh target, as a bit mask: bit `n` is arm `n`.

#### ik_converged

```
ik_converged: int
```

The arms whose latest IK solution converged, as a bit mask; 0 before the first solve.

#### ik_iterations

```
ik_iterations: list[int]
```

Steps the latest IK solve took per arm; 0 before the first.

#### ik_residual_m

```
ik_residual_m: list[float]
```

Tip position error of the latest IK solution per arm, in meters; 0 before the first.

### GripperMap

```
GripperMap(
    closed_rad: float, open_rad: float, opening_m: float
)
```

Bases: `Struct`

A gripper's linear map from jaw opening to motor angle, as its description states it.

Every width here is the opening between the jaws, both fingers, which is what a person means by how far a gripper is open. One comes from a `GripperTuning`'s `map`, or from an `anvil.workcell.Gripper`'s `gripper_map`.

Builds a map.

Parameters:

| Name         | Type    | Description                                 | Default    |
| ------------ | ------- | ------------------------------------------- | ---------- |
| `closed_rad` | `float` | Motor angle with the jaws shut, in radians. | *required* |
| `open_rad`   | `float` | Motor angle at opening_m, in radians.       | *required* |
| `opening_m`  | `float` | The jaw opening at full travel, in meters.  | *required* |

Methods:

| Name      | Description                                                                                             |
| --------- | ------------------------------------------------------------------------------------------------------- |
| `width_m` | Returns the jaw opening a motor angle corresponds to, in meters, for seeding an application from state. |

Attributes:

| Name         | Type    | Description                                 |
| ------------ | ------- | ------------------------------------------- |
| `closed_rad` | `float` | Motor angle with the jaws shut, in radians. |
| `open_rad`   | `float` | Motor angle at opening_m, in radians.       |
| `opening_m`  | `float` | The jaw opening at full travel, in meters.  |

#### closed_rad

```
closed_rad: float
```

Motor angle with the jaws shut, in radians.

#### open_rad

```
open_rad: float
```

Motor angle at `opening_m`, in radians.

#### opening_m

```
opening_m: float
```

The jaw opening at full travel, in meters.

#### width_m

```
width_m(position_rad: float) -> float
```

Returns the jaw opening a motor angle corresponds to, in meters, for seeding an application from state.

The inverse of the map a commanded opening goes through. It is linear and not clamped, so an angle past the travel gives an opening past it.

Parameters:

| Name           | Type    | Description                                               | Default    |
| -------------- | ------- | --------------------------------------------------------- | ---------- |
| `position_rad` | `float` | The motor angle, in radians, as a JointState measures it. | *required* |

### GripperTuning

```
GripperTuning(
    kp: float, kd: float, gripper_map: GripperMap
)
```

Bases: `Struct`

The gripper motor's tuning: its gains, the grip assist, and the map from a jaw opening to its motor angle.

One comes from an `ArmTuning`'s `gripper`. The one a controller's `settings()` hands back is the settings' own, so an assignment here, then `configure`, is what the gripper runs with from then on. `configure` refuses a gain or an assist number that is negative or not finite.

The grip assist is a feedforward torque toward the commanded opening, on top of the gains. Closing pulls `max_torque * tanh(stiffness * x / max_torque)` on the jaw-opening error `x`, in meters; opening pulls `opening_scale` times the same curve the other way; and a command that asks the jaws shut holds at least `shut_torque`. `max_torque` is the squeeze and, against `kd`, the closing speed; `stiffness` is how sharply the jaws stop, and the number to lower for a gripper that hunts.

Builds a tuning with the grip assist off, as a description without one leaves it.

Each of the assist's numbers is assignable afterward; `opening_scale` starts at 1.

Parameters:

| Name          | Type         | Description                                                                 | Default    |
| ------------- | ------------ | --------------------------------------------------------------------------- | ---------- |
| `kp`          | `float`      | Position gain, in newton meters per radian of motor angle error.            | *required* |
| `kd`          | `float`      | Velocity gain, in newton meters per radian per second of motor speed error. | *required* |
| `gripper_map` | `GripperMap` | The gripper's map from jaw opening to motor angle.                          | *required* |

Attributes:

| Name            | Type         | Description                                                                                            |
| --------------- | ------------ | ------------------------------------------------------------------------------------------------------ |
| `kp`            | `float`      | Position gain, in newton meters per radian of motor angle error.                                       |
| `kd`            | `float`      | Velocity gain, in newton meters per radian per second of motor speed error.                            |
| `max_torque`    | `float`      | The torque the assist curve saturates at, in newton meters.                                            |
| `stiffness`     | `float`      | The assist curve's slope at zero error, in newton meters per meter of jaw opening.                     |
| `opening_scale` | `float`      | Opening as a fraction of closing, torque, speed and slope together; 0 turns the opening assist off.    |
| `shut_torque`   | `float`      | The least closing torque while a command asks the jaws shut, in newton meters.                         |
| `gripper_map`   | `GripperMap` | The map from a commanded jaw opening to the motor angle: the gripper's geometry, from its description. |

#### kp

```
kp: float
```

Position gain, in newton meters per radian of motor angle error.

#### kd

```
kd: float
```

Velocity gain, in newton meters per radian per second of motor speed error.

Against the assist's pull, it is what caps the jaws' speed in free air.

#### max_torque

```
max_torque: float
```

The torque the assist curve saturates at, in newton meters.

0 turns the assist off, leaving `kp` and `kd` to move the jaws: where `kp` is a few per cent of the assist, as on the Anvil gripper, they creep.

#### stiffness

```
stiffness: float
```

The assist curve's slope at zero error, in newton meters per meter of jaw opening.

#### opening_scale

```
opening_scale: float
```

Opening as a fraction of closing, torque, speed and slope together; 0 turns the opening assist off.

#### shut_torque

```
shut_torque: float
```

The least closing torque while a command asks the jaws shut, in newton meters.

#### gripper_map

```
gripper_map: GripperMap
```

The map from a commanded jaw opening to the motor angle: the gripper's geometry, from its description.

### LimitGuards

```
LimitGuards(position: bool = True)
```

Bases: `Struct`

Which of the limits an arm's description states the tick holds the arm's chain targets to.

One comes from a `JointGuards`' `limits`.

Builds the guards, the travel held unless asked otherwise.

Parameters:

| Name       | Type   | Description                                                                                                                                                         | Default |
| ---------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `position` | `bool` | Whether every chain joint's target is clamped to the arm's travel; off, a command past a joint's limit is driven there, into the hardstop if that is where it lies. | `True`  |

Attributes:

| Name       | Type   | Description                                                        |
| ---------- | ------ | ------------------------------------------------------------------ |
| `position` | `bool` | Whether every chain joint's target is clamped to the arm's travel. |

#### position

```
position: bool = True
```

Whether every chain joint's target is clamped to the arm's travel.

### SpeedLimits

```
SpeedLimits(
    max_dps: Sequence[float] | None = None,
    tcp_max_mps: float | None = None,
)
```

Bases: `Counted`

How fast a controller moves an arm's chain targets: each joint's, and the tool frame's.

A step either would outrun slows the whole arm alike, so it keeps the path it was asked along, and a velocity target either would outrun is slowed alike; no joint's target ever moves faster than the top speed its description states. A step is measured from the target last sent, so an arm lagging or overshooting it never widens the next.

One comes from an `ArmTuning`'s `speed`, where each field is assignable on its own: `tuning.speed.tcp_max_mps = 0.1` keeps the joints' speeds, and `max_dps` reads one entry per chain joint of the arm. One built here reads as many entries as it was last given. `configure` refuses a speed that is not positive.

Builds the limits, every joint at up to its top speed and the tool frame at any unless asked otherwise.

Parameters:

| Name          | Type                      | Description                                                                                                                                             | Default |
| ------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `max_dps`     | `Sequence[float] \| None` | The fastest each chain joint's target moves, in degrees per second, chain order; None moves each at up to its top speed, as does a joint past the list. | `None`  |
| `tcp_max_mps` | `float \| None`           | The fastest the tool frame moves, in meters per second; None for any speed.                                                                             | `None`  |

Raises:

| Type                   | Description                                             |
| ---------------------- | ------------------------------------------------------- |
| `TypeError`            | max_dps holds something other than numbers.             |
| `InvalidArgumentError` | max_dps has more entries than anvil.kinematics.MAX_DOF. |

Attributes:

| Name          | Type                  | Description                                                                                               |
| ------------- | --------------------- | --------------------------------------------------------------------------------------------------------- |
| `max_dps`     | `list[float] \| None` | The fastest each chain joint's target moves, in degrees per second; None for each at up to its top speed. |
| `tcp_max_mps` | `float \| None`       | The fastest the tool frame moves, meters per second; None for any speed.                                  |

#### max_dps

```
max_dps: list[float] | None = None
```

The fastest each chain joint's target moves, in degrees per second; `None` for each at up to its top speed.

One entry per chain joint, chain order: the arm's joints for limits read from an `ArmTuning`, as many as were last given for limits built here. A shorter list assigned leaves the joints past it at up to their top speed, and reads back infinite there.

Raises:

| Type                   | Description                                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | On assignment, a list holding something other than numbers.                                                                     |
| `InvalidArgumentError` | On assignment, a list with more entries than the arm has chain joints, or for limits built here, than anvil.kinematics.MAX_DOF. |

#### tcp_max_mps

```
tcp_max_mps: float | None = None
```

The fastest the tool frame moves, meters per second; `None` for any speed.

### JointGuards

```
JointGuards(
    max_kp: Sequence[float] | None = None,
    max_kd: Sequence[float] | None = None,
    gain_slew_s: float | None = _DEFAULT_GAIN_SLEW_S,
    limits: LimitGuards | None = None,
)
```

Bases: `Counted`

What the tick clamps an arm to, whatever asked for it: the arm's own tuning, or a command's override.

`None` is a guard that is off. The gain ceilings are chain order, one entry per chain joint; the gripper's slot is not guarded here. A tuning read from a description brings the guards its gains imply; one built from bare numbers slews its gains, holds the arm to its travel, and bounds nothing else. A guard clamps silently inside the tick, and the trace's `guards_fired` reports it; `configure` refuses a tuning whose gains are past its own ceilings. One comes from an `ArmTuning`'s `guards`, whose ceilings read one entry per chain joint of the arm; one built here reads as many entries as the longer ceiling it was given.

Builds guards with every gain ceiling off and the gains slewing, unless asked otherwise.

Parameters:

| Name          | Type                      | Description                                                                                                                                                           | Default                |
| ------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| `max_kp`      | `Sequence[float] \| None` | Ceiling on the position gain per chain joint, in newton meters per radian, chain order; None for none. A joint past the list is not bounded.                          | `None`                 |
| `max_kd`      | `Sequence[float] \| None` | Ceiling on the velocity gain per chain joint, in newton meters per radian per second, as max_kp is.                                                                   | `None`                 |
| `gain_slew_s` | `float \| None`           | How long a gain takes to reach what it is asked for, in seconds, zero or more; None steps it there. The default is the slew a tuning built from bare numbers carries. | `_DEFAULT_GAIN_SLEW_S` |
| `limits`      | `LimitGuards \| None`     | Which of the arm's described limits its targets are held to; None for LimitGuards(), the travel held.                                                                 | `None`                 |

Raises:

| Type                   | Description                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
| `TypeError`            | A ceiling holds something other than numbers.                                                       |
| `InvalidArgumentError` | gain_slew_s is negative or not finite, or a ceiling has more entries than anvil.kinematics.MAX_DOF. |

Attributes:

| Name          | Type                  | Description                                                                                            |
| ------------- | --------------------- | ------------------------------------------------------------------------------------------------------ |
| `max_kp`      | `list[float] \| None` | Ceiling on the position gain per chain joint, in newton meters per radian, chain order; None when off. |
| `max_kd`      | `list[float] \| None` | Ceiling on the velocity gain per chain joint, in newton meters per radian per second; None when off.   |
| `gain_slew_s` | `float \| None`       | How long a gain takes to reach what it is asked for, in seconds; None steps it there.                  |
| `limits`      | `LimitGuards`         | Which of the arm's described limits its targets are held to.                                           |

#### max_kp

```
max_kp: list[float] | None = None
```

Ceiling on the position gain per chain joint, in newton meters per radian, chain order; `None` when off.

One entry per chain joint, infinite for a joint the ceiling does not bound.

#### max_kd

```
max_kd: list[float] | None = None
```

Ceiling on the velocity gain per chain joint, in newton meters per radian per second; `None` when off.

The list reads as `max_kp`'s does.

#### gain_slew_s

```
gain_slew_s: float | None = 1.0
```

How long a gain takes to reach what it is asked for, in seconds; `None` steps it there.

Raises:

| Type                   | Description                                               |
| ---------------------- | --------------------------------------------------------- |
| `InvalidArgumentError` | On assignment, a duration that is negative or not finite. |

#### limits

```
limits: LimitGuards = LimitGuards(position=True)
```

Which of the arm's described limits its targets are held to.

### ArmTuning

```
ArmTuning(
    kp: Sequence[float],
    kd: Sequence[float],
    gravity_scale: Sequence[float],
    gravity_offset_nm: Sequence[float] | None = None,
    gripper: GripperTuning | None = None,
    speed: SpeedLimits | None = None,
    guards: JointGuards | None = None,
)
```

Bases: `Struct`

One arm's tuning: gains and gravity correction per chain joint, the gripper's tuning, speed limits and guards.

One comes from a controller's `settings()` or a `ControlArm`'s `tuning`, or is built here from bare numbers. The per-joint lists read as one entry per chain joint, chain order, and an entry is assignable in place, `tuning.kp[3] = 20.0`; what a controller runs is the tuning as it stands when it is handed to `configure`. `configure` refuses a gain or gravity scale that is negative or not finite, an offset that is not finite, or a gain past its guard's ceiling.

Builds a tuning from bare numbers, one entry per chain joint in chain order.

Parameters:

| Name                | Type                      | Description                                                                                                        | Default    |
| ------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------- |
| `kp`                | `Sequence[float]`         | Position gain per chain joint, in newton meters per radian.                                                        | *required* |
| `kd`                | `Sequence[float]`         | Velocity gain per chain joint, in newton meters per radian per second.                                             | *required* |
| `gravity_scale`     | `Sequence[float]`         | What the gravity feedforward is multiplied by per chain joint: 1 sends gravity as computed, 0 none.                | *required* |
| `gravity_offset_nm` | `Sequence[float] \| None` | What is added to the gravity feedforward after, per chain joint, in newton meters; None adds nothing.              | `None`     |
| `gripper`           | `GripperTuning \| None`   | The gripper motor's tuning, for an arm driven with one; None for an arm without.                                   | `None`     |
| `speed`             | `SpeedLimits \| None`     | How fast the chain's targets move; None moves each joint at up to its top speed and the tool frame at any.         | `None`     |
| `guards`            | `JointGuards \| None`     | What the tick clamps the arm to; None for JointGuards(), whose gains slew and which bounds nothing but the travel. | `None`     |

Raises:

| Type                   | Description                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------- |
| `TypeError`            | A list holds something other than numbers.                                             |
| `InvalidArgumentError` | The lists are not all one length, or that length is 0 or more than a chain has joints. |

Methods:

| Name     | Description                                                                                              |
| -------- | -------------------------------------------------------------------------------------------------------- |
| `scaled` | Returns the same tuning with the gains and the feedforward dialed down, for bringing an arm up in rungs. |

Attributes:

| Name                | Type                    | Description                                                                                          |
| ------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------- |
| `kp`                | `list[float]`           | Position gain per chain joint, in newton meters per radian; an entry is assignable in place.         |
| `kd`                | `list[float]`           | Velocity gain per chain joint, in newton meters per radian per second; assignable in place as kp is. |
| `gravity_scale`     | `list[float]`           | What the gravity feedforward is multiplied by per chain joint: 1 sends it as computed, 0 none.       |
| `gravity_offset_nm` | `list[float]`           | What is added to the gravity feedforward after, per chain joint, in newton meters.                   |
| `gripper`           | `GripperTuning \| None` | The gripper motor's tuning, this tuning's own; None for an arm driven without one.                   |
| `speed`             | `SpeedLimits`           | How fast the chain's targets move, this tuning's own; assigning one copies it in.                    |
| `guards`            | `JointGuards`           | What the tick clamps this arm to, a command's overrides included; assigning one copies it in.        |

#### kp

```
kp: list[float]
```

Position gain per chain joint, in newton meters per radian; an entry is assignable in place.

Assigning an entry writes it into this tuning; adding or removing one raises `anvil.InvalidArgumentError`.

#### kd

```
kd: list[float]
```

Velocity gain per chain joint, in newton meters per radian per second; assignable in place as `kp` is.

#### gravity_scale

```
gravity_scale: list[float]
```

What the gravity feedforward is multiplied by per chain joint: 1 sends it as computed, 0 none.

#### gravity_offset_nm

```
gravity_offset_nm: list[float]
```

What is added to the gravity feedforward after, per chain joint, in newton meters.

#### gripper

```
gripper: GripperTuning | None
```

The gripper motor's tuning, this tuning's own; `None` for an arm driven without one.

#### speed

```
speed: SpeedLimits
```

How fast the chain's targets move, this tuning's own; assigning one copies it in.

#### guards

```
guards: JointGuards
```

What the tick clamps this arm to, a command's overrides included; assigning one copies it in.

#### scaled

```
scaled(
    kp_scale: float = 1.0,
    kd_scale: float = 1.0,
    gravity_scale: float = 1.0,
) -> Self
```

Returns the same tuning with the gains and the feedforward dialed down, for bringing an arm up in rungs.

The chain's and the gripper's gains scale by `kp_scale` and `kd_scale`. Each joint's gravity correction, scale and offset alike, and the grip assist's `max_torque`, `stiffness` and `shut_torque` scale by `gravity_scale`; the gripper's map and the assist's `opening_scale` do not. A negative scale is not refused here; `configure` refuses the negative gains it gives.

Parameters:

| Name            | Type    | Description                                                        | Default |
| --------------- | ------- | ------------------------------------------------------------------ | ------- |
| `kp_scale`      | `float` | Factor on every position gain: 0 is limp, 1 the gain as it stands. | `1.0`   |
| `kd_scale`      | `float` | Factor on every velocity gain.                                     | `1.0`   |
| `gravity_scale` | `float` | Factor on the gravity feedforward and the grip assist.             | `1.0`   |

Returns:

| Type   | Description                          |
| ------ | ------------------------------------ |
| `Self` | A new tuning; this one is unchanged. |

### JointControllerSettings

```
JointControllerSettings(
    tuning: Sequence[ArmTuning | None] | None = None,
)
```

Bases: `_ArmTuningSettings`

What the `JointController` is configured with: one `ArmTuning` per arm.

The controller's `settings()` comes back with every arm it drives filled in, and its tunings are the settings' own: `settings.tuning[0].kp[3] = 20.0`, then `configure`, is a retune.

Builds settings that retune the arms given and leave the rest as they are.

Parameters:

| Name     | Type                                  | Description                                                                                                                                                                                              | Default |
| -------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `tuning` | `Sequence[ArmTuning \| None] \| None` | One ArmTuning or None per arm, in slot order; a None entry, an arm past the list, or a None list leaves that arm's tuning as it is. configure refuses a tuning for an arm the controller does not drive. | `None`  |

Raises:

| Type                   | Description                                 |
| ---------------------- | ------------------------------------------- |
| `TypeError`            | tuning is one ArmTuning rather than a list. |
| `InvalidArgumentError` | tuning has more than MAX_ARMS entries.      |

Attributes:

| Name     | Type                      | Description                                                                                 |
| -------- | ------------------------- | ------------------------------------------------------------------------------------------- |
| `tuning` | `list[ArmTuning \| None]` | One ArmTuning or None per arm in slot order, through the last arm given: the settings' own. |

#### tuning

```
tuning: list[ArmTuning | None] = []
```

One `ArmTuning` or `None` per arm in slot order, through the last arm given: the settings' own.

### JointTrajectoryControllerSettings

```
JointTrajectoryControllerSettings(
    tuning: Sequence[ArmTuning | None] | None = None,
)
```

Bases: `_ArmTuningSettings`

What the `JointTrajectoryController` is configured with: one `ArmTuning` per arm.

The controller's `settings()` comes back with every arm it drives filled in, and its tunings are the settings' own: `settings.tuning[0].kp[3] = 20.0`, then `configure`, is a retune, which a trajectory playing goes on under.

Builds settings that retune the arms given and leave the rest as they are.

Parameters:

| Name     | Type                                  | Description                                                                                                                                                                                              | Default |
| -------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `tuning` | `Sequence[ArmTuning \| None] \| None` | One ArmTuning or None per arm, in slot order; a None entry, an arm past the list, or a None list leaves that arm's tuning as it is. configure refuses a tuning for an arm the controller does not drive. | `None`  |

Raises:

| Type                   | Description                                 |
| ---------------------- | ------------------------------------------- |
| `TypeError`            | tuning is one ArmTuning rather than a list. |
| `InvalidArgumentError` | tuning has more than MAX_ARMS entries.      |

Attributes:

| Name     | Type                      | Description                                                                                 |
| -------- | ------------------------- | ------------------------------------------------------------------------------------------- |
| `tuning` | `list[ArmTuning \| None]` | One ArmTuning or None per arm in slot order, through the last arm given: the settings' own. |

#### tuning

```
tuning: list[ArmTuning | None] = []
```

One `ArmTuning` or `None` per arm in slot order, through the last arm given: the settings' own.

### ForceFeedback

```
ForceFeedback(enabled: bool, stiffness_share: float)
```

Bases: `Struct`

How hard each leader is pulled toward its follower, so the operator's hand feels what holds the follower back.

One comes from a `LeaderFollowerControllerSettings`' `force_feedback`. `configure` refuses a `stiffness_share` that is not a number from 0 to 0.4.

Builds the feedback.

Parameters:

| Name              | Type    | Description                                                                                                | Default    |
| ----------------- | ------- | ---------------------------------------------------------------------------------------------------------- | ---------- |
| `enabled`         | `bool`  | Whether the leaders are pulled at all; off, they float under the hand but for gravity, whatever the share. | *required* |
| `stiffness_share` | `float` | Each leader joint's position gain as a share of its follower's, from 0 to 0.4.                             | *required* |

Attributes:

| Name              | Type    | Description                                                                    |
| ----------------- | ------- | ------------------------------------------------------------------------------ |
| `enabled`         | `bool`  | Whether the leaders are pulled toward their followers at all.                  |
| `stiffness_share` | `float` | Each leader joint's position gain as a share of its follower's, from 0 to 0.4. |

#### enabled

```
enabled: bool
```

Whether the leaders are pulled toward their followers at all.

#### stiffness_share

```
stiffness_share: float
```

Each leader joint's position gain as a share of its follower's, from 0 to 0.4.

### LeaderFollowerControllerSettings

```
LeaderFollowerControllerSettings(
    tuning: Sequence[ArmTuning | None] | None = None,
    force_feedback: ForceFeedback | None = None,
    engaged: bool | None = None,
)
```

Bases: `Struct`

What the `LeaderFollowerController` is configured with: a tuning per arm, the force feedback, and the engagement.

A leader's gains are the feedback's, so only its gravity correction and guards are taken from its tuning. Disengaged, the followers hold still and the leaders float; engaging moves each follower to its leader first. The controller's `settings()` comes back with everything filled, and its objects are the settings' own: `settings.force_feedback.enabled = True`, then `configure`, turns the feedback on.

Builds settings that change what is given and leave the rest as it is.

Parameters:

| Name             | Type                                  | Description                                                                                                                                                      | Default |
| ---------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `tuning`         | `Sequence[ArmTuning \| None] \| None` | One ArmTuning or None per arm, leaders and followers alike, in slot order; a None entry, an arm past the list, or a None list leaves that arm's tuning as it is. | `None`  |
| `force_feedback` | `ForceFeedback \| None`               | How hard each leader is pulled toward its follower; None leaves the feedback as it is.                                                                           | `None`  |
| `engaged`        | `bool \| None`                        | Whether the followers mirror their leaders; None leaves the engagement as it is.                                                                                 | `None`  |

Raises:

| Type                   | Description                                 |
| ---------------------- | ------------------------------------------- |
| `TypeError`            | tuning is one ArmTuning rather than a list. |
| `InvalidArgumentError` | tuning has more than MAX_ARMS entries.      |

Attributes:

| Name             | Type                      | Description                                                                                 |
| ---------------- | ------------------------- | ------------------------------------------------------------------------------------------- |
| `tuning`         | `list[ArmTuning \| None]` | One ArmTuning or None per arm in slot order, through the last arm given: the settings' own. |
| `force_feedback` | `ForceFeedback \| None`   | The force feedback, the settings' own; None leaves the controller's as it is.               |
| `engaged`        | `bool \| None`            | Whether the followers mirror their leaders; None leaves it as it is.                        |

#### tuning

```
tuning: list[ArmTuning | None] = []
```

One `ArmTuning` or `None` per arm in slot order, through the last arm given: the settings' own.

#### force_feedback

```
force_feedback: ForceFeedback | None = None
```

The force feedback, the settings' own; `None` leaves the controller's as it is.

#### engaged

```
engaged: bool | None = None
```

Whether the followers mirror their leaders; `None` leaves it as it is.

### EePoseControllerSettings

```
EePoseControllerSettings(
    tuning: Sequence[ArmTuning | None] | None = None,
    ik: Sequence[IkParams | None] | None = None,
)
```

Bases: `Struct`

What the `EePoseController` is configured with: one `ArmTuning` and one set of IK parameters per arm.

The controller's `settings()` comes back with every arm filled, and its objects are the settings' own: `settings.ik[0].rest_q[1] = 0.2`, then `configure`, is a retune. A command's own IK parameters win over these while that command is tracked.

Builds settings that retune the arms given and leave the rest as they are.

Parameters:

| Name     | Type                                  | Description                                                                                                                         | Default |
| -------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `tuning` | `Sequence[ArmTuning \| None] \| None` | One ArmTuning or None per arm, in slot order; a None entry, an arm past the list, or a None list leaves that arm's tuning as it is. | `None`  |
| `ik`     | `Sequence[IkParams \| None] \| None`  | One anvil.kinematics.IkParams or None per arm, in slot order, left as tuning is.                                                    | `None`  |

Raises:

| Type                   | Description                                   |
| ---------------------- | --------------------------------------------- |
| `TypeError`            | tuning or ik is one value rather than a list. |
| `InvalidArgumentError` | tuning or ik has more than MAX_ARMS entries.  |

Attributes:

| Name     | Type                      | Description                                                                                           |
| -------- | ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `tuning` | `list[ArmTuning \| None]` | One ArmTuning or None per arm in slot order, through the last arm given: the settings' own.           |
| `ik`     | `list[IkParams \| None]`  | One anvil.kinematics.IkParams or None per arm, slot order, through the last given: the settings' own. |

#### tuning

```
tuning: list[ArmTuning | None] = []
```

One `ArmTuning` or `None` per arm in slot order, through the last arm given: the settings' own.

#### ik

```
ik: list[IkParams | None] = []
```

One `anvil.kinematics.IkParams` or `None` per arm, slot order, through the last given: the settings' own.

### ControlArm

```
ControlArm()
```

One arm as a controller drives it: the slots it owns, its chain, and the tuning its description states.

Its slots run from its first: its chain joints in chain order, then its gripper when it has one. One comes from `control_arms`.

Get one from `control_arms`, never by calling the class.

Methods:

| Name              | Description                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------------- |
| `tip_pose`        | Returns where its tool frame is at the slot positions q, in the world frame.                       |
| `gripper_width_m` | Returns the jaws' opening at the slot positions q, in meters, read back through the gripper's map. |

Attributes:

| Name           | Type          | Description                                                                                        |
| -------------- | ------------- | -------------------------------------------------------------------------------------------------- |
| `chain_slots`  | `list[int]`   | Its chain joints' slots, in chain order.                                                           |
| `gripper_slot` | `int \| None` | Its gripper's slot — the last it owns — or None without one.                                       |
| `tuning`       | `ArmTuning`   | A copy of the tuning it runs with, as its description states it for the controller it was cut for. |

#### chain_slots

```
chain_slots: list[int]
```

Its chain joints' slots, in chain order.

#### gripper_slot

```
gripper_slot: int | None
```

Its gripper's slot — the last it owns — or `None` without one.

#### tuning

```
tuning: ArmTuning
```

A copy of the tuning it runs with, as its description states it for the controller it was cut for.

#### tip_pose

```
tip_pose(q: Sequence[float]) -> Pose
```

Returns where its tool frame is at the slot positions `q`, in the world frame.

Parameters:

| Name | Type              | Description                                                                                                                                 | Default    |
| ---- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | Position per slot of the whole robot, in radians, as a Snapshot's q or q_commanded carries them; entries past the arm's slots are not read. | *required* |

Returns:

| Type   | Description            |
| ------ | ---------------------- |
| `Pose` | The tool frame's pose. |

Raises:

| Type                   | Description                           |
| ---------------------- | ------------------------------------- |
| `TypeError`            | q holds something other than numbers. |
| `InvalidArgumentError` | q stops short of the arm's last slot. |

#### gripper_width_m

```
gripper_width_m(q: Sequence[float]) -> float | None
```

Returns the jaws' opening at the slot positions `q`, in meters, read back through the gripper's map.

Parameters:

| Name | Type              | Description                                                             | Default    |
| ---- | ----------------- | ----------------------------------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | Position per slot of the whole robot, in radians, as tip_pose takes it. | *required* |

Returns:

| Type            | Description                                     |
| --------------- | ----------------------------------------------- |
| `float \| None` | The opening; None for an arm without a gripper. |

Raises:

| Type                   | Description                           |
| ---------------------- | ------------------------------------- |
| `TypeError`            | q holds something other than numbers. |
| `InvalidArgumentError` | q stops short of the arm's last slot. |

### CollisionClosing

Bases: `NamedTuple`

The link whose closing the collision stop held the arms for, what on, and how near, from `CollisionHold`.

Attributes:

| Name         | Type          | Description                                                                                                  |
| ------------ | ------------- | ------------------------------------------------------------------------------------------------------------ |
| `link`       | `str`         | The link that was closing, as the composed URDF names it.                                                    |
| `on_link`    | `str \| None` | The other link it was closing on; None where it was closing on a keep-out box.                               |
| `keep_out`   | `str \| None` | The keep-out box it was closing on, by name; None where it was closing on another link.                      |
| `distance_m` | `float`       | How near the two stood as the stop last measured them before it took hold, in meters; negative inside a box. |
| `margin_m`   | `float`       | The margin the stop held them short of, in meters.                                                           |

#### link

```
link: str
```

The link that was closing, as the composed URDF names it.

#### on_link

```
on_link: str | None
```

The other link it was closing on; `None` where it was closing on a keep-out box.

#### keep_out

```
keep_out: str | None
```

The keep-out box it was closing on, by name; `None` where it was closing on another link.

#### distance_m

```
distance_m: float
```

How near the two stood as the stop last measured them before it took hold, in meters; negative inside a box.

#### margin_m

```
margin_m: float
```

The margin the stop held them short of, in meters.

### CollisionHold

Bases: `NamedTuple`

Why the collision stop held the arms, as `CollisionStop.last_stop` reports it.

Attributes:

| Name      | Type                       | Description                                                                                             |
| --------- | -------------------------- | ------------------------------------------------------------------------------------------------------- |
| `why`     | `str`                      | Which link was closing on what, and how near, as a person reads it.                                     |
| `closing` | `CollisionClosing \| None` | The link that was closing and what on; None where the stop failed, holding the arms wherever they were. |

#### why

```
why: str
```

Which link was closing on what, and how near, as a person reads it.

#### closing

```
closing: CollisionClosing | None
```

The link that was closing and what on; `None` where the stop failed, holding the arms wherever they were.

### CollisionStop

```
CollisionStop(
    workcell: WorkcellConfig,
    within_arm_margin_m: float | None = DEFAULT_MARGIN_M,
    between_arms_margin_m: float | None = DEFAULT_MARGIN_M,
    keep_out_margins_m: Mapping[str, float] | None = None,
)
```

A guard that holds the arms short of hitting themselves or a keep-out box, whichever controller drives them.

Hand the one stop to every controller of the robot, `JointController(robot=robot, collision_stop=stop)`, and each tick it slows the step a controller is about to send, but only along what that step closes on. Every pair, two links or a link and a box, that stands within its margin and a few centimeters past it is a gap the arms may close only so fast: by a share of what is left of it past the margin each tick, so a pair slows as it nears its margin and stops there, within about 150 milliseconds. The rest of the step goes through, so an arm driven sideways into something slides along it, and one driven away leaves at once. A pair already inside its margin, as the arms start or as a box goes up around them, may only open: the arms may move out of a box, never deeper in. Two links of one arm are held to one margin, a link of one arm and one of another arm or of the torso to another, and each box to its own; `anvil.workcell.Clearance.arm_of` says whose a link is. A trajectory the stop slows ends there. The gaps are measured on a thread of the stop's own; should it fail, or fall behind where the arms go, every controller holds the arms where they are.

The cut is made joint by joint, so an arm driven by joint targets that run on through what it would hit, as a joint stream or a leader does, comes to rest at the point nearest its target joint by joint, which slides it less than a hand driven by pose. It is a stop, not a guarantee: it measures the collision pieces' hulls, and steps along each gap as a straight line from where it was measured, so an arm can come inside a margin by what that misses. It judges the targets, not the measured arm: an arm too soft to hold its own weight, under a low gain override, sags past where it is held. It lives as long as this object or any controller holding it does.

Builds a stop for the robot of `workcell` and starts its thread.

Parameters:

| Name                    | Type                          | Description                                                                                                                                       | Default            |
| ----------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `workcell`              | `WorkcellConfig`              | The workcell whose robot and keep-out boxes the stop guards.                                                                                      | *required*         |
| `within_arm_margin_m`   | `float \| None`               | How near two links of one arm may close, in meters; None lets them touch.                                                                         | `DEFAULT_MARGIN_M` |
| `between_arms_margin_m` | `float \| None`               | How near a link of one arm may close on one of another arm, or of the torso the arms stand on, in meters; None lets them touch.                   | `DEFAULT_MARGIN_M` |
| `keep_out_margins_m`    | `Mapping[str, float] \| None` | How near a link may close on each keep-out box, in meters, by box name; a box left out is not guarded. None guards every box at DEFAULT_MARGIN_M. | `None`             |

Raises:

| Type                   | Description                                                                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A margin is negative or not a number, keep_out_margins_m names a box the workcell has none of, or the robot's collision geometry cannot be built. |

Methods:

| Name        | Description                                                                                             |
| ----------- | ------------------------------------------------------------------------------------------------------- |
| `configure` | Changes the margins and the keep-out boxes guarded, from the stop's next measure.                       |
| `holding`   | Returns whether the stop holds the arms short now: it slowed a step in the last 0.3 seconds, or failed. |
| `last_stop` | Returns why the stop last held the arms, kept after it lets them go; None until it first has.           |

Attributes:

| Name               | Type    | Description                                                                     |
| ------------------ | ------- | ------------------------------------------------------------------------------- |
| `DEFAULT_MARGIN_M` | `float` | The margin a stop holds links and boxes at unless asked for another, in meters. |

#### DEFAULT_MARGIN_M

```
DEFAULT_MARGIN_M: float = 0.002
```

The margin a stop holds links and boxes at unless asked for another, in meters.

#### configure

```
configure(
    workcell: WorkcellConfig,
    within_arm_margin_m: float | None = DEFAULT_MARGIN_M,
    between_arms_margin_m: float | None = DEFAULT_MARGIN_M,
    keep_out_margins_m: Mapping[str, float] | None = None,
) -> None
```

Changes the margins and the keep-out boxes guarded, from the stop's next measure.

The robot stays the one the stop was made for; only `workcell`'s keep-out boxes are read. A box put up around the arms holds them in no more than it must: they may move out of it, never deeper. Settings equal to the ones the stop has change nothing.

Parameters:

| Name                    | Type                          | Description                                                                                                                                       | Default            |
| ----------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `workcell`              | `WorkcellConfig`              | The workcell whose keep-out boxes the stop guards from now on.                                                                                    | *required*         |
| `within_arm_margin_m`   | `float \| None`               | How near two links of one arm may close, in meters; None lets them touch.                                                                         | `DEFAULT_MARGIN_M` |
| `between_arms_margin_m` | `float \| None`               | How near a link of one arm may close on one of another arm, or of the torso the arms stand on, in meters; None lets them touch.                   | `DEFAULT_MARGIN_M` |
| `keep_out_margins_m`    | `Mapping[str, float] \| None` | How near a link may close on each keep-out box, in meters, by box name; a box left out is not guarded. None guards every box at DEFAULT_MARGIN_M. | `None`             |

Raises:

| Type                   | Description                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A margin is negative or not a number, or keep_out_margins_m names a box workcell has none of. |

#### holding

```
holding() -> bool
```

Returns whether the stop holds the arms short now: it slowed a step in the last 0.3 seconds, or failed.

#### last_stop

```
last_stop() -> CollisionHold | None
```

Returns why the stop last held the arms, kept after it lets them go; `None` until it first has.

### Controller

```
Controller(
    robot: RobotConfig,
    collision_stop: CollisionStop | None = None,
)
```

Bases: `Part`

What the four controllers share: built for a robot, warm-started, ticked and retuned the same way.

It is the type a `Runtime` takes its controllers as and `Runtime.controller` looks one up by. Build one of the four, `JointController`, `JointTrajectoryController`, `EePoseController` or `LeaderFollowerController`, never this.

Builds the controller for `robot`, each arm tuned as its description states for this controller.

Its arms are `robot`'s in slot order, as `control_arms` cuts them for this class, and a command's slots are theirs. Hand it to a `Runtime`, which takes it over; ticked by hand instead, call `activate` before the first `tick`.

Parameters:

| Name             | Type                    | Description                                                                                                                                             | Default    |
| ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `robot`          | `RobotConfig`           | The robot to drive.                                                                                                                                     | *required* |
| `collision_stop` | `CollisionStop \| None` | The stop that holds every target this controller sends short of a collision, built from the workcell whose robot is robot; None sends targets into one. | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | robot is refused or its chains cannot be cut from its URDF; collision_stop was built for another robot, its URDF, placement or slots not robot's; for a LeaderFollowerController, robot has no leader. |

Methods:

| Name                | Description                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------ |
| `activate`          | Warm-starts the controller from measured state; call it before the first tick.                   |
| `tick`              | Runs one tick on measured state, tracking command, and returns what to send and why.             |
| `deactivate`        | Stops tracking, before another controller takes over; whatever comes next does its own blending. |
| `configure`         | Retunes the arms; an arm the settings leave None keeps what it runs with.                        |
| `settings`          | Returns what the controller is configured with now, every arm it drives filled in.               |
| `validate_settings` | Checks settings against what the controller runs with now, without applying it.                  |
| `close`             | Frees the part now rather than once it is collected.                                             |

#### activate

```
activate(
    t_ns: int,
    seq: int,
    state: JointState,
    rate_hz: int | None = None,
    last_written: RobotCommand | None = None,
) -> None
```

Warm-starts the controller from measured state; call it before the first `tick`.

The gains ramp from those of `last_written` to what the tunings ask for, over each arm's `JointGuards.gain_slew_s`; out of idle that is from zero, so the arm comes up limp and takes its gains on. Each slot's target starts where `last_written` held it where that had a position gain, and where the slot measures otherwise, clamped to its limits; the first tick's command moves the chain on from there no faster than the arm's `SpeedLimits`, as every later one does.

Parameters:

| Name           | Type                   | Description                                                                                               | Default    |
| -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- | ---------- |
| `t_ns`         | `int`                  | When state was measured, in nanoseconds, on the hw interface's clock.                                     | *required* |
| `seq`          | `int`                  | The state's sequence number.                                                                              | *required* |
| `state`        | `JointState`           | Measured state of every slot.                                                                             | *required* |
| `rate_hz`      | `int \| None`          | The rate the caller ticks at, which the gain ramp is counted in: 250 or 500; None for the loop's default. | `None`     |
| `last_written` | `RobotCommand \| None` | What was sent the tick before: another controller's output, at a switch; None for limp, as out of idle.   | `None`     |

Raises:

| Type                   | Description                             |
| ---------------------- | --------------------------------------- |
| `InvalidArgumentError` | rate_hz is not a rate the loop runs at. |

#### tick

```
tick(
    t_ns: int,
    seq: int,
    state: JointState,
    command: JointCommand
    | JointTrajectoryCommand
    | TimedWaypoints
    | EePoseCommand
    | None = None,
    command_t_ns: int | None = None,
    command_seq: int | None = None,
) -> tuple[
    RobotCommand,
    JointTrace
    | JointTrajectoryTrace
    | EePoseTrace
    | LeaderFollowerTrace,
]
```

Runs one tick on measured `state`, tracking `command`, and returns what to send and why.

A command is taken only on a tick where `command_seq` differs from the last one taken, and where it covers every slot and arm the controller drives; the `JointController` and the `EePoseController` also skip one older than 100 milliseconds by `command_t_ns` against `t_ns`. A command not taken leaves the controller on its last target. The `JointTrajectoryController` starts a trajectory at `t_ns` of the tick it takes it, whatever its stamp.

Parameters:

| Name           | Type                                                                                | Description                                                                                                                                                                          | Default    |
| -------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `t_ns`         | `int`                                                                               | When state was measured, in nanoseconds, on the hw interface's clock.                                                                                                                | *required* |
| `seq`          | `int`                                                                               | The state's sequence number.                                                                                                                                                         | *required* |
| `state`        | `JointState`                                                                        | Measured state of every slot.                                                                                                                                                        | *required* |
| `command`      | `JointCommand \| JointTrajectoryCommand \| TimedWaypoints \| EePoseCommand \| None` | The latest command, of this controller's command class, or for the JointTrajectoryController the anvil.workcell.TimedWaypoints it is built from; None for none, as between commands. | `None`     |
| `command_t_ns` | `int \| None`                                                                       | When the command was sent, in nanoseconds, on the clock t_ns is on; required with a command.                                                                                         | `None`     |
| `command_seq`  | `int \| None`                                                                       | The command's sequence number, counting from 1; required with a command.                                                                                                             | `None`     |

Returns:

| Type                                                                                            | Description                                                                                              |
| ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `tuple[RobotCommand, JointTrace \| JointTrajectoryTrace \| EePoseTrace \| LeaderFollowerTrace]` | The tick's RobotCommand, and this controller's trace: a JointTrace, JointTrajectoryTrace or EePoseTrace. |

Raises:

| Type                   | Description                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `TypeError`            | command is another controller's command class, or is given without command_t_ns and command_seq. |
| `InvalidArgumentError` | command_seq is 0 with a command.                                                                 |

#### deactivate

```
deactivate() -> None
```

Stops tracking, before another controller takes over; whatever comes next does its own blending.

#### configure

```
configure(settings: ControllerSettings) -> None
```

Retunes the arms; an arm the settings leave `None` keeps what it runs with.

The gains slew to the new tuning over each arm's `JointGuards.gain_slew_s`. Nothing changes when `settings` is refused.

Parameters:

| Name       | Type                 | Description                                                           | Default    |
| ---------- | -------------------- | --------------------------------------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class, usually its settings(), edited. | *required* |

Raises:

| Type                   | Description                                      |
| ---------------------- | ------------------------------------------------ |
| `TypeError`            | settings is another controller's settings class. |
| `InvalidArgumentError` | settings is one validate_settings refuses.       |

#### settings

```
settings() -> ControllerSettings
```

Returns what the controller is configured with now, every arm it drives filled in.

The objects in it are the settings' own, so an edit to one, then `configure`, is a retune.

#### validate_settings

```
validate_settings(settings: ControllerSettings) -> None
```

Checks `settings` against what the controller runs with now, without applying it.

Parameters:

| Name       | Type                 | Description                           | Default    |
| ---------- | -------------------- | ------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | settings is another controller's settings class.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `InvalidArgumentError` | A tuning or IK parameters for an arm the controller does not drive; a tuning whose chain joints or gripper differ from that arm's; a gain or gravity scale that is negative or not finite, an offset that is not finite, or a gain past its guard's ceiling; a speed that is not positive; a grip assist number that is negative or not finite; for the EePoseController, IK parameters that are refused; for the LeaderFollowerController, a stiffness_share that is not from 0 to 0.4. |

#### close

```
close() -> None
```

Frees the part now rather than once it is collected.

A hw interface lets go of its buses, disabling the drives it enabled. A part a runtime took is the runtime's to free, and closing it does nothing.

### JointController

```
JointController(
    robot: RobotConfig,
    collision_stop: CollisionStop | None = None,
)
```

Bases: `Controller`

The controller that tracks streamed joint targets, one `JointCommand` at a time, for every arm of a robot.

Each command's positions are carried on at its velocities until the next arrives, and every command after the first since `activate` glides in from the targets being sent over the interval commands have been arriving at, so a stream that arrives jittered moves the arm as smoothly as it was sent; one that sends no velocities trails by about one interval. A command older than 100 milliseconds on the state's clock is stale: the controller holds where it carried the arm, at rest, and eases a command's overrides back to the tunings. A position past a joint's limit is commanded at the limit. Each gripper is asked for by its arm's `GripperCommand`, not by its slot. `configure` and `settings` speak a `JointControllerSettings`.

Every method but `close` raises `anvil.OperationError` once it is closed, or once a `Runtime` has taken it.

Builds the controller for `robot`, each arm tuned as its description states for this controller.

Its arms are `robot`'s in slot order, as `control_arms` cuts them for this class, and a command's slots are theirs. Hand it to a `Runtime`, which takes it over; ticked by hand instead, call `activate` before the first `tick`.

Parameters:

| Name             | Type                    | Description                                                                                                                                             | Default    |
| ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `robot`          | `RobotConfig`           | The robot to drive.                                                                                                                                     | *required* |
| `collision_stop` | `CollisionStop \| None` | The stop that holds every target this controller sends short of a collision, built from the workcell whose robot is robot; None sends targets into one. | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | robot is refused or its chains cannot be cut from its URDF; collision_stop was built for another robot, its URDF, placement or slots not robot's; for a LeaderFollowerController, robot has no leader. |

Methods:

| Name                | Description                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------ |
| `close`             | Frees the part now rather than once it is collected.                                             |
| `activate`          | Warm-starts the controller from measured state; call it before the first tick.                   |
| `tick`              | Runs one tick on measured state, tracking command, and returns what to send and why.             |
| `deactivate`        | Stops tracking, before another controller takes over; whatever comes next does its own blending. |
| `configure`         | Retunes the arms; an arm the settings leave None keeps what it runs with.                        |
| `settings`          | Returns what the controller is configured with now, every arm it drives filled in.               |
| `validate_settings` | Checks settings against what the controller runs with now, without applying it.                  |

#### close

```
close() -> None
```

Frees the part now rather than once it is collected.

A hw interface lets go of its buses, disabling the drives it enabled. A part a runtime took is the runtime's to free, and closing it does nothing.

#### activate

```
activate(
    t_ns: int,
    seq: int,
    state: JointState,
    rate_hz: int | None = None,
    last_written: RobotCommand | None = None,
) -> None
```

Warm-starts the controller from measured state; call it before the first `tick`.

The gains ramp from those of `last_written` to what the tunings ask for, over each arm's `JointGuards.gain_slew_s`; out of idle that is from zero, so the arm comes up limp and takes its gains on. Each slot's target starts where `last_written` held it where that had a position gain, and where the slot measures otherwise, clamped to its limits; the first tick's command moves the chain on from there no faster than the arm's `SpeedLimits`, as every later one does.

Parameters:

| Name           | Type                   | Description                                                                                               | Default    |
| -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- | ---------- |
| `t_ns`         | `int`                  | When state was measured, in nanoseconds, on the hw interface's clock.                                     | *required* |
| `seq`          | `int`                  | The state's sequence number.                                                                              | *required* |
| `state`        | `JointState`           | Measured state of every slot.                                                                             | *required* |
| `rate_hz`      | `int \| None`          | The rate the caller ticks at, which the gain ramp is counted in: 250 or 500; None for the loop's default. | `None`     |
| `last_written` | `RobotCommand \| None` | What was sent the tick before: another controller's output, at a switch; None for limp, as out of idle.   | `None`     |

Raises:

| Type                   | Description                             |
| ---------------------- | --------------------------------------- |
| `InvalidArgumentError` | rate_hz is not a rate the loop runs at. |

#### tick

```
tick(
    t_ns: int,
    seq: int,
    state: JointState,
    command: JointCommand
    | JointTrajectoryCommand
    | TimedWaypoints
    | EePoseCommand
    | None = None,
    command_t_ns: int | None = None,
    command_seq: int | None = None,
) -> tuple[
    RobotCommand,
    JointTrace
    | JointTrajectoryTrace
    | EePoseTrace
    | LeaderFollowerTrace,
]
```

Runs one tick on measured `state`, tracking `command`, and returns what to send and why.

A command is taken only on a tick where `command_seq` differs from the last one taken, and where it covers every slot and arm the controller drives; the `JointController` and the `EePoseController` also skip one older than 100 milliseconds by `command_t_ns` against `t_ns`. A command not taken leaves the controller on its last target. The `JointTrajectoryController` starts a trajectory at `t_ns` of the tick it takes it, whatever its stamp.

Parameters:

| Name           | Type                                                                                | Description                                                                                                                                                                          | Default    |
| -------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `t_ns`         | `int`                                                                               | When state was measured, in nanoseconds, on the hw interface's clock.                                                                                                                | *required* |
| `seq`          | `int`                                                                               | The state's sequence number.                                                                                                                                                         | *required* |
| `state`        | `JointState`                                                                        | Measured state of every slot.                                                                                                                                                        | *required* |
| `command`      | `JointCommand \| JointTrajectoryCommand \| TimedWaypoints \| EePoseCommand \| None` | The latest command, of this controller's command class, or for the JointTrajectoryController the anvil.workcell.TimedWaypoints it is built from; None for none, as between commands. | `None`     |
| `command_t_ns` | `int \| None`                                                                       | When the command was sent, in nanoseconds, on the clock t_ns is on; required with a command.                                                                                         | `None`     |
| `command_seq`  | `int \| None`                                                                       | The command's sequence number, counting from 1; required with a command.                                                                                                             | `None`     |

Returns:

| Type                                                                                            | Description                                                                                              |
| ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `tuple[RobotCommand, JointTrace \| JointTrajectoryTrace \| EePoseTrace \| LeaderFollowerTrace]` | The tick's RobotCommand, and this controller's trace: a JointTrace, JointTrajectoryTrace or EePoseTrace. |

Raises:

| Type                   | Description                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `TypeError`            | command is another controller's command class, or is given without command_t_ns and command_seq. |
| `InvalidArgumentError` | command_seq is 0 with a command.                                                                 |

#### deactivate

```
deactivate() -> None
```

Stops tracking, before another controller takes over; whatever comes next does its own blending.

#### configure

```
configure(settings: ControllerSettings) -> None
```

Retunes the arms; an arm the settings leave `None` keeps what it runs with.

The gains slew to the new tuning over each arm's `JointGuards.gain_slew_s`. Nothing changes when `settings` is refused.

Parameters:

| Name       | Type                 | Description                                                           | Default    |
| ---------- | -------------------- | --------------------------------------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class, usually its settings(), edited. | *required* |

Raises:

| Type                   | Description                                      |
| ---------------------- | ------------------------------------------------ |
| `TypeError`            | settings is another controller's settings class. |
| `InvalidArgumentError` | settings is one validate_settings refuses.       |

#### settings

```
settings() -> ControllerSettings
```

Returns what the controller is configured with now, every arm it drives filled in.

The objects in it are the settings' own, so an edit to one, then `configure`, is a retune.

#### validate_settings

```
validate_settings(settings: ControllerSettings) -> None
```

Checks `settings` against what the controller runs with now, without applying it.

Parameters:

| Name       | Type                 | Description                           | Default    |
| ---------- | -------------------- | ------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | settings is another controller's settings class.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `InvalidArgumentError` | A tuning or IK parameters for an arm the controller does not drive; a tuning whose chain joints or gripper differ from that arm's; a gain or gravity scale that is negative or not finite, an offset that is not finite, or a gain past its guard's ceiling; a speed that is not positive; a grip assist number that is negative or not finite; for the EePoseController, IK parameters that are refused; for the LeaderFollowerController, a stiffness_share that is not from 0 to 0.4. |

### JointTrajectoryController

```
JointTrajectoryController(
    robot: RobotConfig,
    collision_stop: CollisionStop | None = None,
)
```

Bases: `Controller`

The controller that plays a `JointTrajectoryCommand`'s timed waypoints and holds the last one.

A trajectory starts from the targets the controller is sending when it takes the command, so nothing steps, and a new one replaces the one playing. It is played once, so it is never stale. The gripper is a slot like any other: a waypoint states its motor angle. It plays the `anvil.workcell.TimedWaypoints` a robot plans, such as `anvil.workcell.RobotConfig.home_trajectory`, as they are. `configure` and `settings` speak a `JointTrajectoryControllerSettings`.

Every method but `close` raises `anvil.OperationError` once it is closed, or once a `Runtime` has taken it.

Builds the controller for `robot`, each arm tuned as its description states for this controller.

Its arms are `robot`'s in slot order, as `control_arms` cuts them for this class, and a command's slots are theirs. Hand it to a `Runtime`, which takes it over; ticked by hand instead, call `activate` before the first `tick`.

Parameters:

| Name             | Type                    | Description                                                                                                                                             | Default    |
| ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `robot`          | `RobotConfig`           | The robot to drive.                                                                                                                                     | *required* |
| `collision_stop` | `CollisionStop \| None` | The stop that holds every target this controller sends short of a collision, built from the workcell whose robot is robot; None sends targets into one. | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | robot is refused or its chains cannot be cut from its URDF; collision_stop was built for another robot, its URDF, placement or slots not robot's; for a LeaderFollowerController, robot has no leader. |

Methods:

| Name                | Description                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------ |
| `close`             | Frees the part now rather than once it is collected.                                             |
| `activate`          | Warm-starts the controller from measured state; call it before the first tick.                   |
| `tick`              | Runs one tick on measured state, tracking command, and returns what to send and why.             |
| `deactivate`        | Stops tracking, before another controller takes over; whatever comes next does its own blending. |
| `configure`         | Retunes the arms; an arm the settings leave None keeps what it runs with.                        |
| `settings`          | Returns what the controller is configured with now, every arm it drives filled in.               |
| `validate_settings` | Checks settings against what the controller runs with now, without applying it.                  |

#### close

```
close() -> None
```

Frees the part now rather than once it is collected.

A hw interface lets go of its buses, disabling the drives it enabled. A part a runtime took is the runtime's to free, and closing it does nothing.

#### activate

```
activate(
    t_ns: int,
    seq: int,
    state: JointState,
    rate_hz: int | None = None,
    last_written: RobotCommand | None = None,
) -> None
```

Warm-starts the controller from measured state; call it before the first `tick`.

The gains ramp from those of `last_written` to what the tunings ask for, over each arm's `JointGuards.gain_slew_s`; out of idle that is from zero, so the arm comes up limp and takes its gains on. Each slot's target starts where `last_written` held it where that had a position gain, and where the slot measures otherwise, clamped to its limits; the first tick's command moves the chain on from there no faster than the arm's `SpeedLimits`, as every later one does.

Parameters:

| Name           | Type                   | Description                                                                                               | Default    |
| -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- | ---------- |
| `t_ns`         | `int`                  | When state was measured, in nanoseconds, on the hw interface's clock.                                     | *required* |
| `seq`          | `int`                  | The state's sequence number.                                                                              | *required* |
| `state`        | `JointState`           | Measured state of every slot.                                                                             | *required* |
| `rate_hz`      | `int \| None`          | The rate the caller ticks at, which the gain ramp is counted in: 250 or 500; None for the loop's default. | `None`     |
| `last_written` | `RobotCommand \| None` | What was sent the tick before: another controller's output, at a switch; None for limp, as out of idle.   | `None`     |

Raises:

| Type                   | Description                             |
| ---------------------- | --------------------------------------- |
| `InvalidArgumentError` | rate_hz is not a rate the loop runs at. |

#### tick

```
tick(
    t_ns: int,
    seq: int,
    state: JointState,
    command: JointCommand
    | JointTrajectoryCommand
    | TimedWaypoints
    | EePoseCommand
    | None = None,
    command_t_ns: int | None = None,
    command_seq: int | None = None,
) -> tuple[
    RobotCommand,
    JointTrace
    | JointTrajectoryTrace
    | EePoseTrace
    | LeaderFollowerTrace,
]
```

Runs one tick on measured `state`, tracking `command`, and returns what to send and why.

A command is taken only on a tick where `command_seq` differs from the last one taken, and where it covers every slot and arm the controller drives; the `JointController` and the `EePoseController` also skip one older than 100 milliseconds by `command_t_ns` against `t_ns`. A command not taken leaves the controller on its last target. The `JointTrajectoryController` starts a trajectory at `t_ns` of the tick it takes it, whatever its stamp.

Parameters:

| Name           | Type                                                                                | Description                                                                                                                                                                          | Default    |
| -------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `t_ns`         | `int`                                                                               | When state was measured, in nanoseconds, on the hw interface's clock.                                                                                                                | *required* |
| `seq`          | `int`                                                                               | The state's sequence number.                                                                                                                                                         | *required* |
| `state`        | `JointState`                                                                        | Measured state of every slot.                                                                                                                                                        | *required* |
| `command`      | `JointCommand \| JointTrajectoryCommand \| TimedWaypoints \| EePoseCommand \| None` | The latest command, of this controller's command class, or for the JointTrajectoryController the anvil.workcell.TimedWaypoints it is built from; None for none, as between commands. | `None`     |
| `command_t_ns` | `int \| None`                                                                       | When the command was sent, in nanoseconds, on the clock t_ns is on; required with a command.                                                                                         | `None`     |
| `command_seq`  | `int \| None`                                                                       | The command's sequence number, counting from 1; required with a command.                                                                                                             | `None`     |

Returns:

| Type                                                                                            | Description                                                                                              |
| ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `tuple[RobotCommand, JointTrace \| JointTrajectoryTrace \| EePoseTrace \| LeaderFollowerTrace]` | The tick's RobotCommand, and this controller's trace: a JointTrace, JointTrajectoryTrace or EePoseTrace. |

Raises:

| Type                   | Description                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `TypeError`            | command is another controller's command class, or is given without command_t_ns and command_seq. |
| `InvalidArgumentError` | command_seq is 0 with a command.                                                                 |

#### deactivate

```
deactivate() -> None
```

Stops tracking, before another controller takes over; whatever comes next does its own blending.

#### configure

```
configure(settings: ControllerSettings) -> None
```

Retunes the arms; an arm the settings leave `None` keeps what it runs with.

The gains slew to the new tuning over each arm's `JointGuards.gain_slew_s`. Nothing changes when `settings` is refused.

Parameters:

| Name       | Type                 | Description                                                           | Default    |
| ---------- | -------------------- | --------------------------------------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class, usually its settings(), edited. | *required* |

Raises:

| Type                   | Description                                      |
| ---------------------- | ------------------------------------------------ |
| `TypeError`            | settings is another controller's settings class. |
| `InvalidArgumentError` | settings is one validate_settings refuses.       |

#### settings

```
settings() -> ControllerSettings
```

Returns what the controller is configured with now, every arm it drives filled in.

The objects in it are the settings' own, so an edit to one, then `configure`, is a retune.

#### validate_settings

```
validate_settings(settings: ControllerSettings) -> None
```

Checks `settings` against what the controller runs with now, without applying it.

Parameters:

| Name       | Type                 | Description                           | Default    |
| ---------- | -------------------- | ------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | settings is another controller's settings class.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `InvalidArgumentError` | A tuning or IK parameters for an arm the controller does not drive; a tuning whose chain joints or gripper differ from that arm's; a gain or gravity scale that is negative or not finite, an offset that is not finite, or a gain past its guard's ceiling; a speed that is not positive; a grip assist number that is negative or not finite; for the EePoseController, IK parameters that are refused; for the LeaderFollowerController, a stiffness_share that is not from 0 to 0.4. |

### EePoseController

```
EePoseController(
    robot: RobotConfig,
    collision_stop: CollisionStop | None = None,
)
```

Bases: `Controller`

The controller that tracks streamed end-effector poses, one `EePoseCommand` at a time, solving each with IK.

Each new target is solved from the previous solution, or from the measured joints where it asks to reseed, and the arm walks toward the solution no faster than the hand driving it plus a floor. A command older than 100 milliseconds on the state's clock is stale: the controller holds where it is, at rest. Positions are clamped to each joint's limits and the gripper's travel. An arm without a gripper is driven too, its grip not read. `configure` and `settings` speak an `EePoseControllerSettings`, which carries each arm's IK parameters as well as its tuning.

Every method but `close` raises `anvil.OperationError` once it is closed, or once a `Runtime` has taken it.

Builds the controller for `robot`, each arm tuned as its description states for this controller.

Its arms are `robot`'s in slot order, as `control_arms` cuts them for this class, and a command's slots are theirs. Hand it to a `Runtime`, which takes it over; ticked by hand instead, call `activate` before the first `tick`.

Parameters:

| Name             | Type                    | Description                                                                                                                                             | Default    |
| ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `robot`          | `RobotConfig`           | The robot to drive.                                                                                                                                     | *required* |
| `collision_stop` | `CollisionStop \| None` | The stop that holds every target this controller sends short of a collision, built from the workcell whose robot is robot; None sends targets into one. | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | robot is refused or its chains cannot be cut from its URDF; collision_stop was built for another robot, its URDF, placement or slots not robot's; for a LeaderFollowerController, robot has no leader. |

Methods:

| Name                | Description                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------ |
| `close`             | Frees the part now rather than once it is collected.                                             |
| `activate`          | Warm-starts the controller from measured state; call it before the first tick.                   |
| `tick`              | Runs one tick on measured state, tracking command, and returns what to send and why.             |
| `deactivate`        | Stops tracking, before another controller takes over; whatever comes next does its own blending. |
| `configure`         | Retunes the arms; an arm the settings leave None keeps what it runs with.                        |
| `settings`          | Returns what the controller is configured with now, every arm it drives filled in.               |
| `validate_settings` | Checks settings against what the controller runs with now, without applying it.                  |

#### close

```
close() -> None
```

Frees the part now rather than once it is collected.

A hw interface lets go of its buses, disabling the drives it enabled. A part a runtime took is the runtime's to free, and closing it does nothing.

#### activate

```
activate(
    t_ns: int,
    seq: int,
    state: JointState,
    rate_hz: int | None = None,
    last_written: RobotCommand | None = None,
) -> None
```

Warm-starts the controller from measured state; call it before the first `tick`.

The gains ramp from those of `last_written` to what the tunings ask for, over each arm's `JointGuards.gain_slew_s`; out of idle that is from zero, so the arm comes up limp and takes its gains on. Each slot's target starts where `last_written` held it where that had a position gain, and where the slot measures otherwise, clamped to its limits; the first tick's command moves the chain on from there no faster than the arm's `SpeedLimits`, as every later one does.

Parameters:

| Name           | Type                   | Description                                                                                               | Default    |
| -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- | ---------- |
| `t_ns`         | `int`                  | When state was measured, in nanoseconds, on the hw interface's clock.                                     | *required* |
| `seq`          | `int`                  | The state's sequence number.                                                                              | *required* |
| `state`        | `JointState`           | Measured state of every slot.                                                                             | *required* |
| `rate_hz`      | `int \| None`          | The rate the caller ticks at, which the gain ramp is counted in: 250 or 500; None for the loop's default. | `None`     |
| `last_written` | `RobotCommand \| None` | What was sent the tick before: another controller's output, at a switch; None for limp, as out of idle.   | `None`     |

Raises:

| Type                   | Description                             |
| ---------------------- | --------------------------------------- |
| `InvalidArgumentError` | rate_hz is not a rate the loop runs at. |

#### tick

```
tick(
    t_ns: int,
    seq: int,
    state: JointState,
    command: JointCommand
    | JointTrajectoryCommand
    | TimedWaypoints
    | EePoseCommand
    | None = None,
    command_t_ns: int | None = None,
    command_seq: int | None = None,
) -> tuple[
    RobotCommand,
    JointTrace
    | JointTrajectoryTrace
    | EePoseTrace
    | LeaderFollowerTrace,
]
```

Runs one tick on measured `state`, tracking `command`, and returns what to send and why.

A command is taken only on a tick where `command_seq` differs from the last one taken, and where it covers every slot and arm the controller drives; the `JointController` and the `EePoseController` also skip one older than 100 milliseconds by `command_t_ns` against `t_ns`. A command not taken leaves the controller on its last target. The `JointTrajectoryController` starts a trajectory at `t_ns` of the tick it takes it, whatever its stamp.

Parameters:

| Name           | Type                                                                                | Description                                                                                                                                                                          | Default    |
| -------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `t_ns`         | `int`                                                                               | When state was measured, in nanoseconds, on the hw interface's clock.                                                                                                                | *required* |
| `seq`          | `int`                                                                               | The state's sequence number.                                                                                                                                                         | *required* |
| `state`        | `JointState`                                                                        | Measured state of every slot.                                                                                                                                                        | *required* |
| `command`      | `JointCommand \| JointTrajectoryCommand \| TimedWaypoints \| EePoseCommand \| None` | The latest command, of this controller's command class, or for the JointTrajectoryController the anvil.workcell.TimedWaypoints it is built from; None for none, as between commands. | `None`     |
| `command_t_ns` | `int \| None`                                                                       | When the command was sent, in nanoseconds, on the clock t_ns is on; required with a command.                                                                                         | `None`     |
| `command_seq`  | `int \| None`                                                                       | The command's sequence number, counting from 1; required with a command.                                                                                                             | `None`     |

Returns:

| Type                                                                                            | Description                                                                                              |
| ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `tuple[RobotCommand, JointTrace \| JointTrajectoryTrace \| EePoseTrace \| LeaderFollowerTrace]` | The tick's RobotCommand, and this controller's trace: a JointTrace, JointTrajectoryTrace or EePoseTrace. |

Raises:

| Type                   | Description                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `TypeError`            | command is another controller's command class, or is given without command_t_ns and command_seq. |
| `InvalidArgumentError` | command_seq is 0 with a command.                                                                 |

#### deactivate

```
deactivate() -> None
```

Stops tracking, before another controller takes over; whatever comes next does its own blending.

#### configure

```
configure(settings: ControllerSettings) -> None
```

Retunes the arms; an arm the settings leave `None` keeps what it runs with.

The gains slew to the new tuning over each arm's `JointGuards.gain_slew_s`. Nothing changes when `settings` is refused.

Parameters:

| Name       | Type                 | Description                                                           | Default    |
| ---------- | -------------------- | --------------------------------------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class, usually its settings(), edited. | *required* |

Raises:

| Type                   | Description                                      |
| ---------------------- | ------------------------------------------------ |
| `TypeError`            | settings is another controller's settings class. |
| `InvalidArgumentError` | settings is one validate_settings refuses.       |

#### settings

```
settings() -> ControllerSettings
```

Returns what the controller is configured with now, every arm it drives filled in.

The objects in it are the settings' own, so an edit to one, then `configure`, is a retune.

#### validate_settings

```
validate_settings(settings: ControllerSettings) -> None
```

Checks `settings` against what the controller runs with now, without applying it.

Parameters:

| Name       | Type                 | Description                           | Default    |
| ---------- | -------------------- | ------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | settings is another controller's settings class.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `InvalidArgumentError` | A tuning or IK parameters for an arm the controller does not drive; a tuning whose chain joints or gripper differ from that arm's; a gain or gravity scale that is negative or not finite, an offset that is not finite, or a gain past its guard's ceiling; a speed that is not positive; a grip assist number that is negative or not finite; for the EePoseController, IK parameters that are refused; for the LeaderFollowerController, a stiffness_share that is not from 0 to 0.4. |

### LeaderFollowerController

```
LeaderFollowerController(
    robot: RobotConfig,
    collision_stop: CollisionStop | None = None,
)
```

Bases: `Controller`

The controller that mirrors each leader arm, moved by hand, on the follower on its side, every tick.

Every follower tracks the joints measured on its leader that tick, positions and rates, clamped to its own limits and held still at one, its gripper opened to the share of its travel the leader's handle stands at. Each leader's chain is sent where its follower stands, pulled there as hard as the `ForceFeedback` asks and floating while it is off, and its handle where it stands, limp; every arm gets gravity compensation, and only a follower's gripper the grip assist. It takes no command: the measured state is the command. Gains slew from what was on the wire over each arm's `JointGuards.gain_slew_s` after `activate`, and over the same slew on a retune, the feedback's included. Disengaged, each follower holds where it stood and each leader floats; engaging, `activate` included, moves each follower to its leader at a bounded joint speed, the leaders floating, before mirroring resumes. `configure` and `settings` speak a `LeaderFollowerControllerSettings`.

Every method but `close` raises `anvil.OperationError` once it is closed, or once a `Runtime` has taken it.

Builds the controller for `robot`, each arm tuned as its description states for this controller.

Its arms are `robot`'s in slot order, as `control_arms` cuts them for this class, and a command's slots are theirs. Hand it to a `Runtime`, which takes it over; ticked by hand instead, call `activate` before the first `tick`.

Parameters:

| Name             | Type                    | Description                                                                                                                                             | Default    |
| ---------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `robot`          | `RobotConfig`           | The robot to drive.                                                                                                                                     | *required* |
| `collision_stop` | `CollisionStop \| None` | The stop that holds every target this controller sends short of a collision, built from the workcell whose robot is robot; None sends targets into one. | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | robot is refused or its chains cannot be cut from its URDF; collision_stop was built for another robot, its URDF, placement or slots not robot's; for a LeaderFollowerController, robot has no leader. |

Methods:

| Name                | Description                                                                                            |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| `tick`              | Runs one tick on measured state and returns what to send and why; the leaders' joints are the command. |
| `close`             | Frees the part now rather than once it is collected.                                                   |
| `activate`          | Warm-starts the controller from measured state; call it before the first tick.                         |
| `deactivate`        | Stops tracking, before another controller takes over; whatever comes next does its own blending.       |
| `configure`         | Retunes the arms; an arm the settings leave None keeps what it runs with.                              |
| `settings`          | Returns what the controller is configured with now, every arm it drives filled in.                     |
| `validate_settings` | Checks settings against what the controller runs with now, without applying it.                        |

#### tick

```
tick(
    t_ns: int,
    seq: int,
    state: JointState,
    command: None = None,
    command_t_ns: int | None = None,
    command_seq: int | None = None,
) -> tuple[RobotCommand, LeaderFollowerTrace]
```

Runs one tick on measured `state` and returns what to send and why; the leaders' joints are the command.

Parameters:

| Name           | Type          | Description                                                           | Default    |
| -------------- | ------------- | --------------------------------------------------------------------- | ---------- |
| `t_ns`         | `int`         | When state was measured, in nanoseconds, on the hw interface's clock. | *required* |
| `seq`          | `int`         | The state's sequence number.                                          | *required* |
| `state`        | `JointState`  | Measured state of every slot, leaders and followers alike.            | *required* |
| `command`      | `None`        | Must be None: the controller takes no command.                        | `None`     |
| `command_t_ns` | `int \| None` | Not read.                                                             | `None`     |
| `command_seq`  | `int \| None` | Not read.                                                             | `None`     |

Returns:

| Type                                       | Description                                          |
| ------------------------------------------ | ---------------------------------------------------- |
| `tuple[RobotCommand, LeaderFollowerTrace]` | The tick's RobotCommand and its LeaderFollowerTrace. |

Raises:

| Type        | Description          |
| ----------- | -------------------- |
| `TypeError` | command is not None. |

#### close

```
close() -> None
```

Frees the part now rather than once it is collected.

A hw interface lets go of its buses, disabling the drives it enabled. A part a runtime took is the runtime's to free, and closing it does nothing.

#### activate

```
activate(
    t_ns: int,
    seq: int,
    state: JointState,
    rate_hz: int | None = None,
    last_written: RobotCommand | None = None,
) -> None
```

Warm-starts the controller from measured state; call it before the first `tick`.

The gains ramp from those of `last_written` to what the tunings ask for, over each arm's `JointGuards.gain_slew_s`; out of idle that is from zero, so the arm comes up limp and takes its gains on. Each slot's target starts where `last_written` held it where that had a position gain, and where the slot measures otherwise, clamped to its limits; the first tick's command moves the chain on from there no faster than the arm's `SpeedLimits`, as every later one does.

Parameters:

| Name           | Type                   | Description                                                                                               | Default    |
| -------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- | ---------- |
| `t_ns`         | `int`                  | When state was measured, in nanoseconds, on the hw interface's clock.                                     | *required* |
| `seq`          | `int`                  | The state's sequence number.                                                                              | *required* |
| `state`        | `JointState`           | Measured state of every slot.                                                                             | *required* |
| `rate_hz`      | `int \| None`          | The rate the caller ticks at, which the gain ramp is counted in: 250 or 500; None for the loop's default. | `None`     |
| `last_written` | `RobotCommand \| None` | What was sent the tick before: another controller's output, at a switch; None for limp, as out of idle.   | `None`     |

Raises:

| Type                   | Description                             |
| ---------------------- | --------------------------------------- |
| `InvalidArgumentError` | rate_hz is not a rate the loop runs at. |

#### deactivate

```
deactivate() -> None
```

Stops tracking, before another controller takes over; whatever comes next does its own blending.

#### configure

```
configure(settings: ControllerSettings) -> None
```

Retunes the arms; an arm the settings leave `None` keeps what it runs with.

The gains slew to the new tuning over each arm's `JointGuards.gain_slew_s`. Nothing changes when `settings` is refused.

Parameters:

| Name       | Type                 | Description                                                           | Default    |
| ---------- | -------------------- | --------------------------------------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class, usually its settings(), edited. | *required* |

Raises:

| Type                   | Description                                      |
| ---------------------- | ------------------------------------------------ |
| `TypeError`            | settings is another controller's settings class. |
| `InvalidArgumentError` | settings is one validate_settings refuses.       |

#### settings

```
settings() -> ControllerSettings
```

Returns what the controller is configured with now, every arm it drives filled in.

The objects in it are the settings' own, so an edit to one, then `configure`, is a retune.

#### validate_settings

```
validate_settings(settings: ControllerSettings) -> None
```

Checks `settings` against what the controller runs with now, without applying it.

Parameters:

| Name       | Type                 | Description                           | Default    |
| ---------- | -------------------- | ------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | This controller's own settings class. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | settings is another controller's settings class.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `InvalidArgumentError` | A tuning or IK parameters for an arm the controller does not drive; a tuning whose chain joints or gripper differ from that arm's; a gain or gravity scale that is negative or not finite, an offset that is not finite, or a gain past its guard's ceiling; a speed that is not positive; a grip assist number that is negative or not finite; for the EePoseController, IK parameters that are refused; for the LeaderFollowerController, a stiffness_share that is not from 0 to 0.4. |

### ControllerHandle

```
ControllerHandle()
```

A controller a `Runtime` holds, reached through that runtime.

The runtime owns its controllers from the moment it is built, so a controller's settings are read and retuned through this handle, and it is activated by passing this handle to `Runtime.activate`. One comes from `Runtime.controller`.

Get one from `Runtime.controller`, never by calling the class.

Methods:

| Name             | Description                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| `configure`      | Retunes the controller, active or not, returning once a running loop has applied the new settings. |
| `settings`       | Returns what the controller is configured with now, with every arm it drives filled in.            |
| `settings_json`  | Returns settings as JSON, for a caller that passes it on rather than reading it.                   |
| `configure_json` | Retunes the controller from JSON in the form settings_json writes, as configure does.              |
| `tuned_robot`    | Returns robot with each arm's description stating the tuning this controller runs now.             |

Attributes:

| Name   | Type  | Description                                                                                 |
| ------ | ----- | ------------------------------------------------------------------------------------------- |
| `name` | `str` | Name of the controller, as Runtime.active and Snapshot.active report it, such as "ee_pose". |

#### name

```
name: str
```

Name of the controller, as `Runtime.active` and `Snapshot.active` report it, such as `"ee_pose"`.

#### configure

```
configure(settings: ControllerSettings) -> None
```

Retunes the controller, active or not, returning once a running loop has applied the new settings.

The loop applies them at the next tick boundary, and `settings` read after this returns shows them. The controller keeps them until the next retune, through any activation, and the runtime's other controllers are left as they were. The gains and gravity scale it sends slew to the new ones over the tuning's slew rather than stepping. On a runtime not yet started they are applied at its first tick, and `settings` shows them from then; one stopped refuses them, since no tick would apply them.

Parameters:

| Name       | Type                 | Description                                                                                                                                        | Default    |
| ---------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | The settings type the controller's class takes, such as a JointControllerSettings for a JointController; an arm left None keeps what it runs with. | *required* |

Raises:

| Type                   | Description                                                                                                                                    |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | settings is the settings type of another controller class.                                                                                     |
| `InvalidArgumentError` | A gain is not finite or is negative, a tuning is not the shape of its arm, or the IK parameters are ones no solve can run on; nothing changed. |
| `HwFaultError`         | The loop has faulted.                                                                                                                          |
| `OperationError`       | The runtime has stopped; or the loop is running and did not apply the settings within a second, its thread stalled or dead.                    |

#### settings

```
settings() -> ControllerSettings
```

Returns what the controller is configured with now, with every arm it drives filled in.

Returns:

| Type                 | Description                                                                                                                                      |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ControllerSettings` | The settings type the controller's class takes, such as a JointControllerSettings for a JointController. A configure shows here once it returns. |

#### settings_json

```
settings_json() -> str
```

Returns `settings` as JSON, for a caller that passes it on rather than reading it.

Returns:

| Type  | Description                                                                                                                                                                                                                               |
| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `str` | A JSON object holding tuning, a list with one tuning per arm slot and null past the arms the controller drives, and for an EePoseController also ik, one set of IK parameters per arm the same way. It is what configure_json takes back. |

#### configure_json

```
configure_json(settings: str) -> None
```

Retunes the controller from JSON in the form `settings_json` writes, as `configure` does.

Parameters:

| Name       | Type  | Description    | Default    |
| ---------- | ----- | -------------- | ---------- |
| `settings` | `str` | The JSON text. | *required* |

Raises:

| Type                   | Description                                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | The text is not that JSON or names a field no setting has, or the settings are refused as configure refuses them; nothing changed. |
| `HwFaultError`         | The loop has faulted.                                                                                                              |
| `OperationError`       | As for configure: the runtime has stopped, or a running loop did not apply the settings within a second.                           |

#### tuned_robot

```
tuned_robot(robot: RobotConfig) -> RobotConfig
```

Returns `robot` with each arm's description stating the tuning this controller runs now.

This is what saving a workcell retuned live keeps for its next build. Each chain joint's gains go into this controller's section of the arm's tuning, its gravity correction onto the joint, and the gripper's gains and grip assist onto the gripper, which every controller reads. An `EePoseController`'s IK parameters are not part of a description, so they are not kept.

Parameters:

| Name    | Type          | Description                          | Default    |
| ------- | ------------- | ------------------------------------ | ---------- |
| `robot` | `RobotConfig` | The robot the runtime was built for. | *required* |

Returns:

| Type          | Description                                         |
| ------------- | --------------------------------------------------- |
| `RobotConfig` | A new robot configuration; robot is left as it was. |

Raises:

| Type                   | Description                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | robot is not a anvil.workcell.RobotConfig.                                                                                                      |
| `InvalidArgumentError` | robot has another number of arms than the controller drives, or an arm whose chain is not the length of the tuning the controller runs it with. |

### HwFaultError

Bases: `OperationError`

Raised by an `anvil.control.Runtime` call that would drive the arms or read their state, once the loop faulted.

The loop faults when a call into its hw interface fails, or when a controller hits a bug of its own. It then takes itself to idle and writes nothing more, so the drives hold the last command they were sent, and it goes on running against the last state it read. The fault lasts the runtime's life: every such call raises this from then on, its message saying what failed first. Stop and close the runtime, fix what failed, and build a new one from a new hw interface.

### JointTrajectoryHandle

```
JointTrajectoryHandle()
```

Bases: `ControllerHandle`

The handle to a runtime's `JointTrajectoryController`, which can also play a trajectory through to its end.

One comes from `Runtime.controller(kind=JointTrajectoryController)`.

Get one from `Runtime.controller`, never by calling the class.

Methods:

| Name             | Description                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| `configure`      | Retunes the controller, active or not, returning once a running loop has applied the new settings. |
| `settings`       | Returns what the controller is configured with now, with every arm it drives filled in.            |
| `settings_json`  | Returns settings as JSON, for a caller that passes it on rather than reading it.                   |
| `configure_json` | Retunes the controller from JSON in the form settings_json writes, as configure does.              |
| `tuned_robot`    | Returns robot with each arm's description stating the tuning this controller runs now.             |
| `play`           | Runs trajectory on this controller to its last waypoint, blocking until it is done.                |

Attributes:

| Name   | Type  | Description                                                                                 |
| ------ | ----- | ------------------------------------------------------------------------------------------- |
| `name` | `str` | Name of the controller, as Runtime.active and Snapshot.active report it, such as "ee_pose". |

#### name

```
name: str
```

Name of the controller, as `Runtime.active` and `Snapshot.active` report it, such as `"ee_pose"`.

#### configure

```
configure(settings: ControllerSettings) -> None
```

Retunes the controller, active or not, returning once a running loop has applied the new settings.

The loop applies them at the next tick boundary, and `settings` read after this returns shows them. The controller keeps them until the next retune, through any activation, and the runtime's other controllers are left as they were. The gains and gravity scale it sends slew to the new ones over the tuning's slew rather than stepping. On a runtime not yet started they are applied at its first tick, and `settings` shows them from then; one stopped refuses them, since no tick would apply them.

Parameters:

| Name       | Type                 | Description                                                                                                                                        | Default    |
| ---------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `settings` | `ControllerSettings` | The settings type the controller's class takes, such as a JointControllerSettings for a JointController; an arm left None keeps what it runs with. | *required* |

Raises:

| Type                   | Description                                                                                                                                    |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | settings is the settings type of another controller class.                                                                                     |
| `InvalidArgumentError` | A gain is not finite or is negative, a tuning is not the shape of its arm, or the IK parameters are ones no solve can run on; nothing changed. |
| `HwFaultError`         | The loop has faulted.                                                                                                                          |
| `OperationError`       | The runtime has stopped; or the loop is running and did not apply the settings within a second, its thread stalled or dead.                    |

#### settings

```
settings() -> ControllerSettings
```

Returns what the controller is configured with now, with every arm it drives filled in.

Returns:

| Type                 | Description                                                                                                                                      |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ControllerSettings` | The settings type the controller's class takes, such as a JointControllerSettings for a JointController. A configure shows here once it returns. |

#### settings_json

```
settings_json() -> str
```

Returns `settings` as JSON, for a caller that passes it on rather than reading it.

Returns:

| Type  | Description                                                                                                                                                                                                                               |
| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `str` | A JSON object holding tuning, a list with one tuning per arm slot and null past the arms the controller drives, and for an EePoseController also ik, one set of IK parameters per arm the same way. It is what configure_json takes back. |

#### configure_json

```
configure_json(settings: str) -> None
```

Retunes the controller from JSON in the form `settings_json` writes, as `configure` does.

Parameters:

| Name       | Type  | Description    | Default    |
| ---------- | ----- | -------------- | ---------- |
| `settings` | `str` | The JSON text. | *required* |

Raises:

| Type                   | Description                                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | The text is not that JSON or names a field no setting has, or the settings are refused as configure refuses them; nothing changed. |
| `HwFaultError`         | The loop has faulted.                                                                                                              |
| `OperationError`       | As for configure: the runtime has stopped, or a running loop did not apply the settings within a second.                           |

#### tuned_robot

```
tuned_robot(robot: RobotConfig) -> RobotConfig
```

Returns `robot` with each arm's description stating the tuning this controller runs now.

This is what saving a workcell retuned live keeps for its next build. Each chain joint's gains go into this controller's section of the arm's tuning, its gravity correction onto the joint, and the gripper's gains and grip assist onto the gripper, which every controller reads. An `EePoseController`'s IK parameters are not part of a description, so they are not kept.

Parameters:

| Name    | Type          | Description                          | Default    |
| ------- | ------------- | ------------------------------------ | ---------- |
| `robot` | `RobotConfig` | The robot the runtime was built for. | *required* |

Returns:

| Type          | Description                                         |
| ------------- | --------------------------------------------------- |
| `RobotConfig` | A new robot configuration; robot is left as it was. |

Raises:

| Type                   | Description                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | robot is not a anvil.workcell.RobotConfig.                                                                                                      |
| `InvalidArgumentError` | robot has another number of arms than the controller drives, or an arm whose chain is not the length of the tuning the controller runs it with. |

#### play

```
play(
    trajectory: JointTrajectoryCommand | TimedWaypoints,
    timeout_s: float,
) -> None
```

Runs `trajectory` on this controller to its last waypoint, blocking until it is done.

The controller must be active. The gains first finish ramping from the previous controller's, then the trajectory is sent, and only a trace from a tick after the loop picked it up counts as done. This moves the arms. It waits on ticks and runs none: on a runtime built `stepped`, another thread must `Runtime.advance` it meanwhile, or this times out.

Parameters:

| Name         | Type                                       | Description                                                                                                                                                                 | Default    |
| ------------ | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `trajectory` | `JointTrajectoryCommand \| TimedWaypoints` | The waypoints to play, timed from when the loop picks it up: a trajectory, or a run a robot planned, such as anvil.workcell.RobotConfig.home_trajectory's, played as it is. | *required* |
| `timeout_s`  | `float`                                    | How long to wait for the whole run, the ramp included, in seconds, to the millisecond.                                                                                      | *required* |

Raises:

| Type                   | Description                                                                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | timeout_s is negative, not finite, or past about 49 days; a run JointTrajectoryCommand.from_waypoints refuses.                                |
| `HwFaultError`         | The loop faulted before or during the run.                                                                                                    |
| `TimedOutError`        | timeout_s passed before the trajectory finished.                                                                                              |
| `StoppedShortError`    | The collision stop slowed a step of the trajectory, which ended it.                                                                           |
| `PreemptedError`       | Before it finished, another trajectory was sent to this controller, which replaced it, or another controller, idle or a freeze was activated. |
| `OperationError`       | The controller is not active, trajectory is not for as many slots as the controller drives, or the loop is not running.                       |

### LinkCounts

```
LinkCounts()
```

Bases: `Counted`

Frames the hw interface sent each drive, the replies that came back and the sends the kernel refused.

Each is a list with one count per slot, counted since the interface opened, so the difference of two reads is what went through between them. An interface that does not count reports zeros. One comes from `Snapshot.link`.

Get one from `Snapshot.link`, never by calling the class.

Attributes:

| Name       | Type        | Description                                                                             |
| ---------- | ----------- | --------------------------------------------------------------------------------------- |
| `sent`     | `list[int]` | Frames sent, or tried, per slot: one the kernel refused counts, since it was asked for. |
| `answered` | `list[int]` | Replies received to the frames sent, per slot.                                          |
| `refused`  | `list[int]` | Frames sent that the kernel refused because its transmit queue was full, per slot.      |

#### sent

```
sent: list[int]
```

Frames sent, or tried, per slot: one the kernel refused counts, since it was asked for.

#### answered

```
answered: list[int]
```

Replies received to the frames sent, per slot.

#### refused

```
refused: list[int]
```

Frames sent that the kernel refused because its transmit queue was full, per slot.

These are commands that never left this host, which lost comms alone cannot tell from a loose cable. Always 0 from an interface with no queue to fill, such as the sim's.

### LoopTiming

```
LoopTiming()
```

Bases: `Struct`

Where one tick of the loop's time went, as marks and the spans between them.

The marks are `CLOCK_MONOTONIC` in nanoseconds, the clock `time.monotonic_ns()` reads and the loop's own, not the hw interface's stamp on the state (`Snapshot.t_ns`). The spans are in microseconds. One comes from `Snapshot.timing`, `Snapshot.slowest` or `Snapshot.most_late`.

Get one from `Snapshot.timing`, `Snapshot.slowest` or `Snapshot.most_late`, never by calling the class.

Attributes:

| Name                  | Type  | Description                                                                                         |
| --------------------- | ----- | --------------------------------------------------------------------------------------------------- |
| `due_ns`              | `int` | When the tick was due.                                                                              |
| `read_start_ns`       | `int` | When the tick started, and the hw interface's read with it.                                         |
| `read_end_ns`         | `int` | When the hw interface's read returned.                                                              |
| `controller_start_ns` | `int` | When the active controller's tick started, or idle's null command was built.                        |
| `controller_end_ns`   | `int` | When the controller's tick, or idle's null command, was done.                                       |
| `write_start_ns`      | `int` | When the hw interface's write started.                                                              |
| `write_end_ns`        | `int` | When the hw interface's write returned, where the tick's work ends.                                 |
| `late_us`             | `int` | How late the tick started after it was due.                                                         |
| `read_us`             | `int` | How long the hw interface's read took.                                                              |
| `controller_us`       | `int` | How long the controller's tick, or idle's null command, took.                                       |
| `write_us`            | `int` | How long the hw interface's write took.                                                             |
| `total_us`            | `int` | Time from the tick's start to the end of its write, the loop's own work between the steps included. |

#### due_ns

```
due_ns: int
```

When the tick was due.

#### read_start_ns

```
read_start_ns: int
```

When the tick started, and the hw interface's read with it.

#### read_end_ns

```
read_end_ns: int
```

When the hw interface's read returned.

#### controller_start_ns

```
controller_start_ns: int
```

When the active controller's tick started, or idle's null command was built.

#### controller_end_ns

```
controller_end_ns: int
```

When the controller's tick, or idle's null command, was done.

#### write_start_ns

```
write_start_ns: int
```

When the hw interface's write started.

#### write_end_ns

```
write_end_ns: int
```

When the hw interface's write returned, where the tick's work ends.

#### late_us

```
late_us: int
```

How late the tick started after it was due.

#### read_us

```
read_us: int
```

How long the hw interface's read took.

#### controller_us

```
controller_us: int
```

How long the controller's tick, or idle's null command, took.

#### write_us

```
write_us: int
```

How long the hw interface's write took.

#### total_us

```
total_us: int
```

Time from the tick's start to the end of its write, the loop's own work between the steps included.

### PreemptedError

Bases: `OperationError`

Raised by `anvil.control.JointTrajectoryHandle.play` when something took over before the trajectory played out.

Another trajectory was sent to the controller, which replaced it, or another controller, idle or a freeze was activated, as an emergency stop does. The arms do what took over says; the message names it. Send one trajectory at a time, or wait for `play` to return before sending the next.

### Runtime

```
Runtime(
    hw: HwInterface,
    controllers: Sequence[Controller],
    rate_hz: int | None = None,
    stepped: bool = False,
)
```

The control loop, running on a thread of its own at a fixed rate and holding every controller it was given.

Each tick it reads the hw interface, applies any requested switch between controllers, hands the active controller the latest command, ticks it, and writes its output; with no controller active it writes the null command, which leaves the arms limp. Build one from an enabled hw interface and the controllers, `start` it, `activate` a controller through its handle from `controller`, and send it commands with `send_command`. `with` stops it on the way out, as `stop` does, however the block is left: on an exception too, the drives are disabled and an arm that is not at rest falls under gravity. Bring the arms to rest inside the block, in a `try`/`finally` where an exception could skip it, before leaving it.

Builds the loop over `hw` and `controllers`, taking them over.

From here they belong to the runtime, and any call on them raises `anvil.OperationError`. Scan and enable the drives before this; the runtime only runs the loop. A refused build takes nothing, and every part stays usable.

Parameters:

| Name          | Type                   | Description                                                                                                                                                                                                                                                  | Default    |
| ------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `hw`          | `HwInterface`          | The hw interface the loop drives, an anvil.hw.SimHw or an anvil.hw.DamiaoHw.                                                                                                                                                                                 | *required* |
| `controllers` | `Sequence[Controller]` | One to four controllers, at most one of each class: a JointController, a JointTrajectoryController, an EePoseController or a LeaderFollowerController.                                                                                                       | *required* |
| `rate_hz`     | `int \| None`          | The rate the loop runs at for its life: 250 or 500, since at 1000 a seven-drive arm's frames overflow its 1 Mbit/s CAN bus. None is the rate of an anvil.hw.SimHw in a lockstep world, and 500 otherwise.                                                    | `None`     |
| `stepped`     | `bool`                 | Over an anvil.hw.SimHw in a lockstep world, ticks the loop only when advance asks and never against the wall clock, so the same commands give the same run twice. The first tick runs on start, and an activation or a retune runs the tick that applies it. | `False`    |

Raises:

| Type                   | Description                                                                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `TypeError`            | hw is not a hw interface, controllers is a single controller rather than a list, or one of them is not a controller.                                                                             |
| `InvalidArgumentError` | rate_hz is not 250 or 500, or differs from the rate of a lockstep world; two controllers are of one class; controllers is empty; or stepped is set over a hw interface that keeps its own clock. |
| `OperationError`       | hw or a controller was closed, or already belongs to another runtime.                                                                                                                            |

Methods:

| Name             | Description                                                                                              |
| ---------------- | -------------------------------------------------------------------------------------------------------- |
| `start`          | Starts the loop on its own thread, returning once it has a measured state or a second has passed.        |
| `advance`        | Runs ticks more ticks of a stepped loop, blocking until they have run.                                   |
| `stop`           | Stops the loop and waits for its thread to end, closing the hw interface with it.                        |
| `controller`     | Returns the handle to this runtime's controller of class kind, to activate it and to read and retune it. |
| `activate`       | Activates controller at the next tick boundary, blocking until the loop has done it.                     |
| `deactivate`     | Deactivates the active controller at the next tick boundary, leaving the arms limp, blocking until done. |
| `freeze`         | Holds the arms where they are measured at the next tick boundary, blocking until the loop has done it.   |
| `active`         | Returns the name of the active controller, such as "ee_pose"; None when the loop is idle or frozen.      |
| `send_command`   | Submits a command for the active controller, which the loop picks up at its next tick.                   |
| `latest`         | Returns a copy of what the loop last measured, wrote and did.                                            |
| `fault_snapshot` | Returns what the loop measured, wrote and did on the tick it faulted on; None while it has not faulted.  |
| `fault`          | Returns the message of the fault the loop idled on; None while it has not faulted.                       |
| `transitions`    | Returns the switches between controllers since the last call, oldest first.                              |

Attributes:

| Name               | Type          | Description                                                                        |
| ------------------ | ------------- | ---------------------------------------------------------------------------------- |
| `rate_hz`          | `int`         | Rate the loop is paced to, in hertz, fixed when the runtime was built.             |
| `realtime_refusal` | `str \| None` | Why the loop thread is not real-time, with what would grant it where anything can. |

#### rate_hz

```
rate_hz: int
```

Rate the loop is paced to, in hertz, fixed when the runtime was built.

#### realtime_refusal

```
realtime_refusal: str | None
```

Why the loop thread is not real-time, with what would grant it where anything can.

It is not real-time when it runs at the default scheduling policy rather than `SCHED_FIFO`, or off the cores the kernel isolated for it. Only Linux has `SCHED_FIFO`, so on any other OS nothing is asked and this says so. `None` where the OS granted both, and before `start`.

#### start

```
start() -> None
```

Starts the loop on its own thread, returning once it has a measured state or a second has passed.

On Linux the thread asks the OS for real-time scheduling before its first tick; `realtime_refusal` says what came of it. Once this returns with a measured state, `latest` holds it to read and a controller can be activated. A drive that has not answered within the second leaves `activate` refused until it does.

Raises:

| Type             | Description                                                              |
| ---------------- | ------------------------------------------------------------------------ |
| `OperationError` | The runtime was started before; a runtime runs once, so build a new one. |

#### advance

```
advance(ticks: int) -> None
```

Runs `ticks` more ticks of a `stepped` loop, blocking until they have run.

Each tick is a read, a tick of the active controller and a write that steps the world. The last of them is what `latest` shows on return. Ticks the loop ran on its own, to apply an activation or a retune, are not counted against these.

Parameters:

| Name    | Type  | Description                                      | Default    |
| ------- | ----- | ------------------------------------------------ | ---------- |
| `ticks` | `int` | How many ticks to run past the one latest shows. | *required* |

Raises:

| Type                   | Description                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | ticks is negative.                                                                                |
| `OperationError`       | The runtime was not built stepped, is not started, or was stopped, before or while the ticks ran. |

#### stop

```
stop() -> None
```

Stops the loop and waits for its thread to end, closing the hw interface with it.

Closing the hw interface disables the drives, so an arm that is not at rest falls under gravity, and stops a sim server its `anvil.sim.World` started once nothing else holds that world. `latest` and `active` go on reporting the last tick. Stopping again, or stopping a runtime never started, does nothing; one never started closes its hw interface when it is collected.

#### controller

```
controller(
    kind: type[JointTrajectoryController],
) -> JointTrajectoryHandle
```

```
controller(kind: type[Controller]) -> ControllerHandle
```

```
controller(kind: type[Controller]) -> ControllerHandle
```

Returns the handle to this runtime's controller of class `kind`, to activate it and to read and retune it.

Parameters:

| Name   | Type               | Description                                                     | Default    |
| ------ | ------------------ | --------------------------------------------------------------- | ---------- |
| `kind` | `type[Controller]` | The controller's class, such as anvil.control.EePoseController. | *required* |

Returns:

| Type               | Description                                                                                                                   |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `ControllerHandle` | A JointTrajectoryHandle, which can also play a trajectory, for a JointTrajectoryController; a ControllerHandle for any other. |

Raises:

| Type                   | Description                                    |
| ---------------------- | ---------------------------------------------- |
| `TypeError`            | kind is not a controller class.                |
| `InvalidArgumentError` | The runtime holds no controller of that class. |

Example

```
ee_pose = runtime.controller(kind=anvil.control.EePoseController)
runtime.activate(controller=ee_pose)
```

#### activate

```
activate(controller: ControllerHandle) -> None
```

Activates `controller` at the next tick boundary, blocking until the loop has done it.

`active` and `latest` show it on return. When another controller is active this is the switch: it is deactivated and this one warm-starts from that tick's measured state and ticks on the same tick, its gains ramping from the outgoing controller's, with no idle tick between. A command sent before this is dropped, so send the first one after it. Activating the active controller does nothing.

Parameters:

| Name         | Type               | Description                              | Default    |
| ------------ | ------------------ | ---------------------------------------- | ---------- |
| `controller` | `ControllerHandle` | A handle from this runtime's controller. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                            |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | controller is not a controller handle.                                                                                                                                                                                                                                                                                                                                                                                 |
| `InvalidArgumentError` | controller is another runtime's.                                                                                                                                                                                                                                                                                                                                                                                       |
| `HwFaultError`         | The loop has faulted.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `OperationError`       | The runtime is not started, or was stopped; the hw interface has not yet measured a state, since warm-starting from one nothing measured would command a jump; the controller drives another number of slots than the hw interface measures, built for another robot than the one it drives, which would leave the arms it does not know limp; or the loop did not answer within a second, its thread stalled or dead. |

#### deactivate

```
deactivate() -> None
```

Deactivates the active controller at the next tick boundary, leaving the arms limp, blocking until done.

Idle writes the null command, which leaves the arms limp, unless the loop has faulted, when it writes nothing. Deactivating an idle loop does nothing, a faulted one's included: it idled as it faulted, and its drives hold the last command written rather than going limp.

Raises:

| Type             | Description                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------- |
| `OperationError` | The runtime is not started, or was stopped, or the loop did not answer within a second, its thread stalled or dead. |

#### freeze

```
freeze() -> None
```

Holds the arms where they are measured at the next tick boundary, blocking until the loop has done it.

The active controller is deactivated, and every tick after writes the same command until the next activation: each slot at the position it measured then, at rest, under the gains last written, and each gripper without the grip assist's torque, which would go on closing it. No target the arms were moving to is held to, and commands are refused, as with no controller active. This is what an emergency stop wants. `activate` goes on from the frozen command; `deactivate` lets the arms go limp.

Raises:

| Type             | Description                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------- |
| `HwFaultError`   | The loop has faulted, and writes nothing more to hold the arms; the drives hold the last command written.           |
| `OperationError` | The runtime is not started, or was stopped, or the loop did not answer within a second, its thread stalled or dead. |

#### active

```
active() -> str | None
```

Returns the name of the active controller, such as `"ee_pose"`; `None` when the loop is idle or frozen.

#### send_command

```
send_command(command: Struct | TimedWaypoints) -> None
```

Submits a command for the active controller, which the loop picks up at its next tick.

The latest submission wins: one not yet picked up is replaced. The loop stamps the command with the measured state of the tick that picks it up, so the controller judges how stale it is from then.

Parameters:

| Name      | Type                       | Description                                                                                                                                                                                                                                      | Default    |
| --------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `command` | `Struct \| TimedWaypoints` | A JointCommand for the JointController, a JointTrajectoryCommand or the anvil.workcell.TimedWaypoints it is built from for the JointTrajectoryController, or an EePoseCommand for the EePoseController. The LeaderFollowerController takes none. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | command is not one of those, or is for a controller this runtime does not hold.                                                                                                                     |
| `InvalidArgumentError` | A TimedWaypoints that JointTrajectoryCommand.from_waypoints refuses.                                                                                                                                |
| `HwFaultError`         | The loop has faulted.                                                                                                                                                                               |
| `OperationError`       | Nothing would follow it: the runtime is not started or was stopped, no controller is active, another controller is, or it is for another number of slots or arms than the active controller drives. |

#### latest

```
latest() -> Snapshot
```

Returns a copy of what the loop last measured, wrote and did.

Before `start` it holds nothing measured: `ticks` is 0 and every per-slot list is empty. After `stop` it holds the last tick.

Raises:

| Type           | Description                                                                                                                |
| -------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `HwFaultError` | The loop has faulted, whether or not the runtime has stopped since: what it last saw is no longer what the arms are doing. |

#### fault_snapshot

```
fault_snapshot() -> Snapshot | None
```

Returns what the loop measured, wrote and did on the tick it faulted on; `None` while it has not faulted.

After a fault the loop writes nothing, and drives that answer only to a command fall silent, so what it reads from then on soon shows every drive lost; this tick shows which one failed. Asking raises nothing, and it stays after `stop`.

#### fault

```
fault() -> str | None
```

Returns the message of the fault the loop idled on; `None` while it has not faulted.

It is what the first call into the hw interface to fail said, or, where none failed, what a controller that hit a bug of its own said: the message every `anvil.control.HwFaultError` from this runtime carries. Asking raises nothing, so a caller can learn why the run ended without a call that is refused. It stays after `stop`.

#### transitions

```
transitions() -> list[tuple[str, list[tuple[str, str]]]]
```

Returns the switches between controllers since the last call, oldest first.

Each activation, deactivation or freeze that changed the active controller is one, and so is the idle the loop takes on a hw fault. A recorder reading this runtime keeps a cursor of its own, so both get every one.

Returns:

| Type                                      | Description                                                                                                                                                                                                                         |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list[tuple[str, list[tuple[str, str]]]]` | One (kind, fields) tuple per switch: kind is "runtime.transition", and fields is a list of (key, value) pairs, ("from", name) then ("to", name), with "idle" for no controller. Empty when there has been none since the last call. |

### Snapshot

```
Snapshot()
```

Bases: `Struct`

A copy of what the loop last measured, wrote and did, taken when `Runtime.latest` was called.

The copy does not change: call `Runtime.latest` again for a newer one. Every per-slot list holds one entry per slot the hw interface measured, and is empty before the loop's first sample.

Get one from `Runtime.latest`, never by calling the class.

Attributes:

| Name                                   | Type                  | Description                                                                                             |
| -------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
| `t_ns`                                 | `int`                 | The hw interface's stamp on the tick's measured state, in nanoseconds; 0 before the first sample.       |
| `num_joints`                           | `int`                 | Number of slots the hw interface measured, one entry in every per-slot list; 0 before the first sample. |
| `q`                                    | `list[float]`         | Measured position per slot, in radians.                                                                 |
| `q_commanded`                          | `list[float]`         | Position per slot the loop last wrote, in radians: where the arms are being held.                       |
| `qd`                                   | `list[float]`         | Measured velocity per slot, in radians per second.                                                      |
| `tau`                                  | `list[float]`         | Torque per slot as the drives report it, in newton meters.                                              |
| `status`                               | `list[int]`           | Drive status code per slot, verbatim from the drive; status_names decodes them.                         |
| `status_names`                         | `list[str]`           | Drive status per slot, by name: Enabled, Disabled, CommunicationLoss, or a fault such as Overcurrent.   |
| `status_faults`                        | `list[bool]`          | Whether each slot's drive reports a fault: anything but Enabled or Disabled, an unknown code included.  |
| `temp_mos_c`                           | `list[int]`           | Driver (MOSFET) temperature per slot, in degrees Celsius.                                               |
| `temp_rotor_c`                         | `list[int]`           | Motor winding temperature per slot, in degrees Celsius.                                                 |
| `temp_rotor_cutoff_c`                  | `list[float \| None]` | Winding temperature each slot's drive cuts out at, in degrees Celsius, per slot.                        |
| `robot_command`                        | `RobotCommand`        | What the loop last wrote to the drives: the active controller's output, or idle's null command.         |
| `trace`                                | `Struct \| None`      | The tick trace of whichever controller ticked last; None until the first tick with a controller active. |
| `active`                               | `str \| None`         | Name of the active controller, such as "ee_pose"; None when the loop is idle or frozen.                 |
| `rate_hz`                              | `int`                 | Rate the loop is paced to, in hertz, which achieved_hz is measured against.                             |
| `achieved_hz`                          | `float`               | Ticks per second, averaged since Runtime.start.                                                         |
| `ticks`                                | `int`                 | Ticks completed since Runtime.start.                                                                    |
| `timing`                               | `LoopTiming`          | Where the last tick's time went.                                                                        |
| `slowest`                              | `LoopTiming`          | Timing of the tick with the largest total_us since Runtime.start.                                       |
| `most_late`                            | `LoopTiming`          | Timing of the tick with the largest late_us since Runtime.start.                                        |
| `guards_fired_since_activation`        | `int`                 | Bitmask of the slots whose guard clamped on any tick since the last activation, bit n for slot n.       |
| `guards_fired_since_activation_slots`  | `list[int]`           | Slots in guards_fired_since_activation, lowest first.                                                   |
| `limit_clamped_since_activation`       | `int`                 | Bitmask of the slots held at a joint limit on any tick since the last activation, bit n for slot n.     |
| `limit_clamped_since_activation_slots` | `list[int]`           | Slots in limit_clamped_since_activation, lowest first.                                                  |
| `q_min_since_activation`               | `list[float]`         | Lowest position each slot has been measured at since the last activation, in radians.                   |
| `q_max_since_activation`               | `list[float]`         | Highest position each slot has been measured at since the last activation, in radians.                  |
| `link`                                 | `LinkCounts`          | Frames the hw interface has sent each drive, had answered and had refused since it opened.              |

#### t_ns

```
t_ns: int
```

The hw interface's stamp on the tick's measured state, in nanoseconds; 0 before the first sample.

The stamp is on the hw interface's own clock: `CLOCK_MONOTONIC` on real drives, simulated time in sim. When this host had the state, on the loop's clock, is `timing`.

#### num_joints

```
num_joints: int
```

Number of slots the hw interface measured, one entry in every per-slot list; 0 before the first sample.

#### q

```
q: list[float]
```

Measured position per slot, in radians.

To hold the arms where they are, command `q_commanded`, not this: under gravity a hold at the measured position sags a little more every tick.

#### q_commanded

```
q_commanded: list[float]
```

Position per slot the loop last wrote, in radians: where the arms are being held.

#### qd

```
qd: list[float]
```

Measured velocity per slot, in radians per second.

#### tau

```
tau: list[float]
```

Torque per slot as the drives report it, in newton meters.

#### status

```
status: list[int]
```

Drive status code per slot, verbatim from the drive; `status_names` decodes them.

#### status_names

```
status_names: list[str]
```

Drive status per slot, by name: `Enabled`, `Disabled`, `CommunicationLoss`, or a fault such as `Overcurrent`.

A code this build does not know comes back as hex. Empty before the loop's first tick.

#### status_faults

```
status_faults: list[bool]
```

Whether each slot's drive reports a fault: anything but `Enabled` or `Disabled`, an unknown code included.

#### temp_mos_c

```
temp_mos_c: list[int]
```

Driver (MOSFET) temperature per slot, in degrees Celsius.

#### temp_rotor_c

```
temp_rotor_c: list[int]
```

Motor winding temperature per slot, in degrees Celsius.

#### temp_rotor_cutoff_c

```
temp_rotor_cutoff_c: list[float | None]
```

Winding temperature each slot's drive cuts out at, in degrees Celsius, per slot.

The limit `temp_rotor_c` is heading for, read off each drive when the hw interface last enabled it; past it the drive faults and lets go. `None` for a slot whose drive did not answer, and for every slot of the sim, whose motors do not heat.

#### robot_command

```
robot_command: RobotCommand
```

What the loop last wrote to the drives: the active controller's output, or idle's null command.

Once the hw interface has faulted, it is the last command written before the fault, which the drives hold.

#### trace

```
trace: Struct | None
```

The tick trace of whichever controller ticked last; `None` until the first tick with a controller active.

It is the trace class of that controller: a `JointTrace`, a `JointTrajectoryTrace`, an `EePoseTrace` or a `LeaderFollowerTrace`. Idle ticks leave the last one in place.

#### active

```
active: str | None
```

Name of the active controller, such as `"ee_pose"`; `None` when the loop is idle or frozen.

#### rate_hz

```
rate_hz: int
```

Rate the loop is paced to, in hertz, which `achieved_hz` is measured against.

#### achieved_hz

```
achieved_hz: float
```

Ticks per second, averaged since `Runtime.start`.

#### ticks

```
ticks: int
```

Ticks completed since `Runtime.start`.

#### timing

```
timing: LoopTiming
```

Where the last tick's time went.

#### slowest

```
slowest: LoopTiming
```

Timing of the tick with the largest `total_us` since `Runtime.start`.

#### most_late

```
most_late: LoopTiming
```

Timing of the tick with the largest `late_us` since `Runtime.start`.

#### guards_fired_since_activation

```
guards_fired_since_activation: int
```

Bitmask of the slots whose guard clamped on any tick since the last activation, bit `n` for slot `n`.

It is the bitwise OR of every tick's `guards_fired`, so a guard that clamped on one tick out of thousands is still set when a reader polling at a human pace gets round to looking. `guards_fired_since_activation_slots` lists the same slots.

#### guards_fired_since_activation_slots

```
guards_fired_since_activation_slots: list[int]
```

Slots in `guards_fired_since_activation`, lowest first.

#### limit_clamped_since_activation

```
limit_clamped_since_activation: int
```

Bitmask of the slots held at a joint limit on any tick since the last activation, bit `n` for slot `n`.

It is the bitwise OR of every tick's `limit_clamped`, on the same terms as `guards_fired_since_activation`.

#### limit_clamped_since_activation_slots

```
limit_clamped_since_activation_slots: list[int]
```

Slots in `limit_clamped_since_activation`, lowest first.

#### q_min_since_activation

```
q_min_since_activation: list[float]
```

Lowest position each slot has been measured at since the last activation, in radians.

Before any activation, since the loop started.

#### q_max_since_activation

```
q_max_since_activation: list[float]
```

Highest position each slot has been measured at since the last activation, in radians.

Before any activation, since the loop started.

#### link

```
link: LinkCounts
```

Frames the hw interface has sent each drive, had answered and had refused since it opened.

### StoppedShortError

Bases: `OperationError`

Raised by `anvil.control.JointTrajectoryHandle.play` when the collision stop slowed the trajectory, ending it.

The arms hold where the stop sent them, and the trajectory controller stays active. `anvil.control.CollisionStop.last_stop` says which links were closing on what.

### control_arms

```
control_arms(
    robot: RobotConfig, kind: type[Controller]
) -> list[ControlArm]
```

Returns the arms of `robot` as the controller class `kind` drives them, in slot order.

Each carries its slots, its chain and the tuning its description states for that controller.

Parameters:

| Name    | Type               | Description                                     | Default    |
| ------- | ------------------ | ----------------------------------------------- | ---------- |
| `robot` | `RobotConfig`      | The robot whose arms to cut.                    | *required* |
| `kind`  | `type[Controller]` | The controller class, such as EePoseController. | *required* |

Returns:

| Type               | Description             |
| ------------------ | ----------------------- |
| `list[ControlArm]` | One ControlArm per arm. |

Raises:

| Type                   | Description                                                 |
| ---------------------- | ----------------------------------------------------------- |
| `TypeError`            | kind is not one of the controller classes.                  |
| `InvalidArgumentError` | robot is refused or its chains cannot be cut from its URDF. |

# anvil.hw

## anvil.hw

The hw interfaces a run drives the arms through, the bench tools for Damiao motors, and the readiness check.

`SimHw` and `DamiaoHw` are the two hw interfaces: a robot in an `anvil.sim.World`, and real Damiao drives on CAN. Both stand in the same place, drives to enable, a state to read and a command to write, with no loop around them: a script drives one tick by tick, or hands it to an `anvil.control.Runtime`, which runs the loop. `buses` says where a robot's drives are for a `DamiaoHw`, and `check_readiness`, or a `Survey` asked several questions, says whether they are there and fit to run before anything is commanded.

`DamiaoBus` is one CAN interface held at the bench: what answers on it, what each motor is, and the writes that configure and zero a motor. `ConfigureSequence` walks an arm's joints one motor at a time, and `ZeroLimitSweep` and `ZeroAuto` zero a whole arm, by hand or by driving it into its stops. A bench result with no class of its own comes back as a dict, its keys named on the call that returns it.

`RealHand` is a RealHand L6 on a CAN link, its own or an arm's, written its six angles directly and never part of a run; `RealHand.scan` finds which links the hands answer on.

Classes:

| Name                | Description                                                                                                |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| `HwInterface`       | What a anvil.control.Runtime drives the arms through: an anvil.hw.SimHw or an anvil.hw.DamiaoHw.           |
| `ResolvedMotor`     | One motor of a ResolvedBus: the slot it drives and the addresses it listens and replies on.                |
| `ResolvedBus`       | One arm's bus as DamiaoHw takes it: the CAN interface the arm answers on, and its motors.                  |
| `Finding`           | One line of a Report: what was looked at, and what was found.                                              |
| `Report`            | What a readiness check found, from check_readiness or Survey.check: a verdict and every finding behind it. |
| `SimHw`             | The sim hw interface: the robot of an anvil.sim.World, driven there, standing where a DamiaoHw stands.     |
| `DamiaoHw`          | Real Damiao drives over SocketCAN, one CAN interface per arm.                                              |
| `DamiaoMotor`       | A motor on a bus, by the address it listens on and the one it replies from.                                |
| `BusScan`           | What DamiaoBus.scan found on a bus.                                                                        |
| `DamiaoBus`         | One CAN interface held at the bench, and the bench operations on the motors on it.                         |
| `Survey`            | What this host's drives are, read once and held: every CAN link it has and every joint address on each.    |
| `ConfigureSequence` | Every joint of an arm configured in turn, one motor on the bus at a time.                                  |
| `ZeroLimitSweep`    | An arm held passive while someone sweeps each joint through its whole range by hand, then zeroed from it.  |
| `ZeroAuto`          | An arm zeroed by driving each joint into its hard stops by itself, from rest, in about four minutes.       |
| `FoundRealHand`     | A RealHand L6 answering on a CAN link, as RealHand.scan finds it.                                          |
| `RealHand`          | A RealHand L6 on a CAN link, written its six angles directly.                                              |

Functions:

| Name                    | Description                                                                                                   |
| ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `torque_full_scales_nm` | Returns each slot's torque full scale, in newton meters, in slot order: what a tau reading is a share of.     |
| `buses`                 | Returns the buses of robot as DamiaoHw takes them, one per arm in slot order, each on the link it answers on. |
| `check_readiness`       | Returns whether the machine on the bench is workcell, and fit to run, as a Report.                            |
| `status_names`          | Returns the first num_joints drive status codes by name, e.g. "Enabled" or "Overcurrent".                     |
| `status_faults`         | Returns whether each of the first num_joints drive status codes is a fault.                                   |

### HwInterface

Bases: `Part`

What a `anvil.control.Runtime` drives the arms through: an `anvil.hw.SimHw` or an `anvil.hw.DamiaoHw`.

It is the type a runtime takes its `hw` as. Build one of the two, never this.

Methods:

| Name    | Description                                          |
| ------- | ---------------------------------------------------- |
| `close` | Frees the part now rather than once it is collected. |

#### close

```
close() -> None
```

Frees the part now rather than once it is collected.

A hw interface lets go of its buses, disabling the drives it enabled. A part a runtime took is the runtime's to free, and closing it does nothing.

### ResolvedMotor

```
ResolvedMotor(
    slot: int, joint: str, slave_id: int, master_id: int
)
```

One motor of a `ResolvedBus`: the slot it drives and the addresses it listens and replies on.

`DamiaoHw` scales the motor's frames by the maxima the motor itself holds, read off it at `DamiaoHw.enable`.

Attributes:

| Name        | Type  | Description                                                                                    |
| ----------- | ----- | ---------------------------------------------------------------------------------------------- |
| `slot`      | `int` | Where the motor's joint sits in an anvil.control.JointState and an anvil.control.RobotCommand. |
| `joint`     | `str` | The joint's name in the arm's description, e.g. "joint_3" or "gripper".                        |
| `slave_id`  | `int` | The address the motor listens on, 0 to 255.                                                    |
| `master_id` | `int` | The address the motor replies from, 0 to 255.                                                  |

#### slot

```
slot: int
```

Where the motor's joint sits in an `anvil.control.JointState` and an `anvil.control.RobotCommand`.

#### joint

```
joint: str
```

The joint's name in the arm's description, e.g. `"joint_3"` or `"gripper"`.

#### slave_id

```
slave_id: int
```

The address the motor listens on, 0 to 255.

#### master_id

```
master_id: int
```

The address the motor replies from, 0 to 255.

### ResolvedBus

```
ResolvedBus(interface: str, motors: list[ResolvedMotor])
```

One arm's bus as `DamiaoHw` takes it: the CAN interface the arm answers on, and its motors.

`buses` returns one per arm; edit one before constructing a `DamiaoHw` to drive an arm somewhere else.

Attributes:

| Name        | Type                  | Description                                                                           |
| ----------- | --------------------- | ------------------------------------------------------------------------------------- |
| `interface` | `str`                 | The CAN interface, e.g. "can0", or a bus name a workcell declares, e.g. "follower_l". |
| `motors`    | `list[ResolvedMotor]` | The arm's motors, chain joints in description order, gripper last.                    |

#### interface

```
interface: str
```

The CAN interface, e.g. `"can0"`, or a bus name a workcell declares, e.g. `"follower_l"`.

`DamiaoHw` resolves a declared name to the link this host has for it when it opens the bus.

#### motors

```
motors: list[ResolvedMotor]
```

The arm's motors, chain joints in description order, gripper last.

### Finding

```
Finding(
    severity: str,
    holds_up: bool,
    subject: str,
    detail: str,
    arm: str | None,
    joint: str | None,
    interface: str | None,
    camera: str | None,
)
```

One line of a `Report`: what was looked at, and what was found.

Attributes:

| Name        | Type          | Description                                                                                      |
| ----------- | ------------- | ------------------------------------------------------------------------------------------------ |
| `severity`  | `str`         | How much it matters: "pass", "warn", "missing" (a part that did not answer) or "fail".           |
| `holds_up`  | `bool`        | Whether it holds the run up: a failure, or a part missing in a strict check.                     |
| `subject`   | `str`         | What it is about, e.g. "left" or "left joint_3".                                                 |
| `detail`    | `str`         | What was found, in the terms an operator would act on.                                           |
| `arm`       | `str \| None` | The arm it concerns, by its name in the workcell, or None where it concerns no arm.              |
| `joint`     | `str \| None` | The joint it concerns, by its name in the arm's description, or None where it concerns no joint. |
| `interface` | `str \| None` | The CAN interface it concerns, by its own name, e.g. "can0", or None where it concerns none.     |
| `camera`    | `str \| None` | The camera it concerns, by its name in the workcell, or None where it concerns no camera.        |

#### severity

```
severity: str
```

How much it matters: `"pass"`, `"warn"`, `"missing"` (a part that did not answer) or `"fail"`.

#### holds_up

```
holds_up: bool
```

Whether it holds the run up: a failure, or a part missing in a strict check.

#### subject

```
subject: str
```

What it is about, e.g. `"left"` or `"left joint_3"`.

#### detail

```
detail: str
```

What was found, in the terms an operator would act on.

#### arm

```
arm: str | None
```

The arm it concerns, by its name in the workcell, or `None` where it concerns no arm.

#### joint

```
joint: str | None
```

The joint it concerns, by its name in the arm's description, or `None` where it concerns no joint.

#### interface

```
interface: str | None
```

The CAN interface it concerns, by its own name, e.g. `"can0"`, or `None` where it concerns none.

#### camera

```
camera: str | None
```

The camera it concerns, by its name in the workcell, or `None` where it concerns no camera.

### Report

```
Report(
    ok: bool,
    summary: str,
    findings: list[Finding],
    hardware: list[dict[str, Any]],
)
```

What a readiness check found, from `check_readiness` or `Survey.check`: a verdict and every finding behind it.

Printing it prints `summary`; `render()` is the whole report. A warning is worth reading and does not hold a run up.

Methods:

| Name       | Description                                                                                |
| ---------- | ------------------------------------------------------------------------------------------ |
| `render`   | Returns the whole report as an operator reads it, one marked line per finding.             |
| `failures` | Returns the findings that failed, for a script that wants to say why it is stopping.       |
| `missing`  | Returns the findings of parts that did not answer, which a lenient start comes up without. |

Attributes:

| Name       | Type                   | Description                                                                                                    |
| ---------- | ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| `ok`       | `bool`                 | Whether the workcell can run: false when a finding failed, or when a part is missing and the check was strict. |
| `summary`  | `str`                  | The verdict in a line or two: what printing the report shows.                                                  |
| `findings` | `list[Finding]`        | Every finding, in the order the check made them.                                                               |
| `hardware` | `list[dict[str, Any]]` | The physical robot it was judged on, one dict per arm in the robot's order.                                    |

#### ok

```
ok: bool
```

Whether the workcell can run: false when a finding failed, or when a part is missing and the check was strict.

#### summary

```
summary: str
```

The verdict in a line or two: what printing the report shows.

#### findings

```
findings: list[Finding]
```

Every finding, in the order the check made them.

#### hardware

```
hardware: list[dict[str, Any]]
```

The physical robot it was judged on, one dict per arm in the robot's order.

Each holds `arm`, the arm's name in the workcell; `bus`, the bus the workcell declares for it; `interface`, the CAN link it was placed on, `None` for none, which its finding says why; and `motors`, every motor that answered there, each a dict as `Survey.links` describes one. What an `anvil.recorder.Recorder` given the report writes into each episode.

#### render

```
render() -> str
```

Returns the whole report as an operator reads it, one marked line per finding.

#### failures

```
failures() -> list[Finding]
```

Returns the findings that failed, for a script that wants to say why it is stopping.

#### missing

```
missing() -> list[Finding]
```

Returns the findings of parts that did not answer, which a lenient start comes up without.

### SimHw

```
SimHw(world: World)
```

Bases: `HwInterface`

The sim hw interface: the robot of an `anvil.sim.World`, driven there, standing where a `DamiaoHw` stands.

The world comes first and says the robot, the scene and the clock; this takes the robot over where it stands and drives its simulated drives, which come up disabled and saturate at their motor model's maxima. Drive it tick by tick with `read` and `write`, or hand it to an `anvil.control.Runtime`, which takes it over. Closing it, or letting it be collected, disables its drives and leaves the robot in the world where it falls; a server the world started runs on while this does. Every method but `close` raises `anvil.OperationError` once it is closed, or once a runtime has taken it.

Once the sim stops serving the robot (its server went away, let go of the robot, or did not answer within a minute) `enable`, `disable`, `read` and `write` raise `anvil.UnreachableError` saying why, and send nothing more: take the robot over again with a new `SimHw`.

Takes over the robot standing in `world` and waits for its first state, its drives disabled.

Its cameras go on watching, as a loop comes up on an arm already bolted down. A world serves one `SimHw` at a time, so while another drives it this waits until that one lets go.

Parameters:

| Name    | Type    | Description                     | Default    |
| ------- | ------- | ------------------------------- | ---------- |
| `world` | `World` | The world whose robot to drive. | *required* |

Raises:

| Type             | Description                                                                                                                                                                                                      |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OperationError` | world is closed, its server would not take a connection, is from another release of anvil, which the message names, or stands another robot than world.robot, which only a server shared with another world can. |

Methods:

| Name      | Description                                                                                         |
| --------- | --------------------------------------------------------------------------------------------------- |
| `close`   | Disables the drives and lets go of the robot, which stays in the world where it falls.              |
| `enable`  | Enables the drives, which come up disabled, as the hardware's do.                                   |
| `disable` | Disables the drives: the arm goes limp and falls under gravity.                                     |
| `read`    | Returns the latest state the sim sent, as (t_ns, q, qd), without waiting for one.                   |
| `write`   | Sends one tick's commands, blocking in a lockstep world until the tick they drive has been stepped. |

Attributes:

| Name              | Type                         | Description                                                                                                  |
| ----------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `lockstep_hz`     | `int \| None`                | The rate a lockstep world steps at, in hertz, or None for a world that keeps its own clock.                  |
| `motors`          | `list[str]`                  | The motors the sim drives, in slot order, each <arm>\_<joint> by the workcell's and the description's names. |
| `motor_addresses` | `list[tuple[str, int, int]]` | A (bus, slave_id, master_id) tuple per motor, in slot order, at the addresses the robot declares.            |

#### lockstep_hz

```
lockstep_hz: int | None
```

The rate a lockstep world steps at, in hertz, or `None` for a world that keeps its own clock.

#### motors

```
motors: list[str]
```

The motors the sim drives, in slot order, each `<arm>_<joint>` by the workcell's and the description's names.

For example `"follower_joint_1"`, or `"follower_gripper"`.

#### motor_addresses

```
motor_addresses: list[tuple[str, int, int]]
```

A `(bus, slave_id, master_id)` tuple per motor, in slot order, at the addresses the robot declares.

The sim is never told the addresses; these are what a scan of the metal would find answering. Sends nothing.

#### close

```
close() -> None
```

Disables the drives and lets go of the robot, which stays in the world where it falls.

A server the world started stops once this and the world are both closed or collected. Closing twice does nothing.

#### enable

```
enable() -> None
```

Enables the drives, which come up disabled, as the hardware's do.

Raises:

| Type               | Description                                                    |
| ------------------ | -------------------------------------------------------------- |
| `UnreachableError` | The sim did not enable them, or has stopped serving the robot. |

#### disable

```
disable() -> None
```

Disables the drives: the arm goes limp and falls under gravity.

Raises:

| Type               | Description                                                     |
| ------------------ | --------------------------------------------------------------- |
| `UnreachableError` | The sim did not disable them, or has stopped serving the robot. |

#### read

```
read() -> tuple[int, list[float], list[float]]
```

Returns the latest state the sim sent, as `(t_ns, q, qd)`, without waiting for one.

`t_ns` is the world's simulated time, in nanoseconds, not the host's clock. `q` is each motor's position in radians and `qd` its velocity in radians per second, one entry per motor in slot order. A lockstep world sends a state for each `write`; a free-running one sends them at its own pace, and this takes the newest.

Raises:

| Type               | Description                            |
| ------------------ | -------------------------------------- |
| `UnreachableError` | The sim has stopped serving the robot. |

#### write

```
write(robot_command: RobotCommand) -> None
```

Sends one tick's commands, blocking in a lockstep world until the tick they drive has been stepped.

A lockstep world steps one tick here, waiting up to a minute for it; a free-running one has already moved on and picks the commands up at its next tick. Only the first `len(motors)` joint commands are sent.

Parameters:

| Name            | Type           | Description           | Default    |
| --------------- | -------------- | --------------------- | ---------- |
| `robot_command` | `RobotCommand` | One command per slot. | *required* |

Raises:

| Type                   | Description                                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A value in one of the first num_joints joint commands is NaN or infinite; nothing is sent.                  |
| `UnreachableError`     | The server went away, let go of this robot, or did not step a lockstep tick within a minute, now or before. |

### DamiaoHw

```
DamiaoHw(buses: Sequence[ResolvedBus])
```

Bases: `HwInterface`

Real Damiao drives over `SocketCAN`, one CAN interface per arm.

Get its buses from `buses`, check the machine with `check_readiness`, then construct it, `enable` it and drive it tick by tick with `write` and `read`, or hand it to an `anvil.control.Runtime`, which takes it over. Closing it, or letting it be collected, disables the drives and closes the buses. Every method raises `anvil.OperationError` once it is closed, or once a runtime has taken it.

A drive `enable` enabled that stops reading `Enabled`, as one that lost power, timed out, went silent or faulted does, drops its joint, so from the `write` that finds it until the next `enable`, `read`, `state` and `write` raise `anvil.UnreachableError` naming each such drive and what it reads; a runtime driving it faults, and the other drives hold their last command.

Opens each arm's bus and takes the arm on it as declared, enabling nothing.

Nothing is asked of the drives: whether they are there and agree with the declaration is `check_readiness`'s question, asked before this. A listed motor that does not answer, or one on a bus that would not open, is driven all the same and reads `CommunicationLoss` once silent; a bus that would not open is tried again at `enable`. Each bus is held while this is, so no other program, and no `DamiaoBus`, can open it meanwhile.

Parameters:

| Name    | Type                    | Description                                        | Default    |
| ------- | ----------------------- | -------------------------------------------------- | ---------- |
| `buses` | `Sequence[ResolvedBus]` | One per arm, as buses returns them, edited or not. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A slot that is negative, or an address outside 0 to 255; buses is empty, two arms land on one interface, or a slot is used twice or is not below anvil.control.MAX_JOINTS. |

Methods:

| Name      | Description                                                                                                   |
| --------- | ------------------------------------------------------------------------------------------------------------- |
| `enable`  | Enables the drives and checks they took it, as is needed before commanding them or handing this to a runtime. |
| `disable` | Disables the drives: the arm is unpowered afterwards and falls under gravity.                                 |
| `read`    | Returns the state the last write collected, as (t_ns, q, qd), without touching the bus.                       |
| `state`   | Returns the state read returns, whole: torques, each slot's drive status and its temperatures too.            |
| `write`   | Sends one tick's commands and takes the replies already waiting, without blocking.                            |
| `close`   | Frees the part now rather than once it is collected.                                                          |

Attributes:

| Name              | Type                         | Description                                                                                            |
| ----------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| `motor_addresses` | `list[tuple[str, int, int]]` | A (bus, slave_id, master_id) tuple per motor this drives, in the order the buses were given.           |
| `aliased`         | `list[str]`                  | Each bus this host had under another name, as "follower_l -> can0"; empty where every name was a link. |

#### motor_addresses

```
motor_addresses: list[tuple[str, int, int]]
```

A `(bus, slave_id, master_id)` tuple per motor this drives, in the order the buses were given.

`bus` is the interface each was opened on, or tried, e.g. `"can0"`. Sends nothing, so it says nothing of whether they answer: `state()`'s status does.

#### aliased

```
aliased: list[str]
```

Each bus this host had under another name, as `"follower_l -> can0"`; empty where every name was a link.

#### enable

```
enable(
    require_enabled: bool = True,
    stop_on_dropout: bool = False,
) -> None
```

Enables the drives and checks they took it, as is needed before commanding them or handing this to a runtime.

A drive not on Anvil firmware is never enabled. Each other drive is first put in MIT mode until it is powered off, and its scaling maxima are read off it; one that answers the enable with a latched fault is cleared and enabled again. A drive that did not enable ignores the commands the loop is about to send, so the arm would be driven from a joint that cannot answer; one that has never answered is sent none until an enable hears it, so it is never driven toward a position nothing measured.

Parameters:

| Name              | Type   | Description                                                                                                                                                                                                                                                                                                                               | Default |
| ----------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `require_enabled` | `bool` | Whether a drive left not enabled is an error. False brings the robot up without it: the drives that could enable are, the rest say what they are in state()'s status, and a warning is logged naming them.                                                                                                                                | `True`  |
| `stop_on_dropout` | `bool` | Whether a drive that stops reading Enabled while the loop runs, as one unplugged, unpowered or faulted does, stops the loop driving every arm: the runtime then raises anvil.control.HwFaultError naming it. False drives the rest on, and the dropped drive's slot says what it reads in each snapshot's status_names and status_faults. | `False` |

Raises:

| Type             | Description                                                                                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OperationError` | With require_enabled, a drive is not enabled afterwards; the message names each one and what it reported, or the bus it is on and why that would not open. |

#### disable

```
disable() -> None
```

Disables the drives: the arm is unpowered afterwards and falls under gravity.

A drive that was enabled and does not confirm the disable is logged as a warning, since it may still hold its last command with torque on.

#### read

```
read() -> tuple[int, list[float], list[float]]
```

Returns the state the last `write` collected, as `(t_ns, q, qd)`, without touching the bus.

`t_ns` is when the last exchange with the drives began, a write, an enable or a disable, in nanoseconds on the host's monotonic clock, `time.monotonic_ns()`; 0 before the first. `q` is each slot's position in radians and `qd` its velocity in radians per second. A write takes the replies already waiting rather than waiting for its own, so the state is the previous tick's.

#### state

```
state() -> JointState
```

Returns the state `read` returns, whole: torques, each slot's drive status and its temperatures too.

#### write

```
write(robot_command: RobotCommand) -> None
```

Sends one tick's commands and takes the replies already waiting, without blocking.

A motor that has not answered for five writes in a row reads `CommunicationLoss` in `state()`'s status.

Parameters:

| Name            | Type           | Description           | Default    |
| --------------- | -------------- | --------------------- | ---------- |
| `robot_command` | `RobotCommand` | One command per slot. | *required* |

Raises:

| Type                   | Description                                                                                                    |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A value in one of the first num_joints joint commands is NaN or infinite; nothing is sent.                     |
| `UnreachableError`     | A drive that was enabled no longer reads Enabled, found by this write or an earlier one since the last enable. |

#### close

```
close() -> None
```

Frees the part now rather than once it is collected.

A hw interface lets go of its buses, disabling the drives it enabled. A part a runtime took is the runtime's to free, and closing it does nothing.

### DamiaoMotor

```
DamiaoMotor(slave_id: int, master_id: int)
```

A motor on a bus, by the address it listens on and the one it replies from.

What `DamiaoBus.scan` finds, and what every per-motor call of a `DamiaoBus` takes.

Raises:

| Type                   | Description                  |
| ---------------------- | ---------------------------- |
| `InvalidArgumentError` | An address outside 0 to 255. |

Attributes:

| Name        | Type  | Description                                   |
| ----------- | ----- | --------------------------------------------- |
| `slave_id`  | `int` | The address the motor listens on, 0 to 255.   |
| `master_id` | `int` | The address the motor replies from, 0 to 255. |

#### slave_id

```
slave_id: int
```

The address the motor listens on, 0 to 255.

#### master_id

```
master_id: int
```

The address the motor replies from, 0 to 255.

### BusScan

```
BusScan(found: list[DamiaoMotor], stopped_by: str | None)
```

What `DamiaoBus.scan` found on a bus.

Attributes:

| Name         | Type                | Description                                            |
| ------------ | ------------------- | ------------------------------------------------------ |
| `found`      | `list[DamiaoMotor]` | Every motor that answered, by ascending slave address. |
| `stopped_by` | `str \| None`       | Why the scan ended early, or None where it did not.    |

#### found

```
found: list[DamiaoMotor]
```

Every motor that answered, by ascending slave address.

#### stopped_by

```
stopped_by: str | None
```

Why the scan ended early, or `None` where it did not.

Set where the bus stopped accepting frames, which is what an interface that is up but unplugged looks like.

### DamiaoBus

```
DamiaoBus(interface: str)
```

Bases: `Handle`

One CAN interface held at the bench, and the bench operations on the motors on it.

Every per-motor call takes the motor's addresses, a `DamiaoMotor`, from `scan()`, from `bind()` or written out. The interface is held while this is open: no other program, and no other `DamiaoBus` or `DamiaoHw`, can open it meanwhile, so nothing else talks on the wire. It is let go when this is closed or collected, or when a zeroing run takes it over, whether or not the run then starts. Every method but `close` raises `anvil.OperationError` once it is closed, and every one but `close` and `interface` once a zeroing run has taken it.

Opens `interface` and holds it.

Parameters:

| Name        | Type  | Description                                                                                                                    | Default    |
| ----------- | ----- | ------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `interface` | `str` | A CAN interface, e.g. "can0", or a bus name a workcell declares, e.g. "follower_l", which opens the link this host has for it. | *required* |

Raises:

| Type             | Description                                                                                                     |
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
| `NotFoundError`  | This host has no such interface.                                                                                |
| `BusyError`      | Another program, or another bus or hw interface in this one, holds the interface; the message names the holder. |
| `OperationError` | The interface would not open for another reason, which the message names.                                       |

Methods:

| Name               | Description                                                                                                 |
| ------------------ | ----------------------------------------------------------------------------------------------------------- |
| `available`        | Returns every CAN interface this host has, by name, sorted, reading /sys and opening nothing.               |
| `scan`             | Returns every motor that answers on the bus, asking every address and writing nothing.                      |
| `bind`             | Returns which of the arm's joints answer on the bus where the description puts them, writing nothing.       |
| `versions`         | Returns the motor's version registers, writing nothing.                                                     |
| `identity`         | Returns the motor's identity record, and whether its calibration still matches it, writing nothing.         |
| `registers`        | Returns every register the protocol names, read off the motor one at a time, writing nothing.               |
| `state`            | Returns what the motor reports about itself, decoded by the scaling maxima read off it, writing nothing.    |
| `set_mit_mode`     | Puts the motor into MIT mode, the only mode the control stack commands in, moving nothing.                  |
| `zero`             | Zeroes the motor where it stands and saves it to flash, disabling it first but moving nothing itself.       |
| `configure`        | Configures whatever motor answers on the bus: its addresses and, for a joint, MIT mode and identity record. |
| `stamp_version`    | Stamps the arm's version into the identity record of each of its motors on the bus, reading each back.      |
| `enable_limp`      | Enables motors limp, lights green, commanded no stiffness, no damping and no torque, so nothing moves.      |
| `disable`          | Disables motors, each as far as it answers: one that does not is off already, or gone.                      |
| `zero_limit_sweep` | Returns a ZeroLimitSweep of the arm on this bus, which takes the bus over; see that class.                  |
| `zero_auto`        | Returns a ZeroAuto run of the arm on this bus, which takes the bus over; see that class.                    |
| `close`            | Frees the handle now rather than once this object is collected. Closing twice does nothing.                 |

Attributes:

| Name        | Type  | Description                                                                                      |
| ----------- | ----- | ------------------------------------------------------------------------------------------------ |
| `interface` | `str` | The interface that was opened: what the name it was given resolved to on this host, e.g. "can0". |

#### interface

```
interface: str
```

The interface that was opened: what the name it was given resolved to on this host, e.g. `"can0"`.

#### available

```
available() -> list[str]
```

Returns every CAN interface this host has, by name, sorted, reading `/sys` and opening nothing.

#### scan

```
scan() -> BusScan
```

Returns every motor that answers on the bus, asking every address and writing nothing.

#### bind

```
bind(description: ArmDescription) -> dict[str, Any]
```

Returns which of the arm's joints answer on the bus where the description puts them, writing nothing.

Only the description's addresses are asked, so a motor that merely shares the bus is never touched; the maxima of each one that answered as described are read off it. A gripper whose description names an `alternate_motor` is that motor where it answers and the gripper's own address does not.

Parameters:

| Name          | Type             | Description                       | Default    |
| ------------- | ---------------- | --------------------------------- | ---------- |
| `description` | `ArmDescription` | The arm whose joints to look for. | *required* |

Returns:

| Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dict[str, Any]` | A dict of joints, one dict per joint in description order, and stopped_by, as BusScan.stopped_by. A joint holds motor, its name, e.g. "J3"; slave and master, the addresses the description gives it; answered, the address something at slave replied from, or None; and limits, the scaling maxima it holds as {"ok": {"q_max", "dq_max", "tau_max"}}, in radians, radians per second and newton meters, or why they could not be read as {"error": ...}. |

#### versions

```
versions(motor: DamiaoMotor) -> dict[str, str]
```

Returns the motor's version registers, writing nothing.

Parameters:

| Name    | Type          | Description       | Default    |
| ------- | ------------- | ----------------- | ---------- |
| `motor` | `DamiaoMotor` | The motor to ask. | *required* |

Returns:

| Type             | Description                                                                                                                               |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `dict[str, str]` | A dict of software, which names the firmware image flashed, e.g. "5017"; sub, the firmware revision; and hardware, the hardware revision. |

Raises:

| Type               | Description                                 |
| ------------------ | ------------------------------------------- |
| `UnreachableError` | The motor did not answer.                   |
| `OperationError`   | A register held something that is not text. |

#### identity

```
identity(motor: DamiaoMotor) -> dict[str, Any]
```

Returns the motor's identity record, and whether its calibration still matches it, writing nothing.

Parameters:

| Name    | Type          | Description       | Default    |
| ------- | ------------- | ----------------- | ---------- |
| `motor` | `DamiaoMotor` | The motor to ask. | *required* |

Returns:

| Type             | Description                                                                                                                                                                                                                                                                                                                                            |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `dict[str, Any]` | A dict whose outcome is "stock_firmware", which has no record; "invalid" or "unread", with the error; or "named", with the record, its uuid, the versions and control mode as {"ok": ...} or {"error": ...}, and calibration, whose outcome is "matches", "drifted" or "unread": whether the motor's registers still hash to the record's fingerprint. |

#### registers

```
registers(motor: DamiaoMotor) -> list[dict[str, Any]]
```

Returns every register the protocol names, read off the motor one at a time, writing nothing.

Parameters:

| Name    | Type          | Description        | Default    |
| ------- | ------------- | ------------------ | ---------- |
| `motor` | `DamiaoMotor` | The motor to read. | *required* |

Returns:

| Type                   | Description                                                                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list[dict[str, Any]]` | One dict per register: name, e.g. "position_limit"; wire_name, the Damiao documentation's, e.g. "PMAX"; writable; and value, as {"ok": ...} or why it did not read as {"error": ...}. |

#### state

```
state(motor: DamiaoMotor) -> dict[str, Any]
```

Returns what the motor reports about itself, decoded by the scaling maxima read off it, writing nothing.

Parameters:

| Name    | Type          | Description       | Default    |
| ------- | ------------- | ----------------- | ---------- |
| `motor` | `DamiaoMotor` | The motor to ask. | *required* |

Returns:

| Type             | Description                                                                                                                                                                                                                                             |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dict[str, Any]` | A dict of status, e.g. "disabled"; slave, the address it identified itself as; position in radians; velocity in radians per second; torque in newton meters; and temp_mos and temp_rotor, its driver stage and winding temperatures in degrees Celsius. |

Raises:

| Type               | Description                                                                  |
| ------------------ | ---------------------------------------------------------------------------- |
| `UnreachableError` | The motor did not answer its scaling registers or the state request.         |
| `OperationError`   | Its scaling registers hold a maximum that is zero, negative or not a number. |

#### set_mit_mode

```
set_mit_mode(
    motor: DamiaoMotor, save: bool
) -> dict[str, Any]
```

Puts the motor into MIT mode, the only mode the control stack commands in, moving nothing.

Parameters:

| Name    | Type          | Description                                                        | Default    |
| ------- | ------------- | ------------------------------------------------------------------ | ---------- |
| `motor` | `DamiaoMotor` | The motor to set.                                                  | *required* |
| `save`  | `bool`        | Whether to commit the mode to flash, so it survives a power cycle. | *required* |

Returns:

| Type             | Description                                                                                                                                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `dict[str, Any]` | A dict of mode, {"ok": "mit"} or {"error": ...}; and, where a save was asked for and the mode took, saved, whose outcome is "saved", "not_acknowledged" or "failed", with the error. |

#### zero

```
zero(motor: DamiaoMotor, kind: str) -> dict[str, Any]
```

Zeroes the motor where it stands and saves it to flash, disabling it first but moving nothing itself.

A zero is only accepted while the motor is disabled, so a motor holding a joint up lets go of it here.

Parameters:

| Name    | Type          | Description                                                                                                                                                                                                                                   | Default    |
| ------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `motor` | `DamiaoMotor` | The motor to zero.                                                                                                                                                                                                                            | *required* |
| `kind`  | `str`         | "auto", the factory zero where its firmware has one and the working zero where it does not, with the identity record's fingerprint re-stamped; "working", the working zero only; or "restore", the working zero back to the factory baseline. | *required* |

Returns:

| Type             | Description                                                                                                                                                                                                                                                                 |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dict[str, Any]` | A dict whose outcome is "disable_failed" or "failed", with the error, or "zeroed", with zeroed: the angle_offset the zero landed on, in radians, as {"ok": ...} or {"error": ...}, and the restamp. stock_firmware says, for "auto", whether the motor runs stock firmware. |

Raises:

| Type                   | Description                                                                  |
| ---------------------- | ---------------------------------------------------------------------------- |
| `InvalidArgumentError` | kind is not one of the three.                                                |
| `UnreachableError`     | The motor did not answer its scaling registers.                              |
| `OperationError`       | Its scaling registers hold a maximum that is zero, negative or not a number. |

#### configure

```
configure(
    gripper_only: bool,
    allow_multiple: bool,
    save: bool,
    master_id: int | None = None,
    slave_id: int | None = None,
    description: ArmDescription | None = None,
    motor: str | None = None,
    side: str | None = None,
    role: str | None = None,
) -> dict[str, Any]
```

Configures whatever motor answers on the bus: its addresses and, for a joint, MIT mode and identity record.

Named as a joint, `motor` of `description`, the motor takes that joint's addresses, MIT mode and a fresh identity record naming `side` and `role`; `master_id` and `slave_id` override the joint's addresses, or set just those where no joint is named. The bus is scanned first. Writes to the motors; moves nothing.

Parameters:

| Name             | Type                     | Description                                                                                                           | Default    |
| ---------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------- |
| `gripper_only`   | `bool`                   | Whether the record is a standalone gripper's rather than the arm's.                                                   | *required* |
| `allow_multiple` | `bool`                   | Whether to configure every motor that answers rather than refuse more than one.                                       | *required* |
| `save`           | `bool`                   | Whether to commit the writes to flash.                                                                                | *required* |
| `master_id`      | `int \| None`            | The reply address to set, 0 to 255, or None for the joint's.                                                          | `None`     |
| `slave_id`       | `int \| None`            | The listening address to set, 0 to 255, or None for the joint's.                                                      | `None`     |
| `description`    | `ArmDescription \| None` | The arm the motor belongs to, or None to set addresses only.                                                          | `None`     |
| `motor`          | `str \| None`            | The joint to configure the motor as, e.g. "J3"; required with description, ignored without.                           | `None`     |
| `side`           | `str \| None`            | "left" or "right", which the record names; None is the left for a symmetric build, and is refused for a mirrored one. | `None`     |
| `role`           | `str \| None`            | "leader" or "follower", which J8's record carries; None is "leader" for any other joint, and is refused for J8.       | `None`     |

Returns:

| Type             | Description                                                                                                                                                                       |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dict[str, Any]` | A dict of motors, one dict per motor in scan order: what was found, the writes made in order up to the first that failed, and the record where one was owed and every write took. |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | master_id or slave_id is outside 0 to 255; description names no such motor, motor is missing, side or role is not one of its two or is missing where it is required, the description's family has no identity record, or gripper_only names an arm whose gripper has no motor of its own. |
| `UnreachableError`     | No motor answered on the bus.                                                                                                                                                                                                                                                             |
| `OperationError`       | More than one answered without allow_multiple, or the bus stopped accepting frames part-way through the scan.                                                                                                                                                                             |

#### stamp_version

```
stamp_version(
    description: ArmDescription,
) -> list[dict[str, Any]]
```

Stamps the arm's version into the identity record of each of its motors on the bus, reading each back.

What changes when an arm is rebuilt as another version of its family, as turning an OpenYAM's base plate switches it between `openyam/v1` and `openyam/v2`, so detecting the arm reads the new build. Only the version changes, and only in records that name the arm's family; a gripper stamped as a gripper of its own keeps its record. Every record is read before any is written, so an arm that is not whole is refused with nothing changed, and J1 is written last, so a run interrupted part-way is finished by running it again. The UUID a record names a motor by carries its version, so each motor stamped is named anew. Writes to the motors; moves nothing.

Parameters:

| Name          | Type             | Description                                         | Default    |
| ------------- | ---------------- | --------------------------------------------------- | ---------- |
| `description` | `ArmDescription` | The arm as it is now built, whose version to stamp. | *required* |

Returns:

| Type                   | Description                                                                                                                                                                                                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list[dict[str, Any]]` | One dict per motor, J1 last, of motor, e.g. "J3", and outcome: "no_record", stock firmware or no record to stamp; "own_gripper"; "already", carrying this version; "failed", with the error; or "stamped", with the version it carried from. |

Raises:

| Type                   | Description                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | The arm's family has no identity record, or a chain motor's record does not read or names another family. |
| `UnreachableError`     | A chain motor did not answer where the description puts it.                                               |

#### enable_limp

```
enable_limp(motors: Sequence[DamiaoMotor]) -> None
```

Enables `motors` limp, lights green, commanded no stiffness, no damping and no torque, so nothing moves.

The arm hangs as it did switched off; what shows someone which arm is on the bus. Writes no flash.

Parameters:

| Name     | Type                    | Description           | Default    |
| -------- | ----------------------- | --------------------- | ---------- |
| `motors` | `Sequence[DamiaoMotor]` | The motors to enable. | *required* |

Raises:

| Type             | Description                                                                                                            |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `OperationError` | A motor's scaling maxima would not read or it would not enable; every one is disabled again, and the message names it. |

#### disable

```
disable(motors: Sequence[DamiaoMotor]) -> None
```

Disables `motors`, each as far as it answers: one that does not is off already, or gone.

Parameters:

| Name     | Type                    | Description            | Default    |
| -------- | ----------------------- | ---------------------- | ---------- |
| `motors` | `Sequence[DamiaoMotor]` | The motors to disable. | *required* |

#### zero_limit_sweep

```
zero_limit_sweep(
    description: ArmDescription,
    allow_missing: bool,
    side: str | None = None,
    motors: Sequence[str] | None = None,
) -> ZeroLimitSweep
```

Returns a `ZeroLimitSweep` of the arm on this bus, which takes the bus over; see that class.

#### zero_auto

```
zero_auto(
    description: ArmDescription,
    side: str | None = None,
    motors: Sequence[str] | None = None,
) -> ZeroAuto
```

Returns a `ZeroAuto` run of the arm on this bus, which takes the bus over; see that class.

#### close

```
close() -> None
```

Frees the handle now rather than once this object is collected. Closing twice does nothing.

### Survey

```
Survey(configure_buses: bool = True)
```

Bases: `Handle`

What this host's drives are, read once and held: every CAN link it has and every joint address on each.

One survey answers three questions without reading the buses again: what is on each link (`links`), which shipped workcell the links are (`detect`), and whether they are fit to run as a given workcell (`check`). Reads only: nothing is written, no drive is enabled and nothing moves. Every method but `close` raises `anvil.OperationError` once it is closed.

Surveys the host, about a second for a bimanual robot with its leaders.

Parameters:

| Name              | Type   | Description                                                        | Default |
| ----------------- | ------ | ------------------------------------------------------------------ | ------- |
| `configure_buses` | `bool` | Whether to bring every link up first, as check_readiness takes it. | `True`  |

Methods:

| Name     | Description                                                                                      |
| -------- | ------------------------------------------------------------------------------------------------ |
| `links`  | Returns every link the survey found, as one dict each.                                           |
| `detect` | Returns which shipped workcells the surveyed links could be.                                     |
| `check`  | Returns whether the surveyed machine is workcell, and fit to run, as check_readiness reports it. |
| `close`  | Frees the handle now rather than once this object is collected. Closing twice does nothing.      |

#### links

```
links() -> list[dict[str, Any]]
```

Returns every link the survey found, as one dict each.

Returns:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list[dict[str, Any]]` | One dict per link: interface; named_for, the bus a workcell declares that the link's name stands for, or None; error, why it could not be read, or None; can_state; bit_rate_bps; rx_errors and tx_errors; and motors. A motor is slave, master, model, firmware, record, the identity record as the bench tools print it, its uuid, mode, timeout_ms, status, position_deg, temp_mos_c and temp_rotor_c, each read that failed None. |

#### detect

```
detect() -> dict[str, Any]
```

Returns which shipped workcells the surveyed links could be.

Only those at the most trusted level any fits are offered: the records' roles and sides, then their families, then the motors alone, `records_ignored` saying which. Trusting the records, an arm is a leader only on a link whose gripper's record names one, since a follower taken for a leader is left limp.

Returns:

| Type             | Description                                                                                                                                                                                                                                                                                                              |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `dict[str, Any]` | A dict of presets and links. A preset is name, records_ignored, and arms, one {"arm", "interface", "placed_by"} each, placed_by being "records", "only", "link_name" or "guess". A link, one per link carrying motors, is interface, family, side, role and candidates, each candidate {"variant", "family", "missing"}. |

#### check

```
check(
    workcell: WorkcellConfig, strict: bool = True
) -> Report
```

Returns whether the surveyed machine is `workcell`, and fit to run, as `check_readiness` reports it.

The drives are judged as surveyed, and the cameras as they answer now.

Parameters:

| Name       | Type             | Description                                                                       | Default    |
| ---------- | ---------------- | --------------------------------------------------------------------------------- | ---------- |
| `workcell` | `WorkcellConfig` | The workcell the machine should be.                                               | *required* |
| `strict`   | `bool`           | Whether a part that did not answer holds the run up, as check_readiness takes it. | `True`     |

Raises:

| Type        | Description                                       |
| ----------- | ------------------------------------------------- |
| `TypeError` | workcell is not an anvil.workcell.WorkcellConfig. |

#### close

```
close() -> None
```

Frees the handle now rather than once this object is collected. Closing twice does nothing.

### ConfigureSequence

```
ConfigureSequence(
    description: ArmDescription,
    gripper_only: bool,
    allow_fw_mismatch: bool,
    role: str | None = None,
    side: str | None = None,
)
```

Bases: `Handle`

Every joint of an arm configured in turn, one motor on the bus at a time.

Whatever motor is connected is written to the current joint's addresses and MIT mode, saved to flash, and given a fresh identity record. The walk holds no bus of its own, since the operator swaps motors on it between joints: swap the motor in, call `configure(bus=...)`, then call it again to retry or `advance()` to move on. A joint that was not configured is never advanced past. Every method but `close` raises `anvil.OperationError` once it is closed.

Starts a walk over the joints of `description` at its first joint, writing nothing until `configure()`.

Parameters:

| Name                | Type             | Description                                                                                                                                | Default    |
| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `description`       | `ArmDescription` | The arm whose joints to configure.                                                                                                         | *required* |
| `gripper_only`      | `bool`           | Whether to walk the gripper alone, its record a standalone gripper's.                                                                      | *required* |
| `allow_fw_mismatch` | `bool`           | Whether to configure a motor on other firmware than the motors configured before it; without it, such a motor is reported and not written. | *required* |
| `role`              | `str \| None`    | "leader" or "follower", which J8's record carries, so it must be given.                                                                    | `None`     |
| `side`              | `str \| None`    | "left" or "right"; None is the left for a symmetric build, and is refused for a mirrored one.                                              | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | role is missing, side is missing for a mirrored build, either is not one of its two, the description's family has no identity record, or gripper_only names an arm whose gripper has no motor of its own. |

Methods:

| Name        | Description                                                                                  |
| ----------- | -------------------------------------------------------------------------------------------- |
| `joints`    | Returns every joint the walk configures, in order, e.g. ["J1", "J2"].                        |
| `remaining` | Returns the joints not yet configured, the one configure() writes first; empty once all are. |
| `configure` | Configures the one motor on bus as the current joint, writing its flash but moving nothing.  |
| `advance`   | Moves on to the next joint.                                                                  |
| `close`     | Frees the handle now rather than once this object is collected. Closing twice does nothing.  |

#### joints

```
joints() -> list[str]
```

Returns every joint the walk configures, in order, e.g. `["J1", "J2"]`.

#### remaining

```
remaining() -> list[str]
```

Returns the joints not yet configured, the one `configure()` writes first; empty once all are.

#### configure

```
configure(bus: DamiaoBus) -> dict[str, Any]
```

Configures the one motor on `bus` as the current joint, writing its flash but moving nothing.

Parameters:

| Name  | Type        | Description                     | Default    |
| ----- | ----------- | ------------------------------- | ---------- |
| `bus` | `DamiaoBus` | The bus the motor is on, alone. | *required* |

Returns:

| Type             | Description                                                                                                                                                                                                                                                                                               |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dict[str, Any]` | A dict of motor, the joint; found, the motor as it answered; custom_firmware, as {"ok": ...} or {"error": ...}; firmware_mismatch, set where it disagrees with the motors before it; the writes; and the record. The joint is configured, and advance() will move on, only where every one of those took. |

Raises:

| Type                   | Description                                                                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | Every joint is already configured.                                                                                                                                                              |
| `UnreachableError`     | No motor answered on the bus.                                                                                                                                                                   |
| `OperationError`       | More than one answered, the bus stopped accepting frames part-way through the scan, or bus is closed or taken by a zeroing run. Nothing is written on any of these: fix the bus and call again. |

#### advance

```
advance() -> None
```

Moves on to the next joint.

Raises:

| Type                   | Description                                               |
| ---------------------- | --------------------------------------------------------- |
| `InvalidArgumentError` | The last configure() did not configure the current joint. |

#### close

```
close() -> None
```

Frees the handle now rather than once this object is collected. Closing twice does nothing.

### ZeroLimitSweep

```
ZeroLimitSweep(
    bus: DamiaoBus,
    description: ArmDescription,
    allow_missing: bool,
    side: str | None = None,
    motors: Sequence[str] | None = None,
)
```

Bases: `Handle`

An arm held passive while someone sweeps each joint through its whole range by hand, then zeroed from it.

Each joint is held in MIT mode at zero torque, so it backdrives by hand; `poll()` reads every joint and says how far each is from ready, and committing trims each joint onto the zero its observed range puts it at, moving the arm a little, and zeroes it. The sweep holds its bus: closing it, letting it be collected or `abort()` disables every joint, and the first two close the bus. Every method but `close` raises `anvil.OperationError` once it is closed, and `poll`, `commit_joint` and `commit` after `commit()` or `abort()` raise `anvil.InvalidArgumentError`.

Finds the arm's joints on `bus`, takes the bus over, and arms each joint that answered passive.

`bus` answers nothing from then on. A wrong argument is refused before the bus is taken; a joint that does not answer is refused after, and the bus is closed all the same: open the interface again.

Parameters:

| Name            | Type                    | Description                                                                                                                                    | Default    |
| --------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `bus`           | `DamiaoBus`             | The bus the arm is on.                                                                                                                         | *required* |
| `description`   | `ArmDescription`        | The arm to sweep.                                                                                                                              | *required* |
| `allow_missing` | `bool`                  | Whether to sweep the joints that answer when some do not. Without it, every joint but the gripper has to answer where the description puts it. | *required* |
| `side`          | `str \| None`           | "left" or "right", required for a mirrored build.                                                                                              | `None`     |
| `motors`        | `Sequence[str] \| None` | The joints to sweep, e.g. ["J1", "J2"], or None for all of them.                                                                               | `None`     |

Raises:

| Type                   | Description                                                                                                    |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | side is missing for a mirrored build or not one of its two, or motors names one the description does not have. |
| `UnreachableError`     | A joint that has to answer did not, or no joint answered at all.                                               |
| `OperationError`       | bus is closed or already taken by a zeroing run.                                                               |

Methods:

| Name           | Description                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| `armed`        | Returns each swept joint and whether arming it passive took, as {"motor", "armed"}.                          |
| `poll`         | Reads every joint once, widening its observed range, and returns how each stands.                            |
| `commit_joint` | Zeroes one joint without ending the sweep, moving it onto its zero first for up to two seconds.              |
| `commit`       | Zeroes every joint that is ready, each moved onto its zero first, then ends the sweep, every joint disabled. |
| `abort`        | Ends the sweep with every joint disabled, zeroing nothing.                                                   |
| `close`        | Frees the handle now rather than once this object is collected. Closing twice does nothing.                  |

#### armed

```
armed() -> list[dict[str, Any]]
```

Returns each swept joint and whether arming it passive took, as `{"motor", "armed"}`.

`armed` is `{"ok": None}` or `{"error": ...}`; a joint that failed to arm is disabled and still swept.

#### poll

```
poll() -> list[dict[str, Any]]
```

Reads every joint once, widening its observed range, and returns how each stands.

Returns:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                        |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `list[dict[str, Any]]` | One {"motor", "reading"} per joint, reading being {"error": ...} where it did not read, else {"ok": ...} holding current_deg; min_deg and max_deg, how far it has been each way; limits, its configured travel; and readiness, once it is read: coverage_percent, target_deg, error_deg and the blocker that stops it being committed, or None where nothing does. |

Raises:

| Type                   | Description          |
| ---------------------- | -------------------- |
| `InvalidArgumentError` | The sweep has ended. |

#### commit_joint

```
commit_joint(motor: str) -> dict[str, Any]
```

Zeroes one joint without ending the sweep, moving it onto its zero first for up to two seconds.

The joint is armed passive again afterwards, and its observed range starts over.

Parameters:

| Name    | Type  | Description                   | Default    |
| ------- | ----- | ----------------------------- | ---------- |
| `motor` | `str` | The joint to zero, e.g. "J3". | *required* |

Returns:

| Type             | Description                                                                                                                                                                                              |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dict[str, Any]` | A dict whose outcome is "no_data", nothing read from it yet; "blocked", with the blocker; "failed", with the error, the joint disabled; or "zeroed", with what the zero left and whether it was rearmed. |

Raises:

| Type                   | Description                                 |
| ---------------------- | ------------------------------------------- |
| `InvalidArgumentError` | The sweep has no joint motor, or has ended. |

#### commit

```
commit() -> list[dict[str, Any]]
```

Zeroes every joint that is ready, each moved onto its zero first, then ends the sweep, every joint disabled.

Returns:

| Type                   | Description                                                           |
| ---------------------- | --------------------------------------------------------------------- |
| `list[dict[str, Any]]` | One {"motor", "zeroed"} per joint, zeroed as commit_joint returns it. |

Raises:

| Type                   | Description                  |
| ---------------------- | ---------------------------- |
| `InvalidArgumentError` | The sweep has already ended. |

#### abort

```
abort() -> None
```

Ends the sweep with every joint disabled, zeroing nothing.

#### close

```
close() -> None
```

Frees the handle now rather than once this object is collected. Closing twice does nothing.

### ZeroAuto

```
ZeroAuto(
    bus: DamiaoBus,
    description: ArmDescription,
    side: str | None = None,
    motors: Sequence[str] | None = None,
)
```

Bases: `Handle`

An arm zeroed by driving each joint into its hard stops by itself, from rest, in about four minutes.

This moves the arm, under power, with no hand on it. Only a yam or an openarm has a run of every joint, and only an openyam one of J1 alone, for a base plate turned to switch its mount: it stands the arm up, turns the base into both its stops and back to rest, and zeroes J1 and nothing else, in about two minutes. The arm is parked at rest first: what each joint reads there is where the run puts its zero until it finds the joint's stops, so the zero it starts from may be off by any amount. `start()` enables the motors; each `poll()` from then on is one 10 ms tick, and the run paces itself, so polling faster does not move the arm faster. A step that goes wrong holds the arm where it stopped, as `hold()` does, and later polls keep holding it until `abort()` lets it go; a finished run is zeroed at rest by `commit()`. The run holds its bus: closing it, or letting it be collected, lets the arm go wherever it is and closes the bus, but for a held run, whose motors go on holding the arm on the last command sent, with nothing driving them, until they are disabled or powered off. Every method but `close` raises `anvil.OperationError` once it is closed.

Connects to the arm on `bus`, taking over the bus, and checks the arm is whole, enabling nothing.

`bus` answers nothing from then on. A wrong argument is refused before the bus is taken; an arm that is not whole is refused after, and the bus is closed all the same: open the interface again.

Parameters:

| Name          | Type                    | Description                                                                                                                                                | Default    |
| ------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `bus`         | `DamiaoBus`             | The bus the arm is on.                                                                                                                                     | *required* |
| `description` | `ArmDescription`        | The arm to zero, a yam or an openarm.                                                                                                                      | *required* |
| `side`        | `str \| None`           | "left" or "right", required for a mirrored build.                                                                                                          | `None`     |
| `motors`      | `Sequence[str] \| None` | ["J1"] for an openyam's base alone, every other joint driven to stand the arm up but keeping its zero and the gripper not driven, or None for every joint. | `None`     |

Raises:

| Type                   | Description                                                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | The arm has no run, or no run of the joints motors names; or side is missing for a mirrored build or not one of its two. |
| `UnreachableError`     | A chain motor did not answer, or not on the address the description gives it.                                            |
| `OperationError`       | bus is closed or already taken by a zeroing run.                                                                         |

Methods:

| Name               | Description                                                                                                |
| ------------------ | ---------------------------------------------------------------------------------------------------------- |
| `plan`             | Returns the plan of the run ZeroAuto would make of the same arguments, without a bus or a motor moving.    |
| `gripper_left_out` | Returns why the gripper is not in the run, or None where it is.                                            |
| `start`            | Enables every motor where it stands, taken as parked at rest; the arm moves from the next poll().          |
| `poll`             | Runs one tick, waiting out the rest of the last one first, and returns where the run is.                   |
| `hold`             | Stops moving and holds the arm where it is, for someone to support it; only abort() lets it go.            |
| `commit`           | Zeroes each joint of a finished run where it is held at rest, then releases the arm, every motor disabled. |
| `abort`            | Lets the arm go wherever it is, every motor disabled, zeroing nothing.                                     |
| `close`            | Frees the handle now rather than once this object is collected. Closing twice does nothing.                |

#### plan

```
plan(
    description: ArmDescription,
    motors: Sequence[str] | None = None,
) -> str
```

Returns the plan of the run `ZeroAuto` would make of the same arguments, without a bus or a motor moving.

One step per line, degrees, the arm's joints in chain order as the left arm takes them, `hold` for a joint left where it is: `pin_deg`, how far a setpoint leads its joint; `parked`, the rest it starts from; then each `move` to a posture, `search` of a joint into its `min` or `max` stop, and `gripper` search, in order.

Parameters:

| Name          | Type                    | Description                   | Default    |
| ------------- | ----------------------- | ----------------------------- | ---------- |
| `description` | `ArmDescription`        | The arm, a yam or an openarm. | *required* |
| `motors`      | `Sequence[str] \| None` | As ZeroAuto takes them.       | `None`     |

Raises:

| Type                   | Description                                               |
| ---------------------- | --------------------------------------------------------- |
| `InvalidArgumentError` | The arm has no run, or no run of the joints motors names. |

#### gripper_left_out

```
gripper_left_out() -> str | None
```

Returns why the gripper is not in the run, or `None` where it is.

It is left out at connect where it does not answer, and during the run where a search of it gives up: jaws that stop short or meet no stop. Without it the arm is zeroed alone, and the gripper by hand with a `ZeroLimitSweep` of it alone.

#### start

```
start() -> None
```

Enables every motor where it stands, taken as parked at rest; the arm moves from the next `poll()`.

Raises:

| Type                   | Description                                              |
| ---------------------- | -------------------------------------------------------- |
| `InvalidArgumentError` | The run has already started.                             |
| `UnreachableError`     | A motor would not enable; every motor is disabled again. |

#### poll

```
poll() -> dict[str, Any]
```

Runs one tick, waiting out the rest of the last one first, and returns where the run is.

Returns:

| Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dict[str, Any]` | A dict of phase, "ready", "running", "finished", "held" with the why, or "released"; step, the step it is on, counting from 0; steps, how many the run has; parked, what each chain joint read as the arm stood parked, as {"motor", "deg"}, in degrees, where the run puts its zero until it finds the joint's stops; and found, every stop met so far, as {"motor", "end", "stop_deg", "zero_deg"}, in degrees, end being "min" or "max". |

#### hold

```
hold() -> None
```

Stops moving and holds the arm where it is, for someone to support it; only `abort()` lets it go.

#### commit

```
commit() -> list[dict[str, Any]]
```

Zeroes each joint of a finished run where it is held at rest, then releases the arm, every motor disabled.

Each joint is disabled only as it is zeroed, so nothing moves between the two. A run of J1 alone zeroes J1 alone.

Returns:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                          |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list[dict[str, Any]]` | One {"motor", "outcome"} per joint, the gripper last, outcome being "blocked" with the reason, "unread" or "failed" with the error, or "zeroed" with what the zero left, off_deg, how far from the found zero it was held when zeroed, in degrees, and a warning, or None, saying how far the zero may be off where the joint's stops disagree with its description. |

Raises:

| Type                   | Description               |
| ---------------------- | ------------------------- |
| `InvalidArgumentError` | The run has not finished. |

#### abort

```
abort() -> None
```

Lets the arm go wherever it is, every motor disabled, zeroing nothing.

It falls under gravity: support it first.

#### close

```
close() -> None
```

Frees the handle now rather than once this object is collected. Closing twice does nothing.

### FoundRealHand

```
FoundRealHand(side: Side, link: str)
```

A RealHand L6 answering on a CAN link, as `RealHand.scan` finds it.

Attributes:

| Name   | Type   | Description                                   |
| ------ | ------ | --------------------------------------------- |
| `side` | `Side` | Which hand, by the CAN ID it answered on.     |
| `link` | `str`  | The CAN link it answered on, e.g. anvil_can1. |

#### side

```
side: Side
```

Which hand, by the CAN ID it answered on.

#### link

```
link: str
```

The CAN link it answered on, e.g. `anvil_can1`.

### RealHand

```
RealHand(link: str, side: Side)
```

Bases: `Handle`

A RealHand L6 on a CAN link, written its six angles directly.

Construct one for the hand on a link, which may be its own or the arm's it is bolted to: it answers on the CAN ID its description gives its side, which no arm motor uses. Nothing here runs a loop or reads the hand back: the caller sends the angles it wants at its own pace, typically those of an `anvil.apps.GloveTeleop`, and the hand holds the last ones it was sent, also once this is closed. Every method but `close` raises `anvil.OperationError` once it is closed. Linux only: elsewhere no hand opens.

Opens `link` for the `side` hand; nothing is sent.

Parameters:

| Name   | Type   | Description                                   | Default    |
| ------ | ------ | --------------------------------------------- | ---------- |
| `link` | `str`  | The CAN link the hand is on, e.g. anvil_can1. | *required* |
| `side` | `Side` | Which hand: each answers on its own CAN ID.   | *required* |

Raises:

| Type             | Description                                                                          |
| ---------------- | ------------------------------------------------------------------------------------ |
| `ValueError`     | side is not a side.                                                                  |
| `OperationError` | link cannot be opened, as one the host does not have cannot, or the host has no CAN. |

Methods:

| Name        | Description                                                                                 |
| ----------- | ------------------------------------------------------------------------------------------- |
| `scan`      | Returns every hand answering on any CAN link that is up, asked without moving it.           |
| `answers`   | Returns whether the hand answers on its link, asked without moving it.                      |
| `set_speed` | Sets every motor's speed, from 0 to 100; at 0 the fingers do not move.                      |
| `send`      | Commands the hand's six angles, each from 0 closed to 100 open.                             |
| `close`     | Frees the handle now rather than once this object is collected. Closing twice does nothing. |

#### scan

```
scan() -> list[FoundRealHand]
```

Returns every hand answering on any CAN link that is up, asked without moving it.

Each link is asked for the left hand's CAN ID and the right's, a few times each, since a hand on an arm's busy link leaves some questions unanswered. A link that cannot be opened is passed over, and a host without CAN has none. Takes up to a second for each link and hand that does not answer.

Returns:

| Type                  | Description                                           |
| --------------------- | ----------------------------------------------------- |
| `list[FoundRealHand]` | Each hand found, by link name then left before right. |

#### answers

```
answers() -> bool
```

Returns whether the hand answers on its link, asked without moving it.

#### set_speed

```
set_speed(speed: float) -> None
```

Sets every motor's speed, from 0 to 100; at 0 the fingers do not move.

Parameters:

| Name    | Type    | Description                                                      | Default    |
| ------- | ------- | ---------------------------------------------------------------- | ---------- |
| `speed` | `float` | The speed, from 0 to 100; a value outside that is clamped to it. | *required* |

Raises:

| Type             | Description                                             |
| ---------------- | ------------------------------------------------------- |
| `OperationError` | The frame could not be sent, as on a link that is down. |

#### send

```
send(angles: Sequence[float]) -> None
```

Commands the hand's six angles, each from 0 closed to 100 open.

Parameters:

| Name     | Type              | Description                                                                                                              | Default    |
| -------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `angles` | `Sequence[float]` | Thumb flexion, thumb abduction, index, middle, ring and pinky, in that order; a value outside 0 to 100 is clamped to it. | *required* |

Raises:

| Type                   | Description                                             |
| ---------------------- | ------------------------------------------------------- |
| `TypeError`            | angles is not a flat sequence of numbers.               |
| `InvalidArgumentError` | angles is not six values.                               |
| `OperationError`       | The frame could not be sent, as on a link that is down. |

#### close

```
close() -> None
```

Frees the handle now rather than once this object is collected. Closing twice does nothing.

### torque_full_scales_nm

```
torque_full_scales_nm(robot: RobotConfig) -> list[float]
```

Returns each slot's torque full scale, in newton meters, in slot order: what a `tau` reading is a share of.

Each is the maximum its motor model scales torques against as shipped. Nothing touches a bus.

Parameters:

| Name    | Type          | Description                    | Default    |
| ------- | ------------- | ------------------------------ | ---------- |
| `robot` | `RobotConfig` | The robot whose slots to read. | *required* |

Raises:

| Type        | Description                                 |
| ----------- | ------------------------------------------- |
| `TypeError` | robot is not an anvil.workcell.RobotConfig. |

### buses

```
buses(robot: RobotConfig) -> list[ResolvedBus]
```

Returns the buses of `robot` as `DamiaoHw` takes them, one per arm in slot order, each on the link it answers on.

An arm whose drives' identity records name it is put on the link they answer on, whatever the workcell declares. In a workcell with leaders the role on each gripper's record places an arm. An arm no record names keeps its declared bus where drives answer there, and is otherwise guessed onto a link with drives that no other arm took, sorted, the earlier to the leaders, so links named anything run; only where those links are as many as the arms guessed, and never onto one whose records name another arm. A gripper whose description names an `anvil.workcell.Gripper.alternate_motor` is addressed as that motor where, on its arm's link, its own address is silent and the alternate's answers. To find the records it opens each CAN link this host has and reads one drive per arm, and the gripper of such an arm on its link; it writes nothing, and a link another program holds is passed over.

Parameters:

| Name    | Type          | Description                    | Default    |
| ------- | ------------- | ------------------------------ | ---------- |
| `robot` | `RobotConfig` | The robot whose buses to find. | *required* |

Raises:

| Type        | Description                                 |
| ----------- | ------------------------------------------- |
| `TypeError` | robot is not an anvil.workcell.RobotConfig. |

### check_readiness

```
check_readiness(
    workcell: WorkcellConfig,
    configure_buses: bool = True,
    strict: bool = True,
) -> Report
```

Returns whether the machine on the bench is `workcell`, and fit to run, as a `Report`.

One read-only pass over the drives and the cameras, about half a second, before anything is commanded: nothing is written, no drive is enabled and nothing moves, and a camera is asked what modes it offers, which it answers whatever else has it open. Print the report, and read `Report.ok` to decide whether to go on. Only for a run on the drives: the sim has no buses to ask.

Parameters:

| Name              | Type             | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Default    |
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `workcell`        | `WorkcellConfig` | The workcell the machine should be.                                                                                                                                                                                                                                                                                                                                                                                                                                                        | *required* |
| `configure_buses` | `bool`           | Whether to bring every CAN link the host has up first, the same way on every machine, since a drive's record may put an arm on any of them: down if it is up, then up at 1 Mbit/s, sample point 0.75 and a transmit queue of 8. That needs sudo without a password for ip link set, which the installer arranges, and is best effort: a link it could not set up is a warning, and the pass after it decides whether the bus is fit to run. False takes the links as the machine has them. | `True`     |
| `strict`          | `bool`           | Whether a part that did not answer, a motor, a bus or a camera, holds the run up. False lets the robot come up without it. Anything saying the machine is not this workcell holds it up either way, a drive not on Anvil firmware among it.                                                                                                                                                                                                                                                | `True`     |

Raises:

| Type        | Description                                       |
| ----------- | ------------------------------------------------- |
| `TypeError` | workcell is not an anvil.workcell.WorkcellConfig. |

### status_names

```
status_names(
    status: Sequence[int], num_joints: int
) -> list[str]
```

Returns the first `num_joints` drive status codes by name, e.g. `"Enabled"` or `"Overcurrent"`.

The names are `Disabled`, `Enabled`, `Overvoltage`, `Undervoltage`, `Overcurrent`, `MosOvertemperature`, `CoilOvertemperature`, `CommunicationLoss` and `Overload`; a code this build does not know comes back in hex, e.g. `"0x0f"`.

Parameters:

| Name         | Type            | Description                                               | Default    |
| ------------ | --------------- | --------------------------------------------------------- | ---------- |
| `status`     | `Sequence[int]` | Drive status codes, e.g. anvil.control.JointState.status. | *required* |
| `num_joints` | `int`           | How many of them to name.                                 | *required* |

Raises:

| Type                   | Description                                                           |
| ---------------------- | --------------------------------------------------------------------- |
| `InvalidArgumentError` | status holds fewer than num_joints codes, or a code outside 0 to 255. |

### status_faults

```
status_faults(
    status: Sequence[int], num_joints: int
) -> list[bool]
```

Returns whether each of the first `num_joints` drive status codes is a fault.

A fault is anything but `Enabled` or `Disabled`, a code this build does not know included. A faulted drive will not enable until the fault clears.

Parameters:

| Name         | Type            | Description                                               | Default    |
| ------------ | --------------- | --------------------------------------------------------- | ---------- |
| `status`     | `Sequence[int]` | Drive status codes, e.g. anvil.control.JointState.status. | *required* |
| `num_joints` | `int`           | How many of them to judge.                                | *required* |

Raises:

| Type                   | Description                                                           |
| ---------------------- | --------------------------------------------------------------------- |
| `InvalidArgumentError` | status holds fewer than num_joints codes, or a code outside 0 to 255. |

# anvil.kinematics

## anvil.kinematics

Kinematics of an arm cut from a URDF: poses, forward kinematics, the Jacobian, gravity torque and inverse kinematics.

A `Chain` is the joints from a URDF's root link to a tip link; `Pose` is where a frame is; `solve_ik` finds the joint positions that put a chain's tip at a pose, tuned by `IkParams` and answered with an `IkSolution`. Every call here is pure computation: nothing moves an arm or needs a runtime.

Classes:

| Name         | Description                                                                                                      |
| ------------ | ---------------------------------------------------------------------------------------------------------------- |
| `Pose`       | A rigid-body pose: a position in meters and an orientation as a unit quaternion, x y z w.                        |
| `IkParams`   | Tuning for solve_ik: when to stop, the rest posture it leans toward, and how hard.                               |
| `IkSolution` | What solve_ik found: the joint positions, whether they reach the target, and how close they got.                 |
| `Chain`      | The driven joints of one URDF from its root link to a tip link: forward kinematics, Jacobian and gravity torque. |

Functions:

| Name       | Description                                                                      |
| ---------- | -------------------------------------------------------------------------------- |
| `solve_ik` | Returns the joint positions that put chain's tip at target, iterating from seed. |

Attributes:

| Name      | Type  | Description                                                             |
| --------- | ----- | ----------------------------------------------------------------------- |
| `MAX_DOF` | `int` | Most driven joints a chain can have, and the length of IkParams.rest_q. |

### MAX_DOF

```
MAX_DOF: int = 8
```

Most driven joints a chain can have, and the length of `IkParams.rest_q`.

### Pose

```
Pose(
    position: Sequence[float], orientation: Sequence[float]
)
```

Bases: `Struct`

A rigid-body pose: a position in meters and an orientation as a unit quaternion, x y z w.

The frame a pose is stated in is named where it is used: a chain's tip pose is in its URDF's root link's frame, the world frame for a robot's chain. A pose holds any values; one with a component that is not finite, or an orientation whose norm is more than 0.001 from 1, is refused by the call it is passed to. An orientation and its negation are the same rotation, and a pose a call returns may carry either sign, so compare orientations with `angle_rad` rather than `==`.

Builds a pose from a position and an orientation.

Parameters:

| Name          | Type              | Description                                                                 | Default    |
| ------------- | ----------------- | --------------------------------------------------------------------------- | ---------- |
| `position`    | `Sequence[float]` | x, y and z, in meters.                                                      | *required* |
| `orientation` | `Sequence[float]` | The unit quaternion as x, y, z, w, w last, so the identity is [0, 0, 0, 1]. | *required* |

Raises:

| Type                   | Description                                                      |
| ---------------------- | ---------------------------------------------------------------- |
| `TypeError`            | A field is a string, or an entry is not a number.                |
| `InvalidArgumentError` | position does not have 3 entries or orientation does not have 4. |

Methods:

| Name         | Description                                                                                       |
| ------------ | ------------------------------------------------------------------------------------------------- |
| `identity`   | Returns the pose with no translation and no rotation: a frame stated in itself.                   |
| `compose`    | Returns other, stated relative to this pose, as it is in the frame this pose is stated in.        |
| `inverse`    | Returns the pose that undoes this one: where the frame this one is stated in sits relative to it. |
| `distance_m` | Returns how far apart the two positions are, in meters.                                           |
| `angle_rad`  | Returns the angle of the rotation from one orientation to the other, in radians, from 0 to pi.    |

Attributes:

| Name          | Type          | Description                                           |
| ------------- | ------------- | ----------------------------------------------------- |
| `position`    | `list[float]` | Position as x, y, z, in meters.                       |
| `orientation` | `list[float]` | Orientation as a unit quaternion, x, y, z, w, w last. |

#### position

```
position: list[float]
```

Position as x, y, z, in meters.

Raises:

| Type                   | Description                                                |
| ---------------------- | ---------------------------------------------------------- |
| `TypeError`            | On assignment, a string, or an entry that is not a number. |
| `InvalidArgumentError` | On assignment, a value without exactly 3 entries.          |

#### orientation

```
orientation: list[float]
```

Orientation as a unit quaternion, x, y, z, w, w last.

Raises:

| Type                   | Description                                                |
| ---------------------- | ---------------------------------------------------------- |
| `TypeError`            | On assignment, a string, or an entry that is not a number. |
| `InvalidArgumentError` | On assignment, a value without exactly 4 entries.          |

#### identity

```
identity() -> Self
```

Returns the pose with no translation and no rotation: a frame stated in itself.

#### compose

```
compose(other: Pose) -> Pose
```

Returns `other`, stated relative to this pose, as it is in the frame this pose is stated in.

`pose.compose(other=pose.inverse())` is the identity. The result's orientation is normalized.

Parameters:

| Name    | Type   | Description                                    | Default    |
| ------- | ------ | ---------------------------------------------- | ---------- |
| `other` | `Pose` | A pose stated in the frame this one describes. | *required* |

Raises:

| Type                   | Description                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | Either pose has a component that is not finite, or an orientation that is not a unit quaternion. |

#### inverse

```
inverse() -> Pose
```

Returns the pose that undoes this one: where the frame this one is stated in sits relative to it.

Raises:

| Type                   | Description                                                                                    |
| ---------------------- | ---------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | This pose has a component that is not finite, or an orientation that is not a unit quaternion. |

#### distance_m

```
distance_m(other: Pose) -> float
```

Returns how far apart the two positions are, in meters.

Parameters:

| Name    | Type   | Description                                            | Default    |
| ------- | ------ | ------------------------------------------------------ | ---------- |
| `other` | `Pose` | The pose to measure to, in the same frame as this one. | *required* |

Raises:

| Type                   | Description                                                                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | Either pose has a component that is not finite, or an orientation that is not a unit quaternion, though only the positions are compared. |

#### angle_rad

```
angle_rad(other: Pose) -> float
```

Returns the angle of the rotation from one orientation to the other, in radians, from 0 to pi.

Parameters:

| Name    | Type   | Description                                            | Default    |
| ------- | ------ | ------------------------------------------------------ | ---------- |
| `other` | `Pose` | The pose to measure to, in the same frame as this one. | *required* |

Raises:

| Type                   | Description                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | Either pose has a component that is not finite, or an orientation that is not a unit quaternion. |

### IkParams

```
IkParams(
    max_iterations: int | None = None,
    residual_m: float | None = None,
    residual_rad: float | None = None,
    rest_q: Sequence[float] = (),
    damping_lambda_sq: float | None = None,
    rest_gain: float | None = None,
    limit_gain: float | None = None,
)
```

Bases: `Struct`

Tuning for `solve_ik`: when to stop, the rest posture it leans toward, and how hard.

`IkParams()` holds the solver's own defaults; `anvil.workcell.RobotConfig.ik_params` gives each arm of a robot the parameters its description states. Fields are checked when a solve runs, not when they are set: `solve_ik` refuses a value no solve can run on, and each field says which.

Builds parameters from the solver's defaults, overriding the fields given.

Parameters:

| Name                | Type              | Description                                                                                                     | Default |
| ------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- | ------- |
| `max_iterations`    | `int \| None`     | Most steps a solve takes; None keeps the default.                                                               | `None`  |
| `residual_m`        | `float \| None`   | Tip position error, in meters, at or under which the position counts as reached; None keeps the default.        | `None`  |
| `residual_rad`      | `float \| None`   | Tip orientation error, in radians, at or under which the orientation counts as reached; None keeps the default. | `None`  |
| `rest_q`            | `Sequence[float]` | The posture to lean toward, chain order, at most MAX_DOF entries; zero-padded to MAX_DOF.                       | `()`    |
| `damping_lambda_sq` | `float \| None`   | Damping of each step; None keeps the default.                                                                   | `None`  |
| `rest_gain`         | `float \| None`   | How hard each joint is pulled toward its rest pose; None keeps the default.                                     | `None`  |
| `limit_gain`        | `float \| None`   | How hard a joint past a limit is pushed back inside it; None keeps the default.                                 | `None`  |

Raises:

| Type                   | Description                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------- |
| `TypeError`            | max_iterations is not an integer; rest_q is a string, or an entry is not a number.     |
| `InvalidArgumentError` | max_iterations is negative or too large to hold; rest_q has more than MAX_DOF entries. |

Attributes:

| Name                | Type          | Description                                                                                                |
| ------------------- | ------------- | ---------------------------------------------------------------------------------------------------------- |
| `max_iterations`    | `int`         | Most steps a solve takes; solve_ik refuses fewer than 1 or more than 200.                                  |
| `residual_m`        | `float`       | Tip position error, in meters, at or under which the position counts as reached.                           |
| `residual_rad`      | `float`       | Tip orientation error, in radians, at or under which the orientation counts as reached.                    |
| `rest_q`            | `list[float]` | Posture the solve leans toward, MAX_DOF positions in chain order, in radians or meters.                    |
| `damping_lambda_sq` | `float`       | Damping of each step, the square of lambda; solve_ik refuses less than 1e-6 or a value that is not finite. |
| `rest_gain`         | `float`       | Pull of each joint toward its rest pose, per unit away from it; solve_ik refuses one outside 0 to 1e6.     |
| `limit_gain`        | `float`       | Push of a joint back inside a limit it is past, per span of travel; solve_ik refuses one outside 0 to 1e6. |

#### max_iterations

```
max_iterations: int = 50
```

Most steps a solve takes; `solve_ik` refuses fewer than 1 or more than 200.

Raises:

| Type                   | Description                                                |
| ---------------------- | ---------------------------------------------------------- |
| `TypeError`            | On assignment, a value that is not an integer.             |
| `InvalidArgumentError` | On assignment, a negative value, or one too large to hold. |

#### residual_m

```
residual_m: float = 0.001
```

Tip position error, in meters, at or under which the position counts as reached.

`solve_ik` refuses one that is not finite and positive.

#### residual_rad

```
residual_rad: float = 0.01
```

Tip orientation error, in radians, at or under which the orientation counts as reached.

`solve_ik` refuses one that is not finite and positive.

#### rest_q

```
rest_q: list[float] = [
    0.0,
    0.0,
    0.0,
    0.0,
    0.0,
    0.0,
    0.0,
    0.0,
]
```

Posture the solve leans toward, `MAX_DOF` positions in chain order, in radians or meters.

Only the first `Chain.num_joints` entries are used, though `solve_ik` refuses any that is not finite. The list writes through: `params.rest_q[1] = 0.1` is what the next solve runs with, and a change to its length is refused.

Raises:

| Type                   | Description                                                                                                                      |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | On assignment, a string, or an entry that is not a number.                                                                       |
| `InvalidArgumentError` | On assignment, more than MAX_DOF entries, which are zero-padded to MAX_DOF otherwise; or a change to the returned list's length. |

#### damping_lambda_sq

```
damping_lambda_sq: float = 0.36
```

Damping of each step, the square of lambda; `solve_ik` refuses less than 1e-6 or a value that is not finite.

Larger takes shorter, steadier steps near a singular configuration; smaller converges faster away from one.

#### rest_gain

```
rest_gain: float = 0.001
```

Pull of each joint toward its rest pose, per unit away from it; `solve_ik` refuses one outside 0 to 1e6.

The pull acts only in the directions that leave the tip where it is.

#### limit_gain

```
limit_gain: float = 10.0
```

Push of a joint back inside a limit it is past, per span of travel; `solve_ik` refuses one outside 0 to 1e6.

Limits only bias the solve, through this push in the directions that leave the tip where it is; they are never enforced, so `IkSolution.q` can sit outside them.

### IkSolution

```
IkSolution()
```

Bases: `Counted`

What `solve_ik` found: the joint positions, whether they reach the target, and how close they got.

Get one from `solve_ik`, never by calling the class.

Attributes:

| Name           | Type          | Description                                                                                                |
| -------------- | ------------- | ---------------------------------------------------------------------------------------------------------- |
| `q`            | `list[float]` | Joint positions, one per joint of the chain in chain order, in radians or meters; possibly outside limits. |
| `converged`    | `bool`        | Whether the tip is within both IkParams.residual_m and IkParams.residual_rad of the target at q.           |
| `iterations`   | `int`         | Steps taken: 0 from a seed already at the target, IkParams.max_iterations when the solve ran out.          |
| `residual_m`   | `float`       | Distance from the tip to the target position at q, in meters.                                              |
| `residual_rad` | `float`       | Angle from the tip's orientation to the target's at q, in radians.                                         |

#### q

```
q: list[float]
```

Joint positions, one per joint of the chain in chain order, in radians or meters; possibly outside limits.

#### converged

```
converged: bool
```

Whether the tip is within both `IkParams.residual_m` and `IkParams.residual_rad` of the target at `q`.

#### iterations

```
iterations: int
```

Steps taken: 0 from a seed already at the target, `IkParams.max_iterations` when the solve ran out.

#### residual_m

```
residual_m: float
```

Distance from the tip to the target position at `q`, in meters.

#### residual_rad

```
residual_rad: float
```

Angle from the tip's orientation to the target's at `q`, in radians.

### Chain

```
Chain()
```

The driven joints of one URDF from its root link to a tip link: forward kinematics, Jacobian and gravity torque.

One comes from `from_urdf_file` or `from_urdf_text`, or for each arm of a robot from `anvil.workcell.RobotConfig.chains`. A configuration `q` is one position per driven joint in chain order, root first: radians for a revolute joint, meters for a prismatic one. Every pose and position a chain returns is in its URDF's root link's frame, the root link being the one no joint hangs from; for a robot's chain that is the world frame, the composed URDF's `world` link, which every `anvil.workcell.Mount` is placed in. A `q` is not checked against the limits, and one that is not finite gives results that are not finite.

Get one from `Chain.from_urdf_file` or `Chain.from_urdf_text`, never by calling the class.

Methods:

| Name             | Description                                                                                                 |
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `from_urdf_file` | Reads a URDF file and builds the chain of driven joints from its root link to the link tip.                 |
| `from_urdf_text` | Builds the chain from URDF text in hand, as from_urdf_file does from a file.                                |
| `fk`             | Returns the tip link's pose at configuration q.                                                             |
| `joint_origins`  | Returns where each driven joint's origin is at configuration q: the elbow's position without a chain to it. |
| `jacobian`       | Returns the geometric Jacobian at the tip link's origin at configuration q.                                 |
| `gravity_torque` | Returns the torque at each driven joint that holds the chain still under gravity at configuration q.        |

Attributes:

| Name          | Type                              | Description                                                                                         |
| ------------- | --------------------------------- | --------------------------------------------------------------------------------------------------- |
| `num_joints`  | `int`                             | Number of driven joints from the root link to the tip, at most MAX_DOF.                             |
| `joint_names` | `list[str]`                       | URDF names of the driven joints, chain order.                                                       |
| `limits`      | `tuple[list[float], list[float]]` | Position limits from the URDF as a (lower, upper) pair of lists, chain order, in radians or meters. |

#### num_joints

```
num_joints: int
```

Number of driven joints from the root link to the tip, at most `MAX_DOF`.

#### joint_names

```
joint_names: list[str]
```

URDF names of the driven joints, chain order.

#### limits

```
limits: tuple[list[float], list[float]]
```

Position limits from the URDF as a `(lower, upper)` pair of lists, chain order, in radians or meters.

#### from_urdf_file

```
from_urdf_file(path: str | Path, tip: str) -> Self
```

Reads a URDF file and builds the chain of driven joints from its root link to the link `tip`.

Revolute and prismatic joints are driven; fixed joints on the path are folded into the links around them.

Parameters:

| Name   | Type          | Description                                                                  | Default    |
| ------ | ------------- | ---------------------------------------------------------------------------- | ---------- |
| `path` | `str \| Path` | The URDF file.                                                               | *required* |
| `tip`  | `str`         | The link the chain ends at. The root link itself gives a chain of no joints. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                                                    |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NotFoundError`        | There is no file at path.                                                                                                                                                                                                                      |
| `InvalidArgumentError` | The file is not UTF-8 text or not a URDF; tip is not one of its links; a joint on the path is neither revolute, prismatic nor fixed; the path has more than MAX_DOF driven joints; or a driven joint's lower limit is not below its upper one. |
| `OperationError`       | The file could not be read for another reason, such as path being a directory.                                                                                                                                                                 |

#### from_urdf_text

```
from_urdf_text(urdf: str, tip: str) -> Self
```

Builds the chain from URDF text in hand, as `from_urdf_file` does from a file.

Parameters:

| Name   | Type  | Description                                                                  | Default    |
| ------ | ----- | ---------------------------------------------------------------------------- | ---------- |
| `urdf` | `str` | The URDF's text, such as anvil.workcell.RobotConfig.composed_urdf() returns. | *required* |
| `tip`  | `str` | The link the chain ends at. The root link itself gives a chain of no joints. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | urdf is not a URDF; tip is not one of its links; a joint on the path is neither revolute, prismatic nor fixed; the path has more than MAX_DOF driven joints; or a driven joint's lower limit is not below its upper one. |

#### fk

```
fk(q: Sequence[float]) -> Pose
```

Returns the tip link's pose at configuration `q`.

In the URDF's root link's frame, the world frame for a robot's chain.

Parameters:

| Name | Type              | Description                                 | Default    |
| ---- | ----------------- | ------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | One position per driven joint, chain order. | *required* |

Raises:

| Type                   | Description                                                    |
| ---------------------- | -------------------------------------------------------------- |
| `TypeError`            | q is a string, or an entry is not a number.                    |
| `InvalidArgumentError` | q does not have num_joints entries, or one is NaN or infinite. |

#### joint_origins

```
joint_origins(q: Sequence[float]) -> list[list[float]]
```

Returns where each driven joint's origin is at configuration `q`: the elbow's position without a chain to it.

Joint `i`'s origin is the far end of the links the joints before it carry, so between two configurations these are how far each link's end travels; the tip's own position is `fk`'s.

Parameters:

| Name | Type              | Description                                 | Default    |
| ---- | ----------------- | ------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | One position per driven joint, chain order. | *required* |

Returns:

| Type                | Description                                                                                                                  |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `list[list[float]]` | One [x, y, z] per driven joint, chain order, in meters in the URDF's root link's frame, the world frame for a robot's chain. |

Raises:

| Type                   | Description                                                    |
| ---------------------- | -------------------------------------------------------------- |
| `TypeError`            | q is a string, or an entry is not a number.                    |
| `InvalidArgumentError` | q does not have num_joints entries, or one is NaN or infinite. |

#### jacobian

```
jacobian(q: Sequence[float]) -> list[list[float]]
```

Returns the geometric Jacobian at the tip link's origin at configuration `q`.

In the URDF's root link's frame, the world frame for a robot's chain.

Parameters:

| Name | Type              | Description                                 | Default    |
| ---- | ----------------- | ------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | One position per driven joint, chain order. | *required* |

Returns:

| Type                | Description                                                                                                                                                                                                                                              |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list[list[float]]` | 6 rows of num_joints columns, one column per driven joint in chain order. Rows 0 to 2 are the tip's linear velocity along x, y and z per unit of joint velocity, rows 3 to 5 its angular velocity about them; a prismatic joint's angular rows are zero. |

Raises:

| Type                   | Description                                                    |
| ---------------------- | -------------------------------------------------------------- |
| `TypeError`            | q is a string, or an entry is not a number.                    |
| `InvalidArgumentError` | q does not have num_joints entries, or one is NaN or infinite. |

#### gravity_torque

```
gravity_torque(q: Sequence[float]) -> list[float]
```

Returns the torque at each driven joint that holds the chain still under gravity at configuration `q`.

Gravity is 9.81 meters per second squared along the negative z of the URDF's root link's frame, the world frame for a robot's chain, acting on the link masses and centers of mass the URDF states. A URDF with a gripper grafted on, such as `anvil.workcell.RobotConfig.composed_urdf()` returns, carries the gripper's mass too. Links that branch off the chain, such as a gripper's fingers, load the link they hang from, held at their joints' zero.

Parameters:

| Name | Type              | Description                                 | Default    |
| ---- | ----------------- | ------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | One position per driven joint, chain order. | *required* |

Returns:

| Type          | Description                                                                                               |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| `list[float]` | One entry per driven joint, chain order: newton meters for a revolute joint, newtons for a prismatic one. |

Raises:

| Type                   | Description                                                    |
| ---------------------- | -------------------------------------------------------------- |
| `TypeError`            | q is a string, or an entry is not a number.                    |
| `InvalidArgumentError` | q does not have num_joints entries, or one is NaN or infinite. |

### solve_ik

```
solve_ik(
    chain: Chain,
    seed: Sequence[float],
    target: Pose,
    params: IkParams | None = None,
) -> IkSolution
```

Returns the joint positions that put `chain`'s tip at `target`, iterating from `seed`.

Each step is a damped least-squares step toward the target pose, leaning toward `IkParams.rest_q` and back inside the joint limits in the directions that leave the tip where it is, and no joint moves more than pi/4 in one step. It stops when the tip is within both residuals or `IkParams.max_iterations` steps have been taken, so a solve that runs out returns its last configuration with `IkSolution.converged` false rather than raising.

Parameters:

| Name     | Type               | Description                                                                                                 | Default    |
| -------- | ------------------ | ----------------------------------------------------------------------------------------------------------- | ---------- |
| `chain`  | `Chain`            | The chain to solve for.                                                                                     | *required* |
| `seed`   | `Sequence[float]`  | The configuration to start from, one position per driven joint, chain order.                                | *required* |
| `target` | `Pose`             | The tip link's pose to reach, in the chain's URDF's root link's frame, the world frame for a robot's chain. | *required* |
| `params` | `IkParams \| None` | The solver's tuning; None is IkParams().                                                                    | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | seed is a string, or an entry is not a number.                                                                                                                                                                                     |
| `InvalidArgumentError` | seed does not have chain.num_joints entries or has one that is not finite; target has a component that is not finite or an orientation that is not a unit quaternion; or a field of params is outside the range that field states. |

# anvil.recorder

## anvil.recorder

Records episodes of a running workcell into MCAP files, and compresses, annotates and labels them once finished.

An episode holds the loop's state, what it wrote, where each tick's time went, each controller's trace and command stream, the events, and every camera's JPEG with every stamp it got, in one MCAP file that opens in Foxglove as it is.

Episodes go into a `Session`, a directory `<root>/<session>/` under a data root the caller names: each start is the session's next episode, `0001.mcap`, `0002.mcap` and on, so a session lists in the order it was recorded, and beside each is written `<NNNN>.foxglove-layout.json`, the layout that draws its robot, cameras and joints. Every file says which session and episode it is, and the session's task, in its `anvil.episode` metadata.

A `Recorder` records on threads of its own and reads the runtime and the cameras the way any other reader does, so the loop never waits on it and a camera window reading the same cameras still gets every frame. `RecorderConfig` says which samples of each source go in. A `Compression` turns a finished episode's JPEGs into H.264 in place, and can be cancelled from another thread while it runs. `annotate` says things about a finished episode, in place, `annotations` reads them back, and `label` relabels how it went.

A `DatasetExport` turns finished episodes into one LeRobot dataset, in its v3.0 layout, for a policy to train on. Each dataset frame is one frame of an anchor camera, carrying each slot's position as the loop measured it as `observation.state`, as it commanded it as `action`, and each camera's image as video, encoded by ffmpeg. Encoding is the cost, several core-seconds per second of 480p video per camera at LeRobot's encoder settings: its ffmpeg runs at the lowest priority, and on the robot, cancel it the moment the robot needs the CPU. It needs `ffmpeg` on `PATH`, built with `libsvtav1` for AV1 or `libx264` for H.264.

Classes:

| Name               | Description                                                                                               |
| ------------------ | --------------------------------------------------------------------------------------------------------- |
| `RecordingMode`    | Which samples of each source an episode keeps.                                                            |
| `Outcome`          | How an episode went, as whoever stopped it judged; a failed episode is kept and labeled, not deleted.     |
| `RecorderConfig`   | How an episode is recorded; rated, free and synced build one of each mode.                                |
| `RecorderStats`    | What an episode counted, as Recorder.stop returns it and the file's anvil.stats metadata keeps it.        |
| `Session`          | A directory of episodes, <root>/<name>/, created or reopened when this is constructed.                    |
| `SessionSummary`   | One session under a data root, as sessions lists it.                                                      |
| `Annotation`       | One thing said about an episode: a key and its value, on the whole episode or pinned to one moment of it. |
| `CameraCompressed` | One camera of a compressed episode, as CompressStats.cameras holds it.                                    |
| `CompressStats`    | What Compression.run did: the file's size before and after, and each camera's.                            |
| `Compression`      | A compression of a finished episode in place, every camera's JPEGs to H.264, lossy and for good.          |
| `Recorder`         | Records episodes of a running workcell, one at a time, each into the next file of a Session.              |
| `VideoCodec`       | Codec a dataset's videos are encoded with.                                                                |
| `LeRobotExport`    | What DatasetExport.run wrote.                                                                             |
| `DatasetExport`    | An export of recorded episodes into a new LeRobot dataset.                                                |

Functions:

| Name          | Description                                                                                                     |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| `sessions`    | Lists every session under the data root root, the most recently written first.                                  |
| `layout_path` | Returns where the Foxglove layout that draws episode is written, beside it; whether either exists is not asked. |
| `annotations` | Reads what was said about the finished episode: its newest annotations, as annotate last wrote them.            |
| `annotate`    | Replaces the finished episode's annotations with annotations, in place, keeping the ones before as history.     |
| `label`       | Relabels how the finished episode went, in place, keeping the labels before as history.                         |
| `mesh`        | Returns the bytes of a mesh a composed URDF names, from the copy shipped in the package.                        |

### RecordingMode

Bases: `StrEnum`

Which samples of each source an episode keeps.

Attributes:

| Name     | Type | Description                                                                                           |
| -------- | ---- | ----------------------------------------------------------------------------------------------------- |
| `RATED`  |      | Each source at a rate of its own, keeping the newest sample of each period.                           |
| `FREE`   |      | Every sample of every source: every frame and every tick, with the ticks the recorder missed counted. |
| `SYNCED` |      | One observation per frame of an anchor camera, the way a LeRobot dataset lays one out.                |

#### RATED

```
RATED
```

Each source at a rate of its own, keeping the newest sample of each period.

By default the loop at 100 Hz, or its own rate where that is slower, and each camera at its fps.

#### FREE

```
FREE
```

Every sample of every source: every frame and every tick, with the ticks the recorder missed counted.

#### SYNCED

```
SYNCED
```

One observation per frame of an anchor camera, the way a LeRobot dataset lays one out.

Each other source contributes its latest sample at or before the frame's stamp.

### Outcome

Bases: `StrEnum`

How an episode went, as whoever stopped it judged; a failed episode is kept and labeled, not deleted.

Attributes:

| Name        | Type | Description                                                                           |
| ----------- | ---- | ------------------------------------------------------------------------------------- |
| `UNLABELED` |      | Nobody said: stopped without a judgment, or finished when its recorder was collected. |
| `SUCCESS`   |      | It did what its task asks.                                                            |
| `FAILURE`   |      | It did not do what its task asks.                                                     |

#### UNLABELED

```
UNLABELED
```

Nobody said: stopped without a judgment, or finished when its recorder was collected.

#### SUCCESS

```
SUCCESS
```

It did what its task asks.

#### FAILURE

```
FAILURE
```

It did not do what its task asks.

### RecorderConfig

```
RecorderConfig(
    mode: RecordingMode,
    rates_hz: Mapping[str, float] = {},
    anchor_camera: str | None = None,
    staleness_ns: int | None = None,
)
```

How an episode is recorded; `rated`, `free` and `synced` build one of each mode.

Nothing is checked here: `Recorder.start` refuses a config its sources cannot meet, with `anvil.InvalidArgumentError`, naming what is wrong. That covers a rate for a source the recording does not have, one that is not a positive number or is above what its source produces, an `anchor_camera` that is not one of the cameras recorded, a `staleness_ns` of 0, and a field the mode does not take, such as `anchor_camera` on a rated config.

Methods:

| Name     | Description                                                                                 |
| -------- | ------------------------------------------------------------------------------------------- |
| `rated`  | Builds a rated config: each source at rates_hz, or at its default where that leaves it out. |
| `free`   | Builds a free config, which keeps every sample of every source.                             |
| `synced` | Builds a synced config: one observation per frame of the camera anchor_camera.              |

Attributes:

| Name            | Type                  | Description                                                                                             |
| --------------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
| `mode`          | `RecordingMode`       | Which samples of each source the episode keeps.                                                         |
| `rates_hz`      | `Mapping[str, float]` | Rated only: the rate to record each source at, in hertz, by source.                                     |
| `anchor_camera` | `str \| None`         | Synced only: the name of the camera whose frames make the observations.                                 |
| `staleness_ns`  | `int \| None`         | Synced only: how old another source's sample may be, in nanoseconds, before the observation is dropped. |

#### mode

```
mode: RecordingMode
```

Which samples of each source the episode keeps.

#### rates_hz

```
rates_hz: Mapping[str, float] = {}
```

Rated only: the rate to record each source at, in hertz, by source.

`"robot"` for the loop and otherwise a camera's name; a source left out runs at its default, as `RecordingMode.RATED` says.

#### anchor_camera

```
anchor_camera: str | None = None
```

Synced only: the name of the camera whose frames make the observations.

#### staleness_ns

```
staleness_ns: int | None = None
```

Synced only: how old another source's sample may be, in nanoseconds, before the observation is dropped.

A dropped observation is counted. `None` is one anchor frame period.

#### rated

```
rated(rates_hz: Mapping[str, float] | None = None) -> Self
```

Builds a rated config: each source at `rates_hz`, or at its default where that leaves it out.

Parameters:

| Name       | Type                          | Description                                                                                    | Default |
| ---------- | ----------------------------- | ---------------------------------------------------------------------------------------------- | ------- |
| `rates_hz` | `Mapping[str, float] \| None` | The rate per source, in hertz, as rates_hz holds it; None records every source at its default. | `None`  |

Returns:

| Type   | Description                                          |
| ------ | ---------------------------------------------------- |
| `Self` | The config, checked when a recording starts with it. |

#### free

```
free() -> Self
```

Builds a free config, which keeps every sample of every source.

#### synced

```
synced(
    anchor_camera: str, staleness_ns: int | None = None
) -> Self
```

Builds a synced config: one observation per frame of the camera `anchor_camera`.

Parameters:

| Name            | Type          | Description                                                                                                                                                        | Default    |
| --------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `anchor_camera` | `str`         | The name of the camera whose frames make the observations; one of the cameras recorded.                                                                            | *required* |
| `staleness_ns`  | `int \| None` | How old another source's sample may be before the observation is dropped and counted; None is one anchor frame period, and 0 is refused when the recording starts. | `None`     |

Returns:

| Type   | Description                                          |
| ------ | ---------------------------------------------------- |
| `Self` | The config, checked when a recording starts with it. |

### RecorderStats

```
RecorderStats(
    messages_written: int,
    ticks_missed: int,
    commands_missed: int,
    frames_missed: dict[str, int],
    frames_received: dict[str, int],
    link_sent: int,
    link_answered: int,
    observations_written: int,
    observations_dropped: int,
    stale_by_source: dict[str, int],
)
```

What an episode counted, as `Recorder.stop` returns it and the file's `anvil.stats` metadata keeps it.

Attributes:

| Name                   | Type             | Description                                                                                     |
| ---------------------- | ---------------- | ----------------------------------------------------------------------------------------------- |
| `messages_written`     | `int`            | Messages in the file, on every channel; 0 until the file is finished.                           |
| `ticks_missed`         | `int`            | Ticks the loop ran that the recorder never saw: gaps in the tick counter.                       |
| `commands_missed`      | `int`            | Commands the loop picked up that the recorder never saw: gaps in their sequence numbers.        |
| `frames_missed`        | `dict[str, int]` | Frames the recorder never got, by camera name.                                                  |
| `frames_received`      | `dict[str, int]` | Frames the recorder got, by camera name.                                                        |
| `link_sent`            | `int`            | Frames the hw interface sent the drives over the episode, every slot's added up.                |
| `link_answered`        | `int`            | Replies the drives sent back to link_sent's frames.                                             |
| `observations_written` | `int`            | Observations written, when synced; 0 otherwise.                                                 |
| `observations_dropped` | `int`            | Observations dropped because some source had no sample recent enough, when synced; 0 otherwise. |
| `stale_by_source`      | `dict[str, int]` | The dropped observations each source was too stale for, by source.                              |

#### messages_written

```
messages_written: int
```

Messages in the file, on every channel; 0 until the file is finished.

#### ticks_missed

```
ticks_missed: int
```

Ticks the loop ran that the recorder never saw: gaps in the tick counter.

#### commands_missed

```
commands_missed: int
```

Commands the loop picked up that the recorder never saw: gaps in their sequence numbers.

#### frames_missed

```
frames_missed: dict[str, int]
```

Frames the recorder never got, by camera name.

Gaps in the driver's sequence, whether the driver dropped them or the recorder read too slowly.

#### frames_received

```
frames_received: dict[str, int]
```

Frames the recorder got, by camera name.

#### link_sent

```
link_sent: int
```

Frames the hw interface sent the drives over the episode, every slot's added up.

#### link_answered

```
link_answered: int
```

Replies the drives sent back to `link_sent`'s frames.

#### observations_written

```
observations_written: int
```

Observations written, when synced; 0 otherwise.

#### observations_dropped

```
observations_dropped: int
```

Observations dropped because some source had no sample recent enough, when synced; 0 otherwise.

#### stale_by_source

```
stale_by_source: dict[str, int]
```

The dropped observations each source was too stale for, by source.

One dropped observation counts against every source that was.

### Session

```
Session(
    root: Path,
    name: str | None = None,
    task: str | None = None,
    operator: str | None = None,
)
```

A directory of episodes, `<root>/<name>/`, created or reopened when this is constructed.

Hand one to `Recorder.start` to record its next episode. The directory and its episodes outlive this object.

Opens the session `name` under the data root `root`, creating either directory as needed.

A session that exists is reused, its episodes continuing from the next free index. `task` and `operator` are kept in the session's `session.json` and written into every episode recorded in it.

Parameters:

| Name       | Type          | Description                                                                                                                                                                                                                                                          | Default    |
| ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `root`     | `Path`        | The data root the session's directory sits under; nothing guesses it.                                                                                                                                                                                                | *required* |
| `name`     | `str \| None` | The session's directory name: not empty, starting with no ., neither starting nor ending with a space, and with no /, backslash or control character. None creates a new session under a generated name of two words, like brisk-heron, that nothing under root has. | `None`     |
| `task`     | `str \| None` | What every episode in the session is of, e.g. "pick up the red cube"; None keeps what an existing session last said, or none for a new one.                                                                                                                          | `None`     |
| `operator` | `str \| None` | Who records the episodes; None as for task.                                                                                                                                                                                                                          | `None`     |

Raises:

| Type                   | Description                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------- |
| `TypeError`            | root is not a path, None included.                                                                |
| `InvalidArgumentError` | root is empty, name is not one directory name, or the session's session.json cannot be parsed.    |
| `OperationError`       | root or the session's directory cannot be created, or its session.json cannot be read or written. |

Methods:

| Name                   | Description                                                                                        |
| ---------------------- | -------------------------------------------------------------------------------------------------- |
| `discard_last_episode` | Deletes the newest episode and the layout beside it, so the next episode recorded takes its index. |
| `remove`               | Deletes the session, which holds no episode: its session.json and its directory.                   |

Attributes:

| Name       | Type          | Description                                                            |
| ---------- | ------------- | ---------------------------------------------------------------------- |
| `name`     | `str`         | The session's name: its directory's under the data root.               |
| `path`     |               | The session's directory, <root>/<name>.                                |
| `task`     | `str \| None` | What every episode in the session is of; None where it was never said. |
| `operator` | `str \| None` | Who records the session's episodes; None where it was never said.      |

#### name

```
name: str
```

The session's name: its directory's under the data root.

#### path

```
path
```

The session's directory, `<root>/<name>`.

#### task

```
task: str | None
```

What every episode in the session is of; `None` where it was never said.

#### operator

```
operator: str | None
```

Who records the session's episodes; `None` where it was never said.

#### discard_last_episode

```
discard_last_episode() -> str | None
```

Deletes the newest episode and the layout beside it, so the next episode recorded takes its index.

Returns:

| Type          | Description                                                                   |
| ------------- | ----------------------------------------------------------------------------- |
| `str \| None` | The deleted episode's name, e.g. "0003"; None for a session with no episodes. |

Raises:

| Type             | Description                                                                                        |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| `BusyError`      | The newest episode is still being recorded, by this process or another; stop that recording first. |
| `OperationError` | The session's directory cannot be read, or the episode or its layout cannot be deleted.            |

#### remove

```
remove() -> None
```

Deletes the session, which holds no episode: its `session.json` and its directory.

Raises:

| Type             | Description                                                                                                    |
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
| `OperationError` | The session still holds an episode, or a file that is not its own, or its directory cannot be read or deleted. |

### SessionSummary

```
SessionSummary(
    name: str,
    path: Path,
    task: str | None,
    operator: str | None,
    episodes: int,
    modified_at: datetime,
)
```

One session under a data root, as `sessions` lists it.

Attributes:

| Name          | Type          | Description                                                                              |
| ------------- | ------------- | ---------------------------------------------------------------------------------------- |
| `name`        | `str`         | The session's name: its directory's under the data root.                                 |
| `path`        | `Path`        | The session's directory, <root>/<name>.                                                  |
| `task`        | `str \| None` | What every episode in the session is of; None where it was never said.                   |
| `operator`    | `str \| None` | Who records the session's episodes; None where it was never said.                        |
| `episodes`    | `int`         | How many episodes the session holds.                                                     |
| `modified_at` | `datetime`    | When the session's newest episode was last written, else when its directory was, in UTC. |

#### name

```
name: str
```

The session's name: its directory's under the data root.

#### path

```
path: Path
```

The session's directory, `<root>/<name>`.

#### task

```
task: str | None
```

What every episode in the session is of; `None` where it was never said.

#### operator

```
operator: str | None
```

Who records the session's episodes; `None` where it was never said.

#### episodes

```
episodes: int
```

How many episodes the session holds.

#### modified_at

```
modified_at: datetime
```

When the session's newest episode was last written, else when its directory was, in UTC.

### Annotation

```
Annotation(
    key: str, value: str, log_time_ns: int | None = None
)
```

One thing said about an episode: a `key` and its `value`, on the whole episode or pinned to one moment of it.

Attributes:

| Name          | Type          | Description                                                                                          |
| ------------- | ------------- | ---------------------------------------------------------------------------------------------------- |
| `key`         | `str`         | What is said, e.g. "grasp"; never empty, which annotate refuses.                                     |
| `value`       | `str`         | What it is, e.g. "slipped"; may be empty.                                                            |
| `log_time_ns` | `int \| None` | The moment it is pinned to, a log time on the episode's timeline, in nanoseconds on CLOCK_MONOTONIC. |

#### key

```
key: str
```

What is said, e.g. `"grasp"`; never empty, which `annotate` refuses.

#### value

```
value: str
```

What it is, e.g. `"slipped"`; may be empty.

#### log_time_ns

```
log_time_ns: int | None = None
```

The moment it is pinned to, a log time on the episode's timeline, in nanoseconds on `CLOCK_MONOTONIC`.

`None` for the whole episode.

### CameraCompressed

```
CameraCompressed(
    frames: int,
    gop_frames: int,
    jpeg_bytes: int,
    h264_bytes: int,
)
```

One camera of a compressed episode, as `CompressStats.cameras` holds it.

The file's `anvil.compression` metadata keeps the same.

Attributes:

| Name         | Type  | Description                                                                                                  |
| ------------ | ----- | ------------------------------------------------------------------------------------------------------------ |
| `frames`     | `int` | The camera's frames, each a JPEG before and one H.264 frame after.                                           |
| `gop_frames` | `int` | Frames from one keyframe to the next: about a second of them at the rate the episode recorded the camera at. |
| `jpeg_bytes` | `int` | The camera's JPEGs' size before, added up, in bytes.                                                         |
| `h264_bytes` | `int` | The camera's H.264 frames' size after, added up, in bytes.                                                   |

#### frames

```
frames: int
```

The camera's frames, each a JPEG before and one H.264 frame after.

#### gop_frames

```
gop_frames: int
```

Frames from one keyframe to the next: about a second of them at the rate the episode recorded the camera at.

#### jpeg_bytes

```
jpeg_bytes: int
```

The camera's JPEGs' size before, added up, in bytes.

#### h264_bytes

```
h264_bytes: int
```

The camera's H.264 frames' size after, added up, in bytes.

### CompressStats

```
CompressStats(
    source_bytes: int,
    compressed_bytes: int,
    cameras: dict[str, CameraCompressed],
)
```

What `Compression.run` did: the file's size before and after, and each camera's.

Attributes:

| Name               | Type                          | Description                          |
| ------------------ | ----------------------------- | ------------------------------------ |
| `source_bytes`     | `int`                         | The episode's size before, in bytes. |
| `compressed_bytes` | `int`                         | The episode's size after, in bytes.  |
| `cameras`          | `dict[str, CameraCompressed]` | Each camera compressed, by name.     |

#### source_bytes

```
source_bytes: int
```

The episode's size before, in bytes.

#### compressed_bytes

```
compressed_bytes: int
```

The episode's size after, in bytes.

#### cameras

```
cameras: dict[str, CameraCompressed]
```

Each camera compressed, by name.

### Compression

```
Compression(
    path: Path,
    crf: int | None = None,
    preset: str | None = None,
)
```

A compression of a finished episode in place, every camera's JPEGs to H.264, lossy and for good.

`run` does it, blocking, and `cancel`, from any other thread, stops it. Nothing happens to the file until `run`. Running it needs `ffmpeg` on `PATH`.

Prepares the compression of the finished episode at `path`; nothing is read until `run`.

Parameters:

| Name     | Type          | Description                                                                                                                                       | Default    |
| -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `path`   | `Path`        | The episode's file.                                                                                                                               | *required* |
| `crf`    | `int \| None` | x264's constant rate factor, 0 to 51, lower keeping more; None is 23, x264's own default.                                                         | `None`     |
| `preset` | `str \| None` | x264's preset, "ultrafast" to "placebo", how much time it spends per frame for the size; None is "veryfast". A name x264 does not know fails run. | `None`     |

Raises:

| Type                   | Description                        |
| ---------------------- | ---------------------------------- |
| `TypeError`            | path is not a path, None included. |
| `InvalidArgumentError` | crf is negative or above 51.       |

Methods:

| Name     | Description                                                                                              |
| -------- | -------------------------------------------------------------------------------------------------------- |
| `run`    | Compresses the episode, blocking until done; the compressed file replaces it once written and read back. |
| `cancel` | Stops a run under way on another thread promptly, killing its encoders; a run after it does nothing.     |

Attributes:

| Name   | Type | Description                   |
| ------ | ---- | ----------------------------- |
| `path` |      | The episode's file, as given. |

#### path

```
path
```

The episode's file, as given.

#### run

```
run() -> CompressStats | None
```

Compresses the episode, blocking until done; the compressed file replaces it once written and read back.

It runs `ffmpeg`, one process per camera, and takes seconds to minutes. A failure or a cancel leaves the episode as it was. Never run it on an episode being recorded: one with no summary yet is refused, but only the caller knows whether a recording is about to finish into it.

Returns:

| Type                    | Description                                                                    |
| ----------------------- | ------------------------------------------------------------------------------ |
| `CompressStats \| None` | What the compression did; None once cancelled, at once for a run after cancel. |

Raises:

| Type             | Description                                                                                                                                                                                                                                                    |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NotFoundError`  | There is no file at path.                                                                                                                                                                                                                                      |
| `OperationError` | ffmpeg is not on PATH; the file is not a finished episode or has no JPEG camera, one already compressed included; <path>.compressing is already there; preset is not one x264 knows, or the encoder failed on a frame; or reading or writing the files failed. |

#### cancel

```
cancel() -> None
```

Stops a `run` under way on another thread promptly, killing its encoders; a `run` after it does nothing.

A `run` that has already replaced the file by then finishes as if it was not cancelled.

### Recorder

```
Recorder(
    workcell: WorkcellConfig,
    runtime: Runtime,
    cameras: Mapping[str, Camera] | None = None,
    config: RecorderConfig | None = None,
    readiness: Report | None = None,
)
```

Records episodes of a running workcell, one at a time, each into the next file of a `Session`.

`start` begins an episode and `stop` finishes it and returns what it counted; a recorder then records the next one the same way. Each episode reads the runtime and the cameras through readers of its own, on threads of its own, so the loop never waits on it. An episode's file is never another's: two recorders starting in one session at once get the next index each. An episode still recording when the recorder is garbage collected is finished then, labeled `Outcome.UNLABELED`.

`event`, `log`, `failed` and `stats` may be called from any thread, a `stop` on another one included.

Holds what to record; nothing is checked or opened until `start`.

Parameters:

| Name        | Type                           | Description                                                                                                                                                             | Default    |
| ----------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `workcell`  | `WorkcellConfig`               | The workcell the runtime runs, written into every episode.                                                                                                              | *required* |
| `runtime`   | `Runtime`                      | The anvil.control.Runtime to record.                                                                                                                                    | *required* |
| `cameras`   | `Mapping[str, Camera] \| None` | The cameras to record, by the names workcell gives them: an anvil.camera.Cameras, or any mapping of some of them; None records none.                                    | `None`     |
| `config`    | `RecorderConfig \| None`       | How to record; None is RecorderConfig.rated(), every source at its default rate.                                                                                        | `None`     |
| `readiness` | `Report \| None`               | The anvil.hw.Report the robot came up on, whose hardware, the physical robot it found, each episode records; None for a run with no readiness check, such as the sim's. | `None`     |

Methods:

| Name        | Description                                                                                           |
| ----------- | ----------------------------------------------------------------------------------------------------- |
| `recording` | Returns whether an episode is being recorded: from a start that returned until its stop is called.    |
| `start`     | Creates session's next episode and its layout, and records into it until stop; path says where.       |
| `event`     | Writes an event of the caller's into the episode being recorded, stamped now.                         |
| `annotate`  | Pins key and its value to now in the episode being recorded, as one of its annotations.               |
| `log`       | Writes message on topic, a channel of the caller's own, into the episode being recorded, stamped now. |
| `failed`    | Returns why the episode being recorded stopped being written before stop asked it to, once it has.    |
| `stats`     | Returns what the episode being recorded has counted so far, as of a moment ago.                       |
| `stop`      | Stops reading, writes what was read and finishes the file, blocking until it is written.              |

Attributes:

| Name        | Type           | Description                                                                        |
| ----------- | -------------- | ---------------------------------------------------------------------------------- |
| `workcell`  |                | The workcell recorded, as given.                                                   |
| `runtime`   |                | The runtime recorded, as given.                                                    |
| `cameras`   |                | The cameras recorded, by name.                                                     |
| `config`    |                | How each episode is recorded.                                                      |
| `readiness` |                | The readiness report each episode records the hardware of; None for none.          |
| `path`      | `Path \| None` | The file of the episode being recorded, <session>/<NNNN>.mcap; None while none is. |

#### workcell

```
workcell
```

The workcell recorded, as given.

#### runtime

```
runtime
```

The runtime recorded, as given.

#### cameras

```
cameras
```

The cameras recorded, by name.

#### config

```
config
```

How each episode is recorded.

#### readiness

```
readiness
```

The readiness report each episode records the hardware of; `None` for none.

#### path

```
path: Path | None
```

The file of the episode being recorded, `<session>/<NNNN>.mcap`; `None` while none is.

#### recording

```
recording() -> bool
```

Returns whether an episode is being recorded: from a `start` that returned until its `stop` is called.

#### start

```
start(session: Session) -> None
```

Creates `session`'s next episode and its layout, and records into it until `stop`; `path` says where.

Recording runs on threads of its own; this returns once it has started. A start that fails leaves the recorder idle, to start again; one that fails writing the episode's file removes it and its layout.

Parameters:

| Name      | Type      | Description                                                    | Default    |
| --------- | --------- | -------------------------------------------------------------- | ---------- |
| `session` | `Session` | The session to record the episode into; it stays the caller's. | *required* |

Raises:

| Type                   | Description                                                                                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | workcell is not an anvil.workcell.WorkcellConfig.                                                                                                                               |
| `InvalidArgumentError` | config is one the sources cannot meet, as RecorderConfig says; a camera is not one workcell names; or the robot's URDF does not compose. Each is refused before a file is made. |
| `NotFoundError`        | The session's directory is not there.                                                                                                                                           |
| `OperationError`       | An episode is being recorded already; the runtime or a camera is closed; or the episode's file or layout cannot be created or written.                                          |

#### event

```
event(
    source: str,
    kind: str,
    fields: Mapping[str, str] | None = None,
) -> None
```

Writes an event of the caller's into the episode being recorded, stamped now.

An event is something the loop cannot know, such as who drives the arms, written to the episode's `/events` channel with its fields in key order.

Parameters:

| Name     | Type                        | Description                                                             | Default    |
| -------- | --------------------------- | ----------------------------------------------------------------------- | ---------- |
| `source` | `str`                       | Who says it, e.g. "web".                                                | *required* |
| `kind`   | `str`                       | What happened, dotted, e.g. "driver.attached".                          | *required* |
| `fields` | `Mapping[str, str] \| None` | What it says, strings by name, e.g. {"driver": "quest"}; None for none. | `None`     |

Raises:

| Type                   | Description                                                                                                             |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A field's value is not a string.                                                                                        |
| `OperationError`       | Nothing is being recorded, a stop on another thread included; or the episode has stopped being written, as failed says. |

#### annotate

```
annotate(key: str, value: str) -> None
```

Pins `key` and its `value` to now in the episode being recorded, as one of its annotations.

The episode holds them once `stop` finishes it, in the order they were pinned, each at the log time of the call, and `annotations` reads them back as it does the ones `annotate` writes into a finished episode, which replaces them. Each is also written into `/events` as it is pinned, an `annotation` event from `recorder` with `key` and `value` fields, so it shows on the episode's timeline and is kept by an episode that never stops.

Parameters:

| Name    | Type  | Description                              | Default    |
| ------- | ----- | ---------------------------------------- | ---------- |
| `key`   | `str` | What is said, e.g. "phase"; never empty. | *required* |
| `value` | `str` | What it is, e.g. "grasp"; may be empty.  | *required* |

Raises:

| Type                   | Description                                                                                                             |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | key is empty.                                                                                                           |
| `OperationError`       | Nothing is being recorded, a stop on another thread included; or the episode has stopped being written, as failed says. |

#### log

```
log(topic: str, message: object) -> None
```

Writes `message` on `topic`, a channel of the caller's own, into the episode being recorded, stamped now.

For what a part above the loop saw, such as a headset's raw input; it is written in every mode, as it comes.

Parameters:

| Name      | Type     | Description                                                                                                                                                                     | Default    |
| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `topic`   | `str`    | The channel, / and a name, e.g. "/quest/input"; never under one the recorder writes itself, /robot/, /controller/ or /camera/, nor /events, /observation or /robot_description. | *required* |
| `message` | `object` | The message, anything json.dumps takes, written as that JSON.                                                                                                                   | *required* |

Raises:

| Type                   | Description                                                                                                             |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | json.dumps cannot encode message.                                                                                       |
| `InvalidArgumentError` | topic is not / and a name, or is one the recorder writes itself.                                                        |
| `OperationError`       | Nothing is being recorded, a stop on another thread included; or the episode has stopped being written, as failed says. |

#### failed

```
failed() -> str | None
```

Returns why the episode being recorded stopped being written before `stop` asked it to, once it has.

The disk filling is one reason. Nothing more goes into the episode from then, what was written before stays, and `stop` raises saying the same.

Returns:

| Type          | Description                                                   |
| ------------- | ------------------------------------------------------------- |
| `str \| None` | Why it stopped being written; None while it is being written. |

Raises:

| Type             | Description                |
| ---------------- | -------------------------- |
| `OperationError` | Nothing is being recorded. |

#### stats

```
stats() -> RecorderStats
```

Returns what the episode being recorded has counted so far, as of a moment ago.

`messages_written` stays 0 until `stop` finishes the file.

Raises:

| Type             | Description                |
| ---------------- | -------------------------- |
| `OperationError` | Nothing is being recorded. |

#### stop

```
stop(
    outcome: Outcome = UNLABELED, note: str | None = None
) -> RecorderStats
```

Stops reading, writes what was read and finishes the file, blocking until it is written.

How the episode went goes in the file's `anvil.outcome` metadata, and what it counted in its `anvil.stats`. The recorder is idle afterwards, whatever this raises, ready to `start` the next episode.

Parameters:

| Name      | Type          | Description                                     | Default     |
| --------- | ------------- | ----------------------------------------------- | ----------- |
| `outcome` | `Outcome`     | How the episode went; label changes it later.   | `UNLABELED` |
| `note`    | `str \| None` | Why, in the operator's words; None for no note. | `None`      |

Returns:

| Type            | Description               |
| --------------- | ------------------------- |
| `RecorderStats` | What the episode counted. |

Raises:

| Type             | Description                                                                                             |
| ---------------- | ------------------------------------------------------------------------------------------------------- |
| `OperationError` | Nothing is being recorded, or writing the file failed, saying why; what was written before stays in it. |

### VideoCodec

Bases: `StrEnum`

Codec a dataset's videos are encoded with.

Attributes:

| Name   | Type | Description                                                          |
| ------ | ---- | -------------------------------------------------------------------- |
| `AV1`  |      | AV1 through SVT-AV1, LeRobot's default: small files, slow to encode. |
| `H264` |      | H.264 through x264: larger files, several times faster to encode.    |

#### AV1

```
AV1
```

AV1 through SVT-AV1, LeRobot's default: small files, slow to encode.

#### H264

```
H264
```

H.264 through x264: larger files, several times faster to encode.

### LeRobotExport

```
LeRobotExport(
    path: Path,
    num_episodes: int,
    num_frames: int,
    fps: int,
    cameras: list[str],
    tasks: list[str],
)
```

What `DatasetExport.run` wrote.

Attributes:

| Name           | Type        | Description                                                                                   |
| -------------- | ----------- | --------------------------------------------------------------------------------------------- |
| `path`         | `Path`      | Dataset's root directory, what LeRobot's LeRobotDataset(root=...) is given.                   |
| `num_episodes` | `int`       | Number of episodes written, one per episode given.                                            |
| `num_frames`   | `int`       | Number of frames written across every episode.                                                |
| `fps`          | `int`       | Dataset's frame rate, in frames per second: the anchor camera's, as its episodes recorded it. |
| `cameras`      | `list[str]` | Each camera's name, in feature order; its video is the feature observation.images.<camera>.   |
| `tasks`        | `list[str]` | Each distinct task, at its task index.                                                        |

#### path

```
path: Path
```

Dataset's root directory, what LeRobot's `LeRobotDataset(root=...)` is given.

#### num_episodes

```
num_episodes: int
```

Number of episodes written, one per episode given.

#### num_frames

```
num_frames: int
```

Number of frames written across every episode.

#### fps

```
fps: int
```

Dataset's frame rate, in frames per second: the anchor camera's, as its episodes recorded it.

#### cameras

```
cameras: list[str]
```

Each camera's name, in feature order; its video is the feature `observation.images.<camera>`.

#### tasks

```
tasks: list[str]
```

Each distinct task, at its task index.

### DatasetExport

```
DatasetExport(
    episodes: Sequence[Path],
    out_dir: Path,
    anchor_camera: str | None = None,
    codec: VideoCodec | None = None,
    fallback_task: str | None = None,
)
```

An export of recorded episodes into a new LeRobot dataset.

`run` does it, blocking for minutes while it encodes video, and `cancel`, from any other thread, stops it. Nothing is read or written until `run`. Running it needs `ffmpeg` on `PATH`.

Prepares the export of `episodes` into `out_dir`; nothing is read until `run`.

Parameters:

| Name            | Type                 | Description                                                                                                                                                                                       | Default    |
| --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `episodes`      | `Sequence[Path]`     | Paths of the episodes, in the order the dataset numbers them.                                                                                                                                     | *required* |
| `out_dir`       | `Path`               | Directory to write the dataset into; it must not exist. Its missing parent directories are created, and stay on a failure.                                                                        | *required* |
| `anchor_camera` | `str \| None`        | Name of the camera whose frames the dataset's are, and whose recorded rate is the dataset's; it must have been recorded at a whole number of frames per second. None is the first camera by name. | `None`     |
| `codec`         | `VideoCodec \| None` | Codec to encode every video with; None is VideoCodec.AV1.                                                                                                                                         | `None`     |
| `fallback_task` | `str \| None`        | Task of an episode whose session named none (anvil.recorder.Session's task); None is "unspecified".                                                                                               | `None`     |

Raises:

| Type                   | Description                                                                                             |
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
| `TypeError`            | episodes is one path rather than a list of them, or out_dir or an episode is not a path, None included. |
| `InvalidArgumentError` | codec is not one of VideoCodec.                                                                         |

Methods:

| Name     | Description                                                                              |
| -------- | ---------------------------------------------------------------------------------------- |
| `run`    | Writes the dataset, blocking until done; a failure or a cancel removes out_dir again.    |
| `cancel` | Stops a run under way on another thread at the next record it reads, killing its ffmpeg. |

Attributes:

| Name      | Type | Description                                          |
| --------- | ---- | ---------------------------------------------------- |
| `out_dir` |      | The directory the dataset is written into, as given. |

#### out_dir

```
out_dir
```

The directory the dataset is written into, as given.

#### run

```
run() -> LeRobotExport | None
```

Writes the dataset, blocking until done; a failure or a cancel removes `out_dir` again.

The episodes are MCAP files as the recorder writes them, compressed or not, in any mix, and must be one workcell's: episodes whose workcell, slots, cameras or frame rate differ are refused, naming the first difference. There is one dataset frame per period of the anchor camera's recorded rate, each the anchor's frame nearest it and every other source's latest sample at or before that frame, so a camera that delivers fewer frames than its rate repeats one rather than playing back fast. Frames before every source has a sample are left out, and so is any frame where the loop commanded no position, as when it holds the arms limp while idle, so `LeRobotExport.num_frames` can be far fewer than the frames recorded. Each camera is encoded on a thread of its own, by an ffmpeg at the lowest priority. `out_dir` is created before any episode is read, so one already there is refused at once.

Returns:

| Type                    | Description                                                                                                              |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `LeRobotExport \| None` | What was written, with LeRobotExport.path ready to open in LeRobot; None once cancelled, at once for a run after cancel. |

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                     |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | episodes is empty, or out_dir is an empty path.                                                                                                                                                                                                                                                                 |
| `NotFoundError`        | An episode is not there, or ffmpeg is not on PATH.                                                                                                                                                                                                                                                              |
| `OperationError`       | An episode cannot be read, holds nothing yet, as one still being recorded does, or differs from the first in workcell, slots, cameras or frame rate; anchor_camera names a camera an episode lacks, or one recorded at a rate that is not a whole number; out_dir exists or cannot be created; or ffmpeg fails. |

#### cancel

```
cancel() -> None
```

Stops a `run` under way on another thread at the next record it reads, killing its ffmpeg.

A `run` after it does nothing.

### sessions

```
sessions(root: Path) -> list[SessionSummary]
```

Lists every session under the data root `root`, the most recently written first.

Every directory under `root` whose name a session could have is a session; anything else there, a hidden directory among them, is passed over.

Parameters:

| Name   | Type   | Description    | Default    |
| ------ | ------ | -------------- | ---------- |
| `root` | `Path` | The data root. | *required* |

Returns:

| Type                   | Description                                                                                           |
| ---------------------- | ----------------------------------------------------------------------------------------------------- |
| `list[SessionSummary]` | A summary of each session, the most recently written first; empty for a root that does not exist yet. |

Raises:

| Type                   | Description                                                     |
| ---------------------- | --------------------------------------------------------------- |
| `TypeError`            | root is not a path, None included.                              |
| `InvalidArgumentError` | A session's session.json cannot be parsed.                      |
| `OperationError`       | root, a session's directory or its session.json cannot be read. |

### layout_path

```
layout_path(episode: Path) -> Path
```

Returns where the Foxglove layout that draws `episode` is written, beside it; whether either exists is not asked.

Foxglove opens the episode with this layout to draw its robot, cameras and joints.

Parameters:

| Name      | Type   | Description                                | Default    |
| --------- | ------ | ------------------------------------------ | ---------- |
| `episode` | `Path` | The episode's file, <session>/<NNNN>.mcap. | *required* |

Returns:

| Type   | Description                                               |
| ------ | --------------------------------------------------------- |
| `Path` | The layout's path, <session>/<NNNN>.foxglove-layout.json. |

Raises:

| Type                   | Description                                               |
| ---------------------- | --------------------------------------------------------- |
| `TypeError`            | episode is not a path, None included.                     |
| `InvalidArgumentError` | episode names no file, as Path(""), which is ., does not. |

### annotations

```
annotations(episode: Path) -> list[Annotation]
```

Reads what was said about the finished `episode`: its newest annotations, as `annotate` last wrote them.

Parameters:

| Name      | Type   | Description         | Default    |
| --------- | ------ | ------------------- | ---------- |
| `episode` | `Path` | The episode's file. | *required* |

Returns:

| Type               | Description                                    |
| ------------------ | ---------------------------------------------- |
| `list[Annotation]` | The annotations; empty where nothing was said. |

Raises:

| Type                   | Description                                                            |
| ---------------------- | ---------------------------------------------------------------------- |
| `TypeError`            | episode is not a path, None included.                                  |
| `InvalidArgumentError` | The file is not a finished episode, or its annotations cannot be read. |
| `NotFoundError`        | There is no file at episode.                                           |
| `OperationError`       | The file cannot be opened.                                             |

### annotate

```
annotate(
    episode: Path, annotations: Sequence[Annotation]
) -> None
```

Replaces the finished `episode`'s annotations with `annotations`, in place, keeping the ones before as history.

It appends to the file, so it takes as long on an episode of any size. Never call it on an episode being recorded or compressed: one with no summary yet is refused, but only the caller knows whether a recording or a compression is about to finish into it. A failure leaves the file as it was, except one partway through writing, which leaves its messages whole and its summary cut; `mcap recover` rebuilds it.

Parameters:

| Name          | Type                   | Description                                              | Default    |
| ------------- | ---------------------- | -------------------------------------------------------- | ---------- |
| `episode`     | `Path`                 | The episode's file.                                      | *required* |
| `annotations` | `Sequence[Annotation]` | The annotations it holds from now on; empty clears them. | *required* |

Raises:

| Type                   | Description                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| `TypeError`            | episode is not a path, None included.                                                                     |
| `InvalidArgumentError` | An annotation has an empty key, or the file is not a finished episode, one still being recorded included. |
| `NotFoundError`        | There is no file at episode.                                                                              |
| `OperationError`       | The file cannot be read or written.                                                                       |

### label

```
label(
    episode: Path, outcome: Outcome, note: str | None = None
) -> None
```

Relabels how the finished `episode` went, in place, keeping the labels before as history.

The new label is a new `anvil.outcome` record, the one `Recorder.stop` wrote and any since kept behind it. As with `annotate`, it takes as long on an episode of any size, and is never for an episode being recorded or compressed.

Parameters:

| Name      | Type          | Description                                     | Default    |
| --------- | ------------- | ----------------------------------------------- | ---------- |
| `episode` | `Path`        | The episode's file.                             | *required* |
| `outcome` | `Outcome`     | How it went.                                    | *required* |
| `note`    | `str \| None` | Why, in the operator's words; None for no note. | `None`     |

Raises:

| Type                   | Description                                                            |
| ---------------------- | ---------------------------------------------------------------------- |
| `TypeError`            | episode is not a path, None included.                                  |
| `InvalidArgumentError` | The file is not a finished episode, one still being recorded included. |
| `NotFoundError`        | There is no file at episode.                                           |
| `OperationError`       | The file cannot be read or written.                                    |

### mesh

```
mesh(filename: str) -> bytes
```

Returns the bytes of a mesh a composed URDF names, from the copy shipped in the package.

So drawing the robot, or carrying a copy of it off the box, needs no description tree.

Parameters:

| Name       | Type  | Description                                                                                                                | Default    |
| ---------- | ----- | -------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `filename` | `str` | The mesh's filename as a composed URDF gives it, its path in robot-descriptions/, e.g. "arms/openarm/v2/meshes/link1.stl". | *required* |

Returns:

| Type    | Description               |
| ------- | ------------------------- |
| `bytes` | The mesh file's contents. |

Raises:

| Type                   | Description                            |
| ---------------------- | -------------------------------------- |
| `InvalidArgumentError` | No shipped URDF names a mesh filename. |

# anvil.sim

## anvil.sim

The sim: MuJoCo standing in for the drives, a robot that collides with itself and with the scene it stands in.

A `World` is the scene and the clock, served by a process of its own on this machine or another; an `anvil.hw.SimHw` drives the robot in it as an `anvil.hw.DamiaoHw` drives real drives, and an `anvil.camera.SimCamera` watches it. `anvil-robotics sim` serves a world on a port, for a loop on another machine to drive. Importing this module loads no MuJoCo: the physics runs in the server.

Classes:

| Name       | Description                                                                                 |
| ---------- | ------------------------------------------------------------------------------------------- |
| `Contact`  | Two geoms of the world touching.                                                            |
| `Contacts` | What a world is touching, and what it has touched since it was set up, from World.contacts. |
| `World`    | A sim's world: a robot standing in a scene, and the clock it runs on.                       |

### Contact

```
Contact(first: str, second: str, depth_m: float)
```

Two geoms of the world touching.

Attributes:

| Name      | Type    | Description                                                                              |
| --------- | ------- | ---------------------------------------------------------------------------------------- |
| `first`   | `str`   | One of them, by the name its scene or the robot's URDF gives it, e.g. "table_top".       |
| `second`  | `str`   | The other.                                                                               |
| `depth_m` | `float` | How far apart they are, in meters: negative is how deep they are pushed into each other. |

#### first

```
first: str
```

One of them, by the name its scene or the robot's URDF gives it, e.g. `"table_top"`.

#### second

```
second: str
```

The other.

#### depth_m

```
depth_m: float
```

How far apart they are, in meters: negative is how deep they are pushed into each other.

### Contacts

```
Contacts(
    now: list[Contact],
    touched: set[tuple[str, str]],
    worst_depth_m: float,
)
```

What a world is touching, and what it has touched since it was set up, from `World.contacts`.

Attributes:

| Name            | Type                   | Description                                                                                            |
| --------------- | ---------------------- | ------------------------------------------------------------------------------------------------------ |
| `now`           | `list[Contact]`        | Every pair touching at the instant the question was asked.                                             |
| `touched`       | `set[tuple[str, str]]` | Every pair of geoms that has touched, whether or not it still is, as (first, second).                  |
| `worst_depth_m` | `float`                | The deepest any two have been pushed into each other, in meters: negative, or 0 where nothing touched. |

#### now

```
now: list[Contact]
```

Every pair touching at the instant the question was asked.

#### touched

```
touched: set[tuple[str, str]]
```

Every pair of geoms that has touched, whether or not it still is, as `(first, second)`.

#### worst_depth_m

```
worst_depth_m: float
```

The deepest any two have been pushed into each other, in meters: negative, or 0 where nothing touched.

### World

```
World(
    robot: RobotConfig,
    scene: str | None = None,
    lockstep_hz: int | None = None,
    viewer: bool = False,
    address: str | None = None,
)
```

Bases: `Handle`

A sim's world: a robot standing in a scene, and the clock it runs on.

Without an `address` it starts a server on this machine, `anvil-robotics sim` in a process of its own. With one it sets up the world of a server already running, as often as not on another machine: the sim stands in for metal, so the box that would drive the metal need not also simulate it, and the window is nicer on the machine you are sitting at.

The robot stands in the world from the start with its drives disabled, as an arm stands on the bench switched off, so an `anvil.camera.SimCamera` watches it before anything drives it; an `anvil.hw.SimHw` then takes it over and drives it. This answers what is touching and places loose objects, beside whatever drives it. Another robot is another world. Closing it, or letting it be collected, ends this connection; a server it started stops once every `anvil.hw.SimHw` driving it is gone too. Every method but `close` raises `anvil.OperationError` once it is closed.

Sets the world up with `robot` standing in it, starting a server for it unless `address` names one.

Whatever world the server held goes, and its cameras are told so.

Parameters:

| Name          | Type          | Description                                                                                                                                                                                                                                                                                                                                                                                             | Default    |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `robot`       | `RobotConfig` | The robot standing in the world, e.g. anvil.workcell.WorkcellConfig.OPENYAM_V1_SINGLE.robot: its grippers grafted on, and every motor it declares, each saturating at its model's maxima. Its meshes are the ones the package ships, so only a robot Anvil ships stands in a world.                                                                                                                     | *required* |
| `scene`       | `str \| None` | The world the robot is bolted into: "empty", "table", "two_tables", "lift" or "lift_bracket" as shipped, or a path to an MJCF file the server can read, wherever the server is, with a <frame name="robot_mount"> where the robot stands. None is the scene of the robot's anvil.workcell.RobotConfig.stand, named as it is, or for a robot on no stand the server's own, "empty" for one started here. | `None`     |
| `lockstep_hz` | `int \| None` | The rate to step at, 250 or 500, giving up the sim's own clock for repeatability: the world then steps once per anvil.hw.SimHw.write and not otherwise, and an anvil.control.Runtime driving it runs at that rate. None leaves the world keeping its own clock.                                                                                                                                         | `None`     |
| `viewer`      | `bool`        | Whether to open a window on the world. Only for a server started here.                                                                                                                                                                                                                                                                                                                                  | `False`    |
| `address`     | `str \| None` | A server already listening, "host:port", or None to start one here.                                                                                                                                                                                                                                                                                                                                     | `None`     |

Raises:

| Type                   | Description                                                                                                                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `TypeError`            | robot is not an anvil.workcell.RobotConfig.                                                                                                                                                                                                            |
| `InvalidArgumentError` | lockstep_hz is not one of the two rates, viewer is given with an address, address does not read as "host:port", or robot's URDF cannot be composed.                                                                                                    |
| `UnreachableError`     | Nothing answered at address.                                                                                                                                                                                                                           |
| `OperationError`       | The server would not start, or it refused the world: a scene it does not have, a robot it cannot build, or an anvil.hw.SimHw driving it; or it is from another release of anvil, which the message names, since a sim and its clients run one release. |

Methods:

| Name          | Description                                                                                          |
| ------------- | ---------------------------------------------------------------------------------------------------- |
| `close`       | Ends this connection; a server it started stops once every anvil.hw.SimHw driving it is gone too.    |
| `contacts`    | Returns what the world is touching, and what it has touched since it was set up.                     |
| `place`       | Puts a loose body of the scene, a block on the table, where a run wants it, and stops it dead there. |
| `hold`        | Holds each named joint of the robot at its angle, at rest, whatever drives it, until told again.     |
| `show_window` | Opens MuJoCo's window on the world, unless one is open already.                                      |

Attributes:

| Name      | Type          | Description                                                                                   |
| --------- | ------------- | --------------------------------------------------------------------------------------------- |
| `address` | `str`         | Where the server is, "host:port": what an anvil.hw.SimHw and an anvil.camera.SimCamera reach. |
| `robot`   | `RobotConfig` | The robot standing in the world.                                                              |

#### address

```
address: str
```

Where the server is, `"host:port"`: what an `anvil.hw.SimHw` and an `anvil.camera.SimCamera` reach.

#### robot

```
robot: RobotConfig
```

The robot standing in the world.

#### close

```
close() -> None
```

Ends this connection; a server it started stops once every `anvil.hw.SimHw` driving it is gone too.

Closing twice does nothing.

#### contacts

```
contacts() -> Contacts
```

Returns what the world is touching, and what it has touched since it was set up.

The world looks every physics step, so a touch between two of these questions is still reported, which is how "nothing collided during the whole sweep" is asked. A round trip to the server, never a tick.

Raises:

| Type             | Description           |
| ---------------- | --------------------- |
| `OperationError` | The server went away. |

#### place

```
place(
    body: str,
    position_m: Sequence[float],
    orientation_xyzw: Sequence[float] | None = None,
) -> None
```

Puts a loose body of the scene, a block on the table, where a run wants it, and stops it dead there.

Parameters:

| Name               | Type                      | Description                                                                         | Default    |
| ------------------ | ------------------------- | ----------------------------------------------------------------------------------- | ---------- |
| `body`             | `str`                     | The body's name in the scene.                                                       | *required* |
| `position_m`       | `Sequence[float]`         | Where to put it, x, y and z in the world frame, in meters.                          | *required* |
| `orientation_xyzw` | `Sequence[float] \| None` | How to turn it, a unit quaternion with w last, or None to leave it turned as it is. | `None`     |

Raises:

| Type                   | Description                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | position_m does not have 3 entries or orientation_xyzw does not have 4.                         |
| `OperationError`       | body is not one of the scene's loose objects, which the message lists, or the server went away. |

#### hold

```
hold(q_rad: Mapping[str, float]) -> None
```

Holds each named joint of the robot at its angle, at rest, whatever drives it, until told again.

A hand holding the arm still, and moving it as a hand would when called again with the next angles: how a run moves an arm nothing drives, such as a leader the operator would move by hand, which an `anvil.control.LeaderFollowerController` leaves limp. The joints read back exactly these angles, still. Each call replaces the last, so a joint it does not name goes back to its drive.

Parameters:

| Name    | Type                  | Description                                                                                                                                                                        | Default    |
| ------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `q_rad` | `Mapping[str, float]` | Angles in radians, or meters for a prismatic joint, by URDF joint name as anvil.workcell.RobotConfig.urdf_joints names them, e.g. {"leader_joint_1": 0.3}; {} lets every joint go. | *required* |

Raises:

| Type                   | Description                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | q_rad holds something other than a number.                                                                |
| `OperationError`       | q_rad names a joint the robot has no hinge or slide of, which the message names, or the server went away. |

#### show_window

```
show_window() -> None
```

Opens MuJoCo's window on the world, unless one is open already.

The window opens on the machine the server runs on, which for a world started here is this one: orbit, pan and zoom with the mouse, and pick a body with a double-click to push it with Ctrl and a drag. Closing it leaves the world running. Unlike the `viewer` argument, this works on a world already set up, and on a server something else started.

Raises:

| Type             | Description                                                                                               |
| ---------------- | --------------------------------------------------------------------------------------------------------- |
| `OperationError` | The server's machine has no display to open a window on, which the message says, or the server went away. |

# anvil.workcell

## anvil.workcell

What a workcell is: its robot's arms, where each answers and which slots it owns, its cameras and keep-out boxes.

The configuration here is plain dataclasses, so an edit is an assignment: `arm.description.tuning.ee_pose.joints[3].kp = 20.0` is what that arm then runs under the `ee_pose` controller. Start from a shipped preset, `WorkcellConfig.OPENYAM_V1_SINGLE` or `WorkcellConfig.shipped(...)`, edit what your bench differs in, and `save` it.

A `RobotConfig` and a `WorkcellConfig` are checked whole when they are built. An edit made afterwards, to them or to anything they hold, is checked where the robot or workcell is next handed over: to a hw interface, a controller, a `Clearance`, `save`, or a property such as `RobotConfig.num_slots` that has to work something out from it. A value refused there raises `anvil.InvalidArgumentError` saying what is wrong with it, and nothing reaches the drives.

The release wheel drives only the arms Anvil ships: a robot whose arms' joints or `excluded_pairs` differ from the shipped description of their variant, or whose URDF is not the one that variant ships, is refused when it is built, the error naming what differs. Tuning, a joint's gravity correction among it, homing, the gripper, the mounts, the cameras and the keep-out boxes stay yours to change.

Classes:

| Name                | Description                                                                                                      |
| ------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `Correction`        | A correction to one feedforward term of one joint: the term as computed, times scale, plus offset_nm.            |
| `JointDescription`  | One joint of an arm: the motor driving it, the addresses it answers on, and how far and how fast it may go.      |
| `GripAssist`        | The grip assist: the feedforward torque that does the gripping, on top of the gripper's own gains.               |
| `FingerSet`         | A set of interchangeable fingers a gripper carries, one directory under the gripper's fingers/.                  |
| `Gripper`           | A gripper the stack drives: its one motor, the gains it holds a width at, and the map from jaw opening to angle. |
| `ExternalGripper`   | A gripper the stack does not drive: your own, run by your code on the arm's bus beside the stack.                |
| `Gains`             | A drive's two gains for one joint.                                                                               |
| `ControllerTuning`  | How one controller drives an arm's chain joints: their gains and how fast it moves their targets.                |
| `TuningDescription` | How each controller drives an arm's chain joints, as the tuning.yaml beside its arm.yaml states it.              |
| `ArmDescription`    | One arm variant: what it physically is and how it is driven.                                                     |
| `Side`              | Which arm of a robot an arm is, where that means something.                                                      |
| `ArmCount`          | How many followers a shipped workcell stands: one, or a left and a right.                                        |
| `Role`              | What an arm does in a robot.                                                                                     |
| `ArmLayout`         | Where one arm of a robot sits in the robot's slots, as RobotConfig.layout worked it out.                         |
| `RobotLayout`       | What follows from a robot's arms rather than being stated with them, as RobotConfig.layout worked it out.        |
| `ArmConfig`         | One arm of a robot: its name, the bus it answers on, what it does, which side it is, and its description.        |
| `Mount`             | Where a robot stands one copy of its variant's unit: an OpenArm torso carrying both arms, or one yam arm.        |
| `Stand`             | What a robot's units stand on, as Anvil's CAD draws it.                                                          |
| `TimedWaypoint`     | One point of a TimedWaypoints: where every slot is at one time, and how fast it moves through there.             |
| `TimedWaypoints`    | Timed waypoints over every slot of a robot: a run the robot plans, such as its homing.                           |
| `UrdfJoint`         | One moving joint of a robot's composed URDF and the slot it follows, as RobotConfig.urdf_joints lists it.        |
| `RobotConfig`       | The robot one run drives: its arms in slot order, the URDF they stand by, and where each copy of it is mounted.  |
| `NearestPair`       | Two links of a robot checked against each other and their gap, from Clearance.nearest or within.                 |
| `NearestLink`       | A link of the robot and its gap to a keep-out box, from Clearance.nearest_to or within_of.                       |
| `KeepOutBox`        | A box in the workcell the robot is kept out of: a table, a wall, a fixture.                                      |
| `Clearance`         | How near a robot's links come to one another and to keep-out boxes, posed from its motors' positions.            |
| `CameraRole`        | What a camera looks at.                                                                                          |
| `Lens`              | The lens fitted to a camera, which cannot be read off the camera, so a workcell says which.                      |
| `CameraConfig`      | One camera of a workcell: its name, the device it opens, the mode it streams, and where it is.                   |
| `Preset`            | A shipped workcell's name and shape, from WorkcellConfig.preset_shapes, building nothing until asked.            |
| `WorkcellConfig`    | A workcell: its robot, the cameras it streams, and the boxes its robot is kept out of.                           |

Functions:

| Name                  | Description                                                                                                      |
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `shipped_urdf`        | Returns the URDF text a shipped variant carries: the unit a robot's mounts stand copies of.                      |
| `realhand_gripper`    | Returns the external gripper an arm states for the RealHand L6 bolted to it, as the hand's description gives it. |
| `shipped_finger_sets` | Returns every finger set Anvil ships for gripper, in name order: what its Gripper.fingers may be set to.         |

### Correction

```
Correction(scale: float = 1.0, offset_nm: float = 0.0)
```

A correction to one feedforward term of one joint: the term as computed, times `scale`, plus `offset_nm`.

A model off by a factor or by a constant torque is corrected here; the defaults send the term as computed. Checked where the arm is next handed over: a `scale` that is negative or not a number, or an `offset_nm` that is not a number, is refused there.

Attributes:

| Name        | Type    | Description                                                           |
| ----------- | ------- | --------------------------------------------------------------------- |
| `scale`     | `float` | What the term is multiplied by: 1 sends it as computed, 0 none of it. |
| `offset_nm` | `float` | What is added after scaling, in newton meters.                        |

#### scale

```
scale: float = 1.0
```

What the term is multiplied by: 1 sends it as computed, 0 none of it.

#### offset_nm

```
offset_nm: float = 0.0
```

What is added after scaling, in newton meters.

### JointDescription

```
JointDescription(
    name: str,
    motor: str,
    model: str,
    slave_id: int,
    master_id: int,
    limits_deg: tuple[float, float],
    max_dps: float | None = None,
    gravity: Correction = Correction(
        scale=1.0, offset_nm=0.0
    ),
    rest_pose_deg: float = 0.0,
    reflected: bool = False,
)
```

One joint of an arm: the motor driving it, the addresses it answers on, and how far and how fast it may go.

How hard a controller drives the joint is that controller's section of the arm's `ArmDescription.tuning`. Every field is checked where the arm is next handed over. In the release wheel the joints of a robot must be exactly those of the shipped description of its variant, so an edit to any field here is refused there.

Attributes:

| Name            | Type                  | Description                                                                                    |
| --------------- | --------------------- | ---------------------------------------------------------------------------------------------- |
| `name`          | `str`                 | The joint's name, e.g. joint_1; a gripper's motor is gripper.                                  |
| `motor`         | `str`                 | The assembly-side motor name, J<number>, e.g. J5: what the bench tools print.                  |
| `model`         | `str`                 | The Damiao motor model driving it, as the descriptions name it, e.g. DM8009.                   |
| `slave_id`      | `int`                 | The CAN address the motor listens on, 0 to 255.                                                |
| `master_id`     | `int`                 | The CAN address the motor replies on, 0 to 255.                                                |
| `limits_deg`    | `tuple[float, float]` | Its travel as (min, max), in degrees, in the joint's own frame.                                |
| `max_dps`       | `float \| None`       | The fastest its target may move, in degrees per second; None leaves its speed unguarded.       |
| `gravity`       | `Correction`          | The correction applied to the gravity feedforward torque; as computed unless stated.           |
| `rest_pose_deg` | `float`               | The posture IK pulls the joint toward, in degrees, in the joint's own frame.                   |
| `reflected`     | `bool`                | Whether the joint's direction negates between the left and right arms of a mirror-image build. |

#### name

```
name: str
```

The joint's name, e.g. `joint_1`; a gripper's motor is `gripper`.

#### motor

```
motor: str
```

The assembly-side motor name, `J<number>`, e.g. `J5`: what the bench tools print.

Any other form is refused.

#### model

```
model: str
```

The Damiao motor model driving it, as the descriptions name it, e.g. `DM8009`.

One this build does not know is refused.

#### slave_id

```
slave_id: int
```

The CAN address the motor listens on, 0 to 255.

#### master_id

```
master_id: int
```

The CAN address the motor replies on, 0 to 255.

#### limits_deg

```
limits_deg: tuple[float, float]
```

Its travel as `(min, max)`, in degrees, in the joint's own frame.

A `reflected` joint negates it on the right arm.

#### max_dps

```
max_dps: float | None = None
```

The fastest its target may move, in degrees per second; `None` leaves its speed unguarded.

Each controller's `ControllerTuning.max_dps` for the joint may not exceed it.

#### gravity

```
gravity: Correction = Correction(scale=1.0, offset_nm=0.0)
```

The correction applied to the gravity feedforward torque; as computed unless stated.

#### rest_pose_deg

```
rest_pose_deg: float = 0.0
```

The posture IK pulls the joint toward, in degrees, in the joint's own frame.

A `reflected` joint negates it on the right arm.

#### reflected

```
reflected: bool = False
```

Whether the joint's direction negates between the left and right arms of a mirror-image build.

### GripAssist

```
GripAssist(
    max_torque: float = 0.0,
    stiffness: float = 0.0,
    opening_scale: float = 1.0,
    shut_torque: float = 0.0,
)
```

The grip assist: the feedforward torque that does the gripping, on top of the gripper's own gains.

Each tick, with `x` the jaw-opening error in meters, measured minus commanded, closing pulls `max_torque * tanh(stiffness * x / max_torque)` toward shut, and opening pulls `opening_scale` times the same curve toward open. Near the target that is `stiffness * x`; past the knee, `1.5 * max_torque / stiffness` meters of error, it sits at `max_torque`. A `max_torque` of 0 turns the assist off, leaving the gripper's own gains to move the jaws: where `kp` is a few per cent of the assist, as on the Anvil gripper, they creep, at the pace `kp` against `kd` sets. Every value must be a number of zero or more, checked where the arm is next handed over.

Attributes:

| Name            | Type    | Description                                                                      |
| --------------- | ------- | -------------------------------------------------------------------------------- |
| `max_torque`    | `float` | The torque the curve saturates at, in newton meters.                             |
| `stiffness`     | `float` | The curve's slope at zero error, in newton meters per meter of jaw opening.      |
| `opening_scale` | `float` | Opening as a fraction of closing, torque, speed and slope together.              |
| `shut_torque`   | `float` | The least closing torque while the command asks the jaws shut, in newton meters. |

#### max_torque

```
max_torque: float = 0.0
```

The torque the curve saturates at, in newton meters.

How hard the gripper squeezes, and so, against the drive's `Gripper.kd`, how fast the jaws close.

#### stiffness

```
stiffness: float = 0.0
```

The curve's slope at zero error, in newton meters per meter of jaw opening.

How sharply the jaws stop on an object. Higher holds a width more tightly and hunts sooner; a gripper that hunts wants less.

#### opening_scale

```
opening_scale: float = 1.0
```

Opening as a fraction of closing, torque, speed and slope together.

1 is the same both ways, 0.5 releases at half speed, 0 turns the opening assist off.

#### shut_torque

```
shut_torque: float = 0.0
```

The least closing torque while the command asks the jaws shut, in newton meters.

So a gripper closing on nothing holds itself shut.

### FingerSet

```
FingerSet(name: str, urdf: str | None = None)
```

A set of interchangeable fingers a gripper carries, one directory under the gripper's `fingers/`.

`shipped_finger_sets` lists the ones Anvil ships for a gripper; set one as a `Gripper.fingers` to swap to it.

Attributes:

| Name   | Type          | Description                                                                                                |
| ------ | ------------- | ---------------------------------------------------------------------------------------------------------- |
| `name` | `str`         | The set's name, its directory under fingers/, e.g. fine_tip.                                               |
| `urdf` | `str \| None` | The URDF fragment RobotConfig.composed_urdf lays over the gripper's own; None for the set shipped as name. |

#### name

```
name: str
```

The set's name, its directory under `fingers/`, e.g. `fine_tip`.

#### urdf

```
urdf: str | None = None
```

The URDF fragment `RobotConfig.composed_urdf` lays over the gripper's own; `None` for the set shipped as `name`.

The printed finger on each carrier and the tool frame their tips set.

### Gripper

```
Gripper(
    family: str,
    version: int,
    joint: JointDescription,
    kp: float,
    kd: float,
    closed_deg: float,
    open_deg: float,
    opening_m: float,
    urdf: str | None = None,
    fingers: FingerSet | None = None,
    grip_assist: GripAssist = GripAssist(
        max_torque=0.0,
        stiffness=0.0,
        opening_scale=1.0,
        shut_torque=0.0,
    ),
    mass_kg: float = 0.0,
    com_m: tuple[float, float, float] = (0.0, 0.0, 0.0),
    inertia_kgm2: tuple[
        float, float, float, float, float, float
    ] = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
    alternate_motor: str | None = None,
)
```

A gripper the stack drives: its one motor, the gains it holds a width at, and the map from jaw opening to angle.

Every width the stack commands is the opening between the jaws, from 0 shut to `opening_m` fully open, mapped linearly onto the motor angles `closed_deg` and `open_deg`. The gripper holds a width the same way under every controller. A shipped description comes with its gripper filled in; a gripper Anvil does not ship needs its own `urdf`.

Checked where the arm carrying it is next handed over: an `opening_m` that is not more than zero, a `closed_deg` or `open_deg` outside `joint`'s limits, a mass that is negative or not a number, a `grip_assist` value below zero, and a `urdf` that is not a gripper fragment, or that `fingers` does not lay over, are refused there.

Attributes:

| Name              | Type                                              | Description                                                                                                  |
| ----------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `family`          | `str`                                             | The gripper family, e.g. anvil-gripper.                                                                      |
| `version`         | `int`                                             | The hardware revision, e.g. 1.                                                                               |
| `joint`           | `JointDescription`                                | The motor driving the fingers.                                                                               |
| `kp`              | `float`                                           | The drive's position gain, in newton meters per radian of motor angle error.                                 |
| `kd`              | `float`                                           | The drive's velocity gain, in newton meters per radian per second of motor speed error.                      |
| `closed_deg`      | `float`                                           | The motor angle with the jaws shut, in degrees.                                                              |
| `open_deg`        | `float`                                           | The motor angle with the jaws open by opening_m, in degrees.                                                 |
| `opening_m`       | `float`                                           | The jaw opening at full travel, in meters, between the jaws: what every commanded width is measured against. |
| `urdf`            | `str \| None`                                     | The unit's own URDF fragment, its gripper.urdf, or None for the one its family and version ship.             |
| `fingers`         | `FingerSet \| None`                               | The finger set laid over urdf, for a unit whose fingers are interchangeable.                                 |
| `grip_assist`     | `GripAssist`                                      | The feedforward torque that does the gripping.                                                               |
| `mass_kg`         | `float`                                           | The unit's mass, in kilograms.                                                                               |
| `com_m`           | `tuple[float, float, float]`                      | The unit's center of mass, in meters, in the frame of the link its fingers hang from.                        |
| `inertia_kgm2`    | `tuple[float, float, float, float, float, float]` | The unit's inertia about its center of mass, in kilogram square meters, as (ixx, ixy, ixz, iyy, iyz, izz).   |
| `alternate_motor` | `str \| None`                                     | The motor name some units of this gripper were flashed as instead, e.g. J7, or None for none.                |
| `gripper_map`     | `GripperMap`                                      | Map from a commanded jaw opening to the motor angle, linear between the shut and open angles.                |

#### family

```
family: str
```

The gripper family, e.g. `anvil-gripper`.

#### version

```
version: int
```

The hardware revision, e.g. `1`.

#### joint

```
joint: JointDescription
```

The motor driving the fingers.

#### kp

```
kp: float
```

The drive's position gain, in newton meters per radian of motor angle error.

#### kd

```
kd: float
```

The drive's velocity gain, in newton meters per radian per second of motor speed error.

#### closed_deg

```
closed_deg: float
```

The motor angle with the jaws shut, in degrees.

#### open_deg

```
open_deg: float
```

The motor angle with the jaws open by `opening_m`, in degrees.

#### opening_m

```
opening_m: float
```

The jaw opening at full travel, in meters, between the jaws: what every commanded width is measured against.

#### urdf

```
urdf: str | None = None
```

The unit's own URDF fragment, its `gripper.urdf`, or `None` for the one its `family` and `version` ship.

`RobotConfig.composed_urdf` grafts it onto the `hand` link the arm ends at. It carries the tool frame and the two finger joints the motor turns.

#### fingers

```
fingers: FingerSet | None = None
```

The finger set laid over `urdf`, for a unit whose fingers are interchangeable.

`None` for one whose fingers are its own. A shipped arm carries the set its gripper ships with unless its `arm.yaml` names another.

#### grip_assist

```
grip_assist: GripAssist = GripAssist(
    max_torque=0.0,
    stiffness=0.0,
    opening_scale=1.0,
    shut_torque=0.0,
)
```

The feedforward torque that does the gripping.

#### mass_kg

```
mass_kg: float = 0.0
```

The unit's mass, in kilograms.

The arm's URDF carries none of it; `RobotConfig.composed_urdf` adds it to the link the fingers hang from. A gripper stated without it weighs nothing.

#### com_m

```
com_m: tuple[float, float, float] = (0.0, 0.0, 0.0)
```

The unit's center of mass, in meters, in the frame of the link its fingers hang from.

#### inertia_kgm2

```
inertia_kgm2: tuple[
    float, float, float, float, float, float
] = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
```

The unit's inertia about its center of mass, in kilogram square meters, as `(ixx, ixy, ixz, iyy, iyz, izz)`.

#### alternate_motor

```
alternate_motor: str | None = None
```

The motor name some units of this gripper were flashed as instead, e.g. `J7`, or `None` for none.

A shipped arm's `arm.yaml` names it, as the OpenYAM's does, never the gripper's own description. The motor is then driven at that name's addresses, slave `n` and master `n + 0x10` for `J<n>`, on a link where the gripper's own address is silent and that one answers; and a record numbered as it places the arm as a follower. An address a chain joint uses, or a name not `J<number>`, is refused where the arm is next handed over.

#### gripper_map

```
gripper_map: GripperMap
```

Map from a commanded jaw opening to the motor angle, linear between the shut and open angles.

Built from the gripper as it is now; an edit after reading it does not change the map read.

Raises:

| Type                   | Description                                             |
| ---------------------- | ------------------------------------------------------- |
| `InvalidArgumentError` | The gripper is one the class docstring says is refused. |

### ExternalGripper

```
ExternalGripper(
    can_ids: list[int] = [],
    mass_kg: float = 0.0,
    com_m: tuple[float, float, float] = (0.0, 0.0, 0.0),
    inertia_kgm2: tuple[
        float, float, float, float, float, float
    ] = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
)
```

A gripper the stack does not drive: your own, run by your code on the arm's bus beside the stack.

Set one as `ArmDescription.gripper` in place of a `Gripper`. It takes no slot and adds no URDF, so the arm's chain ends at its `hand` flange, which carries the mass stated here. Checked where the arm is next handed over: a CAN ID past 0x7FF, one stated twice or one the arm's motors use, and a mass that is negative or not a number, are refused there.

Attributes:

| Name           | Type                                              | Description                                                                                         |
| -------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `can_ids`      | `list[int]`                                       | The standard 11-bit CAN IDs your code sends and answers on, on the arm's bus.                       |
| `mass_kg`      | `float`                                           | The gripper's mass, in kilograms; 0 weighs nothing.                                                 |
| `com_m`        | `tuple[float, float, float]`                      | Its center of mass, in meters, in the frame of the hand link it bolts to.                           |
| `inertia_kgm2` | `tuple[float, float, float, float, float, float]` | Its inertia about its center of mass, in kilogram square meters, as (ixx, ixy, ixz, iyy, iyz, izz). |

#### can_ids

```
can_ids: list[int] = []
```

The standard 11-bit CAN IDs your code sends and answers on, on the arm's bus.

#### mass_kg

```
mass_kg: float = 0.0
```

The gripper's mass, in kilograms; 0 weighs nothing.

#### com_m

```
com_m: tuple[float, float, float] = (0.0, 0.0, 0.0)
```

Its center of mass, in meters, in the frame of the `hand` link it bolts to.

#### inertia_kgm2

```
inertia_kgm2: tuple[
    float, float, float, float, float, float
] = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
```

Its inertia about its center of mass, in kilogram square meters, as `(ixx, ixy, ixz, iyy, iyz, izz)`.

### Gains

```
Gains(kp: float, kd: float)
```

A drive's two gains for one joint.

The drive applies `kp * (position target - measured position) + kd * (velocity target - measured velocity)`, plus the feedforward torque. Each must be a number of zero or more, checked where the arm is next handed over.

Attributes:

| Name | Type    | Description                                            |
| ---- | ------- | ------------------------------------------------------ |
| `kp` | `float` | Position gain, in newton meters per radian.            |
| `kd` | `float` | Velocity gain, in newton meters per radian per second. |

#### kp

```
kp: float
```

Position gain, in newton meters per radian.

#### kd

```
kd: float
```

Velocity gain, in newton meters per radian per second.

### ControllerTuning

```
ControllerTuning(
    joints: list[Gains],
    max_dps: list[float],
    tcp_max_mps: float,
)
```

How one controller drives an arm's chain joints: their gains and how fast it moves their targets.

Checked where the arm is next handed over: `joints` and `max_dps` must each hold exactly one entry per chain joint, and every speed must be more than zero.

Attributes:

| Name          | Type          | Description                                                                                        |
| ------------- | ------------- | -------------------------------------------------------------------------------------------------- |
| `joints`      | `list[Gains]` | The gains of each chain joint, in chain order.                                                     |
| `max_dps`     | `list[float]` | The fastest the controller moves each chain joint's target, in degrees per second, in chain order. |
| `tcp_max_mps` | `float`       | The fastest the controller moves the arm's tool frame, in meters per second.                       |

#### joints

```
joints: list[Gains]
```

The gains of each chain joint, in chain order.

#### max_dps

```
max_dps: list[float]
```

The fastest the controller moves each chain joint's target, in degrees per second, in chain order.

Never past the joint's own `JointDescription.max_dps`.

#### tcp_max_mps

```
tcp_max_mps: float
```

The fastest the controller moves the arm's tool frame, in meters per second.

### TuningDescription

```
TuningDescription(
    joint: ControllerTuning,
    joint_trajectory: ControllerTuning,
    ee_pose: ControllerTuning,
    leader_follower: ControllerTuning,
)
```

How each controller drives an arm's chain joints, as the `tuning.yaml` beside its `arm.yaml` states it.

One complete section per controller, nothing shared between them. An edit before a controller is built, such as `arm.description.tuning.ee_pose.joints[6].kp = 15.0`, is what that controller then runs; once it is running, its handle's `configure` is how a gain moves.

Attributes:

| Name               | Type               | Description                                                                                          |
| ------------------ | ------------------ | ---------------------------------------------------------------------------------------------------- |
| `joint`            | `ControllerTuning` | The anvil.control.JointController's section.                                                         |
| `joint_trajectory` | `ControllerTuning` | The anvil.control.JointTrajectoryController's section.                                               |
| `ee_pose`          | `ControllerTuning` | The anvil.control.EePoseController's section.                                                        |
| `leader_follower`  | `ControllerTuning` | The anvil.control.LeaderFollowerController's section, for the followers; it drives the leaders limp. |

#### joint

```
joint: ControllerTuning
```

The `anvil.control.JointController`'s section.

#### joint_trajectory

```
joint_trajectory: ControllerTuning
```

The `anvil.control.JointTrajectoryController`'s section.

#### ee_pose

```
ee_pose: ControllerTuning
```

The `anvil.control.EePoseController`'s section.

#### leader_follower

```
leader_follower: ControllerTuning
```

The `anvil.control.LeaderFollowerController`'s section, for the followers; it drives the leaders limp.

### ArmDescription

```
ArmDescription(
    family: str,
    version: int,
    joints: list[JointDescription],
    gripper: Gripper | ExternalGripper,
    tuning: TuningDescription,
    home: dict[str, Any] | None = None,
    excluded_pairs: list[tuple[str, str]] = [],
)
```

One arm variant: what it physically is and how it is driven.

Where it stands is the `RobotConfig`'s business. The shipped variants are class attributes, `ArmDescription.OPENARM_V2`, each built fresh on every access with nothing read from disk; `shipped` takes the variant as text, and `read` takes a description directory of your own. Every field holds the object itself, so an edit through one is what an arm carrying this description then runs. Checked where the arm is next handed over: no chain joint or more than `anvil.kinematics.MAX_DOF`, a motor not named `J<number>`, a joint or tuning refused as its class says, a gripper refused as its class says, or a `home` its field says is refused.

Methods:

| Name           | Description                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| `shipped`      | Returns the shipped arm variant names, built fresh: for a caller that has the variant as text.               |
| `read`         | Reads the arm <root>/arms/<variant>/arm.yaml describes, with its tuning, homing and gripper.                 |
| `shipped_home` | Returns the homing run the variant ships with, its grips opened to the same share of the gripper it carries. |
| `to_json`      | Returns the description as JSON, the form from_json reads back.                                              |
| `from_json`    | Builds a description from the JSON to_json wrote, checked as it is when handed over.                         |

Attributes:

| Name             | Type                         | Description                                                                                                |
| ---------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `family`         | `str`                        | The arm family, e.g. openarm.                                                                              |
| `version`        | `int`                        | The hardware revision, e.g. 2.                                                                             |
| `joints`         | `list[JointDescription]`     | The chain joints, in order; the gripper's motor is not among them.                                         |
| `gripper`        | `Gripper \| ExternalGripper` | The gripper the arm carries: a Gripper the stack drives, or an ExternalGripper your own code drives.       |
| `tuning`         | `TuningDescription`          | The gains and speeds each controller drives the chain joints at.                                           |
| `home`           | `dict[str, Any] \| None`     | The homing run, as the left or only arm sees it: the one the variant ships with, or one a workcell taught. |
| `excluded_pairs` | `list[tuple[str, str]]`      | Pairs of the arm's links, by its own URDF names, that Clearance never checks one against the other.        |
| `OPENARM_V1`     |                              | The OpenArm v1, built fresh on every access.                                                               |
| `OPENARM_V2`     |                              | The OpenArm v2, built fresh on every access.                                                               |
| `OPENYAM_V1`     |                              | The OpenYAM v1, built fresh on every access.                                                               |
| `OPENYAM_V2`     |                              | The OpenYAM v2, built fresh on every access.                                                               |
| `YAM_V1`         |                              | The YAM v1, built fresh on every access.                                                                   |
| `variant`        | `str`                        | The variant, family/vN, e.g. openyam/v1: the name the sim and shipped know the arm by.                     |
| `num_slots`      | `int`                        | Slots one such arm owns in a robot: one per chain joint, then one for a Gripper the stack drives.          |
| `gripper_map`    | `GripperMap \| None`         | Gripper.gripper_map of the gripper the stack drives; None for an external gripper.                         |

#### family

```
family: str
```

The arm family, e.g. `openarm`.

#### version

```
version: int
```

The hardware revision, e.g. `2`.

#### joints

```
joints: list[JointDescription]
```

The chain joints, in order; the gripper's motor is not among them.

#### gripper

```
gripper: Gripper | ExternalGripper
```

The gripper the arm carries: a `Gripper` the stack drives, or an `ExternalGripper` your own code drives.

#### tuning

```
tuning: TuningDescription
```

The gains and speeds each controller drives the chain joints at.

#### home

```
home: dict[str, Any] | None = None
```

The homing run, as the left or only arm sees it: the one the variant ships with, or one a workcell taught.

`{"tcp_speed_mps": float, "rows": [...]}`, each row `{"q_deg": [...], "grip_m": float, "tcp_speed_mps": float | None}`. The first row is the park pose, where the arm sits while the robot is off; the last is the home pose, where homing leaves it ready to work; the rows between are waypoints. Every shipped run is authored for an arm standing upright. Teach your own from `RobotConfig.home_rows_at`, as a unit hung or on a wall has to, and go back with `shipped_home`. `None` for an arm that ships none, which `RobotConfig.home_trajectory` then refuses.

Refused for a `tcp_speed_mps` that is not positive and finite, no rows, or a row that does not state one angle per chain joint inside its limits, whose grip is outside the gripper's opening, or whose own `tcp_speed_mps` is not positive and finite.

#### excluded_pairs

```
excluded_pairs: list[tuple[str, str]] = []
```

Pairs of the arm's links, by its own URDF names, that `Clearance` never checks one against the other.

Their collision meshes overlap however it stands.

#### OPENARM_V1

```
OPENARM_V1
```

The OpenArm v1, built fresh on every access.

#### OPENARM_V2

```
OPENARM_V2
```

The OpenArm v2, built fresh on every access.

#### OPENYAM_V1

```
OPENYAM_V1
```

The OpenYAM v1, built fresh on every access.

#### OPENYAM_V2

```
OPENYAM_V2
```

The OpenYAM v2, built fresh on every access.

#### YAM_V1

```
YAM_V1
```

The YAM v1, built fresh on every access.

#### variant

```
variant: str
```

The variant, `family/vN`, e.g. `openyam/v1`: the name the sim and `shipped` know the arm by.

Raises:

| Type                   | Description                                                 |
| ---------------------- | ----------------------------------------------------------- |
| `InvalidArgumentError` | The description is one the class docstring says is refused. |

#### num_slots

```
num_slots: int
```

Slots one such arm owns in a robot: one per chain joint, then one for a `Gripper` the stack drives.

Raises:

| Type                   | Description                                                 |
| ---------------------- | ----------------------------------------------------------- |
| `InvalidArgumentError` | The description is one the class docstring says is refused. |

#### gripper_map

```
gripper_map: GripperMap | None
```

`Gripper.gripper_map` of the gripper the stack drives; `None` for an external gripper.

Raises:

| Type                   | Description                                 |
| ---------------------- | ------------------------------------------- |
| `InvalidArgumentError` | The gripper is one Gripper says is refused. |

#### shipped

```
shipped(variant: str) -> ArmDescription
```

Returns the shipped arm `variant` names, built fresh: for a caller that has the variant as text.

Parameters:

| Name      | Type  | Description                                | Default    |
| --------- | ----- | ------------------------------------------ | ---------- |
| `variant` | `str` | The variant, family/vN, e.g. "openarm/v2". | *required* |

Raises:

| Type                   | Description                                                      |
| ---------------------- | ---------------------------------------------------------------- |
| `InvalidArgumentError` | Anvil ships no such variant; the message lists the ones it does. |

#### read

```
read(root: str, variant: str) -> ArmDescription
```

Reads the arm `<root>/arms/<variant>/arm.yaml` describes, with its tuning, homing and gripper.

How a description the SDK was not built with gets in: `root` is a directory laid out as Anvil's descriptions are. The arm is joined to the gripper its `arm.yaml` names under `<root>/grippers/`, the `tuning.yaml` beside it, and the `home.yaml` and `collision.yaml` beside it where they exist. The release wheel refuses a robot built from a description that differs from the shipped one; see the module docstring.

Parameters:

| Name      | Type  | Description                                | Default    |
| --------- | ----- | ------------------------------------------ | ---------- |
| `root`    | `str` | The directory holding arms/ and grippers/. | *required* |
| `variant` | `str` | The variant, family/vN, e.g. "openyam/v1". | *required* |

Raises:

| Type                   | Description                                                                                                                 |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A file could not be read, a missing one included, or does not describe an arm this SDK can use; the message names the file. |

#### shipped_home

```
shipped_home() -> dict[str, Any] | None
```

Returns the homing run the variant ships with, its grips opened to the same share of the gripper it carries.

What a description whose `home` was taught goes back to: `description.home = description.shipped_home()`.

Returns:

| Type                     | Description                                                                |
| ------------------------ | -------------------------------------------------------------------------- |
| `dict[str, Any] \| None` | The run in the shape home holds it, or None for a variant that ships none. |

Raises:

| Type                   | Description                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | Anvil ships no such variant, or the description is one the class docstring says is refused. |

#### to_json

```
to_json() -> str
```

Returns the description as JSON, the form `from_json` reads back.

#### from_json

```
from_json(wire: str) -> Self
```

Builds a description from the JSON `to_json` wrote, checked as it is when handed over.

JSON an older release wrote reads as it was written: a field added since takes its default.

Parameters:

| Name   | Type  | Description    | Default    |
| ------ | ----- | -------------- | ---------- |
| `wire` | `str` | The JSON text. | *required* |

Raises:

| Type                   | Description                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | wire is not a description's JSON, or the description is one the class docstring says is refused. |

### Side

Bases: `StrEnum`

Which arm of a robot an arm is, where that means something.

The left or the right of a pair, or of a mirror-image build even alone. The only arm of its role in a symmetric build, a lone yam or either arm of a single leader and follower, has no side.

Attributes:

| Name    | Type | Description                                            |
| ------- | ---- | ------------------------------------------------------ |
| `LEFT`  |      | The left arm, which takes the lower slots of its role. |
| `RIGHT` |      | The right arm.                                         |

#### LEFT

```
LEFT
```

The left arm, which takes the lower slots of its role.

#### RIGHT

```
RIGHT
```

The right arm.

### ArmCount

Bases: `StrEnum`

How many followers a shipped workcell stands: one, or a left and a right.

Attributes:

| Name       | Type | Description                  |
| ---------- | ---- | ---------------------------- |
| `SINGLE`   |      | One follower, with no side.  |
| `BIMANUAL` |      | A left and a right follower. |

#### SINGLE

```
SINGLE
```

One follower, with no side.

#### BIMANUAL

```
BIMANUAL
```

A left and a right follower.

### Role

Bases: `StrEnum`

What an arm does in a robot.

Followers take the lower slots, so adding leaders moves no follower's slots.

Attributes:

| Name       | Type | Description                                                                        |
| ---------- | ---- | ---------------------------------------------------------------------------------- |
| `LEADER`   |      | Moved by hand; its measured state becomes the command of the follower on its side. |
| `FOLLOWER` |      | Driven to a command.                                                               |

#### LEADER

```
LEADER
```

Moved by hand; its measured state becomes the command of the follower on its side.

Its URDF names carry a `leader_` prefix ahead of its side.

#### FOLLOWER

```
FOLLOWER
```

Driven to a command.

### ArmLayout

```
ArmLayout(
    first_slot: int,
    num_slots: int,
    chain_slots: tuple[int, ...],
    gripper_slot: int | None,
    tcp_link: str,
)
```

Where one arm of a robot sits in the robot's slots, as `RobotConfig.layout` worked it out.

Attributes:

| Name           | Type              | Description                                                                                            |
| -------------- | ----------------- | ------------------------------------------------------------------------------------------------------ |
| `first_slot`   | `int`             | The first slot the arm owns.                                                                           |
| `num_slots`    | `int`             | How many slots it owns: its chain joints, then its gripper where the stack drives one.                 |
| `chain_slots`  | `tuple[int, ...]` | The slot of each chain joint, in description order.                                                    |
| `gripper_slot` | `int \| None`     | The slot of its gripper motor, the last it owns; None for an external gripper, which owns none.        |
| `tcp_link`     | `str`             | The URDF link its chain ends at: the gripper's tool frame, or the hand flange for an external gripper. |

#### first_slot

```
first_slot: int
```

The first slot the arm owns.

#### num_slots

```
num_slots: int
```

How many slots it owns: its chain joints, then its gripper where the stack drives one.

#### chain_slots

```
chain_slots: tuple[int, ...]
```

The slot of each chain joint, in description order.

#### gripper_slot

```
gripper_slot: int | None
```

The slot of its gripper motor, the last it owns; `None` for an external gripper, which owns none.

#### tcp_link

```
tcp_link: str
```

The URDF link its chain ends at: the gripper's tool frame, or the `hand` flange for an external gripper.

### RobotLayout

```
RobotLayout(
    variant: str,
    num_slots: int,
    slot_summary: str,
    slot_names: tuple[str, ...],
    slot_travel_rad: tuple[tuple[float, float], ...],
    arms: tuple[ArmLayout, ...],
)
```

What follows from a robot's arms rather than being stated with them, as `RobotConfig.layout` worked it out.

A snapshot: reading it costs nothing, and an edit to the robot after it was taken does not change it.

Attributes:

| Name              | Type                              | Description                                                                                           |
| ----------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `variant`         | `str`                             | The variant the arms share, e.g. openyam/v1.                                                          |
| `num_slots`       | `int`                             | Slots the arms occupy: the num_joints of an anvil.control.JointState or anvil.control.RobotCommand.   |
| `slot_summary`    | `str`                             | The arms and the slots they own, for an operator's log: left owns slots 0..8, right owns slots 8..16. |
| `slot_names`      | `tuple[str, ...]`                 | Each slot by what it drives, in slot order.                                                           |
| `slot_travel_rad` | `tuple[tuple[float, float], ...]` | Each slot's travel as (lower, upper), in radians, in slot order.                                      |
| `arms`            | `tuple[ArmLayout, ...]`           | One per arm, in the robot's order.                                                                    |

#### variant

```
variant: str
```

The variant the arms share, e.g. `openyam/v1`.

#### num_slots

```
num_slots: int
```

Slots the arms occupy: the `num_joints` of an `anvil.control.JointState` or `anvil.control.RobotCommand`.

#### slot_summary

```
slot_summary: str
```

The arms and the slots they own, for an operator's log: `left owns slots 0..8, right owns slots 8..16`.

Each range is half-open: `0..8` is slots 0 to 7.

#### slot_names

```
slot_names: tuple[str, ...]
```

Each slot by what it drives, in slot order.

The arm's name, then the joint's or `gripper`, e.g. `"left joint_3"`, `"left gripper"`.

#### slot_travel_rad

```
slot_travel_rad: tuple[tuple[float, float], ...]
```

Each slot's travel as `(lower, upper)`, in radians, in slot order.

A chain joint's limits from the arm's description, negated on the right arm for a `JointDescription.reflected` joint; a gripper motor's between its shut and open angles.

#### arms

```
arms: tuple[ArmLayout, ...]
```

One per arm, in the robot's order.

### ArmConfig

```
ArmConfig(
    name: str,
    bus: str,
    description: ArmDescription,
    side: Side | None = None,
    role: Role = Role.FOLLOWER,
    bus_overridden: bool = False,
)
```

One arm of a robot: its name, the bus it answers on, what it does, which side it is, and its description.

Put it in a `RobotConfig` to give it slots; the slot properties ask that robot, so they follow any edit to it.

Attributes:

| Name             | Type             | Description                                                                                         |
| ---------------- | ---------------- | --------------------------------------------------------------------------------------------------- |
| `name`           | `str`            | Its name, unique in the robot: what logs, errors and RobotLayout.slot_names call it.                |
| `bus`            | `str`            | The bus its motors answer on, e.g. "follower_l", resolved against the CAN links the host has.       |
| `description`    | `ArmDescription` | Everything the arm physically is and how it is driven, as the object itself.                        |
| `side`           | `Side \| None`   | Side.LEFT or Side.RIGHT in a robot with more than one arm of its role, or for a mirror-image build. |
| `role`           | `Role`           | What the arm does in the robot.                                                                     |
| `bus_overridden` | `bool`           | Whether an operator set bus by hand over what the drives' identity records say.                     |
| `num_slots`      | `int`            | Slots the arm owns: one per chain joint, then one for a Gripper the stack drives.                   |
| `first_slot`     | `int \| None`    | The first slot the arm owns in the robot it was last put in; None until it is in a RobotConfig.     |
| `chain_slots`    | `list[int]`      | The slot of each chain joint, in description order.                                                 |
| `gripper_slot`   | `int \| None`    | The slot of its gripper motor, the last one it owns; None for an external gripper, which owns none. |

#### name

```
name: str
```

Its name, unique in the robot: what logs, errors and `RobotLayout.slot_names` call it.

#### bus

```
bus: str
```

The bus its motors answer on, e.g. `"follower_l"`, resolved against the CAN links the host has.

Where the drives' identity records name a link, they decide and this is the fallback, unless `bus_overridden`; where it resolves to no link with drives, the arm is guessed onto one that has them, as `anvil.hw.buses` says.

#### description

```
description: ArmDescription
```

Everything the arm physically is and how it is driven, as the object itself.

So `arm.description.tuning.ee_pose.joints[3].kp = 20.0` is what this arm then runs.

#### side

```
side: Side | None = None
```

`Side.LEFT` or `Side.RIGHT` in a robot with more than one arm of its role, or for a mirror-image build.

`None` for the only arm of its role in a symmetric build. Any other choice is refused where the robot is built.

#### role

```
role: Role = Role.FOLLOWER
```

What the arm does in the robot.

#### bus_overridden

```
bus_overridden: bool = False
```

Whether an operator set `bus` by hand over what the drives' identity records say.

The arm then stays on `bus`, and the readiness check only warns where its records disagree.

#### num_slots

```
num_slots: int
```

Slots the arm owns: one per chain joint, then one for a `Gripper` the stack drives.

Raises:

| Type                   | Description                                            |
| ---------------------- | ------------------------------------------------------ |
| `InvalidArgumentError` | The description is one ArmDescription says is refused. |

#### first_slot

```
first_slot: int | None
```

The first slot the arm owns in the robot it was last put in; `None` until it is in a `RobotConfig`.

Raises:

| Type                   | Description                                              |
| ---------------------- | -------------------------------------------------------- |
| `InvalidArgumentError` | That robot, as edited since, is one RobotConfig refuses. |

#### chain_slots

```
chain_slots: list[int]
```

The slot of each chain joint, in description order.

Raises:

| Type                   | Description                                                           |
| ---------------------- | --------------------------------------------------------------------- |
| `InvalidArgumentError` | The robot the arm is in, as edited since, is one RobotConfig refuses. |
| `OperationError`       | The arm is in no RobotConfig yet.                                     |

#### gripper_slot

```
gripper_slot: int | None
```

The slot of its gripper motor, the last one it owns; `None` for an external gripper, which owns none.

Raises:

| Type                   | Description                                                           |
| ---------------------- | --------------------------------------------------------------------- |
| `InvalidArgumentError` | The robot the arm is in, as edited since, is one RobotConfig refuses. |
| `OperationError`       | The arm is in no RobotConfig yet.                                     |

### Mount

```
Mount(
    prefix: str,
    xyz_m: tuple[float, float, float],
    yaw_rad: float,
    roll_rad: float = 0.0,
    pitch_rad: float = 0.0,
)
```

Where a robot stands one copy of its variant's unit: an OpenArm torso carrying both arms, or one yam arm.

Mounts are stated in the world frame, the composed URDF's `world` link, which every pose a robot's chains and controllers state is in too. A preset's mounts are where the shipped stands bolt the units; move them to match your bench. The unit is turned as a URDF origin is: rolled about the world's x, then pitched about its y, then turned about its vertical. Hung from a ceiling is `roll_rad=math.pi`; bolted to a wall behind it, `pitch_rad=math.pi / 2`. Gravity stays the world's -z, so a tilted unit's chains, gravity compensation and IK follow the tilt. Each mount of a robot has its own `prefix`, and every coordinate must be a finite number, checked where the robot is built or next handed over.

Attributes:

| Name        | Type                         | Description                                                                                                |
| ----------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `prefix`    | `str`                        | What the copy's URDF link and joint names are prefixed with.                                               |
| `xyz_m`     | `tuple[float, float, float]` | Where the unit's base stands in the world frame, in meters.                                                |
| `yaw_rad`   | `float`                      | How far the unit is turned about the world frame's vertical, in radians, counterclockwise seen from above. |
| `roll_rad`  | `float`                      | How far the unit is rolled about the world frame's x, in radians; zero stands it upright.                  |
| `pitch_rad` | `float`                      | How far the unit is pitched about the world frame's y, in radians; zero stands it upright.                 |

#### prefix

```
prefix: str
```

What the copy's URDF link and joint names are prefixed with.

For one arm, its role's prefix then its side's, `"left_"`, `"leader_left_"` or `""` for an arm with no side; for an OpenArm torso, its role's, `""` or `"leader_"`.

#### xyz_m

```
xyz_m: tuple[float, float, float]
```

Where the unit's base stands in the world frame, in meters.

#### yaw_rad

```
yaw_rad: float
```

How far the unit is turned about the world frame's vertical, in radians, counterclockwise seen from above.

#### roll_rad

```
roll_rad: float = 0.0
```

How far the unit is rolled about the world frame's x, in radians; zero stands it upright.

The roll is applied first, before the pitch and then the yaw.

#### pitch_rad

```
pitch_rad: float = 0.0
```

How far the unit is pitched about the world frame's y, in radians; zero stands it upright.

The pitch is applied after the roll and before the yaw.

### Stand

Bases: `StrEnum`

What a robot's units stand on, as Anvil's CAD draws it.

Its frame is the one the robot's `mounts` are stated in, so a mount is where the stand bolts a unit, and `anvil.sim.World` stands the robot in the scene of the stand's name unless told another.

Attributes:

| Name           | Type | Description                                                                                              |
| -------------- | ---- | -------------------------------------------------------------------------------------------------------- |
| `LIFT`         |      | The wheeled lifting platform. Its frame is the top plate's face on the column's axis, x along the reach. |
| `LIFT_BRACKET` |      | The lift carrying the OpenYAM middle bracket, in the lift's frame.                                       |

#### LIFT

```
LIFT
```

The wheeled lifting platform. Its frame is the top plate's face on the column's axis, x along the reach.

#### LIFT_BRACKET

```
LIFT_BRACKET
```

The lift carrying the OpenYAM middle bracket, in the lift's frame.

It is the rail a yam pair stands on, and the pole between the arms with a camera on its ball head: the composed URDF's `pole_cam` link, which a `CameraConfig.link` may name.

### TimedWaypoint

```
TimedWaypoint(
    time_from_start_s: float,
    q: list[float],
    qd: list[float],
)
```

One point of a `TimedWaypoints`: where every slot is at one time, and how fast it moves through there.

Attributes:

| Name                | Type          | Description                                                                                            |
| ------------------- | ------------- | ------------------------------------------------------------------------------------------------------ |
| `time_from_start_s` | `float`       | When the run reaches it, in seconds after the run starts.                                              |
| `q`                 | `list[float]` | Position per slot, in radians; a gripper's slot is its motor angle.                                    |
| `qd`                | `list[float]` | Velocity per slot as the run passes through, in radians per second; zero stops there before moving on. |

#### time_from_start_s

```
time_from_start_s: float
```

When the run reaches it, in seconds after the run starts.

#### q

```
q: list[float]
```

Position per slot, in radians; a gripper's slot is its motor angle.

#### qd

```
qd: list[float]
```

Velocity per slot as the run passes through, in radians per second; zero stops there before moving on.

### TimedWaypoints

```
TimedWaypoints(num_slots: int, points: list[TimedWaypoint])
```

Timed waypoints over every slot of a robot: a run the robot plans, such as its homing.

One comes from `RobotConfig.home_trajectory`, `dehome_trajectory` or `rehome_trajectory`. The first waypoint is reached from wherever the arms are when it starts, and the last at rest. Play it with `anvil.control.JointTrajectoryHandle.play`, send it with `anvil.control.Runtime.send_command`, or build the command from it with `anvil.control.JointTrajectoryCommand.from_waypoints`: the controller plays it as it is.

Attributes:

| Name         | Type                  | Description                                                                  |
| ------------ | --------------------- | ---------------------------------------------------------------------------- |
| `num_slots`  | `int`                 | Slots every waypoint states: the robot's RobotConfig.num_slots.              |
| `points`     | `list[TimedWaypoint]` | The waypoints, in time order.                                                |
| `duration_s` | `float`               | Time from the start to the last waypoint, in seconds; 0 for a run with none. |

#### num_slots

```
num_slots: int
```

Slots every waypoint states: the robot's `RobotConfig.num_slots`.

#### points

```
points: list[TimedWaypoint]
```

The waypoints, in time order.

#### duration_s

```
duration_s: float
```

Time from the start to the last waypoint, in seconds; 0 for a run with none.

### UrdfJoint

```
UrdfJoint(
    name: str, slot: int | None, scale: float, offset: float
)
```

One moving joint of a robot's composed URDF and the slot it follows, as `RobotConfig.urdf_joints` lists it.

The joint's value is `scale` times the measured position in `slot`, plus `offset`: one for one for a motor's own joint, while a gripper's finger follows its motor through the linkage.

Attributes:

| Name     | Type          | Description                                                                         |
| -------- | ------------- | ----------------------------------------------------------------------------------- |
| `name`   | `str`         | The joint's name in the composed URDF.                                              |
| `slot`   | `int \| None` | The slot whose measured position drives the joint; None for a joint nothing drives. |
| `scale`  | `float`       | What the slot's position is multiplied by; 1 for a motor's own joint.               |
| `offset` | `float`       | What is added after scaling; 0 for a motor's own joint.                             |

#### name

```
name: str
```

The joint's name in the composed URDF.

#### slot

```
slot: int | None
```

The slot whose measured position drives the joint; `None` for a joint nothing drives.

#### scale

```
scale: float
```

What the slot's position is multiplied by; 1 for a motor's own joint.

#### offset

```
offset: float
```

What is added after scaling; 0 for a motor's own joint.

### RobotConfig

```
RobotConfig(
    arms: list[ArmConfig],
    urdf: str | None = None,
    mounts: list[Mount] = [],
    stand: Stand | None = None,
)
```

The robot one run drives: its arms in slot order, the URDF they stand by, and where each copy of it is mounted.

Get one from a workcell, `WorkcellConfig.OPENYAM_V1_SINGLE.robot`, or build one from its fields. Each arm owns the slots after the arm before it, chain joints then gripper, so a bimanual openarm's left arm owns slots 0 to 7 and its right 8 to 15, and a single openyam's arm 0 to 6. Arms are declared followers before leaders and, within a role, the left before the right.

It is checked as it is built, and again wherever it is handed over after an edit. Whether the URDF really carries each arm is found where a chain is cut from it, by `chains`, `composed_urdf` and the calls that use them.

Raises:

| Type                   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | No arm, or more than anvil.control.MAX_ARMS; more slots than anvil.control.MAX_JOINTS; a name or bus used twice; a side missing or given against the rule on ArmConfig.side; two arms of one role on one side; a leader before a follower, or a right arm before the left of its role; in a robot with leaders, an arm with no partner of the other role on its side; arms of different variants; a mount that is not a finite number on every axis, or two mounts of one prefix; an arm whose description is refused; or, in the release wheel, a robot Anvil does not ship. |

Methods:

| Name                | Description                                                                                                |
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
| `arms_where`        | Returns the arms of the side and role asked for, in slot order: the objects themselves.                    |
| `arm`               | Returns the arm named name, the object itself, or None when the robot has no arm of that name.             |
| `layout`            | Returns the robot's slot layout as it stands now, as a snapshot.                                           |
| `camera_links`      | Returns every \*\_cam link of the composed URDF: the links a CameraConfig.link may name.                   |
| `mount_joints`      | Returns the URDF joint each of mounts stands its copy of the unit by, in the mounts' order.                |
| `mesh_files`        | Returns every mesh the composed URDF names, each once, by its path under the robot descriptions' root.     |
| `urdf_joints`       | Returns every moving joint of composed_urdf with the slot it follows, in the composed URDF's order.        |
| `composed_urdf`     | Returns the whole robot as URDF text: what gravity compensation, the sim and a recording read.             |
| `chains`            | Returns one anvil.kinematics.Chain per arm, in slot order, cut from the composed URDF to its tool frame.   |
| `home_rows_at`      | Returns one homing row per arm, in slot order, standing each where q has it: what a row is taught from.    |
| `home_trajectory`   | Returns the run that homes the robot from where its arms are, through the homing sequence each describes.  |
| `dehome_trajectory` | Returns the homing run backwards, from where the arms are to the row a homing starts from, ending at rest. |
| `rehome_trajectory` | Returns a run straight to where a homing ends, in one segment from where the arms are.                     |
| `to_json`           | Returns the robot as JSON, the form from_json reads back.                                                  |
| `from_json`         | Builds a robot from the JSON to_json wrote, checked as one built from its fields is.                       |

Attributes:

| Name           | Type              | Description                                                                                              |
| -------------- | ----------------- | -------------------------------------------------------------------------------------------------------- |
| `arms`         | `list[ArmConfig]` | The arms, in slot order: the objects themselves, so an edit through one is what that arm then runs.      |
| `urdf`         | `str \| None`     | The URDF text the arms stand by, without their grippers, or None for the one their variant ships.        |
| `mounts`       | `list[Mount]`     | Where each copy of urdf stands; empty for a urdf that places the arms itself.                            |
| `stand`        | `Stand \| None`   | What the units stand on, in whose frame the mounts are; None for a bench nothing draws.                  |
| `variant`      | `str`             | The variant the arms share, e.g. openyam/v1.                                                             |
| `num_slots`    | `int`             | Slots the arms occupy: the num_joints of an anvil.control.JointState or anvil.control.RobotCommand.      |
| `slot_summary` | `str`             | The arms and the slots they own, for an operator's log: left owns slots 0..8, right owns slots 8..16.    |
| `ik_params`    | `list[IkParams]`  | Each arm's IK parameters, in slot order, as an anvil.control.EePoseController over the robot starts out. |

#### arms

```
arms: list[ArmConfig]
```

The arms, in slot order: the objects themselves, so an edit through one is what that arm then runs.

#### urdf

```
urdf: str | None = None
```

The URDF text the arms stand by, without their grippers, or `None` for the one their variant ships.

For example `Path("mine.urdf").read_text()`. With `mounts` it is the variant's unit, placed once per mount; with none it places the arms itself.

#### mounts

```
mounts: list[Mount] = []
```

Where each copy of `urdf` stands; empty for a `urdf` that places the arms itself.

#### stand

```
stand: Stand | None = None
```

What the units stand on, in whose frame the `mounts` are; `None` for a bench nothing draws.

A preset without leaders stands on the lift, a yam pair on the bracket it carries; one with leaders on none.

#### variant

```
variant: str
```

The variant the arms share, e.g. `openyam/v1`.

Raises:

| Type                   | Description                                          |
| ---------------------- | ---------------------------------------------------- |
| `InvalidArgumentError` | The robot, as edited since it was built, is refused. |

#### num_slots

```
num_slots: int
```

Slots the arms occupy: the `num_joints` of an `anvil.control.JointState` or `anvil.control.RobotCommand`.

Raises:

| Type                   | Description                                          |
| ---------------------- | ---------------------------------------------------- |
| `InvalidArgumentError` | The robot, as edited since it was built, is refused. |

#### slot_summary

```
slot_summary: str
```

The arms and the slots they own, for an operator's log: `left owns slots 0..8, right owns slots 8..16`.

Each range is half-open: `0..8` is slots 0 to 7.

Raises:

| Type                   | Description                                          |
| ---------------------- | ---------------------------------------------------- |
| `InvalidArgumentError` | The robot, as edited since it was built, is refused. |

#### ik_params

```
ik_params: list[IkParams]
```

Each arm's IK parameters, in slot order, as an `anvil.control.EePoseController` over the robot starts out.

The rest posture is the one each arm's description states, as that arm sees it; the rest are the solver's defaults. Each read builds them afresh, so an edit to one changes nothing until it is handed to a solve or a controller.

Raises:

| Type                   | Description                                          |
| ---------------------- | ---------------------------------------------------- |
| `InvalidArgumentError` | The robot, as edited since it was built, is refused. |

#### arms_where

```
arms_where(
    side: Side | None = None, role: Role | None = None
) -> list[ArmConfig]
```

Returns the arms of the side and role asked for, in slot order: the objects themselves.

A filter left `None` matches every arm, so `arms_where()` is `arms`. An arm with no side matches no side: on a single-arm cell, `arms_where(side=Side.LEFT)` is `[]` and the arm is `arms[0]`.

Parameters:

| Name   | Type           | Description                         | Default |
| ------ | -------------- | ----------------------------------- | ------- |
| `side` | `Side \| None` | The side to match, or None for any. | `None`  |
| `role` | `Role \| None` | The role to match, or None for any. | `None`  |

#### arm

```
arm(name: str) -> ArmConfig | None
```

Returns the arm named `name`, the object itself, or `None` when the robot has no arm of that name.

Parameters:

| Name   | Type  | Description                                 | Default    |
| ------ | ----- | ------------------------------------------- | ---------- |
| `name` | `str` | The arm's name, as its ArmConfig states it. | *required* |

#### layout

```
layout() -> RobotLayout
```

Returns the robot's slot layout as it stands now, as a snapshot.

Each slot property of the robot and of its arms works the layout out again; take it once where a loop reads it repeatedly.

Raises:

| Type                   | Description                                          |
| ---------------------- | ---------------------------------------------------- |
| `InvalidArgumentError` | The robot, as edited since it was built, is refused. |

#### camera_links

```
camera_links() -> list[str]
```

Returns every `*_cam` link of the composed URDF: the links a `CameraConfig.link` may name.

The sim renders each of these as the camera riding on it.

Raises:

| Type                   | Description              |
| ---------------------- | ------------------------ |
| `InvalidArgumentError` | As composed_urdf raises. |

#### mount_joints

```
mount_joints() -> list[str]
```

Returns the URDF joint each of `mounts` stands its copy of the unit by, in the mounts' order.

That joint is what moves the whole unit, as a viewer dragging it would. Empty for a robot with no mounts.

Raises:

| Type                   | Description                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | The robot is refused, or its urdf does not parse or hangs no joint off its world link. |

#### mesh_files

```
mesh_files() -> list[str]
```

Returns every mesh the composed URDF names, each once, by its path under the robot descriptions' root.

These are what has to travel beside the composed URDF for it to load elsewhere, e.g. `arms/openyam/v1/meshes/base_link_collision_c00.stl`.

Raises:

| Type                   | Description              |
| ---------------------- | ------------------------ |
| `InvalidArgumentError` | As composed_urdf raises. |

#### urdf_joints

```
urdf_joints() -> list[UrdfJoint]
```

Returns every moving joint of `composed_urdf` with the slot it follows, in the composed URDF's order.

This is how to pose a drawing of the robot from the loop's state.

Raises:

| Type                   | Description              |
| ---------------------- | ------------------------ |
| `InvalidArgumentError` | As composed_urdf raises. |

#### composed_urdf

```
composed_urdf() -> str
```

Returns the whole robot as URDF text: what gravity compensation, the sim and a recording read.

`urdf` placed once per mount, each arm's gripper URDF and finger set grafted onto the `hand` link it ends at, and each gripper's mass added to the link its fingers hang from, or an external gripper's to `hand`; the camera a `stand` carries is a link fixed to `world`. Every mesh is named by its path under the robot descriptions' root.

Raises:

| Type                   | Description                                                                                                                                                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | The robot is refused, or a URDF does not parse, lacks the link a gripper mounts on, or already carries a joint a gripper or the stand adds; the message names the link or joint; a stand's camera on a URDF with no world link. |

#### chains

```
chains() -> list[Chain]
```

Returns one `anvil.kinematics.Chain` per arm, in slot order, cut from the composed URDF to its tool frame.

Raises:

| Type                   | Description                                                                                                                                             |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | As composed_urdf raises, or as anvil.kinematics.Chain.from_urdf_text raises for an arm's chain, such as the composed URDF lacking the arm's tool frame. |

#### home_rows_at

```
home_rows_at(q: Sequence[float]) -> list[dict[str, Any]]
```

Returns one homing row per arm, in slot order, standing each where `q` has it: what a row is taught from.

Move the arms there by hand, then put each row in its arm's `ArmDescription.home`: the first row is the park pose, the last the home pose, the rows between waypoints. Each row is as the left or only arm sees it, as `home` states its rows, every joint held inside its travel and the gripper inside its opening, so a reading a hair past a stop still plays; its leg is paced at the run's own speed.

Example

```
rows = robot.home_rows_at(q=runtime.latest().q)
for arm, row in zip(robot.arms, rows, strict=True):
    arm.description.home["rows"][0] = row  # the park pose
```

Parameters:

| Name | Type              | Description                                                                                               | Default    |
| ---- | ----------------- | --------------------------------------------------------------------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | Position per slot, in radians: a anvil.control.Snapshot's q. Entries past the robot's slots are not read. | *required* |

Returns:

| Type                   | Description                                                                                                                                                                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list[dict[str, Any]]` | One row per arm, in slot order, each a dict as ArmDescription.home holds its rows: q_deg, the chain joints in degrees; grip_m, the gripper's opening in meters, 0.0 for an arm with no gripper the stack drives; and tcp_speed_mps, None. |

Raises:

| Type                   | Description                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------- |
| `TypeError`            | q holds something other than numbers.                                                     |
| `InvalidArgumentError` | The robot is refused, or q does not cover every slot or holds a value that is not finite. |

#### home_trajectory

```
home_trajectory(from_q: Sequence[float]) -> TimedWaypoints
```

Returns the run that homes the robot from where its arms are, through the homing sequence each describes.

Each segment lasts as long as the arm whose tip travels farthest needs at the speed the description states for it, or the joint that turns farthest needs at 60 degrees a second, whichever is longer, and never less than half a second; the waypoints are passed through without overshoot and the last is reached at rest. Play it on a `anvil.control.JointTrajectoryController` over the same robot.

Parameters:

| Name     | Type              | Description                                                                                                                      | Default    |
| -------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `from_q` | `Sequence[float]` | Position per slot where the arms are now, in radians: a anvil.control.Snapshot's q. Entries past the robot's slots are not read. | *required* |

Raises:

| Type                   | Description                                                                                                                                                    |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | from_q holds something other than numbers.                                                                                                                     |
| `InvalidArgumentError` | The robot is refused; an arm's description states no homing sequence, or the arms' sequences have different numbers of rows; from_q does not cover every slot. |

#### dehome_trajectory

```
dehome_trajectory(
    from_q: Sequence[float],
) -> TimedWaypoints
```

Returns the homing run backwards, from where the arms are to the row a homing starts from, ending at rest.

It passes through the home rows last to first, each leg at the pace it is played forward, timed as `home_trajectory` times it. What a teleop session plays before its drives are disabled.

Parameters:

| Name     | Type              | Description                                                                         | Default    |
| -------- | ----------------- | ----------------------------------------------------------------------------------- | ---------- |
| `from_q` | `Sequence[float]` | Position per slot where the arms are now, in radians: a anvil.control.Snapshot's q. | *required* |

Raises:

| Type                   | Description                                |
| ---------------------- | ------------------------------------------ |
| `TypeError`            | from_q holds something other than numbers. |
| `InvalidArgumentError` | As home_trajectory raises.                 |

#### rehome_trajectory

```
rehome_trajectory(
    from_q: Sequence[float],
) -> TimedWaypoints
```

Returns a run straight to where a homing ends, in one segment from where the arms are.

What a teleop session plays when the operator asks for the arms back at home mid-session.

Parameters:

| Name     | Type              | Description                                                                         | Default    |
| -------- | ----------------- | ----------------------------------------------------------------------------------- | ---------- |
| `from_q` | `Sequence[float]` | Position per slot where the arms are now, in radians: a anvil.control.Snapshot's q. | *required* |

Raises:

| Type                   | Description                                |
| ---------------------- | ------------------------------------------ |
| `TypeError`            | from_q holds something other than numbers. |
| `InvalidArgumentError` | As home_trajectory raises.                 |

#### to_json

```
to_json() -> str
```

Returns the robot as JSON, the form `from_json` reads back.

#### from_json

```
from_json(wire: str) -> Self
```

Builds a robot from the JSON `to_json` wrote, checked as one built from its fields is.

JSON an older release wrote reads as it was written: a field added since takes its default.

Parameters:

| Name   | Type  | Description    | Default    |
| ------ | ----- | -------------- | ---------- |
| `wire` | `str` | The JSON text. | *required* |

Raises:

| Type                   | Description                                                                       |
| ---------------------- | --------------------------------------------------------------------------------- |
| `InvalidArgumentError` | wire is not a robot's JSON, or the robot is refused, as the class docstring says. |

### NearestPair

```
NearestPair(
    link_a: str,
    link_b: str,
    distance_m: float,
    point_a_m: tuple[float, float, float],
    point_b_m: tuple[float, float, float],
)
```

Two links of a robot checked against each other and their gap, from `Clearance.nearest` or `within`.

Attributes:

| Name         | Type                         | Description                                                                        |
| ------------ | ---------------------------- | ---------------------------------------------------------------------------------- |
| `link_a`     | `str`                        | One piece's link, as the composed URDF names it.                                   |
| `link_b`     | `str`                        | The other piece's link.                                                            |
| `distance_m` | `float`                      | The gap between them, in meters; zero where they touch or overlap.                 |
| `point_a_m`  | `tuple[float, float, float]` | The point of link_a's piece at one end of the gap, in the world frame, in meters.  |
| `point_b_m`  | `tuple[float, float, float]` | The point of link_b's piece at the gap's other end, in the world frame, in meters. |

#### link_a

```
link_a: str
```

One piece's link, as the composed URDF names it.

#### link_b

```
link_b: str
```

The other piece's link.

#### distance_m

```
distance_m: float
```

The gap between them, in meters; zero where they touch or overlap.

#### point_a_m

```
point_a_m: tuple[float, float, float]
```

The point of `link_a`'s piece at one end of the gap, in the world frame, in meters.

#### point_b_m

```
point_b_m: tuple[float, float, float]
```

The point of `link_b`'s piece at the gap's other end, in the world frame, in meters.

### NearestLink

```
NearestLink(
    link: str,
    distance_m: float,
    point_m: tuple[float, float, float],
    box_point_m: tuple[float, float, float],
)
```

A link of the robot and its gap to a keep-out box, from `Clearance.nearest_to` or `within_of`.

Attributes:

| Name          | Type                         | Description                                                                                            |
| ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| `link`        | `str`                        | The piece's link, as the composed URDF names it.                                                       |
| `distance_m`  | `float`                      | The gap between it and the box, in meters. Where they touch or overlap, zero from nearest_to, and from |
| `point_m`     | `tuple[float, float, float]` | The point of the piece at one end of the gap, in the world frame, in meters.                           |
| `box_point_m` | `tuple[float, float, float]` | The point of the box at the gap's other end, in the world frame, in meters.                            |

#### link

```
link: str
```

The piece's link, as the composed URDF names it.

#### distance_m

```
distance_m: float
```

The gap between it and the box, in meters. Where they touch or overlap, zero from `nearest_to`, and from `within_of` minus how far the link must move to leave the box.

#### point_m

```
point_m: tuple[float, float, float]
```

The point of the piece at one end of the gap, in the world frame, in meters.

#### box_point_m

```
box_point_m: tuple[float, float, float]
```

The point of the box at the gap's other end, in the world frame, in meters.

### KeepOutBox

```
KeepOutBox(
    name: str,
    center_m: tuple[float, float, float],
    size_m: tuple[float, float, float],
)
```

A box in the workcell the robot is kept out of: a table, a wall, a fixture.

It stands square to the world frame, the one `Mount` places the units in. Its name must be unique in the workcell and every side a positive length, checked where the workcell is built or next handed over.

Attributes:

| Name       | Type                         | Description                                                 |
| ---------- | ---------------------------- | ----------------------------------------------------------- |
| `name`     | `str`                        | Its name, unique in the workcell, e.g. table.               |
| `center_m` | `tuple[float, float, float]` | Its center in the world frame, in meters.                   |
| `size_m`   | `tuple[float, float, float]` | Its length along each of the world frame's axes, in meters. |

#### name

```
name: str
```

Its name, unique in the workcell, e.g. `table`.

#### center_m

```
center_m: tuple[float, float, float]
```

Its center in the world frame, in meters.

#### size_m

```
size_m: tuple[float, float, float]
```

Its length along each of the world frame's axes, in meters.

### Clearance

```
Clearance(robot: RobotConfig)
```

How near a robot's links come to one another and to keep-out boxes, posed from its motors' positions.

Built once for a robot and asked as often as needed; it keeps no reference to the robot, so a later edit to the robot does not reach it. Every collision piece of the composed URDF is checked, except the pairs each arm's `ArmDescription.excluded_pairs` names. Each piece stands for a few dozen points on its convex hull, so a gap reads a few millimeters wide of the meshes' on a rounded piece. Freed when the object is garbage-collected.

Builds the clearance of `robot` as it is now.

Parameters:

| Name    | Type          | Description                        | Default    |
| ------- | ------------- | ---------------------------------- | ---------- |
| `robot` | `RobotConfig` | The robot whose links are checked. | *required* |

Raises:

| Type                   | Description                                                                                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | robot is not a RobotConfig, such as a whole WorkcellConfig.                                                                                                      |
| `InvalidArgumentError` | The robot is refused, its URDF cannot be composed, or it names a collision mesh the SDK has no hull for, as a description read from a directory of your own can. |

Methods:

| Name         | Description                                                                                         |
| ------------ | --------------------------------------------------------------------------------------------------- |
| `arm_of`     | Returns the index into the robot's RobotConfig.arms of the arm that carries link.                   |
| `nearest`    | Returns the nearest two pieces that are checked against each other, with the motors at q.           |
| `nearest_to` | Returns the robot's piece nearest keep_out, with the motors at q, checking every piece.             |
| `within`     | Returns every pair of links checked against each other that stand no more than within_m apart at q. |
| `within_of`  | Returns every link of the robot that stands no more than within_m from keep_out at q.               |

#### arm_of

```
arm_of(link: str) -> int | None
```

Returns the index into the robot's `RobotConfig.arms` of the arm that carries `link`.

An arm carries every link whose name starts with its prefix in the composed URDF, the longest prefix where several match; a robot of one follower carries every link.

Parameters:

| Name   | Type  | Description                                               | Default    |
| ------ | ----- | --------------------------------------------------------- | ---------- |
| `link` | `str` | The link, as the composed URDF names it, e.g. left_link5. | *required* |

Returns:

| Type          | Description                                                                                  |
| ------------- | -------------------------------------------------------------------------------------------- |
| `int \| None` | The arm's index; None for a link no arm carries, such as the torso a pair of arms stands on. |

#### nearest

```
nearest(q: Sequence[float]) -> NearestPair | None
```

Returns the nearest two pieces that are checked against each other, with the motors at `q`.

Parameters:

| Name | Type              | Description                                                                                                                                       | Default    |
| ---- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `q`  | `Sequence[float]` | Each slot's motor position, in radians, in slot order, as an anvil.control.Snapshot's q carries them. Entries past the robot's slots are ignored. | *required* |

Returns:

| Type                  | Description                                                                                        |
| --------------------- | -------------------------------------------------------------------------------------------------- |
| `NearestPair \| None` | The pair and the gap between them; None for a robot with no two pieces checked against each other. |

Raises:

| Type                   | Description                                                                 |
| ---------------------- | --------------------------------------------------------------------------- |
| `TypeError`            | q is not a flat sequence of numbers.                                        |
| `InvalidArgumentError` | q has fewer entries than the robot has slots, or one of them is not finite. |

#### nearest_to

```
nearest_to(
    q: Sequence[float], keep_out: KeepOutBox
) -> NearestLink | None
```

Returns the robot's piece nearest `keep_out`, with the motors at `q`, checking every piece.

Parameters:

| Name       | Type              | Description                                                | Default    |
| ---------- | ----------------- | ---------------------------------------------------------- | ---------- |
| `q`        | `Sequence[float]` | Each slot's motor position, in slot order, as for nearest. | *required* |
| `keep_out` | `KeepOutBox`      | The box to measure to.                                     | *required* |

Returns:

| Type                  | Description                                                       |
| --------------------- | ----------------------------------------------------------------- |
| `NearestLink \| None` | The piece and the gap; None for a robot with no collision pieces. |

Raises:

| Type                   | Description                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | q is not a flat sequence of numbers.                                                                                                            |
| `InvalidArgumentError` | q has fewer entries than the robot has slots or one of them is not finite, or keep_out does not have three numbers for its center and its size. |

#### within

```
within(
    q: Sequence[float], within_m: float
) -> list[NearestPair]
```

Returns every pair of links checked against each other that stand no more than `within_m` apart at `q`.

Each pair is judged on its own, so a pair that stands close however the arms move does not hide another one coming near, as it would behind `nearest`.

Parameters:

| Name       | Type              | Description                                                                                          | Default    |
| ---------- | ----------------- | ---------------------------------------------------------------------------------------------------- | ---------- |
| `q`        | `Sequence[float]` | Each slot's motor position, in slot order, as for nearest.                                           | *required* |
| `within_m` | `float`           | How far apart a pair may stand and still be returned, in meters; a negative or NaN one returns none. | *required* |

Returns:

| Type                | Description                                                                                        |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| `list[NearestPair]` | Each pair at the gap between its nearest two pieces, nearest first; empty where none is that near. |

Raises:

| Type                   | Description                                                                 |
| ---------------------- | --------------------------------------------------------------------------- |
| `TypeError`            | q is not a flat sequence of numbers.                                        |
| `InvalidArgumentError` | q has fewer entries than the robot has slots, or one of them is not finite. |

#### within_of

```
within_of(
    q: Sequence[float],
    keep_out: KeepOutBox,
    within_m: float,
) -> list[NearestLink]
```

Returns every link of the robot that stands no more than `within_m` from `keep_out` at `q`.

Parameters:

| Name       | Type              | Description                                                                                                                                           | Default    |
| ---------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `q`        | `Sequence[float]` | Each slot's motor position, in slot order, as for nearest.                                                                                            | *required* |
| `keep_out` | `KeepOutBox`      | The box to measure to.                                                                                                                                | *required* |
| `within_m` | `float`           | How far from the box a link may stand and still be returned, in meters; a negative one returns only links at least that deep inside it, and NaN none. | *required* |

Returns:

| Type                | Description                                                                                                                                                                                                                      |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list[NearestLink]` | Each link at the gap from its nearest piece, nearest first; a link overlapping the box at minus how far it must move along one of the box's axes to leave it, so one sinking deeper reads nearer. Empty where none is that near. |

Raises:

| Type                   | Description                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `TypeError`            | q is not a flat sequence of numbers.                                                                                                            |
| `InvalidArgumentError` | q has fewer entries than the robot has slots or one of them is not finite, or keep_out does not have three numbers for its center and its size. |

### CameraRole

Bases: `StrEnum`

What a camera looks at.

An operator's screen shows the scene cameras big and the wrist cameras small.

Attributes:

| Name    | Type | Description                                                         |
| ------- | ---- | ------------------------------------------------------------------- |
| `SCENE` |      | The workspace, from off the arms: a head, chest or overhead camera. |
| `WRIST` |      | One arm's gripper, from a camera riding on that arm.                |

#### SCENE

```
SCENE
```

The workspace, from off the arms: a head, chest or overhead camera.

#### WRIST

```
WRIST
```

One arm's gripper, from a camera riding on that arm.

### Lens

Bases: `StrEnum`

The lens fitted to a camera, which cannot be read off the camera, so a workcell says which.

Attributes:

| Name             | Type    | Description                                                      |
| ---------------- | ------- | ---------------------------------------------------------------- |
| `STANDARD`       |         | Rectilinear, 92 degrees diagonal.                                |
| `PSEUDO_FISHEYE` |         | 151 degrees diagonal.                                            |
| `FISHEYE`        |         | 186 degrees diagonal.                                            |
| `fov_h_deg`      | `float` | Horizontal field of view, in degrees, from the lens's datasheet. |
| `fov_v_deg`      | `float` | Vertical field of view, in degrees, from the lens's datasheet.   |

#### STANDARD

```
STANDARD
```

Rectilinear, 92 degrees diagonal.

#### PSEUDO_FISHEYE

```
PSEUDO_FISHEYE
```

151 degrees diagonal.

#### FISHEYE

```
FISHEYE
```

186 degrees diagonal.

#### fov_h_deg

```
fov_h_deg: float
```

Horizontal field of view, in degrees, from the lens's datasheet.

#### fov_v_deg

```
fov_v_deg: float
```

Vertical field of view, in degrees, from the lens's datasheet.

### CameraConfig

```
CameraConfig(
    name: str,
    device: str,
    mode: Mode,
    role: CameraRole = CameraRole.SCENE,
    link: str | None = None,
    lens: Lens = Lens.STANDARD,
    controls: dict[str, int] = {},
)
```

One camera of a workcell: its name, the device it opens, the mode it streams, and where it is.

Checked where the workcell is built: a name that is empty or used twice, or a device used twice, is refused. An empty `device` opens nothing, so any number of sim cameras share it.

Attributes:

| Name       | Type             | Description                                                                                          |
| ---------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
| `name`     | `str`            | Its name, unique in the workcell, e.g. "head" or "wrist_l": what a recording and a viewer call it.   |
| `device`   | `str`            | The device node it opens, as given.                                                                  |
| `mode`     | `Mode`           | The anvil.camera.Mode it streams, one of those anvil.camera.modes lists for the device.              |
| `role`     | `CameraRole`     | What it looks at.                                                                                    |
| `link`     | `str \| None`    | The robot link it rides on, one of RobotConfig.camera_links(), which the sim renders as this camera. |
| `lens`     | `Lens`           | The lens fitted to it.                                                                               |
| `controls` | `dict[str, int]` | The camera's own settings to write when it opens, each by the key anvil.camera.controls gives it.    |

#### name

```
name: str
```

Its name, unique in the workcell, e.g. `"head"` or `"wrist_l"`: what a recording and a viewer call it.

#### device

```
device: str
```

The device node it opens, as given.

`"/dev/video0"`, or a path that stays put across boots, such as its USB port's under `/dev/v4l/by-path/`.

#### mode

```
mode: Mode
```

The `anvil.camera.Mode` it streams, one of those `anvil.camera.modes` lists for the device.

#### role

```
role: CameraRole = CameraRole.SCENE
```

What it looks at.

A workcell saved before cameras had a role reads as `CameraRole.SCENE`.

#### link

```
link: str | None = None
```

The robot link it rides on, one of `RobotConfig.camera_links()`, which the sim renders as this camera.

`None` for a camera on nothing of the robot's, which the sim has no view of.

#### lens

```
lens: Lens = Lens.STANDARD
```

The lens fitted to it.

A workcell saved before cameras had a lens reads as `Lens.STANDARD`.

#### controls

```
controls: dict[str, int] = {}
```

The camera's own settings to write when it opens, each by the key `anvil.camera.controls` gives it.

For example `{"exposure_time_absolute": 156}`. A setting not named keeps what the camera holds.

### Preset

```
Preset(
    name: str, variant: str, arms: ArmCount, leaders: bool
)
```

A shipped workcell's name and shape, from `WorkcellConfig.preset_shapes`, building nothing until asked.

Methods:

| Name    | Description                                                              |
| ------- | ------------------------------------------------------------------------ |
| `build` | Returns the workcell, built fresh: WorkcellConfig.shipped of this shape. |

Attributes:

| Name      | Type       | Description                                                             |
| --------- | ---------- | ----------------------------------------------------------------------- |
| `name`    | `str`      | The name of its WorkcellConfig class attribute, e.g. OPENYAM_V1_SINGLE. |
| `variant` | `str`      | The arms' variant, e.g. openyam/v1.                                     |
| `arms`    | `ArmCount` | How many followers it stands.                                           |
| `leaders` | `bool`     | Whether each follower has a leader.                                     |

#### name

```
name: str
```

The name of its `WorkcellConfig` class attribute, e.g. `OPENYAM_V1_SINGLE`.

#### variant

```
variant: str
```

The arms' variant, e.g. `openyam/v1`.

#### arms

```
arms: ArmCount
```

How many followers it stands.

#### leaders

```
leaders: bool
```

Whether each follower has a leader.

#### build

```
build() -> WorkcellConfig
```

Returns the workcell, built fresh: `WorkcellConfig.shipped` of this shape.

### WorkcellConfig

```
WorkcellConfig(
    robot: RobotConfig,
    cameras: list[CameraConfig] = [],
    keep_out: list[KeepOutBox] = [],
)
```

A workcell: its robot, the cameras it streams, and the boxes its robot is kept out of.

The shipped workcells are class attributes, `WorkcellConfig.OPENARM_V2_BIMANUAL`, each built fresh on every access with no cameras and no keep-out boxes, since which a bench has is the bench's. Edit the arms, append a `CameraConfig` per camera and a `KeepOutBox` per obstacle, and `save` it; then hand `robot` to the hw interface and controllers you compose, and the workcell to `anvil.camera.Cameras`.

Raises:

| Type                   | Description                                                                                                                                                                                                                        |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | A camera with no name, two cameras of one name or of one non-empty device, a keep-out box with no name, two boxes of one name, or a box side that is not a positive length; or a robot refused as RobotConfig says, after an edit. |

Methods:

| Name            | Description                                                                                                 |
| --------------- | ----------------------------------------------------------------------------------------------------------- |
| `to_json`       | Returns the workcell as JSON, the form from_json reads back.                                                |
| `from_json`     | Builds a workcell from the JSON to_json wrote, checked as one built from its fields is.                     |
| `load`          | Reads the workcell save wrote to path, checked as one built from its fields is.                             |
| `save`          | Writes the workcell to path as indented JSON a person can edit, for load to read back.                      |
| `presets`       | Returns every shipped workcell by its class attribute's name, each built fresh, e.g. for a --workcell flag. |
| `preset_shapes` | Returns every shipped workcell's name and shape, building none: what to choose among by variant and shape.  |
| `shipped`       | Builds a workcell of a shipped variant's arms, standing where the shipped stands bolt them.                 |

Attributes:

| Name                                | Type                 | Description                                                                                                 |
| ----------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------- |
| `robot`                             | `RobotConfig`        | The robot the run drives.                                                                                   |
| `cameras`                           | `list[CameraConfig]` | The cameras, in the order stated.                                                                           |
| `keep_out`                          | `list[KeepOutBox]`   | The boxes the robot is kept out of.                                                                         |
| `OPENARM_V1_BIMANUAL`               |                      | Two OpenArm v1 arms, one torso, on the lift.                                                                |
| `OPENARM_V2_BIMANUAL`               |                      | Two OpenArm v2 arms, one torso, on the lift.                                                                |
| `OPENYAM_V1_BIMANUAL`               |                      | Two OpenYAM v1 arms on the lift's bracket, where its ports place them, 0.61 m apart.                        |
| `OPENYAM_V1_SINGLE`                 |                      | One OpenYAM v1 arm, follower, with no side, on the lift.                                                    |
| `OPENARM_V1_LEADER_FOLLOWER`        |                      | Two OpenArm v1 torsos on no stand: the followers at the origin, and a leader torso 1 m to their left.       |
| `OPENARM_V2_LEADER_FOLLOWER`        |                      | Two OpenArm v2 torsos on no stand: the followers at the origin, and a leader torso 1 m to their left.       |
| `OPENYAM_V1_LEADER_FOLLOWER`        |                      | Four OpenYAM v1 arms on no stand: the followers spaced as on the bracket, each leader 0.6 m behind its own. |
| `OPENYAM_V1_SINGLE_LEADER_FOLLOWER` |                      | One OpenYAM v1 follower on no stand and its leader 0.6 m behind it, neither with a side.                    |
| `OPENYAM_V2_BIMANUAL`               |                      | Two OpenYAM v2 arms on the lift's bracket, where its ports place them, 0.61 m apart.                        |
| `OPENYAM_V2_SINGLE`                 |                      | One OpenYAM v2 arm, follower, with no side, on the lift.                                                    |
| `OPENYAM_V2_LEADER_FOLLOWER`        |                      | Four OpenYAM v2 arms on no stand: the followers spaced as on the bracket, each leader 0.6 m behind its own. |
| `OPENYAM_V2_SINGLE_LEADER_FOLLOWER` |                      | One OpenYAM v2 follower on no stand and its leader 0.6 m behind it, neither with a side.                    |

#### robot

```
robot: RobotConfig
```

The robot the run drives.

#### cameras

```
cameras: list[CameraConfig] = []
```

The cameras, in the order stated.

#### keep_out

```
keep_out: list[KeepOutBox] = []
```

The boxes the robot is kept out of.

#### OPENARM_V1_BIMANUAL

```
OPENARM_V1_BIMANUAL
```

Two OpenArm v1 arms, one torso, on the lift.

#### OPENARM_V2_BIMANUAL

```
OPENARM_V2_BIMANUAL
```

Two OpenArm v2 arms, one torso, on the lift.

#### OPENYAM_V1_BIMANUAL

```
OPENYAM_V1_BIMANUAL
```

Two OpenYAM v1 arms on the lift's bracket, where its ports place them, 0.61 m apart.

#### OPENYAM_V1_SINGLE

```
OPENYAM_V1_SINGLE
```

One OpenYAM v1 arm, `follower`, with no side, on the lift.

#### OPENARM_V1_LEADER_FOLLOWER

```
OPENARM_V1_LEADER_FOLLOWER
```

Two OpenArm v1 torsos on no stand: the followers at the origin, and a leader torso 1 m to their left.

#### OPENARM_V2_LEADER_FOLLOWER

```
OPENARM_V2_LEADER_FOLLOWER
```

Two OpenArm v2 torsos on no stand: the followers at the origin, and a leader torso 1 m to their left.

#### OPENYAM_V1_LEADER_FOLLOWER

```
OPENYAM_V1_LEADER_FOLLOWER
```

Four OpenYAM v1 arms on no stand: the followers spaced as on the bracket, each leader 0.6 m behind its own.

#### OPENYAM_V1_SINGLE_LEADER_FOLLOWER

```
OPENYAM_V1_SINGLE_LEADER_FOLLOWER
```

One OpenYAM v1 follower on no stand and its leader 0.6 m behind it, neither with a side.

#### OPENYAM_V2_BIMANUAL

```
OPENYAM_V2_BIMANUAL
```

Two OpenYAM v2 arms on the lift's bracket, where its ports place them, 0.61 m apart.

#### OPENYAM_V2_SINGLE

```
OPENYAM_V2_SINGLE
```

One OpenYAM v2 arm, `follower`, with no side, on the lift.

#### OPENYAM_V2_LEADER_FOLLOWER

```
OPENYAM_V2_LEADER_FOLLOWER
```

Four OpenYAM v2 arms on no stand: the followers spaced as on the bracket, each leader 0.6 m behind its own.

#### OPENYAM_V2_SINGLE_LEADER_FOLLOWER

```
OPENYAM_V2_SINGLE_LEADER_FOLLOWER
```

One OpenYAM v2 follower on no stand and its leader 0.6 m behind it, neither with a side.

#### to_json

```
to_json() -> str
```

Returns the workcell as JSON, the form `from_json` reads back.

#### from_json

```
from_json(wire: str) -> Self
```

Builds a workcell from the JSON `to_json` wrote, checked as one built from its fields is.

JSON an older release wrote reads as it was written: a field added since takes its default.

Parameters:

| Name   | Type  | Description    | Default    |
| ------ | ----- | -------------- | ---------- |
| `wire` | `str` | The JSON text. | *required* |

Raises:

| Type                   | Description                                                |
| ---------------------- | ---------------------------------------------------------- |
| `InvalidArgumentError` | wire is not a workcell's JSON, or the workcell is refused. |

#### load

```
load(path: Path) -> Self
```

Reads the workcell `save` wrote to `path`, checked as one built from its fields is.

A file an older release saved reads as it was saved: a field added since takes its default.

Parameters:

| Name   | Type   | Description       | Default    |
| ------ | ------ | ----------------- | ---------- |
| `path` | `Path` | The file to read. | *required* |

Raises:

| Type                   | Description                                                                                                   |
| ---------------------- | ------------------------------------------------------------------------------------------------------------- |
| `NotFoundError`        | There is no file at path.                                                                                     |
| `InvalidArgumentError` | The file is not a workcell, a newer release saved it, or the workcell is refused; the message names the path. |
| `OperationError`       | The file could not be read for another reason, such as being a directory.                                     |

#### save

```
save(path: Path) -> None
```

Writes the workcell to `path` as indented JSON a person can edit, for `load` to read back.

The file is `to_json`'s JSON with the format's `version` beside it. The directory is made if it is missing, and the file is replaced whole, so a crash mid-save leaves the old file or the new one, never a torn one.

Parameters:

| Name   | Type   | Description        | Default    |
| ------ | ------ | ------------------ | ---------- |
| `path` | `Path` | The file to write. | *required* |

Raises:

| Type                   | Description                                                                                                                                                |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidArgumentError` | The workcell is refused; nothing is written.                                                                                                               |
| `OperationError`       | The directory could not be made or the file could not be written; a subclass where the failure names the reason, as for load, such as anvil.NotFoundError. |

#### presets

```
presets() -> dict[str, WorkcellConfig]
```

Returns every shipped workcell by its class attribute's name, each built fresh, e.g. for a `--workcell` flag.

Builds all of them; `preset_shapes` lists them without building any.

#### preset_shapes

```
preset_shapes() -> list[Preset]
```

Returns every shipped workcell's name and shape, building none: what to choose among by variant and shape.

#### shipped

```
shipped(
    variant: str, arms: ArmCount, leaders: bool
) -> Self
```

Builds a workcell of a shipped variant's arms, standing where the shipped stands bolt them.

Every preset is one of these shapes, and a shape no preset names is built the same way, e.g. a single `yam/v1`. A pair's arms are `left` and `right`, their leaders `leader_left` and `leader_right`; a single arm is `follower` and its leader `leader`, neither with a side. Each leader carries the leader handle in place of a gripper. Without leaders it stands on `Stand.LIFT`, a yam pair on `Stand.LIFT_BRACKET`; with leaders on no stand, a yam pair spaced as the bracket spaces it. The workcell has no cameras and no keep-out boxes.

Parameters:

| Name      | Type       | Description                                | Default    |
| --------- | ---------- | ------------------------------------------ | ---------- |
| `variant` | `str`      | The variant, family/vN, e.g. "openyam/v1". | *required* |
| `arms`    | `ArmCount` | One follower or a pair.                    | *required* |
| `leaders` | `bool`     | Whether each follower gets a leader.       | *required* |

Raises:

| Type                   | Description                                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `InvalidArgumentError` | Anvil ships no description or URDF for variant, or arms is ArmCount.SINGLE for an OpenArm, whose unit is a torso carrying both arms. |

### shipped_urdf

```
shipped_urdf(variant: str) -> str
```

Returns the URDF text a shipped variant carries: the unit a robot's `mounts` stand copies of.

For a yam that is one arm on its base plate; for an OpenArm, the torso carrying both arms.

Parameters:

| Name      | Type  | Description                                | Default    |
| --------- | ----- | ------------------------------------------ | ---------- |
| `variant` | `str` | The variant, family/vN, e.g. "openyam/v1". | *required* |

Raises:

| Type                   | Description                      |
| ---------------------- | -------------------------------- |
| `InvalidArgumentError` | Anvil ships no URDF for variant. |

### realhand_gripper

```
realhand_gripper(
    family: str, version: int, side: Side
) -> ExternalGripper
```

Returns the external gripper an arm states for the RealHand L6 bolted to it, as the hand's description gives it.

It holds the CAN ID the `side` hand answers on, which the arm's motors must not use, and the hand's mass in the arm's `hand` frame, which gravity compensation carries. Set it as that arm's `ArmDescription.gripper`.

Parameters:

| Name      | Type   | Description                                                                          | Default    |
| --------- | ------ | ------------------------------------------------------------------------------------ | ---------- |
| `family`  | `str`  | The arm's family, ArmDescription.family, e.g. "openarm".                             | *required* |
| `version` | `int`  | The arm's version, ArmDescription.version, e.g. 2.                                   | *required* |
| `side`    | `Side` | Which hand: the two answer on different CAN IDs, and their masses mirror each other. | *required* |

Raises:

| Type                   | Description                                     |
| ---------------------- | ----------------------------------------------- |
| `ValueError`           | side is not a side.                             |
| `InvalidArgumentError` | The hand's description has no mass on that arm. |

### shipped_finger_sets

```
shipped_finger_sets(gripper: str) -> list[FingerSet]
```

Returns every finger set Anvil ships for `gripper`, in name order: what its `Gripper.fingers` may be set to.

Parameters:

| Name      | Type  | Description                                      | Default    |
| --------- | ----- | ------------------------------------------------ | ---------- |
| `gripper` | `str` | The gripper, family/vN, e.g. "anvil-gripper/v1". | *required* |

Returns:

| Type              | Description                                                                                               |
| ----------------- | --------------------------------------------------------------------------------------------------------- |
| `list[FingerSet]` | The finger sets; empty for a gripper whose fingers are its own, and for a name Anvil ships no gripper by. |
