diff --git a/src/Mesh.cpp b/src/Mesh.cpp index c11f37cacf..8772309cd5 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() @@ -709,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); 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; }