mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 20:19:56 +00:00
- 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
125 lines
4.1 KiB
C++
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 */
|