Skip to content

Hardware profiles and optional services

ESP-IDF remains pinned to v6.1. ESP32 and ESP32-S3 have separate build profiles and SDK configuration files. Cross-builds establish compilation and image fit; mounted motion, electrical behaviour, optical dithering and radio reliability require acceptance on the exact hardware.

ESP32-C6 is no longer a supported target. AtomS3 Lite is the reference for a possible v1.1; the experimental v2 profiles use ESP32-S3 with the fixed WROOM GPIO map. ESP32-S31 remains a future experiment without a firmware build profile. Any port must qualify pins, driver capabilities, LED timing and OTA image identity separately.

Build and configure

Board Preset First-boot ear topology
Original Atom Matrix normal 18 per ear, 12 long-edge pixels
AtomS3 Lite s3-normal 18 per ear, 12 long-edge pixels; both unreversed for short-edge DIN
AtomS3R atoms3r-normal 18 per ear, 12 long-edge pixels; onboard BMI270 and optional ST7735 status screen
Custom S3 custom-s3-normal 50 per ear with default WS2812, configure pins
ESPurr v2 WROOM-1-N16R8 v2-normal (experimental) Fixed 69 per ear; four chains 48/21/48/21
ESPurr v2 WROOM-1-N8 v2-8mb (experimental) Same fixed geometry/pins; PDM on, contact audio and PSRAM off by default

All profiles use WS2812 output; APA102 support has been retired. Existing SDK configs that still select APA102 are rejected before regeneration: retain that config and configure a separate build for actual WS2812 hardware. Saved topology settings retain precedence; changing first-boot defaults does not resize an existing assembly's saved counts or long edges.

The AtomS3 Lite v1 profile uses 18 active pixels per ear. For the v1 WS2812 ears, physical DIN starts at the short-edge baseline: 6 pixels up the short edge, then 12 down the long edge. Logical order follows that chain directly; outer_pixels=12 remains the long-edge count. The v1 reverse switch is an optional output transform; v2 has no switch. Each ear's saved count, long-edge split and direction remain independently editable.

idf.py --preset s3-normal menuconfig
idf.py --preset s3-normal build

Firmware outputs use build/firmware/<preset>; profiles retain their existing ignored root-level SDK configuration files. The classic preset retains sdkconfig. Never use set-target, delete configs or erase NVS to switch between these presets. Saved runtime settings take precedence over first-boot defaults. Target-specific dependency locks retain exact managed dependency versions; dependencies.lock remains the classic-ESP32 lock.

The v1 controller upgrade now uses atoms3r-normal, with the internal BMI270 on SDA45/SCL0 and the existing GPIO2/1 Grove ear cable. The former Lite/MPU6500 local config remains separate. atoms3r-recovery is the matching module-only recovery build. See AtomS3R hardware, status screen and bring-up.

For v2 optional services, run idf.py --preset v2-normal menuconfig and open Espurr cat ears:

Option Build-time behaviour
Audio capture, diagnostics and sound effects — ESPURR_AUDIO Off omits capture tasks/receivers, PCM processing and recording. Sound bands remains a stable catalogue identity but reports unavailable support; local effect cycling skips it.
Dual PDM microphones and sound-reactive lighting — ESPURR_AUDIO_PDM Audio sub-option; off omits PDM receiver setup and spectral processing. Independent of contact capture, VAD and recognition.
BX TDM contact-audio capture — ESPURR_AUDIO_CONTACT Audio sub-option requiring the BX IMU; off omits the TDM receiver path. Enables diagnostic capture only, not VAD/recognition.
Ambient light and ear sensing service — ESPURR_SENSING Off omits the shared sensing task and all ALS/AW96105 sampling.
Ear touch/proximity sampling — ESPURR_TOUCH Sub-option of sensing; off omits AW96105 handles/state/register traffic while keeping ALS sampling. Ear diagnostics report unsupported.

The master switches and both source switches default on for v2 where supported, preserving existing behaviour; v2-8mb explicitly defaults contact capture off. They are unavailable on legacy profiles. Audio sources still start off and need explicit effect or diagnostic demand. These switches are firmware build choices, independent of saved user settings. Disabling audio does not disable BX motion/SFLP. No touch gestures, automatic brightness or production contact VAD are enabled by these options.

