mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 04:08:22 +00:00
add cad.busycap knob for hilltop repeaters
This commit is contained in:
@@ -73,6 +73,14 @@ FP has already bottomed out. Probes sample the operating level and **both
|
||||
neighbours** (op−1 more sensitive, op+1 less sensitive; op weighted half, each
|
||||
neighbour a quarter) so the staircase can read the local curve *slope*:
|
||||
|
||||
- **Airtime protection** (highest priority): step up when the operating
|
||||
level's *total* busy rate — real traffic plus false positives — exceeds
|
||||
the busy cap (`set cad.busycap`, percent; default **25%**, 0 = off). The
|
||||
knee logic only minimises *false* busy, but on a congested hilltop most
|
||||
busy verdicts are real distant traffic we'd win on capture anyway;
|
||||
deferring for all of it starves the node's own airtime. Self-targeting —
|
||||
a quiet node's busy rate never reaches the cap, so only a genuinely busy
|
||||
backbone backs off. Shown as `bc:` in `get cad`.
|
||||
- **Step up** (less sensitive) when the level above is markedly cleaner —
|
||||
FP drops ≥5%/level (`CAD_KNEE_SLOPE_PERMILLE`). That means we're on the
|
||||
steep part below the knee; climb toward it.
|
||||
|
||||
@@ -208,7 +208,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
|
||||
| `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 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 + per-level probe statistics: auto on/off, operating detPeak offset and absolute peak (with family base), probe interval, then one line per probed level with probe/busy/fp/tp counts and false-positive rate. 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 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 |
|
||||
| `get bootloader.ver` | Bootloader version string |
|
||||
| `get public.key` | *(USB only)* Node's public key as hex |
|
||||
@@ -256,6 +256,7 @@ Changes are persisted immediately unless noted. Some require a reboot.
|
||||
| `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 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)* |
|
||||
|
||||
|
||||
@@ -740,6 +740,15 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
|
||||
prefs.cad_probe_interval = 10;
|
||||
}
|
||||
}
|
||||
|
||||
/* Offset 158: cad_busycap (ZephCore extension, percent; 0 = off).
|
||||
* Absent in pre-existing files → keep the in-RAM default (25). */
|
||||
if (off < len) {
|
||||
prefs.cad_busycap = buf[off++];
|
||||
if (prefs.cad_busycap > 90) {
|
||||
prefs.cad_busycap = 90;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
|
||||
@@ -827,7 +836,9 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
|
||||
buf[off++] = (uint8_t)prefs.cad_offset;
|
||||
/* Offset 157: cad_probe_interval (ZephCore extension, seconds) */
|
||||
buf[off++] = prefs.cad_probe_interval;
|
||||
/* Total: 158 bytes */
|
||||
/* Offset 158: cad_busycap (ZephCore extension, percent) */
|
||||
buf[off++] = prefs.cad_busycap;
|
||||
/* Total: 159 bytes */
|
||||
|
||||
bool ok = atomicReplaceFile(PREFS_FILE, buf, off);
|
||||
LOG_DBG("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'",
|
||||
|
||||
@@ -48,6 +48,7 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
|
||||
_rx_head(0), _rx_tail(0),
|
||||
_noise_floor(DEFAULT_NOISE_FLOOR), _calibration_threshold(0), _ema_unguarded(0),
|
||||
_cad_auto(false), _cad_offset(0), _cad_probe_interval_s(0),
|
||||
_cad_busycap_pct(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),
|
||||
@@ -948,7 +949,7 @@ bool LoRaRadioBase::isChannelActive(int threshold)
|
||||
/* ── Adaptive CAD (LBT detPeak calibration) ───────────────────────────── */
|
||||
|
||||
void LoRaRadioBase::setCadParams(bool auto_enabled, int8_t offset,
|
||||
uint16_t probe_interval_s)
|
||||
uint16_t probe_interval_s, uint8_t busycap_pct)
|
||||
{
|
||||
if (offset < CAD_LEVEL_MIN) offset = CAD_LEVEL_MIN;
|
||||
if (offset > CAD_LEVEL_MAX) offset = CAD_LEVEL_MAX;
|
||||
@@ -956,10 +957,12 @@ void LoRaRadioBase::setCadParams(bool auto_enabled, int8_t offset,
|
||||
_cad_auto = auto_enabled;
|
||||
_cad_offset = offset;
|
||||
_cad_probe_interval_s = probe_interval_s;
|
||||
_cad_busycap_pct = busycap_pct;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
|
||||
LOG_INF("cad: auto=%d offset=%d probe_interval=%us",
|
||||
(int)auto_enabled, (int)offset, (unsigned)probe_interval_s);
|
||||
LOG_INF("cad: auto=%d offset=%d probe_interval=%us busycap=%u%%",
|
||||
(int)auto_enabled, (int)offset, (unsigned)probe_interval_s,
|
||||
(unsigned)busycap_pct);
|
||||
}
|
||||
|
||||
void LoRaRadioBase::resetCadStats()
|
||||
@@ -1014,24 +1017,49 @@ void LoRaRadioBase::cadStaircaseStep()
|
||||
* bottomed out), using slopes so the decision is site-floor-independent. */
|
||||
int oi = _cad_offset - CAD_LEVEL_MIN;
|
||||
|
||||
/* Per-level FP rate in permille, or -1 when too few samples to trust. */
|
||||
auto rate = [&](int idx) -> int {
|
||||
if (idx < 0 || idx >= CAD_NUM_LEVELS) {
|
||||
auto warm = [&](int idx) -> bool {
|
||||
return idx >= 0 && idx < CAD_NUM_LEVELS &&
|
||||
_cad_stats[idx].probes >= CAD_STEP_MIN_PROBES;
|
||||
};
|
||||
/* Per-level FALSE-positive rate in permille, or -1 when too few samples. */
|
||||
auto fp_rate = [&](int idx) -> int {
|
||||
if (!warm(idx)) {
|
||||
return -1;
|
||||
}
|
||||
CadLevelStats &s = _cad_stats[idx];
|
||||
if (s.probes < CAD_STEP_MIN_PROBES) {
|
||||
return (int)(((uint32_t)_cad_stats[idx].fp * 1000U)
|
||||
/ _cad_stats[idx].probes);
|
||||
};
|
||||
/* Per-level TOTAL busy (defer) rate in permille — false + real traffic. */
|
||||
auto busy_rate = [&](int idx) -> int {
|
||||
if (!warm(idx)) {
|
||||
return -1;
|
||||
}
|
||||
return (int)(((uint32_t)s.fp * 1000U) / s.probes);
|
||||
return (int)(((uint32_t)_cad_stats[idx].busy * 1000U)
|
||||
/ _cad_stats[idx].probes);
|
||||
};
|
||||
|
||||
int r_op = rate(oi);
|
||||
int r_op = fp_rate(oi);
|
||||
if (r_op < 0) {
|
||||
return; /* operating level not warm yet — no basis to step */
|
||||
}
|
||||
int r_up = rate(oi + 1); /* one step less sensitive */
|
||||
int r_dn = rate(oi - 1); /* frontier, one step more sensitive */
|
||||
int b_op = busy_rate(oi);
|
||||
int r_up = fp_rate(oi + 1); /* one step less sensitive */
|
||||
int r_dn = fp_rate(oi - 1); /* frontier, one step more sensitive */
|
||||
|
||||
/* Airtime protection (highest priority): if the operating level defers
|
||||
* too large a fraction of TX attempts — real traffic included — back off
|
||||
* to a less sensitive detPeak. On a congested hilltop most of that busy
|
||||
* is distant traffic we'd win on capture anyway; deferring for all of it
|
||||
* just starves our own airtime. Cap is `set cad.busycap` percent (0 =
|
||||
* off); only binds on genuinely busy channels. */
|
||||
int cap_permille = (int)_cad_busycap_pct * 10;
|
||||
if (cap_permille && _cad_offset < CAD_LEVEL_MAX && b_op > cap_permille) {
|
||||
_cad_offset++;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
LOG_INF("cad: step up -> offset %d (airtime, busy %d cap %d)",
|
||||
(int)_cad_offset, b_op, cap_permille);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Step UP (less sensitive) when the level above is markedly cleaner —
|
||||
* we're on the steep part of the curve, below the knee. */
|
||||
@@ -1047,15 +1075,19 @@ void LoRaRadioBase::cadStaircaseStep()
|
||||
/* Step DOWN (more sensitive) only on a flat plateau that is already
|
||||
* clean: the frontier is no worse than operating (nothing to lose) AND
|
||||
* FP here is low enough that reclaiming sensitivity is cheap. The clean
|
||||
* guard is what keeps a flat-but-noisy curve from descending to the
|
||||
* sensitive rail — there, holding position is the least-bad move. */
|
||||
* guard keeps a flat-but-noisy curve from descending to the sensitive
|
||||
* rail; the busy-hysteresis guard keeps us from descending into the
|
||||
* airtime cap and bouncing straight back up. */
|
||||
int b_dn = busy_rate(oi - 1);
|
||||
bool busy_ok = (cap_permille == 0) ||
|
||||
(b_dn <= cap_permille - CAD_BUSY_DEFER_HYST_PERMILLE);
|
||||
if (_cad_offset > CAD_LEVEL_MIN && r_dn >= 0 &&
|
||||
r_dn - r_op < CAD_KNEE_SLOPE_PERMILLE &&
|
||||
r_op <= CAD_PLATEAU_CLEAN_PERMILLE) {
|
||||
r_op <= CAD_PLATEAU_CLEAN_PERMILLE && busy_ok) {
|
||||
_cad_offset--;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
LOG_INF("cad: step down -> offset %d (op %d dn %d)",
|
||||
(int)_cad_offset, r_op, r_dn);
|
||||
LOG_INF("cad: step down -> offset %d (op %d dn %d busy %d)",
|
||||
(int)_cad_offset, r_op, r_dn, b_dn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1199,16 +1231,17 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
|
||||
}
|
||||
|
||||
/* Terse on purpose — remote replies are capped at ~160 B over LoRa.
|
||||
* Header: a:on o:1 pk:22(b21/4s) iv:15s
|
||||
* Header: a:on o:1 pk:22(b21/4s) iv:15s bc:25%
|
||||
* a auto on/off o offset pk operating peak
|
||||
* b family base 4s symbols iv probe interval
|
||||
* b family base 4s symbols iv probe interval bc busy cap
|
||||
* Level: *+1(22) 22p 18b 16f 2t 72%
|
||||
* '*' = operating rung level(peak) probes busy fp tp fp-rate%%. */
|
||||
n += snprintf(buf + n, cap > n ? cap - n : 0,
|
||||
"a:%s o:%d pk:%d(b%u/4s) iv:%us",
|
||||
"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)_cad_probe_interval_s,
|
||||
(unsigned)_cad_busycap_pct);
|
||||
|
||||
/* Only the 3 rungs around the operating offset — the far rungs are mildly
|
||||
* irrelevant; what matters is where we sit on the ladder. The window is
|
||||
|
||||
@@ -111,7 +111,7 @@ public:
|
||||
|
||||
/* Adaptive CAD (LBT detPeak calibration) */
|
||||
void setCadParams(bool auto_enabled, int8_t offset,
|
||||
uint16_t probe_interval_s) override;
|
||||
uint16_t probe_interval_s, uint8_t busycap_pct) override;
|
||||
void cadMaintenance() override;
|
||||
int8_t getCadOffset() const override { return _cad_offset; }
|
||||
void resetCadStats() override;
|
||||
@@ -211,6 +211,7 @@ protected:
|
||||
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 */
|
||||
uint8_t _cad_busycap_pct; /* airtime cap: max % TX deferred (0 = off) */
|
||||
int64_t _cad_last_probe_ms;
|
||||
int64_t _cad_last_decay_ms;
|
||||
uint8_t _cad_probe_rr; /* round-robin index (sweep) / frontier mix counter */
|
||||
|
||||
@@ -54,6 +54,17 @@
|
||||
#define CAD_KNEE_SLOPE_PERMILLE 50 /* >=5%/level FP change = steep */
|
||||
#define CAD_PLATEAU_CLEAN_PERMILLE 50 /* <=5% FP = clean enough to descend */
|
||||
#define CAD_STEP_MIN_PROBES 120 /* per-level samples before a step call */
|
||||
/* Airtime-protection cap on the TOTAL busy (defer) rate — false positives AND
|
||||
* real traffic. The knee controller only minimises *false* busy, but on a
|
||||
* congested hilltop most busy verdicts are real distant traffic we'd never
|
||||
* actually collide with (capture effect), and deferring for all of it starves
|
||||
* the node's own airtime. When the operating level's busy rate exceeds the
|
||||
* cap, step UP (less sensitive) regardless of FP — self-targeting, since a
|
||||
* quiet node's busy rate never reaches it. HYST keeps a descend from bouncing
|
||||
* straight back into the cap. The cap itself is a per-node pref
|
||||
* (`cad_busycap`, percent, `set cad.busycap`; default 25, 0 = off) since it is
|
||||
* a policy call (airtime vs. collision/capture), not a physical constant. */
|
||||
#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 */
|
||||
|
||||
|
||||
@@ -208,6 +208,8 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
|
||||
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));
|
||||
/* cad_busycap absent in <301-byte files; EOF read keeps default 25 */
|
||||
fs_read(&file, &prefs.cad_busycap, sizeof(prefs.cad_busycap));
|
||||
|
||||
fs_close(&file);
|
||||
|
||||
@@ -242,6 +244,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
|
||||
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.cad_busycap > 90) prefs.cad_busycap = 90;
|
||||
|
||||
/* One-time format upgrade: old files (< 294 bytes) never saved the ZephCore
|
||||
* extension fields, and stored path_hash_mode/loop_detect as zero padding.
|
||||
@@ -341,10 +344,11 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
|
||||
fs_write(&file, &prefs.flood_max_advert, sizeof(prefs.flood_max_advert));
|
||||
/* Mesh time sync on/off (offset 296) */
|
||||
fs_write(&file, &prefs.meshtimesync, sizeof(prefs.meshtimesync));
|
||||
/* Adaptive CAD (offsets 297-299) */
|
||||
/* 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.cad_busycap, sizeof(prefs.cad_busycap));
|
||||
|
||||
ret = fs_sync(&file);
|
||||
fs_close(&file);
|
||||
|
||||
@@ -185,7 +185,7 @@ protected:
|
||||
}
|
||||
void applyCadPrefs() override {
|
||||
_radio->setCadParams(_prefs.cad_auto != 0, _prefs.cad_offset,
|
||||
_prefs.cad_probe_interval);
|
||||
_prefs.cad_probe_interval, _prefs.cad_busycap);
|
||||
}
|
||||
void resetCadStats() override {
|
||||
_radio->resetCadStats();
|
||||
|
||||
@@ -154,7 +154,7 @@ protected:
|
||||
}
|
||||
void applyCadPrefs() override {
|
||||
_radio->setCadParams(_prefs.cad_auto != 0, _prefs.cad_offset,
|
||||
_prefs.cad_probe_interval);
|
||||
_prefs.cad_probe_interval, _prefs.cad_busycap);
|
||||
}
|
||||
void resetCadStats() override {
|
||||
_radio->resetCadStats();
|
||||
|
||||
@@ -124,6 +124,7 @@ void CommonCLI::loadPrefs(const char* path) {
|
||||
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->cad_busycap, sizeof(_prefs->cad_busycap)); // 300
|
||||
|
||||
if (!ok) {
|
||||
LOG_WRN("Prefs file %s truncated, some fields use defaults", path);
|
||||
@@ -175,6 +176,7 @@ void CommonCLI::loadPrefs(const char* path) {
|
||||
if (_prefs->cad_probe_interval != 0 && _prefs->cad_probe_interval < 10) {
|
||||
_prefs->cad_probe_interval = 10;
|
||||
}
|
||||
_prefs->cad_busycap = constrain(_prefs->cad_busycap, (uint8_t)0, (uint8_t)90);
|
||||
|
||||
LOG_INF("Loaded prefs from %s", path);
|
||||
}
|
||||
@@ -246,6 +248,7 @@ void CommonCLI::savePrefs(const char* path) {
|
||||
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->cad_busycap, sizeof(_prefs->cad_busycap));
|
||||
|
||||
fs_close(&file);
|
||||
LOG_INF("Saved prefs to %s", path);
|
||||
@@ -682,6 +685,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "cad.busycap ", 12) == 0) {
|
||||
int val = atoi(&config[12]);
|
||||
if (val != 0 && (val < 10 || val > 90)) {
|
||||
strcpy(reply, "Error: busycap is 0 (off) or 10-90 percent");
|
||||
} else {
|
||||
_prefs->cad_busycap = (uint8_t)val;
|
||||
_callbacks->applyCadPrefs();
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "cad.reset", 9) == 0) {
|
||||
_callbacks->resetCadStats();
|
||||
strcpy(reply, "OK - CAD probe stats cleared");
|
||||
|
||||
@@ -80,6 +80,7 @@ struct NodePrefs {
|
||||
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 cad_busycap; // airtime-protection: max % of TX attempts deferred before backing off detPeak (0 = off, default 25)
|
||||
|
||||
/* ---- Companion-only fields ---- */
|
||||
uint8_t manual_add_contacts;
|
||||
@@ -154,6 +155,7 @@ static inline void initNodePrefs(NodePrefs* prefs) {
|
||||
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->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)
|
||||
prefs->v_battery_alert_mv = 0xFFFF; // Sentinel: derive from board auto-shutdown threshold
|
||||
|
||||
@@ -44,8 +44,9 @@ public:
|
||||
/* Adaptive CAD (LBT detPeak calibration). Default no-ops for radios
|
||||
* without hardware CAD (SX127x). */
|
||||
virtual void setCadParams(bool auto_enabled, int8_t offset,
|
||||
uint16_t probe_interval_s) {
|
||||
uint16_t probe_interval_s, uint8_t busycap_pct) {
|
||||
(void)auto_enabled; (void)offset; (void)probe_interval_s;
|
||||
(void)busycap_pct;
|
||||
}
|
||||
/* One housekeeping tick of the CAD calibrator: maybe run a probe,
|
||||
* update stats, maybe step the staircase (auto mode). */
|
||||
|
||||
@@ -719,7 +719,8 @@ 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.cad_probe_interval,
|
||||
companion_mesh.prefs.cad_busycap);
|
||||
}
|
||||
void resetCadStats() override {
|
||||
lora_radio.resetCadStats();
|
||||
@@ -1567,7 +1568,8 @@ 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.cad_probe_interval,
|
||||
companion_mesh.prefs.cad_busycap);
|
||||
#ifdef CONFIG_ZEPHCORE_APC
|
||||
ui_set_radio_runtime(
|
||||
lora_radio.getEffectiveTxPower(),
|
||||
|
||||
@@ -631,7 +631,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_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_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