Fix ee02 button-wakeup build: ext1 fallback for ESP32-S3
Firmware build check / build-check (push) Successful in 2m44s

esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown() only exists on
ESP32-C6 (SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP), so the ee02
(ESP32-S3) build failed with implicit-declaration errors in
{back,next,combo}_button.c once the epd13in3e driver's #error stopped
masking it.

Each button file now branches on that capability macro: the C6 path
(devkit/xiao) is untouched, and ESP32-S3 uses
esp_sleep_enable_ext1_wakeup_io() instead. The earlier ext1 attempt was
rejected on C6 hardware because its pull resistor didn't hold across
RTC_PERIPH power-down -- tracing the same path in ESP-IDF source shows
gpio_config()'s pull_up_en already delegates to rtc_gpio_pullup_en()
for RTC-capable pins on every non-original-ESP32 target, so the pull-up
should already survive the same power-down on S3. The _io() variant is
additive, so the three button files don't need cross-file mask
coordination. Also widens the button GPIO Kconfig range for
IDF_TARGET_ESP32S3 (0-21, matching its RTC-IO set) instead of the
C6-shaped 0-7.

Verified: ee02, devkit, and xiao all build clean end-to-end locally
(native ESP-IDF v6.0, no Docker in this sandbox). NOT verified: whether
this actually avoids the spurious-instant-wakeup bug on real EE02
hardware -- that failure mode was only ever confirmed empirically, not
root-caused in a way a compile can check. continue-on-error stays on
in CI's ee02 build step until that's confirmed.
This commit is contained in:
2026-08-04 22:46:13 +00:00
parent 454c03586e
commit c0fefc19f1
7 changed files with 165 additions and 73 deletions
+49 -29
View File
@@ -163,7 +163,7 @@ an ESP32-S3) existed and "xiao" alone stopped disambiguating hardware.
The server keeps accepting the old bare names indefinitely, since
already-flashed devices can't be retroactively renamed.
## 13.3" Spectra 6 panel on Seeed's EE02 board (panel driver ported; board still doesn't build end-to-end)
## 13.3" Spectra 6 panel on Seeed's EE02 board (panel driver ported, `ee02` builds end-to-end; unverified on real hardware)
A second panel size is supported server-side (the web UI shows a
read-only "Panel: 13.3\" Spectra 6" once a frame's device reports
@@ -183,25 +183,38 @@ correction that came with it: the panel's SPI wire raster is a *native
doesn't just rotate the image, it shreds it (1600x1200 and 1200x1600
don't share a row stride).
**A full `ee02` build still fails**, but for an unrelated, pre-existing
reason now that the panel driver's own `#error` no longer stops the
build early: `firmware/main/{back,next,combo}_button.c` call
`esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown()`, an ESP32-C6-only
deep-sleep GPIO-wakeup API (gated by `SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP`,
which ESP32-S3's `soc_caps.h` doesn't define) with no ESP32-S3 fallback
path. This was always broken for `ee02`, just masked behind the panel
driver's own compile failure -- see the note two paragraphs up about the
button GPIO range not yet being verified against the ESP32-S3, which was
already flagging this same gap before it had an actual compile error
attached to it. ESP32-S3 does support GPIO deep-sleep wakeup via
`esp_sleep_enable_ext1_wakeup()`/`esp_sleep_get_ext1_wakeup_status()`
(and so, notably, does ESP32-C6 -- `SOC_PM_SUPPORT_EXT1_WAKEUP` is set on
both chips), but EXT1 wakeup takes one combined GPIO mask/mode across all
three button files' independent calls rather than each button
registering its own -- porting to it is a real (if likely small)
cross-board refactor, not a mechanical swap, and hasn't been done. CI's
`firmware-build-check.yml`/`firmware-release-build.yml` `continue-on-error`
on this board's step stays in place until it is.
**A full `ee02` build now succeeds** (verified locally with a native,
non-Docker ESP-IDF v6.0 install -- see
`.claude/skills/build-firmware/SKILL.md`); `firmware/main/{back,next,combo}_button.c`
used to call `esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown()`, an
ESP32-C6-only deep-sleep GPIO-wakeup API (gated by
`SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP`, which ESP32-S3's
`soc_caps.h` doesn't define) with no ESP32-S3 fallback path. Each of the
three button files now branches on that same capability macro: the
ESP32-C6 path (devkit/xiao) is untouched, and a new ESP32-S3 path uses
`esp_sleep_enable_ext1_wakeup_io()` (not the non-`_io()`
`esp_sleep_enable_ext1_wakeup()`, which resets any previously-registered
mask -- the `_io()` variant is additive, confirmed by reading
`esp_hw_support/sleep_modes.c`, so the three button files can each keep
registering their own GPIO independently, no combined-mask coordination
needed) plus `esp_sleep_get_ext1_wakeup_status()` for the wake-cause
check. The original C6 EXT1 attempt was rejected on hardware because
its pull resistor didn't hold across the RTC_PERIPH power-down (see
`firmware/main/next_button.c`'s `next_button_init()` comment) -- tracing
the same code path for ESP32-S3 shows `gpio_config()`'s `pull_up_en`
(already used by all three button files) delegates to
`rtc_gpio_pullup_en()` for RTC-capable pins on every non-original-ESP32
target (confirmed in `esp_driver_gpio/gpio.c`: `GPIO_RTCIO_ARE_INDEPENDENT`
is 1 for both C6 and S3, meaning the digital and RTC pull registers are
independent hardware and `gpio_config()` already sets the RTC one), so
the pull-up should already survive the same power-down on ESP32-S3
without any extra `rtc_gpio_*` calls. That reasoning is verified against
IDF source, **not against real EE02 hardware** -- a clean compile
confirms the code builds and links, not that it's actually
spurious-wakeup-free on a real board. CI's
`firmware-build-check.yml`/`firmware-release-build.yml`
`continue-on-error` on this board's step is intentionally still in place
until that hardware verification happens.
Confirmed so far:
@@ -235,11 +248,16 @@ Confirmed so far:
| Panel power-enable | 43 | `EPD_PIN_POWER_EN` |
User buttons are reportedly at GPIO 2/3/5, but which physical button
maps to which logical role (next/back/menu) isn't confirmed, and the
maps to which logical role (next/back/menu) still isn't confirmed. The
firmware's button Kconfig options (`FRAME_NEXT_BUTTON_GPIO` etc.,
`firmware/main/Kconfig.projbuild`) still range-limit to GPIO 0-7 --
the ESP32-C6's deep-sleep-wakeup-capable set (see the build-blocker
note above). SPI clock is reportedly reliable only up to 2MHz on this
`firmware/main/Kconfig.projbuild`) now range to GPIO -1 to 21 under
`IDF_TARGET_ESP32S3` (the ESP32-S3's own ext1-wakeup-capable RTC-IO
range) instead of the ESP32-C6-shaped -1 to 7, so GPIO 2/3/5 fit
regardless -- but `firmware/sdkconfig.ee02` still deliberately doesn't
override the defaults inherited from the C6 boards (GPIO 2/0/1) until
the role mapping above is confirmed.
SPI clock is reportedly reliable only up to 2MHz on this
panel/board per the community ESPHome integration (vs. epd7in3e's 4MHz
default) -- see `firmware/sdkconfig.ee02`. Waveshare's own
ESP32-S3-ePaper-13.3E6 example defaults to 10MHz, but that's a
@@ -247,8 +265,10 @@ Confirmed so far:
hardware exists, not a reason to bump the current conservative default
blind.
Once the button-wakeup gap above is fixed, remaining unknowns before
trusting this on real hardware: the wiring table (community-sourced, not
official), and `PANEL_WIRE_TRANSPOSE`'s rotation *direction*
(`ROTATE_90` vs `ROTATE_270` -- a physical-assembly fact no vendor driver
encodes, see that dict's own comment in `image_pipeline.py`).
Remaining unknowns before trusting this on real hardware: whether the
ESP32-S3 button-wakeup path above actually avoids a spurious-instant-wakeup
on a real board (not just compiles), the button-to-role mapping, the
wiring table (community-sourced, not official), and
`PANEL_WIRE_TRANSPOSE`'s rotation *direction* (`ROTATE_90` vs
`ROTATE_270` -- a physical-assembly fact no vendor driver encodes, see
that dict's own comment in `image_pipeline.py`).
+24 -22
View File
@@ -2,9 +2,9 @@
ESP-IDF firmware for the ESP32-C6 (devkit/xiao boards, 7.3" panel) or
ESP32-S3 (ee02 board, 13.3" panel -- see
[Building for Seeed's EE02](#building-for-seeeds-ee02-esp32-s3--133-panel-driver-ported-board-still-doesnt-build-end-to-end)
below; the panel driver itself works now, but the board doesn't build
end-to-end yet for an unrelated reason). On first boot it provisions itself over
[Building for Seeed's EE02](#building-for-seeeds-ee02-esp32-s3--133-panel-driver-ported-ee02-builds-end-to-end-unverified-on-real-hardware)
below; it builds end-to-end now, but is still unverified on real EE02
hardware). On first boot it provisions itself over
a WiFi captive portal; after that it wakes on a timer, fetches an
already-processed frame from the [server](../server/), streams it straight
to the panel over SPI, and goes back to deep sleep.
@@ -52,7 +52,7 @@ never clobbers the other:
(`./build_for_board.sh devkit ...` does the same for the dev board --
equivalent to a plain `idf.py`, just consistent with the XIAO invocation.)
### Building for Seeed's EE02 (ESP32-S3 + 13.3" panel, driver ported; board still doesn't build end-to-end)
### Building for Seeed's EE02 (ESP32-S3 + 13.3" panel, driver ported; `ee02` builds end-to-end, unverified on real hardware)
EE02 is a different chip (ESP32-S3, not C6), so it needs `set-target
esp32s3` instead of `esp32c6`, and its own partition table/flash-size
@@ -64,25 +64,27 @@ Kconfig sized for its 16MB flash
./build_for_board.sh ee02 build
```
**This still fails to compile, but no longer because of the panel
driver.** `firmware/components/epd13in3e`'s panel init/LUT/refresh
register sequence is now a real, vendor-confirmed port (see that
component's own top comment and
[`docs/hardware.md`](../docs/hardware.md#133-spectra-6-panel-on-seeeds-ee02-board-panel-driver-ported-board-still-doesnt-build-end-to-end)
**This now compiles and links clean end-to-end**, verified locally with
a native, non-Docker ESP-IDF v6.0 install (see
`.claude/skills/build-firmware/SKILL.md`).
`firmware/components/epd13in3e`'s panel init/LUT/refresh register
sequence is a real, vendor-confirmed port (see that component's own top
comment and
[`docs/hardware.md`](../docs/hardware.md#133-spectra-6-panel-on-seeeds-ee02-board-panel-driver-ported-ee02-builds-end-to-end-unverified-on-real-hardware)
for the vendor sources and the load-bearing native-raster-orientation
correction that came with it) -- it compiles clean on its own. The build
now fails one step later, in `main/{back,next,combo}_button.c`: they call
an ESP32-C6-only deep-sleep GPIO-wakeup API with no ESP32-S3 fallback, a
pre-existing gap that the panel driver's old `#error` simply stopped the
build before reaching. See `docs/hardware.md`'s same section for what a
real fix looks like (not a mechanical swap -- ESP32-S3's equivalent API
takes one combined mask across all three button files' independent
calls). Everything else around the panel driver (target selection,
Kconfig, partition table, sdkconfig layering, `main/CMakeLists.txt`'s
component selection) is in place and exercised by CI
(`.gitea/workflows/firmware-build-check.yml`/`firmware-release-build.yml`,
both with `continue-on-error` on this board's step until a full build is
actually green).
correction that came with it). `main/{back,next,combo}_button.c` used to
call an ESP32-C6-only deep-sleep GPIO-wakeup API with no ESP32-S3
fallback; each now branches on `SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP`
to keep the ESP32-C6 path (devkit/xiao) untouched while using
`esp_sleep_enable_ext1_wakeup_io()` on ESP32-S3 -- see `docs/hardware.md`'s
same section for why the additive `_io()` variant needs no combined-mask
coordination across the three button files, and why the pull-resistor
concern that ruled out EXT1 wakeup on ESP32-C6 doesn't apply the same
way here. That reasoning is confirmed against ESP-IDF source, **not
against real EE02 hardware** -- CI's
(`.gitea/workflows/firmware-build-check.yml`/`firmware-release-build.yml`)
`continue-on-error` on this board's step is intentionally still in place
until it is.
## Configuration (`idf.py menuconfig`)
+12 -7
View File
@@ -142,6 +142,7 @@ menu "ESPresso Frame Configuration"
config FRAME_NEXT_BUTTON_GPIO
int "Next-photo button GPIO (-1 to disable)"
default 2
range -1 21 if IDF_TARGET_ESP32S3
range -1 7
help
Button wired between this GPIO and GND (active-low, internal
@@ -149,14 +150,17 @@ menu "ESPresso Frame Configuration"
Pressing it wakes the device (if asleep), forces the server to
advance to the next photo immediately (POST /frame/advance)
regardless of the configured refresh interval, and displays
it. Must be GPIO 0-7 -- the only pins the ESP32-C6 can use as
a deep-sleep GPIO wakeup source, which is what lets a press
wake the device promptly instead of only being noticed during
its brief awake windows. Set to -1 to disable the feature.
it. Must be a deep-sleep-wakeup-capable GPIO: 0-7 on the
ESP32-C6, 0-21 on the ESP32-S3 (RTC-IO pins reachable by
esp_sleep_enable_ext1_wakeup_io()) -- required so a press
wakes the device promptly instead of only being noticed
during its brief awake windows. Set to -1 to disable the
feature.
config FRAME_BACK_BUTTON_GPIO
int "Back-photo button GPIO (-1 to disable)"
default 0
range -1 21 if IDF_TARGET_ESP32S3
range -1 7
help
Button wired between this GPIO and GND (active-low, internal
@@ -165,13 +169,14 @@ menu "ESPresso Frame Configuration"
to return to the previously-current photo immediately
(POST /frame/back), and displays it. Pressing next
afterwards returns to where you were before pressing back.
Must be GPIO 0-7 for the same deep-sleep-wakeup reason as
Must be a deep-sleep-wakeup-capable GPIO, same range as
FRAME_NEXT_BUTTON_GPIO above; defaults to a different pin
than the other buttons. Set to -1 to disable the feature.
config FRAME_COMBO_BUTTON_GPIO
int "Menu/reset button GPIO (-1 to disable)"
default 1
range -1 21 if IDF_TARGET_ESP32S3
range -1 7
help
Button wired between this GPIO and GND (active-low, internal
@@ -182,8 +187,8 @@ menu "ESPresso Frame Configuration"
then releasing shows the management menu; holding it all the
way to FRAME_COMBO_FACTORY_RESET_HOLD_MS clears the stored
config and restarts into provisioning, regardless of whether
it's released yet. Must be GPIO 0-7 for the same deep-sleep-
wakeup reason as FRAME_NEXT_BUTTON_GPIO above; defaults to
it's released yet. Must be a deep-sleep-wakeup-capable GPIO,
same range as FRAME_NEXT_BUTTON_GPIO above; defaults to
a different pin than the other buttons. Set to -1 to
disable the feature entirely (also disables the management
menu, both reset tiers, and factory-reset-via-button --
+12
View File
@@ -3,6 +3,7 @@
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "soc/soc_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -35,10 +36,17 @@ void back_button_init(void)
};
gpio_config(&io_conf);
#if SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP
/* See next_button.c for why this API (not ext1) -- it manages the
* pull resistor across the sleep transition itself, so the pin
* doesn't float and wake the device spuriously. */
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << BACK_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
#else
/* See next_button.c for why ext1 is safe here on targets without the
* API above (e.g. ESP32-S3), and why _io() needs no cross-file mask
* coordination. */
ESP_ERROR_CHECK(esp_sleep_enable_ext1_wakeup_io(1ULL << BACK_BUTTON_GPIO, ESP_EXT1_WAKEUP_ANY_LOW));
#endif
}
back_button_result_t back_button_check(void)
@@ -49,7 +57,11 @@ back_button_result_t back_button_check(void)
* status register is latched at the moment of waking and isn't
* cleared until the next sleep entry, so it reliably reflects a tap
* regardless of how quickly it was released. */
#if SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP
bool caused_wake = esp_sleep_get_gpio_wakeup_status() & (1ULL << BACK_BUTTON_GPIO);
#else
bool caused_wake = esp_sleep_get_ext1_wakeup_status() & (1ULL << BACK_BUTTON_GPIO);
#endif
if (!caused_wake) {
/* Not a GPIO-wakeup-from-this-pin boot (normal timer wake, or a
+12
View File
@@ -1,6 +1,7 @@
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "soc/soc_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -31,10 +32,17 @@ void combo_button_init(void)
};
gpio_config(&io_conf);
#if SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP
/* See next_button.c for why this API (not ext1) -- it manages the
* pull resistor across the sleep transition itself, so the pin
* doesn't float and wake the device spuriously. */
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << COMBO_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
#else
/* See next_button.c for why ext1 is safe here on targets without the
* API above (e.g. ESP32-S3), and why _io() needs no cross-file mask
* coordination. */
ESP_ERROR_CHECK(esp_sleep_enable_ext1_wakeup_io(1ULL << COMBO_BUTTON_GPIO, ESP_EXT1_WAKEUP_ANY_LOW));
#endif
}
bool combo_button_check(void)
@@ -48,7 +56,11 @@ bool combo_button_check(void)
* caused the wake even if it's since been released -- in which case
* the poll loop below simply measures 0ms held, correctly resolving
* to a quick press rather than "not pressed at all." */
#if SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP
bool caused_wake = esp_sleep_get_gpio_wakeup_status() & (1ULL << COMBO_BUTTON_GPIO);
#else
bool caused_wake = esp_sleep_get_ext1_wakeup_status() & (1ULL << COMBO_BUTTON_GPIO);
#endif
if (!caused_wake && gpio_get_level(COMBO_BUTTON_GPIO) != 0) {
return false; /* not pressed, and didn't cause this wake either */
}
+42 -6
View File
@@ -3,6 +3,7 @@
#include "driver/gpio.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "soc/soc_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -36,13 +37,44 @@ void next_button_init(void)
};
gpio_config(&io_conf);
/* Not esp_sleep_enable_ext1_wakeup_io(): its internal pull resistors
* don't hold once the RTC_PERIPH domain powers down for deep sleep, so
* the pin floats and reads spuriously low, waking the device instantly
* on every sleep entry (confirmed on hardware). This GPIO-wakeup
* variant manages the pull resistor itself across the sleep
* transition. */
#if SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP
/* Not esp_sleep_enable_ext1_wakeup_io() on its own: on a target
* without RTC-independent digital pull registers, ext1's internal
* pull resistors don't hold once the RTC_PERIPH domain powers down
* for deep sleep, so the pin floats and reads spuriously low, waking
* the device instantly on every sleep entry (confirmed on hardware,
* ESP32-C6). This GPIO-wakeup variant manages the pull resistor
* itself across the sleep transition, sidestepping the issue
* entirely -- but it only exists on chips with this capability
* (currently just ESP32-C6; see the #else below for other targets,
* e.g. ESP32-S3). */
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << NEXT_BUTTON_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW);
#else
/* No HP-periph-powerdown wakeup API here (e.g. ESP32-S3) -- fall
* back to ext1, but NOT naively: on every non-original-ESP32 target
* (ESP32-S3 included), gpio_pullup_en() -- which the gpio_config()
* call above invokes via pull_up_en -- delegates to
* rtc_gpio_pullup_en() for RTC-capable pins (confirmed in
* esp_driver_gpio's gpio.c: GPIO_RTCIO_ARE_INDEPENDENT is 1 for
* every target except the original ESP32, meaning digital and RTC
* pull registers are independent hardware and gpio_config() already
* routes the pull-up through the RTC pad's own register for these
* pins, not just the digital one). That's exactly what was missing
* in the ext1 attempt that failed on hardware above -- so on this
* target the pull-up already survives the RTC_PERIPH power-down
* ext1 wakeup requires, without needing a separate rtc_gpio_*_en()
* call here. _io() (not the bare esp_sleep_enable_ext1_wakeup(),
* which resets any previously-configured mask) is additive across
* this file's, back_button.c's, and combo_button.c's independent
* init calls -- confirmed in esp_hw_support's sleep_modes.c -- so no
* shared-mask coordination between the three button files is
* needed. Still unconfirmed on real EE02 hardware: this avoids the
* *documented* failure mode of the earlier ext1 attempt, but that
* attempt was never root-caused beyond "confirmed spurious wakeup on
* hardware" -- treat this as untested until it's actually run on an
* EE02 board. */
ESP_ERROR_CHECK(esp_sleep_enable_ext1_wakeup_io(1ULL << NEXT_BUTTON_GPIO, ESP_EXT1_WAKEUP_ANY_LOW));
#endif
}
next_button_result_t next_button_check(void)
@@ -53,7 +85,11 @@ next_button_result_t next_button_check(void)
* status register is latched at the moment of waking and isn't
* cleared until the next sleep entry, so it reliably reflects a tap
* regardless of how quickly it was released. */
#if SOC_GPIO_SUPPORT_HP_PERIPH_PD_SLEEP_WAKEUP
bool caused_wake = esp_sleep_get_gpio_wakeup_status() & (1ULL << NEXT_BUTTON_GPIO);
#else
bool caused_wake = esp_sleep_get_ext1_wakeup_status() & (1ULL << NEXT_BUTTON_GPIO);
#endif
if (!caused_wake) {
/* Not a GPIO-wakeup-from-this-pin boot (normal timer wake, or a
+14 -9
View File
@@ -34,12 +34,17 @@ CONFIG_PARTITION_TABLE_FILENAME="partitions_ee02.csv"
# Deliberately NOT overriding FRAME_NEXT_BUTTON_GPIO/FRAME_BACK_BUTTON_
# GPIO/FRAME_COMBO_BUTTON_GPIO/FRAME_BATTERY_ADC_GPIO here, even though
# the same community source that gave the epd13in3e pinout also reports
# EE02 has 3 user buttons at GPIO2/3/5: (1) which physical button maps to
# which logical role (next/back/combo) isn't confirmed, and (2) more
# importantly, those Kconfig options' `range -1 7`/`range -1 6`
# constraints (main/Kconfig.projbuild) are hardcoded to the ESP32-C6's
# deep-sleep-wakeup-capable GPIO set -- NOT verified against which pins
# can actually wake the ESP32-S3 from deep sleep, which is a different
# set on a different chip. Confirm both before wiring this up; widen/
# adjust those Kconfig ranges if the real wakeup-capable pins for
# whichever GPIOs EE02's buttons land on fall outside them.
# EE02 has 3 user buttons at GPIO2/3/5: which physical button maps to
# which logical role (next/back/combo) still isn't confirmed. The
# button GPIOs' `range -1 7` constraint (main/Kconfig.projbuild) -- which
# used to be hardcoded to the ESP32-C6's deep-sleep-wakeup-capable GPIO
# set -- now widens to `range -1 21` under IDF_TARGET_ESP32S3 (the
# ESP32-S3's own ext1-wakeup-capable RTC-IO range), so GPIO2/3/5 fit
# either way and nothing here needs adjusting on that front. What's
# still unconfirmed: (1) the button-to-role mapping above, and (2)
# whether the S3 button-wakeup path itself (ext1 + RTC pull-up, see
# main/next_button.c) actually avoids the spurious-instant-wakeup bug
# that ruled out ext1 on the ESP32-C6 -- that needs real EE02 hardware,
# not just a clean compile. FRAME_BATTERY_ADC_GPIO's `range -1 6` is a
# separate, still-unwidened concern -- it's the ESP32-C6's ADC-capable
# pin set, not a deep-sleep-wakeup range, and out of scope here.