diff --git a/zephcore/ADAPTIVE_CAD.md b/zephcore/ADAPTIVE_CAD.md index f4ed7f4..9c86262 100644 --- a/zephcore/ADAPTIVE_CAD.md +++ b/zephcore/ADAPTIVE_CAD.md @@ -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. diff --git a/zephcore/Repeater_CLI_commands.md b/zephcore/Repeater_CLI_commands.md index cdfe6b6..b03410b 100644 --- a/zephcore/Repeater_CLI_commands.md +++ b/zephcore/Repeater_CLI_commands.md @@ -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 ` | 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 ` | −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 ` | 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 ` | 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 ` | 64-char hex (32-byte key) | Replace private key; derive new identity *(reboot to apply)* | diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index f33f991..11919f4 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -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'", diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index e5a9ef7..c928fc2 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -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 diff --git a/zephcore/adapters/radio/LoRaRadioBase.h b/zephcore/adapters/radio/LoRaRadioBase.h index ca62815..462330f 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.h +++ b/zephcore/adapters/radio/LoRaRadioBase.h @@ -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 */ diff --git a/zephcore/adapters/radio/radio_common.h b/zephcore/adapters/radio/radio_common.h index c1ffea1..e968406 100644 --- a/zephcore/adapters/radio/radio_common.h +++ b/zephcore/adapters/radio/radio_common.h @@ -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 */ diff --git a/zephcore/app/RepeaterDataStore.cpp b/zephcore/app/RepeaterDataStore.cpp index 9085075..7aafb38 100644 --- a/zephcore/app/RepeaterDataStore.cpp +++ b/zephcore/app/RepeaterDataStore.cpp @@ -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); diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index 830b5a9..6887c6d 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -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(); diff --git a/zephcore/app/RoomServerMesh.h b/zephcore/app/RoomServerMesh.h index 2e43daf..8040f0d 100644 --- a/zephcore/app/RoomServerMesh.h +++ b/zephcore/app/RoomServerMesh.h @@ -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(); diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index b65fcbd..fcc4b02 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -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"); diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 6a777b8..fc88c99 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -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 diff --git a/zephcore/include/mesh/Radio.h b/zephcore/include/mesh/Radio.h index 4af9647..c9ca97e 100644 --- a/zephcore/include/mesh/Radio.h +++ b/zephcore/include/mesh/Radio.h @@ -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). */ diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 0369cc7..292eea9 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -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(), diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index 45bb87d..7720ef5 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -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); diff --git a/zephcore/src/main_room_server.cpp b/zephcore/src/main_room_server.cpp index 40307e3..914d344 100644 --- a/zephcore/src/main_room_server.cpp +++ b/zephcore/src/main_room_server.cpp @@ -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);