Files
clawdmeter/docs/porting/hal-contract.md
T
tobby168andClaude Opus 4.7 20351212b2 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>
2026-05-20 18:27:24 -07:00

5.1 KiB
Raw Blame History

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.