diff --git a/firmware/src/idle.cpp b/firmware/src/idle.cpp new file mode 100644 index 0000000..c8d9192 --- /dev/null +++ b/firmware/src/idle.cpp @@ -0,0 +1,103 @@ +#include +#include "idle.h" +#include "idle_cfg.h" +#include "display_cfg.h" // declares `extern Arduino_CO5300 *gfx;` + +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(); + + 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; + } +} diff --git a/firmware/src/idle.h b/firmware/src/idle.h new file mode 100644 index 0000000..0ce424c --- /dev/null +++ b/firmware/src/idle.h @@ -0,0 +1,16 @@ +#pragma once +#include + +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); diff --git a/firmware/src/idle_cfg.h b/firmware/src/idle_cfg.h new file mode 100644 index 0000000..33ce2ec --- /dev/null +++ b/firmware/src/idle_cfg.h @@ -0,0 +1,11 @@ +#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 diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 65552cf..91b2b1e 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -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(); + } } }