Reproduceerbare ESP32 Firmware-ontwikkeling met Docker en Docker Sandboxes
Het officiële espressif/idf Docker-image lost het probleem van reproduceerbaarheid op. Docker Sandboxes (de sbx CLI) lossen een nieuwer probleem op: het mogelijk maken dat AI-coding agents op volle snelheid aan je firmware kunnen werken zonder dat je ze de sleutels van je laptop hoeft te geven. Dit artikel beschrijft een praktische workflow die beide combineert: schone builds, parallelle omgevingen voor nieuwe en legacy-firmware, en veilige, onbeheerde AI-sessies.
Deel 1: De basis – Bouwen met het officiële image
Het espressif/idf image bevat een volledige, vastgepinde ESP-IDF-installatie: het framework zelf, de Xtensa/RISC-V toolchains, de Python-omgeving, CMake, ninja en alles wat daarbij hoort. Een build vereist slechts één commando:
docker run --rm -v $PWD:/project -w /project \
-u $UID -e HOME=/tmp \
espressif/idf:release-v5.4 idf.py build
Belangrijke details over de configuratie
- Gebruikersrechten en cache:
-u $UID -e HOME=/tmpzorgt ervoor dat de container draait als jouw gebruiker, zodat build-artefacten in debuild/map niet eigendom worden van root.HOME=/tmpgeeft de IDF-tools een beschrijfbare home-map voor hun caches. - Tagging: Pin je tag.
latestvolgt de master-branch en zal uiteindelijk je build breken.vX.Ytags zijn vaste releases;release-vX.Ytags volgen de release-branch en ontvangen bugfixes. Voor producten in onderhoud zijn exactevX.Y.Ztags het veiligst; voor actieve ontwikkeling isrelease-vX.Yeen goede balans. - Git-eigendom: Als je gemounte project eigendom is van een andere gebruiker dan die in de container, zal Git klagen over "dubieuze eigendom" (dubious ownership). Het image ondersteunt
-e IDFGITSAFE_DIR='/project'om het pad op een witte lijst te zetten (gebruik:om meerdere paden te scheiden). - Compiler cache: Schakel de compiler cache in met
-e IDFCCACHEENABLE=1en bewaar deze tussen runs door er een volume voor te mounten. Volledige rebuilds van een middelgroot project gaan hiermee van minuten naar seconden.
Flashen en monitoren
Op Linux kun je het seriële apparaat direct doorgeven:
docker run --rm -it \
--device=/dev/ttyUSB0 \
--group-add $(getent group dialout | cut -d: -f3) \
-v $PWD:/project -w /project \
-u $UID -e HOME=/tmp \
espressif/idf:release-v5.4 idf.py flash monitor
De --group-add is nodig omdat je als $UID draait en niet als root, terwijl de device-node toebehoort aan de groep dialout.
Op macOS en Windows kan Docker Desktop geen USB-apparaten doorgeven aan containers. Een schone workaround is een netwerk-seriële bridge met behulp van RFC2217, wat esptool native ondersteunt.
Op de host:
pip install esptool
esp_rfc2217_server -p 4000 /dev/cu.usbserial-1420
Binnen de container wijs je idf.py vervolgens aan de netwerkpoort:
idf.py --port 'rfc2217://host.docker.internal:4000?ign_set_control' flash monitor
Dit lijkt een hack, maar het is een krachtige functie: zodra de seriële poort een netwerkeindpunt is, kan alles erbij, inclusief containers, CI-runners en gesandboxed AI-agents.
Automatiseren met een Makefile
Om te voorkomen dat deze commando's telkens handmatig getypt moeten worden, kan een kleine Makefile de interface stabiel houden:
IDF_IMAGE ?= espressif/idf:release-v5.4
PORT ?= /dev/ttyUSB0
DOCKER_RUN = docker run --rm -it \
--device=$(PORT) \
--group-add $(shell getent group dialout | cut -d: -f3) \
-v $(PWD):/project -w /project \
-v idf-ccache:/ccache -e CCACHE_DIR=/ccache -e IDF_CCACHE_ENABLE=1 \
-u $(shell id -u) -e HOME=/tmp -e IDF_GIT_SAFE_DIR=/project \
$(IDF_IMAGE)
build:
$(DOCKER_RUN) idf.py build
flash:
$(DOCKER_RUN) idf.py flash
monitor:
$(DOCKER_RUN) idf.py monitor
menuconfig:
$(DOCKER_RUN) idf.py menuconfig
shell:
$(DOCKER_RUN) bash
Nu werkt make build identiek voor elke ontwikkelaar en in CI. Het wisselen van IDF-versies is simpelweg: make build IDF_IMAGE=espressif/idf:release-v5.3.
Deel 2: Parallelle omgevingen – Nieuwe functies en legacy naast elkaar
Omdat elke container volledig geïsoleerd is, kun je twee verschillende IDF-versies tegen twee verschillende boards tegelijkertijd op dezelfde machine draaien.
Terminal 1 - nieuwe feature branch, IDF 5.4, experimenteel board:
docker run --rm -it --device=/dev/esp32-experimental \
-v $PWD/new-feature:/project -w /project \
-u $UID -e HOME=/tmp \
espressif/idf:release-v5.4
Terminal 2 - legacy firmware, IDF 5.3, productieboard:
docker run --rm -it --device=/dev/esp32-production \
-v $PWD/legacy:/project -w /project \
-u $UID -e HOME=/tmp \
espressif/idf:release-v5.3
Typische gebruiksscenario's zijn:
- Het flashen van experimentele code op het ene board terwijl een langdurige soak-test of klantdemo op het andere board ongemoeid blijft.
- A/B-vergelijkingen van het stroomverbruik tussen firmware-versies.
- Het reproduceren van een bug in het veld met de exacte legacy toolchain, terwijl de fix wordt ontwikkeld in de huidige versie.
Stabiele apparaatnamen met udev
/dev/ttyUSB0 en /dev/ttyUSB1 wisselen vaak van plek afhankelijk van de volgorde van aansluiten. Op Linux kun je deze vastzetten met udev-regels op basis van het serienummer van de adapter:
- Serienummers vinden:
udevadm info -a /dev/ttyUSB0 | grep '{serial}'
- Configuratie in
/etc/udev/rules.d/99-esp32.rules:
``text SUBSYSTEM=="tty", ATTRS{serial}=="A50285BI", SYMLINK+="esp32-experimental" SUBSYSTEM=="tty", ATTRS{serial}=="B7743NM0", SYMLINK+="esp32-production" ` Na udevadm control --reload` blijven deze symlinks bestaan na reboots, zodat je Makefile-targets kunnen verwijzen naar de rol van het board in plaats van naar een willekeurig nummer.
Codificatie met Docker Compose
Als deze twee-omgevingen setup permanent is, is een compose.yaml overzichtelijker:
services:
new-feature:
image: espressif/idf:release-v5.4
volumes: ["./new-feature:/project"]
working_dir: /project
devices: ["/dev/esp32-experimental:/dev/ttyUSB0"]
stdin_open: true
tty: true
legacy:
image: espressif/idf:release-v5.3
volumes: ["./legacy:/project"]
working_dir: /project
devices: ["/dev/esp32-production:/dev/ttyUSB0"]
stdin_open: true
tty: true
Met docker compose run new-feature idf.py flash monitor is de koppeling tussen rol en fysiek board versiebeheerd.
Deel 3: Docker Sandboxes – AI-agents onbewaakt laten werken
Coding agents zoals Claude Code zijn zeer nuttig voor firmware-werk: het porteren van componenten tussen IDF-versies, het schrijven van unit tests of het opsporen van config-drift in sdkconfig. Om nuttig te zijn, moeten ze echter acties kunnen uitvoeren: builds, flashes, pip install en soms Docker zelf. Een agent deze vrijheid direct op je host-machine te geven is om goede redenen oncomfortabel.
Docker Sandboxes lossen dit op met een sterker primitief dan een container: elke sandbox is een microVM met een eigen kernel, bestandssysteem, netwerkstack en een eigen private Docker-daemon. De agent kan pakketten installeren, systeemconfiguraties wijzigen en containers bouwen en draaien, zonder dat dit je host raakt. Je workspace-directory wordt gesynchroniseerd in de sandbox op hetzelfde pad, zodat foutmeldingen in beide werelden overeenkomen.
De CLI is eenvoudig:
sbx run claude: Start Claude Code in een sandbox voor het huidige project.sbx run claude ~/firmware/new-feature: Werk aan een specifieke directory.sbx: Bekijk wat er draait, het resourcegebruik en netwerkverzoeken.sbx lsensbx rm new-feature: Lijsten en opruimen van sandboxes.
Voor firmware-ontwikkeling zijn drie eigenschappen cruciaal:
- Wegwerpbaarheid (Disposability): De agent kan zijn omgeving volledig vernielen tijdens het experimenteren met
esptoolversies of partitietabellen. Metsbx rmis alles weg; je eigen host-installatie blijft onaangetast. - Netwerkbeleid: Sandboxes routeren verkeer via een host-proxy met drie modi: open, balanced (default-deny met goedgekeurde domeinen) en locked down. Een agent die besluit je firmware naar een onverwachte locatie te
curl-en, kan dat simpelweg niet. - Isolatie van inloggegevens: API-sleutels en tokens worden door de host-proxy geïnjecteerd in uitgaande verzoeken; de sandbox zelf ziet ze nooit.
Hoe flasht de agent een board?
Hier komt de RFC2217-truc uit Deel 1 van pas. Omdat de sandbox een VM is, is er geen USB-passthrough, maar is er wel een netwerkpad naar de host.
Stel de seriële poort beschikbaar als netwerkservice op de host: esprfc2217server -p 4000 /dev/esp32-experimental
En instrueer de agent (bijvoorbeeld in een CLAUDE.md bestand) om te flashen met: idf.py --port 'rfc2217://host.docker.internal:4000?ignsetcontrol' flash monitor
Nu draait de volledige loop van de agent end-to-end in de sandbox: bewerken, bouwen in een zelf aangemaakte container, flashen op echte hardware, de monitor-output lezen en de bug oplossen. Het enige dat de agent op je machine kan bereiken, is één seriële poort die je expliciet hebt gepubliceerd.
Deel 4: Alles samenvoegen – Een dagelijkse workflow
- Reguliere ontwikkeling: VS Code Dev Containers met het
espressif/idfimage (inclusief de Espressif IDF-extensie in de container). Dit biedt dezelfde image als CI, volledige IntelliSense en de snelheid van native containers. - AI-ondersteunde experimenten:
sbx run claude --branch <feature>. De--branchflag houdt de commits van de agent op een worktree, zodat je checkout schoon blijft. Review en merge zodra het klaar is. - Multi-board testen: Parallelle containers (voor jou) of parallelle sandboxes (voor agents), één per device, met udev-stabiele namen en één
esprfc2217serverper board. - CI: GitHub Actions met de officiële
espressif/esp-idf-ci-action, vastgepind op dezelfde IDF-versie als je dev-image. Als een build lokaal slaagt, slaagt deze ook in CI.
Voorbeeld .github/workflows/build.yml:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { submodules: recursive }
- uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v5.4
target: esp32s3
Pro Tips
- Pin exacte image tags (bijv.
release-v5.4in plaats vanlatest) en leg deze vast in de repo (Makefile of compose file). - Gebruik één projectmap per productlijn (
new-feature/,legacy/) met een eigen gepinde image. Deel nooit eenbuild/directory tussen verschillende IDF-versies. - Optimaliseer snelheid:
IDFGITSAFEDIR=/projectelimineert Git-waarschuwingen;IDFCCACHE_ENABLE=1met een ccache-volume verkort de rebuild-tijden aanzienlijk. - Rechten: Voeg
--group-addtoe voor de dialout GID wanneer je--devicecombineert met-u $UID. - Seriële transport: Gebruik op macOS/Windows, en altijd bij sandboxes, RFC2217. Eén server per board, één poort per server.
- Agent-instructies: Zet de flash/monitor-commando's en poortmapping in
CLAUDE.md, zodat agents de hardware-setup ontdekken zonder dat je dit elke sessie opnieuw hoeft uit te leggen. - Custom images: Als je team standaard extra tools gebruikt (zoals
clang-tidyofcppcheck), maak dan een dunne custom imageFROM espressif/idf:release-v5.4in plaats van deze in elke sessie opnieuw te installeren.
Conclusie
Docker heeft ESP32-builds veranderd van een fragiel, machine-specifiek ritueel in iets dat reproduceerbaar genoeg is om op te vertrouwen. Parallelle containers maken van één bureau een klein hardwarelab, waar legacy- en next-gen firmware zonder wrijving naast elkaar bestaan. En Docker Sandboxes dichten het laatste gat: ze maken het verantwoord, in plaats van roekeloos, om een AI-agent een echt board te geven en het werk te laten doen.
Groetjes,