Recovery OS
A minimal Rust firmware in ota_0 that starts a WiFi hotspot, serves a captive portal, downloads the selected board profile plus matching drivers and firmware from the catalog, verifies hashes and Ed25519 signatures, flashes ota_1, and reboots into ThistleOS without a programmer.
Purpose and design
The Recovery OS occupies the ota_0 partition, which is 0x280000 bytes in the current 16 MB layout. The ESP-IDF bootloader can fall back to it when the main OS in ota_1 is missing or invalid. Its job is simple: make sure there is a working ThistleOS in ota_1. If there is not, it provides a way to install one.
Key design choices:
- Rust + esp-rs — smaller binary than equivalent C code, and memory safety means fewer panic-class bugs in the one piece of code that must always work.
- Catalog-led board selection — Recovery detects the chip variant (ESP32/S3/C3/etc.) and lets the selected board profile drive firmware, driver, and window-manager selection. Generic I2C/SPI/UART probing is deliberately kept out of the minimal recovery path.
- Multi-arch chip detection — a single Recovery binary identifies whether it is running on Xtensa or RISC-V and selects the correct firmware architecture from the catalog.
- Downloadable board list — after WiFi connects, the web UI fetches the latest board catalog from
https://wan0net.github.io/thistle-os/catalog.jsonand keeps the built-in list as the offline fallback. - Full bundle download — Recovery downloads the firmware binary, matching drivers, board config, and the appropriate window manager in one provisioning flow. Catalog SHA-256 hashes and Ed25519 signatures are checked before executable files are installed or firmware is flashed.
- No LVGL — the recovery UI is a captive portal web page served over WiFi, viewable on any phone or laptop. The e-paper display shows status messages via raw framebuffer writes.
- No SD card required — if the SD card is missing or corrupt, recovery can still download firmware over WiFi.
- Web-first control loop — recovery continuously polls captive-portal requests so WiFi connect and bundle download actions do not depend on serial-console input.
Boot flow
Captive portal web UI
When Recovery OS starts its WiFi AP, any device that connects to ThistleOS-Recovery (open network, no password) is automatically redirected to the captive portal. The portal is a single self-contained HTML page served from flash — no SD card needed.
The portal provides a 3-step guided flow:
- Step 1 — WiFi Connect — enter SSID and password to connect the ESP32 to your network (WPA2 client mode simultaneously with the AP).
- Step 2 — Board Select — downloads the latest board list after WiFi connects, filters it by detected chip architecture, and keeps the built-in list available offline.
- Step 3 — Dry Run & Install — dry-runs the selected board against the bundle catalog, then fetches the firmware binary, matching drivers, board config, and window manager from the official bundle catalog URL (
https://wan0net.github.io/thistle-apps/catalog.json). Firmware is verified and flashed directly toota_1; other bundle files are verified and installed to SD. - Step 3 — Reboot — confirms installation; reboots the device into a fully provisioned ThistleOS.
The portal design reuses the same link42 dark theme as these docs:
/generate_204 (Android), /hotspot-detect.html (iOS), and /connecttest.txt (Windows), redirecting all of them to /.
Recovery control path
The captive portal is the primary control path. Recovery keeps the WiFi AP and HTTP server alive, polls queued web requests, connects STA WiFi, installs the selected bundle, and lets the web UI trigger reboot. The UART console is intentionally not part of the minimal install loop, so web-triggered work cannot stall behind blocking serial input.
REST API endpoints
The captive portal web server also exposes a small REST API for scripted recovery:
| Method | Path | Description |
|---|---|---|
GET | / | Captive portal HTML page |
GET | /api/status | JSON: version, mode, ota1 state, sd_firmware |
GET | /api/boards | JSON: board list from the downloadable board catalog, or the built-in fallback list when offline |
POST | /api/board/select | Body: {"board":"tdeck-pro"}. Stores the board selection. |
GET | /api/bundle/plan | Dry-run JSON plan for the selected board using the bundle catalog. Does not write flash or SD card. |
POST | /api/wifi/connect | Body: {"ssid":"...","password":"..."}. Returns {"ok":true,"ip":"..."} |
POST | /api/bundle/download | Triggers full bundle download for the selected board. Returns {"ok":true} and exposes progress through /api/bundle/status. |
POST | /api/reboot | Reboots the device after 1 second. |
Building from source
The Recovery OS is a standard esp-idf-svc Rust project. You need:
- Rust toolchain with the
xtensa-esp32s3-espidftarget (and/or RISC-V target for C3/C6) espup(Espressif's Rust toolchain installer)
# Install espup and the Xtensa Rust toolchain
cargo install espup
espup install
# Activate the toolchain in your shell
. $HOME/export-esp.sh
# Build the recovery firmware
cd recovery
cargo build --release
# Flash to ota_0 partition specifically
espflash flash --partition-table partitions.csv \
--partition ota_0 \
target/xtensa-esp32s3-espidf/release/thistle-recovery
esp-idf-svc which builds a vendored ESP-IDF internally. The first build takes several minutes and requires internet access to download the ESP-IDF toolchain managed by .embuild/.
Source layout
| File | Contents |
|---|---|
recovery/src/main.rs | Boot sequence, WiFi AP setup, captive-portal polling loop |
recovery/src/recovery_ota.rs | check_ota1(), check_sd_firmware(), apply_sd_firmware(), download_and_flash() |
recovery/src/recovery_web.rs | Captive portal HTML (embedded as a &str), HTTP handler registration |
recovery/Cargo.toml | Crate dependencies: esp-idf-svc, esp-idf-hal, esp-idf-sys, anyhow |
recovery/build.rs | ESP-IDF build integration |