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:

Boot flow

Power on │ ▼ [ESP-IDF bootloader] ├─ ota_1 marked valid? ─► YES ─► Boot ThistleOS (done) │ └─ NO ─► Boot Recovery OS (ota_0) │ ▼ [Step 0] Detect supported chip type (ESP32/S3/C3/S2/C6) │ ▼ [Step 1] Check ota_1 partition state ├─ Valid ────────► boot_ota1() — reboot into main OS ├─ PendingVerify ─► boot_ota1() — let it verify itself └─ Invalid / NotFound ─► continue recovery │ ▼ [Step 2] Check SD card for firmware ├─ /sdcard/update/thistle_os.bin found? │ └─ YES ─► flash to ota_1 ─► reboot └─ NO ─► continue │ ▼ [Step 3] Start WiFi AP "ThistleOS-Recovery" [Step 4] Start HTTP captive portal (192.168.4.1) — WiFi, board list download, dry-run plan, install, reboot [Step 5] Poll captive portal requests │ ▼ (user action via web) Download selected board config + matching drivers + WM bundle Verify SHA-256 hashes + Ed25519 signatures Flash to ota_1 ─► Reboot into ThistleOS

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:

  1. Step 1 — WiFi Connect — enter SSID and password to connect the ESP32 to your network (WPA2 client mode simultaneously with the AP).
  2. 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.
  3. 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 to ota_1; other bundle files are verified and installed to SD.
  4. 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:

Captive portal redirect paths
iOS, Android, and Windows all use different URLs to detect captive portals. The Recovery OS handles /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:

MethodPathDescription
GET/Captive portal HTML page
GET/api/statusJSON: version, mode, ota1 state, sd_firmware
GET/api/boardsJSON: board list from the downloadable board catalog, or the built-in fallback list when offline
POST/api/board/selectBody: {"board":"tdeck-pro"}. Stores the board selection.
GET/api/bundle/planDry-run JSON plan for the selected board using the bundle catalog. Does not write flash or SD card.
POST/api/wifi/connectBody: {"ssid":"...","password":"..."}. Returns {"ok":true,"ip":"..."}
POST/api/bundle/downloadTriggers full bundle download for the selected board. Returns {"ok":true} and exposes progress through /api/bundle/status.
POST/api/rebootReboots the device after 1 second.

Building from source

The Recovery OS is a standard esp-idf-svc Rust project. You need:

# 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
Toolchain
The Recovery OS uses 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

FileContents
recovery/src/main.rsBoot sequence, WiFi AP setup, captive-portal polling loop
recovery/src/recovery_ota.rscheck_ota1(), check_sd_firmware(), apply_sd_firmware(), download_and_flash()
recovery/src/recovery_web.rsCaptive portal HTML (embedded as a &str), HTTP handler registration
recovery/Cargo.tomlCrate dependencies: esp-idf-svc, esp-idf-hal, esp-idf-sys, anyhow
recovery/build.rsESP-IDF build integration