From 2bd3afdd96692ff139d489623d0c13aa8410b39a Mon Sep 17 00:00:00 2001 From: overkillfpv Date: Fri, 4 Sep 2026 23:58:35 +1000 Subject: [PATCH 1/2] Add next-hop reliability: listen-for-repeat retry on direct packets When a repeater forwards a direct (path-routed) packet and there is a further hop in its remaining path, it now tracks the packet's hash and waits to overhear the next hop repeating it as implicit confirmation of receipt. If no repeat is heard within a timeout, the packet is resent, up to 3 retries, before being dropped. - Mesh.h: add PendingNextHopConfirm table + getNextHopReliabilityEnabled(), getNextHopMaxRetries(), getNextHopConfirmTimeout() virtual hooks for tuning. - Mesh.cpp: register a pending confirm after forwarding a direct packet with a remaining path, check overheard direct packets against the pending table, and process retries/timeouts each loop(). --- src/Mesh.cpp | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Mesh.h | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index c11f37cacf..442c2a6122 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -9,6 +9,7 @@ void Mesh::begin() { void Mesh::loop() { Dispatcher::loop(); + processNextHopRetries(); } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -30,6 +31,12 @@ uint32_t Mesh::getCADFailRetryDelay() const { return _rng->nextInt(1, 4)*120; } +uint32_t Mesh::getNextHopConfirmTimeout(const Packet* packet) const { + // allow for the next hop's own (possibly randomised) forwarding delay, its airtime to repeat, plus margin + uint32_t airtime = _radio->getEstAirtimeFor(packet->getRawLength()); + return airtime * 3 + 2000; +} + int Mesh::searchPeersByHash(const uint8_t* hash) { return 0; // not found } @@ -39,6 +46,11 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int } DispatcherAction Mesh::onRecvPacket(Packet* pkt) { + if (pkt->isRouteDirect()) { + // any overheard direct packet may be the next hop repeating one of ours -- check before anything else + checkNextHopConfirm(pkt); + } + if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; @@ -102,6 +114,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { _tables->markSeen(pkt); removeSelfFromPath(pkt); + if (pkt->getPathHashCount() > 0) { // only worth tracking if there is a further hop to overhear + registerNextHopConfirm(pkt); + } + uint32_t d = getDirectRetransmitDelay(pkt); return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority } @@ -341,6 +357,64 @@ void Mesh::removeSelfFromPath(Packet* pkt) { } } +void Mesh::registerNextHopConfirm(const Packet* pkt) { + if (!getNextHopReliabilityEnabled() || getNextHopMaxRetries() == 0) return; + + for (int i = 0; i < MAX_PENDING_NEXTHOP_CONFIRMS; i++) { + auto& e = _pending_confirms[i]; + if (!e.active) { + e.active = true; + e.retries = 0; + e.pkt = *pkt; // keep a copy, so it can be resent unchanged if not confirmed + pkt->calculatePacketHash(e.hash); + e.deadline = futureMillis(getNextHopConfirmTimeout(pkt)); + return; + } + } + MESH_DEBUG_PRINTLN("%s Mesh::registerNextHopConfirm(): pending table full, skipping reliability tracking", getLogDateTime()); +} + +void Mesh::checkNextHopConfirm(const Packet* pkt) { + uint8_t hash[MAX_HASH_SIZE]; + bool calculated = false; + + for (int i = 0; i < MAX_PENDING_NEXTHOP_CONFIRMS; i++) { + auto& e = _pending_confirms[i]; + if (!e.active) continue; + + if (!calculated) { + pkt->calculatePacketHash(hash); + calculated = true; + } + if (memcmp(hash, e.hash, MAX_HASH_SIZE) == 0) { + e.active = false; // next hop has repeated it -- confirmed, no retry needed + } + } +} + +void Mesh::processNextHopRetries() { + for (int i = 0; i < MAX_PENDING_NEXTHOP_CONFIRMS; i++) { + auto& e = _pending_confirms[i]; + if (!e.active || !millisHasNowPassed(e.deadline)) continue; + + if (e.retries >= getNextHopMaxRetries()) { + MESH_DEBUG_PRINTLN("%s Mesh::processNextHopRetries(): giving up, no confirm heard after %d retries", getLogDateTime(), (uint32_t)e.retries); + e.active = false; + continue; + } + + Packet* retry_pkt = obtainNewPacket(); + if (retry_pkt == NULL) { + e.deadline = futureMillis(100); // packet pool busy, back off briefly and try again + continue; + } + *retry_pkt = e.pkt; + e.retries++; + e.deadline = futureMillis(getNextHopConfirmTimeout(&e.pkt)); + sendPacket(retry_pkt, 0); // resend immediately, same priority as a fresh direct forward + } +} + DispatcherAction Mesh::routeRecvPacket(Packet* packet) { uint8_t n = packet->getPathHashCount(); if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() diff --git a/src/Mesh.h b/src/Mesh.h index 49a299a6a4..15c10da5d3 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -20,6 +20,22 @@ class MeshTables { virtual void clear(const Packet* packet) = 0; // remove this packet hash from table }; +#ifndef MAX_PENDING_NEXTHOP_CONFIRMS + #define MAX_PENDING_NEXTHOP_CONFIRMS 4 // max concurrent direct packets awaiting next-hop confirmation +#endif + +/** + * \brief Tracks a direct (path-routed) packet this node has repeated, while it waits to + * overhear the next hop repeating it in turn (as implicit confirmation of receipt). +*/ +struct PendingNextHopConfirm { + bool active; + uint8_t retries; + uint32_t deadline; // millis() at which to retry (or give up if retries exhausted) + uint8_t hash[MAX_HASH_SIZE]; + Packet pkt; // copy of the packet as it was (re)transmitted, for resending +}; + /** * \brief The next layer in the basic Dispatcher task, Mesh recognises the particular Payload TYPES, * and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets. @@ -28,12 +44,30 @@ class Mesh : public Dispatcher { RTCClock* _rtc; RNG* _rng; MeshTables* _tables; + PendingNextHopConfirm _pending_confirms[MAX_PENDING_NEXTHOP_CONFIRMS]; void removeSelfFromPath(Packet* packet); void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis); //void routeRecvAcks(Packet* packet, uint32_t delay_millis); DispatcherAction forwardMultipartDirect(Packet* pkt); + /** + * \brief Start tracking 'pkt' (just repeated by this node) until the next hop is heard repeating it. + */ + void registerNextHopConfirm(const Packet* pkt); + + /** + * \brief Check an incoming direct packet against the pending-confirm table, and mark any + * match as confirmed (the next hop has repeated it, so no retry is needed). + */ + void checkNextHopConfirm(const Packet* pkt); + + /** + * \brief Called each loop(), resends any pending packets whose confirm deadline has passed, + * up to getNextHopMaxRetries() times, then drops them. + */ + void processNextHopRetries(); + protected: DispatcherAction onRecvPacket(Packet* pkt) override; @@ -71,6 +105,22 @@ class Mesh : public Dispatcher { */ virtual uint8_t getExtraAckTransmitCount() const; + /** + * \returns true if 'next-hop reliability' (listen-for-repeat retry) is enabled for repeated + * direct packets. Default is enabled wherever allowPacketForward() also permits forwarding. + */ + virtual bool getNextHopReliabilityEnabled() const { return true; } + + /** + * \returns max number of retries (resends) attempted, if no repeat from the next hop is heard. + */ + virtual uint8_t getNextHopMaxRetries() const { return 3; } + + /** + * \returns number of milliseconds to wait for the next hop to repeat 'packet', before retrying. + */ + virtual uint32_t getNextHopConfirmTimeout(const Packet* packet) const; + /** * \brief Perform search of local DB of peers/contacts. * \returns Number of peers with matching hash @@ -169,6 +219,7 @@ class Mesh : public Dispatcher { Mesh(Radio& radio, MillisecondClock& ms, RNG& rng, RTCClock& rtc, PacketManager& mgr, MeshTables& tables) : Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc), _tables(&tables) { + memset(_pending_confirms, 0, sizeof(_pending_confirms)); } MeshTables* getTables() const { return _tables; } From 27b64c528d8f3c7eef87d714f4b89368eafff687 Mon Sep 17 00:00:00 2001 From: overkillfpv Date: Sat, 5 Sep 2026 14:51:05 +1000 Subject: [PATCH 2/2] Extend next-hop reliability to originating sendDirect() calls Companions, repeaters, and room servers that originate a direct packet (via Mesh::sendDirect()) now also register it for next-hop reliability tracking when there's at least one hop in the path, so the sender itself listens for the first hop to repeat it and retries just like an in-transit repeater would. --- src/Mesh.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 442c2a6122..8772309cd5 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -783,6 +783,10 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin } else { pri = 0; } + + if (packet->getPathHashCount() > 0) { // there's a next hop to listen for repeating this + registerNextHopConfirm(packet); + } } _tables->markSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us sendPacket(packet, pri, delay_millis);