Files
ZephCore/zephcore/adapters/radio/CadController.h
T
liquidraver 10b36094ae radio: one LoRaRadio adapter over per-family ops tables; lr20xx cleanup
- LoRaRadioBase -> LoRaRadio (final); SX126x/SX127x/LR1110/LR2021Radio
  replaced by const LoRaRadioOps tables (radio_ops_<family>.cpp), one
  linked per build; mains construct the radio in one line
- lr20xx: vendored Semtech SDK + HAL moved beside the driver (no app-tree
  sources), DIO1 handler split (CCN 52 -> 10), zc_lora_timing.h adopted,
  dead exports removed, header rewritten, comment diet (image-identical)
- semtech,lr2021 uses the lr1110 property names (irq-gpios, tcxo-voltage,
  rfsw-*), compiled config byte-identical
- correct the stale "LR2021 is not CAD-probed" claim
2026-09-23 19:51:08 +02:00

125 lines
4.1 KiB
C++

/*
* SPDX-License-Identifier: MIT
* Adaptive CAD controller: learns the LBT detPeak offset from probe verdicts.
*
* Pure algorithm over a four-call hardware interface. LoRaRadio 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 */