Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion protobufs
17 changes: 17 additions & 0 deletions src/mesh/LR11x0Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,23 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
return true;
}

template <typename T> void LR11x0Interface<T>::setTransmitPower(int dbm)
{
// Never exceed the configured `power` (see SX126x override).
// LR11x0 low-power path minimum is -17 dBm.
if (dbm > power) {
LOG_WARN("LR11x0 TX power %d dBm above configured %d dBm, clamping down", dbm, power);
dbm = power;
}
if (dbm < -17) {
LOG_WARN("LR11x0 TX power %d dBm below chip minimum, clamping to -17 dBm", dbm);
dbm = -17;
}
int err = lora.setOutputPower((int8_t)dbm);
if (err != RADIOLIB_ERR_NONE)
LOG_ERROR("LR11x0 setOutputPower %s%d", radioLibErr, err);
}

template <typename T> void LR11x0Interface<T>::disableInterrupt()
{
lora.clearIrqAction();
Expand Down
3 changes: 3 additions & 0 deletions src/mesh/LR11x0Interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ template <class T> class LR11x0Interface : public RadioLibInterface
void resetAGC() override;
#endif

/// Push a TX power to the chip for the next packet. See base class for semantics.
void setTransmitPower(int dbm) override;

protected:
/**
* Specific module instance
Expand Down
27 changes: 27 additions & 0 deletions src/mesh/RF95Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,33 @@ bool RF95Interface::init()
return res == RADIOLIB_ERR_NONE;
}

void RF95Interface::setTransmitPower(int dbm)
{
// Never exceed the configured `power` (see SX126x override).
// PA_BOOST path minimum is 2 dBm; RFO path minimum is -4 dBm.
const int minDbm =
#ifdef USE_RF95_RFO
-4;
#else
2;
#endif
if (dbm > power) {
LOG_WARN("RF95 TX power %d dBm above configured %d dBm, clamping down", dbm, power);
dbm = power;
}
if (dbm < minDbm) {
LOG_WARN("RF95 TX power %d dBm below chip minimum, clamping to %d dBm", dbm, minDbm);
dbm = minDbm;
}
#ifdef USE_RF95_RFO
int err = lora->setOutputPower((int8_t)dbm, true);
#else
int err = lora->setOutputPower((int8_t)dbm);
#endif
if (err != RADIOLIB_ERR_NONE)
LOG_ERROR("RF95 setOutputPower err=%d", err);
}

void RF95Interface::disableInterrupt()
{
lora->clearDio0Action();
Expand Down
3 changes: 3 additions & 0 deletions src/mesh/RF95Interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class RF95Interface : public RadioLibInterface
/// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep.
virtual bool sleep() override;

/// Push a TX power to the chip for the next packet. See base class for semantics.
void setTransmitPower(int dbm) override;

protected:
/**
* Glue functions called from ISR land
Expand Down
17 changes: 17 additions & 0 deletions src/mesh/RadioInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,23 @@ void RadioInterface::limitPower(int8_t loraMaxPower)
LOG_INFO("Final Tx power: %d dBm", power);
}

void RadioInterface::applyTxPowerPenalty(meshtastic_MeshPacket *const txp, bool fromUs)
{
uint8_t penalty = txp->tx_power_penalty_db;
if (penalty > MAX_TX_PENALTY_DB) {
LOG_WARN("Whisper penalty %u dB exceeds max, clamping to %u dB", (unsigned)penalty, (unsigned)MAX_TX_PENALTY_DB);
penalty = MAX_TX_PENALTY_DB;
}
isWhispering = penalty > 0 && fromUs;
if (isWhispering) {
// Promote both to int before subtracting so we don't underflow
// int8_t. The per-radio override does the final hw-range clamp.
int target = (int)power - (int)penalty;
LOG_INFO("Whisper: applying %u dB penalty (target %d dBm)", (unsigned)penalty, target);
setTransmitPower(target);
}
}

void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p)
{
if (router) {
Expand Down
41 changes: 41 additions & 0 deletions src/mesh/RadioInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,47 @@ class RadioInterface
/** If the packet is not already in the late rebroadcast window, move it there */
virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; }

/**
* Set the radio's TX power for the next transmission.
*
* Takes an int (not int8_t) so callers can pass the result of
* `power - penalty` without pre-clamping. Per-radio overrides are
* responsible for clamping `dbm` to the chip's hardware range before
* touching the chip, and must never exceed the configured `power`.
*
* Does not mutate `power`, so a subsequent setTransmitPower(power)
* call restores the configured value.
*
* Default is a no-op so non-RadioLib interfaces still compile.
*/
virtual void setTransmitPower(int dbm) { (void)dbm; }

