Merge pull request #24 from tobby168/feature/auto-sleep
Auto-sleep screen after 30 min idle
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
+65
-4
@@ -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,27 +381,50 @@ 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
|
||||
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
|
||||
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 (!idle_consume_wake_press()) {
|
||||
if (ui_get_current_screen() == SCREEN_SPLASH) splash_next();
|
||||
else ui_cycle_screen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle_rotation_change();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user