LED diagnostics are always included in the ordinary image. Hold the wearable button at power-up to request Wi-Fi recovery, release it, then make the next press a fresh five-second hold to enter diagnostics. Legacy single-click resumes animations while retaining the radio mode; v2 button 1 single-click turns LEDs off and button 2 click resumes. A shorter first press cancels the sequence. With web compiled out or no AP password, the same diagnostic gesture works without an AP. Continuing the original boot hold never changes mode. See the diagnostic sequence. Four quick primary/state-button clicks also enter diagnostics on both profiles, including when the S3 bootloader consumes a reset-held press for recovery. A profile with no wearable button has no physical diagnostic gesture.

Flash layout and reserved recovery

Legacy S3 profiles assume at least 8 MB flash and select partitions.8mb.csv. Both normal app slots are 3072 KiB; a 1920 KiB recovery app partition occupies the remaining space. The original 4 MB Atom retains IDF's two 1700 KiB OTA slots.

V2 selects partitions.v2.csv on the N16R8's 16 MB flash: two 4096 KiB OTA slots at 0x20000 and 0x420000, a 1920 KiB recovery test-app reserve at 0x820000, and a 6144 KiB model data partition at 0xa00000. NVS, OTA metadata and PHY locations stay the same. The app project name is espurr-v2; CMake rejects a v2 board using the legacy image family, and rejects that family on a legacy board.

The v2-8mb preset reuses partitions.8mb.csv: 3072 KiB OTA slots at 0x20000 and 0x320000, with a separate 1920 KiB recovery partition at 0x620000. It emits espurr-v2-8mb, with 8 MB in the native image header. CMake checks the matching flash/table/family combination. Both phones and browser reject cross-family OTA, even for a small binary: changing installed layouts requires a deliberate USB migration. The N8 layout has no model partition. Neither profile introduces a production ESP-SR dependency.

The N16R8 model label and raw data partition follow ESP-SR's packed-model convention. This reserves space only: normal builds do not create/flash a model image, mount or format the region, or load erased/unvalidated model data. Normal app OTA writes only the inactive application slot; recovery and models are excluded. Firmware and model data will be updated separately. The shared model region has no A/B slots: an interrupted update, corrupt/incompatible data or failed load must report an active error and disable voice recognition at runtime while lighting, motion, controls and independently enabled PDM reactivity keep working. A firmware rollback may therefore disable recognition until a compatible model is installed. A future updater still needs authenticated, bounded writes under the render/audio flash pause, integrity and compatibility checks, and explicit model validation before loading. Neither model upload nor recognition is implemented. See firmware diagnostics for warnings and recent logs.

The N8 preset deliberately disables PSRAM to build-check PDM reactivity without external RAM or speech recognition. Levels and Sound bands use internal bounded storage; diagnostic WAV recording still requires PSRAM and reports unavailable without it. A fitted octal PSRAM can be enabled through menuconfig using capability allocation; flash capacity and audio-source selection are separate choices. Enabling contact capture does not automatically enable VAD, require 16 MB flash or load a model.

The 16 MB experimental profile uses 80 MHz DIO flash and 80 MHz octal PSRAM, consistent with the N16R8's quad-flash/octal-RAM arrangement in the module datasheet. PSRAM is boot-initialised/tested with capability-only allocation. Existing malloc, task stacks, executable/static data and LED DMA/RMT payloads stay internal. Missing PSRAM may boot degraded; future audio allocation must check actual available capability memory rather than assuming 8 MB was initialised. This build is not memory/timing acceptance on a mounted module.

V2 currently starts with an unknown, zero supply allowance and therefore emits black, including flashlight/diagnostics/update indicators. The source/current service must supply a qualified valid allowance before lighting can operate. PCB-derived ring/centre rendering is connected. BX motion defaults to timestamped 120 Hz FIFO samples with the existing host gravity estimator; the menuconfig SFLP option remains experimental and off by default. See the BX ownership and qualification contract. Capture ownership supports independent internal air/contact requests, both off by default, with BX reset and flash pause acknowledgement. Both phones provide temporary audio capture and level diagnostics; v2 two-button gestures and independent brightness ramps are connected. Optional ambient/ear sensing provides LTR-F216A light and AW96105 raw/ baseline/electrode observations with independent health/recovery. Both phone apps include diagnostics, bounded audio recording/export and Sound bands controls. Automatic brightness, touch/proximity reactions and qualified contact VAD remain pending hardware/recordings; further VAD work is deferred until those inputs exist. This is a linked integration profile, not a completed wearable release.