/// Upper bound on a per-packet whisper penalty before we treat it as a
/// malformed value and clamp. 60 dB is well past anything the chip can
/// actually produce, but keeps a runaway value from underflowing the
/// int8 math downstream.
static constexpr uint8_t MAX_TX_PENALTY_DB = 60;

/**
* Apply (or skip) the whisper TX-power penalty for an outgoing packet.
*
* Sets `isWhispering` and calls `setTransmitPower()` only when the
* packet asked for a non-zero penalty AND the caller indicates the
* local node originated the packet. Relays/rebroadcasts (`fromUs ==
* false`) are deliberately skipped so they keep their own configured
* power.
*
* fromUs is taken as a parameter rather than computed inside (via
* `isFromUs(txp)`) so the method can be exercised from unit tests
* without standing up the global NodeDB.
*/
void applyTxPowerPenalty(meshtastic_MeshPacket *const txp, bool fromUs);

/// True between applyTxPowerPenalty() and the matching restore in
/// completeSending(). Tracks whether the chip's TX power has been
/// dropped from the configured value for the in-flight packet.
bool isWhispering = false;

/**
* If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version
* @return Whether a pending packet was removed
Expand Down
11 changes: 11 additions & 0 deletions src/mesh/RadioLibInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,15 @@ void RadioLibInterface::handleTransmitInterrupt()

void RadioLibInterface::completeSending()
{
// Restore TX power if the just-finished packet was whispered.
// Done here (not after startTransmit) because setOutputPower is only
// safe in standby, and by the time we hit completeSending the TX-done
// IRQ has already returned the chip to standby.
if (isWhispering) {
setTransmitPower(power);
isWhispering = false;
}

// We are careful to clear sending packet before calling printPacket because
// that can take a long time
auto p = sendingPacket;
Expand Down Expand Up @@ -594,6 +603,8 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
} else {
configHardwareForSend(); // must be after setStandby

applyTxPowerPenalty(txp, isFromUs(txp));

size_t numbytes = beginSending(txp);

int res = iface->startTransmit((uint8_t *)&radioBuffer, numbytes);
Expand Down
1 change: 1 addition & 0 deletions src/mesh/RadioLibInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
/// are _trying_ to receive a packet currently (note - we might just be waiting for one)
bool isReceiving = false;


public:
/** Our ISR code currently needs this to find our active instance
*/
Expand Down
18 changes: 18 additions & 0 deletions src/mesh/SX126xInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,24 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
return true;
}

template <typename T> void SX126xInterface<T>::setTransmitPower(int dbm)
{
// Never exceed the configured `power` (already clamped to
// regional/chip ceiling by limitPower() at init). -9 dBm is the
// SX126x published minimum and matches the existing init clamp.
if (dbm > power) {
LOG_WARN("SX126X TX power %d dBm above configured %d dBm, clamping down", dbm, power);
dbm = power;
}
if (dbm < -9) {
LOG_WARN("SX126X TX power %d dBm below chip minimum, clamping to -9 dBm", dbm);
dbm = -9;
}
int err = lora.setOutputPower((int8_t)dbm);
if (err != RADIOLIB_ERR_NONE)
LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err);
}

template <typename T> void SX126xInterface<T>::disableInterrupt()
{
lora.clearDio1Action();
Expand Down
3 changes: 3 additions & 0 deletions src/mesh/SX126xInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ template <class T> class SX126xInterface : public RadioLibInterface

void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; }

/// Push a TX power to the chip for the next packet. See base class for semantics.
void setTransmitPower(int dbm) override;

protected:
float currentLimit = 140; // Higher OCP limit for SX126x PA
float tcxoVoltage = 0.0;
Expand Down
17 changes: 17 additions & 0 deletions src/mesh/SX128xInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,23 @@ template <typename T> bool SX128xInterface<T>::reconfigure()
return true;
}

template <typename T> void SX128xInterface<T>::setTransmitPower(int dbm)
{
// Never exceed the configured `power` (see SX126x override).
// SX128x published minimum is -18 dBm.
if (dbm > power) {
LOG_WARN("SX128X TX power %d dBm above configured %d dBm, clamping down", dbm, power);
dbm = power;
}
if (dbm < -18) {
LOG_WARN("SX128X TX power %d dBm below chip minimum, clamping to -18 dBm", dbm);
dbm = -18;
}
int err = lora.setOutputPower((int8_t)dbm);
if (err != RADIOLIB_ERR_NONE)
LOG_ERROR("SX128X setOutputPower %s%d", radioLibErr, err);
}

