mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-21 02:02:23 +00:00
* If a packet is heard multiple times, rebroadcast using the highest hop limit Sometimes a packet will be in the TX queue waiting to be transmitted, when it is overheard being rebroadcast by another node, with a higher hop limit remaining. When this occurs, modify the pending packet in the TX queue to avoid unnecessarily wasting hops. * Reprocess instead of modifying queued packet In order to ensure that the traceroute module works correctly, rather than modifying the hop limnit of the existing queued version of the packet, simply drop it ifrom the queue and reprocess the version of the packet with the superior hop limit. * Update protobufs submodule * Merge upstream/develop into overheard-hoptimisation branch Resolved conflicts in: - src/mesh/FloodingRouter.cpp: Integrated hop limit optimization with refactored duplicate handling - src/mesh/MeshPacketQueue.h: Kept both hop_limit_lt parameter and new find() method * Improve method naming and code clarity - Rename findPacket() to getPacketFromQueue() for better clarity - Make code DRY by having find() use getPacketFromQueue() internally - Resolves method overloading conflict with clearer naming * If a packet is heard multiple times, rebroadcast using the highest hop limit Sometimes a packet will be in the TX queue waiting to be transmitted, when it is overheard being rebroadcast by another node, with a higher hop limit remaining. When this occurs, modify the pending packet in the TX queue to avoid unnecessarily wasting hops. * Improve router role checking using IS_ONE_OF macro - Replace multiple individual role checks with cleaner IS_ONE_OF macro - Add CLIENT_BASE support as suggested in PR #7992 - Include MeshTypes.h for IS_ONE_OF macro - Makes code more maintainable and consistent with other parts of codebase * Apply IS_ONE_OF improvement to NextHopRouter.cpp - Replace multiple individual role checks with cleaner IS_ONE_OF macro - Add CLIENT_BASE support for consistency - Include MeshTypes.h for IS_ONE_OF macro - Matches the pattern used in FloodingRouter.cpp * Create and apply IS_ROUTER_ROLE() macro across codebase - Add IS_ROUTER_ROLE() macro to meshUtils.h for consistent router role checking - Update FloodingRouter.cpp to use macro in multiple locations - Update NextHopRouter.cpp to use macro - Include CLIENT_BASE role support * Core Changes: - Add hop_limit field to PacketRecord (17B→20B due to alignment) - Extend wasSeenRecently() with wasUpgraded parameter - Enable router optimization without duplicate app delivery - Handle ROUTER_LATE delayed transmission properly Technical Details: - Memory overhead: ~4000 bytes for 1000 records - Prevents duplicate message delivery while enabling routing optimization - Maintains protocol integrity for ACK/NAK handling - Supports upgrade from hop_limit=0 to hop_limit>0 scenarios * Delete files accdentally added for merge * Trunk formatting * Packets are supposed to be unsigned. Thankfully, it's only a log message. * Upgrade all packets, not just 0 hop packets. * Not just unsigned, but hex. Updating packet log IDs. * Fixed order of operations issue that prevented packetrs from being removed from the queue * Fixing some bugs after testing. Only storing the maximum hop value in PacketRecord which makes sense. Also, updating messaging to make more sense in the logs. * Fixed flow logic about how to handle re-inserting duplicate packets. Removed IS_ROUTER_ROLE macro and replaced it with better isRebroadcaster(). * Add logic to re-run modules, but avoid re-sending to phone. * Refactor how to process the new packet with hops. Only update nodeDB and traceRouteModule. * - Apply changes to both FloodingRouter and NextHopRouter classes to make packets mutable for traceroute - MESHTASTIC_EXCLUDE_TRACEROUTE guard for when we don't want traceroute * Allow MeshPacket to be modified in-place in processUpgradePacket * let's not make a copy where a copy is unncessary. --------- Co-authored-by: Clive Blackledge <clive@ansible.org> Co-authored-by: Clive Blackledge <git@ansible.org> Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
335 lines
14 KiB
C++
335 lines
14 KiB
C++
#include "NextHopRouter.h"
|
|
#include "MeshTypes.h"
|
|
#include "meshUtils.h"
|
|
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
|
#include "modules/TraceRouteModule.h"
|
|
#endif
|
|
#include "NodeDB.h"
|
|
|
|
NextHopRouter::NextHopRouter() {}
|
|
|
|
PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions)
|
|
{
|
|
packet = p;
|
|
this->numRetransmissions = numRetransmissions - 1; // We subtract one, because we assume the user just did the first send
|
|
}
|
|
|
|
/**
|
|
* Send a packet
|
|
*/
|
|
ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p)
|
|
{
|
|
// Add any messages _we_ send to the seen message list (so we will ignore all retransmissions we see)
|
|
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); // set the next hop
|
|
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
|
|
if ((!isFromUs(p) || !p->want_ack) && p->next_hop != NO_NEXT_HOP_PREFERENCE && (p->hop_limit > 0 || p->want_ack))
|
|
startRetransmission(packetPool.allocCopy(*p)); // start retransmission for relayed packet
|
|
|
|
return Router::send(p);
|
|
}
|
|
|
|
bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
|
{
|
|
bool wasFallback = false;
|
|
bool weWereNextHop = false;
|
|
bool wasUpgraded = false;
|
|
bool seenRecently = wasSeenRecently(p, true, &wasFallback, &weWereNextHop,
|
|
&wasUpgraded); // Updates history; returns false when an upgrade is detected
|
|
|
|
// Handle hop_limit upgrade scenario for rebroadcasters
|
|
// isRebroadcaster() is duplicated in perhapsRelay(), but this avoids confusing log messages
|
|
if (wasUpgraded && isRebroadcaster() && iface && p->hop_limit > 0) {
|
|
// Upgrade detection bypasses the duplicate short-circuit so we replace the queued packet before exiting
|
|
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
|
|
if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) {
|
|
LOG_DEBUG("Processing upgraded packet 0x%08x for relay with hop limit %d (dropping queued < %d)", p->id, p->hop_limit,
|
|
dropThreshold);
|
|
|
|
if (nodeDB)
|
|
nodeDB->updateFrom(*p);
|
|
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
|
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
|
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP)
|
|
traceRouteModule->processUpgradedPacket(*p);
|
|
#endif
|
|
|
|
perhapsRelay(p);
|
|
|
|
// We already enqueued the improved copy, so make sure the incoming packet stops here.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (seenRecently) {
|
|
printPacket("Ignore dupe incoming msg", p);
|
|
|
|
if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
|
|
rxDupe++;
|
|
stopRetransmission(p->from, p->id);
|
|
}
|
|
|
|
// If it was a fallback to flooding, try to relay again
|
|
if (wasFallback) {
|
|
LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node);
|
|
// Check if it's still in the Tx queue, if not, we have to relay it again
|
|
if (!findInTxQueue(p->from, p->id))
|
|
perhapsRelay(p);
|
|
} else {
|
|
bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit;
|
|
// If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again
|
|
if (isRepeated) {
|
|
if (!findInTxQueue(p->from, p->id) && !perhapsRelay(p) && isToUs(p) && p->want_ack)
|
|
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
|
} else if (!weWereNextHop) {
|
|
perhapsCancelDupe(p); // If it's a dupe, cancel relay if we were not explicitly asked to relay
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return Router::shouldFilterReceived(p);
|
|
}
|
|
|
|
void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c)
|
|
{
|
|
NodeNum ourNodeNum = getNodeNum();
|
|
uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(ourNodeNum);
|
|
bool isAckorReply = (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) &&
|
|
(p->decoded.request_id != 0 || p->decoded.reply_id != 0);
|
|
if (isAckorReply) {
|
|
// Update next-hop for the original transmitter of this successful transmission to the relay node, but ONLY if "from" is
|
|
// not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to the destination
|
|
if (p->from != 0) {
|
|
meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from);
|
|
if (origTx) {
|
|
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came directly
|
|
// from the destination
|
|
if (wasRelayer(p->relay_node, p->decoded.request_id, p->to) ||
|
|
(p->hop_start != 0 && p->hop_start == p->hop_limit &&
|
|
wasSoleRelayer(ourRelayID, p->decoded.request_id, p->to))) {
|
|
if (origTx->next_hop != p->relay_node) { // Not already set
|
|
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply", p->from, p->relay_node);
|
|
origTx->next_hop = p->relay_node;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!isToUs(p)) {
|
|
Router::cancelSending(p->to, p->decoded.request_id); // cancel rebroadcast for this DM
|
|
// stop retransmission for the original packet
|
|
stopRetransmission(p->to, p->decoded.request_id); // for original packet, from = to and id = request_id
|
|
}
|
|
}
|
|
|
|
perhapsRelay(p);
|
|
|
|
// handle the packet as normal
|
|
Router::sniffReceived(p, c);
|
|
}
|
|
|
|
/* Check if we should be relaying this packet if so, do so. */
|
|
bool NextHopRouter::perhapsRelay(const meshtastic_MeshPacket *p)
|
|
{
|
|
if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) {
|
|
if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) {
|
|
if (isRebroadcaster()) {
|
|
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
|
|
LOG_INFO("Relaying received message coming from %x", p->relay_node);
|
|
|
|
// Use shared logic to determine if hop_limit should be decremented
|
|
if (shouldDecrementHopLimit(p)) {
|
|
tosend->hop_limit--; // bump down the hop count
|
|
} else {
|
|
LOG_INFO("Router/CLIENT_BASE-to-favorite-router/CLIENT_BASE relay: preserving hop_limit");
|
|
}
|
|
|
|
NextHopRouter::send(tosend);
|
|
|
|
return true;
|
|
} else {
|
|
LOG_DEBUG("Not rebroadcasting: Role = CLIENT_MUTE or Rebroadcast Mode = NONE");
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the next hop for a destination, given the relay node
|
|
* @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter)
|
|
*/
|
|
uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
|
|
{
|
|
// When we're a repeater router->sniffReceived will call NextHopRouter directly without checking for broadcast
|
|
if (isBroadcast(to))
|
|
return NO_NEXT_HOP_PREFERENCE;
|
|
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to);
|
|
if (node && node->next_hop) {
|
|
// We are careful not to return the relay node as the next hop
|
|
if (node->next_hop != relay_node) {
|
|
// LOG_DEBUG("Next hop for 0x%x is 0x%x", to, node->next_hop);
|
|
return node->next_hop;
|
|
} else
|
|
LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop);
|
|
}
|
|
return NO_NEXT_HOP_PREFERENCE;
|
|
}
|
|
|
|
PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key)
|
|
{
|
|
auto old = pending.find(key); // If we have an old record, someone messed up because id got reused
|
|
if (old != pending.end()) {
|
|
return &old->second;
|
|
} else
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Stop any retransmissions we are doing of the specified node/packet ID pair
|
|
*/
|
|
bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id)
|
|
{
|
|
auto key = GlobalPacketId(from, id);
|
|
return stopRetransmission(key);
|
|
}
|
|
|
|
bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p)
|
|
{
|
|
// Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once)
|
|
|
|
// Return false for roles like ROUTER, REPEATER, ROUTER_LATE which should always transmit the packet at least once.
|
|
|
|
return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe
|
|
}
|
|
|
|
bool NextHopRouter::stopRetransmission(GlobalPacketId key)
|
|
{
|
|
auto old = findPendingPacket(key);
|
|
if (old) {
|
|
auto p = old->packet;
|
|
/* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue
|
|
to avoid canceling a transmission if it was ACKed super fast via MQTT */
|
|
if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) {
|
|
// We only cancel it if we are the original sender or if we're not a router(_late)/repeater
|
|
if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) {
|
|
// remove the 'original' (identified by originator and packet->id) from the txqueue and free it
|
|
cancelSending(getFrom(p), p->id);
|
|
}
|
|
}
|
|
|
|
// Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it doesn't
|
|
// get scheduled again. (This is the core of stopRetransmission.)
|
|
auto numErased = pending.erase(key);
|
|
assert(numErased == 1);
|
|
|
|
// When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the call
|
|
// to startRetransmission.
|
|
packetPool.release(p);
|
|
|
|
return true;
|
|
} else
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add p to the list of packets to retransmit occasionally. We will free it once we stop retransmitting.
|
|
*/
|
|
PendingPacket *NextHopRouter::startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx)
|
|
{
|
|
auto id = GlobalPacketId(p);
|
|
auto rec = PendingPacket(p, numReTx);
|
|
|
|
stopRetransmission(getFrom(p), p->id);
|
|
|
|
setNextTx(&rec);
|
|
pending[id] = rec;
|
|
|
|
return &pending[id];
|
|
}
|
|
|
|
/**
|
|
* Do any retransmissions that are scheduled (FIXME - for the time being called from loop)
|
|
*/
|
|
int32_t NextHopRouter::doRetransmissions()
|
|
{
|
|
uint32_t now = millis();
|
|
int32_t d = INT32_MAX;
|
|
|
|
// FIXME, we should use a better datastructure rather than walking through this map.
|
|
// for(auto el: pending) {
|
|
for (auto it = pending.begin(), nextIt = it; it != pending.end(); it = nextIt) {
|
|
++nextIt; // we use this odd pattern because we might be deleting it...
|
|
auto &p = it->second;
|
|
|
|
bool stillValid = true; // assume we'll keep this record around
|
|
|
|
// FIXME, handle 51 day rolloever here!!!
|
|
if (p.nextTxMsec <= now) {
|
|
if (p.numRetransmissions == 0) {
|
|
if (isFromUs(p.packet)) {
|
|
LOG_DEBUG("Reliable send failed, returning a nak for fr=0x%x,to=0x%x,id=0x%x", p.packet->from, p.packet->to,
|
|
p.packet->id);
|
|
sendAckNak(meshtastic_Routing_Error_MAX_RETRANSMIT, getFrom(p.packet), p.packet->id, p.packet->channel);
|
|
}
|
|
// Note: we don't stop retransmission here, instead the Nak packet gets processed in sniffReceived
|
|
stopRetransmission(it->first);
|
|
stillValid = false; // just deleted it
|
|
} else {
|
|
LOG_DEBUG("Sending retransmission fr=0x%x,to=0x%x,id=0x%x, tries left=%d", p.packet->from, p.packet->to,
|
|
p.packet->id, p.numRetransmissions);
|
|
|
|
if (!isBroadcast(p.packet->to)) {
|
|
if (p.numRetransmissions == 1) {
|
|
// Last retransmission, reset next_hop (fallback to FloodingRouter)
|
|
p.packet->next_hop = NO_NEXT_HOP_PREFERENCE;
|
|
// Also reset it in the nodeDB
|
|
meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to);
|
|
if (sentTo) {
|
|
LOG_INFO("Resetting next hop for packet with dest 0x%x\n", p.packet->to);
|
|
sentTo->next_hop = NO_NEXT_HOP_PREFERENCE;
|
|
}
|
|
FloodingRouter::send(packetPool.allocCopy(*p.packet));
|
|
} else {
|
|
NextHopRouter::send(packetPool.allocCopy(*p.packet));
|
|
}
|
|
} else {
|
|
// Note: we call the superclass version because we don't want to have our version of send() add a new
|
|
// retransmission record
|
|
FloodingRouter::send(packetPool.allocCopy(*p.packet));
|
|
}
|
|
|
|
// Queue again
|
|
--p.numRetransmissions;
|
|
setNextTx(&p);
|
|
}
|
|
}
|
|
|
|
if (stillValid) {
|
|
// Update our desired sleep delay
|
|
int32_t t = p.nextTxMsec - now;
|
|
|
|
d = min(t, d);
|
|
}
|
|
}
|
|
|
|
return d;
|
|
}
|
|
|
|
void NextHopRouter::setNextTx(PendingPacket *pending)
|
|
{
|
|
assert(iface);
|
|
auto d = iface->getRetransmissionMsec(pending->packet);
|
|
pending->nextTxMsec = millis() + d;
|
|
LOG_DEBUG("Setting next retransmission in %u msecs: ", d);
|
|
printPacket("", pending->packet);
|
|
setReceivedMessage(); // Run ASAP, so we can figure out our correct sleep time
|
|
}
|