SmartCAD p1

This commit is contained in:
liquidraver
2026-07-12 10:48:37 +02:00
parent 0d70256d2e
commit dc1239537e
36 changed files with 1154 additions and 24 deletions
+179
View File
@@ -0,0 +1,179 @@
# Adaptive CAD — Site-Calibrated Listen-Before-Talk
ZephCore's listen-before-talk (LBT) runs a hardware Channel Activity
Detection (CAD) before every transmission: the radio's LoRa correlator
searches the channel for chirps, and TX is deferred while activity is
detected. How sensitive that search is comes down to one register value,
`cadDetPeak` — and the right value depends on where the node lives.
- **Too sensitive** (detPeak too low): chirp-like interference — other
LoRa networks, other spreading factors on the same frequency — triggers
*false positives*. The node keeps deferring TX for nothing, wasting
100200 ms retry cycles and adding latency; in bad cases it hits the
4-second CAD watchdog.
- **Not sensitive enough** (detPeak too high): real transmissions are
*missed* and the node transmits over ongoing packets, causing on-air
collisions.
A quiet valley node and a 50-network hilltop node need different values.
Adaptive CAD measures the site and finds the value, instead of shipping
one hardcoded number to everyone.
Important physics note: `cadDetPeak` is a correlation peak-to-noise
threshold inside the radio's despreader — **not** a dBm level. The RSSI
noise floor cannot be converted into a detPeak value, which is why this
feature measures CAD behaviour directly instead of deriving it from RSSI.
## How it works
Every `cad.probe.interval` seconds (default 60), 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
is roughly 0.1%. Probes also run in RX duty-cycle (sniff) mode: the
duty cycle is briefly interrupted and re-armed, within the same
preamble-catch budget philosophy the sniff-mode math already accepts.
Each probe tests one **level**: a signed offset from the chip family's
per-SF base detPeak (SX126x: `SF+13`; LR11xx/LR2021: the 5668 table).
Results accumulate per level:
- `probes` — how many CADs ran at this level
- `busy` — raw "activity detected" verdicts
- `fp` — suspected **false positives**: busy verdicts where no packet
materialised right after (see filtering below)
- `tp`**true positives**: busy verdicts confirmed by actual RX
activity immediately after the probe
Filtering, because a "busy" verdict during apparent silence may be a real
below-noise-floor packet (detecting those is CAD's whole purpose):
1. Probes are skipped entirely when the channel is visibly busy (RSSI
more than 7 dB above the learned noise floor) or a packet is being
received — those teach nothing about false positives.
2. After a busy verdict, RX is restarted and the firmware waits ~8 symbol
times: a real transmitter is still on the air and trips the receive
path. If nothing shows up, the verdict counts as a suspected false
positive. Residual contamination by real traffic biases the estimate
*conservative* (higher detPeak), which is the safe direction.
Statistics decay (halve) every 6 hours so the picture stays fresh, and
reset completely whenever the radio parameters change (frequency, SF, BW
— the data is only valid for one configuration).
### Auto mode (staircase)
With `cad.auto on`, a one-sided staircase controller acts on the stats:
- Probes concentrate on the **frontier** — one level more sensitive than
the current operating point (3 of every 4 probes), with the remainder
self-checking the operating level.
- **Step down** (more sensitive) when the frontier has ≥300 samples and
its false-positive rate is ≤1%.
- **Step up** (less sensitive) quickly when the operating level itself
shows a false-positive rate above 2% over ≥50 samples — false positives
at the operating point cost real transmissions.
- The offset is clamped to 4…+4 around the family base and persisted to
flash whenever it steps (steps are hours apart).
Quiet sites converge *below* the standard value — more sensitive LBT than
any fixed-config firmware, meaning fewer TX-over-RX stomps. Noisy sites
ratchet up until false positives vanish.
## Recommended workflow (dry-run first)
Everything ships **observing but not acting**: probes run and counters
accumulate from first boot, but the operating detPeak stays at the
family default until you enable auto mode or set an offset by hand.
1. **Let it observe.** Optionally speed up data collection during the
observation window:
```
set cad.probe.interval 15
```
2. **Read the curve.** After some hours:
```
get cad
```
Example output:
```
> auto:off offset:0 peak:21 (base 21, 4 sym) probe:15s
lvl -3 (peak 18): 240 probes, 31 busy, 28 fp, 3 tp (fp 11.6%)
lvl -2 (peak 19): 241 probes, 9 busy, 7 fp, 2 tp (fp 2.9%)
lvl -1 (peak 20): 240 probes, 3 busy, 1 fp, 2 tp (fp 0.4%)
lvl +0 (peak 21): 241 probes, 2 busy, 0 fp, 2 tp (fp 0.0%)
lvl +1 (peak 22): 240 probes, 2 busy, 0 fp, 2 tp (fp 0.0%)
lvl +2 (peak 23): 240 probes, 1 busy, 0 fp, 1 tp (fp 0.0%)
```
Read it bottom-up: the false-positive rate jumps somewhere (here
between 1 and 2). The lowest clean level (1) is your site's knee.
In dry-run mode the sweep covers levels 3…+2 evenly.
How long to wait (at the default 60 s interval; divide by 4 at 15 s):
| After | You get |
|---|---|
| 1224 h | Knee location, coarsely — enough for a manual offset |
| 23 days | Per-level rates at ~1% resolution, incl. day/night variation |
| 1 week | Solid, weekday/weekend-proof picture |
3. **Act.** Either pin it manually:
```
set cad.offset -1
```
or let the staircase manage it from here on:
```
set cad.auto on
set cad.probe.interval 60
```
With auto on, `get cad` keeps showing the live state; offset changes
appear in the log as `cad: step down/up -> offset N` and are saved to
flash automatically.
## Command reference
| Command | Default | Description |
|---|---|---|
| `get cad` | | Status + per-level statistics (see above). |
| `set cad.auto <on\|off>` | off | Staircase controller acts on the stats. |
| `set cad.offset <n>` | 0 | Operating offset, 4…4. Negative = more sensitive. Applied live. |
| `set cad.probe.interval <sec>` | 60 | Probe cadence; 0 disables probing (and freezes auto), 10255 otherwise. |
| `set cad.reset` | | Clear accumulated statistics (RAM only). |
All settings persist in prefs and apply to every role — repeater, room
server, and companion (companions reach the CLI via the v-contact admin
chat or USB serial).
## Notes and limits
- **SX127x boards** (TTGO LoRa32, T-Beam classic) have no hardware CAD;
`get cad` reports `not available` and the settings are inert. Their LBT
remains the RSSI-based `int.thresh` gate.
- **The false-positive side is measured; the miss side mostly is not.**
A too-high detPeak shows up as on-air collisions, which a single node
cannot observe. That is why the workflow is "find the lowest clean
level and sit at it", never "raise it until problems stop being
visible". The `tp` column is the one local miss-side signal: probes
that detected real below-noise-floor traffic.
- **detPeak is the only adaptive knob.** `cadDetMin` stays at Semtech's
universal 10; the symbol count is fixed at 4 (better mid-payload
detection than 2 — most of a mesh packet's airtime is payload, and
that is where a pre-TX CAD usually lands). The drivers scale their
CAD timeout with symbol count and preset automatically.
- Statistics are RAM-only and restart after a reboot; the learned
*offset* is persisted. Mixed fleets are fine — this feature changes
only when *this* node decides the channel is busy, not anything on
the air.
- Related but separate: `int.thresh` (RSSI-above-floor software gate)
and `dc.restarts` (sniff-mode false-preamble counter) still work as
before; adaptive CAD complements them.
+20
View File
@@ -425,6 +425,26 @@ Algorithm in `triggerNoiseFloorCalibrate()`:
- Periodic bypass: every 16th tick accepts unconditionally
- EMA: `floor += round_nearest((sample - floor) / 8)`, clamped to [-120, -50] dBm
### 5.3.1 Adaptive CAD (LBT detPeak calibration)
`cadDetPeak` is a correlation peak-to-noise threshold in the despreader (not
dBm), so the right LBT sensitivity is site-dependent (chirp-like interference
varies) and cannot be derived from the RSSI floor. `LoRaRadioBase::cadMaintenance()`
(housekeeping tick) runs one calibration CAD probe per `cad.probe.interval`
(default 60 s) at a signed **level** relative to the family's per-SF base
detPeak, restarts RX, and classifies busy verdicts via a ground-truth filter
(pre-probe RSSI near floor; post-busy ~8-symbol wait for real RX activity →
`tp`, else suspected `fp`). Per-level counters decay 6-hourly and reset on any
RF param change. With `cad.auto on`, a one-sided staircase steps the operating
offset down when the frontier level shows FP ≤ 1% over ≥300 probes, up quickly
when the operating level exceeds 2× target over ≥50 probes; offset clamped
4…+4, persisted via `Dispatcher::onCadOffsetChanged()`. Probe + offset plumbing
is per-driver extension API (`*_cad_probe`, `*_cad_set_peak_offset`,
`*_cad_base_peak`); LBT CAD runs 4 symbols (set in `buildModemConfig`), and the
drivers scale their blocking-CAD timeout to `nSym·Tsym + margin`. CLI: `get cad`,
`set cad.auto/offset/probe.interval/reset`. SX127x: unsupported (no HW CAD).
User doc: `ADAPTIVE_CAD.md`.
### 5.4 LR1110 Driver Errata Workarounds
The custom `lr11xx_lora.c` driver handles several LR1110 firmware bugs:
+5 -1
View File
@@ -687,6 +687,11 @@ else()
helpers/TransportKeyStore.cpp
helpers/ui/ui_mesh_actions.cpp
app/CompanionMesh.cpp
# CommonCLI backs the v-contact loopback admin chat (companion_cli_exec in
# main_companion.cpp), which is NOT gated on the wired-USB stack — so it
# must be compiled unconditionally. Builds without CONFIG_LOG/COMPANION_USB/
# COMPANION_SERIAL (e.g. plain ESP32/S3 companions) failed to link otherwise.
helpers/CommonCLI.cpp
)
# Companion transport: TCP socket on native Linux, serial (UART) on boards
# with no Bluetooth controller (e.g. STM32WL), BLE NUS everywhere else.
@@ -724,7 +729,6 @@ else()
if(CONFIG_LOG OR CONFIG_ZEPHCORE_COMPANION_USB OR CONFIG_ZEPHCORE_COMPANION_SERIAL)
target_sources(app PRIVATE
adapters/usb/ZephyrCompanionUSB.cpp
helpers/CommonCLI.cpp # backs the wired text CLI dispatch
)
if(NOT CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT AND (CONFIG_USB_CDC_ACM OR CONFIG_USBD_CDC_ACM_CLASS))
target_sources(app PRIVATE
+5
View File
@@ -208,6 +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 adc.multiplier` | Battery voltage ADC calibration multiplier |
| `get bootloader.ver` | Bootloader version string |
| `get public.key` | *(USB only)* Node's public key as hex |
@@ -252,6 +253,10 @@ Changes are persisted immediately unless noted. Some require a reboot.
| `set rxduty <0\|1\|on\|off>` | | RX duty cycle mode *(reboot required)*. Window timing auto-sized per SF/BW/preamble from the SX126x datasheet constraints (boot log line `rxduty:` shows the result). Zero-loss guarantee assumes senders on preamble-32 firmware (current MeshCore at SF≤8); legacy preamble-16 senders are only caught ~50% worst-phase — keep off until the local mesh has converted. Presets with 16-symbol preambles (SF≥9) fall back to continuous RX automatically. |
| `set adc.multiplier <mult>` | (0 = use board default) | Battery voltage ADC calibration multiplier |
| `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 **off** | Adaptive CAD: let the staircase controller move the operating detPeak offset based on probe statistics. Leave off (dry-run) until `get cad` has accumulated a few days of per-level data. See `ADAPTIVE_CAD.md`. |
| `set cad.offset <n>` | 4 to 4, default 0 | Operating detPeak offset from the chip family's per-SF base (SX126x: SF+13; LR11xx/LR20xx: 5668 table). Negative = more sensitive LBT (catches weaker signals, risks false busy), positive = less sensitive. Applied live; the auto staircase may move it later if `cad.auto` is on. |
| `set cad.probe.interval <sec>` | 0 (off) or 10255, default **60** | Seconds between calibration CAD probes. Lower (1520 s) during a dry-run observation window to accumulate statistics faster; 0 disables probing entirely (also freezes auto adaptation). |
| `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)* |
---
@@ -693,6 +693,31 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
} else {
prefs.v_battery_alert_mv = 0xFFFF;
}
/* Offset 155: cad_auto (ZephCore extension, default 0 = dry-run) */
if (off < len) {
prefs.cad_auto = buf[off++];
if (prefs.cad_auto > 1) {
prefs.cad_auto = 0;
}
}
/* Offset 156: cad_offset (ZephCore extension, signed -4..4, default 0) */
if (off < len) {
prefs.cad_offset = (int8_t)buf[off++];
if (prefs.cad_offset < -4 || prefs.cad_offset > 4) {
prefs.cad_offset = 0;
}
}
/* Offset 157: cad_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;
}
}
}
void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
@@ -774,7 +799,13 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
/* Offset 153: v_battery_alert_mv (ZephCore extension, 2 bytes LE) */
buf[off++] = prefs.v_battery_alert_mv & 0xFF;
buf[off++] = (prefs.v_battery_alert_mv >> 8) & 0xFF;
/* Total: 155 bytes */
/* Offset 155: cad_auto (ZephCore extension) */
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;
/* Total: 158 bytes */
bool ok = atomicReplaceFile(PREFS_FILE, buf, off);
LOG_DBG("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'",
+15
View File
@@ -84,4 +84,19 @@ uint32_t LR1110Radio::hwWakeupTimeUs()
return lr11xx_get_wakeup_time_us(_dev);
}
int LR1110Radio::hwCadProbe(int8_t level)
{
return lr11xx_cad_probe(_dev, level);
}
void LR1110Radio::hwCadSetPeakOffset(int8_t offset)
{
lr11xx_cad_set_peak_offset(_dev, offset);
}
uint8_t LR1110Radio::hwCadBasePeak()
{
return lr11xx_cad_base_peak(_dev);
}
} /* namespace mesh */
+3
View File
@@ -29,6 +29,9 @@ protected:
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
uint32_t hwWakeupTimeUs() override;
int hwCadProbe(int8_t level) override;
void hwCadSetPeakOffset(int8_t offset) override;
uint8_t hwCadBasePeak() override;
};
} /* namespace mesh */
+15
View File
@@ -78,4 +78,19 @@ void LR2021Radio::hwResetAGC()
lr20xx_reset_agc(_dev);
}
int LR2021Radio::hwCadProbe(int8_t level)
{
return lr20xx_cad_probe(_dev, level);
}
void LR2021Radio::hwCadSetPeakOffset(int8_t offset)
{
lr20xx_cad_set_peak_offset(_dev, offset);
}
uint8_t LR2021Radio::hwCadBasePeak()
{
return lr20xx_cad_base_peak(_dev);
}
} /* namespace mesh */
+3
View File
@@ -28,6 +28,9 @@ protected:
bool hwIsReceiving() override;
void hwSetRxBoost(bool enable) override;
void hwResetAGC() override;
int hwCadProbe(int8_t level) override;
void hwCadSetPeakOffset(int8_t offset) override;
uint8_t hwCadBasePeak() override;
};
} /* namespace mesh */
+238
View File
@@ -9,6 +9,7 @@
#include <zephyr/kernel.h>
#include <zephyr/random/random.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
@@ -46,6 +47,8 @@ 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),
_cad_auto(false), _cad_offset(0), _cad_probe_interval_s(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),
_tx_power_reduction_db(0),
@@ -62,6 +65,7 @@ LoRaRadioBase::LoRaRadioBase(const struct device *lora_dev, MainBoard &board,
k_poll_signal_init(&_tx_signal);
k_sem_init(&_tx_start_sem, 0, 1);
memset(_rx_ring, 0, sizeof(_rx_ring));
memset(_cad_stats, 0, sizeof(_cad_stats));
}
/* ── TX wait thread ──────────────────────────────────────────── */
@@ -246,6 +250,14 @@ void LoRaRadioBase::buildModemConfig(struct lora_modem_config &cfg, bool tx)
* direction-only fast path (which skips hwConfigure). RX paths
* never read cad.mode, so this is harmless during receive. */
cfg.cad.mode = LORA_CAD_MODE_LBT;
/* 4-symbol CAD at every SF (drivers default to 2 when this is 0).
* Our LBT runs against mesh packets that are mostly payload airtime;
* payload chirps correlate less reliably per symbol than preamble
* upchirps, so the extra looks matter AN1200.48 itself recommends
* 4 symbols at SF9+. The drivers scale their blocking-CAD timeout
* from this value, so slow presets stay covered. */
cfg.cad.symbol_num = LORA_CAD_SYMB_4;
}
uint32_t LoRaRadioBase::getActiveFrequencyHz() const
@@ -446,6 +458,8 @@ void LoRaRadioBase::reconfigure()
hwCancelReceive();
atomic_set(&_in_recv_mode, 0);
_config_cached = false; /* Force full reconfigure */
/* CAD probe statistics are only valid for one freq/SF/BW config. */
resetCadStats();
startReceive();
uint32_t freq = _prefs ? (uint32_t)(_prefs->freq * 1000000.0f)
@@ -931,6 +945,230 @@ bool LoRaRadioBase::isChannelActive(int threshold)
return rssi > (_noise_floor + threshold);
}
/* ── Adaptive CAD (LBT detPeak calibration) ───────────────────────────── */
void LoRaRadioBase::setCadParams(bool auto_enabled, int8_t offset,
uint16_t probe_interval_s)
{
if (offset < CAD_LEVEL_MIN) offset = CAD_LEVEL_MIN;
if (offset > CAD_LEVEL_MAX) offset = CAD_LEVEL_MAX;
_cad_auto = auto_enabled;
_cad_offset = offset;
_cad_probe_interval_s = probe_interval_s;
hwCadSetPeakOffset(_cad_offset);
LOG_INF("cad: auto=%d offset=%d probe_interval=%us",
(int)auto_enabled, (int)offset, (unsigned)probe_interval_s);
}
void LoRaRadioBase::resetCadStats()
{
memset(_cad_stats, 0, sizeof(_cad_stats));
_cad_probe_rr = 0;
}
void LoRaRadioBase::decayCadStats()
{
for (int i = 0; i < CAD_NUM_LEVELS; i++) {
_cad_stats[i].probes >>= 1;
_cad_stats[i].busy >>= 1;
_cad_stats[i].fp >>= 1;
_cad_stats[i].tp >>= 1;
}
}
int8_t LoRaRadioBase::pickCadProbeLevel()
{
_cad_probe_rr++;
if (!_cad_auto) {
/* Dry-run: even sweep across the observation window so the
* user sees the whole FP-vs-detPeak curve in `get cad`. */
int span = CAD_SWEEP_MAX - CAD_SWEEP_MIN + 1;
return (int8_t)(CAD_SWEEP_MIN + (_cad_probe_rr % span));
}
/* Auto: concentrate samples on the frontier (one step more sensitive
* than the operating point); every 4th probe self-checks the
* operating level. */
int8_t frontier = _cad_offset > CAD_LEVEL_MIN ? (int8_t)(_cad_offset - 1)
: _cad_offset;
return ((_cad_probe_rr & 3) == 0) ? _cad_offset : frontier;
}
void LoRaRadioBase::cadStaircaseStep()
{
/* Step down (more sensitive) when the frontier level has enough
* samples and its suspected-FP rate is at or under target. */
if (_cad_offset > CAD_LEVEL_MIN) {
CadLevelStats &f = _cad_stats[(_cad_offset - 1) - CAD_LEVEL_MIN];
if (f.probes >= CAD_STEP_DOWN_MIN_PROBES &&
(uint32_t)f.fp * 1000U <=
(uint32_t)f.probes * CAD_FP_TARGET_PERMILLE) {
_cad_offset--;
hwCadSetPeakOffset(_cad_offset);
LOG_INF("cad: step down -> offset %d (frontier %up/%ufp)",
(int)_cad_offset, f.probes, f.fp);
return;
}
}
/* Step up (less sensitive) when the operating level itself shows
* FPs well above target. Lower sample bar: FPs at the operating
* point cost real TX opportunities, react quickly. */
if (_cad_offset < CAD_LEVEL_MAX) {
CadLevelStats &o = _cad_stats[_cad_offset - CAD_LEVEL_MIN];
if (o.probes >= CAD_STEP_UP_MIN_PROBES &&
(uint32_t)o.fp * 1000U >
(uint32_t)o.probes * 2U * CAD_FP_TARGET_PERMILLE) {
_cad_offset++;
hwCadSetPeakOffset(_cad_offset);
LOG_INF("cad: step up -> offset %d (operating %up/%ufp)",
(int)_cad_offset, o.probes, o.fp);
}
}
}
void LoRaRadioBase::cadMaintenance()
{
if (_cad_probe_interval_s == 0) {
return;
}
int64_t now = k_uptime_get();
/* Periodic decay keeps the stats fresh (and counters bounded). */
if (_cad_last_decay_ms == 0) {
_cad_last_decay_ms = now;
} else if (now - _cad_last_decay_ms > (int64_t)CAD_STATS_DECAY_MS) {
decayCadStats();
_cad_last_decay_ms = now;
}
if (now - _cad_last_probe_ms < (int64_t)_cad_probe_interval_s * 1000) {
return;
}
/* 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()) {
return;
}
/* 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) {
return;
}
_cad_last_probe_ms = now;
int8_t level = pickCadProbeLevel();
int ret = hwCadProbe(level);
/* The probe leaves the chip in STANDBY (driver state REST) — re-arm
* RX immediately so an incoming packet isn't lost while we classify.
* In duty-cycle mode this re-enters the DC cycle (same path as the
* parked-RX watchdog re-arm). */
atomic_set(&_in_recv_mode, 0);
startReceive();
if (ret < 0) {
if (ret != -ENOSYS) {
LOG_WRN("cad: probe failed (%d)", ret);
}
return;
}
CadLevelStats &s = _cad_stats[level - CAD_LEVEL_MIN];
if (s.probes >= 0xFFF0) {
decayCadStats();
}
s.probes++;
if (ret > 0) {
s.busy++;
/* Ground-truth post-check: a real LoRa signal that tripped
* CAD keeps transmitting after RX restart its preamble or
* header trips the receive path within a few symbols. Wait
* ~8 symbols, then classify. RX is already armed, so the
* packet itself is not at risk during this sleep. */
uint8_t sf = getActiveSpreadingFactor();
uint16_t bw_x10 = getActiveBandwidthKHzX10();
uint32_t tsym_us = bw_x10 ? (uint32_t)(((1UL << sf) * 10000UL)
/ bw_x10) : 1024;
uint32_t wait_ms = (8U * tsym_us) / 1000U;
if (wait_ms < 20) wait_ms = 20;
if (wait_ms > 400) wait_ms = 400;
k_sleep(K_MSEC(wait_ms));
if (isReceiving()) {
s.tp++;
} else {
s.fp++;
}
}
if (_cad_auto) {
cadStaircaseStep();
}
}
int LoRaRadioBase::formatCadStatus(char *buf, int cap)
{
uint8_t base = hwCadBasePeak();
int n = 0;
if (base == 0) {
return snprintf(buf, cap, "CAD: not supported by this radio");
}
int peak = (int)base + _cad_offset;
n += snprintf(buf + n, cap > n ? cap - n : 0,
"auto:%s offset:%d peak:%d (base %u, 4 sym) probe:%us",
_cad_auto ? "on" : "off", (int)_cad_offset, peak,
base, (unsigned)_cad_probe_interval_s);
for (int i = 0; i < CAD_NUM_LEVELS; i++) {
CadLevelStats &s = _cad_stats[i];
if (s.probes == 0) {
continue;
}
/* FP rate in tenths of a percent */
uint32_t fp_pm = ((uint32_t)s.fp * 1000U) / s.probes;
n += snprintf(buf + n, cap > n ? cap - n : 0,
"\nlvl %+d (peak %d): %u probes, %u busy, %u fp, %u tp (fp %u.%u%%)",
i + CAD_LEVEL_MIN, (int)base + i + CAD_LEVEL_MIN,
s.probes, s.busy, s.fp, s.tp,
(unsigned)(fp_pm / 10), (unsigned)(fp_pm % 10));
}
return n;
}
/* ── Power saving ─────────────────────────────────────────────────────── */
void LoRaRadioBase::enableRxDutyCycle(bool enable)
+39
View File
@@ -109,6 +109,14 @@ public:
void setTxPowerReduction(int8_t reduction_db) override { _tx_power_reduction_db = reduction_db; }
int8_t getTxPowerReduction() const override { return _tx_power_reduction_db; }
/* Adaptive CAD (LBT detPeak calibration) */
void setCadParams(bool auto_enabled, int8_t offset,
uint16_t probe_interval_s) override;
void cadMaintenance() override;
int8_t getCadOffset() const override { return _cad_offset; }
void resetCadStats() override;
int formatCadStatus(char *buf, int cap) override;
protected:
/* ── Hardware primitives — subclass MUST implement ─────────── */
@@ -126,6 +134,18 @@ protected:
/** GPIO-only BUSY check (no SPI). Default false for chips without duty-cycle sleep. */
virtual bool hwIsChipBusy() { return false; }
/* ── Adaptive-CAD primitives — defaults suit chips without hardware
* CAD (SX127x): probing unsupported, offset ignored. */
/** Blocking calibration CAD at (family base detPeak + level).
* Leaves the chip in STANDBY; caller restarts RX.
* Returns 1 = busy, 0 = free, <0 = error / unsupported. */
virtual int hwCadProbe(int8_t level) { (void)level; return -ENOSYS; }
/** Apply the operating detPeak offset for all subsequent LBT CADs. */
virtual void hwCadSetPeakOffset(int8_t offset) { (void)offset; }
/** Per-SF base detPeak for the current config (0 = unsupported). */
virtual uint8_t hwCadBasePeak() { return 0; }
/** Radio deaf time per duty-cycle wake transition (context restore +
* PLL lock + TCXO startup where fitted), in microseconds. Counts
* against the duty-cycle preamble-catch budget: per SX126x DS rev 2.2
@@ -180,6 +200,25 @@ protected:
int _calibration_threshold;
uint8_t _ema_unguarded; /* tick counter for warmup + periodic bypass */
/* Adaptive CAD state */
struct CadLevelStats {
uint16_t probes; /* probes run at this level */
uint16_t busy; /* raw busy verdicts */
uint16_t fp; /* busy that passed the ground-truth filter (suspected false positive) */
uint16_t tp; /* busy confirmed by RX activity right after */
};
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 */
int64_t _cad_last_probe_ms;
int64_t _cad_last_decay_ms;
uint8_t _cad_probe_rr; /* round-robin index (sweep) / frontier mix counter */
int8_t pickCadProbeLevel();
void decayCadStats();
void cadStaircaseStep();
/* Power saving */
bool _rx_duty_cycle_enabled;
bool _rx_boost_enabled;
+15
View File
@@ -96,6 +96,21 @@ uint32_t SX126xRadio::hwWakeupTimeUs()
return sx126x_get_wakeup_time_us(_dev);
}
int SX126xRadio::hwCadProbe(int8_t level)
{
return sx126x_cad_probe(_dev, level);
}
void SX126xRadio::hwCadSetPeakOffset(int8_t offset)
{
sx126x_cad_set_peak_offset(_dev, offset);
}
uint8_t SX126xRadio::hwCadBasePeak()
{
return sx126x_cad_base_peak(_dev);
}
uint32_t SX126xRadio::getDutyCycleTimeoutRestarts() const
{
return sx126x_get_dc_timeout_restarts(_dev);
+3
View File
@@ -32,6 +32,9 @@ protected:
void hwResetAGC() override;
bool hwIsChipBusy() override;
uint32_t hwWakeupTimeUs() override;
int hwCadProbe(int8_t level) override;
void hwCadSetPeakOffset(int8_t offset) override;
uint8_t hwCadBasePeak() override;
};
} /* namespace mesh */
+17
View File
@@ -20,6 +20,23 @@
#define NOISE_FLOOR_SAMPLING_THRESHOLD 14 /* dB above floor to reject as interference */
#define DEFAULT_NOISE_FLOOR 0 /* sentinel: seed from first sample */
/* --- Adaptive CAD (LBT detPeak calibration) ---
* Housekeeping-tick CAD probes accumulate per-level busy/free statistics;
* a one-sided staircase converges on the lowest detPeak offset whose
* false-positive rate stays under target. Levels are signed offsets from
* the chip family's per-SF base detPeak (SX126x: SF+13; LR11xx/LR20xx:
* 56-68 table) so the C++ layer stays scale-independent. */
#define CAD_LEVEL_MIN (-4) /* most sensitive probe level */
#define CAD_LEVEL_MAX 4 /* least sensitive probe level */
#define CAD_NUM_LEVELS (CAD_LEVEL_MAX - CAD_LEVEL_MIN + 1)
#define CAD_SWEEP_MIN (-3) /* dry-run sweep window */
#define CAD_SWEEP_MAX 2
#define CAD_FP_TARGET_PERMILLE 10 /* step-down needs FP rate <= 1% */
#define CAD_STEP_DOWN_MIN_PROBES 300 /* samples before a down-step call */
#define CAD_STEP_UP_MIN_PROBES 50 /* samples before an up-step call */
#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 */
/* --- RX ring buffer --- */
#define RX_RING_SIZE 8 /* ~2 KB; buffers burst arrivals at SF7/BW500 */
+8
View File
@@ -353,6 +353,14 @@ protected:
uint8_t getDutyCyclePercent() const override;
uint8_t getExtraAckTransmitCount() const override;
/* Adaptive CAD: persist the staircase's learned offset */
void onCadOffsetChanged(int8_t offset) override {
prefs.cad_offset = offset;
if (_store) {
_store->savePrefs(prefs);
}
}
/* Auto-add filtering overrides */
bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override;
+12
View File
@@ -203,6 +203,11 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
fs_read(&file, &prefs.flood_max_advert, sizeof(prefs.flood_max_advert));
/* Mesh time sync (absent in <297-byte files; no-op EOF read keeps default 0 = off) */
fs_read(&file, &prefs.meshtimesync, sizeof(prefs.meshtimesync));
/* Adaptive CAD (absent in <300-byte files; no-op EOF reads keep defaults
* 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_close(&file);
@@ -234,6 +239,9 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
if (prefs.apc_enabled > 1) prefs.apc_enabled = 0;
if (prefs.apc_margin < 6 || prefs.apc_margin > 30) prefs.apc_margin = 16;
if (prefs.meshtimesync > 1) prefs.meshtimesync = 0;
if (prefs.cad_auto > 1) prefs.cad_auto = 0;
if (prefs.cad_offset < -4 || prefs.cad_offset > 4) prefs.cad_offset = 0;
if (prefs.cad_probe_interval != 0 && prefs.cad_probe_interval < 10) prefs.cad_probe_interval = 10;
/* One-time format upgrade: old files (< 294 bytes) never saved the ZephCore
* extension fields, and stored path_hash_mode/loop_detect as zero padding.
@@ -333,6 +341,10 @@ 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) */
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));
ret = fs_sync(&file);
fs_close(&file);
+18
View File
@@ -179,6 +179,24 @@ protected:
return _prefs.multi_acks;
}
/* Adaptive CAD */
int formatCadStatus(char* buf, int cap) override {
return _radio->formatCadStatus(buf, cap);
}
void applyCadPrefs() override {
_radio->setCadParams(_prefs.cad_auto != 0, _prefs.cad_offset,
_prefs.cad_probe_interval);
}
void resetCadStats() override {
_radio->resetCadStats();
}
void onCadOffsetChanged(int8_t offset) override {
/* Staircase steps are hours apart — persisting immediately is
* fine for flash wear and survives unexpected reboots. */
_prefs.cad_offset = offset;
savePrefs();
}
mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
+16
View File
@@ -148,6 +148,22 @@ protected:
return _prefs.multi_acks;
}
/* Adaptive CAD */
int formatCadStatus(char* buf, int cap) override {
return _radio->formatCadStatus(buf, cap);
}
void applyCadPrefs() override {
_radio->setCadParams(_prefs.cad_auto != 0, _prefs.cad_offset,
_prefs.cad_probe_interval);
}
void resetCadStats() override {
_radio->resetCadStats();
}
void onCadOffsetChanged(int8_t offset) override {
_prefs.cad_offset = offset;
savePrefs();
}
mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;
@@ -26,6 +26,15 @@ CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM=17
# lora_recv_async, but disable it to avoid the spurious attempt)
CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE=n
# Companion RAM budget — same constraint as the T-Beam: the classic ESP32's
# contiguous DRAM segment can't fit the default contact/queue arrays (350/256)
# alongside the BT controller heap. The T-Beam sizes (160/8/128) still
# overflow here by ~1.2 KB — the SX1276 loramac-node backend carries more
# static DRAM than the native SX126x driver — so contacts go to 150.
CONFIG_ZEPHCORE_MAX_CONTACTS=150
CONFIG_ZEPHCORE_MAX_CHANNELS=8
CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE=128
# Flash mode: ESP32-PICO-D4 (rev 1.0) — use DIO, not QIO.
# esp32_common.conf enables QIO for the ESP32-S3/C3/C6 boards in this repo.
# Classic ESP32 PICO-D4 rev 1.0 has an issue where bootloader_enable_qio_mode()
@@ -62,6 +62,18 @@
status = "okay";
};
/*
* BT HCI controller — the upstream ttgo_lora32 DTS leaves the node disabled
* (esp32_common.dtsi already sets chosen zephyr,bt-hci to it). Companion
* builds set CONFIG_BT=y, and the hal_espressif CMake compiles the ESP32 BT
* controller under CONFIG_BT alone; without this node BT_ESP32 stays off and
* its Kconfig symbols (CONFIG_ESP32_BT_CONTROLLER_TASK_PRIO etc.) are
* undefined, breaking the bt.c compile. Same fix as ttgo_tbeam.
*/
&esp32_bt_hci {
status = "okay";
};
/* I2C sensors — auto-detected at runtime */
&i2c0 {
#include "../../common/sensors-i2c.dtsi"
+5 -2
View File
@@ -34,7 +34,10 @@ CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
# Companion RAM budget — the classic ESP32 has a much smaller contiguous DRAM
# segment than the ESP32-S3 boards, and the default contact/queue arrays
# (350/256) overflow it. Use the Arduino MeshCore T-Beam companion sizes.
CONFIG_ZEPHCORE_MAX_CONTACTS=160
# (350/256) overflow it. Started from the Arduino MeshCore T-Beam companion
# sizes (160 contacts); trimmed to 140 when CommonCLI (v-contact admin chat)
# became part of every companion build and its ~3 KB of static state pushed
# DRAM back over the edge.
CONFIG_ZEPHCORE_MAX_CONTACTS=140
CONFIG_ZEPHCORE_MAX_CHANNELS=8
CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE=128
+52
View File
@@ -121,6 +121,9 @@ void CommonCLI::loadPrefs(const char* path) {
ok = ok && prefs_read(&file, &_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 294
ok = ok && prefs_read(&file, &_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 295
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
if (!ok) {
LOG_WRN("Prefs file %s truncated, some fields use defaults", path);
@@ -167,6 +170,11 @@ void CommonCLI::loadPrefs(const char* path) {
_prefs->flood_max_unscoped = constrain(_prefs->flood_max_unscoped, (uint8_t)0, (uint8_t)64);
_prefs->flood_max_advert = constrain(_prefs->flood_max_advert, (uint8_t)0, (uint8_t)64);
_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)-4, (int8_t)4);
if (_prefs->cad_probe_interval != 0 && _prefs->cad_probe_interval < 10) {
_prefs->cad_probe_interval = 10;
}
LOG_INF("Loaded prefs from %s", path);
}
@@ -235,6 +243,9 @@ void CommonCLI::savePrefs(const char* path) {
fs_write(&file, &_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped));
fs_write(&file, &_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert));
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_close(&file);
LOG_INF("Saved prefs to %s", path);
@@ -560,6 +571,15 @@ 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, "cad", 3) == 0) {
/* Runtime state + per-level probe stats live in the radio.
* Remote replies get the truncated buffer like meshtimesync. */
size_t cap = (sender_timestamp == 0) ? CLI_REPLY_SIZE
: CLI_REMOTE_REPLY_SIZE;
int n = snprintf(reply, cap, "> ");
if (_callbacks->formatCadStatus(reply + n, (int)cap - n) == 0) {
strcpy(reply, "not available");
}
} else if (memcmp(config, "meshtimesync", 12) == 0) {
MeshTimeSync* ts = _callbacks->getMeshTimeSync();
if (ts == nullptr) {
@@ -606,6 +626,38 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->agc_reset_interval = atoi(&config[19]) / 4;
savePrefs();
snprintf(reply, CLI_REPLY_SIZE, "OK - interval rounded to %u", ((uint32_t)_prefs->agc_reset_interval) * 4);
} else if (memcmp(config, "cad.auto ", 9) == 0) {
if (memcmp(&config[9], "on", 2) == 0 || memcmp(&config[9], "off", 3) == 0) {
_prefs->cad_auto = (config[9] == 'o' && config[10] == 'n') ? 1 : 0;
_callbacks->applyCadPrefs();
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: must be on or off");
}
} else if (memcmp(config, "cad.offset ", 11) == 0) {
int val = atoi(&config[11]);
if (val < -4 || val > 4) {
strcpy(reply, "Error: offset range is -4..4");
} else {
_prefs->cad_offset = (int8_t)val;
_callbacks->applyCadPrefs();
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "cad.probe.interval ", 19) == 0) {
int val = atoi(&config[19]);
if (val != 0 && (val < 10 || val > 255)) {
strcpy(reply, "Error: interval is 0 (off) or 10-255 seconds");
} else {
_prefs->cad_probe_interval = (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");
} else if (memcmp(config, "multi.acks ", 11) == 0) {
int val = atoi(&config[11]);
if (val == 0 || val == 1) {
+5
View File
@@ -82,6 +82,11 @@ public:
virtual uint8_t getAPCTargetMargin() const { return 16; }
virtual void setAPCTargetMargin(uint8_t margin_db) { (void)margin_db; }
// Adaptive CAD (LBT detPeak calibration)
virtual int formatCadStatus(char* buf, int cap) { (void)buf; (void)cap; return 0; }
virtual void applyCadPrefs() {}
virtual void resetCadStats() {}
// Mesh time sync (all roles wire one; nullptr = not compiled/available)
virtual MeshTimeSync* getMeshTimeSync() { return nullptr; }
+6
View File
@@ -65,6 +65,9 @@ struct NodePrefs {
uint8_t apc_enabled; // 1 = APC on, 0 = fixed TX power
uint8_t apc_margin; // APC target link margin dB (6-30)
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)
/* ---- Companion-only fields ---- */
uint8_t manual_add_contacts;
@@ -136,6 +139,9 @@ static inline void initNodePrefs(NodePrefs* prefs) {
prefs->rx_duty_cycle = 0; // Default OFF — continuous RX for best reliability
prefs->apc_enabled = 0; // Default OFF — fixed TX power
prefs->apc_margin = 16; // Default 16 dB target link margin
prefs->cad_auto = 0; // Default OFF — dry-run: probes + counters only
prefs->cad_offset = 0; // Family base detPeak (SF+13 on SX126x)
prefs->cad_probe_interval = 60; // One CAD probe per minute
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
+5
View File
@@ -59,6 +59,8 @@ class Dispatcher {
uint32_t radio_nonrx_start;
uint32_t next_agc_reset_time;
bool prev_isrecv_mode;
int8_t cad_offset_shadow;
bool cad_offset_shadow_valid;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
tx_queued_callback_t _tx_queued_cb;
@@ -89,6 +91,9 @@ protected:
virtual int getInterferenceThreshold() const { return 0; }
virtual int getAGCResetInterval() const { return 0; }
virtual uint32_t getDutyCycleWindowMs() const { return 3600000UL; } /* 1h default */
/* Adaptive CAD: called when the auto staircase moved the operating
* detPeak offset subclasses persist it to prefs. */
virtual void onCadOffsetChanged(int8_t offset) { (void)offset; }
public:
void begin();
+17
View File
@@ -41,6 +41,23 @@ public:
virtual void setTxPowerReduction(int8_t reduction_db) { (void)reduction_db; }
virtual int8_t getTxPowerReduction() const { return 0; }
/* 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) {
(void)auto_enabled; (void)offset; (void)probe_interval_s;
}
/* One housekeeping tick of the CAD calibrator: maybe run a probe,
* update stats, maybe step the staircase (auto mode). */
virtual void cadMaintenance() {}
virtual int8_t getCadOffset() const { return 0; }
virtual void resetCadStats() {}
/* Writes a human-readable status block; returns chars written (0 = not
* supported by this radio). */
virtual int formatCadStatus(char *buf, int cap) {
(void)buf; (void)cap; return 0;
}
/* Packet statistics */
virtual uint32_t getPacketsRecv() const { return 0; }
virtual uint32_t getPacketsSent() const { return 0; }
@@ -95,6 +95,10 @@ struct lr11xx_data {
struct k_sem cad_sem;
int cad_result;
bool cad_active;
/* Adaptive-CAD: signed offset applied to the per-SF base detPeak on
* every LBT CAD; cad_probe_peak overrides for one calibration probe. */
int8_t cad_peak_offset;
uint8_t cad_probe_peak;
/* Deferred hardware init — heavy SPI/radio work runs on first config() */
bool hw_initialized;
@@ -710,6 +714,29 @@ static uint32_t lr11xx_lora_airtime(const struct device *dev,
/* Forward declaration — needed by LBT in send_async */
static int lr11xx_lora_cad(const struct device *dev, k_timeout_t timeout);
/* Blocking-CAD wait budget scaled to the actual CAD duration:
* nSym * Tsym + startup radio-side, plus IRQ latency margin. A fixed
* 200 ms fits 2-symbol CAD everywhere but is exceeded by 4-symbol CAD
* on slow presets (SF12 @ 62.5 kHz = ~262 ms). */
static uint32_t lr11xx_cad_timeout_ms(struct lr11xx_data *data)
{
struct lora_modem_config *mc = &data->modem_cfg;
uint8_t sf = (uint8_t)mc->datarate;
uint8_t symb_nb = mc->cad.symbol_num ?
(uint8_t)mc->cad.symbol_num : 2;
uint32_t bw_hz = (uint32_t)(bw_enum_to_khz(mc->bandwidth) * 1000.0f);
if (bw_hz == 0 || sf < 5 || sf > 12) {
return 200;
}
uint32_t tsym_us = ((1UL << sf) * 1000000UL) / bw_hz;
/* +1 symbol covers radio startup + internal processing tail */
uint32_t ms = ((symb_nb + 1U) * tsym_us) / 1000U + 100U;
return MAX(ms, 200U);
}
/* ── Driver API: send_async ─────────────────────────────────────────── */
static int lr11xx_lora_send_async(const struct device *dev,
@@ -732,7 +759,8 @@ static int lr11xx_lora_send_async(const struct device *dev,
* to re-arm. */
if (data->modem_cfg.cad.mode == LORA_CAD_MODE_LBT) {
bool was_in_rx = data->in_rx_mode;
int cad_ret = lr11xx_lora_cad(dev, K_MSEC(200));
int cad_ret = lr11xx_lora_cad(dev,
K_MSEC(lr11xx_cad_timeout_ms(data)));
if (cad_ret > 0) {
if (was_in_rx && data->async_rx_cb != NULL) {
k_mutex_lock(&data->spi_mutex, K_FOREVER);
@@ -1136,6 +1164,21 @@ static int lr11xx_do_cad(struct lr11xx_data *data)
}
if (mc->cad.detection_peak != 0) {
detect_peak = mc->cad.detection_peak;
} else if (data->cad_peak_offset != 0) {
/* Adaptive-CAD operating offset (base +/- learned delta).
* LR11xx detPeak scale is ~48-90 never mix with SX126x. */
int peak = (int)detect_peak + data->cad_peak_offset;
if (peak < 48) {
peak = 48;
} else if (peak > 90) {
peak = 90;
}
detect_peak = (uint8_t)peak;
}
if (data->cad_probe_peak != 0) {
/* One-shot calibration probe: absolute peak wins over all. */
detect_peak = data->cad_probe_peak;
}
lr11xx_radio_cad_params_t cad = {
@@ -1228,6 +1271,44 @@ static int lr11xx_lora_cad_async(const struct device *dev,
return ret;
}
/* ── Extension API: adaptive CAD ────────────────────────────────────── */
void lr11xx_cad_set_peak_offset(const struct device *dev, int8_t offset)
{
struct lr11xx_data *data = dev->data;
data->cad_peak_offset = offset;
}
uint8_t lr11xx_cad_base_peak(const struct device *dev)
{
struct lr11xx_data *data = dev->data;
return lr11xx_cad_detect_peak((uint8_t)data->modem_cfg.datarate);
}
int lr11xx_cad_probe(const struct device *dev, int8_t peak_offset)
{
struct lr11xx_data *data = dev->data;
int base = (int)lr11xx_cad_base_peak(dev);
int peak = base + peak_offset;
int ret;
if (peak < 48) {
peak = 48;
} else if (peak > 90) {
peak = 90;
}
/* One-shot absolute override consumed by lr11xx_do_cad(). Probes and
* LBT both run on the mesh loop thread, so no concurrent CAD exists. */
data->cad_probe_peak = (uint8_t)peak;
ret = lr11xx_lora_cad(dev, K_MSEC(lr11xx_cad_timeout_ms(data)));
data->cad_probe_peak = 0;
return ret;
}
/* ── Deferred hardware init ─────────────────────────────────────────── */
static int lr11xx_hw_init(struct lr11xx_data *data,
@@ -86,6 +86,39 @@ uint32_t lr11xx_get_random(const struct device *dev);
*/
void lr11xx_reset_agc(const struct device *dev);
/**
* @brief Set the adaptive-CAD operating detPeak offset
*
* Signed delta applied to the per-SF base cadDetPeak on every LBT CAD.
* Takes effect on the next CAD no reconfigure needed. Clamped
* in-driver to the LR11xx scale (48-90).
*
* @param dev LoRa device
* @param offset Signed offset from the base table value
*/
void lr11xx_cad_set_peak_offset(const struct device *dev, int8_t offset);
/**
* @brief Per-SF base cadDetPeak for the currently configured SF
*
* @param dev LoRa device
* @return Base detPeak (56-68 on this family)
*/
uint8_t lr11xx_cad_base_peak(const struct device *dev);
/**
* @brief Run one blocking calibration CAD at base detPeak + peak_offset
*
* Uses the operating modem config (SF/BW/symbol count). Leaves the chip
* in STANDBY the caller must restart RX afterwards. Mesh loop thread
* only.
*
* @param dev LoRa device
* @param peak_offset Signed offset from the base table value
* @return 1 = activity detected, 0 = channel free, <0 = error
*/
int lr11xx_cad_probe(const struct device *dev, int8_t peak_offset);
#ifdef __cplusplus
}
#endif
@@ -99,6 +99,10 @@ struct lr20xx_data {
struct k_sem cad_sem;
int cad_result; /* 0=free, 1=busy, <0=error */
bool cad_active;
/* Adaptive-CAD: signed offset applied to the per-SF base detPeak on
* every LBT CAD; cad_probe_peak overrides for one calibration probe. */
int8_t cad_peak_offset;
uint8_t cad_probe_peak;
/* Deferred hardware init */
bool hw_initialized;
@@ -838,6 +842,29 @@ static uint32_t lr20xx_lora_airtime(const struct device *dev,
static int lr20xx_lora_cad(const struct device *dev, k_timeout_t timeout);
/* Blocking-CAD wait budget scaled to the actual CAD duration:
* nSym * Tsym + startup radio-side, plus IRQ latency margin. A fixed
* 200 ms fits 2-symbol CAD everywhere but is exceeded by 4-symbol CAD
* on slow presets (SF12 @ 62.5 kHz = ~262 ms). */
static uint32_t lr20xx_cad_timeout_ms(struct lr20xx_data *data)
{
struct lora_modem_config *mc = &data->modem_cfg;
uint8_t sf = (uint8_t)mc->datarate;
uint8_t symb_nb = mc->cad.symbol_num ?
(uint8_t)mc->cad.symbol_num : 2;
uint32_t bw_hz = (uint32_t)(bw_enum_to_khz(mc->bandwidth) * 1000.0f);
if (bw_hz == 0 || sf < 5 || sf > 12) {
return 200;
}
uint32_t tsym_us = ((1UL << sf) * 1000000UL) / bw_hz;
/* +1 symbol covers radio startup + internal processing tail */
uint32_t ms = ((symb_nb + 1U) * tsym_us) / 1000U + 100U;
return MAX(ms, 200U);
}
static int lr20xx_lora_send_async(const struct device *dev,
uint8_t *buf, uint32_t data_len,
struct k_poll_signal *async)
@@ -858,7 +885,8 @@ static int lr20xx_lora_send_async(const struct device *dev,
* to re-arm. */
if (data->modem_cfg.cad.mode == LORA_CAD_MODE_LBT) {
bool was_in_rx = data->in_rx_mode;
int cad_ret = lr20xx_lora_cad(dev, K_MSEC(200));
int cad_ret = lr20xx_lora_cad(dev,
K_MSEC(lr20xx_cad_timeout_ms(data)));
if (cad_ret > 0) {
LOG_DBG("LBT: channel busy");
if (was_in_rx && data->async_rx_cb != NULL) {
@@ -1175,6 +1203,21 @@ static int lr20xx_do_cad(struct lr20xx_data *data)
}
if (mc->cad.detection_peak != 0) {
cad.cad_detect_peak = mc->cad.detection_peak;
} else if (data->cad_peak_offset != 0) {
/* Adaptive-CAD operating offset (base +/- learned delta).
* LR20xx detPeak scale matches LR11xx (~48-90). */
int peak = (int)cad.cad_detect_peak + data->cad_peak_offset;
if (peak < 48) {
peak = 48;
} else if (peak > 90) {
peak = 90;
}
cad.cad_detect_peak = (uint8_t)peak;
}
if (data->cad_probe_peak != 0) {
/* One-shot calibration probe: absolute peak wins over all. */
cad.cad_detect_peak = data->cad_probe_peak;
}
lr20xx_radio_lora_configure_cad_params(ctx, &cad);
@@ -1264,6 +1307,44 @@ static int lr20xx_lora_cad_async(const struct device *dev,
return ret;
}
/* ── Extension API: adaptive CAD ────────────────────────────────────── */
void lr20xx_cad_set_peak_offset(const struct device *dev, int8_t offset)
{
struct lr20xx_data *data = dev->data;
data->cad_peak_offset = offset;
}
uint8_t lr20xx_cad_base_peak(const struct device *dev)
{
struct lr20xx_data *data = dev->data;
return lr20xx_cad_detect_peak((uint8_t)data->modem_cfg.datarate);
}
int lr20xx_cad_probe(const struct device *dev, int8_t peak_offset)
{
struct lr20xx_data *data = dev->data;
int base = (int)lr20xx_cad_base_peak(dev);
int peak = base + peak_offset;
int ret;
if (peak < 48) {
peak = 48;
} else if (peak > 90) {
peak = 90;
}
/* One-shot absolute override consumed by lr20xx_do_cad(). Probes and
* LBT both run on the mesh loop thread, so no concurrent CAD exists. */
data->cad_probe_peak = (uint8_t)peak;
ret = lr20xx_lora_cad(dev, K_MSEC(lr20xx_cad_timeout_ms(data)));
data->cad_probe_peak = 0;
return ret;
}
/* ── Driver API: recv_duty_cycle ────────────────────────────────────── */
static int lr20xx_lora_recv_duty_cycle(const struct device *dev,
@@ -60,6 +60,39 @@ uint32_t lr20xx_get_random(const struct device *dev);
*/
void lr20xx_reset_agc(const struct device *dev);
/**
* @brief Set the adaptive-CAD operating detPeak offset
*
* Signed delta applied to the per-SF base cadDetPeak on every LBT CAD.
* Takes effect on the next CAD no reconfigure needed. Clamped
* in-driver to the LR20xx scale (48-90).
*
* @param dev LoRa device
* @param offset Signed offset from the base table value
*/
void lr20xx_cad_set_peak_offset(const struct device *dev, int8_t offset);
/**
* @brief Per-SF base cadDetPeak for the currently configured SF
*
* @param dev LoRa device
* @return Base detPeak (56-68 on this family)
*/
uint8_t lr20xx_cad_base_peak(const struct device *dev);
/**
* @brief Run one blocking calibration CAD at base detPeak + peak_offset
*
* Uses the operating modem config (SF/BW/symbol count). Leaves the chip
* in STANDBY the caller must restart RX afterwards. Mesh loop thread
* only.
*
* @param dev LoRa device
* @param peak_offset Signed offset from the base table value
* @return 1 = activity detected, 0 = channel free, <0 = error
*/
int lr20xx_cad_probe(const struct device *dev, int8_t peak_offset);
#ifdef __cplusplus
}
#endif
@@ -123,6 +123,39 @@ uint32_t sx126x_get_dc_timeout_restarts(const struct device *dev);
*/
void sx126x_reset_dc_timeout_restarts(const struct device *dev);
/**
* @brief Set the adaptive-CAD operating detPeak offset
*
* Signed delta applied to the per-SF base cadDetPeak on every LBT CAD.
* Takes effect on the next CAD no reconfigure needed. The absolute
* value is clamped in-driver to a sane window for the chip family.
*
* @param dev LoRa device
* @param offset Signed offset from the base table value
*/
void sx126x_cad_set_peak_offset(const struct device *dev, int8_t offset);
/**
* @brief Per-SF base cadDetPeak for the currently configured SF
*
* @param dev LoRa device
* @return Base detPeak (SF + 13 on this family)
*/
uint8_t sx126x_cad_base_peak(const struct device *dev);
/**
* @brief Run one blocking calibration CAD at base detPeak + peak_offset
*
* Uses the operating modem config (SF/BW/symbol count). Leaves the chip
* in STANDBY the caller must restart RX afterwards. Must be called
* from the mesh loop thread only (same thread as the LBT CAD).
*
* @param dev LoRa device
* @param peak_offset Signed offset from the base table value
* @return 1 = activity detected, 0 = channel free, <0 = error
*/
int sx126x_cad_probe(const struct device *dev, int8_t peak_offset);
#ifdef __cplusplus
}
#endif
@@ -68,7 +68,7 @@ index 17689720dd2..09982dc2e70 100644
return -EINVAL;
}
diff --git a/drivers/lora/native/sx126x/sx126x.c b/drivers/lora/native/sx126x/sx126x.c
index 30243ba5dc7..ef06d9273ae 100644
index 30243ba5dc7..5315e5d80ef 100644
--- a/drivers/lora/native/sx126x/sx126x.c
+++ b/drivers/lora/native/sx126x/sx126x.c
@@ -6,14 +6,21 @@
@@ -748,16 +748,17 @@ index 30243ba5dc7..ef06d9273ae 100644
/* Set sync word */
ret = sx126x_set_sync_word(dev, config->public_network);
if (ret < 0) {
@@ -721,6 +1207,8 @@ out:
@@ -721,6 +1207,9 @@ out:
return ret;
}
+static int sx126x_lora_cad(const struct device *dev, k_timeout_t timeout);
+static uint32_t sx126x_cad_timeout_ms(struct sx126x_data *data);
+
static int sx126x_lora_send_async(const struct device *dev,
uint8_t *data_buf, uint32_t data_len,
struct k_poll_signal *async)
@@ -738,11 +1226,27 @@ static int sx126x_lora_send_async(const struct device *dev,
@@ -738,11 +1227,27 @@ static int sx126x_lora_send_async(const struct device *dev,
return -EINVAL;
}
@@ -787,7 +788,7 @@ index 30243ba5dc7..ef06d9273ae 100644
k_mutex_lock(&data->lock, K_FOREVER);
ret = sx126x_ensure_ready(dev);
@@ -752,6 +1256,58 @@ static int sx126x_lora_send_async(const struct device *dev,
@@ -752,6 +1257,59 @@ static int sx126x_lora_send_async(const struct device *dev,
return ret;
}
@@ -795,7 +796,8 @@ index 30243ba5dc7..ef06d9273ae 100644
+ if (data->config.cad.mode == LORA_CAD_MODE_LBT) {
+ k_mutex_unlock(&data->lock);
+ atomic_set(&data->state, SX126X_REST_STATE);
+ int cad_ret = sx126x_lora_cad(dev, K_MSEC(200));
+ int cad_ret = sx126x_lora_cad(dev,
+ K_MSEC(sx126x_cad_timeout_ms(data)));
+ if (cad_ret > 0) {
+ LOG_DBG("LBT: channel busy");
+ k_mutex_lock(&data->lock, K_FOREVER);
@@ -846,7 +848,7 @@ index 30243ba5dc7..ef06d9273ae 100644
data->tx_async_signal = async;
k_msgq_purge(&data->tx_msgq);
@@ -777,6 +1333,31 @@ static int sx126x_lora_send_async(const struct device *dev,
@@ -777,6 +1335,31 @@ static int sx126x_lora_send_async(const struct device *dev,
/* Enable antenna and set TX path */
sx126x_set_rf_path(dev, true, true);
@@ -878,7 +880,7 @@ index 30243ba5dc7..ef06d9273ae 100644
/* Start transmission with 10 second timeout */
ret = sx126x_set_tx(dev, 10000);
if (ret < 0) {
@@ -847,6 +1428,8 @@ static int sx126x_lora_recv(const struct device *dev, uint8_t *data_buf,
@@ -847,6 +1430,8 @@ static int sx126x_lora_recv(const struct device *dev, uint8_t *data_buf,
}
data->rx_cb = NULL;
@@ -887,7 +889,7 @@ index 30243ba5dc7..ef06d9273ae 100644
k_msgq_purge(&data->rx_msgq);
/* Set packet parameters for variable length reception */
@@ -918,6 +1501,8 @@ static int sx126x_lora_recv_async(const struct device *dev,
@@ -918,6 +1503,8 @@ static int sx126x_lora_recv_async(const struct device *dev,
/* Stop async reception */
data->rx_cb = NULL;
data->rx_cb_user_data = NULL;
@@ -896,7 +898,7 @@ index 30243ba5dc7..ef06d9273ae 100644
if (atomic_cas(&data->state, SX126X_STATE_RX, SX126X_STATE_IDLE)) {
sx126x_set_standby(dev, SX126X_STANDBY_RC);
sx126x_set_sleep(dev);
@@ -932,6 +1517,19 @@ static int sx126x_lora_recv_async(const struct device *dev,
@@ -932,6 +1519,19 @@ static int sx126x_lora_recv_async(const struct device *dev,
return -EINVAL;
}
@@ -916,7 +918,7 @@ index 30243ba5dc7..ef06d9273ae 100644
if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_RX)) {
LOG_ERR("Busy");
k_mutex_unlock(&data->lock);
@@ -945,8 +1543,18 @@ static int sx126x_lora_recv_async(const struct device *dev,
@@ -945,8 +1545,18 @@ static int sx126x_lora_recv_async(const struct device *dev,
return ret;
}
@@ -935,7 +937,7 @@ index 30243ba5dc7..ef06d9273ae 100644
/* Set packet parameters */
ret = sx126x_set_packet_params(dev,
@@ -959,6 +1567,7 @@ static int sx126x_lora_recv_async(const struct device *dev,
@@ -959,6 +1569,7 @@ static int sx126x_lora_recv_async(const struct device *dev,
SX126X_LORA_IQ_INVERTED : SX126X_LORA_IQ_STANDARD);
if (ret < 0) {
data->rx_cb = NULL;
@@ -943,7 +945,7 @@ index 30243ba5dc7..ef06d9273ae 100644
sx126x_set_sleep(dev);
k_mutex_unlock(&data->lock);
return ret;
@@ -971,11 +1580,21 @@ static int sx126x_lora_recv_async(const struct device *dev,
@@ -971,11 +1582,21 @@ static int sx126x_lora_recv_async(const struct device *dev,
ret = sx126x_set_rx(dev, 0);
if (ret < 0) {
data->rx_cb = NULL;
@@ -965,7 +967,7 @@ index 30243ba5dc7..ef06d9273ae 100644
k_mutex_unlock(&data->lock);
return 0;
}
@@ -1051,7 +1670,7 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
@@ -1051,7 +1672,7 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
/* Set PA config and TX power */
ret = sx126x_hal_configure_tx_params(dev, tx_power, frequency,
@@ -974,7 +976,7 @@ index 30243ba5dc7..ef06d9273ae 100644
if (ret < 0) {
sx126x_set_sleep(dev);
k_mutex_unlock(&data->lock);
@@ -1083,14 +1702,595 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
@@ -1083,14 +1704,671 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
return 0;
}
@@ -1224,6 +1226,29 @@ index 30243ba5dc7..ef06d9273ae 100644
+ return sf + 13;
+}
+
+/* Blocking-CAD wait budget scaled to the actual CAD duration:
+ * nSym * Tsym + 32/BW radio-side, plus IRQ latency / work-queue margin.
+ * The old fixed 200 ms fit 2-symbol CAD everywhere but is exceeded by
+ * 4-symbol CAD on slow presets (SF12 @ 62.5 kHz = ~262 ms). */
+static uint32_t sx126x_cad_timeout_ms(struct sx126x_data *data)
+{
+ struct lora_modem_config *mc = &data->config;
+ uint8_t sf = (uint8_t)mc->datarate;
+ uint8_t symb_nb = mc->cad.symbol_num ?
+ (uint8_t)mc->cad.symbol_num : 2;
+ uint32_t bw_hz = bandwidth_to_hz(mc->bandwidth);
+
+ if (bw_hz == 0 || sf < 5 || sf > 12) {
+ return 200;
+ }
+
+ uint32_t tsym_us = ((1UL << sf) * 1000000UL) / bw_hz;
+ /* +1 symbol covers the 32/BW startup + internal processing tail */
+ uint32_t ms = ((symb_nb + 1U) * tsym_us) / 1000U + 100U;
+
+ return MAX(ms, 200U);
+}
+
+static int sx126x_do_cad(const struct device *dev, struct sx126x_data *data)
+{
+ struct lora_modem_config *mc = &data->config;
@@ -1237,6 +1262,21 @@ index 30243ba5dc7..ef06d9273ae 100644
+ }
+ if (mc->cad.detection_peak != 0) {
+ detect_peak = mc->cad.detection_peak;
+ } else if (data->cad_peak_offset != 0) {
+ /* Adaptive-CAD operating offset (base +/- learned delta).
+ * Clamped to a sane absolute window around the family scale. */
+ int peak = (int)detect_peak + data->cad_peak_offset;
+
+ if (peak < 15) {
+ peak = 15;
+ } else if (peak > 40) {
+ peak = 40;
+ }
+ detect_peak = (uint8_t)peak;
+ }
+ if (data->cad_probe_peak != 0) {
+ /* One-shot calibration probe: absolute peak wins over all. */
+ detect_peak = data->cad_probe_peak;
+ }
+
+ /* SET_CAD must run from STANDBY_RC (DS). LBT is entered from continuous RX
@@ -1383,6 +1423,44 @@ index 30243ba5dc7..ef06d9273ae 100644
+ return ret;
+}
+
+/* -- Extension API: adaptive CAD -- */
+
+void sx126x_cad_set_peak_offset(const struct device *dev, int8_t offset)
+{
+ struct sx126x_data *data = dev->data;
+
+ data->cad_peak_offset = offset;
+}
+
+uint8_t sx126x_cad_base_peak(const struct device *dev)
+{
+ struct sx126x_data *data = dev->data;
+
+ return sx126x_cad_detect_peak((uint8_t)data->config.datarate);
+}
+
+int sx126x_cad_probe(const struct device *dev, int8_t peak_offset)
+{
+ struct sx126x_data *data = dev->data;
+ int base = (int)sx126x_cad_base_peak(dev);
+ int peak = base + peak_offset;
+ int ret;
+
+ if (peak < 15) {
+ peak = 15;
+ } else if (peak > 40) {
+ peak = 40;
+ }
+
+ /* One-shot absolute override consumed by sx126x_do_cad(). Probes and
+ * LBT both run on the mesh loop thread, so no concurrent CAD exists. */
+ data->cad_probe_peak = (uint8_t)peak;
+ ret = sx126x_lora_cad(dev, K_MSEC(sx126x_cad_timeout_ms(data)));
+ data->cad_probe_peak = 0;
+
+ return ret;
+}
+
+/* -- Driver API: recv_duty_cycle -- */
+
+static int sx126x_lora_recv_duty_cycle(const struct device *dev,
@@ -1577,7 +1655,7 @@ index 30243ba5dc7..ef06d9273ae 100644
};
#ifdef CONFIG_PM_DEVICE
@@ -1112,6 +2312,7 @@ static int sx126x_pm_action(const struct device *dev,
@@ -1112,6 +2390,7 @@ static int sx126x_pm_action(const struct device *dev,
static int sx126x_init(const struct device *dev)
{
struct sx126x_data *data = dev->data;
@@ -1585,7 +1663,7 @@ index 30243ba5dc7..ef06d9273ae 100644
int ret;
/* Initialize data structures */
@@ -1121,9 +2322,29 @@ static int sx126x_init(const struct device *dev)
@@ -1121,9 +2400,29 @@ static int sx126x_init(const struct device *dev)
k_msgq_init(&data->rx_msgq, (char *)&data->rx_result,
sizeof(struct sx126x_rx_result), 1);
k_work_init(&data->irq_work, sx126x_irq_work_handler);
@@ -1616,10 +1694,10 @@ index 30243ba5dc7..ef06d9273ae 100644
/* Initialize HAL */
ret = sx126x_hal_init(dev);
diff --git a/drivers/lora/native/sx126x/sx126x.h b/drivers/lora/native/sx126x/sx126x.h
index 9dbf3f26586..2598b56d048 100644
index 9dbf3f26586..b18bebceec7 100644
--- a/drivers/lora/native/sx126x/sx126x.h
+++ b/drivers/lora/native/sx126x/sx126x.h
@@ -56,13 +56,69 @@ struct sx126x_data {
@@ -56,13 +56,74 @@ struct sx126x_data {
/* Async RX callback */
lora_recv_cb rx_cb;
void *rx_cb_user_data;
@@ -1686,6 +1764,11 @@ index 9dbf3f26586..2598b56d048 100644
+ struct k_sem cad_sem;
+ int cad_result;
+ bool cad_active;
+ /* Adaptive-CAD: signed offset applied to the per-SF base detPeak on
+ * every LBT CAD (set via sx126x_cad_set_peak_offset). cad_probe_peak,
+ * when non-zero, overrides everything for one calibration probe. */
+ int8_t cad_peak_offset;
+ uint8_t cad_probe_peak;
};
#endif /* ZEPHYR_DRIVERS_LORA_SX126X_SX126X_INTERNAL_H_ */
+16
View File
@@ -38,6 +38,8 @@ Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
_err_flags = 0;
radio_nonrx_start = 0;
prev_isrecv_mode = true;
cad_offset_shadow = 0;
cad_offset_shadow_valid = false;
n_sent_flood = n_sent_direct = 0;
n_recv_flood = n_recv_direct = 0;
_tx_queued_cb = nullptr;
@@ -181,6 +183,20 @@ void Dispatcher::maintenanceLoop()
_radio->resetAGC();
next_agc_reset_time = futureMillis(getAGCResetInterval());
}
/* Adaptive CAD: probe scheduling + staircase live in the radio;
* we only surface offset changes so the app layer can persist them. */
_radio->cadMaintenance();
int8_t cad_off = _radio->getCadOffset();
if (!cad_offset_shadow_valid) {
cad_offset_shadow = cad_off;
cad_offset_shadow_valid = true;
} else if (cad_off != cad_offset_shadow) {
cad_offset_shadow = cad_off;
onCadOffsetChanged(cad_off);
}
}
bool Dispatcher::tryParsePacket(Packet *pkt, const uint8_t *raw, int len)
+16
View File
@@ -708,6 +708,19 @@ public:
return lora_radio.setRxBoost(enable);
}
/* Adaptive CAD */
int formatCadStatus(char* buf, int cap) override {
return lora_radio.formatCadStatus(buf, cap);
}
void applyCadPrefs() override {
lora_radio.setCadParams(companion_mesh.prefs.cad_auto != 0,
companion_mesh.prefs.cad_offset,
companion_mesh.prefs.cad_probe_interval);
}
void resetCadStats() override {
lora_radio.resetCadStats();
}
#ifdef CONFIG_ZEPHCORE_APC
int8_t getAPCReduction() const override {
return companion_mesh.getAPCReduction();
@@ -1507,6 +1520,9 @@ int main(void)
/* Apply RX boost and duty cycle from prefs */
lora_radio.setRxBoost(companion_mesh.prefs.rx_boost != 0);
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);
#ifdef CONFIG_ZEPHCORE_APC
ui_set_radio_runtime(
lora_radio.getEffectiveTxPower(),
+2
View File
@@ -630,6 +630,8 @@ int main(void)
/* Apply RX boost and duty cycle from prefs */
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);
/* Feed initial UI state from loaded prefs */
ui_set_node_name(prefs->node_name);
+2
View File
@@ -609,6 +609,8 @@ int main(void)
/* Apply RX boost and duty cycle from prefs */
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);
/* Feed initial UI state from loaded prefs */
ui_set_node_name(prefs->node_name);