First installation of this layout requires USB/ROM recovery, not app-only OTA. Before an authorised install, verify the physical module and serial destination, back up the existing NVS range (0x9000, 0x6000 bytes) into private ignored artifacts, and verify the backup. Flashing the generated v2 plan installs the bootloader/table/application and initialises OTA selection for the new layout; it does not erase the NVS partition. Preserve credentials and do not run erase-flash. Verify the installed table and NVS after reboot. Normal builds do not flash the recovery partition; install its separate image only after confirming this layout. The S3 web installer uses recovery-only bootstrap bundles with the native GPIO hold enabled; see bootstrap plans. No first-install procedure has been physically accepted yet.

Legacy 8 MB S3 layout:

Partition Offset Size
NVS 0x9000 24 KiB
OTA metadata 0xf000 8 KiB
PHY data 0x11000 4 KiB
ota_0 0x20000 3072 KiB
ota_1 0x320000 3072 KiB
recovery 0x620000 1920 KiB

The recovery partition uses subtype app,test. Normal builds do not include it in their USB flash plans; normal OTA cannot write it. Erased OTA metadata selects ota_0, rather than an empty factory image.

Dedicated recovery builds

Board/layout Recovery preset Image USB application offset
AtomS3 Lite, 8 MB s3-recovery espurr-recovery.bin 0x620000
Custom S3, 8 MB custom-s3-recovery espurr-recovery.bin 0x620000
V2 N16R8, 16 MB v2-recovery espurr-v2-recovery.bin 0x820000
V2 N8, 8 MB v2-8mb-recovery espurr-v2-8mb-recovery.bin 0x620000

Each is a separate application build with its own ignored sdkconfig.<preset>. The board/flash family must match the installed module; a normal profile's local menuconfig changes are not inherited. Only optional LED progress uses external pin and output settings. They do not gate Wi-Fi or firmware upload. The original 4 MB Atom Matrix layout has no recovery partition and no dedicated recovery preset. Its existing normal-firmware Wi-Fi recovery remains available.

idf.py --preset v2-recovery menuconfig
idf.py --preset v2-recovery build

Set Espurr cat ears → Private configuration AP password locally in the recovery config before installation. This is a private fallback, not a shared repository password. Recovery first uses a valid saved AP password; missing, empty, corrupt or unsupported settings fall back to its configured password. Without either valid password the AP stays disabled and serial reports the missing fallback. Builds do not require or generate a password, and never enable an open AP.

Recovery starts the Wi-Fi updater without a boot-held button and keeps it open until reboot, including when there are no clients. A dedicated module-only runtime serves the existing offline browser, bounded logs and shared OTA worker. It does not start board buses, the IMU, power monitors, ambient/touch sensors, audio, matrix clearing or the animation task. PSRAM is omitted. Bonded BLE defaults on alongside Wi-Fi. Only the primary user button is started for fresh numeric-comparison confirmation; the boot hold must be released first. Button startup failure leaves Wi-Fi available. Disconnected or failed external hardware therefore cannot prevent AP startup or firmware recovery. The ESP32-S3 module, matching flash layout and usable saved/fallback AP credentials are sufficient; no wearable button confirmation is required.

Best-effort WS2812 recovery update progress (ESPURR_RECOVERY_LEDS) is enabled by default on legacy WS2812 profiles and can be disabled in menuconfig. The OTA worker owns it and attempts pin validation and LED initialisation only on its first upload, after networking is available. Validation/allocation/transmission failures disable the progress output for the rest of that boot and still return successful progress acknowledgement to the updater. Recovery RMT waits have a 100 ms bound; on a failed wait IDF is asked to stop the channel and its interrupts before the next write. A failed output is never retried or its retained payload mutated. Normal firmware retains its stricter render/flash-pause contract and existing waits.

