active project · a $40 toy robot · driven by an LLM

# DASH.
A robot with a marker taped to it.

Laptop owns the BLE socket. Phone is the eyes. A vision model runs the OODA loop: see a frame, pick one verb, drive, look again — until the goal is done.

“Find the fridge and say hello.” — the one-prompt demo, typed live in front of a room · the north star
the machine · three boxes
current state · where the project sits
desktop FastAPI + PWA shipped dash-pi OODA wired smiley ▶ one-click launch letter-sim v1 shipped sim never calibrated to metal closed-loop move(0x23) unwired

The arc is a 3-part blog post: Part 1 system prompt on local llama 7B → Part 2 MCP (`dashRobotMCP`, 12 tools) → Part 3 (now): the letter machine — eraser front, marker back, O → numbers → alphabet, calibrate sim → reality. The simulator exists; the metal hasn't met it yet.

Field truth · 2026-05-18 runs (5 runs, 1 clean win) | So what
--- | ---
`turn 360` drew a clean ~25cm circle first try | the known-good stroke — the "O" already exists on metal
`turn` is open-loop timed, no encoder (`dash_ble.py:214-232`) | 90°/180° drift; closed-loop `move(0x23)` is encoded but not wired to `/command` — the precision unlock
`forward` drift direction flips run-to-run; `backward` retraces the last arc | probe with 5cm moves; snapshot before any `forward > 10cm`
all 4 board edges drop onto carpet/keyboard | once off-board, no software command recovers — refuse moves near edges

▶ the one next thing · when you sit down

Put the sim on metal. Tape marker (back) + eraser (front) to Dash, drive ONE real O (`turn_right 360` is the proven stroke), photograph it, then turn the three knobs below until sim-O matches real-O. The open questions — pen mount, erase strategy, letter size, camera mount — get answered by doing this, not by deciding first.

knob 1 · wheelbase knob 2 · deg/s @ spin 200 knob 3 · signed forward-drift
jump in · the letter-sim, live

Same verbs as the robot (`dash-pi/system.md`: forward/backward 5–50cm, turn 10–180°), eraser-front/marker-back pen logic, the three calibration knobs. Standalone: `letter-sim.html`.

jump in · run the real robot
backend
max-iters dry-run

    (no output yet)

Spawns `smiley/run.sh` via the local-viewer's `/api/run-script` (auto-starts the desktop server on :5174). Only works through `localhost:7373`. Manual: `cd nodes/robot-framework/smiley && bash run.sh` · drive by hand: `cd nodes/robot-framework/desktop && bash run.sh` → phone on same wifi hits the LAN URL.

API contract · file map · prior art · hardware limits

    POST /connect     -> scans, connects to first Dash found (handles unit-swap)
    POST /command     {"cmd": str, "params": object} — forward, backward, turn_left,
                      turn_right, head_*, say, stop
    GET  /snapshot    -> one webcam JPEG
    GET  /telemetry · WS /ws -> live state

Path / repo | What
--- | ---
desktop/server.py · dash_ble.py | FastAPI + bleak BLE driver, calibration constants
dash-pi/orchestrate.js · system.md | OODA orchestrator (spawns `pi` per turn) + verb schema
smiley/ · smiley/runs/2026-05-18-*/ | experiment-1 harness, per-run learnings, calibration-notes.md
android/ | parked Expo client (camera-on-head milestone)
dashRobotMCP · DriveTheDashWebApp · bleak-dash-Jake | prior art: the Part-2 MCP server, the turn-bug origin, the BLE driver fork

Hardware limits: no arbitrary TTS (15-char firmware asset names; route speech out laptop speakers) · 48 noises in `constants.py`, ~26 shipped · unused BLE verbs: eye-ring (100), synth (304), launcher (400/401), closed-loop `move(0x23)` · macOS: first Connect needs a one-time Bluetooth permission for your terminal.

nodes/robot-framework · streetlampBeta · super-node since 2026-05-13 · briefing refresh 2026-06-09