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.
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 |
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.
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.
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.
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.