Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/mesh/NextHopRouter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions src/mesh/PacketHistory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
77 changes: 76 additions & 1 deletion test/test_packet_history/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,63 @@
#include "PacketHistory.h"

#include "TestUtil.h"
#include <memory>
#include <unity.h>
#include <vector>

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
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<meshtastic_NodeInfoLite> nodes;
};

static std::unique_ptr<MockNodeDB> 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;
Expand All @@ -45,14 +79,21 @@ 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);
}

void tearDown(void)
{
delete ph;
ph = nullptr;
nodeDB = nullptr;
mockNodeDB.reset();
}

// ===========================================================================
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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);
Expand Down