Skip to content

~3 min read

The guard·

Every servo write passes fomo.guard.Guard. It refuses on hard limits only — the measured windows, the head window, the gate, the rail — and executes everything inside them. Each rule was written the night something went wrong on the real leader, 2026-09-05 to 08.

A refusal is a sentence: elbow -> 60.00 is outside the rule 4 measured window 68.73..262.44. Any angle inside a window is one command — elbow to 68.7 from the fold is one call, planned as 20 legs; how far a command reaches is never a refusal.

The refusals·

# rule constant the incident
1 Gate. Nothing moves without i_know=True or ~/.fomo/bench.json {"ok": true}. a tool call from a phone moved the head while the owner's hand was on it
3 Head window. Pan ±160°, tilt ±80° about home — on this arm 27.82..347.82 and 100.35..260.35. HEAD_PAN_DEG, HEAD_TILT_DEG the pan/tilt horns spin freely; only the Nicla's USB cable stops them. The old ±60 fallback refused a legitimate pan +90
4 Measured window. Joints 1–4 stay inside what the hand found: base 66.36..251.02, shoulder 83.23..290.04, elbow 68.73..262.44, wrist 84.29..284.85. calib.windows() the base's factory EEPROM window 111..251 wasted 45° on the minus side; the hand found 66.4
7 Upright. An upright arm is never dropped. torque off is refused until I'm folded; release() folds first. the elbow fell back onto its stop at 04:05Z, 2026-09-07
8 Power. The shoulder and elbow are not asked to lift away from the fold while the rail reads under 5 V. Folding is always allowed. VOLT_FLOOR_V the 5.5 V leader supply could not lift strands-arm's elbow (25 % load, stalled)
9 Stall. A joint 6° behind its streamed goal and not moving, or 3° short after settling, is pushing into something: every joint is re-goaled to where it is and the move returns stalled. Not a refusal — a hold. STALL_LAG_DEG, SHORT_DEG the wrist was driven into the forearm for 2 s with the rail sagging to 5.0 V (2026-09-08)

The execution·

Inside the limits, the guard decides how, never whether:

# detail constant
2 Legs. A target is cut into ≤ 10° legs, validated whole before the first write, so stop lands on a leg boundary. STEP_DEG, STEP_EPS_DEG 0.02 (floats made a 10° leg 10.000000000000028 once)
5 Ramp. Goals are streamed from the host at speed °/s — default 30, ceiling 90 — at 20 Hz, so stop aborts mid-leg instead of waiting for the servo's own profile. SPEED_DPS, SPEED_MAX_DPS, STEP_HZ, ACC 10
6 Idle. Torque drops 2 s after the last command — only when folded (within 15° of home). IDLE_S, FOLD_TOL_DEG

guard.windows() is rules 3 + 4 in one dict; check() tests a target against it, check_power() is rule 8, plan_legs() the execution.

What I cannot do·

  • See obstacles. Rule 1 exists because a human must. i_know is a promise about the sweep, not a flag.
  • Doubt my calibration. I trust calib.json. A manual pass that ended too early once saved min == max for every joint and the guard refused everything — calibrate --reset, then again.
  • Lift on 5 V. The leader kit's supply reads 5.6 V on the bus (state.voltage). On it the elbow does not lift the forearm from the fold at all (load climbs, shaft still; rule 9 holds it) — that needs the 7.4 V supply. Base, wrist, head and a small shoulder lift work.
  • Watch temperature per move. GET /api/servos reads it; the guard does not. The STS3215 warns at 55 °C; factory protection trips near 70 °C.

Where the numbers come from·

Read at build time from the code and the calibration file, never typed into the prose:

Provenance — 2026-09-07T08:43:36Z

