mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:09:17 +00:00
joystick UI: replace per-screen poll() with onEnter/onExit + k_timers
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.
This commit is contained in:
@@ -14,17 +14,28 @@
|
||||
#include <helpers/ChannelDetails.h>
|
||||
#include <mesh/Mesh.h>
|
||||
#include <stdint.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ContactsScreen *>(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<RepeaterAdminScreen *>(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) {
|
||||
|
||||
@@ -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<SplashScreen *>(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UnreadScreen *>(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),
|
||||
|
||||
@@ -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<GPSSettingsScreen *>(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];
|
||||
|
||||
@@ -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<CountdownScreen *>(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<SnakeScreen *>(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();
|
||||
|
||||
Reference in New Issue
Block a user