From 7a4a91531288bf250d53b4313f36fd4d7802d2b1 Mon Sep 17 00:00:00 2001 From: Wilson Date: Fri, 25 Jul 2025 06:23:45 +0800 Subject: [PATCH 01/13] Add Trace Route on BaseUI (#7386) * Add TraceRoute function to menus and modules to support node path tracing * Adjust text spacing and line wrapping logic in trace route result result. * Add HAS_SCREEN for TraceRouteModule drawFrame. --------- Co-authored-by: Tom Fifield Co-authored-by: Ben Meadors --- src/graphics/draw/MenuHandler.cpp | 31 +- src/graphics/draw/MenuHandler.h | 4 +- src/modules/TraceRouteModule.cpp | 585 +++++++++++++++++++++++++++++- src/modules/TraceRouteModule.h | 39 +- 4 files changed, 652 insertions(+), 7 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 5eaa2c6bf..731cae3df 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -14,6 +14,7 @@ #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" +#include "modules/TraceRouteModule.h" extern uint16_t TFT_MESH; @@ -428,7 +429,7 @@ void menuHandler::systemBaseMenu() void menuHandler::favoriteBaseMenu() { - enum optionsNumbers { Back, Preset, Freetext, Remove, enumEnd }; + enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd }; static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; static int optionsEnumArray[enumEnd] = {Back, Preset}; int options = 2; @@ -437,6 +438,8 @@ void menuHandler::favoriteBaseMenu() optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; optionsArray[options] = "Remove Favorite"; optionsEnumArray[options++] = Remove; @@ -453,6 +456,10 @@ void menuHandler::favoriteBaseMenu() } else if (selected == Remove) { menuHandler::menuQueue = menuHandler::remove_favorite; screen->runNow(); + } else if (selected == TraceRoute) { + if (traceRouteModule) { + traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum); + } } }; screen->showOverlayBanner(bannerOptions); @@ -491,12 +498,12 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, Verify, Reset }; - static const char *optionsArray[] = {"Back", "Add Favorite", "Key Verification", "Reset NodeDB"}; + enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; + static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; + bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Favorite) { menuQueue = add_favorite; @@ -507,6 +514,9 @@ void menuHandler::nodeListMenu() } else if (selected == Reset) { menuQueue = reset_node_db_menu; screen->runNow(); + } else if (selected == TraceRoute) { + menuQueue = trace_route_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -859,6 +869,16 @@ void menuHandler::removeFavoriteMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::traceRouteMenu() +{ + screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void { + LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule); + if (traceRouteModule) { + traceRouteModule->startTraceRoute(nodenum); + } + }); +} + void menuHandler::testMenu() { @@ -1131,6 +1151,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case remove_favorite: removeFavoriteMenu(); break; + case trace_route_menu: + traceRouteMenu(); + break; case test_menu: testMenu(); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 1f989be79..2e4923241 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -36,7 +36,8 @@ class menuHandler system_base_menu, key_verification_init, key_verification_final_prompt, - throttle_message + trace_route_menu, + throttle_message, }; static screenMenus menuQueue; @@ -64,6 +65,7 @@ class menuHandler static void shutdownMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); + static void traceRouteMenu(); static void testMenu(); static void numberTest(); static void wifiBaseMenu(); diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 41cb35649..bd75c6983 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -1,6 +1,13 @@ #include "TraceRouteModule.h" #include "MeshService.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "mesh/Router.h" #include "meshUtils.h" +#include + +extern graphics::Screen *screen; TraceRouteModule *traceRouteModule; @@ -27,6 +34,123 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti // Set updated route to the payload of the to be flooded packet p.decoded.payload.size = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r); + + if (tracingNode != 0) { + // check isResponseFromTarget + bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode); + bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0); + + // Check if this is a trace route response containing our target node + bool containsTargetNode = false; + for (uint8_t i = 0; i < r->route_count; i++) { + if (r->route[i] == tracingNode) { + containsTargetNode = true; + break; + } + } + for (uint8_t i = 0; i < r->route_back_count; i++) { + if (r->route_back[i] == tracingNode) { + containsTargetNode = true; + break; + } + } + + // Check if this response contains a complete route to our target + bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) || + (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0)); + + LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode, + p.from, p.to, incoming.request_id); + LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d", + isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute); + + if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) { + LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs); + + LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count); + for (int i = 0; i < r->snr_towards_count; i++) { + LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f); + } + for (int i = 0; i < r->snr_back_count; i++) { + LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f); + } + + String result = ""; + + // Show request path (from initiator to target) + if (r->route_count > 0) { + result += getNodeName(nodeDB->getNodeNum()); + for (uint8_t i = 0; i < r->route_count; i++) { + result += " > "; + const char *name = getNodeName(r->route[i]); + float snr = + (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } else { + // Direct connection (no intermediate hops) + result += getNodeName(nodeDB->getNodeNum()); + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[0] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } + + // Show response path (from target back to initiator) + if (r->route_back_count > 0) { + result += getNodeName(tracingNode); + for (int8_t i = r->route_back_count - 1; i >= 0; i--) { + result += " > "; + const char *name = getNodeName(r->route_back[i]); + float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + // add initiator node + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1); + result += "dB)"; + } + } else { + // Direct return path (no intermediate hops) + result += getNodeName(tracingNode); + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[0] / 4.0f, 1); + result += "dB)"; + } + } + + LOG_INFO("Trace route result: %s", result.c_str()); + handleTraceRouteResult(result); + } + } } void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination) @@ -173,8 +297,467 @@ meshtastic_MeshPacket *TraceRouteModule::allocReply() } TraceRouteModule::TraceRouteModule() - : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg) + : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute") { ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; isPromiscuous = true; // We need to update the route even if it is not destined to us +} + +const char *TraceRouteModule::getNodeName(NodeNum node) +{ + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user) { + if (strlen(info->user.short_name) > 0) { + return info->user.short_name; + } + if (strlen(info->user.long_name) > 0) { + return info->user.long_name; + } + } + + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; +} + +bool TraceRouteModule::startTraceRoute(NodeNum node) +{ + LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node); + unsigned long now = millis(); + + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Invalid node"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } + + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Cannot trace self"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } + + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + LOG_INFO("TraceRoute already in progress"); + return false; + } + + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + // Cooldown + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return false; + } + + tracingNode = node; + lastTraceRouteTime = now; + runState = TRACEROUTE_STATE_TRACKING; + bannerText = String("Tracing ") + getNodeName(node); + + LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); + + // 请求焦点,然后触发UI更新事件 + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + // 设置定时器来处理超时检查 + setIntervalFromNow(1000); // 每秒检查一次状态 + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + // Allocate a packet directly from router like the reference code + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + // Set destination and port + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + // Manually encode the RouteDiscovery payload + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, + p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); + LOG_INFO("About to call service->sendToMesh..."); + + if (service) { + LOG_INFO("MeshService is available, sending packet..."); + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Service unavailable"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Failed to send"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } + return true; +} + +void TraceRouteModule::launch(NodeNum node) +{ + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Invalid node"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } + + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Cannot trace self"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } + + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } + + unsigned long now = millis(); + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return; + } + + runState = TRACEROUTE_STATE_TRACKING; + tracingNode = node; + lastTraceRouteTime = now; + bannerText = String("Tracing ") + getNodeName(node); + + requestFocus(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(1000); + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, + p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); + + if (service) { + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Service unavailable"; + resultShowTime = millis(); + tracingNode = 0; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Failed to send"; + resultShowTime = millis(); + tracingNode = 0; + } +} + +void TraceRouteModule::handleTraceRouteResult(const String &result) +{ + resultText = result; + runState = TRACEROUTE_STATE_RESULT; + resultShowTime = millis(); + tracingNode = 0; + + LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str()); + + setIntervalFromNow(1000); + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + LOG_INFO("=== TraceRoute handleTraceRouteResult END ==="); +} + +bool TraceRouteModule::shouldDraw() +{ + bool draw = (runState != TRACEROUTE_STATE_IDLE); + static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE; + if (runState != lastLoggedState) { + LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw); + lastLoggedState = runState; + } + return draw; +} +#if HAS_SCREEN +void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState); + + display->setTextAlignment(TEXT_ALIGN_CENTER); + + if (runState == TRACEROUTE_STATE_TRACKING) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + + } else if (runState == TRACEROUTE_STATE_RESULT) { + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + display->drawString(x, y, "Route Result"); + + int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + if (resultText.length() > 0) { + std::vector lines; + String currentLine = ""; + int maxWidth = display->getWidth() - 4; + + int start = 0; + int newlinePos = resultText.indexOf('\n', start); + + while (newlinePos != -1 || start < resultText.length()) { + String segment; + if (newlinePos != -1) { + segment = resultText.substring(start, newlinePos); + start = newlinePos + 1; + newlinePos = resultText.indexOf('\n', start); + } else { + segment = resultText.substring(start); + start = resultText.length(); + } + + if (display->getStringWidth(segment) <= maxWidth) { + lines.push_back(segment); + } else { + // Try to break at better positions (space, >, <, -) + String remaining = segment; + + while (remaining.length() > 0) { + String tempLine = ""; + int lastGoodBreak = -1; + bool lineComplete = false; + + for (int i = 0; i < remaining.length(); i++) { + char ch = remaining.charAt(i); + String testLine = tempLine + ch; + + if (display->getStringWidth(testLine) > maxWidth) { + if (lastGoodBreak >= 0) { + // Break at the last good position + lines.push_back(remaining.substring(0, lastGoodBreak + 1)); + remaining = remaining.substring(lastGoodBreak + 1); + lineComplete = true; + break; + } else if (tempLine.length() > 0) { + lines.push_back(tempLine); + remaining = remaining.substring(i); + lineComplete = true; + break; + } else { + // Single character exceeds width + lines.push_back(String(ch)); + remaining = remaining.substring(i + 1); + lineComplete = true; + break; + } + } else { + tempLine = testLine; + // Mark good break positions + if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')') { + lastGoodBreak = i; + } + } + } + + if (!lineComplete) { + // Reached end of remaining text + if (tempLine.length() > 0) { + lines.push_back(tempLine); + } + break; + } + } + } + } + + int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing + for (size_t i = 0; i < lines.size(); i++) { + int lineY = contentStartY + (i * lineHeight); + if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { + display->drawString(x + 2, lineY, lines[i]); + } + } + } + + } else if (runState == TRACEROUTE_STATE_COOLDOWN) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + } +} +#endif // HAS_SCREEN +int32_t TraceRouteModule::runOnce() +{ + unsigned long now = millis(); + + if (runState == TRACEROUTE_STATE_IDLE) { + return INT32_MAX; + } + + // Check for tracking timeout + if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { + LOG_INFO("TraceRoute timeout, no response received"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "No response received"; + resultShowTime = now; + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(resultDisplayMs); + return resultDisplayMs; + } + + // Update cooldown display every second + if (runState == TRACEROUTE_STATE_COOLDOWN) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + if (wait > 0) { + String newBannerText = String("Wait for ") + String(wait) + String("s"); + bannerText = newBannerText; + LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str()); + + // Force flash UI + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + if (screen) { + screen->forceDisplay(); + } + + return 1000; + } else { + // Cooldown finished + LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + bannerText = ""; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } + } + + if (runState == TRACEROUTE_STATE_RESULT) { + if (now - resultShowTime >= resultDisplayMs) { + LOG_INFO("TraceRoute result display timeout, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + resultText = ""; + bannerText = ""; + tracingNode = 0; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } else { + return 1000; + } + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + return 1000; + } + + return INT32_MAX; } \ No newline at end of file diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h index afe2b3871..51d98826e 100644 --- a/src/modules/TraceRouteModule.h +++ b/src/modules/TraceRouteModule.h @@ -1,16 +1,40 @@ #pragma once #include "ProtobufModule.h" +#include "concurrency/OSThread.h" +#include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#include "input/InputBroker.h" +#if HAS_SCREEN +#include "OLEDDisplayUi.h" +#endif #define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0]) /** * A module that traces the route to a certain destination node */ -class TraceRouteModule : public ProtobufModule +enum TraceRouteRunState { TRACEROUTE_STATE_IDLE, TRACEROUTE_STATE_TRACKING, TRACEROUTE_STATE_RESULT, TRACEROUTE_STATE_COOLDOWN }; + +class TraceRouteModule : public ProtobufModule, + public Observable, + private concurrency::OSThread { public: TraceRouteModule(); + bool startTraceRoute(NodeNum node); + void launch(NodeNum node); + void handleTraceRouteResult(const String &result); + bool shouldDraw(); +#if HAS_SCREEN + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif + + const char *getNodeName(NodeNum node); + + virtual bool wantUIFrame() override { return shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } + protected: bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override; @@ -20,6 +44,8 @@ class TraceRouteModule : public ProtobufModule the route array containing the IDs of nodes this packet went through */ void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override; + virtual int32_t runOnce() override; + private: // Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination); @@ -31,6 +57,17 @@ class TraceRouteModule : public ProtobufModule Set origin to where the request came from. Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */ void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination); + + TraceRouteRunState runState = TRACEROUTE_STATE_IDLE; + unsigned long lastTraceRouteTime = 0; + unsigned long resultShowTime = 0; + unsigned long cooldownMs = 30000; + unsigned long resultDisplayMs = 10000; + unsigned long trackingTimeoutMs = 10000; + String bannerText; + String resultText; + NodeNum tracingNode = 0; + bool initialized = false; }; extern TraceRouteModule *traceRouteModule; \ No newline at end of file From d1fbf65c5d7db35e8ea52553d92aa6ba92596a61 Mon Sep 17 00:00:00 2001 From: Pedro <40841971+barbabarros@users.noreply.github.com> Date: Thu, 24 Jul 2025 23:57:40 -0300 Subject: [PATCH 02/13] Fix timezone definition for UTC in TZPicker function (#7442) --- src/graphics/draw/MenuHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 731cae3df..dc6470565 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -188,7 +188,7 @@ void menuHandler::TZPicker() } else if (selected == 7) { // Eastern strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); } else if (selected == 8) { // UTC - strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef)); + strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef)); } else if (selected == 9) { // EU/Western strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); } else if (selected == 10) { // EU/Central From bbe548bc980827145114b76260b1b2377dd633e5 Mon Sep 17 00:00:00 2001 From: Pedro <40841971+barbabarros@users.noreply.github.com> Date: Fri, 25 Jul 2025 00:42:42 -0300 Subject: [PATCH 03/13] Add BRT3 timezone option to TZPicker menu (#7438) --- src/graphics/draw/MenuHandler.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index dc6470565..cf19c4825 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -154,6 +154,7 @@ void menuHandler::TZPicker() "US/Mountain", "US/Central", "US/Eastern", + "BR/Brasilia", "UTC", "EU/Western", "EU/" @@ -168,7 +169,7 @@ void menuHandler::TZPicker() BannerOverlayOptions bannerOptions; bannerOptions.message = "Pick Timezone"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 17; + bannerOptions.optionsCount = 19; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { menuHandler::menuQueue = menuHandler::clock_menu; @@ -187,25 +188,27 @@ void menuHandler::TZPicker() strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); } else if (selected == 7) { // Eastern strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 8) { // UTC + } else if (selected == 8) { // Brazil + strncpy(config.device.tzdef, "BRT3", sizeof(config.device.tzdef)); + } else if (selected == 9) { // UTC strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef)); - } else if (selected == 9) { // EU/Western + } else if (selected == 10) { // EU/Western strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); - } else if (selected == 10) { // EU/Central + } else if (selected == 11) { // EU/Central strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); - } else if (selected == 11) { // EU/Eastern + } else if (selected == 12) { // EU/Eastern strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); - } else if (selected == 12) { // Asia/Kolkata + } else if (selected == 13) { // Asia/Kolkata strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); - } else if (selected == 13) { // China + } else if (selected == 14) { // China strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); - } else if (selected == 14) { // AU/AWST + } else if (selected == 15) { // AU/AWST strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); - } else if (selected == 15) { // AU/ACST + } else if (selected == 16) { // AU/ACST strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 16) { // AU/AEST + } else if (selected == 17) { // AU/AEST strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 17) { // NZ + } else if (selected == 18) { // NZ strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); } if (selected != 0) { From 4c6db2c5bdf04856e31e25668e28b8583e9e0925 Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 25 Jul 2025 08:10:35 -0500 Subject: [PATCH 04/13] Fix MHz label (#7455) --- src/graphics/draw/DebugRenderer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 5420d1b4b..5d9b5a33b 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -412,9 +412,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); if (config.lora.channel_num == 0) { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr); + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num); + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { From 4f57a2e2485778d92a98bc2e719a6314cc45539d Mon Sep 17 00:00:00 2001 From: Austin Date: Fri, 25 Jul 2025 16:25:50 -0400 Subject: [PATCH 05/13] Build RP2350 (Pi Pico 2) (#7441) --- .github/workflows/main_matrix.yml | 51 +++++++++++++++++++++--- bin/build-firmware.sh | 2 +- variants/rp2040/rpipicow/platformio.ini | 1 + variants/rp2350/rpipico2w/platformio.ini | 1 + 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index c62d80888..a98bdc011 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -30,7 +30,16 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32, check] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + - check runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -56,6 +65,7 @@ jobs: esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }} nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }} rp2040: ${{ steps.jsonStep.outputs.rp2040 }} + rp2350: ${{ steps.jsonStep.outputs.rp2350 }} stm32: ${{ steps.jsonStep.outputs.stm32 }} check: ${{ steps.jsonStep.outputs.check }} @@ -145,7 +155,7 @@ jobs: pio_env: ${{ matrix.board }} platform: nrf52840 - build-rpi2040: + build-rp2040: needs: [setup, version] strategy: fail-fast: false @@ -156,6 +166,17 @@ jobs: pio_env: ${{ matrix.board }} platform: rp2040 + build-rp2350: + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.rp2350) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: rp2350 + build-stm32: needs: [setup, version] strategy: @@ -243,7 +264,15 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 runs-on: ubuntu-latest needs: [ @@ -253,7 +282,8 @@ jobs: build-esp32c3, build-esp32c6, build-nrf52840, - build-rpi2040, + build-rp2040, + build-rp2350, build-stm32, ] steps: @@ -392,7 +422,15 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-artifacts, version] @@ -449,7 +487,8 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-firmware, version] env: - targets: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,stm32 + targets: |- + esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh index c53f1b660..fdd7caa11 100644 --- a/bin/build-firmware.sh +++ b/bin/build-firmware.sh @@ -11,7 +11,7 @@ elif (echo $2 | grep -q "nrf52"); then elif (echo $2 | grep -q "stm32"); then bin/build-stm32.sh $1 elif (echo $2 | grep -q "rpi2040"); then - bin/build-rpi2040.sh $1 + bin/build-rp2xx0.sh $1 else echo "Unknown target $2" exit 1 diff --git a/variants/rp2040/rpipicow/platformio.ini b/variants/rp2040/rpipicow/platformio.ini index 658d113d7..60845ba39 100644 --- a/variants/rp2040/rpipicow/platformio.ini +++ b/variants/rp2040/rpipicow/platformio.ini @@ -2,6 +2,7 @@ extends = rp2040_base board = rpipicow board_level = pr +board_check = true upload_protocol = picotool # add our variants files to the include and src paths build_flags = diff --git a/variants/rp2350/rpipico2w/platformio.ini b/variants/rp2350/rpipico2w/platformio.ini index 59b1354b4..5dbce533b 100644 --- a/variants/rp2350/rpipico2w/platformio.ini +++ b/variants/rp2350/rpipico2w/platformio.ini @@ -2,6 +2,7 @@ extends = rp2350_base board = rpipico2w board_level = pr +board_check = true upload_protocol = jlink # debug settings for external openocd with RP2040 support (custom build) debug_tool = custom From bbc638ab82b3834d11325067f38641c0112c2459 Mon Sep 17 00:00:00 2001 From: Iris Date: Fri, 25 Jul 2025 23:36:37 +0300 Subject: [PATCH 06/13] Create Platformio.ini (#7450) --- variants/nrf52840/diy/WashTastic/Platformio.ini | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 variants/nrf52840/diy/WashTastic/Platformio.ini diff --git a/variants/nrf52840/diy/WashTastic/Platformio.ini b/variants/nrf52840/diy/WashTastic/Platformio.ini new file mode 100644 index 000000000..e67c25910 --- /dev/null +++ b/variants/nrf52840/diy/WashTastic/Platformio.ini @@ -0,0 +1,13 @@ +; Promicro + E22900M30S +[env:WashTastic] +extends = nrf52840_base +board = promicro-nrf52840 +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo + -D PRIVATE_HW + -D EBYTE_E22_900M30S +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink \ No newline at end of file From fc1e6ccb8cd90463c9e2647e3fba9c10b7543ad3 Mon Sep 17 00:00:00 2001 From: Iris Date: Sat, 26 Jul 2025 13:13:02 +0300 Subject: [PATCH 07/13] Rename Platformio.ini to platformio.ini (#7468) --- .../nrf52840/diy/WashTastic/{Platformio.ini => platformio.ini} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename variants/nrf52840/diy/WashTastic/{Platformio.ini => platformio.ini} (95%) diff --git a/variants/nrf52840/diy/WashTastic/Platformio.ini b/variants/nrf52840/diy/WashTastic/platformio.ini similarity index 95% rename from variants/nrf52840/diy/WashTastic/Platformio.ini rename to variants/nrf52840/diy/WashTastic/platformio.ini index e67c25910..881b961e1 100644 --- a/variants/nrf52840/diy/WashTastic/Platformio.ini +++ b/variants/nrf52840/diy/WashTastic/platformio.ini @@ -10,4 +10,4 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> lib_deps = ${nrf52840_base.lib_deps} -debug_tool = jlink \ No newline at end of file +debug_tool = jlink From a506dc6b65d8d4dbf5b7106de598e89d8d25484f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 26 Jul 2025 05:38:24 -0500 Subject: [PATCH 08/13] Fix MQTT config bugs (#7446) * Fix mqtt config bugs * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add client notification * Verbiage * Update src/mqtt/MQTT.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mqtt/MQTT.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mqtt/MQTT.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mqtt/MQTT.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove test that is no longer true * This test no longer exists * Fix client notification crap * Suppress false positive * Revert "Suppress false positive" This reverts commit bead96eaee31f11ed631c016eb0424055e923a29. * Try macro exclusion * Derp * Fix --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jason P --- src/mesh/MeshService.h | 2 +- src/modules/AdminModule.cpp | 7 ++++++- src/mqtt/MQTT.cpp | 27 +++++++++++++++++++++------ test/test_mqtt/MQTT.cpp | 23 +++++++++++++---------- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index 89d3b15d0..f7d79366e 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -146,7 +146,7 @@ class MeshService virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); /// Send a ClientNotification to the phone - void sendClientNotification(meshtastic_ClientNotification *cn); + virtual void sendClientNotification(meshtastic_ClientNotification *cn); /// Send an error response to the phone void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 33d5e1016..4b910a959 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -798,8 +798,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { - if (!hasOpenEditTransaction) + // If we are in an open transaction or configuring MQTT, defer disabling Bluetooth + // Otherwise, disable Bluetooth to prevent the phone from interfering with the config + if (!hasOpenEditTransaction && c.which_payload_variant != meshtastic_ModuleConfig_mqtt_tag) disableBluetooth(); + switch (c.which_payload_variant) { case meshtastic_ModuleConfig_mqtt_tag: #if MESHTASTIC_EXCLUDE_MQTT @@ -810,6 +813,8 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { return false; } + // Disable Bluetooth to prevent interference during MQTT configuration + disableBluetooth(); moduleConfig.has_mqtt = true; moduleConfig.mqtt = c.payload_variant.mqtt; #endif diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 091612827..21d4a8fa0 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -39,6 +39,7 @@ #include #define ntohl __ntohl #endif +#include MQTT *mqtt; @@ -624,18 +625,32 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); } #else - LOG_ERROR("Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"); + const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif return false; #endif } const bool defaultServer = isDefaultServer(parsed.serverAddr); - if (defaultServer && config.tls_enabled) { - LOG_ERROR("Invalid MQTT config: TLS was enabled, but the default server does not support TLS"); - return false; - } if (defaultServer && parsed.serverPort != PubSubConfig::defaultPort) { - LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", parsed.serverPort); + const char *warning = "Invalid MQTT config: default server address must not have a port specified"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif return false; } return true; diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 8047079ba..32d81f6b4 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -27,6 +27,12 @@ #include #include +#if defined(UNIT_TEST) +#define IS_RUNNING_TESTS 1 +#else +#define IS_RUNNING_TESTS 0 +#endif + namespace { // Minimal router needed to receive messages from MQTT. @@ -56,7 +62,13 @@ class MockMeshService : public MeshService messages_.emplace_back(*m); releaseMqttClientProxyMessageToPool(m); } - std::list messages_; // Messages received from the MeshService. + void sendClientNotification(meshtastic_ClientNotification *n) override + { + notifications_.emplace_back(*n); + releaseClientNotificationToPool(n); + } + std::list messages_; // Messages received from the MeshService. + std::list notifications_; // Notifications received from the MeshService. }; // Minimal NodeDB needed to return values from getMeshNode. @@ -823,14 +835,6 @@ void test_configWithDefaultServerAndInvalidPort(void) TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } -// Configuration with the default server and tls_enabled = true is invalid. -void test_configWithDefaultServerAndInvalidTLSEnabled(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {.tls_enabled = true}; - - TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); -} - // isValidConfig connects to a custom host and port. void test_configCustomHostAndPort(void) { @@ -911,7 +915,6 @@ void setup() RUN_TEST(test_configEnabledEmptyIsValid); RUN_TEST(test_configWithDefaultServer); RUN_TEST(test_configWithDefaultServerAndInvalidPort); - RUN_TEST(test_configWithDefaultServerAndInvalidTLSEnabled); RUN_TEST(test_configCustomHostAndPort); RUN_TEST(test_configWithConnectionFailure); RUN_TEST(test_configWithTLSEnabled); From df8b629c2c4c43b5939e7153a427100c1b769a42 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 26 Jul 2025 23:09:26 +1000 Subject: [PATCH 09/13] Clear position on GPS deactivation, unless using fixed position (#7464) * Clear position on GPS deactivation, unless using fixed position As reported by @dreimal8 , and confirmed by @tuxmobil , when using and then subsequently disabling GPS the last position retrieved from the GPS was stored and continued to be broadcast. This change introduces a check to see if we are transitioning from GPS Enabled to GPS Disabled or Not Present. If we are, and fixed position is not in use, then we clear the local position. This will prevent inaccurate and undesired position broadcasts for those who disable their GPS. Fixes https://github.com/meshtastic/firmware/issues/7228 * Update triple click to also clear position --------- Co-authored-by: Ben Meadors --- src/modules/AdminModule.cpp | 9 +++++++++ src/modules/SystemCommandsModule.cpp | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4b910a959..87d423f21 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -638,7 +638,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) case meshtastic_Config_position_tag: LOG_INFO("Set config: Position"); config.has_position = true; + // If we have turned off the GPS (disabled or not present) and we're not using fixed position, + // clear the stored position since it may not get updated + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED && + config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + } config.position = c.payload_variant.position; + // Save nodedb as well in case we got a fixed position packet break; case meshtastic_Config_power_tag: diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index 2d534bd67..74b9678f4 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -89,6 +89,11 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) #if !MESHTASTIC_EXCLUDE_GPS if (gps) { LOG_WARN("GPS Toggle2"); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + config.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + nodeDB->saveToDisk(); + } gps->toggleGpsMode(); const char *msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; From 7c5e2c539387b178661afa4279f6c5e92a4dec2f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 16:21:49 -0500 Subject: [PATCH 10/13] Update protobufs (#7473) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/deviceonly.pb.h | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/protobufs b/protobufs index d31cd890d..9bac2886f 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit d31cd890d58ffa7e3524e0685a8617bbd181a1c6 +Subproject commit 9bac2886f9344f25716921467a82e8b0326107cd diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index e709db6c4..ba1a52b27 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -362,7 +362,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size #define meshtastic_BackupPreferences_size 2271 #define meshtastic_ChannelFile_size 718 -#define meshtastic_DeviceState_size 1724 +#define meshtastic_DeviceState_size 1728 #define meshtastic_NodeInfoLite_size 196 #define meshtastic_PositionLite_size 28 #define meshtastic_UserLite_size 98 diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index abc06e635..8e6524042 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -935,6 +935,9 @@ typedef struct _meshtastic_MyNodeInfo { char pio_env[40]; /* The indicator for whether this device is running event firmware and which */ meshtastic_FirmwareEdition firmware_edition; + /* The number of nodes in the nodedb. + This is used by the phone to know how many NodeInfo packets to expect on want_config */ + uint16_t nodedb_count; } meshtastic_MyNodeInfo; /* Debug output from the device. @@ -1322,7 +1325,7 @@ extern "C" { #define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0} #define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0} -#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN} +#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_default {0, 0, 0, 0} #define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}} @@ -1353,7 +1356,7 @@ extern "C" { #define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0} #define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0} -#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN} +#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} #define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}} @@ -1477,6 +1480,7 @@ extern "C" { #define meshtastic_MyNodeInfo_device_id_tag 12 #define meshtastic_MyNodeInfo_pio_env_tag 13 #define meshtastic_MyNodeInfo_firmware_edition_tag 14 +#define meshtastic_MyNodeInfo_nodedb_count_tag 15 #define meshtastic_LogRecord_message_tag 1 #define meshtastic_LogRecord_time_tag 2 #define meshtastic_LogRecord_source_tag 3 @@ -1710,7 +1714,8 @@ X(a, STATIC, SINGULAR, UINT32, reboot_count, 8) \ X(a, STATIC, SINGULAR, UINT32, min_app_version, 11) \ X(a, STATIC, SINGULAR, BYTES, device_id, 12) \ X(a, STATIC, SINGULAR, STRING, pio_env, 13) \ -X(a, STATIC, SINGULAR, UENUM, firmware_edition, 14) +X(a, STATIC, SINGULAR, UENUM, firmware_edition, 14) \ +X(a, STATIC, SINGULAR, UINT32, nodedb_count, 15) #define meshtastic_MyNodeInfo_CALLBACK NULL #define meshtastic_MyNodeInfo_DEFAULT NULL @@ -1993,7 +1998,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_LowEntropyKey_size 0 #define meshtastic_MeshPacket_size 378 #define meshtastic_MqttClientProxyMessage_size 501 -#define meshtastic_MyNodeInfo_size 79 +#define meshtastic_MyNodeInfo_size 83 #define meshtastic_NeighborInfo_size 258 #define meshtastic_Neighbor_size 22 #define meshtastic_NodeInfo_size 323 From 28aeb0f09e9bb633d5ba8b6511050ac778e030c0 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 26 Jul 2025 19:55:54 -0500 Subject: [PATCH 11/13] Validate Serial config console override modes (#7470) * Validate serial config console override modes * Update src/modules/SerialModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Disable * Guard serial module * Guards --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/modules/AdminModule.cpp | 32 ++++-- src/modules/SerialModule.cpp | 20 ++++ src/modules/SerialModule.h | 2 + test/test_serial/SerialModule.cpp | 156 ++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 test/test_serial/SerialModule.cpp diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 87d423f21..1a9c3a7a7 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -43,6 +43,10 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #include "motion/AccelerometerThread.h" #endif +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) +#include "SerialModule.h" +#endif AdminModule *adminModule; bool hasOpenEditTransaction; @@ -807,10 +811,12 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { - // If we are in an open transaction or configuring MQTT, defer disabling Bluetooth + // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth // Otherwise, disable Bluetooth to prevent the phone from interfering with the config - if (!hasOpenEditTransaction && c.which_payload_variant != meshtastic_ModuleConfig_mqtt_tag) + if (!hasOpenEditTransaction && + !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { disableBluetooth(); + } switch (c.which_payload_variant) { case meshtastic_ModuleConfig_mqtt_tag: @@ -830,6 +836,14 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) break; case meshtastic_ModuleConfig_serial_tag: LOG_INFO("Set module config: Serial"); +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + if (!SerialModule::isValidConfig(c.payload_variant.serial)) { + LOG_ERROR("Invalid serial config"); + return false; + } + disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration +#endif moduleConfig.has_serial = true; moduleConfig.serial = c.payload_variant.serial; break; @@ -985,9 +999,10 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32 // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private - // and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password); - // r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag; + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); @@ -1071,9 +1086,10 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private - // and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password); - // r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag; + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index f3921ef19..f3091e5bf 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -74,6 +74,26 @@ static Print *serialPrint = &Serial2; char serialBytes[512]; size_t serialPayloadSize; +bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) +{ + if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO)) { + const char *warning = + "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + snprintf(cn->message, sizeof(cn->message), "%s", warning); + service->sendClientNotification(cn); +#endif + return false; + } + + return true; +} + SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") { switch (moduleConfig.serial.mode) { diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h index fa86db28f..1c74c927c 100644 --- a/src/modules/SerialModule.h +++ b/src/modules/SerialModule.h @@ -20,6 +20,8 @@ class SerialModule : public StreamAPI, private concurrency::OSThread public: SerialModule(); + static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config); + protected: virtual int32_t runOnce() override; diff --git a/test/test_serial/SerialModule.cpp b/test/test_serial/SerialModule.cpp new file mode 100644 index 000000000..1bccf04a7 --- /dev/null +++ b/test/test_serial/SerialModule.cpp @@ -0,0 +1,156 @@ +#include "DebugConfiguration.h" +#include "TestUtil.h" +#include + +#ifdef ARCH_PORTDUINO +#include "configuration.h" + +#if defined(UNIT_TEST) +#define IS_RUNNING_TESTS 1 +#else +#define IS_RUNNING_TESTS 0 +#endif + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) +#include "modules/SerialModule.h" +#endif + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + +// Test that empty configuration is valid. +void test_serialConfigEmptyIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that basic enabled configuration is valid. +void test_serialConfigEnabledIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and NMEA mode is valid. +void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and CalTopo mode is valid. +void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and DEFAULT mode is invalid. +void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and SIMPLE mode is invalid. +void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and TEXTMSG mode is invalid. +void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and PROTO mode is invalid. +void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that various modes work without override_console_serial_port. +void test_serialConfigVariousModesWithoutOverrideAreValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false}; + + // Test DEFAULT mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test SIMPLE mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test TEXTMSG mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test PROTO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test NMEA mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test CALTOPO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +#endif // Architecture check + +void setup() +{ + initializeTestEnvironment(); + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + UNITY_BEGIN(); + RUN_TEST(test_serialConfigEmptyIsValid); + RUN_TEST(test_serialConfigEnabledIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid); + RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid); + exit(UNITY_END()); +#else + LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture"); + UNITY_BEGIN(); + UNITY_END(); +#endif +} +#else +void setup() +{ + initializeTestEnvironment(); + LOG_WARN("This test requires the ARCH_PORTDUINO variant"); + UNITY_BEGIN(); + UNITY_END(); +} +#endif +void loop() {} From aa3b14ce720b2f52cdd553b1a39477673c2c3783 Mon Sep 17 00:00:00 2001 From: mikecarper <135079168+mikecarper@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:03:01 -0700 Subject: [PATCH 12/13] bugfix Add rssi and snr to the store and forward code. (#7462) * Update StoreForwardModule.cpp * Update StoreForwardModule.h --------- Co-authored-by: Ben Meadors --- src/modules/StoreForwardModule.cpp | 6 +++++- src/modules/StoreForwardModule.h | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 0a6e1b4c4..72ac99118 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -202,6 +202,8 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id; this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji; this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size; + this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi; + this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr; memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); this->packetHistoryTotalCount++; @@ -252,6 +254,8 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t p->decoded.reply_id = this->packetHistory[i].reply_id; p->rx_time = this->packetHistory[i].time; p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji; + p->rx_rssi = this->packetHistory[i].rx_rssi; + p->rx_snr = this->packetHistory[i].rx_snr; // Let's assume that if the server received the S&F request that the client is in range. // TODO: Make this configurable. @@ -623,4 +627,4 @@ StoreForwardModule::StoreForwardModule() disable(); } #endif -} \ No newline at end of file +} diff --git a/src/modules/StoreForwardModule.h b/src/modules/StoreForwardModule.h index 30db1625c..25836eded 100644 --- a/src/modules/StoreForwardModule.h +++ b/src/modules/StoreForwardModule.h @@ -19,6 +19,8 @@ struct PacketHistoryStruct { bool emoji; uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; pb_size_t payload_size; + int32_t rx_rssi; + float rx_snr; }; class StoreForwardModule : private concurrency::OSThread, public ProtobufModule @@ -108,4 +110,4 @@ class StoreForwardModule : private concurrency::OSThread, public ProtobufModule< virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p); }; -extern StoreForwardModule *storeForwardModule; \ No newline at end of file +extern StoreForwardModule *storeForwardModule; From 3ecff48722d3a60e8c84f6d2573cd3688915a279 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 28 Jul 2025 07:31:33 -0500 Subject: [PATCH 13/13] Set firmware edition (for events) from userprefs (#7488) * Set firmware edition (for events) from userprefs * Spaces in the right places --- src/mesh/NodeDB.cpp | 3 +++ userPrefs.jsonc | 1 + 2 files changed, 4 insertions(+) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index b7120a064..881dc6ab7 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -406,6 +406,9 @@ NodeDB::NodeDB() config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; config.position.gps_enabled = 0; } +#ifdef USERPREFS_FIRMWARE_EDITION + myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION; +#endif #ifdef USERPREFS_FIXED_GPS if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset. meshtastic_Position fixedGPS = meshtastic_Position_init_default; diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 3da8e7ba6..f6f3ef995 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -23,6 +23,7 @@ // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", // "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, LOST AND FOUND, and REPEATER roles are restricted. // "USERPREFS_EVENT_MODE": "1", + // "USERPREFS_FIRMWARE_EDITION": "meshtastic_FirmwareEdition_BURNING_MAN", // "USERPREFS_FIXED_BLUETOOTH": "121212", // "USERPREFS_FIXED_GPS": "", // "USERPREFS_FIXED_GPS_ALT": "0",