From 10547ecb4507fe56a96533f5ae7a5b9a3e063ca4 Mon Sep 17 00:00:00 2001 From: Charles Date: Fri, 6 Mar 2026 10:35:16 +0100 Subject: [PATCH 1/2] fix(lorawan): prevent permanent WOULD_BLOCK when duty-cycle backoff_time is zero In schedule_tx(), when set_next_channel() returns DUTYCYCLE_RESTRICTED with backoff_time == 0 (which can occur due to sub-millisecond rounding when remaining duty-cycle time is nearly expired), the original code did not start the backoff timer and did not set _can_cancel_tx. However, the caller (process_scheduling_state) still set tx_ongoing=true and transitioned to DEVICE_STATE_SENDING. With no timer scheduled to retry, on_backoff_timer_expiry() never fires, handle_scheduling_failure() is never called, and reset_ongoing_tx() is never reached. The MAC is permanently stuck with tx_ongoing=true, causing all subsequent lorawan.send() calls to return LORAWAN_STATUS_WOULD_BLOCK (-1001) forever. Additionally, stop_sending() cannot recover the state because _can_cancel_tx is false, making clear_tx_pipe() return BUSY. Fix: enforce a minimum backoff of 1ms so the timer always fires regardless of how small the computed remaining time is. Co-Authored-By: Claude Sonnet 4.6 --- .../lorawan/lorastack/mac/LoRaMac.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/connectivity/lorawan/lorastack/mac/LoRaMac.cpp b/connectivity/lorawan/lorastack/mac/LoRaMac.cpp index fca30bad90b..adb7dd21383 100644 --- a/connectivity/lorawan/lorastack/mac/LoRaMac.cpp +++ b/connectivity/lorawan/lorastack/mac/LoRaMac.cpp @@ -1134,14 +1134,19 @@ lorawan_status_t LoRaMac::schedule_tx() _mcps_confirmation.status = LORAMAC_EVENT_INFO_STATUS_ERROR; return status; case LORAWAN_STATUS_DUTYCYCLE_RESTRICTED: - if (backoff_time != 0) { - tr_debug("DC enforced: Transmitting in %lu ms", backoff_time); - _can_cancel_tx = true; - if (_device_class != CLASS_C) { - _lora_phy->put_radio_to_sleep(); - } - _lora_time.start(_params.timers.backoff_timer, backoff_time); + // Enforce a minimum backoff of 1ms so the timer always fires. + // If backoff_time is 0, no timer would be started, leaving + // tx_ongoing=true permanently and all future sends returning + // LORAWAN_STATUS_WOULD_BLOCK with no recovery path. + if (backoff_time == 0) { + backoff_time = 1; + } + tr_debug("DC enforced: Transmitting in %lu ms", backoff_time); + _can_cancel_tx = true; + if (_device_class != CLASS_C) { + _lora_phy->put_radio_to_sleep(); } + _lora_time.start(_params.timers.backoff_timer, backoff_time); return LORAWAN_STATUS_OK; default: break; From 7325f3be791722ee7d77d12db0d037a8d6387579 Mon Sep 17 00:00:00 2001 From: Charles Date: Fri, 6 Mar 2026 11:16:07 +0100 Subject: [PATCH 2/2] fix(lorawan): fix two more tx_ongoing stuck cases Bug 1 - LoRaMac::disconnect() does not clear tx_ongoing: All timers (backoff, RX windows, ACK timeout) are stopped in disconnect(), which prevents the state machine from ever calling reset_ongoing_tx(). If a TX was in-flight at disconnect time, tx_ongoing remains true. After reconnect, _lw_session.active becomes true again but tx_ongoing is still true, so every subsequent lorawan.send() returns LORAWAN_STATUS_WOULD_BLOCK (-1001) permanently. Fix: call reset_ongoing_tx(true) at end of disconnect(). Bug 2 - QoS nb_trans retry leaves tx_ongoing stuck on re-send failure: When the network server configures nb_trans > LORAWAN_DEFAULT_QOS, post_process_tx_no_reception() queues a new state_controller(SCHEDULING) call via _queue->call() and returns early, leaving tx_ongoing=true from the first TX. If the queued scheduling fires but send_ongoing_tx() fails with a direct error (e.g. LORAWAN_STATUS_NO_CHANNEL_FOUND), process_scheduling_state silently ignores the failure because the _queue->call() return value is discarded, tx_ongoing stays true, and there is no path to reset_ongoing_tx(). Fix: in process_scheduling_state(), detect the case where send_ongoing_tx() failed while tx_ongoing was already true and explicitly clean up the state. Co-Authored-By: Claude Sonnet 4.6 --- connectivity/lorawan/lorastack/mac/LoRaMac.cpp | 7 +++++++ connectivity/lorawan/source/LoRaWANStack.cpp | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/connectivity/lorawan/lorastack/mac/LoRaMac.cpp b/connectivity/lorawan/lorastack/mac/LoRaMac.cpp index adb7dd21383..6b8f7b6f1c0 100644 --- a/connectivity/lorawan/lorastack/mac/LoRaMac.cpp +++ b/connectivity/lorawan/lorastack/mac/LoRaMac.cpp @@ -1877,6 +1877,13 @@ void LoRaMac::disconnect() reset_mcps_confirmation(); reset_mlme_confirmation(); reset_mcps_indication(); + + // Clear any in-progress TX so that reconnecting after disconnect does not + // permanently return LORAWAN_STATUS_WOULD_BLOCK. All timers that would + // normally drive the state machine to call reset_ongoing_tx() (backoff, + // RX windows, ACK timeout) have already been stopped above, so without + // this explicit reset the tx_ongoing flag would be stuck at true. + reset_ongoing_tx(true); } uint8_t LoRaMac::get_max_possible_tx_size(uint8_t fopts_len) diff --git a/connectivity/lorawan/source/LoRaWANStack.cpp b/connectivity/lorawan/source/LoRaWANStack.cpp index 22d45454739..c44f7f65fd6 100644 --- a/connectivity/lorawan/source/LoRaWANStack.cpp +++ b/connectivity/lorawan/source/LoRaWANStack.cpp @@ -1170,6 +1170,14 @@ void LoRaWANStack::process_scheduling_state(lorawan_status_t &op_status) _ctrl_flags &= ~TX_DONE_FLAG; _loramac.set_tx_ongoing(true); _device_current_state = DEVICE_STATE_SENDING; + } else if (_loramac.tx_ongoing()) { + // tx_ongoing was already true from a previous successful send (e.g. a + // QoS nb_trans retry queued via post_process_tx_no_reception). The + // re-send failed with a non-recoverable error and the return value of + // the queued _queue->call() is ignored, so no failure handler would + // otherwise run. Explicitly clean up so tx_ongoing does not get stuck. + _loramac.set_tx_ongoing(false); + _loramac.reset_ongoing_tx(); } }