joystick UI: pause periodic timers on display-off, reset lock on wake

UIScreen gains onDisplayOff()/onDisplayOn() hooks. JoystickUITask tracks
display state and dispatches them on transition — at top of loop()
(catches display.c's auto-off, which fires behind our back) and right
after _display.turnOn() in the wake path (immediate resume).

Override in SnakeScreen and GPSSettingsScreen: stop their periodic
k_timers while the screen is off, restart on wake. Game state and
GPS-fix state are preserved across sleep. Snake doesn't crash into a
wall five seconds after the screen sleeps anymore.

Other screen timers (Countdown alarm, Contacts/Admin response timeout,
the global lock timer) intentionally keep running — their job is to
fire while the user is idle.

Also: wake-from-off now reschedules the lock timer if not already
locked, so a keypress near the end of the lock window gives you a
fresh LOCK_AFTER_MS instead of being immediately re-locked.
This commit is contained in:
liquidraver
2026-05-21 13:18:49 +02:00
parent a38f789b29
commit 2630ce9096
5 changed files with 69 additions and 8 deletions
@@ -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 ===== */
@@ -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;
}
@@ -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;
@@ -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;
@@ -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;