Migrate to Waveshare ESP32-S3-Touch-AMOLED-2.16 with auto-rotation, splash, and battery

Full hardware swap from Panlee SC01 Plus (480×320 IPS) to Waveshare 2.16"
square AMOLED (480×480, CO5300 + CST9220 + AXP2101 + QMI8658). Library
stack moves to Arduino_GFX, SensorLib, XPowersLib on the pioarduino
platform 55.03.38-1 (Arduino Core 3.x).

UI:
- 4 screens (splash, usage, controller, bluetooth) with 3-button physical
  navigation: GPIO 0 = prev, AXP PKEY = cycle, GPIO 18 = next.
- IMU-driven 90° auto-rotation. CO5300 can't rotate via MADCTL, so flush
  callback does CPU strip-rotation in PARTIAL render mode. Rotation
  transitions use AMOLED brightness flash (instant black → redraw → ramp).
- Battery indicator (Lucide icons) in upper-right, RGB565A8 alpha so it
  blends over the splash animations.
- USB plugged/unplugged auto-switches between Usage and Controller
  screens (suppressed while on splash).
- Fonts and icons re-scaled ~1.9× for the higher-DPI panel; 20px margins
  to clear rounded corners.

Splash:
- 13 × 20×20 pixel-art creature animations sourced from
  claudepix.vercel.app via tools/scrape_claudepix.js (scraper handles
  both PRESET creature-engine and standalone FRAMES+PAL formats).
  tools/convert_to_c.js emits firmware/src/splash_animations.h.
  Attribution preserved in README and the generated header.

Tooling:
- tools/png_to_lvgl.js converts alpha PNGs to LVGL RGB565A8 (planar
  layout, with --tint to colorize Lucide black-on-transparent sources).
- tools/scrape_claudepix.js, tools/convert_to_c.js for the splash
  pipeline.

Docs:
- New worktree CLAUDE.md with hardware pin map, file inventory, build
  commands, and the 8 critical gotchas (CO5300 rotation, OPI PSRAM,
  pioarduino requirement, LVGL 9 font patching, centralized touch read,
  even-aligned flush regions, touch swap/mirror values, RGB565A8 layout).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hermann Björgvin Haraldsson
2026-05-09 22:04:04 +00:00
co-authored by Claude Opus 4.7
parent 10b8052bcc
commit 97a5443601
65 changed files with 121542 additions and 801 deletions
+87
View File
@@ -0,0 +1,87 @@
# Project context
ESP32-S3 firmware for a desk-side Claude Code usage monitor on a **Waveshare ESP32-S3-Touch-AMOLED-2.16** board (480×480 square AMOLED). 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.
## Hardware (critical pins)
- Display: **CO5300** AMOLED via QSPI (CS=12, SCLK=38, SDIO0..3=4..7, RST=2)
- Touch: **CST9220** via I2C (SDA=15, SCL=14, INT=11, addr=0x5A)
- PMU: **AXP2101** on same I2C bus (addr=0x34) — battery, USB VBUS, PWR button IRQ
- IMU: **QMI8658** on same I2C bus (addr=0x6B) — accelerometer for auto-rotation
- Buttons: GPIO 0 (left/back), GPIO 18 (right/forward), AXP PKEY (middle/PWR)
## Architecture
```
main.cpp — setup(), loop(), button polling, rotation flash, USB-state screen switching
display_cfg.h — pin defines, extern object decls
ui.{h,cpp} — 4-screen UI (splash, usage, controller, bluetooth), screen cycling
splash.{h,cpp} — 20×20 pixel-art animation engine, 24× upscale to 480×480
imu.{h,cpp} — accelerometer-driven rotation tracker (returns 0..3)
power.{h,cpp} — AXP2101 wrapper (battery %, charging, VBUS, PWR button)
touch.{h,cpp} — gesture state machine (zone taps, swipes, holds) on controller screen
ble.{h,cpp} — NimBLE peripheral: custom data service + HID keyboard
hid.{h,cpp} — gesture → HID keycode mapping
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, Styrene 48/28/24/20, Mono 32)
splash_animations.h — generated, do not hand-edit
```
## Build / flash
```bash
pio run -d firmware # build
pio run -d firmware -t upload --upload-port /dev/ttyACM0 # flash (binary path uses USB JTAG)
```
`/home/hermann/.platformio/penv/bin/pio` if `pio` isn't on PATH.
Device shows up as `/dev/ttyACM0` (Espressif USB JTAG/serial debug unit). No boot-mode gymnastics needed — direct flash works.
## 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.
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 both LVGL `my_touch_cb` and `touch.cpp:get_touch()` consumed each other's data, causing key-spam and broken swipes. Now `touch_read()` is called once per loop in main.cpp; both LVGL and gesture engine read from `touch_pressed/touch_x/touch_y` shared 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°).
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`.
## Icons
`tools/png_to_lvgl.js <input.png> <symbol> [W_MACRO] [H_MACRO] [--tint=RRGGBB | --no-tint]` converts an alpha PNG to RGB565A8. Default tint is white (`0xFFFFFF`) — necessary for Lucide PNGs. Splice output into `firmware/src/icons.h` and use `init_icon_dsc_rgb565a8()` in ui.cpp. Currently only the 5 battery icons use this format; the rest are still raw RGB565 baked over the panel background, fine because they live inside opaque zones.
## Splash animations
13 × 20×20 pixel-art creature animations sourced from
[claudepix.vercel.app](https://claudepix.vercel.app). Pipeline:
```bash
node tools/scrape_claudepix.js # → tools/claudepix_data/*.json
node tools/convert_to_c.js # → firmware/src/splash_animations.h
```
Each animation has a per-animation 10-color RGB565 palette. Cell values 0..9 index it. Default boot screen.
## User profile / preferences
See `~/.claude/projects/.../memory/` files for persistent context (user is an embedded-beginner senior dev, brand-conscious, prefers iterative UI refinement, dislikes me authoring my own art when third-party assets are intended). Always read those memory files at session start.
## Recent session highlights
- 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.
- Fonts and icons re-scaled ~1.9× for the higher-DPI panel.
- All UI margins widened to 20px to clear the rounded display corners.
- Battery icons converted to RGB565A8 alpha so they blend cleanly over the splash animations.
## Daemon / host side
Unchanged from original project. Python daemon (`daemon/`) reads OAuth token, polls Anthropic API, sends JSON over BLE GATT. Run with `systemctl --user start claude-usage-daemon`.