joystick UI: kill heartbeat + type the screen members

Auto-off and auto-lock were the last polling-style deadline checks.
display.c already owns the auto-off (k_work_delayable rescheduled via
mc_display_reset_auto_off()), so its tracker in JoystickUITask was
fully redundant — remove _auto_off and the loop() check. Auto-lock
becomes a one-shot k_timer (_lock_timer) scheduled per activity; ISR
callback sets _locked + signals refresh.

The 2-second heartbeat timer in joystick_ui_hooks is gone, along with
the start/stopHeartbeatFns plumbing it served. The mesh thread now
wakes only on actual events: input, mesh, screen-owned timers, the
lock timer, and display.c's own auto-off work.

Also: screen member pointers in JoystickUITask are now their concrete
subclass types instead of UIScreen *; removes 14 static_cast<>s at
call sites. _curr stays UIScreen * (polymorphic).
This commit is contained in:
liquidraver
2026-05-21 13:04:54 +02:00
parent a01af82c2b
commit 385c88592a
3 changed files with 76 additions and 96 deletions
@@ -45,30 +45,13 @@ static void joystick_render_timer_fn(struct k_timer *timer)
if (s_signal_refresh) s_signal_refresh();
}
static void joystick_heartbeat_timer_fn(struct k_timer *timer)
{
ARG_UNUSED(timer);
if (s_signal_refresh) s_signal_refresh();
}
K_TIMER_DEFINE(s_joystick_render_timer, joystick_render_timer_fn, NULL);
K_TIMER_DEFINE(s_joystick_heartbeat_timer, joystick_heartbeat_timer_fn, NULL);
static void joystick_schedule_render(uint32_t delay_ms)
{
k_timer_start(&s_joystick_render_timer, K_MSEC(delay_ms), K_NO_WAIT);
}
static void joystick_start_heartbeat(void)
{
k_timer_start(&s_joystick_heartbeat_timer, K_MSEC(2000), K_MSEC(2000));
}
static void joystick_stop_heartbeat(void)
{
k_timer_stop(&s_joystick_heartbeat_timer);
}
extern "C" void joystick_ui_hooks_register(
void *task,
ui_signal_fn signal_refresh,
@@ -79,8 +62,6 @@ extern "C" void joystick_ui_hooks_register(
s_signal_tx = signal_tx;
JoystickUITask::setSignalFn(signal_refresh);
JoystickUITask::setScheduleRenderFn(joystick_schedule_render);
JoystickUITask::setHeartbeatFns(joystick_start_heartbeat, joystick_stop_heartbeat);
joystick_start_heartbeat();
}
extern "C" void ui_signal_refresh(void)
@@ -50,15 +50,26 @@ struct k_msgq JoystickUITask::_key_queue;
void (*JoystickUITask::s_signal_fn)(void) = nullptr;
void (*JoystickUITask::s_schedule_render_fn)(uint32_t) = nullptr;
void (*JoystickUITask::s_start_heartbeat_fn)(void) = nullptr;
void (*JoystickUITask::s_stop_heartbeat_fn)(void) = nullptr;
void JoystickUITask::setSignalFn(void (*fn)(void)) { s_signal_fn = fn; }
void JoystickUITask::setScheduleRenderFn(void (*fn)(uint32_t)) { s_schedule_render_fn = fn; }
void JoystickUITask::setHeartbeatFns(void (*start_fn)(void), void (*stop_fn)(void))
void JoystickUITask::lockTimerCb(struct k_timer *t)
{
s_start_heartbeat_fn = start_fn;
s_stop_heartbeat_fn = stop_fn;
/* ISR — atomic writes to mark locked; signal refresh so the loop wakes
* and the lock overlay renders. */
auto *self = static_cast<JoystickUITask *>(k_timer_user_data_get(t));
if (!self) return;
self->_locked = true;
self->_lock_step = 0;
self->_next_refresh = 0;
if (s_signal_fn) s_signal_fn();
}
void JoystickUITask::scheduleLockTimer()
{
k_timer_stop(&_lock_timer);
k_timer_start(&_lock_timer, K_MSEC(LOCK_AFTER_MS), K_NO_WAIT);
}
static bool joystick_queue_initialized;
@@ -174,12 +185,12 @@ JoystickUITask::JoystickUITask()
_doom(nullptr),
#endif
_repeater_admin(nullptr), _curr(nullptr),
_next_refresh(0), _auto_off(0), _screen_off_ms(AUTO_OFF_MILLIS),
_next_refresh(0), _screen_off_ms(AUTO_OFF_MILLIS),
_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),
_pkt_recv(0), _pkt_sent(0), _pkt_errors(0), _alert_expiry(0),
_locked(false), _lock_at(0), _lock_step(0),
_locked(false), _lock_step(0),
_compose_is_contact(false), _compose_channel_idx(-1),
_ch_preview_count(0), _ch_preview_head(JOYSTICK_OFFLINE_QUEUE_SIZE - 1),
_started_at(0), _initialized(false)
@@ -214,8 +225,11 @@ void JoystickUITask::begin(BaseChatMesh *mesh, mesh::ZephyrRTCClock *rtc, NodePr
}
}
applyBrightness();
_auto_off = _started_at + _screen_off_ms;
_lock_at = _started_at + LOCK_AFTER_MS;
/* Lock timer: fires once when LOCK_AFTER_MS passes without activity. */
k_timer_init(&_lock_timer, lockTimerCb, NULL);
k_timer_user_data_set(&_lock_timer, this);
scheduleLockTimer();
/* Init key queue */
k_msgq_init(&_key_queue, _key_buf, sizeof(char), JOYSTICK_KEY_QUEUE_DEPTH);
@@ -289,14 +303,14 @@ void JoystickUITask::gotoDoomScreen() { setCurrScreen(_doom); }
void JoystickUITask::gotoUnreadScreen()
{
if (!_unread) return;
static_cast<UnreadScreen *>(_unread)->activatePreview(false, 0);
_unread->activatePreview(false, 0);
setCurrScreen(_unread);
}
void JoystickUITask::gotoContactForAdvert(const uint8_t *prefix, int prefix_len)
{
if (!_contacts) return;
static_cast<ContactsScreen *>(_contacts)->openForPubKey(prefix, prefix_len);
_contacts->openForPubKey(prefix, prefix_len);
setCurrScreen(_contacts);
}
@@ -304,7 +318,7 @@ void JoystickUITask::gotoRepeaterAdminScreen(const uint8_t *pub_key, const char
bool from_contacts)
{
if (!_repeater_admin) return;
static_cast<RepeaterAdminScreen *>(_repeater_admin)->openForContact(pub_key, name,
_repeater_admin->openForContact(pub_key, name,
from_contacts);
setCurrScreen(_repeater_admin);
}
@@ -359,8 +373,7 @@ void JoystickUITask::handleLockInput(char key, uint32_t now)
if (key == KEY_CANCEL) {
_locked = false;
_lock_step = 0;
_lock_at = now + LOCK_AFTER_MS;
if (s_start_heartbeat_fn) s_start_heartbeat_fn();
scheduleLockTimer();
} else {
_lock_step = 0;
}
@@ -407,7 +420,7 @@ void JoystickUITask::renderLockOverlay()
mc_display_text((w - batt_w) / 2, 6 + fh + 6, batt_buf, false);
char unread_buf[16];
int unread = (_unread) ? static_cast<UnreadScreen *>(_unread)->getUnreadCount() : 0;
int unread = (_unread) ? _unread->getUnreadCount() : 0;
snprintf(unread_buf, sizeof(unread_buf), "Unread: %d", unread);
int unread_w = (int)strlen(unread_buf) * fw;
mc_display_text((w - unread_w) / 2, 6 + fh + 6 + fh + 2, unread_buf, false);
@@ -442,18 +455,11 @@ void JoystickUITask::loop()
uint32_t now = k_uptime_get_32();
/* Auto off */
if (_display.isOn() && now >= _auto_off) {
_display.turnOff();
}
/* Auto lock */
if (!_locked && now >= _lock_at) {
_locked = true;
_lock_step = 0;
_next_refresh = 0;
if (s_stop_heartbeat_fn) s_stop_heartbeat_fn();
}
/* Auto-off is owned by display.c (k_work_delayable in display.c rescheduled
* via mc_display_reset_auto_off() — fires mc_display_off() when due).
* Auto-lock is owned by _lock_timer (fires lockTimerCb in ISR which sets
* _locked = true and signals refresh). Both are fully event-driven; no
* deadline checks needed here. */
/* Dequeue key events */
char key = 0;
@@ -464,19 +470,17 @@ 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() */
_auto_off = now + _screen_off_ms;
_next_refresh = 0;
goto do_render;
}
/* Reset auto off on any keypress */
/* Reset auto-off (display.c) on any keypress */
mc_display_reset_auto_off();
_auto_off = now + _screen_off_ms;
if (_locked) {
handleLockInput(key, now);
} else {
_lock_at = now + LOCK_AFTER_MS;
scheduleLockTimer(); /* push lock deadline forward */
bool consumed = false;
if (key == KEY_BUZZ_TOGGLE) {
toggleBuzzer();
@@ -633,7 +637,6 @@ void JoystickUITask::adjustScreenOff(int32_t delta_ms)
if (v < 5000) v = 5000;
if (v > 300000) v = 300000;
_screen_off_ms = (uint32_t)v;
_auto_off = k_uptime_get_32() + _screen_off_ms;
mc_display_set_auto_off_ms(_screen_off_ms);
mc_display_reset_auto_off();
mesh_save_screen_off_secs((uint16_t)(_screen_off_ms / 1000));
@@ -652,13 +655,13 @@ void JoystickUITask::newMsg(uint8_t path_len, const char *from_name, const char
if (_unread) {
/* When BLE phone is connected it pulls offline queue and marks read;
* keep the message in our local history but don't count as unread. */
static_cast<UnreadScreen *>(_unread)->addPreview(path_len, from_name, text,
_unread->addPreview(path_len, from_name, text,
/*initially_read=*/_ble_connected);
}
/* Wake display if wake on msg is enabled and no BLE peer is connected */
/* Wake display if wake on msg is enabled and no BLE peer is connected.
* mc_display_on() already reschedules display.c's auto-off timer. */
if (_wake_on_msg && !_ble_connected && !_display.isOn()) {
_display.turnOn(); /* mc_display_on() already calls mc_display_reset_auto_off() */
_auto_off = k_uptime_get_32() + _screen_off_ms;
_display.turnOn();
}
_next_refresh = 0;
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
@@ -687,12 +690,11 @@ void JoystickUITask::newChannelMsg(const char *channel_name, const char *text,
char ch_from[48];
snprintf(ch_from, sizeof(ch_from), "#%s", channel_name ? channel_name : "?");
/* BLE-connected → phone reads via offline queue, don't count as unread. */
static_cast<UnreadScreen *>(_unread)->addPreview(path_len, ch_from, text,
_unread->addPreview(path_len, ch_from, text,
/*initially_read=*/_ble_connected);
}
if (_wake_on_msg && !_ble_connected && !_display.isOn()) {
_display.turnOn();
_auto_off = k_uptime_get_32() + _screen_off_ms;
_display.turnOn(); /* display.c reschedules auto-off internally */
}
_next_refresh = 0;
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
@@ -720,7 +722,7 @@ void JoystickUITask::notify()
int JoystickUITask::getUnreadCount()
{
if (!_unread) return 0;
return static_cast<UnreadScreen *>(_unread)->getUnreadCount();
return _unread->getUnreadCount();
}
int JoystickUITask::getStoredMsgCount() const
@@ -812,7 +814,7 @@ bool JoystickUITask::sendComposedMessage(const char *text)
ui_signal_tx();
if (_unread) {
/* Sent message: in history but not "unread" (user sent it). */
static_cast<UnreadScreen *>(_unread)->addPreview(OUT_PATH_SENT, _compose_contact_name, text,
_unread->addPreview(OUT_PATH_SENT, _compose_contact_name, text,
/*initially_read=*/true);
}
/* Notify BLE app so it can mirror the sent message in its UI. */
@@ -897,9 +899,8 @@ void JoystickUITask::onRepeaterAdminLoginResult(const uint8_t *pub_key_prefix,
bool success, uint8_t permissions, uint32_t server_time)
{
if (!_repeater_admin || _curr != _repeater_admin) return;
auto *s = static_cast<RepeaterAdminScreen *>(_repeater_admin);
(void)pub_key_prefix;
s->onLoginResult(success, permissions, server_time);
_repeater_admin->onLoginResult(success, permissions, server_time);
_next_refresh = 0;
ui_signal_refresh();
}
@@ -909,7 +910,7 @@ void JoystickUITask::onRepeaterAdminCliResponse(const uint8_t *pub_key_prefix,
{
if (!_repeater_admin || _curr != _repeater_admin) return;
(void)pub_key_prefix;
static_cast<RepeaterAdminScreen *>(_repeater_admin)->onCliResponse(text);
_repeater_admin->onCliResponse(text);
_next_refresh = 0;
ui_signal_refresh();
}
@@ -922,7 +923,7 @@ void JoystickUITask::onRepeaterReqResponse(const uint8_t *pub_key_prefix,
{
/* Route binary response to repeater admin screen if it's active and waiting */
if (_repeater_admin && _curr == _repeater_admin && data && data_len > 0) {
static_cast<RepeaterAdminScreen *>(_repeater_admin)->onReqResponse(pub_key_prefix,
_repeater_admin->onReqResponse(pub_key_prefix,
data, data_len);
_next_refresh = 0;
ui_signal_refresh();
@@ -934,7 +935,7 @@ void JoystickUITask::onRepeaterReqResponse(const uint8_t *pub_key_prefix,
memset(_pending_ping_pubkey, 0, sizeof(_pending_ping_pubkey));
_pending_ping_sent_ms = 0;
if (!_contacts || _curr != _contacts) return;
static_cast<ContactsScreen *>(_contacts)->onPingResponse(snr_local, snr_remote, rtt_ms);
_contacts->onPingResponse(snr_local, snr_remote, rtt_ms);
_next_refresh = 0;
ui_signal_refresh();
}
@@ -971,10 +972,10 @@ void JoystickUITask::notifyPacketSent()
_pending_ping_sent_ms = k_uptime_get_32();
}
if (_contacts && _curr == _contacts) {
static_cast<ContactsScreen *>(_contacts)->onPacketSent();
_contacts->onPacketSent();
}
if (_repeater_admin && _curr == _repeater_admin) {
static_cast<RepeaterAdminScreen *>(_repeater_admin)->onPacketSent();
_repeater_admin->onPacketSent();
}
}
+27 -29
View File
@@ -184,52 +184,51 @@ public:
/* Set by joystick_ui_hooks after registration */
static void setSignalFn(void (*fn)(void));
static void setScheduleRenderFn(void (*fn)(uint32_t delay_ms));
static void setHeartbeatFns(void (*start_fn)(void), void (*stop_fn)(void));
private:
static void (*s_signal_fn)(void);
static void (*s_schedule_render_fn)(uint32_t);
static void (*s_start_heartbeat_fn)(void);
static void (*s_stop_heartbeat_fn)(void);
JoystickDisplay _display;
BaseChatMesh *_mesh;
mesh::ZephyrRTCClock *_rtc;
NodePrefs *_prefs;
/* Screen instances */
UIScreen *_splash;
UIScreen *_home;
UIScreen *_contacts;
UIScreen *_unread;
UIScreen *_advert;
UIScreen *_gps;
UIScreen *_system;
UIScreen *_system_time;
UIScreen *_telemetry;
UIScreen *_tools;
UIScreen *_radio_stats;
UIScreen *_repeaters;
UIScreen *_channels;
UIScreen *_t9_input;
UIScreen *_rename_node;
UIScreen *_ble_code;
UIScreen *_stats;
UIScreen *_stopwatch;
UIScreen *_countdown;
UIScreen *_snake;
SplashScreen *_splash;
HomeScreen *_home;
ContactsScreen *_contacts;
UnreadScreen *_unread;
AdvertScreen *_advert;
GPSSettingsScreen *_gps;
SystemScreen *_system;
SystemTimeScreen *_system_time;
TelemetryScreen *_telemetry;
ToolsScreen *_tools;
RadioStatsScreen *_radio_stats;
RepeatersScreen *_repeaters;
ChannelsScreen *_channels;
T9InputScreen *_t9_input;
RenameNodeScreen *_rename_node;
BLECodeScreen *_ble_code;
StatsScreen *_stats;
StopwatchScreen *_stopwatch;
CountdownScreen *_countdown;
SnakeScreen *_snake;
#ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM
UIScreen *_doom;
DoomScreen *_doom;
#endif
UIScreen *_repeater_admin;
UIScreen *_curr;
RepeaterAdminScreen *_repeater_admin;
UIScreen *_curr; /* polymorphic — points at whichever subclass is active */
void setCurrScreen(UIScreen *s);
void applyBrightness();
/* Timing */
uint32_t _next_refresh;
uint32_t _auto_off;
uint32_t _screen_off_ms;
uint32_t _screen_off_ms; /* user setting; display.c owns the auto-off timer */
struct k_timer _lock_timer; /* one-shot, fires LOCK_AFTER_MS after last activity */
static void lockTimerCb(struct k_timer *t);
void scheduleLockTimer();
/* State */
uint16_t _cached_batt_mv;
@@ -265,7 +264,6 @@ private:
/* Screen lock */
static constexpr uint32_t LOCK_AFTER_MS = 60000UL;
bool _locked;
uint32_t _lock_at;
uint8_t _lock_step;
void renderLockOverlay();
void handleLockInput(char key, uint32_t now);