V2 recovery builds are headless. V2 recovery does not run the external power-qualification service and cannot grant an LED allowance; it must not bypass that policy just to show a bar. The browser still displays upload progress on every profile. Recovery does not clear previously latched LEDs; disconnected/failed outputs and LEDs retaining an earlier frame are not evidence of failed recovery. Hardware/LED diagnostics remain in the normal firmware.

Settings load and migrate in memory, but recovery never saves the settings record or applies settings/control edits. The common browser's edit requests report unsupported without changing state. This protects newer normal-firmware settings from an older recovery image. The browser's Reboot action returns through the normal boot sequence; Exit remains unavailable because recovery stays active until reboot.

Recovery image names end in -recovery, so ordinary firmware/browser/phone update preflight rejects them. Recovery accepts only its matching normal family (espurr, espurr-v2 or espurr-v2-8mb) and S3 chip identity. It chooses the inactive OTA slot relative to the selected normal boot partition, rather than always selecting ota_0 from the running test partition. Native final image validation, interruption handling and rollback remain in use. Recovery never updates itself or model data.

Both recovery flash and app-flash plans contain only the recovery app at the offset above. They do not install a bootloader/table, reset OTA metadata or write normal apps, NVS or models. Install only over a verified matching normal partition table, after the usual private NVS backup and destination checks. Normal full USB flashing continues to exclude recovery. No device was flashed during implementation.

Public S3 recovery presets enable the native five-second GPIO41 low hold by default. The web flasher uses recovery-only USB bootstrap bundles, with separate sparse reset/preserve manifests. Reset clears A/B and OTA selection so native image-validation fallback boots recovery; preserve retains those regions. Both preserve NVS, PHY and model storage and never mass-erase the chip. Preservation requires the same installed layout. Custom S3 recovery and the optional normal-image preparation helper retain explicit button configuration. First-use credentials and phone release discovery/download remain deferred. Both phone apps support BLE recovery with manually selected normal app files. No installation was performed.

With an enabled bootloader, short reset-held presses are consumed before normal input startup. Runtime radio gestures remain unchanged; four quick state-button clicks enter normal LED diagnostics. Default gesture-disabled bootloaders retain the original boot-held Wi-Fi and release/fresh-hold diagnostic sequence.

Existing generated SDK configs override defaults. To adopt the new layout, use the selected preset's menuconfig: set Serial flasher config → Flash size to at least 8 MB, then Partition Table → Custom partition table CSV with filename partitions.8mb.csv. A full USB flash installs the new bootloader/partition table, initial OTA metadata and normal app; an app-only OTA cannot migrate the layout. NVS, PHY and OTA-metadata offsets/sizes are preserved. Migration changes ota_1's location and resets OTA selection during full flashing; no NVS erase is required.

Legacy profiles do not enable or require PSRAM. The packed SPI DMA buffer for two custom-S3 50-pixel ears totals 2880 bytes across two buffers, including reset and excluding handles/driver allocations. LED payloads and render/IMU state stay internal. External RAM could hold future noncritical web/catalogue or larger asset buffers. IDF supports external-RAM DMA on S3 with limitations and cannot place DMA descriptors there; external RAM is normally inaccessible while flash cache is disabled. See the pinned PSRAM guide.

Pins

These are configurable ESPurr external wiring defaults. Onboard assignments follow AtomS3 Lite documentation.

Board IMU SDA/SCL WS2812 left/right Button Onboard RGB
Atom Matrix 25/21 26/32 39, active low, external pull 27, 25 pixels
AtomS3 Lite 5/6 2/1 41, active low, external pull 35, 1 pixel
AtomS3R atoms3r-normal 18 per ear, 12 long-edge pixels; onboard BMI270 and optional ST7735 status screen
Custom S3 Explicit Explicit Optional Optional

AtomS3 Lite needs an external IMU. The custom S3 profile deliberately leaves required pins unset and rejects incomplete configuration before touching outputs. Button GPIO -1 disables button input; zero onboard pixels disables onboard clearing.

