mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-23 15:42:50 +00:00
x1 debug
This commit is contained in:
1 parent
329d0058fb
commit
3494411f83
25 files changed
+659
-63
No files matched your search
@@ -525,7 +525,7 @@ add_definitions(-DFIRMWARE_BUILD_EPOCH=${ZEPHCORE_BUILD_EPOCH}u)
|
||||
# release is tagged/named, and what the Mesh America catalog uses as its version
|
||||
# key — so the configurator can match a running device against the catalog. Keep
|
||||
# all four identical; the release workflow reads this value directly.
|
||||
set(ZEPHCORE_FIRMWARE_VERSION "1.16.8-zephcore")
|
||||
set(ZEPHCORE_FIRMWARE_VERSION "1.17.0-zephcore")
|
||||
add_definitions(-DFIRMWARE_VERSION="${ZEPHCORE_FIRMWARE_VERSION}")
|
||||
|
||||
add_subdirectory(lib/monocypher)
|
||||
|
||||
@@ -253,6 +253,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
|
||||
| `get probe.interval` | Seconds between periodic radio measurements (noise-floor sample + CAD probe). 0 = CAD probing off |
|
||||
| `get dc.restarts` | Duty-cycle re-arm counter — RxTimeout re-arms **plus** parked-RX watchdog recoveries, sharing one total. **Read it as a rate: divide by uptime.** A bare count is not interpretable, and the two sources it merges cost very differently. An RxTimeout re-arm is ~7 ms of deaf time (the `Calibrate(ALL)` gap in the driver's `restart_rx`) after which the chip returns to duty cycle immediately — packets, not power. A watchdog recovery means the chip sat parked in *full RX* for one to two watchdog periods (`2·(preamble+8)` symbols, floored at 250 ms) — power, not packets, since parked RX still receives. The counter cannot tell you which, so read the worst case. **Measured normal: ~250/hr on a high site at SF8/BW 62.5** (one every ~14 s), where the worst case — every event a park — costs about 3.5% of the duty cycle's savings. Nothing to act on below roughly **2000/hr**; above that the parked-RX share starts eating a meaningful fraction of the saving and it becomes worth splitting the counter to find out. A high rate means the preamble detector is tripping without a decodable packet following, which on an elevated site is usually distant marginal traffic rather than interference — cross-check `get cad`, whose adaptive detPeak offset rises independently in a genuinely busy RF environment. Reset by `clear stats`. |
|
||||
| `get cad` | Adaptive-CAD status: header (`a` auto on/off, `o` operating detPeak offset, `pk` absolute peak with family base, `sp` noise-floor RSSI burst quality as `mean-spread-dB/zero-spread-%` (plus `(burst-count)` on the local USB console, omitted over the air to protect the 161 B reply budget) — a non-zero mean proves the 8 reads are independent however high the share climbs; only mean `0.0` with a high share indicts the sampler. See `ADAPTIVE_CAD.md`. `bc` busy cap), then a 3-rung window around the operating offset (`*` marks it) with probe/busy/fp/tp counts and false-positive rate — the three levels the knee controller reads. Probing runs even while `cad.auto` is off (dry-run), so this is the observation tool for picking a site-appropriate detPeak. See `ADAPTIVE_CAD.md`. Not available on SX127x boards (no hardware CAD). |
|
||||
| `get extra.sf` | LR2021 side detectors: the extra spreading factors currently received alongside `sf`, comma-separated, or `none`. Reflects the saved prefs, not what the chip accepted — if the set became invalid after an `sf`/`bw` change it is reported here but was refused at boot (a `WRN` line says so). |
|
||||
| `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 |
|
||||
@@ -302,6 +303,7 @@ Changes are persisted immediately unless noted. Some require a reboot.
|
||||
| `set probe.interval <sec>` | 0 (off) or 10–255, default **15** | Seconds between periodic radio measurements. ONE reading serves both: the noise-floor RSSI sample (median of 8) and the CAD calibration probe, which consumes that same reading rather than measuring separately — so this is also the noise-floor sampling rate, and it sets how often an idle repeater wakes. Default 15 s → ~1–2 h CAD staircase response; the floor EMA warms up over 8 samples (~2 min) and its unguarded bypass runs every 16th (~4 min). Longer = fewer wakes, slower to track a changing RF environment. 0 disables CAD probing entirely (also freezes auto adaptation); the floor sampler then falls back to its build-time default. |
|
||||
| `set cad.busycap <pct>` | 0 (off) or 10–90, default **25** | Airtime-protection cap: the max percentage of TX attempts the node will let CAD defer before the staircase backs off to a less sensitive detPeak — counting **real** traffic, not just false positives. On a congested hilltop most busy verdicts are distant traffic won on capture anyway, so deferring for all of it starves the node's own airtime. Self-targeting: a quiet node's busy rate never reaches the cap. Shown as `bc:` in `get cad`. 0 disables the cap (pure knee-seeking). |
|
||||
| `set cad.reset` | | Clear the accumulated per-level CAD probe statistics (RAM only; also cleared automatically on any radio parameter change). |
|
||||
| `set extra.sf <sf> [sf] [sf]` | up to 3 SFs, `0`/`off` clears | **LR2021 only** (`Error: unsupported` elsewhere) — LoRa *side detectors*: demodulate up to three extra spreading factors concurrently with `sf`, on the same bandwidth, so one repeater can serve several SF communities. Which SF a packet arrived on is a chip-side readout, not a guess. Chip constraints, enforced in the driver and reported as `Error: unsupported or invalid extra SF config`: every extra SF must be **greater** than `sf`, all distinct, highest−lowest ≤ 4, and at BW ≥ 500 kHz at most 2 (only 1 when `sf` ≥ 10). **Receive only, and the bridge it creates is one-way.** TX always uses the single configured `sf`, and all detectors share one bandwidth, so this is multi-SF, not multi-channel. A node with `sf 7` + `extra.sf 8` hears SF8 traffic and *does* forward it — but the forward goes out at SF7, so traffic moves SF8 -> SF7 only and nothing comes back. An SF8 node's direct messages are delivered while its ACKs never arrive, so it retries to its limit every time; adverts and one-way flood traffic propagate fine. Because every extra SF must be **greater** than `sf`, the main SF is always the lowest in the set and TX always uses it — so the bridge direction is fixed at high-SF-in / low-SF-out and **cannot be reversed**. Two nodes back to back both point the same way; there is no configuration that carries SF7 -> SF8. Treat it as a collector for slower-SF stragglers, not as a link between two SF islands. Applied live and restored on every RX entry. **Interaction with CAD:** the chip's SF constraint for CAD is the inverse of the one for RX, so the driver switches side detectors off for each LBT CAD and back on when RX re-arms — two extra SPI commands per TX, no configuration required. Persisted; a set that no longer fits after an `sf`/`bw` change is refused at boot and logged. |
|
||||
| `set prv.key <hex>` | 64-char hex (32-byte key) | Replace private key; derive new identity *(reboot to apply)* |
|
||||
|
||||
---
|
||||
|
||||
@@ -807,6 +807,14 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
|
||||
prefs.adc_multiplier = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/* Offset 163-165: extra_sf (ZephCore extension, LR2021 side detectors).
|
||||
* Absent in pre-existing files → stays zeroed = feature off. Values
|
||||
* are re-validated by the driver when applied, so a corrupt byte here
|
||||
* costs a rejected config, not a bad radio state. */
|
||||
for (int i = 0; i < EXTRA_SF_MAX && off < len; i++) {
|
||||
prefs.extra_sf[i] = buf[off++];
|
||||
}
|
||||
}
|
||||
|
||||
void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
|
||||
@@ -901,7 +909,10 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
|
||||
* field existed — battery calibration silently reset every reboot. */
|
||||
memcpy(&buf[off], &prefs.adc_multiplier, sizeof(float));
|
||||
off += 4;
|
||||
/* Total: 163 bytes */
|
||||
/* 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 */
|
||||
|
||||
bool ok = atomicReplaceFile(PREFS_FILE, buf, off);
|
||||
LOG_DBG("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'",
|
||||
|
||||
@@ -31,6 +31,20 @@ void LR2021Radio::begin()
|
||||
LoRaRadioBase::begin();
|
||||
}
|
||||
|
||||
bool LR2021Radio::configSideDetectors(const uint8_t *sfs, uint8_t num)
|
||||
{
|
||||
/* The driver validates against the configured SF/BW, so this must run
|
||||
* after the modem is configured — which it is: the CLI path only
|
||||
* reaches here on a live radio. */
|
||||
int ret = lr20xx_configure_side_detectors(_dev, sfs, num);
|
||||
|
||||
if (ret < 0) {
|
||||
LOG_WRN("side detector config rejected: %d", ret);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── Hardware primitives ──────────────────────────────────────────────── */
|
||||
|
||||
bool LR2021Radio::hwConfigure(const struct lora_modem_config &cfg)
|
||||
|
||||
@@ -18,6 +18,9 @@ public:
|
||||
|
||||
void begin() override;
|
||||
|
||||
/* LoRa side detectors — LR2021-only multi-SF receive. */
|
||||
bool configSideDetectors(const uint8_t *sfs, uint8_t num) override;
|
||||
|
||||
protected:
|
||||
/* Hardware primitives */
|
||||
bool hwConfigure(const struct lora_modem_config &cfg) override;
|
||||
|
||||
@@ -81,6 +81,16 @@ public:
|
||||
virtual bool setRxBoost(bool enable);
|
||||
bool isRxBoostEnabled() const { return _rx_boost_enabled; }
|
||||
|
||||
/* Multi-SF receive via LoRa side detectors. Only the LR2021 has them;
|
||||
* every other radio reports the feature as unsupported. `num` = 0
|
||||
* disables. Returns false if the radio has no side detectors or the
|
||||
* requested set violates a chip constraint (see
|
||||
* lr20xx_configure_side_detectors). */
|
||||
virtual bool configSideDetectors(const uint8_t *sfs, uint8_t num) {
|
||||
(void)sfs; (void)num;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Read-only view of the modem config currently used by buildModemConfig().
|
||||
* These honor temporary radio overrides for freq/bw/sf/cr and the same TX
|
||||
* clamps as the actual lora_config() path. */
|
||||
|
||||
@@ -216,6 +216,9 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
|
||||
fs_read(&file, &prefs.probe_interval, sizeof(prefs.probe_interval));
|
||||
/* cad_busycap absent in <301-byte files; EOF read keeps default 25 */
|
||||
fs_read(&file, &prefs.cad_busycap, sizeof(prefs.cad_busycap));
|
||||
/* LR2021 side-detector SFs, offsets 301-303. Absent in <304-byte files;
|
||||
* the no-op EOF read leaves the zeroed default = feature off. */
|
||||
fs_read(&file, prefs.extra_sf, sizeof(prefs.extra_sf));
|
||||
|
||||
fs_close(&file);
|
||||
|
||||
@@ -363,6 +366,8 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
|
||||
fs_write(&file, &prefs.cad_offset, sizeof(prefs.cad_offset));
|
||||
fs_write(&file, &prefs.probe_interval, sizeof(prefs.probe_interval));
|
||||
fs_write(&file, &prefs.cad_busycap, sizeof(prefs.cad_busycap));
|
||||
/* LR2021 side-detector SFs (offsets 301-303) */
|
||||
fs_write(&file, prefs.extra_sf, sizeof(prefs.extra_sf));
|
||||
|
||||
ret = fs_sync(&file);
|
||||
fs_close(&file);
|
||||
|
||||
@@ -1205,6 +1205,10 @@ bool RepeaterMesh::setRxBoostedGain(bool enable) {
|
||||
return getRadioDriver(_radio).setRxBoost(enable);
|
||||
}
|
||||
|
||||
bool RepeaterMesh::configSideDetectors(const uint8_t* sfs, uint8_t num) {
|
||||
return getRadioDriver(_radio).configSideDetectors(sfs, num);
|
||||
}
|
||||
|
||||
void RepeaterMesh::formatNeighborsReply(char* reply) {
|
||||
char* dp = reply;
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ public:
|
||||
void dumpLogFile() override;
|
||||
void setTxPower(int8_t power_dbm) override;
|
||||
bool setRxBoostedGain(bool enable) override;
|
||||
bool configSideDetectors(const uint8_t* sfs, uint8_t num) override;
|
||||
void formatNeighborsReply(char* reply) override;
|
||||
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
|
||||
void formatStatsReply(char* reply) override;
|
||||
|
||||
@@ -944,6 +944,10 @@ bool RoomServerMesh::setRxBoostedGain(bool enable) {
|
||||
return getRadioDriver(_radio).setRxBoost(enable);
|
||||
}
|
||||
|
||||
bool RoomServerMesh::configSideDetectors(const uint8_t* sfs, uint8_t num) {
|
||||
return getRadioDriver(_radio).configSideDetectors(sfs, num);
|
||||
}
|
||||
|
||||
/* A room server keeps no neighbour table (it is not a repeater). */
|
||||
void RoomServerMesh::formatNeighborsReply(char* reply) {
|
||||
strcpy(reply, "not supported");
|
||||
|
||||
@@ -203,6 +203,7 @@ public:
|
||||
void dumpLogFile() override;
|
||||
void setTxPower(int8_t power_dbm) override;
|
||||
bool setRxBoostedGain(bool enable) override;
|
||||
bool configSideDetectors(const uint8_t* sfs, uint8_t num) override;
|
||||
void formatNeighborsReply(char* reply) override;
|
||||
void formatStatsReply(char* reply) override;
|
||||
void formatRadioStatsReply(char* reply) override;
|
||||
|
||||
@@ -156,7 +156,7 @@ CONFIG_BT_DIS_FW_REV=y
|
||||
# of truth for the C side, injected as -DFIRMWARE_VERSION). This Kconfig value can't
|
||||
# read a C macro, so it must be bumped here too. Phones that read DIS and phones
|
||||
# that query CMD_DEVICE_QUERY should see the same version.
|
||||
CONFIG_BT_DIS_FW_REV_STR="1.16.8-zephcore"
|
||||
CONFIG_BT_DIS_FW_REV_STR="1.17.0-zephcore"
|
||||
CONFIG_BT_DIS_SW_REV=y
|
||||
CONFIG_BT_DIS_SW_REV_STR="Zephyr"
|
||||
CONFIG_BT_DIS_PNP=n
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
# - Battery ADC on AIN4 (P0.28), 1.75:1 voltage divider, ADC_CTRL=P0.11
|
||||
# - LEDs: RED=P0.12, BLUE=P0.07 (active-HIGH), TX LED = BLUE
|
||||
# - Button on P0.17 (active-low, pull-up)
|
||||
# - QSPI Flash MX25R1635F 2MB
|
||||
# - No external flash — the QSPI bus is routed but the chip is not
|
||||
# populated (see thinknode_m6.dts), so everything lives on /lfs
|
||||
|
||||
# Board identification (matches Arduino MeshCore variant name)
|
||||
CONFIG_ZEPHCORE_BOARD_NAME="ThinkNode M6"
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
* TX LED = BLUE (P_LORA_TX_LED = PIN_LED_BLUE)
|
||||
* - Button on P0.17 (active-low, pull-up)
|
||||
* - I2C on P1.09/P0.08 for on-board sensors
|
||||
* - QSPI Flash MX25R1635F (2MB): SCK=P1.03, CS=P0.23, IO0=P1.01,
|
||||
* IO1=P1.02, IO2=P1.04, IO3=P1.05, enable=P0.21
|
||||
* - No external flash. The board routes QSPI (SCK=P1.03, CS=P0.23,
|
||||
* IO0=P1.01, IO1=P1.02, IO2=P1.04, IO3=P1.05, enable=P0.21) but the
|
||||
* MX25R1635F is not populated — see the note at the bottom of this file.
|
||||
* - Power enable: PIN_PWR_EN=P0.27 (regulator-boot-on)
|
||||
*/
|
||||
|
||||
@@ -105,17 +106,6 @@
|
||||
regulator-boot-on;
|
||||
};
|
||||
|
||||
/* QSPI flash enable (QSPI_FLASH_EN = P0.21) — must be HIGH before flash responds.
|
||||
* WARNING: Nordic QSPI NOR driver hangs forever if flash doesn't respond.
|
||||
* This regulator ensures flash is powered before driver init (priority 41). */
|
||||
qspi_flash_enable: qspi-flash-enable {
|
||||
compatible = "regulator-fixed";
|
||||
regulator-name = "qspi-flash-enable";
|
||||
enable-gpios = <&gpio0 21 GPIO_ACTIVE_HIGH>;
|
||||
regulator-boot-on;
|
||||
startup-delay-us = <1000>;
|
||||
};
|
||||
|
||||
/* ADC_CTRL (PIN_ADC_CTRL = P0.11) — enables the VBAT resistor divider only
|
||||
* while a battery read is in progress, matching Arduino MeshCore
|
||||
* (variant.cpp holds it LOW at rest; ThinkNodeM6Board pulses it HIGH for
|
||||
@@ -294,26 +284,24 @@
|
||||
};
|
||||
};
|
||||
|
||||
/* QSPI Flash — MX25R1635F 2MB */
|
||||
&qspi {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&qspi_default>;
|
||||
pinctrl-1 = <&qspi_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
|
||||
qspi_flash: qspi_flash@0 {
|
||||
compatible = "nordic,qspi-nor";
|
||||
reg = <0>;
|
||||
writeoc = "pp";
|
||||
readoc = "fastread";
|
||||
sck-frequency = <16000000>;
|
||||
jedec-id = [c2 28 15]; /* MX25R1635F */
|
||||
size = <16777216>; /* 2MB = 16Mbit */
|
||||
has-dpd;
|
||||
t-enter-dpd = <10000>;
|
||||
t-exit-dpd = <35000>;
|
||||
};
|
||||
};
|
||||
/* No QSPI flash node.
|
||||
*
|
||||
* The board routes a QSPI bus and the datasheet lists an MX25R1635F, but the
|
||||
* chip is not actually populated: upstream Arduino MeshCore dropped its
|
||||
* QSPIFLASH=1 build flag in 5c534c43's sibling commit e4fbd386 — "M6 doesn't
|
||||
* have flash chip available, so companion roles fail to start" — after
|
||||
* hitting exactly that on real hardware. ZephCore declared the part (with a
|
||||
* P0.21 enable regulator and a comment warning that the Nordic QSPI NOR
|
||||
* driver hangs if the flash does not answer), which is the same wrong
|
||||
* assumption arrived at independently, so companion builds most likely never
|
||||
* came up here either.
|
||||
*
|
||||
* Everything now lives on internal flash: /lfs holds identity, prefs,
|
||||
* contacts and channels, and ZephyrDataStore reports _has_ext_fs = 0.
|
||||
* If a unit ever turns up with the chip fitted, restore the &qspi node, its
|
||||
* partition, the P0.21 enable regulator and the qspi-ext.dtsi include —
|
||||
* the pinctrl groups are still in thinknode_m6-pinctrl.dtsi.
|
||||
*/
|
||||
|
||||
/* USB CDC for console/CLI */
|
||||
zephyr_udc0: &usbd {
|
||||
@@ -328,21 +316,4 @@ zephyr_udc0: &usbd {
|
||||
/* SoftDevice v6 partition layout (S140 v6 — Adafruit nRF52 bootloader) */
|
||||
#include "../../common/nrf52_partitions_sdv6.dtsi"
|
||||
|
||||
/* External QSPI flash partitions */
|
||||
&qspi_flash {
|
||||
partitions {
|
||||
compatible = "fixed-partitions";
|
||||
#address-cells = <1>;
|
||||
#size-cells = <1>;
|
||||
|
||||
/* Full 2MB for LittleFS storage */
|
||||
qspi_storage_partition: partition@0 {
|
||||
label = "qspi_storage";
|
||||
reg = <0x00000000 0x00200000>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/* External QSPI /ext automount */
|
||||
#include "../../common/qspi-ext.dtsi"
|
||||
#include "../../common/nrf52_wakeup.dtsi"
|
||||
@@ -134,6 +134,7 @@ void CommonCLI::loadPrefs(const char* path) {
|
||||
ok = ok && prefs_read(&file, &_prefs->cad_offset, sizeof(_prefs->cad_offset)); // 298
|
||||
ok = ok && prefs_read(&file, &_prefs->probe_interval, sizeof(_prefs->probe_interval)); // 299
|
||||
ok = ok && prefs_read(&file, &_prefs->cad_busycap, sizeof(_prefs->cad_busycap)); // 300
|
||||
ok = ok && prefs_read(&file, _prefs->extra_sf, sizeof(_prefs->extra_sf)); // 301-303
|
||||
|
||||
if (!ok) {
|
||||
LOG_WRN("Prefs file %s truncated, some fields use defaults", path);
|
||||
@@ -265,6 +266,7 @@ void CommonCLI::savePrefs(const char* path) {
|
||||
fs_write(&file, &_prefs->cad_offset, sizeof(_prefs->cad_offset));
|
||||
fs_write(&file, &_prefs->probe_interval, sizeof(_prefs->probe_interval));
|
||||
fs_write(&file, &_prefs->cad_busycap, sizeof(_prefs->cad_busycap));
|
||||
fs_write(&file, _prefs->extra_sf, sizeof(_prefs->extra_sf));
|
||||
|
||||
fs_close(&file);
|
||||
LOG_INF("Saved prefs to %s", path);
|
||||
@@ -606,6 +608,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
if (_callbacks->formatCadStatus(reply + n, (int)cap - n) == 0) {
|
||||
strcpy(reply, "not available");
|
||||
}
|
||||
} else if (memcmp(config, "extra.sf", 8) == 0) {
|
||||
char* dp = reply;
|
||||
dp += sprintf(dp, "> ");
|
||||
int shown = 0;
|
||||
for (int i = 0; i < EXTRA_SF_MAX && _prefs->extra_sf[i] != 0; i++) {
|
||||
dp += sprintf(dp, "%s%u", shown++ ? "," : "",
|
||||
(unsigned)_prefs->extra_sf[i]);
|
||||
}
|
||||
if (shown == 0) {
|
||||
strcpy(reply, "> none");
|
||||
}
|
||||
} else if (memcmp(config, "meshtimesync", 12) == 0) {
|
||||
MeshTimeSync* ts = _callbacks->getMeshTimeSync();
|
||||
if (ts == nullptr) {
|
||||
@@ -761,6 +774,32 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
} else if (memcmp(config, "cad.reset", 9) == 0) {
|
||||
_callbacks->resetCadStats();
|
||||
strcpy(reply, "OK - CAD probe stats cleared");
|
||||
} else if (memcmp(config, "extra.sf ", 9) == 0) {
|
||||
/* LR2021 side detectors: up to 3 extra SFs received alongside
|
||||
* `sf`. "0" / "off" clears the set. The chip-side constraints
|
||||
* (each > sf, distinct, spread <= 4, BW>=500 caps the count) are
|
||||
* enforced in the driver, so an accepted set is a valid one. */
|
||||
char tmp[32];
|
||||
const char* parts[EXTRA_SF_MAX + 1];
|
||||
uint8_t sfs[EXTRA_SF_MAX] = {0};
|
||||
StrHelper::strncpy(tmp, &config[9], sizeof(tmp));
|
||||
int num = mesh::Utils::parseTextParts(tmp, parts, EXTRA_SF_MAX + 1, ' ');
|
||||
if (num == 1 && (strcmp(parts[0], "0") == 0 || strcmp(parts[0], "off") == 0)) {
|
||||
num = 0;
|
||||
}
|
||||
if (num > EXTRA_SF_MAX) {
|
||||
sprintf(reply, "Error: at most %d extra SFs", EXTRA_SF_MAX);
|
||||
} else {
|
||||
for (int i = 0; i < num; i++) sfs[i] = (uint8_t)atoi(parts[i]);
|
||||
if (_callbacks->configSideDetectors(sfs, (uint8_t)num)) {
|
||||
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
||||
for (int i = 0; i < num; i++) _prefs->extra_sf[i] = sfs[i];
|
||||
savePrefs();
|
||||
strcpy(reply, num ? "OK - extra SFs set" : "OK - extra SFs cleared");
|
||||
} else {
|
||||
strcpy(reply, "Error: unsupported or invalid extra SF config");
|
||||
}
|
||||
}
|
||||
} else if (memcmp(config, "multi.acks ", 11) == 0) {
|
||||
int val = atoi(&config[11]);
|
||||
if (val == 0 || val == 1) {
|
||||
|
||||
@@ -46,6 +46,12 @@ public:
|
||||
/* Apply RX boosted gain live; returns false when the radio has no
|
||||
* RX boost feature (upstream PR #2844 semantics). */
|
||||
virtual bool setRxBoostedGain(bool enable) { (void)enable; return false; }
|
||||
/* Configure LR2021 side detectors (multi-SF receive); num = 0 disables.
|
||||
* Returns false when the radio has no side detectors, or when the set
|
||||
* violates a chip constraint — the driver is the validator. */
|
||||
virtual bool configSideDetectors(const uint8_t* sfs, uint8_t num) {
|
||||
(void)sfs; (void)num; return false;
|
||||
}
|
||||
/* Repeater-specific — default replies keep companion builds clean.
|
||||
* Repeater overrides all four; companions get "not available". */
|
||||
virtual void formatNeighborsReply(char* reply) { strcpy(reply, "not available"); }
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
#define LEDS_PREF_ON 0xA0
|
||||
#define LEDS_PREF_OFF 0xA1
|
||||
|
||||
/* Number of LR2021 side-detector slots stored in prefs. Matches the chip's
|
||||
* ConfigureSideDetectors limit of 3. */
|
||||
#define EXTRA_SF_MAX 3
|
||||
|
||||
struct NodePrefs {
|
||||
/* ---- Common fields (both roles) ---- */
|
||||
float airtime_factor;
|
||||
@@ -107,6 +111,13 @@ struct NodePrefs {
|
||||
// one noise-floor sample, and the CAD probe that
|
||||
// consumes it (0 = CAD probing off, default 15)
|
||||
uint8_t cad_busycap; // airtime-protection: max % of TX attempts deferred before backing off detPeak (0 = off, default 25)
|
||||
/* LR2021 side detectors: extra spreading factors received concurrently
|
||||
* with `sf`, on the same bandwidth. Zero-terminated list — the first 0
|
||||
* ends it, so an all-zero array means the feature is off (which is what
|
||||
* every node upgrading from a build without this field has). Every
|
||||
* entry must be greater than `sf`, all distinct, spread <= 4; the
|
||||
* driver rejects anything else. Ignored on non-LR2021 radios. */
|
||||
uint8_t extra_sf[EXTRA_SF_MAX];
|
||||
|
||||
/* ---- Companion-only fields ---- */
|
||||
uint8_t manual_add_contacts;
|
||||
|
||||
@@ -130,6 +130,18 @@ struct lr11xx_data {
|
||||
* bit is cleared and TX released. All accesses under spi_mutex. */
|
||||
uint32_t preamble_seen_at_ms;
|
||||
|
||||
/* Timestamp (k_uptime_get_32(), ms) at which lr11xx_is_receiving()
|
||||
* first saw SYNC_WORD_HEADER_VALID latched in the current RX cycle;
|
||||
* 0 = no payload phase being timed. Bounds the payload phase the same
|
||||
* way preamble_seen_at_ms bounds the preamble phase: the header bit is
|
||||
* cleared only by the terminal DIO1 event's bulk clear, and continuous
|
||||
* RX (SetRx 0xFFFFFF) has no symbol timer, so a header whose packet
|
||||
* never completes produces no terminal IRQ and would pin the TX gate
|
||||
* true forever — the node keeps receiving but never transmits again,
|
||||
* silently. Released after lr11xx_max_payload_ms(). All accesses
|
||||
* under spi_mutex. */
|
||||
uint32_t header_seen_at_ms;
|
||||
|
||||
/* Wedge-recovery watchdog: the LR1110 can rarely be left BUSY-high with
|
||||
* DIO1 low (a command racing the autonomous SetRxDutyCycle sleep phase) —
|
||||
* no IRQ ever fires, so the event-driven driver never re-arms and the node
|
||||
@@ -146,6 +158,17 @@ struct lr11xx_data {
|
||||
|
||||
/* ── Helpers ────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Reset all software state that says "we are currently receiving": the
|
||||
* preamble-grace timestamp and the payload-phase deadline. Paired write so
|
||||
* the two never drift out of sync — same shape as the SX126x driver's
|
||||
* sx126x_reset_rx_busy_signals(). Called from every RX (re)start site and
|
||||
* before TX / CAD entry. */
|
||||
static inline void lr11xx_reset_rx_busy_signals(struct lr11xx_data *data)
|
||||
{
|
||||
data->preamble_seen_at_ms = 0;
|
||||
data->header_seen_at_ms = 0;
|
||||
}
|
||||
|
||||
static lr11xx_radio_lora_bw_t bw_enum_to_lr11xx(enum lora_signal_bandwidth bw)
|
||||
{
|
||||
switch (bw) {
|
||||
@@ -362,7 +385,7 @@ static void lr11xx_start_rx(struct lr11xx_data *data,
|
||||
}
|
||||
|
||||
lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK);
|
||||
data->preamble_seen_at_ms = 0;
|
||||
lr11xx_reset_rx_busy_signals(data);
|
||||
|
||||
/* Apply modem config for RX */
|
||||
lr11xx_apply_modem_config(data, cfg, false);
|
||||
@@ -423,7 +446,7 @@ static void lr11xx_restart_rx(struct lr11xx_data *data)
|
||||
void *ctx = &data->hal_ctx;
|
||||
|
||||
lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK);
|
||||
data->preamble_seen_at_ms = 0;
|
||||
lr11xx_reset_rx_busy_signals(data);
|
||||
|
||||
if (data->rx_duty_cycle_enabled &&
|
||||
data->dc_rx_ms != 0 && data->dc_sleep_ms != 0) {
|
||||
@@ -874,7 +897,7 @@ static int lr11xx_lora_send_async(const struct device *dev,
|
||||
|
||||
/* Clear IRQ, enable DIO1 */
|
||||
lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK);
|
||||
data->preamble_seen_at_ms = 0;
|
||||
lr11xx_reset_rx_busy_signals(data);
|
||||
lr11xx_hal_enable_dio1_irq(&data->hal_ctx);
|
||||
|
||||
/* Store signal and start TX. 10 s timeout: worst legal preset
|
||||
@@ -1063,6 +1086,51 @@ static uint32_t lr11xx_preamble_grace_ms(struct lr11xx_data *data)
|
||||
return (uint32_t)((us + 999U) / 1000U);
|
||||
}
|
||||
|
||||
/* Upper bound on the payload phase: airtime of a maximum-length (255 byte)
|
||||
* explicit-header packet at the current SF/BW, worst-case coding rate 4/8,
|
||||
* plus margin. Bounds the lifetime of the SYNC_WORD_HEADER_VALID busy state
|
||||
* in lr11xx_is_receiving().
|
||||
*
|
||||
* Why the header bit needs a deadline at all: it is cleared only by the
|
||||
* terminal DIO1 event's bulk clear or an RX (re)start, and continuous RX
|
||||
* (SetRx 0xFFFFFF) has no symbol timer, so neither is guaranteed to arrive.
|
||||
* A header whose packet never completes would pin the TX gate true forever
|
||||
* and silently mute the node until reboot. Same reasoning and same formula
|
||||
* as sx126x_max_payload_ms() (patch 0013); Arduino MeshCore added the
|
||||
* equivalent bound for the LR11x0 in 0bd871cd.
|
||||
*
|
||||
* Deliberately generous — this is a stuck-state safety net, and releasing
|
||||
* early would let TX start on top of a packet that is still arriving. LDRO
|
||||
* (DE) is pinned at 1 because that yields the larger symbol count, i.e. the
|
||||
* safer bound. */
|
||||
static uint32_t lr11xx_max_payload_ms(struct lr11xx_data *data)
|
||||
{
|
||||
uint8_t sf = (uint8_t)data->modem_cfg.datarate;
|
||||
uint32_t bw_hz = (uint32_t)(bw_enum_to_khz(data->modem_cfg.bandwidth) * 1000.0f);
|
||||
|
||||
if (bw_hz == 0 || sf < 5 || sf > 12) {
|
||||
return 30000; /* safe default if config is uninitialised */
|
||||
}
|
||||
|
||||
/* Semtech payload-symbol count with PL=255, CRC on, explicit header,
|
||||
* CR = 4/8 (coded_bits = 8), DE = 1:
|
||||
* n = 8 + ceil((8*PL - 4*SF + 28 + 16) / (4*(SF - DE))) * 8
|
||||
* SF5/SF6 use SF-1 >= 4 so the divisor is always non-zero. */
|
||||
uint32_t numer = 8U * 255U + 28U + 16U;
|
||||
uint32_t denom = 4U * (uint32_t)(sf - 1U);
|
||||
|
||||
if (numer > 4U * (uint32_t)sf) {
|
||||
numer -= 4U * (uint32_t)sf;
|
||||
}
|
||||
uint32_t n_sym = 8U + ((numer + denom - 1U) / denom) * 8U;
|
||||
|
||||
/* n_sym * 2^sf * 1000000 / bw_hz -> us, then +25% and +100 ms margin. */
|
||||
uint64_t us = ((uint64_t)n_sym << sf) * 1000000ULL / bw_hz;
|
||||
uint32_t ms = (uint32_t)((us + 999U) / 1000U);
|
||||
|
||||
return ms + (ms / 4U) + 100U;
|
||||
}
|
||||
|
||||
bool lr11xx_is_receiving(const struct device *dev)
|
||||
{
|
||||
struct lr11xx_data *data = dev->data;
|
||||
@@ -1087,10 +1155,35 @@ bool lr11xx_is_receiving(const struct device *dev)
|
||||
|
||||
/* Header landed: payload phase in progress. The bit stays latched
|
||||
* until the terminal DIO1 event bulk-clears it, so this covers the
|
||||
* whole packet. */
|
||||
* whole packet — bounded by a payload deadline, because in continuous
|
||||
* RX that terminal event is not guaranteed to arrive and a header that
|
||||
* never completes would otherwise mute TX until reboot. */
|
||||
if (irq & LR11XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID) {
|
||||
uint32_t now = k_uptime_get_32();
|
||||
uint32_t seen = data->header_seen_at_ms;
|
||||
|
||||
if (seen == 0) {
|
||||
data->header_seen_at_ms = (now == 0) ? 1U : now;
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
return true;
|
||||
}
|
||||
if ((now - seen) < lr11xx_max_payload_ms(data)) {
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
return true;
|
||||
}
|
||||
LOG_WRN("RX header latched %u ms with no packet, releasing TX gate",
|
||||
now - seen);
|
||||
/* Drop the sticky reception bits so the next poll starts clean.
|
||||
* CMD_ERROR goes with them — the LR1110 sets it whenever ClearIrq
|
||||
* is called with a mask that excludes it (all FW versions). */
|
||||
lr11xx_system_clear_irq_status(&data->hal_ctx,
|
||||
LR11XX_SYSTEM_IRQ_PREAMBLE_DETECTED |
|
||||
LR11XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID |
|
||||
LR11XX_SYSTEM_IRQ_HEADER_ERROR |
|
||||
LR11XX_SYSTEM_IRQ_CMD_ERROR);
|
||||
lr11xx_reset_rx_busy_signals(data);
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* PREAMBLE_DETECTED with SF-aware grace (SX126x-parity). Within
|
||||
@@ -1116,13 +1209,13 @@ bool lr11xx_is_receiving(const struct device *dev)
|
||||
lr11xx_system_clear_irq_status(&data->hal_ctx,
|
||||
LR11XX_SYSTEM_IRQ_PREAMBLE_DETECTED |
|
||||
LR11XX_SYSTEM_IRQ_CMD_ERROR);
|
||||
data->preamble_seen_at_ms = 0;
|
||||
lr11xx_reset_rx_busy_signals(data);
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* No preamble, no header: nothing in flight. */
|
||||
data->preamble_seen_at_ms = 0;
|
||||
lr11xx_reset_rx_busy_signals(data);
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
return false;
|
||||
}
|
||||
@@ -1282,7 +1375,7 @@ static int lr11xx_do_cad(struct lr11xx_data *data)
|
||||
lr11xx_radio_set_cad_params(ctx, &cad);
|
||||
|
||||
lr11xx_system_clear_irq_status(ctx, LR11XX_SYSTEM_IRQ_ALL_MASK);
|
||||
data->preamble_seen_at_ms = 0;
|
||||
lr11xx_reset_rx_busy_signals(data);
|
||||
data->cad_active = true;
|
||||
lr11xx_radio_set_cad(ctx);
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ LOG_MODULE_REGISTER(lr20xx_lora, CONFIG_LORA_LOG_LEVEL);
|
||||
#define LR20XX_DIO1_WQ_STACK_SIZE 2560
|
||||
K_THREAD_STACK_DEFINE(lr20xx_dio1_wq_stack, LR20XX_DIO1_WQ_STACK_SIZE);
|
||||
|
||||
/* Hardware limit on concurrent LoRa side detectors (ConfigureSideDetectors
|
||||
* takes n in [0:3]). */
|
||||
#define LR20XX_MAX_SIDE_DETECTORS 3
|
||||
|
||||
/* ── Driver data structures ─────────────────────────────────────────── */
|
||||
|
||||
struct lr20xx_config {
|
||||
@@ -113,6 +117,16 @@ struct lr20xx_data {
|
||||
int dio1_stuck_count;
|
||||
bool tcxo_disabled; /* set when the TCXO fallback has already fired */
|
||||
|
||||
/* LoRa side detectors — up to 3 extra spreading factors demodulated
|
||||
* concurrently with the main one, on the same bandwidth. Stored here
|
||||
* because the chip forgets them on every SetModulationParams, so they
|
||||
* have to be re-issued after each apply_modem_config, and because CAD
|
||||
* has to switch them off (see lr20xx_apply_side_detectors).
|
||||
* side_det_num == 0 means the feature is off. */
|
||||
uint8_t side_det_sf[LR20XX_MAX_SIDE_DETECTORS];
|
||||
uint8_t side_det_num;
|
||||
bool side_det_applied; /* currently programmed into the chip */
|
||||
|
||||
/* RX data buffer */
|
||||
uint8_t rx_buf[256];
|
||||
};
|
||||
@@ -485,6 +499,69 @@ static void lr20xx_check_cmd(void *ctx, const char *what)
|
||||
#define CHECK_CMD(ctx, what) do { } while (0)
|
||||
#endif
|
||||
|
||||
/* Program the stored side-detector set into the chip (or clear it).
|
||||
*
|
||||
* The chip drops every side detector on SetModulationParams, so this has to
|
||||
* run after each apply_modem_config and after anything else that reprograms
|
||||
* the modem — the caller owns spi_mutex and must have the chip out of Rx/Tx.
|
||||
*
|
||||
* `enable == false` writes the n=0 form, which is how the datasheet says to
|
||||
* turn the feature off. That is needed around CAD: the constraints are
|
||||
* directional and mutually exclusive with ours — normal Rx requires the main
|
||||
* SF to be *lower* than every side SF, CAD requires it to be *higher*. Since
|
||||
* this driver runs LBT CAD before every TX, side detectors are switched off
|
||||
* for the CAD and restored when RX is re-armed. (Upstream MeshCore hit the
|
||||
* same wall from the other side: RadioLib returned -706 on the startReceive
|
||||
* after a hardware CAD with side detectors enabled.) */
|
||||
static void lr20xx_apply_side_detectors(struct lr20xx_data *data, bool enable)
|
||||
{
|
||||
void *ctx = &data->hal_ctx;
|
||||
lr20xx_radio_lora_side_detector_cfg_t det[LR20XX_MAX_SIDE_DETECTORS];
|
||||
uint8_t syncwords[LR20XX_MAX_SIDE_DETECTORS];
|
||||
uint8_t n = enable ? data->side_det_num : 0;
|
||||
uint32_t bw_hz;
|
||||
|
||||
if (n == 0) {
|
||||
if (!data->side_det_applied) {
|
||||
return; /* already off — don't spend an SPI command */
|
||||
}
|
||||
lr20xx_radio_lora_configure_side_detectors(ctx, NULL, 0);
|
||||
data->side_det_applied = false;
|
||||
return;
|
||||
}
|
||||
|
||||
bw_hz = (uint32_t)(bw_enum_to_khz(data->modem_cfg.bandwidth) * 1000.0f);
|
||||
|
||||
for (uint8_t i = 0; i < n; i++) {
|
||||
uint32_t symbol_time_us = bw_hz ?
|
||||
((1U << data->side_det_sf[i]) * 1000000U) / bw_hz : 0;
|
||||
|
||||
det[i].sf = (lr20xx_radio_lora_sf_t)data->side_det_sf[i];
|
||||
/* Same LDRO rule as the main modem config — per side detector,
|
||||
* since each runs at its own SF and a longer symbol crosses the
|
||||
* 16.38 ms threshold before the main one does. */
|
||||
det[i].ppm = (symbol_time_us > 16380) ? LR20XX_RADIO_LORA_PPM_1_4
|
||||
: LR20XX_RADIO_LORA_NO_PPM;
|
||||
det[i].iq = data->modem_cfg.iq_inverted
|
||||
? LR20XX_RADIO_LORA_IQ_INVERTED
|
||||
: LR20XX_RADIO_LORA_IQ_STANDARD;
|
||||
/* One mesh, one sync word: side detectors carry the same
|
||||
* public/private word as the main detector. */
|
||||
syncwords[i] = data->modem_cfg.public_network ? 0x34 : 0x12;
|
||||
}
|
||||
|
||||
lr20xx_radio_lora_configure_side_detectors(ctx, det, n);
|
||||
CHECK_CMD(ctx, "configure_side_detectors");
|
||||
lr20xx_radio_lora_set_side_detector_syncwords(ctx, syncwords, n);
|
||||
CHECK_CMD(ctx, "set_side_detector_syncwords");
|
||||
data->side_det_applied = true;
|
||||
|
||||
LOG_DBG("side detectors: %u active (SF%u/%u/%u)", n,
|
||||
data->side_det_sf[0],
|
||||
n > 1 ? data->side_det_sf[1] : 0,
|
||||
n > 2 ? data->side_det_sf[2] : 0);
|
||||
}
|
||||
|
||||
static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
const struct lr20xx_config *cfg,
|
||||
bool tx_mode)
|
||||
@@ -549,6 +626,8 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
LOG_DBG("modem_cfg: set_mod(SF%d BW%d CR%d PPM%d)=%d",
|
||||
mod.sf, mod.bw, mod.cr, mod.ppm, rc);
|
||||
CHECK_CMD(ctx, "set_mod_params");
|
||||
/* SetModulationParams disables every side detector, chip-side. */
|
||||
data->side_det_applied = false;
|
||||
|
||||
/* DCDC workaround removed — LDO mode, RadioLib doesn't do it */
|
||||
|
||||
@@ -610,6 +689,11 @@ static void lr20xx_apply_modem_config(struct lr20xx_data *data,
|
||||
LOG_DBG("modem_cfg: set_dio_irq=%d", rc);
|
||||
CHECK_CMD(ctx, "set_dio_irq");
|
||||
|
||||
/* Re-arm side detectors for RX; leave them off for the TX path (their
|
||||
* constraints are RX-directional, and set_mod_params just cleared
|
||||
* them anyway). */
|
||||
lr20xx_apply_side_detectors(data, !tx_mode);
|
||||
|
||||
DUMP_CHIP_STATE(ctx, &data->hal_ctx, tx_mode ? "modem-TX" : "modem-RX");
|
||||
}
|
||||
|
||||
@@ -695,6 +779,11 @@ static void lr20xx_restart_rx(struct lr20xx_data *data)
|
||||
|
||||
lr20xx_system_clear_irq_status(ctx, LR20XX_SYSTEM_IRQ_ALL_MASK);
|
||||
|
||||
/* LBT CAD switches side detectors off (their SF constraint is the
|
||||
* inverse of the Rx one), so restore them on the way back into RX.
|
||||
* No-op when none are configured or they are still programmed. */
|
||||
lr20xx_apply_side_detectors(data, true);
|
||||
|
||||
/* TX rewrites payload_len to the length it sent, and in explicit-header
|
||||
* RX that field is a filter: "accept 1..payload_len, reject anything
|
||||
* longer with a header error". Left alone, a node silently stops hearing
|
||||
@@ -1294,6 +1383,73 @@ bool lr20xx_is_receiving(const struct device *dev)
|
||||
LR20XX_SYSTEM_IRQ_SYNC_WORD_HEADER_VALID)) != 0;
|
||||
}
|
||||
|
||||
int lr20xx_configure_side_detectors(const struct device *dev,
|
||||
const uint8_t *sfs, uint8_t num)
|
||||
{
|
||||
struct lr20xx_data *data = dev->data;
|
||||
const struct lr20xx_config *cfg = dev->config;
|
||||
uint8_t main_sf = (uint8_t)data->modem_cfg.datarate;
|
||||
uint8_t lowest = main_sf, highest = main_sf;
|
||||
uint8_t max_allowed = LR20XX_MAX_SIDE_DETECTORS;
|
||||
|
||||
if (num > LR20XX_MAX_SIDE_DETECTORS) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Constraints are the datasheet's, via the vendor driver header:
|
||||
* - normal Rx: the main SF must be lower than every side SF
|
||||
* - all SFs distinct
|
||||
* - highest - lowest <= 4 (main SF counts, it is one of the set)
|
||||
* - BW >= 500 kHz caps the count at 2, or 1 when main SF >= SF10
|
||||
* They are checked here rather than at the CLI so the chip limits live
|
||||
* next to the chip. */
|
||||
if (data->modem_cfg.bandwidth >= BW_500_KHZ) {
|
||||
max_allowed = (main_sf >= 10) ? 1 : 2;
|
||||
}
|
||||
if (num > max_allowed) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < num; i++) {
|
||||
if (sfs[i] < 5 || sfs[i] > 12 || sfs[i] <= main_sf) {
|
||||
return -EINVAL;
|
||||
}
|
||||
for (uint8_t j = 0; j < i; j++) {
|
||||
if (sfs[j] == sfs[i]) {
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
if (sfs[i] < lowest) { lowest = sfs[i]; }
|
||||
if (sfs[i] > highest) { highest = sfs[i]; }
|
||||
}
|
||||
if (highest - lowest > 4) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
k_mutex_lock(&data->spi_mutex, K_FOREVER);
|
||||
|
||||
data->side_det_num = num;
|
||||
for (uint8_t i = 0; i < num; i++) {
|
||||
data->side_det_sf[i] = sfs[i];
|
||||
}
|
||||
|
||||
/* Take effect now. A full RX restart is the cheapest correct way to
|
||||
* get there: the command needs the chip out of Rx, and start_rx runs
|
||||
* apply_modem_config, which re-applies the set on our behalf. */
|
||||
if (data->in_rx_mode && !data->tx_active) {
|
||||
lr20xx_start_rx(data, cfg);
|
||||
} else {
|
||||
/* Not receiving — nothing to reprogram yet, the next RX entry
|
||||
* picks the stored set up. */
|
||||
data->side_det_applied = false;
|
||||
}
|
||||
|
||||
k_mutex_unlock(&data->spi_mutex);
|
||||
|
||||
LOG_INF("side detectors configured: %u", num);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void lr20xx_set_rx_boost(const struct device *dev, bool enable)
|
||||
{
|
||||
struct lr20xx_data *data = dev->data;
|
||||
@@ -1429,6 +1585,11 @@ static int lr20xx_do_cad(struct lr20xx_data *data)
|
||||
cad.cad_detect_peak = data->cad_probe_peak;
|
||||
}
|
||||
|
||||
/* Side detectors must be off for CAD: normal Rx needs the main SF
|
||||
* below every side SF, CAD needs it above — the two cannot hold at
|
||||
* once. restart_rx puts them back. */
|
||||
lr20xx_apply_side_detectors(data, false);
|
||||
|
||||
lr20xx_radio_lora_configure_cad_params(ctx, &cad);
|
||||
CHECK_CMD(ctx, "configure_cad_params");
|
||||
|
||||
|
||||
@@ -45,6 +45,27 @@ bool lr20xx_is_receiving(const struct device *dev);
|
||||
*/
|
||||
void lr20xx_set_rx_boost(const struct device *dev, bool enable);
|
||||
|
||||
/**
|
||||
* @brief Configure LoRa side detectors (multi-SF receive)
|
||||
*
|
||||
* Programs up to 3 extra spreading factors to be demodulated concurrently
|
||||
* with the configured one, on the same bandwidth. Applies immediately and
|
||||
* is re-applied on every RX entry (the chip drops the set on each
|
||||
* SetModulationParams, and LBT CAD has to switch it off — the datasheet's
|
||||
* SF constraints for CAD are the inverse of those for RX).
|
||||
*
|
||||
* Datasheet constraints, enforced here: every side SF must be greater than
|
||||
* the configured SF, all SFs distinct, highest - lowest <= 4, and at
|
||||
* BW >= 500 kHz at most 2 side detectors (1 when the main SF is >= 10).
|
||||
*
|
||||
* @param dev LoRa device
|
||||
* @param sfs Array of side spreading factors (5..12)
|
||||
* @param num Number of side detectors, 0..3 (0 disables the feature)
|
||||
* @return 0 on success, -EINVAL if the set violates a chip constraint
|
||||
*/
|
||||
int lr20xx_configure_side_detectors(const struct device *dev,
|
||||
const uint8_t *sfs, uint8_t num);
|
||||
|
||||
/**
|
||||
* @brief Get a random number from the radio hardware RNG
|
||||
*
|
||||
|
||||
@@ -732,6 +732,10 @@ public:
|
||||
return lora_radio.setRxBoost(enable);
|
||||
}
|
||||
|
||||
bool configSideDetectors(const uint8_t* sfs, uint8_t num) override {
|
||||
return lora_radio.configSideDetectors(sfs, num);
|
||||
}
|
||||
|
||||
/* Adaptive CAD */
|
||||
int formatCadStatus(char* buf, int cap) override {
|
||||
return lora_radio.formatCadStatus(buf, cap);
|
||||
@@ -1554,6 +1558,18 @@ int main(void)
|
||||
companion_mesh.prefs.cad_offset,
|
||||
companion_mesh.prefs.probe_interval,
|
||||
companion_mesh.prefs.cad_busycap);
|
||||
|
||||
/* LR2021 side detectors (multi-SF RX) from prefs. extra_sf is a
|
||||
* zero-terminated list; a rejected set (SF/BW changed since it was
|
||||
* saved) just leaves the feature off, and non-LR2021 radios report it
|
||||
* unsupported. */
|
||||
{
|
||||
uint8_t n = 0;
|
||||
while (n < EXTRA_SF_MAX && companion_mesh.prefs.extra_sf[n] != 0) n++;
|
||||
if (n > 0 && !lora_radio.configSideDetectors(companion_mesh.prefs.extra_sf, n)) {
|
||||
LOG_WRN("extra.sf %u SFs rejected for current SF/BW — side detectors off", n);
|
||||
}
|
||||
}
|
||||
ui_set_radio_runtime(
|
||||
lora_radio.getActiveSyncWord(),
|
||||
lora_radio.getActivePreambleLength(),
|
||||
|
||||
@@ -749,6 +749,18 @@ int main(void)
|
||||
lora_radio.setCadParams(prefs->cad_auto != 0, prefs->cad_offset,
|
||||
prefs->probe_interval, prefs->cad_busycap);
|
||||
|
||||
/* LR2021 side detectors (multi-SF RX) from prefs. extra_sf is a
|
||||
* zero-terminated list; a rejected set (SF/BW changed since it was
|
||||
* saved) just leaves the feature off, and non-LR2021 radios report it
|
||||
* unsupported. */
|
||||
{
|
||||
uint8_t n = 0;
|
||||
while (n < EXTRA_SF_MAX && prefs->extra_sf[n] != 0) n++;
|
||||
if (n > 0 && !lora_radio.configSideDetectors(prefs->extra_sf, n)) {
|
||||
LOG_WRN("extra.sf %u SFs rejected for current SF/BW — side detectors off", n);
|
||||
}
|
||||
}
|
||||
|
||||
/* Feed initial UI state from loaded prefs */
|
||||
ui_set_node_name(prefs->node_name);
|
||||
refresh_repeater_ui_radio_state();
|
||||
|
||||
@@ -637,6 +637,18 @@ int main(void)
|
||||
lora_radio.setCadParams(prefs->cad_auto != 0, prefs->cad_offset,
|
||||
prefs->probe_interval, prefs->cad_busycap);
|
||||
|
||||
/* LR2021 side detectors (multi-SF RX) from prefs. extra_sf is a
|
||||
* zero-terminated list; a rejected set (SF/BW changed since it was
|
||||
* saved) just leaves the feature off, and non-LR2021 radios report it
|
||||
* unsupported. */
|
||||
{
|
||||
uint8_t n = 0;
|
||||
while (n < EXTRA_SF_MAX && prefs->extra_sf[n] != 0) n++;
|
||||
if (n > 0 && !lora_radio.configSideDetectors(prefs->extra_sf, n)) {
|
||||
LOG_WRN("extra.sf %u SFs rejected for current SF/BW — side detectors off", n);
|
||||
}
|
||||
}
|
||||
|
||||
/* Feed initial UI state from loaded prefs */
|
||||
ui_set_node_name(prefs->node_name);
|
||||
ui_set_radio_params(
|
||||
|
||||
Reference in new issue
Block a user