From 7e00054fd7ec5c77d8a33384c7357d06c42a128d Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 5 Sep 2025 13:38:01 -0700 Subject: [PATCH 01/16] Rename startTransmitTimerSNR to startTransmitTimerRebroadcast --- src/mesh/RadioLibInterface.cpp | 4 ++-- src/mesh/RadioLibInterface.h | 2 +- src/platform/portduino/SimRadio.cpp | 4 ++-- src/platform/portduino/SimRadio.h | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index c18612101..ced8e48a5 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -323,7 +323,7 @@ void RadioLibInterface::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerSNR(p->rx_snr); + startTransmitTimerRebroadcast(p->rx_snr); } } @@ -336,7 +336,7 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) } } -void RadioLibInterface::startTransmitTimerSNR(float snr) +void RadioLibInterface::startTransmitTimerRebroadcast(float snr) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 2ab2679c0..f0c6027a8 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -161,7 +161,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified * timer scaled to SNR of to be flooded packet * @return Timestamp after which the packet may be sent */ - void startTransmitTimerSNR(float snr); + void startTransmitTimerRebroadcast(float snr); void handleTransmitInterrupt(); void handleReceiveInterrupt(); diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 4e748c5f9..6771c30c9 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -43,7 +43,7 @@ void SimRadio::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerSNR(p->rx_snr); + startTransmitTimerRebroadcast(p->rx_snr); } } @@ -57,7 +57,7 @@ void SimRadio::startTransmitTimer(bool withDelay) } } -void SimRadio::startTransmitTimerSNR(float snr) +void SimRadio::startTransmitTimerRebroadcast(float snr) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index ea534bd65..a8d3a6394 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -64,7 +64,7 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr void startTransmitTimer(bool withDelay = true); /** timer scaled to SNR of to be flooded packet */ - void startTransmitTimerSNR(float snr); + void startTransmitTimerRebroadcast(float snr); void handleTransmitInterrupt(); void handleReceiveInterrupt(); From 3cc2b70e4f35d44daac0db3584bd5170aa175eb6 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 5 Sep 2025 13:53:59 -0700 Subject: [PATCH 02/16] Pass meshtastic_MeshPacket down into startTransmitTimerRebroadcast and getTxDelayMsecWeighted --- src/mesh/RadioInterface.cpp | 2 +- src/mesh/RadioInterface.h | 2 +- src/mesh/RadioLibInterface.cpp | 8 ++++---- src/mesh/RadioLibInterface.h | 2 +- src/platform/portduino/SimRadio.cpp | 6 +++--- src/platform/portduino/SimRadio.h | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 20a0bdbd1..c7b57a36c 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -315,7 +315,7 @@ uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) } /** The delay to use when we want to flood a message */ -uint32_t RadioInterface::getTxDelayMsecWeighted(float snr) +uint32_t RadioInterface::getTxDelayMsecWeighted(float snr, meshtastic_MeshPacket *p) { // high SNR = large CW size (Long Delay) // low SNR = small CW size (Short Delay) diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index c9e71cfa8..7e36ac442 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -181,7 +181,7 @@ class RadioInterface uint32_t getTxDelayMsecWeightedWorst(float snr); /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ - uint32_t getTxDelayMsecWeighted(float snr); + uint32_t getTxDelayMsecWeighted(float snr, meshtastic_MeshPacket *p); /** If the packet is not already in the late rebroadcast window, move it there */ virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index ced8e48a5..3fcced2f5 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -310,7 +310,7 @@ void RadioLibInterface::setTransmitDelay() // So we want to make sure the other side has had a chance to reconfigure its radio. if (p->tx_after) { - unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p->rx_snr) : getTxDelayMsec(); + unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p->rx_snr, p) : getTxDelayMsec(); unsigned long now = millis(); p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); @@ -323,7 +323,7 @@ void RadioLibInterface::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerRebroadcast(p->rx_snr); + startTransmitTimerRebroadcast(p->rx_snr, p); } } @@ -336,11 +336,11 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) } } -void RadioLibInterface::startTransmitTimerRebroadcast(float snr) +void RadioLibInterface::startTransmitTimerRebroadcast(float snr, meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { - uint32_t delay = getTxDelayMsecWeighted(snr); + uint32_t delay = getTxDelayMsecWeighted(snr, p); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } } diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index f0c6027a8..224ac6376 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -161,7 +161,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified * timer scaled to SNR of to be flooded packet * @return Timestamp after which the packet may be sent */ - void startTransmitTimerRebroadcast(float snr); + void startTransmitTimerRebroadcast(float snr, meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 6771c30c9..504383aee 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -43,7 +43,7 @@ void SimRadio::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerRebroadcast(p->rx_snr); + startTransmitTimerRebroadcast(p->rx_snr, p); } } @@ -57,11 +57,11 @@ void SimRadio::startTransmitTimer(bool withDelay) } } -void SimRadio::startTransmitTimerRebroadcast(float snr) +void SimRadio::startTransmitTimerRebroadcast(float snr, meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { - uint32_t delayMsec = getTxDelayMsecWeighted(snr); + uint32_t delayMsec = getTxDelayMsecWeighted(snr, p); // LOG_DEBUG("xmit timer %d", delay); notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); } diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index a8d3a6394..86f07c7b3 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -64,7 +64,7 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr void startTransmitTimer(bool withDelay = true); /** timer scaled to SNR of to be flooded packet */ - void startTransmitTimerRebroadcast(float snr); + void startTransmitTimerRebroadcast(float snr, meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); From 484b4cd8486b6e8c3d140e928c73a7df26c482ef Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 5 Sep 2025 14:25:09 -0700 Subject: [PATCH 03/16] Add NodeDB::isFavorite, NodeDB::isFromOrToFavoritedNode --- src/mesh/NodeDB.cpp | 14 ++++++++++++++ src/mesh/NodeDB.h | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 52a18a53f..3bdfad30f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1770,6 +1770,20 @@ void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) } } +bool NodeDB::isFavorite(uint32_t nodeId) +{ + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + if (lite) { + return lite->is_favorite; + } + return false; +} + +bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) +{ + return isFavorite(p.from) || isFavorite(p.to); +} + void NodeDB::pause_sort(bool paused) { sortingIsPaused = paused; diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 167dc1337..f73f64f92 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -185,6 +185,16 @@ class NodeDB */ void set_favorite(bool is_favorite, uint32_t nodeId); + /* + * Returns true if the node is in the NodeDB and marked as favorite + */ + bool isFavorite(uint32_t nodeId); + + /* + * Returns true if p->from or p->to is a favorited node + */ + bool isFromOrToFavoritedNode(const meshtastic_MeshPacket &p); + /** * Other functions like the node picker can request a pause in the node sorting */ From ab5332950c628f5dbbc34c8d08b15b33f70a2564 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 5 Sep 2025 14:26:06 -0700 Subject: [PATCH 04/16] Add RadioInterface::shouldRebroadcastEarlyLikeRouter and add CLIENT_BASE condition --- src/mesh/RadioInterface.cpp | 20 ++++++++++++++++++-- src/mesh/RadioInterface.h | 3 +++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index c7b57a36c..1cbee6f47 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -314,6 +314,23 @@ uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; } +/** Returns true if we should rebroadcast early like a ROUTER */ +bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) +{ + // If we are a ROUTER or REPEATER, we always rebroadcast early + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + return true; + } + + // If we are a CLIENT_BASE and the packet is from or to a favorited node, we should rebroadcast early + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + return nodeDB->isFromOrToFavoritedNode(*p); + } + + return false; +} + /** The delay to use when we want to flood a message */ uint32_t RadioInterface::getTxDelayMsecWeighted(float snr, meshtastic_MeshPacket *p) { @@ -322,8 +339,7 @@ uint32_t RadioInterface::getTxDelayMsecWeighted(float snr, meshtastic_MeshPacket uint32_t delay = 0; uint8_t CWsize = getCWsize(snr); // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || - config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + if (shouldRebroadcastEarlyLikeRouter(p)) { delay = random(0, 2 * CWsize) * slotTimeMsec; LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); } else { diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 7e36ac442..a89fa33dd 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -180,6 +180,9 @@ class RadioInterface /** The worst-case SNR_based packet delay */ uint32_t getTxDelayMsecWeightedWorst(float snr); + /** Returns true if we should rebroadcast early like a ROUTER */ + bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); + /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ uint32_t getTxDelayMsecWeighted(float snr, meshtastic_MeshPacket *p); From b305acf7e500f0855c51e039a6aba4627d327984 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 5 Sep 2025 15:10:37 -0700 Subject: [PATCH 05/16] Add FloodingRouter::roleAllowsCancelingDupe and condition for CLIENT_BASE --- src/mesh/FloodingRouter.cpp | 25 ++++++++++++++++++++++--- src/mesh/FloodingRouter.h | 3 +++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index dbd458b61..ce9b91029 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -43,11 +43,30 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) return Router::shouldFilterReceived(p); } +bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) +{ + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + // ROUTER, REPEATER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return false; + } + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // CLIENT_BASE: if the packet is from or to a favorited node, + // we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return !nodeDB->isFromOrToFavoritedNode(*p); + } + + // All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast. + return true; +} + void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { - if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && - config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && + if (roleAllowsCancelingDupe(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! // But only LoRa packets should be able to trigger this. diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index 36c6ad8aa..68ba2a6e1 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -59,6 +59,9 @@ class FloodingRouter : public Router */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + // Return false for roles like ROUTER or REPEATER which should always rebroadcast even when we've heard another rebroadcast of the same packet + bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); + /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ void perhapsCancelDupe(const meshtastic_MeshPacket *p); From b1f55ef6e83c5a5e62b66f53171675e03dce0296 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 7 Sep 2025 17:11:35 -0700 Subject: [PATCH 06/16] Fix linter --- src/mesh/FloodingRouter.cpp | 3 +-- src/mesh/FloodingRouter.h | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index ce9b91029..31c0d6bd6 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -66,8 +66,7 @@ bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { - if (roleAllowsCancelingDupe(p) && - p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + if (roleAllowsCancelingDupe(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! // But only LoRa packets should be able to trigger this. if (Router::cancelSending(p->from, p->id)) diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index 68ba2a6e1..30ad5945b 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -59,7 +59,8 @@ class FloodingRouter : public Router */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; - // Return false for roles like ROUTER or REPEATER which should always rebroadcast even when we've heard another rebroadcast of the same packet + // Return false for roles like ROUTER or REPEATER which should always rebroadcast even when we've heard another rebroadcast of + // the same packet bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ From c63102a312ce1716cf71fd03dd848cd282d012df Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 7 Sep 2025 17:13:46 -0700 Subject: [PATCH 07/16] Swap expression order to allow short-circuit evaluation --- src/mesh/FloodingRouter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 31c0d6bd6..f805055c8 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -66,7 +66,7 @@ bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { - if (roleAllowsCancelingDupe(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) { // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! // But only LoRa packets should be able to trigger this. if (Router::cancelSending(p->from, p->id)) From b768860866c378be7512ecb121e3886f0516b3e0 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 7 Sep 2025 21:08:00 -0700 Subject: [PATCH 08/16] NodeDB::isFromOrToFavoritedNode: skip search for NODENUM_BROADCAST; one-pass search and early exit --- src/mesh/NodeDB.cpp | 47 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 3bdfad30f..1859ca27b 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1772,7 +1772,14 @@ void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) bool NodeDB::isFavorite(uint32_t nodeId) { + // returns true if nodeId is_favorite; false if not or not found + + // NODENUM_BROADCAST will never be in the DB + if (nodeId == NODENUM_BROADCAST) + return false; + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + if (lite) { return lite->is_favorite; } @@ -1781,7 +1788,45 @@ bool NodeDB::isFavorite(uint32_t nodeId) bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) { - return isFavorite(p.from) || isFavorite(p.to); + // This method is logically equivalent to: + // return isFavorite(p.from) || isFavorite(p.to); + // but is more efficient by: + // 1. doing only one pass through the database, instead of two + // 2. exiting early when a favorite is found, or if both from and to have been seen + + if (p.to == NODENUM_BROADCAST) + return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from + + meshtastic_NodeInfoLite *lite = NULL; + + bool seenFrom = false; + bool seenTo = false; + + for (int i = 0; i < numMeshNodes; i++) { + lite = &meshNodes->at(i); + + if (lite->num == p.from) { + if (lite->is_favorite) + return true; + + seenFrom = true; + } + + if (lite->num == p.to) { + if (lite->is_favorite) + return true; + + seenTo = true; + } + + if (seenFrom && seenTo) + return false; // we've seen both, and neither is a favorite, so we can stop searching early + + // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching + // all favorited nodes first. + } + + return false; } void NodeDB::pause_sort(bool paused) From 5a463373f22f1790f0e6405fea8728227a539ddf Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 7 Sep 2025 21:12:07 -0700 Subject: [PATCH 09/16] Remove changes to src/mesh/generated/meshtastic/config.pb.h from this PR --- src/mesh/generated/meshtastic/config.pb.h | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 59e55db3f..67d461611 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -64,12 +64,7 @@ typedef enum _meshtastic_Config_DeviceConfig_Role { in areas not already covered by other routers, or to bridge around problematic terrain, but should not be given priority over other routers in order to avoid unnecessaraily consuming hops. */ - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, - /* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. - Technical Details: Used for stronger attic/roof nodes to distribute messages more widely - from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes - where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ - meshtastic_Config_DeviceConfig_Role_CLIENT_BASE = 12 + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11 } meshtastic_Config_DeviceConfig_Role; /* Defines the device's behavior for how messages are rebroadcast */ @@ -651,8 +646,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT -#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_CLIENT_BASE -#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_CLIENT_BASE+1)) +#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_ROUTER_LATE +#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_ROUTER_LATE+1)) #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY From 27cdd464d1f3e3b9b61c9921e7ca085c3a27d943 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 7 Sep 2025 21:46:49 -0700 Subject: [PATCH 10/16] getTxDelayMsecWeighted and startTransmitTimerRebroadcast: extract p->rxSnr --- src/mesh/RadioInterface.cpp | 3 ++- src/mesh/RadioInterface.h | 2 +- src/mesh/RadioLibInterface.cpp | 8 ++++---- src/mesh/RadioLibInterface.h | 2 +- src/platform/portduino/SimRadio.cpp | 6 +++--- src/platform/portduino/SimRadio.h | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 1cbee6f47..31c68c302 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -332,10 +332,11 @@ bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) } /** The delay to use when we want to flood a message */ -uint32_t RadioInterface::getTxDelayMsecWeighted(float snr, meshtastic_MeshPacket *p) +uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) { // high SNR = large CW size (Long Delay) // low SNR = small CW size (Short Delay) + float snr = p->rx_snr; uint32_t delay = 0; uint8_t CWsize = getCWsize(snr); // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index a89fa33dd..eff284747 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -184,7 +184,7 @@ class RadioInterface bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ - uint32_t getTxDelayMsecWeighted(float snr, meshtastic_MeshPacket *p); + uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); /** If the packet is not already in the late rebroadcast window, move it there */ virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 3fcced2f5..19d0f794a 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -310,7 +310,7 @@ void RadioLibInterface::setTransmitDelay() // So we want to make sure the other side has had a chance to reconfigure its radio. if (p->tx_after) { - unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p->rx_snr, p) : getTxDelayMsec(); + unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); unsigned long now = millis(); p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); @@ -323,7 +323,7 @@ void RadioLibInterface::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerRebroadcast(p->rx_snr, p); + startTransmitTimerRebroadcast(p); } } @@ -336,11 +336,11 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) } } -void RadioLibInterface::startTransmitTimerRebroadcast(float snr, meshtastic_MeshPacket *p) +void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { - uint32_t delay = getTxDelayMsecWeighted(snr, p); + uint32_t delay = getTxDelayMsecWeighted(p); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } } diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 224ac6376..9f497812f 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -161,7 +161,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified * timer scaled to SNR of to be flooded packet * @return Timestamp after which the packet may be sent */ - void startTransmitTimerRebroadcast(float snr, meshtastic_MeshPacket *p); + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 504383aee..cea1eab3a 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -43,7 +43,7 @@ void SimRadio::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerRebroadcast(p->rx_snr, p); + startTransmitTimerRebroadcast(p); } } @@ -57,11 +57,11 @@ void SimRadio::startTransmitTimer(bool withDelay) } } -void SimRadio::startTransmitTimerRebroadcast(float snr, meshtastic_MeshPacket *p) +void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { - uint32_t delayMsec = getTxDelayMsecWeighted(snr, p); + uint32_t delayMsec = getTxDelayMsecWeighted(p); // LOG_DEBUG("xmit timer %d", delay); notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); } diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index 86f07c7b3..d8b53739f 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -64,7 +64,7 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr void startTransmitTimer(bool withDelay = true); /** timer scaled to SNR of to be flooded packet */ - void startTransmitTimerRebroadcast(float snr, meshtastic_MeshPacket *p); + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); From 4140ecfb4983a0530bee348a2638c20ceb07677f Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Mon, 8 Sep 2025 11:06:27 -0700 Subject: [PATCH 11/16] Bring src/mesh/generated/meshtastic/config.pb.h from develop after rebase --- src/mesh/generated/meshtastic/config.pb.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 67d461611..59e55db3f 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -64,7 +64,12 @@ typedef enum _meshtastic_Config_DeviceConfig_Role { in areas not already covered by other routers, or to bridge around problematic terrain, but should not be given priority over other routers in order to avoid unnecessaraily consuming hops. */ - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11 + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, + /* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. + Technical Details: Used for stronger attic/roof nodes to distribute messages more widely + from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes + where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */ + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE = 12 } meshtastic_Config_DeviceConfig_Role; /* Defines the device's behavior for how messages are rebroadcast */ @@ -646,8 +651,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT -#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_ROUTER_LATE -#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_ROUTER_LATE+1)) +#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_CLIENT_BASE +#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_CLIENT_BASE+1)) #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY From 527e88ca46107df1fb9325a40a52cbe3234ef89f Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Mon, 8 Sep 2025 11:22:56 -0700 Subject: [PATCH 12/16] Add CLIENT_BASE to DisplayFormatters::getDeviceRole --- src/DisplayFormatters.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index b2749806c..5193e1cb4 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -52,6 +52,9 @@ const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: return "Client Hidden"; break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE: + return "Client Base"; + break; case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: return "Lost and Found"; break; From 87eff2c4a96ec0bce7ed8f3f608adf68691686aa Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Mon, 8 Sep 2025 11:34:29 -0700 Subject: [PATCH 13/16] Fix logic in Screen::shouldWakeOnReceivedMessage and add CLIENT_HIDDEN and CLIENT_BASE to be treated the same as CLIENT and CLIENT_MUTE --- src/graphics/Screen.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index eb8093947..22840ccb2 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1605,13 +1605,15 @@ bool shouldWakeOnReceivedMessage() /* The goal here is to determine when we do NOT wake up the screen on message received: - Any ext. notifications are turned on - - If role is not client / client_mute + - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - If the battery level is very low */ if (moduleConfig.external_notification.enabled) { return false; } - if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) { + if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { return false; } if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { From 4ab125bbf74408dca8d7867d102e69eae52fecfb Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Mon, 8 Sep 2025 11:57:46 -0700 Subject: [PATCH 14/16] src/graphics/Screen.cpp: move #include "meshUtils.h" outside of "#ifdef HAS_SCREEN" so IS_ONE_OF works on all devices --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 22840ccb2..14ed91a1e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -25,6 +25,7 @@ along with this program. If not, see . #include "PowerMon.h" #include "Throttle.h" #include "configuration.h" +#include "meshUtils.h" #if HAS_SCREEN #include @@ -58,7 +59,6 @@ along with this program. If not, see . #include "mesh-pb-constants.h" #include "mesh/Channels.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" -#include "meshUtils.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "modules/WaypointModule.h" From 35340fc6e23d71a048b029e0de6b3a9f4de78c9f Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Tue, 9 Sep 2025 23:17:48 -0700 Subject: [PATCH 15/16] NextHopRouter::roleAllowsCancelingFromTxQueue (same logic as FloodingRouter::roleAllowsCancelingDupe) --- src/mesh/NextHopRouter.cpp | 13 ++++++++++--- src/mesh/NextHopRouter.h | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 7ceca2195..608e069e6 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -161,6 +161,15 @@ bool NextHopRouter::stopRetransmission(NodeNum from, PacketId 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); @@ -170,9 +179,7 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) 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) || (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && - config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + 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); // now free the pooled copy for retransmission too diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h index 6c2764aff..0022644e9 100644 --- a/src/mesh/NextHopRouter.h +++ b/src/mesh/NextHopRouter.h @@ -121,6 +121,9 @@ class NextHopRouter : public FloodingRouter */ PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) + bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p); + /** * Stop any retransmissions we are doing of the specified node/packet ID pair * From 0fc33c352a4b5541e7054064fee9dcddfcd24fb4 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 12 Sep 2025 10:40:13 -0700 Subject: [PATCH 16/16] Fix memory leak in NextHopRouter: always free packet copy when removing from pending --- src/mesh/NextHopRouter.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 608e069e6..9bb8b240c 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -182,12 +182,18 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) 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); - // now free the pooled copy for retransmission too - packetPool.release(p); } } + + // 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;