Merge pull request #24 from tobby168/feature/auto-sleep

Auto-sleep screen after 30 min idle
This commit is contained in:
Hermann Björgvin
2026-05-20 17:36:20 +00:00
committed by GitHub
6 changed files with 230 additions and 8 deletions
+114
View File
@@ -0,0 +1,114 @@
#include <Arduino.h>
#include "idle.h"
#include "idle_cfg.h"
#include "display_cfg.h" // declares `extern Arduino_CO5300 *gfx;`
#include "power.h"
enum IdleState {
STATE_AWAKE,
STATE_FADING_OUT,
STATE_ASLEEP,
STATE_FADING_IN,
};
static IdleState state = STATE_AWAKE;
static uint32_t last_activity_ms = 0;
static uint32_t fade_started_ms = 0;
static uint32_t fade_last_step_ms = 0;
static uint8_t fade_from = DISPLAY_DEFAULT_BRIGHTNESS;
static uint8_t fade_to = 0;
static void apply_brightness(uint8_t b) {
gfx->setBrightness(b);
}
static void begin_fade(uint8_t to, uint32_t now) {
fade_from = (to == 0) ? DISPLAY_DEFAULT_BRIGHTNESS : 0;
fade_to = to;
fade_started_ms = now;
fade_last_step_ms = now;
}
void idle_init(void) {
state = STATE_AWAKE;
last_activity_ms = millis();
apply_brightness(DISPLAY_DEFAULT_BRIGHTNESS);
}
void idle_note_activity(void) {
last_activity_ms = millis();
if (state == STATE_FADING_IN) return;
if (state == STATE_AWAKE) return;
// Asleep/fading-out shouldn't reach here in normal flow (callers gate via
// idle_consume_wake_press first), but if it does: trigger a wake.
begin_fade(DISPLAY_DEFAULT_BRIGHTNESS, last_activity_ms);
state = STATE_FADING_IN;
}
bool idle_consume_wake_press(void) {
if (state == STATE_ASLEEP || state == STATE_FADING_OUT) {
uint32_t now = millis();
last_activity_ms = now;
begin_fade(DISPLAY_DEFAULT_BRIGHTNESS, now);
state = STATE_FADING_IN;
return true;
}
if (state == STATE_FADING_IN) {
// Mid-wake — still swallow this press; the user shouldn't get a
// half-wake half-action surprise.
last_activity_ms = millis();
return true;
}
last_activity_ms = millis();
return false;
}
bool idle_is_asleep(void) {
return state == STATE_ASLEEP || state == STATE_FADING_OUT;
}
void idle_tick(void) {
uint32_t now = millis();
// 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()) {
last_activity_ms = now;
if (state == STATE_ASLEEP || state == STATE_FADING_OUT) {
begin_fade(DISPLAY_DEFAULT_BRIGHTNESS, now);
state = STATE_FADING_IN;
}
}
switch (state) {
case STATE_AWAKE:
if (now - last_activity_ms >= IDLE_TIMEOUT_MS) {
begin_fade(0, now);
state = STATE_FADING_OUT;
}
break;
case STATE_FADING_OUT:
case STATE_FADING_IN: {
if (now - fade_last_step_ms < IDLE_FADE_STEP_MS) break;
fade_last_step_ms = now;
uint32_t dur = (state == STATE_FADING_OUT) ? IDLE_FADE_OUT_MS : IDLE_FADE_IN_MS;
uint32_t elapsed = now - fade_started_ms;
if (elapsed >= dur) {
apply_brightness(fade_to);
state = (state == STATE_FADING_OUT) ? STATE_ASLEEP : STATE_AWAKE;
} else {
// Linear interpolation fade_from -> fade_to over dur ms.
int32_t span = (int32_t)fade_to - (int32_t)fade_from;
int32_t b = (int32_t)fade_from + (span * (int32_t)elapsed) / (int32_t)dur;
if (b < 0) b = 0;
if (b > 255) b = 255;
apply_brightness((uint8_t)b);
}
break;
}
case STATE_ASLEEP:
break;
}
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <stdbool.h>
void idle_init(void);
void idle_tick(void);
void idle_note_activity(void);
// Returns true if this press was consumed as a wake-up (caller MUST skip the
// button's normal action). Returns false when already awake — also notes the
// activity, so callers don't need a separate idle_note_activity() call.
bool idle_consume_wake_press(void);
// Touch should NOT count as activity (avoids accidental wakes from pets,
// sleeves, etc.). Callers use this to silently drop touch events while the
// panel is dark.
bool idle_is_asleep(void);
+23
View File
@@ -0,0 +1,23 @@
#pragma once
// Auto-sleep / idle screen-off configuration.
// All tunables live here so nothing is hard-coded in main.cpp / idle.cpp.
#define IDLE_TIMEOUT_MS (30UL * 60UL * 1000UL) // 30 min
#define IDLE_FADE_OUT_MS 400 // fade-to-black duration
#define IDLE_FADE_IN_MS 180 // wake fade-in (snappier)
#define IDLE_FADE_STEP_MS 20 // tick interval per fade step
#define DISPLAY_DEFAULT_BRIGHTNESS 200 // active-screen brightness
// When false, the device never enters sleep while USB power is present (also
// wakes from sleep when USB is plugged back in). Useful when sitting on a
// desk plugged in — also covers battery-less hardware that's always on USB.
// Set true to sleep regardless of power source.
#define IDLE_SLEEP_WHEN_CHARGING false
// When true, a touch on the dark panel wakes the device (first touch is
// consumed for wake only, second touch acts normally). When false, touch is
// fully ignored during sleep — useful if cats/sleeves brushing the panel
// overnight would be a problem.
#define IDLE_WAKE_ON_TOUCH true
+69 -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,36 @@ static void touch_read() {
} else {
touch_pressed = false;
}
// 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) ----
@@ -234,7 +266,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 +342,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 +361,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 +369,7 @@ static void handle_rotation_change(void) {
void loop() {
touch_read();
idle_tick();
lv_timer_handler();
ui_tick_anim();
ble_tick();
@@ -343,25 +381,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();
}
}
}
+7
View File
@@ -8,6 +8,7 @@
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;
@@ -30,6 +31,7 @@ void power_init(void) {
pmu.enableIRQ(XPOWERS_AXP2101_PKEY_SHORT_IRQ);
cached_charging = pmu.isCharging();
cached_vbus = pmu.isVbusIn();
cached_pct = pmu.getBatteryPercent();
}
@@ -39,6 +41,7 @@ void power_tick(void) {
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) {
@@ -65,6 +68,10 @@ bool power_is_charging(void) {
return cached_charging;
}
bool power_is_vbus_in(void) {
return cached_vbus;
}
bool power_pwr_pressed(void) {
if (pwr_pressed_flag) {
pwr_pressed_flag = false;
+1
View File
@@ -4,4 +4,5 @@ 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