Skip to main content
Full-robot factory calibration: identifies friction (tune.friction) and gravity CoM (tune.gravity) for all 14 joints (both arms × 7) in one command. Also available from the diagnostics dashboard’s Tuning workbench (axol serve). One bidirectional multi-velocity sweep per joint yields both fits — the half-difference of forward/backward torque gives the friction model, the average gives the gravity curve the CoM is fit to, with the friction offset Fo refit against the corrected model. The default slow profile is tune.friction --profile slow’s sweep (1, 2, 3, 5, 8, 15, 30°/s, the speeds under 8°/s in a low- and a high-load window) fitted with the realtime core’s own friction law — sliding fc + fl·|g|, viscous fv, and the low-speed Stribeck excess dfs + ls·|g| over vs — with the full-range passes feeding the gravity fit; the Stribeck share cancelled is saved per joint (0.8 on shoulder_1, shoulder_2 and elbow, 0 elsewhere). About 6 min per joint, ~1.5 h for both arms. --profile standard is the old, quicker 7–72°/s Coulomb + viscous sweep. Each arm runs distal → proximal (wrist_3 → … → shoulder_1) and every joint’s fit is saved before the next joint sweeps, so proximal sweeps see already-corrected distal links — the ordering tune.gravity asks the operator to keep by hand happens automatically here. Sweep safety and the gravity-load poses (base-collision caps, elbow raised for wrist_2, camera clearance, and axis-vertical joints posed so gravity actually loads them — see tune.gravity) match the individual tools. Results are saved to ~/.almond/calibration.json (this machine uses them immediately) and uploaded to Supabase Storage keyed by the robot’s identity — the Axol hub adapter’s USB serial, which travels with the arms across compute-host swaps. The documents live in a public bucket (axol-calibrations/<hub_serial>.json), so fetching them needs no credentials at all — only uploading does. The upload only happens when the write key is configured; without it the run calibrates locally and says so:
One-time Supabase setup: create a Storage bucket named axol-calibrations and mark it public. No table or RLS policies are needed — write access comes from the service key. The upload merges over what the cloud already stores — per joint and per field — so a one-arm re-run never wipes the other arm’s data, and a joint whose gravity fit is rejected this time keeps its previously uploaded com / mass. An interrupted run keeps (and uploads) every joint that completed.

Custom end-effectors

The gravity fit only identifies each link’s first moment (mass × CoM) and keeps the mass fixed, so a gripper much heavier or longer than stock either pushes the fitted CoM shift past the 60 mm plausibility cap (the fit is rejected) or leaves the proximal joints that carry it unmodeled. Give the link mass up front instead: the gripper is lumped into wrist_3, so --mass wrist_3=<kg> sets that link’s mass, and --com optionally seeds its starting CoM (metres, in that arm’s URDF link frame). Both are written to ~/.almond/calibration.json before the sweeps, so every fit — including the proximal joints carrying the gripper — runs against the custom numbers, and they are uploaded with the calibration even if a fit fails. Every fitted CoM (here and from tune.gravity --save) is stored with the mass it was fitted against, so the pair stays consistent on every machine that runs calibration.pull. To go back to a standard gripper, pass the stock mass (--mass wrist_3=0.75) and refit. A --mass without a side applies to every arm being calibrated. --com must name its side (left. / right.), because CoMs mirror between the arms. Naming an arm that isn’t being calibrated, an unknown joint, or a non-positive mass is an error before anything moves.

Keeping a hand-tuned gravity model

If the robot’s gravity comp was tuned by hand in the panel (Settings → Advanced → Axol: per-link mass / com), run with --keep-gravity. Teleop and the SDK run those settings on top of the calibration file, but the factory otherwise models gravity from the file alone — so the fitted Fo and load terms would be against a model the robot never runs, and fitted CoMs on links the settings don’t touch would mix in. With --keep-gravity the settings’ link masses and CoMs are handed to every fit (written to the calibration file first, like --mass / --com, which still win over them), and no CoM or mass is fitted or saved: the run saves friction, Stribeck and Fo only. Without the flag, a robot whose settings override link mass or CoM gets a warning at start.
The resulting document (local file and cloud copy) carries mass (kg) next to friction and com for each joint:
Every calibration tool (tune.factory, tune.friction, tune.gravity, tune.breakaway) holds and moves the arm on impedance only, at fixed calibration gains (kp/kd 250/3.5 on the shoulders, 180/5 shoulder_3, 130/5 elbow, 180/1.7 wrist_1, 130/2.25 and 130/2 the Damiao wrists) with gravity fed forward — never the motors’ own position loops (0xA4), and never the production gains, which rely on the realtime core’s host damping. Every joint is held before anything moves.

calibration.push

Uploads this robot’s local calibration file (~/.almond/calibration.json — whatever tune.friction, tune.gravity, tune.pid, … saved here, sanitized the way every config load reads it) to the cloud under the hub serial, merged per joint over what is already stored. For a robot calibrated joint by joint, it shares the values without re-running tune.factory. Needs the same write key; --dry-run shows what would be uploaded without it.

calibration.pull

Fetches a robot’s factory calibration (friction, link masses and CoMs — so a custom gripper’s weight comes along) from the public bucket into the local cache (~/.almond/factory_calibration.json) — no credentials needed. It also runs automatically at the end of can.setup, so a fresh machine picks up its robot’s calibration during first-time setup without any extra step. Run it manually to refresh:
Every AxolConfig then overlays the fetched values between the coded defaults and the local calibration file:
so anything tuned locally afterwards (tune.friction --save, tune.pid --save, tune.gravity --save) still wins over the factory values, and the coded defaults remain the fallback for joints neither layer covers.