diff --git a/zephcore/Repeater_CLI_commands.md b/zephcore/Repeater_CLI_commands.md index 87753ff..55108a4 100644 --- a/zephcore/Repeater_CLI_commands.md +++ b/zephcore/Repeater_CLI_commands.md @@ -194,6 +194,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot. | `get loop.detect` | Loop detection level: `off`, `minimal`, `moderate`, or `strict` | | `get radio.rxgain` | RX gain boost: `0` or `1` | | `get rxduty` | RX duty cycle mode: `0` or `1` | +| `get dc.restarts` | Duty-cycle preamble false-positive re-arm counter. High values mean the preamble detector is tripping on noise/interference without real packets arriving — inflates RX-on time and drains battery. Reset by `clear stats`. | | `get adc.multiplier` | Battery voltage ADC calibration multiplier | | `get bootloader.ver` | Bootloader version string | | `get public.key` | *(USB only)* Node's public key as hex | diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index 2d9cca9..e17974b 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -71,6 +71,16 @@ public: void setRxBoost(bool enable); bool isRxBoostEnabled() const { return _rx_boost_enabled; } + /* Duty-cycle preamble false-positive counter. + * Incremented by the driver whenever RX_TX_TIMEOUT fires in + * duty-cycle mode and the chip is silently re-armed. High + * values indicate a noisy RF environment or too-loose preamble + * detection — each event extends real RX time past the nominal + * duty cycle, inflating current draw. + * Default returns 0 on radios that don't support the stat. */ + virtual uint32_t getDutyCycleTimeoutRestarts() const { return 0; } + virtual void resetDutyCycleTimeoutRestarts() {} + /* Adaptive Power Control */ void setTxPowerReduction(int8_t reduction_db) override { _tx_power_reduction_db = reduction_db; } int8_t getTxPowerReduction() const override { return _tx_power_reduction_db; } diff --git a/zephcore/adapters/radio/SX126xRadio.cpp b/zephcore/adapters/radio/SX126xRadio.cpp index 5ab404e..5aa56c9 100644 --- a/zephcore/adapters/radio/SX126xRadio.cpp +++ b/zephcore/adapters/radio/SX126xRadio.cpp @@ -1,88 +1,98 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SX126x hardware hooks for LoRaRadioBase — native Zephyr driver. - */ - -#include "SX126xRadio.h" -#include - -/* Native SX126x driver extension API */ -extern "C" { -#include "sx126x_ext.h" -} - -#include -LOG_MODULE_REGISTER(sx126x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); - -namespace mesh { - -K_THREAD_STACK_DEFINE(sx126x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE); - -SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board, - NodePrefs *prefs) - : LoRaRadioBase(lora_dev, board, prefs) -{ -} - -void SX126xRadio::begin() -{ - startTxThread(sx126x_tx_wait_stack, - K_THREAD_STACK_SIZEOF(sx126x_tx_wait_stack)); - LoRaRadioBase::begin(); - -#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH) - /* Apply undocumented register 0x8B5 RX improvement (MeshCore PR#1398). - * Must run after lora_config() has been called (via startReceive above). */ - sx126x_apply_heltec_reg_patch(_dev); - LOG_INF("Applied Heltec reg 0x8B5 RX patch"); -#endif -} - -/* ── Hardware primitives ──────────────────────────────────────────────── */ - -void SX126xRadio::hwConfigure(const struct lora_modem_config &cfg) -{ - int ret = lora_config(_dev, const_cast(&cfg)); - if (ret < 0) { - LOG_ERR("lora_config failed: %d", ret); - } -} - -void SX126xRadio::hwCancelReceive() -{ - lora_recv_async(_dev, NULL, NULL); -} - -int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) -{ - return lora_send_async(_dev, buf, len, sig); -} - -int16_t SX126xRadio::hwGetCurrentRSSI() -{ - return sx126x_get_rssi_inst(_dev); -} - -bool SX126xRadio::hwIsPreambleDetected() -{ - return sx126x_is_receiving(_dev); -} - -void SX126xRadio::hwSetRxBoost(bool enable) -{ - sx126x_set_rx_boost(_dev, enable); -} - -void SX126xRadio::hwResetAGC() -{ - /* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX settings */ - sx126x_reset_agc(_dev); -} - -bool SX126xRadio::hwIsChipBusy() -{ - return sx126x_is_chip_busy(_dev); -} - -} /* namespace mesh */ +/* + * SPDX-License-Identifier: Apache-2.0 + * SX126x hardware hooks for LoRaRadioBase — native Zephyr driver. + */ + +#include "SX126xRadio.h" +#include + +/* Native SX126x driver extension API */ +extern "C" { +#include "sx126x_ext.h" +} + +#include +LOG_MODULE_REGISTER(sx126x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL); + +namespace mesh { + +K_THREAD_STACK_DEFINE(sx126x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE); + +SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board, + NodePrefs *prefs) + : LoRaRadioBase(lora_dev, board, prefs) +{ +} + +void SX126xRadio::begin() +{ + startTxThread(sx126x_tx_wait_stack, + K_THREAD_STACK_SIZEOF(sx126x_tx_wait_stack)); + LoRaRadioBase::begin(); + +#if IS_ENABLED(CONFIG_ZEPHCORE_SX126X_HELTEC_REG_PATCH) + /* Apply undocumented register 0x8B5 RX improvement (MeshCore PR#1398). + * Must run after lora_config() has been called (via startReceive above). */ + sx126x_apply_heltec_reg_patch(_dev); + LOG_INF("Applied Heltec reg 0x8B5 RX patch"); +#endif +} + +/* ── Hardware primitives ──────────────────────────────────────────────── */ + +void SX126xRadio::hwConfigure(const struct lora_modem_config &cfg) +{ + int ret = lora_config(_dev, const_cast(&cfg)); + if (ret < 0) { + LOG_ERR("lora_config failed: %d", ret); + } +} + +void SX126xRadio::hwCancelReceive() +{ + lora_recv_async(_dev, NULL, NULL); +} + +int SX126xRadio::hwSendAsync(uint8_t *buf, uint32_t len, + struct k_poll_signal *sig) +{ + return lora_send_async(_dev, buf, len, sig); +} + +int16_t SX126xRadio::hwGetCurrentRSSI() +{ + return sx126x_get_rssi_inst(_dev); +} + +bool SX126xRadio::hwIsPreambleDetected() +{ + return sx126x_is_receiving(_dev); +} + +void SX126xRadio::hwSetRxBoost(bool enable) +{ + sx126x_set_rx_boost(_dev, enable); +} + +void SX126xRadio::hwResetAGC() +{ + /* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX settings */ + sx126x_reset_agc(_dev); +} + +bool SX126xRadio::hwIsChipBusy() +{ + return sx126x_is_chip_busy(_dev); +} + +uint32_t SX126xRadio::getDutyCycleTimeoutRestarts() const +{ + return sx126x_get_dc_timeout_restarts(_dev); +} + +void SX126xRadio::resetDutyCycleTimeoutRestarts() +{ + sx126x_reset_dc_timeout_restarts(_dev); +} + +} /* namespace mesh */ diff --git a/zephcore/adapters/radio/SX126xRadio.h b/zephcore/adapters/radio/SX126xRadio.h index 081b667..fedeb7e 100644 --- a/zephcore/adapters/radio/SX126xRadio.h +++ b/zephcore/adapters/radio/SX126xRadio.h @@ -1,32 +1,36 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using native Zephyr driver - */ - -#pragma once - -#include "LoRaRadioBase.h" - -namespace mesh { - -class SX126xRadio : public LoRaRadioBase { -public: - SX126xRadio(const struct device *lora_dev, MainBoard &board, - NodePrefs *prefs = nullptr); - - void begin() override; - -protected: - /* Hardware primitives */ - void hwConfigure(const struct lora_modem_config &cfg) override; - void hwCancelReceive() override; - int hwSendAsync(uint8_t *buf, uint32_t len, - struct k_poll_signal *sig) override; - int16_t hwGetCurrentRSSI() override; - bool hwIsPreambleDetected() override; - void hwSetRxBoost(bool enable) override; - void hwResetAGC() override; - bool hwIsChipBusy() override; -}; - -} /* namespace mesh */ +/* + * SPDX-License-Identifier: Apache-2.0 + * ZephCore Radio adapter for SX126x (SX1261/SX1262/SX1268) using native Zephyr driver + */ + +#pragma once + +#include "LoRaRadioBase.h" + +namespace mesh { + +class SX126xRadio : public LoRaRadioBase { +public: + SX126xRadio(const struct device *lora_dev, MainBoard &board, + NodePrefs *prefs = nullptr); + + void begin() override; + + /* Duty-cycle preamble false-positive stats (SX126x-specific) */ + uint32_t getDutyCycleTimeoutRestarts() const override; + void resetDutyCycleTimeoutRestarts() override; + +protected: + /* Hardware primitives */ + void hwConfigure(const struct lora_modem_config &cfg) override; + void hwCancelReceive() override; + int hwSendAsync(uint8_t *buf, uint32_t len, + struct k_poll_signal *sig) override; + int16_t hwGetCurrentRSSI() override; + bool hwIsPreambleDetected() override; + void hwSetRxBoost(bool enable) override; + void hwResetAGC() override; + bool hwIsChipBusy() override; +}; + +} /* namespace mesh */ diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 5ad4684..71ff33e 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -1137,10 +1137,19 @@ void RepeaterMesh::saveIdentity(const mesh::LocalIdentity& new_id) { void RepeaterMesh::clearStats() { auto& radio_driver = getRadioDriver(_radio); radio_driver.resetStats(); + radio_driver.resetDutyCycleTimeoutRestarts(); resetStats(); ((mesh::SimpleMeshTables *)getTables())->resetStats(); } +uint32_t RepeaterMesh::getDutyCycleTimeoutRestarts() const { + return getRadioDriver(_radio).getDutyCycleTimeoutRestarts(); +} + +void RepeaterMesh::resetDutyCycleTimeoutRestarts() { + getRadioDriver(_radio).resetDutyCycleTimeoutRestarts(); +} + void RepeaterMesh::handleCommand(uint32_t sender_timestamp, char* command, char* reply) { if (region_load_active) { if (StrHelper::isBlank(command)) { diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index a585ae3..0396bfe 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -234,6 +234,11 @@ public: getContentionTracker().setBackoffMultiplier(m); } + /* Duty-cycle preamble false-positive stats (SX126x only; + * other radios return 0 from the base class). */ + uint32_t getDutyCycleTimeoutRestarts() const override; + void resetDutyCycleTimeoutRestarts() override; + #ifdef CONFIG_ZEPHCORE_APC /* Adaptive Power Control callbacks */ int8_t getAPCReduction() const override { diff --git a/zephcore/boards/common/esp32_common.conf b/zephcore/boards/common/esp32_common.conf index fac9540..4306689 100644 --- a/zephcore/boards/common/esp32_common.conf +++ b/zephcore/boards/common/esp32_common.conf @@ -8,12 +8,23 @@ # DLE managed internally by Espressif blob — no CONFIG_BT_CTLR_DATA_LENGTH_MAX # TX power managed internally by Espressif blob — no CONFIG_BT_CTLR_TX_PWR_* -# Host privacy: required for reliable iOS SMP on Espressif controller (Zephyr #84182-class). -# Android MeshCore may need in-app bond or system pairing when using RPA. +# Host privacy: ESP32-only override. Required for reliable iOS SMP on the +# Espressif controller (Zephyr #84182-class). nRF/MG24 keep privacy off +# because it breaks Android companion "connect from app" (manual pairing +# from system BT settings works, but the app flow silently fails). CONFIG_BT_PRIVACY=y -# BLE thread stacks — ESP32 software BLE controller needs larger stacks -CONFIG_BT_TX_PROCESSOR_STACK_SIZE=2048 +# BLE thread stacks — ESP32 software BLE controller needs larger stacks. +# Zephyr 2026-Q1 added a synchronous sc_store() on the RX WQ in the +# identity-resolved path (commit 2c6c80ddf65) which pushes the LittleFS +# settings write onto the SMP callback chain. Default 2048 overflowed. +CONFIG_BT_TX_PROCESSOR_STACK_SIZE=4096 +CONFIG_BT_RX_STACK_SIZE=4096 + +# Stack-overflow detection — xtensa has no MPU stack guard in Zephyr, so +# use the software sentinel. ARM boards rely on MPU_STACK_GUARD (already +# on via HW_STACK_PROTECTION) which is incompatible with this symbol. +CONFIG_STACK_SENTINEL=y # BLE TX buffers — the Espressif BLE controller blob bursts more aggressively # than the nRF softdevice and will deadlock the system workqueue if ACL TX diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index bfb7d4b..17bea62 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -507,6 +507,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_boost); } else if (memcmp(config, "rxduty", 6) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_duty_cycle); + } else if (memcmp(config, "dc.restarts", 11) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", + (uint32_t)_callbacks->getDutyCycleTimeoutRestarts()); } else { snprintf(reply, CLI_REPLY_SIZE, "??: %s", config); } diff --git a/zephcore/helpers/CommonCLI.h b/zephcore/helpers/CommonCLI.h index 83ca1eb..71ed6c1 100644 --- a/zephcore/helpers/CommonCLI.h +++ b/zephcore/helpers/CommonCLI.h @@ -52,6 +52,10 @@ public: virtual float getFloodDelayFactor() const { return 0.5f; } virtual void setBackoffMultiplier(float m) { (void)m; } + // Duty-cycle preamble false-positive counter (SX126x only) + virtual uint32_t getDutyCycleTimeoutRestarts() const { return 0; } + virtual void resetDutyCycleTimeoutRestarts() {} + // Adaptive Power Control virtual int8_t getAPCReduction() const { return 0; } virtual float getAPCMargin() const { return 0.0f; } diff --git a/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h b/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h index 56abd59..05be0c9 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h +++ b/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h @@ -1,95 +1,116 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SX126x native driver — extension API - * - * Functions extending the standard Zephyr lora_driver_api with - * SX126x-specific features (duty cycle, RX boost, RSSI readout, - * preamble detection). - */ - -#ifndef SX126X_EXT_H -#define SX126X_EXT_H - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Get instantaneous RSSI (for noise floor calibration) - * - * Reads the current RSSI from the radio while in RX mode. - * Uses non-blocking mutex — returns -128 if SPI is busy. - * - * @param dev LoRa device - * @return RSSI in dBm, or -128 on error - */ -int16_t sx126x_get_rssi_inst(const struct device *dev); - -/** - * @brief Check if radio is actively receiving a packet - * - * Checks IRQ status for preamble/header detection. - * Uses non-blocking mutex — returns false if SPI is busy. - * - * @param dev LoRa device - * @return true if preamble or header detected - */ -bool sx126x_is_receiving(const struct device *dev); - -/** - * @brief Enable/disable RX boosted mode - * - * Boosted mode increases LNA gain for +3dB sensitivity at +2mA cost. - * - * @param dev LoRa device - * @param enable true to enable boost - */ -void sx126x_set_rx_boost(const struct device *dev, bool enable); - -/** - * @brief Check if the radio chip is busy (cannot accept SPI commands) - * - * Reads the BUSY GPIO pin directly — no SPI, no blocking. - * Returns true when the chip is in its duty-cycle sleep phase. - * Safe to call at any time. - * - * @param dev LoRa device - * @return true if BUSY pin is high (chip sleeping / processing) - */ -bool sx126x_is_chip_busy(const struct device *dev); - -/** - * @brief Apply undocumented register 0x8B5 RX improvement for Heltec V4 - * - * Sets the LSB of register 0x8B5 which consistently improves RX reception - * on boards with GC1109 or KCT8103L PA (Heltec V4/V4.3). Described by - * Heltec engineer @Quency-D in MeshCore PR#1398. - * - * Must be called after the first lora_config() completes. - * - * @param dev LoRa device - */ -void sx126x_apply_heltec_reg_patch(const struct device *dev); - -/** - * @brief Reset AGC by performing warm sleep + full recalibration - * - * Warm sleep powers down the analog frontend (resets AGC gain state), - * then Calibrate(0x7F) refreshes all blocks (ADC, PLL, image, oscillators). - * Re-applies DIO2 RF switch, RX boosted gain, and image calibration afterward. - * - * Must be called while NOT actively receiving a packet. - * - * @param dev LoRa device - */ -void sx126x_reset_agc(const struct device *dev); - -#ifdef __cplusplus -} -#endif - -#endif /* SX126X_EXT_H */ +/* + * SPDX-License-Identifier: Apache-2.0 + * SX126x native driver — extension API + * + * Functions extending the standard Zephyr lora_driver_api with + * SX126x-specific features (duty cycle, RX boost, RSSI readout, + * preamble detection). + */ + +#ifndef SX126X_EXT_H +#define SX126X_EXT_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Get instantaneous RSSI (for noise floor calibration) + * + * Reads the current RSSI from the radio while in RX mode. + * Uses non-blocking mutex — returns -128 if SPI is busy. + * + * @param dev LoRa device + * @return RSSI in dBm, or -128 on error + */ +int16_t sx126x_get_rssi_inst(const struct device *dev); + +/** + * @brief Check if radio is actively receiving a packet + * + * Checks IRQ status for preamble/header detection. + * Uses non-blocking mutex — returns false if SPI is busy. + * + * @param dev LoRa device + * @return true if preamble or header detected + */ +bool sx126x_is_receiving(const struct device *dev); + +/** + * @brief Enable/disable RX boosted mode + * + * Boosted mode increases LNA gain for +3dB sensitivity at +2mA cost. + * + * @param dev LoRa device + * @param enable true to enable boost + */ +void sx126x_set_rx_boost(const struct device *dev, bool enable); + +/** + * @brief Check if the radio chip is busy (cannot accept SPI commands) + * + * Reads the BUSY GPIO pin directly — no SPI, no blocking. + * Returns true when the chip is in its duty-cycle sleep phase. + * Safe to call at any time. + * + * @param dev LoRa device + * @return true if BUSY pin is high (chip sleeping / processing) + */ +bool sx126x_is_chip_busy(const struct device *dev); + +/** + * @brief Apply undocumented register 0x8B5 RX improvement for Heltec V4 + * + * Sets the LSB of register 0x8B5 which consistently improves RX reception + * on boards with GC1109 or KCT8103L PA (Heltec V4/V4.3). Described by + * Heltec engineer @Quency-D in MeshCore PR#1398. + * + * Must be called after the first lora_config() completes. + * + * @param dev LoRa device + */ +void sx126x_apply_heltec_reg_patch(const struct device *dev); + +/** + * @brief Reset AGC by performing warm sleep + full recalibration + * + * Warm sleep powers down the analog frontend (resets AGC gain state), + * then Calibrate(0x7F) refreshes all blocks (ADC, PLL, image, oscillators). + * Re-applies DIO2 RF switch, RX boosted gain, and image calibration afterward. + * + * Must be called while NOT actively receiving a packet. + * + * @param dev LoRa device + */ +void sx126x_reset_agc(const struct device *dev); + +/** + * @brief Get duty-cycle preamble false-positive counter + * + * Returns the number of times duty-cycle RX tripped IRQ_RX_TX_TIMEOUT + * and was silently re-armed. High values mean the preamble detector is + * firing on noise/neighbour interference without a real packet arriving, + * which inflates RX-on time beyond the nominal duty cycle and shortens + * battery life. + * + * @param dev LoRa device + * @return Cumulative re-arm count since last reset + */ +uint32_t sx126x_get_dc_timeout_restarts(const struct device *dev); + +/** + * @brief Reset the duty-cycle preamble false-positive counter to zero. + * + * @param dev LoRa device + */ +void sx126x_reset_dc_timeout_restarts(const struct device *dev); + +#ifdef __cplusplus +} +#endif + +#endif /* SX126X_EXT_H */ diff --git a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch index 397771f..3a53739 100644 --- a/zephcore/patches/zephyr/0003-lora-sx126x-native.patch +++ b/zephcore/patches/zephyr/0003-lora-sx126x-native.patch @@ -68,7 +68,7 @@ index 17689720dd2..09982dc2e70 100644 return -EINVAL; } diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c -index 30243ba5dc7..ddc4ce19660 100644 +index 30243ba5dc7..4f2254c1f79 100644 --- a/drivers/lora/native/sx126x/sx126x.c +++ b/drivers/lora/native/sx126x/sx126x.c @@ -10,10 +10,19 @@ @@ -262,7 +262,7 @@ index 30243ba5dc7..ddc4ce19660 100644 sx126x_hal_set_rf_switch(dev, enable, tx); } } -@@ -455,6 +523,58 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev) +@@ -455,6 +523,67 @@ static int sx126x_reconnect_rf_gpios(const struct device *dev) } #endif /* CONFIG_PM_DEVICE */ @@ -294,6 +294,15 @@ index 30243ba5dc7..ddc4ce19660 100644 + sx126x_set_dio2_as_rf_switch(dev, true); + } + ++ /* StopTimerOnPreamble: without it, the duty-cycle RX timer ++ * keeps running even after a preamble is detected — a ++ * preamble arriving at the tail of the RX window can get ++ * cut off when the sleep phase begins. Must be re-issued ++ * every time because Calibrate(ALL) above resets it. */ ++ uint8_t stop_on_preamble = 1; ++ sx126x_hal_write_cmd(dev, SX126X_CMD_STOP_TIMER_ON_PREAMBLE, ++ &stop_on_preamble, 1); ++ + /* Re-apply duty cycle with stored timing */ + uint8_t dc_buf[6]; + sys_put_be24(data->dc_rx_time, &dc_buf[0]); @@ -321,7 +330,7 @@ index 30243ba5dc7..ddc4ce19660 100644 static int sx126x_set_sleep(const struct device *dev) { struct sx126x_data *data = dev->data; -@@ -566,19 +686,23 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta +@@ -566,19 +695,23 @@ static void sx126x_handle_irq_rx_done(const struct device *dev, uint16_t irq_sta /* Handle async callback or signal sync receiver */ if (data->rx_cb != NULL) { @@ -354,7 +363,28 @@ index 30243ba5dc7..ddc4ce19660 100644 } else { /* Sync mode */ sx126x_set_sleep(dev); -@@ -637,6 +761,25 @@ static void sx126x_irq_work_handler(struct k_work *work) +@@ -591,6 +724,20 @@ static void sx126x_handle_irq_timeout(const struct device *dev) + struct sx126x_data *data = dev->data; + + LOG_DBG("Timeout"); ++ ++ /* Duty-cycle RX: a preamble false-positive (noise, partial preamble ++ * from another modulation) can fire RxTimeout — but the chip has left ++ * duty cycle and is parked in STDBY_RC. Without this re-arm the ++ * driver silently falls out of duty cycle after the first noisy ++ * window and goes to sleep. Re-entering via sx126x_restart_rx() ++ * preserves the stored rx/sleep timing and re-applies AGC reset + ++ * StopTimerOnPreamble. */ ++ if (data->rx_duty_cycle_enabled && data->rx_cb != NULL) { ++ atomic_inc(&data->dc_timeout_restarts); ++ sx126x_restart_rx(dev, data); ++ return; ++ } ++ + sx126x_set_sleep(dev); + + if (data->tx_async_signal != NULL) { +@@ -637,6 +784,25 @@ static void sx126x_irq_work_handler(struct k_work *work) sx126x_handle_irq_timeout(dev); } @@ -380,7 +410,7 @@ index 30243ba5dc7..ddc4ce19660 100644 /* Re-enable the DIO1 interrupt for the next event (unless sleeping) */ if (atomic_get(&data->state) != SX126X_REST_STATE) { sx126x_hal_dio1_irq_enable(dev); -@@ -698,6 +841,29 @@ static int sx126x_lora_config(const struct device *dev, +@@ -698,6 +864,29 @@ static int sx126x_lora_config(const struct device *dev, goto out; } @@ -410,7 +440,7 @@ index 30243ba5dc7..ddc4ce19660 100644 /* Set sync word */ ret = sx126x_set_sync_word(dev, config->public_network); if (ret < 0) { -@@ -721,6 +887,8 @@ out: +@@ -721,6 +910,8 @@ out: return ret; } @@ -419,7 +449,7 @@ index 30243ba5dc7..ddc4ce19660 100644 static int sx126x_lora_send_async(const struct device *dev, uint8_t *data_buf, uint32_t data_len, struct k_poll_signal *async) -@@ -752,6 +920,34 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -752,6 +943,34 @@ static int sx126x_lora_send_async(const struct device *dev, return ret; } @@ -454,7 +484,7 @@ index 30243ba5dc7..ddc4ce19660 100644 data->tx_async_signal = async; k_msgq_purge(&data->tx_msgq); -@@ -777,6 +973,29 @@ static int sx126x_lora_send_async(const struct device *dev, +@@ -777,6 +996,29 @@ static int sx126x_lora_send_async(const struct device *dev, /* Enable antenna and set TX path */ sx126x_set_rf_path(dev, true, true); @@ -484,7 +514,7 @@ index 30243ba5dc7..ddc4ce19660 100644 /* Start transmission with 10 second timeout */ ret = sx126x_set_tx(dev, 10000); if (ret < 0) { -@@ -947,6 +1166,7 @@ static int sx126x_lora_recv_async(const struct device *dev, +@@ -947,6 +1189,7 @@ static int sx126x_lora_recv_async(const struct device *dev, data->rx_cb = cb; data->rx_cb_user_data = user_data; @@ -492,7 +522,7 @@ index 30243ba5dc7..ddc4ce19660 100644 /* Set packet parameters */ ret = sx126x_set_packet_params(dev, -@@ -1083,14 +1303,423 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, +@@ -1083,14 +1326,454 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency, return 0; } @@ -598,6 +628,20 @@ index 30243ba5dc7..ddc4ce19660 100644 + k_mutex_unlock(&data->lock); +} + ++uint32_t sx126x_get_dc_timeout_restarts(const struct device *dev) ++{ ++ struct sx126x_data *data = dev->data; ++ ++ return (uint32_t)atomic_get(&data->dc_timeout_restarts); ++} ++ ++void sx126x_reset_dc_timeout_restarts(const struct device *dev) ++{ ++ struct sx126x_data *data = dev->data; ++ ++ atomic_set(&data->dc_timeout_restarts, 0); ++} ++ +void sx126x_apply_heltec_reg_patch(const struct device *dev) +{ + struct sx126x_data *data = dev->data; @@ -780,7 +824,16 @@ index 30243ba5dc7..ddc4ce19660 100644 + if (cb == NULL) { + data->rx_cb = NULL; + data->rx_cb_user_data = NULL; ++ data->rx_duty_cycle_enabled = false; + if (atomic_cas(&data->state, SX126X_STATE_RX, SX126X_STATE_IDLE)) { ++ /* If we're called during the duty-cycle sleep phase the ++ * chip is actually asleep and BUSY is asserted — issuing ++ * SetStandby straight away stalls the HAL in ++ * wait_busy(). Poke CS via sx126x_hal_wakeup() first so ++ * BUSY drops before we try to talk to the chip. */ ++ if (sx126x_hal_is_busy(dev)) { ++ sx126x_hal_wakeup(dev); ++ } + sx126x_set_standby(dev, SX126X_STANDBY_RC); + sx126x_set_sleep(dev); + } @@ -884,6 +937,14 @@ index 30243ba5dc7..ddc4ce19660 100644 + data->dc_sleep_time = sleep_time; + data->rx_duty_cycle_enabled = true; + ++ /* StopTimerOnPreamble: keep the RX timer from expiring mid-packet ++ * when a preamble is detected late in the RX window. Without it, ++ * a packet whose preamble lands near the tail of the RX window ++ * gets cut off by the sleep transition. */ ++ uint8_t stop_on_preamble = 1; ++ sx126x_hal_write_cmd(dev, SX126X_CMD_STOP_TIMER_ON_PREAMBLE, ++ &stop_on_preamble, 1); ++ + uint8_t buf[6]; + + sys_put_be24(rx_time, &buf[0]); @@ -923,7 +984,7 @@ index 30243ba5dc7..ddc4ce19660 100644 }; #ifdef CONFIG_PM_DEVICE -@@ -1121,9 +1750,18 @@ static int sx126x_init(const struct device *dev) +@@ -1121,9 +1804,19 @@ static int sx126x_init(const struct device *dev) k_msgq_init(&data->rx_msgq, (char *)&data->rx_result, sizeof(struct sx126x_rx_result), 1); k_work_init(&data->irq_work, sx126x_irq_work_handler); @@ -933,6 +994,7 @@ index 30243ba5dc7..ddc4ce19660 100644 data->config_valid = false; + data->rx_duty_cycle_enabled = false; + data->rx_boost_enabled = false; ++ atomic_set(&data->dc_timeout_restarts, 0); + + /* Start dedicated DIO1 work queue */ + k_work_queue_start(&data->dio1_wq, sx126x_dio1_wq_stack, @@ -943,10 +1005,10 @@ index 30243ba5dc7..ddc4ce19660 100644 /* Initialize HAL */ ret = sx126x_hal_init(dev); diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h -index 9dbf3f26586..3aa0c7e588c 100644 +index 9dbf3f26586..79f34b67e19 100644 --- a/drivers/lora/native/sx126x/sx126x.h +++ b/drivers/lora/native/sx126x/sx126x.h -@@ -62,7 +62,21 @@ struct sx126x_data { +@@ -62,7 +62,29 @@ struct sx126x_data { /* Deferred work for interrupt handling */ struct k_work irq_work; @@ -959,6 +1021,14 @@ index 9dbf3f26586..3aa0c7e588c 100644 + uint32_t dc_rx_time; /* stored duty cycle rx period (15.625us steps) */ + uint32_t dc_sleep_time; /* stored duty cycle sleep period (15.625us steps) */ + ++ /* Duty-cycle preamble false-positive counter: incremented whenever ++ * IRQ_RX_TX_TIMEOUT fires during duty-cycle RX (preamble detector ++ * tripped but no valid sync/header followed — re-arm triggered). ++ * High values indicate a noisy RF environment or preamble-detect ++ * threshold too loose; each event extends the real RX time past ++ * the nominal rx_period, increasing current draw. */ ++ atomic_t dc_timeout_restarts; ++ + /* CAD state */ + lora_cad_cb cad_cb; + void *cad_user_data;