~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_knowis 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/servosreads 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 |
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 |