GitHub

Lessons learned

Pitfalls met while building and testing the converter, and what avoids them. Each cost real time once. The research they came from is in automatic-wifi.md and the other documents here.

Building firmware

Bridge build settings. .gitignore had sdkconfig.*, which also ignored archive/bridge/sdkconfig.defaults, so the repository silently lacked the settings v3 was built with, and every rebuild got ESP-IDF’s defaults. The result booted, aborted at once and the bridge’s own fallback put the plug back on stock within seconds: it looked like the plug rejecting the image. Diffing the rebuilt machine code against v3 (task stack size, httpd_req_t offsets, the Wi-Fi NVS check) recovered four settings:

.gitignore now has !sdkconfig.defaults. Delete any generated sdkconfig.* before rebuilding, or old settings are reused. Theories that looked plausible and were wrong: image size alignment (a padded build bounced too) and the updater itself (the shipped v3 stayed up for over an hour).

Comparing a rebuild with a published image. Neither build is byte-for-byte reproducible. Compare with cmp -l and map the differing offsets:

Anything else differing means the source or toolchain differs. Publish from a clean build (pio run -t clean first), even though an incremental one came out identical.

tools/build_recovery_safeboot.sh expects a project-local PlatformIO (.platformio-core, .pio-home). On a machine with PlatformIO installed normally (~/.platformio/penv/bin/pio) it stops with “No module named platformio”. The images published in this project were built by doing its steps by hand: check the tree is clean at the pinned commit, apply volatile-wifi-defaults.patch then persist-settings.patch, install the header from tools/configure_imprintable_safeboot.py, pio run -e tasmota32c3-safeboot, then tools/sanitise_safeboot.py and tools/validate_imprintable_safeboot.py. Reverse the patches and remove the header afterwards; the script’s checks refuse a dirty tree.

Compiler folding. A test that padded the bridge with a const array had the array optimised away; it needed a const char *volatile reference to be kept. Check the image size actually changed.

ESP-IDF and NVS

Tasmota

Testing on hardware

Use a converted plug where you can. The settings template was captured and verified on one, backing up its configuration first and restoring it after; tools/capture_tasmota_template.py keeps the backup in memory and only offers to save it as a file if the restore fails.

Testing firmware with ESPHome’s rollback. An ESPHome plug built with ESP-IDF has bootloader rollback (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y in its build’s sdkconfig). A firmware that never confirms itself gets exactly one boot: any restart, crash or power cycle returns to ESPHome. Tasmota builds never confirm themselves, because the Arduino core only does so when its own CONFIG_APP_ROLLBACK_ENABLE is set, and it is not for the C3.

Stale connections can lock you out of an ESPHome plug. ESPHome accepts only a few API connections at once. docker compose run containers outlive the command: Ctrl-C or a timeout stops the client, not the container, so each stopped esphome logs session stayed connected. Six of them, one left over from each attempt, used up the plug’s connections, and it refused every new one with “EOF received” (SocketClosedAPIError). The plug looked broken; it was only full. Give the container a --name and docker stop it, and check docker ps for left-over esphome-run containers before suspecting the plug.

Piped Python output is buffered. esphome logs ... | grep showed nothing until -e PYTHONUNBUFFERED=1 was set.

Watching a plug. A network that lasts five seconds is missed by a 10 s scan; ping and scan in separate loops. pkill -f can match its own command line.

A one-off startup message can be missed. A log session that reconnects after the message was printed never sees it; have the device report the state repeatedly instead, as the temporary key-count build did.

Process