From ac4bcd2f5639130f1b0aaac697c754a1d9a6de45 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 11 Sep 2025 18:57:30 -0500 Subject: [PATCH 01/45] Cleanup --- src/mesh/MeshModule.cpp | 1 - src/mesh/Router.cpp | 4 +--- src/modules/NodeInfoModule.cpp | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 22fcec663..c5748a560 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -100,7 +100,6 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets auto ourNodeNum = nodeDB->getNodeNum(); bool toUs = isBroadcast(mp.to) || isToUs(&mp); - bool fromUs = mp.from == ourNodeNum; for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 4442b5d50..44d09637f 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -662,7 +662,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) // call modules here // If this could be a spoofed packet, don't let the modules see it. - if (!skipHandle && p->from != nodeDB->getNodeNum()) { + if (!skipHandle) { MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT @@ -676,8 +676,6 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) !isFromUs(p) && mqtt) mqtt->onSend(*p_encrypted, *p, p->channel); #endif - } else if (p->from == nodeDB->getNodeNum() && !skipHandle) { - MeshModule::callModules(*p, src); } packetPool.release(p_encrypted); // Release the encrypted packet diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 86ceaae24..276a11b3a 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -12,12 +12,12 @@ NodeInfoModule *nodeInfoModule; bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) { - auto p = *pptr; - if (mp.from == nodeDB->getNodeNum()) { LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); return false; } + + auto p = *pptr; if (p.is_licensed != owner.is_licensed) { LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); return true; From 7e00054fd7ec5c77d8a33384c7357d06c42a128d Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 5 Sep 2025 13:38:01 -0700 Subject: [PATCH 02/45] 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 03/45] 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 04/45] 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 05/45] 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 06/45] 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 07/45] 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 08/45] 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 09/45] 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 10/45] 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 11/45] 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 12/45] 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 13/45] 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 14/45] 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 15/45] 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 16/45] 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 17/45] 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; From 962e5d513cb4dda47518ae76034e703245693367 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 12 Sep 2025 10:40:13 -0700 Subject: [PATCH 18/45] 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 7ceca2195..db3d62038 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -175,12 +175,18 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { // 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; From 43cf12edfbe39304f2fb8c7150d464d53a49cabf Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 12 Sep 2025 13:00:17 -0700 Subject: [PATCH 19/45] Fix memory leak in NimbleBluetooth: allocate BluetoothStatus on stack, not heap --- src/nimble/NimbleBluetooth.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 95e191c8e..ee95168c3 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -133,7 +133,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey))); + meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); + bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { @@ -173,7 +174,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE authentication complete"); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { @@ -187,8 +189,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE disconnect"); - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); if (bluetoothPhoneAPI) { std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); From ead67446a3ff154e4d85058b0d4b680729197ca7 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Fri, 12 Sep 2025 13:15:52 -0700 Subject: [PATCH 20/45] Fix memory leak in NRF52Bluetooth: allocate BluetoothStatus on stack, not heap --- src/platform/nrf52/NRF52Bluetooth.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 6f0e7250f..f8366ae32 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -59,7 +59,8 @@ void onConnect(uint16_t conn_handle) LOG_INFO("BLE Connected to %s", central_name); // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); } /** * Callback invoked when a connection is dropped @@ -74,7 +75,8 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason) } // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); } void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { @@ -326,7 +328,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke textkey += (char)passkey[i]; // Notify UI (or other components) of pairing event and passkey - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey)); + meshtastic::BluetoothStatus newStatus(textkey); + bluetoothStatus->updateStatus(&newStatus); #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { @@ -398,12 +401,13 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu { if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newConnectedStatus); } else { LOG_INFO("BLE pair failed"); // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newDisconnectedStatus); } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus From 1914fa0321ebbe801dbae04548f4d1bbcb256d5f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 15:49:56 -0500 Subject: [PATCH 21/45] Formatting --- src/mesh/MemoryPool.h | 76 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index ea7c8f583..0c5ba6c71 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -6,6 +6,7 @@ #include #include "PointerQueue.h" +#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP template class Allocator { @@ -14,13 +15,14 @@ template class Allocator Allocator() : deleter([this](T *p) { this->release(p); }) {} virtual ~Allocator() {} - /// Return a queable object which has been prefilled with zeros. Panic if no buffer is available + /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available /// Note: this method is safe to call from regular OR ISR code T *allocZeroed() { T *p = allocZeroed(0); - - assert(p); // FIXME panic instead + if (!p) { + LOG_WARN("Failed to allocate zeroed memory"); + } return p; } @@ -39,10 +41,12 @@ template class Allocator T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) { T *p = alloc(maxWait); - assert(p); + if (!p) { + LOG_WARN("Failed to allocate memory for copy"); + return nullptr; + } - if (p) - *p = src; + *p = src; return p; } @@ -83,7 +87,9 @@ template class MemoryDynamic : public Allocator /// Return a buffer for use by others virtual void release(T *p) override { - assert(p); + if (p == nullptr) + return; + LOG_HEAP("Freeing 0x%x", p); free(p); @@ -98,3 +104,59 @@ template class MemoryDynamic : public Allocator return p; } }; + +/** + * A static memory pool that uses a fixed buffer instead of heap allocation + */ +template class MemoryPool : public Allocator +{ + private: + T pool[MaxSize]; + bool used[MaxSize]; + + public: + MemoryPool() + { + // Initialize the used array to false (all slots free) + for (int i = 0; i < MaxSize; i++) { + used[i] = false; + } + } + + /// Return a buffer for use by others + virtual void release(T *p) override + { + if (!p) { + LOG_DEBUG("Failed to release memory, pointer is null"); + return; + } + + // Find the index of this pointer in our pool + int index = p - pool; + if (index >= 0 && index < MaxSize) { + assert(used[index]); // Should be marked as used + used[index] = false; + LOG_HEAP("Released static pool item %d at 0x%x", index, p); + } else { + LOG_WARN("Pointer 0x%x not from our pool!", p); + } + } + + protected: + // Alloc some storage from our static pool + virtual T *alloc(TickType_t maxWait) override + { + // Find first free slot + for (int i = 0; i < MaxSize; i++) { + if (!used[i]) { + used[i] = true; + LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); + return &pool[i]; + } + } + + // No free slots available - return nullptr instead of asserting + LOG_WARN("No free slots available in static memory pool!"); + return nullptr; + } +}; From 8989de118cd61f1aac75b27a28b4db8c90ea895c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 16:07:27 -0500 Subject: [PATCH 22/45] Only queue 2 client notification --- src/mesh/mesh-pb-constants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 224f45de2..12aec98cd 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -30,7 +30,7 @@ /// max number of ClientNotification packets which can be waiting for delivery to phone #ifndef MAX_RX_NOTIFICATION_TOPHONE -#define MAX_RX_NOTIFICATION_TOPHONE 4 +#define MAX_RX_NOTIFICATION_TOPHONE 2 #endif /// Verify baseline assumption of node size. If it increases, we need to reevaluate From e49b07ac8ca1e71936f44c0a16f46ca90568b8d8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 17:12:18 -0500 Subject: [PATCH 23/45] Merge pull request #7965 from compumike/compumike/fix-nrf52-bluetooth-memory-leak Fix memory leak in `NRF52Bluetooth`: allocate `BluetoothStatus` on stack, not heap --- src/platform/nrf52/NRF52Bluetooth.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 6f0e7250f..f8366ae32 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -59,7 +59,8 @@ void onConnect(uint16_t conn_handle) LOG_INFO("BLE Connected to %s", central_name); // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); } /** * Callback invoked when a connection is dropped @@ -74,7 +75,8 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason) } // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); } void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { @@ -326,7 +328,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke textkey += (char)passkey[i]; // Notify UI (or other components) of pairing event and passkey - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey)); + meshtastic::BluetoothStatus newStatus(textkey); + bluetoothStatus->updateStatus(&newStatus); #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { @@ -398,12 +401,13 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu { if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newConnectedStatus); } else { LOG_INFO("BLE pair failed"); // Notify UI (or any other interested firmware components) - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newDisconnectedStatus); } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus From d00b2afe1d5a7ffb9fc571fc60efefcd872bc349 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 17:12:27 -0500 Subject: [PATCH 24/45] Merge pull request #7964 from compumike/compumike/fix-nimble-bluetooth-memory-leak Fix memory leak in `NimbleBluetooth`: allocate `BluetoothStatus` on stack, not heap --- src/nimble/NimbleBluetooth.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 95e191c8e..ee95168c3 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -133,7 +133,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks LOG_INFO("*** Enter passkey %d on the peer side ***", passkey); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey))); + meshtastic::BluetoothStatus newStatus(std::to_string(passkey)); + bluetoothStatus->updateStatus(&newStatus); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (screen) { @@ -173,7 +174,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE authentication complete"); - bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); + bluetoothStatus->updateStatus(&newStatus); // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { @@ -187,8 +189,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE disconnect"); - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); + bluetoothStatus->updateStatus(&newStatus); if (bluetoothPhoneAPI) { std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); From b6dd99917d365872a94138c74d04658180265afd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 06:37:58 -0500 Subject: [PATCH 25/45] Update protobufs (#7973) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/device_ui.pb.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index a84657c22..8caf42396 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit a84657c220421536f18d11fc5edf680efadbceeb +Subproject commit 8caf42396438f0d8a0305143485fd671c1fc7126 diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h index 8313438f8..8f693e570 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.h +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -66,6 +66,8 @@ typedef enum _meshtastic_Language { meshtastic_Language_UKRAINIAN = 16, /* Bulgarian */ meshtastic_Language_BULGARIAN = 17, + /* Czech */ + meshtastic_Language_CZECH = 18, /* Simplified Chinese (experimental) */ meshtastic_Language_SIMPLIFIED_CHINESE = 30, /* Traditional Chinese (experimental) */ From 566c2c3fdf8389ced45846a59c7b27dd611e30a4 Mon Sep 17 00:00:00 2001 From: WillyJL Date: Sat, 13 Sep 2025 13:50:02 +0200 Subject: [PATCH 26/45] T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --- variants/esp32s3/tlora-pager/rfswitch.h | 18 ++++++++++++++++++ variants/esp32s3/tlora-pager/variant.h | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 variants/esp32s3/tlora-pager/rfswitch.h diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h new file mode 100644 index 000000000..1e5eb7a9e --- /dev/null +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -0,0 +1,18 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = { + RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, { LOW, LOW } }, + { LR11x0::MODE_RX, { LOW, HIGH } }, + { LR11x0::MODE_TX, { HIGH, LOW } }, + { LR11x0::MODE_TX_HP, { HIGH, LOW } }, + { LR11x0::MODE_TX_HF, { LOW, LOW } }, + { LR11x0::MODE_GNSS, { LOW, LOW } }, + { LR11x0::MODE_WIFI, { LOW, LOW } }, + END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index ee48088c8..2875f6804 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -105,14 +105,16 @@ // LoRa #define USE_SX1262 #define USE_SX1268 +#define USE_SX1280 +#define USE_LR1121 #define LORA_SCK 35 #define LORA_MISO 33 #define LORA_MOSI 34 #define LORA_CS 36 +#define LORA_RESET 47 #define LORA_DIO0 -1 // a No connect on the SX1262 module -#define LORA_RESET 47 #define LORA_DIO1 14 // SX1262 IRQ #define LORA_DIO2 48 // SX1262 BUSY #define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled @@ -123,3 +125,18 @@ #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 3.0 + +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY LORA_DIO2 +#define SX128X_RESET LORA_RESET + +#define LR1121_IRQ_PIN LORA_DIO1 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO2 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 3.0 +#define LR11X0_DIO_AS_RF_SWITCH From 6d2093650ad16b4c3a8fe8c71d9cc96350710f3f Mon Sep 17 00:00:00 2001 From: WillyJL Date: Sat, 13 Sep 2025 13:50:02 +0200 Subject: [PATCH 27/45] T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --- variants/esp32s3/tlora-pager/rfswitch.h | 18 ++++++++++++++++++ variants/esp32s3/tlora-pager/variant.h | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 variants/esp32s3/tlora-pager/rfswitch.h diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h new file mode 100644 index 000000000..1e5eb7a9e --- /dev/null +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -0,0 +1,18 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = { + RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, { LOW, LOW } }, + { LR11x0::MODE_RX, { LOW, HIGH } }, + { LR11x0::MODE_TX, { HIGH, LOW } }, + { LR11x0::MODE_TX_HP, { HIGH, LOW } }, + { LR11x0::MODE_TX_HF, { LOW, LOW } }, + { LR11x0::MODE_GNSS, { LOW, LOW } }, + { LR11x0::MODE_WIFI, { LOW, LOW } }, + END_OF_MODE_TABLE, +}; \ No newline at end of file diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index ee48088c8..2875f6804 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -105,14 +105,16 @@ // LoRa #define USE_SX1262 #define USE_SX1268 +#define USE_SX1280 +#define USE_LR1121 #define LORA_SCK 35 #define LORA_MISO 33 #define LORA_MOSI 34 #define LORA_CS 36 +#define LORA_RESET 47 #define LORA_DIO0 -1 // a No connect on the SX1262 module -#define LORA_RESET 47 #define LORA_DIO1 14 // SX1262 IRQ #define LORA_DIO2 48 // SX1262 BUSY #define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled @@ -123,3 +125,18 @@ #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 3.0 + +#define SX128X_CS LORA_CS +#define SX128X_DIO1 LORA_DIO1 +#define SX128X_BUSY LORA_DIO2 +#define SX128X_RESET LORA_RESET + +#define LR1121_IRQ_PIN LORA_DIO1 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO2 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 3.0 +#define LR11X0_DIO_AS_RF_SWITCH From 51acd92a37e1dd760fdcc0af1007f584a0de590c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 06:51:18 -0500 Subject: [PATCH 28/45] Trunk --- variants/esp32s3/tlora-pager/rfswitch.h | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 1e5eb7a9e..0fba5a305 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -1,18 +1,11 @@ #include "RadioLib.h" -static const uint32_t rfswitch_dio_pins[] = { - RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, - RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC -}; +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - { LR11x0::MODE_STBY, { LOW, LOW } }, - { LR11x0::MODE_RX, { LOW, HIGH } }, - { LR11x0::MODE_TX, { HIGH, LOW } }, - { LR11x0::MODE_TX_HP, { HIGH, LOW } }, - { LR11x0::MODE_TX_HF, { LOW, LOW } }, - { LR11x0::MODE_GNSS, { LOW, LOW } }, - { LR11x0::MODE_WIFI, { LOW, LOW } }, - END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file From 70ac3601b04ef3f9243777fde545befbe254c08e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 06:57:12 -0500 Subject: [PATCH 29/45] Trunk From 9211b1bb4b0cf9fcfa6a889cbab49938743f7414 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 07:01:07 -0500 Subject: [PATCH 30/45] Static memory pool allocation (#7966) * Static memory pool * Initializer * T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --------- Co-authored-by: WillyJL --- src/mesh/MemoryPool.h | 9 ++++----- src/mesh/MeshService.cpp | 9 ++++++--- src/mesh/Router.cpp | 3 +-- variants/esp32s3/tlora-pager/rfswitch.h | 12 ++++++++---- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index 0c5ba6c71..eb5ac5109 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -115,12 +115,11 @@ template class MemoryPool : public Allocator bool used[MaxSize]; public: - MemoryPool() + MemoryPool() : pool{}, used{} { - // Initialize the used array to false (all slots free) - for (int i = 0; i < MaxSize; i++) { - used[i] = false; - } + // Arrays are now zero-initialized by member initializer list + // pool array: all elements are default-constructed (zero for POD types) + // used array: all elements are false (zero-initialized) } /// Return a buffer for use by others diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 607766ab6..96782cda5 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -46,11 +46,14 @@ the new node can build its node db) MeshService *service; -static MemoryDynamic staticMqttClientProxyMessagePool; +#define MAX_MQTT_PROXY_MESSAGES 16 +static MemoryPool staticMqttClientProxyMessagePool; -static MemoryDynamic staticQueueStatusPool; +#define MAX_QUEUE_STATUS 4 +static MemoryPool staticQueueStatusPool; -static MemoryDynamic staticClientNotificationPool; +#define MAX_CLIENT_NOTIFICATIONS 4 +static MemoryPool staticClientNotificationPool; Allocator &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 44d09637f..b5ae1ec0c 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -31,8 +31,7 @@ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -// static MemoryPool staticPool(MAX_PACKETS); -static MemoryDynamic staticPool; +static MemoryPool staticPool; Allocator &packetPool = staticPool; diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 0fba5a305..337346ec5 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -4,8 +4,12 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, - {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, + {LR11x0::MODE_RX, {LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, LOW}}, + {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, + {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, }; \ No newline at end of file From 90ddbf6f2cb381616e1f5d144b3795124976470f Mon Sep 17 00:00:00 2001 From: "Trent V." Date: Sat, 13 Sep 2025 11:56:23 -0500 Subject: [PATCH 31/45] updated shebang to use a more standard path for bash (#7922) Signed-off-by: Trenton VanderWert --- bin/device-install.sh | 2 +- bin/device-update.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/device-install.sh b/bin/device-install.sh index 594f9dd6b..ede75bbba 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PYTHON=${PYTHON:-$(which python3 python | head -n 1)} BPS_RESET=false diff --git a/bin/device-update.sh b/bin/device-update.sh index 6f29496e9..f64280a5b 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PYTHON=${PYTHON:-$(which python3 python|head -n 1)} CHANGE_MODE=false From 78dfb05eeb475af7dfff1903f7d2923e5bba74ca Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 11:59:50 -0500 Subject: [PATCH 32/45] Portduino dynamic alloc --- src/mesh/Router.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index b5ae1ec0c..c5eed5180 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -5,6 +5,7 @@ #include "MeshService.h" #include "NodeDB.h" #include "RTC.h" + #include "configuration.h" #include "detect/LoRaRadioType.h" #include "main.h" @@ -27,13 +28,24 @@ // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX // And every TX packet might have a retransmission packet or an ack alive at any moment + +#ifdef ARCH_PORTDUINO +// Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes #define MAX_PACKETS \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -static MemoryPool staticPool; +static MemoryDynamic dynamicPool(MAX_PACKETS); +Allocator &packetPool = dynamicPool; +#else +// Embedded targets use static memory pools with compile-time constants +#define MAX_PACKETS_STATIC \ + (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ + 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +static MemoryPool staticPool; Allocator &packetPool = staticPool; +#endif static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); From 4ee07226e4574b35fb864f337ff3753fee40cae1 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 11:59:58 -0500 Subject: [PATCH 33/45] Missed From ae814b54630c470d023b00b5b53146692208f394 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 12:07:14 -0500 Subject: [PATCH 34/45] Drop the limit --- src/mesh/Router.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index c5eed5180..6c5d08a93 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -35,7 +35,7 @@ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) -static MemoryDynamic dynamicPool(MAX_PACKETS); +static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; #else // Embedded targets use static memory pools with compile-time constants From de3a65579dd0d31d36a6e764563c4fb4dde413a4 Mon Sep 17 00:00:00 2001 From: Jason P Date: Sat, 13 Sep 2025 15:06:36 -0500 Subject: [PATCH 35/45] Add formatting and menu picking for other GPS format options (#7974) * Add back options for other GPS format options * Rename variables and don't overlap elements * Fix default value * Should probably add a menu while I'm here! * Shorten names just a bit to fit on screens * Fix off by one * Labels try to make things better * Missed a label --- src/graphics/draw/MenuHandler.cpp | 55 ++++++++++++++-- src/graphics/draw/MenuHandler.h | 2 + src/graphics/draw/UIRenderer.cpp | 100 +++++++++++++++++------------- src/graphics/draw/UIRenderer.h | 3 +- 4 files changed, 111 insertions(+), 49 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index dab3040f0..73381a92b 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -588,11 +588,11 @@ void menuHandler::favoriteBaseMenu() void menuHandler::positionBaseMenu() { - enum optionsNumbers { Back, GPSToggle, CompassMenu, CompassCalibrate, enumEnd }; + enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd }; - static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "Compass"}; - static int optionsEnumArray[enumEnd] = {Back, GPSToggle, CompassMenu}; - int options = 3; + static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"}; + static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu}; + int options = 4; if (accelerometerThread) { optionsArray[options] = "Compass Calibrate"; @@ -608,6 +608,9 @@ void menuHandler::positionBaseMenu() if (selected == GPSToggle) { menuQueue = gps_toggle_menu; screen->runNow(); + } else if (selected == GPSFormat) { + menuQueue = gps_format_menu; + screen->runNow(); } else if (selected == CompassMenu) { menuQueue = compass_point_north_menu; screen->runNow(); @@ -726,6 +729,47 @@ void menuHandler::GPSToggleMenu() bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2; screen->showOverlayBanner(bannerOptions); } +void menuHandler::GPSFormatMenu() +{ + + static const char *optionsArray[] = {"Back", + isHighResolution ? "Decimal Degrees" : "DEC", + isHighResolution ? "Degrees Minutes Seconds" : "DMS", + isHighResolution ? "Universal Transverse Mercator" : "UTM", + isHighResolution ? "Military Grid Reference System" : "MGRS", + isHighResolution ? "Open Location Code" : "OLC", + isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "GPS Format"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 7; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 2) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 3) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 4) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 5) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 6) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR; + service->reloadConfig(SEGMENT_CONFIG); + } else { + menuQueue = position_base_menu; + screen->runNow(); + } + }; + bannerOptions.InitialSelected = config.display.gps_format + 1; + screen->showOverlayBanner(bannerOptions); +} #endif void menuHandler::BluetoothToggleMenu() @@ -1378,6 +1422,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case gps_toggle_menu: GPSToggleMenu(); break; + case gps_format_menu: + GPSFormatMenu(); + break; #endif case compass_point_north_menu: compassNorthMenu(); diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 2be8e58a6..e8a2904a0 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -19,6 +19,7 @@ class menuHandler clock_menu, position_base_menu, gps_toggle_menu, + gps_format_menu, compass_point_north_menu, reset_node_db_menu, buzzermodemenupicker, @@ -63,6 +64,7 @@ class menuHandler static void positionBaseMenu(); static void compassNorthMenu(); static void GPSToggleMenu(); + static void GPSFormatMenu(); static void BuzzerModeMenu(); static void switchToMUIMenu(); static void TFTColorPickerMenu(OLEDDisplay *display); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index e9da66712..2a7f3aeee 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -116,64 +116,78 @@ void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, con } // Draw GPS status coordinates -void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, + const char *mode) { auto gpsFormat = config.display.gps_format; char displayLine[32]; if (!gps->getIsConnected() && !config.position.fixed_position) { strcpy(displayLine, "No GPS present"); - display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + display->drawString(x, y, displayLine); } else if (!gps->getHasLock() && !config.position.fixed_position) { strcpy(displayLine, "No GPS Lock"); - display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + display->drawString(x, y, displayLine); } else { geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { - char coordinateLine[22]; + char coordinateLine_1[22]; + char coordinateLine_2[22]; if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees - snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, - geoCoord.getLongitude() * 1e-7); + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), - geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), + geoCoord.getUTMBand(), geoCoord.getUTMEasting()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), - geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), - geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), + geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), + geoCoord.getMGRSNorthing()); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code - geoCoord.getOLCCode(coordinateLine); + geoCoord.getOLCCode(coordinateLine_1); + coordinateLine_2[0] = '\0'; } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference - if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region - snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); - else - snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), - geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); + if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); + coordinateLine_2[0] = '\0'; + } else { + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(), + geoCoord.getOSGRN100k()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(), + geoCoord.getOSGRNorthing()); + } } - // If fixed position, display text "Fixed GPS" alternating with the coordinates. - if (config.position.fixed_position) { - if ((millis() / 10000) % 2) { - display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, - coordinateLine); - } else { - display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); + if (strcmp(mode, "line1") == 0) { + display->drawString(x, y, coordinateLine_1); + } else if (strcmp(mode, "line2") == 0) { + display->drawString(x, y, coordinateLine_2); + } else if (strcmp(mode, "combined") == 0) { + display->drawString(x, y, coordinateLine_1); + if (coordinateLine_2[0] != '\0') { + display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2); } - } else { - display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); } + } else { - char latLine[22]; - char lonLine[22]; - snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), - geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); - snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), - geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); - display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, - latLine); - display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine); + char coordinateLine_1[22]; + char coordinateLine_2[22]; + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), + geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), + geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); + if (strcmp(mode, "line1") == 0) { + display->drawString(x, y, coordinateLine_1); + } else if (strcmp(mode, "line2") == 0) { + display->drawString(x, y, coordinateLine_2); + } else { // both + display->drawString(x, y, coordinateLine_1); + display->drawString(x, y + 10, coordinateLine_2); + } } } } @@ -978,17 +992,15 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(0, getTextPositions(display)[line++], "Last: ?"); } - // === Third Row: Latitude === - char latStr[32]; - snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x, getTextPositions(display)[line++], latStr); + // === Third Row: Line 1 GPS Info === + UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); - // === Fourth Row: Longitude === - char lonStr[32]; - snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); - display->drawString(x, getTextPositions(display)[line++], lonStr); + if (config.display.gps_format != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { + // === Fourth Row: Line 2 GPS Info === + UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); + } - // === Fifth Row: Altitude === + // === Fourth/Fifth Row: Altitude === char DisplayLineTwo[32] = {0}; int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode)) ? ourNode->position.altitude diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index eada150f9..438d56cc2 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -38,7 +38,8 @@ class UIRenderer // GPS status functions static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus, + const char *mode = "line1"); static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); From 6165b4f7a9b7ad43936c338cd3d43002d0cbaae0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 16:31:56 -0500 Subject: [PATCH 36/45] Update meshtastic-esp8266-oled-ssd1306 digest to 0cbc26b (#7977) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index e2e5e1a18..47b5f823d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -60,7 +60,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master From 760471d62021b0a02f1481e9b1cdce267c430f19 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 18:52:46 -0500 Subject: [PATCH 37/45] Fix json report crashes on esp32 (#7978) --- src/mesh/http/ContentHandler.cpp | 39 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 74953d8fc..fb66dae7c 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -292,11 +292,14 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels) JSONObject thisFileMap; thisFileMap["size"] = new JSONValue((int)file.size()); #ifdef ARCH_ESP32 - thisFileMap["name"] = new JSONValue(String(file.path()).substring(1).c_str()); + String fileName = String(file.path()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); #else - thisFileMap["name"] = new JSONValue(String(file.name()).substring(1).c_str()); + String fileName = String(file.name()).substring(1); + thisFileMap["name"] = new JSONValue(fileName.c_str()); #endif - if (String(file.name()).substring(1).endsWith(".gz")) { + String tempName = String(file.name()).substring(1); + if (tempName.endsWith(".gz")) { #ifdef ARCH_ESP32 String modifiedFile = String(file.path()).substring(1); #else @@ -339,7 +342,8 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; @@ -367,7 +371,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; return; } else { @@ -376,7 +381,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("Error"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; return; } @@ -622,10 +628,7 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) tempArray.push_back(new JSONValue((int)logArray[i])); } JSONValue *result = new JSONValue(tempArray); - // Clean up original array to prevent memory leak - for (auto *val : tempArray) { - delete val; - } + // Note: Don't delete tempArray elements here - JSONValue now owns them return result; }; @@ -656,7 +659,9 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) // data->wifi JSONObject jsonObjWifi; jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI()); - jsonObjWifi["ip"] = new JSONValue(WiFi.localIP().toString().c_str()); + String wifiIPString = WiFi.localIP().toString(); + std::string wifiIP = wifiIPString.c_str(); + jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str()); // data->memory JSONObject jsonObjMemory; @@ -702,7 +707,8 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) jsonObjOuter["status"] = new JSONValue("ok"); // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; } @@ -773,7 +779,8 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res) jsonObjOuter["status"] = new JSONValue("ok"); // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; // Clean up the nodesArray to prevent memory leak @@ -926,7 +933,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) JSONObject jsonObjOuter; jsonObjOuter["status"] = new JSONValue("ok"); JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; } @@ -968,7 +976,8 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) // serialize and write it to the stream JSONValue *value = new JSONValue(jsonObjOuter); - res->print(value->Stringify().c_str()); + std::string jsonString = value->Stringify(); + res->print(jsonString.c_str()); delete value; // Clean up the networkObjs to prevent memory leak From 096afa07f8bda37629a9ba1eafc51cde5890c2ea Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 18:57:00 -0500 Subject: [PATCH 38/45] Tweak maximums --- src/mesh/mesh-pb-constants.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 12aec98cd..868670f42 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -20,12 +20,12 @@ /// max number of QueueStatus packets which can be waiting for delivery to phone #ifndef MAX_RX_QUEUESTATUS_TOPHONE -#define MAX_RX_QUEUESTATUS_TOPHONE 4 +#define MAX_RX_QUEUESTATUS_TOPHONE 2 #endif /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone #ifndef MAX_RX_MQTTPROXY_TOPHONE -#define MAX_RX_MQTTPROXY_TOPHONE 32 +#define MAX_RX_MQTTPROXY_TOPHONE 16 #endif /// max number of ClientNotification packets which can be waiting for delivery to phone From 99770354995c9b463fbf570993b7e67289f65dc9 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 13 Sep 2025 20:14:10 -0500 Subject: [PATCH 39/45] Fix DRAM overflow on old esp32 targets --- src/mesh/mesh-pb-constants.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index 868670f42..e4f65aa28 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -15,8 +15,12 @@ // FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in // RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0])) #ifndef MAX_RX_TOPHONE +#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) +#define MAX_RX_TOPHONE 8 +#else #define MAX_RX_TOPHONE 32 #endif +#endif /// max number of QueueStatus packets which can be waiting for delivery to phone #ifndef MAX_RX_QUEUESTATUS_TOPHONE @@ -25,7 +29,7 @@ /// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone #ifndef MAX_RX_MQTTPROXY_TOPHONE -#define MAX_RX_MQTTPROXY_TOPHONE 16 +#define MAX_RX_MQTTPROXY_TOPHONE 8 #endif /// max number of ClientNotification packets which can be waiting for delivery to phone From d201f6a1ed07bf3b159cfdfdc29a230c7f0c10dc Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 8 Sep 2025 10:29:26 +1000 Subject: [PATCH 40/45] Guard bad time warning logs using GPS_DEBUG (#7897) In 2.7.7 / 2.7.8 we introduced some new checks for time accuracy. In combination, these result in a spamming of the logs when a bad time is found When the GPS is active, we're calling the GPS thread every 0.2secs. So this log could be printed 4,500 times in a no-lock scenario :) Reserve this experience for developers using GPS_DEBUG. Fixes https://github.com/meshtastic/firmware/issues/7896 --- src/gps/RTC.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index e208e2df9..39b633e47 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -130,11 +130,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); +#endif return RTCSetResultInvalidTime; } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, BUILD_EPOCH + FORTY_YEARS); +#endif return RTCSetResultInvalidTime; } #endif @@ -252,11 +256,15 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); +#endif return RTCSetResultInvalidTime; } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { +#ifdef GPS_DEBUG LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, BUILD_EPOCH + FORTY_YEARS); +#endif return RTCSetResultInvalidTime; } #endif From 00772996b69b2b5ab614564e861a1587f4d5058b Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 14 Sep 2025 03:05:06 -0700 Subject: [PATCH 41/45] Fix GPS gm_mktime memory leak (#7981) --- src/gps/RTC.cpp | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 39b633e47..e75102deb 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -324,14 +324,40 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) time_t gm_mktime(struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ - setenv("TZ", "GMT0", 1); - time_t res = mktime(tm); - if (*config.device.tzdef) { - setenv("TZ", config.device.tzdef, 1); - } else { - setenv("TZ", "UTC0", 1); + time_t result = 0; + + // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + int year = 1900 + tm->tm_year; // tm_year is years since 1900 + int year_minus_one = year - 1; + int days_before_this_year = 0; + days_before_this_year += year_minus_one * 365; + // leap days: every 4 years, except 100s, but including 400s. + days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; + // subtract from 1970-01-01 to get days since epoch + days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); + + // Now, within this tm->year, compute the days *before* this tm->month starts. + int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year + int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 + + // If this is a leap year, and we're past February, add a day: + if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { + days_this_year_before_this_month += 1; } - return res; + + // And within this month: + int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 + + // Now combine them all together, and convert days to seconds: + result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); + result *= 86400L; + + // Finally, add in the hours, minutes, and seconds of today: + result += tm->tm_hour * 3600; + result += tm->tm_min * 60; + result += tm->tm_sec; + + return result; #else return mktime(tm); #endif From 2dc7760508360732be6605d708b5f046530f2be7 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 14 Sep 2025 06:31:17 -0500 Subject: [PATCH 42/45] Scale probe buffer size based on current baud rate (#7975) * Scale probe buffer size based on current baud rate * Throttle bad time validation logging and fix time comparison logic * Remove comment * Missed the other instances * Copy pasta --- src/gps/GPS.cpp | 19 +++++++++++++---- src/gps/GPS.h | 2 +- src/gps/RTC.cpp | 54 ++++++++++++++++++++++++++++++++----------------- src/gps/RTC.h | 2 +- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index d4e9076d9..a663f46c4 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1205,7 +1205,7 @@ static const char *DETECTED_MESSAGE = "%s detected"; LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ clearBuffer(); \ _serial_gps->write(COMMAND "\r\n"); \ - GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \ + GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ return detectedDriver; \ } \ @@ -1367,9 +1367,18 @@ GnssModel_t GPS::probe(int serialSpeed) return GNSS_MODEL_UNKNOWN; } -GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap) +GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed) { - char response[256] = {0}; // Fixed buffer instead of String + // Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline + // Higher baud rates get proportionally larger buffers to handle more data + int bufferSize = (serialSpeed * 256) / 9600; + // Clamp buffer size between reasonable limits + if (bufferSize < 128) + bufferSize = 128; + if (bufferSize > 2048) + bufferSize = 2048; + + char *response = new char[bufferSize](); // Dynamically allocate based on baud rate uint16_t responseLen = 0; unsigned long start = millis(); while (millis() - start < timeout) { @@ -1377,7 +1386,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vectorread(); // Add char to buffer if there's space - if (responseLen < sizeof(response) - 1) { + if (responseLen < bufferSize - 1) { response[responseLen++] = c; response[responseLen] = '\0'; } @@ -1390,6 +1399,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector &responseMap); + GnssModel_t getProbeResponse(unsigned long timeout, const std::vector &responseMap, int serialSpeed); // Get GNSS model GnssModel_t probe(int serialSpeed); diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 39b633e47..3e410d236 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -9,6 +9,9 @@ static RTCQuality currentQuality = RTCQualityNone; uint32_t lastSetFromPhoneNtpOrGps = 0; +static uint32_t lastTimeValidationWarning = 0; +static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds + RTCQuality getRTCQuality() { return currentQuality; @@ -48,7 +51,9 @@ RTCSetResult readFromRTC() #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + } return RTCSetResultInvalidTime; } #endif @@ -87,7 +92,10 @@ RTCSetResult readFromRTC() #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif @@ -130,15 +138,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv->tv_sec < BUILD_EPOCH) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); -#endif + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; - } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, - BUILD_EPOCH + FORTY_YEARS); -#endif + } else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif @@ -256,15 +269,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms #ifdef BUILD_EPOCH if (tv.tv_sec < BUILD_EPOCH) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); -#endif + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; - } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { -#ifdef GPS_DEBUG - LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, - BUILD_EPOCH + FORTY_YEARS); -#endif + } else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { + // Calculate max allowed time safely to avoid overflow in logging + uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; + uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime; + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, + (uint32_t)BUILD_EPOCH, maxAllowedPrintable); + lastTimeValidationWarning = millis(); + } return RTCSetResultInvalidTime; } #endif diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 03350823c..1ecde79ae 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm); #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 #ifdef BUILD_EPOCH -#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware +#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow #endif From bf4e2e8e866c2f522f2e8f24ad14bb76f356fd7f Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 14 Sep 2025 03:05:06 -0700 Subject: [PATCH 43/45] Fix GPS gm_mktime memory leak (#7981) --- src/gps/RTC.cpp | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 3e410d236..4a629d755 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -342,14 +342,40 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) time_t gm_mktime(struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ - setenv("TZ", "GMT0", 1); - time_t res = mktime(tm); - if (*config.device.tzdef) { - setenv("TZ", config.device.tzdef, 1); - } else { - setenv("TZ", "UTC0", 1); + time_t result = 0; + + // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + int year = 1900 + tm->tm_year; // tm_year is years since 1900 + int year_minus_one = year - 1; + int days_before_this_year = 0; + days_before_this_year += year_minus_one * 365; + // leap days: every 4 years, except 100s, but including 400s. + days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400; + // subtract from 1970-01-01 to get days since epoch + days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400); + + // Now, within this tm->year, compute the days *before* this tm->month starts. + int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year + int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11 + + // If this is a leap year, and we're past February, add a day: + if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) { + days_this_year_before_this_month += 1; } - return res; + + // And within this month: + int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31 + + // Now combine them all together, and convert days to seconds: + result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today); + result *= 86400L; + + // Finally, add in the hours, minutes, and seconds of today: + result += tm->tm_hour * 3600; + result += tm->tm_min * 60; + result += tm->tm_sec; + + return result; #else return mktime(tm); #endif From 70724bef72684c96f8a6d2972d80d37648eb8de4 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 14 Sep 2025 08:12:38 -0500 Subject: [PATCH 44/45] Fix overflow of time value (#7984) * Fix overflow of time value * Revert "Fix overflow of time value" This reverts commit 084796920179e80a7500d36c25fd4d82b3ef4214. * That got boogered up --- src/gps/RTC.cpp | 4 ++-- src/gps/RTC.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 4a629d755..da20e28eb 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -143,7 +143,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; - } else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + } else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { // Calculate max allowed time safely to avoid overflow in logging uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; @@ -274,7 +274,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) lastTimeValidationWarning = millis(); } return RTCSetResultInvalidTime; - } else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) { + } else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) { if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { // Calculate max allowed time safely to avoid overflow in logging uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS; diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 1ecde79ae..eca17bf35 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm); #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 #ifdef BUILD_EPOCH -#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow +static constexpr uint64_t FORTY_YEARS = (40ULL * 365 * SEC_PER_DAY); // Use 64-bit arithmetic to prevent overflow #endif From b9d53d667ee9e6b8eeede142c0e9c6c92dfeab97 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Sep 2025 02:29:47 +0200 Subject: [PATCH 45/45] Feature: Seamless Cross-Preset Communication via UDP Multicast Bridging (#7753) * Added compatibility between nodes on different Presets through `Mesh via UDP` * Optimize multicast handling and channel mapping - FloodingRouter: remove redundant UDP-encrypted rebroadcast suppression. - Router: guard multicast fallback with HAS_UDP_MULTICAST and map fallback-decoded packets to the local default channel via isDefaultChannel() - UdpMulticastHandler: set transport_mechanism only after successful decode * trunk fmt * Move setting transport mechanism. --------- Co-authored-by: GUVWAF --- src/mesh/Channels.cpp | 26 +++++++++++++++++++++++ src/mesh/Channels.h | 2 ++ src/mesh/Router.cpp | 30 +++++++++++++++++++++++++++ src/mesh/udp/UdpMulticastHandler.h | 2 +- variants/esp32s3/t-deck-pro/variant.h | 15 +++++++------- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4ef41ddfb..4c0a0edad 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -423,6 +423,32 @@ bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash) } } +bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) +{ + // Iterate all known presets + for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; + ++preset) { + const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false); + if (!name) + continue; + if (strcmp(name, "Invalid") == 0) + continue; // skip invalid placeholder + uint8_t h = xorHash((const uint8_t *)name, strlen(name)); + // Expand default PSK alias 1 to actual bytes and xor into hash + uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); + if (tmp == channelHash) { + // Set crypto to defaultpsk and report success + CryptoKey k; + memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); + k.length = sizeof(defaultpsk); + crypto->setKey(k); + LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); + return true; + } + } + return false; +} + /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) * * This method is called before encoding outbound packets diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index 7873a306a..b53f552fa 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -94,6 +94,8 @@ class Channels bool ensureLicensedOperation(); + bool setDefaultPresetCryptoForHash(ChannelHash channelHash); + private: /** Given a channel index, change to use the crypto key specified by that index * diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 37b70dab6..09fb079c5 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -430,6 +430,36 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) } } } + +#if HAS_UDP_MULTICAST + // Fallback: for UDP multicast, try default preset names with default PSK if normal channel match failed + if (!decrypted && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { + if (channels.setDefaultPresetCryptoForHash(p->channel)) { + memcpy(bytes, p->encrypted.bytes, rawSize); + crypto->decrypt(p->from, p->id, rawSize, bytes); + + meshtastic_Data decodedtmp; + memset(&decodedtmp, 0, sizeof(decodedtmp)); + if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && + decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { + p->decoded = decodedtmp; + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; + // Map to our local default channel index (name+PSK default), not necessarily primary + ChannelIndex defaultIndex = channels.getPrimaryIndex(); + for (ChannelIndex i = 0; i < channels.getNumChannels(); ++i) { + if (channels.isDefaultChannel(i)) { + defaultIndex = i; + break; + } + } + chIndex = defaultIndex; + decrypted = true; + } else { + LOG_WARN("UDP fallback decode attempted but failed for hash 0x%x", p->channel); + } + } + } +#endif if (decrypted) { // parsing was successful p->channel = chIndex; // change to store the index instead of the hash diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 9650668a8..2df8686a3 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -50,10 +50,10 @@ class UdpMulticastHandler final LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif meshtastic_MeshPacket mp; - mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; mp.pki_encrypted = false; mp.public_key.size = 0; memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index abe0a772a..35cb99435 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -93,11 +93,10 @@ // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) -#define MODEM_POWER_EN 41 -#define MODEM_PWRKEY 40 -#define MODEM_RST 9 -#define MODEM_RI 7 -#define MODEM_DTR 8 -#define MODEM_RX 10 -#define MODEM_TX 11 - +#define MODEM_POWER_EN 41 +#define MODEM_PWRKEY 40 +#define MODEM_RST 9 +#define MODEM_RI 7 +#define MODEM_DTR 8 +#define MODEM_RX 10 +#define MODEM_TX 11