From 7c1dfd9a95ef157323a1c4c7355c4e19f9aefe43 Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Sat, 28 Mar 2026 12:25:31 +0100 Subject: [PATCH 1/3] refactor: move tx duty cycle check to function As a preparation to make the tx duty cycle check more precise, we move it into a function. --- src/Dispatcher.cpp | 18 ++++++++++-------- src/Dispatcher.h | 7 +++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 9d7a11131d..50ddbe5a38 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -271,19 +271,21 @@ void Dispatcher::processRecvPacket(Packet* pkt) { } } -void Dispatcher::checkSend() { - if (_mgr->getOutboundCount(_ms->getMillis()) == 0) return; - - updateTxBudget(); - - uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT); +void Dispatcher::ensureTxDutyCycle(int tx_len) { + uint32_t est_airtime = _radio->getEstAirtimeFor(tx_len); if (tx_budget_ms < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV) { float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); unsigned long needed = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV - tx_budget_ms; next_tx_time = futureMillis((unsigned long)(needed / duty_cycle)); - return; } - +} + +void Dispatcher::checkSend() { + if (_mgr->getOutboundCount(_ms->getMillis()) == 0) return; + + updateTxBudget(); + ensureTxDutyCycle(MAX_TRANS_UNIT); + if (!millisHasNowPassed(next_tx_time)) return; if (_radio->isReceiving()) { if (cad_busy_start == 0) { diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2a99d0682b..c488400f85 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -195,6 +195,13 @@ class Dispatcher { private: bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len); + /** + * \brief ensure the next tx time obeys the duty cycle + * + * Internally this updates the next_tx_time so that the Tx + * is executed by earliest the next allowed time slot. + */ + void ensureTxDutyCycle(int tx_len); void checkRecv(); void checkSend(); }; From e797d55c7a117826c838bd81a05a71632011644f Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Sat, 28 Mar 2026 12:35:14 +0100 Subject: [PATCH 2/3] refactor: move max airtime computation to macro By that we make it reusable and also ensure the integer division is executed correctly (it was correct before, but just because the mult and div operator are left associating). --- src/Dispatcher.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 50ddbe5a38..b67ad70690 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -11,6 +11,8 @@ namespace mesh { #define MAX_RX_DELAY_MILLIS 32000 // 32 seconds #define MIN_TX_BUDGET_RESERVE_MS 100 // min budget (ms) required before allowing next TX #define MIN_TX_BUDGET_AIRTIME_DIV 2 // require at least 1/N of estimated airtime as budget before TX +/// add some time to get the max allowed airtime for the est. airtime +#define MAX_TX_AIRTIME_FOR_EST(est_airtime) (((est_airtime) * 3) / 2) #ifndef NOISE_FLOOR_CALIB_INTERVAL #define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds @@ -325,7 +327,7 @@ void Dispatcher::checkSend() { } else { memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len; - uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; + uint32_t max_airtime = MAX_TX_AIRTIME_FOR_EST(_radio->getEstAirtimeFor(len)); outbound_start = _ms->getMillis(); bool success = _radio->startSendRaw(raw, len); if (!success) { From 0ded46a975006484c133af39372bec4317a6b97a Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Sat, 28 Mar 2026 12:37:32 +0100 Subject: [PATCH 3/3] fix: use max estimated airtime for duty cycle enforcement Previously the max required airtime to enter the next transmit slot was computed to be half the estimated time. However, the transmit logic uses 1.5 times the estimated time as maximum TX duration. By that, the duty cycle could still be violated as the actual transmit logic might send for longer than what we ensured to stay within the cycle. We fix this by aligning both computations and always use 1.5 times the estimated airtime. Xref: #817 --- src/Dispatcher.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index b67ad70690..5464b32c78 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -10,7 +10,6 @@ namespace mesh { #define MAX_RX_DELAY_MILLIS 32000 // 32 seconds #define MIN_TX_BUDGET_RESERVE_MS 100 // min budget (ms) required before allowing next TX -#define MIN_TX_BUDGET_AIRTIME_DIV 2 // require at least 1/N of estimated airtime as budget before TX /// add some time to get the max allowed airtime for the est. airtime #define MAX_TX_AIRTIME_FOR_EST(est_airtime) (((est_airtime) * 3) / 2) @@ -275,9 +274,9 @@ void Dispatcher::processRecvPacket(Packet* pkt) { void Dispatcher::ensureTxDutyCycle(int tx_len) { uint32_t est_airtime = _radio->getEstAirtimeFor(tx_len); - if (tx_budget_ms < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV) { + if (tx_budget_ms < MAX_TX_AIRTIME_FOR_EST(est_airtime)) { float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor()); - unsigned long needed = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV - tx_budget_ms; + unsigned long needed = MAX_TX_AIRTIME_FOR_EST(est_airtime) - tx_budget_ms; next_tx_time = futureMillis((unsigned long)(needed / duty_cycle)); } }