Skip to content

S3 recovery bootstrap and boot selection

The S3 web installer installs recovery rather than a hardware-specific normal app. Recovery remains in the reserved app/test slot. Both normal OTA slots stay available for later app-only updates. Atom Matrix (classic ESP32, 4 MB) retains its normal USB installer and has no dedicated recovery slot.

Boot and radios

The stock IDF v6.1 bootloader enters recovery when the primary user button is held across reset/power-up for five seconds: GPIO41 low on AtomS3 Lite, AtomS3R and both v2 layouts. Release the button and reboot to return to the selected normal OTA image. The gesture does not clear OTA selection or NVS. Deep-sleep wake follows the SDK's ordinary wake path. No custom bootloader code or eFuse changes are introduced.

When A/B contain no bootable image, the native bootloader falls back to the test app. This is image validation fallback, not crash-loop counting. The test app is suitable for initial installation without a factory app partition. See the pinned SDK's bootloader_support/src/bootloader_utility.c and bootloader guide.

Recovery starts protected Wi-Fi and optional bonded BLE without sensor buses, motion, power qualification, audio, PSRAM or the animation/render owner. BLE defaults on and can coexist with recovery Wi-Fi; normal firmware retains exclusive radio ownership. BLE startup failure does not reject Wi-Fi. Both transports use the shared OTA worker; settings/control mutations remain unsupported. New pairing is rejected while updating, pending comparisons are cancelled, and bond/non-OTA flash writes are excluded by the runtime update/flash gate. Existing secured BLE carries OTA.

BLE recovery starts only the primary user button, including on v2. The startup hold is quarantined until release. A fresh click confirms numeric comparison through the existing 30-second pairing policy; no fixed passkey or Just Works fallback is added. If button startup fails, Wi-Fi and previously bonded BLE remain available. Optional legacy WS2812 progress is lazy and best-effort; v2 stays headless without its power qualification service. External LED or sensor failures never gate network startup.

Build and release artifacts

Build the ordinary recovery presets:

idf.py --preset s3-recovery build
idf.py --preset v2-recovery build
idf.py --preset v2-8mb-recovery build

These presets append sdkconfig.recovery-bootstrap.defaults, enabling the native GPIO hold by default. Existing local SDK configs still take precedence: use that preset's menuconfig to enable the native five-second GPIO41 low test-app hold if a saved config disabled it. Use idf.py --preset <recovery-preset> -D ESPURR_RECOVERY_BOOTSTRAP=OFF menuconfig to edit such a config, then build with the ordinary preset again. CMake rejects a bootstrap with the gesture disabled. Local configs and private values are not rewritten to impose new defaults. custom-s3-recovery remains a separate configurable app-only recovery preset; its button/polarity require an explicit board configuration and it is not in the public installer.

Release packaging validates the S3 chip, 8/16 MB capacity, app family, complete image checksum/digest, 1920 KiB recovery capacity, exact OTA/recovery/NVS layout, native button configuration and bounded bootloader/table segments. Families remain espurr-recovery, espurr-v2-recovery and espurr-v2-8mb-recovery; each accepts only its matching normal image family. Shared 8 MB geometry does not remove this identity boundary. Normal S3 release packages carry app-only OTA payloads, without redundant merged USB firmware images.

Recovery packages contain two sparse USB manifests:

  • manifest.json: bootloader, partition table, recovery, and explicit erased-byte images for A/B and OTA metadata. Recovery starts automatically afterwards.
  • manifest-preserve.json: bootloader, partition table and recovery only. A/B, OTA metadata and NVS are untouched. Existing normal firmware can still boot; use the GPIO hold to enter recovery. This requires the same installed partition layout.

The web installer also offers Install selected release in slot A and boot it. The site builder validates the paired normal app's complete checksum/digest, family, S3 identity and slot capacity, then generates manifest-app.json. It writes the same bootloader/table/recovery plus that release's app at ota_0 (0x20000). It writes the complete 8 KiB OTA metadata partition at 0xf000 last, after image writes. Slot B, NVS, PHY and model storage are absent from this plan.

