diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 5fe079a19e0..271386a3f49 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -26,8 +26,18 @@ ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us wasSeenRecently(p); // FIXME, move this to a sniffSent method - p->next_hop = getNextHop(p->to, p->relay_node).value_or(NO_NEXT_HOP_PREFERENCE); // set the next hop - LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); + // For unicast user traffic, bootstrap the route with one flood-heard hop before any directed routing kicks in. + // This gives nearby relays a chance to hear the packet even when the sender's cached next_hop is stale or wrong. + // Routing control packets keep their existing behavior because they participate in path maintenance themselves. + bool forceFloodFirstHop = + !isBroadcast(p->to) && isFromUs(p) && p->hop_limit > 0 && + !(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && p->decoded.portnum == meshtastic_PortNum_ROUTING_APP); + p->next_hop = forceFloodFirstHop ? NO_NEXT_HOP_PREFERENCE : getNextHop(p->to, p->relay_node).value_or(NO_NEXT_HOP_PREFERENCE); + if (forceFloodFirstHop) { + LOG_DEBUG("Disabling next hop for first unicast hop to 0x%x", p->to); + } else { + LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); + } // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is // not 0 or want_ack is set, start retransmissions @@ -188,7 +198,11 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) } #endif - if (p->next_hop == NO_NEXT_HOP_PREFERENCE) { + int8_t hopsAway = getHopsAway(*p); + // Keep the deliberate flood bootstrap alive only for the first hop window. Once a downstream relay sees the + // packet as >0 hops away, it can resume directed next-hop routing from its own local view of the mesh. + bool keepFloodingFirstHop = (p->next_hop == NO_NEXT_HOP_PREFERENCE && hopsAway == 0); + if (keepFloodingFirstHop) { FloodingRouter::send(tosend); } else { NextHopRouter::send(tosend); diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index e4f565d1a80..9f7ffa39747 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -191,7 +191,9 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd r.relayed_by[i + startIdx] = found->relayed_by[i]; } } - r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) + // Preserve the originally requested next hop across duplicate updates. Other code uses this to answer + // "were we the chosen directed relay?" even if a later copy arrived as flood fallback. + r.next_hop = found->next_hop; #if VERBOSE_PACKET_HISTORY LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER", r.sender, r.id, r.next_hop, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec); @@ -602,4 +604,4 @@ inline uint8_t PacketHistory::getOurTxHopLimit(const PacketRecord &r) inline void PacketHistory::setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit) { r.hop_limit = (r.hop_limit & ~HOP_LIMIT_OUR_TX_MASK) | ((hopLimit << HOP_LIMIT_OUR_TX_SHIFT) & HOP_LIMIT_OUR_TX_MASK); -} \ No newline at end of file +} diff --git a/test/test_packet_history/test_main.cpp b/test/test_packet_history/test_main.cpp index 4f4782ab6be..e9751d3407c 100644 --- a/test/test_packet_history/test_main.cpp +++ b/test/test_packet_history/test_main.cpp @@ -11,7 +11,9 @@ #include "PacketHistory.h" #include "TestUtil.h" +#include #include +#include // --------------------------------------------------------------------------- // Constants @@ -19,21 +21,53 @@ static constexpr uint32_t OUR_NODE_NUM = 0xDEAD1234; static constexpr uint8_t OUR_RELAY_ID = 0x34; // getLastByteOfNodeNum(OUR_NODE_NUM) static constexpr uint32_t SMALL_CAPACITY = 8; +static constexpr uint32_t SENDER_NODE_NUM = 0x01020311; +static constexpr uint32_t DEST_NODE_NUM = 0x01020399; +static constexpr uint32_t PACKET_ID = 0x12345678; +static constexpr uint8_t DIRECTED_RELAY_ID = 0x22; +static constexpr uint8_t DIRECTED_NEXT_HOP = 0x55; +static constexpr uint8_t FALLBACK_RELAY_ID = 0x66; // --------------------------------------------------------------------------- // Per-test state // --------------------------------------------------------------------------- +class MockNodeDB : public NodeDB +{ + public: + meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override + { + for (auto &node : nodes) { + if (node.num == n) { + return &node; + } + } + return nullptr; + } + + void addNode(NodeNum n) + { + meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero; + node.num = n; + nodes.push_back(node); + } + + private: + std::vector nodes; +}; + +static std::unique_ptr mockNodeDB; static PacketHistory *ph = nullptr; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- static meshtastic_MeshPacket makePacket(uint32_t from, uint32_t id, uint8_t hop_limit = 3, - uint8_t next_hop = NO_NEXT_HOP_PREFERENCE, uint8_t relay_node = 0) + uint8_t next_hop = NO_NEXT_HOP_PREFERENCE, uint8_t relay_node = 0, uint32_t to = 0) { meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero; p.from = from; p.id = id; + p.to = to; p.hop_limit = hop_limit; p.next_hop = next_hop; p.relay_node = relay_node; @@ -45,7 +79,12 @@ static meshtastic_MeshPacket makePacket(uint32_t from, uint32_t id, uint8_t hop_ // --------------------------------------------------------------------------- void setUp(void) { + mockNodeDB.reset(new MockNodeDB()); + nodeDB = mockNodeDB.get(); myNodeInfo.my_node_num = OUR_NODE_NUM; + mockNodeDB->addNode(OUR_NODE_NUM); + mockNodeDB->addNode(SENDER_NODE_NUM); + mockNodeDB->addNode(DEST_NODE_NUM); ph = new PacketHistory(SMALL_CAPACITY); } @@ -53,6 +92,8 @@ void tearDown(void) { delete ph; ph = nullptr; + nodeDB = nullptr; + mockNodeDB.reset(); } // =========================================================================== @@ -511,6 +552,37 @@ void test_merge_heard_back_stores_relay_node(void) // Group 8 — Fallback-to-Flooding Detection // =========================================================================== +void test_off_path_directed_duplicate_is_suppressed(void) +{ + auto directed = makePacket(SENDER_NODE_NUM, PACKET_ID, 2, DIRECTED_NEXT_HOP, DIRECTED_RELAY_ID, DEST_NODE_NUM); + + TEST_ASSERT_FALSE(ph->wasSeenRecently(&directed, true)); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&directed, true)); +} + +void test_off_path_flood_from_unknown_relay_is_not_fallback(void) +{ + // A flood copy from a relay we never saw before should not be treated as fallback. + auto directed = makePacket(SENDER_NODE_NUM, PACKET_ID, 2, DIRECTED_NEXT_HOP, DIRECTED_RELAY_ID, DEST_NODE_NUM); + auto fallback = makePacket(SENDER_NODE_NUM, PACKET_ID, 2, NO_NEXT_HOP_PREFERENCE, FALLBACK_RELAY_ID, DEST_NODE_NUM); + bool wasFallback = false; + + TEST_ASSERT_FALSE(ph->wasSeenRecently(&directed, true)); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&fallback, true, &wasFallback)); + TEST_ASSERT_FALSE(wasFallback); +} + +void test_destination_directed_duplicate_is_still_suppressed(void) +{ + auto directed = makePacket(SENDER_NODE_NUM, PACKET_ID, 2, DIRECTED_NEXT_HOP, DIRECTED_RELAY_ID, OUR_NODE_NUM); + auto fallback = makePacket(SENDER_NODE_NUM, PACKET_ID, 2, NO_NEXT_HOP_PREFERENCE, FALLBACK_RELAY_ID, OUR_NODE_NUM); + bool wasFallback = false; + + TEST_ASSERT_FALSE(ph->wasSeenRecently(&directed, true)); + TEST_ASSERT_TRUE(ph->wasSeenRecently(&fallback, true, &wasFallback)); + TEST_ASSERT_FALSE(wasFallback); +} + void test_fallback_detected(void) { // The fallback condition requires wasRelayer(relay_node) && !wasRelayer(ourRelayID). @@ -788,6 +860,9 @@ void setup() RUN_TEST(test_merge_heard_back_stores_relay_node); // Group 8 — Fallback-to-Flooding Detection + RUN_TEST(test_off_path_directed_duplicate_is_suppressed); + RUN_TEST(test_off_path_flood_from_unknown_relay_is_not_fallback); + RUN_TEST(test_destination_directed_duplicate_is_still_suppressed); RUN_TEST(test_fallback_detected); RUN_TEST(test_fallback_not_when_we_relayed); RUN_TEST(test_fallback_not_on_first_observation);