From a01af82c2b80914888bc8e69fcc23e5daf67add4 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Thu, 21 May 2026 12:53:00 +0200 Subject: [PATCH] joystick UI: replace per-screen poll() with onEnter/onExit + k_timers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UIScreen gains onEnter()/onExit() lifecycle hooks; poll() and the _curr->poll() call in the main loop are removed. Each screen with periodic or deadline-based work owns its own k_timer: - one-shot timers: Splash dismiss, Countdown alarm, Contacts ping timeout, RepeaterAdmin cmd/login timeout, Unread preview expiry - periodic timers: Snake tick, GPSSettings sample - onEnter()-only: Repeaters discover, Doom start - deleted: Home, Stopwatch (were empty) Timer ISR callbacks only signal _task->notify() — never mutate screen state. Main-thread render() handles transitions. Setting _curr now fires onExit on the outgoing screen and onEnter on the incoming one, so timers are scoped to screen lifetime and can't fire stale events on the wrong screen. --- .../helpers/ui-joystick/joystick_screens.h | 59 +++++++-- .../helpers/ui-joystick/joystick_ui_task.cpp | 8 +- .../helpers/ui-joystick/screens/contacts.cpp | 125 ++++++++++++------ zephcore/helpers/ui-joystick/screens/home.cpp | 36 +++-- .../helpers/ui-joystick/screens/messaging.cpp | 42 ++++-- .../helpers/ui-joystick/screens/system.cpp | 25 +++- .../helpers/ui-joystick/screens/tools.cpp | 97 ++++++++++---- 7 files changed, 282 insertions(+), 110 deletions(-) diff --git a/zephcore/helpers/ui-joystick/joystick_screens.h b/zephcore/helpers/ui-joystick/joystick_screens.h index 63a70ab..0d321b9 100644 --- a/zephcore/helpers/ui-joystick/joystick_screens.h +++ b/zephcore/helpers/ui-joystick/joystick_screens.h @@ -14,17 +14,28 @@ #include #include #include +#include #include "CompanionMesh.h" class JoystickUITask; -/* ===== UIScreen base class ===== */ +/* ===== 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. + */ class UIScreen { public: virtual ~UIScreen() {} virtual int render(JoystickDisplay &display) = 0; virtual bool handleInput(char c) { (void)c; return false; } - virtual void poll() {} + virtual void onEnter() {} + virtual void onExit() {} }; /* ===== Admin command size limits ===== */ @@ -39,10 +50,13 @@ public: class SplashScreen : public UIScreen { JoystickUITask *_task; uint32_t _dismiss_after; + struct k_timer _dismiss_timer; + static void dismissTimerCb(struct k_timer *t); public: SplashScreen(JoystickUITask *task); int render(JoystickDisplay &display) override; - void poll() override; + void onEnter() override; + void onExit() override; }; /* ===== HomeScreen ===== */ @@ -52,7 +66,6 @@ class HomeScreen : public UIScreen { int _selected; public: HomeScreen(JoystickUITask *task, mesh::RTCClock *rtc); - void poll() override; int render(JoystickDisplay &display) override; bool handleInput(char c) override; }; @@ -89,11 +102,15 @@ class GPSSettingsScreen : public UIScreen { float _speed_kmh, _heading_deg; bool _heading_valid; uint32_t _heading_hold_until; + struct k_timer _sample_timer; + static void sampleTimerCb(struct k_timer *t); + void sampleGPS(); public: GPSSettingsScreen(JoystickUITask *task, mesh::RTCClock *rtc); int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; + void onEnter() override; + void onExit() override; }; /* ===== SystemScreen ===== */ @@ -164,7 +181,7 @@ public: RepeatersScreen(JoystickUITask *task, mesh::RTCClock *rtc); int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; + void onEnter() override; }; /* ===== ChannelsScreen ===== */ @@ -261,7 +278,6 @@ public: StopwatchScreen(JoystickUITask *task); int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; }; /* ===== CountdownScreen ===== */ @@ -272,11 +288,13 @@ class CountdownScreen : public UIScreen { int _set_seconds; int _edit_field; bool _alarmed; + struct k_timer _alarm_timer; + static void alarmTimerCb(struct k_timer *t); public: CountdownScreen(JoystickUITask *task); int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; + void onExit() override; }; /* ===== SnakeScreen ===== */ @@ -288,16 +306,21 @@ class SnakeScreen : public UIScreen { int8_t _snake_y[GRID_W * GRID_H]; int _snake_len; int8_t _food_x, _food_y; - uint32_t _next_move; int _score; + struct k_timer _tick_timer; + volatile bool _tick_due; /* set in timer ISR, consumed in render */ + static void tickTimerCb(struct k_timer *t); void placeFood(); void reset(); + void advanceGame(); + void startTicking(); public: SnakeScreen(JoystickUITask *task); int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; + void onEnter() override; + void onExit() override; }; /* ===== DoomScreen ===== */ @@ -308,7 +331,7 @@ public: DoomScreen(JoystickUITask *task); int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; + void onEnter() override; }; #endif @@ -352,6 +375,8 @@ class ContactsScreen : public UIScreen { int8_t _ping_snr_remote; /* SNR of our ping as received by repeater, INT8_MIN if unknown */ uint32_t _ping_rtt_ms; /* RTT: 0=no result yet, UINT32_MAX=timeout, else ms */ bool _ping_modal_active; /* modal overlay visible */ + struct k_timer _ping_timeout_timer; + static void pingTimeoutCb(struct k_timer *t); int clampStart(int contactCount) const; int getFilteredContactCount() const; @@ -366,7 +391,7 @@ public: void onPacketSent(); int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; + void onExit() override; }; /* ===== UnreadScreen ===== */ @@ -392,6 +417,8 @@ class UnreadScreen : public UIScreen { int _detail_scroll; bool _transient_preview; uint32_t _preview_expiry; + struct k_timer _preview_timer; + static void previewTimerCb(struct k_timer *t); void normalizeUnreadState(); const MsgEntry *getByListIndex(int idx) const; @@ -414,7 +441,7 @@ public: uint32_t &out_ts, uint8_t *out_path = nullptr) const; int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; + void onExit() override; }; /* ===== RepeaterAdminScreen ===== */ @@ -432,9 +459,13 @@ public: void onPacketSent(); int render(JoystickDisplay &display) override; bool handleInput(char c) override; - void poll() override; + void onExit() override; private: + struct k_timer _timeout_timer; + static void timeoutTimerCb(struct k_timer *t); + void onTimeout(); /* called from main thread when _timeout_timer fires */ + JoystickUITask *_task; mesh::RTCClock *_rtc; AdminState _state; diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index f67ba23..a1e8123 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -257,7 +257,10 @@ void JoystickUITask::begin(BaseChatMesh *mesh, mesh::ZephyrRTCClock *rtc, NodePr /* ===== setCurrScreen / navigation ===== */ void JoystickUITask::setCurrScreen(UIScreen *s) { + if (_curr == s) return; /* no-op for re-entry */ + if (_curr) _curr->onExit(); _curr = s; + if (_curr) _curr->onEnter(); _next_refresh = 0; } @@ -492,11 +495,6 @@ void JoystickUITask::loop() } } - /* Poll current screen */ - if (_curr) { - _curr->poll(); - } - do_render: /* Render if due */ if (_display.isOn() && now >= _next_refresh && _curr) { diff --git a/zephcore/helpers/ui-joystick/screens/contacts.cpp b/zephcore/helpers/ui-joystick/screens/contacts.cpp index 4f798c8..f9cd618 100644 --- a/zephcore/helpers/ui-joystick/screens/contacts.cpp +++ b/zephcore/helpers/ui-joystick/screens/contacts.cpp @@ -45,6 +45,20 @@ ContactsScreen::ContactsScreen(JoystickUITask *task, mesh::RTCClock *rtc) { _active_contact = ContactInfo{}; memset(_editpath_hexbuf, 0, sizeof(_editpath_hexbuf)); + k_timer_init(&_ping_timeout_timer, pingTimeoutCb, NULL); + k_timer_user_data_set(&_ping_timeout_timer, this); +} + +void ContactsScreen::pingTimeoutCb(struct k_timer *t) +{ + /* ISR — just wake the main loop; render() handles the timeout. */ + auto *self = static_cast(k_timer_user_data_get(t)); + if (self && self->_task) self->_task->notify(); +} + +void ContactsScreen::onExit() +{ + k_timer_stop(&_ping_timeout_timer); } /* Filter helpers */ @@ -263,6 +277,13 @@ static void drawPingModal(JoystickDisplay &display, int ContactsScreen::render(JoystickDisplay &display) { + /* Ping timeout: timer fires when _ping_timeout_ms elapsed; mark timeout. */ + if (_ping_sent_at > 0 && _ping_timeout_ms > 0 && + (k_uptime_get_32() - _ping_sent_at) >= _ping_timeout_ms) { + _ping_sent_at = 0; + _ping_rtt_ms = UINT32_MAX; + } + if (_mode == CMODE_SUBMENU || _mode == CMODE_MSGVIEW) { if (!refreshActiveContact()) { _mode = CMODE_LIST; @@ -690,6 +711,7 @@ void ContactsScreen::onPingResponse(int8_t snr_local, int8_t snr_remote, uint32_ _ping_rtt_ms = (rtt_ms == 0) ? 1 : rtt_ms; _ping_sent_at = 0; _ping_modal_active = true; + k_timer_stop(&_ping_timeout_timer); /* got a response before timeout */ _task->forceRefresh(); } @@ -698,19 +720,10 @@ void ContactsScreen::onPacketSent() /* RF TX just completed — start the ping timeout clock now. */ if (_ping_modal_active && _ping_sent_at == 0) { _ping_sent_at = k_uptime_get_32(); - _task->forceRefresh(); - } -} - -void ContactsScreen::poll() -{ - if (_ping_sent_at > 0 && _ping_timeout_ms > 0) { - uint32_t elapsed = k_uptime_get_32() - _ping_sent_at; - if (elapsed >= _ping_timeout_ms) { - _ping_sent_at = 0; - _ping_rtt_ms = UINT32_MAX; - _task->forceRefresh(); + if (_ping_timeout_ms > 0) { + k_timer_start(&_ping_timeout_timer, K_MSEC(_ping_timeout_ms), K_NO_WAIT); } + _task->forceRefresh(); } } @@ -743,6 +756,42 @@ RepeaterAdminScreen::RepeaterAdminScreen(JoystickUITask *task, mesh::RTCClock *r memset(_password, 0, sizeof(_password)); memset(_hist, 0, sizeof(_hist)); memset(_cmd_buf, 0, sizeof(_cmd_buf)); + k_timer_init(&_timeout_timer, timeoutTimerCb, NULL); + k_timer_user_data_set(&_timeout_timer, this); +} + +void RepeaterAdminScreen::timeoutTimerCb(struct k_timer *t) +{ + /* ISR — wake main loop; render() detects elapsed and dispatches to onTimeout(). */ + auto *self = static_cast(k_timer_user_data_get(t)); + if (self && self->_task) self->_task->notify(); +} + +void RepeaterAdminScreen::onExit() +{ + k_timer_stop(&_timeout_timer); +} + +void RepeaterAdminScreen::onTimeout() +{ + if (_state == STATE_LOGGING_IN) { + _awaiting_tx = false; + _state = STATE_PASSWORD_ENTRY; + _task->showAlert("Login timeout", 1500); + } else if ((_state == STATE_SUBMENU || _state == STATE_MAIN || _state == STATE_CMD_INPUT) && + _pending != PENDING_NONE) { + _awaiting_tx = false; + if (_hist_count > 0) { + CmdEntry &newest = histAt(_hist_count - 1); + if (!newest.has_resp) { + snprintf(newest.resp, sizeof(newest.resp), "(no response)"); + newest.has_resp = true; + } + } + _pending = PENDING_NONE; + _task->clearAdminReqTag(); + } + _last_sent_at = 0; /* prevent re-fire from the render() elapsed-check */ } /* ===== openForContact ===== */ @@ -1010,6 +1059,7 @@ void RepeaterAdminScreen::onReqResponse(const uint8_t *pub_key_prefix, } _pending = PENDING_NONE; _task->clearAdminReqTag(); + k_timer_stop(&_timeout_timer); } /* ===== sendCLI ===== */ @@ -1039,6 +1089,8 @@ bool RepeaterAdminScreen::sendCLI(const char *cmd) uint32_t rt = est_timeout * 2 + 3000; if (rt > _cmd_timeout_ms) _cmd_timeout_ms = rt; } + k_timer_stop(&_timeout_timer); + k_timer_start(&_timeout_timer, K_MSEC(_cmd_timeout_ms), K_NO_WAIT); return true; } @@ -1047,6 +1099,7 @@ bool RepeaterAdminScreen::sendCLI(const char *cmd) void RepeaterAdminScreen::onLoginResult(bool success, uint8_t permissions, uint32_t server_time) { if (_state != STATE_LOGGING_IN) return; + k_timer_stop(&_timeout_timer); if (success) { _permissions = permissions; _server_time = server_time; @@ -1065,6 +1118,7 @@ void RepeaterAdminScreen::onCliResponse(const char *text) { if (!text || _pending != PENDING_CMD) return; if (_state != STATE_MAIN && _state != STATE_CMD_INPUT && _state != STATE_SUBMENU) return; + k_timer_stop(&_timeout_timer); if (_hist_count > 0) { CmdEntry &newest = histAt(_hist_count - 1); if (!newest.has_resp) { @@ -1080,38 +1134,13 @@ void RepeaterAdminScreen::onCliResponse(const char *text) void RepeaterAdminScreen::onPacketSent() { - /* RF TX completed — reset timeout clock so LBT/queue delay is excluded */ + /* RF TX completed — reset timeout clock so LBT/queue delay is excluded. + * Restart the response-timeout timer with the full window from now. */ if (_awaiting_tx) { _last_sent_at = k_uptime_get_32(); _awaiting_tx = false; - } -} - -/* ===== poll ===== */ - -void RepeaterAdminScreen::poll() -{ - if (_state == STATE_LOGGING_IN) { - if (_last_sent_at > 0 && - k_uptime_get_32() - _last_sent_at > _cmd_timeout_ms) { - _awaiting_tx = false; - _state = STATE_PASSWORD_ENTRY; - _task->showAlert("Login timeout", 1500); - } - } else if ((_state == STATE_SUBMENU || _state == STATE_MAIN || _state == STATE_CMD_INPUT) && _pending != PENDING_NONE) { - if (_last_sent_at > 0 && - k_uptime_get_32() - _last_sent_at > _cmd_timeout_ms) { - _awaiting_tx = false; - if (_hist_count > 0) { - CmdEntry &newest = histAt(_hist_count - 1); - if (!newest.has_resp) { - snprintf(newest.resp, sizeof(newest.resp), "(no response)"); - newest.has_resp = true; - } - } - _pending = PENDING_NONE; - _task->clearAdminReqTag(); - } + k_timer_stop(&_timeout_timer); + k_timer_start(&_timeout_timer, K_MSEC(_cmd_timeout_ms), K_NO_WAIT); } } @@ -1150,6 +1179,12 @@ static void renderAdminHeader(JoystickDisplay &display, const char *name, bool i int RepeaterAdminScreen::render(JoystickDisplay &display) { + /* Timeout check: timer fired (or any wakeup arrived past deadline). */ + if (_last_sent_at > 0 && + (k_uptime_get_32() - _last_sent_at) > _cmd_timeout_ms) { + onTimeout(); + } + switch (_state) { case STATE_PASSWORD_ENTRY: { @@ -1363,6 +1398,8 @@ bool RepeaterAdminScreen::handleInput(char c) uint32_t rt = est_timeout * 2 + 3000; if (rt > _cmd_timeout_ms) _cmd_timeout_ms = rt; } + k_timer_stop(&_timeout_timer); + k_timer_start(&_timeout_timer, K_MSEC(_cmd_timeout_ms), K_NO_WAIT); } else { _task->showAlert("Send failed", 1500); } @@ -1486,7 +1523,11 @@ bool RepeaterAdminScreen::handleInput(char c) } sent = sendBinaryReqHelper(_task, _contact_pubkey, req, req_len, _cmd_timeout_ms, _awaiting_tx, _last_sent_at); - if (sent) _pending = d.pending; + if (sent) { + _pending = d.pending; + k_timer_stop(&_timeout_timer); + k_timer_start(&_timeout_timer, K_MSEC(_cmd_timeout_ms), K_NO_WAIT); + } } if (!sent) { diff --git a/zephcore/helpers/ui-joystick/screens/home.cpp b/zephcore/helpers/ui-joystick/screens/home.cpp index a4a35df..ee02c3b 100644 --- a/zephcore/helpers/ui-joystick/screens/home.cpp +++ b/zephcore/helpers/ui-joystick/screens/home.cpp @@ -18,11 +18,6 @@ HomeScreen::HomeScreen(JoystickUITask *task, mesh::RTCClock *rtc) { } -void HomeScreen::poll() -{ - /* Nothing periodic needed */ -} - int HomeScreen::render(JoystickDisplay &display) { static char unread_item[24]; @@ -219,13 +214,35 @@ bool HomeScreen::handleInput(char c) #endif SplashScreen::SplashScreen(JoystickUITask *task) - : _task(task) + : _task(task), _dismiss_after(0) +{ + k_timer_init(&_dismiss_timer, dismissTimerCb, NULL); + k_timer_user_data_set(&_dismiss_timer, this); +} + +void SplashScreen::dismissTimerCb(struct k_timer *t) +{ + auto *self = static_cast(k_timer_user_data_get(t)); + if (self && self->_task) self->_task->notify(); +} + +void SplashScreen::onEnter() { _dismiss_after = k_uptime_get_32() + BOOT_SCREEN_MILLIS; + k_timer_start(&_dismiss_timer, K_MSEC(BOOT_SCREEN_MILLIS), K_NO_WAIT); +} + +void SplashScreen::onExit() +{ + k_timer_stop(&_dismiss_timer); } int SplashScreen::render(JoystickDisplay &display) { + if (k_uptime_get_32() >= _dismiss_after) { + _task->gotoHomeScreen(); + return 0; + } int cx = display.width() / 2; display.setColor(JoystickDisplay::GREEN); display.drawTextCentered(cx, display.height() / 4, "MeshCore"); @@ -234,10 +251,3 @@ int SplashScreen::render(JoystickDisplay &display) display.drawTextCentered(cx, display.height() / 2 + 4 + display.fontH() + 2, FIRMWARE_BUILD_DATE); return 250; } - -void SplashScreen::poll() -{ - if (k_uptime_get_32() >= _dismiss_after) { - _task->gotoHomeScreen(); - } -} diff --git a/zephcore/helpers/ui-joystick/screens/messaging.cpp b/zephcore/helpers/ui-joystick/screens/messaging.cpp index 81d450c..5b6402f 100644 --- a/zephcore/helpers/ui-joystick/screens/messaging.cpp +++ b/zephcore/helpers/ui-joystick/screens/messaging.cpp @@ -26,6 +26,20 @@ UnreadScreen::UnreadScreen(JoystickUITask *task, mesh::RTCClock *rtc) _entries[i].msg[0] = '\0'; _entries[i].read = true; } + k_timer_init(&_preview_timer, previewTimerCb, NULL); + k_timer_user_data_set(&_preview_timer, this); +} + +void UnreadScreen::previewTimerCb(struct k_timer *t) +{ + /* ISR — just wake main loop; render() detects the expiry and dismisses. */ + auto *self = static_cast(k_timer_user_data_get(t)); + if (self && self->_task) self->_task->notify(); +} + +void UnreadScreen::onExit() +{ + k_timer_stop(&_preview_timer); } void UnreadScreen::normalizeUnreadState() @@ -64,7 +78,13 @@ void UnreadScreen::activatePreview(bool transient, uint32_t timeout_ms) _list_scroll = 0; if (_selected >= _visible_unread_count) _selected = (_visible_unread_count > 0) ? _visible_unread_count - 1 : 0; - _preview_expiry = transient ? (k_uptime_get_32() + timeout_ms) : 0; + if (transient) { + _preview_expiry = k_uptime_get_32() + timeout_ms; + k_timer_start(&_preview_timer, K_MSEC(timeout_ms), K_NO_WAIT); + } else { + _preview_expiry = 0; + k_timer_stop(&_preview_timer); + } } const UnreadScreen::MsgEntry *UnreadScreen::getByListIndex(int idx) const @@ -205,6 +225,15 @@ void UnreadScreen::markCurrentRead() int UnreadScreen::render(JoystickDisplay &display) { + /* Preview expiry: dismiss transient preview when its timer elapses. */ + if (_transient_preview && _preview_expiry > 0 && k_uptime_get_32() >= _preview_expiry) { + _transient_preview = false; + _preview_expiry = 0; + _details = false; + _task->gotoHomeScreen(); + return 0; + } + normalizeUnreadState(); if (_visible_unread_count == 0) { renderScreenHeader(display, "Unread", 0, 0); @@ -316,17 +345,6 @@ bool UnreadScreen::handleInput(char key) return false; } -void UnreadScreen::poll() -{ - normalizeUnreadState(); - if (_transient_preview && _preview_expiry > 0 && k_uptime_get_32() >= _preview_expiry) { - _transient_preview = false; - _preview_expiry = 0; - _details = false; - _task->gotoHomeScreen(); - } -} - ChannelsScreen::ChannelsScreen(JoystickUITask *task, mesh::RTCClock *rtc) : _task(task), _rtc(rtc), _selected(0), _show_msgs(false), _msg_channel_idx(-1), _msg_scroll(0), diff --git a/zephcore/helpers/ui-joystick/screens/system.cpp b/zephcore/helpers/ui-joystick/screens/system.cpp index 7e1e181..61e3e32 100644 --- a/zephcore/helpers/ui-joystick/screens/system.cpp +++ b/zephcore/helpers/ui-joystick/screens/system.cpp @@ -623,9 +623,30 @@ GPSSettingsScreen::GPSSettingsScreen(JoystickUITask *task, mesh::RTCClock *rtc) _last_sample_ms(0), _speed_kmh(0.0f), _heading_deg(0.0f), _heading_valid(false), _heading_hold_until(0) { + k_timer_init(&_sample_timer, sampleTimerCb, NULL); + k_timer_user_data_set(&_sample_timer, this); } -void GPSSettingsScreen::poll() +void GPSSettingsScreen::sampleTimerCb(struct k_timer *t) +{ + /* ISR context — just wake the main loop; sampling happens in + * render() (main thread) via sampleGPS(). */ + auto *self = static_cast(k_timer_user_data_get(t)); + if (self && self->_task) self->_task->notify(); +} + +void GPSSettingsScreen::onEnter() +{ + /* Sample GPS once per second while this screen is active. */ + k_timer_start(&_sample_timer, K_MSEC(1000), K_MSEC(1000)); +} + +void GPSSettingsScreen::onExit() +{ + k_timer_stop(&_sample_timer); +} + +void GPSSettingsScreen::sampleGPS() { if (!_task->isGPSAvailable() || !_task->getGPSState()) return; struct gps_position pos; @@ -670,6 +691,8 @@ void GPSSettingsScreen::poll() int GPSSettingsScreen::render(JoystickDisplay &display) { + sampleGPS(); /* timer fires every 1s and wakes us here; sample now */ + bool gps_enabled = _task->getGPSState(); char gps_state_line[24]; diff --git a/zephcore/helpers/ui-joystick/screens/tools.cpp b/zephcore/helpers/ui-joystick/screens/tools.cpp index a7bead5..8e4ba5e 100644 --- a/zephcore/helpers/ui-joystick/screens/tools.cpp +++ b/zephcore/helpers/ui-joystick/screens/tools.cpp @@ -83,8 +83,9 @@ RepeatersScreen::RepeatersScreen(JoystickUITask *task, mesh::RTCClock *rtc) s_scan_until = 0; s_scan_sent = false; } -void RepeatersScreen::poll() +void RepeatersScreen::onEnter() { + /* One-shot discover per device boot — guarded by s_scan_sent. */ if (s_scan_sent) return; s_scan_sent = true; s_scan_until = k_uptime_get_32() + REPEATER_SCAN_WINDOW_MS; @@ -264,21 +265,34 @@ CountdownScreen::CountdownScreen(JoystickUITask *task) : _task(task), _running(false), _end_ms(0), _set_seconds(60), _edit_field(0), _alarmed(false) { + k_timer_init(&_alarm_timer, alarmTimerCb, NULL); + k_timer_user_data_set(&_alarm_timer, this); } -void CountdownScreen::poll() +void CountdownScreen::alarmTimerCb(struct k_timer *t) { - if (!_running) return; - if (k_uptime_get_32() >= _end_ms) { + /* ISR context — just signal refresh; render() detects the elapsed + * deadline against _end_ms and fires the alarm UX in main thread. */ + auto *self = static_cast(k_timer_user_data_get(t)); + if (self && self->_task) self->_task->notify(); +} + +void CountdownScreen::onExit() +{ + k_timer_stop(&_alarm_timer); +} + +int CountdownScreen::render(JoystickDisplay &display) +{ + /* Alarm path: timer fired (or any wakeup arrived after deadline); + * fire alarm UX once and clear _running. */ + if (_running && k_uptime_get_32() >= _end_ms) { _running = false; _alarmed = true; _task->playCountdownAlarm(); _task->showAlert("Countdown done!", 1000); } -} -int CountdownScreen::render(JoystickDisplay &display) -{ renderScreenHeader(display, "Countdown", 0, 0); int remaining = _set_seconds; @@ -309,10 +323,12 @@ bool CountdownScreen::handleInput(char c) if (c == KEY_ENTER) { if (_running) { _running = false; + k_timer_stop(&_alarm_timer); } else { _end_ms = k_uptime_get_32() + (uint32_t)_set_seconds * 1000UL; _running = true; _alarmed = false; + k_timer_start(&_alarm_timer, K_MSEC((uint32_t)_set_seconds * 1000UL), K_NO_WAIT); } return true; } @@ -333,6 +349,7 @@ bool CountdownScreen::handleInput(char c) _alarmed = false; _set_seconds = 60; _end_ms = 0; + k_timer_stop(&_alarm_timer); return true; } if (c == KEY_CANCEL || c == KEY_HOME) { _task->gotoToolsScreen(); return true; } @@ -346,8 +363,6 @@ StopwatchScreen::StopwatchScreen(JoystickUITask *task) memset(_lap_times, 0, sizeof(_lap_times)); } -void StopwatchScreen::poll() { /* no autonomous action needed */ } - int StopwatchScreen::render(JoystickDisplay &display) { renderScreenHeader(display, "Stopwatch", 0, 0); @@ -433,14 +448,40 @@ static int s_state = STATE_READY; SnakeScreen::SnakeScreen(JoystickUITask *task) : _task(task), _snake_len(0), - _food_x(0), _food_y(0), - _next_move(0), _score(0) + _food_x(0), _food_y(0), _score(0), + _tick_due(false) { memset(_snake_x, 0, sizeof(_snake_x)); memset(_snake_y, 0, sizeof(_snake_y)); + k_timer_init(&_tick_timer, tickTimerCb, NULL); + k_timer_user_data_set(&_tick_timer, this); reset(); } +void SnakeScreen::tickTimerCb(struct k_timer *t) +{ + auto *self = static_cast(k_timer_user_data_get(t)); + if (!self) return; + self->_tick_due = true; + if (self->_task) self->_task->notify(); +} + +void SnakeScreen::startTicking() +{ + k_timer_start(&_tick_timer, K_MSEC(SNAKE_TICK_MS), K_MSEC(SNAKE_TICK_MS)); +} + +void SnakeScreen::onEnter() +{ + if (s_state == STATE_PLAYING) startTicking(); +} + +void SnakeScreen::onExit() +{ + k_timer_stop(&_tick_timer); + _tick_due = false; +} + void SnakeScreen::reset() { int cx = GRID_W / 2, cy = GRID_H / 2; @@ -452,7 +493,7 @@ void SnakeScreen::reset() s_ndir_x = 1; s_ndir_y = 0; _score = 0; s_state = STATE_READY; - _next_move = 0; + _tick_due = false; placeFood(); } @@ -478,20 +519,17 @@ static bool headHitsBody(const int8_t *sx, const int8_t *sy, int len) return false; } -void SnakeScreen::poll() +void SnakeScreen::advanceGame() { - if (s_state != STATE_PLAYING) return; - uint32_t now = k_uptime_get_32(); - if (now < _next_move) return; - _next_move = now + SNAKE_TICK_MS; - s_dir_x = s_ndir_x; s_dir_y = s_ndir_y; int8_t nx = _snake_x[0] + s_dir_x; int8_t ny = _snake_y[0] + s_dir_y; if (nx < 0 || ny < 0 || nx >= GRID_W || ny >= GRID_H) { - s_state = STATE_OVER; return; + s_state = STATE_OVER; + k_timer_stop(&_tick_timer); + return; } bool eat = (nx == _food_x && ny == _food_y); @@ -504,13 +542,23 @@ void SnakeScreen::poll() _snake_x[0] = nx; _snake_y[0] = ny; if (headHitsBody(_snake_x, _snake_y, _snake_len)) { - s_state = STATE_OVER; return; + s_state = STATE_OVER; + k_timer_stop(&_tick_timer); + return; } if (eat) { _score++; placeFood(); } } int SnakeScreen::render(JoystickDisplay &display) { + /* Advance game on tick fire (set by k_timer ISR). */ + if (_tick_due) { + _tick_due = false; + if (s_state == STATE_PLAYING) { + advanceGame(); + } + } + char title[24]; snprintf(title, sizeof(title), "SNAKE Score:%d", _score); display.setColor(JoystickDisplay::GREEN); @@ -549,11 +597,14 @@ bool SnakeScreen::handleInput(char c) if (c == KEY_RIGHT && s_dir_x == 0) { s_ndir_x = 1; s_ndir_y = 0; return true; } if (c == KEY_ENTER) { if (s_state == STATE_READY || s_state == STATE_OVER) { - reset(); s_state = STATE_PLAYING; _next_move = k_uptime_get_32() + SNAKE_TICK_MS; + reset(); s_state = STATE_PLAYING; + startTicking(); } else if (s_state == STATE_PLAYING) { s_state = STATE_PAUSED; + k_timer_stop(&_tick_timer); } else if (s_state == STATE_PAUSED) { - s_state = STATE_PLAYING; _next_move = k_uptime_get_32() + SNAKE_TICK_MS; + s_state = STATE_PLAYING; + startTicking(); } return true; } @@ -566,7 +617,7 @@ bool SnakeScreen::handleInput(char c) DoomScreen::DoomScreen(JoystickUITask *task) : _task(task) {} -void DoomScreen::poll() +void DoomScreen::onEnter() { if (!doom_game_is_running()) { doom_game_start();