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
@@ -0,0 +1,134 @@
#include "../../hal/display_hal.h"
#include "../../hal/imu_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Arduino_GFX_Library.h>
#include <esp_heap_caps.h>
#include <lvgl.h>
// Render strip used when rotating in software. Sized to the largest LVGL
// partial flush we ever do (LCD_WIDTH × BUF_LINES, set in main.cpp).
#define ROT_BUF_LINES 40
static uint16_t* rot_buf = nullptr;
static Arduino_DataBus* bus = nullptr;
static Arduino_CO5300* gfx = nullptr;
void display_hal_init(void) {
bus = new Arduino_ESP32QSPI(
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
// CO5300 constructor: (bus, rst, rotation, w, h, col_offset1..2, row_offset1..2)
gfx = new Arduino_CO5300(
bus, LCD_RESET, 0 /* rotation handled in software */,
LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
}
void display_hal_begin(void) {
gfx->begin();
gfx->fillScreen(0x0000);
gfx->setBrightness(200);
// Allocate rotation strip (PSRAM). Sized to match main.cpp's BUF_LINES.
rot_buf = (uint16_t*)heap_caps_malloc(LCD_WIDTH * ROT_BUF_LINES * 2, MALLOC_CAP_SPIRAM);
}
void display_hal_set_brightness(uint8_t level) {
if (gfx) gfx->setBrightness(level);
}
void display_hal_fill_screen(uint16_t color) {
if (gfx) gfx->fillScreen(color);
}
// Rotate a w×h strip into rot_buf and compute destination coordinates on the
// 480×480 panel. Src is row-major over the rectangle (sx, sy, w, h).
static void rotate_strip(const uint16_t* src, int32_t w, int32_t h,
int32_t sx, int32_t sy, uint8_t r,
int32_t* dx, int32_t* dy, int32_t* dw, int32_t* dh) {
const int S = LCD_WIDTH;
switch (r) {
case 1: // 90° CW: (x,y) -> (S-1-y, x)
*dw = h; *dh = w;
*dx = S - sy - h;
*dy = sx;
for (int32_t y = 0; y < h; y++) {
for (int32_t x = 0; x < w; x++) {
rot_buf[x * h + (h - 1 - y)] = src[y * w + x];
}
}
break;
case 2: // 180°: (x,y) -> (S-1-x, S-1-y)
*dw = w; *dh = h;
*dx = S - sx - w;
*dy = S - sy - h;
for (int32_t y = 0; y < h; y++) {
for (int32_t x = 0; x < w; x++) {
rot_buf[(h - 1 - y) * w + (w - 1 - x)] = src[y * w + x];
}
}
break;
case 3: // 270° CW: (x,y) -> (y, S-1-x)
*dw = h; *dh = w;
*dx = sy;
*dy = S - sx - w;
for (int32_t y = 0; y < h; y++) {
for (int32_t x = 0; x < w; x++) {
rot_buf[(w - 1 - x) * h + y] = src[y * w + x];
}
}
break;
default:
*dx = sx; *dy = sy; *dw = w; *dh = h;
break;
}
}
void display_hal_draw_bitmap(int32_t x, int32_t y, int32_t w, int32_t h,
const uint16_t* pixels) {
if (!gfx) return;
uint8_t r = imu_hal_rotation_quadrant();
if (r == 0 || !rot_buf) {
gfx->draw16bitRGBBitmap(x, y, (uint16_t*)pixels, w, h);
return;
}
int32_t dx, dy, dw, dh;
rotate_strip(pixels, w, h, x, y, r, &dx, &dy, &dw, &dh);
gfx->draw16bitRGBBitmap(dx, dy, rot_buf, dw, dh);
}
// On rotation change, blank the panel, force a full LVGL redraw at the new
// orientation, then ramp brightness back up over ~125ms so the transition
// reads as deliberate.
void display_hal_tick(void) {
static uint8_t last_rotation = 0;
static uint8_t ramp_step = 0; // 0=idle, 1..4=ramping
static uint32_t ramp_last = 0;
uint8_t rot = imu_hal_rotation_quadrant();
if (rot != last_rotation) {
display_hal_set_brightness(0);
last_rotation = rot;
lv_obj_invalidate(lv_screen_active());
ramp_step = 1;
return;
}
if (ramp_step == 0) return;
uint32_t now = millis();
if (now - ramp_last < 25) return;
ramp_last = now;
static const uint8_t levels[] = {60, 120, 170, 200};
display_hal_set_brightness(levels[ramp_step - 1]);
if (ramp_step >= 4) ramp_step = 0;
else ramp_step++;
}
// CO5300 requires even-aligned flush regions.
void display_hal_round_area(int32_t* x1, int32_t* y1, int32_t* x2, int32_t* y2) {
*x1 = *x1 & ~1;
*y1 = *y1 & ~1;
*x2 = *x2 | 1;
*y2 = *y2 | 1;
}