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.