mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-08-29 14:58:16 +00:00
vcontact and cad fixes
This commit is contained in:
+48
-14
@@ -32,7 +32,7 @@ ZephCore is a LoRa mesh networking firmware running on Zephyr RTOS. It supports
|
||||
- **Room Server**: Headless store-and-forward shared message room (BBS). Reuses the repeater's ACL/region/CLI; pushes new posts to logged-in clients (per-client sync cursor + ACK).
|
||||
- **Observer** (ESP32): Listen-only node that publishes received LoRa packets to MQTT over WiFi.
|
||||
|
||||
Supported hardware: nRF52840, nRF54L15, ESP32 (classic PICO-D4 and C3/C6/S3), EFR32MG24, and STM32WL (LoRa-E5). Radios: SX126x family (SX1261/62/68, LLCC68, STM32WL sub-GHz), LR1110, SX127x (SX1272/76/78, loramac-node backend), and LR2021 (experimental bring-up). A native Linux port runs the full stack on SBCs (Femtofox, Raspberry Pi) via Zephyr `native_sim` — see `LINUX_NATIVE.md`.
|
||||
Supported hardware: nRF52840, nRF54L15, ESP32 (classic PICO-D4 and C3/C6/S3), EFR32MG24, and STM32WL (LoRa-E5). Radios: SX126x family (SX1261/62/68, LLCC68, STM32WL sub-GHz), LR1110, SX127x (SX1272/76/78, loramac-node backend), and LR2021 (validated on the MeshTracker X1). A native Linux port runs the full stack on SBCs (Femtofox, Raspberry Pi) via Zephyr `native_sim` — see `LINUX_NATIVE.md`.
|
||||
|
||||
### Upstream Relationship
|
||||
|
||||
@@ -73,7 +73,7 @@ zephcore/
|
||||
│ │ ├── SX126xRadio.cpp/h # SX126x adapter (native Zephyr driver, patched)
|
||||
│ │ ├── SX127xRadio.cpp/h # SX127x adapter (loramac-node backend)
|
||||
│ │ ├── LR1110Radio.cpp/h # LR1110 adapter (custom Zephyr driver)
|
||||
│ │ ├── LR2021Radio.cpp/h # LR2021 adapter (custom driver, experimental)
|
||||
│ │ ├── LR2021Radio.cpp/h # LR2021 adapter (custom driver)
|
||||
│ │ ├── radio_common.h # Shared radio types and constants
|
||||
│ │ ├── lr11xx/ # LR11xx low-level HAL (SPI, GPIO, Semtech SDK)
|
||||
│ │ └── lr20xx/ # LR20xx low-level HAL (Semtech SDK)
|
||||
@@ -363,7 +363,7 @@ mesh::Radio (abstract interface)
|
||||
├── SX126xRadio → Zephyr native SX126x driver + sx126x_ext.h
|
||||
├── SX127xRadio → Zephyr loramac-node backend (SX1272/76/78)
|
||||
├── LR1110Radio → Custom lr11xx_lora.c driver + Semtech HAL
|
||||
└── LR2021Radio → Custom lr20xx_lora.c driver + Semtech HAL (experimental)
|
||||
└── LR2021Radio → Custom lr20xx_lora.c driver + Semtech HAL
|
||||
```
|
||||
|
||||
Compile-time selection via the `CONFIG_ZEPHCORE_RADIO_NATIVE` / `_LR1110` / `_LR2021` / `_SX127X` Kconfig options, resolved in `RadioIncludes.h`. The native SX126x path is the default and covers SX1261/62/68, LLCC68, and the STM32WL integrated sub-GHz radio.
|
||||
@@ -449,10 +449,20 @@ independent of a site's FP floor. Highest-priority override: **airtime / faint
|
||||
cap** — step up when the operating busy rate exceeds `cad_busycap` (percent,
|
||||
`set cad.busycap`, default 25, 0=off); self-targeting since only busy nodes
|
||||
reach it, and effectively a faint-tolerance dial (lower = reject faint harder).
|
||||
Each step needs ≥`CAD_STEP_MIN_PROBES` (120); offset clamped **−8…+12**,
|
||||
persisted via `Dispatcher::onCadOffsetChanged()`. Driver absolute clamp (SX126x
|
||||
15–40, LR 48–90) is a guardrail; AN1200.48 recommends 21–29 for SX126x (base
|
||||
`SF+13`), tuned to catch faint — LBT may deliberately sit above it. Probe +
|
||||
Each step needs ≥`CAD_STEP_MIN_PROBES` (120); offset clamped to **−8…+12**
|
||||
*narrowed by the driver's own detPeak clamp* (SX126x 15–40, LR 48–90), persisted
|
||||
via `Dispatcher::onCadOffsetChanged()`. The narrowing is not cosmetic: where
|
||||
`base + offset` falls outside the hardware clamp, several offsets program the
|
||||
**same** peak, and the staircase then compares rungs that are physically
|
||||
identical and reads sampling noise as curvature. `hwCadPeakMin/Max()` report the
|
||||
driver clamp and `cadLevelMinEff()/MaxEff()` derive the usable window, so every
|
||||
level the controller can reach is a distinct configuration and the `pk` shown by
|
||||
`get cad` is what the chip was actually given. It binds on the LR2021, whose
|
||||
4-symbol base is 51 at SF5–7 (effective −3…+12) and 54 at SF8 (−6…+12); the
|
||||
LR11xx's lowest base of 56 already lands exactly on the 48 floor at −8, so its
|
||||
full window is usable and it keeps the static range. AN1200.48 recommends 21–29
|
||||
for SX126x (base `SF+13`), tuned to catch faint — LBT may deliberately sit above
|
||||
it. 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`), drivers scale their blocking-CAD timeout to
|
||||
@@ -472,7 +482,12 @@ The custom `lr11xx_lora.c` driver handles several LR1110 firmware bugs:
|
||||
### 5.5 SX127x and LR2021 Paths
|
||||
|
||||
- **SX127x** (`CONFIG_ZEPHCORE_RADIO_SX127X`): uses Zephyr's loramac-node LoRa backend instead of the native driver (`CONFIG_LORA_MODULE_BACKEND_LORAMAC_NODE`). Patch `0004-lora-sx127x-62k5-bandwidth` adds the 62.5 kHz bandwidth MeshCore defaults to. No RX duty cycle and no RX gain boost on this path. Reference board: TTGO LoRa32 (SX1276).
|
||||
- **LR2021** (`CONFIG_ZEPHCORE_RADIO_LR2021`): custom driver in `patches/zephyr-new/drivers/lora/lr20xx/` (copied into the Zephyr tree at configure time, like LR11xx). Experimental — bring-up on the ProMicro LR2021 is still in progress; not listed as a supported board.
|
||||
- **LR2021** (`CONFIG_ZEPHCORE_RADIO_LR2021`): custom driver in `patches/zephyr-new/drivers/lora/lr20xx/` (copied into the Zephyr tree at configure time, like LR11xx). **Validated on the SenseCAP MeshTracker X1** — RX, TX, LBT and RX duty cycle all confirmed on hardware after a full driver audit (2026-08-12). `promicro_lr2021` builds but is untested; its module was destroyed by overvoltage during bring-up. Notable properties that differ from the SX126x/LR11xx paths:
|
||||
- **Firmware Patch RAM.** DS §22.3 calls the PRAM "highly recommended"; without it the chip runs unpatched. `lr20xx_load_pram()` writes the 560-word image from `0x801000`, activates it with opcode `0x012D`, and verifies the magic word at `0x800FF8` — so the `PRAM loaded:` log line is proof the chip took it, not merely that the writes were accepted. Volatile: reloaded from both reset paths, survives every sleep this driver issues (all with retention).
|
||||
- **Hardware CAD→TX (`CadExitMode = 0x10`).** The chip runs the LBT CAD and, on a clear channel, transmits itself with no host round-trip. Payload and packet params are staged *before* `SetLoraCAD` and DIO1 stays enabled across it. Bounded by `cad_timeout`, which is 24 bits of 32 MHz periods = **524 ms max Tx timeout** — transmits whose airtime exceeds that take the classic CAD→host→`SetTx` route rather than being truncated (at SF7/BW62.5 the crossover is ~96 bytes).
|
||||
- **Front-end calibration is a point calibration, not a band.** `CalibFE` takes up to three individual frequencies (4 MHz steps, bit 15 = LF/HF), unlike the SX126x/LR11xx `CalibrateImage` freq1/freq2 band with datasheet-prescribed edges. It is issued only at config, after a hardware reset, and on AGC reset — never on the Tx/Rx path (DS §6.4.2 keeps the values on chip across retention sleep). Both 4 MHz neighbours of the operating frequency are calibrated, nearest first, because the SDK rounds the argument up where the chip's own default truncates down.
|
||||
- **Side detectors** (multi-SF receive) are LR2021-only; see `lr20xx_configure_side_detectors()`. Mutually exclusive with CAD, whose SF ordering constraint is the inverse.
|
||||
- **Per-packet frequency error** is decoded and accumulated (`get freqerr`) — diagnostic only, nothing acts on it.
|
||||
|
||||
### 5.6 Default Radio Parameters
|
||||
|
||||
@@ -531,7 +546,8 @@ key exists anywhere.
|
||||
and the reply is written straight into the offline queue. **No packet
|
||||
object is ever created**, so nothing can reach the dispatcher or radio.
|
||||
2. The v-contact never enters the real contacts table (`CMD_ADD_UPDATE_CONTACT`
|
||||
for its key is intercepted to a no-op OK), so it is never in the RF RX
|
||||
for its key is intercepted — it keeps only the app-owned `flags` byte, in
|
||||
`prefs.v_contact_flags`, and replies OK), so it is never in the RF RX
|
||||
matching path. Every other pubkey-addressed opcode (login, telemetry,
|
||||
binary req, path discovery…) misses `lookupContactByPubKey()` and fails
|
||||
`ERR_NOT_FOUND` before a packet exists.
|
||||
@@ -540,11 +556,27 @@ key exists anywhere.
|
||||
|
||||
**App plumbing**: appears as a virtual tail entry in the `CMD_GET_CONTACTS`
|
||||
iteration (and `+1` in the CONTACT_START total); pushed as `NEW_ADVERT` on
|
||||
runtime enable and rename, `CONTACT_DELETED` on disable. App-side contact
|
||||
delete (`CMD_REMOVE_CONTACT`) turns the feature off. Send/ack choreography is
|
||||
synthesized (SENT + immediate SEND_CONFIRMED, trip time 0). CLI replies are
|
||||
runtime enable and rename, `CONTACT_DELETED` on disable. Send/ack choreography
|
||||
is synthesized (SENT + immediate SEND_CONFIRMED, trip time 0). CLI replies are
|
||||
chunked at ≤150 chars on line breaks (offline-queue frames cap at 172 bytes).
|
||||
|
||||
`_vcontact_lastmod` is re-stamped once per app session at `CMD_APP_START`,
|
||||
before the `CMD_GET_CONTACTS` that follows it. Without that the timestamp only
|
||||
moved on boot/rename/identity-import, so the app showed an ever-growing "last
|
||||
seen" age *and* — because the sync gate is `_vcontact_lastmod >
|
||||
_contact_iter_since` — the v-contact was streamed exactly once ever, leaving
|
||||
the app holding a contact the node no longer mentioned.
|
||||
|
||||
**App-side delete is session-scoped** (`_vcontact_app_hidden`): the v-contact is
|
||||
withheld from sync and adverts for the rest of that session, and returns at the
|
||||
next `CMD_APP_START`. It deliberately does **not** touch
|
||||
`prefs.v_contact_enabled` — the v-contact is an ordinary entry in the app's
|
||||
contact list, so a "purge all contacts" walks it like any other, and the old
|
||||
behaviour (delete ⇒ pref off) let a routine purge silently disable a firmware
|
||||
feature with no way back except the USB CLI. Durable disable is node-side only:
|
||||
`set v.contact off`. Notices queued while hidden stay in the offline queue and
|
||||
drain on the next connect; only their `MSG_WAITING` prompt is suppressed.
|
||||
|
||||
**Clock gating (no 1970 timestamps)**: while the RTC has never been synced
|
||||
(time < firmware build epoch) the v-contact is *deferred* — withheld from
|
||||
contact sync and adverts, and notices are buffered in a small RAM slot
|
||||
@@ -570,8 +602,10 @@ are delivered on the first app connect/sync. RAM-backed: lost on reboot (the
|
||||
restart-reason message partially compensates) and bounded by
|
||||
`CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE`.
|
||||
|
||||
**Settings** (companion `v.*` CLI namespace, prefs offsets 152–154):
|
||||
- `set/get v.contact on|off` — default on.
|
||||
**Settings** (companion `v.*` CLI namespace, prefs offsets 152–154 plus
|
||||
`v_contact_flags` at 166):
|
||||
- `set/get v.contact on|off` — default on. The only durable disable; turning it
|
||||
off also clears `v_contact_flags`, since the app drops the contact.
|
||||
- `set/get v.batteryalert <mV>|0|default` — default = board auto-shutdown
|
||||
threshold + 200 mV (so the alert wins the race against the 90 s shutdown
|
||||
confirm window), 3500 mV on boards without auto-shutdown. Alert latches
|
||||
|
||||
@@ -834,6 +834,13 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
|
||||
for (int i = 0; i < EXTRA_SF_MAX && off < len; i++) {
|
||||
prefs.extra_sf[i] = buf[off++];
|
||||
}
|
||||
|
||||
/* Offset 166: v_contact_flags (ZephCore extension). Absent in pre-existing
|
||||
* files → stays 0, which is exactly the old behaviour (no favourite, no
|
||||
* telemetry permissions). */
|
||||
if (off < len) {
|
||||
prefs.v_contact_flags = buf[off++];
|
||||
}
|
||||
}
|
||||
|
||||
void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
|
||||
@@ -931,7 +938,9 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
|
||||
/* Offset 163-165: extra_sf (ZephCore extension, LR2021 side detectors) */
|
||||
memcpy(&buf[off], prefs.extra_sf, EXTRA_SF_MAX);
|
||||
off += EXTRA_SF_MAX;
|
||||
/* Total: 166 bytes */
|
||||
/* Offset 166: v_contact_flags (ZephCore extension) */
|
||||
buf[off++] = prefs.v_contact_flags;
|
||||
/* Total: 167 bytes */
|
||||
|
||||
bool ok = atomicReplaceFile(PREFS_FILE, buf, off);
|
||||
LOG_DBG("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'",
|
||||
|
||||
@@ -142,6 +142,21 @@ uint8_t LR2021Radio::hwCadBasePeak()
|
||||
return lr20xx_cad_base_peak(_dev);
|
||||
}
|
||||
|
||||
/* The detPeak range lr20xx_do_cad() will actually program. Must match the
|
||||
* driver's clamp exactly: if the adapter thinks the range is wider, the
|
||||
* staircase explores offsets that collapse onto one peak and reads the noise
|
||||
* between them as curvature — which is how it random-walked to -8 at SF7,
|
||||
* where base 51 puts everything from -3 down onto peak 48. */
|
||||
uint8_t LR2021Radio::hwCadPeakMin()
|
||||
{
|
||||
return lr20xx_cad_peak_min();
|
||||
}
|
||||
|
||||
uint8_t LR2021Radio::hwCadPeakMax()
|
||||
{
|
||||
return lr20xx_cad_peak_max();
|
||||
}
|
||||
|
||||
uint32_t LR2021Radio::hwWakeupTimeUs()
|
||||
{
|
||||
/* Per-device, because the TCXO term dominates and is board-specific:
|
||||
|
||||
@@ -42,6 +42,8 @@ protected:
|
||||
int hwCadProbe(int8_t level) override;
|
||||
void hwCadSetPeakOffset(int8_t offset) override;
|
||||
uint8_t hwCadBasePeak() override;
|
||||
uint8_t hwCadPeakMin() override;
|
||||
uint8_t hwCadPeakMax() override;
|
||||
uint32_t hwWakeupTimeUs() override;
|
||||
};
|
||||
|
||||
|
||||
@@ -1078,11 +1078,44 @@ bool LoRaRadioBase::isChannelActive(int threshold)
|
||||
|
||||
/* ── Adaptive CAD (LBT detPeak calibration) ───────────────────────────── */
|
||||
|
||||
/* The offset window is [CAD_LEVEL_MIN, CAD_LEVEL_MAX], but an offset is only
|
||||
* meaningful while base+offset still lands somewhere the driver will actually
|
||||
* program. Past the hardware clamp several offsets collapse onto one peak, and
|
||||
* the staircase cannot tell them apart — it reads sampling noise as curvature.
|
||||
* Narrow the window so every level it can reach is a distinct configuration.
|
||||
*
|
||||
* Radios that report no clamp (hwCadPeakMin/Max == 0) keep the static window,
|
||||
* which is also what a radio with no adaptive CAD at all gets. */
|
||||
int8_t LoRaRadioBase::cadLevelMinEff()
|
||||
{
|
||||
uint8_t base = hwCadBasePeak();
|
||||
uint8_t pmin = hwCadPeakMin();
|
||||
|
||||
if (base == 0 || pmin == 0 || pmin <= base - CAD_LEVEL_MIN) {
|
||||
return CAD_LEVEL_MIN;
|
||||
}
|
||||
return (int8_t)((int)pmin - (int)base);
|
||||
}
|
||||
|
||||
int8_t LoRaRadioBase::cadLevelMaxEff()
|
||||
{
|
||||
uint8_t base = hwCadBasePeak();
|
||||
uint8_t pmax = hwCadPeakMax();
|
||||
|
||||
if (base == 0 || pmax == 0 || (int)pmax - (int)base >= CAD_LEVEL_MAX) {
|
||||
return CAD_LEVEL_MAX;
|
||||
}
|
||||
return (int8_t)((int)pmax - (int)base);
|
||||
}
|
||||
|
||||
void LoRaRadioBase::setCadParams(bool auto_enabled, int8_t offset,
|
||||
uint16_t probe_interval_s, uint8_t busycap_pct)
|
||||
{
|
||||
if (offset < CAD_LEVEL_MIN) offset = CAD_LEVEL_MIN;
|
||||
if (offset > CAD_LEVEL_MAX) offset = CAD_LEVEL_MAX;
|
||||
const int8_t lo = cadLevelMinEff();
|
||||
const int8_t hi = cadLevelMaxEff();
|
||||
|
||||
if (offset < lo) offset = lo;
|
||||
if (offset > hi) offset = hi;
|
||||
|
||||
_cad_auto = auto_enabled;
|
||||
_cad_offset = offset;
|
||||
@@ -1146,7 +1179,7 @@ int8_t LoRaRadioBase::pickCadProbeLevel()
|
||||
case 3: lvl = (int8_t)(_cad_offset + 1); break; /* less sensitive */
|
||||
default: lvl = _cad_offset; break; /* operating (0, 2) */
|
||||
}
|
||||
if (lvl < CAD_LEVEL_MIN || lvl > CAD_LEVEL_MAX) {
|
||||
if (lvl < cadLevelMinEff() || lvl > cadLevelMaxEff()) {
|
||||
lvl = _cad_offset;
|
||||
}
|
||||
return lvl;
|
||||
@@ -1196,7 +1229,7 @@ void LoRaRadioBase::cadStaircaseStep()
|
||||
* just starves our own airtime. Cap is `set cad.busycap` percent (0 =
|
||||
* off); only binds on genuinely busy channels. */
|
||||
int cap_permille = (int)_cad_busycap_pct * 10;
|
||||
if (cap_permille && _cad_offset < CAD_LEVEL_MAX && b_op > cap_permille) {
|
||||
if (cap_permille && _cad_offset < cadLevelMaxEff() && b_op > cap_permille) {
|
||||
_cad_offset++;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
LOG_INF("cad: step up -> offset %d (airtime, busy %d cap %d)",
|
||||
@@ -1206,7 +1239,7 @@ void LoRaRadioBase::cadStaircaseStep()
|
||||
|
||||
/* Step UP (less sensitive) when the level above is markedly cleaner —
|
||||
* we're on the steep part of the curve, below the knee. */
|
||||
if (_cad_offset < CAD_LEVEL_MAX && r_up >= 0 &&
|
||||
if (_cad_offset < cadLevelMaxEff() && r_up >= 0 &&
|
||||
r_op - r_up >= CAD_KNEE_SLOPE_PERMILLE) {
|
||||
_cad_offset++;
|
||||
hwCadSetPeakOffset(_cad_offset);
|
||||
@@ -1224,7 +1257,7 @@ void LoRaRadioBase::cadStaircaseStep()
|
||||
int b_dn = busy_rate(oi - 1);
|
||||
bool busy_ok = (cap_permille == 0) ||
|
||||
(b_dn <= cap_permille - CAD_BUSY_DEFER_HYST_PERMILLE);
|
||||
if (_cad_offset > CAD_LEVEL_MIN && r_dn >= 0 &&
|
||||
if (_cad_offset > cadLevelMinEff() && r_dn >= 0 &&
|
||||
r_dn - r_op < CAD_KNEE_SLOPE_PERMILLE &&
|
||||
r_op <= CAD_PLATEAU_CLEAN_PERMILLE && busy_ok) {
|
||||
_cad_offset--;
|
||||
@@ -1477,15 +1510,25 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
|
||||
(unsigned)_cad_busycap_pct);
|
||||
|
||||
/* Only the 3 rungs around the operating offset — the far rungs are mildly
|
||||
* irrelevant; what matters is where we sit on the ladder. The window is
|
||||
* clamped to stay inside [CAD_LEVEL_MIN, CAD_LEVEL_MAX] while still showing
|
||||
* 3 rungs, so at either end it slides inward rather than dropping a line. */
|
||||
* irrelevant; what matters is where we sit on the ladder. The window is
|
||||
* clamped to the EFFECTIVE range while still showing 3 rungs, so at either
|
||||
* end it slides inward rather than dropping a line.
|
||||
*
|
||||
* Effective, not the static constants: past the hardware detPeak clamp
|
||||
* several offsets program the same peak, and showing them as separate
|
||||
* rungs invited exactly the wrong reading — three lines of distinct
|
||||
* statistics for one physical configuration. With the range narrowed,
|
||||
* every rung printed is a real one and `pk` below is what the chip got. */
|
||||
const int lmin = cadLevelMinEff();
|
||||
const int lmax = cadLevelMaxEff();
|
||||
int cur = _cad_offset;
|
||||
if (cur < CAD_LEVEL_MIN) cur = CAD_LEVEL_MIN;
|
||||
if (cur > CAD_LEVEL_MAX) cur = CAD_LEVEL_MAX;
|
||||
if (cur < lmin) cur = lmin;
|
||||
if (cur > lmax) cur = lmax;
|
||||
int lo = cur - 1, hi = cur + 1;
|
||||
if (lo < CAD_LEVEL_MIN) { lo = CAD_LEVEL_MIN; hi = lo + 2; }
|
||||
if (hi > CAD_LEVEL_MAX) { hi = CAD_LEVEL_MAX; lo = hi - 2; }
|
||||
if (lo < lmin) { lo = lmin; hi = lo + 2; }
|
||||
if (hi > lmax) { hi = lmax; lo = hi - 2; }
|
||||
if (lo < CAD_LEVEL_MIN) lo = CAD_LEVEL_MIN;
|
||||
if (hi > CAD_LEVEL_MAX) hi = CAD_LEVEL_MAX;
|
||||
|
||||
for (int lvl = lo; lvl <= hi; lvl++) {
|
||||
CadLevelStats &s = _cad_stats[lvl - CAD_LEVEL_MIN];
|
||||
|
||||
@@ -123,6 +123,14 @@ public:
|
||||
void cadMaintenance() override;
|
||||
uint32_t msUntilNextMaintenance() override;
|
||||
int8_t getCadOffset() const override { return _cad_offset; }
|
||||
|
||||
/* Offset bounds the controller may actually use: the static
|
||||
* [CAD_LEVEL_MIN, CAD_LEVEL_MAX] window narrowed to whatever the
|
||||
* hardware clamp leaves distinguishable at the current base. Every
|
||||
* range decision goes through these; the raw constants stay in use only
|
||||
* for indexing _cad_stats[], which is sized to the static window. */
|
||||
int8_t cadLevelMinEff();
|
||||
int8_t cadLevelMaxEff();
|
||||
void resetCadStats() override;
|
||||
int formatCadStatus(char *buf, int cap) override;
|
||||
|
||||
@@ -154,6 +162,19 @@ protected:
|
||||
/** Per-SF base detPeak for the current config (0 = unsupported). */
|
||||
virtual uint8_t hwCadBasePeak() { return 0; }
|
||||
|
||||
/** Absolute detPeak range the driver will actually program, inclusive.
|
||||
* 0/0 means "no known limit" and the offset range stays as-is.
|
||||
*
|
||||
* This exists because the offset window and the hardware clamp are two
|
||||
* different things, and when they disagree the controller explores
|
||||
* levels that are physically identical: on the LR2021 at SF7 the base is
|
||||
* 51 and the driver clamps to 48, so offsets -3 through -8 all programmed
|
||||
* the same peak. The staircase then compared three rungs of the same
|
||||
* configuration, found only sampling noise between them, and random-walked
|
||||
* into the floor with nothing to climb back out on. */
|
||||
virtual uint8_t hwCadPeakMin() { return 0; }
|
||||
virtual uint8_t hwCadPeakMax() { 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
|
||||
|
||||
@@ -218,6 +218,7 @@ CompanionMesh::CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mes
|
||||
memset(_vcontact_pending, 0, sizeof(_vcontact_pending));
|
||||
_vcontact_pending_count = 0;
|
||||
_vcontact_hold_msgwait = false;
|
||||
_vcontact_app_hidden = false;
|
||||
memset(&prefs, 0, sizeof(prefs));
|
||||
prefs.node_lat = 0;
|
||||
prefs.node_lon = 0;
|
||||
@@ -864,7 +865,12 @@ void CompanionMesh::buildVContact(ContactInfo &c) const
|
||||
{
|
||||
memcpy(c.id.pub_key, _vcontact_pubkey, PUB_KEY_SIZE);
|
||||
c.type = ADV_TYPE_CHAT;
|
||||
c.flags = 0;
|
||||
/* App-owned flags (bit 0 = favourite, upper bits = telemetry permissions).
|
||||
* The v-contact has no contacts-table record to hold them, so they live in
|
||||
* prefs — see the CMD_ADD_UPDATE_CONTACT interception below. Echoing a
|
||||
* hardcoded 0 here is what used to clear the favourite star on every
|
||||
* contact sync. */
|
||||
c.flags = prefs.v_contact_flags;
|
||||
c.out_path_len = 0; /* zero-hop direct — renders as "0 hops" in the app */
|
||||
c.shared_secret_valid = false;
|
||||
memset(c.out_path, 0, sizeof(c.out_path));
|
||||
@@ -950,8 +956,13 @@ void CompanionMesh::vcontactQueueText(const char *text)
|
||||
* makes the app interleave message-sync into the contact stream and
|
||||
* truncate it. The message is already safe in the offline queue and the
|
||||
* app's own initial message-sync drains it — so no prompt is needed inside
|
||||
* the window. Outside it, the prompt goes out immediately. */
|
||||
if (!_vcontact_hold_msgwait) {
|
||||
* the window. Outside it, the prompt goes out immediately.
|
||||
*
|
||||
* Also suppressed while the app has deleted the v-contact this session:
|
||||
* prompting for messages from a contact the app just dropped is noise. The
|
||||
* messages stay in the offline queue and drain on the next connect, when
|
||||
* the contact is back. */
|
||||
if (!_vcontact_hold_msgwait && !_vcontact_app_hidden) {
|
||||
sendPush(PUSH_CODE_MSG_WAITING);
|
||||
}
|
||||
}
|
||||
@@ -993,6 +1004,9 @@ void CompanionMesh::deriveVContactKey()
|
||||
void CompanionMesh::vcontactPushAdvert()
|
||||
{
|
||||
if (!isVContactEnabled()) return;
|
||||
/* The app deleted it this session — don't push it straight back at them.
|
||||
* It returns on its own at the next CMD_APP_START. */
|
||||
if (_vcontact_app_hidden) return;
|
||||
if (!vcontactClockValid()) {
|
||||
/* Defer — an advert stamped now would carry a 1970 timestamp.
|
||||
* vcontactClockSynced() re-runs this once a time source arrives. */
|
||||
@@ -1148,10 +1162,29 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len)
|
||||
return false;
|
||||
|
||||
case CMD_ADD_UPDATE_CONTACT:
|
||||
/* Never let the v-contact into the real contacts table (it must stay out
|
||||
* of the RF RX matching path) — but do keep the one field the app owns
|
||||
* and expects back: the flags byte (bit 0 = favourite, upper bits =
|
||||
* telemetry permissions). Everything else in the frame (name, path,
|
||||
* lat/lon, advert timestamp) is ours to generate in buildVContact().
|
||||
* Frame layout matches the real handler: [cmd][32-byte pubkey][type]
|
||||
* [flags][...]. Reply OK so app-side flows don't surface errors. */
|
||||
if (len >= 1 + PUB_KEY_SIZE && isVContactKey(&data[1], PUB_KEY_SIZE)) {
|
||||
if (len >= 1 + PUB_KEY_SIZE + 2) {
|
||||
uint8_t flags = data[1 + PUB_KEY_SIZE + 1];
|
||||
if (flags != prefs.v_contact_flags) {
|
||||
prefs.v_contact_flags = flags;
|
||||
_store->savePrefs(prefs);
|
||||
}
|
||||
}
|
||||
sendPacketOk();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case CMD_RESET_PATH:
|
||||
/* Never let the v-contact into the real contacts table (it must stay
|
||||
* out of the RF RX matching path); path resets are meaningless for a
|
||||
* loopback contact. Reply OK so app-side flows don't surface errors. */
|
||||
/* Path resets are meaningless for a loopback contact — accept and drop
|
||||
* so app-side flows don't surface errors. */
|
||||
if (len >= 1 + PUB_KEY_SIZE && isVContactKey(&data[1], PUB_KEY_SIZE)) {
|
||||
sendPacketOk();
|
||||
return true;
|
||||
@@ -1159,11 +1192,19 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len)
|
||||
return false;
|
||||
|
||||
case CMD_REMOVE_CONTACT:
|
||||
/* App-side delete turns the feature off (mirrors user intent);
|
||||
* `set v.contact on` (USB CLI) brings it back. */
|
||||
/* App-side delete hides the v-contact for the rest of this session and
|
||||
* nothing more — see _vcontact_app_hidden. It used to set
|
||||
* prefs.v_contact_enabled = 0, which made a routine "purge all
|
||||
* contacts" in the app silently disable the feature for good: the
|
||||
* v-contact is an ordinary list entry, so a purge removes it like any
|
||||
* other, and only the USB CLI could turn it back on. Durable disable
|
||||
* stays with the node-side pref (`set v.contact off`).
|
||||
*
|
||||
* Flags are deliberately kept: the same contact returns on the next
|
||||
* connect, so its favourite star should return with it. */
|
||||
if (len >= 1 + PUB_KEY_SIZE && isVContactKey(&data[1], PUB_KEY_SIZE)) {
|
||||
prefs.v_contact_enabled = 0;
|
||||
_store->savePrefs(prefs);
|
||||
_vcontact_app_hidden = true;
|
||||
LOG_INF("vcontact: hidden by app delete (this session only)");
|
||||
sendPacketOk();
|
||||
return true;
|
||||
}
|
||||
@@ -2075,11 +2116,29 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
/* New session: suppress v-contact notice MSG_WAITING until the initial
|
||||
* sync (contacts + messages) completes at PACKET_NO_MORE_MSGS. */
|
||||
_vcontact_hold_msgwait = true;
|
||||
/* An app-side delete only hides the v-contact for the session it
|
||||
* happened in — this is that session boundary, so it comes back. */
|
||||
_vcontact_app_hidden = false;
|
||||
|
||||
/* If a time source already ran (hardware RTC, GPS), activate the
|
||||
* deferred v-contact and flush buffered notices for this session. */
|
||||
vcontactClockSynced();
|
||||
|
||||
/* Re-stamp the v-contact once per app session so it stays "fresh".
|
||||
* _vcontact_lastmod feeds both lastmod and last_advert_timestamp, and
|
||||
* it used to move only on boot / rename / identity import: the app
|
||||
* showed an ever-growing "last seen" age, and — worse — the contact
|
||||
* sync gate is `_vcontact_lastmod > _contact_iter_since`, so after the
|
||||
* first sync the v-contact was never streamed again and app-side state
|
||||
* (flags, name) could never be corrected. Bumping here, before the
|
||||
* CMD_GET_CONTACTS that follows APP_START, means every session's sync
|
||||
* carries a current timestamp; deferring it to the end of sync would
|
||||
* always land one session late. Silent on purpose — no NEW_ADVERT push
|
||||
* mid-handshake; the sync itself delivers it. */
|
||||
if (vcontactReady() && vcontactClockValid()) {
|
||||
_vcontact_lastmod = (uint32_t)getRTCClock()->getCurrentTime();
|
||||
}
|
||||
|
||||
// Return SELF_INFO
|
||||
uint8_t rsp[90]; // 58 fixed + up to 32 bytes name
|
||||
size_t i = 0;
|
||||
@@ -2950,6 +3009,13 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
* and vcontactClockSynced() (the lastmod==0 path) emits at the
|
||||
* next time-sync instead of only after a reboot. */
|
||||
_vcontact_lastmod = 0;
|
||||
/* Different key → different contact in the app; the old
|
||||
* favourite/telemetry flags don't carry over. Persist
|
||||
* before the push so a reboot can't resurrect them. */
|
||||
if (prefs.v_contact_flags != 0) {
|
||||
prefs.v_contact_flags = 0;
|
||||
_store->savePrefs(prefs);
|
||||
}
|
||||
if (vc_was_enabled) vcontactPushAdvert();
|
||||
/* Reload contacts to invalidate ECDH shared secrets */
|
||||
resetContacts();
|
||||
|
||||
@@ -506,8 +506,18 @@ private:
|
||||
* guard and truncates the sync. Held from CMD_APP_START until the first
|
||||
* PACKET_NO_MORE_MSGS (end of the contacts+messages initial sync). */
|
||||
bool _vcontact_hold_msgwait;
|
||||
/* App-side delete (CMD_REMOVE_CONTACT for the loopback key) hides the
|
||||
* v-contact for the rest of the session only — it deliberately does NOT
|
||||
* touch prefs.v_contact_enabled. The v-contact is an ordinary entry in the
|
||||
* app's contact list, so a "purge all contacts" walks it like any other and
|
||||
* used to permanently disable a firmware feature with no way back except
|
||||
* the USB CLI. The pref is node-side state: `set v.contact off` is the only
|
||||
* durable disable. Cleared at CMD_APP_START (the session reset). */
|
||||
bool _vcontact_app_hidden;
|
||||
bool vcontactClockValid();
|
||||
bool vcontactReady() { return isVContactEnabled() && _vcontact_lastmod != 0; }
|
||||
bool vcontactReady() {
|
||||
return isVContactEnabled() && !_vcontact_app_hidden && _vcontact_lastmod != 0;
|
||||
}
|
||||
void buildVContact(ContactInfo &c) const;
|
||||
bool isVContactKey(const uint8_t *key, int prefix_len) const;
|
||||
/** (Re)derive _vcontact_pubkey from the current identity. Call on boot and
|
||||
|
||||
@@ -140,6 +140,12 @@ struct NodePrefs {
|
||||
uint16_t auto_shutdown_mv; // low-batt auto-shutdown threshold; 0 = off, else 2900–4200
|
||||
uint8_t v_contact_enabled; // v-contact (loopback admin chat via BLE/USB); 1 = on (default)
|
||||
uint16_t v_battery_alert_mv; // 0 = alert off; 0xFFFF = board default (auto_shutdown+200); else mV
|
||||
/* App-owned ContactInfo.flags byte for the v-contact. The v-contact never
|
||||
* enters the contacts table, so it has no record to hold the flags the app
|
||||
* sets via CMD_ADD_UPDATE_CONTACT -- bit 0 is the 'favourite' star, the
|
||||
* upper bits are telemetry permissions. Kept here so a toggle survives
|
||||
* reconnects and reboots instead of being echoed back as 0. */
|
||||
uint8_t v_contact_flags;
|
||||
};
|
||||
|
||||
/* Default prefs -- must match LoRaConfig.h defaults for radio interop. */
|
||||
|
||||
@@ -2557,6 +2557,28 @@ static const uint8_t lr20xx_cad_peak_table[4][8] = {
|
||||
/* 4 */ { 51, 51, 51, 54, 56, 60, 60, 64 },
|
||||
};
|
||||
|
||||
/* The detPeak range this driver will actually program. Exported through
|
||||
* lr20xx_cad_peak_min/max() so the C++ adaptive-CAD controller can narrow its
|
||||
* offset window to match: where base+offset falls outside this, several offsets
|
||||
* collapse onto one peak and the staircase reads sampling noise between
|
||||
* identical configurations as curvature.
|
||||
*
|
||||
* 48 sits three steps below the lowest value DS Table 6-19 documents anywhere
|
||||
* (51, at SF5-7 with 3-4 symbols), so the range already allows more sensitivity
|
||||
* than the datasheet tabulates; the datasheet gives no absolute bound. */
|
||||
#define LR20XX_CAD_PEAK_MIN 48
|
||||
#define LR20XX_CAD_PEAK_MAX 90
|
||||
|
||||
uint8_t lr20xx_cad_peak_min(void)
|
||||
{
|
||||
return LR20XX_CAD_PEAK_MIN;
|
||||
}
|
||||
|
||||
uint8_t lr20xx_cad_peak_max(void)
|
||||
{
|
||||
return LR20XX_CAD_PEAK_MAX;
|
||||
}
|
||||
|
||||
static uint8_t lr20xx_cad_detect_peak(uint8_t sf, uint8_t symb_nb)
|
||||
{
|
||||
if (sf < 5 || sf > 12) {
|
||||
@@ -2624,10 +2646,10 @@ static int lr20xx_do_cad(struct lr20xx_data *data)
|
||||
* 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;
|
||||
if (peak < LR20XX_CAD_PEAK_MIN) {
|
||||
peak = LR20XX_CAD_PEAK_MIN;
|
||||
} else if (peak > LR20XX_CAD_PEAK_MAX) {
|
||||
peak = LR20XX_CAD_PEAK_MAX;
|
||||
}
|
||||
cad.cad_detect_peak = (uint8_t)peak;
|
||||
}
|
||||
@@ -2761,10 +2783,10 @@ int lr20xx_cad_probe(const struct device *dev, int8_t peak_offset)
|
||||
int peak = base + peak_offset;
|
||||
int ret;
|
||||
|
||||
if (peak < 48) {
|
||||
peak = 48;
|
||||
} else if (peak > 90) {
|
||||
peak = 90;
|
||||
if (peak < LR20XX_CAD_PEAK_MIN) {
|
||||
peak = LR20XX_CAD_PEAK_MIN;
|
||||
} else if (peak > LR20XX_CAD_PEAK_MAX) {
|
||||
peak = LR20XX_CAD_PEAK_MAX;
|
||||
}
|
||||
|
||||
/* One-shot absolute override consumed by lr20xx_do_cad(). Probes and
|
||||
|
||||
@@ -173,6 +173,19 @@ void lr20xx_cad_set_peak_offset(const struct device *dev, int8_t offset);
|
||||
*/
|
||||
uint8_t lr20xx_cad_base_peak(const struct device *dev);
|
||||
|
||||
/**
|
||||
* @brief Absolute detPeak range this driver will program, inclusive
|
||||
*
|
||||
* The adaptive-CAD controller needs these to keep its offset window inside the
|
||||
* range where every level is a distinct configuration. Outside it the clamp
|
||||
* folds several offsets onto one peak, and the staircase compares rungs that
|
||||
* are physically identical.
|
||||
*
|
||||
* @return lowest / highest detPeak the driver will ever send
|
||||
*/
|
||||
uint8_t lr20xx_cad_peak_min(void);
|
||||
uint8_t lr20xx_cad_peak_max(void);
|
||||
|
||||
/**
|
||||
* @brief Run one blocking calibration CAD at base detPeak + peak_offset
|
||||
*
|
||||
|
||||
@@ -957,6 +957,9 @@ static bool handle_vcontact_cli(const char *line, char *reply)
|
||||
companion_mesh.vcontactPushAdvert();
|
||||
} else if (!en && was) {
|
||||
companion_mesh.vcontactPushDeleted();
|
||||
/* The app drops the contact, so its flags (favourite / telemetry
|
||||
* permissions) go with it — same as the app-side delete path. */
|
||||
companion_mesh.prefs.v_contact_flags = 0;
|
||||
}
|
||||
snprintf(reply, CLI_REPLY_SIZE, "OK - v.contact %s", en ? "on" : "off");
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user