mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 18:08:47 +00:00
SmartCAD p1
This commit is contained in:
@@ -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
|
||||
100–200 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 56–68 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 |
|
||||
|---|---|
|
||||
| 12–24 h | Knee location, coarsely — enough for a manual offset |
|
||||
| 2–3 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), 10–255 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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: 56–68 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 10–255, default **60** | Seconds between calibration CAD probes. Lower (15–20 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'",
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user