BaseUI: Implementation of Status Message for Favorite and NodeList views (#9504)

* Implementation of Status Message

* Change drawNodeInfo to drawFavoriteNode

* Truncate overflow on Favorite frame

* Set MAX_RECENT_STATUSMESSAGES to 5 to meet memory usage targets
This commit is contained in:
Jason P
2026-04-17 08:42:56 -05:00
committed by GitHub
co-authored by GitHub
parent 3a87e746a8
commit 6208c243f9
7 changed files with 124 additions and 9 deletions
-1
View File
@@ -56,7 +56,6 @@ build_flags = -Wno-missing-field-initializers
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
-DMESHTASTIC_EXCLUDE_POWERMON=1 -DMESHTASTIC_EXCLUDE_POWERMON=1
-DMESHTASTIC_EXCLUDE_STATUS=1
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
#-D OLED_PL=1 #-D OLED_PL=1
+2 -2
View File
@@ -1197,7 +1197,7 @@ void Screen::setFrames(FrameFocus focus)
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
} }
} }
@@ -1226,7 +1226,7 @@ void Screen::setFrames(FrameFocus focus)
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
// Focus on a specific frame, in the frame set we just created // Focus on a specific frame, in the frame set we just created
switch (focus) { switch (focus) {
+38 -2
View File
@@ -3,6 +3,9 @@
#include "CompassRenderer.h" #include "CompassRenderer.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "NodeListRenderer.h" #include "NodeListRenderer.h"
#if !MESHTASTIC_EXCLUDE_STATUS
#include "modules/StatusMessageModule.h"
#endif
#include "UIRenderer.h" #include "UIRenderer.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
#include "gps/RTC.h" // for getTime() function #include "gps/RTC.h" // for getTime() function
@@ -92,8 +95,41 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
// 1) Choose target candidate (long vs short) only if present // 1) Choose target candidate (long vs short) only if present
const char *raw = nullptr; const char *raw = nullptr;
if (node && node->has_user) {
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; #if !MESHTASTIC_EXCLUDE_STATUS
// If long-name mode is enabled, and we have a recent status for this node,
// prefer "(short_name) statusText" as the raw candidate.
std::string composedFromStatus;
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) {
const auto &recent = statusMessageModule->getRecentReceived();
const StatusMessageModule::RecentStatus *found = nullptr;
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
if (it->fromNodeId == node->num && !it->statusText.empty()) {
found = &(*it);
break;
}
}
if (found) {
const char *shortName = node->user.short_name;
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size());
composedFromStatus += "(";
if (shortName && *shortName) {
composedFromStatus += shortName;
}
composedFromStatus += ") ";
composedFromStatus += found->statusText;
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
}
}
#endif
// If we didn't compose from status, use normal long/short selection
if (!raw) {
if (node && node->has_user) {
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
}
} }
// 2) Preserve UTF-8 names so emotes can be detected and rendered. // 2) Preserve UTF-8 names so emotes can be detected and rendered.
+55 -1
View File
@@ -5,6 +5,9 @@
#include "MeshService.h" #include "MeshService.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "NodeListRenderer.h" #include "NodeListRenderer.h"
#if !MESHTASTIC_EXCLUDE_STATUS
#include "modules/StatusMessageModule.h"
#endif
#include "UIRenderer.h" #include "UIRenderer.h"
#include "airtime.h" #include "airtime.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
@@ -290,7 +293,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
// * Favorite Node Info * // * Favorite Node Info *
// ********************** // **********************
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library // cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
if (favoritedNodes.empty()) if (favoritedNodes.empty())
return; return;
@@ -342,6 +345,57 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
} }
#if !MESHTASTIC_EXCLUDE_STATUS
// === Optional: Last received StatusMessage line for this node ===
// Display it directly under the username line (if we have one).
if (statusMessageModule) {
const auto &recent = statusMessageModule->getRecentReceived();
const StatusMessageModule::RecentStatus *found = nullptr;
// Search newest-to-oldest
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
if (it->fromNodeId == node->num && !it->statusText.empty()) {
found = &(*it);
break;
}
}
if (found) {
std::string statusLine = std::string(" Status: ") + found->statusText;
{
const int screenW = display->getWidth();
const int ellipseW = display->getStringWidth("...");
int w = display->getStringWidth(statusLine.c_str());
// Only do work if it overflows
if (w > screenW) {
bool truncated = false;
if (ellipseW > screenW) {
statusLine.clear();
} else {
while (!statusLine.empty()) {
// remove one char (byte) at a time
statusLine.pop_back();
truncated = true;
// Measure candidate with ellipsis appended
std::string candidate = statusLine + "...";
if (display->getStringWidth(candidate.c_str()) <= screenW) {
statusLine = std::move(candidate);
break;
}
}
if (statusLine.empty() && ellipseW <= screenW) {
statusLine = "...";
}
}
}
}
display->drawString(x, getTextPositions(display)[line++], statusLine.c_str());
}
}
#endif
// === 2. Signal and Hops (combined on one line, if available) === // === 2. Signal and Hops (combined on one line, if available) ===
char signalHopsStr[32] = ""; char signalHopsStr[32] = "";
bool haveSignal = false; bool haveSignal = false;
+1 -1
View File
@@ -50,7 +50,7 @@ class UIRenderer
// Navigation bar overlay // Navigation bar overlay
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
+14 -1
View File
@@ -29,10 +29,23 @@ int32_t StatusMessageModule::runOnce()
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp) ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
{ {
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
meshtastic_StatusMessage incomingMessage; meshtastic_StatusMessage incomingMessage = meshtastic_StatusMessage_init_zero;
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields, if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
&incomingMessage)) { &incomingMessage)) {
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status); LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
RecentStatus entry;
entry.fromNodeId = mp.from;
entry.statusText = incomingMessage.status;
recentReceived.push_back(std::move(entry));
// Keep only last MAX_RECENT_STATUSMESSAGES
if (recentReceived.size() > MAX_RECENT_STATUSMESSAGES) {
recentReceived.erase(recentReceived.begin()); // drop oldest
}
} }
} }
return ProcessMessage::CONTINUE; return ProcessMessage::CONTINUE;
+14 -1
View File
@@ -2,10 +2,11 @@
#if !MESHTASTIC_EXCLUDE_STATUS #if !MESHTASTIC_EXCLUDE_STATUS
#include "SinglePortModule.h" #include "SinglePortModule.h"
#include "configuration.h" #include "configuration.h"
#include <string>
#include <vector>
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
{ {
public: public:
/** Constructor /** Constructor
* name is for debugging output * name is for debugging output
@@ -19,16 +20,28 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
this->setInterval(1000 * 12 * 60 * 60); this->setInterval(1000 * 12 * 60 * 60);
} }
// TODO: If we have a string, set the initial delay (15 minutes maybe) // TODO: If we have a string, set the initial delay (15 minutes maybe)
// Keep vector from reallocating as we fill up to MAX_RECENT_STATUSMESSAGES
recentReceived.reserve(MAX_RECENT_STATUSMESSAGES);
} }
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
struct RecentStatus {
uint32_t fromNodeId; // mp.from
std::string statusText; // incomingMessage.status
};
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
protected: protected:
/** Called to handle a particular incoming message /** Called to handle a particular incoming message
*/ */
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
private: private:
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
std::vector<RecentStatus> recentReceived;
}; };
extern StatusMessageModule *statusMessageModule; extern StatusMessageModule *statusMessageModule;