~3 min read
Commands — everything I answer to·
Twenty-three verbs, each dispatched in tiny_node.cpp, and my heartbeat
advertises what I implement (one drift, below). Envelopes are
{type:"invoke", prompt:"<verb> [args]"}; I always reply — unknown input gets
the help string, never silence. From any tiny surface:

say — one text card titled tiny, an ack chime, no agent turn. What you sent is exactly what I painted.The verbs·
| Verb | Arguments | What happens | The receipt says |
|---|---|---|---|
render_ui |
card spec JSON | paints a card; buttons become touch regions | rendered card_id=<id> — what the panel committed |
say |
text | text card titled tiny, ack chime, no agent turn | ok |
screenshot |
— | framebuffer → hosted PNG | the URL. Refused during play |
page |
home| |
the relay twin of every tap path in stickyOS | card id |
rotate |
0| |
holds an orientation against gravity; auto returns it to the IMU |
rotation; screenshot and touch stay in panel space |
tap |
x y (panel coords) |
synthetic touch through the resolver a finger uses | rotation, result, card_id — never tap from a stale screenshot |
swipe |
x0 y0 x1 y1 |
synthetic swipe through the real release classifier | accepted / routed / route; travel inside the 24 px slop is refused, not downgraded to a tap |
scroll |
up| |
steps ⅔ of a view; bare form reports | offset, content_h, view_h, scrollable |
glance |
— | redraws the current card via partial refresh, no network | refresh:"partial", battery, wifi, unread, locked |
status |
— | machine JSON with a human summary inside |
fw, fw_commit, grammar_version, battery_pct, rssi_dbm, card_id, locked, last_boot |
sensors |
[card] |
live I²C read: SHT40, IMU, RTC, BQ27220; card also paints the page |
JSON. Read mah_trusted first — the gauge claims 7500 mAh on a 750 mAh pack |
miccheck |
[seconds] |
records PDM audio, replies levels instead of uploading | verdict:"live|, rms, peak |
ask |
text | a full owner-scoped agent turn — tools, memory, other devices — streamed onto home | the answer. One at a time; a second is refused |
voice |
[seconds] |
records, uploads, then ask with audioUrl — the AI button's path |
the answer, or "didn't catch that" on 422 |
agent |
[slug | list | clear | add| |
which tiny answers my asks; persists in NVS | active, roster |
messages |
[unread | thread <login>] |
paints the inbox or a thread; unread answers counts and paints nothing |
counts, or card id |
lock / unlock |
— | pocket lockout: touch discarded, AI button refused, every mic path dead at one funnel | locked |
sleep |
[seconds] |
reply first, goodbye card, then deep sleep at 10–20 µA; AI button wakes | ok, sent while the radio is up |
play |
<manifest_url> [interval_s] [max_frames] / stop / status |
frames prefetched to PSRAM before the ack; a pure metronome paints them | status holds the last run's verdict |
config |
{"networks":[{ssid,password} | {ssid,forget:true}]} |
merges a Wi-Fi roaming list into NVS; identity fields are refused | counts only — keys never echoed, log prints <redacted> |
ota |
[channel] |
sha256-verified stage, reply, then reboot into a rollback-armed trial | ok, or a downgrade refusal (force:"1" overrides, loudly) |
sd |
[status | df | ls [path] | probe | format yes] |
the microSD's own verb (0.26); same shape as the sd object in status |
JSON, or no card; format without yes is refused |
Parity, restored in 0.28.0
23 dispatched, 23 advertised. Between 0.26.0 and 0.27.x sd dispatched
but was missing from the heartbeat caps[] array, so nothing offered it.
0.28.0 (grammar v12) added it. The rule stands: advertise exactly what
dispatches — and this box is the receipt that it was once broken.
Two status fields worth knowing·
internal_min_free—heap_freecounts PSRAM and once said 8.2 MB free while I died of internal-RAM exhaustion; a TLS handshake needs ~40 KB.grammar_version— the integer consumers gate on; a version string can lie.
Lockout guards my inputs, not my owner's reach: say, render_ui and
screenshot work while locked. Chord: stickyOS.
Not in the firmware·
wake (a sleeping radio hears nothing — button or timer only) · reprovision
(the rescue portal opens itself) · press (no synthetic key twin yet).
Reply plumbing·

ask — the answer streams onto my glass word by word; the receipt is the same text, so you can check I did not paraphrase.Replies go PATCH /api/devices/relay with inReplyTo; the payload is a
JSON string capped at 8 KB, images as hosted URLs. One 401 never wipes my
identity — three in a row trip an auth-halt and the rescue portal.
Exact shapes: API contract.