| key | value | source | |---|---|---| | step_deg | 10 | fomo/guard.py STEP_DEG | | step_eps_deg | 0.02 | fomo/guard.py STEP_EPS_DEG | | speed_dps | 30 | fomo/guard.py SPEED_DPS | | step_hz | 20 | fomo/guard.py STEP_HZ | | idle_s | 2 | fomo/guard.py IDLE_S | | fold_tol_deg | 15 | fomo/guard.py FOLD_TOL_DEG | | head_pan_deg | 160 | fomo/guard.py HEAD_PAN_DEG | | head_tilt_deg | 80 | fomo/guard.py HEAD_TILT_DEG | | max_legs | 40 | fomo/guard.py MAX_LEGS | | acc | 10 | fomo/guard.py ACC | | speed_max_dps | 90 | fomo/guard.py SPEED_MAX_DPS | | volt_floor_v | 5 | fomo/guard.py VOLT_FLOOR_V | | stall_lag_deg | 6 | fomo/guard.py STALL_LAG_DEG | | short_deg | 3 | fomo/guard.py SHORT_DEG | | speed_ticks | 341 | fomo/guard.py SPEED_TICKS = SPEED_DPS/360·4096 | | max_span_per_command | 400 | MAX_LEGS × STEP_DEG | | leg_seconds | 0.33 | STEP_DEG / SPEED_DPS | | stall_grace_s | 0.6 | fomo/feetech.py STALL_GRACE_S | | motions | 9 | fomo/motions.py BUILTINS | | motion_names | approve deny happy frustrated curious wave look_around up sleep | fomo/motions.py BUILTINS | | motion_frames_total | 40 | fomo/motions.py BUILTINS frames | | motion_table | | motion | what it says | frames | planned s | on the metal s | |---|---|---|---|---| | approve | a nod: the head dips and lifts twice | 5 | 3.3 | 6.5 | | deny | a shake: the head turns left-right twice | 5 | 3.9 | 7.0 | | happy | head up, a quick left-right wiggle with bobbing, a little bounce of the whole arm | 7 | 5.4 | 8.7 | | frustrated | head drops, a slow look away, then a short sharp shake | 7 | 6.8 | 11.1 | | curious | head tilts and lifts toward what it looks at, a small rise of the arm | 3 | 4.6 | 7.0 | | wave | head raised on the wrist, three fast pans | 6 | 8.6 | 12.4 | | look_around | a slow survey: far left, far right, up, back | 4 | 7.1 | 9.8 | | up · stays | head up on the wrist and looking up; stays there | 1 | 2.5 | 3.2 | | sleep | a slow fold onto the bench | 2 | 1.1 | 7.5 | | fomo/motions.py BUILTINS (travel/speed per frame + hold, fold at SPEED_DPS) · measured: LIVE.md 2026-09-08 | | legs_elbow_full | 20 | ceil((262.4 − 68.7) / STEP_DEG) — the elbow's whole window in one command | | dash_port | 8091 | fomo/config.py DASH_PORT | | baud | 1 000 000 | fomo/config.py BAUD | | joints | 6 | fomo/config.py JOINTS | | arm_joints | 4 | fomo/config.py ARM_IDS | | hold_min_s | 20 | fomo/tools/calibrate.py max(20.0, hold_seconds) | | poses | 5 | fomo/poses.py BUILTINS | | pose_names | home attention look_around_left look_around_right up | fomo/poses.py BUILTINS | | pose_home | folded, all six at home | fomo/poses.py BUILTINS['home'] | | pose_attention | tilt +20° | fomo/poses.py BUILTINS['attention'] | | pose_look_around_left | pan +40° | fomo/poses.py BUILTINS['look_around_left'] | | pose_look_around_right | pan -40° | fomo/poses.py BUILTINS['look_around_right'] | | pose_up | tilt +30° | fomo/poses.py BUILTINS['up'] | | calib_t | 2026-09-07T08:43:36Z | ~/.fomo/calib.json t | | base_home | 176.66 | ~/.fomo/calib.json servo 1 home_deg | | base_window | 66.36..251.02 | ~/.fomo/calib.json servo 1 min/max_deg | | base_span | 184.7 | ~/.fomo/calib.json servo 1 max−min | | shoulder_lift_home | 83.85 | ~/.fomo/calib.json servo 2 home_deg | | shoulder_lift_window | 83.23..290.04 | ~/.fomo/calib.json servo 2 min/max_deg | | shoulder_lift_span | 206.8 | ~/.fomo/calib.json servo 2 max−min | | elbow_home | 262.35 | ~/.fomo/calib.json servo 3 home_deg | | elbow_window | 68.73..262.44 | ~/.fomo/calib.json servo 3 min/max_deg | | elbow_span | 193.7 | ~/.fomo/calib.json servo 3 max−min | | wrist_flex_home | 256.46 | ~/.fomo/calib.json servo 4 home_deg | | wrist_flex_window | 84.29..284.85 | ~/.fomo/calib.json servo 4 min/max_deg | | wrist_flex_span | 200.6 | ~/.fomo/calib.json servo 4 max−min | | pan_home | 187.82 | ~/.fomo/calib.json servo 5 home_deg | | pan_window | continuous (free-spinning horn) | ~/.fomo/calib.json servo 5 continuous | | pan_span | 360 | ~/.fomo/calib.json servo 5 continuous | | tilt_home | 180.35 | ~/.fomo/calib.json servo 6 home_deg | | tilt_window | continuous (free-spinning horn) | ~/.fomo/calib.json servo 6 continuous | | tilt_span | 360 | ~/.fomo/calib.json servo 6 continuous | | pan_head_window | 27.82..347.82 | guard.head_window(): pan home ± HEAD_PAN_DEG | | tilt_head_window | 100.35..260.35 | guard.head_window(): tilt home ± HEAD_TILT_DEG | | motion_twin |

| hooks/gen_numbers.py from calib + motions.BUILTINS | | rl_step_deg | 5 | fomo/rl.py MAX_STEP_DEG (== strands-arm head_tool.MAX_STEP_DEG the envs trained with) | | rl_hz | 10 | fomo/rl.py 1 / DT | | rl_range_deg | 60 | fomo/rl.py RANGE_DEG (track env head half-width) | | rl_arm_volt_min | 7 | fomo/rl.py ARM_VOLT_MIN | | rl_gate_head | BENCH OK FOR RL HEAD | fomo/rl.py GATE_HEAD | | rl_live_max_s | 30 | fomo/rl.py HeadLive.MAX_SECONDS | | rl_gate_arm | BENCH OK FOR RL ARM | fomo/rl.py GATE_ARM | | rl_signs_verified | not yet verified | fomo/rl.py SIGNS_VERIFIED | | fw_model | fomo_face_detection.tflite | firmware/nicla/fomo_node.py DETECT_MODEL | | rl_hf_frame | cagataydev/strands-arm-frame-2026-09-06 | fomo/policies/frame/PROVENANCE | | rl_hf_track | cagataydev/strands-arm-track-2026-09-06 | fomo/policies/track/PROVENANCE |