Context
#936 added a panel power-enable rail to the display Add (ws.display.Add.power, a prerequisite ws.digitalio.Add — see adafruit/Wippersnapper_Protobuf#208). The PR implements deliberately two-party arbitration, per review:
- pin free at Add → the display driver owns the rail (enable level before init, released to disable level in its destructor, i.e. display Remove/replace);
- pin already registered with the digitalio controller → pass through only if it's an
OUTPUT already at the enable level, else the Add fails ("Power-enable pin unavailable").
That's correct for the direct display↔digitalio conflict, but it does not generalize. This issue captures the gaps so the design discussion has a home.
Failure scenarios (current behavior, verified against the code)
Assume one rail (e.g. GPIO15) powers the display and I²C sensors and NeoPixels, with components added/removed in arbitrary order at check-in or runtime:
| Order of events |
What happens today |
| digitalio Add (OUTPUT@enable) → display Add |
✅ pass-through works |
| display Add → digitalio Add, same pin |
❌ digitalio never queries display ownership — it reconfigures the pin per its own Add (possibly INPUT or LOW) and kills the panel |
| display owns rail → display Remove |
❌ driver dtor drives the rail to disable — I²C sensors / NeoPixels on the same rail are power-cut silently |
| passed-through digitalio pin → digitalio Remove |
❌ DigitalIOHardware dtor does LOW + pinMode(INPUT) — the rail floats and the panel dies with no notification path |
| two displays sharing one rail |
❌ ownership is private to each driver instance; the second display sees the pin as "free", both own it, first Remove powers down the second |
I²C bus init when PIN_I2C_POWER == the rail |
❌ i2c/hardware.cpp drives the macro pin directly — it even flips it through INPUT to sniff rest polarity (momentary float/brownout), invisible to any check |
| NeoPixel power pin |
v2 pixels doesn't model a power pin at all |
Root cause: ownership is pairwise, one-directional, and implicit — there is no shared registry, no refcount, and time-of-Add-only checking.
Deep sleep / light sleep (v2 sleep component)
The sleep controller configures wakeups and sleeps; it does not tear down component hardware and never calls gpio_hold_en:
- Deep sleep: the digital GPIO matrix powers down → all non-RTC pins float. An active-high rail sags off (accidentally the right power-saving outcome, by luck). An
is_inverted rail may float ON through sleep (or oscillate, per board pulls) — burning the battery budget the sleep exists to save. Correct handling needs gpio_hold_en + gpio_deep_sleep_hold_en at the disable level, or an RTC GPIO.
- Wake from deep sleep = reset → check-in replay → componentAdds re-run → the add-order nondeterminism above repeats on every wake cycle.
- Light sleep: GPIO state is retained and execution resumes — the panel stays lit between sensor reads, so no display power saving unless something deliberately drops the rail (policy question).
Proposed direction
A central refcounted pin/rail registry (in the digitalio controller, or a small PinOwnershipRegistry):
acquire(pin, level [, flags]) / release(pin): same-level acquires bump a refcount (add-order independence falls out for free); a conflicting-level acquire fails the component Add deterministically; the rail is only driven to its disable level when the last holder releases.
- Symmetric: digitalio Add/Remove, display power/backlight,
PIN_I2C_POWER, and a future pixels power pin all go through it — no component can stomp or power-cut another's rail.
- Sleep hook: a pre-sleep pass over held rails —
gpio_hold the appropriate level for deep sleep (critical for inverted rails); a future proto policy flag (e.g. keep_powered_during_sleep) covers wake-on-display use cases; light sleep policy (blank/power-down the panel?) decided per component.
Scope
Touches digitalio, display, i2c, pixels, and sleep — hence split out of #936 rather than grown inside a board-support PR.
Refs: #936 (review discussion), adafruit/Wippersnapper_Protobuf#208 ("prerequisite components" thread).
🤖 Generated with Claude Code
Context
#936 added a panel power-enable rail to the display Add (
ws.display.Add.power, a prerequisitews.digitalio.Add— see adafruit/Wippersnapper_Protobuf#208). The PR implements deliberately two-party arbitration, per review:OUTPUTalready at the enable level, else the Add fails ("Power-enable pin unavailable").That's correct for the direct display↔digitalio conflict, but it does not generalize. This issue captures the gaps so the design discussion has a home.
Failure scenarios (current behavior, verified against the code)
Assume one rail (e.g. GPIO15) powers the display and I²C sensors and NeoPixels, with components added/removed in arbitrary order at check-in or runtime:
DigitalIOHardwaredtor doesLOW + pinMode(INPUT)— the rail floats and the panel dies with no notification pathPIN_I2C_POWER== the raili2c/hardware.cppdrives the macro pin directly — it even flips it throughINPUTto sniff rest polarity (momentary float/brownout), invisible to any checkpixelsdoesn't model a power pin at allRoot cause: ownership is pairwise, one-directional, and implicit — there is no shared registry, no refcount, and time-of-Add-only checking.
Deep sleep / light sleep (v2 sleep component)
The sleep controller configures wakeups and sleeps; it does not tear down component hardware and never calls
gpio_hold_en:is_invertedrail may float ON through sleep (or oscillate, per board pulls) — burning the battery budget the sleep exists to save. Correct handling needsgpio_hold_en+gpio_deep_sleep_hold_enat the disable level, or an RTC GPIO.Proposed direction
A central refcounted pin/rail registry (in the digitalio controller, or a small
PinOwnershipRegistry):acquire(pin, level [, flags])/release(pin): same-level acquires bump a refcount (add-order independence falls out for free); a conflicting-level acquire fails the component Add deterministically; the rail is only driven to its disable level when the last holder releases.PIN_I2C_POWER, and a future pixels power pin all go through it — no component can stomp or power-cut another's rail.gpio_holdthe appropriate level for deep sleep (critical for inverted rails); a future proto policy flag (e.g.keep_powered_during_sleep) covers wake-on-display use cases; light sleep policy (blank/power-down the panel?) decided per component.Scope
Touches digitalio, display, i2c, pixels, and sleep — hence split out of #936 rather than grown inside a board-support PR.
Refs: #936 (review discussion), adafruit/Wippersnapper_Protobuf#208 ("prerequisite components" thread).
🤖 Generated with Claude Code