Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
7e00054
Rename startTransmitTimerSNR to startTransmitTimerRebroadcast
compumike Sep 5, 2025
3cc2b70
Pass meshtastic_MeshPacket down into startTransmitTimerRebroadcast an…
compumike Sep 5, 2025
484b4cd
Add NodeDB::isFavorite, NodeDB::isFromOrToFavoritedNode
compumike Sep 5, 2025
ab53329
Add RadioInterface::shouldRebroadcastEarlyLikeRouter and add CLIENT_B…
compumike Sep 5, 2025
b305acf
Add FloodingRouter::roleAllowsCancelingDupe and condition for CLIENT_…
compumike Sep 5, 2025
b1f55ef
Fix linter
compumike Sep 8, 2025
c63102a
Swap expression order to allow short-circuit evaluation
compumike Sep 8, 2025
b768860
NodeDB::isFromOrToFavoritedNode: skip search for NODENUM_BROADCAST; o…
compumike Sep 8, 2025
5a46337
Remove changes to src/mesh/generated/meshtastic/config.pb.h from this PR
compumike Sep 8, 2025
27cdd46
getTxDelayMsecWeighted and startTransmitTimerRebroadcast: extract p->…
compumike Sep 8, 2025
4140ecf
Bring src/mesh/generated/meshtastic/config.pb.h from develop after re…
compumike Sep 8, 2025
527e88c
Add CLIENT_BASE to DisplayFormatters::getDeviceRole
compumike Sep 8, 2025
87eff2c
Fix logic in Screen::shouldWakeOnReceivedMessage and add CLIENT_HIDDE…
compumike Sep 8, 2025
4ab125b
src/graphics/Screen.cpp: move #include "meshUtils.h" outside of "#ifd…
compumike Sep 8, 2025
35340fc
NextHopRouter::roleAllowsCancelingFromTxQueue (same logic as Flooding…
compumike Sep 10, 2025
0fc33c3
Fix memory leak in NextHopRouter: always free packet copy when removi…
compumike Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/DisplayFormatters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 5 additions & 3 deletions src/graphics/Screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "PowerMon.h"
#include "Throttle.h"
#include "configuration.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include <OLEDDisplay.h>

Expand Down Expand Up @@ -58,7 +59,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#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"
Expand Down Expand Up @@ -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)) {
Comment thread
compumike marked this conversation as resolved.
return false;
}
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {
Expand Down
26 changes: 22 additions & 4 deletions src/mesh/FloodingRouter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +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 &&
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))
Expand Down
4 changes: 4 additions & 0 deletions src/mesh/FloodingRouter.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ 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);

Expand Down
23 changes: 18 additions & 5 deletions src/mesh/NextHopRouter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -170,17 +179,21 @@ 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
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;
Expand Down
3 changes: 3 additions & 0 deletions src/mesh/NextHopRouter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
59 changes: 59 additions & 0 deletions src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,65 @@ 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;
}
return false;
}

bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p)
{
// 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.
Comment thread
thebentern marked this conversation as resolved.
}

return false;
}

void NodeDB::pause_sort(bool paused)
{
sortingIsPaused = paused;
Expand Down
10 changes: 10 additions & 0 deletions src/mesh/NodeDB.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
23 changes: 20 additions & 3 deletions src/mesh/RadioInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,16 +314,33 @@ 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)
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);
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 {
Expand Down
5 changes: 4 additions & 1 deletion src/mesh/RadioInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,11 @@ 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);
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; }
Expand Down
8 changes: 4 additions & 4 deletions src/mesh/RadioLibInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) : 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);
Expand All @@ -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);
}
}

Expand All @@ -336,11 +336,11 @@ void RadioLibInterface::startTransmitTimer(bool withDelay)
}
}

void RadioLibInterface::startTransmitTimerSNR(float snr)
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);
uint32_t delay = getTxDelayMsecWeighted(p);
notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/mesh/RadioLibInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(meshtastic_MeshPacket *p);

void handleTransmitInterrupt();
void handleReceiveInterrupt();
Expand Down
6 changes: 3 additions & 3 deletions src/platform/portduino/SimRadio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -57,11 +57,11 @@ void SimRadio::startTransmitTimer(bool withDelay)
}
}

void SimRadio::startTransmitTimerSNR(float snr)
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);
uint32_t delayMsec = getTxDelayMsecWeighted(p);
// LOG_DEBUG("xmit timer %d", delay);
notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false);
}
Expand Down
2 changes: 1 addition & 1 deletion src/platform/portduino/SimRadio.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(meshtastic_MeshPacket *p);

void handleTransmitInterrupt();
void handleReceiveInterrupt();
Expand Down