rx path fixes, dc.restart wired in to lr chips,

TX timeout magic number is gone
This commit is contained in:
liquidraver
2026-08-14 13:27:54 +02:00
parent c45e7ae774
commit 4473b261ee
11 changed files with 426 additions and 33 deletions
+10
View File
@@ -31,6 +31,16 @@ void LR1110Radio::begin()
LoRaRadioBase::begin();
}
uint32_t LR1110Radio::getDutyCycleTimeoutRestarts() const
{
return lr11xx_get_dc_timeout_restarts(_dev);
}
void LR1110Radio::resetDutyCycleTimeoutRestarts()
{
lr11xx_reset_dc_timeout_restarts(_dev);
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool LR1110Radio::hwConfigure(const struct lora_modem_config &cfg)
+7
View File
@@ -18,6 +18,13 @@ public:
void begin() override;
/* Duty-cycle false-preamble re-arm count, behind `get dc.restarts`.
* Without these the base class's stub answers 0 forever, and the one
* instrument for tuning the RX window reads clean on a node that is
* re-arming constantly. */
uint32_t getDutyCycleTimeoutRestarts() const override;
void resetDutyCycleTimeoutRestarts() override;
protected:
/* Hardware primitives */
bool hwConfigure(const struct lora_modem_config &cfg) override;
+10
View File
@@ -51,6 +51,16 @@ void LR2021Radio::resetStats()
lr20xx_reset_freq_offset(_dev);
}
uint32_t LR2021Radio::getDutyCycleTimeoutRestarts() const
{
return lr20xx_get_dc_timeout_restarts(_dev);
}
void LR2021Radio::resetDutyCycleTimeoutRestarts()
{
lr20xx_reset_dc_timeout_restarts(_dev);
}
int LR2021Radio::formatFreqErrorStatus(char *buf, int cap)
{
struct lr20xx_freq_offset_stats st;
+7
View File
@@ -18,6 +18,13 @@ public:
void begin() override;
/* Duty-cycle false-preamble re-arm count, behind `get dc.restarts`.
* Without these the base class's stub answers 0 forever, and the one
* instrument for tuning the RX window reads clean on a node that is
* re-arming constantly. */
uint32_t getDutyCycleTimeoutRestarts() const override;
void resetDutyCycleTimeoutRestarts() override;
/* LoRa side detectors — LR2021-only multi-SF receive. */
bool configSideDetectors(const uint8_t *sfs, uint8_t num) override;
+7 -1
View File
@@ -6,6 +6,7 @@
#include "LoRaRadioBase.h"
#include "radio_common.h"
#include <mesh/LoRaConfig.h>
#include <mesh/MeshCore.h> /* MAX_TRANS_UNIT */
#include <zephyr/kernel.h>
#include <zephyr/random/random.h>
#include <string.h>
@@ -94,7 +95,12 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
* it from this thread costs no SPI and cannot race the radio. */
uint32_t LoRaRadioBase::txWaitBudgetMs() const
{
uint32_t air = lora_airtime(_dev, _tx_len ? _tx_len : 255);
/* _tx_len is published by startSendRaw() before it releases
* _tx_start_sem, so by the time this thread runs it is always the length
* of the transmit in flight — the fallback is defensive only, and uses
* the protocol maximum rather than a literal so it tracks MAX_TRANS_UNIT
* if the FIFO bound ever moves. */
uint32_t air = lora_airtime(_dev, _tx_len ? _tx_len : MAX_TRANS_UNIT);
/* All four drivers behind this class implement .airtime (native sx126x,
* lr11xx, lr20xx, loramac-node sx127x), and lora_airtime() dereferences
@@ -99,19 +99,33 @@ struct lr11xx_data {
uint32_t dc_rx_ms;
uint32_t dc_sleep_ms;
/* Duty-cycle re-arms triggered by a timeout, i.e. the false-preamble
* case: UM §7.2.6 restarts the window timer with 2*RxPeriod +
* SleepPeriod on preamble detection, and when that expires with no
* packet the chip leaves the loop and the host puts it back. A climbing
* rate means the window is catching noise rather than packets. Exposed
* as `get dc.restarts`, which reported a hardcoded 0 on this radio until
* nothing counted them. */
atomic_t dc_timeout_restarts;
/* CAD state */
lora_cad_cb cad_cb;
void *cad_user_data;
struct k_sem cad_sem;
int cad_result;
/* No cad_active flag: there was one, written at four sites and read at
* none. The SX126x driver's copy IS read (sx126x_handle_irq_timeout()
* returns early on it), so this one was inherited without the read that
* gave it a purpose. Not needed here: the TIMEOUT branch below is only
* reachable during a CAD when the chip took the CAD->TX exit, and that
* path sets tx_active first. Exclusion between CADs comes from both
* entry points (LBT from startSendRaw, probes from cadMaintenance)
* running on the mesh loop thread. */
* none. The SX126x driver's copy IS read sx126x_handle_irq_timeout()
* returns early on it so this was inherited without the read that gave
* it a purpose.
*
* It is not needed here, and the reason is the locking model, not luck:
* sx126x_irq_work_handler() takes no lock at all, so there its timeout
* path genuinely races a CAD being set up on the mesh thread and the
* flag is the only thing preventing a teardown. This handler holds
* spi_mutex across its whole body, and every CAD entry point holds that
* same mutex across SetStandby -> clear_irq_status(ALL) -> SetCad — so
* the two cannot interleave, and any TIMEOUT latched before the CAD is
* discarded by the clear inside lr11xx_do_cad(). */
/* Adaptive-CAD: signed offset applied to the per-SF base detPeak on
* every LBT CAD; cad_probe_peak overrides for one calibration probe. */
int8_t cad_peak_offset;
@@ -448,7 +462,16 @@ static void lr11xx_start_rx(struct lr11xx_data *data,
* Packet params are NOT re-applied here — they persist through SetRx.
* RadioLib (Arduino) also skips re-applying packet params on RX restart.
* Only TX changes pld_len, and TX→RX goes through full start_rx(). */
static void lr11xx_restart_rx(struct lr11xx_data *data)
/* Returns 0 if the receiver is believed to be back on air, <0 if a command the
* re-arm depends on was rejected and the caller should escalate.
*
* "Believed" on the duty-cycle path: verifying there would mean a GetStatus
* after SetRxDutyCycle, and that NSS edge terminates the very cycle being
* checked (UM §7.2.6) — the probe would cause the fault it looks for. The check
* sits on the SetStandby instead, which is the command that actually fails on a
* wedged chip and is safe to poll because the standby has just ended any live
* cycle. */
static int lr11xx_restart_rx(struct lr11xx_data *data)
{
void *ctx = &data->hal_ctx;
@@ -461,17 +484,66 @@ static void lr11xx_restart_rx(struct lr11xx_data *data)
* or a false-preamble 2*rx+sleep timeout dropped the chip to
* standby). Re-apply boost — same warm-start caution as
* start_rx. */
/* SetStandby first. The cycle is not necessarily over when we
* get here: UM §7.2.6 terminates the loop on exactly three
* things — a packet detected, a host SetStandby, or an NSS wake
* from sleep — and a LoRa header error is none of them (it
* raises no RX_DONE), so the chip is still cycling on that path.
* Re-arming then means driving an NSS edge into a live cycle,
* and the manual is explicit about that case: the device "is
* woken up from Sleep mode with a falling edge of NSS. In that
* case, the user should send the SetStandby() command to avoid
* race conditions". LR2021 DS §6.3.8 says the same in nearly
* the same words, and there the missing standby was observed on
* hardware as a latched CMD_ERROR with DIO1 stuck high, five
* strikes to a reset, ~88 ms deaf per noise header error. Free
* when the chip is already in standby. */
lr11xx_system_set_standby(ctx, LR11XX_SYSTEM_STANDBY_CFG_RC);
/* Safe to poll: the standby above ended any live cycle, so this
* NSS edge has nothing left to disturb. A chip that will not
* even take a SetStandby will not take a duty cycle either. */
{
lr11xx_system_stat1_t s1 = { 0 };
if (lr11xx_system_get_status(ctx, &s1, NULL, NULL) ==
LR11XX_STATUS_OK &&
s1.command_status != LR11XX_SYSTEM_CMD_STATUS_OK &&
s1.command_status != LR11XX_SYSTEM_CMD_STATUS_DATA) {
LOG_WRN("restart_rx: standby rejected (cmd=%d) "
"before duty-cycle re-arm",
(int)s1.command_status);
return -EIO;
}
}
if (data->rx_boost_enabled) {
lr11xx_radio_cfg_rx_boosted(ctx, true);
}
lr11xx_radio_set_rx_duty_cycle(ctx, data->dc_rx_ms,
data->dc_sleep_ms, LR11XX_RADIO_RX_DUTY_CYCLE_MODE_RX);
} else {
lr11xx_radio_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF);
/* RX boost persists through SetRx — no re-apply needed. */
data->in_rx_mode = true;
return 0;
}
lr11xx_radio_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF);
/* RX boost persists through SetRx — no re-apply needed. */
data->in_rx_mode = true;
/* No duty cycle armed, so polling is free of the NSS hazard. */
{
lr11xx_system_stat2_t s2 = { 0 };
if (lr11xx_system_get_status(ctx, NULL, &s2, NULL) ==
LR11XX_STATUS_OK &&
s2.chip_mode != LR11XX_SYSTEM_CHIP_MODE_RX) {
LOG_WRN("restart_rx: chip is in mode %d, not RX",
(int)s2.chip_mode);
return -EIO;
}
}
return 0;
}
/* ── DIO1 IRQ handler (work queue, thread context) ──────────────────── */
@@ -652,7 +724,24 @@ static void lr11xx_dio1_work_handler(struct k_work *work)
lr11xx_start_rx(data, cfg);
rx_restarted = true;
} else {
lr11xx_restart_rx(data);
/* Under a duty cycle this is the false-preamble case:
* the 2*RxPeriod + SleepPeriod window the preamble
* detect restarted (UM §7.2.6) expired with no packet,
* so the chip left the loop. Counting it is what makes
* `get dc.restarts` mean something on this radio. */
if (data->rx_duty_cycle_enabled) {
atomic_inc(&data->dc_timeout_restarts);
}
if (lr11xx_restart_rx(data) < 0) {
/* Escalate to the full path: forces standby,
* reprograms the modem and re-issues SetRx from
* scratch. The LR11xx counterpart of the
* SX126x's retry-from-sleep. If that fails too
* the stuck-DIO1 counter still reaches its
* hardware reset. */
LOG_WRN("Timeout: light re-arm failed — full RX restart");
lr11xx_start_rx(data, cfg);
}
rx_restarted = true;
}
}
@@ -921,12 +1010,40 @@ static int lr11xx_lora_send_async(const struct device *dev,
lr11xx_reset_rx_busy_signals(data);
lr11xx_hal_enable_dio1_irq(&data->hal_ctx);
/* Store signal and start TX. 10 s timeout: worst legal preset
* (SF12 / BW62.5, max payload) exceeds 5 s airtime — a shorter
* timeout would abort mid-transmission. Matches the SX126x driver. */
/* Store signal and start TX.
*
* Chip-side Tx safeguard, scaled from airtime. UM §7.2.3: "If the RTC
* event fires before the end of transmission, it will trigger a TIMEOUT
* IRQ, and stop the device transmission" — so this must exceed real
* airtime. The fixed 10 s it replaces carried the right reasoning with
* the arithmetic never done: the comment said the worst preset "exceeds
* 5 s", but SF12/BW62.5 at 255 bytes CR 4/8 is 28.6 s, so 10 s truncated
* every packet from 76 B up there, and from 31 B at SF12/BW31.25.
*
* Floored at the previous 10000 so nothing that works today tightens.
* Programmed in RTC steps rather than through the millisecond wrapper:
* lr11xx_radio_convert_time_in_ms_to_rtc_step() computes ms * 32768 in
* uint32 and overflows above 131071 ms, which SF12/BW7.81 at 255 B
* (~229 s) reaches. Saturating at the 24-bit field is 512 s. */
data->tx_signal = async;
data->tx_active = true;
lr11xx_radio_set_tx(ctx, 10000);
{
uint32_t air_ms = lr11xx_lora_airtime(dev, data_len);
uint32_t tmo_ms = air_ms + (air_ms / 4U) + 500U;
uint64_t steps;
if (tmo_ms < 10000U) {
tmo_ms = 10000U;
}
steps = ((uint64_t)tmo_ms * 32768U) / 1000U;
if (steps > 0x00FFFFFFU) {
steps = 0x00FFFFFFU;
}
LOG_DBG("SET_TX: airtime=%u ms, timeout=%u ms (%u steps)",
air_ms, tmo_ms, (uint32_t)steps);
lr11xx_radio_set_tx_with_timeout_in_rtc_step(ctx,
(uint32_t)steps);
}
k_mutex_unlock(&data->spi_mutex);
return 0;
@@ -1241,6 +1358,20 @@ bool lr11xx_is_receiving(const struct device *dev)
return false;
}
uint32_t lr11xx_get_dc_timeout_restarts(const struct device *dev)
{
struct lr11xx_data *data = dev->data;
return (uint32_t)atomic_get(&data->dc_timeout_restarts);
}
void lr11xx_reset_dc_timeout_restarts(const struct device *dev)
{
struct lr11xx_data *data = dev->data;
atomic_set(&data->dc_timeout_restarts, 0);
}
uint32_t lr11xx_get_wakeup_time_us(const struct device *dev)
{
const struct lr11xx_config *cfg = dev->config;
@@ -63,6 +63,28 @@ void lr11xx_set_rx_boost(const struct device *dev, bool enable);
*/
uint32_t lr11xx_get_wakeup_time_us(const struct device *dev);
/**
* @brief Duty-cycle re-arms caused by a timeout since boot or reset
*
* Counts the false-preamble case: UM §7.2.6 restarts the window timer with
* 2*RxPeriod + SleepPeriod on preamble detection, and when that expires with no
* packet the chip leaves the loop and the driver puts it back. A climbing rate
* means the RX window is catching noise rather than packets. Backs
* `get dc.restarts`, which reported a hardcoded 0 on this radio before anything
* counted them.
*
* @param dev LoRa device
* @return re-arm count
*/
uint32_t lr11xx_get_dc_timeout_restarts(const struct device *dev);
/**
* @brief Clear the duty-cycle timeout re-arm counter
*
* @param dev LoRa device
*/
void lr11xx_reset_dc_timeout_restarts(const struct device *dev);
/**
* @brief Get a random number from the radio
*
@@ -95,6 +95,15 @@ struct lr20xx_data {
bool rx_boost_enabled;
bool rx_boost_applied;
/* Duty-cycle re-arms triggered by a timeout, i.e. the false-preamble
* case: the preamble-restarted rx_max_time + cycle_time window expired
* with no packet, so the chip left the loop and the host put it back.
* The rate is the instrument for duty-cycle tuning a climbing count
* means the window is catching noise, not packets. Exposed as
* `get dc.restarts`, which reported a hardcoded 0 on this radio until
* nothing counted them. */
atomic_t dc_timeout_restarts;
/* Stored duty-cycle timing from recv_duty_cycle() — re-arm paths
* must reuse these exact values, never recompute: the window sizing
* (detection budget, datasheet completion rule, wake transition) is
@@ -108,11 +117,18 @@ struct lr20xx_data {
struct k_sem cad_sem;
int cad_result; /* 0=free, 1=busy, <0=error */
/* No cad_active flag: there was one, written at five sites and read at
* none. It read like a concurrent-CAD guard and guarded nothing the
* real exclusion is that every CAD entry point (LBT from startSendRaw,
* probes from cadMaintenance) runs on the mesh loop thread, so two CADs
* cannot overlap by construction. A flag nothing tests is worse than no
* flag: it invites the next reader to assume the invariant is enforced. */
* none. The SX126x driver's copy IS read sx126x_handle_irq_timeout()
* returns early on it so this was inherited without the read that gave
* it a purpose.
*
* It is not needed here, and the reason is the locking model, not luck:
* sx126x_irq_work_handler() takes no lock at all, so there its timeout
* path genuinely races a CAD being set up on the mesh thread and the
* flag is the only thing preventing a teardown. This handler holds
* spi_mutex across its whole body, and every CAD entry point holds that
* same mutex across SetStandby -> clear_irq_status(ALL) -> SetCad so
* the two cannot interleave, and any TIMEOUT latched before the CAD is
* discarded by the clear inside lr20xx_do_cad(). */
/* Adaptive-CAD: signed offset applied to the per-SF base detPeak on
* every LBT CAD; cad_probe_peak overrides for one calibration probe. */
int8_t cad_peak_offset;
@@ -1075,7 +1091,17 @@ static void lr20xx_start_rx(struct lr20xx_data *data,
/* ── Lightweight RX restart (no modem reconfig) ─────────────────────── */
static void lr20xx_restart_rx(struct lr20xx_data *data)
/* Returns 0 if the receiver is believed to be back on air, <0 if a command the
* re-arm depends on was rejected and the caller should escalate.
*
* "Believed" is deliberate on the duty-cycle path: verification there would mean
* a GetStatus after SetRxDutyCycle, and an NSS falling edge terminates the cycle
* being checked (DS §6.3.8) the probe would cause the fault it looks for. So
* the check is placed on the SetStandby that precedes the re-arm, which is the
* command that actually fails when the chip is wedged, and is safe to poll
* because the standby has just ended any live cycle. Continuous RX has no such
* constraint and is checked after SetRx. */
static int lr20xx_restart_rx(struct lr20xx_data *data)
{
void *ctx = &data->hal_ctx;
@@ -1123,26 +1149,62 @@ static void lr20xx_restart_rx(struct lr20xx_data *data)
if (lr20xx_system_get_status(ctx, NULL, &s2, NULL) == LR20XX_STATUS_OK &&
s2.chip_mode == LR20XX_SYSTEM_CHIP_MODE_RX) {
data->in_rx_mode = true;
return;
return 0;
}
}
if (data->rx_duty_cycle_enabled) {
/* SetStandby first: a header/CRC error does not terminate the
* duty-cycle loop (DS 6.3.8 ends it on packet reception), and
* re-arming a live cycle is refused the refusal latches
* CMD_ERROR and holds DIO1 high. Near-free when already in
* STDBY_RC. See ARCHITECTURE.md 5.5.1. */
lr20xx_system_stat1_t s1 = { 0 };
/* SetStandby first. A header error does not terminate the
* duty-cycle loop DS §6.3.8 lists exactly three terminators
* (packet reception, a host SetStandby, an NSS wake from sleep)
* and a header error is none of them so the chip can still be
* cycling here. Re-arming then drives an NSS edge into a live
* cycle, and the datasheet is explicit for that case: on a wake
* "by the device with a falling edge of NSS. A SetStandby
* command should also be sent, to avoid the race conditions".
* Observed on hardware without it: latched CMD_ERROR, DIO1 held
* high, five strikes to a reset, ~88 ms deaf per noise header
* error. Near-free when already in STDBY_RC.
* See ARCHITECTURE.md 5.5.1. */
lr20xx_system_set_standby_mode(ctx,
LR20XX_SYSTEM_STANDBY_MODE_RC);
/* Safe to poll: the standby above ended any live cycle, so this
* NSS edge has nothing left to disturb. If the chip would not
* even take a SetStandby, arming a duty cycle on top of it is
* pointless say so and let the caller escalate. */
if (lr20xx_system_get_status(ctx, &s1, NULL, NULL) ==
LR20XX_STATUS_OK &&
s1.command_status != 2 && s1.command_status != 3) {
LOG_WRN("restart_rx: standby rejected (cmd=%d) before "
"duty-cycle re-arm", s1.command_status);
return -EIO;
}
lr20xx_apply_rx_duty_cycle(data);
} else {
lr20xx_radio_common_set_rx_with_timeout_in_rtc_step(
ctx, 0xFFFFFF);
data->in_rx_mode = true;
return 0;
}
lr20xx_radio_common_set_rx_with_timeout_in_rtc_step(ctx, 0xFFFFFF);
CHECK_CMD(ctx, "restart_rx set_rx");
data->in_rx_mode = true;
/* No duty cycle armed, so polling is free of the NSS hazard. */
{
lr20xx_system_stat2_t s2 = { 0 };
if (lr20xx_system_get_status(ctx, NULL, &s2, NULL) ==
LR20XX_STATUS_OK &&
s2.chip_mode != LR20XX_SYSTEM_CHIP_MODE_RX) {
LOG_WRN("restart_rx: chip is in mode %d, not RX",
s2.chip_mode);
return -EIO;
}
}
return 0;
}
/* Is the receiver still live? Conservative: true (leave it alone) whenever it
@@ -1474,8 +1536,26 @@ static void lr20xx_dio1_work_handler(struct k_work *work)
lr20xx_start_rx(data, cfg);
rx_restarted = true;
} else {
/* Under a duty cycle this is the false-preamble case:
* the preamble-restarted rx_max_time + cycle_time window
* expired with no packet, so the chip left the loop.
* Counting it is what makes `get dc.restarts` mean
* something on this radio. */
if (data->rx_duty_cycle_enabled) {
atomic_inc(&data->dc_timeout_restarts);
}
LOG_DBG("Timeout IRQ — restarting RX");
lr20xx_restart_rx(data);
if (lr20xx_restart_rx(data) < 0) {
/* The light re-arm could not get the chip back on
* air. Escalate to the full path, which forces
* standby, reprograms the modem and re-issues
* SetRx from scratch the LR20xx counterpart of
* the SX126x's retry-from-sleep. If that fails
* too the chip is wedged, and the stuck-DIO1
* counter below still reaches its reset. */
LOG_WRN("Timeout: light re-arm failed — full RX restart");
lr20xx_start_rx(data, cfg);
}
rx_restarted = true;
}
}
@@ -2294,6 +2374,20 @@ void lr20xx_reset_freq_offset(const struct device *dev)
k_mutex_unlock(&data->spi_mutex);
}
uint32_t lr20xx_get_dc_timeout_restarts(const struct device *dev)
{
struct lr20xx_data *data = dev->data;
return (uint32_t)atomic_get(&data->dc_timeout_restarts);
}
void lr20xx_reset_dc_timeout_restarts(const struct device *dev)
{
struct lr20xx_data *data = dev->data;
atomic_set(&data->dc_timeout_restarts, 0);
}
uint32_t lr20xx_get_wakeup_time_us(const struct device *dev)
{
const struct lr20xx_config *cfg = dev->config;
@@ -65,6 +65,28 @@ uint32_t lr20xx_get_freq_offset(const struct device *dev,
*/
void lr20xx_reset_freq_offset(const struct device *dev);
/**
* @brief Duty-cycle re-arms caused by a timeout since boot or reset
*
* Counts the false-preamble case: the preamble-restarted rx_max_time +
* cycle_time window expired with no packet, the chip left the duty-cycle loop,
* and the driver put it back. A climbing rate means the RX window is catching
* noise rather than packets, which is the signal for retuning it. Backs
* `get dc.restarts`, which reported a hardcoded 0 on this radio before anything
* counted them.
*
* @param dev LoRa device
* @return re-arm count
*/
uint32_t lr20xx_get_dc_timeout_restarts(const struct device *dev);
/**
* @brief Clear the duty-cycle timeout re-arm counter
*
* @param dev LoRa device
*/
void lr20xx_reset_dc_timeout_restarts(const struct device *dev);
/**
* @brief Radio deaf time for one duty-cycle wake transition, in microseconds
*
@@ -0,0 +1,70 @@
Scale the chip-side Tx timeout from airtime instead of a fixed 10 s.
sx126x_lora_send_async() programmed SetTx with a constant 10000 ms. DS section
13.1.5 is explicit that when the Tx timer fires the transmission is stopped, so
that constant is not a safeguard on slow presets -- it is a truncation.
Airtime of a 255-byte packet at CR 4/8 reaches 17.7 s at SF10/BW31.25, 15.9 s at
SF11/BW62.5 and 28.6 s at SF12/BW62.5. Against a 10 s ceiling those presets lost
every packet from 136 B, 154 B and 76 B up respectively, cut mid-transmission, with
nothing in the log connecting the loss to this line: TX_DONE never arrives, the
timeout handler re-arms RX, and the host wait is what eventually reports it.
Scale as airtime + 25% + 500 ms, floored at the previous 10000 so no preset that
works today gets a tighter deadline. Clamp to 262143 ms: sx126x_set_tx() converts
via SX126X_MS_TO_TIMEOUT (64 steps/ms) and sys_put_be24(), which truncates
silently above that. The ceiling is 262 s against a worst reachable airtime of
~229 s (SF12/BW7.81 at 255 B), so it never binds on a real packet.
Kept separate from 0003 because that patch cannot be regenerated from the tree,
and the line being changed is upstream context within it rather than an added
line -- same reason 0011, 0012 and 0013 are separate.
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
--- a/drivers/lora/native/sx126x/sx126x.c
+++ b/drivers/lora/native/sx126x/sx126x.c
@@ -1432,6 +1432,7 @@
static int sx126x_lora_cad(const struct device *dev, k_timeout_t timeout);
static uint32_t sx126x_cad_timeout_ms(struct sx126x_data *data);
+static uint32_t sx126x_lora_airtime(const struct device *dev, uint32_t data_len);
static int sx126x_lora_send_async(const struct device *dev,
uint8_t *data_buf, uint32_t data_len,
@@ -1614,8 +1615,34 @@
}
#endif /* CONFIG_LORA_SX126X_NATIVE_STANDALONE */
- /* Start transmission with 10 second timeout */
- ret = sx126x_set_tx(dev, 10000);
+ /* Chip-side Tx safeguard, scaled from airtime.
+ *
+ * DS §13.1.5: when the Tx timer fires the transmission is stopped, so
+ * this has to exceed real airtime — and a fixed 10 s cannot. A 255-byte
+ * packet at CR 4/8 is 17.7 s at SF10/BW31.25 and 28.6 s at SF12/BW62.5;
+ * on those presets the old constant truncated every packet from 136 B
+ * and 76 B up respectively, mid-transmission and with nothing in the log
+ * tying the loss to this line.
+ *
+ * Floored at the previous 10000 so no preset that works today gets a
+ * tighter deadline, and clamped to the 24-bit field: sx126x_set_tx()
+ * converts with SX126X_MS_TO_TIMEOUT (64 steps/ms at 15.625 us) and then
+ * sys_put_be24(), which would silently truncate above 262143 ms. That
+ * ceiling is 262 s against a worst reachable airtime of ~229 s
+ * (SF12/BW7.81, 255 B), so the clamp never binds on a real packet. */
+ {
+ uint32_t air_ms = sx126x_lora_airtime(dev, data_len);
+ uint32_t tmo_ms = air_ms + (air_ms / 4U) + 500U;
+
+ if (tmo_ms < 10000U) {
+ tmo_ms = 10000U;
+ }
+ if (tmo_ms > 262143U) {
+ tmo_ms = 262143U;
+ }
+ LOG_DBG("SET_TX: airtime=%u ms, timeout=%u ms", air_ms, tmo_ms);
+ ret = sx126x_set_tx(dev, tmo_ms);
+ }
if (ret < 0) {
goto out_error;
}
+14
View File
@@ -762,9 +762,23 @@ public:
void clearStats() override {
lora_radio.resetStats();
lora_radio.resetDutyCycleTimeoutRestarts();
companion_mesh.resetStats();
}
/* Duty-cycle false-preamble re-arm count. Without these the
* CommonCLICallbacks default answers 0 forever, so `get dc.restarts`
* read clean on every companion regardless of what the radio was doing
* and the companion is the role the duty cycle actually runs in.
* Mirrors RepeaterMesh::getDutyCycleTimeoutRestarts(). */
uint32_t getDutyCycleTimeoutRestarts() const override {
return lora_radio.getDutyCycleTimeoutRestarts();
}
void resetDutyCycleTimeoutRestarts() override {
lora_radio.resetDutyCycleTimeoutRestarts();
}
/* Temp radio params — deferred; stub for now. */
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr,
int timeout_mins) override {