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
+29 -22
View File
@@ -2,23 +2,18 @@
#include "splash_animations.h"
#include "theme.h"
#include "usage_rate.h"
#include "display_cfg.h"
#include "hal/board_caps.h"
#include <Arduino.h>
#include <string.h>
#include <esp_heap_caps.h>
// 20x20 grid. CELL chosen per board so the canvas fits the screen
// (must satisfy GRID*CELL <= min(LCD_WIDTH, LCD_HEIGHT)).
// AMOLED-2.16 (480x480 square): CELL=24 → 480x480 fills screen
// AMOLED-1.8 (368x448 portrait): CELL=18 → 360x360 centered, vertical margin
// 20×20 grid. CELL sized so the canvas fits the smaller display dimension —
// the canvas is square and centered, so on portrait or letterboxed panels
// it leaves vertical margin rather than cropping.
#define GRID 20
#ifdef BOARD_AMOLED_18
#define CELL 18
#else
#define CELL 24
#endif
#define CANVAS_W (GRID * CELL)
#define CANVAS_H (GRID * CELL)
static int cell = 24; // recomputed in splash_init()
static int canvas_w = GRID * 24;
static int canvas_h = GRID * 24;
// Background fallback when palette is missing
#define COL_EMPTY 0x0000 // true black (matches THEME_BG)
@@ -76,17 +71,19 @@ static void resolve_group_lists(void) {
}
}
static uint16_t *row_buf = NULL; // scratch row, sized to canvas_w
static void render_frame(const uint8_t *cells, const uint16_t *palette) {
if (!row_buf || !canvas_buf) return;
for (int gy = 0; gy < GRID; gy++) {
uint16_t row[CANVAS_W];
for (int gx = 0; gx < GRID; gx++) {
uint8_t code = cells[gy * GRID + gx];
uint16_t color = (palette && code < SPLASH_PALETTE_SIZE) ? palette[code] : COL_EMPTY;
uint16_t *p = &row[gx * CELL];
for (int i = 0; i < CELL; i++) p[i] = color;
uint16_t *p = &row_buf[gx * cell];
for (int i = 0; i < cell; i++) p[i] = color;
}
for (int dy = 0; dy < CELL; dy++) {
memcpy(&canvas_buf[(gy * CELL + dy) * CANVAS_W], row, CANVAS_W * 2);
for (int dy = 0; dy < cell; dy++) {
memcpy(&canvas_buf[(gy * cell + dy) * canvas_w], row_buf, canvas_w * 2);
}
}
if (canvas) lv_obj_invalidate(canvas);
@@ -94,20 +91,30 @@ static void render_frame(const uint8_t *cells, const uint16_t *palette) {
static void show_placeholder() {
// Solid dark background + centered status label.
for (int i = 0; i < CANVAS_W * CANVAS_H; i++) canvas_buf[i] = COL_EMPTY;
if (canvas_buf) {
for (int i = 0; i < canvas_w * canvas_h; i++) canvas_buf[i] = COL_EMPTY;
}
if (canvas) lv_obj_invalidate(canvas);
if (label_status) lv_obj_clear_flag(label_status, LV_OBJ_FLAG_HIDDEN);
}
void splash_init(lv_obj_t *parent) {
canvas_buf = (uint16_t*)heap_caps_malloc(CANVAS_W * CANVAS_H * 2, MALLOC_CAP_SPIRAM);
if (!canvas_buf) {
const BoardCaps& c = board_caps();
int min_dim = (c.width < c.height) ? c.width : c.height;
cell = min_dim / GRID; // fits within the smaller display dimension
if (cell < 4) cell = 4;
canvas_w = GRID * cell;
canvas_h = GRID * cell;
canvas_buf = (uint16_t*)heap_caps_malloc(canvas_w * canvas_h * 2, MALLOC_CAP_SPIRAM);
row_buf = (uint16_t*)heap_caps_malloc(canvas_w * 2, MALLOC_CAP_SPIRAM);
if (!canvas_buf || !row_buf) {
Serial.println("splash: failed to alloc canvas buffer");
return;
}
splash_container = lv_obj_create(parent);
lv_obj_set_size(splash_container, LCD_WIDTH, LCD_HEIGHT);
lv_obj_set_size(splash_container, c.width, c.height);
lv_obj_set_pos(splash_container, 0, 0);
lv_obj_set_style_bg_color(splash_container, THEME_BG, 0);
lv_obj_set_style_bg_opa(splash_container, LV_OPA_COVER, 0);
@@ -116,7 +123,7 @@ void splash_init(lv_obj_t *parent) {
lv_obj_clear_flag(splash_container, LV_OBJ_FLAG_SCROLLABLE);
canvas = lv_canvas_create(splash_container);
lv_canvas_set_buffer(canvas, canvas_buf, CANVAS_W, CANVAS_H, LV_COLOR_FORMAT_RGB565);
lv_canvas_set_buffer(canvas, canvas_buf, canvas_w, canvas_h, LV_COLOR_FORMAT_RGB565);
lv_obj_center(canvas);
// Placeholder label (visible only when no animations are loaded)