mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:38:19 +00:00
refactor / unify probes
This commit is contained in:
@@ -90,7 +90,7 @@ caveat noted at the end, viewed through the strong/faint lens.)
|
||||
|
||||
## How it works
|
||||
|
||||
Every `cad.probe.interval` seconds (default 15), when the radio is idle
|
||||
Every `probe.interval` seconds (default 15), when the radio is idle
|
||||
in receive mode, the firmware runs one **calibration CAD probe** and
|
||||
immediately re-arms RX. A probe takes one CAD duration — about 4 ms at
|
||||
SF7/250 kHz, up to ~130 ms at SF12/125 kHz — so the added radio deaf time
|
||||
@@ -248,7 +248,7 @@ any time with `set cad.auto on`. Offset changes (manual or automatic)
|
||||
appear in the log as `cad: step down/up -> offset N`.
|
||||
|
||||
To collect dry-run data faster, drop the interval (probing is temporary
|
||||
then): `set cad.probe.interval 10`. At the default 15 s, allow ~a day for
|
||||
then): `set probe.interval 10`. At the default 15 s, allow ~a day for
|
||||
a knee to resolve clearly, longer to capture day/night variation.
|
||||
|
||||
## Command reference
|
||||
@@ -258,7 +258,7 @@ a knee to resolve clearly, longer to capture day/night variation.
|
||||
| `get cad` | | Status + per-level statistics (see above). |
|
||||
| `set cad.auto <on\|off>` | **on** | Staircase controller acts on the stats. Off = observe/hand-tune. |
|
||||
| `set cad.offset <n>` | 0 | Operating offset, −8…12. Negative = more sensitive. Applied live. |
|
||||
| `set cad.probe.interval <sec>` | 15 | Probe cadence; 0 disables probing (and freezes auto), 10–255 otherwise. |
|
||||
| `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>` | 25 | 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). |
|
||||
|
||||
|
||||
+1055
-1055
File diff suppressed because it is too large
Load Diff
+13
-9
@@ -396,24 +396,28 @@ config ZEPHCORE_MAINTENANCE_BACKSTOP_MS
|
||||
UNCONDITIONAL, so this is a safety net against a deadline that is missed
|
||||
or mis-reported — not a value that should ever bind in normal operation.
|
||||
|
||||
MUST stay above ZEPHCORE_NOISE_FLOOR_INTERVAL_MS and the CAD probe
|
||||
interval (prefs cad_probe_interval, default 15 s), which are the shortest
|
||||
recurring deadlines on an idle repeater. Set at or below them and this
|
||||
silently becomes the wake period, reproducing the old fixed 5 s tick and
|
||||
making the deadline scheduling a no-op. That is exactly the bug the
|
||||
original 5000 default shipped with.
|
||||
MUST stay above the radio measurement interval ("set probe.interval",
|
||||
default 15 s; ZEPHCORE_NOISE_FLOOR_INTERVAL_MS when probing is off) —
|
||||
the shortest recurring deadline on an idle repeater. Set at or below it
|
||||
and this silently becomes the wake period, reproducing the old fixed 5 s
|
||||
tick and making the deadline scheduling a no-op. That is exactly the bug
|
||||
the original 5000 default shipped with.
|
||||
|
||||
Lowering it costs wakes without improving responsiveness — real work is
|
||||
already scheduled at its own deadline. Raising it widens the window in
|
||||
which a scheduling bug goes unnoticed.
|
||||
|
||||
config ZEPHCORE_NOISE_FLOOR_INTERVAL_MS
|
||||
int "Noise floor sampling interval (ms)"
|
||||
default 5000 if ZEPHCORE_ROLE_COMPANION || ZEPHCORE_ROLE_ROOM_SERVER
|
||||
int "Default radio measurement interval (ms)"
|
||||
default 15000
|
||||
range 1000 120000
|
||||
help
|
||||
How often the radio samples ambient RSSI to update the noise floor EMA.
|
||||
Fallback cadence for periodic radio measurements — the noise-floor RSSI
|
||||
sample and the CAD probe that consumes it.
|
||||
|
||||
This is only the DEFAULT. At runtime the "probe.interval" pref governs
|
||||
both (see set probe.interval); this value applies when that pref is 0,
|
||||
i.e. CAD probing is switched off but the floor sampler still has to run.
|
||||
|
||||
This used to be implicit: the sampler ran once per housekeeping tick, so
|
||||
it inherited that 5 s period. The sampler lives in shared radio code, so
|
||||
|
||||
@@ -206,6 +206,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
|
||||
| `get rxduty` | RX duty cycle mode: `0` or `1` |
|
||||
| `get gps duty` | Now-effective GPS duty interval in seconds (`always on (0)` when continuous) |
|
||||
| `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = counted toward the verdict above). Entries that count print first, so a size-capped reply never hides the ones that explain the summary; if the table doesn't fully fit, a trailing `+N more` shows how many were left out. Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. |
|
||||
| `get probe.interval` | Seconds between periodic radio measurements (noise-floor sample + CAD probe). 0 = CAD probing off |
|
||||
| `get dc.restarts` | Duty-cycle preamble false-positive re-arm counter (RxTimeout re-arms + parked-RX watchdog recoveries). High values mean the preamble detector is tripping on noise/interference without real packets arriving — inflates RX-on time and drains battery; packets are never lost to it. Reset by `clear stats`. |
|
||||
| `get cad` | Adaptive-CAD status: header (`a` auto on/off, `o` operating detPeak offset, `pk` absolute peak with family base, `iv` probe interval, `bc` busy cap), then a 3-rung window around the operating offset (`*` marks it) with probe/busy/fp/tp counts and false-positive rate — the three levels the knee controller reads. Probing runs even while `cad.auto` is off (dry-run), so this is the observation tool for picking a site-appropriate detPeak. See `ADAPTIVE_CAD.md`. Not available on SX127x boards (no hardware CAD). |
|
||||
| `get adc.multiplier` | Battery voltage ADC calibration multiplier |
|
||||
@@ -253,7 +254,7 @@ Changes are persisted immediately unless noted. Some require a reboot.
|
||||
| `set meshtimesync <on\|off>` | default **off** | Mesh time sync: automatically correct this node's clock from the consensus of Ed25519-signed advert timestamps heard on the mesh. Steps at most ±1 h per step, one step per 6 h; abstains without a quorum (default 6) of tenured agreeing senders; never overrides a clock set in the last 7 days, whether from GPS (re-armed on every fix) or a manual set. See `MESHTIMESYNC.md`. |
|
||||
| `set cad.auto <on\|off>` | default **on** | Adaptive CAD: let the staircase controller move the operating detPeak offset based on probe statistics. On by default (repeaters and companions); at the default 15 s probe interval it responds to environment change in ~1–2 h. Turn off to observe/hand-tune via `get cad` + `set cad.offset`. See `ADAPTIVE_CAD.md`. |
|
||||
| `set cad.offset <n>` | −8 to 12, default 0 | Operating detPeak offset from the chip family's per-SF base (SX126x: SF+13; LR11xx/LR20xx: 56–68 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 15–40, LR 48–90) is a firmware guardrail against a CAD that never/always fires, not a chip limit (`cadDetPeak` is a full `uint8_t`). Applied live; the auto staircase may move it later if `cad.auto` is on. |
|
||||
| `set cad.probe.interval <sec>` | 0 (off) or 10–255, default **15** | Seconds between calibration CAD probes. Default 15 s → ~1–2 h staircase response. 0 disables probing entirely (also freezes auto adaptation). |
|
||||
| `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 **25** | Airtime-protection cap: the max percentage of TX attempts the node will let CAD defer before the staircase backs off to a less sensitive detPeak — counting **real** traffic, not just false positives. 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`. 0 disables the cap (pure knee-seeking). |
|
||||
| `set cad.reset` | | Clear the accumulated per-level CAD probe statistics (RAM only; also cleared automatically on any radio parameter change). |
|
||||
| `set prv.key <hex>` | 64-char hex (32-byte key) | Replace private key; derive new identity *(reboot to apply)* |
|
||||
|
||||
@@ -777,12 +777,12 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
|
||||
}
|
||||
}
|
||||
|
||||
/* Offset 157: cad_probe_interval (ZephCore extension, seconds; 0 = off).
|
||||
/* Offset 157: probe_interval (ZephCore extension, seconds; 0 = off).
|
||||
* Absent in pre-existing files → keep the in-RAM default (60). */
|
||||
if (off < len) {
|
||||
prefs.cad_probe_interval = buf[off++];
|
||||
if (prefs.cad_probe_interval != 0 && prefs.cad_probe_interval < 10) {
|
||||
prefs.cad_probe_interval = 10;
|
||||
prefs.probe_interval = buf[off++];
|
||||
if (prefs.probe_interval != 0 && prefs.probe_interval < 10) {
|
||||
prefs.probe_interval = 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,8 +892,8 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
|
||||
buf[off++] = prefs.cad_auto;
|
||||
/* Offset 156: cad_offset (ZephCore extension, signed) */
|
||||
buf[off++] = (uint8_t)prefs.cad_offset;
|
||||
/* Offset 157: cad_probe_interval (ZephCore extension, seconds) */
|
||||
buf[off++] = prefs.cad_probe_interval;
|
||||
/* Offset 157: probe_interval (ZephCore extension, seconds) */
|
||||
buf[off++] = prefs.probe_interval;
|
||||
/* Offset 158: cad_busycap (ZephCore extension, percent) */
|
||||
buf[off++] = prefs.cad_busycap;
|
||||
/* Offset 159: adc_multiplier (ZephCore extension, float LE, 0 = board
|
||||
|
||||
@@ -47,10 +47,12 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
|
||||
_last_rssi(0), _last_snr(0),
|
||||
_rx_head(0), _rx_tail(0),
|
||||
_noise_floor(DEFAULT_NOISE_FLOOR), _calibration_threshold(0), _ema_unguarded(0),
|
||||
_noise_floor_next_ms(0),
|
||||
_cad_auto(false), _cad_offset(0), _cad_probe_interval_s(0),
|
||||
_noise_floor_next_ms(0), _noise_floor_retries(0),
|
||||
_measure_interval_ms(CONFIG_ZEPHCORE_NOISE_FLOOR_INTERVAL_MS),
|
||||
_sample_rssi(0), _sample_channel_quiet(false), _sample_fresh(false),
|
||||
_cad_auto(false), _cad_offset(0), _probe_interval_s(0),
|
||||
_cad_busycap_pct(0),
|
||||
_cad_last_probe_ms(0), _cad_last_decay_ms(0), _cad_retry_ms(0),
|
||||
_cad_last_probe_ms(0), _cad_last_decay_ms(0),
|
||||
_cad_probe_rr(0),
|
||||
_rx_duty_cycle_enabled(IS_ENABLED(CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE)),
|
||||
_rx_boost_enabled(true),
|
||||
@@ -792,13 +794,28 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold)
|
||||
* calls are a no-op, so this is safe to invoke from any wake. */
|
||||
int64_t now = k_uptime_get();
|
||||
|
||||
/* Invalidate first: "fresh" must mean a sample landed in THIS pass, not
|
||||
* merely at some point in the past. cadMaintenance() runs immediately
|
||||
* after us and treats the verdict as current-channel ground truth, so a
|
||||
* carried-over sample would let it probe on a reading taken a full
|
||||
* interval ago — on a different channel state entirely. */
|
||||
_sample_fresh = false;
|
||||
|
||||
if (_noise_floor_next_ms != 0 && now < _noise_floor_next_ms) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Due. Any bail-out below is a blocked attempt, not a completed one —
|
||||
* push the deadline out by the short retry so msUntilNextMaintenance()
|
||||
* cannot report "due now" on a loop. */
|
||||
* push the deadline out by the retry so msUntilNextMaintenance() cannot
|
||||
* report "due now" on a loop. Bounded for the same reason as the CAD
|
||||
* probe: an unbounded retry grid makes the retry period the de-facto
|
||||
* wake period whenever the radio is persistently busy. */
|
||||
if (_noise_floor_retries >= NOISE_FLOOR_MAX_RETRIES) {
|
||||
_noise_floor_retries = 0;
|
||||
_noise_floor_next_ms = now + _measure_interval_ms;
|
||||
return;
|
||||
}
|
||||
_noise_floor_retries++;
|
||||
_noise_floor_next_ms = now + NOISE_FLOOR_RETRY_MS;
|
||||
|
||||
if (!atomic_get(&_in_recv_mode) || atomic_get(&_tx_active)) {
|
||||
@@ -831,7 +848,8 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold)
|
||||
}
|
||||
|
||||
/* A full sample landed: next one is a full interval away. */
|
||||
_noise_floor_next_ms = now + NOISE_FLOOR_INTERVAL_MS;
|
||||
_noise_floor_next_ms = now + _measure_interval_ms;
|
||||
_noise_floor_retries = 0;
|
||||
|
||||
/* Insertion sort — tiny array, branch-friendly on Cortex-M */
|
||||
for (int i = 1; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
|
||||
@@ -846,10 +864,31 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold)
|
||||
int16_t rssi = (samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2 - 1] +
|
||||
samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2]) / 2;
|
||||
|
||||
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly. */
|
||||
/* Publish this sample for cadMaintenance(). The CAD probe needs exactly
|
||||
* the same fact we just established — "is the channel at its floor right
|
||||
* now?" — and used to answer it with its own single hwGetCurrentRSSI() on
|
||||
* its own deadline. That cost a second wake per interval (measured: two
|
||||
* 15 s grids ~3 s apart) and made the worse decision, since one raw read
|
||||
* is precisely what the median-of-8 exists to defend against.
|
||||
*
|
||||
* The verdict is taken against the floor BEFORE this sample is folded in,
|
||||
* so it compares a new observation to the established floor rather than
|
||||
* to one already dragged toward it. */
|
||||
_sample_rssi = rssi;
|
||||
_sample_channel_quiet = (_noise_floor == DEFAULT_NOISE_FLOOR) ||
|
||||
(rssi <= _noise_floor + CAD_PROBE_RSSI_GUARD);
|
||||
_sample_fresh = true;
|
||||
|
||||
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly.
|
||||
* The lower clamp tracks the active bandwidth — thermal noise is
|
||||
* 10*log10(BW) so a fixed rail pins narrow-BW presets several dB high
|
||||
* (BW 31.25 kHz sits ~3 dB below BW 62.5) and never engages at all on
|
||||
* wide ones. */
|
||||
int16_t floor_min = noise_floor_min_dbm(getActiveBandwidthKHzX10() / 10);
|
||||
|
||||
if (_noise_floor == DEFAULT_NOISE_FLOOR) {
|
||||
_noise_floor = rssi;
|
||||
if (_noise_floor < -120) _noise_floor = -120;
|
||||
if (_noise_floor < floor_min) _noise_floor = floor_min;
|
||||
if (_noise_floor > -50) _noise_floor = -50;
|
||||
_ema_unguarded = 0;
|
||||
LOG_DBG("noise_floor_cal: seed=%d", _noise_floor);
|
||||
@@ -886,7 +925,7 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold)
|
||||
int half = W / 2; /* 4 */
|
||||
int step = (diff + (diff > 0 ? half : -half)) / W;
|
||||
_noise_floor += step;
|
||||
if (_noise_floor < -120) _noise_floor = -120;
|
||||
if (_noise_floor < floor_min) _noise_floor = floor_min;
|
||||
if (_noise_floor > -50) _noise_floor = -50;
|
||||
|
||||
LOG_DBG("noise_floor_cal: rssi=%d, floor=%d, tick=%u",
|
||||
@@ -949,12 +988,25 @@ void LoRaRadioBase::setCadParams(bool auto_enabled, int8_t offset,
|
||||
|
||||
_cad_auto = auto_enabled;
|
||||
_cad_offset = offset;
|
||||
_cad_probe_interval_s = probe_interval_s;
|
||||
_probe_interval_s = probe_interval_s;
|
||||
_cad_busycap_pct = busycap_pct;
|
||||
|
||||
/* One interval governs every periodic radio measurement, because there
|
||||
* is only one measurement: the noise-floor sampler takes a median-of-8
|
||||
* and the CAD probe consumes that same reading (see cadMaintenance).
|
||||
* Splitting them into two knobs could only ever express a rate the
|
||||
* hardware does not actually run at.
|
||||
*
|
||||
* 0 means "CAD probing off" — the floor sampler still has to run, so it
|
||||
* falls back to the build-time default. */
|
||||
_measure_interval_ms = probe_interval_s
|
||||
? (uint32_t)probe_interval_s * 1000U
|
||||
: (uint32_t)CONFIG_ZEPHCORE_NOISE_FLOOR_INTERVAL_MS;
|
||||
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
|
||||
LOG_INF("cad: auto=%d offset=%d probe_interval=%us busycap=%u%%",
|
||||
(int)auto_enabled, (int)offset, (unsigned)probe_interval_s,
|
||||
LOG_INF("cad: auto=%d offset=%d measure_interval=%ums busycap=%u%%",
|
||||
(int)auto_enabled, (int)offset, (unsigned)_measure_interval_ms,
|
||||
(unsigned)busycap_pct);
|
||||
}
|
||||
|
||||
@@ -1090,7 +1142,7 @@ void LoRaRadioBase::cadStaircaseStep()
|
||||
|
||||
void LoRaRadioBase::cadMaintenance()
|
||||
{
|
||||
if (_cad_probe_interval_s == 0) {
|
||||
if (_probe_interval_s == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1104,39 +1156,30 @@ void LoRaRadioBase::cadMaintenance()
|
||||
_cad_last_decay_ms = now;
|
||||
}
|
||||
|
||||
if (now - _cad_last_probe_ms < (int64_t)_cad_probe_interval_s * 1000) {
|
||||
return;
|
||||
}
|
||||
if (now < _cad_retry_ms) {
|
||||
return;
|
||||
}
|
||||
/* No separate probe-interval check: the probe interval IS the measurement
|
||||
* interval (setCadParams derives _measure_interval_ms from it), so a
|
||||
* fresh sample means a probe is due by construction. */
|
||||
|
||||
/* Due. _cad_last_probe_ms only advances on a probe that actually runs,
|
||||
* so hold a short retry deadline across the guards below — otherwise a
|
||||
* blocked probe reports "due now" to msUntilNextMaintenance() forever. */
|
||||
_cad_retry_ms = now + CAD_PROBE_RETRY_MS;
|
||||
|
||||
/* Same guards as the noise-floor calibrator: only probe from idle
|
||||
* continuous/duty-cycle RX, never during TX or an active packet,
|
||||
* never while the chip is in its duty-cycle sleep (BUSY) phase. */
|
||||
if (!atomic_get(&_in_recv_mode) || atomic_get(&_tx_active)) {
|
||||
return;
|
||||
}
|
||||
if (!isRadioReady() || isReceiving()) {
|
||||
/* Ride on the noise-floor sampler rather than measuring independently.
|
||||
*
|
||||
* A fresh sample means the sampler ran THIS pass, which already proves
|
||||
* everything the probe needs: the radio was idle in RX, not transmitting,
|
||||
* not mid-packet, and out of its duty-cycle sleep window — the sampler
|
||||
* applies exactly those guards before it reads. So there is nothing left
|
||||
* to re-check, no separate deadline, and no retry budget: if no sample
|
||||
* landed this pass, the probe simply waits for the next one.
|
||||
*
|
||||
* This is what makes the wake cost one per interval instead of two. It
|
||||
* also upgrades the ground-truth prefilter from a single raw RSSI read to
|
||||
* the sampler's median-of-8 — the probe is trying to establish that the
|
||||
* channel is quiet, and a busy verdict taken over real traffic teaches
|
||||
* nothing about false positives, so the outlier rejection matters here. */
|
||||
if (!_sample_fresh) {
|
||||
return;
|
||||
}
|
||||
_sample_fresh = false;
|
||||
|
||||
/* Ground-truth prefilter: skip when the channel is visibly busy —
|
||||
* a busy verdict against strong traffic teaches us nothing about
|
||||
* false positives. (Below-noise-floor LoRa can't be excluded here;
|
||||
* the post-probe RX check below handles that side.) */
|
||||
int16_t rssi = hwGetCurrentRSSI();
|
||||
|
||||
if (rssi == -128) {
|
||||
return;
|
||||
}
|
||||
if (_noise_floor != DEFAULT_NOISE_FLOOR &&
|
||||
rssi > _noise_floor + CAD_PROBE_RSSI_GUARD) {
|
||||
if (!_sample_channel_quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1251,18 +1294,15 @@ uint32_t LoRaRadioBase::msUntilNextMaintenance()
|
||||
}
|
||||
next = clampDeadline(_noise_floor_next_ms - now);
|
||||
|
||||
if (_cad_probe_interval_s == 0) {
|
||||
if (_probe_interval_s == 0) {
|
||||
return next;
|
||||
}
|
||||
|
||||
/* CAD probe: whichever is later of the interval since the last probe
|
||||
* that actually ran and any retry deadline left by a blocked one. */
|
||||
int64_t probe_at = _cad_last_probe_ms + (int64_t)_cad_probe_interval_s * 1000;
|
||||
|
||||
if (probe_at < _cad_retry_ms) {
|
||||
probe_at = _cad_retry_ms;
|
||||
}
|
||||
next = mesh::maintenanceSooner(next, clampDeadline(probe_at - now));
|
||||
/* The CAD probe deliberately contributes NO deadline of its own. It runs
|
||||
* off the noise-floor sampler's measurement (see cadMaintenance), so its
|
||||
* wake is already accounted for above. Giving it a second deadline is
|
||||
* what produced two independent 15 s grids ~3 s apart — one extra wake
|
||||
* per interval, forever, on every repeater. */
|
||||
|
||||
/* Stats decay. _cad_last_decay_ms == 0 means the first call latches it
|
||||
* rather than decaying, so treat that as due now. */
|
||||
@@ -1292,7 +1332,7 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
|
||||
"a:%s o:%d pk:%d(b%u/4s) iv:%us bc:%u%%",
|
||||
_cad_auto ? "on" : "off", (int)_cad_offset,
|
||||
(int)base + _cad_offset, base,
|
||||
(unsigned)_cad_probe_interval_s,
|
||||
(unsigned)_probe_interval_s,
|
||||
(unsigned)_cad_busycap_pct);
|
||||
|
||||
/* Only the 3 rungs around the operating offset — the far rungs are mildly
|
||||
|
||||
@@ -200,6 +200,16 @@ protected:
|
||||
* shorter retry when an attempt is turned away because the radio was
|
||||
* mid-packet / transmitting / in its duty-cycle sleep window. */
|
||||
int64_t _noise_floor_next_ms;
|
||||
uint8_t _noise_floor_retries; /* consecutive blocked attempts, capped */
|
||||
/* Shared cadence for every periodic radio measurement (floor sample +
|
||||
* CAD probe). Runtime, from the probe.interval pref. */
|
||||
uint32_t _measure_interval_ms;
|
||||
/* Latest floor sample, published for cadMaintenance() so the CAD probe
|
||||
* shares this measurement instead of taking its own single RSSI read.
|
||||
* _sample_fresh is true only within the pass that produced it. */
|
||||
int16_t _sample_rssi;
|
||||
bool _sample_channel_quiet;
|
||||
bool _sample_fresh;
|
||||
|
||||
/* Adaptive CAD state */
|
||||
struct CadLevelStats {
|
||||
@@ -211,7 +221,7 @@ protected:
|
||||
CadLevelStats _cad_stats[CAD_NUM_LEVELS];
|
||||
bool _cad_auto; /* staircase acts on the stats */
|
||||
int8_t _cad_offset; /* operating detPeak offset (levels) */
|
||||
uint16_t _cad_probe_interval_s; /* 0 = probing disabled */
|
||||
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) */
|
||||
int64_t _cad_last_probe_ms;
|
||||
int64_t _cad_last_decay_ms;
|
||||
@@ -219,7 +229,6 @@ protected:
|
||||
* 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. */
|
||||
int64_t _cad_retry_ms;
|
||||
uint8_t _cad_probe_rr; /* round-robin index (sweep) / frontier mix counter */
|
||||
|
||||
int8_t pickCadProbeLevel();
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
* rather than the exception — a shorter retry there can push the wake rate
|
||||
* ABOVE the fixed tick this conversion replaced, inverting the whole point. */
|
||||
#define NOISE_FLOOR_RETRY_MS 5000
|
||||
/* 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;
|
||||
@@ -85,11 +87,11 @@
|
||||
#define CAD_BUSY_DEFER_HYST_PERMILLE 100 /* descend only if frontier busy <= cap-10% */
|
||||
#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 */
|
||||
/* Retry deadline for a due probe turned away by the idle-RX guards or the
|
||||
* RSSI prefilter. Those paths leave _cad_last_probe_ms untouched (by design —
|
||||
* a skipped probe is not a probe), so the deadline query needs its own marker
|
||||
* to avoid reporting "due now" on every wake. */
|
||||
#define CAD_PROBE_RETRY_MS 2000
|
||||
/* 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 */
|
||||
@@ -146,6 +148,39 @@ static inline enum lora_signal_bandwidth bw_khz_to_enum(uint16_t bw_khz)
|
||||
}
|
||||
}
|
||||
|
||||
/* Lowest physically-possible noise floor for a given bandwidth, in dBm.
|
||||
*
|
||||
* This is raw thermal noise — kTB at 290 K, with NO noise-figure term:
|
||||
* -174 dBm/Hz + 10*log10(BW_Hz)
|
||||
* A passive receiver cannot read below it, so it is the one place a sanity
|
||||
* clamp belongs: anything under this line is a bad RSSI read, not a quiet
|
||||
* site. Adding a receiver noise figure here would clamp ABOVE what the
|
||||
* hardware can legitimately report and manufacture a floor — which is exactly
|
||||
* what the old fixed -120 rail did to BW 62.5 kHz, pinning it on every EMA
|
||||
* update because -120 happens to be that preset's kTB+NF.
|
||||
*
|
||||
* Bandwidth is the only term that moves. SF changes the SNR the demodulator
|
||||
* can decode at, not the noise power in the channel, so it must NOT appear.
|
||||
*
|
||||
* For reference, a typical SX126x (NF ~6 dB) reads about 6 dB above these:
|
||||
* BW 62.5 kHz measures ~-120 dBm on a quiet site against a -126 kTB limit. */
|
||||
static inline int16_t noise_floor_min_dbm(uint16_t bw_khz)
|
||||
{
|
||||
switch (bw_khz) {
|
||||
case 7: return -135; /* 10*log10(7800) = 38.9 */
|
||||
case 10: return -134; /* 10*log10(10400) = 40.2 */
|
||||
case 15: return -132; /* 10*log10(15600) = 41.9 */
|
||||
case 20: return -131; /* 10*log10(20800) = 43.2 */
|
||||
case 31: return -129; /* 10*log10(31250) = 45.0 */
|
||||
case 41: return -128; /* 10*log10(41700) = 46.2 */
|
||||
case 62: return -126; /* 10*log10(62500) = 48.0 */
|
||||
case 125: return -123; /* 10*log10(125000) = 51.0 */
|
||||
case 250: return -120; /* 10*log10(250000) = 54.0 */
|
||||
case 500: return -117; /* 10*log10(500000) = 57.0 */
|
||||
default: return -123; /* matches bw_khz_to_enum's 125 kHz fallback */
|
||||
}
|
||||
}
|
||||
|
||||
/* CR 5-8 → Zephyr coding_rate enum */
|
||||
static inline enum lora_coding_rate cr_to_enum(uint8_t cr)
|
||||
{
|
||||
|
||||
@@ -209,7 +209,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
|
||||
* auto=0, offset=0, probe_interval=60) */
|
||||
fs_read(&file, &prefs.cad_auto, sizeof(prefs.cad_auto));
|
||||
fs_read(&file, &prefs.cad_offset, sizeof(prefs.cad_offset));
|
||||
fs_read(&file, &prefs.cad_probe_interval, sizeof(prefs.cad_probe_interval));
|
||||
fs_read(&file, &prefs.probe_interval, sizeof(prefs.probe_interval));
|
||||
/* cad_busycap absent in <301-byte files; EOF read keeps default 25 */
|
||||
fs_read(&file, &prefs.cad_busycap, sizeof(prefs.cad_busycap));
|
||||
|
||||
@@ -243,7 +243,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
|
||||
if (prefs.meshtimesync > 1) prefs.meshtimesync = 0;
|
||||
if (prefs.cad_auto > 1) prefs.cad_auto = 0;
|
||||
if (prefs.cad_offset < CAD_OFFSET_MIN || prefs.cad_offset > CAD_OFFSET_MAX) prefs.cad_offset = 0;
|
||||
if (prefs.cad_probe_interval != 0 && prefs.cad_probe_interval < 10) prefs.cad_probe_interval = 10;
|
||||
if (prefs.probe_interval != 0 && prefs.probe_interval < 10) prefs.probe_interval = 10;
|
||||
if (prefs.cad_busycap > 90) prefs.cad_busycap = 90;
|
||||
|
||||
/* One-time format upgrade: old files (< 294 bytes) never saved the ZephCore
|
||||
@@ -349,7 +349,7 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
|
||||
/* Adaptive CAD (offsets 297-300) */
|
||||
fs_write(&file, &prefs.cad_auto, sizeof(prefs.cad_auto));
|
||||
fs_write(&file, &prefs.cad_offset, sizeof(prefs.cad_offset));
|
||||
fs_write(&file, &prefs.cad_probe_interval, sizeof(prefs.cad_probe_interval));
|
||||
fs_write(&file, &prefs.probe_interval, sizeof(prefs.probe_interval));
|
||||
fs_write(&file, &prefs.cad_busycap, sizeof(prefs.cad_busycap));
|
||||
|
||||
ret = fs_sync(&file);
|
||||
|
||||
@@ -59,6 +59,8 @@ static void uplink_time_sync_cb(uint32_t unix_ts)
|
||||
if (s_uplink_mesh) {
|
||||
s_uplink_mesh->getRTCClock()->setCurrentTime(unix_ts);
|
||||
atomic_set(&s_uplink_sntp_pending, 1);
|
||||
/* Wake the main loop rather than waiting for its next deadline. */
|
||||
s_uplink_mesh->notifyWake();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1018,6 +1020,11 @@ void RepeaterMesh::begin(RepeaterDataStore* store) {
|
||||
* loop via an atomic flag drained in maintenanceLoop(). */
|
||||
if (s_uplink_mesh) {
|
||||
atomic_set(&s_uplink_mesh->_uplink_connect_pending, 1);
|
||||
/* Wake the main loop so "online" publishes immediately.
|
||||
* Without this the flag waits for whatever deadline fires
|
||||
* next — up to UPLINK_STATUS_INTERVAL_MS (5 min) on an idle
|
||||
* repeater, since maintenance no longer ticks every 5 s. */
|
||||
s_uplink_mesh->notifyWake();
|
||||
}
|
||||
});
|
||||
_uplink_next_status_at = futureMillis(UPLINK_STATUS_INTERVAL_MS);
|
||||
|
||||
@@ -179,7 +179,7 @@ protected:
|
||||
}
|
||||
void applyCadPrefs() override {
|
||||
_radio->setCadParams(_prefs.cad_auto != 0, _prefs.cad_offset,
|
||||
_prefs.cad_probe_interval, _prefs.cad_busycap);
|
||||
_prefs.probe_interval, _prefs.cad_busycap);
|
||||
}
|
||||
void resetCadStats() override {
|
||||
_radio->resetCadStats();
|
||||
|
||||
@@ -148,7 +148,7 @@ protected:
|
||||
}
|
||||
void applyCadPrefs() override {
|
||||
_radio->setCadParams(_prefs.cad_auto != 0, _prefs.cad_offset,
|
||||
_prefs.cad_probe_interval, _prefs.cad_busycap);
|
||||
_prefs.probe_interval, _prefs.cad_busycap);
|
||||
}
|
||||
void resetCadStats() override {
|
||||
_radio->resetCadStats();
|
||||
|
||||
@@ -126,7 +126,7 @@ void CommonCLI::loadPrefs(const char* path) {
|
||||
ok = ok && prefs_read(&file, &_prefs->meshtimesync, sizeof(_prefs->meshtimesync)); // 296
|
||||
ok = ok && prefs_read(&file, &_prefs->cad_auto, sizeof(_prefs->cad_auto)); // 297
|
||||
ok = ok && prefs_read(&file, &_prefs->cad_offset, sizeof(_prefs->cad_offset)); // 298
|
||||
ok = ok && prefs_read(&file, &_prefs->cad_probe_interval, sizeof(_prefs->cad_probe_interval)); // 299
|
||||
ok = ok && prefs_read(&file, &_prefs->probe_interval, sizeof(_prefs->probe_interval)); // 299
|
||||
ok = ok && prefs_read(&file, &_prefs->cad_busycap, sizeof(_prefs->cad_busycap)); // 300
|
||||
|
||||
if (!ok) {
|
||||
@@ -174,8 +174,8 @@ void CommonCLI::loadPrefs(const char* path) {
|
||||
_prefs->meshtimesync = constrain(_prefs->meshtimesync, (uint8_t)0, (uint8_t)1);
|
||||
_prefs->cad_auto = constrain(_prefs->cad_auto, (uint8_t)0, (uint8_t)1);
|
||||
_prefs->cad_offset = constrain(_prefs->cad_offset, (int8_t)CAD_OFFSET_MIN, (int8_t)CAD_OFFSET_MAX);
|
||||
if (_prefs->cad_probe_interval != 0 && _prefs->cad_probe_interval < 10) {
|
||||
_prefs->cad_probe_interval = 10;
|
||||
if (_prefs->probe_interval != 0 && _prefs->probe_interval < 10) {
|
||||
_prefs->probe_interval = 10;
|
||||
}
|
||||
_prefs->cad_busycap = constrain(_prefs->cad_busycap, (uint8_t)0, (uint8_t)90);
|
||||
|
||||
@@ -249,7 +249,7 @@ void CommonCLI::savePrefs(const char* path) {
|
||||
fs_write(&file, &_prefs->meshtimesync, sizeof(_prefs->meshtimesync));
|
||||
fs_write(&file, &_prefs->cad_auto, sizeof(_prefs->cad_auto));
|
||||
fs_write(&file, &_prefs->cad_offset, sizeof(_prefs->cad_offset));
|
||||
fs_write(&file, &_prefs->cad_probe_interval, sizeof(_prefs->cad_probe_interval));
|
||||
fs_write(&file, &_prefs->probe_interval, sizeof(_prefs->probe_interval));
|
||||
fs_write(&file, &_prefs->cad_busycap, sizeof(_prefs->cad_busycap));
|
||||
|
||||
fs_close(&file);
|
||||
@@ -566,6 +566,11 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
} else if (memcmp(config, "dc.restarts", 11) == 0) {
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %u",
|
||||
(uint32_t)_callbacks->getDutyCycleTimeoutRestarts());
|
||||
} else if (memcmp(config, "probe.interval", 14) == 0) {
|
||||
/* Seconds between periodic radio measurements — the noise-floor
|
||||
* sample and the CAD probe that consumes it. 0 = probing off. */
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %u",
|
||||
(uint32_t)_prefs->probe_interval);
|
||||
} else if (memcmp(config, "cad", 3) == 0) {
|
||||
/* Runtime state + per-level probe stats live in the radio.
|
||||
* Remote replies get the truncated buffer like meshtimesync. */
|
||||
@@ -653,12 +658,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "cad.probe.interval ", 19) == 0) {
|
||||
int val = atoi(&config[19]);
|
||||
/* Governs every periodic radio measurement, not just CAD — the
|
||||
* noise-floor sampler and the CAD probe share one reading. */
|
||||
} else if (memcmp(config, "probe.interval ", 15) == 0) {
|
||||
int val = atoi(&config[15]);
|
||||
if (val != 0 && (val < 10 || val > 255)) {
|
||||
strcpy(reply, "Error: interval is 0 (off) or 10-255 seconds");
|
||||
strcpy(reply, "Error: interval is 0 (probing off) or 10-255 seconds");
|
||||
} else {
|
||||
_prefs->cad_probe_interval = (uint8_t)val;
|
||||
_prefs->probe_interval = (uint8_t)val;
|
||||
_callbacks->applyCadPrefs();
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
|
||||
@@ -90,7 +90,9 @@ struct NodePrefs {
|
||||
uint8_t meshtimesync; // 1 = mesh time-sync clock correction on (default off)
|
||||
uint8_t cad_auto; // 1 = adaptive-CAD staircase acts on probe stats (default off = dry-run)
|
||||
int8_t cad_offset; // operating detPeak offset from family base (-4..4)
|
||||
uint8_t cad_probe_interval; // seconds between CAD probes (0 = probing off, default 60)
|
||||
uint8_t probe_interval; // seconds between periodic radio measurements:
|
||||
// one noise-floor sample, and the CAD probe that
|
||||
// consumes it (0 = CAD probing off, default 15)
|
||||
uint8_t cad_busycap; // airtime-protection: max % of TX attempts deferred before backing off detPeak (0 = off, default 25)
|
||||
|
||||
/* ---- Companion-only fields ---- */
|
||||
@@ -165,7 +167,7 @@ static inline void initNodePrefs(NodePrefs* prefs) {
|
||||
prefs->_reserved_apc_margin = 0; // reserved (was APC), see NodePrefs
|
||||
prefs->cad_auto = 1; // Default ON — adaptive staircase acts on probe stats
|
||||
prefs->cad_offset = 0; // Start at family base detPeak (SF+13 on SX126x)
|
||||
prefs->cad_probe_interval = 15; // 15 s → staircase responds to change in ~1-2 h
|
||||
prefs->probe_interval = 15; // floor sample + CAD probe; staircase responds in ~1-2 h
|
||||
prefs->cad_busycap = 25; // back off detPeak once >25% of TX attempts are deferred
|
||||
prefs->wake_on_msg = 1; // Default ON — wake display when message arrives
|
||||
prefs->v_contact_enabled = 1; // Default ON — v-contact loopback admin chat (companion)
|
||||
|
||||
@@ -35,6 +35,12 @@ public:
|
||||
/* Notifies event loop of pending TX so it can schedule a wake. */
|
||||
typedef void (*tx_queued_callback_t)(uint32_t delay_ms, void *user_data);
|
||||
|
||||
/* Wakes the event loop so loop() runs at the next opportunity. For off-main
|
||||
* code that sets state loop() must drain (MQTT CONNACK, SNTP): without it that
|
||||
* state waits for whatever deadline happens to fire next, which since the move
|
||||
* to deadline-driven maintenance can be minutes rather than the old 5 s tick. */
|
||||
typedef void (*wake_callback_t)(void *user_data);
|
||||
|
||||
typedef uint32_t DispatcherAction;
|
||||
|
||||
#define ACTION_RELEASE (0)
|
||||
@@ -66,6 +72,8 @@ class Dispatcher {
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
tx_queued_callback_t _tx_queued_cb;
|
||||
void *_tx_queued_user_data;
|
||||
wake_callback_t _wake_cb;
|
||||
void *_wake_user_data;
|
||||
|
||||
void processRecvPacket(Packet *pkt);
|
||||
|
||||
@@ -126,6 +134,15 @@ public:
|
||||
_tx_queued_cb = cb;
|
||||
_tx_queued_user_data = user_data;
|
||||
}
|
||||
void setWakeCallback(wake_callback_t cb, void *user_data) {
|
||||
_wake_cb = cb;
|
||||
_wake_user_data = user_data;
|
||||
}
|
||||
/* Safe from any thread: the callback only posts an event. Public because
|
||||
* off-main C callbacks (e.g. the SNTP hook) are not class members. */
|
||||
void notifyWake() {
|
||||
if (_wake_cb) _wake_cb(_wake_user_data);
|
||||
}
|
||||
bool millisHasNowPassed(uint32_t timestamp) const;
|
||||
uint32_t futureMillis(int millis_from_now) const;
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
_tx_queued_cb = nullptr;
|
||||
_tx_queued_user_data = nullptr;
|
||||
_wake_cb = nullptr;
|
||||
_wake_user_data = nullptr;
|
||||
}
|
||||
|
||||
void Dispatcher::begin()
|
||||
|
||||
@@ -737,7 +737,7 @@ public:
|
||||
void applyCadPrefs() override {
|
||||
lora_radio.setCadParams(companion_mesh.prefs.cad_auto != 0,
|
||||
companion_mesh.prefs.cad_offset,
|
||||
companion_mesh.prefs.cad_probe_interval,
|
||||
companion_mesh.prefs.probe_interval,
|
||||
companion_mesh.prefs.cad_busycap);
|
||||
}
|
||||
void resetCadStats() override {
|
||||
@@ -1385,7 +1385,7 @@ int main(void)
|
||||
* its proper default. A hand-maintained subset here silently drifts: any
|
||||
* field not listed defaults to 0, and on upgrade the past-EOF read in
|
||||
* loadPrefs then keeps that 0 instead of the real default (this is what
|
||||
* zeroed cad_probe_interval / cad_auto and, earlier, the GPS settings). */
|
||||
* zeroed probe_interval / cad_auto and, earlier, the GPS settings). */
|
||||
initNodePrefs(&companion_mesh.prefs);
|
||||
/* Companion-specific overrides vs. initNodePrefs defaults: */
|
||||
companion_mesh.prefs.auto_shutdown_mv = CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS; /* low-batt cutoff (0=off) */
|
||||
@@ -1547,7 +1547,7 @@ int main(void)
|
||||
lora_radio.enableRxDutyCycle(companion_mesh.prefs.rx_duty_cycle != 0);
|
||||
lora_radio.setCadParams(companion_mesh.prefs.cad_auto != 0,
|
||||
companion_mesh.prefs.cad_offset,
|
||||
companion_mesh.prefs.cad_probe_interval,
|
||||
companion_mesh.prefs.probe_interval,
|
||||
companion_mesh.prefs.cad_busycap);
|
||||
ui_set_radio_runtime(
|
||||
lora_radio.getActiveSyncWord(),
|
||||
|
||||
@@ -103,7 +103,8 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios);
|
||||
#define MESH_EVENT_TX_DRAIN BIT(5) /* Outbound packet delay expired, run checkSend */
|
||||
#define MESH_EVENT_RTC_SAVE BIT(6) /* Hardware-RTC write requested off-main */
|
||||
#define MESH_EVENT_INIT_ADVERT BIT(7) /* Deferred boot advert — send on main thread */
|
||||
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_MAINTENANCE | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_RTC_SAVE | MESH_EVENT_INIT_ADVERT)
|
||||
#define MESH_EVENT_WAKE BIT(8) /* Off-main state set; run loop() promptly */
|
||||
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_MAINTENANCE | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_RTC_SAVE | MESH_EVENT_INIT_ADVERT | MESH_EVENT_WAKE)
|
||||
|
||||
/* Maintenance is deadline-driven, not periodic: after every pass the loop asks
|
||||
* the mesh when its soonest pending deadline is (msUntilNextMaintenance) and
|
||||
@@ -290,10 +291,11 @@ static void maintenance_timer_fn(struct k_timer *timer)
|
||||
static void arm_maintenance_wake(void)
|
||||
{
|
||||
uint32_t delay = MAINTENANCE_BACKSTOP_MS;
|
||||
uint32_t next = MAINTENANCE_BACKSTOP_MS;
|
||||
|
||||
#ifdef ZEPHCORE_LORA
|
||||
if (repeater_mesh_ptr) {
|
||||
uint32_t next = repeater_mesh_ptr->msUntilNextMaintenance();
|
||||
next = repeater_mesh_ptr->msUntilNextMaintenance();
|
||||
|
||||
if (next < delay) {
|
||||
delay = next;
|
||||
@@ -305,6 +307,15 @@ static void arm_maintenance_wake(void)
|
||||
delay = MAINTENANCE_MIN_MS;
|
||||
}
|
||||
|
||||
/* Both figures, because which one is binding is the whole diagnosis:
|
||||
* next=15000 armed=15000 — a real deadline won; working as intended
|
||||
* next=15000 armed=5000 — the backstop is clamping (it must stay
|
||||
* above the noise-floor/CAD intervals)
|
||||
* next=0 armed=50 — a deadline reports due but its state is
|
||||
* not advancing; repeated = spin
|
||||
* mesh::MAINTENANCE_IDLE (0x7FFFFFFF) as `next` means nothing pending. */
|
||||
LOG_DBG("maint: next=%u armed=%u", (unsigned)next, (unsigned)delay);
|
||||
|
||||
k_timer_start(&maintenance_timer, K_MSEC(delay), K_NO_WAIT);
|
||||
}
|
||||
|
||||
@@ -346,6 +357,15 @@ static void tx_queued_callback(uint32_t delay_ms, void *user_data)
|
||||
ARG_UNUSED(user_data);
|
||||
k_work_reschedule(&tx_drain_work, K_MSEC(delay_ms));
|
||||
}
|
||||
|
||||
/* Off-main code (MQTT CONNACK, SNTP) set state that loop() must drain. Post
|
||||
* only — this runs on the MQTT publisher / WiFi thread, and k_event_post is
|
||||
* the sole thing safe to do from there. */
|
||||
static void wake_callback(void *user_data)
|
||||
{
|
||||
ARG_UNUSED(user_data);
|
||||
k_event_post(&mesh_events, MESH_EVENT_WAKE);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Global instances */
|
||||
@@ -487,23 +507,44 @@ static void repeater_event_loop(void)
|
||||
/* Packet processing — only on radio/CLI/TX events */
|
||||
if (repeater_mesh_ptr &&
|
||||
(events & (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE |
|
||||
MESH_EVENT_CLI_RX | MESH_EVENT_TX_DRAIN))) {
|
||||
MESH_EVENT_CLI_RX | MESH_EVENT_TX_DRAIN |
|
||||
MESH_EVENT_WAKE))) {
|
||||
repeater_mesh_ptr->loop();
|
||||
}
|
||||
#endif
|
||||
|
||||
/* A maintenance deadline came due — run the pass. */
|
||||
#ifdef ZEPHCORE_LORA
|
||||
/* Radio maintenance runs OPPORTUNISTICALLY, on every pass, whatever
|
||||
* woke us — not only when its own timer fired.
|
||||
*
|
||||
* Every item inside is deadline-gated internally, so this is nearly
|
||||
* free when nothing is due. The point is what it does to a busy
|
||||
* node: a hilltop repeater is already waking constantly for packets,
|
||||
* so maintenance rides along on those wakes, its deadlines advance,
|
||||
* and arm_maintenance_wake() below keeps pushing the timer out — the
|
||||
* maintenance timer then almost never fires and costs no wakes at
|
||||
* all. Running it only on its own event (as this did originally)
|
||||
* inverted that: the busiest nodes, which can least afford it, paid
|
||||
* the full timer cadence on top of their packet wakes, and every
|
||||
* blocked sample burned a retry against a channel that is busy for
|
||||
* sustained reasons (real traffic in isReceiving(), the RSSI
|
||||
* prefilter) rather than the transient duty-cycle sleep window the
|
||||
* retries were sized for.
|
||||
*
|
||||
* Ordering matters: this sits AFTER packet processing so an inbound
|
||||
* frame is handled before we spend SPI time on an RSSI sweep. */
|
||||
if (repeater_mesh_ptr) {
|
||||
repeater_mesh_ptr->maintenanceLoop();
|
||||
}
|
||||
#endif
|
||||
|
||||
/* A maintenance deadline came due — run the time-based work too. */
|
||||
if (events & MESH_EVENT_MAINTENANCE) {
|
||||
#ifdef ZEPHCORE_LORA
|
||||
/* Radio maintenance: noise floor calibration, AGC reset,
|
||||
* RX watchdog. Separated from loop() so these never run
|
||||
* on packet-driven events. */
|
||||
/* Drive loop() so time-based actions (advert timers,
|
||||
* tempradio set/revert, contacts flush, uplink status)
|
||||
* still fire when no LoRa/CLI traffic wakes the loop. */
|
||||
if (repeater_mesh_ptr) {
|
||||
repeater_mesh_ptr->maintenanceLoop();
|
||||
/* Also drive loop() so time-based actions (advert
|
||||
* timers, tempradio set/revert, contacts flush,
|
||||
* uplink status) still fire when no LoRa/CLI
|
||||
* traffic wakes the event loop. */
|
||||
repeater_mesh_ptr->loop();
|
||||
}
|
||||
#endif
|
||||
@@ -633,6 +674,7 @@ int main(void)
|
||||
lora_radio.setRxCallback(lora_rx_callback, nullptr);
|
||||
lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr);
|
||||
repeater_mesh.setTxQueuedCallback(tx_queued_callback, nullptr);
|
||||
repeater_mesh.setWakeCallback(wake_callback, nullptr);
|
||||
|
||||
/* Load or generate identity BEFORE begin(). First-boot keygen runs
|
||||
* the layered entropy mixer + Ed25519 derive + reserved-prefix
|
||||
@@ -680,7 +722,7 @@ int main(void)
|
||||
lora_radio.setRxBoost(prefs->rx_boost != 0);
|
||||
lora_radio.enableRxDutyCycle(prefs->rx_duty_cycle != 0);
|
||||
lora_radio.setCadParams(prefs->cad_auto != 0, prefs->cad_offset,
|
||||
prefs->cad_probe_interval, prefs->cad_busycap);
|
||||
prefs->probe_interval, prefs->cad_busycap);
|
||||
|
||||
/* Feed initial UI state from loaded prefs */
|
||||
ui_set_node_name(prefs->node_name);
|
||||
|
||||
@@ -610,7 +610,7 @@ int main(void)
|
||||
lora_radio.setRxBoost(prefs->rx_boost != 0);
|
||||
lora_radio.enableRxDutyCycle(prefs->rx_duty_cycle != 0);
|
||||
lora_radio.setCadParams(prefs->cad_auto != 0, prefs->cad_offset,
|
||||
prefs->cad_probe_interval, prefs->cad_busycap);
|
||||
prefs->probe_interval, prefs->cad_busycap);
|
||||
|
||||
/* Feed initial UI state from loaded prefs */
|
||||
ui_set_node_name(prefs->node_name);
|
||||
|
||||
Reference in New Issue
Block a user