mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 07:49:57 +00:00
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:
@@ -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
@@ -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 ==========
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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". */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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 */
|
||||
@@ -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");
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user