diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bc660170c..e3f076ce0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "features": { "ghcr.io/devcontainers/features/python:1": { "installTools": true, - "version": "3.13" + "version": "3.14" } }, "customizations": { diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 0a43c3079..73baa5345 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,15 +8,15 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.486 - - renovate@41.157.0 + - checkov@3.2.490 + - renovate@41.173.1 - prettier@3.6.2 - - trufflehog@3.90.11 + - trufflehog@3.90.13 - yamllint@1.37.1 - bandit@1.8.6 - trivy@0.67.2 - taplo@0.10.0 - - ruff@0.14.1 + - ruff@0.14.3 - isort@7.0.0 - markdownlint@0.45.0 - oxipng@9.1.5 @@ -28,7 +28,7 @@ lint: - shellcheck@0.11.0 - black@25.9.0 - git-diff-check - - gitleaks@8.28.0 + - gitleaks@8.29.0 - clang-format@16.0.3 ignore: - linters: [ALL] diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index f3fd00de7..bce06f907 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/d3f6e339534233c7217818867368767590ce549e.zip + https://github.com/meshtastic/platform-native/archive/f566d364204416cdbf298e349213f7d551f793d9.zip framework = arduino build_src_filter = diff --git a/platformio.ini b/platformio.ini index 7c63ad7ad..d62504ae3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -120,7 +120,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/19b7855e9a1d9deff37391659ca7194e4ef57c43.zip + https://github.com/meshtastic/device-ui/archive/28167c67dfd13015a0b5eef1828f95fe8e3ab7c3.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -176,7 +176,7 @@ lib_deps = # renovate: datasource=custom.pio depName=Adafruit LTR390 Library packageName=adafruit/library/Adafruit LTR390 Library adafruit/Adafruit LTR390 Library@1.1.2 # renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075 - adafruit/Adafruit PCT2075@1.0.5 + adafruit/Adafruit PCT2075@1.0.6 # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 dfrobot/DFRobot_BMM150@1.0.0 # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 diff --git a/protobufs b/protobufs index bf149bbdc..7654db2e2 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit bf149bbdcce45ba7cd8643db7cb25e5c8815072b +Subproject commit 7654db2e2d1834aebde40090a9b74162ad1048ae diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 10c20cbd6..e1d309a10 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -936,7 +936,9 @@ void menuHandler::BluetoothToggleMenu() bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1 || selected == 2) { + if (selected == 0) + return; + else if (selected != (config.bluetooth.enabled ? 1 : 2)) { InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } diff --git a/src/input/TDeckProKeyboard.cpp b/src/input/TDeckProKeyboard.cpp index 098e0804a..eeafe4949 100644 --- a/src/input/TDeckProKeyboard.cpp +++ b/src/input/TDeckProKeyboard.cpp @@ -57,7 +57,7 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = { {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x20, 0x00, 0x00}, - {0x00, 0x00, 0x00}, + {0x00, 0x00, '0'}, {0x00, 0x00, 0x00} // R_Shift, sym, space, mic, L_Shift }; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index dec8411fe..bda6f4ea4 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -978,12 +978,25 @@ void NodeDB::installDefaultChannels() channelFile.version = DEVICESTATE_CUR_VER; } -void NodeDB::resetNodes() +void NodeDB::resetNodes(bool keepFavorites) { if (!config.position.fixed_position) clearLocalPosition(); numMeshNodes = 1; - std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); + if (keepFavorites) { + LOG_INFO("Clearing node database - preserving favorites"); + for (size_t i = 0; i < meshNodes->size(); i++) { + meshtastic_NodeInfoLite &node = meshNodes->at(i); + if (i > 0 && !node.is_favorite) { + node = meshtastic_NodeInfoLite(); + } else { + numMeshNodes += 1; + } + }; + } else { + LOG_INFO("Clearing node database - removing favorites"); + std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); + } devicestate.has_rx_text_message = false; devicestate.has_rx_waypoint = false; saveNodeDatabaseToDisk(); @@ -1632,13 +1645,32 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) // If should_ignore is set, // we need to clear the public key and other cruft, in addition to setting the node as ignored info->is_ignored = true; + info->is_favorite = false; info->has_device_metrics = false; info->has_position = false; info->user.public_key.size = 0; info->user.public_key.bytes[0] = 0; } else { - info->last_heard = getValidTime(RTCQualityNTP); - info->is_favorite = true; + /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with + * public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM! + */ + + /* "Boring old nodes" are the first to be evicted out of the node database when full. This includes a newly-zeroed + * nodeinfo because it has: !is_favorite && last_heard==0. To keep this from happening when we addFromContact, we set the + * new node as a favorite, and we leave last_heard alone (even if it's zero). + */ + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it + // without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to add + // contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll set + // last_heard to now, so that the add_contact node doesn't immediately get evicted. + info->last_heard = getTime(); + } else { + // Normal case: set is_favorite to prevent expiration. + // last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB). + info->is_favorite = true; + } + // As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified if (contact.manually_verified) { info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index e8724f2c9..444ac13e4 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -229,7 +229,8 @@ class NodeDB */ size_t getNumOnlineMeshNodes(bool localOnly = false); - void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(), removeNodeByNum(NodeNum nodeNum); + void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(bool keepFavorites = false), + removeNodeByNum(NodeNum nodeNum); bool factoryReset(bool eraseBleBonds = false); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 5cf8bfa7d..05f47d7f4 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -479,6 +479,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id); } else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) { LOG_ERROR("Invalid portnum (bad psk?)!"); +#if !(MESHTASTIC_EXCLUDE_PKI) + } else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) { + LOG_WARN("Rejecting legacy DM"); + return DecodeState::DECODE_FAILURE; +#endif } else { p->decoded = decodedtmp; p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index 7cc896292..a542cf29c 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -272,8 +272,9 @@ typedef struct _meshtastic_AdminMessage { int32_t shutdown_seconds; /* Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. */ int32_t factory_reset_config; - /* Tell the node to reset the nodedb. */ - int32_t nodedb_reset; + /* Tell the node to reset the nodedb. + When true, favorites are preserved through reset. */ + bool nodedb_reset; }; /* The node generates this key and sends it with any get_x_response packets. The client MUST include the same key with any set_x commands. Key expires after 300 seconds. @@ -459,7 +460,7 @@ X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulato X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_seconds,reboot_seconds), 97) \ X(a, STATIC, ONEOF, INT32, (payload_variant,shutdown_seconds,shutdown_seconds), 98) \ X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_config,factory_reset_config), 99) \ -X(a, STATIC, ONEOF, INT32, (payload_variant,nodedb_reset,nodedb_reset), 100) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,nodedb_reset,nodedb_reset), 100) \ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) #define meshtastic_AdminMessage_CALLBACK NULL #define meshtastic_AdminMessage_DEFAULT NULL diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 059af57ae..0da44cce0 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -284,6 +284,10 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_T_WATCH_ULTRA = 114, /* Elecrow ThinkNode M3 */ meshtastic_HardwareModel_THINKNODE_M3 = 115, + /* RAK WISMESH_TAP_V2 with ESP32-S3 CPU */ + meshtastic_HardwareModel_WISMESH_TAP_V2 = 116, + /* RAK3401 */ + meshtastic_HardwareModel_RAK3401 = 117, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index d300ff53b..a98515059 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -104,9 +104,18 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta (config.security.admin_key[2].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { LOG_INFO("PKC admin payload with authorized sender key"); + + // Automatically favorite the node that is using the admin key auto remoteNode = nodeDB->getMeshNode(mp.from); if (remoteNode && !remoteNode->is_favorite) { - remoteNode->is_favorite = true; + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it + // without the user doing so deliberately. + LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); + } else { + LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); + remoteNode->is_favorite = true; + } } } else { myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); @@ -280,7 +289,12 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta case meshtastic_AdminMessage_nodedb_reset_tag: { disableBluetooth(); LOG_INFO("Initiate node-db reset"); - nodeDB->resetNodes(); + // CLIENT_BASE, ROUTER and ROUTER_LATE are able to preserve the remaining hop count when relaying a packet via a + // favorited node, so ensure that their favorites are kept on reset + bool rolePreference = + isOneOf(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE, + meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE); + nodeDB->resetNodes(rolePreference ? rolePreference : r->nodedb_reset); reboot(DEFAULT_REBOOT_SECONDS); break; } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 9f95a9e20..f435f6060 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -973,8 +973,14 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha LOG_INFO("Send message id=%u, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); if (p->to != 0xffffffff) { - LOG_INFO("Proactively adding %x as favorite node", p->to); - nodeDB->set_favorite(true, p->to); + // Only add as favorite if our role is NOT CLIENT_BASE + if (config.device.role != 12) { + LOG_INFO("Proactively adding %x as favorite node", p->to); + nodeDB->set_favorite(true, p->to); + } else { + LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", p->to); + } + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); p->pki_encrypted = true; p->channel = 0; diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 72ac99118..b8a710bf5 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -204,6 +204,10 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) 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; + this->packetHistory[this->packetHistoryTotalCount].hop_start = mp.hop_start; + this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit; + this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt; + this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism; memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); this->packetHistoryTotalCount++; @@ -256,6 +260,10 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t 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; + p->hop_start = this->packetHistory[i].hop_start; + p->hop_limit = this->packetHistory[i].hop_limit; + p->via_mqtt = this->packetHistory[i].via_mqtt; + p->transport_mechanism = (meshtastic_MeshPacket_TransportMechanism)this->packetHistory[i].transport_mechanism; // Let's assume that if the server received the S&F request that the client is in range. // TODO: Make this configurable. diff --git a/src/modules/StoreForwardModule.h b/src/modules/StoreForwardModule.h index 25836eded..148568e1b 100644 --- a/src/modules/StoreForwardModule.h +++ b/src/modules/StoreForwardModule.h @@ -21,6 +21,10 @@ struct PacketHistoryStruct { pb_size_t payload_size; int32_t rx_rssi; float rx_snr; + uint8_t hop_start; + uint8_t hop_limit; + bool via_mqtt; + uint8_t transport_mechanism; }; class StoreForwardModule : private concurrency::OSThread, public ProtobufModule diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 5bdde1919..87a2f1bd2 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -11,6 +11,113 @@ extern graphics::Screen *screen; TraceRouteModule *traceRouteModule; +void TraceRouteModule::setResultText(const String &text) +{ + resultText = text; + resultLines.clear(); + resultLinesDirty = true; +} + +void TraceRouteModule::clearResultLines() +{ + resultLines.clear(); + resultLinesDirty = false; +} +#if HAS_SCREEN +void TraceRouteModule::rebuildResultLines(OLEDDisplay *display) +{ + if (!display) { + resultLinesDirty = false; + return; + } + + resultLines.clear(); + + if (resultText.length() == 0) { + resultLinesDirty = false; + return; + } + + int maxWidth = display->getWidth() - 4; + if (maxWidth <= 0) { + resultLinesDirty = false; + return; + } + + int start = 0; + int textLength = resultText.length(); + + while (start <= textLength) { + int newlinePos = resultText.indexOf('\n', start); + String segment; + + if (newlinePos != -1) { + segment = resultText.substring(start, newlinePos); + start = newlinePos + 1; + } else { + segment = resultText.substring(start); + start = textLength + 1; + } + + if (segment.length() == 0) { + resultLines.push_back(""); + continue; + } + + if (display->getStringWidth(segment) <= maxWidth) { + resultLines.push_back(segment); + continue; + } + + String remaining = segment; + + while (remaining.length() > 0) { + String tempLine = ""; + int lastGoodBreak = -1; + bool lineComplete = false; + + for (int i = 0; i < static_cast(remaining.length()); i++) { + char ch = remaining.charAt(i); + String testLine = tempLine + ch; + + if (display->getStringWidth(testLine) > maxWidth) { + if (lastGoodBreak >= 0) { + resultLines.push_back(remaining.substring(0, lastGoodBreak + 1)); + remaining = remaining.substring(lastGoodBreak + 1); + lineComplete = true; + break; + } else if (tempLine.length() > 0) { + resultLines.push_back(tempLine); + remaining = remaining.substring(i); + lineComplete = true; + break; + } else { + resultLines.push_back(String(ch)); + remaining = remaining.substring(i + 1); + lineComplete = true; + break; + } + } else { + tempLine = testLine; + if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') { + lastGoodBreak = i; + } + } + } + + if (!lineComplete) { + if (tempLine.length() > 0) { + resultLines.push_back(tempLine); + } + break; + } + } + } + + resultLinesDirty = false; +} +#endif + bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) { // We only alter the packet in alterReceivedProtobuf() @@ -406,7 +513,7 @@ bool TraceRouteModule::startTraceRoute(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"; + setResultText("Invalid node"); resultShowTime = millis(); tracingNode = 0; @@ -420,7 +527,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node) if (node == nodeDB->getNodeNum()) { LOG_ERROR("Cannot trace route to self: 0x%08x", node); runState = TRACEROUTE_STATE_RESULT; - resultText = "Cannot trace self"; + setResultText("Cannot trace self"); resultShowTime = millis(); tracingNode = 0; @@ -447,6 +554,8 @@ bool TraceRouteModule::startTraceRoute(NodeNum node) unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; bannerText = String("Wait for ") + String(wait) + String("s"); runState = TRACEROUTE_STATE_COOLDOWN; + resultText = ""; + clearResultLines(); requestFocus(); UIFrameEvent e; @@ -459,6 +568,8 @@ bool TraceRouteModule::startTraceRoute(NodeNum node) tracingNode = node; lastTraceRouteTime = now; runState = TRACEROUTE_STATE_TRACKING; + resultText = ""; + clearResultLines(); bannerText = String("Tracing ") + getNodeName(node); LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); @@ -501,7 +612,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node) } else { LOG_ERROR("MeshService is NULL!"); runState = TRACEROUTE_STATE_RESULT; - resultText = "Service unavailable"; + setResultText("Service unavailable"); resultShowTime = millis(); tracingNode = 0; @@ -514,7 +625,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node) } else { LOG_ERROR("Failed to allocate TraceRoute packet from router"); runState = TRACEROUTE_STATE_RESULT; - resultText = "Failed to send"; + setResultText("Failed to send"); resultShowTime = millis(); tracingNode = 0; @@ -532,7 +643,7 @@ 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"; + setResultText("Invalid node"); resultShowTime = millis(); tracingNode = 0; @@ -546,7 +657,7 @@ void TraceRouteModule::launch(NodeNum node) if (node == nodeDB->getNodeNum()) { LOG_ERROR("Cannot trace route to self: 0x%08x", node); runState = TRACEROUTE_STATE_RESULT; - resultText = "Cannot trace self"; + setResultText("Cannot trace self"); resultShowTime = millis(); tracingNode = 0; @@ -568,6 +679,8 @@ void TraceRouteModule::launch(NodeNum node) unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; bannerText = String("Wait for ") + String(wait) + String("s"); runState = TRACEROUTE_STATE_COOLDOWN; + resultText = ""; + clearResultLines(); requestFocus(); UIFrameEvent e; @@ -580,6 +693,8 @@ void TraceRouteModule::launch(NodeNum node) runState = TRACEROUTE_STATE_TRACKING; tracingNode = node; lastTraceRouteTime = now; + resultText = ""; + clearResultLines(); bannerText = String("Tracing ") + getNodeName(node); requestFocus(); @@ -614,14 +729,14 @@ void TraceRouteModule::launch(NodeNum node) } else { LOG_ERROR("MeshService is NULL!"); runState = TRACEROUTE_STATE_RESULT; - resultText = "Service unavailable"; + setResultText("Service unavailable"); resultShowTime = millis(); tracingNode = 0; } } else { LOG_ERROR("Failed to allocate TraceRoute packet from router"); runState = TRACEROUTE_STATE_RESULT; - resultText = "Failed to send"; + setResultText("Failed to send"); resultShowTime = millis(); tracingNode = 0; } @@ -629,7 +744,7 @@ void TraceRouteModule::launch(NodeNum node) void TraceRouteModule::handleTraceRouteResult(const String &result) { - resultText = result; + setResultText(result); runState = TRACEROUTE_STATE_RESULT; resultShowTime = millis(); tracingNode = 0; @@ -679,83 +794,15 @@ void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state 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 < static_cast(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 < static_cast(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; - } - } - } + if (resultLinesDirty) { + rebuildResultLines(display); } int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing - for (size_t i = 0; i < lines.size(); i++) { + for (size_t i = 0; i < resultLines.size(); i++) { int lineY = contentStartY + (i * lineHeight); if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { - display->drawString(x + 2, lineY, lines[i]); + display->drawString(x + 2, lineY, resultLines[i]); } } } @@ -779,7 +826,7 @@ int32_t TraceRouteModule::runOnce() if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { LOG_INFO("TraceRoute timeout, no response received"); runState = TRACEROUTE_STATE_RESULT; - resultText = "No response received"; + setResultText("No response received"); resultShowTime = now; tracingNode = 0; @@ -815,6 +862,8 @@ int32_t TraceRouteModule::runOnce() // Cooldown finished LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); runState = TRACEROUTE_STATE_IDLE; + resultText = ""; + clearResultLines(); bannerText = ""; UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; @@ -828,6 +877,7 @@ int32_t TraceRouteModule::runOnce() LOG_INFO("TraceRoute result display timeout, returning to IDLE"); runState = TRACEROUTE_STATE_IDLE; resultText = ""; + clearResultLines(); bannerText = ""; tracingNode = 0; UIFrameEvent e; diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h index dac422388..a40ed7733 100644 --- a/src/modules/TraceRouteModule.h +++ b/src/modules/TraceRouteModule.h @@ -7,6 +7,7 @@ #if HAS_SCREEN #include "OLEDDisplayUi.h" #endif +#include #define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0]) @@ -49,6 +50,11 @@ class TraceRouteModule : public ProtobufModule, virtual int32_t runOnce() override; private: + void setResultText(const String &text); + void clearResultLines(); +#if HAS_SCREEN + void rebuildResultLines(OLEDDisplay *display); +#endif // 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); @@ -74,6 +80,8 @@ class TraceRouteModule : public ProtobufModule, unsigned long trackingTimeoutMs = 10000; String bannerText; String resultText; + std::vector resultLines; + bool resultLinesDirty = false; NodeNum tracingNode = 0; bool initialized = false; }; diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 33887557f..40d03de63 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -59,7 +59,13 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length) LOG_ERROR("Invalid MQTT service envelope, topic %s, len %u!", topic, length); return; } + const meshtastic_Channel &ch = channels.getByName(e.channel_id); + // Find channel by channel_id and check downlink_enabled + if (!(strcmp(e.channel_id, "PKI") == 0 || + (strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) { + return; + } // Generate node ID from nodenum for comparison std::string nodeId = nodeDB->getNodeId(); if (strcmp(e.gateway_id, nodeId.c_str()) == 0) { @@ -77,11 +83,6 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length) return; } - // Find channel by channel_id and check downlink_enabled - if (!(strcmp(e.channel_id, "PKI") == 0 || - (strcmp(e.channel_id, channels.getGlobalId(ch.index)) == 0 && ch.settings.downlink_enabled))) { - return; - } LOG_INFO("Received MQTT topic %s, len=%u", topic, length); if (e.packet->hop_limit > HOP_MAX || e.packet->hop_start > HOP_MAX) { LOG_INFO("Invalid hop_limit(%u) or hop_start(%u)", e.packet->hop_limit, e.packet->hop_start); diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 53b23124d..9b5abfba0 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -191,6 +191,8 @@ #define HW_VENDOR meshtastic_HardwareModel_CROWPANEL #elif defined(RAK3312) #define HW_VENDOR meshtastic_HardwareModel_RAK3312 +#elif defined(RAK_WISMESH_TAP_V2) +#define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAP_V2 #elif defined(LINK_32) #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #elif defined(T_DECK_PRO) diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index cee0e5a10..c74f02c44 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -60,6 +60,8 @@ // MAke sure all custom RAK4630 boards are defined before the generic RAK4630 #elif defined(RAK4630) #define HW_VENDOR meshtastic_HardwareModel_RAK4631 +#elif defined(RAK3401) +#define HW_VENDOR meshtastic_HardwareModel_RAK3401 #elif defined(TTGO_T_ECHO) #define HW_VENDOR meshtastic_HardwareModel_T_ECHO #elif defined(T_ECHO_LITE) diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index 49a8a71c7..474d45492 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${native_base.build_flags} !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : !pkg-config --cflags --libs sdl2 --silence-errors || : + !pkg-config --cflags --libs libbsd-overlay --silence-errors || : [env:native-tft] extends = native_base @@ -43,6 +44,7 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : !pkg-config --cflags --libs sdl2 --silence-errors || : + !pkg-config --cflags --libs libbsd-overlay --silence-errors || : build_src_filter = ${native_base.build_src_filter} @@ -71,6 +73,7 @@ build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -D MAP_FULL_REDRAW !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : + !pkg-config --cflags --libs libbsd-overlay --silence-errors || : build_src_filter = ${native_base.build_src_filter} @@ -103,6 +106,7 @@ build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -l -D VIEW_320x240 !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : + !pkg-config --cflags --libs libbsd-overlay --silence-errors || : build_src_filter = ${env:native-tft.build_src_filter} [env:coverage]