Local button ownership¶
All profiles use input.c with the pinned Espressif button component's managed
GPIO backend. V2 uses external pull-ups and active-low inputs; single-button
profiles retain their configured GPIO polarity/pull settings.
Only debounced DOWN/UP events enter input_pair_policy.c; the managed component
still owns GPIO setup and debounce. Gesture arbitration cannot use two independent
library click callbacks: a chord must consume both sequences, and a second press
held after a click must remain eligible for a hold. A small pure policy handles
those combinations without changing the managed dependency.
The gesture policy and its 20 ms, skip-missed timeout callback run on ESP_TIMER_TASK. Callbacks publish a short snapshot and enqueue discrete actions; they never drive LEDs, switch radios, access I2C or save settings. No new task is created. Failed creation/registration of an input or the timeout timer unwinds all created inputs.
Single-button gesture contract¶
| Gesture | Action |
|---|---|
| Single click | Global LED on/off; off clears flashlight; in diagnostics, resume normal operation |
| Double click | Toggle temporary flashlight |
| Triple click | Switch BLE/Wi-Fi |
| Four clicks | Enter LED diagnostics |
| Hold, starting at 600 ms | Ramp normal brightness, or independent flashlight brightness while active; release stops it |
Classification waits 250 ms after the final release, including double clicks so that triple clicks cannot also toggle flashlight. Five or more clicks do nothing. A click followed by a hold is only a ramp; holding past five seconds never switches radios. The ramp alternates direction between holds and stops at endpoints, just like v2. Effect selection remains available through the app/browser.
Boot-held recovery still owns the entire startup hold. After release, the next fresh five-second press enters diagnostics without ramping or changing radios. An intervening short press cancels that special sequence. The startup-only raw release fallback below also applies to the single input. Later ordinary holds ramp.
V2 gesture contract¶
| Gesture | Action |
|---|---|
| Button 1 single click | Global LED on/off; off clears flashlight; in diagnostics, stop diagnostics and turn LEDs off |
| Button 1 double click | Toggle temporary flashlight |
| Button 1 four clicks | Enter LED diagnostics |
| Button 1 five-second hold | Switch BLE/Wi-Fi; retain the existing service/password checks |
| Both held for five seconds | Request toggle of the temporary known-source current override |
| Button 2 single click | Dismiss flashlight without changing effect; otherwise next catalogue effect; in diagnostics, resume normal operation |
| Button 2 double click | Previous catalogue effect; during flashlight, dismiss only; in diagnostics, resume normal operation |
| Button 2 hold, starting at 600 ms | Ramp the selected normal/flashlight brightness; release stops it |
Single/double classification waits 250 ms after release. Three or more clicks have no v2 action and do not execute constituent single/double gestures. Partial 600 ms–five-second button-1 holds are consumed without a click. The five-second threshold can also be recognised on release if a timer tick was delayed.
Overlapping presses form a chord only before either individual hold has acted. The chord timer starts when the second button goes down. Neither radio switching nor brightness ramping runs during that chord, and its action fires once. An early release cancels it; a remaining held button cannot become an individual hold. Overlap after an individual hold acted cancels remaining gestures/ramping and requires both releases, without requesting an override. No triple-click bypass exists on v2. The override binding does not select a new allowance: the power owner still requires a qualified configured override allowance, known attached source, fresh monitors and no fault. Detach, stale data, faults and reboot retain their existing expiry rules. The actual active flag remains visible in phone and browser power diagnostics; gesture admission is not proof of activation.
Either button held at startup quarantines both until release. Only button 1 requests boot recovery; afterwards its next fresh five-second press enters LED diagnostics without changing the radio. An intervening press cancels that special sequence. A boot tap can disappear before managed debounce reports DOWN, so the startup-only fallback also accepts both raw pins released continuously for 50 ms. The original boot hold never becomes a chord, radio hold or brightness ramp.
Brightness and application state¶
input_ramp_snapshot() publishes a physical-hold generation, active state and
observation time, independently of the lossy discrete command queue. Render polls
it and stops on release or observations older than 100 ms. A stale/cancelled hold
cannot resume without a new generation. Power/flashlight requests, diagnostics,
updates and external flash holds cancel the current ramp. Inputs do not bypass
ordinary output suppression or current limits.
Render owns brightness_ramp.c. A hold selects its target when recognised; it
does not switch targets mid-hold. Normal and flashlight retain independent next
directions. Each accepted hold alternates that target's direction, with endpoints
always moving inward. One percentage point is due every 50 ms; the deadline phase
is retained on the normal 20 ms render cadence. Missed whole intervals are discarded,
with no burst or automatic endpoint bounce. Each step merges against the latest
authoritative setting, rather than a captured settings copy. While LEDs are off,
brightness holds (and v2 button-2 effect shortcuts) may edit settings but do not enable lighting.
Steps publish the live brightness and mark settings dirty, postponing the existing coalesced save. A ramp changes only one independent setting until it finishes. Its configuration revision commits at release, endpoint or cancellation; discrete commands retain their own revisions. This avoids invalidating every multi-batch BLE read at 20 Hz, while retaining coherent snapshots: with an unchanged revision, all other settings are fixed and the ramped field belongs to the instant it was read. New hold/target boundaries commit the preceding ramp. Existing clients already read live values even when the revision is unchanged; see the wire contract.
Host tests cover policy boundaries, boot/chord/hold consumption, reverse catalogue order, endpoint/direction/freshness behaviour, real input callback registration, allocation failures, concurrent publication and continuous-ramp client reads. They do not establish mounted button feel, external pull-ups, physical GPIO bounce, radio handover or current/flashlight acceptance. No hardware was flashed for this work.