Flashing
Three ways to get firmware onto the box: your browser, idf.py, or OTA.
On this page
Which route?
| Situation | Use |
|---|---|
| First flash, or a board in an unknown state | Browser flasher — nothing to install. |
| You are developing the firmware | idf.py — you need the build anyway. |
| A working Klingelbox you want to update | OTA — keeps all configuration. |
The routes are the same for every supported board — the release image is one image that boots on the 16 MB dev board and the 4 MB ESP32-S3 Zero alike (see one image, every board).
A full-image flash factory-resets the device: Wi-Fi credentials, hostname, MQTT settings, the web password, the TLS certificate and key, every learned signal and the whole node graph are wiped. Only OTA preserves them. (That wipe is also the one and only recovery from a forgotten web password — see security.md.)
Route 1 — the browser flasher
Open the Klingelbox Web Flasher
It runs entirely in the page: it fetches the release image from this site, verifies its SHA-256, and writes it to your board over the ESP32-S3’s native USB. No firmware, credentials or serial data leave your computer.
Requirements
- Chrome or Edge on a desktop (Windows, macOS or Linux). The Web Serial API does not exist in Safari, does not exist in Firefox, and does not exist in any browser on iOS or Android — including Chrome on those platforms. The page detects this up front and says so rather than failing halfway through.
- A data USB cable. Charge-only cables are the single most common reason the port list comes up empty.
- The board’s native USB socket (USB-Serial/JTAG), not a separate UART bridge port, if it has both.
Download mode (the “BOOT dance”)
Many boards enumerate without any of this. If yours does not appear in the browser’s port prompt:
- Unplug the board.
- Hold
BOOT(labelled0orIO0on some boards). - Still holding it, plug the USB cable in.
- Release
BOOT.
Already plugged in? Hold BOOT, tap RESET (RST/EN), release BOOT.
Let go of
BOOTonce the board is plugged in.BOOTis not a “flash mode” switch you hold down for the duration. It is read only at the instant of power-up or reset. If you keep it pressed — or wedge it down with tape because it seemed to help — the chip re-enters the ROM download loader on every boot and the firmware you just wrote never runs. The flash reports success, the board looks dead, and nothing anywhere tells you why. This genuinely cost the author an afternoon.
What it writes
One segment: klingelbox-esp32s3-full.bin at offset 0x0. That is the merged image — bootloader, partition table, otadata, app and the web-UI SPIFFS in one file — so a board that had nothing on it is complete afterwards.
After flashing
- Make sure
BOOTis free, then let the board reset (unplug/replug if needed). - It boots with no Wi-Fi and raises an open recovery hotspot called
Klingelbox-XXXX(the last two bytes of its MAC), on channel 1. - Join it. The captive portal opens the Wi-Fi wizard automatically; if it does not, browse to
http://192.168.66.1. - Enter your Wi-Fi. The box reboots onto your network at
http://klingelbox.local.
Can I brick the board?
No. The ESP32-S3’s first-stage bootloader is in mask ROM and cannot be overwritten. Whatever is (or is not) in flash, the BOOT dance always brings the chip back into download mode. The flasher also offers a full erase for boards in a state you no longer trust — that too is recoverable.
Route 2 — idf.py
ESP-IDF v5.3.1, target esp32s3.
cd firmware
idf.py set-target esp32s3
idf.py build
idf.py -p /dev/ttyACM0 flash monitor
The port is /dev/ttyACM0-ish on Linux for the native USB port (/dev/ttyUSB0 if you are going through a UART bridge), /dev/cu.usbmodem* on macOS, COMx on Windows.
The web UI lives in the storage SPIFFS partition and is built and flashed as a separate image. idf.py flash handles the app; the SPIFFS image is produced by the build and written the same way.
One image, every board
There is exactly one partition layout (partitions.csv, sized for 4 MB flash) and one release image, and it boots on every supported board. On the ESP32-S3 Zero it fills the chip; on a 16 MB dev board it works identically and leaves the upper 12 MB unused. Nothing to build yourself, no variant to pick — the ESP32-S3 Zero uses the same browser flasher, the same release download and the same OTA as everything else.
Boxes that were flashed before the unified layout (v0.8.0 and earlier releases used a 16 MB table) are fine and need nothing: OTA never rewrites the partition table, so they keep their old table and simply keep working — the app update and the web-UI update land in their old partitions exactly as before. Do not re-flash a working box at 0x0 to “get the new table”; that is a factory reset and buys you nothing.
Route 3 — OTA (updating a working device)
OTA is the right way to update a Klingelbox that already has your Wi-Fi, signals and graph, because it keeps all of them. The app has two slots: the updater writes the inactive one, reboots into it, and a bad image is rolled back on the next boot.
The app and the web UI are separate partitions and separate updates. Updating the app leaves the old UI in place until you update the UI too — so after a release that changes both, do both. Since v0.9.0 the web UI notices the drift itself: when the page was built for a different firmware version than the box reports, it shows a small dismissible banner suggesting the pending web-UI update.
The examples below assume the out-of-the-box posture (no password, no TLS). If you set a web password, add -u admin:PASSWORD to every command (a 401 is what forgetting it looks like). If TLS is enabled, target https://klingelbox.local and give curl the pinned certificate (--cacert klingelbox.pem, or -k to skip verification) — plain-HTTP POSTs are refused with a 403 pointing at port 443. Details in API.md — Authentication & TLS.
# app image
curl -X POST http://klingelbox.local/api/ota \
-H 'Content-Type: application/json' \
-d '{"url":"https://github.com/MarvAmBass/klingelbox/releases/download/<tag>/klingelbox.bin"}'
# web UI image
curl -X POST http://klingelbox.local/api/ota/webui \
-H 'Content-Type: application/json' \
-d '{"url":"https://github.com/MarvAmBass/klingelbox/releases/download/<tag>/storage.bin"}'
Or upload the files directly, without the device needing internet access:
curl -X POST -H 'Content-Type: application/octet-stream' \
--data-binary @klingelbox.bin http://klingelbox.local/api/ota/upload
curl -X POST -H 'Content-Type: application/octet-stream' \
--data-binary @storage.bin http://klingelbox.local/api/ota/webui/upload
Both reboot on success. See the REST API for the full contract.
The release assets
| File | What it is | Where it goes |
|---|---|---|
klingelbox-esp32s3-full.bin | Merged image: bootloader + partition table + otadata + app + web UI. | Flash at offset 0x0 (browser flasher, or esptool.py write_flash 0x0). |
klingelbox.bin | The app only. | POST /api/ota — never flash this at 0x0. |
storage.bin | The web-UI SPIFFS image. | POST /api/ota/webui. |
Flashing an app-only or SPIFFS image at 0x0 overwrites the bootloader with something that is not a bootloader. The board will not boot; it is recoverable with a full image, but it is an entirely avoidable half hour.
Troubleshooting
The port list is empty. Charge-only cable, or the board is not in download mode. Swap the cable first, then redo the BOOT dance. On Linux you also need access to the device node — usually membership of the dialout group, then log out and back in.
“Failed to connect” / the connection stalls. Something else owns the serial port. Close any serial monitor, Arduino IDE, ESPHome dashboard or idf.py monitor session; only one process can hold a port at a time. Then redo the BOOT dance.
“Checksum mismatch” or “size mismatch” in the browser flasher. The download did not arrive intact and the page refused to write it. That is the intended behaviour — a truncated image at 0x0 leaves a board that cannot boot and cannot report why. Hard-reload the page (Ctrl/Cmd+Shift+R) and try again.
It flashed fine and the board does nothing. Check BOOT is not still held or jammed — see the warning above. Otherwise unplug and replug; the USB-Serial/JTAG port re-enumerates silently after a reset. If it still does nothing, attach a serial monitor at 115200 baud: a wrong image at 0x0 (an app-only or SPIFFS image, or a truncated download) shows up there as a bootloader, partition-table or SPIFFS mount failure. (An old 16 MB-layout release image — v0.8.0 or earlier — on a 4 MB Zero fails the same way; flash a current release, which fits every board.)
klingelbox.local does not resolve. mDNS is not universal. Find the device’s IP in your router’s DHCP list and use that. Some Android versions and some corporate networks block mDNS entirely.
Is it running at all, and which version? (no serial cable needed) The box advertises its firmware version in its mDNS service record, so on macOS/Linux one line answers “what is actually running?” even when the web server itself is the thing that died:
dns-sd -L Klingelbox _http._tcp # macOS (use _https._tcp when TLS is on)
avahi-browse -r _http._tcp # Linux
The TXT record shows version=…. If mDNS answers but the page will not load, the app is alive with a dead web server — check GET /api/system’s reset_reason once it is back, and see security.md for the TLS fallback behaviour. No mDNS answer plus no ping means the box is off the network entirely.
I flashed it and it never joins my Wi-Fi. It falls back to the Klingelbox-XXXX recovery hotspot. Join that and re-enter the credentials — a typo in the password is by far the most common cause. The recovery portal is deliberately exempt from every “disable the AP” setting, so the box can never make itself unreachable.