Web tool for planning the pinout of an ESP32-S3 devkit:
- GPIO assignment per electrical interface (free grouping of signals) ;
- hardware constraint validation (strapping, native USB, programming UART) ;
- and C header export (
.hper interface, or a combined.zip).
Disclaimer :
First and foremost, I would like to acknowledge and express my sincere gratitude for the work of Devender Gupta and specifically the ESP32-PINOUT project — which inspired the design of this tool. I highly recommend checking out his work.
The whole code has been generated by Claude.ai. A code review and some improvements will be made if needed. The choice between Podman and Docker are motivated by my returned experience while using containered environment and user rights management.
The project works with both engines without any changes: Dockerfile is the base for Docker, Containerfile (an identical copy) is the default name looked up by Podman.
Docker:
docker compose up --buildPodman:
podman-compose up --build
# or, with Podman's compose plugin (Podman >= 4.7):
podman compose up --buildThen open http://localhost:8082
Without compose:
# Docker
docker build -t esp32-pinout-planner .
docker run -p 8082:8082 esp32-pinout-planner
# Podman
podman build -t esp32-pinout-planner .
podman run -p 8082:8082 esp32-pinout-plannerpip install -r requirements.txt
python server.pyesp32-pinout-container.sh automatically detects the available engine
(Docker first if it responds, otherwise Podman) and bundles the common
commands:
chmod +x esp32-pinout-container.sh
./esp32-pinout-container.sh build
./esp32-pinout-container.sh start
./esp32-pinout-container.sh logs
./esp32-pinout-container.sh status
./esp32-pinout-container.sh stopTo force a specific engine instead of auto-detection:
CONTAINER_ENGINE=podman ./esp32-pinout-container.sh start
CONTAINER_ENGINE=docker ./esp32-pinout-container.sh startAvailable commands: build, start, stop, restart, logs, status, shell, clean, dev.
Environment variables: PORT (default: 8082), CONTAINER_ENGINE (docker or podman, default: auto-detect).
With rootless Podman, dev mode (mounted volumes) automatically adds the :Z SELinux label to mounts — harmless on systems without SELinux.
start verifies the container is actually running afterwards (some compose tools, notably podman-compose, can exit successfully even when the startup failed internally, e.g. port already in use). On failure, any existing container with the same name is automatically cleaned up before retrying, and the conflicting port is identified when possible.
Dockerfile / Containerfile Image definition (identical), for Docker / Podman
.dockerignore / .containerignore Files excluded from the build (identical)
server.py Flask backend (API /api/devkits + static files)
static/ Frontend (HTML/CSS/JS, no build dependency)
devkits/<id>/ One subfolder per supported devkit
<id>.png Devkit image, fixed 400px height, no text or marker
<id>.xml Metadata + pin coordinates and names
scripts/ Script used to generate the 2 example devkits
Create a subfolder devkits/<Devkit-Name>/ containing:
<Devkit-Name>.png— devkit image, fixed 400px height (free width), no text or marker baked in: pin colours and names are drawn dynamically by the frontend on top of the image.<Devkit-Name>.xml— in the following format:
<?xml version="1.0" encoding="UTF-8"?>
<devkit>
<name>Commercial name of the devkit</name>
<created>YYYY-MM-DD</created>
<version>1.0</version>
<updated>YYYY-MM-DD</updated>
<notes lang="fr">Remarques libres en français (variante flash/PSRAM, etc.)</notes>
<notes lang="en">Free-form remarks in English</notes>
<vendor_url>https://example.com/product-page</vendor_url>
<description lang="fr">Description affichée à côté du schéma, en français.</description>
<description lang="en">Description shown next to the diagram, in English.</description>
<image height="400"/>
<pins>
<pin x="46" y="20" label="3V3" side="left"/>
<pin x="46" y="38" label="EN" side="left"/>
<pin gpio="0" x="46" y="56" label="GPIO0" side="left"/>
<pin gpio="43" x="200" y="300" label="TX0" side="right"/>
<!-- one line per pin visible on the connector:
gpio="" is omitted for non-GPIO pins (EN, GND, 3V3, 5V…),
label carries the real displayed name (GPIO0, TX0, RX0, EN, GND, 3V3…),
side is "left" or "right" (connector side, for text alignment) -->
</pins>
</devkit>vendor_url, notes and description are optional; if present, notes is shown in the title above the diagram, and description + vendor_url are shown next to the diagram, in whichever language is currently selected (FR/EN button).
Multilingual format: use lang="fr" / lang="en" on <notes> and <description> to provide different text per language (as above). If a single <notes>/<description> tag is present without a lang attribute (legacy format, still supported), the same text is shown regardless of the selected language. If only one of the two languages is provided with lang=, the other language automatically falls back to that same text instead of showing an empty field.
The devkit automatically appears in the selector on the next page load — no code changes needed.
Important: the two example devkits (ESP32-S3-DevKitC-1 and ESP32-S3-DevKitM-1) use synthetic diagrams generated by a script (scripts/generate_devkit_assets.py), not photos of the real products, and their pin list (including the TX0/RX0/TX2/RX2 renamings) is indicative. Replace them with your own visuals and double-check the coordinates/pins against your exact hardware's datasheet before manufacturing.
Hard-coded in static/app.js (GPIO_FLAGS), independent of the chosen devkit:
- strapping GPIO0/3/45/46 ;
- native USB GPIO19/20 ;
- programming UART GPIO43/44;
- JTAG GPIO39-42.
Double-check against the datasheet of the module actually used.
- A single devkit diagram is shown, below the legend and above the interface grid. Every pin used by every interface is overlaid on it: one colour per interface (blue, green, cyan, yellow, purple…), grey for free GPIOs, red for a GPIO used by 2 different interfaces. Pins are labelled with their real name (
GPIO4,EN,GND,3V3,TX0,RX0…), not a plain number. - The legend updates automatically on every interface creation/deletion.
- A GPIO taken by one interface disappears from the other interfaces' dropdowns (global scope, only one devkit selected at a time); a residual conflict (e.g. after switching devkits) is still detected and flagged with a badge and a warning banner.
- Electrical interfaces are displayed in a grid (
div#boardsGrid) of 1, 2, or 3 columns as chosen ("Columns" setting in the top bar, 2 by default). Deleting an interface removes its card and the following ones automatically shift to fill the grid. - The + Add electrical interface button is unique, in the top bar, next to "Reset" and "Export all (.zip)".
- Reset deletes every electrical interface.
The interface is available in French and English (flag buttons "- FR" / "- EN" in the top bar). Default language: English (first load, no saved preference). The choice is remembered in the browser (localStorage).
On first load (no saved configuration), no electrical interface is predefined — the grid starts empty.
The configuration (chosen devkit, columns, interfaces, signals) is saved in the browser's localStorage — no server-side database.
