refactor(ui-core): UiCore becomes MyMesh's Listener for ui-new

ui-new hands MyMesh the UI Core (UITaskBase::meshListener()). The Core
applies the display filter, labels room posts, files history, keeps DM /
channel / room unread and the queue count, and reports incoming messages
and adverts as MessageArrived / AdvertHeard events. UITask now derives
UITaskBase + UiCoreHost: it answers "is this conversation on screen" and
keeps the selection after an insert, forwards not-yet-extracted parts
(room login / admin, bot device actions, removal cleanup, shutdown), and
turns events into alert / wake / sound, drained at the start and end of
its loop(). Mesh callbacks no longer touch the display or buzzer.

AbstractUITask is split into UITaskBase (lifecycle, link state, helpers)
and the Listener glue ui-orig / ui-tiny keep using unchanged. Screenshot
handler reaches ui_task directly instead of casting the Listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-24 23:28:58 +02:00
co-authored by Claude Opus 5.5
parent 124265b8be
commit 74db0d69a5
10 changed files with 305 additions and 142 deletions
@@ -553,10 +553,10 @@ class MessagesScreen : public UIScreen {
if (ok && _sending_to_channel) {
_hist_sel = 0;
_hist_scroll = 0;
_phase = CHANNEL_HIST; // set before addChannelMsg so viewing=true, no unread bump
_phase = CHANNEL_HIST; // set before filing so the Core sees viewing=true, no unread bump
char entry[sizeof(ChHistEntry::text)];
snprintf(entry, sizeof(entry), "Me: %s", msg);
int pos = addChannelMsg(_sel_channel_idx, entry);
int pos = _task->core().addChannelMsg(_sel_channel_idx, entry);
// Arm the "relayed into mesh" marker on this exact entry — MyMesh tracked
// the flood it just originated and reports a heard repeater echo by seq.
if (pos >= 0) _history.armChannelRelay(pos, the_mesh.lastChannelRelaySeq());
@@ -967,37 +967,24 @@ public:
// (suppressed while picking a channel for the bot).
int channelPickTotal() const { return _num_channels + (_pick_bot_channel ? 0 : 1); }
// Public entry points (routed from MyMesh / the bot via UITask) — thin
// forwarders to the history store. addChannelMsg computes the "viewing" flag
// (a phase-machine fact the store can't see) and returns the ring position so
// the outgoing path can attach a relay seq to that exact entry.
int addChannelMsg(uint8_t ch_idx, const char* text, uint32_t timestamp = 0,
const uint8_t* path = nullptr, uint8_t path_len = 0,
bool own_message = false) {
bool viewing = (_phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx);
int pos = _history.addChannelMsg(ch_idx, text, viewing, timestamp, path, path_len, own_message);
// Ring entries are numbered newest-first (0 == newest), so a new insert
// shifts every older message's index up by one. If the user has scrolled
// up to an older message (_hist_sel > 0), re-point the selection at that
// same message instead of silently relabeling a different one in under
// them. At _hist_sel <= 0 (already at newest, or -1 == compose button
// focused) there's nothing to preserve.
if (viewing && _hist_sel > 0) { _hist_sel++; _hist_scroll++; }
return pos;
// View state the UI Core consults while filing a message (via UiCoreHost):
// a conversation open on screen doesn't count the message unread, and after
// the insert the selection is kept on the same message. Ring entries are
// numbered newest-first (0 == newest), so a new insert shifts every older
// message's index up by one; if the user has scrolled up to an older message
// (sel > 0), re-point the selection at that same message instead of silently
// relabeling a different one in under them. At sel <= 0 (already at newest,
// or -1 == compose button focused) there's nothing to preserve.
bool isViewingChannel(uint8_t ch_idx) const {
return _phase == CHANNEL_HIST && _sel_channel_idx == (int)ch_idx;
}
void markChannelRelayed(uint32_t seq, const uint8_t* repeater_hash = nullptr, uint8_t hash_size = 0) {
_history.markChannelRelayed(seq, repeater_hash, hash_size);
bool isViewingDM(const uint8_t* pub_key) const {
return _phase == DM_HIST && memcmp(_sel_contact.id.pub_key, pub_key, 4) == 0;
}
void armChannelRelay(int pos, uint32_t seq) { _history.armChannelRelay(pos, seq); }
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text,
uint32_t sender_timestamp = 0, uint32_t ack_tag = 0,
uint32_t ack_deadline_ms = 0, uint8_t resends = 0,
const uint8_t* path = nullptr, uint8_t path_len = 0) {
bool viewing = (_phase == DM_HIST && memcmp(_sel_contact.id.pub_key, pub_key, 4) == 0);
_history.addDMMsg(pub_key, outgoing, text, sender_timestamp, ack_tag, ack_deadline_ms, resends, path, path_len);
if (viewing && _dm_hist_sel > 0) { _dm_hist_sel++; _dm_hist_scroll++; } // see addChannelMsg
void onViewedHistoryGrew(bool channel) {
if (channel) { if (_hist_sel > 0) { _hist_sel++; _hist_scroll++; } }
else { if (_dm_hist_sel > 0) { _dm_hist_sel++; _dm_hist_scroll++; } }
}
void markDmDelivered(uint32_t ack_crc) { _history.markDmDelivered(ack_crc); }
// Rooms successfully logged in to this power-on session. RAM-only — the
// server's ACL (see ClientACL) is the real permission store and survives
@@ -1066,7 +1053,7 @@ public:
}
// Result of an on-device sendRoomLogin() (MyMesh::onContactResponse(), routed
// via AbstractUITask::onRoomLoginResult()). Surfaces as a transient alert.
// via the UI Core and UITask::onRoomLoginResult()). Surfaces as a transient alert.
void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
(void)permissions;
if (success) {
+40 -50
View File
@@ -1635,7 +1635,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
ui_started_at = millis();
_alert_expiry = 0;
_batt_mv = AbstractUITask::getBattMilliVolts(); // seed EMA with first reading
_batt_mv = UITaskBase::getBattMilliVolts(); // seed EMA with first reading
// Load persisted waypoints (table survives reboots, unlike the RAM trail).
{
@@ -1647,7 +1647,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
}
_core = new UiCore(); // before any screen -- MessagesScreen binds to its history
_core->begin(node_prefs, sensors);
_core->begin(node_prefs, sensors, this);
splash = new SplashScreen(this);
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
syncLockToHome(); // booted locked (e.g. cover closed) → home starts on the LOCK page
@@ -1730,6 +1730,12 @@ void UITask::dismissRing() { stopMelody(); _core->clock.dismi
void UITask::tickCore() {
_core->loop();
drainCoreEvents();
// Repeat the ring melody until dismissed or the ring window elapses.
if (_core->clock.isRinging() && !isMelodyPlaying()) playMelody(CLOCK_ALARM_MELODY);
}
void UITask::drainCoreEvents() {
UiEvent ev;
while (_core->events.pop(ev)) {
switch (ev.type) {
@@ -1753,12 +1759,16 @@ void UITask::tickCore() {
case UiEventType::LocatorBeep:
playMelody("locp:d=32,o=7,b=200:c");
break;
case UiEventType::MessageArrived:
onMessageArrived(ev);
break;
case UiEventType::AdvertHeard:
notify(ev.flag ? UIEventType::advertReceivedFlood : UIEventType::advertReceivedZeroHop);
break;
default:
break;
}
}
// Repeat the ring melody until dismissed or the ring window elapses.
if (_core->clock.isRinging() && !isMelodyPlaying()) playMelody(CLOCK_ALARM_MELODY);
}
// Ringtone takes a slot argument that onShow() can't carry — pass it after the
@@ -1857,15 +1867,22 @@ void UITask::pickBotRoomTarget() {
setCurrScreen(messages_screen);
}
int UITask::addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp,
const uint8_t* path, uint8_t path_len, bool own_message) {
_last_notif_ch_idx = (int)channel_idx;
return ((MessagesScreen*)messages_screen)->addChannelMsg(channel_idx, text, timestamp, path, path_len, own_message);
// ── UI Core wiring ──────────────────────────────────────────────────────────
MyMesh::Listener* UITask::meshListener() { return _core; }
bool UITask::isViewingChannel(uint8_t channel_idx) {
return ((MessagesScreen*)messages_screen)->isViewingChannel(channel_idx);
}
bool UITask::isViewingDM(const uint8_t* pub_key) {
return ((MessagesScreen*)messages_screen)->isViewingDM(pub_key);
}
void UITask::onViewedHistoryGrew(bool channel) {
((MessagesScreen*)messages_screen)->onViewedHistoryGrew(channel);
}
void UITask::armChannelRelay(int pos, uint32_t seq) {
((MessagesScreen*)messages_screen)->armChannelRelay(pos, seq);
}
int UITask::getMsgCount() const { return _core->msgCount(); }
int UITask::getRoomUnreadCount() const { return _core->roomUnread(); }
void UITask::clearRoomUnread() { _core->clearRoomUnread(); }
int UITask::getChannelUnreadCount() const {
return ((MessagesScreen*)messages_screen)->getTotalChannelUnread();
@@ -1887,14 +1904,6 @@ bool UITask::getAnyUnreadOverflow() const {
return getAnyChannelUnreadOverflow() || getAnyDMUnreadOverflow();
}
void UITask::onMsgAck(uint32_t ack_crc) {
((MessagesScreen*)messages_screen)->markDmDelivered(ack_crc);
}
void UITask::onChannelRelayed(uint32_t seq, const uint8_t* repeater_hash, uint8_t hash_size) {
((MessagesScreen*)messages_screen)->markChannelRelayed(seq, repeater_hash, hash_size);
}
void UITask::onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
// Only one on-device login can be in flight at a time (MyMesh::ui_pending_login
// is a single slot) -- route the result to whichever of the two screens that
@@ -1913,13 +1922,6 @@ void UITask::onAdminReply(const uint8_t* pub_key, const char* text) {
_next_refresh = 0; // same reasoning as onRoomLoginResult above
}
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp,
uint32_t ack_tag, uint32_t ack_deadline_ms, uint8_t resends,
const uint8_t* path, uint8_t path_len) {
((MessagesScreen*)messages_screen)->addDMMsg(pub_key, outgoing, text, sender_timestamp, ack_tag, ack_deadline_ms, resends, path, path_len);
_core->afterDMInsert();
}
int UITask::getDMUnreadTotal() const { return _core->dmUnreadTotal(); }
uint8_t UITask::getDMUnread(const uint8_t* pub_key) const { return _core->dmUnread(pub_key); }
bool UITask::getDMUnreadOverflow(const uint8_t* pub_key) const { return _core->dmUnreadOverflow(pub_key); }
@@ -1973,25 +1975,17 @@ void UITask::notify(UIEventType t) {
}
void UITask::msgRead(int msgcount) {
_msgcount = msgcount;
if (msgcount == 0) {
_room_unread = 0;
_core->clearAllUnread();
}
}
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type, const uint8_t* pub_key) {
_msgcount = msgcount;
if (contact_type == ADV_TYPE_ROOM && _room_unread < _msgcount) _room_unread++;
if (contact_type == ADV_TYPE_CHAT && pub_key != nullptr) {
memcpy(_last_notif_dm_prefix, pub_key, 4);
// Incoming message (UiEventType::MessageArrived, filed by the Core already):
// alert overlay, wake the display, and the per-contact / per-channel sound.
void UITask::onMessageArrived(const UiEvent& ev) {
if (ev.kind == UIEventType::contactMessage && ev.flag) {
memcpy(_last_notif_dm_prefix, ev.key, 4);
_last_notif_dm_valid = true;
_core->noteIncomingDM(pub_key); // eviction/overflow is checked in addDMMsg(), after the ring insert
}
if (ev.kind == UIEventType::channelMessage) _last_notif_ch_idx = ev.idx;
char alert_buf[80];
snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", from_name);
snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", ev.text);
showAlert(alert_buf, 3000);
if (_display != NULL && !_locked) {
@@ -2005,6 +1999,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
_next_refresh = 100;
}
}
notify(ev.kind);
}
void UITask::userLedHandler() {
@@ -2013,7 +2008,7 @@ void UITask::userLedHandler() {
if (cur_time > next_led_change) {
if (led_state == 0) {
led_state = 1;
if (_msgcount > 0) {
if (_core->msgCount() > 0) {
last_led_increment = LED_ON_MSG_MILLIS;
} else {
last_led_increment = LED_ON_MILLIS;
@@ -2488,7 +2483,8 @@ void UITask::pollHallSensor() {
}
void UITask::loop() {
pollConnection(); // BLE link state -> hasConnection() (see AbstractUITask)
pollConnection(); // BLE link state -> hasConnection() (see UITaskBase)
drainCoreEvents(); // react to what the Core filed during mesh processing (alerts, wake, sounds)
// Background delivery: resend pending on-device DMs whose ACK timed out, and
// finalise the ✗ marker — runs regardless of which screen is active.
((MessagesScreen*)messages_screen)->tickDmResends();
@@ -2807,7 +2803,7 @@ void UITask::loop() {
#endif
if ((int32_t)(millis() - next_batt_chck) >= 0) {
uint16_t raw = AbstractUITask::getBattMilliVolts();
uint16_t raw = UITaskBase::getBattMilliVolts();
if (raw > 0) {
#ifdef SIM_PLATFORM
// SimMainBoard::getBattMilliVolts() returns exactly whatever value the
@@ -3017,12 +3013,6 @@ TrailStore& UITask::trail() { return _core->trail.store(); }
bool UITask::currentCourse(int& deg_out) const { return _core->course.currentCourse(deg_out); }
bool UITask::currentLocation(int32_t& lat, int32_t& lon) const { return _core->course.currentLocation(lat, lon); }
void UITask::onSharedLocation(const uint8_t* pub_key, const char* name,
int32_t lat_1e6, int32_t lon_1e6,
uint32_t ts, bool verified) {
_core->live_share.onSharedLocation(pub_key, name, lat_1e6, lon_1e6, ts, verified);
}
bool UITask::sendLocationShare(int32_t lat, int32_t lon) { return _core->live_share.send(lat, lon); }
void UITask::restartLocShareSession() { _core->live_share.restartSession(); }
void UITask::restartLocShareClock() { _core->live_share.restartClock(); }
+18 -24
View File
@@ -41,10 +41,14 @@
#include "../Waypoint.h"
#include "../LiveTrack.h"
#include "KeyboardWidget.h"
#include "../ui-core/UiCoreHost.h"
class UiCore;
struct UiEvent;
class UITask : public AbstractUITask {
// ui-new is the lite frontend of the UI Core: MyMesh talks to the Core (see
// meshListener()), the Core calls back through UiCoreHost and UiEventQueue.
class UITask : public UITaskBase, public UiCoreHost {
DisplayDriver* _display;
SensorManager* _sensors;
#ifdef PIN_BUZZER
@@ -68,8 +72,6 @@ class UITask : public AbstractUITask {
char _bot_buzz_buf[400];
KeyboardWidget _kb; // shared across all screens — only one active at a time
unsigned long _alert_expiry;
int _msgcount;
int _room_unread;
int _last_notif_ch_idx;
uint8_t _last_notif_dm_prefix[4];
bool _last_notif_dm_valid;
@@ -121,6 +123,8 @@ class UITask : public AbstractUITask {
// Runs the UI Core engines and reacts to their events (alert overlay, buzzer,
// display wake). Driven from loop() regardless of the current screen.
void tickCore();
void drainCoreEvents();
void onMessageArrived(const UiEvent& ev); // alert + wake + sound for an incoming message
@@ -228,11 +232,10 @@ private:
public:
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) {
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) {
next_batt_chck = _next_refresh = 0;
ui_started_at = 0;
_batt_mv = 0;
_msgcount = _room_unread = 0;
_locked = false;
_lock_wake_until = 0;
_lock_seq_count = 0; _lock_seq_ms = 0; _lock_seq_used = false;
@@ -247,7 +250,7 @@ public:
NodePrefs* getNodePrefs() const { return _node_prefs; }
// Global metric/imperial preference for distance/speed display.
bool useImperial() const { return _node_prefs && _node_prefs->units_imperial; }
uint16_t getBattMilliVolts() const { return _batt_mv > 0 ? _batt_mv : AbstractUITask::getBattMilliVolts(); }
uint16_t getBattMilliVolts() const { return _batt_mv > 0 ? _batt_mv : UITaskBase::getBattMilliVolts(); }
void gotoHomeScreen() { setCurrScreen(home); }
void gotoSettingsScreen();
void gotoMessagesScreen();
@@ -357,25 +360,21 @@ public:
void stopMelody();
bool isMelodyPlaying();
void showAlert(const char* text, int duration_millis);
int addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp = 0,
const uint8_t* path = nullptr, uint8_t path_len = 0,
bool own_message = false) override;
void armChannelRelay(int pos, uint32_t seq) override;
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp = 0,
uint32_t ack_tag = 0, uint32_t ack_deadline_ms = 0, uint8_t resends = 0,
const uint8_t* path = nullptr, uint8_t path_len = 0) override;
void onMsgAck(uint32_t ack_crc) override;
void onChannelRelayed(uint32_t seq, const uint8_t* repeater_hash = nullptr, uint8_t hash_size = 0) override;
MyMesh::Listener* meshListener() override; // MyMesh talks to the UI Core, not to UITask
// UiCoreHost
bool isViewingChannel(uint8_t channel_idx) override;
bool isViewingDM(const uint8_t* pub_key) override;
void onViewedHistoryGrew(bool channel) override;
void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) override;
void onAdminReply(const uint8_t* pub_key, const char* text) override;
int getDMUnreadTotal() const;
int getMsgCount() const { return _msgcount; }
int getMsgCount() const;
int getChannelUnreadCount() const;
uint8_t getChannelUnread(uint8_t channel_idx) const;
bool getChannelUnreadOverflow(uint8_t channel_idx) const;
bool getAnyChannelUnreadOverflow() const;
int getRoomUnreadCount() const { return _room_unread; }
void clearRoomUnread() { _room_unread = 0; }
int getRoomUnreadCount() const;
void clearRoomUnread();
// Clamped to the DM ring's actual occupancy for this contact (UiCore).
uint8_t getDMUnread(const uint8_t* pub_key) const;
bool getDMUnreadOverflow(const uint8_t* pub_key) const;
@@ -512,13 +511,8 @@ public:
}
// from AbstractUITask
void msgRead(int msgcount) override;
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type = 0, const uint8_t* pub_key = nullptr) override;
// from UITaskBase
void notify(UIEventType t = UIEventType::none) override;
void onSharedLocation(const uint8_t* pub_key, const char* name,
int32_t lat_1e6, int32_t lon_1e6,
uint32_t ts, bool verified) override;
void loop() override;
// Send one [LOC] message to the configured live-share target. Returns false
// if the target can't be resolved (no such channel / contact).