Compare commits

...

9 Commits

Author SHA1 Message Date
Jason P
0d57a49b51 Merge branch 'develop' into baseui_statusmessage 2026-02-03 09:19:43 -06:00
Jason P
d8a0b6a737 Reduce MAX_RECENT_STATUSMESSAGES to 5 to meet memory usage targets 2026-02-03 07:43:45 -06:00
Eric Sesterhenn
0703e0e6d7 Make sure we always return a value in NodeDB::restorePreferences() (#9516)
In case FScom is not defined there is no return statement. This
moves the return outside of the ifdef to make sure a defined
value is returned.
2026-02-03 06:22:33 -06:00
Jonathan Bennett
f514bc230b Prefer EXT_PWR_DETECT pin over chargingVolt to detect power unplugged (#9511) 2026-02-03 00:13:49 -06:00
Jason P
697dd2b5b2 Truncate overflow on Favorite frame 2026-02-02 14:26:15 -06:00
Jason P
0b8b757fb0 Rename variable, set max status to 20, added Node List View. 2026-02-02 12:00:42 -06:00
Jason P
62f897eab3 Change drawNodeInfo to drawFavoriteNode 2026-02-01 21:39:44 -06:00
Jason P
523906d031 Merge branch 'develop' into baseui_statusmessage 2026-02-01 19:10:29 -06:00
Jason P
78f29c0f87 Work through implementation of Status Message 2026-02-01 17:27:59 -06:00
8 changed files with 127 additions and 9 deletions

View File

@@ -459,6 +459,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
} }
// if it's not HIGH - check the battery // if it's not HIGH - check the battery
#endif #endif
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
return false;
// technically speaking this should work for all(?) NRF52 boards // technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if // but needs testing across multiple devices. NRF52 USB would not even work if

View File

@@ -1175,7 +1175,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);
} }
} }
@@ -1204,7 +1204,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) {

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
@@ -90,8 +93,41 @@ const char *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) Sanitize (empty if raw is null/empty) // 2) Sanitize (empty if raw is null/empty)

View File

@@ -4,6 +4,9 @@
#include "GPSStatus.h" #include "GPSStatus.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"
@@ -287,7 +290,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
// ********************** // **********************
// * Favorite Node Info * // * Favorite Node Info *
// ********************** // **********************
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) void UIRenderer::drawFavoriteNode(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
if (favoritedNodes.empty()) if (favoritedNodes.empty())
return; return;
@@ -341,6 +344,57 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
} }
#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) ===
// If both are present: "Sig: 97% [2hops]" // If both are present: "Sig: 97% [2hops]"
// If only one: show only that one // If only one: show only that one

View File

@@ -49,7 +49,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, const OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawFavoriteNode(OLEDDisplay *display, const 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);

View File

@@ -2223,8 +2223,8 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) { } else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
// TODO: After more mainline SD card support // TODO: After more mainline SD card support
} }
return success;
#endif #endif
return success;
} }
/// Record an error that should be reported via analytics /// Record an error that should be reported via analytics

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;

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;