GitHub

One-shot OTA bridge

This is a native ESP-IDF ESP32-C3 application, not a factory image. It is designed to be wrapped with tools/build_vendor_ota.py and written by a plug’s stock updater - the Sonoff S60’s, so far - to its inactive application slot. The application is the same for every plug; the wrapper names the plug’s model, so each kind of plug has a wrapped file of its own.

Bridge v3 first confirms itself so ESP-IDF rollback cannot later discard the recovery slot, then selects the peer application for the next boot. If Wi-Fi or the HTTP server fails, a power cycle returns to that peer. Immediately before an upload erases the peer, the bridge selects itself instead. It selects the new image only after esp_ota_end validates it.

The remaining irreducible risk is failure before app_main executes. The stock 1.2.0 bootloader was built without ESP-IDF application rollback, so no software running in an app partition can eliminate that risk.

Bridge v5 added two things. It reads the Wi-Fi the stock firmware saved and joins that network, so the run finds it where the plug was. And it takes Tasmota’s settings template, fills in the Wi-Fi it joined with, and saves it where Tasmota reads its settings, so Tasmota joins by itself. The password leaves the plug only inside the backup the run reads through GET /flash.

Bridge 4.4.2, the one the run installs, joins the stock network alone and raises its own network only if it has had no address there for 45 seconds, at start-up or later. v5 ran both at once on one radio, and on a plug the joined network dropped out for seconds at a time while its own was up; tried on the same plug, 4.4.1 (then called “v6”) stayed on for ten minutes without one. Where stock saved no Wi-Fi it raises its own network at once, as v3 did.

From 4.4.1 on, a bridge is named by one version: BRIDGE_VERSION in main/main.c and PROJECT_VER in CMakeLists.txt (a test keeps them equal) are the version its file is wrapped with and the one /status reports. Change both for the next bridge, 4.4.5. The stock updater is told 9.9.9 instead: the sender wraps the app again as that, above any Sonoff release.

Bridge 4.4.3 starts even when Sonoff’s slot is no longer bootable - an upload of Tasmota into it (D1) cut off part way, by a power cut or the computer stopping. 4.4.2 refused to start then, so the plug came up with no network at all, every time, and only a serial cable could recover it. With no fallback left there is nothing to keep: 4.4.3 says so in its log and on /status (fallback: none), stays the one that starts, and takes the upload again. And once on its own network, 4.4.3 tries the home network again every three minutes, for 20 seconds, while nobody is joined to its own: a router back after a restart is found again, where 4.4.2 kept to its own network for good. Never both at once, as 4.4.1 found on one radio.

Bridge 4.4.4 goes by autoflash-recovery-XXXX (the last two bytes of the plug’s Wi-Fi MAC) on the home network and as its own network, as every firmware a plug runs part way through a conversion does, and its page is titled “Autoflash bridge”. 4.4.3 joined as s60-ota-bridge, raised Sonoff-Setup-XXXX and was titled “S60 OTA Bridge”; the run still knows a plug left on it by those names. On its own network, with no proven Wi-Fi, 4.4.4 still saves the Tasmota settings it is sent - without Wi-Fi details - where 4.4.3 refused them (409): Tasmota then starts named autoflash-recovery-XXXX and makes its own network by that name.

From 4.4.2 every build has the read-only GET /flash (the whole chip, with its size in X-Flash-Size), GET /flash-sha256 (the SHA-256 of the whole chip, worked out on the plug) and GET /efuse (the eFuse read registers), so the run keeps a backup of the Sonoff firmware, checked byte for byte against the plug, before Tasmota is written.

sdkconfig.defaults holds the settings v3 was built with; with them a rebuild matches the published v3 except for the build time, date and ELF hash. Keep CONFIG_ESP_WIFI_NVS_ENABLED=n: the bridge never initialises NVS, so with it on esp_wifi_init() fails and the bridge aborts at start-up (the plug then falls back to stock by itself). Before rebuilding, delete the generated sdkconfig.autoflash-bridge* files, or old settings are reused - but never sdkconfig.defaults: a sdkconfig.* wildcard removes it too, the build then silently turns Wi-Fi NVS back on, and the bridge downloads in full but never starts. Check a rebuild against the published bridge: built from the same source it differs only in the build time and date, the ELF hash and the image checksum.

Build with PlatformIO from a path without spaces (the ESP-IDF builder used here does not correctly quote every generated CMake path):

cp -a archive/bridge /tmp/autoflash-bridge-build
rm -f /tmp/autoflash-bridge-build/sdkconfig.autoflash-bridge*
pio run -d /tmp/autoflash-bridge-build -e autoflash-bridge

The application image is then:

/tmp/autoflash-bridge-build/.pio/build/autoflash-bridge/firmware.bin

Wrap the application for the stock updater - the version comes from the image, and the model is the S60’s unless --model says another - and validate it:

python3 tools/build_vendor_ota.py \
  /tmp/autoflash-bridge-build/.pio/build/autoflash-bridge/firmware.bin \
  artifacts/autoflash-bridge-sonoff-s60-4.4.4.ota
python3 tools/analyze_vendor_ota.py artifacts/autoflash-bridge-sonoff-s60-4.4.4.ota

When it cannot join the stock network, or stock saved none, it creates autoflash-recovery-XXXX (the last four hex digits of the plug’s Wi-Fi MAC), an open network. Browse to http://192.168.4.1/ to upload a native ESP32-C3 application image. The browser sends the selected file as a raw request body; JavaScript must be enabled.

The research env autoflash-bridge-flash-readout images a stock plug without opening it or converting it: it has the same GET /flash and GET /efuse, and leaves out /update and /tasmota-settings, so the only flash it writes is the startup otadata selection that returns the plug to stock on the next power cycle. The stock updater writes it over the inactive slot, so whatever was there (often the previous Sonoff version) is lost; the running stock version, bootloader, partition table and NVS are kept. Build it with pio run -e autoflash-bridge-flash-readout, wrap it as above into captures/flash-readout/autoflash-bridge-sonoff-s60-flash-readout-4.4.4.ota, then run tools/flash_readout.py: it installs it, reads and checks the dump, and walks you through putting the plug back to stock.

Do not deploy this bridge until its build artifact and wrapped OTA file pass the offline validators and a compatible final Tasmota image has been prepared.