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

87 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` / `| 1` to coordinates. |
## `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.