The AtomS3 Lite Grove yellow/white positions are GPIO2/1, matching the original Matrix ear cable's GPIO26/32 positions. Its external IMU uses rear-header GPIO5/6 for SDA/SCL instead. Existing generated configurations keep their explicit pin values; update those four fields through menuconfig when adopting this mapping. Validation rejects pin overlap, invalid outputs and reserved flash/USB pins.

Feature switches

Under Espurr cat ears, BLE, web/Wi-Fi, OTA and IMU sampling can be enabled independently; OTA requires at least one enabled radio transport. Both targets default to all enabled. BLE requires IDF Bluetooth with the NimBLE host, supplied by project SDK defaults.

Disabled services have no startup task, uploaded asset or registered endpoint. OTA-off still retains the pending-image rollback health gate and USB recovery. IMU-off publishes invalid motion and refuses calibration, while effects retain their degraded appearances. Available capability bits govern phone/watch editors. An unavailable radio request preserves the current mode; exiting a web-only build returns to standalone output. BLE/Wi-Fi remain mutually exclusive when both exist.

LED hardware

WS2812 uses independent data wires. Normal S3 profiles drive them from one SPI2 DMA transaction: dual for v1 ears, quad for v2's four outer/inner chains. The GPIO matrix retains the existing pin assignments; SPI3 remains available for the AtomS3R display. Each LED bit occupies four 3.2 MHz SPI clocks: zero is 312.5 ns high / 937.5 ns low, one is 625 ns high / 625 ns low. Shorter chains receive encoded black padding before the common 300 µs low reset. Both bit values take 1.25 µs; 50 pixels take 1800 µs including reset, leaving about 422 µs before the next slot in the 450 Hz, nine-phase schedule. The eligibility estimate reserves at least 200 µs for software. Counts above the estimated 57-pixel full-speed S3 limit select the highest fitting 50 Hz step instead; 64-pixel v1 ears fit 400 Hz. See output timing and ownership for buffer sizes and failure handling.

The original ESP32 retains independent non-DMA RMT channels with 256 symbols each, the existing 10 MHz, 1.2 µs waveform and 280 µs reset, with a 300 µs software allowance and a 47-pixel limit at 500 Hz; longer chains select a lower fitting rate. Optional S3 recovery progress retains RMT and its bounded timeout/stop path. The S3's sole DMA-capable RMT TX channel is hardware channel 3; normal packed SPI output avoids that restriction. The owner reports the original ESP32 timing works well on the mounted 18-pixel strings. The packed dual-SPI AtomS3R/v1 setup has a 450 Hz worker/output serial smoke check; active radio interaction, electrical measurements and v2 quad hardware checks remain pending.

Effects render at 50 Hz. S3 starts with up to nine phases at 450 Hz; ESP32 starts with up to ten phases at 500 Hz. Pulses are evenly distributed with pixel offsets. Only completed output advances the phase. Four consecutive overruns lower the rate one 50 Hz step. Ten clean seconds retry one step upward; ordinary edits do not reset the learned rate. The lowest step is non-dithered 50 Hz. Deliberate pauses are excluded from adaptation; missed time is discarded. Diagnostics and flash/update holds suppress dithering.

Gamma, composition, layer priorities, trims, the 0.5-count whole-pixel black floor and instantaneous current bounds are shared. Existing profiles retain the legacy 1 mA idle/pixel and 5 mA/full-channel defaults. ESPURR_LED_IDLE_UA supplies an explicit microamp idle coefficient: set it to 200 for the confirmed v2 WS2812C 0.2 mA/pixel figure. Zero selects the existing ESPURR_LED_IDLE_MA value multiplied by 1000; it does not mean zero idle current. Existing mA configurations and channel coefficients retain their meaning. Set both estimates for the actual LED part, because package/interface alone does not establish current. Fractional mA are retained through limiting and telemetry. Measure the complete strip to validate the model. The budget remains 250 mA by default.

Package size “2020” does not establish internal PWM performance; verify optical dithering separately from transmitted frame cadence on the fitted LED part. Retain regulated strip power, common ground and suitable data signal level shifting; controller power and onboard LED current are outside the estimate.

IMUs

