fix(eink): fall back to polling when no GPIOTE channel is free

begin() claimed the trampoline slot and set _isr_slot before calling
attachInterrupt(), so a button that lost the GPIOTE race (only 8 channels,
shared with the radio's DIO1) would take the ISR branch forever while its
interrupt never fired. attachInterrupt() returns 0 when channels are
exhausted — only commit the slot on success, else leave _isr_slot = -1 so
check() uses the polling path. Combined with the live-pin reconcile, an
exhausted button degrades cleanly to plain polling instead of misbehaving.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-28 12:21:30 +02:00
parent d8c5fa83de
commit 57ebd80fa7

View File

@@ -89,9 +89,17 @@ void MomentaryButton::begin() {
#ifdef BUTTON_USE_INTERRUPTS
for (uint8_t i = 0; i < MAX_ISR_BUTTONS; i++) {
if (_isr_table[i] == nullptr) {
_isr_table[i] = this;
_isr_slot = i;
attachInterrupt(digitalPinToInterrupt(_pin), ISR_TRAMPOLINES[i], CHANGE);
// The nRF52 has only 8 GPIOTE channels, shared across every pin using
// attachInterrupt() (the radio's DIO1 takes one too). It returns 0 when
// they're exhausted — only claim the trampoline slot if a channel was
// actually allocated. Otherwise leave _isr_slot = -1 so check() uses the
// polling path: that button still works, it just won't capture edges
// that land during a blocking display refresh. Retrying another index is
// pointless — the channel pool, not the trampoline slot, is what ran out.
if (attachInterrupt(digitalPinToInterrupt(_pin), ISR_TRAMPOLINES[i], CHANGE) != 0) {
_isr_table[i] = this;
_isr_slot = i;
}
break;
}
}