firmware: auto-sleep screen after 30 min idle

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.
This commit is contained in:
tobby168
2026-05-19 12:41:49 -07:00
parent 9ff81966fb
commit 6655b0f6d0
4 changed files with 174 additions and 8 deletions
+44 -8
View File
@@ -9,6 +9,8 @@
#include "imu.h"
#include "splash.h"
#include "usage_rate.h"
#include "idle.h"
#include "idle_cfg.h"
// Physical buttons (global, screen-independent):
// BTN_BACK (GPIO 0) — left, send Space (Claude Code voice mode push-to-talk)
@@ -52,6 +54,11 @@ static void touch_read() {
} else {
touch_pressed = false;
}
// Touch never counts as user activity and is fully swallowed while the
// panel is dark — avoids accidental wakes and prevents LVGL from secretly
// toggling splash<->usage behind a black screen.
if (idle_is_asleep()) touch_pressed = false;
}
// ---- LVGL draw buffers (PSRAM-backed, partial render) ----
@@ -234,7 +241,7 @@ void setup() {
// Init display
gfx->begin();
gfx->fillScreen(0x0000);
gfx->setBrightness(200);
idle_init(); // sets brightness to DISPLAY_DEFAULT_BRIGHTNESS and starts idle timer
// Init PMU
power_init();
@@ -310,6 +317,11 @@ static void handle_rotation_change(void) {
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);
@@ -324,7 +336,7 @@ static void handle_rotation_change(void) {
if (now - ramp_last < 25) return;
ramp_last = now;
static const uint8_t levels[] = {60, 120, 170, 200};
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++;
@@ -332,6 +344,7 @@ static void handle_rotation_change(void) {
void loop() {
touch_read();
idle_tick();
lv_timer_handler();
ui_tick_anim();
ble_tick();
@@ -343,25 +356,48 @@ void loop() {
// LEFT (GPIO 0) → Space (voice-mode push-to-talk; press & release tracked)
// RIGHT (GPIO 18) → Shift+Tab (Claude Code mode toggle)
// PWR (AXP) → cycle screens; on splash, cycle animations
// 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.
{
static bool back_was = false, fwd_was = false;
static bool back_wake_swallowed = false, fwd_wake_swallowed = false;
bool back_now = (digitalRead(BTN_BACK) == LOW);
bool fwd_now = (digitalRead(BTN_FWD) == LOW);
if (back_now != back_was) {
if (back_now) ble_keyboard_press(0x2C, 0); // HID Space, no mods
else ble_keyboard_release();
if (back_now) {
if (idle_consume_wake_press()) {
back_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();
}
back_was = back_now;
}
if (fwd_now != fwd_was) {
if (fwd_now) ble_keyboard_press(0x2B, 0x02); // HID Tab + LEFT_SHIFT
else ble_keyboard_release();
if (fwd_now) {
if (idle_consume_wake_press()) {
fwd_wake_swallowed = true;
} else {
ble_keyboard_press(0x2B, 0x02); // HID Tab + LEFT_SHIFT
}
} else {
if (fwd_wake_swallowed) fwd_wake_swallowed = false;
else ble_keyboard_release();
}
fwd_was = fwd_now;
}
if (power_pwr_pressed()) {
if (ui_get_current_screen() == SCREEN_SPLASH) splash_next();
else ui_cycle_screen();
if (!idle_consume_wake_press()) {
if (ui_get_current_screen() == SCREEN_SPLASH) splash_next();
else ui_cycle_screen();
}
}
}