The fresh metadata contains a CRC-checked sequence 1 entry in ESP_OTA_IMG_NEW, with the other sector erased. IDF selects A and changes it to PENDING_VERIFY on first boot, so normal firmware's input/render health gate remains active. This replaces old OTA selection/rollback history; retaining B's bytes does not assert that B was healthy. This USB install is not an atomic app-only OTA operation. First-use recovery credentials are not needed to install normal firmware by USB. The app-install and preserve choices are mutually exclusive; leaving both off keeps the recovery-only reset default. The downloadable recovery ZIP still contains the two recovery plans; the optional app plan is generated by the site from the paired normal release package without compiling another firmware image.

All plans leave NVS, PHY and v2 model storage untouched and disable whole-chip erase in the installer call. Separate segments are required: merged image padding would write through NVS and OTA slots. bootstrap-plan.json records both plans and flash settings; install.zip downloads all parts/manifests/checksums. Do not feed a sparse bundle to a single-offset merged-image command.

The native recovery flash and app-flash targets remain recovery app only; release packaging adds the bootloader/table and optional OTA-clearing segments. Normal firmware's native full flash plans are unchanged. The old tools/prepare_recovery_boot.py remains available for a normal full-install bundle with an explicitly configured/custom button and validated companion recovery image; it is no longer a prerequisite for the public recovery bootstrap installer. Its existing-device two-write plan still requires a matching installed table.

Phone BLE recovery

After ordinary authenticated discovery, iOS and Android use firmware.recovery and firmware.upload_project to distinguish the running recovery image from its accepted normal upload family. firmware.project continues to identify the running binary. The new fields are read-only and validated together; their absence retains older normal-firmware behaviour. See wire metadata.

Recovery opens an updater screen instead of the wearable control tabs. Check the Pages release catalogue and download matching normal firmware, or choose a local application .bin. Both clients retain live chip/capacity preflight, bonded GATT/OTS transport, progress, cancellation and retry. Recovery binaries cannot be uploaded as normal apps. Settings and calibration are blocked locally as well as by firmware. LED progress is optional. Watch displays a phone-only recovery message and offers disconnect; it does not OTA.

Android retains its automatic post-upload reconnect and checks normal mode, running family and version before reporting installation. iOS retains its explicit reconnect/check message after the confirmed reboot request; upload completion is not proof of a healthy normal boot. Both clients can reconnect to recovery for retry. The hosted picker is described below; file import remains available offline.

First-use setup and storage repair

Blank NVS is supported through bonded BLE. Pair with a fresh headset click, then use Set up recovery Wi-Fi in either phone app to enter or generate a password for the ears' own AP. The app shows the firmware-reported SSID. Saving credentials starts the AP in recovery; an already running AP uses the new password after its next restart. Join that network and open http://192.168.4.1/, or upload a manually selected normal app directly over BLE without configuring Wi-Fi. No open AP or shared default password is created. Passwords are not read back or saved by the phone app. Normal firmware exposes the same setup under Settings → Wi-Fi and retains its ordinary physical BLE/Wi-Fi mode switch.

AP credentials have a separate versioned NVS record, independent of normal settings and BLE bonds. Supported corrupt settings are repaired automatically at startup, retaining valid fields where possible. Recovery defers partial salvage writes until normal firmware can validate the actual sensor identity. Split settings keep controls, palette library, calibration and hardware in separate records; recovery reads them without writing group repairs. See the migration contract. Unknown newer schemas and NVS initialisation/I/O failures are preserved, not erased. See storage and provisioning.

If the whole NVS partition cannot be used, an explicit USB reset is available. This removes settings, AP credentials and BLE bonds; forget the old OS Bluetooth pairing afterwards and pair again. With the pinned IDF environment active:

# Inspection only: does not open USB.
python tools/reset_storage.py --chip esp32s3 --port /dev/cu.YOUR_BOARD
# Destructive: run only when deliberately resetting storage.
python tools/reset_storage.py --chip esp32s3 --port /dev/cu.YOUR_BOARD --execute --confirm-reset-storage

The tool reads and validates the installed checksummed partition table before erasing only its nvs partition. Apps, OTA metadata, recovery and model storage are kept. It supports ESPurr's standard partition-table offset 0x8000, not custom layouts with a relocated table. It never mass-erases or runs automatically.

Browser storage tools

