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>
Waveshare ships two hardware revisions under "ESP32-S3-Touch-AMOLED-1.8":
the original (SH8601 display + FT3168 touch @0x38) and a later one
(CO5300 display + CST816 touch @0x15). On the later board the SH8601
driver left the panel blank and the FT3168 probe spammed
ESP_ERR_INVALID_STATE because nothing answers at 0x38.
Detect the revision at boot by probing the touch address and pick the
matching drivers, so one binary drives both boards:
- board_rev.{h}/board_init.cpp: probe 0x15 (CST816) vs 0x38 (FT3168)
- display.cpp: instantiate Arduino_CO5300 or Arduino_SH8601 behind a
shared Arduino_OLED*; CO5300 gets a 16px column offset to center the
368-wide image (verified on hardware)
- touch.cpp: one FocalTech-style reader (regs 0x02..0x06) for both
chips, address chosen by revision
- board.h: add CST816_ADDR 0x15
- CLAUDE.md: document the dual revision
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The post-rotation brightness ramp targeted a hard-coded 200 (the old default),
which reset whatever the user had chosen via PWR-short-press after every IMU
rotation. Read brightness_get() instead and scale the 4-step ramp (30/60/85/100%)
toward that target.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brand consistency: the device now advertises as "Clawdmeter". Updated the
three functional spots that must agree for discovery-by-name to work —
ble.cpp DEVICE_NAME (advertised) and both daemons' DEVICE_NAME (scan match) —
plus the README/CLAUDE.md/install-script references (and fixed the stray
"Claudemeter" typo in the README).
Note: hosts cache the GATT name, so an already-paired device keeps showing the
old name until removed/re-paired (bluetoothctl remove <mac>).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- convert_to_c.js: remap the claudepix body color (#CD7F6A) to Anthropic brand
terracotta (#D97757) so the splash creature matches the brand; regenerate
splash_animations.h.
- README: document the 3-second hold-to-pair gesture, drop the now-removed
Bluetooth screen from the screenshots table + prose, and delete the
Bluetooth screenshots (216 + 1.8").
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removes the Bluetooth screen — the device now has just splash + Usage. Keeps
main's responsive Layout intact.
- ui: drop the Bluetooth screen, ui_cycle_screen, and SCREEN_BLUETOOTH. The
bottom spinner becomes a status line — Connected / Disconnected / Pairing…
(via ble_has_bonds()) plus the whimsical messages when connected and idle.
A "To pair / hold the power button / for 3 seconds, then release" hint shows
while disconnected so the screen isn't empty.
- main: hold-to-pair gesture (pair_tick) — hold PWR ~3s then RELEASE clears
bonds and re-advertises. Clearing on release (not while held) means holding
to power off (8s hardware shutdown) leaves the bond intact; disarms at 6s.
PWR short-press now cycles brightness on the Usage view (animations on splash).
- ble: add ble_has_bonds() for the status line.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
idle owns the panel brightness (it fades between a "full" level and 0), so
user brightness control routes through a new idle_set_awake_brightness():
idle now fades to a runtime awake level instead of the hard-coded
DISPLAY_DEFAULT_BRIGHTNESS, and the chosen level survives idle fades + wake.
brightness.{h,cpp} holds a 4-step ramp {64,128,200,255} (default 200 == the
old constant, so default behavior is unchanged) persisted to NVS, and applies
the level through idle. Wired to the PWR button in the next commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds power_hal_pwr_long_pressed() and power_hal_pwr_released() to the HAL
contract so shared code can implement a hold-to-pair gesture board-agnostically.
- 216: enable the AXP2101 PKEY LONG + POSITIVE IRQs; bump press-off to 8s so a
~3-6s pair hold doesn't trip the hardware shutdown.
- 1.8": synthesize the same long/release edges in software from the polled
XCA9554 EXIO4 level (short-press now fires on release-if-short).
- template: stub the two new functions (no gesture) for new ports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On macOS the firmware is auto-connected by the OS as a BLE HID keyboard,
and CoreBluetooth excludes already-connected peripherals from scan
results. bleak's connect-by-address path also scans internally, so the
daemon's scan loop never found the device ("Device not found" forever)
even though it was plainly visible in System Settings.
Discover the target on macOS via CoreBluetooth's
retrieveConnectedPeripheralsWithServices_ and connect to the returned
peripheral directly (no scan). The custom service UUID is matched first
(unambiguous); the generic HID service 0x1812 is only trusted on an exact
DEVICE_NAME match so it can't grab an unrelated keyboard/mouse. A
peripheral that fails to connect is skipped for one cycle so the scan
fallback stays reachable. The device's two connection slots let the OS
HID link and the daemon run simultaneously.
Linux/BlueZ path is unchanged (still scans + caches address); the
redundant per-reconnect address save was removed so caching happens only
on a fresh scan.
Adds daemon/test_macos_connect.py, a foreground smoke test for the macOS
connect path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- These settings were causing conflicts with the on-chip fuel gauge reference configuration
- Simplify power initialization by relying on BSP defaults instead of overriding them
- Enable BTN_FWD_GPIO (GPIO 10) as secondary button for mode toggle (Shift+Tab)
- Update BOARD_HAS_SECONDARY_BUTTON flag from 0 to 1
- Clarify button layout: BOOT (primary PTT), PWR (cycle screens via AXP), KEY (secondary mode toggle)
- Document KEY GPIO discovery method (empirical scanning of unused GPIOs)
- Update button_count calculation to include secondary button when available
- Initialize both BTN_BACK_GPIO and BTN_FWD_GPIO in input_hal_init()
- Refactor input_hal_is_held() to use switch statement for cleaner multi-button handling
- 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>
Addresses review feedback on #24:
- New config IDLE_SLEEP_WHEN_CHARGING (default false): while USB is
plugged in we don't enter sleep and we wake immediately if power
comes back. Covers both "I like watching Clawd animations on my
plugged-in desk device" and "alternative hardware without a battery
is always on USB, sleep would be odd."
- New config IDLE_WAKE_ON_TOUCH (default true): a touch on the dark
panel wakes the device. The first touch is swallowed (mirrors the
button wake-consumption), so tapping a dark screen wakes without
also toggling splash<->usage. Set false to keep the original
pets/sleeves-safe behaviour where touch is fully ignored while
asleep.
power.{h,cpp} grow a power_is_vbus_in() helper backed by
pmu.isVbusIn() — distinct from isCharging() so the no-battery case
("plugged in, nothing to charge") is detected correctly.
AMOLED brightness fades to 0 after IDLE_TIMEOUT_MS without a physical
button press; any of the three buttons (BTN_BACK / BTN_FWD / PWR) fades
it back in. The first press from sleep is consumed for wake only —
Space / Shift+Tab / cycle-screen only fire on the second press, so
reaching for the device to glance at it doesn't accidentally send PTT
to Claude.
Touch never counts as activity and is fully swallowed while asleep:
pets and sleeves can't wake it, and LVGL can't secretly toggle
splash<->usage behind a black panel.
Rotation handling defers while asleep so its blank+ramp doesn't fight
the idle fade; a rotation that happens during sleep is detected and
ramped in on wake.
All tunables live in idle_cfg.h (timeout, fade durations, default
brightness) — nothing is hard-coded in main.cpp.
AMOLED brightness=0 is true 0 emission on these self-emissive panels,
so no extra power-management plumbing (light-sleep, AXP rail gating)
is needed to get the battery savings.
After #18 (MAX_CONNECTIONS=2), onConnect sets need_advertise=true to
fill the second slot. start_advertising() then unconditionally overwrote
state to ADVERTISING, so with a single connected client the UI flipped
CONNECTED -> ADVERTISING on the next tick and stayed there until a
second central attached.
Only flip the state to ADVERTISING when no clients are connected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macOS Bluetooth quirks made the original BLE setup unusable from a Mac
host. Five focused changes to fix discovery, pairing, the keyboard
identification wizard, and concurrent connections, plus a couple of
cross-platform daemon/script niceties.
Firmware (ble.cpp):
* Advertise the standard HID Service UUID (0x1812) in the primary
packet. Without it, macOS Sequoia's Bluetooth Settings GUI
recognizes the device internally but silently hides it from the
"Nearby Devices" list. Service UUIDs >16-bit overflow the 31-byte
advertising packet, so the custom data-service UUID moved to the
scan response.
* Switch PnP ID from Apple's USB vendor (0x05AC + Magic Keyboard
PID 0x820A) to Espressif's BT SIG vendor (0x02E5). macOS validates
Apple-claimed HIDs against known device IDs and refuses to surface
a Connect button for spoofers.
* Add the LED output report (Num/Caps/Scroll Lock) to the HID
descriptor — macOS treats a keyboard descriptor without LEDs as
"incomplete" and triggers the Keyboard Setup Assistant repeatedly.
* Set HID country code to 33 (US ANSI) instead of 0 (Not Supported)
so macOS can identify the layout without asking the user.
* Bump CONFIG_BT_NIMBLE_MAX_CONNECTIONS to 2 and restart advertising
after each accept. macOS holds one connection for the HID keyboard
link; the daemon now gets its own slot for the data service in
parallel, instead of either side starving the other.
screenshot.sh: auto-pick /dev/cu.usbmodem101 on macOS vs /dev/ttyACM0
on Linux, fall back to PlatformIO's bundled Python if pyserial isn't
on the system Python (PEP 668 blocks `pip install` on Homebrew Python),
and pass the actual framebuffer dimensions to ffmpeg instead of
hardcoding 480x480.
daemon/claude_usage_daemon.py: log API HTTP status + response body on
4xx/5xx so silent token-expiry failures (the daemon was reporting
{"s":0,"w":0} payloads instead of surfacing a 401) are visible in the
daemon log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cherry-picks the macOS-specific pieces from PR #5: Python daemon using
bleak (CoreBluetooth backend), LaunchAgent template, flash-mac.sh, and
install-mac.sh. Token is read from the macOS Keychain ("Claude
Code-credentials" service); Linux behavior is unchanged. README split
into "macOS installation" and "Linux installation" sections with
parallel Flash / Pair / Install subsections.
Co-Authored-By: Chris Davidson <36679917+lorddavidson@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>