mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-16 17:22:41 +00:00
properly reset CAD offset when needed
This commit is contained in:
+17
-1
@@ -237,6 +237,11 @@ Compact output (kept short so it survives a truncated LoRa reply):
|
||||
Header: `a` auto on/off · `o` operating offset · `pk` operating detPeak ·
|
||||
`b` family base · `4s` 4 symbols · `sp` RSSI burst quality · `bc` busy cap.
|
||||
|
||||
`a:tmp` means a `tempradio` window is open: the node is on a preset it is
|
||||
only visiting, so adaptation is suspended and `o`/`pk` describe the visit
|
||||
rather than the configured preset. The learned offset is untouched and
|
||||
comes back when the window reverts.
|
||||
|
||||
**`sp` — are the noise-floor sampler's reads independent?** The floor
|
||||
sampler takes a median of 8 RSSI reads. The chip refreshes RSSI only once
|
||||
per averaging window (~16 µs at BW 62.5, ~134 µs at BW 7.8); reads issued
|
||||
@@ -324,7 +329,18 @@ a knee to resolve clearly, longer to capture day/night variation.
|
||||
| `set cad.offset <n>` | 0 | Operating offset, −8…12. Negative = more sensitive. Applied live. |
|
||||
| `set probe.interval <sec>` | 15 | Shared cadence for the noise-floor sample and the CAD probe that consumes it; 0 disables probing (and freezes auto), 10–255 otherwise. |
|
||||
| `set cad.busycap <pct>` | 15 | Faint-tolerance / airtime cap: raise detPeak once more than this % of (quiet-moment) probes trip on faint signals. Lower = reject faint/echo harder (busy backbones); 0 = off. 10–90 otherwise. |
|
||||
| `set cad.reset` | | Clear accumulated statistics (RAM only). |
|
||||
| `set cad.reset` | | Full reset: clear the statistics **and** return the offset to the family base. |
|
||||
|
||||
A full reset also happens by itself whenever the radio moves to a different
|
||||
preset — `set radio`/`set freq` (at the moment the command is accepted, so
|
||||
the new preset boots clean), or the app's set-radio-params on a companion
|
||||
(live). Coding rate is excluded: it changes airtime, not the channel or the
|
||||
per-SF/per-bandwidth base, so a cr-only edit keeps a converged offset.
|
||||
Without this the persisted offset was re-anchored onto the new preset's base
|
||||
to preserve the absolute detPeak it named — correct after a firmware
|
||||
base-table change, badly wrong after an SF or bandwidth change, where the
|
||||
base table's own step is the physics (SF7→SF12 is 5 counts on SX126x and 16
|
||||
on LR11xx, enough to rail the offset).
|
||||
|
||||
All settings persist in prefs and apply to every role — repeater, room
|
||||
server, and companion (companions reach the CLI via the v-contact admin
|
||||
|
||||
@@ -204,7 +204,7 @@ Caveats: the `sys=` tally needs `CONFIG_ZEPHCORE_GPS_SAT_DIAG` (default on for r
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>` | Apply temporary radio parameters; automatically reverts after `timeout_mins`. Constraints: freq 150–2500 MHz, bw 7–500 kHz, sf 5–12, cr 5–8. Saved prefs are never mutated — concurrent `set` commands and reboots both restore the real saved values. |
|
||||
| `tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>` | Apply temporary radio parameters; automatically reverts after `timeout_mins`. Constraints: freq 150–2500 MHz, bw 7–500 kHz, sf 5–12, cr 5–8. Saved prefs are never mutated — concurrent `set` commands and reboots both restore the real saved values. That now covers adaptive CAD too: the visited preset runs at its own base detPeak (the learned offset addresses a different ladder there), the staircase is suspended for the duration so no visitor value can reach prefs, and the revert restores the learned offset. `get cad` shows `a:tmp` while the window is open. The anti-mute TX-starvation safety still runs, on the visitor's offset. |
|
||||
|
||||
---
|
||||
|
||||
@@ -317,8 +317,8 @@ four radio parameters together, since they are one interop-critical set.
|
||||
|---------|-------------|-------------|
|
||||
| `set name <name>` | No `[ ] \ : , ? *` | Set node name |
|
||||
| `set repeat <on\|off>` | | Enable or disable packet forwarding |
|
||||
| `set radio <freq>,<bw>,<sf>,<cr>` | freq 150–2500, bw 7–500, sf 5–12, cr 5–8 | **Comma-separated**, not space-separated — spaces parse as a single argument and the command is rejected. Set radio params *(reboot required)* |
|
||||
| `set freq <mhz>` | 150–2500 *(USB only)* | Set frequency alone *(reboot required)* |
|
||||
| `set radio <freq>,<bw>,<sf>,<cr>` | freq 150–2500, bw 7–500, sf 5–12, cr 5–8 | **Comma-separated**, not space-separated — spaces parse as a single argument and the command is rejected. Set radio params *(reboot required)*. If freq, bw or sf actually changed, this also performs a full `set cad.reset` and replies `OK - reboot to apply (CAD reset)` — the learned detPeak offset and its probe statistics belong to the old preset. The radio keeps running the old preset, at its learned offset, until the reboot; CAD adaptation is suspended in the meantime so it cannot re-dirty the reset. A cr-only change resets nothing. |
|
||||
| `set freq <mhz>` | 150–2500 *(USB only)* | Set frequency alone *(reboot required)*. Same CAD reset as `set radio` when the frequency actually changes. |
|
||||
| `set tx <dbm>` | −9 to board max (default 30) | Set TX power |
|
||||
| `set lat <latitude>` | | Set stored latitude |
|
||||
| `set lon <longitude>` | | Set stored longitude |
|
||||
@@ -359,7 +359,7 @@ four radio parameters together, since they are one interop-critical set.
|
||||
| `set cad.offset <n>` | −8 to 12, default 0 | Operating detPeak offset from the chip family's base for the current SF, bandwidth and CAD symbol count (Semtech LoRa Basics Modem reference tables; SX126x ~18–34, LR11xx ~50–85, LR20xx its own symbol-indexed table). Negative = more sensitive LBT (catches weaker signals, risks false busy), positive = less sensitive. Wide range so dense hilltops / quiet valleys can settle far from base. The per-family absolute clamp in the driver (SX126x 12–48, LR11xx 40–100, LR20xx 48–90) is a firmware guardrail against a CAD that never/always fires, not a chip limit (`cadDetPeak` is a full `uint8_t`); the driver reports it so the controller narrows this range to match rather than exploring offsets that collapse onto one peak. Applied live; the auto staircase may move it later if `cad.auto` is on. |
|
||||
| `set probe.interval <sec>` | 0 (off) or 10–255, default **15** | Seconds between periodic radio measurements. ONE reading serves both: the noise-floor RSSI sample (median of 8) and the CAD calibration probe, which consumes that same reading rather than measuring separately — so this is also the noise-floor sampling rate, and it sets how often an idle repeater wakes. Default 15 s → ~1–2 h CAD staircase response; the floor EMA warms up over 8 samples (~2 min) and its unguarded bypass runs every 16th (~4 min). Longer = fewer wakes, slower to track a changing RF environment. 0 disables CAD probing entirely (also freezes auto adaptation); the floor sampler then falls back to its build-time default. |
|
||||
| `set cad.busycap <pct>` | 0 (off) or 10–90, default **15** | Faint-tolerance / airtime cap: the max percentage of **quiet-moment probes** that may trip on a faint signal before the staircase backs off to a less sensitive detPeak. Not a percentage of TX attempts — probes are prefiltered to quiet moments, so strong traffic never enters the statistic. On a congested hilltop most busy verdicts are distant traffic won on capture anyway, so deferring for all of it starves the node's own airtime. Self-targeting: a quiet node's busy rate never reaches the cap. Shown as `bc:` in `get cad.stats`. 0 disables the cap (pure knee-seeking). |
|
||||
| `set cad.reset` | | Full CAD reset: clears the accumulated per-level probe statistics **and** returns the operating detPeak offset to the family base (`cad.offset` = 0), applying it to the radio live and persisting it. Use after a change to the base tables, or to undo a staircase that has walked somewhere unhelpful. Before 1.17.4 this cleared only the statistics, leaving the node to re-converge *from* the walked offset with no evidence for why it was there. |
|
||||
| `set cad.reset` | | Full CAD reset: clears the accumulated per-level probe statistics **and** returns the operating detPeak offset to the family base (`cad.offset` = 0), applying it to the radio live and persisting it. Use after a change to the base tables, or to undo a staircase that has walked somewhere unhelpful. Before 1.17.4 this cleared only the statistics, leaving the node to re-converge *from* the walked offset with no evidence for why it was there. `set radio`, `set freq` and the companion app's set-radio-params now run this automatically when they move freq, bandwidth or SF, so it is only needed by hand after a firmware base-table change. |
|
||||
| `set extra.sf <sf> [sf] [sf]` | up to 3 SFs, `0`/`off` clears | **LR2021 only** (`Error: unsupported` elsewhere) — LoRa *side detectors*: demodulate up to three extra spreading factors concurrently with `sf`, on the same bandwidth, so one repeater can serve several SF communities. Which SF a packet arrived on is a chip-side readout, not a guess. Chip constraints, enforced in the driver and reported as `Error: unsupported or invalid extra SF config`: every extra SF must be **greater** than `sf`, all distinct, highest−lowest ≤ 4, and at BW ≥ 500 kHz at most 2 (only 1 when `sf` ≥ 10). **Receive only, and the bridge it creates is one-way.** TX always uses the single configured `sf`, and all detectors share one bandwidth, so this is multi-SF, not multi-channel. A node with `sf 7` + `extra.sf 8` hears SF8 traffic and *does* forward it — but the forward goes out at SF7, so traffic moves SF8 -> SF7 only and nothing comes back. An SF8 node's direct messages are delivered while its ACKs never arrive, so it retries to its limit every time; adverts and one-way flood traffic propagate fine. Because every extra SF must be **greater** than `sf`, the main SF is always the lowest in the set and TX always uses it — so the bridge direction is fixed at high-SF-in / low-SF-out and **cannot be reversed**. Two nodes back to back both point the same way; there is no configuration that carries SF7 -> SF8. Treat it as a collector for slower-SF stragglers, not as a link between two SF islands. Applied live and restored on every RX entry. **Interaction with CAD:** the chip's SF constraint for CAD is the inverse of the one for RX, so the driver switches side detectors off for each LBT CAD and back on when RX re-arms — two extra SPI commands per TX, no configuration required. Persisted; a set that no longer fits after an `sf`/`bw` change is refused at boot and logged. |
|
||||
| `set prv.key <hex>` | **128-char hex** (64-byte expanded Ed25519 key) | Replace private key; derive new identity *(reboot to apply)*. The length must be exact — `fromHex` rejects anything else with `Error, bad key`. `get prv.key` returns the same 128-char form. Not USB-gated. |
|
||||
|
||||
|
||||
@@ -76,6 +76,30 @@ Thanks to **bisbille** for finding this and fixing the first two boards.
|
||||
|
||||
---
|
||||
|
||||
## Changing frequency or spreading factor now resets adaptive CAD
|
||||
|
||||
Adaptive CAD learns a listen-before-talk threshold as an offset from a base value the chip family
|
||||
publishes per spreading factor and per bandwidth. Change SF or bandwidth and that base moves under
|
||||
the offset — but the node kept the old offset and, at the next boot, shifted it further to preserve
|
||||
the absolute threshold it used to name. Correct after a firmware table change, wrong after a preset
|
||||
change, where the base table's own step is the physics. On an LR1110 the SF7-to-SF12 step alone is
|
||||
16 counts, enough to slam the offset to its rail: too sensitive and the node defers transmitting on
|
||||
noise, or too deaf and it transmits over live receptions.
|
||||
|
||||
`set radio`, `set freq` and the app's radio settings now perform a full `set cad.reset` whenever they
|
||||
move frequency, bandwidth or spreading factor — the learned offset and the probe statistics behind it
|
||||
both belong to the preset you just left. Coding rate is excluded; it changes airtime, not the
|
||||
threshold. `tempradio` visits its preset at that preset's own base and hands the learned offset back
|
||||
on revert, without ever writing it to flash; `get cad` shows `a:tmp` while a window is open.
|
||||
|
||||
> [!NOTE]
|
||||
> **Nothing to change on your side.** The reset is automatic, and the node re-converges in an hour or
|
||||
> two. Reaching for `set cad.reset` by hand after a preset change is no longer necessary.
|
||||
|
||||
Thanks to **Codes** for reporting it.
|
||||
|
||||
---
|
||||
|
||||
## Also in this release
|
||||
|
||||
*To be filled in as further changes land.*
|
||||
|
||||
@@ -53,7 +53,7 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
|
||||
_sample_rssi(0), _sample_channel_quiet(false), _sample_fresh(false),
|
||||
_rx_entry_cyc(0),
|
||||
_rssi_bursts(0), _rssi_spread_sum(0), _rssi_degenerate(0),
|
||||
_cad_auto(false), _cad_offset(0), _probe_interval_s(0),
|
||||
_cad_auto(false), _cad_offset(0), _cad_temp_offset(0), _probe_interval_s(0),
|
||||
_cad_busycap_pct(0), _cad_pending_level(INT8_MIN),
|
||||
_cad_pending_deadline_ms(0),
|
||||
_cad_last_probe_ms(0), _cad_last_decay_ms(0),
|
||||
@@ -583,7 +583,8 @@ void LoRaRadioBase::reconfigureWithParams(float freq, float bw, uint8_t sf, uint
|
||||
reconfigure();
|
||||
}
|
||||
|
||||
void LoRaRadioBase::setRadioOverride(float freq, float bw, uint8_t sf, uint8_t cr)
|
||||
void LoRaRadioBase::setRadioOverride(float freq, float bw, uint8_t sf, uint8_t cr,
|
||||
bool visiting_new_preset)
|
||||
{
|
||||
_override_freq = freq;
|
||||
_override_bw = bw;
|
||||
@@ -591,6 +592,19 @@ void LoRaRadioBase::setRadioOverride(float freq, float bw, uint8_t sf, uint8_t c
|
||||
_override_cr = cr;
|
||||
_has_radio_override = true;
|
||||
reconfigure();
|
||||
|
||||
/* Visit a new preset at its own base detPeak. The learned offset belongs
|
||||
* to the preset it was measured on and the base table it addresses is
|
||||
* per-SF/per-bandwidth, so carrying it across programs a threshold nobody
|
||||
* measured (SF7->SF12 is 5 counts on SX126x, 16 on LR11xx). A freeze
|
||||
* changes nothing on air, so it keeps the offset that is already right.
|
||||
*
|
||||
* _cad_offset is left untouched — not snapshotted — so the configured
|
||||
* preset gets its offset back verbatim in clearRadioOverride(), and no
|
||||
* visitor value can reach prefs in between. Adaptation is suspended for
|
||||
* the duration; see cadMaintenance(). */
|
||||
_cad_temp_offset = visiting_new_preset ? 0 : _cad_offset;
|
||||
hwCadSetPeakOffset(cadEffectiveOffset());
|
||||
}
|
||||
|
||||
void LoRaRadioBase::clearRadioOverride()
|
||||
@@ -600,6 +614,7 @@ void LoRaRadioBase::clearRadioOverride()
|
||||
}
|
||||
_has_radio_override = false;
|
||||
reconfigure();
|
||||
hwCadSetPeakOffset(cadEffectiveOffset());
|
||||
}
|
||||
|
||||
void LoRaRadioBase::startReceive()
|
||||
@@ -1650,7 +1665,11 @@ void LoRaRadioBase::setCadParams(bool auto_enabled, int8_t offset,
|
||||
? (uint32_t)probe_interval_s * 1000U
|
||||
: (uint32_t)CONFIG_ZEPHCORE_NOISE_FLOOR_INTERVAL_MS;
|
||||
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
/* Effective, not _cad_offset: applyCadPrefs() reaches here from ordinary
|
||||
* `set cad.*` commands, which an operator can issue while a `tempradio`
|
||||
* window is open. Programming the configured offset then would put the
|
||||
* visited preset back on a threshold that does not belong to it. */
|
||||
hwCadSetPeakOffset(cadEffectiveOffset());
|
||||
|
||||
LOG_INF("cad: auto=%d offset=%d base=%u measure_interval=%ums busycap=%u%%",
|
||||
(int)auto_enabled, (int)offset, (unsigned)base,
|
||||
@@ -1666,6 +1685,15 @@ void LoRaRadioBase::resetCadStats()
|
||||
{
|
||||
memset(_cad_stats, 0, sizeof(_cad_stats));
|
||||
_cad_probe_rr = 0;
|
||||
|
||||
/* Drop any probe still in flight with the table it belongs to.
|
||||
* cadMaintenance() books a verdict into whatever stats exist when it
|
||||
* resolves, not the ones that were current when it was armed — so a
|
||||
* probe left pending across reconfigure() writes an old-preset sample
|
||||
* into the fresh table, and across `set cad.reset` writes a pre-reset
|
||||
* sample into a table the operator just cleared. */
|
||||
_cad_pending_level = INT8_MIN;
|
||||
_cad_pending_deadline_ms = 0;
|
||||
}
|
||||
|
||||
void LoRaRadioBase::decayCadStats()
|
||||
@@ -1868,16 +1896,26 @@ bool LoRaRadioBase::cadRelaxOnTxStarvation()
|
||||
*
|
||||
* One step at a time, never a jump to base: on a genuinely congested
|
||||
* site the operator's sensitive setting may be almost right, and the
|
||||
* smallest change that restores transmission is the one to make. */
|
||||
if (_cad_offset >= cadLevelMaxEff()) {
|
||||
* smallest change that restores transmission is the one to make.
|
||||
*
|
||||
* This one safety DOES run during a temporary override — unlike the
|
||||
* staircase, which cadMaintenance() suspends there. A node muted for
|
||||
* the length of a `tempradio` window, or until the reboot a `set radio`
|
||||
* is waiting for, is muted for real. It steps the visitor's offset, so
|
||||
* the recovery lasts exactly as long as the visit and still cannot reach
|
||||
* prefs: getCadOffset() keeps reporting the configured value. */
|
||||
int8_t &off = _has_radio_override ? _cad_temp_offset : _cad_offset;
|
||||
|
||||
if (off >= cadLevelMaxEff()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_cad_offset++;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
LOG_WRN("cad: TX starvation override -> offset %d (auto=%d) — LBT was "
|
||||
off++;
|
||||
hwCadSetPeakOffset(cadEffectiveOffset());
|
||||
LOG_WRN("cad: TX starvation override -> offset %d (auto=%d%s) — LBT was "
|
||||
"refusing every transmit",
|
||||
(int)_cad_offset, (int)_cad_auto);
|
||||
(int)off, (int)_cad_auto,
|
||||
_has_radio_override ? ", temp preset" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1887,6 +1925,21 @@ void LoRaRadioBase::cadMaintenance()
|
||||
return;
|
||||
}
|
||||
|
||||
/* Nothing to learn on a preset the node is only visiting: the statistics
|
||||
* would describe that preset, and every mover of _cad_offset below is
|
||||
* persisted by Dispatcher::maintenanceLoop() -> onCadOffsetChanged() ->
|
||||
* prefs, so letting the staircase run inside a `tempradio` window would
|
||||
* write a visitor's offset to flash and leave it there after the revert.
|
||||
* The chip runs at the visited preset's own base (setRadioOverride).
|
||||
*
|
||||
* The pending sample is dropped rather than carried: it was taken under
|
||||
* the other preset, and consuming it after the revert would admit a
|
||||
* probe on a quiet verdict that is a whole interval stale. */
|
||||
if (_has_radio_override) {
|
||||
_sample_fresh = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t now = k_uptime_get();
|
||||
|
||||
/* Periodic decay keeps the stats fresh (and counters bounded). */
|
||||
@@ -2150,6 +2203,9 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
|
||||
* Header: a:on o:1 pk:22(b21/4s) sp:0.9/84%(312) bc:25%
|
||||
* a auto on/off o offset pk operating peak
|
||||
* b family base 4s symbols bc busy cap
|
||||
* a:tmp a temporary radio override is running a preset the node is
|
||||
* only visiting: adaptation is suspended and o/pk describe that
|
||||
* visit, not the configured preset (see setRadioOverride).
|
||||
* sp RSSI burst quality: mean spread in dB across the median-of-N
|
||||
* reads, the share of bursts whose spread was 0, and the burst
|
||||
* count. The count is not decoration: a share without its
|
||||
@@ -2190,10 +2246,12 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
|
||||
* still gets the mean and the share, which is the actual verdict. */
|
||||
bool room_for_count = (cap >= 200);
|
||||
|
||||
const int eff_off = cadEffectiveOffset();
|
||||
|
||||
n += snprintf(buf + n, cap > n ? cap - n : 0,
|
||||
"a:%s o:%d pk:%d(b%u/4s) sp:%u.%u/%u%%",
|
||||
_cad_auto ? "on" : "off", (int)_cad_offset,
|
||||
(int)base + _cad_offset, base,
|
||||
_has_radio_override ? "tmp" : (_cad_auto ? "on" : "off"),
|
||||
eff_off, (int)base + eff_off, base,
|
||||
spread_mean10 / 10U, spread_mean10 % 10U, degen_pct);
|
||||
if (room_for_count) {
|
||||
/* bursts(ok reads/busy reads/abandoned bursts/dc-blocked) — the
|
||||
@@ -2232,7 +2290,7 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
|
||||
* every rung printed is a real one and `pk` below is what the chip got. */
|
||||
const int lmin = cadLevelMinEff();
|
||||
const int lmax = cadLevelMaxEff();
|
||||
int cur = _cad_offset;
|
||||
int cur = eff_off;
|
||||
if (cur < lmin) cur = lmin;
|
||||
if (cur > lmax) cur = lmax;
|
||||
int lo = cur - 1, hi = cur + 1;
|
||||
|
||||
@@ -41,7 +41,17 @@ public:
|
||||
* _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. */
|
||||
void setRadioOverride(float freq, float bw, uint8_t sf, uint8_t cr);
|
||||
/* 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(). */
|
||||
void setRadioOverride(float freq, float bw, uint8_t sf, uint8_t cr,
|
||||
bool visiting_new_preset = true);
|
||||
void clearRadioOverride();
|
||||
bool hasRadioOverride() const { return _has_radio_override; }
|
||||
int recvRaw(uint8_t *bytes, int sz) override;
|
||||
@@ -388,6 +398,22 @@ protected:
|
||||
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;
|
||||
}
|
||||
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
|
||||
|
||||
@@ -2809,13 +2809,21 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
bw >= 7000 && bw <= 500000 &&
|
||||
sf >= 5 && sf <= 12 &&
|
||||
cr >= 5 && cr <= 8) {
|
||||
/* Coding rate is deliberately not in this test:
|
||||
* it changes airtime, not the channel or the
|
||||
* per-SF/per-bandwidth detPeak base, so it does
|
||||
* not invalidate the learned CAD offset. */
|
||||
bool preset_changed =
|
||||
prefs.freq != (float)freq / 1000.0f ||
|
||||
prefs.bw != (float)bw / 1000.0f ||
|
||||
prefs.sf != sf;
|
||||
prefs.freq = (float)freq / 1000.0f;
|
||||
prefs.bw = (float)bw / 1000.0f;
|
||||
prefs.sf = sf;
|
||||
prefs.cr = cr;
|
||||
prefs.client_repeat = repeat;
|
||||
_store->savePrefs(prefs);
|
||||
if (_radio_reconfig_cb) _radio_reconfig_cb();
|
||||
if (_radio_reconfig_cb) _radio_reconfig_cb(preset_changed);
|
||||
LOG_INF("SET_RADIO_PARAMS: client_repeat=%d", repeat);
|
||||
sendPacketOk();
|
||||
} else {
|
||||
@@ -2832,7 +2840,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
if (power >= -9 && power <= MAX_LORA_TX_POWER) {
|
||||
prefs.tx_power_dbm = power;
|
||||
_store->savePrefs(prefs);
|
||||
if (_radio_reconfig_cb) _radio_reconfig_cb();
|
||||
if (_radio_reconfig_cb) _radio_reconfig_cb(false);
|
||||
sendPacketOk();
|
||||
} else {
|
||||
sendPacketError(ERR_ILLEGAL_ARG);
|
||||
|
||||
@@ -85,8 +85,10 @@ typedef size_t (*WriteFrameCallback)(const uint8_t *data, size_t len);
|
||||
/* Battery millivolt read callback */
|
||||
typedef uint16_t (*GetBatteryCallback)(void);
|
||||
|
||||
/* Radio reconfigure callback */
|
||||
typedef void (*RadioReconfigureCallback)(void);
|
||||
/* Radio reconfigure callback. preset_changed = the channel or the modem
|
||||
* config moved (freq/bw/sf), not just TX power — the adaptive-CAD state is
|
||||
* tied to those and has to be reset with them (see main_companion). */
|
||||
typedef void (*RadioReconfigureCallback)(bool preset_changed);
|
||||
|
||||
/* BLE PIN change callback */
|
||||
typedef void (*PinChangeCallback)(uint32_t new_pin);
|
||||
|
||||
@@ -1199,7 +1199,10 @@ void RepeaterMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_
|
||||
void RepeaterMesh::freezeRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
auto& radio = getRadioDriver(_radio);
|
||||
if (!radio.hasRadioOverride()) {
|
||||
radio.setRadioOverride(freq, bw, sf, cr);
|
||||
/* Holds the radio on the preset it is ALREADY running while _prefs
|
||||
* move ahead of it, so the learned CAD offset still applies — see
|
||||
* setRadioOverride(). */
|
||||
radio.setRadioOverride(freq, bw, sf, cr, /*visiting_new_preset=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -903,7 +903,10 @@ void RoomServerMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint
|
||||
void RoomServerMesh::freezeRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
auto& radio = getRadioDriver(_radio);
|
||||
if (!radio.hasRadioOverride()) {
|
||||
radio.setRadioOverride(freq, bw, sf, cr);
|
||||
/* Holds the radio on the preset it is ALREADY running while _prefs
|
||||
* move ahead of it, so the learned CAD offset still applies — see
|
||||
* setRadioOverride(). */
|
||||
radio.setRadioOverride(freq, bw, sf, cr, /*visiting_new_preset=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,41 @@ void CommonCLI::savePrefs() {
|
||||
_callbacks->savePrefs();
|
||||
}
|
||||
|
||||
/* Drop everything the adaptive-CAD controller has learned.
|
||||
*
|
||||
* Called both by `set cad.reset` and by every command that moves the radio to a
|
||||
* different preset, because such a change invalidates all three pieces at once:
|
||||
* detPeak's base table is per-SF and per-bandwidth, so the ladder the offset
|
||||
* addresses is re-based under it (SF7->SF12 is 5 counts on SX126x and 16 on the
|
||||
* LR11xx), and the per-level statistics that justified the offset describe a
|
||||
* preset that is no longer on air. Re-converging from the new preset's own
|
||||
* 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().
|
||||
*
|
||||
* 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
|
||||
* freezeRadioParams until the reboot the caller is about to ask for).
|
||||
* applyCadPrefs() stamps cad_base from the RUNNING radio, i.e. the preset being
|
||||
* left, so that stamp has to be cleared again afterwards: a stale anchor in
|
||||
* flash is precisely what makes setCadParams() re-anchor a freshly reset offset
|
||||
* by (old_base - new_base) on the next boot, which is the bug this reset
|
||||
* exists to avoid. 0 means "no anchor recorded", which that re-anchor
|
||||
* deliberately treats as a no-op, and the next boot re-stamps it.
|
||||
*/
|
||||
void CommonCLI::resetCadState(bool preset_pending) {
|
||||
_prefs->cad_offset = cliDefaults()->cad_offset;
|
||||
_prefs->cad_base = 0;
|
||||
_callbacks->resetCadStats();
|
||||
_callbacks->applyCadPrefs();
|
||||
if (preset_pending) {
|
||||
_prefs->cad_base = 0;
|
||||
}
|
||||
savePrefs();
|
||||
}
|
||||
|
||||
uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
||||
if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) {
|
||||
AdvertDataBuilder builder(node_type, _prefs->node_name);
|
||||
@@ -851,19 +886,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
* FROM wherever the staircase had already walked detPeak, with no
|
||||
* evidence left to justify sitting there — the opposite of a reset,
|
||||
* and useless for the one job this command has (recovering after a
|
||||
* base-table change). The operating offset lives in two places:
|
||||
* _prefs->cad_offset, and _cad_offset inside the radio, which
|
||||
* applyCadPrefs() reloads through setCadParams(). */
|
||||
_prefs->cad_offset = cliDefaults()->cad_offset;
|
||||
/* Clear the recorded base too. applyCadPrefs() below re-stamps it
|
||||
* from the radio, so a reset always leaves offset and base
|
||||
* describing the same configuration -- leaving a stale base here
|
||||
* would make the NEXT base-table change re-anchor a freshly reset
|
||||
* offset away from zero. */
|
||||
_prefs->cad_base = 0;
|
||||
_callbacks->resetCadStats();
|
||||
_callbacks->applyCadPrefs();
|
||||
savePrefs();
|
||||
* base-table change).
|
||||
*
|
||||
* preset_pending = false: the radio is running the preset this
|
||||
* reset is for, so the base applyCadPrefs() stamps is the right
|
||||
* one to keep. */
|
||||
resetCadState(false);
|
||||
snprintf(reply, CLI_REPLY_SIZE,
|
||||
"OK - CAD probe stats cleared, detPeak offset reset to %d",
|
||||
(int)_prefs->cad_offset);
|
||||
@@ -1006,7 +1034,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
_prefs->cr = cr;
|
||||
_callbacks->savePrefs();
|
||||
_callbacks->freezeRadioParams(old_freq, old_bw, old_sf, old_cr);
|
||||
strcpy(reply, "OK - reboot to apply");
|
||||
/* Anything that moves the channel or the detPeak base table
|
||||
* invalidates the learned CAD offset and every statistic
|
||||
* behind it — see resetCadState(). Coding rate does neither
|
||||
* (it changes airtime, not the threshold or the band), so a
|
||||
* cr-only edit keeps a converged offset. */
|
||||
if (freq != old_freq || bw != old_bw || sf != old_sf) {
|
||||
resetCadState(true);
|
||||
strcpy(reply, "OK - reboot to apply (CAD reset)");
|
||||
} else {
|
||||
strcpy(reply, "OK - reboot to apply");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "Error: freq 150-2500, bw 7-500, sf 5-12, cr 5-8, or default");
|
||||
}
|
||||
@@ -1163,7 +1201,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
* back; freeze the running radio on the old freq until reboot,
|
||||
* mirroring the "set radio" handler above. */
|
||||
_callbacks->freezeRadioParams(old_freq, _prefs->bw, _prefs->sf, _prefs->cr);
|
||||
strcpy(reply, "OK - reboot to apply");
|
||||
/* New channel, new interference: the offset and the statistics
|
||||
* behind it were learned somewhere else. See resetCadState(). */
|
||||
if (f != old_freq) {
|
||||
resetCadState(true);
|
||||
strcpy(reply, "OK - reboot to apply (CAD reset)");
|
||||
} else {
|
||||
strcpy(reply, "OK - reboot to apply");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "Error: range 150-2500 MHz, or default");
|
||||
}
|
||||
|
||||
@@ -149,4 +149,11 @@ public:
|
||||
|
||||
void handleCommand(uint32_t sender_timestamp, const char* command, char* reply);
|
||||
uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
|
||||
|
||||
/* Full adaptive-CAD reset: the learned detPeak offset, the probe statistics
|
||||
* that justified it, and the recorded base anchor, all dropped together.
|
||||
* Public so the companion's app-driven radio change (CMD_SET_RADIO_PARAMS,
|
||||
* which applies live rather than at reboot) can reach the same code path
|
||||
* instead of open-coding it. See the definition for `preset_pending`. */
|
||||
void resetCadState(bool preset_pending);
|
||||
};
|
||||
|
||||
@@ -673,11 +673,6 @@ static uint16_t get_battery_mv(void)
|
||||
return zephyr_board.getBattMilliVolts();
|
||||
}
|
||||
|
||||
static void radio_reconfigure(void)
|
||||
{
|
||||
lora_radio.reconfigure();
|
||||
}
|
||||
|
||||
static mesh::ZephyrMillisecondClock ms_clock;
|
||||
static mesh::ZephyrRNG zephyr_rng;
|
||||
static mesh::SimpleMeshTables mesh_tables;
|
||||
@@ -886,6 +881,24 @@ static ClientACL companion_acl; /* unused by CommonCLI but required by construc
|
||||
static CommonCLI companion_cli(zephyr_board, rtc_clock, companion_acl,
|
||||
&companion_mesh.prefs, &companion_cli_cbs);
|
||||
|
||||
/* Radio reconfigure, reached from the app protocol (CMD_SET_RADIO_PARAMS /
|
||||
* CMD_SET_RADIO_TX_POWER). Defined here rather than up with the other radio
|
||||
* callbacks because it needs companion_cli, which needs the mesh and the
|
||||
* datastore — same reason save_prefs_to_flash() sits below them.
|
||||
*
|
||||
* Unlike the CLI's `set radio`, this path applies LIVE: there is no reboot and
|
||||
* no frozen old preset, so by the time the CAD reset runs the radio is already
|
||||
* on the new one and the base it stamps is the right one to keep — hence
|
||||
* preset_pending = false. */
|
||||
static void radio_reconfigure(bool preset_changed)
|
||||
{
|
||||
lora_radio.reconfigure();
|
||||
|
||||
if (preset_changed) {
|
||||
companion_cli.resetCadState(false);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS) && \
|
||||
CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS > 0
|
||||
#define ZEPHCORE_HAS_AUTO_SHUTDOWN 1
|
||||
|
||||
Reference in New Issue
Block a user