diff --git a/zephcore/helpers/ui-joystick/joystick_screens.h b/zephcore/helpers/ui-joystick/joystick_screens.h index b8c29c2..49ce533 100644 --- a/zephcore/helpers/ui-joystick/joystick_screens.h +++ b/zephcore/helpers/ui-joystick/joystick_screens.h @@ -21,13 +21,20 @@ class JoystickUITask; /* ===== UIScreen base class ===== * Lifecycle: - * onEnter() — called by JoystickUITask::setCurrScreen when this screen - * becomes active. Start any per-screen k_timer here. - * onExit() — called when navigating away from this screen. Stop any - * per-screen k_timer here so it can't fire on a stale screen. - * render() — draws the current state. Triggered by signals (key event, - * mesh event, screen-owned timer fire), never polled. - * handleInput() — receives one queued key character. + * onEnter() — called by setCurrScreen when this screen becomes + * active. Start any per-screen k_timer here. + * onExit() — called when navigating away. Stop any per-screen + * k_timer here so it can't fire on a stale screen. + * onDisplayOff() — called when the display turns off while this screen + * is active. Pause periodic timers whose work is + * invisible while the screen is off (e.g. game ticks, + * GPS sampling). Keep one-shot timers running if their + * purpose is to fire while idle (alarms, timeouts). + * onDisplayOn() — counterpart of onDisplayOff; resume what you paused. + * render() — draws the current state. Triggered by signals (key + * event, mesh event, screen-owned timer fire), never + * polled. + * handleInput() — receives one queued key character. */ class UIScreen { public: @@ -36,6 +43,8 @@ public: virtual bool handleInput(char c) { (void)c; return false; } virtual void onEnter() {} virtual void onExit() {} + virtual void onDisplayOff() {} + virtual void onDisplayOn() {} }; /* ===== Admin command size limits ===== */ @@ -111,6 +120,8 @@ public: bool handleInput(char c) override; void onEnter() override; void onExit() override; + void onDisplayOff() override; + void onDisplayOn() override; }; /* ===== SystemScreen ===== */ @@ -323,6 +334,8 @@ public: bool handleInput(char c) override; void onEnter() override; void onExit() override; + void onDisplayOff() override; + void onDisplayOn() override; }; /* ===== DoomScreen ===== */ diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index 8e7f31d..1523f93 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -72,6 +72,17 @@ void JoystickUITask::scheduleLockTimer() k_timer_start(&_lock_timer, K_MSEC(LOCK_AFTER_MS), K_NO_WAIT); } +void JoystickUITask::onDisplayStateChanged() +{ + bool is_on = _display.isOn(); + if (is_on == _was_display_on) return; + if (_curr) { + if (is_on) _curr->onDisplayOn(); + else _curr->onDisplayOff(); + } + _was_display_on = is_on; +} + static bool joystick_queue_initialized; #define ENTER_LONG_PRESS_MS 500 @@ -185,7 +196,7 @@ JoystickUITask::JoystickUITask() _doom(nullptr), #endif _repeater_admin(nullptr), _curr(nullptr), - _next_refresh(0), _screen_off_ms(AUTO_OFF_MILLIS), + _next_refresh(0), _screen_off_ms(AUTO_OFF_MILLIS), _was_display_on(false), _cached_batt_mv(0), _battery_display_mode(0), _brightness(100), _wake_on_msg(true), _ble_connected(false), _ble_enabled(true), _msgcount(0), _noise_floor(-120), @@ -461,6 +472,11 @@ void JoystickUITask::loop() * _locked = true and signals refresh). Both are fully event-driven; no * deadline checks needed here. */ + /* Catch display state transitions (mainly display.c's auto-off, which + * happens behind our back) so the current screen can pause/resume any + * periodic timers it owns. */ + onDisplayStateChanged(); + /* Dequeue key events */ char key = 0; if (k_msgq_get(&_key_queue, &key, K_NO_WAIT) == 0) { @@ -470,6 +486,10 @@ void JoystickUITask::loop() * for the lock screen, which the user is about to see. */ ui_invalidate_battery_cache(); _display.turnOn(); /* mc_display_on() already calls mc_display_reset_auto_off() */ + onDisplayStateChanged(); /* dispatch onDisplayOn now, no one-frame delay */ + /* Push the lock deadline forward so a wake-press at the tail end + * of the lock window doesn't get re-locked immediately. */ + if (!_locked) scheduleLockTimer(); _next_refresh = 0; goto do_render; } diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.h b/zephcore/helpers/ui-joystick/joystick_ui_task.h index b9b8320..f9af094 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.h +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.h @@ -229,6 +229,8 @@ private: struct k_timer _lock_timer; /* one-shot, fires LOCK_AFTER_MS after last activity */ static void lockTimerCb(struct k_timer *t); void scheduleLockTimer(); + bool _was_display_on; /* previous display state for transition detection */ + void onDisplayStateChanged(); /* dispatches onDisplayOff/On to _curr on transition */ /* State */ uint16_t _cached_batt_mv; diff --git a/zephcore/helpers/ui-joystick/screens/system.cpp b/zephcore/helpers/ui-joystick/screens/system.cpp index 61e3e32..7334f3d 100644 --- a/zephcore/helpers/ui-joystick/screens/system.cpp +++ b/zephcore/helpers/ui-joystick/screens/system.cpp @@ -646,6 +646,18 @@ void GPSSettingsScreen::onExit() k_timer_stop(&_sample_timer); } +void GPSSettingsScreen::onDisplayOff() +{ + /* Pause the 1 Hz GPS sample loop while the screen is off — the user + * can't see speed/heading and we're just burning ADC + math. */ + k_timer_stop(&_sample_timer); +} + +void GPSSettingsScreen::onDisplayOn() +{ + k_timer_start(&_sample_timer, K_MSEC(1000), K_MSEC(1000)); +} + void GPSSettingsScreen::sampleGPS() { if (!_task->isGPSAvailable() || !_task->getGPSState()) return; diff --git a/zephcore/helpers/ui-joystick/screens/tools.cpp b/zephcore/helpers/ui-joystick/screens/tools.cpp index 48f7e5f..29ddce5 100644 --- a/zephcore/helpers/ui-joystick/screens/tools.cpp +++ b/zephcore/helpers/ui-joystick/screens/tools.cpp @@ -491,6 +491,20 @@ void SnakeScreen::onExit() _tick_due = false; } +void SnakeScreen::onDisplayOff() +{ + /* Pause the game-tick timer while the screen is off — no point burning + * wakes to advance a snake nobody can see. The game state is preserved; + * we'll resume on onDisplayOn(). */ + k_timer_stop(&_tick_timer); + _tick_due = false; +} + +void SnakeScreen::onDisplayOn() +{ + if (s_state == STATE_PLAYING) startTicking(); +} + void SnakeScreen::reset() { int cx = GRID_W / 2, cy = GRID_H / 2;