One board, one job — erase the flash, install the firmware, deploy the meteo
station's main.py, verify — done two ways. First with bare
pyserial/esptool and no harness. Then through the ironharness MCP server: the same
operations, plus the two things a bare script cannot give you — a gate that
refused to destroy the board until explicitly told to, and a journal that
replays every step.
{"ts": 1789910900.024, "seq": 1, "actor": "mcp", "kind": "esp_denied",
"op": "erase", "port": "COM6",
"error": "IRONHARNESS_ALLOW_REAL_FLASH=1 is required for live flash/erase
— these operations modify real hardware"}
The esp_erase call was made without the opt-in flag first. The
tool refused in 0.0 s and journaled its own refusal. The bare-hands road has no such
concept: the first run of the script simply erased the flash.
| Step | Command | Time |
|---|---|---|
| wipe | esptool --chip esp32 --port COM6 erase-flash | 5.3 s |
| flash | esptool write-flash 0x1000 firmware.bin (hash verified) | 21.3 s |
| station | main.py over the raw REPL: 15 hex chunks, byte-check | 24.6 s |
| verify | RTS reset + 15 s of boot read | PASS |
First run of this road FAILED at verify — and the lesson is the point: pyserial asserts DTR/RTS on open, and on this board's reset circuit a held DTR blocks the RTS→EN pair, so the board goes silent (0 bytes in two 8-second windows). The harness kills DTR/RTS right after open; the bare script has to know that. It also has to know the Ctrl-B/Ctrl-C×2/Ctrl-A dance, the hex-chunk raw-REPL protocol and the cooked-mode limits. Evidence trail of road A: whatever the script happened to write into its own log files.
| Step | Tool | Time |
|---|---|---|
| open / reset / read boot signature | serial_open, serial_reset, serial_read_until | 0.1–2.1 s |
| release the port for esptool | serial_close | 0.0 s |
| wipe | esp_erase | 4.8 s |
| flash | esp_flash @0x1000 (hash verified inside) | 19.7 s |
| main.py into the sandbox | file_write | 0.0 s |
| main.py onto the board | serial_put 3787 B (raw REPL) | 47.3 s |
| reset + boot signature | serial_reset + serial_read_until | 2.1 s |
| independent round-trip | serial_get 3787 B → byte-for-byte compare — PASS | 41.5 s |
143 journal events (who / when / what / answer): every step —
including the refusal and every hex chunk in both directions — replays from
journal.jsonl. The meteo station was verified by an independent full
round-trip, not by trusting the put's return value.
main.py after the roads started.OSError ENODEV at its OLED init —
the display is physically off the I2C bus; that is the documented expected signature
of this bench, not a failure.