Merge pull request #7873 from compumike/compumike/client-base-role

Add `CLIENT_BASE` role: `ROUTER` for favorites, `CLIENT` otherwise (for attic/roof nodes!)
This commit is contained in:
Ben Meadors 2025-09-12 13:11:53 -05:00 committed by GitHub
commit 106a052950
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 157 additions and 25 deletions

View File

@ -52,6 +52,9 @@ const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role
case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN:
return "Client Hidden"; return "Client Hidden";
break; break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE:
return "Client Base";
break;
case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND:
return "Lost and Found"; return "Lost and Found";
break; break;

View File

@ -25,6 +25,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "PowerMon.h" #include "PowerMon.h"
#include "Throttle.h" #include "Throttle.h"
#include "configuration.h" #include "configuration.h"
#include "meshUtils.h"
#if HAS_SCREEN #if HAS_SCREEN
#include <OLEDDisplay.h> #include <OLEDDisplay.h>
@ -58,7 +59,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "mesh/Channels.h" #include "mesh/Channels.h"
#include "mesh/generated/meshtastic/deviceonly.pb.h" #include "mesh/generated/meshtastic/deviceonly.pb.h"
#include "meshUtils.h"
#include "modules/ExternalNotificationModule.h" #include "modules/ExternalNotificationModule.h"
#include "modules/TextMessageModule.h" #include "modules/TextMessageModule.h"
#include "modules/WaypointModule.h" #include "modules/WaypointModule.h"
@ -1605,13 +1605,15 @@ bool shouldWakeOnReceivedMessage()
/* /*
The goal here is to determine when we do NOT wake up the screen on message received: The goal here is to determine when we do NOT wake up the screen on message received:
- Any ext. notifications are turned on - 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 the battery level is very low
*/ */
if (moduleConfig.external_notification.enabled) { if (moduleConfig.external_notification.enabled) {
return false; 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; return false;
} }
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {

View File

@ -43,12 +43,30 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
return Router::shouldFilterReceived(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) void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p)
{ {
if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) {
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) {
// cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! // 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. // But only LoRa packets should be able to trigger this.
if (Router::cancelSending(p->from, p->id)) if (Router::cancelSending(p->from, p->id))

View File

@ -59,6 +59,10 @@ class FloodingRouter : public Router
*/ */
virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; 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 */ /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */
void perhapsCancelDupe(const meshtastic_MeshPacket *p); void perhapsCancelDupe(const meshtastic_MeshPacket *p);

View File

@ -161,6 +161,15 @@ bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id)
return stopRetransmission(key); 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) bool NextHopRouter::stopRetransmission(GlobalPacketId key)
{ {
auto old = findPendingPacket(key); auto old = findPendingPacket(key);
@ -170,17 +179,21 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
to avoid canceling a transmission if it was ACKed super fast via MQTT */ to avoid canceling a transmission if it was ACKed super fast via MQTT */
if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { 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 // 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 && if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) {
config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
// remove the 'original' (identified by originator and packet->id) from the txqueue and free it // remove the 'original' (identified by originator and packet->id) from the txqueue and free it
cancelSending(getFrom(p), p->id); 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); auto numErased = pending.erase(key);
assert(numErased == 1); 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; return true;
} else } else
return false; return false;

View File

@ -121,6 +121,9 @@ class NextHopRouter : public FloodingRouter
*/ */
PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); 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 * Stop any retransmissions we are doing of the specified node/packet ID pair
* *

View File

@ -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.
}
return false;
}
void NodeDB::pause_sort(bool paused) void NodeDB::pause_sort(bool paused)
{ {
sortingIsPaused = paused; sortingIsPaused = paused;

View File

@ -185,6 +185,16 @@ class NodeDB
*/ */
void set_favorite(bool is_favorite, uint32_t nodeId); 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 * Other functions like the node picker can request a pause in the node sorting
*/ */

View File

@ -314,16 +314,33 @@ uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr)
return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; 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 */ /** 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) // high SNR = large CW size (Long Delay)
// low SNR = small CW size (Short Delay) // low SNR = small CW size (Short Delay)
float snr = p->rx_snr;
uint32_t delay = 0; uint32_t delay = 0;
uint8_t CWsize = getCWsize(snr); uint8_t CWsize = getCWsize(snr);
// LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize);
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || if (shouldRebroadcastEarlyLikeRouter(p)) {
config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
delay = random(0, 2 * CWsize) * slotTimeMsec; delay = random(0, 2 * CWsize) * slotTimeMsec;
LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay);
} else { } else {

View File

@ -180,8 +180,11 @@ class RadioInterface
/** The worst-case SNR_based packet delay */ /** The worst-case SNR_based packet delay */
uint32_t getTxDelayMsecWeightedWorst(float snr); 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 */ /** 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 */ /** If the packet is not already in the late rebroadcast window, move it there */
virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; }

View File

@ -310,7 +310,7 @@ void RadioLibInterface::setTransmitDelay()
// So we want to make sure the other side has had a chance to reconfigure its radio. // So we want to make sure the other side has had a chance to reconfigure its radio.
if (p->tx_after) { 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(); unsigned long now = millis();
p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); 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); notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false);
@ -323,7 +323,7 @@ void RadioLibInterface::setTransmitDelay()
} else { } else {
// If there is a SNR, start a timer scaled based on that SNR. // 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); LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr);
startTransmitTimerSNR(p->rx_snr); startTransmitTimerRebroadcast(p);
} }
} }
@ -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 we have work to do and the timer wasn't already scheduled, schedule it now
if (!txQueue.empty()) { if (!txQueue.empty()) {
uint32_t delay = getTxDelayMsecWeighted(snr); uint32_t delay = getTxDelayMsecWeighted(p);
notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable
} }
} }

View File

@ -161,7 +161,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
* timer scaled to SNR of to be flooded packet * timer scaled to SNR of to be flooded packet
* @return Timestamp after which the packet may be sent * @return Timestamp after which the packet may be sent
*/ */
void startTransmitTimerSNR(float snr); void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p);
void handleTransmitInterrupt(); void handleTransmitInterrupt();
void handleReceiveInterrupt(); void handleReceiveInterrupt();

View File

@ -43,7 +43,7 @@ void SimRadio::setTransmitDelay()
} else { } else {
// If there is a SNR, start a timer scaled based on that SNR. // 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); LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr);
startTransmitTimerSNR(p->rx_snr); startTransmitTimerRebroadcast(p);
} }
} }
@ -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 we have work to do and the timer wasn't already scheduled, schedule it now
if (!txQueue.empty()) { if (!txQueue.empty()) {
uint32_t delayMsec = getTxDelayMsecWeighted(snr); uint32_t delayMsec = getTxDelayMsecWeighted(p);
// LOG_DEBUG("xmit timer %d", delay); // LOG_DEBUG("xmit timer %d", delay);
notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false);
} }

View File

@ -64,7 +64,7 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr
void startTransmitTimer(bool withDelay = true); void startTransmitTimer(bool withDelay = true);
/** timer scaled to SNR of to be flooded packet */ /** timer scaled to SNR of to be flooded packet */
void startTransmitTimerSNR(float snr); void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p);
void handleTransmitInterrupt(); void handleTransmitInterrupt();
void handleReceiveInterrupt(); void handleReceiveInterrupt();