mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:09:17 +00:00
x1 debug
This commit is contained in:
@@ -24,7 +24,7 @@ env:
|
||||
# Release artifacts are still built and attached, so a manual download and
|
||||
# flash works exactly as usual.
|
||||
#
|
||||
# 1.16.8: pre-release while ESP32 light sleep is unvalidated on hardware.
|
||||
# 1.17.0: normal release.
|
||||
prerelease: "false"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# ZephCore 1.17.0-zephcore
|
||||
|
||||
> [!IMPORTANT]
|
||||
> ## Before you upgrade
|
||||
>
|
||||
> **From v1.16.5 – v1.16.8** — clean flash, no re-bond, bonds and data survive.
|
||||
>
|
||||
> **From v1.16.2 / v1.16.3 / v1.16.4** — clean flash, bonds and data survive. If you are on an
|
||||
> **ESP32-S3 / ESP32-C board** and have *not* yet taken the v1.16.5 update, you still owe the one-time
|
||||
> serial reflash: the app moved to flash offset `0x10000` in v1.16.5, and a board on the old layout
|
||||
> cannot cross that update over WiFi-OTA or the browser flasher — **flash the `-merged.bin` once over
|
||||
> USB/serial.** Affected boards: *Heltec V3 / V4 / V4.3, Station G2, Wireless Tracker / V2, XIAO
|
||||
> ESP32-S3 / C3 / C6, LilyGo T-Lora C6, ThinkNode M9.* Identity, contacts, channels, prefs, and BLE
|
||||
> bonds are preserved. nRF52, classic ESP32 (T-Beam / PICO-D4 / TTGO LoRa32), STM32WL, and native
|
||||
> Linux are unaffected.
|
||||
>
|
||||
> **From v1.16.1 or older** — flash it; on first boot it clears BLE bonds automatically (identity,
|
||||
> contacts, channels, prefs preserved). Re-bond your phone/desktop once.
|
||||
>
|
||||
> **Coming from Arduino MeshCore** — flash it; auto-formats on first boot (new identity, clean storage).
|
||||
>
|
||||
> **ThinkNode M6 owners: this release stops using the external flash chip, because the board does not
|
||||
> have one.** Contacts, channels and prefs move to internal flash on first boot. See the M6 section.
|
||||
>
|
||||
> **Repeater and room-server telemetry no longer hands out your GPS position to non-admins.**
|
||||
> If you were relying on public position reporting from a fixed node, set the location in prefs and
|
||||
> advertise it instead.
|
||||
>
|
||||
> **ESP32 light sleep (new in 1.16.8) is still awaiting hardware validation.** It remains on by default
|
||||
> for Heltec repeaters, and their USB CLI still stops answering 10 minutes after boot as a result —
|
||||
> reconnecting the terminal resets the node and gives you another window. Rebuild with `-DCONFIG_PM=n`
|
||||
> to take it out.
|
||||
|
||||
---
|
||||
|
||||
A **hardware and receive-path release**. A new tracker board lands (Seeed SenseCAP MeshTracker X1, the
|
||||
first LR2021 target with real hardware behind it), the LR2021 gains multi-SF receive, the LR11xx and
|
||||
LR20xx receive gates get the same stuck-state bound the SX126x got in 1.16.x, GPS module configuration
|
||||
grew a diagnostics path and a dynamic-model setting, and the Zephyr pin moves to 4.4.2 final.
|
||||
|
||||
## Highlights
|
||||
|
||||
### New board — Seeed SenseCAP MeshTracker X1
|
||||
|
||||
`meshtracker_x1` — nRF52840 + **Semtech LR2021**, 863–928 MHz / 22 dBm, dual-band L1+L5 Airoha GNSS,
|
||||
SPA06-003 barometer, DRV2605L haptic motor, RGB LED, buzzer, 1100 mAh high-voltage LiPo, 8 MB QSPI
|
||||
flash and an on-board RTC. Companion, repeater and room-server all build.
|
||||
|
||||
Hardware-wise it is a T1000-E with the radio swapped, so the LoRa SPI pins, button, buzzer, battery
|
||||
divider and the whole GNSS control pin set map straight onto the existing T1000-E paths. What is new
|
||||
on this board:
|
||||
|
||||
- **SPA06-003 barometer driver, written from scratch** (`adapters/sensors/spa06.c` plus its binding) —
|
||||
no upstream Zephyr driver exists. It lives in the app tree rather than the Zephyr tree, so it needs
|
||||
no patch and survives `west update`. Pressure and temperature are reported through the normal
|
||||
environment-telemetry path.
|
||||
- **Haptic notifications.** `helpers/ui/haptic.c` wraps Zephyr's `ti,drv2605`, and the buzzer setting
|
||||
became a four-state notification mode — `set buzzer 0|1|2|3` = silent / sound+vibrate / vibrate /
|
||||
sound, with the 3-tap button cycling through them. Boards with no motor keep a plain on/off toggle.
|
||||
The value rides in the existing `buzzer_quiet` byte, so there is no prefs migration.
|
||||
- **8 MB of QSPI flash as `/ext`** for contacts, channels and the blob cache, and the on-board
|
||||
YSN8900 RTC on its own I²C bus, discovered and BCD-validated at boot.
|
||||
|
||||
> [!NOTE]
|
||||
> The **LR2021 radio path is still bring-up code.** The driver is ZephCore's own, and only one LR2021 board
|
||||
> has been validated on air yet. Treat X1 radio behaviour as untested; the rest of the board (GNSS,
|
||||
> sensors, storage, UI, power) is on well-travelled paths.
|
||||
|
||||
### LR2021 multi-SF receive — `set extra.sf`
|
||||
|
||||
The LR2021 can demodulate up to **three extra spreading factors at once**, alongside the configured
|
||||
one, on the same bandwidth. One repeater can therefore serve several SF communities without splitting
|
||||
the site, and which SF a packet arrived on is a chip-side readout rather than an inference.
|
||||
|
||||
```
|
||||
set extra.sf 9 10 # receive SF9 and SF10 in addition to the configured SF
|
||||
get extra.sf # -> 9,10
|
||||
set extra.sf off # back to single-SF receive
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Receive only, and the bridge it creates is one-way.** Transmission always uses the single
|
||||
> configured `sf`, and every detector shares one bandwidth — multi-SF, not multi-channel.
|
||||
>
|
||||
> A node running `sf 7` with `extra.sf 8` hears SF8 traffic and *does* forward it, but the forward goes
|
||||
> out at SF7. Traffic therefore moves **SF8 → SF7 only, and nothing comes back**. Adverts and one-way
|
||||
> flood traffic propagate fine; an SF8 node's direct messages get delivered but its ACKs never arrive,
|
||||
> so it retries to its limit every time. That is worth weighing before deploying it next to a busy
|
||||
> slow-SF island.
|
||||
>
|
||||
> The direction cannot be reversed. Because every extra SF must be *greater* than `sf`, the main SF is
|
||||
> always the lowest in the set, and TX always uses the main SF — so a side-detector node is always
|
||||
> high-SF-in / low-SF-out. Two of them back to back point the same way; **no configuration carries
|
||||
> SF7 → SF8.** Think of this as a collector that pulls slower-SF stragglers into a fast mesh, not as a
|
||||
> link between two SF islands. A genuine bidirectional bridge needs two nodes joined by a non-LoRa
|
||||
> link, or one node time-slicing its radio config — neither of which this feature provides.
|
||||
|
||||
The chip's constraints are enforced in the driver, so an accepted set is a valid one: every extra SF
|
||||
must be **greater** than the configured `sf`, all distinct, highest−lowest ≤ 4, and at BW ≥ 500 kHz at
|
||||
most two (only one when `sf` ≥ 10). The setting is persisted and re-applied on every RX entry; a set
|
||||
that no longer fits after an `sf` or `bw` change is refused at boot and logged rather than silently
|
||||
half-applied. `Error: unsupported` on every non-LR2021 radio.
|
||||
|
||||
One thing worth knowing about how this interacts with listen-before-talk: the chip's SF constraint for
|
||||
**CAD** is the exact inverse of the one for **receive** — CAD wants the main SF above the side SFs,
|
||||
receive wants it below. Both cannot hold at once, so the driver switches side detectors off for each
|
||||
LBT CAD and restores them when RX re-arms. That costs two extra SPI commands per transmission and
|
||||
needs no configuration. (Arduino MeshCore hit the same wall from the other direction, as a `-706`
|
||||
error on the receive restart after a hardware CAD.)
|
||||
|
||||
### The LR11xx receive gate can no longer wedge
|
||||
|
||||
The driver reports "we are currently receiving" from the chip's latched PREAMBLE_DETECTED and
|
||||
HEADER_VALID bits, and that answer is what gates every transmission. Neither bit is self-clearing:
|
||||
they are dropped only by a terminal interrupt or an RX restart, and in continuous receive there is no
|
||||
symbol timer, so neither is guaranteed to arrive. A header whose packet never completes would pin the
|
||||
gate true **forever** — the node keeps receiving but never transmits again, until reboot, with nothing
|
||||
in the log pointing at it.
|
||||
|
||||
This is the same class of fault the SX126x driver was hardened against earlier (`0013`), and it is now
|
||||
closed on the LR11xx:
|
||||
|
||||
- **LR11xx (LR1110)** already had the preamble grace; it gains the payload deadline.
|
||||
- **LR20xx (LR2021)** still has an unbounded gate — a bare OR of the two bits. The bound was written
|
||||
for it too, then pulled back out: it landed alongside a duty-cycle rework that regressed receive on
|
||||
the MeshTracker X1, and the whole group was reverted to the last configuration confirmed working on
|
||||
hardware. It will return once the LR2021 receive path is settled.
|
||||
|
||||
The deadline is the airtime of a maximum-length packet at the current SF/BW at coding rate 4/8 with
|
||||
LDRO pinned on, plus 25% and 100 ms — deliberately generous, because this is a stuck-state safety net
|
||||
and releasing early would start a transmission on top of a packet that is still arriving. When it
|
||||
fires it logs a warning, which is the tripwire that would prove the fault happens in the field; it has
|
||||
never been reproduced on a bench.
|
||||
|
||||
### ThinkNode M6 — the external flash is gone, because the chip is not there
|
||||
|
||||
The M6 routes a QSPI bus and its datasheet lists an MX25R1635F, so ZephCore declared the part, gave it
|
||||
a power-enable regulator, and mounted it as `/ext`. Upstream Arduino MeshCore has since dropped the
|
||||
same assumption after hitting it on real hardware — *"M6 doesn't have flash chip available, so
|
||||
companion roles fail to start"* — which means companion builds most likely never came up on this board
|
||||
here either.
|
||||
|
||||
The flash node, its partition, the enable regulator and the `/ext` mount are removed. Everything now
|
||||
lives on internal flash: identity, prefs, contacts and channels all on `/lfs`. This is the second
|
||||
ThinkNode M6 assumption disproved by upstream bench data in as many releases — the GPS REINIT pin was
|
||||
the first — so the board is worth treating as under-verified generally.
|
||||
|
||||
### GPS module configuration: dynamic model, and a diagnostics readout
|
||||
|
||||
Two additions on the GNSS side, both aimed at the thing that is hardest to see from outside: whether
|
||||
the module actually accepted what the firmware sent it.
|
||||
|
||||
- **`CONFIG_ZEPHCORE_GPS_NAV_MODE`** sets the dynamic model on CASIC-family modules (Quectel L76K /
|
||||
L76KB, Air530Z) — stationary by default for repeaters and room servers, automotive otherwise. This
|
||||
is worth setting rather than leaving alone, because **the model is stored in the module** and
|
||||
survives reflashing the host: a module pulled from another device can arrive stuck in an airborne
|
||||
profile and behave oddly on a rooftop.
|
||||
- **`set gps diag on` / `get gps diag`** reports what the last module-configuration attempt actually
|
||||
did: which path ran, how many bytes were sent, and tracked satellites per constellation. Not
|
||||
persisted — it clears on reboot.
|
||||
|
||||
### `set leds on|off` — one master switch
|
||||
|
||||
A single persisted switch for every LED on the node, applied live: heartbeat, unread-message and LoRa
|
||||
TX-activity LEDs, plus the message and shutdown flashes. It works on every role, including headless
|
||||
repeaters where the TX LED is the only one that ever lights. The display backlight is not covered —
|
||||
that is the separate UI brightness setting.
|
||||
|
||||
### Repeater and room-server GPS position is now admin-only
|
||||
|
||||
Telemetry from a repeater or room server used to include the node's GPS position for any requester
|
||||
holding the location permission. It is now released to **admins only**. A fixed node that should
|
||||
publish its position can still do so through prefs and adverts, which is the deliberate, visible way
|
||||
to share it.
|
||||
|
||||
## Other changes
|
||||
|
||||
- **Zephyr pinned to 4.4.2 final** (was a 4.4.2 pre-release commit).
|
||||
- **ESP32: the user button is now a light-sleep wake source** on boards using the shared PM overlay, so
|
||||
a sleeping node wakes on a button press rather than only on a received packet.
|
||||
- **LR2021 SPI HAL resynced with RadioLib**, and the driver picked up a batch of datasheet-conformance
|
||||
fixes: the TCXO start-up deadline is counted in 32 MHz periods rather than RTC ticks (the previous
|
||||
unit turned a 5 ms allowance into 5 µs and guaranteed a start-up error), front-end calibration is
|
||||
issued in its full three-slot form, the DIO interrupt mask is narrowed to the events the handler
|
||||
actually services, the CAD detection thresholds now come from the datasheet's 4-symbol row to match
|
||||
the 4-symbol window actually used, and the receive payload-length filter is restored after each
|
||||
transmission — without that, a node silently stopped hearing any packet longer than its own last
|
||||
transmission.
|
||||
- **Adaptive-CAD and noise-floor sampling** picked up further fixes in the SX126x patch set.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **LR2021 is not hardware-validated.** No LR2021 board has been confirmed on air. The X1's radio path
|
||||
and everything in the multi-SF feature above are code-complete and build-verified only.
|
||||
- **ESP32 light sleep is not hardware-validated** and remains on by default for Heltec repeaters. The
|
||||
failure mode to rule out is a deaf repeater; check the boot log for `Pin N is not wakeup capable`.
|
||||
- **ThinkNode M3 / M6 GPS fixes are not hardware-confirmed** — both were derived from upstream bench
|
||||
data on their respective boards, not measured here.
|
||||
@@ -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