Legacy profiles select MPU6050, MPU6500, MPU6886, MPU9250, MPU9255, BMI270 or ICM-20948, with explicit I2C address/pins and a 100–400 kHz bus. Use a breakout with 3.3 V-compatible I2C signals and a supply matching that board. Only the selected driver is built. Motion uses acceleration and gyro, not magnetometer, FIFO or DMP features. All drivers publish g/degrees per second with native axes; mounting calibration establishes wearable axes. Host sampling remains 100 Hz, +/-4 g and +/-1000 degrees/second. ICM-20948's divider produces approximately 102 Hz internally; the host still polls at 100 Hz.

The board creates the configured I2C controller before runtime/device tasks start. Current legacy profiles still configure only I2C0 on the selected IMU pins, with the existing pull-ups/filter settings. The IMU borrows that bus and owns only its device handle; failed device/task creation and sensor reprobes do not delete or reset a bus another service may use. Bus creation failure is reported and permits degraded lighting/control, like an unavailable IMU.

The guarded v2 board contract uses I2C0 SDA/SCL 16/15 and I2C1 10/11, even when motion sampling is disabled. Both have external 4.7 kΩ pull-ups, so their internal pull-ups are disabled. Controller creation does not set transaction clock speed: new v2 device drivers must start at the handoff's 100 kHz and qualify cable loading. V2 pins are immutable board resources, including four outputs, both buttons, PDM, TDM and IRQs; GPIO35–37 are reserved for octal PSRAM and 47/48 remain unused for a possible WROOM-2 change. Native USB remains reserved. BX motion now has a single register/FIFO owner, 120 Hz acquisition and internal backend health; host gravity is the default, SFLP is experimental. The BX bus starts at 100 kHz; 400 kHz remains an explicit option requiring cable/loading qualification. Native audio capture and owner coordination are connected; sources start off until requested by an effect or diagnostic lease. Audio processing and recording/export and ambient/ear-sensor diagnostics are implemented, with matching phone controls. Contact VAD, automatic sensor reactions and physical qualification remain pending. See the BX contract.

The board registry can own both native controllers, with explicit port identity and no automatic allocation. Lifecycle operations are startup/owner-only; device tasks use borrowed native handles and bounded IDF transactions. The driver's transaction locking does not make multi-register/banked operations atomic: the single device owner must serialise those sequences. No render/control callback should wait on sensor I/O. Teardown requires quiescent clients and removed devices; failed deletions retain their handles for owner recovery. The registry does not provide a bus-fault scheduler; individual services own bounded device recovery.

MPU6886 remains the original Atom default; external profiles default to MPU6050. BMI270 uses Bosch SensorAPI v2.86.1, including its configuration image and licence under components/imu/vendor/bmi270. ICM-20948 uses its banked register interface. Identity/configuration errors prevent readiness and trigger bounded recovery.

NVS schema 19 associates calibration with target, chip, bus pins and address. A changed identity clears mounting/bias calibration while retaining other settings. Records predating this metadata are attributed to the original Atom MPU6886 bus. Migration saves remain deferred, records remain bounded to 4096 bytes, and rollback firmware cannot overwrite an unknown newer schema.

Validation

With the pinned environment active, python3 tools/test_hardware_profiles.py runs isolated target/transport, sensor and feature-off builds under build/tests/hardware-profiles/<target>/<case>. Pass case names to select a subset. It checks disabled-service symbols in the linked ELF and verifies generated partition sizes/offsets and image fit. Normal USB plans exclude recovery/model storage; recovery cases check that only the test-app partition is written and Bluetooth/audio/sensing/PSRAM are omitted. S3 cases use the same target-grouped layout. The 2026-10-13 cleanup preserved old logs, firmware/configuration snapshots and screenshots under build/artifacts/legacy/ and removed obsolete caches. A custom-board case supplies example pins only for compilation. Host suites exercise sensor register sequences/scaling/failures, configuration upload, RMT lifetime/failure handling, quantisation and NVS identity migration.

Before hardware acceptance, capture wire cadence under BLE polling and optional Wi-Fi association; check animation cadence separately. Exercise saves, reconfiguration, off, shutdown and updates. Verify mounted calibration and sensor recovery. Measure optical low-brightness behaviour, electrical current/temperature and signal integrity on the exact 90-pixel chain. Record flashed revision, installed clients and physical results separately from successful builds and fixtures.