When the host is connected over BLE but no usage update has landed recently
(~90s — token expired, daemon down, host asleep), show a calm idle screen
instead of leaving hours-old numbers on screen as if they were live: a shrunk,
animated "expression sleep" creature (reused claudepix art) centered between
the header and the status line.
The usage view resolves to three sub-screens via a data-freshness timer: live
panels, this idle screen, or the existing pairing hint. The bottom status line
animates present-participle gerunds: the idle screen alternates "Listening…" /
"No data…" (alive + explicit), the disconnected screen uses "Waiting…"
(replacing the static "Disconnected…").
splash_mini_create()/splash_mini_tick() render any claudepix animation shrunk
into an arbitrary parent, keeping the art data in the splash module.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add complete board support for Waveshare ESP32-C6-Touch-AMOLED-2.16 with SH8601 display driver
- Implement board initialization with AXP2101 PMU rail configuration for LCD and touch power
- Add display driver for SH8601 QSPI panel (480×480 resolution)
- Add CST9217 touch controller support via I2C
- Add QMI8658 IMU initialization and sensor reading
- Add AXP2101 power management and battery monitoring
- Add input handling for BOOT button (GPIO 9)
- Configure PlatformIO environment with C6-specific build flags and 16 MB flash layout
- Update capability flags documentation to clarify BOARD_HAS_PSRAM build-flag macro usage
- C6 has no external PSRAM; shared code gates on this flag to use internal SRAM and reduce LVGL buffer sizes
- Screenshot capture disabled on this board due to internal SRAM constraints
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>
Port the firmware to a second Waveshare AMOLED board variant alongside
the original 2.16" / 480×480. Selection is at build time via a board
macro; both envs continue to build and flash independently.
Hardware delta vs AMOLED-2.16:
* Display: SH8601 QSPI (vs CO5300), 368×448 portrait (vs 480² square),
SCLK on GPIO 11 (vs 38), RST routed through an XCA9554 I/O expander
(vs direct GPIO).
* Touch: FT3168 @ 0x38 (vs CST9220 @ 0x5A). Driven by a ~30-line
inline FocalTech-protocol reader in main.cpp — Waveshare's
Arduino_DriveBus library is GPLv3 and would force the project's
license, which the README explicitly avoids.
* I/O expander: new XCA9554/PCA9554 @ I2C 0x20 gates LCD_RST, TP_RST,
audio amp enable, and the PWR button. Wrapped in io_expander.{h,cpp}.
Must initialize BEFORE display/touch or both stay in reset.
* PMU + IMU: same AXP2101 + QMI8658 as 2.16, so power.cpp/imu.cpp
largely reuse. IMU is initialized for I2C bus health but rotation
is fixed at 0° (the portrait panel doesn't benefit from auto-rotate
and the strip-rotation CPU path is excluded entirely).
* Buttons: only BOOT (GPIO 0) is a direct GPIO; PWR is read via
XCA9554 EXIO4 polling. No third button — the AMOLED-2.16's
Shift+Tab key (GPIO 18) is dropped for this board.
* Flash: 16MB (vs 8MB), partition table set to default_16MB.csv.
Code structure:
* display_cfg.h hosts a #ifdef BOARD_AMOLED_18 / #else / #endif split
with a PlatformDisplay typedef so call sites elsewhere stay clean.
* main.cpp, power.cpp, ui.cpp, splash.cpp gate board-specific bits on
the same macro. Layout constants (panel heights, content Y, font
choices on the Bluetooth screen) compress for the 368×448 portrait
footprint so two usage panels + bottom animation label fit without
horizontal overflow.
* Splash scales the 20×20 pixel-art grid to 360×360 (CELL=18 vs 24)
and centers in the portrait canvas.
* Old AMOLED-2.16 env is unchanged at the source level (gated under
#ifndef BOARD_AMOLED_18) and continues to compile + flash on the
original hardware.
CLAUDE.md updated with the two-board pin maps, build/flash commands
for both envs (macOS + Linux device paths), and the per-board gotchas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six public API functions had no callers anywhere in the firmware:
splash_prev, splash_set_active, splash_current_name, splash_count,
ble_is_connected, imu_set_rotation, power_is_usb_connected,
power_usb_changed. Trimming them lets power.cpp drop the VBUS state
machine (cached_usb, usb_changed_flag, last_vbus_ms,
enableVbusVoltageMeasure). usage_rate_reset becomes file-static since
the only caller is in usage_rate.cpp itself. Montserrat font flags in
platformio.ini were enabled but never referenced.
Group splash animations into four moods (idle/normal/active/heavy) and pick
which group plays based on a 5-min sliding rate of session_pct. Thresholds
sit at 0.10/0.20/0.33 %/min so Heavy fires when usage matches or beats the
5-hour session reset pace. Splash auto-rotates within the current group
every 20s; group recomputes on every BLE poll and the splash re-picks
immediately if it changes mid-display. 4-min minimum window prevents
single-sample bumps after boot from falsely landing in Heavy.
- Extract design tokens to firmware/src/theme.h (THEME_BG, THEME_PANEL,
THEME_TEXT, etc.); ui.cpp aliases COL_* to the THEME_* tokens
- Splash container + canvas COL_EMPTY now use THEME_BG / 0x0000 instead
of the hardcoded 0x0f0f0f / 0x10A2 dark grey
- tools/convert_to_c.js maps "transparent" palette entries to 0x0000
(true black) so regenerated splash_animations.h matches the new bg
- Re-regenerate splash_animations.h
- Convert logo to RGB565A8 (alpha plane) so it composites against any
background — no more grey halo around the upper-left logo on screens
where the bg differs from the previous baked-in 0x18E3 color
UX overhaul:
- Drop the Controller screen entirely (touch zones, swipe gestures, HID
gesture engine, four icon arrays, hand icon)
- Two persistent screens (Usage, Bluetooth) cycled by the middle PWR
button; splash becomes a touch-toggled welcome animation
- Left button (GPIO 0): hold to send Space (Claude Code voice-mode PTT)
- Right button (GPIO 18): press to send Shift+Tab (mode toggle)
- Middle button on splash cycles animations instead of screens
- Remove USB plug/unplug auto-switch (Controller was the unplug target)
- Hide battery indicator on splash when in LOW state — it's noisy over
the pixel-art animations
Implementation:
- Delete firmware/src/hid.{cpp,h} and touch.{cpp,h}; their roles fold
into main.cpp button handlers and LVGL CLICKED events respectively
- splash_get_root() exposes the splash container so ui.cpp can attach
a click handler for the dismiss-on-tap behavior
- Tap detection uses LVGL's built-in LV_EVENT_CLICKED with event
bubbling on the inner panels — debouncing is handled by LVGL
- On Bluetooth screen, only the Reset Bluetooth zone is clickable;
taps elsewhere are no-ops (no conflict with splash toggle)
Docs:
- README: drop Controller from screens table, rewrite Physical buttons
table, remove Gesture controls section
- CLAUDE.md: update file map, button assignments, drop gesture refs
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>