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.
- Choose Set Wi-Fi password or Reset storage, then Connect and inspect storage.
- 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.
- 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.
- 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.
- 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.