Replaces the build-flag-driven #ifdef sprawl (~30 blocks across 6 files)
with a small HAL in firmware/src/hal/ and per-board folders under
firmware/src/boards/. Shared code (main.cpp, ui.cpp, splash.cpp) no
longer contains a single `#ifdef BOARD_*` — optional features are
guarded by BoardCaps (runtime) and BOARD_HAS_* macros (compile-time,
inside the board's own files).
Why: lets community contributors port to new ESP32 + AMOLED + touch
combos by dropping in a boards/<name>/ folder + a PlatformIO env,
without touching shared files. See docs/porting/adding-a-board.md.
Highlights:
- New HAL: display_hal, touch_hal, input_hal, power_hal, imu_hal,
board_caps. Each board provides display.cpp, touch.cpp, input.cpp,
power.cpp, imu.cpp, caps.cpp, board_init.cpp + private hardware
drivers (e.g. io_expander.{h,cpp} on AMOLED-1.8).
- PlatformIO build_src_filter selects each board's folder per env.
- ui.cpp picks fonts and layout from board_caps() via compute_layout()
with screen-height breakpoints (>= 460 → large, else compact).
- splash.cpp computes CELL = min(W,H)/20 — responsive instead of two
hardcoded values.
- idle.cpp (from #24) rewired through display_hal + power_hal — no
longer depends on the deleted display_cfg.h / power.h.
- power_hal gains power_hal_is_vbus_in() for idle's
IDLE_SLEEP_WHEN_CHARGING gate.
- boards/template/ + docs/porting/{adding-a-board,hal-contract,
capability-flags}.md to bootstrap new ports.
- display_cfg.h, power.{h,cpp}, imu.{h,cpp}, io_expander.{h,cpp}
deleted from src/ root (moved into boards/<name>/ or hal/).
Verification: both `pio run -e waveshare_amoled_216` and
`pio run -e waveshare_amoled_18` succeed unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.1 KiB
HAL contract
Each header under firmware/src/hal/ defines functions that a board port
must provide. The shared code (main.cpp, ui.cpp, splash.cpp) calls
these and nothing else. Where a function has non-functional requirements
(latency, ordering), they're listed here — silently violating them tends
to produce subtle bugs (dropped frames, missed events) rather than
crashes.
board_caps.h
Runtime description of the board. Provided by your caps.cpp as a
single const BoardCaps instance returned from board_caps(). The UI
queries this at startup and gates optional features (battery indicator,
secondary-button HID mapping) by what's true here. Keep the struct in
sync with the compile-time BOARD_HAS_* flags in board.h.
display_hal.h
| Function | Responsibility |
|---|---|
display_hal_init |
Construct the QSPI bus + driver. Must run AFTER board_init() so any IO expander has released the LCD reset line. |
display_hal_begin |
gfx->begin(), clear screen, set default brightness. Allocate any rotation buffers needed by display_hal_draw_bitmap. |
display_hal_set_brightness |
Pass-through to the driver. Driver-defined scale (typically 0..255). |
display_hal_fill_screen |
Used by tests / boot screen — gfx->fillScreen(color). |
display_hal_draw_bitmap |
Push a w×h RGB565 strip at (x, y). If the panel can't rotate natively, apply CPU rotation here before pushing — imu_hal_rotation_quadrant() returns the current orientation. Must complete inside LVGL's render budget (a few ms at typical strip sizes). |
display_hal_tick |
Per-loop housekeeping — used by rotation-aware boards to blank the panel + ramp brightness during a rotation transition. No-op on boards without rotation. |
display_hal_round_area |
LVGL invalidate-area hook. Most QSPI AMOLED drivers expect even-aligned flush regions; apply & ~1 / ` |
touch_hal.h
| Function | Responsibility |
|---|---|
touch_hal_init |
Initialize the controller + attach a touch interrupt. Configure axis swap / mirror so coordinates returned in touch_hal_read match the panel's pixel coordinates after any rotation. |
touch_hal_read |
Return the latest sample. Hard requirement: complete in well under 5 ms — LVGL polls this every screen refresh and any I2C burst longer than a screen tick will visibly stutter. |
Avoid GPL-licensed drivers — vendor a minimal reader instead. The existing AMOLED-1.8 port has a ~40-line FT3168 reader you can model on.
input_hal.h
| Function | Responsibility |
|---|---|
input_hal_init |
pinMode() for the physical button GPIOs. |
input_hal_is_held |
Return true while the button is held. Active-low pull-up GPIOs are typical. Boards lacking a secondary button must return false for INPUT_BTN_SECONDARY. |
The PWR button is not here — it belongs to power_hal because on
several boards (including all current reference ports) it's tied to the
PMU or an IO expander, not a GPIO.
power_hal.h
| Function | Responsibility |
|---|---|
power_hal_init |
Bring up the PMU (if any). Configure battery measurement. Subscribe to the PWR button source (PMU IRQ or IO expander polling). |
power_hal_tick |
Refresh battery % and charging state at sensible intervals (the reference ports use 2s / 500 ms). Poll the PWR button if it's not interrupt-driven. |
power_hal_battery_pct |
0..100, or -1 when battery info isn't available. |
power_hal_is_charging |
Bool, false on no-battery boards. |
power_hal_pwr_pressed |
Edge-triggered: returns true once per short-press, then clears. Shared code calls this every loop and expects one true per press. |
Boards with no PMU and no PWR button can return zero/false from all five
— set BOARD_HAS_BATTERY=0 and the UI hides the battery indicator.
imu_hal.h
| Function | Responsibility |
|---|---|
imu_hal_init |
Bring up the accelerometer. |
imu_hal_tick |
Sample the accelerometer at a low rate (~10 Hz) and update the rotation state with hysteresis. |
imu_hal_rotation_quadrant |
Current rotation, 0..3 (quarter turns CW). Used by display_hal_draw_bitmap on rotation-capable boards. Boards without rotation always return 0. |
Responsive UI breakpoints
ui.cpp::compute_layout() picks layout values from board_caps().width
and .height. The current breakpoints are:
height >= 460→ "large" layout, tuned for 480×480.- otherwise → "compact" layout, tuned for 368×448.
A new screen size lands on the closer breakpoint and renders correctly
without pixel-perfect alignment. If you want polish, add another branch
to compute_layout() (please open a PR — others with that size benefit).
The splash screen is fully responsive — CELL is computed as
min(width, height) / 20 so the 20×20 pixel-art creature fills the
smaller display dimension and centers in the larger one.