Device-abstraction refactor: HAL + per-board folders + responsive UI

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>
This commit is contained in:
tobby168
2026-05-20 18:27:24 -07:00
co-authored by Claude Opus 4.7
parent f3ed2425bf
commit 20351212b2
49 changed files with 1604 additions and 775 deletions
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
// Runtime board description consumed by board-agnostic code (UI, main loop).
// Each board provides a single BoardCaps instance via board_caps().
//
// Compile-time-only facts (pin numbers, library choice) belong in
// boards/<name>/board.h and never leak into shared code. Anything the UI or
// main loop needs at runtime — display size, optional-feature presence —
// goes here so shared code stays free of #ifdef BOARD_*.
struct BoardCaps {
const char* name; // human-readable, e.g. "Waveshare AMOLED 2.16"
int16_t width; // active display width in pixels
int16_t height; // active display height in pixels
uint8_t button_count; // 1 = primary (BOOT) only; 2 = primary + secondary
bool has_rotation; // IMU-driven CPU rotation in the flush callback
bool has_battery; // AXP2101 battery measurement is meaningful
bool has_imu; // QMI8658 (or compatible) is populated
};
const BoardCaps& board_caps(void);
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <stdint.h>
// Display abstraction. The board provides the QSPI bus, panel driver, and any
// CPU-side rotation. Shared code (main.cpp, LVGL glue) never sees the GFX
// driver type. Dimensions are not declared here — query board_caps().
// Construct bus + driver objects. Safe to call before display_hal_begin().
// On boards with an IO expander gating the LCD reset, the board's
// implementation is responsible for ensuring the expander has released the
// reset before talking to the panel.
void display_hal_init(void);
// Bring the panel out of reset, clear it, and apply default brightness.
void display_hal_begin(void);
void display_hal_set_brightness(uint8_t level); // 0..255 (driver-defined scale)
void display_hal_fill_screen(uint16_t color565);
// Write a w×h RGB565 bitmap at (x, y). Boards with software rotation
// (e.g. CO5300) transform (x, y, w, h) and the pixel buffer here before
// pushing to the panel. Shared LVGL flush_cb just calls this — no #ifdef.
void display_hal_draw_bitmap(int32_t x, int32_t y, int32_t w, int32_t h,
const uint16_t* pixels);
// Per-loop housekeeping for rotation-aware boards: detects orientation
// changes from the IMU, blanks the panel, invalidates LVGL, and ramps
// brightness back up. No-op on boards without rotation.
void display_hal_tick(void);
// LVGL flush regions must be even-aligned on the CO5300; harmless on others.
void display_hal_round_area(int32_t* x1, int32_t* y1, int32_t* x2, int32_t* y2);
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <stdint.h>
// Optional accelerometer-driven orientation tracker. Returns 0..3 (quarter
// turns CW from default mounting). Boards without an IMU — or boards with
// rotation intentionally disabled, like AMOLED-1.8 fixed at 0° — return 0
// from imu_hal_rotation_quadrant() and no-op on init/tick.
void imu_hal_init(void);
void imu_hal_tick(void);
uint8_t imu_hal_rotation_quadrant(void);
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <stdint.h>
// Physical button abstraction. Boards report up to two screen-independent
// buttons:
// PRIMARY — left button on this project's boards (BOOT / GPIO 0).
// Drives the Claude Code voice-mode PTT (HID Space).
// SECONDARY — right button on boards that have one (e.g. GPIO 18 on
// AMOLED-2.16). Drives mode-toggle (HID Shift+Tab). Boards
// without it report held=false forever and shared code
// handles that gracefully via BoardCaps.button_count.
//
// The PWR button is owned by power_hal (it's tied to the PMU on some boards
// and to an IO expander on others — see power_hal_pwr_pressed()).
enum InputButton {
INPUT_BTN_PRIMARY = 0,
INPUT_BTN_SECONDARY = 1,
};
void input_hal_init(void);
// True while the button is physically held (active-low GPIOs are
// de-bounced at the caller's expense — the existing code polls every
// loop iteration). Boards lacking a button always return false.
bool input_hal_is_held(InputButton btn);
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// Power / battery / power-button abstraction. Replaces the legacy power.h
// API but keeps the same shape so existing call sites stay clean.
//
// Some boards (AMOLED-2.16) wire PWR through the PMU's PKEY IRQ; others
// (AMOLED-1.8) route it through an IO expander. The HAL hides which
// source produced the press — shared code just polls
// power_hal_pwr_pressed() once per loop.
void power_hal_init(void);
void power_hal_tick(void);
int power_hal_battery_pct(void); // 0..100, or -1 if no battery (see BoardCaps.has_battery)
bool power_hal_is_charging(void);
bool power_hal_is_vbus_in(void); // USB cable present (true even without a battery)
// Edge-triggered: returns true once per PWR short-press, then clears.
bool power_hal_pwr_pressed(void);
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <stdint.h>
// Touch abstraction. The board owns the touch controller driver and the
// TP_INT pin wiring. The HAL implementation is responsible for keeping its
// own internal "latest sample" state — shared code calls touch_hal_read()
// once per loop and feeds it into LVGL.
//
// Implementations should complete touch_hal_read() in well under 5 ms (a
// single I2C burst). LVGL polls this at the screen refresh rate.
void touch_hal_init(void);
// Pump the controller and return the latest sample. *pressed reflects
// whether any finger is currently down; coordinates are valid only when
// pressed is true and are in display (post-orientation) coordinates.
void touch_hal_read(uint16_t* x, uint16_t* y, bool* pressed);