template <typename T> void SX128xInterface<T>::disableInterrupt()
{
lora.clearDio1Action();
Expand Down
3 changes: 3 additions & 0 deletions src/mesh/SX128xInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ template <class T> class SX128xInterface : public RadioLibInterface

bool isIRQPending() override { return lora.getIrqFlags() != 0; }

/// Push a TX power to the chip for the next packet. See base class for semantics.
void setTransmitPower(int dbm) override;

protected:
/**
* Specific module instance
Expand Down
2 changes: 1 addition & 1 deletion src/mesh/generated/meshtastic/deviceonly.pb.h
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
#define meshtastic_BackupPreferences_size 2432
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737
#define meshtastic_DeviceState_size 1749
#define meshtastic_NodeInfoLite_size 196
#define meshtastic_PositionLite_size 28
#define meshtastic_UserLite_size 98
Expand Down
28 changes: 24 additions & 4 deletions src/mesh/generated/meshtastic/mesh.pb.h
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,24 @@ typedef struct _meshtastic_MeshPacket {
uint32_t tx_after;
/* Indicates which transport mechanism this packet arrived over */
meshtastic_MeshPacket_TransportMechanism transport_mechanism;
/* dB to subtract from the configured tx_power for this single packet.
Used to "whisper" a message to nearby nodes without raising the
noise floor for the wider mesh: lower power keeps the packet local
but still reaches physically close neighbors.

Only the originating device applies the penalty. Relays and
rebroadcasts use their own configured tx_power, so the whisper
does not propagate. The originator's want_ack retries reuse the
same MeshPacket and inherit the penalty.

*Never* sent over the radio links. Clamped on the originating
firmware to the radio's hardware minimum on the low end and to
60 dB on the high end.

Note: the generated C struct is uint8_t (see mesh.options
`int_size:8`), so values above 255 are silently truncated on
decode. The 60 dB upper clamp catches whatever survives. */
uint8_t tx_power_penalty_db;
} meshtastic_MeshPacket;

/* The bluetooth to device link:
Expand Down Expand Up @@ -1522,7 +1540,7 @@ extern "C" {
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_StatusMessage_init_default {""}
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN, 0}
#define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0}
#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
#define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN}
Expand Down Expand Up @@ -1556,7 +1574,7 @@ extern "C" {
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_StatusMessage_init_zero {""}
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN, 0}
#define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0}
#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0}
#define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN}
Expand Down Expand Up @@ -1686,6 +1704,7 @@ extern "C" {
#define meshtastic_MeshPacket_relay_node_tag 19
#define meshtastic_MeshPacket_tx_after_tag 20
#define meshtastic_MeshPacket_transport_mechanism_tag 21
#define meshtastic_MeshPacket_tx_power_penalty_db_tag 22
#define meshtastic_NodeInfo_num_tag 1
#define meshtastic_NodeInfo_user_tag 2
#define meshtastic_NodeInfo_position_tag 3
Expand Down Expand Up @@ -1944,7 +1963,8 @@ X(a, STATIC, SINGULAR, BOOL, pki_encrypted, 17) \
X(a, STATIC, SINGULAR, UINT32, next_hop, 18) \
X(a, STATIC, SINGULAR, UINT32, relay_node, 19) \
X(a, STATIC, SINGULAR, UINT32, tx_after, 20) \
X(a, STATIC, SINGULAR, UENUM, transport_mechanism, 21)
X(a, STATIC, SINGULAR, UENUM, transport_mechanism, 21) \
X(a, STATIC, SINGULAR, UINT32, tx_power_penalty_db, 22)
#define meshtastic_MeshPacket_CALLBACK NULL
#define meshtastic_MeshPacket_DEFAULT NULL
#define meshtastic_MeshPacket_payload_variant_decoded_MSGTYPE meshtastic_Data
Expand Down Expand Up @@ -2263,7 +2283,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_KeyVerification_size 79
#define meshtastic_LogRecord_size 426
#define meshtastic_LowEntropyKey_size 0
#define meshtastic_MeshPacket_size 381
#define meshtastic_MeshPacket_size 385
#define meshtastic_MqttClientProxyMessage_size 501
#define meshtastic_MyNodeInfo_size 83
#define meshtastic_NeighborInfo_size 258
Expand Down
Loading