~3 min read
The five verbs·
Everything an agent can say to me. Five Strands @tools, one return shape, one guard behind them all.
from strands import Agent
from fomo.tools import calibrate, photo, move, pose, motion
fomo = Agent(tools=[calibrate, photo, move, pose, motion])
fomo("look 40 degrees left, tilt up a little, take a photo, then nod")
Every tool returns {"status": "success"|"error", "content": [{"text": "..."}], ...extras} and never
raises into the agent. When the dashboard is running it owns my serial port and the tools talk to it over
HTTP; otherwise they open the bus themselves. fomo mcp serves the same five over MCP.
calibrate·
calibrate(mode="report", i_know=False, hold_seconds=0, extremes=None, write_eeprom=False)
| mode | what I do |
|---|---|
report |
print ~/.fomo/calib.json. Nothing moves. |
manual |
drop torque, sample the bus for hold_seconds (≥ 20) while you move me by hand; the fold becomes home, the extremes my window. Needs i_know. |
reset |
move calib.json aside. Poses survive — they are relative to home. |
extremes={"1": [66.4, 251.0], ...} types the windows instead of sampling. write_eeprom=True burns
MIN/MAX into servos 1–4 (LOCK 0 → write → LOCK 1). No automatic stop search: on me it only ever found
the fold, so I trust the hand. See the install page.
photo·
photo(source="auto", quality=60, inline=False)
One JPEG. Sources tried in order — lan (the Nicla's /stream, first frame out of the MJPEG), usb
(Nicla raw REPL _snap(q)), dash (/api/camera/snapshot.jpg), local (this machine's webcam via
imagesnap or OpenCV). Saved to ~/.fomo/photos/YYYYmmdd-HHMMSS.jpg. The result names the source — I
never pretend a webcam frame came from my head, and if every source fails I say so, source by source.
inline=True adds the JPEG as an image block so the agent can see what I saw.
move·
move(actions, i_know=False)
actions is a list applied together as one ramped, guarded command:
| action | meaning |
|---|---|
{"joint": j, "to": deg} |
absolute servo degrees (tick 2048 = 180.0) |
{"joint": j, "by": deg} |
relative to the present reading |
{"joint": j, "rel": deg} |
relative to my calibrated home |
{"look": {"pan", "tilt"}} |
the head, about home |
{"home": true} |
fold everything |
{"stop": true} |
abort in flight; torque held unless folded |
{"torque": false} |
release — refused while I'm upright (rule 7) |
{"read": true} |
present degrees plus what the head feels — ToF distance, roll/pitch, bench or head — no motion |
Joints by name or id: base 1 · shoulder_lift 2 · elbow 3 · wrist_flex 4 · pan 5 · tilt 6. Any angle
inside a joint's window is one command; the guard cuts it into ≤ 10° legs and ramps them at
speed °/s (default 30, ceiling 90), validated whole before the first write.
Only a hard limit refuses — the window, the head window, the gate, a weak rail (rule 8).
pose·
pose(action="list", name=None, description="", i_know=False, seconds=0, nudge=None)
list · show · save · goto · nudge · delete. A pose is a place, stored relative to home, so recalibrating
shifts it with the fold instead of breaking it. 5 built-ins ship with me — Poses has
the table and the twin.
motion·
motion(action="list", name=None, i_know=False, speed=None, stay=None, frames=None, description="")
list · show · run · save · delete. A motion is a sentence — keyframes played on the real arm, starting from
wherever I am and ending folded unless it says stay. 9 built-ins: approve deny happy frustrated curious wave look_around up sleep. "Show
me you're happy" in the panel ends up in motion(action="run", name="happy"); speed overrides every
frame's ramp, stay=True leaves me in the last frame. Every frame is checked against the hard limits
before the first write; a stall mid-motion holds me instead of forcing it. Motions shows
all nine in the twin.