radio: extract CadController and NoiseFloorEstimator, one prefs source

- CadController (stats, window, re-anchor, visits, staircase, safety,
  starvation relax) and NoiseFloorEstimator split out of LoRaRadioBase
  behind a 4-call CadHw interface; Zephyr-free, host unit tests added
- prefs mandatory: setPrefs() before begin(), LoRaConfig fallbacks and the
  companion/observer placeholder prefs removed; LoRaConfig is the one
  defaults source (invalid-prefs fallback now uses the board TX default)
- TX-wait thread stack owned by the base; reconfigureWithParams removed
- driver extension headers on the public include path
  (zephyr/drivers/lora/*.h); patch 0003 include updated
- comment diet (image-identical); rationale moved to devdocs LLD 03
This commit is contained in:
liquidraver
2026-09-23 14:33:29 +02:00
parent a7d073900e
commit fd2cbe9d62
36 changed files with 935 additions and 1629 deletions
+1 -1
View File
@@ -453,7 +453,7 @@ via `Dispatcher::onCadOffsetChanged()`. The narrowing is not cosmetic: where
`base + offset` falls outside the hardware clamp, several offsets program the
**same** peak, and the staircase then compares rungs that are physically
identical and reads sampling noise as curvature. `hwCadPeakMin/Max()` report the
driver clamp and `cadLevelMinEff()/MaxEff()` derive the usable window, so every
driver clamp and `CadController::levelMinEff()/levelMaxEff()` derive the usable window, so every
level the controller can reach is a distinct configuration and the `pk` shown by
`get cad.stats` is what the chip was actually given. It binds on the LR2021, whose
4-symbol base is 51 at SF5–7 (effective −3…+12) and 54 at SF8 (−6…+12); the
+1 -11
View File
@@ -342,6 +342,7 @@ target_sources(app PRIVATE
src/Utils.cpp
adapters/board/ZephyrBoard.cpp
adapters/radio/LoRaRadioBase.cpp
adapters/radio/CadController.cpp
adapters/clock/ZephyrMillisecondClock.cpp
adapters/clock/ZephyrRTCClock.cpp
adapters/rng/ZephyrRNG.cpp
@@ -468,10 +469,6 @@ if(CONFIG_ZEPHCORE_RADIO_LR1110)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/radio/lr11xx
)
# Driver header (lr11xx_lora.h) is in the Zephyr tree after patching
target_include_directories(app PRIVATE
${ZEPHYR_DIR}/drivers/lora/lr11xx
)
elseif(CONFIG_ZEPHCORE_RADIO_LR2021)
message(STATUS "ZephCore Radio: LR2021 (Zephyr LoRa driver)")
target_sources(app PRIVATE
@@ -480,9 +477,6 @@ elseif(CONFIG_ZEPHCORE_RADIO_LR2021)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/radio/lr20xx
)
target_include_directories(app PRIVATE
${ZEPHYR_DIR}/drivers/lora/lr20xx
)
elseif(CONFIG_ZEPHCORE_RADIO_SX127X)
message(STATUS "ZephCore Radio: SX127x (Zephyr loramac-node driver)")
target_sources(app PRIVATE
@@ -494,10 +488,6 @@ else()
target_sources(app PRIVATE
adapters/radio/SX126xRadio.cpp
)
# Extension API header (sx126x_ext.h) is in the Zephyr driver tree
target_include_directories(app PRIVATE
${ZEPHYR_DIR}/drivers/lora/native/sx126x
)
endif()
# ========== Role-Specific Sources ==========
+313
View File
@@ -0,0 +1,313 @@
/*
* SPDX-License-Identifier: MIT
* Adaptive CAD controller — see CadController.h.
*/
#include "CadController.h"
#include <string.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(lora_radio_base, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
CadController::CadController(CadHw &hw)
: _hw(hw), _auto(false), _offset(0), _temp_offset(0), _visiting(false),
_busycap_pct(0), _probe_rr(0), _pending_level(INT8_MIN),
_pending_deadline_ms(0)
{
memset(_stats, 0, sizeof(_stats));
}
/* The clamp only binds when base + CAD_LEVEL_MIN would land below it. (Once
* written `base - CAD_LEVEL_MIN`, which is always above pmin: inert.) */
int8_t CadController::levelMinEff()
{
uint8_t base = _hw.hwCadBasePeak();
uint8_t pmin = _hw.hwCadPeakMin();
if (base == 0 || pmin == 0 ||
(int)base + CAD_LEVEL_MIN >= (int)pmin) {
return CAD_LEVEL_MIN;
}
return (int8_t)((int)pmin - (int)base);
}
int8_t CadController::levelMaxEff()
{
uint8_t base = _hw.hwCadBasePeak();
uint8_t pmax = _hw.hwCadPeakMax();
if (base == 0 || pmax == 0 || (int)pmax - (int)base >= CAD_LEVEL_MAX) {
return CAD_LEVEL_MAX;
}
return (int8_t)((int)pmax - (int)base);
}
void CadController::configure(bool auto_enabled, int8_t offset,
uint8_t busycap_pct, uint8_t stored_base)
{
const int8_t lo = levelMinEff();
const int8_t hi = levelMaxEff();
const uint8_t base = _hw.hwCadBasePeak();
/* Re-anchor across a base-table change: the offset is persisted, the
* stats behind it are not, so keep the measured PEAK. A peak the new
* window cannot reach is clamped below. */
if (stored_base != 0 && base != 0 && stored_base != base) {
int adj = (int)offset + (int)stored_base - (int)base;
LOG_INF("cad: base %u -> %u, re-anchoring offset %d -> %d "
"(peak %d held)",
(unsigned)stored_base, (unsigned)base,
(int)offset, adj, (int)stored_base + (int)offset);
offset = (int8_t)(adj < -128 ? -128 : (adj > 127 ? 127 : adj));
}
if (offset < lo) offset = lo;
if (offset > hi) offset = hi;
_auto = auto_enabled;
_offset = offset;
_busycap_pct = busycap_pct;
/* Effective: `set cad.*` may arrive during a visit, and the visited
* preset must keep its own threshold. */
_hw.hwCadSetPeakOffset(effectiveOffset());
}
void CadController::beginVisit(bool new_preset)
{
_temp_offset = new_preset ? 0 : _offset;
_visiting = true;
_hw.hwCadSetPeakOffset(effectiveOffset());
}
void CadController::endVisit()
{
_visiting = false;
_hw.hwCadSetPeakOffset(effectiveOffset());
}
void CadController::resetStats()
{
memset(_stats, 0, sizeof(_stats));
_probe_rr = 0;
/* A probe in flight belongs to the table being dropped. */
_pending_level = INT8_MIN;
_pending_deadline_ms = 0;
}
void CadController::decayStats()
{
for (int i = 0; i < CAD_NUM_LEVELS; i++) {
_stats[i].probes >>= 1;
_stats[i].busy >>= 1;
_stats[i].fp >>= 1;
_stats[i].tp >>= 1;
}
}
int8_t CadController::pickProbeLevel()
{
_probe_rr++;
if (!_auto) {
/* Parked outside the sweep window: probe where the operator is,
* or the safety rung never gets evidence for that level. */
if (_offset < CAD_SWEEP_MIN || _offset > CAD_SWEEP_MAX) {
return _offset;
}
/* Dry run: an even sweep, so `get cad` shows the whole curve. */
int span = CAD_SWEEP_MAX - CAD_SWEEP_MIN + 1;
return (int8_t)(CAD_SWEEP_MIN + (_probe_rr % span));
}
/* Auto: operating level half the time, each neighbour a quarter, so the
* staircase can read the slope on both sides. Out-of-range neighbours
* fall back to the operating level. */
int8_t lvl;
switch (_probe_rr & 3) {
case 1: lvl = (int8_t)(_offset - 1); break; /* more sensitive */
case 3: lvl = (int8_t)(_offset + 1); break; /* less sensitive */
default: lvl = _offset; break; /* operating (0, 2) */
}
if (lvl < levelMinEff() || lvl > levelMaxEff()) {
lvl = _offset;
}
return lvl;
}
void CadController::recordProbe(int8_t level, bool busy)
{
LevelStats &s = _stats[level - CAD_LEVEL_MIN];
if (s.probes >= 0xFFF0) {
decayStats();
}
s.probes++;
if (busy) {
s.busy++;
}
}
void CadController::resolvePending(int outcome)
{
LevelStats &ps = _stats[_pending_level - CAD_LEVEL_MIN];
if (outcome == 1) {
ps.tp++;
} else if (outcome == 2) {
ps.fp++;
} else {
/* No terminal event: an invented verdict would poison the curve,
* so withdraw the sample (probe and busy, to keep both rates
* over the same population). */
if (ps.probes) ps.probes--;
if (ps.busy) ps.busy--;
LOG_WRN("cad: probe at %+d unresolved past deadline, discarded",
(int)_pending_level);
}
_pending_level = INT8_MIN;
}
/* Per-level FALSE-positive rate in permille, or -1 when too few samples. */
int CadController::fpRate(int idx) const
{
if (idx < 0 || idx >= CAD_NUM_LEVELS ||
_stats[idx].probes < CAD_STEP_MIN_PROBES) {
return -1;
}
return (int)(((uint32_t)_stats[idx].fp * 1000U) / _stats[idx].probes);
}
/* Per-level TOTAL busy (defer) rate in permille — false + real traffic. */
int CadController::busyRate(int idx) const
{
if (idx < 0 || idx >= CAD_NUM_LEVELS ||
_stats[idx].probes < CAD_STEP_MIN_PROBES) {
return -1;
}
return (int)(((uint32_t)_stats[idx].busy * 1000U) / _stats[idx].probes);
}
void CadController::stepOffset(int8_t to)
{
_offset = to;
_hw.hwCadSetPeakOffset(_offset);
}
/* Steps toward the knee: the most sensitive level whose FP rate has already
* bottomed out. Uses slopes, so the decision does not depend on a site's FP
* floor. */
void CadController::staircaseStep()
{
int oi = _offset - CAD_LEVEL_MIN;
int r_op = fpRate(oi);
if (r_op < 0) {
return; /* operating level not warm yet */
}
int r_up = fpRate(oi + 1); /* one step less sensitive */
int r_dn = fpRate(oi - 1); /* frontier, one step more sensitive */
int cap_permille = (int)_busycap_pct * 10;
/* Up when the level above is markedly cleaner: below the knee. */
if (_offset < levelMaxEff() && r_up >= 0 &&
r_op - r_up >= CAD_KNEE_SLOPE_PERMILLE) {
stepOffset((int8_t)(_offset + 1));
LOG_INF("cad: step up -> offset %d (op %d dn->up %d)",
(int)_offset, r_op, r_up);
return;
}
/* Down only on a flat, already-clean plateau, and not into the airtime
* cap (hysteresis keeps it from bouncing straight back up). */
int b_dn = busyRate(oi - 1);
bool busy_ok = (cap_permille == 0) ||
(b_dn <= cap_permille -
(cap_permille * CAD_BUSY_DEFER_HYST_PCT) / 100);
if (_offset > levelMinEff() && r_dn >= 0 &&
r_dn - r_op < CAD_KNEE_SLOPE_PERMILLE &&
r_op <= CAD_PLATEAU_CLEAN_PERMILLE && busy_ok) {
stepOffset((int8_t)(_offset - 1));
LOG_INF("cad: step down -> offset %d (op %d dn %d busy %d)",
(int)_offset, r_op, r_dn, b_dn);
return;
}
/* Otherwise: at the knee, or a noisy plateau — hold. */
}
/* The proactive half of the TX safety pair: works off probe statistics, so it
* acts on a silent mesh too, where relaxOnTxStarvation() would wait for the
* next own transmit. Not gated on auto: it is a safety, not an optimiser. */
bool CadController::safetyStep()
{
if (_offset >= levelMaxEff()) {
return false;
}
int oi = _offset - CAD_LEVEL_MIN;
if (oi < 0 || oi >= CAD_NUM_LEVELS) {
return false;
}
uint16_t probes = _stats[oi].probes;
if (probes == 0) {
return false;
}
int b_op = (int)(((uint32_t)_stats[oi].busy * 1000U) / probes);
int cap_permille = (int)_busycap_pct * 10;
/* Pathological: trips on nearly every probe, so it cannot clear a
* transmit either. Few samples suffice; the cap is ignored. */
bool pathological = probes >= CAD_SAFETY_MIN_PROBES &&
b_op >= CAD_SAFETY_PATHOLOGICAL_PERMILLE;
/* Marginal: over the operator's airtime cap (0 = off). */
bool over_cap = cap_permille && probes >= CAD_STEP_MIN_PROBES &&
b_op > cap_permille;
if (!pathological && !over_cap) {
return false;
}
stepOffset((int8_t)(_offset + 1));
if (pathological) {
LOG_WRN("cad: safety step up -> offset %d (busy %d permille over "
"%u probes — detector too sensitive to clear, auto=%d)",
(int)_offset, b_op, (unsigned)probes, (int)_auto);
} else {
LOG_INF("cad: step up -> offset %d (airtime, busy %d cap %d)",
(int)_offset, b_op, cap_permille);
}
return true;
}
/* Ignores auto, the cap and the stats: the caller has seen the harm directly.
* One step at a time. Runs during a visit too (a muted node is muted for
* real), stepping the visitor's offset so nothing reaches prefs. */
bool CadController::relaxOnTxStarvation()
{
int8_t &off = _visiting ? _temp_offset : _offset;
if (off >= levelMaxEff()) {
return false;
}
off++;
_hw.hwCadSetPeakOffset(effectiveOffset());
LOG_WRN("cad: TX starvation override -> offset %d (auto=%d%s) — LBT was "
"refusing every transmit",
(int)off, (int)_auto,
_visiting ? ", temp preset" : "");
return true;
}
} /* namespace mesh */
+124
View File
@@ -0,0 +1,124 @@
/*
* SPDX-License-Identifier: MIT
* Adaptive CAD controller: learns the LBT detPeak offset from probe verdicts.
*
* Pure algorithm over a four-call hardware interface. LoRaRadioBase decides
* when to probe, runs the probe and feeds the verdicts in; this class keeps
* the per-level statistics and moves the operating offset. Design notes:
* devdocs/lld/03-radio-contract.md §13 and docs/ADAPTIVE_CAD.md.
*/
#pragma once
#include <stdint.h>
#include "radio_tuning.h"
namespace mesh {
/* The detPeak side of the chip, as the controller sees it. */
class CadHw {
public:
/* Family base detPeak at the current SF/BW; 0 = no adaptive CAD. */
virtual uint8_t hwCadBasePeak() = 0;
/* The driver's absolute detPeak clamp; 0 = none reported. */
virtual uint8_t hwCadPeakMin() = 0;
virtual uint8_t hwCadPeakMax() = 0;
/* Program the offset used by every subsequent LBT CAD. */
virtual void hwCadSetPeakOffset(int8_t offset) = 0;
protected:
~CadHw() = default;
};
class CadController {
public:
struct LevelStats {
uint16_t probes; /* probes run at this level */
uint16_t busy; /* raw busy verdicts */
uint16_t fp; /* busy with nothing decoded after it (false positive) */
uint16_t tp; /* busy that turned into a received packet */
};
explicit CadController(CadHw &hw);
/* Apply the CAD prefs. stored_base is the base the persisted offset was
* learned against (0 = unknown); across a base-table change the PEAK is
* kept, not the offset. Programs the hardware. */
void configure(bool auto_enabled, int8_t offset, uint8_t busycap_pct,
uint8_t stored_base);
bool autoEnabled() const { return _auto; }
uint8_t busycapPct() const { return _busycap_pct; }
/* The configured offset: the value that is persisted. */
int8_t offset() const { return _offset; }
/* The offset the chip is programmed with (the visitor's during a visit). */
int8_t effectiveOffset() const { return _visiting ? _temp_offset : _offset; }
/* A temporary radio override is up. new_preset: the radio moved to a
* preset it was not running, so the visit starts at that preset's base
* (offset 0); otherwise the running offset is kept. Adaptation is
* suspended and nothing the visit does reaches offset(). */
void beginVisit(bool new_preset);
void endVisit();
bool visiting() const { return _visiting; }
/* The level window the controller may use: [CAD_LEVEL_MIN, CAD_LEVEL_MAX]
* narrowed so every level programs a distinct peak under the driver's
* clamp. Stats stay indexed by the static window. */
int8_t levelMinEff();
int8_t levelMaxEff();
void resetStats();
void decayStats();
const LevelStats &stats(int level) const { return _stats[level - CAD_LEVEL_MIN]; }
/* Probing. */
int8_t pickProbeLevel();
void recordProbe(int8_t level, bool busy);
/* A busy probe whose ground truth arrives later (CAD_RX). */
void setPending(int8_t level, int64_t deadline_ms)
{
_pending_level = level;
_pending_deadline_ms = deadline_ms;
}
bool hasPending() const { return _pending_level != INT8_MIN; }
int8_t pendingLevel() const { return _pending_level; }
int64_t pendingDeadline() const { return _pending_deadline_ms; }
/* outcome: 1 = packet (true positive), 2 = chip timeout (false
* positive), anything else = unresolved, the sample is withdrawn. */
void resolvePending(int outcome);
/* After a verdict: the safety rung first; the optimiser only when the
* safety did not act and auto is on. */
void adapt()
{
if (!safetyStep() && _auto) {
staircaseStep();
}
}
/* Airtime protection. Runs regardless of auto. True when it moved. */
bool safetyStep();
/* Knee-seeking optimiser. */
void staircaseStep();
/* The node's own TX is being refused: one step less sensitive, whatever
* auto and the stats say. During a visit it steps the visitor's offset. */
bool relaxOnTxStarvation();
private:
int fpRate(int idx) const;
int busyRate(int idx) const;
void stepOffset(int8_t to);
CadHw &_hw;
LevelStats _stats[CAD_NUM_LEVELS];
bool _auto;
int8_t _offset;
int8_t _temp_offset;
bool _visiting;
uint8_t _busycap_pct;
uint8_t _probe_rr;
int8_t _pending_level;
int64_t _pending_deadline_ms;
};
} /* namespace mesh */
+3 -13
View File
@@ -8,7 +8,7 @@
/* LR11xx driver extension API */
extern "C" {
#include "lr11xx_lora.h"
#include <zephyr/drivers/lora/lr11xx_lora.h>
}
#include <zephyr/logging/log.h>
@@ -16,21 +16,11 @@ LOG_MODULE_REGISTER(lr1110_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(lr11xx_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
LR1110Radio::LR1110Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
LR1110Radio::LR1110Radio(const struct device *lora_dev, MainBoard &board)
: LoRaRadioBase(lora_dev, board)
{
}
void LR1110Radio::begin()
{
startTxThread(lr11xx_tx_wait_stack,
K_THREAD_STACK_SIZEOF(lr11xx_tx_wait_stack));
LoRaRadioBase::begin();
}
uint32_t LR1110Radio::getDutyCycleTimeoutRestarts() const
{
return lr11xx_get_dc_timeout_restarts(_dev);
+1 -4
View File
@@ -13,10 +13,7 @@ namespace mesh {
class LR1110Radio : public LoRaRadioBase {
public:
LR1110Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
LR1110Radio(const struct device *lora_dev, MainBoard &board);
/* Duty-cycle false-preamble re-arm count, behind `get dc.restarts`.
* Without these the base class's stub answers 0 forever, and the one
+3 -13
View File
@@ -8,7 +8,7 @@
/* LR20xx driver extension API */
extern "C" {
#include "lr20xx_lora.h"
#include <zephyr/drivers/lora/lr20xx_lora.h>
}
#include <zephyr/logging/log.h>
@@ -16,21 +16,11 @@ LOG_MODULE_REGISTER(lr2021_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(lr20xx_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
LR2021Radio::LR2021Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
LR2021Radio::LR2021Radio(const struct device *lora_dev, MainBoard &board)
: LoRaRadioBase(lora_dev, board)
{
}
void LR2021Radio::begin()
{
startTxThread(lr20xx_tx_wait_stack,
K_THREAD_STACK_SIZEOF(lr20xx_tx_wait_stack));
LoRaRadioBase::begin();
}
bool LR2021Radio::configSideDetectors(const uint8_t *sfs, uint8_t num)
{
/* The driver validates against the configured SF/BW, so this must run
+1 -4
View File
@@ -13,10 +13,7 @@ namespace mesh {
class LR2021Radio : public LoRaRadioBase {
public:
LR2021Radio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
LR2021Radio(const struct device *lora_dev, MainBoard &board);
/* Duty-cycle false-preamble re-arm count, behind `get dc.restarts`.
* Without these the base class's stub answers 0 forever, and the one
File diff suppressed because it is too large Load Diff
+60 -228
View File
@@ -12,14 +12,17 @@
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include "radio_common.h"
#include "CadController.h"
#include "NoiseFloorEstimator.h"
namespace mesh {
class LoRaRadioBase : public Radio {
class LoRaRadioBase : public Radio, private CadHw {
public:
LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
LoRaRadioBase(const struct device *lora_dev, MainBoard &board);
/* Mandatory, before begin(): the radio reads freq/bw/sf/cr/power
* through this pointer from then on (LoRaConfig only seeds prefs). */
void setPrefs(NodePrefs *prefs) { _prefs = prefs; }
/* Callbacks */
@@ -35,21 +38,11 @@ public:
/* Radio interface (all implemented in base) */
void begin() override;
void reconfigure();
void reconfigureWithParams(float freq, float bw, uint8_t sf, uint8_t cr);
/* Temporary radio override — applies freq/bw/sf/cr without mutating
* _prefs. Used by tempradio so the saved prefs survive intact and
* concurrent savePrefs() calls don't poison flash. clearRadioOverride()
* reverts to whatever _prefs holds at that moment. */
/* visiting_new_preset = the override moves the radio onto a preset it
* was not already running (`tempradio`), so the learned detPeak offset
* does not apply there and the visit starts at that preset's own base.
* Pass false when the override exists only to HOLD the radio on the
* preset it is already running while _prefs move ahead of it
* (freezeRadioParams, i.e. `set radio` / `set freq` + reboot): nothing
* changed on air, so the offset that was learned for it keeps running
* until the reboot re-reads prefs. Either way CAD adaptation is
* suspended while the override is up — see cadMaintenance(). */
/* Temporary radio override: freq/bw/sf/cr without touching _prefs
* (tempradio, and the `set radio` freeze until reboot). visiting_new_preset:
* the radio moves to a preset it was not running, so CAD starts at that
* preset's base; false holds the running one. CAD adaptation is suspended. */
void setRadioOverride(float freq, float bw, uint8_t sf, uint8_t cr,
bool visiting_new_preset = true);
void clearRadioOverride();
@@ -69,10 +62,8 @@ public:
uint32_t getPacketsRecv() const override { return (uint32_t)atomic_get(&_packets_recv); }
uint32_t getPacketsSent() const override { return (uint32_t)atomic_get(&_packets_sent); }
uint32_t getPacketsRecvErrors() const override { return (uint32_t)atomic_get(&_packets_recv_errors); }
/* Virtual so radios with extra accumulators can clear them on the same
* `clear stats`. Reached through a LoRaRadioBase& from
* RepeaterMesh::clearStats() via getRadioDriver(), so a shadowing
* non-virtual override would silently never run. */
/* Virtual: reached through LoRaRadioBase& (RepeaterMesh::clearStats()),
* so radios with extra accumulators clear them on the same `clear stats`. */
virtual void resetStats() {
atomic_set(&_packets_recv, 0);
atomic_set(&_packets_sent, 0);
@@ -95,22 +86,13 @@ public:
virtual bool setRxBoost(bool enable);
bool isRxBoostEnabled() const { return _rx_boost_enabled; }
/* External FEM/LNA gain during RX. Boards whose front-end module has a
* software-selectable receive path wire that select line to the radio
* (lna-bypass-gpios); disabling this routes RX around the FEM's LNA,
* trading its gain for its supply current while leaving the antenna
* connected. The transmit path and the driver's idle gating are
* unaffected. Returns false on radios that do not implement the knob
* (everything but the native SX126x today) and on SX126x boards that
* wire no such line -- including boards whose only FEM control is the
* chip enable, where there is no gain to trade, only a path to cut. */
/* External FEM LNA in RX (lna-bypass-gpios): false routes RX around the
* LNA, trading its gain for its current. Returns false where the radio
* or the board has no such select line. */
virtual bool setFemRxEnable(bool enable) { (void)enable; return false; }
/* Multi-SF receive via LoRa side detectors. Only the LR2021 has them;
* every other radio reports the feature as unsupported. `num` = 0
* disables. Returns false if the radio has no side detectors or the
* requested set violates a chip constraint (see
* lr20xx_configure_side_detectors). */
/* Multi-SF receive via side detectors (LR2021 only). num = 0 disables.
* False when unsupported or the set violates a chip constraint. */
virtual bool configSideDetectors(const uint8_t *sfs, uint8_t num) {
(void)sfs; (void)num;
return false;
@@ -128,13 +110,8 @@ public:
int8_t getConfiguredTxPower() const;
bool isTxActive() const override { return atomic_get(&_tx_active) != 0; }
/* 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. */
/* Duty-cycle false-preamble re-arms, counted by the driver (each one
* stretches RX time past the nominal cycle). 0 where unsupported. */
virtual uint32_t getDutyCycleTimeoutRestarts() const { return 0; }
virtual void resetDutyCycleTimeoutRestarts() {}
@@ -154,20 +131,9 @@ private:
void imageCalMaintenance(uint32_t now);
public:
uint32_t msUntilNextMaintenance() override;
int8_t getCadOffset() const override { return _cad_offset; }
/* Offset bounds the controller may actually use: the static
* [CAD_LEVEL_MIN, CAD_LEVEL_MAX] window narrowed to whatever the
* hardware clamp leaves distinguishable at the current base. Every
* range decision goes through these; the raw constants stay in use only
* for indexing _cad_stats[], which is sized to the static window. */
int8_t cadLevelMinEff();
int8_t cadLevelMaxEff();
void resetCadStats() override;
bool cadRelaxOnTxStarvation() override;
/* Airtime-protection / stuck-detector rung, run on every maintenance pass
* regardless of _cad_auto. Returns true when it moved the offset. */
bool cadSafetyStep();
int8_t getCadOffset() const override { return _cad.offset(); }
void resetCadStats() override { _cad.resetStats(); }
bool cadRelaxOnTxStarvation() override { return _cad.relaxOnTxStarvation(); }
int formatCadStatus(char *buf, int cap) override;
protected:
@@ -178,30 +144,11 @@ protected:
virtual int hwSendAsync(uint8_t *buf, uint32_t len,
struct k_poll_signal *sig) = 0;
virtual int16_t hwGetCurrentRSSI() = 0;
/** Read `n` instantaneous RSSI samples spaced `spacing_us` apart.
*
* Exists so the whole burst can be bracketed ONCE by whatever a family
* needs to take a reading, instead of once per sample. On the LR
* families a single RSSI read stands the duty cycle down, enters
* continuous Rx, waits 1 ms to settle, reads, and re-arms the cycle --
* and that re-arm clears every IRQ and zeroes the RX-busy latch. Called
* eight times for one median that is eight cycle tear-downs, eight latch
* wipes and 8 ms of settle, every sampling interval, whether or not a
* CAD probe follows.
*
* Returns the number of valid samples written (< n means the read was
* refused partway and the caller should abandon the burst), or a NEGATIVE
* value when the burst must be abandoned for a reason that is not a read
* failure: -EAGAIN says the receiver detected a preamble or header inside
* the window, so the samples describe that signal and not the floor.
* Worth its own return because the caller's counters exist to tell a
* failing bus from a busy channel, and only a family that brackets its own
* Rx entry is in a position to notice the latter.
*
* The default implementation is the old per-sample loop, which is already
* correct for radios whose RSSI read has no such bracket (SX126x bails on
* BUSY and touches nothing; SX127x has no duty cycle at all) and which
* therefore never reports -EAGAIN. */
/* Read `n` RSSI samples `spacing_us` apart, bracketed once (on the LR
* families each single read tears the duty cycle down and back up).
* Returns samples written (< n: refused partway, abandon), or -EAGAIN
* when a preamble/header landed in the window (a busy channel, not a
* failing bus). The default per-sample loop suits SX126x and SX127x. */
virtual int hwGetRssiBurst(int16_t *out, int n, uint32_t spacing_us);
/* Non-destructive read of the radio's "currently receiving" signal —
* latch + raw IRQ bits, never clears. Backs LoRaRadioBase::isReceiving(). */
@@ -233,32 +180,19 @@ protected:
/** How long the chip may stay in a CAD_RX-entered RX before it raises
* its own timeout. Bounds the wait before hwCadRxOutcome() is read. */
virtual uint32_t hwCadRxTimeoutMs() { return 0; }
/** Apply the operating detPeak offset for all subsequent LBT CADs. */
virtual void hwCadSetPeakOffset(int8_t offset) { (void)offset; }
/** Per-SF base detPeak for the current config (0 = unsupported). */
virtual uint8_t hwCadBasePeak() { return 0; }
/** Absolute detPeak range the driver will actually program, inclusive.
* 0/0 means "no known limit" and the offset range stays as-is.
*
* This exists because the offset window and the hardware clamp are two
* different things, and when they disagree the controller explores
* levels that are physically identical: on the LR2021 at SF7 the base is
* 51 and the driver clamps to 48, so offsets -3 through -8 all programmed
* the same peak. The staircase then compared three rungs of the same
* configuration, found only sampling noise between them, and random-walked
* into the floor with nothing to climb back out on. */
virtual uint8_t hwCadPeakMin() { return 0; }
virtual uint8_t hwCadPeakMax() { return 0; }
/* CadHw (see CadController.h): offset, base detPeak (0 = unsupported)
* and the driver's detPeak clamp (0/0 = none). Report the clamp: past it
* several offsets program one peak and the staircase random-walks. */
void hwCadSetPeakOffset(int8_t offset) override { (void)offset; }
uint8_t hwCadBasePeak() override { return 0; }
uint8_t hwCadPeakMin() override { return 0; }
uint8_t hwCadPeakMax() override { return 0; }
/* ── Receiver hygiene — see LoRaRadioBase::radioMaintenance() ─── */
/** Unstick a jammed AGC: warm sleep to drop the analog front end, then
* recalibrate on the way back up. Semtech's stated remedy; the
* datasheets do not describe it, so do not "simplify" it away on
* datasheet grounds alone. Must leave the driver out of RX so the
* caller's startReceive() performs a real re-entry rather than hitting
* an idempotent fast path. Default: unsupported, no-op. */
/* Unstick a jammed AGC: warm sleep + recalibrate (Semtech's remedy, not in
* the datasheets). Must leave the driver out of RX so the caller's
* startReceive() is a real re-entry. Default: no-op. */
virtual void hwResetAgc() {}
/** Redo the frequency-dependent calibrations (image / front end, and
@@ -267,21 +201,8 @@ protected:
* Same RX-state contract as hwResetAgc(). Default: unsupported. */
virtual void hwRecalibrate() {}
/** Chip junction temperature in whole degrees C, or INT16_MIN when the
* backend cannot measure it (which disables drift recalibration). */
/* Does this family actually suffer the jammed-AGC fault the reset is a
* remedy for? Only the SX126x does. Semtech prescribe warm sleep plus
* recalibration for it, ZephCore inherited the idea from Arduino
* MeshCore's `agc_reset_interval`, and both are SX126x-era. Neither the
* LR11xx UM nor the LR2021 DS describes such a fault; those parts need
* image/front-end recalibration on temperature drift, which is a
* different operation on a different trigger (hwRecalibrate()).
*
* Running it anyway is not free. Measured on a T1000-E 2026-08-23: the
* 60 s after an AGC reset carried a 7.4% packet-miss rate against 0.6%
* elsewhere, and one deaf stretch began at one reset and ended at the
* next — the reset was recovering damage it had caused, on a part with
* no AGC fault to fix. */
/* Only the SX126x has the jammed-AGC fault. Elsewhere the reset is pure
* cost (7.4% packet miss in the following minute on a T1000-E, LLD 03 §13). */
virtual bool hwNeedsAgcReset() { return false; }
/* Does this family specify a temperature threshold for image/front-end
@@ -289,26 +210,13 @@ protected:
* and drift recalibration stays inactive there exactly as before. */
virtual bool hwHasDriftRecal() { return false; }
/** Radio deaf time per duty-cycle wake transition (context restore +
* PLL lock + TCXO startup where fitted), in microseconds. Counts
* against the duty-cycle preamble-catch budget: per SX126x DS rev 2.2
* §13.1.7 the TCXO startup delay is inserted between the sleep and RX
* periods, outside both.
*
* This default is a fallback for backends that expose no per-device
* figure; it suits XTAL parts only. SX126x, LR11xx and LR2021 all
* override it, because a board with a TCXO powers the regulator down
* during duty-cycle sleep and pays the oscillator restart on every
* wake — several milliseconds, dwarfing this number. Leaving a TCXO
* board on the default oversizes the sleep window and drops
* window-edge preambles regardless of signal strength. */
/* Deaf time per duty-cycle wake (context restore + PLL + TCXO start), in us,
* counted against the catch budget (DS §13.1.7). The default suits XTAL
* parts only: a TCXO board must override it or window-edge preambles drop. */
virtual uint32_t hwWakeupTimeUs() { return 1500; }
/* Set to true by subclasses using the loramac-node driver backend.
* Disables the direction-only fast path in configureTx()/configureRx():
* loramac-node calls Radio.SetTxConfig() and Radio.SetRxConfig() which
* configure completely disjoint internal state — skipping either leaves
* TxTimeout/RxConfig uninitialized in the loramac-node library. */
/* loramac-node backend: its TX and RX configs are disjoint state, so the
* direction-only fast path in configure() must stay off. */
bool _loramac_node;
/* ── Shared helpers available to subclasses ────────────────── */
@@ -321,20 +229,16 @@ protected:
void configureRx();
void configureTx();
void startReceive();
void startTxThread(k_thread_stack_t *stack, size_t stack_size);
void startTxThread();
const struct device *_dev;
NodePrefs *_prefs;
MainBoard *_board;
atomic_t _in_recv_mode;
atomic_t _tx_active;
/* Completion latch: raised by the TX wait thread only on an affirmative
* success verdict, consumed once by isSendComplete(), and cleared by
* startSendRaw() so a completion the dispatcher never collected (it gave
* up on outbound_expiry first) cannot leak into the next packet. This
* is the Zephyr equivalent of upstream's STATE_INT_READY -> STATE_IDLE
* transition; _tx_active alone cannot serve, because it is also cleared
* on every failure path and is read as a plain state query elsewhere. */
/* Completion latch: set by the wait thread on success only, consumed once
* by isSendComplete(), cleared by startSendRaw() (upstream's
* STATE_INT_READY). _tx_active cannot serve: failures clear it too. */
atomic_t _tx_complete;
volatile float _last_rssi; /* word-aligned: atomic on ARM */
volatile float _last_snr; /* word-aligned: atomic on ARM */
@@ -355,15 +259,10 @@ protected:
struct k_poll_signal _tx_signal;
/* Noise floor calibration state */
int _noise_floor;
NoiseFloorEstimator _floor_est;
int _calibration_threshold;
uint8_t _ema_unguarded; /* tick counter for warmup + periodic bypass */
/* Absolute uptime deadline of the next floor sample. The sampler used
* to run on every housekeeping tick, which pinned its cadence to the
* 5 s timer; owning its own deadline is what lets that timer go away.
* Advanced by NOISE_FLOOR_INTERVAL_MS after a sample lands, and by the
* shorter retry when an attempt is turned away because the radio was
* mid-packet / transmitting / in its duty-cycle sleep window. */
/* Deadline of the next floor sample: an interval after a sample, the
* short retry after a blocked attempt. */
int64_t _noise_floor_next_ms;
uint8_t _noise_floor_retries; /* consecutive blocked attempts, capped */
/* Shared cadence for every periodic radio measurement (floor sample +
@@ -378,61 +277,15 @@ protected:
/* Cycle stamp of the last host-driven RX entry, used to skip a floor
* sample taken before GetRssiInst has settled (DS Table 13-82). */
uint32_t _rx_entry_cyc;
/* Median-of-N quality accounting, surfaced by `get cad` as sp:<mean>/<%>.
* The median only rejects outliers if the N reads are independent; if
* they land inside one RSSI averaging window they are the same sample
* N times over and the median is decorative. Spread (max-min of the
* burst) and the share of zero-spread bursts make that visible without
* a debug build. */
/* Burst quality for `get cad` sp:<mean>/<%>: are the N reads independent? */
uint32_t _rssi_bursts;
uint32_t _rssi_spread_sum;
uint32_t _rssi_degenerate;
/* Adaptive CAD state */
struct CadLevelStats {
uint16_t probes; /* probes run at this level */
uint16_t busy; /* raw busy verdicts */
uint16_t fp; /* busy that passed the ground-truth filter (suspected false positive) */
uint16_t tp; /* busy confirmed by RX activity right after */
};
CadLevelStats _cad_stats[CAD_NUM_LEVELS];
bool _cad_auto; /* staircase acts on the stats */
int8_t _cad_offset; /* operating detPeak offset (levels) */
/* The offset in force while a temporary radio override is active, i.e.
* on a preset the node is only visiting (`tempradio`, and the frozen
* old preset a `set radio` leaves running until reboot). detPeak's base
* table is per-SF and per-bandwidth, so the offset learned for the
* configured preset addresses a different ladder there — on the LR11xx
* the SF7->SF12 base step alone is 16 counts. Kept separate rather than
* saved-and-restored so _cad_offset never moves during the visit, which
* is what keeps it out of prefs: Dispatcher::maintenanceLoop() persists
* any change it sees in getCadOffset(). */
int8_t _cad_temp_offset;
/* The offset the chip is actually programmed with. Every hwCadSetPeakOffset()
* goes through this; getCadOffset() deliberately does not, because that is
* the value the app persists. */
int8_t cadEffectiveOffset() const {
return _has_radio_override ? _cad_temp_offset : _cad_offset;
}
/* Adaptive CAD: the controller, plus the probe schedule that stays here. */
CadController _cad;
uint16_t _probe_interval_s; /* 0 = CAD probing disabled; drives _measure_interval_ms */
uint8_t _cad_busycap_pct; /* airtime cap: max % TX deferred (0 = off) */
/* A CAD_RX probe awaiting its terminal event. _cad_pending_level is the
* stats rung the result belongs to; _cad_pending_deadline_ms is when the
* chip guarantees it has resolved, so the answer is READ once at that
* point rather than polled for. Level INT8_MIN means nothing pending. */
int8_t _cad_pending_level;
int64_t _cad_pending_deadline_ms;
int64_t _cad_last_probe_ms;
int64_t _cad_last_decay_ms;
/* Earliest uptime at which a due-but-blocked probe may be retried. The
* interval check in cadMaintenance() is against _cad_last_probe_ms,
* which only advances on a probe that actually ran — without this a
* blocked probe would report "due now" forever and spin the wake. */
uint8_t _cad_probe_rr; /* round-robin index (sweep) / frontier mix counter */
int8_t pickCadProbeLevel();
void decayCadStats();
void cadStaircaseStep();
/* Power saving */
bool _rx_duty_cycle_enabled;
@@ -442,7 +295,6 @@ protected:
* changes once at INF instead of on every RX restart. 0/0 = never
* computed; UINT32_MAX rx = continuous-RX fallback active. */
uint32_t _dc_last_rx_us;
uint32_t _dc_last_sleep_us;
/* agcIdleMaintenance() bookkeeping. RX activity is inferred by sampling the
@@ -457,25 +309,12 @@ protected:
int16_t _agc_rssi_last;
uint8_t _agc_rssi_frozen;
/* Diagnostics only — no behaviour depends on these.
*
* The RSSI sampler discards a whole burst if any read comes back busy,
* and on a duty-cycled LR1110 it completes ~0.2% of its attempts
* (measured 5 bursts in 9 h against 1007 on an SX1262 beside it). That
* is either "a few reads land in the sleep phase" or "essentially every
* read is refused", and the two want opposite fixes. These counters say
* which, and are reported by `get cad`. */
/* Sampler diagnostics for `get cad` (no behaviour depends on them): reads
* ok/busy, bursts abandoned, and attempts refused before a burst started
* (duty-cycle sleep), without which "refused" and "fine" both read b0/a0. */
uint32_t _rssi_reads_ok;
uint32_t _rssi_reads_busy;
uint32_t _rssi_bursts_abandoned;
/* Attempts turned away BEFORE a burst starts, by the isRadioReady()
* guard — i.e. the duty-cycle sleep window. The three counters above
* cannot see this: they only move once a burst is already running, so a
* sampler that is being refused outright reads as b0/a0, which is
* indistinguishable from one that is running perfectly. That ambiguity
* is what makes the "does the floor go stale under duty cycle?" question
* unanswerable today, and it is the missing denominator for the ~0.2%
* completion rate noted above. */
uint32_t _rssi_dc_blocked;
/* Silence tracking for deafness hunting. Reports only; the recovery
@@ -490,12 +329,8 @@ protected:
bool _image_cal_started;
bool _image_cal_confirming;
/* Start of the most recent transmit, for the post-TX quiet window the
* temperature poll waits out. Start-referenced rather than end-
* referenced so it can be stamped at the single startSendRaw() site
* instead of all five _tx_active clear paths; airtime is bounded by
* seconds and the quiet window by a minute, so the difference does not
* matter. */
/* Start of the last TX, for the drift poll's post-TX quiet window
* (start-referenced: one stamp site; seconds against a minute). */
uint32_t _last_tx_start_ms;
/* Config cache — skip redundant hwConfigure() */
@@ -531,10 +366,7 @@ private:
struct k_thread _tx_wait_thread;
struct k_sem _tx_start_sem;
bool _tx_thread_running;
/* Length of the transmit in flight, for txWaitBudgetMs(). Written by
* startSendRaw() before it releases _tx_start_sem, read by the wait
* thread after it takes that sem — the sem is the handoff, so no
* additional synchronisation is needed. */
/* Length of the TX in flight; _tx_start_sem is the handoff. */
uint16_t _tx_len;
/* Packet statistics */
@@ -0,0 +1,95 @@
/*
* SPDX-License-Identifier: MIT
* Noise-floor estimator: an EMA over idle-channel RSSI samples.
*
* Pure algorithm. LoRaRadioBase owns the sampler (when to read, the
* idle-RX guards, the median-of-N burst) and feeds each median in here.
*/
#pragma once
#include <stdint.h>
#include "radio_tuning.h"
namespace mesh {
/* Sorts `s` in place and returns the median (mean of the two middle values
* for even n). Insertion sort: n is 8. */
inline int16_t sortAndMedian(int16_t *s, int n)
{
for (int i = 1; i < n; i++) {
int16_t key = s[i];
int j = i - 1;
while (j >= 0 && s[j] > key) {
s[j + 1] = s[j];
j--;
}
s[j + 1] = key;
}
return (int16_t)((s[n / 2 - 1] + s[n / 2]) / 2);
}
class NoiseFloorEstimator {
public:
enum Result { SEEDED, REJECTED, UPDATED };
/* DEFAULT_NOISE_FLOOR (0) until the first sample. */
int floor() const { return _floor; }
bool seeded() const { return _floor != DEFAULT_NOISE_FLOOR; }
/* Ticks since seeding (wraps at 255; only the low bits matter). */
uint8_t tick() const { return _unguarded; }
/* Is `rssi` at the floor? Judged against the floor before the sample is
* folded in; an unseeded estimator calls everything quiet. */
bool isQuiet(int rssi) const
{
return !seeded() || rssi <= _floor + CAD_PROBE_RSSI_GUARD;
}
/* Fold one sample in, clamped to [floor_min, NOISE_FLOOR_CEILING_DBM].
* floor_min tracks the bandwidth (thermal noise is 10*log10(BW)).
*
* The first W = 2^EMA_SHIFT ticks accept every sample (a bad seed must
* not lock the real floor out); after that, samples more than
* SAMPLING_THRESHOLD above the floor are rejected as interference,
* except every Pth tick, so a sustained rise is still tracked. */
Result update(int rssi, int floor_min)
{
if (!seeded()) {
_floor = clamp(rssi, floor_min);
_unguarded = 0;
return SEEDED;
}
const int W = (1 << NOISE_FLOOR_EMA_SHIFT);
const int P = NOISE_FLOOR_UNGUARDED_INTERVAL;
bool warmup = (_unguarded < W);
bool periodic = (!warmup && (_unguarded & (P - 1)) == 0);
_unguarded++;
if (!warmup && !periodic &&
rssi >= _floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
return REJECTED;
}
/* floor += round_nearest((sample - floor) / W), symmetric in sign:
* plain >> biases down and plain / has a +-(W-1) dead zone. */
int diff = rssi - _floor;
int half = W / 2;
_floor = clamp(_floor + (diff + (diff > 0 ? half : -half)) / W, floor_min);
return UPDATED;
}
private:
static int clamp(int v, int floor_min)
{
if (v < floor_min) v = floor_min;
if (v > NOISE_FLOOR_CEILING_DBM) v = NOISE_FLOOR_CEILING_DBM;
return v;
}
int _floor = DEFAULT_NOISE_FLOOR;
uint8_t _unguarded = 0;
};
} /* namespace mesh */
+3 -8
View File
@@ -8,7 +8,7 @@
/* Native SX126x driver extension API */
extern "C" {
#include "sx126x_ext.h"
#include <zephyr/drivers/lora/sx126x_ext.h>
}
#include <zephyr/logging/log.h>
@@ -16,18 +16,13 @@ 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)
SX126xRadio::SX126xRadio(const struct device *lora_dev, MainBoard &board)
: LoRaRadioBase(lora_dev, board)
{
}
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)
+1 -2
View File
@@ -11,8 +11,7 @@ namespace mesh {
class SX126xRadio : public LoRaRadioBase {
public:
SX126xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
SX126xRadio(const struct device *lora_dev, MainBoard &board);
void begin() override;
+2 -12
View File
@@ -23,11 +23,8 @@ LOG_MODULE_REGISTER(sx127x_radio, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
namespace mesh {
K_THREAD_STACK_DEFINE(sx127x_tx_wait_stack, TX_WAIT_THREAD_STACK_SIZE);
SX127xRadio::SX127xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs)
: LoRaRadioBase(lora_dev, board, prefs)
SX127xRadio::SX127xRadio(const struct device *lora_dev, MainBoard &board)
: LoRaRadioBase(lora_dev, board)
{
/* SX127x has no RX boost feature — start with boost disabled */
_rx_boost_enabled = false;
@@ -37,13 +34,6 @@ SX127xRadio::SX127xRadio(const struct device *lora_dev, MainBoard &board,
_loramac_node = true;
}
void SX127xRadio::begin()
{
startTxThread(sx127x_tx_wait_stack,
K_THREAD_STACK_SIZEOF(sx127x_tx_wait_stack));
LoRaRadioBase::begin();
}
/* ── Hardware primitives ──────────────────────────────────────────────── */
bool SX127xRadio::hwConfigure(const struct lora_modem_config &cfg)
+1 -4
View File
@@ -16,10 +16,7 @@ namespace mesh {
class SX127xRadio : public LoRaRadioBase {
public:
SX127xRadio(const struct device *lora_dev, MainBoard &board,
NodePrefs *prefs = nullptr);
void begin() override;
SX127xRadio(const struct device *lora_dev, MainBoard &board);
/* SX127x has no RX boost feature — report unsupported so the
* radio.rxgain CLI can reply "Error: unsupported". */
+1 -96
View File
@@ -11,14 +11,7 @@
#include <zephyr/drivers/lora.h>
/* --- Noise floor calibration (EMA) ---
* Median-of-N RSSI reads → EMA. alpha = 1/8, converges in ~8 ticks.
* Samples above floor + SAMPLING_THRESHOLD rejected as interference. */
#define NOISE_FLOOR_EMA_SHIFT 3 /* alpha = 1/(1<<3) = 1/8 */
#define NOISE_FLOOR_SAMPLES_PER_TICK 8 /* median of 8 reads per tick */
#define NOISE_FLOOR_UNGUARDED_INTERVAL 16 /* ticks between unfiltered samples (power of 2) */
#define NOISE_FLOOR_SAMPLING_THRESHOLD 14 /* dB above floor to reject as interference */
#define DEFAULT_NOISE_FLOOR 0 /* sentinel: seed from first sample */
#include "radio_tuning.h"
/* --- RSSI read timing (SX1261/2 DS rev 2.2 Table 13-82) ---
*
@@ -92,94 +85,6 @@ static inline uint32_t rssi_settle_delay_us(uint16_t bw_khz)
/* Blocked attempts allowed before standing down to the next full interval. */
#define NOISE_FLOOR_MAX_RETRIES 2
/* --- Adaptive CAD (LBT detPeak calibration) ---
* Housekeeping-tick CAD probes accumulate per-level busy/free statistics;
* a one-sided staircase converges on the lowest detPeak offset whose
* false-positive rate stays under target. Levels are signed offsets from
* the chip family's per-SF base detPeak, so the C++ layer stays
* scale-independent. Those bases are per-SF AND per-bandwidth tables taken
* from Semtech's own reference stack (LoRa Basics Modem v4.9.0,
* ral_{sx126x,lr11xx}.c) plus the LR2021 datasheet's symbol-indexed Table 6-19;
* the flat "SX126x: SF+13 / LR: 56-68" this comment used to quote has not been
* the whole story since 2026-08-29. */
/* Operating-offset range (levels from the per-SF family base detPeak). Wide
* on purpose — a dense hilltop may need a much higher detPeak than a quiet
* valley node; the per-family absolute clamp in the driver (SX126x 12-48,
* LR11xx 40-100, LR20xx 48-90) is a firmware guardrail against "CAD
* never/always fires", NOT a chip limit (cadDetPeak is a full uint8_t). The
* clamps are exported via hwCadPeakMin/Max so cadLevelMinEff/MaxEff can narrow
* this window honestly where a base sits close to one of them.
* MUST match CAD_OFFSET_MIN/MAX in helpers/NodePrefs.h. */
#define CAD_LEVEL_MIN (-8) /* most sensitive probe level */
#define CAD_LEVEL_MAX 12 /* least sensitive probe level */
#define CAD_NUM_LEVELS (CAD_LEVEL_MAX - CAD_LEVEL_MIN + 1)
#define CAD_SWEEP_MIN (-4) /* dry-run sweep window (get cad with auto off) */
#define CAD_SWEEP_MAX 4
/* Knee-seeking staircase (replaces the earlier absolute-FP-target band). The
* FP-vs-detPeak curve falls as detPeak rises (less sensitive → fewer false
* detects) and flattens past a knee; the sweet spot is the knee — the most
* sensitive detPeak whose FP has already bottomed out. The controller reads
* the local curve SLOPE from three rungs (frontier op-1, operating op, op+1)
* rather than an absolute FP level, so it converges the same way regardless of
* a site's FP floor (which varies with traffic and classifier residual).
* - KNEE_SLOPE: the per-level FP change (permille) that counts as "steep".
* Below the knee the curve drops fast (step up toward the knee); at/above it
* the curve is flat (slope < KNEE_SLOPE).
* - PLATEAU_CLEAN: on a flat plateau, only reclaim sensitivity (step down) if
* FP is already this low — the guard that stops a flat-but-noisy curve from
* walking to the sensitive rail (there, holding is the least-bad move; a
* genuinely quiet flat-low site descends to the floor, which is correct). */
#define CAD_KNEE_SLOPE_PERMILLE 50 /* >=5%/level FP change = steep */
#define CAD_PLATEAU_CLEAN_PERMILLE 50 /* <=5% FP = clean enough to descend */
#define CAD_STEP_MIN_PROBES 120 /* per-level samples before a step call */
/* Airtime-protection cap on the TOTAL busy (defer) rate — false positives AND
* real traffic. The knee controller only minimises *false* busy, but on a
* congested hilltop most busy verdicts are real distant traffic we'd never
* actually collide with (capture effect), and deferring for all of it starves
* the node's own airtime. When the operating level's busy rate exceeds the
* cap, step UP (less sensitive) regardless of FP — self-targeting, since a
* quiet node's busy rate never reaches it. HYST keeps a descend from bouncing
* straight back into the cap. The cap itself is a per-node pref
* (`cad_busycap`, percent, `set cad.busycap`; default 15, 0 = off) since it is
* a policy call (airtime vs. collision/capture), not a physical constant. */
/* Descend hysteresis, as a PERCENTAGE OF THE CAP rather than a fixed permille
* subtrahend. It used to be a flat 100‰, which works at the old default cap of
* 25% (threshold 15%, hysteresis 40% of the cap) and degenerates as the cap
* falls: at cap 15% the threshold is 5%, and at cap 10% — the value
* docs/ADAPTIVE_CAD.md recommends for saturated hilltops, and also the CLI's
* minimum non-zero setting — it reaches ZERO. There, descent requires exactly
* zero busy probes at the frontier, so the staircase becomes a one-way upward
* ratchet at precisely the setting the documentation recommends.
*
* 40% reproduces the old behaviour exactly at cap 25 and holds that same share
* at every other setting. */
#define CAD_BUSY_DEFER_HYST_PCT 40 /* descend only if frontier busy <= 60% of cap */
/* Safety rung thresholds — see LoRaRadioBase::cadSafetyStep().
*
* The airtime cap above is a SAFETY, not an optimisation, so it runs whether or
* not `cad.auto` is on. A detPeak so sensitive that CAD never clears leaves the
* node unable to transmit at all, and the three settings that used to gate the
* whole staircase (`cad.auto off`, `cad.busycap 0`, and a level not yet warm to
* CAD_STEP_MIN_PROBES) are exactly what a hand-tuning operator turns off — the
* CLI help for `set cad.auto` recommends that workflow by name.
*
* PATHOLOGICAL is the fast path for the unambiguous case. Proving a MARGINAL
* busy rate against the cap needs the full 120 samples, but a level that trips
* on essentially every probe needs far fewer to be certain, and it is precisely
* the case where waiting half an hour is unacceptable. It also ignores
* `cad.busycap` entirely: a cap of 0 means "do not trade airtime for
* sensitivity", which is a policy about a working detector, not permission to
* sit mute. */
#define CAD_SAFETY_MIN_PROBES 20 /* samples before the fast path may act */
#define CAD_SAFETY_PATHOLOGICAL_PERMILLE 900 /* >=90% busy = detector, not channel */
#define CAD_PROBE_RSSI_GUARD 7 /* dB above floor = channel visibly busy, skip probe */
#define CAD_STATS_DECAY_MS (6UL * 3600UL * 1000UL) /* halve counters every 6 h */
/* NOTE: the probe has no retry deadline and no wake of its own. It runs off
* the noise-floor sampler's measurement (LoRaRadioBase::cadMaintenance), which
* already applies the idle-RX guards and yields a median-of-8. Consequently
* the effective probe rate is quantised to NOISE_FLOOR_INTERVAL_MS: setting
* probe_interval below that just gets one probe per floor sample. */
/* --- RX ring buffer --- */
#define RX_RING_SIZE 8 /* ~2 KB; buffers burst arrivals at SF7/BW500 */
+36
View File
@@ -0,0 +1,36 @@
/*
* SPDX-License-Identifier: MIT
* Tuning constants of the radio algorithms (noise-floor estimator, adaptive
* CAD controller). Zephyr-free so the algorithms build on the host for unit
* tests. Rationale and measurements: devdocs/lld/03-radio-contract.md §13.
*/
#pragma once
/* --- Noise floor (EMA of median-of-N idle RSSI) --- */
#define NOISE_FLOOR_EMA_SHIFT 3 /* alpha = 1/(1<<3) = 1/8; also the warm-up tick count */
#define NOISE_FLOOR_SAMPLES_PER_TICK 8 /* median of 8 reads per tick */
#define NOISE_FLOOR_UNGUARDED_INTERVAL 16 /* ticks between unfiltered samples (power of 2) */
#define NOISE_FLOOR_SAMPLING_THRESHOLD 14 /* dB above floor to reject as interference */
#define NOISE_FLOOR_CEILING_DBM (-50) /* upper clamp */
#define DEFAULT_NOISE_FLOOR 0 /* sentinel: seed from first sample */
/* --- Adaptive CAD (LBT detPeak calibration) ---
* Levels are signed offsets from the chip family's per-SF/per-BW base detPeak.
* The window must match CAD_OFFSET_MIN/MAX in helpers/NodePrefs.h. */
#define CAD_LEVEL_MIN (-8) /* most sensitive probe level */
#define CAD_LEVEL_MAX 12 /* least sensitive probe level */
#define CAD_NUM_LEVELS (CAD_LEVEL_MAX - CAD_LEVEL_MIN + 1)
#define CAD_SWEEP_MIN (-4) /* dry-run sweep window (auto off) */
#define CAD_SWEEP_MAX 4
/* Knee-seeking staircase: read the FP-vs-level slope from three rungs. */
#define CAD_KNEE_SLOPE_PERMILLE 50 /* >=5%/level FP change = steep */
#define CAD_PLATEAU_CLEAN_PERMILLE 50 /* <=5% FP = clean enough to descend */
#define CAD_STEP_MIN_PROBES 120 /* per-level samples before a step call */
/* Descend only if the frontier's busy rate is <= (100 - HYST)% of the cap. */
#define CAD_BUSY_DEFER_HYST_PCT 40
/* Safety rung (runs regardless of cad.auto; see CadController::safetyStep). */
#define CAD_SAFETY_MIN_PROBES 20 /* samples before the fast path may act */
#define CAD_SAFETY_PATHOLOGICAL_PERMILLE 900 /* >=90% busy = detector, not channel */
#define CAD_PROBE_RSSI_GUARD 7 /* dB above floor = channel visibly busy, skip probe */
#define CAD_STATS_DECAY_MS (6UL * 3600UL * 1000UL) /* halve counters every 6 h */
+4 -9
View File
@@ -7,7 +7,6 @@
#include "observer_creds.h"
#include <mesh/Utils.h>
#include <helpers/LoRaConfig.h>
#include <adapters/radio/LoRaRadioBase.h>
#include <adapters/rng/ZephyrRNG.h> /* generateFirstBootIdentity (hardened keygen) */
#include <helpers/MeshcoreJson.h>
@@ -447,8 +446,7 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size)
if (f >= 300.0f && f <= 1000.0f) {
_prefs.freq = f;
_store->savePrefs(_prefs);
((LoRaRadioBase *)_radio)->reconfigureWithParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
((LoRaRadioBase *)_radio)->reconfigure();
snprintf(reply, reply_size, "freq=%.3f MHz", (double)_prefs.freq);
} else {
snprintf(reply, reply_size, "ERR freq must be 300-1000 MHz");
@@ -459,8 +457,7 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size)
if (sf >= 7 && sf <= 12) {
_prefs.sf = (uint8_t)sf;
_store->savePrefs(_prefs);
((LoRaRadioBase *)_radio)->reconfigureWithParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
((LoRaRadioBase *)_radio)->reconfigure();
snprintf(reply, reply_size, "sf=%u", _prefs.sf);
} else {
snprintf(reply, reply_size, "ERR sf must be 7-12");
@@ -484,8 +481,7 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size)
if (bw >= ZC_RADIO_BW_MIN_KHZ && bw <= ZC_RADIO_BW_MAX_KHZ) {
_prefs.bw = bw;
_store->savePrefs(_prefs);
((LoRaRadioBase *)_radio)->reconfigureWithParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
((LoRaRadioBase *)_radio)->reconfigure();
snprintf(reply, reply_size, "bw=%.2f kHz", (double)_prefs.bw);
} else {
snprintf(reply, reply_size, "ERR bw must be 7-500 kHz (or index 0-5)");
@@ -496,8 +492,7 @@ bool ObserverMesh::handleCLI(const char *command, char *reply, int reply_size)
if (cr >= 5 && cr <= 8) {
_prefs.cr = (uint8_t)cr;
_store->savePrefs(_prefs);
((LoRaRadioBase *)_radio)->reconfigureWithParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
((LoRaRadioBase *)_radio)->reconfigure();
snprintf(reply, reply_size, "cr=%u", _prefs.cr);
} else {
snprintf(reply, reply_size, "ERR cr must be 5-8");
+6 -5
View File
@@ -5,6 +5,7 @@
#include "RepeaterDataStore.h"
#include "../adapters/datastore/ZephyrFsFormat.h"
#include <helpers/LoRaConfig.h>
#include <zephyr/fs/fs.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
@@ -325,11 +326,11 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
prefs.bw < ZC_RADIO_BW_MIN_KHZ || prefs.bw > ZC_RADIO_BW_MAX_KHZ) {
LOG_WRN("Invalid radio params in prefs, using defaults: freq=%.3f sf=%u bw=%.1f",
(double)prefs.freq, prefs.sf, (double)prefs.bw);
prefs.freq = 869.618f;
prefs.bw = 62.5f;
prefs.sf = 7;
prefs.cr = 5;
prefs.tx_power_dbm = 22;
prefs.freq = mesh::LoRaConfig::FREQ_MHZ;
prefs.bw = mesh::LoRaConfig::BANDWIDTH_KHZ;
prefs.sf = mesh::LoRaConfig::SPREADING_FACTOR;
prefs.cr = mesh::LoRaConfig::CODING_RATE;
prefs.tx_power_dbm = mesh::LoRaConfig::TX_POWER_DBM;
}
/* Everything else that came off flash — bounds, NaNs, and the char fields,
* which the file format stores without terminators. */
-6
View File
@@ -1075,12 +1075,6 @@ void RepeaterMesh::begin(RepeaterDataStore* store) {
}
}
/* NOTE: Radio configuration is handled by SX126xRadio adapter using
* LoRaConfig defaults. The repeater uses the same radio params as companion.
* Dynamic radio reconfiguration (via CLI) is not yet supported - radio
* uses compile-time defaults from LoRaConfig. This avoids EBUSY errors
* from trying to reconfigure while radio is in async RX mode. */
updateAdvertTimer();
updateFloodAdvertTimer();
-6
View File
@@ -816,12 +816,6 @@ void RoomServerMesh::begin(RepeaterDataStore* store) {
}
}
/* NOTE: Radio configuration is handled by SX126xRadio adapter using
* LoRaConfig defaults. The room server uses the same radio params as companion.
* Dynamic radio reconfiguration (via CLI) is not yet supported - radio
* uses compile-time defaults from LoRaConfig. This avoids EBUSY errors
* from trying to reconfigure while radio is in async RX mode. */
updateAdvertTimer();
updateFloodAdvertTimer();
+8 -19
View File
@@ -274,19 +274,17 @@ static mesh::ZephyrMillisecondClock s_ms_clock;
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
/* Static-init placeholder only. The radio is rebound to the mesh's own
* NodePrefs via setPrefs() in main() before observer_mesh.begin() — this object
* is not the live radio configuration and is never loaded from flash. */
static NodePrefs s_radio_prefs;
/* Radio is constructed with no prefs pointer; main() binds it to the mesh's
* NodePrefs via setPrefs() before observer_mesh.begin(). */
#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110)
static mesh::LR1110Radio lora_radio(lora_dev, s_board, &s_radio_prefs);
static mesh::LR1110Radio lora_radio(lora_dev, s_board);
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021)
static mesh::LR2021Radio lora_radio(lora_dev, s_board, &s_radio_prefs);
static mesh::LR2021Radio lora_radio(lora_dev, s_board);
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X)
static mesh::SX127xRadio lora_radio(lora_dev, s_board, &s_radio_prefs);
static mesh::SX127xRadio lora_radio(lora_dev, s_board);
#else
static mesh::SX126xRadio lora_radio(lora_dev, s_board, &s_radio_prefs);
static mesh::SX126xRadio lora_radio(lora_dev, s_board);
#endif
static mesh::ObserverMesh observer_mesh(lora_radio, s_ms_clock, s_rtc_clock);
@@ -296,13 +294,6 @@ static RepeaterDataStore data_store;
int main(void)
{
/* Initialize radio prefs with observer-specific defaults */
initNodePrefs(&s_radio_prefs);
s_radio_prefs.cr = 5; /* CR 4/5 (same as initNodePrefs; kept explicit) */
s_radio_prefs.tx_power_dbm = 0; /* observer never TXes */
strncpy(s_radio_prefs.node_name, "Observer",
sizeof(s_radio_prefs.node_name) - 1);
/* Brief boot delay — lets USB enumerate before first log */
k_sleep(K_MSEC(1500));
LOG_INF("=== ZephCore Observer starting ===");
@@ -352,10 +343,8 @@ int main(void)
*
* The radio reads freq/bw/sf/cr through this pointer, both during
* begin() → Dispatcher::begin() → Radio::begin() and on every later
* reconfigure() (LoRaRadioBase::reconfigureWithParams() ignores its
* arguments and re-reads the pointer). Constructed against
* s_radio_prefs, which is never loaded from flash, the radio stayed on
* the compiled-in defaults forever: `set freq/sf/bw/cr` wrote flash and
* reconfigure(). Bound to a placeholder that was never loaded from
* flash, the radio stayed on the compiled-in defaults forever: `set freq/sf/bw/cr` wrote flash and
* updated the CLI/MQTT readback but never reached the hardware, so the
* setting looked accepted and then "reverted" on reboot.
*
+2 -2
View File
@@ -252,8 +252,8 @@ void CommonCLI::savePrefs() {
* base costs an hour or two; carrying a stale offset across can leave the node
* either transmitting over live receptions or unable to transmit at all.
*
* The operating offset lives in two places — _prefs->cad_offset, and _cad_offset
* inside the radio, which applyCadPrefs() reloads through setCadParams().
* The operating offset lives in two places — _prefs->cad_offset, and the radio's
* CadController, which applyCadPrefs() reloads through setCadParams().
*
* preset_pending = the caller has already written the NEW freq/bw/sf/cr to
* _prefs but the radio is still running the OLD preset (frozen by
+11 -4
View File
@@ -1,6 +1,10 @@
/*
* SPDX-License-Identifier: MIT
* ZephCore LoRa default parameters
*
* The one source of radio defaults. They only seed NodePrefs (initNodePrefs(),
* RepeaterDataStore's invalid-prefs fallback); the radio itself always reads
* prefs. Units match NodePrefs.
*/
#pragma once
@@ -10,12 +14,15 @@
namespace mesh {
struct LoRaConfig {
static constexpr uint32_t FREQ_HZ = 869618000; /* MeshCore default channel (EU 869 MHz ISM) */
static constexpr uint16_t BANDWIDTH = 62; /* BW_62_KHZ — MeshCore default */
static constexpr float FREQ_MHZ = 869.618f; /* MeshCore default channel (EU 869 MHz ISM) */
static constexpr float BANDWIDTH_KHZ = 62.5f; /* MeshCore default */
static constexpr uint8_t SPREADING_FACTOR = 7; /* SF7: HU regional default since 2026-08 */
static constexpr uint8_t CODING_RATE = 5; /* CR_4_5: lowest overhead */
static constexpr uint16_t PREAMBLE_LEN = 16; /* MeshCore default; longer aids RX sync */
static constexpr uint8_t CODING_RATE = 5; /* CR 4/5: lowest overhead */
#ifdef CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM
static constexpr int8_t TX_POWER_DBM = CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM; /* per board (external PAs) */
#else
static constexpr int8_t TX_POWER_DBM = 22; /* SX1262 max */
#endif
};
} /* namespace mesh */
+7 -11
View File
@@ -10,6 +10,7 @@
#include <stdint.h>
#include <string.h>
#include "LoRaConfig.h"
/* LEDS_RADIO_* / LEDS_HB_* mode values for the leds_radio_mode and leds_hb_mode
* fields below. They live in led_gate.h because the heartbeat consumers are C
@@ -241,7 +242,6 @@ struct NodePrefs {
uint8_t v_contact_flags;
};
/* Default prefs -- must match LoRaConfig.h defaults for radio interop. */
/* Range guards for prefs that came off flash.
*
* The atomic replace in every savePrefs() plus littlefs's own CRCs make a torn
@@ -391,16 +391,12 @@ static inline void initNodePrefs(NodePrefs* prefs) {
#ifdef CONFIG_ZEPHCORE_GUEST_PASSWORD
strncpy(prefs->guest_password, CONFIG_ZEPHCORE_GUEST_PASSWORD, sizeof(prefs->guest_password) - 1);
#endif
/* Radio params - MUST match LoRaConfig.h for interop with companion nodes */
prefs->freq = 869.618f; // LoRaConfig::FREQ_HZ / 1000000.0
prefs->bw = 62.5f; // LoRaConfig::BANDWIDTH
prefs->sf = 7; // LoRaConfig::SPREADING_FACTOR
prefs->cr = 5; // CR 4/5 (MeshCore uses 5-8 for CR 4/5 through 4/8)
#ifdef CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM
prefs->tx_power_dbm = CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM;
#else
prefs->tx_power_dbm = 22; // LoRaConfig::TX_POWER_DBM
#endif
/* Radio params */
prefs->freq = mesh::LoRaConfig::FREQ_MHZ;
prefs->bw = mesh::LoRaConfig::BANDWIDTH_KHZ;
prefs->sf = mesh::LoRaConfig::SPREADING_FACTOR;
prefs->cr = mesh::LoRaConfig::CODING_RATE; /* 5-8 = CR 4/5 through 4/8 */
prefs->tx_power_dbm = mesh::LoRaConfig::TX_POWER_DBM;
prefs->disable_fwd = 0;
prefs->advert_interval = 0; // 0 = periodic local advert off; else minutes = value * 2
prefs->flood_advert_interval = 47; // hours
+6 -10
View File
@@ -63,18 +63,16 @@ public:
*/
virtual void onSendFinished() = 0;
/**
* \brief do any processing needed on each loop cycle
*/
virtual void loop() { }
// ZEPHCORE: no loop() -- the Dispatcher never polls the radio; periodic radio
// work is deadline-driven through msUntilNextMaintenance()/radioMaintenance().
virtual int getNoiseFloor() const { return 0; }
virtual void triggerNoiseFloorCalibrate(int threshold) { }
virtual void setCADEnabled(bool enable) { }
virtual void resetAGC() { }
// ZEPHCORE: no setCADEnabled() (CAD is always LBT inside the driver) and no
// resetAGC() (the AGC unstick is part of radioMaintenance()). Left out so a
// port calling them fails to compile instead of silently doing nothing.
virtual bool isInRecvMode() const = 0;
@@ -86,9 +84,7 @@ public:
virtual float getLastRSSI() const { return 0; }
virtual float getLastSNR() const { return 0; }
// ZEPHCORE: radio extensions used by the Dispatcher and the roles. CAD is
// always LBT inside the driver (setCADEnabled unused) and the AGC unstick is
// part of radioMaintenance() (resetAGC unused).
// ZEPHCORE: radio extensions used by the Dispatcher and the roles.
// Radio not command-ready (e.g. BUSY high); the Dispatcher defers TX.
virtual bool isRadioReady() { return true; }
+8 -10
View File
@@ -70,11 +70,8 @@ public:
virtual void onPacketReceived() { }
virtual void reboot() = 0;
virtual void powerOff() { /* no op */ }
// Called by example setup() functions to signal that boot is complete.
// Boards may override to stop a boot-indicator LED sequence or similar.
// Default no-op: boards that don't care need not implement anything.
virtual void onBootComplete() { /* no op */ }
virtual uint32_t getIRQGpio() { return -1; } // not supported. Returns DIO1 (SX1262) and DIO0 (SX127x)
// ZEPHCORE: no onBootComplete() (Arduino setup() hook) and no getIRQGpio()
// (Arduino sleep-wake pin) -- nothing on Zephyr would call them.
virtual void sleep(uint32_t secs) { /* no op */ }
virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {}
@@ -83,10 +80,10 @@ public:
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
// Power management interface (boards with power management override these)
virtual bool isPwrMgtInitialised() const { return false; }
// ZEPHCORE: no isPwrMgtInitialised() / getWakeLpcompSupported() -- upstream's
// nRF52 Arduino power manager; Zephyr PM covers this.
virtual bool isExternalPowered() { return false; }
virtual uint16_t getBootVoltage() { return 0; }
virtual bool getWakeLpcompSupported() const { return false; }
virtual uint32_t getResetReason() const { return 0; }
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
virtual uint8_t getShutdownReason() const { return 0; }
@@ -94,9 +91,10 @@ public:
virtual bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { return false; }
// Called from the example main loops. Lets a board feed its watchdog and
// run periodic housekeeping. Default no-op.
virtual void loop() { /* no op */ }
// ZEPHCORE: no loop() -- upstream polls it from the Arduino main loop. The mesh
// thread here is event-driven and never polls; a board with periodic work owns
// a timer or work item. Left out so a port calling board.loop() fails to
// compile instead of silently doing nothing.
};
/**
@@ -17,7 +17,7 @@
#include <string.h>
#include <math.h>
#include "lr11xx_lora.h"
#include <zephyr/drivers/lora/lr11xx_lora.h>
#include "lr11xx_hal_zephyr.h"
#include "lr11xx_radio.h"
#include "lr11xx_radio_types.h"
@@ -2191,7 +2191,7 @@ static uint8_t lr11xx_cad_detect_peak(uint8_t sf, uint16_t bw_khz, uint8_t symb_
*
* 40 is DELIBERATELY left where it was when the sub-125 row was measured down
* to 45 (base 44 after the 4-symbol correction), which means it now binds:
* 44 + CAD_LEVEL_MIN(-8) = 36 is below it, so cadLevelMinEff() narrows the
* 44 + CAD_LEVEL_MIN(-8) = 36 is below it, so CadController::levelMinEff() narrows the
* offset window to -4..+12 at SF6 and SF7 below BW125. That narrowing is
* intended, and it must not be "fixed" by lowering this constant.
*
@@ -17,7 +17,7 @@
#include <string.h>
#include <math.h>
#include "lr20xx_lora.h"
#include <zephyr/drivers/lora/lr20xx_lora.h>
#include "lr20xx_hal_zephyr.h"
#include "lr20xx_radio_common.h"
#include "lr20xx_radio_common_types.h"
@@ -6,8 +6,8 @@ sx126x_hal_stm32wl.c and sx126x_regs.h -- plus the driver's devicetree binding
dts/bindings/lora/semtech,sx126x-base.yaml, which it extends with
lna-bypass-gpios (see "FEM receive-path select" below). No other patch in this
directory touches those files; see "Why this is one patch" at the end. The
extension-API header sx126x_ext.h is not patched in, it is copied from
patches/zephyr-new/.
extension-API header is not patched in: it is copied from patches/zephyr-new/
to the public include/zephyr/drivers/lora/sx126x_ext.h.
== Base driver ==
@@ -696,7 +696,7 @@ index c8e682ee8..b5ab10c4e 100644
#include <zephyr/sys/byteorder.h>
#include "sx126x.h"
+#include "sx126x_ext.h"
+#include <zephyr/drivers/lora/sx126x_ext.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sx126x, CONFIG_LORA_LOG_LEVEL);
+9 -14
View File
@@ -731,25 +731,25 @@ static ZephyrDataStore data_store(rtc_clock);
#ifdef ZEPHCORE_LORA
static mesh::ZephyrBoard zephyr_board;
/* NodePrefs placeholder - will be set from CompanionMesh */
static NodePrefs temp_prefs;
/* Radio is constructed with no prefs pointer; main() binds it to
* companion_mesh.prefs via setPrefs() before companion_mesh.begin(). */
#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110)
/* LR1110 via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board, &temp_prefs);
static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board);
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR2021)
/* LR2021 via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LR2021Radio lora_radio(lora_dev, zephyr_board, &temp_prefs);
static mesh::LR2021Radio lora_radio(lora_dev, zephyr_board);
#elif IS_ENABLED(CONFIG_ZEPHCORE_RADIO_SX127X)
/* SX127x via Zephyr loramac-node driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::SX127xRadio lora_radio(lora_dev, zephyr_board, &temp_prefs);
static mesh::SX127xRadio lora_radio(lora_dev, zephyr_board);
#else
/* SX126x via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board, &temp_prefs);
static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board);
#endif
static uint16_t get_battery_mv(void)
@@ -1554,8 +1554,9 @@ int main(void)
LOG_INF("BLE passkey loaded from prefs: %06u", companion_mesh.prefs.ble_pin);
}
/* Copy prefs to temp_prefs for radio (radio was initialized before companion_mesh) */
temp_prefs = companion_mesh.prefs;
/* Bind the radio to the live prefs before begin(): the radio reads
* freq/bw/sf/cr/power through this pointer from Radio::begin() on. */
lora_radio.setPrefs(&companion_mesh.prefs);
/* Load contacts and channels */
data_store.loadContacts(&companion_mesh);
@@ -1616,12 +1617,6 @@ int main(void)
/* Start mesh */
companion_mesh.begin();
/* Redirect radio prefs pointer to the live companion_mesh.prefs.
* The radio was constructed with &temp_prefs (a one-time copy needed
* for static init). After begin(), point at the real prefs so that
* CMD_SET_RADIO_PARAMS changes are visible to lora_radio.reconfigure(). */
lora_radio.setPrefs(&companion_mesh.prefs);
/* Queue the restart-reason notice from the v-contact. Deferred to here so
* begin() has derived the v-contact key, prefs (enable flag) are loaded,
* and the hardware-RTC restore above gives the message a sane timestamp.
+1 -1
View File
@@ -756,7 +756,7 @@ int main(void)
* Dispatcher::begin() → Radio::begin(). Without this, the radio would
* configure on NodePrefs defaults (869.618 MHz) regardless of saved
* settings: CLI readback looked correct but the hardware stayed on EU.
* Mirrors the temp_prefs pattern in main_companion.cpp. */
* Every role binds this way; begin() refuses to start without it. */
data_store.loadPrefs(*repeater_mesh.getNodePrefs());
lora_radio.setPrefs(repeater_mesh.getNodePrefs());
+1 -1
View File
@@ -641,7 +641,7 @@ int main(void)
* Dispatcher::begin() → Radio::begin(). Without this, the radio would
* configure on NodePrefs defaults (869.618 MHz) regardless of saved
* settings: CLI readback looked correct but the hardware stayed on EU.
* Mirrors the temp_prefs pattern in main_companion.cpp. */
* Every role binds this way; begin() refuses to start without it. */
data_store.loadPrefs(*room_mesh.getNodePrefs());
lora_radio.setPrefs(room_mesh.getNodePrefs());