The web installer's Storage and Wi-Fi panel provides the same explicit reset and can initialise AP credentials over USB. It works independently of the release selector, including when no releases are available. Install ESPurr first: a valid checksummed partition table at 0x8000 is required.

  1. Choose Set Wi-Fi password or Reset storage, then Connect and inspect storage.
  2. Inspection restarts the board into the USB bootloader and reads the chip, flash capacity, installed partition table and NVS. It writes nothing and shows whether storage is blank. Check the detected device and partition range.
  3. For Wi-Fi setup, enter 8–63 printable ASCII characters or generate a 20-character password. This is for the ears' own AP. Copy or note it before saving; the field clears on disconnect. Blank storage can be initialised directly.
  4. Existing storage requires explicit confirmation: this removes settings, Wi-Fi credentials and Bluetooth pairings. Use phone BLE provisioning to change only the Wi-Fi password while preserving existing data. Reset storage also always requires confirmation, even if inspection found blank storage.
  5. The browser writes only the validated NVS range, reads it back to verify every byte, then resets and disconnects the board. Firmware slots, boot selection, recovery and model storage are untouched. Forget old phone Bluetooth pairings after a reset. Enter recovery or switch normal firmware to Wi-Fi mode before joining the AP at http://192.168.4.1/.

Credential images are generated locally as IDF NVS v2 with only the versioned espurr_net/ap record. Use current firmware/recovery that supports this record; older images may ignore it. Explicit clipboard copy is available. Password fields are transient and cleared on disconnect or leaving the page; the installer does not persist or upload them. The Web Serial adapter uses pinned esptool-js 0.6.0 and local spark-md5 3.0.2 checksums, and never requests whole-chip erase. Encrypted NVS and relocated partition tables are unsupported. A failed/unplugged write is unconfirmed; reconnect and inspect before retrying. There is no automatic replay.

Storage tools are a separate explicit operation. The existing reset/preserve/app-A firmware installation plans continue to preserve NVS by default. No credentials are embedded in release files. See implementation evidence.

Hosted phone firmware picker

Both phone updaters use the existing public https://neggles.github.io/espurr/releases.json catalogue (schema 1). Requests begin only with Check for firmware or Refresh releases. Stable releases are shown by default; Include release candidates enables RCs. Candidates are filtered by the authenticated normal upload family. The older espurr family spans classic and S3 hardware, so users still choose their matching hardware profile; the live chip and actual inactive-slot capacity remain authoritative before Begin.

Download firmware fetches the normal app.bin and its SHA256SUMS.txt, then checks the exact checksum, native image header, family, chip and base release version. RC tags share the base application version by release policy. S3 bootstrap entries use app_checksums, never the recovery package's checksum list. New catalogues include the normal image_family; older schema-1 catalogues are recognised through the existing known profile names. Unknown families are omitted.

Downloads require HTTPS on the fixed Pages origin/path, accept no redirects or arbitrary URLs, use bounded reads/timeouts and keep images only in memory. A cancelled/stale request cannot select an image. Catalogue, checksum and app limits are 1 MiB, 128 KiB and the selected family/chip's existing OTA bound respectively. Empty/offline/error states retain refresh and local file import. Demo uses no network; debug UI checks inject explicit in-memory releases/images.

A verified download is only a selection for review. Install firmware remains an explicit action using the existing BLE updater, progress/cancel and final firmware validation. No firmware is installed automatically. Watch remains a phone-only recovery handoff. GitHub Pages receives ordinary download request metadata, not credentials, accessory identifiers, settings or diagnostics. See dated software evidence.

Deferred work and acceptance

Software-requested one-shot recovery remains an optional convenience for later. Custom crash-count fallback is not planned: retain native OTA rollback, bootloader image-validation fallback and the manual GPIO hold. An app already marked healthy can still crash repeatedly; the GPIO gesture remains its recovery path.

Normal LED diagnostics remain available through four quick primary-button clicks; this survives later app-only OTA updates even when a bootloader consumed the reset hold. Five or more clicks are ignored. Existing single/double/triple click and hold policies retain their normal roles.

Builds and host tests validate plans, checksums, button policy and image headroom. They do not establish physical boot, flash preservation, radio coexistence, runtime heap, pairing or OTA acceptance. No device was flashed during this implementation.