build
Phase 3 of AI Closed-Loop Programming — the Build phase, and the driver of the whole loop: locate the project on the chain, name the next act, design and…
Use this skill whenever you need to read serial output or debug logs from ESP32 devices on the testbench. Covers serial monitor with pattern matching (wait for boot messages, WiFi connected, crash dumps), UDP debug log retrieval when USB is occupied (e.g. HID keyboard), boot
$ npx -y skills add SensorsIot/Embedded-AI-Harness --skill testbench-logging --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/testbench-loggingContext preview
The summary Claude sees to decide when to auto-load this skill.
Use this skill whenever you need to read serial output or debug logs from ESP32 devices on the testbench. Covers serial monitor with pattern matching (wait for boot messages, WiFi connected, crash dumps), UDP debug log retrieval when USB is occupied (e.g. HID keyboard), boot
name: testbench-logging description: Use this skill whenever you need to read serial output or debug logs from ESP32 devices on the testbench. Covers serial monitor with pattern matching (wait for boot messages, WiFi connected, crash dumps), UDP debug log retrieval when USB is occupied (e.g. HID keyboard), boot capture, and crash analysis. Use for verifying firmware started correctly, checking WiFi connection status, or diagnosing boot loops. Triggers on "serial monitor", "log", "debug log", "UDP log", "boot output", "crash", "monitor", "pattern", "serial output".
Base URL: `$TESTBENCH_URL` — see Step 0
There are several benches and their addresses move, so nothing here writes one down. `$BENCH` is not usable either — a container cannot resolve mDNS. Discover the bench and export its URL:
export TESTBENCH_URL=$(sudo python3 .claude/skills/esp-idf-handling/discover-testbench.py \
--url --name <bench-hostname>)
curl -s "$TESTBENCH_URL/api/info" # confirm before anything else`--url` refuses to guess when more than one bench answers, so `--name` is required whenever a second bench is powered on. `TESTBENCH_URL` is the same variable `pytest --wt-url` falls back to.
Two logging methods are available. Choose based on your situation:
| | Serial Monitor | UDP Logs | |---|---|---| | **Works without WiFi** | Yes | No | | **Boot/crash output** | Yes | No | | **Pattern matching** | Built-in (regex + timeout) | Manual (poll + grep) | | **Blocks serial port** | Yes (one session per slot) | No | | **Multiple devices** | One slot at a time | All devices simultaneously | | **Long-running** | Limited by timeout | Continuous (buffer persists) |
Request and response shapes: serial in [FSD Appendix D.2](../../../docs/Harness-FSD.md#d2-serial-management), UDP log in [D.7](../../../docs/Harness-FSD.md#d7-udp-log), the portal's own activity log in [D.14](../../../docs/Harness-FSD.md#d14-activity-log).
Three different logs, and picking the wrong one wastes a test run: `/api/serial/*` is what the device printed over USB, `/api/udplog` is what it sent over the network, and `/api/log` is what the *portal* did — never device output.
Reads serial output via RFC2217 proxy. Optionally waits for a regex pattern.
# Wait up to 10s for a pattern match
curl -X POST $TESTBENCH_URL/api/serial/monitor \
-H 'Content-Type: application/json' \
-d '{"slot": "SLOT1", "pattern": "WiFi connected", "timeout": 10}'
# Just capture output for 5s (no pattern)
curl -X POST $TESTBENCH_URL/api/serial/monitor \
-H 'Content-Type: application/json' \
-d '{"slot": "SLOT1", "timeout": 5}'Response: `{"ok": true, "matched": true, "line": "WiFi connected to MyAP", "output": [...]}`
`POST /api/serial/reset` picks its method from the slot. With no debug session it pulses DTR/RTS and returns the boot log as **a list of lines**. With OpenOCD attached it issues a JTAG `reset run` instead and returns **a single string** of OpenOCD's reply — `JTAG tap: esp32c3.tap0 ...` — which is not the device's output at all. Same endpoint, same request, two response shapes and two meanings.
Nothing in the response announces this except a `"method": "jtag"` key that is absent on the serial path. Client code that iterates `output` gets characters instead of lines and quietly finds no boot markers.
**OpenOCD attaches by itself when a device is plugged in**, so this is the default state of a board on a JTAG-capable slot, not something you opted into. Stop it before capturing boot output:
curl -X POST $TESTBENCH_URL/api/debug/stop \
-H 'Content-Type: application/json' -d '{"slot": "SLOT3"}'Check first with `debugging` in `/api/devices` — a slot reads `idle` either way.
**Serial is the lifeline.** Never decide whether a device is alive by pinging it or calling its HTTP endpoint — a device that boots fine but never joins WiFi looks identical to a dead one. Reading what it printed tells you which. Boot-marker patterns for running / download-mode / unknown are in [`references/state-detection.md`](references/state-detection.md).
On a chip whose console is USB-CDC (ESP32-C3/S3/C6), `esp_restart()` resets the USB-JTAG peripheral, the devnode re-enumerates, and the RFC2217 proxy's file descriptor breaks. The capture socket **stays open and simply goes quiet** — and on reconnect it can replay stale scrollback from before the reset. A test that counted "boots seen" from serial once read 25 lines then nothing for 30 minutes while the device restarted six times, and a filtered view showed a clean pass.
Serial is authoritative only **within one boot**. For anything that counts restarts — a 5-minute-rule reboot, a watchdog reset, a crash loop — read a monotonic **boot counter the firmware publishes** (e.g. an MQTT status payload or UDP telemetry), not the console. A silent device and a dead probe look identical in any filtered serial view, so make the harness exit loud (non-zero with the raw captured-line count) rather than reporting `boots: 0`.
# Reset via JTAG slot
curl -X POST $TESTBENCH_URL/api/serial/reset \
-H 'Content-Type: application/json' \
-d '{"slot": "<JTAG-slot>"}'
# Capture boot output from UART slot
curl -X POST $TESTBENCH_URL/api/serial/monSpec to silicon, hands off. A horse is strong, fast, and willing — and useless for heavy loads until you harness it. The harness is not a part of the horse and not a part of the cart: it is the coupling that turns raw strength into pulled weight.
Phase 3 of AI Closed-Loop Programming — the Build phase, and the driver of the whole loop: locate the project on the chain, name the next act, design and…
Phase 2 of AI Closed-Loop Programming — Commissioning: prove the project's OWN never-seen-working parts (its board, its wiring, its peers/simulators), so that…
Phase 0 of AI Closed-Loop Programming — Definition: engineers the WHAT the loop converges on. Writes and evolves the FSD — atomic, falsifiable,…
Complete ESP-IDF lifecycle: project setup, build, flash, monitor, and OTA. Automatically detects whether a testbench is available or the device is connected…
PlatformIO lifecycle for ESP32 firmware: platformio.ini, environment selection, build, upload and serial monitor, on local USB or through the testbench. Covers…
Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to…