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:
co-authored by
Claude Opus 4.7
parent
f3ed2425bf
commit
20351212b2
@@ -1,11 +1,17 @@
|
||||
# Project context
|
||||
|
||||
ESP32-S3 firmware for a desk-side Claude Code usage monitor. Two board variants are supported via a build-flag macro:
|
||||
ESP32-S3 firmware for a desk-side Claude Code usage monitor. Each supported
|
||||
board lives in its own `firmware/src/boards/<name>/` folder and is selected
|
||||
via PlatformIO's `build_src_filter`. Adding a board means dropping in a new
|
||||
folder + a new `[env:...]` block — `main.cpp`, `ui.cpp`, and `splash.cpp`
|
||||
never see board-specific code. See [`docs/porting/adding-a-board.md`](docs/porting/adding-a-board.md).
|
||||
|
||||
- **`-DBOARD_AMOLED_216`** → original Waveshare ESP32-S3-Touch-AMOLED-2.16 (CO5300, 480×480 square, CST9220 touch). Build env: `waveshare_amoled_216`.
|
||||
- **`-DBOARD_AMOLED_18`** → Waveshare ESP32-S3-Touch-AMOLED-1.8 (SH8601, 368×448 portrait, FT3168 touch). Build env: `waveshare_amoled_18`.
|
||||
Two reference ports today:
|
||||
|
||||
`display_cfg.h` selects pins / typedefs / extern decls based on the macro. Per-board layout deltas (panel heights, fonts) are scoped with `#ifdef` in `ui.cpp` and `splash.cpp`.
|
||||
- `boards/waveshare_amoled_216/` — original Waveshare ESP32-S3-Touch-AMOLED-2.16 (CO5300, 480×480 square, CST9220 touch, IMU rotation). Build env: `waveshare_amoled_216`.
|
||||
- `boards/waveshare_amoled_18/` — Waveshare ESP32-S3-Touch-AMOLED-1.8 (SH8601, 368×448 portrait, FT3168 touch, XCA9554 IO expander). Build env: `waveshare_amoled_18`.
|
||||
|
||||
The shared code calls a small HAL (`firmware/src/hal/`) that each board implements: display, touch, input, power, IMU. Optional features are guarded by `BoardCaps` (runtime) and `BOARD_HAS_*` (compile-time) rather than `#ifdef BOARD_*`.
|
||||
|
||||
Connects to a host daemon over BLE; daemon polls Anthropic API for usage data. This file is for future Claude Code sessions to bootstrap quickly. Read this first.
|
||||
|
||||
@@ -30,21 +36,36 @@ Connects to a host daemon over BLE; daemon polls Anthropic API for usage data. T
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
main.cpp — setup(), loop(), button polling, FT3168 minimal reader (AMOLED-1.8), rotation flash (2.16 only)
|
||||
display_cfg.h — board-conditional pin defines, typedefs (PlatformDisplay = CO5300|SH8601), extern object decls
|
||||
io_expander.{h,cpp} — XCA9554/PCA9554 wrapper (AMOLED-1.8 only): LCD/TP reset release + PWR button read
|
||||
ui.{h,cpp} — 3-screen UI (splash, usage, bluetooth); splash is touch-toggled, usage↔bluetooth via PWR button
|
||||
splash.{h,cpp} — 20×20 pixel-art animation engine. CELL = 24 (480²) for 2.16, 18 (360² centered) for 1.8
|
||||
imu.{h,cpp} — accelerometer-driven rotation tracker (returns 0..3). Result is ignored on AMOLED-1.8.
|
||||
power.{h,cpp} — AXP2101 wrapper (battery %, charging, VBUS, PWR button). PWR source is conditional: AXP PKEY IRQ on 2.16, XCA9554 EXIO4 polling on 1.8.
|
||||
ble.{h,cpp} — NimBLE peripheral: custom data service + HID keyboard
|
||||
data.h — UsageData struct
|
||||
icons.h — icon arrays. Battery (5×) are RGB565A8 with alpha; rest are raw RGB565.
|
||||
logo.h — 80×80 RGB565 logo
|
||||
font_*.c — pre-compiled LVGL 9 bitmap fonts (Tiempos 56/34, Styrene 48/28/24/20/16/14/12, Mono 32/18)
|
||||
splash_animations.h — generated, do not hand-edit
|
||||
firmware/src/
|
||||
hal/ — board-agnostic interfaces shared code calls into
|
||||
board_caps.h — runtime BoardCaps struct (W, H, button_count, has_* flags)
|
||||
display_hal.h — init / begin / set_brightness / draw_bitmap / tick / round_area
|
||||
touch_hal.h — init / read(&x, &y, &pressed)
|
||||
input_hal.h — init / is_held(PRIMARY|SECONDARY)
|
||||
power_hal.h — init / tick / battery_pct / is_charging / pwr_pressed (edge)
|
||||
imu_hal.h — init / tick / rotation_quadrant
|
||||
boards/
|
||||
waveshare_amoled_216/ — CO5300 + CST9220 + AXP PKEY + QMI8658 rotation
|
||||
waveshare_amoled_18/ — SH8601 + FT3168 + AXP + XCA9554 (PWR via EXIO4), no rotation
|
||||
template/ — copy this to bootstrap a new port
|
||||
main.cpp — setup() + loop(): HAL calls only, zero #ifdef BOARD_*
|
||||
ui.{h,cpp} — 3-screen UI (splash, usage, bluetooth). compute_layout() picks fonts/positions from board_caps() (responsive — current breakpoint: H >= 460 → large, else compact)
|
||||
splash.{h,cpp} — 20×20 pixel-art engine. CELL = min(W,H)/20, centered.
|
||||
ble.{h,cpp} — NimBLE peripheral: custom data service + HID keyboard
|
||||
data.h — UsageData struct
|
||||
icons.h — icon arrays. Battery (5×) are RGB565A8 with alpha; rest are raw RGB565.
|
||||
logo.h — 80×80 RGB565 logo
|
||||
font_*.c — pre-compiled LVGL 9 bitmap fonts (Tiempos 56/34, Styrene 48/28/24/20/16/14/12, Mono 32/18)
|
||||
splash_animations.h — generated, do not hand-edit
|
||||
docs/porting/ — adding-a-board.md, hal-contract.md, capability-flags.md
|
||||
```
|
||||
|
||||
Each board folder contains: `board.h` (pins, I2C addresses, `BOARD_HAS_*` flags),
|
||||
`board_init.cpp` (Wire.begin + any IO expander), `display.cpp`, `touch.cpp`,
|
||||
`input.cpp`, `power.cpp`, `imu.cpp`, `caps.cpp` (the `BoardCaps` instance), plus
|
||||
any board-private hardware drivers (e.g. `io_expander.{h,cpp}` on AMOLED-1.8).
|
||||
PlatformIO's `build_src_filter` includes shared code + one board's folder per env.
|
||||
|
||||
## Build / flash
|
||||
|
||||
```bash
|
||||
@@ -66,14 +87,16 @@ The boot screen is `SCREEN_SPLASH` and only advances on a physical button press,
|
||||
|
||||
## Critical gotchas
|
||||
|
||||
1. **CO5300 cannot rotate.** Its MADCTL only supports axis flips, not column/row exchange. Rotation is done by **CPU pixel remapping in `my_flush_cb`** in main.cpp. We use **PARTIAL render mode with strip rotation** (small 480×40 strips, fast). On rotation change → AMOLED brightness flash → force redraw.
|
||||
1. **CO5300 cannot rotate.** Its MADCTL only supports axis flips, not column/row exchange. Rotation is done by **CPU pixel remapping inside `display_hal_draw_bitmap`** in `boards/waveshare_amoled_216/display.cpp`. We use **PARTIAL render mode with strip rotation** (small 480×40 strips, fast). On rotation change → AMOLED brightness flash → force redraw (handled inside `display_hal_tick`).
|
||||
2. **OPI PSRAM** required: `board_build.arduino.memory_type = qio_opi` in platformio.ini. Without this, `MALLOC_CAP_SPIRAM` returns NULL and the screen is black.
|
||||
3. **pioarduino platform required.** GFX Library for Arduino needs Arduino Core 3.x (`esp32-hal-periman.h`), not the 2.x that standard `espressif32` ships. We pin `pioarduino/platform-espressif32` 55.03.38-1.
|
||||
4. **LVGL 9 font patching.** `lv_font_conv` outputs LVGL 8 format. Must remove `#if LVGL_VERSION_MAJOR >= 8` guards, drop `.cache` field, add `.release_glyph`, `.kerning`, `.static_bitmap`, `.fallback`, `.user_data`. Without patching, fonts render invisible.
|
||||
5. **Touch reading must be centralized.** CST9220's `getPoint()` does a full I2C transaction. Calling it from multiple places consumed each other's data and broke input. `touch_read()` is called once per loop in main.cpp; both LVGL `my_touch_cb` and `touch.cpp` read from shared `touch_pressed/touch_x/touch_y` state.
|
||||
6. **CO5300 needs even-aligned flush regions.** `rounder_cb` enforces this.
|
||||
7. **Touch `setSwapXY(true)` and `setMirrorXY(true, false)`** are the empirically-correct values for default rotation 0. IMU rotation logic doesn't change touch mapping (it does CPU-side rotation of the rendered pixels, so LVGL still thinks the display is portrait at 0°).
|
||||
5. **Touch reading is centralized inside each board's `touch.cpp`.** The HAL `touch_hal_read()` is called once per loop from `my_touch_cb`; the board's implementation owns its latched `touch_pressed/x/y` state. Don't call the underlying controller from anywhere else — CST9220's `getPoint()` etc. do a full I2C transaction and concurrent callers consume each other's data.
|
||||
6. **Even-aligned flush regions.** `display_hal_round_area` (called from `rounder_cb`) is what each board uses to enforce this. Required on CO5300, harmless on SH8601.
|
||||
7. **Touch axis swap/mirror is per-board.** The 2.16's CST9220 needs `setSwapXY(true)` + `setMirrorXY(true, false)` — applied inside `boards/waveshare_amoled_216/touch.cpp::touch_hal_init()`. New ports apply their own.
|
||||
8. **LVGL RGB565A8 is planar.** `w*h` RGB565 pixels followed by `w*h` alpha bytes; `data_size = w*h*3`, `stride = w*2`. Use `init_icon_dsc_rgb565a8()` for icons that overlap non-uniform backgrounds (e.g. battery over splash). Lucide source PNGs are black-on-transparent — converter must tint to white or icons render invisible. See `tools/png_to_lvgl.js`.
|
||||
9. **Per-board pre-init is `board_init()`.** Each board's `board_init.cpp` brings up `Wire` and any reset-gating IO expander BEFORE `display_hal_init()`. Skipping the IO expander release on AMOLED-1.8 leaves SH8601 + FT3168 in reset and they silently fail to probe.
|
||||
10. **No `#ifdef BOARD_*` in shared code.** The whole point of the refactor — if you're about to add one, you probably want a `BoardCaps` field or a per-board file instead. See `docs/porting/capability-flags.md`.
|
||||
|
||||
## Icons
|
||||
|
||||
@@ -97,6 +120,8 @@ See `~/.claude/projects/.../memory/` files for persistent context (user is an em
|
||||
|
||||
## Recent session highlights
|
||||
|
||||
- **Device-abstraction refactor (2026-05-18).** All board-conditional code moved out of shared files into `boards/<name>/` and behind a HAL in `hal/`. ~30 `#ifdef BOARD_*` blocks went to zero. UI is responsive via `compute_layout()` driven by `board_caps()`. New ports add a folder + a PlatformIO env — no shared file edits.
|
||||
- Added second board port: Waveshare AMOLED-1.8 (368×448 portrait, SH8601, FT3168, XCA9554 IO expander).
|
||||
- Migrated from Panlee SC01 Plus (480×320 IPS) to Waveshare 2.16" AMOLED (480×480 square). Full hardware/library swap.
|
||||
- Added IMU auto-rotation, battery indicator, USB-state-aware screen switching.
|
||||
- Added splash screen with scraped pixel-art animations and 3-button physical input layout.
|
||||
|
||||
@@ -25,10 +25,18 @@ While the splash is up, the middle button cycles animations instead of screens.
|
||||
|
||||
## Hardware
|
||||
|
||||
- [Waveshare ESP32-S3-Touch-AMOLED-2.16](https://www.waveshare.com/esp32-s3-touch-amoled-2.16.htm?&aff_id=149786) - ESP32-S3R8, 2.16" 480×480 AMOLED (CO5300 QSPI), CST9220 cap touch, AXP2101 PMU + Li-Po battery, QMI8658 IMU
|
||||
Two boards are supported out of the box:
|
||||
|
||||
- [Waveshare ESP32-S3-Touch-AMOLED-2.16](https://www.waveshare.com/esp32-s3-touch-amoled-2.16.htm?&aff_id=149786) — ESP32-S3R8, 2.16" 480×480 AMOLED (CO5300 QSPI), CST9220 cap touch, AXP2101 PMU + Li-Po battery, QMI8658 IMU. Three side buttons, IMU auto-rotation. Build env: `waveshare_amoled_216`.
|
||||
- [Waveshare ESP32-S3-Touch-AMOLED-1.8](https://www.waveshare.com/esp32-s3-touch-amoled-1.8.htm) — ESP32-S3R8, 1.8" 368×448 portrait AMOLED (SH8601 QSPI), FT3168 cap touch, AXP2101 PMU, QMI8658 IMU, XCA9554 IO expander, 16 MB flash. Two buttons (BOOT + PWR), fixed orientation. Build env: `waveshare_amoled_18`.
|
||||
|
||||
Plus per board:
|
||||
|
||||
- USB-C cable for flashing firmware and charging
|
||||
- 3.7V Li-Po battery (MX1.25 2-pin connector, optional)
|
||||
|
||||
**Porting to another board:** the firmware is a thin HAL with per-board folders under `firmware/src/boards/`. Drop in a new folder and a new PlatformIO env — `main.cpp`, `ui.cpp`, and `splash.cpp` never need to change. See [`docs/porting/adding-a-board.md`](docs/porting/adding-a-board.md) for the walk-through and [`docs/porting/hal-contract.md`](docs/porting/hal-contract.md) for the interfaces a port must implement.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux (tested on Ubuntu) or macOS
|
||||
@@ -188,6 +196,31 @@ lv_font_conv --font assets/DejaVuSansMono.ttf \
|
||||
|
||||
Without these patches, fonts compile but render as invisible.
|
||||
|
||||
### CJK support
|
||||
|
||||
`firmware/src/font_cjk_16.c` covers the full CJK Unified Ideographs basic
|
||||
block (U+4E00–U+9FFF, ~20k glyphs) plus ASCII, CJK punctuation, and
|
||||
halfwidth/fullwidth forms. Generated from [Noto Sans CJK SC](https://github.com/notofonts/noto-cjk)
|
||||
(SIL OFL 1.1) at 16px, 2bpp:
|
||||
|
||||
```bash
|
||||
lv_font_conv --font NotoSansCJKsc-Regular.otf --size 16 --bpp 2 \
|
||||
--no-compress --format lvgl --lv-include 'lvgl.h' \
|
||||
-r '0x20-0x7E,0xB7,0x2014,0x2018-0x2019,0x201C-0x201D,0x2026,0x3000-0x303F,0x4E00-0x9FFF,0xFF00-0xFFEF' \
|
||||
-o firmware/src/font_cjk_16.c
|
||||
```
|
||||
|
||||
Then apply the four LVGL 9 patches above. Because the font has >65k of
|
||||
glyph bitmap data, the build needs `-DLV_FONT_FMT_TXT_LARGE=1` in
|
||||
`platformio.ini` build flags so font descriptor offsets switch from
|
||||
16-bit to 32-bit.
|
||||
|
||||
The CJK font is used for the Activity screen's user-prompt row and todo
|
||||
content rows. The headline (28pt Styrene B) and titles stay ASCII-only
|
||||
to preserve the brand font — Chinese text in those slots renders as
|
||||
empty boxes. Add a `font_cjk_28.c` if full coverage is needed (~1MB
|
||||
more flash).
|
||||
|
||||
## Converting Lucide icons
|
||||
|
||||
The UI uses a small set of [Lucide](https://lucide.dev) icons (bluetooth + battery states) converted to RGB565 / RGB565A8 C arrays for LVGL.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Porting Clawdmeter to a new board
|
||||
|
||||
A board port is a folder under `firmware/src/boards/` plus a new
|
||||
`[env:...]` block in `firmware/platformio.ini`. You should never need
|
||||
to edit `firmware/src/main.cpp`, `firmware/src/ui.cpp`, or anything
|
||||
under `firmware/src/hal/`. If you find yourself wanting to, that's a
|
||||
gap in the HAL — open an issue.
|
||||
|
||||
## Hardware you need
|
||||
|
||||
At minimum:
|
||||
|
||||
- An **ESP32-S3** (other ESP32 family members may work; this is what the
|
||||
upstream firmware is tested on). OPI PSRAM is **required** — partial
|
||||
flush buffers and the splash canvas are allocated from PSRAM.
|
||||
- A QSPI **AMOLED panel** with a driver supported by
|
||||
[GFX Library for Arduino](https://github.com/moononournation/Arduino_GFX)
|
||||
(CO5300, SH8601, NV3041A, etc.). Other interfaces aren't supported yet.
|
||||
- A **touch controller** over I2C. The HAL just needs init + read; you
|
||||
can use any driver you can compile.
|
||||
- A **primary button** (typically the BOOT/GPIO 0 push button).
|
||||
|
||||
Optional:
|
||||
|
||||
- A second physical button (e.g. for HID Shift+Tab mode toggle).
|
||||
- An AXP2101 PMU for battery monitoring + a power button.
|
||||
- A QMI8658 (or compatible) IMU for automatic rotation.
|
||||
- An XCA9554 / PCA9554 IO expander if reset / enable lines are routed
|
||||
through one (the AMOLED-1.8 board does this).
|
||||
|
||||
## Step-by-step
|
||||
|
||||
1. **Copy the template folder.**
|
||||
|
||||
```bash
|
||||
cp -r firmware/src/boards/template firmware/src/boards/my_board
|
||||
```
|
||||
|
||||
2. **Fill in `boards/my_board/board.h`.** Replace every `TODO` with your
|
||||
board's pins, I2C addresses, dimensions, and capability flags. The
|
||||
capability flags drive both compile-time dead-stripping in the HAL
|
||||
implementations and runtime UI decisions via `BoardCaps`.
|
||||
|
||||
3. **Implement the per-board sources.** Each one corresponds to a HAL
|
||||
header in `firmware/src/hal/`. Look at one of the reference ports for
|
||||
a worked example:
|
||||
|
||||
| File | Reference port (start here) |
|
||||
|-------------------|----------------------------------------------------------------|
|
||||
| `display.cpp` | `boards/waveshare_amoled_216/display.cpp` (with CPU rotation) or `_18/display.cpp` (no rotation) |
|
||||
| `touch.cpp` | `_216/touch.cpp` (library-based) or `_18/touch.cpp` (vendored I2C reader) |
|
||||
| `input.cpp` | `_216/input.cpp` (two buttons) or `_18/input.cpp` (one button) |
|
||||
| `power.cpp` | `_216/power.cpp` (PMU IRQ) or `_18/power.cpp` (PMU + IO expander button) |
|
||||
| `imu.cpp` | `_216/imu.cpp` (full rotation) or `_18/imu.cpp` (init-only stub) |
|
||||
| `caps.cpp` | either reference — just edit the struct literal |
|
||||
| `board_init.cpp` | `_216/board_init.cpp` (no expander) or `_18/board_init.cpp` (with expander) |
|
||||
|
||||
4. **Add a PlatformIO env.** In `firmware/platformio.ini`, copy one of
|
||||
the existing `[env:waveshare_amoled_*]` blocks and adjust:
|
||||
|
||||
```ini
|
||||
[env:my_board]
|
||||
; ... platform / board / framework as before ...
|
||||
|
||||
build_src_filter =
|
||||
+<*>
|
||||
-<boards/>
|
||||
+<boards/my_board/> ; the only line you change here
|
||||
|
||||
build_flags =
|
||||
-DBOARD_MY_BOARD ; identity-only — the shared code never
|
||||
; branches on this; per-board code may
|
||||
```
|
||||
|
||||
If your panel needs flash > 4 MB (extra animations, larger fonts),
|
||||
copy the `board_upload.*` block from the AMOLED-1.8 env.
|
||||
|
||||
5. **Build.** `pio run -d firmware -e my_board`. The link step is the
|
||||
real verification — any missing HAL symbol or duplicated definition
|
||||
shows up here.
|
||||
|
||||
6. **Flash + smoke test.** The first boot should land on the splash
|
||||
screen. If it doesn't, check `pio device monitor` for HAL init
|
||||
messages — every reference port logs OK / failure for display, touch,
|
||||
PMU, IMU during `setup()`.
|
||||
|
||||
7. **Visual QA.** `./screenshot.sh out.png` over USB serial captures
|
||||
the live framebuffer at the active resolution. The UI is responsive
|
||||
(see [hal-contract.md](hal-contract.md) for breakpoint details);
|
||||
most ports will look acceptable out of the box. If your screen size
|
||||
doesn't match an existing breakpoint, you may want to add one to
|
||||
`compute_layout()` in `firmware/src/ui.cpp`.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Display stays black, no panic.** Usually one of: OPI PSRAM not enabled
|
||||
in platformio.ini (check `board_build.arduino.memory_type = qio_opi`);
|
||||
IO expander not released before `gfx->begin()` (run `io_expander_init()`
|
||||
from `board_init()`); GFX library version too old to know about your
|
||||
panel chip.
|
||||
- **Touch reads zeros / wrong coordinates.** The HAL hands LVGL whatever
|
||||
the controller reports — apply any axis swap / mirror inside your
|
||||
`touch.cpp`. CST9220 needs `setSwapXY(true)` + `setMirrorXY(true,
|
||||
false)` on the AMOLED-2.16 board; your controller will likely differ.
|
||||
- **GPL warning when picking a touch driver.** The project intentionally
|
||||
avoids copyleft dependencies. If the only available library is GPL,
|
||||
vendor a minimal I2C reader instead (see `_18/touch.cpp`).
|
||||
- **Both boards built fine but one runs and the other doesn't.** The
|
||||
build_src_filter is per-env — re-check you copied the existing env
|
||||
blocks correctly and the `-<boards/>` then `+<boards/your_one/>`
|
||||
ordering is right (filters apply in declaration order).
|
||||
@@ -0,0 +1,39 @@
|
||||
# Capability flags
|
||||
|
||||
Each board's `board.h` declares these. They're consumed in two places:
|
||||
|
||||
1. **`caps.cpp`** — copies them into the `BoardCaps` instance so shared
|
||||
code (`ui.cpp`, `main.cpp`) can query them at runtime via
|
||||
`board_caps()`.
|
||||
2. **The per-board source files** — `#if BOARD_HAS_*` lets the linker
|
||||
dead-strip entire functions on boards that don't need them.
|
||||
|
||||
Keep the two in sync. The pattern in `caps.cpp` does this for you:
|
||||
```c
|
||||
.button_count = (uint8_t)(1 + BOARD_HAS_SECONDARY_BUTTON),
|
||||
.has_rotation = (bool)BOARD_HAS_ROTATION,
|
||||
```
|
||||
|
||||
## The flags
|
||||
|
||||
| Macro | Default | What it gates |
|
||||
|--------------------------------|---------|---------------|
|
||||
| `BOARD_HAS_SECONDARY_BUTTON` | 0 | A second physical button (HID Shift+Tab on the reference ports). `caps.button_count = 1 + this`. UI uses `caps.button_count >= 2` to decide whether to poll/handle the secondary button — there is no `#ifdef` in shared code. |
|
||||
| `BOARD_HAS_ROTATION` | 0 | IMU-driven auto-rotation via CPU strip transformation in `display_hal_draw_bitmap`. When 0, `display_hal_tick` is a no-op and the rotation buffer in `display.cpp` doesn't get allocated. |
|
||||
| `BOARD_HAS_IMU` | 0 | Whether the accelerometer is populated and initialized. Distinct from `BOARD_HAS_ROTATION` — the AMOLED-1.8 has the QMI8658 (so `HAS_IMU=1`) but the kit's enclosure mounts the panel at a fixed orientation, so rotation is off. |
|
||||
| `BOARD_HAS_BATTERY` | 0 | Whether PMU battery measurement is meaningful on this board. UI hides the battery indicator when false. |
|
||||
| `BOARD_HAS_IO_EXPANDER` | 0 | Whether an IO expander gates display / touch reset lines. Doesn't directly gate any code path — but signals to the porter that `board_init()` must release the expander before `display_hal_init()`. |
|
||||
|
||||
## Future capabilities
|
||||
|
||||
Add a new flag when:
|
||||
|
||||
- A shared-code decision currently uses `if (caps.has_<thing>)` and
|
||||
you want to extend it (e.g. add `BOARD_HAS_HAPTIC` for vibration
|
||||
feedback).
|
||||
- A per-board file conditionally compiles a block of code (e.g.
|
||||
audio amp init under `BOARD_HAS_AUDIO`).
|
||||
|
||||
Don't add a flag for a one-off detail. If only one board cares about it
|
||||
and shared code never queries it, leave it as a constant in that board's
|
||||
`board.h` and use it only in that board's `.cpp` files.
|
||||
@@ -0,0 +1,86 @@
|
||||
# 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.
|
||||
@@ -6,6 +6,13 @@ board_build.arduino.memory_type = qio_opi
|
||||
upload_speed = 921600
|
||||
monitor_speed = 115200
|
||||
|
||||
; Compile shared code + this board's per-board folder; exclude all other
|
||||
; boards. New ports add a `+<boards/<name>/>` line in their own env block.
|
||||
build_src_filter =
|
||||
+<*>
|
||||
-<boards/>
|
||||
+<boards/waveshare_amoled_216/>
|
||||
|
||||
build_flags =
|
||||
-DBOARD_AMOLED_216
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
@@ -56,6 +63,13 @@ board_build.partitions = default_16MB.csv
|
||||
upload_speed = 921600
|
||||
monitor_speed = 115200
|
||||
|
||||
; Compile shared code + this board's per-board folder; exclude all other
|
||||
; boards. New ports add a `+<boards/<name>/>` line in their own env block.
|
||||
build_src_filter =
|
||||
+<*>
|
||||
-<boards/>
|
||||
+<boards/waveshare_amoled_18/>
|
||||
|
||||
build_flags =
|
||||
-DBOARD_AMOLED_18
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
// Template board.h — copy this file (and the rest of boards/template/) to
|
||||
// boards/<your_board>/ and fill in each TODO. See docs/porting/adding-a-board.md
|
||||
// for a walk-through.
|
||||
|
||||
#define BOARD_NAME "TODO: human-readable board name"
|
||||
|
||||
// ---- Display geometry ----
|
||||
// Active panel pixel dimensions (post-orientation).
|
||||
#define LCD_WIDTH 240 // TODO
|
||||
#define LCD_HEIGHT 240 // TODO
|
||||
|
||||
// ---- QSPI display pins ----
|
||||
// Wire your panel datasheet's QSPI pins to MCU GPIOs and list them here.
|
||||
#define LCD_CS 12 // TODO
|
||||
#define LCD_SCLK 38 // TODO
|
||||
#define LCD_SDIO0 4 // TODO
|
||||
#define LCD_SDIO1 5 // TODO
|
||||
#define LCD_SDIO2 6 // TODO
|
||||
#define LCD_SDIO3 7 // TODO
|
||||
#define LCD_RESET 2 // TODO; use GFX_NOT_DEFINED if you reset via an expander
|
||||
|
||||
// ---- I2C bus (shared by touch, PMU, IMU, IO expander) ----
|
||||
#define IIC_SDA 15 // TODO
|
||||
#define IIC_SCL 14 // TODO
|
||||
|
||||
// ---- Touch ----
|
||||
// I2C address depends on the controller. Replace TODO_TP_ADDR below and pick
|
||||
// the matching driver in touch.cpp.
|
||||
#define TP_INT 11 // TODO
|
||||
#define TP_ADDR 0x00 // TODO
|
||||
|
||||
// ---- PMU ----
|
||||
// Drop or change if your board doesn't ship an AXP2101.
|
||||
#define AXP2101_ADDR 0x34
|
||||
|
||||
// ---- Buttons ----
|
||||
#define BTN_BACK_GPIO 0 // BOOT — primary, Space (PTT)
|
||||
// If your board has a second physical button, set its GPIO and bump
|
||||
// BOARD_HAS_SECONDARY_BUTTON to 1 below.
|
||||
|
||||
// ---- Capability flags ----
|
||||
// Compile-time switches the linker uses to dead-strip optional features.
|
||||
// Keep these in sync with the BoardCaps instance in caps.cpp.
|
||||
#define BOARD_HAS_SECONDARY_BUTTON 0 // TODO
|
||||
#define BOARD_HAS_ROTATION 0 // TODO: IMU-driven CPU rotation in display.cpp
|
||||
#define BOARD_HAS_IMU 0 // TODO
|
||||
#define BOARD_HAS_BATTERY 0 // TODO
|
||||
#define BOARD_HAS_IO_EXPANDER 0 // TODO
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
// Called once at the very start of setup(), before any HAL device init.
|
||||
// At minimum bring up the shared I2C bus. If your board has an IO expander
|
||||
// gating the LCD or touch reset lines, initialize and release it here too
|
||||
// (otherwise display_hal_init() will fail to probe the panel).
|
||||
extern "C" void board_init(void) {
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
// TODO: io_expander_init() if your board needs one
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#include "../../hal/board_caps.h"
|
||||
#include "board.h"
|
||||
|
||||
static const BoardCaps caps = {
|
||||
.name = BOARD_NAME,
|
||||
.width = LCD_WIDTH,
|
||||
.height = LCD_HEIGHT,
|
||||
.button_count = (uint8_t)(1 + BOARD_HAS_SECONDARY_BUTTON),
|
||||
.has_rotation = (bool)BOARD_HAS_ROTATION,
|
||||
.has_battery = (bool)BOARD_HAS_BATTERY,
|
||||
.has_imu = (bool)BOARD_HAS_IMU,
|
||||
};
|
||||
|
||||
const BoardCaps& board_caps(void) { return caps; }
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "../../hal/display_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
|
||||
// TODO: pick the right driver class from Arduino_GFX_Library (e.g.
|
||||
// Arduino_CO5300, Arduino_SH8601, Arduino_NV3041A). Most QSPI AMOLED
|
||||
// panels are supported in the upstream library — check the panel's
|
||||
// chip and grep the library include path.
|
||||
|
||||
static Arduino_DataBus* bus = nullptr;
|
||||
// static Arduino_<YOUR_PANEL>* gfx = nullptr;
|
||||
|
||||
void display_hal_init(void) {
|
||||
bus = new Arduino_ESP32QSPI(
|
||||
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
// gfx = new Arduino_<YOUR_PANEL>(bus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, ...);
|
||||
}
|
||||
|
||||
void display_hal_begin(void) {
|
||||
// gfx->begin();
|
||||
// gfx->fillScreen(0x0000);
|
||||
// gfx->setBrightness(200);
|
||||
}
|
||||
|
||||
void display_hal_set_brightness(uint8_t level) {
|
||||
(void)level;
|
||||
// gfx->setBrightness(level);
|
||||
}
|
||||
|
||||
void display_hal_fill_screen(uint16_t color) {
|
||||
(void)color;
|
||||
// gfx->fillScreen(color);
|
||||
}
|
||||
|
||||
void display_hal_draw_bitmap(int32_t x, int32_t y, int32_t w, int32_t h,
|
||||
const uint16_t* pixels) {
|
||||
(void)x; (void)y; (void)w; (void)h; (void)pixels;
|
||||
// gfx->draw16bitRGBBitmap(x, y, (uint16_t*)pixels, w, h);
|
||||
//
|
||||
// If your panel needs CPU rotation (no native MADCTL rotate), set
|
||||
// BOARD_HAS_ROTATION=1 in board.h, allocate a rotation strip buffer
|
||||
// in display_hal_begin(), and transform (x, y, w, h) + pixels here.
|
||||
// See boards/waveshare_amoled_216/display.cpp for a worked example.
|
||||
}
|
||||
|
||||
void display_hal_tick(void) {
|
||||
// Only needed for boards that animate the brightness ramp during a
|
||||
// CPU-rotation transition (see the 2.16 reference port).
|
||||
}
|
||||
|
||||
void display_hal_round_area(int32_t* x1, int32_t* y1, int32_t* x2, int32_t* y2) {
|
||||
// Most QSPI AMOLED drivers expect even-aligned flush regions. Harmless
|
||||
// to apply on panels that don't strictly require it.
|
||||
*x1 = *x1 & ~1;
|
||||
*y1 = *y1 & ~1;
|
||||
*x2 = *x2 | 1;
|
||||
*y2 = *y2 | 1;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "../../hal/imu_hal.h"
|
||||
|
||||
// No IMU on this template. If your board ships an accelerometer (e.g.
|
||||
// QMI8658 + want auto-rotation), copy boards/waveshare_amoled_216/imu.cpp
|
||||
// here and set BOARD_HAS_ROTATION=1 in board.h.
|
||||
|
||||
void imu_hal_init(void) {}
|
||||
void imu_hal_tick(void) {}
|
||||
uint8_t imu_hal_rotation_quadrant(void) { return 0; }
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "../../hal/input_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
void input_hal_init(void) {
|
||||
pinMode(BTN_BACK_GPIO, INPUT_PULLUP);
|
||||
#if BOARD_HAS_SECONDARY_BUTTON
|
||||
// pinMode(BTN_FWD_GPIO, INPUT_PULLUP); // TODO
|
||||
#endif
|
||||
}
|
||||
|
||||
bool input_hal_is_held(InputButton btn) {
|
||||
switch (btn) {
|
||||
case INPUT_BTN_PRIMARY:
|
||||
return digitalRead(BTN_BACK_GPIO) == LOW;
|
||||
case INPUT_BTN_SECONDARY:
|
||||
#if BOARD_HAS_SECONDARY_BUTTON
|
||||
// return digitalRead(BTN_FWD_GPIO) == LOW; // TODO
|
||||
return false;
|
||||
#else
|
||||
return false; // not present on this board
|
||||
#endif
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "../../hal/power_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
// Minimal stub — replace with real power management for your board.
|
||||
//
|
||||
// If your board has an AXP2101 or similar PMU, mirror
|
||||
// boards/waveshare_amoled_216/power.cpp. If the PWR button is wired
|
||||
// somewhere other than the PMU's PKEY pin (e.g. through an IO expander
|
||||
// like the AMOLED-1.8 board), look at that port instead.
|
||||
//
|
||||
// If your board has no PMU and no PWR button, leave the stubs as below
|
||||
// and set BOARD_HAS_BATTERY=0 in board.h — the UI honors caps.has_battery
|
||||
// and hides the battery indicator.
|
||||
|
||||
void power_hal_init(void) {}
|
||||
void power_hal_tick(void) {}
|
||||
|
||||
int power_hal_battery_pct(void) { return -1; }
|
||||
bool power_hal_is_charging(void) { return false; }
|
||||
bool power_hal_is_vbus_in(void) { return false; }
|
||||
bool power_hal_pwr_pressed(void) { return false; }
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "../../hal/touch_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
// TODO: replace the body with a driver for your controller. Two patterns:
|
||||
// 1. A library (SensorLib's CSTxxx, TAMC_GT911, etc.) — add to lib_deps
|
||||
// in platformio.ini, mirror the AMOLED-2.16 port's touch.cpp.
|
||||
// 2. A minimal vendored reader — preferred when the only available
|
||||
// library is GPL-licensed (see boards/waveshare_amoled_18/touch.cpp).
|
||||
//
|
||||
// Whichever you pick, touch_hal_read() must complete in well under 5 ms
|
||||
// (a single I2C burst is fine) so it doesn't drop frames.
|
||||
|
||||
static volatile bool touch_data_ready = false;
|
||||
static volatile bool touch_pressed = false;
|
||||
static volatile uint16_t touch_x = 0;
|
||||
static volatile uint16_t touch_y = 0;
|
||||
|
||||
static void IRAM_ATTR touch_isr(void) {
|
||||
touch_data_ready = true;
|
||||
}
|
||||
|
||||
void touch_hal_init(void) {
|
||||
// TODO: initialize your controller over I2C; configure to active scanning.
|
||||
pinMode(TP_INT, INPUT_PULLUP);
|
||||
attachInterrupt(TP_INT, touch_isr, FALLING);
|
||||
}
|
||||
|
||||
void touch_hal_read(uint16_t* x, uint16_t* y, bool* pressed) {
|
||||
if (touch_data_ready) {
|
||||
touch_data_ready = false;
|
||||
// TODO: read coords from your controller into touch_x, touch_y,
|
||||
// touch_pressed.
|
||||
}
|
||||
*x = touch_x;
|
||||
*y = touch_y;
|
||||
*pressed = touch_pressed;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
// Waveshare ESP32-S3-Touch-AMOLED-1.8 — portrait AMOLED kit.
|
||||
// 368x448 SH8601 + FT3168 touch + AXP2101 PMU + QMI8658 IMU + XCA9554 expander.
|
||||
// IMU is present (initialized for I2C bus health) but rotation is disabled
|
||||
// because the panel mounts in a fixed orientation in the kit's enclosure.
|
||||
|
||||
#define BOARD_NAME "Waveshare AMOLED 1.8"
|
||||
|
||||
// ---- Display geometry (portrait) ----
|
||||
#define LCD_WIDTH 368
|
||||
#define LCD_HEIGHT 448
|
||||
|
||||
// ---- QSPI display pins (SH8601) ----
|
||||
#define LCD_CS 12
|
||||
#define LCD_SCLK 11 // different from 2.16 board (was GPIO 38)
|
||||
#define LCD_SDIO0 4
|
||||
#define LCD_SDIO1 5
|
||||
#define LCD_SDIO2 6
|
||||
#define LCD_SDIO3 7
|
||||
// LCD reset is routed through the XCA9554 IO expander (EXIO1). The Arduino
|
||||
// GFX driver gets GFX_NOT_DEFINED; the expander releases reset before
|
||||
// gfx->begin() runs.
|
||||
|
||||
// ---- I2C bus (touch + PMU + IMU + IO expander all share one bus) ----
|
||||
#define IIC_SDA 15
|
||||
#define IIC_SCL 14
|
||||
|
||||
// ---- Touch (FT3168 via vendored minimal I2C reader) ----
|
||||
#define TP_INT 21
|
||||
#define FT3168_ADDR 0x38
|
||||
|
||||
// ---- PMU ----
|
||||
#define AXP2101_ADDR 0x34
|
||||
|
||||
// ---- IO expander (XCA9554/PCA9554 compatible) ----
|
||||
// Gates LCD_RST, TP_RST, audio amp enable, and reads the PWR button.
|
||||
#define XCA9554_ADDR 0x20
|
||||
#define IOX_PIN_TP_RST 0 // EXIO0 → touch reset (active LOW)
|
||||
#define IOX_PIN_LCD_RST 1 // EXIO1 → display reset (active LOW)
|
||||
#define IOX_PIN_PA_EN 2 // EXIO2 → audio amp enable
|
||||
#define IOX_PIN_PWR_BTN 4 // EXIO4 → PWR button input, active HIGH
|
||||
|
||||
// ---- Buttons ----
|
||||
#define BTN_BACK_GPIO 0 // BOOT — primary, Space (PTT)
|
||||
// PWR comes via XCA9554 EXIO4 (see power.cpp); there is no secondary button.
|
||||
|
||||
// ---- Capability flags ----
|
||||
#define BOARD_HAS_SECONDARY_BUTTON 0
|
||||
#define BOARD_HAS_ROTATION 0
|
||||
#define BOARD_HAS_IMU 1 // present + initialized, but rotation off
|
||||
#define BOARD_HAS_BATTERY 1
|
||||
#define BOARD_HAS_IO_EXPANDER 1
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "board.h"
|
||||
#include "io_expander.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
// AMOLED-1.8 also needs the XCA9554 IO expander up first — the display
|
||||
// and touch controllers stay in reset until EXIO0..1 go HIGH.
|
||||
extern "C" void board_init(void) {
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
io_expander_init();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#include "../../hal/board_caps.h"
|
||||
#include "board.h"
|
||||
|
||||
static const BoardCaps caps = {
|
||||
.name = BOARD_NAME,
|
||||
.width = LCD_WIDTH,
|
||||
.height = LCD_HEIGHT,
|
||||
.button_count = 1,
|
||||
.has_rotation = false,
|
||||
.has_battery = true,
|
||||
.has_imu = true,
|
||||
};
|
||||
|
||||
const BoardCaps& board_caps(void) { return caps; }
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "../../hal/display_hal.h"
|
||||
#include "board.h"
|
||||
#include "io_expander.h"
|
||||
#include <Arduino.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
|
||||
// AMOLED-1.8 is fixed at 0°. No CPU rotation, no rot_buf.
|
||||
// Display reset is routed through the XCA9554 IO expander (EXIO1) which
|
||||
// must be initialized + released before gfx->begin() runs — main.cpp
|
||||
// arranges this by calling display_hal_init() after io_expander_init().
|
||||
|
||||
static Arduino_DataBus* bus = nullptr;
|
||||
static Arduino_SH8601* gfx = nullptr;
|
||||
|
||||
void display_hal_init(void) {
|
||||
bus = new Arduino_ESP32QSPI(
|
||||
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
// SH8601 constructor: (bus, rst, rotation, w, h)
|
||||
gfx = new Arduino_SH8601(
|
||||
bus, GFX_NOT_DEFINED /* reset via XCA9554 */, 0,
|
||||
LCD_WIDTH, LCD_HEIGHT);
|
||||
}
|
||||
|
||||
void display_hal_begin(void) {
|
||||
gfx->begin();
|
||||
gfx->fillScreen(0x0000);
|
||||
gfx->setBrightness(200);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void display_hal_draw_bitmap(int32_t x, int32_t y, int32_t w, int32_t h,
|
||||
const uint16_t* pixels) {
|
||||
if (gfx) gfx->draw16bitRGBBitmap(x, y, (uint16_t*)pixels, w, h);
|
||||
}
|
||||
|
||||
void display_hal_tick(void) {
|
||||
// No rotation handling needed on this board.
|
||||
}
|
||||
|
||||
// SH8601 driver doesn't strictly require even alignment in source, but the
|
||||
// rounder is harmless and keeps behavior consistent with the CO5300 port.
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "../../hal/imu_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <SensorQMI8658.hpp>
|
||||
|
||||
// AMOLED-1.8 ships with QMI8658 populated, but the kit's enclosure mounts
|
||||
// the panel in a fixed orientation. We initialize the device anyway so the
|
||||
// shared I2C bus stays healthy, but always report rotation 0.
|
||||
|
||||
static SensorQMI8658 imu;
|
||||
|
||||
void imu_hal_init(void) {
|
||||
if (!imu.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {
|
||||
Serial.println("QMI8658 init failed");
|
||||
return;
|
||||
}
|
||||
Serial.println("QMI8658 init OK (rotation disabled on this board)");
|
||||
}
|
||||
|
||||
void imu_hal_tick(void) {
|
||||
// No-op — rotation is disabled.
|
||||
}
|
||||
|
||||
uint8_t imu_hal_rotation_quadrant(void) { return 0; }
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "../../hal/input_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
// AMOLED-1.8 has only the BOOT button as a secondary input — the PWR
|
||||
// button comes through power_hal (XCA9554 EXIO4). No secondary button.
|
||||
|
||||
void input_hal_init(void) {
|
||||
pinMode(BTN_BACK_GPIO, INPUT_PULLUP);
|
||||
}
|
||||
|
||||
bool input_hal_is_held(InputButton btn) {
|
||||
switch (btn) {
|
||||
case INPUT_BTN_PRIMARY:
|
||||
return digitalRead(BTN_BACK_GPIO) == LOW;
|
||||
case INPUT_BTN_SECONDARY:
|
||||
return false; // not present on this board
|
||||
}
|
||||
return false;
|
||||
}
|
||||
+9
-16
@@ -1,21 +1,17 @@
|
||||
#include "io_expander.h"
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
|
||||
#include "display_cfg.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
// XCA9554/PCA9554 register map
|
||||
#define IOX_REG_INPUT 0x00
|
||||
#define IOX_REG_OUTPUT 0x01
|
||||
#define IOX_REG_INPUT 0x00
|
||||
#define IOX_REG_OUTPUT 0x01
|
||||
#define IOX_REG_POLARITY 0x02
|
||||
#define IOX_REG_CONFIG 0x03 // 1 = input, 0 = output
|
||||
#define IOX_REG_CONFIG 0x03 // 1 = input, 0 = output
|
||||
|
||||
// EXIO0..2 are outputs (reset lines + audio amp). Everything else is input.
|
||||
// Bit layout: 0bIIIIIOOO = 0xF8
|
||||
#define IOX_CONFIG_MASK 0xF8
|
||||
|
||||
#define IOX_CONFIG_MASK 0xF8
|
||||
// All three outputs HIGH = resets released, amp enabled.
|
||||
#define IOX_OUTPUT_DEFAULT 0x07
|
||||
|
||||
@@ -28,7 +24,7 @@ static bool write_reg(uint8_t reg, uint8_t val) {
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
static bool read_reg(uint8_t reg, uint8_t &val) {
|
||||
static bool read_reg(uint8_t reg, uint8_t& val) {
|
||||
Wire.beginTransmission(XCA9554_ADDR);
|
||||
Wire.write(reg);
|
||||
if (Wire.endTransmission(false) != 0) return false;
|
||||
@@ -38,19 +34,18 @@ static bool read_reg(uint8_t reg, uint8_t &val) {
|
||||
}
|
||||
|
||||
bool io_expander_init(void) {
|
||||
// 1. Configure direction: EXIO0..2 outputs, rest inputs.
|
||||
if (!write_reg(IOX_REG_CONFIG, IOX_CONFIG_MASK)) {
|
||||
Serial.println("XCA9554 init failed (config)");
|
||||
return false;
|
||||
}
|
||||
// 2. Drive all outputs LOW → hold display + touch in reset.
|
||||
// Hold display + touch in reset.
|
||||
output_state = 0x00;
|
||||
write_reg(IOX_REG_OUTPUT, output_state);
|
||||
delay(20);
|
||||
// 3. Release resets and enable audio amp output line.
|
||||
// Release resets and enable audio amp output line.
|
||||
output_state = IOX_OUTPUT_DEFAULT;
|
||||
write_reg(IOX_REG_OUTPUT, output_state);
|
||||
delay(20); // give SH8601 / FT3168 time to come out of reset
|
||||
delay(20);
|
||||
Serial.println("XCA9554 init OK");
|
||||
return true;
|
||||
}
|
||||
@@ -68,5 +63,3 @@ bool io_expander_get(uint8_t pin) {
|
||||
if (!read_reg(IOX_REG_INPUT, v)) return false;
|
||||
return (v & (1u << pin)) != 0;
|
||||
}
|
||||
|
||||
#endif // BOARD_AMOLED_18
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// XCA9554 / PCA9554-compatible 8-bit I2C IO expander.
|
||||
// Board-private to the AMOLED-1.8 port; not exposed in hal/.
|
||||
//
|
||||
// Must be initialized BEFORE the display or touch — skipping the reset
|
||||
// release leaves the SH8601 and FT3168 in reset and they fail to probe.
|
||||
|
||||
bool io_expander_init(void);
|
||||
void io_expander_set(uint8_t pin, bool high);
|
||||
bool io_expander_get(uint8_t pin);
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "../../hal/power_hal.h"
|
||||
#include "board.h"
|
||||
#include "io_expander.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <XPowersLib.h>
|
||||
|
||||
// PWR button comes from XCA9554 EXIO4 (active HIGH). The PMU still
|
||||
// provides battery monitoring; we just don't subscribe to its PKEY IRQ.
|
||||
|
||||
#define BATTERY_POLL_MS 2000
|
||||
#define CHARGING_POLL_MS 500
|
||||
#define PWR_POLL_MS 50
|
||||
|
||||
static XPowersPMU pmu;
|
||||
|
||||
static int cached_pct = -1;
|
||||
static bool cached_charging = false;
|
||||
static bool cached_vbus = false;
|
||||
static bool pwr_pressed_flag = false;
|
||||
static bool last_pwr_state = false; // edge detector for EXIO4
|
||||
static uint32_t last_battery_ms = 0;
|
||||
static uint32_t last_charging_ms = 0;
|
||||
static uint32_t last_pwr_ms = 0;
|
||||
|
||||
void power_hal_init(void) {
|
||||
if (!pmu.begin(Wire, AXP2101_ADDR, IIC_SDA, IIC_SCL)) {
|
||||
Serial.println("AXP2101 init failed");
|
||||
return;
|
||||
}
|
||||
Serial.println("AXP2101 init OK");
|
||||
|
||||
pmu.enableBattDetection();
|
||||
pmu.enableBattVoltageMeasure();
|
||||
// No PMU IRQ wiring — PWR comes via io_expander_get() below.
|
||||
|
||||
cached_charging = pmu.isCharging();
|
||||
cached_vbus = pmu.isVbusIn();
|
||||
cached_pct = pmu.getBatteryPercent();
|
||||
}
|
||||
|
||||
void power_hal_tick(void) {
|
||||
uint32_t now = millis();
|
||||
|
||||
if (now - last_charging_ms >= CHARGING_POLL_MS) {
|
||||
last_charging_ms = now;
|
||||
cached_charging = pmu.isCharging();
|
||||
cached_vbus = pmu.isVbusIn();
|
||||
}
|
||||
if (now - last_battery_ms >= BATTERY_POLL_MS) {
|
||||
last_battery_ms = now;
|
||||
cached_pct = pmu.getBatteryPercent();
|
||||
}
|
||||
if (now - last_pwr_ms >= PWR_POLL_MS) {
|
||||
last_pwr_ms = now;
|
||||
bool pwr_now = io_expander_get(IOX_PIN_PWR_BTN);
|
||||
if (pwr_now && !last_pwr_state) {
|
||||
pwr_pressed_flag = true;
|
||||
}
|
||||
last_pwr_state = pwr_now;
|
||||
}
|
||||
}
|
||||
|
||||
int power_hal_battery_pct(void) { return cached_pct; }
|
||||
bool power_hal_is_charging(void) { return cached_charging; }
|
||||
bool power_hal_is_vbus_in(void) { return cached_vbus; }
|
||||
|
||||
bool power_hal_pwr_pressed(void) {
|
||||
if (pwr_pressed_flag) {
|
||||
pwr_pressed_flag = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "../../hal/touch_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
// Minimal FT3168 reader (FocalTech standard register layout). Avoids
|
||||
// vendoring Waveshare's GPLv3 Arduino_DriveBus library.
|
||||
// reg 0x02: low nibble = active finger count
|
||||
// reg 0x03 / 0x04: X1 high (low nibble) + X1 low
|
||||
// reg 0x05 / 0x06: Y1 high (low nibble) + Y1 low
|
||||
|
||||
static volatile bool touch_data_ready = false;
|
||||
static volatile bool touch_pressed = false;
|
||||
static volatile uint16_t touch_x = 0;
|
||||
static volatile uint16_t touch_y = 0;
|
||||
|
||||
static void IRAM_ATTR touch_isr(void) {
|
||||
touch_data_ready = true;
|
||||
}
|
||||
|
||||
static void ft3168_read_into_shared_state(void) {
|
||||
Wire.beginTransmission(FT3168_ADDR);
|
||||
Wire.write(0x02);
|
||||
if (Wire.endTransmission(false) != 0) { touch_pressed = false; return; }
|
||||
if (Wire.requestFrom(FT3168_ADDR, (uint8_t)5) != 5) { touch_pressed = false; return; }
|
||||
uint8_t fingers = Wire.read() & 0x0F;
|
||||
uint8_t xH = Wire.read();
|
||||
uint8_t xL = Wire.read();
|
||||
uint8_t yH = Wire.read();
|
||||
uint8_t yL = Wire.read();
|
||||
if (fingers == 0 || fingers > 5) {
|
||||
touch_pressed = false;
|
||||
return;
|
||||
}
|
||||
touch_x = ((uint16_t)(xH & 0x0F) << 8) | xL;
|
||||
touch_y = ((uint16_t)(yH & 0x0F) << 8) | yL;
|
||||
touch_pressed = true;
|
||||
}
|
||||
|
||||
void touch_hal_init(void) {
|
||||
// Power-mode register 0xA5 = 0x00: active scanning.
|
||||
Wire.beginTransmission(FT3168_ADDR);
|
||||
Wire.write(0xA5);
|
||||
Wire.write(0x00);
|
||||
Wire.endTransmission();
|
||||
|
||||
// Verify device ID register 0xA0 (FT3168 reports 0x03 but Waveshare's
|
||||
// panel sometimes returns 0x86 — log but don't fail).
|
||||
Wire.beginTransmission(FT3168_ADDR);
|
||||
Wire.write(0xA0);
|
||||
if (Wire.endTransmission(false) == 0 && Wire.requestFrom(FT3168_ADDR, (uint8_t)1) == 1) {
|
||||
Serial.printf("FT3168 ID=0x%02X\n", Wire.read());
|
||||
} else {
|
||||
Serial.println("FT3168 ID read failed");
|
||||
}
|
||||
|
||||
pinMode(TP_INT, INPUT_PULLUP);
|
||||
attachInterrupt(TP_INT, touch_isr, FALLING);
|
||||
Serial.println("FT3168 attached on INT pin");
|
||||
}
|
||||
|
||||
void touch_hal_read(uint16_t* x, uint16_t* y, bool* pressed) {
|
||||
if (touch_data_ready) {
|
||||
touch_data_ready = false;
|
||||
ft3168_read_into_shared_state();
|
||||
}
|
||||
*x = touch_x;
|
||||
*y = touch_y;
|
||||
*pressed = touch_pressed;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
// Waveshare ESP32-S3-Touch-AMOLED-2.16 — original square AMOLED kit.
|
||||
// 480x480 CO5300 + CST9220 touch + AXP2101 PMU + QMI8658 IMU.
|
||||
// IMU-driven CPU rotation is enabled.
|
||||
|
||||
#define BOARD_NAME "Waveshare AMOLED 2.16"
|
||||
|
||||
// ---- Display geometry (matches BoardCaps; duplicated here as compile-time
|
||||
// constants because the buffer-size math runs at file scope) ----
|
||||
#define LCD_WIDTH 480
|
||||
#define LCD_HEIGHT 480
|
||||
|
||||
// ---- QSPI display pins (CO5300) ----
|
||||
#define LCD_CS 12
|
||||
#define LCD_SCLK 38
|
||||
#define LCD_SDIO0 4
|
||||
#define LCD_SDIO1 5
|
||||
#define LCD_SDIO2 6
|
||||
#define LCD_SDIO3 7
|
||||
#define LCD_RESET 2
|
||||
|
||||
// ---- I2C bus (touch + PMU + IMU) ----
|
||||
#define IIC_SDA 15
|
||||
#define IIC_SCL 14
|
||||
|
||||
// ---- Touch (CST9220 via TouchDrvCST92xx library) ----
|
||||
#define TP_INT 11
|
||||
#define TP_RST 2 // shared with LCD_RESET
|
||||
#define CST9220_ADDR 0x5A
|
||||
|
||||
// ---- PMU ----
|
||||
#define AXP2101_ADDR 0x34
|
||||
|
||||
// ---- Buttons ----
|
||||
#define BTN_BACK_GPIO 0 // BOOT — primary, Space (PTT)
|
||||
#define BTN_FWD_GPIO 18 // secondary, Shift+Tab (mode toggle)
|
||||
|
||||
// ---- Capability flags (compile-time; redundant with BoardCaps but lets
|
||||
// the linker dead-strip whole functions on boards that don't need them) ----
|
||||
#define BOARD_HAS_SECONDARY_BUTTON 1
|
||||
#define BOARD_HAS_ROTATION 1
|
||||
#define BOARD_HAS_IMU 1
|
||||
#define BOARD_HAS_BATTERY 1
|
||||
#define BOARD_HAS_IO_EXPANDER 0
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
// Bring up the shared I2C bus. AMOLED-2.16 has no IO expander, so this is
|
||||
// all the early init needed before display/touch/power/imu HAL calls.
|
||||
extern "C" void board_init(void) {
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#include "../../hal/board_caps.h"
|
||||
#include "board.h"
|
||||
|
||||
static const BoardCaps caps = {
|
||||
.name = BOARD_NAME,
|
||||
.width = LCD_WIDTH,
|
||||
.height = LCD_HEIGHT,
|
||||
.button_count = 2,
|
||||
.has_rotation = true,
|
||||
.has_battery = true,
|
||||
.has_imu = true,
|
||||
};
|
||||
|
||||
const BoardCaps& board_caps(void) { return caps; }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,54 +1,47 @@
|
||||
#include "imu.h"
|
||||
#include "display_cfg.h"
|
||||
#include "../../hal/imu_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <SensorQMI8658.hpp>
|
||||
|
||||
// Poll and hysteresis timing
|
||||
#define IMU_POLL_MS 100 // read accel at ~10 Hz
|
||||
#define STABLE_TIME_MS 300 // orientation must be stable this long before rotating
|
||||
#define TILT_THRESHOLD 0.5f // ~30 degrees from axis (sin(30) ~ 0.5)
|
||||
#define IMU_POLL_MS 100 // ~10 Hz
|
||||
#define STABLE_TIME_MS 300 // orientation must hold this long before rotating
|
||||
#define TILT_THRESHOLD 0.5f // ~30° from axis (sin 30° ≈ 0.5)
|
||||
|
||||
static uint8_t current_rotation = 0;
|
||||
static SensorQMI8658 imu;
|
||||
static uint8_t current_rotation = 0;
|
||||
static uint8_t candidate_rotation = 0;
|
||||
static uint32_t candidate_since = 0;
|
||||
static uint32_t last_poll_ms = 0;
|
||||
static bool imu_ok = false;
|
||||
static uint32_t candidate_since = 0;
|
||||
static uint32_t last_poll_ms = 0;
|
||||
static bool imu_ok = false;
|
||||
|
||||
// Determine target rotation from accelerometer gravity vector.
|
||||
// Returns 0-3 or 255 if ambiguous (e.g. face-up/face-down).
|
||||
static uint8_t accel_to_rotation(float ax, float ay) {
|
||||
float abs_ax = fabsf(ax);
|
||||
float abs_ay = fabsf(ay);
|
||||
|
||||
if (abs_ax < TILT_THRESHOLD && abs_ay < TILT_THRESHOLD) {
|
||||
return 255; // ambiguous, keep current
|
||||
}
|
||||
|
||||
if (abs_ay > abs_ax) {
|
||||
return (ay > 0) ? 3 : 1;
|
||||
} else {
|
||||
return (ax > 0) ? 0 : 2;
|
||||
return 255; // ambiguous (face-up/down)
|
||||
}
|
||||
if (abs_ay > abs_ax) return (ay > 0) ? 3 : 1;
|
||||
return (ax > 0) ? 0 : 2;
|
||||
}
|
||||
|
||||
void imu_init(void) {
|
||||
void imu_hal_init(void) {
|
||||
if (!imu.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {
|
||||
Serial.println("QMI8658 init failed");
|
||||
return;
|
||||
}
|
||||
Serial.println("QMI8658 init OK");
|
||||
|
||||
imu.configAccelerometer(
|
||||
SensorQMI8658::ACC_RANGE_4G,
|
||||
SensorQMI8658::ACC_ODR_LOWPOWER_21Hz,
|
||||
SensorQMI8658::LPF_MODE_3);
|
||||
imu.enableAccelerometer();
|
||||
|
||||
imu_ok = true;
|
||||
}
|
||||
|
||||
void imu_tick(void) {
|
||||
void imu_hal_tick(void) {
|
||||
if (!imu_ok) return;
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now - last_poll_ms < IMU_POLL_MS) return;
|
||||
last_poll_ms = now;
|
||||
@@ -61,7 +54,6 @@ void imu_tick(void) {
|
||||
candidate_rotation = current_rotation;
|
||||
return;
|
||||
}
|
||||
|
||||
if (target != candidate_rotation) {
|
||||
candidate_rotation = target;
|
||||
candidate_since = now;
|
||||
@@ -71,6 +63,4 @@ void imu_tick(void) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t imu_get_rotation(void) {
|
||||
return current_rotation;
|
||||
}
|
||||
uint8_t imu_hal_rotation_quadrant(void) { return current_rotation; }
|
||||
@@ -0,0 +1,18 @@
|
||||
#include "../../hal/input_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
void input_hal_init(void) {
|
||||
pinMode(BTN_BACK_GPIO, INPUT_PULLUP);
|
||||
pinMode(BTN_FWD_GPIO, INPUT_PULLUP);
|
||||
}
|
||||
|
||||
bool input_hal_is_held(InputButton btn) {
|
||||
switch (btn) {
|
||||
case INPUT_BTN_PRIMARY:
|
||||
return digitalRead(BTN_BACK_GPIO) == LOW;
|
||||
case INPUT_BTN_SECONDARY:
|
||||
return digitalRead(BTN_FWD_GPIO) == LOW;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,29 +1,26 @@
|
||||
#include "power.h"
|
||||
#include "display_cfg.h"
|
||||
#include "../../hal/power_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <XPowersLib.h>
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
#include "io_expander.h"
|
||||
#endif
|
||||
// PWR button comes from AXP2101 PKEY short-press IRQ.
|
||||
|
||||
// Poll intervals
|
||||
#define BATTERY_POLL_MS 2000
|
||||
#define CHARGING_POLL_MS 500
|
||||
#define BATTERY_POLL_MS 2000
|
||||
#define CHARGING_POLL_MS 500
|
||||
#define PWR_POLL_MS 50
|
||||
|
||||
static int cached_pct = -1;
|
||||
static bool cached_charging = false;
|
||||
static bool cached_vbus = false;
|
||||
static XPowersPMU pmu;
|
||||
|
||||
static int cached_pct = -1;
|
||||
static bool cached_charging = false;
|
||||
static bool cached_vbus = false;
|
||||
static bool pwr_pressed_flag = false;
|
||||
static uint32_t last_battery_ms = 0;
|
||||
static uint32_t last_charging_ms = 0;
|
||||
static uint32_t last_pwr_ms = 0;
|
||||
#define PWR_POLL_MS 50
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
static bool last_pwr_state = false; // edge detection for XCA9554 EXIO4
|
||||
#endif
|
||||
|
||||
void power_init(void) {
|
||||
void power_hal_init(void) {
|
||||
if (!pmu.begin(Wire, AXP2101_ADDR, IIC_SDA, IIC_SCL)) {
|
||||
Serial.println("AXP2101 init failed");
|
||||
return;
|
||||
@@ -33,21 +30,16 @@ void power_init(void) {
|
||||
pmu.enableBattDetection();
|
||||
pmu.enableBattVoltageMeasure();
|
||||
|
||||
#ifndef BOARD_AMOLED_18
|
||||
// AMOLED-2.16: PWR button events come from AXP2101 PKEY short-press IRQ.
|
||||
// AMOLED-1.8 routes the PWR button through XCA9554 EXIO4 instead — we
|
||||
// poll it in power_tick() rather than subscribing to the PMU IRQ.
|
||||
pmu.disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
|
||||
pmu.clearIrqStatus();
|
||||
pmu.enableIRQ(XPOWERS_AXP2101_PKEY_SHORT_IRQ);
|
||||
#endif
|
||||
|
||||
cached_charging = pmu.isCharging();
|
||||
cached_vbus = pmu.isVbusIn();
|
||||
cached_pct = pmu.getBatteryPercent();
|
||||
}
|
||||
|
||||
void power_tick(void) {
|
||||
void power_hal_tick(void) {
|
||||
uint32_t now = millis();
|
||||
|
||||
if (now - last_charging_ms >= CHARGING_POLL_MS) {
|
||||
@@ -55,45 +47,25 @@ void power_tick(void) {
|
||||
cached_charging = pmu.isCharging();
|
||||
cached_vbus = pmu.isVbusIn();
|
||||
}
|
||||
|
||||
if (now - last_battery_ms >= BATTERY_POLL_MS) {
|
||||
last_battery_ms = now;
|
||||
cached_pct = pmu.getBatteryPercent();
|
||||
}
|
||||
|
||||
// Poll PWR button
|
||||
if (now - last_pwr_ms >= PWR_POLL_MS) {
|
||||
last_pwr_ms = now;
|
||||
#ifdef BOARD_AMOLED_18
|
||||
// XCA9554 EXIO4 — active HIGH, edge-trigger on press
|
||||
bool pwr_now = io_expander_get(IOX_PIN_PWR_BTN);
|
||||
if (pwr_now && !last_pwr_state) {
|
||||
pwr_pressed_flag = true;
|
||||
}
|
||||
last_pwr_state = pwr_now;
|
||||
#else
|
||||
pmu.getIrqStatus();
|
||||
if (pmu.isPekeyShortPressIrq()) {
|
||||
pwr_pressed_flag = true;
|
||||
}
|
||||
pmu.clearIrqStatus();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
int power_battery_pct(void) {
|
||||
return cached_pct;
|
||||
}
|
||||
int power_hal_battery_pct(void) { return cached_pct; }
|
||||
bool power_hal_is_charging(void) { return cached_charging; }
|
||||
bool power_hal_is_vbus_in(void) { return cached_vbus; }
|
||||
|
||||
bool power_is_charging(void) {
|
||||
return cached_charging;
|
||||
}
|
||||
|
||||
bool power_is_vbus_in(void) {
|
||||
return cached_vbus;
|
||||
}
|
||||
|
||||
bool power_pwr_pressed(void) {
|
||||
bool power_hal_pwr_pressed(void) {
|
||||
if (pwr_pressed_flag) {
|
||||
pwr_pressed_flag = false;
|
||||
return true;
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "../../hal/touch_hal.h"
|
||||
#include "board.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <TouchDrvCSTXXX.hpp>
|
||||
|
||||
static TouchDrvCST92xx touch;
|
||||
|
||||
static volatile bool touch_data_ready = false;
|
||||
static volatile bool touch_pressed = false;
|
||||
static volatile uint16_t touch_x = 0;
|
||||
static volatile uint16_t touch_y = 0;
|
||||
|
||||
static void IRAM_ATTR touch_isr(void) {
|
||||
touch_data_ready = true;
|
||||
}
|
||||
|
||||
void touch_hal_init(void) {
|
||||
touch.setPins(TP_RST, TP_INT);
|
||||
if (!touch.begin(Wire, CST9220_ADDR, IIC_SDA, IIC_SCL)) {
|
||||
Serial.println("Touch init failed");
|
||||
return;
|
||||
}
|
||||
touch.setMaxCoordinates(LCD_WIDTH, LCD_HEIGHT);
|
||||
touch.setSwapXY(true);
|
||||
touch.setMirrorXY(true, false);
|
||||
pinMode(TP_INT, INPUT_PULLUP);
|
||||
attachInterrupt(TP_INT, touch_isr, FALLING);
|
||||
Serial.println("Touch init OK");
|
||||
}
|
||||
|
||||
void touch_hal_read(uint16_t* x, uint16_t* y, bool* pressed) {
|
||||
if (touch_data_ready) {
|
||||
touch_data_ready = false;
|
||||
int16_t tx[5], ty[5];
|
||||
uint8_t n = touch.getPoint(tx, ty, touch.getSupportTouchPoint());
|
||||
if (n > 0) {
|
||||
touch_pressed = true;
|
||||
touch_x = (uint16_t)tx[0];
|
||||
touch_y = (uint16_t)ty[0];
|
||||
} else {
|
||||
touch_pressed = false;
|
||||
}
|
||||
}
|
||||
*x = touch_x;
|
||||
*y = touch_y;
|
||||
*pressed = touch_pressed;
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino_GFX_Library.h>
|
||||
#include <XPowersLib.h>
|
||||
#include <SensorQMI8658.hpp>
|
||||
#include <Wire.h>
|
||||
|
||||
// ============================================================================
|
||||
// Board variant selection — driven by build flag in platformio.ini
|
||||
// -DBOARD_AMOLED_216 → original Waveshare ESP32-S3-Touch-AMOLED-2.16 (CO5300, CST9220, 480x480)
|
||||
// -DBOARD_AMOLED_18 → newer Waveshare ESP32-S3-Touch-AMOLED-1.8 (SH8601, FT3168, 368x448)
|
||||
// ============================================================================
|
||||
|
||||
#if defined(BOARD_AMOLED_18)
|
||||
|
||||
// ---- Display resolution (portrait) ----
|
||||
#define LCD_WIDTH 368
|
||||
#define LCD_HEIGHT 448
|
||||
|
||||
// ---- QSPI display pins (SH8601) ----
|
||||
#define LCD_CS 12
|
||||
#define LCD_SCLK 11 // NOTE: different from AMOLED-2.16 (was GPIO 38)
|
||||
#define LCD_SDIO0 4
|
||||
#define LCD_SDIO1 5
|
||||
#define LCD_SDIO2 6
|
||||
#define LCD_SDIO3 7
|
||||
#define LCD_RESET GFX_NOT_DEFINED // routed via XCA9554 EXIO1
|
||||
|
||||
// ---- I2C bus (touch + PMU + IMU + IO expander all share one bus) ----
|
||||
#define IIC_SDA 15
|
||||
#define IIC_SCL 14
|
||||
|
||||
// ---- Touch (FT3168 via I2C) ----
|
||||
#define TP_INT 21
|
||||
#define FT3168_ADDR 0x38
|
||||
|
||||
// ---- PMU (AXP2101 via I2C) ----
|
||||
#define AXP2101_ADDR 0x34
|
||||
|
||||
// ---- IO expander (XCA9554/PCA9554-compatible via I2C) ----
|
||||
// Gates LCD_RST, TP_RST, audio amp reset, and PWR button readback.
|
||||
#define XCA9554_ADDR 0x20
|
||||
#define IOX_PIN_TP_RST 0 // EXIO0 → touch reset (active LOW)
|
||||
#define IOX_PIN_LCD_RST 1 // EXIO1 → display reset (active LOW)
|
||||
#define IOX_PIN_PA_EN 2 // EXIO2 → audio amp enable (we keep HIGH to release)
|
||||
#define IOX_PIN_PWR_BTN 4 // EXIO4 → PWR button input, active HIGH
|
||||
|
||||
// ---- Display class typedef ----
|
||||
typedef Arduino_SH8601 PlatformDisplay;
|
||||
|
||||
// ---- Global hardware objects (defined in main.cpp) ----
|
||||
extern Arduino_DataBus *bus;
|
||||
extern PlatformDisplay *gfx;
|
||||
extern XPowersPMU pmu;
|
||||
extern SensorQMI8658 imu;
|
||||
|
||||
#else // BOARD_AMOLED_216 (default / original board)
|
||||
|
||||
#include <TouchDrvCSTXXX.hpp>
|
||||
|
||||
// ---- Display resolution ----
|
||||
#define LCD_WIDTH 480
|
||||
#define LCD_HEIGHT 480
|
||||
|
||||
// ---- QSPI display pins (CO5300) ----
|
||||
#define LCD_CS 12
|
||||
#define LCD_SCLK 38
|
||||
#define LCD_SDIO0 4
|
||||
#define LCD_SDIO1 5
|
||||
#define LCD_SDIO2 6
|
||||
#define LCD_SDIO3 7
|
||||
#define LCD_RESET 2
|
||||
|
||||
// ---- Touch pins (CST9220 via I2C) ----
|
||||
#define IIC_SDA 15
|
||||
#define IIC_SCL 14
|
||||
#define TP_INT 11
|
||||
#define TP_RST 2 // shared with LCD_RESET
|
||||
#define CST9220_ADDR 0x5A
|
||||
|
||||
// ---- PMU (AXP2101 via same I2C) ----
|
||||
#define AXP2101_ADDR 0x34
|
||||
|
||||
// ---- Display class typedef ----
|
||||
typedef Arduino_CO5300 PlatformDisplay;
|
||||
|
||||
// ---- Global hardware objects (defined in main.cpp) ----
|
||||
extern Arduino_DataBus *bus;
|
||||
extern PlatformDisplay *gfx;
|
||||
extern TouchDrvCST92xx touch;
|
||||
extern XPowersPMU pmu;
|
||||
extern SensorQMI8658 imu;
|
||||
|
||||
#endif // BOARD_AMOLED_*
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1,8 +1,8 @@
|
||||
#include <Arduino.h>
|
||||
#include "idle.h"
|
||||
#include "idle_cfg.h"
|
||||
#include "display_cfg.h" // declares `extern Arduino_CO5300 *gfx;`
|
||||
#include "power.h"
|
||||
#include "hal/display_hal.h"
|
||||
#include "hal/power_hal.h"
|
||||
|
||||
enum IdleState {
|
||||
STATE_AWAKE,
|
||||
@@ -19,7 +19,7 @@ static uint8_t fade_from = DISPLAY_DEFAULT_BRIGHTNESS;
|
||||
static uint8_t fade_to = 0;
|
||||
|
||||
static void apply_brightness(uint8_t b) {
|
||||
gfx->setBrightness(b);
|
||||
display_hal_set_brightness(b);
|
||||
}
|
||||
|
||||
static void begin_fade(uint8_t to, uint32_t now) {
|
||||
@@ -72,7 +72,7 @@ void idle_tick(void) {
|
||||
|
||||
// While on USB power (if configured), don't sleep — and wake from sleep
|
||||
// when power comes back. Treats USB-in as continuous activity.
|
||||
if (!IDLE_SLEEP_WHEN_CHARGING && power_is_vbus_in()) {
|
||||
if (!IDLE_SLEEP_WHEN_CHARGING && power_hal_is_vbus_in()) {
|
||||
last_activity_ms = now;
|
||||
if (state == STATE_ASLEEP || state == STATE_FADING_OUT) {
|
||||
begin_fade(DISPLAY_DEFAULT_BRIGHTNESS, now);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
void imu_init(void);
|
||||
void imu_tick(void); // call from loop(), handles auto-rotation
|
||||
uint8_t imu_get_rotation(void); // current rotation 0-3
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// XCA9554 / PCA9554-compatible 8-bit I2C IO expander @ 0x20.
|
||||
// Only compiled for BOARD_AMOLED_18 (the AMOLED-1.8 board routes LCD_RST,
|
||||
// TP_RST, audio amp enable, and the PWR button through this expander).
|
||||
//
|
||||
// Must be initialized BEFORE the display or touch — skipping the reset
|
||||
// release leaves the SH8601 and FT3168 in reset and they will fail to probe.
|
||||
|
||||
bool io_expander_init(void);
|
||||
|
||||
// Drive an EXIO pin (one of IOX_PIN_* in display_cfg.h that is configured
|
||||
// as output). Updates the cached output register.
|
||||
void io_expander_set(uint8_t pin, bool high);
|
||||
|
||||
// Read an EXIO pin configured as input (e.g. PWR button on EXIO4).
|
||||
bool io_expander_get(uint8_t pin);
|
||||
+124
-392
@@ -2,267 +2,91 @@
|
||||
#include <Wire.h>
|
||||
#include <lvgl.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "display_cfg.h"
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
#include "data.h"
|
||||
#include "ui.h"
|
||||
#include "ble.h"
|
||||
#include "power.h"
|
||||
#include "imu.h"
|
||||
#include "splash.h"
|
||||
#include "usage_rate.h"
|
||||
#include "idle.h"
|
||||
#include "idle_cfg.h"
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
#include "io_expander.h"
|
||||
#endif
|
||||
|
||||
// Physical buttons (global, screen-independent):
|
||||
// BTN_BACK (GPIO 0, BOOT) — left, send Space (Claude Code voice-mode PTT)
|
||||
// BTN_FWD (GPIO 18) — AMOLED-2.16 only: Shift+Tab (mode toggle)
|
||||
// PWR — middle, cycle screens; on splash, cycle animations
|
||||
// AMOLED-2.16: AXP2101 PKEY IRQ
|
||||
// AMOLED-1.8 : XCA9554 EXIO4 (polled over I2C)
|
||||
#define BTN_BACK 0
|
||||
#ifndef BOARD_AMOLED_18
|
||||
#define BTN_FWD 18
|
||||
#endif
|
||||
|
||||
// ---- Hardware objects ----
|
||||
Arduino_DataBus *bus = new Arduino_ESP32QSPI(
|
||||
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
#ifdef BOARD_AMOLED_18
|
||||
// SH8601 constructor: (bus, rst, rotation, w, h)
|
||||
PlatformDisplay *gfx = new PlatformDisplay(
|
||||
bus, LCD_RESET /* GFX_NOT_DEFINED — reset via XCA9554 */, 0,
|
||||
LCD_WIDTH, LCD_HEIGHT);
|
||||
#else
|
||||
// CO5300 constructor: (bus, rst, rotation, w, h, col_offset1..2, row_offset1..2)
|
||||
PlatformDisplay *gfx = new PlatformDisplay(
|
||||
bus, LCD_RESET, 0 /* rotation */,
|
||||
LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
|
||||
TouchDrvCST92xx touch;
|
||||
#endif
|
||||
XPowersPMU pmu;
|
||||
SensorQMI8658 imu;
|
||||
#include "hal/board_caps.h"
|
||||
#include "hal/display_hal.h"
|
||||
#include "hal/touch_hal.h"
|
||||
#include "hal/input_hal.h"
|
||||
#include "hal/power_hal.h"
|
||||
#include "hal/imu_hal.h"
|
||||
|
||||
static UsageData usage = {};
|
||||
|
||||
// ---- Touch interrupt + shared state ----
|
||||
// Centralized once-per-loop read (CLAUDE.md gotcha #5): calling getPoint() /
|
||||
// reading FT3168 from multiple sites consumes each other's data.
|
||||
static volatile bool touch_pressed = false;
|
||||
static volatile uint16_t touch_x = 0;
|
||||
static volatile uint16_t touch_y = 0;
|
||||
static volatile bool touch_data_ready = false;
|
||||
|
||||
static void IRAM_ATTR touch_isr(void) {
|
||||
touch_data_ready = true;
|
||||
}
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
// Minimal FT3168 reader (FocalTech standard register layout).
|
||||
// Avoids vendoring Waveshare's GPLv3 Arduino_DriveBus library.
|
||||
// reg 0x02: low nibble = active finger count
|
||||
// reg 0x03/0x04: X1 high (low nibble), X1 low
|
||||
// reg 0x05/0x06: Y1 high (low nibble), Y1 low
|
||||
static void ft3168_init(void) {
|
||||
// Power-mode register 0xA5 = 0x00: active scanning.
|
||||
Wire.beginTransmission(FT3168_ADDR);
|
||||
Wire.write(0xA5);
|
||||
Wire.write(0x00);
|
||||
Wire.endTransmission();
|
||||
// Verify device ID register 0xA0 (FT3168 reports 0x03 but Waveshare's
|
||||
// panel sometimes returns 0x86 — log but don't fail).
|
||||
Wire.beginTransmission(FT3168_ADDR);
|
||||
Wire.write(0xA0);
|
||||
if (Wire.endTransmission(false) == 0 && Wire.requestFrom(FT3168_ADDR, (uint8_t)1) == 1) {
|
||||
Serial.printf("FT3168 ID=0x%02X\n", Wire.read());
|
||||
} else {
|
||||
Serial.println("FT3168 ID read failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void ft3168_read_into_shared_state(void) {
|
||||
Wire.beginTransmission(FT3168_ADDR);
|
||||
Wire.write(0x02);
|
||||
if (Wire.endTransmission(false) != 0) { touch_pressed = false; return; }
|
||||
if (Wire.requestFrom(FT3168_ADDR, (uint8_t)5) != 5) { touch_pressed = false; return; }
|
||||
uint8_t fingers = Wire.read() & 0x0F;
|
||||
uint8_t xH = Wire.read();
|
||||
uint8_t xL = Wire.read();
|
||||
uint8_t yH = Wire.read();
|
||||
uint8_t yL = Wire.read();
|
||||
if (fingers == 0 || fingers > 5) {
|
||||
touch_pressed = false;
|
||||
return;
|
||||
}
|
||||
touch_x = ((uint16_t)(xH & 0x0F) << 8) | xL;
|
||||
touch_y = ((uint16_t)(yH & 0x0F) << 8) | yL;
|
||||
touch_pressed = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void touch_read() {
|
||||
if (!touch_data_ready) return;
|
||||
touch_data_ready = false;
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
ft3168_read_into_shared_state();
|
||||
#else
|
||||
int16_t tx[5], ty[5];
|
||||
uint8_t n = touch.getPoint(tx, ty, touch.getSupportTouchPoint());
|
||||
if (n > 0) {
|
||||
touch_pressed = true;
|
||||
touch_x = (uint16_t)tx[0];
|
||||
touch_y = (uint16_t)ty[0];
|
||||
} else {
|
||||
touch_pressed = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Touch policy is driven by IDLE_WAKE_ON_TOUCH:
|
||||
// true → a press edge while asleep wakes the device and the first
|
||||
// touch is swallowed (mirrors the button wake-consumption); a
|
||||
// press while awake counts as activity.
|
||||
// false → touch never counts as activity and is fully swallowed while
|
||||
// the panel is dark, so pets/sleeves can't wake it overnight
|
||||
// and LVGL can't quietly toggle splash<->usage on a black panel.
|
||||
if (IDLE_WAKE_ON_TOUCH) {
|
||||
static bool touch_was = false;
|
||||
static bool touch_wake_swallowed = false;
|
||||
bool touch_now = touch_pressed;
|
||||
if (touch_now && !touch_was) {
|
||||
if (idle_consume_wake_press()) {
|
||||
touch_wake_swallowed = true;
|
||||
touch_pressed = false; // hide this press from LVGL
|
||||
}
|
||||
} else if (!touch_now && touch_was) {
|
||||
if (touch_wake_swallowed) {
|
||||
touch_wake_swallowed = false;
|
||||
touch_pressed = false; // also hide the corresponding release
|
||||
}
|
||||
} else if (touch_now && touch_wake_swallowed) {
|
||||
// Held finger through wake — keep hiding until release.
|
||||
touch_pressed = false;
|
||||
}
|
||||
touch_was = touch_now;
|
||||
} else {
|
||||
if (idle_is_asleep()) touch_pressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- LVGL draw buffers (PSRAM-backed, partial render) ----
|
||||
// ---- LVGL draw buffers (PSRAM, partial render mode) ----
|
||||
#define BUF_LINES 40
|
||||
static uint16_t *buf1 = nullptr;
|
||||
static uint16_t *buf2 = nullptr;
|
||||
// rot_buf for strip rotation — max size is 480×480 (full invalidation case)
|
||||
// but typical partial strips are much smaller
|
||||
static uint16_t *rot_buf = nullptr;
|
||||
static uint16_t* buf1 = nullptr;
|
||||
static uint16_t* buf2 = nullptr;
|
||||
|
||||
// LVGL tick callback
|
||||
static uint32_t my_tick(void) {
|
||||
return millis();
|
||||
}
|
||||
static uint32_t my_tick(void) { return millis(); }
|
||||
|
||||
#ifndef BOARD_AMOLED_18
|
||||
// Rotate a w×h strip and compute destination coordinates on the 480×480 display.
|
||||
// src pixels are in row-major order for the rectangle (sx, sy, w, h).
|
||||
// Output goes to rot_buf in row-major order for the destination rectangle.
|
||||
// AMOLED-1.8 port is fixed at 0° so this code is excluded.
|
||||
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; // 480
|
||||
|
||||
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++) {
|
||||
// src(x,y) -> dst(h-1-y, 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++) {
|
||||
// src(x,y) -> dst(y, w-1-x)
|
||||
rot_buf[(w - 1 - x) * h + y] = src[y * w + x];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
*dx = sx; *dy = sy; *dw = w; *dh = h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif // !BOARD_AMOLED_18
|
||||
|
||||
// LVGL flush callback — writes pixels to display.
|
||||
// AMOLED-2.16: applies CPU strip rotation based on IMU.
|
||||
// AMOLED-1.8 : fixed orientation, direct pass-through.
|
||||
static void my_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
|
||||
int32_t w = area->x2 - area->x1 + 1;
|
||||
int32_t h = area->y2 - area->y1 + 1;
|
||||
uint16_t *src = (uint16_t*)px_map;
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
gfx->draw16bitRGBBitmap(area->x1, area->y1, src, w, h);
|
||||
#else
|
||||
uint8_t r = imu_get_rotation();
|
||||
if (r == 0) {
|
||||
gfx->draw16bitRGBBitmap(area->x1, area->y1, src, w, h);
|
||||
} else {
|
||||
int32_t dx, dy, dw, dh;
|
||||
rotate_strip(src, w, h, area->x1, area->y1, r, &dx, &dy, &dw, &dh);
|
||||
gfx->draw16bitRGBBitmap(dx, dy, rot_buf, dw, dh);
|
||||
}
|
||||
#endif
|
||||
display_hal_draw_bitmap(area->x1, area->y1, w, h, (uint16_t*)px_map);
|
||||
lv_display_flush_ready(disp);
|
||||
}
|
||||
|
||||
// CO5300 requires even-aligned flush regions. SH8601's driver doesn't
|
||||
// enforce this in source, but keeping the rounder is harmless.
|
||||
static void rounder_cb(lv_event_t* e) {
|
||||
lv_area_t *area = (lv_area_t*)lv_event_get_param(e);
|
||||
area->x1 = area->x1 & ~1;
|
||||
area->y1 = area->y1 & ~1;
|
||||
area->x2 = area->x2 | 1;
|
||||
area->y2 = area->y2 | 1;
|
||||
lv_area_t* area = (lv_area_t*)lv_event_get_param(e);
|
||||
display_hal_round_area(&area->x1, &area->y1, &area->x2, &area->y2);
|
||||
}
|
||||
|
||||
// LVGL touch callback
|
||||
// Touch policy is driven by IDLE_WAKE_ON_TOUCH:
|
||||
// true → a press edge while asleep wakes the device and the first touch is
|
||||
// swallowed (mirrors the button wake-consumption); a press while
|
||||
// awake counts as activity.
|
||||
// false → touch never counts as activity and is fully swallowed while the
|
||||
// panel is dark, so pets/sleeves can't wake it overnight and LVGL
|
||||
// can't quietly toggle splash<->usage on a black panel.
|
||||
static void my_touch_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
if (touch_pressed) {
|
||||
data->point.x = touch_x;
|
||||
data->point.y = touch_y;
|
||||
uint16_t x, y;
|
||||
bool pressed;
|
||||
touch_hal_read(&x, &y, &pressed);
|
||||
const bool raw_pressed = pressed;
|
||||
|
||||
if (IDLE_WAKE_ON_TOUCH) {
|
||||
static bool touch_was = false;
|
||||
static bool touch_wake_swallowed = false;
|
||||
if (raw_pressed && !touch_was) {
|
||||
// Press edge — consume as wake if asleep.
|
||||
if (idle_consume_wake_press()) {
|
||||
touch_wake_swallowed = true;
|
||||
pressed = false;
|
||||
}
|
||||
} else if (!raw_pressed && touch_was) {
|
||||
// Release edge.
|
||||
if (touch_wake_swallowed) {
|
||||
touch_wake_swallowed = false;
|
||||
pressed = false;
|
||||
}
|
||||
} else if (raw_pressed && touch_wake_swallowed) {
|
||||
// Held finger through wake — keep hiding until release.
|
||||
pressed = false;
|
||||
}
|
||||
touch_was = raw_pressed;
|
||||
} else if (idle_is_asleep()) {
|
||||
pressed = false;
|
||||
}
|
||||
|
||||
if (pressed) {
|
||||
data->point.x = x;
|
||||
data->point.y = y;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a JSON line into UsageData
|
||||
// Parse a JSON line into UsageData.
|
||||
static bool parse_json(const char* json, UsageData* out) {
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, json);
|
||||
@@ -281,13 +105,14 @@ static bool parse_json(const char* json, UsageData* out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Serial command buffer
|
||||
// ---- Serial command buffer ----
|
||||
#define CMD_BUF_SIZE 64
|
||||
static char cmd_buf[CMD_BUF_SIZE];
|
||||
static int cmd_pos = 0;
|
||||
|
||||
static void send_screenshot() {
|
||||
const uint32_t w = LCD_WIDTH, h = LCD_HEIGHT;
|
||||
const uint32_t w = board_caps().width;
|
||||
const uint32_t h = board_caps().height;
|
||||
const uint32_t row_bytes = w * 2;
|
||||
const uint32_t buf_size = row_bytes * h;
|
||||
uint8_t* sbuf = (uint8_t*)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
|
||||
@@ -306,13 +131,13 @@ static void send_screenshot() {
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("SCREENSHOT_START %lu %lu %lu\n", (unsigned long)w, (unsigned long)h, (unsigned long)buf_size);
|
||||
Serial.printf("SCREENSHOT_START %lu %lu %lu\n",
|
||||
(unsigned long)w, (unsigned long)h, (unsigned long)buf_size);
|
||||
Serial.flush();
|
||||
Serial.write(sbuf, buf_size);
|
||||
Serial.flush();
|
||||
Serial.println();
|
||||
Serial.println("SCREENSHOT_END");
|
||||
|
||||
heap_caps_free(sbuf);
|
||||
}
|
||||
|
||||
@@ -321,9 +146,7 @@ static void check_serial_cmd() {
|
||||
char c = Serial.read();
|
||||
if (c == '\n' || c == '\r') {
|
||||
cmd_buf[cmd_pos] = '\0';
|
||||
if (strcmp(cmd_buf, "screenshot") == 0) {
|
||||
send_screenshot();
|
||||
}
|
||||
if (strcmp(cmd_buf, "screenshot") == 0) send_screenshot();
|
||||
cmd_pos = 0;
|
||||
} else if (cmd_pos < CMD_BUF_SIZE - 1) {
|
||||
cmd_buf[cmd_pos++] = c;
|
||||
@@ -331,198 +154,115 @@ static void check_serial_cmd() {
|
||||
}
|
||||
}
|
||||
|
||||
// Each board provides this. Must bring up the shared I2C bus (Wire.begin
|
||||
// with the board's SDA/SCL pins) and any board-private hardware that has
|
||||
// to settle before display/touch (e.g. an IO expander gating the LCD
|
||||
// reset line). Called exactly once at the start of setup().
|
||||
extern "C" void board_init(void);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(300);
|
||||
Serial.println("{\"ready\":true}");
|
||||
|
||||
// Init I2C (shared by touch + PMU + IMU + IO expander)
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
board_init();
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
// XCA9554 must come up FIRST — display + touch are held in reset until
|
||||
// EXIO0..2 go HIGH (see io_expander_init()).
|
||||
io_expander_init();
|
||||
#endif
|
||||
display_hal_init();
|
||||
display_hal_begin();
|
||||
idle_init(); // takes over brightness (DISPLAY_DEFAULT_BRIGHTNESS) and starts the idle timer
|
||||
|
||||
// Init display
|
||||
gfx->begin();
|
||||
gfx->fillScreen(0x0000);
|
||||
idle_init(); // sets brightness to DISPLAY_DEFAULT_BRIGHTNESS and starts idle timer
|
||||
power_hal_init();
|
||||
imu_hal_init();
|
||||
touch_hal_init();
|
||||
|
||||
// Init PMU
|
||||
power_init();
|
||||
// ---- LVGL ----
|
||||
const int W = board_caps().width;
|
||||
const int H = board_caps().height;
|
||||
|
||||
// Init IMU (accelerometer for auto-rotation; on AMOLED-1.8 we keep init
|
||||
// for I2C bus health but ignore rotation — see imu.cpp).
|
||||
imu_init();
|
||||
|
||||
// Init touch
|
||||
#ifdef BOARD_AMOLED_18
|
||||
ft3168_init();
|
||||
pinMode(TP_INT, INPUT_PULLUP);
|
||||
attachInterrupt(TP_INT, touch_isr, FALLING);
|
||||
Serial.println("FT3168 attached on INT pin");
|
||||
#else
|
||||
touch.setPins(TP_RST, TP_INT);
|
||||
if (!touch.begin(Wire, CST9220_ADDR, IIC_SDA, IIC_SCL)) {
|
||||
Serial.println("Touch init failed");
|
||||
} else {
|
||||
touch.setMaxCoordinates(LCD_WIDTH, LCD_HEIGHT);
|
||||
touch.setSwapXY(true);
|
||||
touch.setMirrorXY(true, false);
|
||||
attachInterrupt(TP_INT, touch_isr, FALLING);
|
||||
Serial.println("Touch init OK");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Init LVGL
|
||||
lv_init();
|
||||
lv_tick_set_cb(my_tick);
|
||||
|
||||
// Allocate PSRAM-backed partial render buffers
|
||||
buf1 = (uint16_t*)heap_caps_malloc(LCD_WIDTH * BUF_LINES * 2, MALLOC_CAP_SPIRAM);
|
||||
buf2 = (uint16_t*)heap_caps_malloc(LCD_WIDTH * BUF_LINES * 2, MALLOC_CAP_SPIRAM);
|
||||
#ifndef BOARD_AMOLED_18
|
||||
// rot_buf only needed for AMOLED-2.16 (CPU strip rotation).
|
||||
// Holds the largest possible strip after rotation (same pixel count as src).
|
||||
rot_buf = (uint16_t*)heap_caps_malloc(LCD_WIDTH * BUF_LINES * 2, MALLOC_CAP_SPIRAM);
|
||||
#endif
|
||||
buf1 = (uint16_t*)heap_caps_malloc(W * BUF_LINES * 2, MALLOC_CAP_SPIRAM);
|
||||
buf2 = (uint16_t*)heap_caps_malloc(W * BUF_LINES * 2, MALLOC_CAP_SPIRAM);
|
||||
|
||||
lv_display_t* disp = lv_display_create(LCD_WIDTH, LCD_HEIGHT);
|
||||
lv_display_t* disp = lv_display_create(W, H);
|
||||
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
|
||||
lv_display_set_flush_cb(disp, my_flush_cb);
|
||||
lv_display_set_buffers(disp, buf1, buf2, LCD_WIDTH * BUF_LINES * 2,
|
||||
lv_display_set_buffers(disp, buf1, buf2, W * BUF_LINES * 2,
|
||||
LV_DISPLAY_RENDER_MODE_PARTIAL);
|
||||
|
||||
// CO5300 even-alignment rounder
|
||||
lv_display_add_event_cb(disp, rounder_cb, LV_EVENT_INVALIDATE_AREA, NULL);
|
||||
|
||||
lv_indev_t* indev = lv_indev_create();
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
||||
lv_indev_set_read_cb(indev, my_touch_cb);
|
||||
|
||||
// Init BLE data channel
|
||||
ble_init();
|
||||
input_hal_init();
|
||||
|
||||
// Physical buttons
|
||||
pinMode(BTN_BACK, INPUT_PULLUP);
|
||||
#ifndef BOARD_AMOLED_18
|
||||
pinMode(BTN_FWD, INPUT_PULLUP);
|
||||
#endif
|
||||
|
||||
// Build dashboard
|
||||
ui_init();
|
||||
|
||||
// Show initial BLE status on Bluetooth screen
|
||||
ui_update_ble_status(ble_get_state(), ble_get_device_name(), ble_get_mac_address());
|
||||
|
||||
// Show initial battery status
|
||||
ui_update_battery(power_battery_pct(), power_is_charging());
|
||||
|
||||
ui_update_battery(power_hal_battery_pct(), power_hal_is_charging());
|
||||
ui_show_screen(SCREEN_SPLASH);
|
||||
|
||||
Serial.println("Dashboard ready, waiting for data on BLE...");
|
||||
Serial.printf("Dashboard ready (%s, %dx%d), waiting for data on BLE...\n",
|
||||
board_caps().name, W, H);
|
||||
}
|
||||
|
||||
static ble_state_t last_ble_state = BLE_STATE_INIT;
|
||||
|
||||
#ifndef BOARD_AMOLED_18
|
||||
// Brightness ramp state for rotation transition.
|
||||
// AMOLED-2.16 only — the 1.8" port is fixed at 0° (no IMU rotation).
|
||||
// On rotation change we 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 instead of as a glitch.
|
||||
static void handle_rotation_change(void) {
|
||||
static uint8_t last_rotation = 0;
|
||||
static uint8_t ramp_step = 0; // 0=idle, 1-4=ramping
|
||||
static uint32_t ramp_last = 0;
|
||||
|
||||
// While asleep the rotation visual transition (blank + ramp) would fight
|
||||
// the idle fade. Defer: a rotation that happens during sleep will be
|
||||
// detected after wake and ramped in then.
|
||||
if (idle_is_asleep()) return;
|
||||
|
||||
uint8_t rot = imu_get_rotation();
|
||||
if (rot != last_rotation) {
|
||||
gfx->setBrightness(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, DISPLAY_DEFAULT_BRIGHTNESS};
|
||||
gfx->setBrightness(levels[ramp_step - 1]);
|
||||
if (ramp_step >= 4) ramp_step = 0;
|
||||
else ramp_step++;
|
||||
}
|
||||
#endif // !BOARD_AMOLED_18
|
||||
|
||||
void loop() {
|
||||
touch_read();
|
||||
idle_tick();
|
||||
lv_timer_handler();
|
||||
ui_tick_anim();
|
||||
ble_tick();
|
||||
power_tick();
|
||||
imu_tick();
|
||||
power_hal_tick();
|
||||
imu_hal_tick();
|
||||
splash_tick();
|
||||
// Rotation transition (blank + ramp) would fight the idle fade — skip
|
||||
// ticks while the panel is dark. A rotation that happens during sleep
|
||||
// is detected by the next tick after wake and ramped in then.
|
||||
if (!idle_is_asleep()) display_hal_tick();
|
||||
|
||||
// Physical button input (global, screen-independent):
|
||||
// LEFT (GPIO 0 / BOOT) → Space (voice-mode push-to-talk)
|
||||
// RIGHT (GPIO 18) → Shift+Tab (Claude Code mode toggle). AMOLED-2.16 only.
|
||||
// PWR → cycle screens; on splash, cycle animations.
|
||||
// AMOLED-2.16: AXP2101 PKEY IRQ; AMOLED-1.8: XCA9554 EXIO4.
|
||||
// First press from sleep is consumed for wake only (idle_consume_wake_press
|
||||
// returns true) — the normal action only fires from the second press.
|
||||
// Activity bookkeeping happens inside idle_consume_wake_press, so no
|
||||
// separate idle_note_activity() call is needed here.
|
||||
// ---- Physical buttons ----
|
||||
// PRIMARY → HID Space (Claude Code voice-mode PTT)
|
||||
// SECONDARY → HID Shift+Tab (mode toggle; only if the board has one)
|
||||
// PWR → cycle screens; on splash, cycle animations
|
||||
// First press from sleep is consumed as a wake-only event by
|
||||
// idle_consume_wake_press(); the normal action fires from the second
|
||||
// press. Activity bookkeeping happens inside idle_consume_wake_press
|
||||
// so no separate idle_note_activity() call is needed here.
|
||||
{
|
||||
static bool back_was = false;
|
||||
static bool back_wake_swallowed = false;
|
||||
#ifndef BOARD_AMOLED_18
|
||||
static bool fwd_was = false;
|
||||
static bool fwd_wake_swallowed = false;
|
||||
#endif
|
||||
bool back_now = (digitalRead(BTN_BACK) == LOW);
|
||||
if (back_now != back_was) {
|
||||
if (back_now) {
|
||||
if (idle_consume_wake_press()) {
|
||||
back_wake_swallowed = true;
|
||||
} else {
|
||||
ble_keyboard_press(0x2C, 0); // HID Space, no mods
|
||||
}
|
||||
static bool primary_was = false;
|
||||
static bool primary_wake_swallowed = false;
|
||||
bool primary_now = input_hal_is_held(INPUT_BTN_PRIMARY);
|
||||
if (primary_now != primary_was) {
|
||||
if (primary_now) {
|
||||
if (idle_consume_wake_press()) primary_wake_swallowed = true;
|
||||
else ble_keyboard_press(0x2C, 0); // HID Space, no mods
|
||||
} else {
|
||||
if (back_wake_swallowed) back_wake_swallowed = false;
|
||||
else ble_keyboard_release();
|
||||
if (primary_wake_swallowed) primary_wake_swallowed = false;
|
||||
else ble_keyboard_release();
|
||||
}
|
||||
back_was = back_now;
|
||||
primary_was = primary_now;
|
||||
}
|
||||
|
||||
#ifndef BOARD_AMOLED_18
|
||||
bool fwd_now = (digitalRead(BTN_FWD) == LOW);
|
||||
if (fwd_now != fwd_was) {
|
||||
if (fwd_now) {
|
||||
if (idle_consume_wake_press()) {
|
||||
fwd_wake_swallowed = true;
|
||||
if (board_caps().button_count >= 2) {
|
||||
static bool secondary_was = false;
|
||||
static bool secondary_wake_swallowed = false;
|
||||
bool secondary_now = input_hal_is_held(INPUT_BTN_SECONDARY);
|
||||
if (secondary_now != secondary_was) {
|
||||
if (secondary_now) {
|
||||
if (idle_consume_wake_press()) secondary_wake_swallowed = true;
|
||||
else ble_keyboard_press(0x2B, 0x02); // HID Tab + LEFT_SHIFT
|
||||
} else {
|
||||
ble_keyboard_press(0x2B, 0x02); // HID Tab + LEFT_SHIFT
|
||||
if (secondary_wake_swallowed) secondary_wake_swallowed = false;
|
||||
else ble_keyboard_release();
|
||||
}
|
||||
} else {
|
||||
if (fwd_wake_swallowed) fwd_wake_swallowed = false;
|
||||
else ble_keyboard_release();
|
||||
secondary_was = secondary_now;
|
||||
}
|
||||
fwd_was = fwd_now;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (power_pwr_pressed()) {
|
||||
if (power_hal_pwr_pressed()) {
|
||||
if (!idle_consume_wake_press()) {
|
||||
if (ui_get_current_screen() == SCREEN_SPLASH) splash_next();
|
||||
else ui_cycle_screen();
|
||||
@@ -530,32 +270,24 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef BOARD_AMOLED_18
|
||||
handle_rotation_change();
|
||||
#endif
|
||||
|
||||
// Update BLE status on screen when state changes
|
||||
ble_state_t bs = ble_get_state();
|
||||
if (bs != last_ble_state) {
|
||||
last_ble_state = bs;
|
||||
ui_update_ble_status(bs, ble_get_device_name(), ble_get_mac_address());
|
||||
}
|
||||
|
||||
// Update battery indicator
|
||||
static int last_pct = -2;
|
||||
static int last_pct = -2;
|
||||
static bool last_charging = false;
|
||||
int pct = power_battery_pct();
|
||||
bool charging = power_is_charging();
|
||||
int pct = power_hal_battery_pct();
|
||||
bool charging = power_hal_is_charging();
|
||||
if (pct != last_pct || charging != last_charging) {
|
||||
last_pct = pct;
|
||||
last_charging = charging;
|
||||
ui_update_battery(pct, charging);
|
||||
}
|
||||
|
||||
// Check for serial commands (screenshot, etc.)
|
||||
check_serial_cmd();
|
||||
|
||||
// Process incoming BLE data
|
||||
if (ble_has_data()) {
|
||||
if (parse_json(ble_get_data(), &usage)) {
|
||||
int g_before = usage_rate_group();
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void power_init(void);
|
||||
void power_tick(void);
|
||||
int power_battery_pct(void); // 0-100, or -1 if no battery
|
||||
bool power_is_charging(void);
|
||||
bool power_is_vbus_in(void); // USB cable present (true even with no battery)
|
||||
bool power_pwr_pressed(void); // true once per AXP2101 PWR button short-press
|
||||
+29
-22
@@ -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)
|
||||
|
||||
+112
-117
@@ -3,7 +3,7 @@
|
||||
#include <lvgl.h>
|
||||
#include "logo.h"
|
||||
#include "icons.h"
|
||||
#include "display_cfg.h"
|
||||
#include "hal/board_caps.h"
|
||||
|
||||
// Custom fonts (scaled for 314 PPI, ~1.9x from original 165 PPI)
|
||||
LV_FONT_DECLARE(font_tiempos_56);
|
||||
@@ -16,21 +16,76 @@ LV_FONT_DECLARE(font_styrene_16);
|
||||
LV_FONT_DECLARE(font_styrene_14);
|
||||
LV_FONT_DECLARE(font_mono_32);
|
||||
|
||||
// AMOLED-1.8 (368 wide) needs smaller fonts on the Bluetooth screen so the
|
||||
// MAC address and credit lines don't overflow horizontally.
|
||||
#ifdef BOARD_AMOLED_18
|
||||
#define BT_TITLE_FONT font_tiempos_34
|
||||
#define BT_STATUS_FONT font_styrene_28
|
||||
#define BT_DEVICE_FONT font_styrene_20
|
||||
#define BT_CREDIT_1_FONT font_styrene_16
|
||||
#define BT_CREDIT_2_FONT font_styrene_14
|
||||
#else
|
||||
#define BT_TITLE_FONT font_tiempos_56
|
||||
#define BT_STATUS_FONT font_styrene_48
|
||||
#define BT_DEVICE_FONT font_styrene_28
|
||||
#define BT_CREDIT_1_FONT font_styrene_24
|
||||
#define BT_CREDIT_2_FONT font_styrene_20
|
||||
#endif
|
||||
// Layout values computed from the active board's geometry. Populated once
|
||||
// in ui_init() and treated as const for the rest of the program. Adding a
|
||||
// new display size means extending compute_layout() with another
|
||||
// breakpoint — never editing the screen-builder functions below.
|
||||
struct Layout {
|
||||
int16_t scr_w, scr_h;
|
||||
int16_t margin;
|
||||
int16_t title_y;
|
||||
int16_t content_y;
|
||||
int16_t content_w;
|
||||
|
||||
// Usage screen
|
||||
int16_t usage_panel_h;
|
||||
int16_t usage_panel_gap;
|
||||
int16_t usage_bar_y;
|
||||
int16_t usage_reset_y;
|
||||
|
||||
// Bluetooth screen
|
||||
int16_t bt_info_panel_h;
|
||||
int16_t bt_reset_zone_h;
|
||||
const lv_font_t* bt_title_font;
|
||||
const lv_font_t* bt_status_font;
|
||||
const lv_font_t* bt_device_font;
|
||||
const lv_font_t* bt_credit_1_font;
|
||||
const lv_font_t* bt_credit_2_font;
|
||||
};
|
||||
static Layout L = {};
|
||||
|
||||
// Pick layout values from the active board's pixel dimensions. The two
|
||||
// existing boards happen to land on the two breakpoints below; new ports
|
||||
// inherit the closer one — visually OK, may need a polish pass for
|
||||
// pixel-perfect alignment but never blocks the port from booting.
|
||||
static void compute_layout(const BoardCaps& c) {
|
||||
L.scr_w = c.width;
|
||||
L.scr_h = c.height;
|
||||
L.margin = 20;
|
||||
L.title_y = 30;
|
||||
|
||||
if (c.height >= 460) {
|
||||
// Large layout — tuned for 480x480 (AMOLED-2.16).
|
||||
L.content_y = 100;
|
||||
L.usage_panel_h = 150;
|
||||
L.usage_panel_gap = 16;
|
||||
L.usage_bar_y = 56;
|
||||
L.usage_reset_y = 94;
|
||||
L.bt_info_panel_h = 160;
|
||||
L.bt_reset_zone_h = 110;
|
||||
L.bt_title_font = &font_tiempos_56;
|
||||
L.bt_status_font = &font_styrene_48;
|
||||
L.bt_device_font = &font_styrene_28;
|
||||
L.bt_credit_1_font = &font_styrene_24;
|
||||
L.bt_credit_2_font = &font_styrene_20;
|
||||
} else {
|
||||
// Compact layout — tuned for 368x448 (AMOLED-1.8).
|
||||
L.content_y = 85;
|
||||
L.usage_panel_h = 130;
|
||||
L.usage_panel_gap = 12;
|
||||
L.usage_bar_y = 48;
|
||||
L.usage_reset_y = 78;
|
||||
L.bt_info_panel_h = 140;
|
||||
L.bt_reset_zone_h = 90;
|
||||
L.bt_title_font = &font_tiempos_34;
|
||||
L.bt_status_font = &font_styrene_28;
|
||||
L.bt_device_font = &font_styrene_20;
|
||||
L.bt_credit_1_font = &font_styrene_16;
|
||||
L.bt_credit_2_font = &font_styrene_14;
|
||||
}
|
||||
|
||||
L.content_w = L.scr_w - 2 * L.margin;
|
||||
}
|
||||
|
||||
// Anthropic brand palette — design tokens live in theme.h
|
||||
#include "theme.h"
|
||||
@@ -44,20 +99,6 @@ LV_FONT_DECLARE(font_mono_32);
|
||||
#define COL_RED THEME_RED
|
||||
#define COL_BAR_BG THEME_BAR_BG
|
||||
|
||||
// ---- Layout constants ----
|
||||
// Width/height track the active display (480x480 for AMOLED-2.16, 368x448 for AMOLED-1.8).
|
||||
// MARGIN clears rounded display corners on both panels.
|
||||
#define SCR_W LCD_WIDTH
|
||||
#define SCR_H LCD_HEIGHT
|
||||
#define MARGIN 20
|
||||
#define TITLE_Y 30
|
||||
#ifdef BOARD_AMOLED_18
|
||||
#define CONTENT_Y 85 // tighter vertical packing for 448-tall portrait
|
||||
#else
|
||||
#define CONTENT_Y 100
|
||||
#endif
|
||||
#define CONTENT_W (SCR_W - 2 * MARGIN)
|
||||
|
||||
// ---- Usage screen widgets ----
|
||||
static lv_obj_t* usage_container;
|
||||
static lv_obj_t* lbl_title;
|
||||
@@ -101,9 +142,6 @@ static const char* const spinner_frames[] = {
|
||||
#define SPINNER_COUNT 6
|
||||
#define SPINNER_PHASES (2 * (SPINNER_COUNT - 1)) // 10: ping-pong 0..5..0
|
||||
|
||||
// Per-frame hold time. Modeled on Claude Code's spinner (Cavalry triangle
|
||||
// oscillator, range 0..5, period 5s) — turn-around frames (0 and 5) appear
|
||||
// once per cycle, middle frames twice, so 0/5 read as held longer.
|
||||
static const uint16_t spinner_ms[SPINNER_COUNT] = {
|
||||
260, 130, 130, 130, 130, 260,
|
||||
};
|
||||
@@ -178,8 +216,6 @@ static lv_obj_t* make_panel(lv_obj_t* parent, int x, int y, int w, int h) {
|
||||
lv_obj_set_style_pad_top(panel, 12, 0);
|
||||
lv_obj_set_style_pad_bottom(panel, 12, 0);
|
||||
lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
|
||||
// Bubble click events up to the screen / usage_container so a tap anywhere
|
||||
// on the panel fires the global click handler.
|
||||
lv_obj_add_flag(panel, LV_OBJ_FLAG_EVENT_BUBBLE);
|
||||
return panel;
|
||||
}
|
||||
@@ -208,8 +244,6 @@ static void init_icon_dsc(lv_image_dsc_t* dsc, int w, int h, const uint16_t* dat
|
||||
dsc->data_size = w * h * 2;
|
||||
}
|
||||
|
||||
// RGB565A8: planar — w*h RGB565 pixels followed by w*h alpha bytes.
|
||||
// Stride is RGB565-only (w*2); LVGL infers alpha plane location from header.
|
||||
static void init_icon_dsc_rgb565a8(lv_image_dsc_t* dsc, int w, int h, const uint8_t* data) {
|
||||
dsc->header.w = w;
|
||||
dsc->header.h = h;
|
||||
@@ -234,7 +268,6 @@ static lv_obj_t* make_pill(lv_obj_t* parent, const char* text) {
|
||||
return lbl;
|
||||
}
|
||||
|
||||
// ---- Battery icon initialization ----
|
||||
static void init_battery_icons(void) {
|
||||
init_icon_dsc_rgb565a8(&battery_dscs[0], ICON_BATTERY_W, ICON_BATTERY_H, icon_battery_data);
|
||||
init_icon_dsc_rgb565a8(&battery_dscs[1], ICON_BATTERY_LOW_W, ICON_BATTERY_LOW_H, icon_battery_low_data);
|
||||
@@ -245,28 +278,10 @@ static void init_battery_icons(void) {
|
||||
|
||||
// ======== Usage Screen ========
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
// 368x448 portrait — compressed vertical layout so two panels + bottom anim
|
||||
// label fit without overlap.
|
||||
#define PANEL_H 130
|
||||
#define PANEL_GAP 12
|
||||
#define PANEL_BAR_Y 48
|
||||
#define PANEL_RESET_Y 78
|
||||
#else
|
||||
// 480x480 square (original)
|
||||
#define PANEL_H 150
|
||||
#define PANEL_GAP 16
|
||||
#define PANEL_BAR_Y 56
|
||||
#define PANEL_RESET_Y 94
|
||||
#endif
|
||||
|
||||
// One Session/Weekly panel: big % label, pill on the right, bar, reset label.
|
||||
// Pill y=1: symmetric inside the panel — panel-outer-top → pill-top equals
|
||||
// pill-bottom → bar-top.
|
||||
static void make_usage_panel(lv_obj_t* parent, int y, const char* pill_text,
|
||||
lv_obj_t** out_pct, lv_obj_t** out_pill,
|
||||
lv_obj_t** out_bar, lv_obj_t** out_reset) {
|
||||
lv_obj_t* panel = make_panel(parent, MARGIN, y, CONTENT_W, PANEL_H);
|
||||
lv_obj_t* panel = make_panel(parent, L.margin, y, L.content_w, L.usage_panel_h);
|
||||
|
||||
*out_pct = lv_label_create(panel);
|
||||
lv_label_set_text(*out_pct, "---%");
|
||||
@@ -277,18 +292,18 @@ static void make_usage_panel(lv_obj_t* parent, int y, const char* pill_text,
|
||||
*out_pill = make_pill(panel, pill_text);
|
||||
lv_obj_align(*out_pill, LV_ALIGN_TOP_RIGHT, 0, 1);
|
||||
|
||||
*out_bar = make_bar(panel, 0, PANEL_BAR_Y, CONTENT_W - 32, 24);
|
||||
*out_bar = make_bar(panel, 0, L.usage_bar_y, L.content_w - 32, 24);
|
||||
|
||||
*out_reset = lv_label_create(panel);
|
||||
lv_label_set_text(*out_reset, "---");
|
||||
lv_obj_set_style_text_font(*out_reset, &font_styrene_28, 0);
|
||||
lv_obj_set_style_text_color(*out_reset, COL_DIM, 0);
|
||||
lv_obj_set_pos(*out_reset, 0, PANEL_RESET_Y);
|
||||
lv_obj_set_pos(*out_reset, 0, L.usage_reset_y);
|
||||
}
|
||||
|
||||
static void init_usage_screen(lv_obj_t* scr) {
|
||||
usage_container = lv_obj_create(scr);
|
||||
lv_obj_set_size(usage_container, SCR_W, SCR_H);
|
||||
lv_obj_set_size(usage_container, L.scr_w, L.scr_h);
|
||||
lv_obj_set_pos(usage_container, 0, 0);
|
||||
lv_obj_set_style_bg_opa(usage_container, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_width(usage_container, 0, 0);
|
||||
@@ -300,12 +315,13 @@ static void init_usage_screen(lv_obj_t* scr) {
|
||||
lv_label_set_text(lbl_title, "Usage");
|
||||
lv_obj_set_style_text_font(lbl_title, &font_tiempos_56, 0);
|
||||
lv_obj_set_style_text_color(lbl_title, COL_TEXT, 0);
|
||||
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 16, TITLE_Y);
|
||||
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 16, L.title_y);
|
||||
|
||||
make_usage_panel(usage_container, CONTENT_Y, "Current",
|
||||
make_usage_panel(usage_container, L.content_y, "Current",
|
||||
&lbl_session_pct, &lbl_session_label,
|
||||
&bar_session, &lbl_session_reset);
|
||||
make_usage_panel(usage_container, CONTENT_Y + PANEL_H + PANEL_GAP, "Weekly",
|
||||
make_usage_panel(usage_container,
|
||||
L.content_y + L.usage_panel_h + L.usage_panel_gap, "Weekly",
|
||||
&lbl_weekly_pct, &lbl_weekly_label,
|
||||
&bar_weekly, &lbl_weekly_reset);
|
||||
|
||||
@@ -318,34 +334,25 @@ static void init_usage_screen(lv_obj_t* scr) {
|
||||
|
||||
// ======== Bluetooth Screen ========
|
||||
|
||||
#ifdef BOARD_AMOLED_18
|
||||
#define BT_INFO_PANEL_H 140
|
||||
#define BT_RESET_ZONE_H 90
|
||||
#else
|
||||
#define BT_INFO_PANEL_H 160
|
||||
#define BT_RESET_ZONE_H 110
|
||||
#endif
|
||||
|
||||
static void init_bluetooth_screen(lv_obj_t* scr) {
|
||||
ble_container = lv_obj_create(scr);
|
||||
lv_obj_set_size(ble_container, SCR_W, SCR_H);
|
||||
lv_obj_set_size(ble_container, L.scr_w, L.scr_h);
|
||||
lv_obj_set_pos(ble_container, 0, 0);
|
||||
lv_obj_set_style_bg_opa(ble_container, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_style_border_width(ble_container, 0, 0);
|
||||
lv_obj_set_style_pad_all(ble_container, 0, 0);
|
||||
lv_obj_clear_flag(ble_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_event_cb(ble_container, global_click_cb, LV_EVENT_CLICKED, NULL);
|
||||
|
||||
// Title
|
||||
lv_obj_t* lbl_ble_title = lv_label_create(ble_container);
|
||||
lv_label_set_text(lbl_ble_title, "Bluetooth");
|
||||
lv_obj_set_style_text_font(lbl_ble_title, &BT_TITLE_FONT, 0);
|
||||
lv_obj_set_style_text_font(lbl_ble_title, L.bt_title_font, 0);
|
||||
lv_obj_set_style_text_color(lbl_ble_title, COL_TEXT, 0);
|
||||
lv_obj_align(lbl_ble_title, LV_ALIGN_TOP_MID, 16, TITLE_Y);
|
||||
lv_obj_align(lbl_ble_title, LV_ALIGN_TOP_MID, 16, L.title_y);
|
||||
|
||||
// Info panel
|
||||
lv_obj_t* p_info = make_panel(ble_container, MARGIN, CONTENT_Y, CONTENT_W, BT_INFO_PANEL_H);
|
||||
lv_obj_t* p_info = make_panel(ble_container, L.margin, L.content_y,
|
||||
L.content_w, L.bt_info_panel_h);
|
||||
|
||||
// Bluetooth icon + status row
|
||||
static lv_image_dsc_t icon_bt_dsc;
|
||||
init_icon_dsc(&icon_bt_dsc, ICON_BLUETOOTH_W, ICON_BLUETOOTH_H, icon_bluetooth_data);
|
||||
|
||||
@@ -355,27 +362,26 @@ static void init_bluetooth_screen(lv_obj_t* scr) {
|
||||
|
||||
lbl_ble_status = lv_label_create(p_info);
|
||||
lv_label_set_text(lbl_ble_status, "Initializing...");
|
||||
lv_obj_set_style_text_font(lbl_ble_status, &BT_STATUS_FONT, 0);
|
||||
lv_obj_set_style_text_font(lbl_ble_status, L.bt_status_font, 0);
|
||||
lv_obj_set_style_text_color(lbl_ble_status, COL_DIM, 0);
|
||||
lv_obj_set_pos(lbl_ble_status, 56, 2);
|
||||
|
||||
lbl_ble_device = lv_label_create(p_info);
|
||||
lv_label_set_text(lbl_ble_device, "Device: ---");
|
||||
lv_obj_set_style_text_font(lbl_ble_device, &BT_DEVICE_FONT, 0);
|
||||
lv_obj_set_style_text_font(lbl_ble_device, L.bt_device_font, 0);
|
||||
lv_obj_set_style_text_color(lbl_ble_device, COL_DIM, 0);
|
||||
lv_obj_set_pos(lbl_ble_device, 0, 64);
|
||||
|
||||
lbl_ble_mac = lv_label_create(p_info);
|
||||
lv_label_set_text(lbl_ble_mac, "Address: ---");
|
||||
lv_obj_set_style_text_font(lbl_ble_mac, &BT_DEVICE_FONT, 0);
|
||||
lv_obj_set_style_text_font(lbl_ble_mac, L.bt_device_font, 0);
|
||||
lv_obj_set_style_text_color(lbl_ble_mac, COL_DIM, 0);
|
||||
lv_obj_set_pos(lbl_ble_mac, 0, 100);
|
||||
|
||||
// Reset Bluetooth tap zone with trash icon
|
||||
int reset_y = CONTENT_Y + BT_INFO_PANEL_H + 16;
|
||||
int reset_y = L.content_y + L.bt_info_panel_h + 16;
|
||||
lv_obj_t* reset_zone = lv_obj_create(ble_container);
|
||||
lv_obj_set_pos(reset_zone, MARGIN, reset_y);
|
||||
lv_obj_set_size(reset_zone, CONTENT_W, BT_RESET_ZONE_H);
|
||||
lv_obj_set_pos(reset_zone, L.margin, reset_y);
|
||||
lv_obj_set_size(reset_zone, L.content_w, L.bt_reset_zone_h);
|
||||
lv_obj_set_style_bg_color(reset_zone, COL_PANEL, 0);
|
||||
lv_obj_set_style_bg_opa(reset_zone, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_radius(reset_zone, 8, 0);
|
||||
@@ -393,59 +399,51 @@ static void init_bluetooth_screen(lv_obj_t* scr) {
|
||||
|
||||
lv_obj_t* reset_lbl = lv_label_create(reset_zone);
|
||||
lv_label_set_text(reset_lbl, "Reset Bluetooth");
|
||||
lv_obj_set_style_text_font(reset_lbl, &BT_DEVICE_FONT, 0);
|
||||
lv_obj_set_style_text_font(reset_lbl, L.bt_device_font, 0);
|
||||
lv_obj_set_style_text_color(reset_lbl, COL_DIM, 0);
|
||||
|
||||
// Attribution
|
||||
lv_obj_t* lbl_credit = lv_label_create(ble_container);
|
||||
lv_label_set_text(lbl_credit, "Built by @hermannbjorgvin");
|
||||
lv_obj_set_style_text_font(lbl_credit, &BT_CREDIT_1_FONT, 0);
|
||||
lv_obj_set_style_text_font(lbl_credit, L.bt_credit_1_font, 0);
|
||||
lv_obj_set_style_text_color(lbl_credit, COL_DIM, 0);
|
||||
lv_obj_align(lbl_credit, LV_ALIGN_BOTTOM_MID, 0, -46);
|
||||
|
||||
lv_obj_t* lbl_credit2 = lv_label_create(ble_container);
|
||||
lv_label_set_text(lbl_credit2, "Clawd animation by @amaanbuilds");
|
||||
lv_obj_set_style_text_font(lbl_credit2, &BT_CREDIT_2_FONT, 0);
|
||||
lv_obj_set_style_text_font(lbl_credit2, L.bt_credit_2_font, 0);
|
||||
lv_obj_set_style_text_color(lbl_credit2, COL_DIM, 0);
|
||||
lv_obj_align(lbl_credit2, LV_ALIGN_BOTTOM_MID, 0, -20);
|
||||
|
||||
// Start hidden
|
||||
lv_obj_add_flag(ble_container, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
// ======== Public API ========
|
||||
|
||||
void ui_init(void) {
|
||||
compute_layout(board_caps());
|
||||
|
||||
lv_obj_t* scr = lv_screen_active();
|
||||
lv_obj_set_style_bg_color(scr, COL_BG, 0);
|
||||
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
||||
|
||||
// Logo (shared, always visible, on top of all containers)
|
||||
// Logo is RGB565A8 (planar: w*h RGB565 then w*h alpha) so it composites
|
||||
// cleanly against whatever bg is behind it.
|
||||
init_icon_dsc_rgb565a8(&logo_dsc, LOGO_WIDTH, LOGO_HEIGHT, logo_data);
|
||||
|
||||
// Initialize battery icon descriptors
|
||||
init_battery_icons();
|
||||
|
||||
init_usage_screen(scr);
|
||||
init_bluetooth_screen(scr);
|
||||
splash_init(scr);
|
||||
|
||||
// Splash is touch-toggled — tap anywhere on the splash dismisses it
|
||||
if (splash_get_root()) {
|
||||
lv_obj_add_event_cb(splash_get_root(), global_click_cb, LV_EVENT_CLICKED, NULL);
|
||||
}
|
||||
|
||||
// Logo on top of all containers (inset for rounded corners)
|
||||
logo_img = lv_image_create(scr);
|
||||
lv_image_set_src(logo_img, &logo_dsc);
|
||||
lv_obj_set_pos(logo_img, MARGIN, TITLE_Y - 10);
|
||||
lv_obj_set_pos(logo_img, L.margin, L.title_y - 10);
|
||||
|
||||
// Battery indicator on top of all containers (upper-right, inset)
|
||||
battery_img = lv_image_create(scr);
|
||||
lv_image_set_src(battery_img, &battery_dscs[0]);
|
||||
lv_obj_set_pos(battery_img, SCR_W - 48 - MARGIN, TITLE_Y);
|
||||
lv_obj_set_pos(battery_img, L.scr_w - 48 - L.margin, L.title_y);
|
||||
}
|
||||
|
||||
void ui_update(const UsageData* data) {
|
||||
@@ -453,7 +451,6 @@ void ui_update(const UsageData* data) {
|
||||
|
||||
int s_pct = (int)(data->session_pct + 0.5f);
|
||||
|
||||
// Usage screen
|
||||
lv_label_set_text_fmt(lbl_session_pct, "%d%%", s_pct);
|
||||
lv_bar_set_value(bar_session, s_pct, LV_ANIM_ON);
|
||||
lv_obj_set_style_bg_color(bar_session, pct_color(data->session_pct), LV_PART_INDICATOR);
|
||||
@@ -496,22 +493,16 @@ void ui_tick_anim(void) {
|
||||
}
|
||||
|
||||
static screen_t prev_non_splash_screen = SCREEN_USAGE;
|
||||
// Hide the battery indicator on the splash screen — the icon is visually
|
||||
// noisy over the pixel-art creature animations.
|
||||
static void apply_battery_visibility(void) {
|
||||
if (!battery_img) return;
|
||||
if (current_screen == SCREEN_SPLASH) lv_obj_add_flag(battery_img, LV_OBJ_FLAG_HIDDEN);
|
||||
else lv_obj_clear_flag(battery_img, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
// LVGL handles click debouncing internally. Screen-level handler fires when
|
||||
// no child consumed the event (children only consume if they have their own
|
||||
// event callback, e.g. the Reset Bluetooth zone). On BT screen we skip the
|
||||
// splash toggle so only the reset zone is interactive there.
|
||||
static void global_click_cb(lv_event_t* e) {
|
||||
(void)e;
|
||||
if (ui_get_current_screen() == SCREEN_BLUETOOTH) return;
|
||||
ui_toggle_splash();
|
||||
if (current_screen == SCREEN_SPLASH) ui_show_screen(prev_non_splash_screen);
|
||||
else ui_show_screen(SCREEN_SPLASH);
|
||||
}
|
||||
|
||||
static void ble_reset_click_cb(lv_event_t* e) {
|
||||
@@ -531,7 +522,6 @@ void ui_show_screen(screen_t screen) {
|
||||
default: break;
|
||||
}
|
||||
|
||||
// Hide the logo overlay on the splash screen so the animation has a clean canvas
|
||||
if (logo_img) {
|
||||
if (screen == SCREEN_SPLASH) lv_obj_add_flag(logo_img, LV_OBJ_FLAG_HIDDEN);
|
||||
else lv_obj_clear_flag(logo_img, LV_OBJ_FLAG_HIDDEN);
|
||||
@@ -543,7 +533,12 @@ void ui_show_screen(screen_t screen) {
|
||||
}
|
||||
|
||||
void ui_cycle_screen(void) {
|
||||
screen_t next = (current_screen == SCREEN_USAGE) ? SCREEN_BLUETOOTH : SCREEN_USAGE;
|
||||
screen_t next;
|
||||
switch (current_screen) {
|
||||
case SCREEN_USAGE: next = SCREEN_BLUETOOTH; break;
|
||||
case SCREEN_BLUETOOTH: next = SCREEN_USAGE; break;
|
||||
default: next = SCREEN_USAGE; break;
|
||||
}
|
||||
ui_show_screen(next);
|
||||
}
|
||||
|
||||
@@ -591,17 +586,17 @@ void ui_update_ble_status(ble_state_t state, const char* name, const char* mac)
|
||||
void ui_update_battery(int percent, bool charging) {
|
||||
int idx;
|
||||
if (charging) {
|
||||
idx = 4; // charging icon
|
||||
idx = 4;
|
||||
} else if (percent < 0) {
|
||||
idx = 0; // no battery / unknown
|
||||
idx = 0;
|
||||
} else if (percent <= 10) {
|
||||
idx = 0; // empty
|
||||
idx = 0;
|
||||
} else if (percent <= 35) {
|
||||
idx = 1; // low
|
||||
idx = 1;
|
||||
} else if (percent <= 75) {
|
||||
idx = 2; // medium
|
||||
idx = 2;
|
||||
} else {
|
||||
idx = 3; // full
|
||||
idx = 3;
|
||||
}
|
||||
lv_image_set_src(battery_img, &battery_dscs[idx]);
|
||||
apply_battery_visibility();
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Reference in New Issue
Block a user