From 68250dc9375ccb270b0eb5c0280fa83e0b5076d1 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:19:32 +0100 Subject: [PATCH 1/8] Mark implicit ACK for MQTT as MQTT transport (#8939) * Mark implicit ACK for MQTT as MQTT transport * TRUNK * Fix build * Make sure implicit ACKs from MQTT do not stop retransmissions in ReliableRouter --------- Co-authored-by: Ben Meadors --- src/mesh/MeshModule.h | 2 +- src/mesh/ReliableRouter.cpp | 4 +++- src/modules/RoutingModule.cpp | 6 ++++++ src/modules/RoutingModule.h | 3 +++ src/mqtt/MQTT.cpp | 9 ++++++--- src/platform/nrf52/main-nrf52.cpp | 2 +- 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index eda3f8881..e7178bcfe 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -225,4 +225,4 @@ class MeshModule /** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet * This ensures that if the request packet was sent reliably, the reply is sent that way as well. */ -void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to); \ No newline at end of file +void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to); diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 00066a7a3..7619fc106 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -150,7 +150,9 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records - if (ackId || nakId) { + if ((ackId || nakId) && + // Implicit ACKs from MQTT should not stop retransmissions + !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) { LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); if (ackId) { stopRetransmission(p->to, ackId); diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index 05173983c..662f5379a 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -75,6 +75,12 @@ uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit } +meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit) +{ + return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit); +} + RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) { isPromiscuous = true; diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h index a4e0679d0..5d4b9596f 100644 --- a/src/modules/RoutingModule.h +++ b/src/modules/RoutingModule.h @@ -16,6 +16,9 @@ class RoutingModule : public ProtobufModule virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, bool ackWantsAck = false); + meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit = 0); + // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response uint8_t getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit); diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index ad35e152a..7c33f0360 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -87,10 +87,13 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length) // Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message. // We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node // receives it when we get our own packet back. Then we'll stop our retransmissions. - if (isFromUs(e.packet)) - routingModule->sendAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); - else + if (isFromUs(e.packet)) { + auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); + pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; + router->sendLocal(pAck); + } else { LOG_INFO("Ignore downlink message we originally sent"); + } return; } if (isFromUs(e.packet)) { diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 5d1ba20ba..472107229 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -337,7 +337,7 @@ void cpuDeepSleep(uint32_t msecToWake) #endif #ifdef TTGO_T_ECHO - // To power off the T-Echo, the display must be set + // To power off the T-Echo, the display must be set // as an input pin; otherwise, there will be leakage current. pinMode(PIN_EINK_CS, INPUT); pinMode(PIN_EINK_DC, INPUT); From f68ef9f7d7a8c225e8d43b22c49a42c09393a925 Mon Sep 17 00:00:00 2001 From: Jason P Date: Thu, 11 Dec 2025 14:25:48 -0600 Subject: [PATCH 2/8] Reflow Node Lists and TLora Pager Views (#8942) * Add files via upload * Move files into the right place --- src/graphics/Screen.cpp | 33 ++++++-- src/graphics/Screen.h | 8 +- src/graphics/draw/MenuHandler.cpp | 78 +++++++++++------- src/graphics/draw/NodeListRenderer.cpp | 106 +++++++++++++++++++------ src/graphics/draw/NodeListRenderer.h | 15 ++-- 5 files changed, 172 insertions(+), 68 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 9a6b97c01..149dd9eac 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1046,11 +1046,16 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - if (!hiddenFrames.nodelist) { - fsi.positions.nodelist = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + if (!hiddenFrames.nodelist_nodes) { + fsi.positions.nodelist_nodes = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; indicatorIcons.push_back(icon_nodes); } + if (!hiddenFrames.nodelist_location) { + fsi.positions.nodelist_location = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location; + indicatorIcons.push_back(icon_list); + } #endif // Show detailed node views only on E-Ink builds @@ -1072,11 +1077,13 @@ void Screen::setFrames(FrameFocus focus) } #endif #if HAS_GPS +#ifdef USE_EINK if (!hiddenFrames.nodelist_bearings) { fsi.positions.nodelist_bearings = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list); } +#endif if (!hiddenFrames.gps) { fsi.positions.gps = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; @@ -1238,8 +1245,11 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) void Screen::toggleFrameVisibility(const std::string &frameName) { #ifndef USE_EINK - if (frameName == "nodelist") { - hiddenFrames.nodelist = !hiddenFrames.nodelist; + if (frameName == "nodelist_nodes") { + hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; + } + if (frameName == "nodelist_location") { + hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location; } #endif #ifdef USE_EINK @@ -1254,9 +1264,11 @@ void Screen::toggleFrameVisibility(const std::string &frameName) } #endif #if HAS_GPS +#ifdef USE_EINK if (frameName == "nodelist_bearings") { hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; } +#endif if (frameName == "gps") { hiddenFrames.gps = !hiddenFrames.gps; } @@ -1278,8 +1290,10 @@ void Screen::toggleFrameVisibility(const std::string &frameName) bool Screen::isFrameHidden(const std::string &frameName) const { #ifndef USE_EINK - if (frameName == "nodelist") - return hiddenFrames.nodelist; + if (frameName == "nodelist_nodes") + return hiddenFrames.nodelist_nodes; + if (frameName == "nodelist_location") + return hiddenFrames.nodelist_location; #endif #ifdef USE_EINK if (frameName == "nodelist_lastheard") @@ -1290,8 +1304,10 @@ bool Screen::isFrameHidden(const std::string &frameName) const return hiddenFrames.nodelist_distance; #endif #if HAS_GPS +#ifdef USE_EINK if (frameName == "nodelist_bearings") return hiddenFrames.nodelist_bearings; +#endif if (frameName == "gps") return hiddenFrames.gps; #endif @@ -1716,7 +1732,8 @@ int Screen::handleInputEvent(const InputEvent *event) this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { menuHandler::favoriteBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist || + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index d6e71d356..4bb808970 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -654,7 +654,8 @@ class Screen : public concurrency::OSThread uint8_t gps = 255; uint8_t home = 255; uint8_t textMessage = 255; - uint8_t nodelist = 255; + uint8_t nodelist_nodes = 255; + uint8_t nodelist_location = 255; uint8_t nodelist_lastheard = 255; uint8_t nodelist_hopsignal = 255; uint8_t nodelist_distance = 255; @@ -677,7 +678,8 @@ class Screen : public concurrency::OSThread bool home = false; bool clock = false; #ifndef USE_EINK - bool nodelist = false; + bool nodelist_nodes = false; + bool nodelist_location = false; #endif #ifdef USE_EINK bool nodelist_lastheard = false; @@ -685,7 +687,9 @@ class Screen : public concurrency::OSThread bool nodelist_distance = false; #endif #if HAS_GPS +#ifdef USE_EINK bool nodelist_bearings = false; +#endif bool gps = false; #endif bool lora = false; diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 045154343..2770f9cab 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1111,20 +1111,33 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; -#if defined(M5STACK_UNITC6L) - static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"}; -#else - static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; + enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + optionsArray[options] = "Add Favorite"; + optionsEnumArray[options++] = Favorite; + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + +#if !defined(M5STACK_UNITC6L) + optionsArray[options] = "Key Verification"; + optionsEnumArray[options++] = Verify; #endif + +#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR) + optionsArray[options] = "Show Long/Short Name"; + optionsEnumArray[options++] = NodeNameLength; +#endif + optionsArray[options] = "Reset NodeDB"; + optionsEnumArray[options++] = Reset; + BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; -#if defined(M5STACK_UNITC6L) - bannerOptions.optionsCount = 3; -#else - bannerOptions.optionsCount = 5; -#endif + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Favorite) { menuQueue = add_favorite; @@ -1138,6 +1151,9 @@ void menuHandler::nodeListMenu() } else if (selected == TraceRoute) { menuQueue = trace_route_menu; screen->runNow(); + } else if (selected == NodeNameLength) { + menuHandler::menuQueue = menuHandler::node_name_length_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -1731,16 +1747,11 @@ void menuHandler::screenOptionsMenu() hasSupportBrightness = false; #endif - enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits }; + enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; static const char *optionsArray[5] = {"Back"}; static int optionsEnumArray[5] = {Back}; int options = 1; -#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR) - optionsArray[options] = "Show Long/Short Name"; - optionsEnumArray[options++] = NodeNameLength; -#endif - // Only show brightness for B&W displays if (hasSupportBrightness) { optionsArray[options] = "Brightness"; @@ -1772,9 +1783,6 @@ void menuHandler::screenOptionsMenu() } else if (selected == ScreenColor) { menuHandler::menuQueue = menuHandler::tftcolormenupicker; screen->runNow(); - } else if (selected == NodeNameLength) { - menuHandler::menuQueue = menuHandler::node_name_length_menu; - screen->runNow(); } else if (selected == FrameToggles) { menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); @@ -1870,7 +1878,8 @@ void menuHandler::FrameToggles_menu() { enum optionsNumbers { Finish, - nodelist, + nodelist_nodes, + nodelist_location, nodelist_lastheard, nodelist_hopsignal, nodelist_distance, @@ -1891,20 +1900,25 @@ void menuHandler::FrameToggles_menu() static int lastSelectedIndex = 0; #ifndef USE_EINK - optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List"; - optionsEnumArray[options++] = nodelist; -#endif -#ifdef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node List" : "Hide Node List"; + optionsEnumArray[options++] = nodelist_nodes; +#else optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; optionsEnumArray[options++] = nodelist_lastheard; optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; optionsEnumArray[options++] = nodelist_hopsignal; +#endif + +#if HAS_GPS +#ifndef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Node Location List" : "Hide Node Location List"; + optionsEnumArray[options++] = nodelist_location; +#else optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; optionsEnumArray[options++] = nodelist_distance; -#endif -#if HAS_GPS - optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings"; + optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings"; optionsEnumArray[options++] = nodelist_bearings; +#endif optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; optionsEnumArray[options++] = gps; @@ -1943,8 +1957,12 @@ void menuHandler::FrameToggles_menu() if (selected == Finish) { screen->setFrames(Screen::FOCUS_DEFAULT); - } else if (selected == nodelist) { - screen->toggleFrameVisibility("nodelist"); + } else if (selected == nodelist_nodes) { + screen->toggleFrameVisibility("nodelist_nodes"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_location) { + screen->toggleFrameVisibility("nodelist_location"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == nodelist_lastheard) { @@ -2163,4 +2181,4 @@ void menuHandler::saveUIConfig() } // namespace graphics -#endif +#endif \ No newline at end of file diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index b073f17b3..a1020268e 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -46,7 +46,8 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t * } // Static variables for dynamic cycling -static NodeListMode currentMode = MODE_LAST_HEARD; +static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD; +static ListMode_Location currentMode_Location = MODE_DISTANCE; static int scrollIndex = 0; // ============================= @@ -55,7 +56,7 @@ static int scrollIndex = 0; const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node) { - static char nodeName[16]; // single static buffer we return + static char nodeName[25]; // single static buffer we return nodeName[0] = '\0'; auto writeFallbackId = [&] { @@ -109,9 +110,9 @@ const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node) return nodeName; } -const char *getCurrentModeTitle(int screenWidth) +const char *getCurrentModeTitle_Nodes(int screenWidth) { - switch (currentMode) { + switch (currentMode_Nodes) { case MODE_LAST_HEARD: return "Last Heard"; case MODE_HOP_SIGNAL: @@ -120,8 +121,18 @@ const char *getCurrentModeTitle(int screenWidth) #else return (isHighResolution) ? "Hops/Signal" : "Hops/Sig"; #endif + default: + return "Nodes"; + } +} + +const char *getCurrentModeTitle_Location(int screenWidth) +{ + switch (currentMode_Location) { case MODE_DISTANCE: return "Distance"; + case MODE_BEARING: + return "Bearings"; default: return "Nodes"; } @@ -140,10 +151,8 @@ int calculateMaxScroll(int totalEntries, int visibleRows) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { - int columnWidth = display->getWidth() / 2; - int separatorX = x + columnWidth - 2; for (int y = yStart; y <= yEnd; y += 2) { - display->setPixel(separatorX, y); + display->setPixel(x, y); } } @@ -332,18 +341,15 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } -void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - switch (currentMode) { + switch (currentMode_Nodes) { case MODE_LAST_HEARD: drawEntryLastHeard(display, node, x, y, columnWidth); break; case MODE_HOP_SIGNAL: drawEntryHopSignal(display, node, x, y, columnWidth); break; - case MODE_DISTANCE: - drawNodeDistance(display, node, x, y, columnWidth); - break; default: break; } @@ -436,9 +442,15 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t locationScreen = true; #if defined(M5STACK_UNITC6L) int columnWidth = display->getWidth(); +#elif defined(T_LORA_PAGER) + int columnWidth = display->getWidth() / 3; + if (config.display.use_long_node_name) { + columnWidth = display->getWidth() / 2; + } #else int columnWidth = display->getWidth() / 2; #endif + display->clear(); // Draw the battery/time header @@ -453,6 +465,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int visibleNodeRows = totalRowsAvailable; #if defined(M5STACK_UNITC6L) int totalColumns = 1; +#elif defined(T_LORA_PAGER) + int totalColumns = 3; + if (config.display.use_long_node_name) { + totalColumns = 2; + } #else int totalColumns = 2; #endif @@ -502,7 +519,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // Draw column separator if (shownCount > 0) { const int firstNodeY = y + 3; - drawColumnSeparator(display, x, firstNodeY, lastNodeY); + for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { + drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY); + } } #endif @@ -516,10 +535,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // ============================= #ifndef USE_EINK -void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +// Node list for Last Heard and Hop Signal views +void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Static variables to track mode and duration - static NodeListMode lastRenderedMode = MODE_COUNT; + static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; static unsigned long modeStartTime = 0; unsigned long now = millis(); @@ -532,23 +552,65 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, } #endif // On very first call (on boot or state enter) - if (lastRenderedMode == MODE_COUNT) { - currentMode = MODE_LAST_HEARD; + if (lastRenderedMode == MODE_COUNT_NODE) { + currentMode_Nodes = MODE_LAST_HEARD; modeStartTime = now; } // Time to switch to next mode? if (now - modeStartTime >= getModeCycleIntervalMs()) { - currentMode = static_cast((currentMode + 1) % MODE_COUNT); + currentMode_Nodes = static_cast((currentMode_Nodes + 1) % MODE_COUNT_NODE); modeStartTime = now; } // Render screen based on currentMode - const char *title = getCurrentModeTitle(display->getWidth()); - drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + const char *title = getCurrentModeTitle_Nodes(display->getWidth()); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); // Track the last mode to avoid reinitializing modeStartTime - lastRenderedMode = currentMode; + lastRenderedMode = currentMode_Nodes; +} + +// Node list for Distance and Bearings views +void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Static variables to track mode and duration + static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION; + static unsigned long modeStartTime = 0; + + unsigned long now = millis(); + +#if defined(M5STACK_UNITC6L) + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; + } +#endif + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT_LOCATION) { + currentMode_Location = MODE_DISTANCE; + modeStartTime = now; + } + + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode_Location = static_cast((currentMode_Location + 1) % MODE_COUNT_LOCATION); + modeStartTime = now; + } + + // Render screen based on currentMode + const char *title = getCurrentModeTitle_Location(display->getWidth()); + + // Render screen based on currentMode_Location + if (currentMode_Location == MODE_DISTANCE) { + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); + } else if (currentMode_Location == MODE_BEARING) { + drawNodeListWithCompasses(display, state, x, y); + } + + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode_Location; } #endif @@ -569,14 +631,12 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ #endif drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); } - void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const char *title = "Distance"; drawNodeListScreen(display, state, x, y, title, drawNodeDistance); } #endif - void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { float heading = 0; diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index ea8df8bd9..8b82fb117 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -23,8 +23,11 @@ namespace NodeListRenderer typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); -// Node list mode enumeration -enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; +// Node list mode enumeration for Last Heard and Hop Signal views +enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 }; + +// Node list mode enumeration for Distance and Bearings views +enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 }; // Main node list screen function void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, @@ -35,7 +38,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); -void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); // Extras renderers @@ -46,11 +49,13 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); -void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Utility functions -const char *getCurrentModeTitle(int screenWidth); +const char *getCurrentModeTitle_Nodes(int screenWidth); +const char *getCurrentModeTitle_Location(int screenWidth); const char *getSafeNodeName(meshtastic_NodeInfoLite *node); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); From f3ebdec49ef2451026726366e62bd46da0cffc2e Mon Sep 17 00:00:00 2001 From: Jason P Date: Thu, 11 Dec 2025 17:22:17 -0600 Subject: [PATCH 3/8] Short or Long Names for everyone! --- src/graphics/draw/MenuHandler.cpp | 2 -- src/graphics/draw/NodeListRenderer.cpp | 37 +++++++++++--------------- src/graphics/draw/NodeListRenderer.h | 2 +- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 2770f9cab..e3e206a54 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1126,10 +1126,8 @@ void menuHandler::nodeListMenu() optionsEnumArray[options++] = Verify; #endif -#if defined(T_DECK) || defined(T_LORA_PAGER) || defined(HACKADAY_COMMUNICATOR) optionsArray[options] = "Show Long/Short Name"; optionsEnumArray[options++] = NodeNameLength; -#endif optionsArray[options] = "Reset NodeDB"; optionsEnumArray[options++] = Reset; diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index a1020268e..cd1e7943b 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -54,7 +54,7 @@ static int scrollIndex = 0; // Utility Functions // ============================= -const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node) +const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) { static char nodeName[25]; // single static buffer we return nodeName[0] = '\0'; @@ -82,7 +82,7 @@ const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node) // 4) Width-based truncation + ellipsis (long-name mode only) if (config.display.use_long_node_name && display) { - int availWidth = (SCREEN_WIDTH / 2) - 65; + int availWidth = columnWidth - (isHighResolution ? 65 : 38); if (availWidth < 0) availWidth = 0; @@ -181,7 +181,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); char timeStr[10]; uint32_t seconds = sinceLastSeen(node); @@ -226,7 +226,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int barsXOffset = columnWidth - barsOffset; - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -270,7 +270,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -362,7 +362,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 // Adjust max text width depending on column and screen width int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -440,17 +440,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t locationScreen = true; else if (strcmp(title, "Distance") == 0) locationScreen = true; -#if defined(M5STACK_UNITC6L) - int columnWidth = display->getWidth(); -#elif defined(T_LORA_PAGER) - int columnWidth = display->getWidth() / 3; - if (config.display.use_long_node_name) { - columnWidth = display->getWidth() / 2; - } -#else - int columnWidth = display->getWidth() / 2; -#endif - display->clear(); // Draw the battery/time header @@ -459,10 +448,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // Space below header y += COMMON_HEADER_HEIGHT; - int totalEntries = nodeDB->getNumMeshNodes(); - int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; - int numskipped = 0; - int visibleNodeRows = totalRowsAvailable; #if defined(M5STACK_UNITC6L) int totalColumns = 1; #elif defined(T_LORA_PAGER) @@ -472,7 +457,17 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } #else int totalColumns = 2; + if (config.display.use_long_node_name) { + totalColumns = 1; + } #endif + + int columnWidth = display->getWidth() / totalColumns; + + int totalEntries = nodeDB->getNumMeshNodes(); + int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; + int numskipped = 0; + int visibleNodeRows = totalRowsAvailable; int startIndex = scrollIndex * visibleNodeRows * totalColumns; if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { startIndex++; // skip own node diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index 8b82fb117..8f19666ba 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -56,7 +56,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, // Utility functions const char *getCurrentModeTitle_Nodes(int screenWidth); const char *getCurrentModeTitle_Location(int screenWidth); -const char *getSafeNodeName(meshtastic_NodeInfoLite *node); +const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); // Bitmap drawing function From d6bf0a330a1a2945cb3c33fe330188602a1e5c86 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:28:43 -0500 Subject: [PATCH 4/8] Add scrolling to Node list --- src/graphics/Screen.cpp | 20 ++++++++++++++++ src/graphics/draw/NodeListRenderer.cpp | 32 ++++++++++++++++++++++++++ src/graphics/draw/NodeListRenderer.h | 4 ++++ 3 files changed, 56 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 149dd9eac..880cd3553 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1659,6 +1659,26 @@ int Screen::handleInputEvent(const InputEvent *event) } } } + // UP/DOWN in node list screens scrolls through node pages + if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) + { + if (event->inputEvent == INPUT_BROKER_UP) { + graphics::NodeListRenderer::scrollUp(); + setFastFramerate(); + return 0; + } + + if (event->inputEvent == INPUT_BROKER_DOWN) { + graphics::NodeListRenderer::scrollDown(); + setFastFramerate(); + return 0; + } + } // Use left or right input from a keyboard to move between frames, // so long as a mesh module isn't using these events for some other purpose if (showingNormalScreen) { diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index cd1e7943b..ab6ac4c04 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -50,6 +50,38 @@ static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD; static ListMode_Location currentMode_Location = MODE_DISTANCE; static int scrollIndex = 0; +// ============================= +// Scrolling Logic +// ============================= +void scrollUp() +{ + if (scrollIndex > 0) + scrollIndex--; +} + +void scrollDown() +{ + int totalEntries = nodeDB->getNumMeshNodes(); + + const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; + + int screenHeight = screen->getHeight(); + int visibleRows = (screenHeight - COMMON_HEADER_HEIGHT) / rowYOffset; + + int totalColumns = 2; +#if defined(T_LORA_PAGER) + totalColumns = 3; +#endif + if (config.display.use_long_node_name) + totalColumns = 1; + + int maxScroll = std::max(0, (totalEntries - 1) / (visibleRows * totalColumns)); + + if (scrollIndex < maxScroll) + scrollIndex++; +} + // ============================= // Utility Functions // ============================= diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index 8f19666ba..e212c031b 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -59,6 +59,10 @@ const char *getCurrentModeTitle_Location(int screenWidth); const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); +// Scrolling controls +void scrollUp(); +void scrollDown(); + // Bitmap drawing function void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); From b150c56735545f7826e70c35f0a4788da98a76e4 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:43:21 -0500 Subject: [PATCH 5/8] Pagination fix for Latest to oldest per page --- src/graphics/Screen.cpp | 3 +- src/graphics/draw/NodeListRenderer.cpp | 38 ++++++++++++++++---------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 880cd3553..52b26a72b 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1665,8 +1665,7 @@ int Screen::handleInputEvent(const InputEvent *event) ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || - ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) - { + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { if (event->inputEvent == INPUT_BROKER_UP) { graphics::NodeListRenderer::scrollUp(); setFastFramerate(); diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index ab6ac4c04..564bae8ee 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -500,30 +500,40 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int numskipped = 0; int visibleNodeRows = totalRowsAvailable; - int startIndex = scrollIndex * visibleNodeRows * totalColumns; - if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { - startIndex++; // skip own node - } - int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + // Build filtered + ordered list + std::vector drawList; + drawList.reserve(totalEntries); + for (int i = 0; i < totalEntries; i++) { + auto *n = nodeDB->getMeshNodeByIndex(i); + + if (!n) continue; + if (n->num == nodeDB->getNodeNum()) + continue; + if (locationScreen && !n->has_position) + continue; + + drawList.push_back(n->num); + } + totalEntries = drawList.size(); + int startIndex = scrollIndex * visibleNodeRows * totalColumns; + int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); int yOffset = 0; int col = 0; int lastNodeY = y; int shownCount = 0; int rowCount = 0; - for (int i = startIndex; i < endIndex; ++i) { - if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) { - numskipped++; - continue; - } + for (int idx = startIndex; idx < endIndex; idx++) { + uint32_t nodeNum = drawList[idx]; + auto *node = nodeDB->getMeshNode(nodeNum); int xPos = x + (col * columnWidth); int yPos = y + yOffset; - renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth); - if (extras) { - extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon); - } + renderer(display, node, xPos, yPos, columnWidth); + + if (extras) + extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; From bca169ddaf003651d2741577f4d149dfcfd52c77 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:08:56 -0500 Subject: [PATCH 6/8] Page counters --- src/graphics/draw/NodeListRenderer.cpp | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 564bae8ee..b7a7560dd 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -49,6 +49,15 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t * static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD; static ListMode_Location currentMode_Location = MODE_DISTANCE; static int scrollIndex = 0; +// Popup overlay state +static uint32_t popupTime = 0; +static int popupTotal = 0; +static int popupStart = 0; +static int popupEnd = 0; +static int popupPage = 1; +static int popupMaxPage = 1; + +static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible // ============================= // Scrolling Logic @@ -57,6 +66,8 @@ void scrollUp() { if (scrollIndex > 0) scrollIndex--; + + popupTime = millis(); // show popup } void scrollDown() @@ -80,6 +91,8 @@ void scrollDown() if (scrollIndex < maxScroll) scrollIndex++; + + popupTime = millis(); } // ============================= @@ -565,6 +578,54 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t const int scrollStartY = y + 3; drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); graphics::drawCommonFooter(display, x, y); + + // Scroll Popup Overlay + if (millis() - popupTime < POPUP_DURATION_MS) { + popupTotal = totalEntries; + + int perPage = visibleNodeRows * totalColumns; + + popupStart = startIndex + 1; + popupEnd = std::min(startIndex + perPage, totalEntries); + + popupPage = (scrollIndex + 1); + popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); + + char buf[32]; + snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Box padding + int padding = 3; + int textW = display->getStringWidth(buf); + int textH = FONT_HEIGHT_SMALL; + int w = textW + padding * 2; + int h = textH + padding * 2; + + // Center of usable screen area: + int headerHeight = FONT_HEIGHT_SMALL - 1; + int footerHeight = FONT_HEIGHT_SMALL + 2; + + int usableTop = headerHeight; + int usableBottom = display->getHeight() - footerHeight; + int usableHeight = usableBottom - usableTop; + + // Center point inside usable area + int px = (display->getWidth() - w) / 2; + int py = usableTop + (usableHeight - h) / 2; + + // Background + display->setColor(BLACK); + display->fillRect(px, py, w, h); + + // Border + display->setColor(WHITE); + display->drawRect(px, py, w, h); + + // Text + display->drawString(px + padding, py + padding, buf); + } } // ============================= From 11cd6370d4d0da0ab4ad33144a9f72ff75230ac6 Mon Sep 17 00:00:00 2001 From: Jason P Date: Thu, 11 Dec 2025 22:42:40 -0600 Subject: [PATCH 7/8] Dynamic scaling of column counts based upon screen size, clean up box drawing --- src/graphics/draw/MenuHandler.cpp | 8 +++- src/graphics/draw/MenuHandler.h | 1 + src/graphics/draw/NodeListRenderer.cpp | 59 ++++++++++++++++---------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index e3e206a54..36649ed39 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1175,7 +1175,7 @@ void menuHandler::nodeNameLengthMenu() LOG_INFO("Setting names to short"); config.display.use_long_node_name = false; } else if (selected == Back) { - menuQueue = screen_options_menu; + menuQueue = node_base_menu; screen->runNow(); } }; @@ -1203,6 +1203,9 @@ void menuHandler::resetNodeDBMenu() LOG_INFO("Initiate node-db reset but keeping favorites"); nodeDB->resetNodes(1); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 0) { + menuQueue = node_base_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -2076,6 +2079,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case position_base_menu: positionBaseMenu(); break; + case node_base_menu: + nodeListMenu(); + break; #if !MESHTASTIC_EXCLUDE_GPS case gps_toggle_menu: GPSToggleMenu(); diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 33dd5b6a6..deef1e9a8 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -19,6 +19,7 @@ class menuHandler clock_face_picker, clock_menu, position_base_menu, + node_base_menu, gps_toggle_menu, gps_format_menu, compass_point_north_menu, diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index b7a7560dd..440bd6f3d 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -493,19 +493,23 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // Space below header y += COMMON_HEADER_HEIGHT; -#if defined(M5STACK_UNITC6L) - int totalColumns = 1; -#elif defined(T_LORA_PAGER) - int totalColumns = 3; + int totalColumns = 1; // Default to 1 column + if (config.display.use_long_node_name) { - totalColumns = 2; + if (SCREEN_WIDTH <= 240) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 240) { + totalColumns = 2; + } + } else { + if (SCREEN_WIDTH <= 64) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) { + totalColumns = 2; + } else { + totalColumns = 3; + } } -#else - int totalColumns = 2; - if (config.display.use_long_node_name) { - totalColumns = 1; - } -#endif int columnWidth = display->getWidth() / totalColumns; @@ -520,7 +524,8 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t for (int i = 0; i < totalEntries; i++) { auto *n = nodeDB->getMeshNodeByIndex(i); - if (!n) continue; + if (!n) + continue; if (n->num == nodeDB->getNodeNum()) continue; if (locationScreen && !n->has_position) @@ -597,11 +602,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->setTextAlignment(TEXT_ALIGN_LEFT); // Box padding - int padding = 3; + int padding = 2; int textW = display->getStringWidth(buf); int textH = FONT_HEIGHT_SMALL; - int w = textW + padding * 2; - int h = textH + padding * 2; + int boxWidth = textW + padding * 3; + int boxHeight = textH + padding * 2; // Center of usable screen area: int headerHeight = FONT_HEIGHT_SMALL - 1; @@ -612,19 +617,27 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int usableHeight = usableBottom - usableTop; // Center point inside usable area - int px = (display->getWidth() - w) / 2; - int py = usableTop + (usableHeight - h) / 2; + int boxLeft = (display->getWidth() - boxWidth) / 2; + int boxTop = usableTop + (usableHeight - boxHeight) / 2; - // Background + // Draw Box display->setColor(BLACK); - display->fillRect(px, py, w, h); - - // Border + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->setColor(WHITE); - display->drawRect(px, py, w, h); // Text - display->drawString(px + padding, py + padding, buf); + display->drawString(boxLeft + padding, boxTop + padding, buf); } } From f09183f5f9caad81de7029a60c7d135c03c3eed3 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Fri, 17 Oct 2025 09:01:51 +1100 Subject: [PATCH 8/8] Update exempt labels for stale bot workflow Adds triaged and backlog to the list of exempt labels. --- .github/workflows/stale_bot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 1286335bb..fc0702bd8 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -22,5 +22,5 @@ jobs: days-before-stale: 45 stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days. close-issue-message: This issue has not had any comment since the last notice. It has been closed automatically. If this is incorrect, or the issue becomes relevant again, please request that it is reopened. - exempt-issue-labels: pinned,3.0 - exempt-pr-labels: pinned,3.0 + exempt-issue-labels: pinned,3.0,triaged,backlog + exempt-pr-labels: pinned,3.0,triaged,backlog