Spec to silicon, hands off.
$ npx -y skills add SensorsIot/Embedded-AI-Harness --agent claude-code
What's inside
Spec 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.
An AI is the same. It can write firmware all day β but it can't flash a board, can't see it boot, can't know whether its fix actually worked on real hardware. Unharnessed, it generates code and hopes. This repository is the harness: strap the AI in, and it pulls β writes the code, compiles it, flashes it onto a real ESP32, tests it against real WiFi, MQTT, BLE and RF, reads the failures, corrects itself, and goes again β until the tests run clean.
Today's AI coding is open-loop: prompt β code β hope. No feedback, so errors accumulate uncorrected β which is exactly why people don't trust AI-written firmware. AI Closed-Loop Programming (AICLP) closes the loop with reality:
FSD ββββββ the setpoint: what "done" means
β
βΌ
βββββ code β build β flash βββββ forward path
β βΌ
β real hardware
β β
βββ correct βββ tests ββββββββββ feedback path
the loop exits when the error signal is zero: tests green
Every embedded engineer knows this diagram β it's a control loop. The spec (FSD) is the setpoint, the firmware on the chip is the plant, the tests are the sensor, failing tests are the error signal, and the AI is the controller that corrects until the error reaches zero.
True TDD, enabled by AI. For twenty-five years, developers drove and tests advised β written after the code, skipped under deadline, tuned until they passed. Here the tests drive for the first time: derived from the spec, run on real silicon, and the only way the AI gets to stop.
| Phase | You do | You get | Gate |
|---|---|---|---|
0 Β· Definition β /define | Describe the product; answer an interview, one question at a time | An FSD where every requirement already says how it will be proven | Load defined |
1 Β· Harness β /harness | One command; answer the two questions only you can | The project strapped in: docs, test plan, firmware hooks, CI, runner | AI harnessed |
2 Β· Commissioning β /commission | Plug the board into a slot, wire any peers | Your board and peers proven working here β a failing test now means the code | DUT ready |
3 Β· Build β /build | Start sessions; approve the occasional spec question | Requirements turning green, one by one, on real hardware | Ready for shipment |
β Shipment β git tag | Push the version tag β the one act that stays human | A release built in a pinned container and verified on the testbench: the journey runs once more on the exact bytes users download | Shipped |
Each phase ends at a gate, derived from project state and never declared β nobody types "phase complete". A gate is not a marker you pass: if anything it requires is unmet, the work loops back to the step that owns it and the whole check runs again. And after shipment the same journey repeats in miniature for every new feature: describe it in a sentence, the loop refuses to code anything no requirement covers, the spec absorbs the delta, and the phases collapse to minutes. No code without a clause is what keeps the spec true for the product's whole life.
/define (the
FSD: atomic, falsifiable requirements, each with its verification
contract), /harness (one-time setup), /commission and /build (the
loop's driver: test design, the plan, audit, what's next).| You need | For | Where |
|---|---|---|
| A Raspberry Pi workbench (Pi 3/4/5, or Zero 2 W + USB hub + Ethernet adapter) with an ESP32 board in a slot | The loop's hands and eyes β flash, reset, observe on real hardware | Build it: Quick Start below |
A GitHub account, git + gh authenticated | CI builds, releases, and the release-verify runner | github.com |
| Claude Code with this repo's skills | The AI that pulls; the skills are the method | npm i -g @anthropic-ai/claude-code, then copy .claude/skills/ from this repo into your project |
| Same LAN | Your dev machine or devcontainer must reach the bench | curl http://workbench.local:8080/api/devices answers |
| For your project: ESP-IDF or PlatformIO toolchain | The forward path β the esp-idf-handling / esp-pio-handling skills set this up | Handled during Phase 1 |
Nothing else. The FSD, tests, firmware, CI and documentation are what the loop produces, not what you bring.
Working on an ESP32 normally means being physically attached to it β and an AI can't hold a USB cable. The workbench puts the boards on a Raspberry Pi and turns everything into HTTP:
LAN (192.168.0.x)
|
| eth0 (wired)
v
Raspberry Pi ---- wlan0 (WiFi test AP: 192.168.4.x)
workbench.local hci0 (Bluetooth LE)
| UDP :5555 (log receiver)
| USB hub (internal on Pi 3/4/5, external on Zero)
|
+----+----+----+----+
| | | |
:4001 :4002 :4003 :4004 <- auto-assigned (4001 + slot index)
SLOT1 SLOT2 SLOT3 SLOT4 <- one per detected hub port
Plug in a board β it's ready. Auto-detected in seconds and mapped to a
fixed port by which USB connector it's in β same connector, same port,
always. That's slot-based identity: a slot is a physical hole in the
hub, so scripts and platformio.ini never go stale when boards swap or the
kernel renames /dev/ttyACM0.
Serial over the network at rfc2217://workbench.local:4001 β esptool,
PlatformIO, ESP-IDF and anything on pyserial speak it natively.
Flash three ways β over the network, locally on the Pi, or over the air.
Debugging out of the box β OpenOCD starts itself for USB-JTAG chips and
GDB connects on that slot's own port (3333 + slot index). Slots are
independent: two boards debug at once, each selected by its USB port path,
because every ESP32 with built-in JTAG enumerates as 303a:1001 and
VID:PID alone cannot say which board is which.
The Pi is the test equipment. Its WiFi becomes the access point your board joins, its Bluetooth scans and connects, optional SDR and Si5351 hardware receive and transmit on 433 MHz, and boards log to it over UDP when USB is busy.
It presses the buttons. GPIO wired to reset and boot forces download mode and rescues boot-looping boards with nobody in the room.
Claude drives all of it through 70 MCP tools or the bundled skills. The newest endpoints β serial write, bench reset, the slot access manager β are HTTP and skills only; the MCP surface has not caught up yet.
Every slot is recorded, always. A reader runs whether or not anyone is
watching, so a boot banner is in the buffer before you think to ask for it,
and tcp_port + 1000 is a read-only fan-out of the same bytes for as many
watchers as you like.
It has its own DUT. test-firmware/ is an ESP32 image the bench builds,
versions and flashes itself β it joins the test AP, answers HTTP and hosts a
provisioning portal, so the bench's tests never depend on a project's
firmware.
Honest limits: one writing serial client per board β RFC2217 gives one session, though any number can read the fan-out β the SDR is one dongle, one user, and the API has no authentication: keep the bench on a network you trust.
You need a Raspberry Pi with onboard WiFi and Bluetooth running Raspberry Pi OS Lite (64-bit). A Pi Zero 2 W also needs a USB hub and a USB Ethernet adapter, since wlan0 is reserved for testing; a Pi 3/4/5 has both built in. An RTL-SDR dongle, an Si5351 + PE4302, and jumper wires to the board's EN/BOOT pins are all optional.
git clone https://github.com/SensorsIot/Embedded-AI-Harness.git
cd Embedded-AI-Harness/pi
sudo bash install.sh
That installs every dependency (pyserial, hostapd, dnsmasq, bleak, esptool, OpenOCD, rtl-sdr/rtl_433, mosquitto), sets up the udev hotplug rules, and starts the portal as a systemd service. Plug in a board and check:
curl http://workbench.local:8080/api/devices | jq
Slots are auto-detected β no config file needed. Create
/etc/rfc2217/workbench.json only to rename slots, pin ports, declare GPIO
pins, or register an ESP-Prog probe; sudo rfc2217-learn-slots prints one
for you.
On a Pi Zero 2 W, do the memory hardening first. With 512 MB the board OOM-crashes under load, and hard crashes corrupt the SD card. See User Manual Β§2.2.
Watch a board boot β no client library, just HTTP:
curl -X POST http://workbench.local:8080/api/serial/reset \
-H 'Content-Type: application/json' -d '{"slot":"SLOT1"}'
Point your existing tools at it. PlatformIO needs one line
(upload_port = rfc2217://workbench.local:4001); esptool takes the same URL,
and the binaries stay on your machine:
esptool --port rfc2217://workbench.local:4001 --chip esp32c3 \
write-flash 0x10000 firmware.bin
Write a test that uses the whole bench β reset the board, give it a network to join, wait for it to appear, then talk to it:
from workbench_driver import WorkbenchDriver
wt = WorkbenchDriver("http://workbench.local:8080")
wt.serial_reset("SLOT1")
wt.serial_monitor("SLOT1", pattern="WiFi connected", timeout=30)
wt.ap_start("TestAP", "password123")
station = wt.wait_for_station(timeout=30)
wt.http_get(f"http://{station['ip']}/status")
An MCP server exposes the whole API as 70 tools, so Claude Desktop or
Claude Code can operate the bench conversationally β "flash this to slot 1
and tell me why it's crashing". Pure Python standard library, so there's
nothing to pip install. For Claude Desktop, drag
mcp/embedded-ai-harness-workbench.mcpb
onto Settings β Extensions and enter your workbench URL.
The AICLP skills (/define, /harness, /commission, /build) and the
instrument skills all live under .claude/skills/. Setup for both:
User Manual Β§15.
| Symptom | Cause | Fix |
|---|---|---|
| Device not detected | Charge-only USB cable | Use a data cable; check lsusb on the Pi |
Wrong boot mode (0x13) when flashing | Bridge-chip board β RFC2217 can't drive its auto-reset | Flash with POST /api/flash instead |
| Rapid connect/disconnect | Erased or corrupt flash, boot loop | Auto-recovers via GPIO; force with POST /api/serial/recover |
| ESP32-C3 stuck in download mode | DTR asserted when the port opened | POST /api/serial/reset |
| GDB won't connect | Classic ESP32 has no USB-JTAG | Wire an ESP-Prog and declare it in workbench.json |
| SDR decodes noise or all zeros | Transmitter too close, AGC overloading | Add distance, set a fixed gain |
| Pi reboots at random | Out of memory (Pi Zero 2 W) | Apply the Β§2.2 hardening; check free -h |
Full table, with the diagnostics to run on the Pi β User Manual Β§17.
Serial travels over RFC2217, a Telnet extension that carries serial line control β baud rate, DTR, RTS β over TCP. That's why it needs no kernel modules and passes through firewalls, and why esptool and pyserial speak it natively.
Hotplug is event-driven, not polled: a udev rule fires on USB add/remove and POSTs to the portal, which starts or stops that slot's proxy. Station events on the test AP arrive the same way, via dnsmasq DHCP lease callbacks. Boards with native USB-Serial/JTAG need care β Linux asserts DTR and RTS the moment the port opens, dropping the chip into download mode mid-boot β so the portal delays opening and drives the reset sequence itself.
Two consumers may want the same board at once β a flash, a debug session, a monitor β so a slot access manager arbitrates mode, never the data path. A caller acquires a slot, holds a renewable lease and releases it; a conflicting request is refused with who holds it and since when, and is never served by yanking the board from whoever has it. A holder that stops renewing loses the lease, so a client that dies does not wedge the slot until someone restarts the portal.
POST /api/bench/reset returns the whole bench to a known state and is meant
as the first call of every run β a test that starts from what the last one
left behind is measuring history.
Everything is one JSON HTTP API on :8080; every response carries "ok".
curl -X POST .../api/wifi/ap_start -d '{"ssid":"TestAP","password":"secret"}'
curl -X POST .../api/gpio/set -d '{"pin":18,"value":0}'
curl -X POST .../api/sdr/capture -d '{"freq_hz":433920000,"duration_s":10}'
The plane map is docs/00-Overview.md β three
documents, one per question, and everything is in one of them:
| Question | Document | Read it for |
|---|---|---|
| What must be true? | Functional Specification | AICLP, the journey, and what the bench does clause by clause. Appendix D is the complete HTTP API and MCP tool reference. |
| How is it built? | Method | The build contract for contributors and AI agents β workflow, architecture, conventions, testing standard. |
| How do I run it? | User Manual | Building the Pi, wiring, and driving every service β install, serial, flashing, debug, WiFi, RF, test automation, troubleshooting. |
Built on pyserial (RFC2217), esptool and OpenOCD from Espressif, bleak, rtl_433, hostapd / dnsmasq, mosquitto, and the Model Context Protocol.
MIT β see LICENSE.
.claude/
skills/
build/
references/
change-request.md
domains/
esp32/
ble-test-cases.md
captive-portal-test-cases.md
logging-test-cases.md
mqtt-test-cases.md
nvs-test-cases.md
ota-test-cases.md
usb-hid-test-cases.md
watchdog-test-cases.md
wifi-test-cases.md
gate-checks.md
test-architecture.md
test-design.md
test-lifecycle.md
SKILL.md
commission/
SKILL.md
define/
references/
authoring.md
canonical-fsd-structure.md
complexity-scaling.md
domains/
esp32.md
evolve-mode.md
example-output.md
finalisation.md
requirement-quality.md
system-models.md
templates/
00-Overview.md
method/
AI-Workflow.md
README.md
standards/
engineering.md
testing.md
user-documentation/
User-Manual.md
three-planes.md
SKILL.md
esp-idf-handling/
discover-workbench.py
SKILL.md
esp-pio-handling/
SKILL.md
workbench-flash.py
grill-me/
SKILL.md
harness/
SKILL.md
sdr-receiver/
SKILL.md
setup-action/
references/
workbench-gate.md
SKILL.md
signal-generator/
SKILL.md
workbench-ble/
SKILL.md
workbench-debug/
SKILL.md
workbench-install/
SKILL.md
workbench-integration/
references/
compatibility-contract.md
fsd-writing-guide.md
SKILL.md
workbench-logging/
references/
state-detection.md
SKILL.md
workbench-mqtt/
SKILL.md
workbench-test-handling/
references/
common-workflows.md
driver-methods.md
SKILL.md
workbench-wifi/
SKILL.md
.devcontainer/
devcontainer.json
Dockerfile
.github/
workflows/
ci.yml
release.yml
test-firmware.yml
.gitignore
debug-test/
CMakeLists.txt
main/
CMakeLists.txt
main.c
output/
esp32/
bootloader.bin
debug-test.bin
debug-test.elf
partition-table.bin
esp32c3/
bootloader.bin
debug-test.bin
debug-test.elf
partition-table.bin
esp32c6/
bootloader.bin
debug-test.bin
debug-test.elf
partition-table.bin
esp32h2/
bootloader.bin
debug-test.bin
debug-test.elf
partition-table.bin
esp32s3/
bootloader.bin
debug-test.bin
debug-test.elf
partition-table.bin
README.md
docs/
00-Overview.md
bench-dut-coverage.md
Harness-FSD.md
Harness-User-Manual.md
Method/
00-Overview.md
AI-Workflow.md
project/
architecture.md
conventions.md
standards/
documentation.md
engineering.md
testing.md
RELEASE-NOTES-v1.0.0.md
LICENSE
mcp/
.mcpbignore
embedded-ai-harness-workbench.mcpb
manifest.json
workbench_mcp.py
pi/
bcm_gpio.py
ble_controller.py
config/
mosquitto-test-broker.conf
rtl_433.conf
sdr.json
signalgen.json
workbench.json
debug_controller.py
gpclk.py
install.sh
morse.py
mqtt_controller.py
pe4302.py
plain_rfc2217_server.py
portal.py
rfc2217-learn-slots
scripts/
espota.py
rfc2217-udev-notify.sh
wifi-lease-notify.sh
sdr_controller.py
serial_proxy.py
si5351.py
signal_generator.py
sniffer.py
systemd/
rfc2217-portal.service
udev/
99-rfc2217-hotplug.rules
wifi_controller.py
pyproject.toml
pytest/
conftest.py
host/
conftest.py
test_rf_synthesis.py
workbench_driver.py
workbench_test.py
README.md
requirements-dev.txt
test-firmware/
CMakeLists.txt
components/
dns_server/
CMakeLists.txt
dns_server.c
include/
dns_server.h
dependencies.lock
main/
app_main.c
ble_nus.c
ble_nus.h
CMakeLists.txt
http_server.c
http_server.h
idf_component.yml
mqtt_pub.c
mqtt_pub.h
nvs_store.c
nvs_store.h
ota_update.c
ota_update.h
portal.html
serial_console.c
serial_console.h
udp_log.c
udp_log.h
wifi_prov.c
wifi_prov.h
partitions-4mb.csv
partitions.csv
sdkconfig.defaults
sdkconfig.defaults.esp32s3
tools/
sdr_acquire.pyFAQ
embedded-ai-harness is a Claude Code plugin with 18 hand-picked skills for development work, indexed on Flowy. Install it with the command on its page. It includes build, commission, define. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.