Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
481712930c
|
||
|
|
d262abe853
|
||
|
|
23ddc15557
|
||
|
|
4d723a85ce
|
||
|
|
6380bcbf6a
|
||
|
|
7a045edc1a
|
||
|
|
b53570ce66
|
||
|
|
647929ff8d
|
||
|
|
0fbd8412c2
|
||
|
|
ebd576b4e2
|
||
|
|
a2c611841b
|
||
|
|
a1b92356b3
|
||
|
|
7fca5fcb50
|
||
|
|
6867439044
|
||
|
|
bcfe736b30
|
No files matched your search
+189
@@ -7,6 +7,195 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.4.0] - 2026-04-25
|
||||
|
||||
### Added
|
||||
|
||||
- **RAK4631 WisBlock board support** (nRF52840 + SX1262, RAK19007 base
|
||||
+ RAK4631 Core + RAK1921 OLED). The second nRF52840 + SX1262 board
|
||||
alongside the Wio Tracker L1.
|
||||
- `SX126X_POWER_EN` (P1.05) driven HIGH at init — the radio is
|
||||
power-gated on this module; without this the chip has no power
|
||||
and SPI hangs.
|
||||
- HFXO + LFXO external crystals selected in `embassy_nrf::Config`.
|
||||
USB CDC-ACM needs ±500 ppm which the internal RC oscillator
|
||||
can't provide.
|
||||
- `SoftwareVbusDetect(true, true)` so USB enumerates without a
|
||||
`CLOCK_POWER` interrupt handler.
|
||||
- Green LED on P1.03 (RAK19007 Base LED1, active-HIGH) via
|
||||
`SimpleLed`.
|
||||
- Hardware-validated end-to-end: ADVERT round-trip with an external
|
||||
receiver on US915.
|
||||
- Added `debug-checkpoint` blink-map coverage at every TX-phase
|
||||
boundary in `radio.rs`, including a new checkpoint between
|
||||
`prepare_for_cad` and `cad()` so future bring-ups can distinguish
|
||||
a modem-setup hang from a DIO1-wait hang.
|
||||
|
||||
- **Adaptive listen-before-talk.** The CAD pre-TX retry loop scales to
|
||||
the active LoRa config instead of running a fixed 10 retries × 10 ms
|
||||
(which was roughly one max-length packet at SF7/BW500 and a no-op at
|
||||
SF12/BW125).
|
||||
- Per-retry sleep is sized to the time-on-air of an assumed-peer
|
||||
80-byte packet under the current SF/BW/CR — cached on
|
||||
`RadioContext`, recomputed on every `SET_CONFIG`.
|
||||
- Retry count = `clamp(floor(BUDGET/T)+1, 1, 10)` with `BUDGET = 2.5
|
||||
s`. Fast configs still get up to 10 retries; SF11/12 degrade to
|
||||
single-CAD-and-bail (logged at config-apply).
|
||||
- Sleeps jittered ±20% per iteration to decorrelate
|
||||
simultaneously-backing-off contenders.
|
||||
- ToA math, CR→{1..4} mapping, retry-count derivation, and jitter
|
||||
helper live in a pure `src/lbt.rs` module — host-testable via
|
||||
`DONGLORA_HOST_TEST=1 cargo test` (13 tests).
|
||||
- Validated on-device: 20 iterations of the AI Bot test flow that
|
||||
previously saw ~2 `ChannelBusy` errors now sees zero at the same
|
||||
SF/BW.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **First-TX-after-boot reliability on SX1262 boards.** Picked up the
|
||||
upstream `lora-phy` fix that moves the BUSY-handshake check from
|
||||
*after* to *before* each SPI op, per Semtech SX1261/2 §8.3.1, LR1121
|
||||
UM §3, and LR1110 DS §1.2.5. The existing post-op wait is preserved
|
||||
as the belt that closes the BUSY-assertion latency window
|
||||
(T_SW ≤ 600 ns) that a single level-poll can miss. Consumed via
|
||||
`[patch.crates-io]` against `swaits/lora-rs#fix/sx126x-busy-race`
|
||||
until the upstream PR merges.
|
||||
- Hardware-verified on Seeed Wio Tracker L1: 20/20 clean-boot TX
|
||||
(vs ~50% before).
|
||||
- Renames `InterfaceVariant::wait_on_busy` → `wait_until_ready` to
|
||||
match the new semantics.
|
||||
- **BUSY pin pull-down → none on nRF SX1262 boards.** SX1262's BUSY
|
||||
pin has an internal 20 kΩ pull-up (datasheet p. 53); our
|
||||
`Input::new(..., Pull::Down)` was contending with it, leaving BUSY
|
||||
at an indeterminate voltage and intermittently mis-reading the
|
||||
chip's ready state. Per `lora-rs/lora-rs#260`, switched to
|
||||
`Pull::None` on both Wio Tracker L1 and RAK4631. The RAK4631
|
||||
appeared to work before, but was affected by the same class of bug
|
||||
per the upstream issue — now deterministically correct rather than
|
||||
"happens to work".
|
||||
|
||||
### Changed
|
||||
|
||||
- **Build-system DX.** `just flash`, `just flash-probe`, and
|
||||
`just build` accept an optional comma-separated `extra_features`
|
||||
argument that is appended to the board feature, e.g. `just flash
|
||||
wio_tracker_l1 debug-checkpoint`. No more dropping to raw
|
||||
`cargo + rust-objcopy + uf2 cp` for iteration loops.
|
||||
- **`just` recipes now resolve all tools through `mise`** via
|
||||
`set shell := ["mise", "exec", "--", "sh", "-c"]`. `cargo`,
|
||||
`espflash`, `rust-objcopy`, `cargo-hex-to-uf2` etc. always pick up
|
||||
the versions pinned in `mise.toml` regardless of `PATH` ordering.
|
||||
- **`mise.toml` rust pins now carry ARM targets + clippy**, so a fresh
|
||||
`mise install` is sufficient bootstrapping — no manual
|
||||
`rustup target add` for `thumbv6m-none-eabi` /
|
||||
`thumbv7em-none-eabihf` and no separate clippy install.
|
||||
- **New `just flash-dfu` recipe** for the Adafruit nRF52 bootloader
|
||||
(VID:PID `239a:0029`) via `adafruit-nrfutil` under `uvx`, with port
|
||||
autodetect.
|
||||
- **Per-board `_info` chain in the justfile** collapsed from a long
|
||||
per-board `if/elif` ladder + per-board `:=` variables into a single
|
||||
`case` statement — one place per board, two lists (names +
|
||||
metadata).
|
||||
|
||||
## [1.3.1] - 2026-04-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- **LilyGo T-Beam (classic) red LED now flashes on TX/RX** instead of
|
||||
dimming off. The red LED under the OLED is wired active-LOW on this
|
||||
board (anode to V+, cathode to GPIO4); v1.3.0 drove it as active-HIGH,
|
||||
so the LED sat lit at idle and briefly dimmed during activity.
|
||||
- Empirically verified on hardware against both T-Beam variants.
|
||||
- Introduces `src/driver/inverted_pin.rs::InvertedPin<P>`, a generic
|
||||
`OutputPin` wrapper that swaps `set_high`/`set_low`. Polarity is
|
||||
expressed at the pin (where the physical wiring lives), so
|
||||
`SimpleLed` stays polarity-agnostic and every other board's code
|
||||
path is byte-for-byte unchanged.
|
||||
- Classic T-Beam now constructs its LED as
|
||||
`SimpleLed(InvertedPin(led_pin))` with pin init at `Level::High`
|
||||
(dark at boot).
|
||||
|
||||
## [1.3.0] - 2026-04-23
|
||||
|
||||
### Added
|
||||
|
||||
- LilyGo T-Beam S3 Supreme board support (ESP32-S3 + SX1262 + SH1106
|
||||
OLED + AXP2101 PMIC, native USB-C).
|
||||
- Pin map verified against MeshCore's
|
||||
`variants/lilygo_tbeam_supreme_SX1262`.
|
||||
- New `src/driver/axp.rs` module with an AXP2101 init sequence
|
||||
(ALDO3 = 3.3 V LoRa, ALDO1 = 3.3 V OLED). The PMIC gates LoRa and
|
||||
display power — without it the SX1262 never responds on SPI.
|
||||
- PMIC lives on a dedicated I²C1 bus (SDA=42, SCL=41), separate
|
||||
from the SH1106 on I²C0 (SDA=17, SCL=18). I²C1 is used blocking
|
||||
for a one-shot init and then dropped.
|
||||
- TCXO driven at 1.6 V via DIO3 (MeshCore uses the same).
|
||||
- LilyGo T-Beam (classic) board support (ESP32 + SX1276 + SSD1306
|
||||
OLED + AXP192 PMIC, CP2102 USB-UART bridge). This is the first
|
||||
board in the firmware on classic ESP32 (non-S3) and the first on
|
||||
the SX127x radio family.
|
||||
- Pin map verified against MeshCore's `variants/lilygo_tbeam_SX1276`.
|
||||
- New `src/hal/esp32.rs` (classic-ESP32 MCU primitives, peripheral
|
||||
DMA via `DMA_SPI2`) and `src/board/esp32.rs` (SX1276 radio helper
|
||||
+ SSD1306 display pipe).
|
||||
- AXP192 init added to `src/driver/axp.rs`: DCDC1 = 3.3 V for the
|
||||
OLED, LDO2 = 3.3 V for the SX1276 PA_BOOST rail. Shares the
|
||||
OLED I²C bus (SDA=21, SCL=22); the I²C instance is built in
|
||||
blocking mode for PMIC init and converted to async for the
|
||||
display task.
|
||||
- SX127x support via lora-phy 3's `Sx127x<Sx1276>` +
|
||||
`GenericSx127xInterfaceVariant` (DIO0 as the sole IRQ; no
|
||||
BUSY line on SX127x). XTAL clocked (no TCXO), `tx_boost = true`
|
||||
for the PA_BOOST path.
|
||||
- `LoRaBoard` trait gains `supported_sf_bitmap()` and
|
||||
`supported_bw_bitmap()` methods with SX126x defaults
|
||||
(`0x1FE0`, `0x03FF`). T-Beam classic overrides SF to `0x1FC0`
|
||||
(SF6–SF12) — the SX1276 has no SF5.
|
||||
- `radio_chip_id()` reports `0x0011` (`RadioChipId::Sx1276`) and
|
||||
`freq_range_hz()` widens to 137 MHz–1020 MHz per the SX1276
|
||||
datasheet.
|
||||
|
||||
### Changed
|
||||
|
||||
- `Cargo.toml`: the `esp32s3` chip feature has been moved out of
|
||||
the `esp-hal` / `esp-rtos` / `esp-backtrace` / `esp-println`
|
||||
dependency lines and into each ESP board's feature list (as
|
||||
`esp-hal/esp32s3`, etc.). This lets `lilygo_tbeam` select the
|
||||
classic `esp32` chip flavour from the same dependency set.
|
||||
|
||||
## [1.2.1] - 2026-04-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Radio no longer drops to Idle mid-RX-session on transient failures.**
|
||||
Extends the v1.1.0 invariant ("`SET_CONFIG` during RX auto-re-arms")
|
||||
to all failure paths that can occur while a client has RX active:
|
||||
- `Receiving.on(RxFailed) => Idle` now transitions to `ResumingRx`
|
||||
instead of `Idle`. A transient `next_packet` error re-arms the chip
|
||||
rather than ending the RX session.
|
||||
- `TransmittingFromRx.on(TransmitFailed) => Idle` now transitions to
|
||||
`ResumingRx`. TX failure during an active RX session returns
|
||||
`TX_FAILED` to the caller and resumes RX, matching the shape of the
|
||||
adjacent `ChannelBusy` handler.
|
||||
- `ResumingRx.on(StartRxFailed) => Idle` is now a bounded retry
|
||||
(5 × 25 ms backoff via a new `bump_resume_retries` action and a
|
||||
`ResumeRxRetry` / `ResumeRxExhausted` pair of internal events).
|
||||
The chip only lands in `Idle` after exhausting the retry budget —
|
||||
which means it's genuinely wedged and needs reconfiguring.
|
||||
|
||||
Visible effect: the OLED dashboard no longer bounces to Splash when
|
||||
`ai-bot` (or any other client that TXes while RX is running) sends a
|
||||
packet. Previously any of the three transitions above would fire
|
||||
`RadioEvent::Idle`, which the display uses as its `Dashboard → Splash`
|
||||
trigger; the radio would land in `Idle` with no client aware enough
|
||||
to re-send `RX_START`, stranding the dashboard until the session was
|
||||
manually torn down and restarted.
|
||||
|
||||
- Removed `announce_rx_failed` helper action: its only purpose was
|
||||
emitting `RadioEvent::Idle` on the now-deleted `Receiving.on(RxFailed)
|
||||
=> Idle` edge. No replacement needed — `ResumingRx` handles recovery
|
||||
silently.
|
||||
|
||||
## [1.2.0] - 2026-04-22
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+237
-11
@@ -2,6 +2,17 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
"cpubits",
|
||||
"cpufeatures 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
@@ -127,6 +138,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byte-slice-cast"
|
||||
version = "1.2.3"
|
||||
@@ -172,6 +192,33 @@ dependencies = [
|
||||
"pure-rust-locales",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea"
|
||||
dependencies = [
|
||||
"crypto-common 0.2.1",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmac"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac78aa94ce13e432b332a4d1bf2eff167d3a2520188ee05b337180a42fd2e62e"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
"dbl",
|
||||
"digest 0.11.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
@@ -225,6 +272,12 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpubits"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -234,6 +287,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-any"
|
||||
version = "2.5.0"
|
||||
@@ -265,6 +327,24 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
@@ -368,6 +448,15 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbl"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d7a944e61df464668c5f51f56cc667396a8821434273112948ea0b66e405d7"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debug-helper"
|
||||
version = "0.3.13"
|
||||
@@ -442,8 +531,19 @@ version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"block-buffer 0.10.4",
|
||||
"crypto-common 0.1.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.0",
|
||||
"crypto-common 0.2.1",
|
||||
"ctutils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -486,7 +586,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donglora"
|
||||
version = "1.2.0"
|
||||
version = "1.4.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cortex-m",
|
||||
@@ -1022,7 +1122,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"critical-section",
|
||||
"delegate",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"document-features",
|
||||
"embassy-embedded-hal",
|
||||
"embassy-futures",
|
||||
@@ -1060,6 +1160,8 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"rand_core 0.9.5",
|
||||
"riscv",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"strum",
|
||||
"ufmt-write",
|
||||
"xtensa-lx",
|
||||
@@ -1116,6 +1218,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"document-features",
|
||||
"esp-metadata-generated",
|
||||
"esp32",
|
||||
"esp32s3",
|
||||
]
|
||||
|
||||
@@ -1232,6 +1335,12 @@ dependencies = [
|
||||
"vcell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -1280,12 +1389,42 @@ dependencies = [
|
||||
"gcd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
@@ -1305,6 +1444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
]
|
||||
@@ -1402,6 +1542,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hsmc"
|
||||
version = "0.2.0"
|
||||
@@ -1426,6 +1572,15 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -1451,6 +1606,15 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.12"
|
||||
@@ -1485,7 +1649,7 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1569,22 +1733,62 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lora-modulation"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c96c4a41a1f1ad04de765306e1829c7007a37898fe4033204fa82cf2e055624"
|
||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
||||
dependencies = [
|
||||
"defmt 0.3.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lora-phy"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61471c3b2909789e3332083577f6cf6c41a4fcf37674ef15156bcbb20504ac65"
|
||||
version = "3.0.2-alpha"
|
||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
||||
dependencies = [
|
||||
"defmt 0.3.100",
|
||||
"document-features",
|
||||
"embedded-hal 1.0.0",
|
||||
"embedded-hal-async",
|
||||
"lora-modulation",
|
||||
"lorawan-device",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lorawan"
|
||||
version = "0.9.1"
|
||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cmac",
|
||||
"defmt 0.3.100",
|
||||
"hex",
|
||||
"lorawan-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lorawan-device"
|
||||
version = "0.12.2"
|
||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
||||
dependencies = [
|
||||
"defmt 0.3.100",
|
||||
"document-features",
|
||||
"fastrand",
|
||||
"futures",
|
||||
"heapless 0.9.2",
|
||||
"lora-modulation",
|
||||
"lorawan",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lorawan-macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -2204,6 +2408,28 @@ dependencies = [
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2-const-stable"
|
||||
version = "0.1.0"
|
||||
@@ -2216,7 +2442,7 @@ version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"keccak",
|
||||
]
|
||||
|
||||
|
||||
+46
-7
@@ -3,7 +3,7 @@ members = ["."]
|
||||
|
||||
[package]
|
||||
name = "donglora"
|
||||
version = "1.2.0"
|
||||
version = "1.4.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
@@ -32,7 +32,10 @@ ucobs = { version = "0.3", features = ["legacy-msrv"] }
|
||||
heapless = "0.9"
|
||||
donglora-protocol = "1"
|
||||
hsmc = { version = "0.2", default-features = false, features = ["embassy"] }
|
||||
lora-phy = "3.0"
|
||||
# Pulled from swaits/lora-rs fork until lora-rs/lora-rs#428 (pre-command
|
||||
# BUSY check) is merged + released on crates.io. Switch back to a normal
|
||||
# crates.io version then.
|
||||
lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fix/sx126x-busy-race", features = ["defmt-03"] }
|
||||
portable-atomic = { version = "1", optional = true, default-features = false, features = ["critical-section"] }
|
||||
static_cell = "2.1"
|
||||
|
||||
@@ -45,22 +48,20 @@ ssd1306 = { version = "0.10", features = ["async"] }
|
||||
embedded-io-async = "0.7"
|
||||
embassy-futures = "0.1"
|
||||
|
||||
# ESP32-S3 (Heltec V3) — requires esp toolchain (espup)
|
||||
# Xtensa ESP crates — requires esp toolchain (espup). The chip feature
|
||||
# (esp32 / esp32s3) is selected per board via the board-level feature flag
|
||||
# below, so the same dep lines support both classic ESP32 and ESP32-S3.
|
||||
esp-backtrace = { version = "0.18", optional = true, features = [
|
||||
"esp32s3",
|
||||
"panic-handler",
|
||||
"defmt",
|
||||
] }
|
||||
esp-hal = { version = "1.0", optional = true, features = [
|
||||
"esp32s3",
|
||||
"unstable",
|
||||
] }
|
||||
esp-println = { version = "0.16", optional = true, features = [
|
||||
"esp32s3",
|
||||
"defmt-espflash",
|
||||
] }
|
||||
esp-rtos = { version = "0.2", optional = true, features = [
|
||||
"esp32s3",
|
||||
"embassy",
|
||||
] }
|
||||
|
||||
@@ -110,6 +111,10 @@ heltec_v3 = [
|
||||
"dep:esp-hal",
|
||||
"dep:esp-println",
|
||||
"dep:esp-rtos",
|
||||
"esp-backtrace/esp32s3",
|
||||
"esp-hal/esp32s3",
|
||||
"esp-println/esp32s3",
|
||||
"esp-rtos/esp32s3",
|
||||
]
|
||||
heltec_v3_uart = [
|
||||
"embassy",
|
||||
@@ -117,6 +122,10 @@ heltec_v3_uart = [
|
||||
"dep:esp-hal",
|
||||
"dep:esp-println",
|
||||
"dep:esp-rtos",
|
||||
"esp-backtrace/esp32s3",
|
||||
"esp-hal/esp32s3",
|
||||
"esp-println/esp32s3",
|
||||
"esp-rtos/esp32s3",
|
||||
]
|
||||
heltec_v4 = [
|
||||
"embassy",
|
||||
@@ -124,6 +133,10 @@ heltec_v4 = [
|
||||
"dep:esp-hal",
|
||||
"dep:esp-println",
|
||||
"dep:esp-rtos",
|
||||
"esp-backtrace/esp32s3",
|
||||
"esp-hal/esp32s3",
|
||||
"esp-println/esp32s3",
|
||||
"esp-rtos/esp32s3",
|
||||
]
|
||||
elecrow_thinknode_m2 = [
|
||||
"embassy",
|
||||
@@ -131,6 +144,32 @@ elecrow_thinknode_m2 = [
|
||||
"dep:esp-hal",
|
||||
"dep:esp-println",
|
||||
"dep:esp-rtos",
|
||||
"esp-backtrace/esp32s3",
|
||||
"esp-hal/esp32s3",
|
||||
"esp-println/esp32s3",
|
||||
"esp-rtos/esp32s3",
|
||||
]
|
||||
lilygo_tbeam_supreme = [
|
||||
"embassy",
|
||||
"dep:esp-backtrace",
|
||||
"dep:esp-hal",
|
||||
"dep:esp-println",
|
||||
"dep:esp-rtos",
|
||||
"esp-backtrace/esp32s3",
|
||||
"esp-hal/esp32s3",
|
||||
"esp-println/esp32s3",
|
||||
"esp-rtos/esp32s3",
|
||||
]
|
||||
lilygo_tbeam = [
|
||||
"embassy",
|
||||
"dep:esp-backtrace",
|
||||
"dep:esp-hal",
|
||||
"dep:esp-println",
|
||||
"dep:esp-rtos",
|
||||
"esp-backtrace/esp32",
|
||||
"esp-hal/esp32",
|
||||
"esp-println/esp32",
|
||||
"esp-rtos/esp32",
|
||||
]
|
||||
rak_wisblock_4631 = [
|
||||
"embassy",
|
||||
|
||||
@@ -24,6 +24,8 @@ All commands are run from the `firmware/` directory.
|
||||
| Heltec V3 (UART) | `heltec_v3_uart` | ESP32-S3 | SX1262 | CP2102 bridge | xtensa-esp32s3-none-elf |
|
||||
| Heltec V4 | `heltec_v4` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf |
|
||||
| ELECROW ThinkNode-M2 | `elecrow_thinknode_m2` | ESP32-S3 | SX1262 | CP2102 bridge | xtensa-esp32s3-none-elf |
|
||||
| LilyGo T-Beam | `lilygo_tbeam` | ESP32 | SX1276 | CP2102 bridge | xtensa-esp32-none-elf |
|
||||
| LilyGo T-Beam S3 Supreme | `lilygo_tbeam_supreme` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf |
|
||||
| RAK WisBlock 4631 | `rak_wisblock_4631` | nRF52840 | SX1262 | Native CDC-ACM | thumbv7em-none-eabihf |
|
||||
| Waveshare RP2040-LoRa | `waveshare_rp2040_lora` | RP2040 | SX1262 | Native CDC-ACM | thumbv6m-none-eabi |
|
||||
| Wio Tracker L1 | `wio_tracker_l1` | nRF52840 | SX1262 | Native CDC-ACM | thumbv7em-none-eabihf |
|
||||
|
||||
@@ -1,4 +1,165 @@
|
||||
- LED blinking on TX/RX? colored?
|
||||
- publish new client, mux, bridge (updates in client for UART crap)
|
||||
- split out mux and client repos
|
||||
- move to GH organization
|
||||
# Firmware TODO List
|
||||
|
||||
## `lora-phy` fix: drop fork pin once PR #428 merges + releases
|
||||
|
||||
Upstream PR: [lora-rs/lora-rs#428 — *phy: add pre-command BUSY checks
|
||||
per Semtech datasheets*](https://github.com/lora-rs/lora-rs/pull/428)
|
||||
(open).
|
||||
Fork branch we ship from:
|
||||
[`swaits/lora-rs @ fix/sx126x-busy-race`](https://github.com/swaits/lora-rs/tree/fix/sx126x-busy-race).
|
||||
|
||||
Firmware consumes the fix via a direct `git` dependency in
|
||||
`Cargo.toml`:
|
||||
|
||||
```toml
|
||||
lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fix/sx126x-busy-race", features = ["defmt-03"] }
|
||||
```
|
||||
|
||||
When PR #428 merges and a `lora-phy` release ships on crates.io that
|
||||
contains it, switch this line back to a normal `lora-phy = "X.Y"`
|
||||
crates.io version and drop the explanatory comment block above it.
|
||||
|
||||
Hardware-verified on Wio Tracker L1: 20/20 clean-boot TX after the fix
|
||||
(vs ~50% before). The fix adds a pre-command BUSY check before each
|
||||
`SpiInterface` read/write op, keeping the existing post-command wait
|
||||
in place — belt-and-suspenders bracketing that closes the
|
||||
BUSY-assertion latency window (T_SW ≤ 600 ns) a single level-poll can
|
||||
miss.
|
||||
|
||||
---
|
||||
|
||||
## Drop `[patch.crates-io]` + upgrade esp-hal / esp-rtos + add `esp_app_desc!()` + bump espflash to v4
|
||||
|
||||
Single coordinated change that clears our last remaining dep-graph debt
|
||||
on the ESP boards. All four pieces are coupled, so they land together.
|
||||
|
||||
### Why
|
||||
|
||||
Today `Cargo.toml` overrides `esp-hal`, `esp-rtos`, `esp-backtrace`, and
|
||||
`esp-println` with a git pin to `esp-rs/esp-hal#main`. The note in that
|
||||
block explains the historical reason:
|
||||
|
||||
> REVISIT: esp-rtos 0.2.0 on crates.io pins embassy-executor ^0.9. The
|
||||
> esp-rs/esp-hal main branch already supports 0.10. Remove these
|
||||
> patches once esp-rtos publishes a release with embassy-executor 0.10
|
||||
> support.
|
||||
|
||||
**That condition is now met.** `esp-rtos 0.3.0` is published on crates.io
|
||||
and depends on `embassy-executor ^0.10` and `esp-hal ~1.1.0-rc.0` — exactly
|
||||
what our patches were pulling unofficially. Keeping the patches is no
|
||||
longer free: it blocks `espflash` v4, blocks `esp-bootloader-esp-idf`, and
|
||||
ties reproducible builds to whatever commit happens to be on
|
||||
`esp-rs/esp-hal#main`.
|
||||
|
||||
The immediate trigger is `espflash` v4's required
|
||||
`esp_bootloader_esp_idf::esp_app_desc!()` check. `save-image` refuses any
|
||||
ESP binary that lacks the IDF app descriptor. The crate that provides the
|
||||
macro (`esp-bootloader-esp-idf`) uses the same `esp-rom-sys` native
|
||||
library as `esp-hal` — two copies of the same `links = "esp_rom_sys"` is
|
||||
a hard cargo error, so `esp-bootloader-esp-idf` must come from the _same
|
||||
source_ as `esp-hal`. Easiest way to guarantee that: both from crates.io,
|
||||
no patches.
|
||||
|
||||
### What changes
|
||||
|
||||
1. **`Cargo.toml`** — delete the entire `[patch.crates-io]` block and the
|
||||
preceding `REVISIT:` comment. Bump the four esp-rs deps to published
|
||||
versions:
|
||||
- `esp-hal = "1.0"` → `"1.1.0-rc.0"` (pre-release selector — see Gotchas)
|
||||
- `esp-rtos = "0.2"` → `"0.3"`
|
||||
- `esp-backtrace = "0.18"` → whatever matches esp-rtos 0.3 (check its deps)
|
||||
- `esp-println = "0.16"` → same
|
||||
Add a new optional dep + feature wiring for
|
||||
`esp-bootloader-esp-idf = "0.5"` with `defmt` feature, plus per-board
|
||||
chip feature (`esp-bootloader-esp-idf/esp32s3` for the S3 boards,
|
||||
`esp-bootloader-esp-idf/esp32` for LilyGo T-Beam classic).
|
||||
|
||||
2. **`src/main.rs`** — inside the ESP-family branch of the existing
|
||||
`cfg_if!` block (around line 51), invoke the macro at module scope:
|
||||
|
||||
```rust
|
||||
esp_bootloader_esp_idf::esp_app_desc!();
|
||||
```
|
||||
|
||||
Zero runtime cost. Emits ~256 bytes in `.rodata_desc` with app
|
||||
metadata (name, version, build date) that the ESP-IDF 2nd-stage
|
||||
bootloader + `espflash save-image` look for.
|
||||
|
||||
3. **`mise.toml`** — `"cargo:espflash" = "3"` → `"4"`. Drop the deferral
|
||||
comment on that line.
|
||||
|
||||
4. **Possible API drift fixes** — esp-hal's pre-release versions
|
||||
occasionally rename or move items. Most likely touch points:
|
||||
- `src/board/esp32.rs`, `src/board/esp32s3.rs`
|
||||
- Any board file in `src/board/heltec_*.rs`, `lilygo_*.rs`,
|
||||
`elecrow_*.rs` that calls into esp-hal / esp-rtos init
|
||||
Run `just check-all` after steps 1–3 and patch up whatever surfaces.
|
||||
Don't guess — read the actual compiler errors.
|
||||
|
||||
### How (step by step)
|
||||
|
||||
1. `jj new -m "chore(esp): drop esp-hal patches, bump to published versions, add esp_app_desc!()"`
|
||||
2. Edit `Cargo.toml` — delete `[patch.crates-io]` + `REVISIT:` comment;
|
||||
bump the four versions; add `esp-bootloader-esp-idf` dep + feature
|
||||
wiring for each ESP board feature block.
|
||||
3. Edit `src/main.rs` — add `esp_bootloader_esp_idf::esp_app_desc!();`
|
||||
to the ESP branch of the top-level `cfg_if!`.
|
||||
4. Edit `mise.toml` — espflash `"3"` → `"4"`.
|
||||
5. `mise install` to pull espflash 4.x.
|
||||
6. `cargo update` to let the resolver pick a consistent tree with the
|
||||
new requirements.
|
||||
7. `just check-all` — expect some compilation errors from API drift on
|
||||
ESP boards. Fix them one at a time; consult the official examples
|
||||
in `esp-rs/esp-hal` if in doubt.
|
||||
8. `just build heltec_v3` (or any ESP board you have) — verify
|
||||
`espflash save-image` now succeeds end-to-end producing a valid
|
||||
`.bin`.
|
||||
9. Flash to real hardware if available. **The RAK4631 and Wio Tracker
|
||||
L1 (nRF52) boards should be untouched.**
|
||||
|
||||
### Verification
|
||||
|
||||
- `just check-all` clean on all 9 boards.
|
||||
- `just test` (host-side protocol tests) still green.
|
||||
- For each ESP board that has been physically validated before
|
||||
(Heltec V3, LilyGo T-Beam units): flash + confirm radio TX works
|
||||
end-to-end via ai-bot or a known peer.
|
||||
- RAK4631 + Wio Tracker L1 unchanged; re-check via `just check` on
|
||||
both, but no re-flash needed (they don't go through the ESP code
|
||||
path).
|
||||
|
||||
### Gotchas
|
||||
|
||||
- **Pre-release version selector**: `esp-hal = "1.1.0-rc.0"` by itself
|
||||
does NOT match pre-release versions in cargo's default semver rules.
|
||||
Use an exact pre-release like `"1.1.0-rc.0"`, `"~1.1.0-rc.0"` (only
|
||||
`1.1.0-rc.0`, `1.1.0-rc.1`, … with any patch), or `">=1.1.0-rc.0"`.
|
||||
`"1.1"` will NOT resolve to a pre-release.
|
||||
- **Two copies of `esp-rom-sys`**: the root cause of the first failed
|
||||
attempt at this task. If cargo errors with _"Only one package in the
|
||||
dependency graph may specify the same links value"_ for
|
||||
`esp_rom_sys`, it means `esp-hal` and `esp-bootloader-esp-idf` are
|
||||
being pulled from different sources (one crates.io, one git). Either
|
||||
both from crates.io (this task's plan) or both from the same git
|
||||
commit.
|
||||
- **`esp-bootloader-esp-idf` chip features**: the crate needs exactly
|
||||
one of `esp32 / esp32s3 / esp32c3 / esp32c6 / …` to be enabled.
|
||||
Without it, the build panics at its `build.rs`: _"Expected exactly
|
||||
one of the following features to be enabled: esp32, esp32c2, ..."_.
|
||||
Wire per-board the same way we wire `esp-hal/esp32s3` etc.
|
||||
- **Probe-less repro**: `mise install` after bumping espflash to "4"
|
||||
only installs mise's copy. If the user's global
|
||||
`~/.cargo/bin/espflash` is stale/different, it still wins on PATH.
|
||||
The justfile `set shell := ["mise", "exec", "--", "sh", "-c"]`
|
||||
prepends mise's paths but does NOT reorder `~/.cargo/bin` ahead of
|
||||
them. Either `cargo uninstall espflash` globally, or change the
|
||||
justfile recipes to call `$(mise which espflash)` explicitly.
|
||||
|
||||
### Why not now
|
||||
|
||||
Tried it alongside the RAK4631 bring-up in April 2026 and bounced off
|
||||
the `esp-rom-sys` two-copies problem. Backed out the attempt and left
|
||||
espflash pinned at v3 + the patches in place so the bring-up commit
|
||||
stayed atomic and shippable. This task is what's needed to unblock v4
|
||||
properly, but it deserves its own focused session — not as
|
||||
"while-I'm-here" scope on a board bring-up.
|
||||
@@ -1,41 +1,29 @@
|
||||
set shell := ["bash", "-c"]
|
||||
|
||||
# Board definitions: feature target chip
|
||||
# Xtensa targets use the self-contained esp toolchain (espup-installed) with
|
||||
# `-Zbuild-std=core,alloc`. Using `+esp` rather than `+nightly` keeps the
|
||||
# compiler and rust-src versions aligned — the public nightly drifts ahead of
|
||||
# the esp fork and breaks core-stdlib compilation otherwise.
|
||||
# Tool versions are pinned in mise.toml; run `just setup` to install everything.
|
||||
heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3"
|
||||
heltec_v3_uart := "heltec_v3_uart xtensa-esp32s3-none-elf esp32s3"
|
||||
heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3"
|
||||
elecrow_thinknode_m2 := "elecrow_thinknode_m2 xtensa-esp32s3-none-elf esp32s3"
|
||||
rak_wisblock_4631 := "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA"
|
||||
wio_tracker_l1 := "wio_tracker_l1 thumbv7em-none-eabihf nRF52840_xxAA"
|
||||
waveshare_rp2040_lora := "waveshare_rp2040_lora thumbv6m-none-eabi rp2040"
|
||||
# Run every recipe with mise's tool versions active — so `just` invocations
|
||||
# of cargo, espflash, rust-objcopy, etc. always resolve to the pinned
|
||||
# versions in mise.toml, even if the user has a different version elsewhere
|
||||
# on PATH (e.g. a stray `cargo install espflash`).
|
||||
set shell := ["mise", "exec", "--", "sh", "-c"]
|
||||
|
||||
builds_dir := "builds"
|
||||
version := `sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml`
|
||||
|
||||
# All known boards
|
||||
boards := "heltec_v3 heltec_v3_uart heltec_v4 elecrow_thinknode_m2 rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1"
|
||||
# All known boards. ADD A NEW BOARD HERE and in the `_info` case below.
|
||||
#
|
||||
# Board metadata ("feature target chip") lives in the `_info` recipe —
|
||||
# Xtensa targets are built with `+esp` + `-Zbuild-std=core,alloc` (the
|
||||
# self-contained esp toolchain ships its own rust-src; the public
|
||||
# nightly drifts ahead and breaks core-stdlib compilation).
|
||||
boards := "heltec_v3 heltec_v3_uart heltec_v4 elecrow_thinknode_m2 lilygo_tbeam lilygo_tbeam_supreme rak_wisblock_4631 waveshare_rp2040_lora wio_tracker_l1"
|
||||
|
||||
# Install all required tools and toolchains
|
||||
setup:
|
||||
mise trust --yes
|
||||
mise install
|
||||
@# ESP Xtensa toolchain (not managed by mise)
|
||||
@# ESP Xtensa toolchain (not a rustup component — installed out-of-band by espup)
|
||||
@if ! rustup toolchain list | grep -q "^esp"; then \
|
||||
echo "ESP toolchain not found, installing via espup (this may take a while)..."; \
|
||||
espup install || { echo "error: espup install failed" >&2; exit 1; }; \
|
||||
fi
|
||||
@# nightly rust-src for -Zbuild-std
|
||||
@if ! rustup component list --toolchain nightly --installed 2>/dev/null | grep -q "^rust-src"; then \
|
||||
rustup component add rust-src --toolchain nightly; \
|
||||
fi
|
||||
@# ARM targets
|
||||
@rustup target list --installed | grep -q "^thumbv6m-none-eabi$" || rustup target add thumbv6m-none-eabi
|
||||
@rustup target list --installed | grep -q "^thumbv7em-none-eabihf$" || rustup target add thumbv7em-none-eabihf
|
||||
|
||||
# Build release firmware for all boards, installing toolchains as needed
|
||||
build-all:
|
||||
@@ -65,16 +53,20 @@ check-all:
|
||||
check board:
|
||||
@just _cargo {{board}} "clippy --release" "-- -D warnings"
|
||||
|
||||
# Build release firmware and copy to builds/ with a readable name
|
||||
build board profile="release":
|
||||
@just _cargo {{board}} "build --{{profile}}"
|
||||
# Build release firmware and copy to builds/ with a readable name.
|
||||
# `extra_features` (comma-separated) is appended to the board feature,
|
||||
# e.g. `just build wio_tracker_l1 release debug-checkpoint`.
|
||||
build board profile="release" extra_features="":
|
||||
@just _cargo {{board}} "build --{{profile}}" "" "{{extra_features}}"
|
||||
@just _copy_firmware {{board}} {{profile}}
|
||||
|
||||
# Build and flash a board (espflash for Xtensa, UF2 DFU for ARM)
|
||||
flash board:
|
||||
# Build and flash a board (espflash for Xtensa, UF2 DFU for ARM).
|
||||
# `extra_features` is passed through to the build, e.g.
|
||||
# `just flash wio_tracker_l1 debug-checkpoint`.
|
||||
flash board extra_features="":
|
||||
@just _ensure_tools
|
||||
@just build {{board}} release
|
||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
||||
@just build {{board}} release "{{extra_features}}"
|
||||
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
||||
case "$target" in \
|
||||
xtensa-*) \
|
||||
port=$(just _find_port "303a:1001" "1209:5741" "10c4:ea60" "1a86:7522" "1a86:7523"); \
|
||||
@@ -88,11 +80,12 @@ flash board:
|
||||
*) just _flash_uf2 {{board}} ;; \
|
||||
esac
|
||||
|
||||
# Flash ARM board via debug probe (requires J-Link or similar)
|
||||
flash-probe board:
|
||||
# Flash ARM board via debug probe (requires J-Link or similar).
|
||||
# `extra_features` is passed through to the build.
|
||||
flash-probe board extra_features="":
|
||||
@just _ensure_tools
|
||||
@just build {{board}} release
|
||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
||||
@just build {{board}} release "{{extra_features}}"
|
||||
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
||||
probe-rs run --chip $chip "{{builds_dir}}/donglora-{{board}}-v{{version}}.elf"
|
||||
|
||||
# Show binary size for a release build
|
||||
@@ -114,12 +107,14 @@ outdated:
|
||||
_ensure_tools:
|
||||
@mise trust --yes . 2>/dev/null; mise install --quiet
|
||||
|
||||
# Run a cargo command for a board.
|
||||
# Run a cargo command for a board, optionally with extra Cargo features
|
||||
# (comma-separated, e.g. "debug-checkpoint").
|
||||
# Xtensa: esp toolchain (cargo + rustc + rust-src together) + -Zbuild-std=core
|
||||
[private]
|
||||
_cargo board cmd extra_args="":
|
||||
_cargo board cmd extra_args="" extra_features="":
|
||||
@just _ensure_tools
|
||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
||||
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
||||
[ -n "{{extra_features}}" ] && feat="$feat,{{extra_features}}"; \
|
||||
extra=""; buildstd=""; \
|
||||
case "$target" in xtensa-*) \
|
||||
just _require_esp_toolchain; \
|
||||
@@ -131,7 +126,7 @@ _cargo board cmd extra_args="":
|
||||
# Ensure a board's toolchain is available, auto-installing if needed
|
||||
[private]
|
||||
_can_build board:
|
||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
||||
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
||||
case "$target" in \
|
||||
xtensa-*) just _require_esp_toolchain ;; \
|
||||
*) rustup target list --installed | grep -q "^$target$" || rustup target add "$target" ;; \
|
||||
@@ -167,25 +162,25 @@ _find_port +vid_pids:
|
||||
[private]
|
||||
_copy_firmware board profile:
|
||||
@mkdir -p {{builds_dir}}
|
||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
||||
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
||||
src="target/$target/{{profile}}/donglora"; \
|
||||
dst="{{builds_dir}}/donglora-{{board}}-v{{version}}"; \
|
||||
if [ -f "$src" ]; then \
|
||||
cp "$src" "$dst.elf"; echo "→ $dst.elf"; \
|
||||
case "$target" in \
|
||||
xtensa-*) \
|
||||
mise exec -- espflash save-image --chip "$chip" --merge --skip-padding "$src" "$dst.bin"; \
|
||||
espflash save-image --chip "$chip" --merge --skip-padding "$src" "$dst.bin"; \
|
||||
echo "→ $dst.bin"; \
|
||||
;; \
|
||||
thumbv6m-*) \
|
||||
mise exec -- rust-objcopy -O ihex "$src" "$dst.hex"; \
|
||||
mise exec -- cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family rp2040; \
|
||||
rust-objcopy -O ihex "$src" "$dst.hex"; \
|
||||
cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family rp2040; \
|
||||
rm "$dst.hex"; \
|
||||
echo "→ $dst.uf2"; \
|
||||
;; \
|
||||
thumbv7em-*) \
|
||||
mise exec -- rust-objcopy -O ihex "$src" "$dst.hex"; \
|
||||
mise exec -- cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family nrf52840; \
|
||||
rust-objcopy -O ihex "$src" "$dst.hex"; \
|
||||
cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family nrf52840; \
|
||||
rm "$dst.hex"; \
|
||||
echo "→ $dst.uf2"; \
|
||||
;; \
|
||||
@@ -208,13 +203,19 @@ _flash_uf2 board:
|
||||
sync; \
|
||||
echo "Done — board will reboot automatically"
|
||||
|
||||
# Single source of truth for per-board metadata: "feature target chip".
|
||||
# ADD A NEW BOARD HERE and in the `boards` list near the top of this file.
|
||||
[private]
|
||||
_info name:
|
||||
@if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \
|
||||
elif [ "{{name}}" == "heltec_v3_uart" ]; then echo "{{heltec_v3_uart}}"; \
|
||||
elif [ "{{name}}" == "heltec_v4" ]; then echo "{{heltec_v4}}"; \
|
||||
elif [ "{{name}}" == "elecrow_thinknode_m2" ]; then echo "{{elecrow_thinknode_m2}}"; \
|
||||
elif [ "{{name}}" == "rak_wisblock_4631" ]; then echo "{{rak_wisblock_4631}}"; \
|
||||
elif [ "{{name}}" == "waveshare_rp2040_lora" ]; then echo "{{waveshare_rp2040_lora}}"; \
|
||||
elif [ "{{name}}" == "wio_tracker_l1" ]; then echo "{{wio_tracker_l1}}"; \
|
||||
else echo "Unknown board: {{name}}" >&2; exit 1; fi
|
||||
@case "{{name}}" in \
|
||||
heltec_v3) echo "heltec_v3 xtensa-esp32s3-none-elf esp32s3" ;; \
|
||||
heltec_v3_uart) echo "heltec_v3_uart xtensa-esp32s3-none-elf esp32s3" ;; \
|
||||
heltec_v4) echo "heltec_v4 xtensa-esp32s3-none-elf esp32s3" ;; \
|
||||
elecrow_thinknode_m2) echo "elecrow_thinknode_m2 xtensa-esp32s3-none-elf esp32s3" ;; \
|
||||
lilygo_tbeam) echo "lilygo_tbeam xtensa-esp32-none-elf esp32" ;; \
|
||||
lilygo_tbeam_supreme) echo "lilygo_tbeam_supreme xtensa-esp32s3-none-elf esp32s3" ;; \
|
||||
rak_wisblock_4631) echo "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA" ;; \
|
||||
waveshare_rp2040_lora) echo "waveshare_rp2040_lora thumbv6m-none-eabi rp2040" ;; \
|
||||
wio_tracker_l1) echo "wio_tracker_l1 thumbv7em-none-eabihf nRF52840_xxAA" ;; \
|
||||
*) echo "Unknown board: {{name}}" >&2; exit 1 ;; \
|
||||
esac
|
||||
@@ -1,9 +1,24 @@
|
||||
[tools]
|
||||
rust = "1.94"
|
||||
just = "latest"
|
||||
|
||||
"cargo:cargo-binutils" = "latest" # `cargo size`, `cargo objdump`, etc.
|
||||
"cargo:cargo-hex-to-uf2" = "latest" # ELF → UF2 via Intel HEX for nRF52840 DFU
|
||||
"cargo:espup" = "0.16"
|
||||
"cargo:espflash" = "3" # 4.x has broken nix dependency on current stable Rust
|
||||
"cargo:probe-rs-tools" = "0.31"
|
||||
# Cargo-installed tooling via mise's `cargo:` backend.
|
||||
"cargo:cargo-binutils" = "latest" # rust-objcopy, rust-size
|
||||
"cargo:cargo-hex-to-uf2" = "latest" # ELF → UF2 for nRF52 / RP2040 DFU drives
|
||||
"cargo:espup" = "latest" # bootstraps the ESP Xtensa toolchain
|
||||
"cargo:espflash" = "3" # flash + save-image for ESP targets (4.x requires esp_app_desc!() in app — revisit alongside esp-hal 1.1+ / drop-the-patches cleanup)
|
||||
"cargo:probe-rs-tools" = "latest" # debug-probe flash/run for ARM targets
|
||||
|
||||
# Stable Rust + cross-compile targets. Targets land under $RUSTUP_HOME for
|
||||
# the pinned toolchain, so they're always available under `mise exec` — no
|
||||
# extra `rustup target add` step at setup time.
|
||||
#
|
||||
# Xtensa ESP targets are NOT listed here — they use the self-contained `esp`
|
||||
# toolchain installed out-of-band by `espup` (see `just setup`). That
|
||||
# toolchain ships its own rust-src for `-Zbuild-std`.
|
||||
#
|
||||
# profile = "minimal" (just rustc, rust-std, cargo), "default" (adds rust-docs, rustfmt, clippy)
|
||||
#
|
||||
[tools.rust]
|
||||
version = "1.94"
|
||||
profile = "default"
|
||||
targets = "thumbv6m-none-eabi,thumbv7em-none-eabihf"
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Shared types and peripheral wiring for classic-ESP32 SX1276 boards.
|
||||
//!
|
||||
//! Counterpart to `board::esp32s3` for the SX126x line. Builds an
|
||||
//! `Sx127x<Sx1276>` driver from an already-initialized SPI bus plus the
|
||||
//! board's reset and DIO0 pins. SX127x has no `BUSY` line — DIO0 carries
|
||||
//! every IRQ (TX done, RX done, header valid, …) and lora-phy's
|
||||
//! `GenericSx127xInterfaceVariant` waits on it directly.
|
||||
|
||||
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use embassy_time::Delay;
|
||||
use esp_hal::gpio::{Input, InputConfig, InputPin, Level, Output, OutputConfig, OutputPin, Pull};
|
||||
|
||||
use lora_phy::iv::GenericSx127xInterfaceVariant;
|
||||
use lora_phy::sx127x::{self, Sx127x};
|
||||
|
||||
use crate::hal::esp32 as mcu;
|
||||
|
||||
// ── Re-export MCU types ─────────────────────────────────────────────
|
||||
|
||||
pub use mcu::{I2cBus as DisplayI2c, SpiBus};
|
||||
|
||||
// ── Concrete peripheral types ───────────────────────────────────────
|
||||
|
||||
type Nss = Output<'static>;
|
||||
type Iv = GenericSx127xInterfaceVariant<Output<'static>, Input<'static>>;
|
||||
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
||||
pub type RadioDriver = Sx127x<RadioSpiDevice, Iv, sx127x::Sx1276>;
|
||||
|
||||
// ── Peripheral bundles ──────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
pub driver: RadioDriver,
|
||||
pub delay: Delay,
|
||||
}
|
||||
|
||||
pub struct DisplayParts {
|
||||
pub i2c: DisplayI2c,
|
||||
}
|
||||
|
||||
// ── Display driver ──────────────────────────────────────────────────
|
||||
|
||||
pub type DisplayDriver = ssd1306::Ssd1306Async<
|
||||
ssd1306::prelude::I2CInterface<DisplayI2c>,
|
||||
ssd1306::size::DisplaySize128x64,
|
||||
ssd1306::mode::BufferedGraphicsModeAsync<ssd1306::size::DisplaySize128x64>,
|
||||
>;
|
||||
|
||||
/// Construct and initialize an SSD1306 display from raw I²C.
|
||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
||||
use ssd1306::mode::DisplayConfigAsync;
|
||||
use ssd1306::prelude::{Brightness, DisplayRotation};
|
||||
use ssd1306::size::DisplaySize128x64;
|
||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
||||
|
||||
let interface = I2CDisplayInterface::new(i2c);
|
||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
||||
.into_buffered_graphics_mode();
|
||||
|
||||
embassy_time::Timer::after_millis(100).await;
|
||||
if display.init().await.is_err() {
|
||||
defmt::error!("SSD1306 display init failed");
|
||||
return None;
|
||||
}
|
||||
let _ = display.set_brightness(Brightness::BRIGHTEST).await;
|
||||
Some(display)
|
||||
}
|
||||
|
||||
// ── Shared peripheral init helpers ──────────────────────────────────
|
||||
|
||||
/// Construct an SX1276 radio from an initialized SPI bus.
|
||||
///
|
||||
/// `dio0` is the primary IRQ (TX/RX done). The SX1276 module on the T-Beam
|
||||
/// runs from a crystal oscillator, not a TCXO, and power comes from the
|
||||
/// AXP192's PA_BOOST rail — `tx_boost = true` routes TX through the PA
|
||||
/// output for +20 dBm.
|
||||
pub fn init_radio(
|
||||
spi_bus: &'static embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>,
|
||||
nss_pin: impl OutputPin + 'static,
|
||||
reset_pin: impl OutputPin + 'static,
|
||||
dio0_pin: impl InputPin + 'static,
|
||||
) -> RadioParts {
|
||||
let nss = Output::new(nss_pin, Level::High, OutputConfig::default());
|
||||
let spi_device = SpiDevice::new(spi_bus, nss);
|
||||
|
||||
let reset = Output::new(reset_pin, Level::High, OutputConfig::default());
|
||||
let dio0 = Input::new(dio0_pin, InputConfig::default().with_pull(Pull::Down));
|
||||
|
||||
let iv =
|
||||
GenericSx127xInterfaceVariant::new(reset, dio0, None, None).expect("SX1276 interface init");
|
||||
|
||||
let sx_config = sx127x::Config {
|
||||
chip: sx127x::Sx1276,
|
||||
tcxo_used: false,
|
||||
tx_boost: true,
|
||||
rx_boost: false,
|
||||
};
|
||||
|
||||
RadioParts {
|
||||
driver: Sx127x::new(spi_device, iv, sx_config),
|
||||
delay: Delay,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//! LilyGo T-Beam (classic) — ESP32 + SX1276 + SSD1306 OLED + AXP192 PMIC.
|
||||
//!
|
||||
//! First-generation T-Beam. ESP32 (dual-core Xtensa LX6, no native USB),
|
||||
//! SX1276 module with XTAL (no TCXO), 128×64 SSD1306 OLED, AXP192 PMIC
|
||||
//! gating the LoRa and OLED rails, CP2102 USB-UART bridge for host I/O.
|
||||
//!
|
||||
//! The AXP192 shares an I²C bus with the OLED (SDA=21, SCL=22). PMIC init
|
||||
//! runs blocking on that bus first, then the same pins are re-initialized
|
||||
//! in async mode for the display task.
|
||||
//!
|
||||
//! Pin map (verified from MeshCore variants/lilygo_tbeam_SX1276/):
|
||||
//! LoRa SPI: SCK=5, MOSI=27, MISO=19, CS=18
|
||||
//! LoRa ctl: RESET=23, DIO0=26 (primary IRQ — SX127x has no BUSY)
|
||||
//! OLED I²C: SDA=21, SCL=22, SSD1306 @ 0x3C (shared with AXP192 @ 0x34)
|
||||
//! UART host: TX=1, RX=3 via CP2102 bridge (USB 10c4:ea60)
|
||||
//! LED: GPIO4 (active LOW — red LED under the OLED, wrapped in
|
||||
//! InvertedPin so SimpleLed sees normal polarity)
|
||||
//!
|
||||
//! GPS / battery / IMU / PCF8563 RTC are populated on the board but unused
|
||||
//! by this firmware.
|
||||
|
||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||
use esp_hal::i2c::master::{Config as I2cConfig, I2c};
|
||||
|
||||
use super::esp32;
|
||||
use super::traits::{BoardParts, LoRaBoard};
|
||||
use crate::driver::axp;
|
||||
use crate::driver::inverted_pin::InvertedPin;
|
||||
use crate::driver::simple_led::SimpleLed;
|
||||
use crate::hal::esp32 as mcu;
|
||||
use crate::protocol::RadioChipId;
|
||||
|
||||
pub use super::esp32::{create_display, DisplayDriver, DisplayParts, RadioDriver, RadioParts};
|
||||
|
||||
// ── Concrete peripheral types ───────────────────────────────────────
|
||||
|
||||
pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>;
|
||||
pub type LedDriver = SimpleLed<InvertedPin<Output<'static>>>;
|
||||
|
||||
// ── Peripheral bundles ──────────────────────────────────────────────
|
||||
|
||||
pub struct UartParts {
|
||||
pub driver: UartDriver,
|
||||
}
|
||||
|
||||
// ── Board init ──────────────────────────────────────────────────────
|
||||
|
||||
pub struct Board {
|
||||
p: esp_hal::peripherals::Peripherals,
|
||||
}
|
||||
|
||||
impl LoRaBoard for Board {
|
||||
const NAME: &'static str = "LilyGo T-Beam";
|
||||
// SX1276 PA_BOOST range per the datasheet: +2 dBm min, +20 dBm max.
|
||||
const TX_POWER_RANGE: (i8, i8) = (2, 20);
|
||||
|
||||
type RadioParts = RadioParts;
|
||||
type CommParts = UartParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = LedDriver;
|
||||
|
||||
fn init() -> Self {
|
||||
let p = esp_hal::init(esp_hal::Config::default());
|
||||
Self { p }
|
||||
}
|
||||
|
||||
fn mac_address() -> [u8; 6] {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn radio_chip_id() -> u16 {
|
||||
RadioChipId::Sx1276.as_u16()
|
||||
}
|
||||
|
||||
fn freq_range_hz() -> (u32, u32) {
|
||||
// SX1276 tuning window per datasheet (LF + HF bands combined).
|
||||
(137_000_000, 1_020_000_000)
|
||||
}
|
||||
|
||||
fn supported_sf_bitmap() -> u16 {
|
||||
// SX127x supports SF6–SF12 (no SF5 like the SX126x).
|
||||
0x1FC0
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UartParts, DisplayParts, LedDriver> {
|
||||
let p = self.p;
|
||||
|
||||
mcu::start_timer(p.TIMG0, p.SW_INTERRUPT);
|
||||
|
||||
// AXP192 and SSD1306 share I²C0 (SDA=21, SCL=22). Build the bus
|
||||
// once as blocking, drive the PMIC init, then convert to async for
|
||||
// the display task — avoids re-consuming the I²C0 peripheral.
|
||||
// PMIC failure isn't fatal to boot: log and proceed so the rest of
|
||||
// the firmware can still emit diagnostics.
|
||||
let mut i2c_blocking = I2c::new(p.I2C0, I2cConfig::default())
|
||||
.expect("I2C0 init")
|
||||
.with_sda(p.GPIO21)
|
||||
.with_scl(p.GPIO22);
|
||||
if let Err(_e) = axp::axp192_init_lora_oled(&mut i2c_blocking) {
|
||||
defmt::error!("AXP192 init failed — LoRa rail may be dead");
|
||||
}
|
||||
// Let DCDC1 / LDO2 settle before SPI or display traffic.
|
||||
esp_hal::delay::Delay::new().delay_millis(20);
|
||||
|
||||
// SPI2 for SX1276: SCK=5, MOSI=27, MISO=19
|
||||
let spi_bus = mcu::init_spi(p.SPI2, p.DMA_SPI2, p.GPIO5, p.GPIO27, p.GPIO19);
|
||||
|
||||
// SX1276: CS=18, RESET=23, DIO0=26 (primary IRQ)
|
||||
let radio = esp32::init_radio(spi_bus, p.GPIO18, p.GPIO23, p.GPIO26);
|
||||
|
||||
// UART0 via CP2102 bridge: TX=1, RX=3 (ESP32 default console)
|
||||
use esp_hal::uart::{Config as UartConfig, Uart};
|
||||
let host = UartParts {
|
||||
driver: Uart::new(p.UART0, UartConfig::default())
|
||||
.expect("UART init")
|
||||
.with_tx(p.GPIO1)
|
||||
.with_rx(p.GPIO3)
|
||||
.into_async(),
|
||||
};
|
||||
|
||||
// Hand the (now async) I²C bus to the SSD1306 display task.
|
||||
let display = Some(DisplayParts {
|
||||
i2c: i2c_blocking.into_async(),
|
||||
});
|
||||
|
||||
// TX activity LED on GPIO4 (active LOW — pin idles HIGH = LED off).
|
||||
// InvertedPin flips set_high/set_low so SimpleLed stays polarity-agnostic.
|
||||
let led_pin = Output::new(p.GPIO4, Level::High, OutputConfig::default());
|
||||
let led = SimpleLed(InvertedPin(led_pin));
|
||||
|
||||
BoardParts {
|
||||
radio,
|
||||
host,
|
||||
display,
|
||||
led,
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
//! LilyGo T-Beam S3 Supreme — ESP32-S3 + SX1262 + SH1106 OLED + AXP2101 PMIC.
|
||||
//!
|
||||
//! Big-brother of the classic T-Beam: ESP32-S3-WROOM-1 module, SX1262 with a
|
||||
//! TCXO, 128×64 SH1106 OLED, AXP2101 PMIC on a dedicated I²C1 bus. Native USB
|
||||
//! (no UART bridge chip).
|
||||
//!
|
||||
//! The AXP2101 gates power to the SX1262 and OLED. Without its init sequence
|
||||
//! the LoRa chip never responds on SPI and `LoRa::new` fails.
|
||||
//!
|
||||
//! Pin map (verified from MeshCore variants/lilygo_tbeam_supreme_SX1262/):
|
||||
//! LoRa SPI: SCK=12, MOSI=11, MISO=13, CS=10
|
||||
//! LoRa ctl: RESET=5, DIO1=1, BUSY=4 (TCXO @ 1.6 V via DIO3)
|
||||
//! OLED I²C: SDA=17, SCL=18, SH1106 @ 0x3C (I²C0)
|
||||
//! PMIC I²C: SDA=42, SCL=41, AXP2101 @ 0x34 (I²C1, blocking, PMIC-only)
|
||||
//! USB: native USB-C on GPIO19/20 (fixed ESP32-S3 D+/D-)
|
||||
//! LED: GPIO6 (active HIGH indicator, reuses MeshCore's P_LORA_TX_LED)
|
||||
//!
|
||||
//! GPS / battery / IMU / SD / BME280 are unused — DongLoRa is a transparent
|
||||
//! LoRa-over-USB bridge. The PMIC init only enables ALDO1 (OLED) and ALDO3
|
||||
//! (LoRa); other rails are left at their AXP2101 power-on defaults.
|
||||
|
||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||
use esp_hal::i2c::master::{Config as I2cConfig, I2c};
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use super::esp32s3;
|
||||
use super::traits::{BoardParts, LoRaBoard};
|
||||
use crate::driver::axp;
|
||||
use crate::driver::sh1106::Sh1106;
|
||||
use crate::driver::simple_led::SimpleLed;
|
||||
use crate::hal::esp32s3 as mcu;
|
||||
|
||||
pub use super::esp32s3::{DisplayI2c, RadioDriver, RadioParts};
|
||||
|
||||
// ── Concrete peripheral types ───────────────────────────────────────
|
||||
|
||||
pub type DisplayDriver = Sh1106<DisplayI2c>;
|
||||
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||
pub type LedDriver = SimpleLed<Output<'static>>;
|
||||
|
||||
// ── Peripheral bundles ──────────────────────────────────────────────
|
||||
|
||||
pub struct UsbParts {
|
||||
pub driver: UsbDriver,
|
||||
}
|
||||
|
||||
pub struct DisplayParts {
|
||||
pub i2c: DisplayI2c,
|
||||
}
|
||||
|
||||
// ── Display init ────────────────────────────────────────────────────
|
||||
|
||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
||||
let mut display = Sh1106::new(i2c, 0x3C);
|
||||
|
||||
embassy_time::Timer::after_millis(100).await;
|
||||
if display.init().await.is_err() {
|
||||
defmt::error!("SH1106 display init failed");
|
||||
return None;
|
||||
}
|
||||
let _ = display.set_brightness(0xFF).await;
|
||||
Some(display)
|
||||
}
|
||||
|
||||
// ── Board init ──────────────────────────────────────────────────────
|
||||
|
||||
pub struct Board {
|
||||
p: esp_hal::peripherals::Peripherals,
|
||||
}
|
||||
|
||||
impl LoRaBoard for Board {
|
||||
const NAME: &'static str = "LilyGo T-Beam S3 Supreme";
|
||||
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
type RadioParts = RadioParts;
|
||||
type CommParts = UsbParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = LedDriver;
|
||||
|
||||
fn init() -> Self {
|
||||
let p = esp_hal::init(esp_hal::Config::default());
|
||||
Self { p }
|
||||
}
|
||||
|
||||
fn mac_address() -> [u8; 6] {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
||||
let p = self.p;
|
||||
|
||||
mcu::start_timer(p.TIMG0, p.SW_INTERRUPT);
|
||||
|
||||
// AXP2101 PMIC on I²C1 (SDA=42, SCL=41). Blocking, one-shot: we only
|
||||
// need to flip the LoRa/OLED rails on, then the bus can be dropped.
|
||||
// If this fails the LoRa chip stays unpowered — log and keep going so
|
||||
// the rest of the boot path can still produce diagnostics.
|
||||
{
|
||||
let mut pmic = I2c::new(p.I2C1, I2cConfig::default())
|
||||
.expect("I2C1 init")
|
||||
.with_sda(p.GPIO42)
|
||||
.with_scl(p.GPIO41);
|
||||
if let Err(_e) = axp::axp2101_init_lora_oled(&mut pmic) {
|
||||
defmt::error!("AXP2101 init failed — LoRa rail may be dead");
|
||||
}
|
||||
}
|
||||
// Let ALDO1/ALDO3 settle before touching the LoRa SPI or OLED I²C.
|
||||
esp_hal::delay::Delay::new().delay_millis(20);
|
||||
|
||||
// SPI2 for SX1262: SCK=12, MOSI=11, MISO=13
|
||||
let spi_bus = mcu::init_spi(p.SPI2, p.DMA_CH0, p.GPIO12, p.GPIO11, p.GPIO13);
|
||||
|
||||
// SX1262: CS=10, RESET=5, DIO1=1, BUSY=4, TCXO @ 1.6 V via DIO3.
|
||||
let radio = esp32s3::init_radio(
|
||||
spi_bus,
|
||||
p.GPIO10,
|
||||
p.GPIO5,
|
||||
p.GPIO1,
|
||||
p.GPIO4,
|
||||
lora_phy::sx126x::TcxoCtrlVoltage::Ctrl1V6,
|
||||
);
|
||||
|
||||
// Native USB-C on GPIO19/20. Switches the internal USB PHY from
|
||||
// Serial-JTAG to OTG.
|
||||
let usb_inst = esp_hal::otg_fs::Usb::new(p.USB0, p.GPIO20, p.GPIO19);
|
||||
static EP_OUT_BUF: StaticCell<[u8; 1024]> = StaticCell::new();
|
||||
let ep_out_buf = EP_OUT_BUF.init([0u8; 1024]);
|
||||
let host = UsbParts {
|
||||
driver: esp_hal::otg_fs::asynch::Driver::new(
|
||||
usb_inst,
|
||||
ep_out_buf,
|
||||
esp_hal::otg_fs::asynch::Config::default(),
|
||||
),
|
||||
};
|
||||
|
||||
// I2C0 for SH1106 OLED: SDA=17, SCL=18 (async, handed to display task).
|
||||
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||
let display = Some(DisplayParts { i2c });
|
||||
|
||||
// Activity LED on GPIO6 (active HIGH).
|
||||
let led_pin = Output::new(p.GPIO6, Level::Low, OutputConfig::default());
|
||||
let led = SimpleLed(led_pin);
|
||||
|
||||
BoardParts {
|
||||
radio,
|
||||
host,
|
||||
display,
|
||||
led,
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,11 @@ pub mod traits;
|
||||
pub use traits::*;
|
||||
|
||||
{% for helper, helper_features in helpers|items -%}
|
||||
{% if helper_features|length == 1 -%}
|
||||
#[cfg(feature = "{{ helper_features[0] }}")]
|
||||
{% else -%}
|
||||
#[cfg(any({% for f in helper_features %}feature = "{{ f }}"{% if not loop.last %}, {% endif %}{% endfor %}))]
|
||||
{% endif -%}
|
||||
mod {{ helper }};
|
||||
{% endfor %}
|
||||
cfg_if::cfg_if! {
|
||||
|
||||
@@ -20,7 +20,7 @@ type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
|
||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||
pub type UsbDriver = mcu::UsbNrfDriver;
|
||||
pub type DisplayI2c = mcu::I2cBus;
|
||||
pub type LedDriver = ();
|
||||
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
|
||||
|
||||
pub type DisplayDriver = ssd1306::Ssd1306Async<
|
||||
ssd1306::prelude::I2CInterface<DisplayI2c>,
|
||||
@@ -78,10 +78,14 @@ impl LoRaBoard for Board {
|
||||
type CommParts = UsbParts;
|
||||
type DisplayParts = DisplayParts;
|
||||
type DisplayDriver = DisplayDriver;
|
||||
type LedDriver = ();
|
||||
type LedDriver = LedDriver;
|
||||
|
||||
fn init() -> Self {
|
||||
let p = embassy_nrf::init(Default::default());
|
||||
// USB requires the 32 MHz HFXO (±50 ppm); the RC oscillator is out of spec.
|
||||
let mut config = embassy_nrf::config::Config::default();
|
||||
config.hfclk_source = embassy_nrf::config::HfclkSource::ExternalXtal;
|
||||
config.lfclk_source = embassy_nrf::config::LfclkSource::ExternalXtal;
|
||||
let p = embassy_nrf::init(config);
|
||||
Self { p }
|
||||
}
|
||||
|
||||
@@ -89,12 +93,18 @@ impl LoRaBoard for Board {
|
||||
mcu::mac_address()
|
||||
}
|
||||
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, ()> {
|
||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
||||
let p = self.p;
|
||||
|
||||
// SX1262 is power-gated on P1.05 — must be HIGH before any SPI access.
|
||||
let sx_power_en = Output::new(p.P1_05, Level::High, OutputDrive::Standard);
|
||||
core::mem::forget(sx_power_en);
|
||||
|
||||
// ── SPI bus for SX1262 ──────────────────────────────────
|
||||
let mut spi_cfg = spim::Config::default();
|
||||
spi_cfg.frequency = spim::Frequency::M1;
|
||||
// embassy_nrf::spim::Spim::new args: (spim, irq, sck, miso, mosi, config).
|
||||
// Per RAK4631 BSP: SCK=P1.11, MISO=P1.13, MOSI=P1.12.
|
||||
let spi = Spim::new(p.SPI3, mcu::Irqs, p.P1_11, p.P1_13, p.P1_12, spi_cfg);
|
||||
let spi_bus = mcu::share_spi_bus(spi);
|
||||
|
||||
@@ -103,7 +113,11 @@ impl LoRaBoard for Board {
|
||||
|
||||
let reset = Output::new(p.P1_06, Level::High, OutputDrive::Standard);
|
||||
let dio1 = Input::new(p.P1_15, Pull::Down);
|
||||
let busy = Input::new(p.P1_14, Pull::Down);
|
||||
// BUSY has an internal 20 kΩ pull-up on the SX1262 die (datasheet
|
||||
// p. 53); an MCU-side Pull::Down fights it and leaves the line at
|
||||
// an indeterminate voltage, making wait_on_busy unreliable. See
|
||||
// lora-rs/lora-rs#260.
|
||||
let busy = Input::new(p.P1_14, Pull::None);
|
||||
|
||||
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None)
|
||||
.expect("SX1262 interface init");
|
||||
@@ -121,7 +135,9 @@ impl LoRaBoard for Board {
|
||||
};
|
||||
|
||||
// ── USB ─────────────────────────────────────────────────
|
||||
let vbus = mcu::alloc_vbus_detect(true, false);
|
||||
// SoftwareVbusDetect(vbus_detected, power_ready). Mark both true so the
|
||||
// driver doesn't block waiting for a power-ready signal we never raise.
|
||||
let vbus = mcu::alloc_vbus_detect(true, true);
|
||||
let host = UsbParts {
|
||||
driver: Driver::new(p.USBD, mcu::Irqs, vbus),
|
||||
};
|
||||
@@ -138,11 +154,15 @@ impl LoRaBoard for Board {
|
||||
);
|
||||
let display = Some(DisplayParts { i2c });
|
||||
|
||||
// Green LED on P1.03 (RAK19007 Base, active HIGH).
|
||||
let led_pin = Output::new(p.P1_03, Level::Low, OutputDrive::Standard);
|
||||
let led = crate::driver::simple_led::SimpleLed(led_pin);
|
||||
|
||||
BoardParts {
|
||||
radio,
|
||||
host,
|
||||
display,
|
||||
led: (),
|
||||
led,
|
||||
mac: Self::mac_address(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,21 @@ pub trait LoRaBoard: Sized {
|
||||
(150_000_000, 960_000_000)
|
||||
}
|
||||
|
||||
/// Bitmap of supported LoRa spreading factors — bit N = SF N. Defaults
|
||||
/// to SX126x's SF5–SF12 (`0x1FE0`). SX127x boards must override to
|
||||
/// `0x1FC0` (SF6–SF12); LLCC68 boards to `0x0FE0` (SF5–SF11).
|
||||
fn supported_sf_bitmap() -> u16 {
|
||||
0x1FE0
|
||||
}
|
||||
|
||||
/// Bitmap of supported LoRa bandwidths. Bit positions match
|
||||
/// `LoRaBandwidth::as_u8()` (sub-GHz BW enum values 0..9). Defaults
|
||||
/// to all sub-GHz BWs (`0x03FF`). SX128x boards flip the 2.4 GHz
|
||||
/// bits when we add them.
|
||||
fn supported_bw_bitmap() -> u16 {
|
||||
0x03FF
|
||||
}
|
||||
|
||||
/// Decompose initialized board into peripheral bundles for each task.
|
||||
fn into_parts(
|
||||
self,
|
||||
|
||||
@@ -96,7 +96,11 @@ impl LoRaBoard for Board {
|
||||
|
||||
let reset = Output::new(p.P1_07, Level::High, OutputDrive::Standard);
|
||||
let dio1 = Input::new(p.P0_07, Pull::Down);
|
||||
let busy = Input::new(p.P1_10, Pull::Down);
|
||||
// BUSY has an internal 20 kΩ pull-up on the SX1262 die (datasheet
|
||||
// p. 53); an MCU-side Pull::Down fights it and leaves the line at
|
||||
// an indeterminate voltage, making wait_on_busy unreliable. See
|
||||
// lora-rs/lora-rs#260.
|
||||
let busy = Input::new(p.P1_10, Pull::None);
|
||||
|
||||
// RF switch: RXEN on P1.08 (GPIO, toggled by lora_phy), TX via DIO2 (SX1262 internal).
|
||||
let rx_enable = Output::new(p.P1_08, Level::Low, OutputDrive::Standard);
|
||||
|
||||
+10
-9
@@ -138,15 +138,16 @@ fn phase_depth(n: u8) -> u8 {
|
||||
match n {
|
||||
18 => 1, // pre-TX: CmdTransmit just read from commands channel
|
||||
6 => 2, // perform_tx entered
|
||||
7 => 3, // in CAD
|
||||
8 => 4, // in prepare_for_tx
|
||||
9 => 5, // in tx().await
|
||||
10 => 6, // do_tx returned Ok
|
||||
11 => 7, // respond_tx_done entered
|
||||
12 => 8, // respond_tx_done: responses.send returned
|
||||
13 => 9, // host_task received response from channel
|
||||
14 => 10, // host_task wrote response to UART (terminal success)
|
||||
15 => 11, // TX errored (terminal error)
|
||||
7 => 3, // in CAD (before prepare_for_cad)
|
||||
19 => 4, // after prepare_for_cad (awaiting lora.cad IRQ)
|
||||
8 => 5, // in prepare_for_tx
|
||||
9 => 6, // in tx().await
|
||||
10 => 7, // do_tx returned Ok
|
||||
11 => 8, // respond_tx_done entered
|
||||
12 => 9, // respond_tx_done: responses.send returned
|
||||
13 => 10, // host_task received response from channel
|
||||
14 => 11, // host_task wrote response to UART (terminal success)
|
||||
15 => 12, // TX errored (terminal error)
|
||||
_ => 0, // not a TX phase
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -37,8 +37,8 @@ use embassy_time::Timer;
|
||||
use hsmc::statechart;
|
||||
|
||||
use crate::board::{self, Board, DisplayDriver, DisplayParts, LedDriver, LoRaBoard, RgbLed};
|
||||
use crate::driver::DisplayBrightness;
|
||||
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioEvent, RadioEventChannel};
|
||||
use crate::driver::DisplayBrightness;
|
||||
use crate::protocol::LoRaConfig;
|
||||
|
||||
use render::{BoardInfo, RSSI_HISTORY_LEN};
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! AXP PMIC init — turn on the rails that feed LoRa and the OLED.
|
||||
//!
|
||||
//! T-Beam–class boards gate LoRa, OLED (and GPS/sensors, which this firmware
|
||||
//! does not use) through an X-Powers PMIC on I²C. Until the PMIC is told to
|
||||
//! enable the right LDOs/DCDCs, the SX126x/SX127x chip sits unpowered and
|
||||
//! `LoRa::new` fails. This module's only job is to flip those switches.
|
||||
//!
|
||||
//! Two PMICs are covered:
|
||||
//!
|
||||
//! * [`axp2101_init_lora_oled`] — LilyGo T-Beam S3 Supreme. ALDO3 = 3.3 V
|
||||
//! feeds the SX1262; ALDO1 = 3.3 V feeds the SH1106 OLED. Both live on a
|
||||
//! dedicated I²C1 bus (separate from the display).
|
||||
//! * [`axp192_init_lora_oled`] — LilyGo T-Beam (classic). LDO2 = 3.3 V feeds
|
||||
//! the SX1276; DCDC1 = 3.3 V feeds the SSD1306 OLED. PMIC shares the
|
||||
//! display I²C bus.
|
||||
//!
|
||||
//! Everything unrelated to LoRa + OLED (GPS, sensors, SD, batt ADC) is left
|
||||
//! alone. Out-of-scope for a transparent LoRa-over-USB bridge.
|
||||
//!
|
||||
//! Init is blocking on purpose. It runs once during `into_parts`, which is a
|
||||
//! synchronous function called before task spawning. Using the blocking
|
||||
//! `embedded_hal` trait keeps the PMIC wiring out of the async machinery and
|
||||
//! lets the caller drop the I²C bus immediately after init (no bus handoff).
|
||||
|
||||
use embedded_hal::i2c::I2c;
|
||||
|
||||
/// Both chips respond at this 7-bit I²C address.
|
||||
const PMIC_ADDR: u8 = 0x34;
|
||||
|
||||
// ── AXP2101 (T-Beam S3 Supreme) ──────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "lilygo_tbeam_supreme")]
|
||||
mod axp2101 {
|
||||
use super::{I2c, PMIC_ADDR};
|
||||
|
||||
/// LDO on/off control register. Bits:
|
||||
/// 0 = ALDO1, 1 = ALDO2, 2 = ALDO3, 3 = ALDO4, 4 = BLDO1, 5 = BLDO2.
|
||||
const REG_LDO_ONOFF: u8 = 0x90;
|
||||
/// ALDO1 voltage register (OLED rail on T-Beam Supreme).
|
||||
const REG_ALDO1_V: u8 = 0x92;
|
||||
/// ALDO3 voltage register (LoRa rail on T-Beam Supreme).
|
||||
const REG_ALDO3_V: u8 = 0x94;
|
||||
|
||||
/// Voltage code for 3.3 V on ALDOx: 0.5 V + 28 × 100 mV.
|
||||
const V_3V3: u8 = 28;
|
||||
|
||||
/// Mask enabling ALDO1 + ALDO3 in [`REG_LDO_ONOFF`].
|
||||
const ALDO1_ALDO3: u8 = 0b0000_0101;
|
||||
|
||||
/// Bring up LoRa (ALDO3) and OLED (ALDO1) rails at 3.3 V.
|
||||
///
|
||||
/// Voltage registers are set before the enable bits to avoid glitching
|
||||
/// the rails through the reset-default voltage. Other rails are left
|
||||
/// untouched (read-modify-write on the enable register) so any
|
||||
/// board-specific OTP defaults stay intact.
|
||||
pub fn init_lora_oled<I: I2c>(i2c: &mut I) -> Result<(), I::Error> {
|
||||
i2c.write(PMIC_ADDR, &[REG_ALDO3_V, V_3V3])?;
|
||||
i2c.write(PMIC_ADDR, &[REG_ALDO1_V, V_3V3])?;
|
||||
|
||||
let mut onoff = [0u8; 1];
|
||||
i2c.write_read(PMIC_ADDR, &[REG_LDO_ONOFF], &mut onoff)?;
|
||||
i2c.write(PMIC_ADDR, &[REG_LDO_ONOFF, onoff[0] | ALDO1_ALDO3])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lilygo_tbeam_supreme")]
|
||||
pub use axp2101::init_lora_oled as axp2101_init_lora_oled;
|
||||
|
||||
// ── AXP192 (T-Beam classic) ──────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "lilygo_tbeam")]
|
||||
mod axp192 {
|
||||
use super::{I2c, PMIC_ADDR};
|
||||
|
||||
/// DCDC/LDO enable register. Bit layout:
|
||||
/// 0 = DCDC1, 1 = DCDC3, 2 = LDO2, 3 = LDO3, 4 = DCDC2, 6 = EXTEN.
|
||||
const REG_ONOFF: u8 = 0x12;
|
||||
/// DCDC1 output voltage register (OLED rail on T-Beam classic).
|
||||
/// 7-bit code, 25 mV step, 0.7 V offset → 3.3 V = 104 = `0x68`.
|
||||
const REG_DCDC1_V: u8 = 0x26;
|
||||
/// LDO2/LDO3 voltage register. Upper nibble = LDO2 (LoRa), lower nibble
|
||||
/// = LDO3 (GPS). 100 mV step, 1.8 V offset → 3.3 V = 15 = `0xF`.
|
||||
const REG_LDO23_V: u8 = 0x28;
|
||||
|
||||
/// DCDC1 code for 3.3 V.
|
||||
const DCDC1_3V3: u8 = 0x68;
|
||||
/// LDO2 nibble for 3.3 V in the upper half of [`REG_LDO23_V`].
|
||||
const LDO2_3V3_UPPER: u8 = 0xF0;
|
||||
|
||||
/// Mask enabling DCDC1 + LDO2 in [`REG_ONOFF`].
|
||||
const DCDC1_LDO2: u8 = 0b0000_0101;
|
||||
|
||||
/// Bring up LoRa (LDO2) and OLED (DCDC1) rails at 3.3 V on the
|
||||
/// classic T-Beam's AXP192.
|
||||
///
|
||||
/// GPS (LDO3) and EXTEN are not touched — this firmware has no GPS,
|
||||
/// and any board-default for LDO3 voltage is preserved by
|
||||
/// read-modify-write. Voltage is programmed before the enable bit
|
||||
/// flips so the rail doesn't glitch through its reset default.
|
||||
pub fn init_lora_oled<I: I2c>(i2c: &mut I) -> Result<(), I::Error> {
|
||||
let mut ldo23 = [0u8; 1];
|
||||
i2c.write_read(PMIC_ADDR, &[REG_LDO23_V], &mut ldo23)?;
|
||||
i2c.write(
|
||||
PMIC_ADDR,
|
||||
&[REG_LDO23_V, (ldo23[0] & 0x0F) | LDO2_3V3_UPPER],
|
||||
)?;
|
||||
|
||||
i2c.write(PMIC_ADDR, &[REG_DCDC1_V, DCDC1_3V3])?;
|
||||
|
||||
let mut onoff = [0u8; 1];
|
||||
i2c.write_read(PMIC_ADDR, &[REG_ONOFF], &mut onoff)?;
|
||||
i2c.write(PMIC_ADDR, &[REG_ONOFF, onoff[0] | DCDC1_LDO2])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lilygo_tbeam")]
|
||||
pub use axp192::init_lora_oled as axp192_init_lora_oled;
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Generic `OutputPin` wrapper that inverts logic levels.
|
||||
//!
|
||||
//! When a peripheral is wired active-low (LED anode to V+, cathode to GPIO;
|
||||
//! reset line idle-high, etc.) the physical polarity is a property of the
|
||||
//! board, not of the driver. Wrapping the pin in `InvertedPin` lets drivers
|
||||
//! keep "set_high = assert" semantics while the board file expresses the
|
||||
//! wiring reality at construction time.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! let pin = Output::new(p.GPIO4, Level::High, OutputConfig::default()); // off
|
||||
//! let led = SimpleLed(InvertedPin(pin)); // driver sees normal polarity
|
||||
//! ```
|
||||
|
||||
use embedded_hal::digital::{ErrorType, OutputPin};
|
||||
|
||||
pub struct InvertedPin<P>(pub P);
|
||||
|
||||
impl<P: ErrorType> ErrorType for InvertedPin<P> {
|
||||
type Error = P::Error;
|
||||
}
|
||||
|
||||
impl<P: OutputPin> OutputPin for InvertedPin<P> {
|
||||
fn set_low(&mut self) -> Result<(), Self::Error> {
|
||||
self.0.set_high()
|
||||
}
|
||||
|
||||
fn set_high(&mut self) -> Result<(), Self::Error> {
|
||||
self.0.set_low()
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -1,11 +1,24 @@
|
||||
#[cfg(any(feature = "wio_tracker_l1", feature = "elecrow_thinknode_m2"))]
|
||||
#[cfg(any(
|
||||
feature = "wio_tracker_l1",
|
||||
feature = "elecrow_thinknode_m2",
|
||||
feature = "lilygo_tbeam_supreme"
|
||||
))]
|
||||
pub mod sh1106;
|
||||
|
||||
#[cfg(any(feature = "lilygo_tbeam_supreme", feature = "lilygo_tbeam"))]
|
||||
pub mod axp;
|
||||
|
||||
#[cfg(feature = "lilygo_tbeam")]
|
||||
pub mod inverted_pin;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "heltec_v3",
|
||||
feature = "heltec_v3_uart",
|
||||
feature = "heltec_v4",
|
||||
feature = "elecrow_thinknode_m2",
|
||||
feature = "lilygo_tbeam_supreme",
|
||||
feature = "lilygo_tbeam",
|
||||
feature = "rak_wisblock_4631",
|
||||
feature = "wio_tracker_l1",
|
||||
feature = "waveshare_rp2040_lora"
|
||||
))]
|
||||
@@ -51,7 +64,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wio_tracker_l1", feature = "elecrow_thinknode_m2"))]
|
||||
#[cfg(any(
|
||||
feature = "wio_tracker_l1",
|
||||
feature = "elecrow_thinknode_m2",
|
||||
feature = "lilygo_tbeam_supreme"
|
||||
))]
|
||||
impl<I> DisplayBrightness for sh1106::Sh1106<I>
|
||||
where
|
||||
I: embedded_hal_async::i2c::I2c,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//! ESP32 (classic / non-S3) MCU initialization primitives.
|
||||
//!
|
||||
//! Mirror of `hal::esp32s3` with a single delta: the classic ESP32 uses a
|
||||
//! peripheral-bound DMA (`DMA_SPI2`) rather than the S3's channel-addressed
|
||||
//! GDMA (`DMA_CH0`). Timer, I²C, and eFuse access are otherwise identical
|
||||
//! between the two Xtensa parts in this esp-hal vintage.
|
||||
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use esp_hal::dma::{DmaRxBuf, DmaTxBuf};
|
||||
use esp_hal::gpio::interconnect::{PeripheralInput, PeripheralOutput};
|
||||
use esp_hal::i2c::master::I2c;
|
||||
use esp_hal::spi::master::{Config as SpiConfig, Spi, SpiDmaBus};
|
||||
use esp_hal::spi::Mode as SpiMode;
|
||||
use esp_hal::time::Rate;
|
||||
use esp_hal::timer::timg::TimerGroup;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
// ── MCU-level types ─────────────────────────────────────────────────
|
||||
|
||||
pub type SpiBus = SpiDmaBus<'static, esp_hal::Async>;
|
||||
pub type I2cBus = I2c<'static, esp_hal::Async>;
|
||||
|
||||
// ── Timer ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Start the ESP-RTOS scheduler and Embassy time driver.
|
||||
pub fn start_timer(
|
||||
timg0: esp_hal::peripherals::TIMG0<'static>,
|
||||
sw_int: esp_hal::peripherals::SW_INTERRUPT<'static>,
|
||||
) {
|
||||
let timg0 = TimerGroup::new(timg0);
|
||||
let sw_ints = esp_hal::interrupt::software::SoftwareInterruptControl::new(sw_int);
|
||||
esp_rtos::start(timg0.timer0, sw_ints.software_interrupt0);
|
||||
}
|
||||
|
||||
// ── SPI bus ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Initialize SPI2 with peripheral-bound DMA and wrap in a shared bus
|
||||
/// (`StaticCell` + `Mutex`).
|
||||
///
|
||||
/// Generic over pin type. The classic ESP32's matrix of GPIO → peripheral
|
||||
/// routes lets any GPIO serve as any SPI signal — same as the S3.
|
||||
pub fn init_spi(
|
||||
spi2: esp_hal::peripherals::SPI2<'static>,
|
||||
dma_spi2: esp_hal::peripherals::DMA_SPI2<'static>,
|
||||
sck: impl PeripheralOutput<'static>,
|
||||
mosi: impl PeripheralOutput<'static>,
|
||||
miso: impl PeripheralInput<'static>,
|
||||
) -> &'static embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus> {
|
||||
let spi = Spi::new(
|
||||
spi2,
|
||||
SpiConfig::default()
|
||||
.with_frequency(Rate::from_mhz(1))
|
||||
.with_mode(SpiMode::_0),
|
||||
)
|
||||
.expect("SPI init")
|
||||
.with_sck(sck)
|
||||
.with_mosi(mosi)
|
||||
.with_miso(miso)
|
||||
.with_dma(dma_spi2);
|
||||
|
||||
let (rx_buf, rx_desc, tx_buf, tx_desc) = esp_hal::dma_buffers!(256);
|
||||
let dma_rx = DmaRxBuf::new(rx_desc, rx_buf).expect("DMA RX buf");
|
||||
let dma_tx = DmaTxBuf::new(tx_desc, tx_buf).expect("DMA TX buf");
|
||||
|
||||
let spi = spi.with_buffers(dma_rx, dma_tx).into_async();
|
||||
|
||||
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
||||
StaticCell::new();
|
||||
SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi))
|
||||
}
|
||||
|
||||
// ── MAC address ─────────────────────────────────────────────────────
|
||||
|
||||
/// Read the factory-programmed MAC address from eFuse.
|
||||
pub fn mac_address() -> [u8; 6] {
|
||||
esp_hal::efuse::base_mac_address()
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.expect("MAC is 6 bytes")
|
||||
}
|
||||
+5
-1
@@ -7,10 +7,14 @@
|
||||
feature = "heltec_v3",
|
||||
feature = "heltec_v3_uart",
|
||||
feature = "heltec_v4",
|
||||
feature = "elecrow_thinknode_m2"
|
||||
feature = "elecrow_thinknode_m2",
|
||||
feature = "lilygo_tbeam_supreme"
|
||||
))]
|
||||
pub mod esp32s3;
|
||||
|
||||
#[cfg(feature = "lilygo_tbeam")]
|
||||
pub mod esp32;
|
||||
|
||||
#[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))]
|
||||
pub mod nrf52840;
|
||||
|
||||
|
||||
+5
-1
@@ -6,7 +6,11 @@
|
||||
pub mod framing;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(feature = "heltec_v3_uart", feature = "elecrow_thinknode_m2"))] {
|
||||
if #[cfg(any(
|
||||
feature = "heltec_v3_uart",
|
||||
feature = "elecrow_thinknode_m2",
|
||||
feature = "lilygo_tbeam"
|
||||
))] {
|
||||
mod uart;
|
||||
pub use uart::host_task;
|
||||
} else {
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
//! Pure-math helpers for Listen-Before-Talk (CAD) retry timing.
|
||||
//!
|
||||
//! The CAD retry loop in `radio::do_tx` scales its backoff to the active
|
||||
//! LoRa configuration: per-retry sleep is one time-on-air of an assumed
|
||||
//! peer packet, total retry budget is capped at `CAD_BUDGET_US`. This
|
||||
//! module owns the ToA math (Semtech AN1200.13) and the retry-count
|
||||
//! derivation, split out so it can be unit-tested host-side without the
|
||||
//! radio module's hardware dependencies.
|
||||
|
||||
use crate::protocol::{LoRaCodingRate, LoRaConfig, LoRaHeaderMode};
|
||||
|
||||
/// Total LBT budget: the maximum time the CAD retry loop is allowed to
|
||||
/// tie up the radio before returning `ChannelBusy`.
|
||||
pub const CAD_BUDGET_US: u32 = 2_500_000;
|
||||
/// Upper bound on retries, independent of budget — caps the work on
|
||||
/// very fast configs (SF7/BW500 would otherwise get 60+ retries).
|
||||
pub const CAD_HARD_CAP: u8 = 10;
|
||||
/// Assumed peer-packet payload (bytes) used to size the per-retry sleep.
|
||||
/// 80 is a representative median for typical mesh traffic. Peers sending
|
||||
/// larger packets may be partially under-waited (accepted trade-off —
|
||||
/// host-level retry handles collisions).
|
||||
#[allow(dead_code)] // consumed by `radio.rs`, which is gated out of test builds
|
||||
pub const CAD_ASSUMED_PEER_PAYLOAD_B: u16 = 80;
|
||||
/// Jitter applied to each sleep, in tenths of a percent (±20 % = 200).
|
||||
pub const CAD_JITTER_TENTHS_PCT: u32 = 200;
|
||||
|
||||
/// ToA formula expects CR ∈ {1..4} for 4/5..4/8 (each coded symbol carries
|
||||
/// `CR+4` bits). The protocol enum starts at 0, so map explicitly — using
|
||||
/// `as u32` alone would underestimate airtime by one code bit everywhere.
|
||||
pub fn cr_numeric(cr: LoRaCodingRate) -> u32 {
|
||||
match cr {
|
||||
LoRaCodingRate::Cr4_5 => 1,
|
||||
LoRaCodingRate::Cr4_6 => 2,
|
||||
LoRaCodingRate::Cr4_7 => 3,
|
||||
LoRaCodingRate::Cr4_8 => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Worst-case LoRa time-on-air in microseconds (Semtech AN1200.13).
|
||||
///
|
||||
/// Uses `N_preamble + 4.25` quarter-symbols for preamble duration; SX126x
|
||||
/// uses 6.25 at SF5/SF6, so the result is low by 2·T_sym at those SFs
|
||||
/// (sub-millisecond — negligible at the LBT-budget scale).
|
||||
pub fn toa_us(cfg: &LoRaConfig, payload_len: u16) -> u32 {
|
||||
let sf = cfg.sf as u64;
|
||||
let bw_hz = cfg.bw.as_hz() as u64;
|
||||
// Symbol time in µs: (2^SF / BW_Hz) * 1e6. u64 avoids overflow at
|
||||
// SF12/BW7 where T_sym ≈ 524 ms.
|
||||
let t_sym_us = (1u64 << sf) * 1_000_000 / bw_hz.max(1);
|
||||
let de: i64 = if t_sym_us > 16_000 { 1 } else { 0 };
|
||||
let crc: i64 = if cfg.payload_crc { 1 } else { 0 };
|
||||
let h: i64 = if matches!(cfg.header_mode, LoRaHeaderMode::Implicit) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let pl = payload_len as i64;
|
||||
let sf_i = sf as i64;
|
||||
|
||||
// Payload symbol count (signed intermediates — `num` goes negative
|
||||
// for small PL at SF12 with H=1, CRC=0).
|
||||
let num = 8 * pl - 4 * sf_i + 28 + 16 * crc - 20 * h;
|
||||
let den = 4 * (sf_i - 2 * de);
|
||||
let payload_sym_ceil = if num <= 0 || den <= 0 {
|
||||
0i64
|
||||
} else {
|
||||
(num + den - 1) / den
|
||||
};
|
||||
let cr_num = cr_numeric(cfg.cr) as i64;
|
||||
let n_payload = 8 + (payload_sym_ceil * (cr_num + 4)).max(0);
|
||||
let t_payload_us = n_payload as u64 * t_sym_us;
|
||||
|
||||
// Preamble duration: (N + 4.25) * T_sym = (4·N + 17) * T_sym / 4.
|
||||
let t_preamble_us = (cfg.preamble_len as u64 * 4 + 17) * t_sym_us / 4;
|
||||
|
||||
(t_preamble_us + t_payload_us).min(u32::MAX as u64) as u32
|
||||
}
|
||||
|
||||
/// Derive the CAD retry count and per-retry sleep from the cached
|
||||
/// `cad_retry_delay_us`. Retries form: N CADs separated by N-1 sleeps.
|
||||
/// When one assumed-packet wait exceeds the total budget, `retries = 1`
|
||||
/// — a single CAD, bail on busy, no sleep.
|
||||
pub fn cad_retries(cad_retry_delay_us: u32) -> (u8, u32) {
|
||||
if cad_retry_delay_us == 0 {
|
||||
// Unconfigured should never reach this path; fall back to a
|
||||
// single CAD with no sleep rather than divide-by-zero.
|
||||
return (1, 0);
|
||||
}
|
||||
let n_sleeps = CAD_BUDGET_US / cad_retry_delay_us;
|
||||
let retries = (n_sleeps + 1).clamp(1, CAD_HARD_CAP as u32) as u8;
|
||||
(retries, cad_retry_delay_us)
|
||||
}
|
||||
|
||||
/// Apply ±`CAD_JITTER_TENTHS_PCT`/10 % jitter around `base_us`.
|
||||
/// `entropy` is a raw 32-bit source; caller provides it so this stays
|
||||
/// pure (radio task passes `Instant::now().as_ticks() as u32`).
|
||||
pub fn jittered_sleep_us(base_us: u32, entropy: u32) -> u32 {
|
||||
let mixed = entropy.wrapping_mul(0x9E37_79B9);
|
||||
let span = 2 * CAD_JITTER_TENTHS_PCT + 1; // e.g. 401 for ±20 %
|
||||
let r = mixed % span; // 0..=2·CAD_JITTER_TENTHS_PCT
|
||||
// factor_thousandths ∈ [1000 − CAD_JITTER_TENTHS_PCT, 1000 + CAD_JITTER_TENTHS_PCT]
|
||||
let factor_thousandths = 1000 - CAD_JITTER_TENTHS_PCT + r;
|
||||
((base_us as u64 * factor_thousandths as u64) / 1000) as u32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::{LoRaBandwidth, LoRaCodingRate, LoRaConfig, LoRaHeaderMode};
|
||||
|
||||
fn cfg(sf: u8, bw: LoRaBandwidth, cr: LoRaCodingRate, preamble: u16, implicit: bool, crc: bool) -> LoRaConfig {
|
||||
LoRaConfig {
|
||||
freq_hz: 915_000_000,
|
||||
sf,
|
||||
bw,
|
||||
cr,
|
||||
preamble_len: preamble,
|
||||
sync_word: 0x1424,
|
||||
tx_power_dbm: 0,
|
||||
header_mode: if implicit {
|
||||
LoRaHeaderMode::Implicit
|
||||
} else {
|
||||
LoRaHeaderMode::Explicit
|
||||
},
|
||||
payload_crc: crc,
|
||||
iq_invert: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: assert `actual` is within `tolerance_pct`% of `expected_us`.
|
||||
fn approx(actual_us: u32, expected_us: u32, tolerance_pct: u32) {
|
||||
let diff = actual_us.abs_diff(expected_us);
|
||||
let tol = expected_us * tolerance_pct / 100;
|
||||
assert!(
|
||||
diff <= tol,
|
||||
"actual {} us not within {}% of expected {} us (diff {}, tol {})",
|
||||
actual_us,
|
||||
tolerance_pct,
|
||||
expected_us,
|
||||
diff,
|
||||
tol
|
||||
);
|
||||
}
|
||||
|
||||
// Reference values sanity-checked against Semtech's LoRa calculator
|
||||
// and AN1200.13.
|
||||
|
||||
#[test]
|
||||
fn toa_sf7_bw125_cr45_pl10_explicit_crc() {
|
||||
let c = cfg(7, LoRaBandwidth::Khz125, LoRaCodingRate::Cr4_5, 8, false, true);
|
||||
// Hand-computed reference from AN1200.13:
|
||||
// T_sym = 128/125 kHz = 1.024 ms
|
||||
// T_preamble = (8 + 4.25) · 1.024 = 12.544 ms
|
||||
// n_payload = 8 + ceil((80 − 28 + 28 + 16) / 28) · 5 = 8 + 4·5 = 28
|
||||
// T_payload = 28 · 1.024 = 28.672 ms
|
||||
// Total ≈ 41.22 ms
|
||||
approx(toa_us(&c, 10), 41_216, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toa_sf12_bw125_cr45_pl10_explicit_crc() {
|
||||
let c = cfg(12, LoRaBandwidth::Khz125, LoRaCodingRate::Cr4_5, 8, false, true);
|
||||
// Reference: ~991 ms for PL=10 at SF12/BW125/CR4/5 (LDRO on).
|
||||
approx(toa_us(&c, 10), 991_000, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toa_sf10_bw125_cr45_pl255_max() {
|
||||
let c = cfg(10, LoRaBandwidth::Khz125, LoRaCodingRate::Cr4_5, 8, false, true);
|
||||
// Reference: ~2.34 s for PL=255 at SF10/BW125/CR4/5.
|
||||
approx(toa_us(&c, 255), 2_335_000, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toa_ldro_activates_at_sf11_and_sf12_bw125() {
|
||||
// T_sym > 16 ms → DE=1. SF11/BW125: T_sym = 16.384 ms. SF10/BW125: 8.192 ms.
|
||||
let sf10 = cfg(10, LoRaBandwidth::Khz125, LoRaCodingRate::Cr4_5, 8, false, true);
|
||||
let sf11 = cfg(11, LoRaBandwidth::Khz125, LoRaCodingRate::Cr4_5, 8, false, true);
|
||||
let sf12 = cfg(12, LoRaBandwidth::Khz125, LoRaCodingRate::Cr4_5, 8, false, true);
|
||||
// Crude check: each step roughly doubles airtime (LDRO doesn't
|
||||
// break the doubling trend — it just shifts constants).
|
||||
let t10 = toa_us(&sf10, 80);
|
||||
let t11 = toa_us(&sf11, 80);
|
||||
let t12 = toa_us(&sf12, 80);
|
||||
assert!(t11 > t10 * 15 / 10, "SF11 should be > 1.5x SF10, got {} vs {}", t11, t10);
|
||||
assert!(t12 > t11 * 15 / 10, "SF12 should be > 1.5x SF11, got {} vs {}", t12, t11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toa_assumed_peer_80b_matches_plan_table() {
|
||||
let mk = |sf, bw| cfg(sf, bw, LoRaCodingRate::Cr4_5, 8, false, true);
|
||||
// Plan table values (approximate, within 5%).
|
||||
approx(toa_us(&mk(7, LoRaBandwidth::Khz500), 80), 36_000, 5);
|
||||
approx(toa_us(&mk(7, LoRaBandwidth::Khz250), 80), 72_000, 5);
|
||||
approx(toa_us(&mk(7, LoRaBandwidth::Khz125), 80), 144_000, 5);
|
||||
approx(toa_us(&mk(8, LoRaBandwidth::Khz125), 80), 257_000, 5);
|
||||
approx(toa_us(&mk(9, LoRaBandwidth::Khz125), 80), 452_000, 5);
|
||||
approx(toa_us(&mk(10, LoRaBandwidth::Khz125), 80), 862_000, 5);
|
||||
approx(toa_us(&mk(11, LoRaBandwidth::Khz125), 80), 1_806_000, 5);
|
||||
approx(toa_us(&mk(12, LoRaBandwidth::Khz125), 80), 3_285_000, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toa_no_underflow_at_small_pl_sf12_implicit_no_crc() {
|
||||
// num = 8·PL − 4·SF + 28 + 16·CRC − 20·H goes negative here; the
|
||||
// i64 + max(0) guard must kick in without panicking or wrapping.
|
||||
let c = cfg(12, LoRaBandwidth::Khz125, LoRaCodingRate::Cr4_5, 8, true, false);
|
||||
let t = toa_us(&c, 1);
|
||||
// Only 8 payload symbols survive (the formula floors to 0 extra);
|
||||
// should be preamble + 8·T_sym ≈ 401 + 262 = 663 ms roughly.
|
||||
approx(t, 663_000, 5);
|
||||
}
|
||||
|
||||
// Retry-count derivation table from the plan.
|
||||
|
||||
#[test]
|
||||
fn retries_sf7_bw500_hits_hard_cap() {
|
||||
let (n, s) = cad_retries(36_000);
|
||||
assert_eq!(n, 10);
|
||||
assert_eq!(s, 36_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retries_sf10_bw125() {
|
||||
// T_assumed ≈ 862 ms; budget 2.5 s ⇒ 2 sleeps ⇒ 3 retries.
|
||||
let (n, _) = cad_retries(862_000);
|
||||
assert_eq!(n, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retries_sf11_bw125() {
|
||||
// T_assumed ≈ 1.81 s; budget 2.5 s ⇒ 1 sleep ⇒ 2 retries.
|
||||
let (n, _) = cad_retries(1_806_000);
|
||||
assert_eq!(n, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retries_sf12_bw125_degraded() {
|
||||
// T_assumed ≈ 3.29 s > 2.5 s budget ⇒ 0 sleeps ⇒ 1 retry (bail on busy).
|
||||
let (n, _) = cad_retries(3_285_000);
|
||||
assert_eq!(n, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retries_zero_delay_falls_back_to_single_cad() {
|
||||
// Unconfigured safety: avoid divide-by-zero.
|
||||
let (n, s) = cad_retries(0);
|
||||
assert_eq!(n, 1);
|
||||
assert_eq!(s, 0);
|
||||
}
|
||||
|
||||
// Jitter sanity.
|
||||
|
||||
#[test]
|
||||
fn jitter_stays_within_pm_20_pct() {
|
||||
let base = 1_000_000u32;
|
||||
// Exhaustive over the low byte — enough to cover the `% span` range.
|
||||
let mut saw_low = false;
|
||||
let mut saw_high = false;
|
||||
for e in 0u32..=u16::MAX as u32 {
|
||||
let j = jittered_sleep_us(base, e);
|
||||
// ±20% ⇒ [800_000, 1_200_000].
|
||||
assert!(
|
||||
(800_000..=1_200_000).contains(&j),
|
||||
"jitter {} us out of ±20% bounds for entropy {}",
|
||||
j,
|
||||
e
|
||||
);
|
||||
if j <= 850_000 {
|
||||
saw_low = true;
|
||||
}
|
||||
if j >= 1_150_000 {
|
||||
saw_high = true;
|
||||
}
|
||||
}
|
||||
assert!(saw_low && saw_high, "jitter didn't span the expected range");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jitter_zero_base_is_zero() {
|
||||
for e in [0u32, 1, 42, 0xDEAD_BEEF] {
|
||||
assert_eq!(jittered_sleep_us(0, e), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-8
@@ -29,6 +29,7 @@ mod driver;
|
||||
mod hal;
|
||||
#[cfg(not(test))]
|
||||
mod host;
|
||||
mod lbt;
|
||||
mod protocol;
|
||||
#[cfg(not(test))]
|
||||
mod radio;
|
||||
@@ -48,7 +49,7 @@ cfg_if::cfg_if! {
|
||||
if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] {
|
||||
use defmt_rtt as _;
|
||||
use panic_probe as _;
|
||||
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2"))] {
|
||||
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2", feature = "lilygo_tbeam_supreme", feature = "lilygo_tbeam"))] {
|
||||
use esp_backtrace as _;
|
||||
use esp_println as _;
|
||||
}
|
||||
@@ -72,7 +73,7 @@ cfg_if::cfg_if! {
|
||||
async fn main(spawner: Spawner) {
|
||||
run(spawner).await;
|
||||
}
|
||||
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2"))] {
|
||||
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4", feature = "elecrow_thinknode_m2", feature = "lilygo_tbeam_supreme", feature = "lilygo_tbeam"))] {
|
||||
#[esp_rtos::main]
|
||||
async fn main(spawner: Spawner) {
|
||||
run(spawner).await;
|
||||
@@ -140,6 +141,8 @@ fn build_info(mac: [u8; 6]) -> Info {
|
||||
let (fmin, fmax) = <board::Board as LoRaBoard>::freq_range_hz();
|
||||
let chip_id = <board::Board as LoRaBoard>::radio_chip_id();
|
||||
let (tx_min, tx_max) = board::Board::TX_POWER_RANGE;
|
||||
let supported_sf = <board::Board as LoRaBoard>::supported_sf_bitmap();
|
||||
let supported_bw = <board::Board as LoRaBoard>::supported_bw_bitmap();
|
||||
|
||||
let mut mcu_uid = [0u8; MAX_MCU_UID_LEN];
|
||||
mcu_uid[..6].copy_from_slice(&mac);
|
||||
@@ -152,12 +155,8 @@ fn build_info(mac: [u8; 6]) -> Info {
|
||||
fw_patch: parse_u8_or(env!("CARGO_PKG_VERSION_PATCH"), 0),
|
||||
radio_chip_id: chip_id,
|
||||
capability_bitmap: cap::LORA | cap::CAD_BEFORE_TX,
|
||||
// SX126x supports SF5–SF12 (LLCC68 tops out at SF11 — that board
|
||||
// can override this via a board-level trait method later).
|
||||
supported_sf_bitmap: 0x1FE0,
|
||||
// Sub-GHz BW enum values 0..9. SX128x would flip 10..13; add as
|
||||
// boards come online.
|
||||
supported_bw_bitmap: 0x03FF,
|
||||
supported_sf_bitmap: supported_sf,
|
||||
supported_bw_bitmap: supported_bw,
|
||||
max_payload_bytes: crate::protocol::MAX_OTA_PAYLOAD as u16,
|
||||
// Real channel depths from channel.rs.
|
||||
rx_queue_capacity: 32,
|
||||
|
||||
+141
-17
@@ -47,7 +47,11 @@ use crate::protocol::{
|
||||
SetConfigResultCode, TxDonePayload, TxResult, MAX_OTA_PAYLOAD,
|
||||
};
|
||||
|
||||
const CAD_MAX_RETRIES: u8 = 10;
|
||||
// LBT retry timing lives in `crate::lbt` — the math is pure and
|
||||
// host-testable, so it's split out of this (HW-gated) module.
|
||||
use crate::lbt::{
|
||||
cad_retries, jittered_sleep_us, toa_us, CAD_ASSUMED_PEER_PAYLOAD_B, CAD_BUDGET_US,
|
||||
};
|
||||
|
||||
type LoRaDriver = LoRa<RadioDriver, Delay>;
|
||||
|
||||
@@ -102,8 +106,26 @@ pub enum RadioInput {
|
||||
},
|
||||
ChannelBusy,
|
||||
TransmitFailed,
|
||||
|
||||
/// Internal: emitted by `bump_resume_retries` when another
|
||||
/// `try_start_rx_hw` attempt is allowed. Drives a self-transition on
|
||||
/// `ResumingRx` so the entry action runs again after a short backoff.
|
||||
ResumeRxRetry,
|
||||
/// Internal: emitted by `bump_resume_retries` after
|
||||
/// `MAX_RESUME_RETRIES` attempts have failed — the chip is genuinely
|
||||
/// wedged; fall through to `Idle` so the host can reconfigure.
|
||||
ResumeRxExhausted,
|
||||
}
|
||||
|
||||
/// How many back-to-back `try_start_rx_hw` failures to tolerate in
|
||||
/// `ResumingRx` before giving up and parking in `Idle`. 5 × 25 ms ≈
|
||||
/// 125 ms worst-case resume window, enough to ride out SX1262
|
||||
/// post-TX timing flukes without pegging the CPU on a dead chip.
|
||||
const MAX_RESUME_RETRIES: u8 = 5;
|
||||
|
||||
/// Backoff between `ResumingRx` retry attempts.
|
||||
const RESUME_RX_RETRY_BACKOFF_MS: u64 = 25;
|
||||
|
||||
// ── Context ─────────────────────────────────────────────────────────
|
||||
|
||||
pub struct RadioContext {
|
||||
@@ -130,6 +152,15 @@ pub struct RadioContext {
|
||||
/// Tag of a pending `RX_START`, pulled by `respond_ok_start_rx` /
|
||||
/// `respond_err_start_rx` once the chip reports back.
|
||||
pub pending_rx_start_tag: Option<u16>,
|
||||
/// Number of consecutive `try_start_rx_hw` failures while in
|
||||
/// `ResumingRx`. Reset on any `StartRxOk`. See `MAX_RESUME_RETRIES`
|
||||
/// for the limit before falling through to `Idle`.
|
||||
pub resume_retries: u8,
|
||||
/// Per-CAD-retry sleep in microseconds: time-on-air for a
|
||||
/// `CAD_ASSUMED_PEER_PAYLOAD_B`-byte packet under the current
|
||||
/// config. Recomputed on every successful `SET_CONFIG`; `0` while
|
||||
/// `Unconfigured` (the CAD path is unreachable without a config).
|
||||
pub cad_retry_delay_us: u32,
|
||||
}
|
||||
|
||||
impl RadioContext {
|
||||
@@ -199,7 +230,12 @@ statechart! {
|
||||
during: next_packet(lora, rx_buf, config);
|
||||
on(CmdRxStart) => respond_ok;
|
||||
on(PacketReceived(rx: RxPayload)) => record_packet;
|
||||
on(RxFailed) => announce_rx_failed, Idle;
|
||||
// Transient driver error during `next_packet` — re-arm
|
||||
// the chip via ResumingRx rather than parking in Idle
|
||||
// (which would bounce the OLED and leave the radio
|
||||
// waiting for a client-initiated RX_START that will
|
||||
// never come because clients already think RX is up).
|
||||
on(RxFailed) => ResumingRx;
|
||||
on(CmdRxStop) => stop_rx_hw, respond_ok;
|
||||
on(CmdRxStop) => Idle;
|
||||
on(CmdTx) => TransmittingFromRx;
|
||||
@@ -212,10 +248,18 @@ statechart! {
|
||||
on(ConfigApplied) => ResumingRx;
|
||||
}
|
||||
|
||||
// Bounded-retry RX re-arm. Entered after every TX while
|
||||
// already receiving, on RX driver errors, and on mid-session
|
||||
// SET_CONFIG. If `try_start_rx_hw` succeeds we land cleanly
|
||||
// in Receiving; if it fails we back off and retry, only
|
||||
// conceding to Idle after MAX_RESUME_RETRIES attempts (chip
|
||||
// is genuinely wedged at that point).
|
||||
state ResumingRx {
|
||||
entry: try_start_rx_hw;
|
||||
on(StartRxOk) => Receiving;
|
||||
on(StartRxFailed) => Idle;
|
||||
on(StartRxOk) => reset_resume_retries, Receiving;
|
||||
on(StartRxFailed) => bump_resume_retries;
|
||||
on(ResumeRxRetry) => ResumingRx;
|
||||
on(ResumeRxExhausted) => Idle;
|
||||
}
|
||||
|
||||
state Transmitting {
|
||||
@@ -224,7 +268,7 @@ statechart! {
|
||||
default(TransmittingFromIdle);
|
||||
|
||||
state TransmittingFromIdle {
|
||||
during: perform_tx(lora, pending_tx);
|
||||
during: perform_tx(lora, pending_tx, cad_retry_delay_us);
|
||||
on(TransmitDone { airtime_us: u32 }) => respond_tx_done;
|
||||
on(TransmitDone) => Idle;
|
||||
on(ChannelBusy) => respond_tx_channel_busy, Idle;
|
||||
@@ -232,11 +276,15 @@ statechart! {
|
||||
}
|
||||
|
||||
state TransmittingFromRx {
|
||||
during: perform_tx(lora, pending_tx);
|
||||
during: perform_tx(lora, pending_tx, cad_retry_delay_us);
|
||||
on(TransmitDone { airtime_us: u32 }) => respond_tx_done;
|
||||
on(TransmitDone) => ResumingRx;
|
||||
// Even on failure, we were receiving before the TX —
|
||||
// return to RX (the client still gets a TX_FAILED
|
||||
// reply via respond_tx_failed). Dropping to Idle here
|
||||
// would break the "RX session stays up" invariant.
|
||||
on(ChannelBusy) => respond_tx_channel_busy, ResumingRx;
|
||||
on(TransmitFailed) => respond_tx_failed, Idle;
|
||||
on(TransmitFailed) => respond_tx_failed, ResumingRx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,17 +377,32 @@ async fn next_packet(
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_tx(lora: &mut LoRaDriver, pending: &mut Option<PendingTx>) -> RadioInput {
|
||||
async fn perform_tx(
|
||||
lora: &mut LoRaDriver,
|
||||
pending: &mut Option<PendingTx>,
|
||||
cad_retry_delay_us: &mut u32,
|
||||
) -> RadioInput {
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(6);
|
||||
let Some(tx) = pending.take() else {
|
||||
return RadioInput::TransmitFailed;
|
||||
};
|
||||
// Scale CAD retry count to the cached per-retry sleep: fewer retries
|
||||
// when each one-full-packet wait is expensive (slow SFs). When the
|
||||
// one-packet wait exceeds the total budget, the helper returns
|
||||
// `retries = 1` — a single CAD, bail on busy, no sleep.
|
||||
let (retries, sleep_us) = cad_retries(*cad_retry_delay_us);
|
||||
let start = Instant::now();
|
||||
match do_tx(lora, &tx).await {
|
||||
match do_tx(lora, &tx, retries, sleep_us).await {
|
||||
Ok(TxOutcome::Transmitted) => RadioInput::TransmitDone {
|
||||
airtime_us: Instant::now().duration_since(start).as_micros() as u32,
|
||||
},
|
||||
Ok(TxOutcome::ChannelBusy) => RadioInput::ChannelBusy,
|
||||
Err(_) => RadioInput::TransmitFailed,
|
||||
Err(_) => {
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(15);
|
||||
RadioInput::TransmitFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,11 +445,20 @@ impl RadioActions for RadioActionContext<'_> {
|
||||
if let Err(_e) = reconfigure_radio(&mut self.lora, &cfg).await {
|
||||
// Hardware failure: drop to Unconfigured (spec §6.3).
|
||||
self.config = None;
|
||||
self.cad_retry_delay_us = 0;
|
||||
self.send(tag, DeviceMessage::Err(ErrorCode::ERadio)).await;
|
||||
let _ = self.emit(RadioInput::ConfigFailedRadio);
|
||||
return;
|
||||
}
|
||||
self.config = Some(cfg);
|
||||
self.cad_retry_delay_us = toa_us(&cfg, CAD_ASSUMED_PEER_PAYLOAD_B);
|
||||
if self.cad_retry_delay_us > CAD_BUDGET_US {
|
||||
defmt::debug!(
|
||||
"LBT degraded: assumed-packet ToA {=u32} us > budget {=u32} us; single-CAD-and-bail",
|
||||
self.cad_retry_delay_us,
|
||||
CAD_BUDGET_US
|
||||
);
|
||||
}
|
||||
self.packets_dropped = 0;
|
||||
self.announce(RadioEvent::ConfigChanged(cfg));
|
||||
let result = SetConfigResult {
|
||||
@@ -465,6 +537,28 @@ impl RadioActions for RadioActionContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn reset_resume_retries(&mut self) {
|
||||
self.resume_retries = 0;
|
||||
}
|
||||
|
||||
async fn bump_resume_retries(&mut self) {
|
||||
self.resume_retries = self.resume_retries.saturating_add(1);
|
||||
if self.resume_retries >= MAX_RESUME_RETRIES {
|
||||
warn!(
|
||||
"ResumingRx: {} failed attempts, giving up to Idle",
|
||||
self.resume_retries
|
||||
);
|
||||
self.resume_retries = 0;
|
||||
let _ = self.emit(RadioInput::ResumeRxExhausted);
|
||||
return;
|
||||
}
|
||||
// Short backoff before the next `try_start_rx_hw` cycle. The
|
||||
// self-transition on ResumingRx (driven by ResumeRxRetry) will
|
||||
// re-run the entry action.
|
||||
embassy_time::Timer::after_millis(RESUME_RX_RETRY_BACKOFF_MS).await;
|
||||
let _ = self.emit(RadioInput::ResumeRxRetry);
|
||||
}
|
||||
|
||||
async fn respond_ok(&mut self) {
|
||||
let tag = self.incoming_tag;
|
||||
self.send(tag, DeviceMessage::Ok(OkPayload::Empty)).await;
|
||||
@@ -530,10 +624,6 @@ impl RadioActions for RadioActionContext<'_> {
|
||||
self.announce(RadioEvent::PacketTx);
|
||||
}
|
||||
|
||||
async fn announce_rx_failed(&mut self) {
|
||||
self.announce(RadioEvent::Idle);
|
||||
}
|
||||
|
||||
// ── Packet bookkeeping ────────────────────────────────────────────
|
||||
|
||||
async fn record_packet(&mut self, rx: RxPayload) {
|
||||
@@ -644,19 +734,39 @@ async fn reconfigure_radio(lora: &mut LoRaDriver, cfg: &LoRaConfig) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_tx(lora: &mut LoRaDriver, tx: &PendingTx) -> Result<TxOutcome, RadioError> {
|
||||
async fn do_tx(
|
||||
lora: &mut LoRaDriver,
|
||||
tx: &PendingTx,
|
||||
retries: u8,
|
||||
sleep_us: u32,
|
||||
) -> Result<TxOutcome, RadioError> {
|
||||
let mdltn = to_lora_mod_params(lora, &tx.config)?;
|
||||
|
||||
if !tx.skip_cad {
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(7);
|
||||
lora.prepare_for_cad(&mdltn).await?;
|
||||
// Reaches here → prepare_for_cad (SPI setup) completed.
|
||||
// Hang past here means lora.cad() is waiting on DIO1.
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(19);
|
||||
let mut saw_activity = false;
|
||||
for _ in 0..CAD_MAX_RETRIES {
|
||||
for i in 0..retries {
|
||||
if !lora.cad(&mdltn).await? {
|
||||
saw_activity = false;
|
||||
break;
|
||||
}
|
||||
saw_activity = true;
|
||||
embassy_time::Timer::after_millis(10).await;
|
||||
// Skip the trailing sleep: if the last CAD was busy, we're
|
||||
// about to return ChannelBusy, so sleeping + re-preparing
|
||||
// is wasted work.
|
||||
if i + 1 == retries {
|
||||
break;
|
||||
}
|
||||
let entropy = embassy_time::Instant::now().as_ticks() as u32;
|
||||
let jittered_us = jittered_sleep_us(sleep_us, entropy);
|
||||
embassy_time::Timer::after(embassy_time::Duration::from_micros(jittered_us as u64))
|
||||
.await;
|
||||
lora.prepare_for_cad(&mdltn).await?;
|
||||
}
|
||||
if saw_activity {
|
||||
@@ -671,9 +781,15 @@ async fn do_tx(lora: &mut LoRaDriver, tx: &PendingTx) -> Result<TxOutcome, Radio
|
||||
tx.config.iq_invert,
|
||||
&mdltn,
|
||||
)?;
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(8);
|
||||
lora.prepare_for_tx(&mdltn, &mut tx_pkt, tx.config.tx_power_dbm as i32, &tx.data)
|
||||
.await?;
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(9);
|
||||
lora.tx().await?;
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(10);
|
||||
Ok(TxOutcome::Transmitted)
|
||||
}
|
||||
|
||||
@@ -724,13 +840,19 @@ pub async fn radio_task(
|
||||
events: &'static RadioEventChannel,
|
||||
info: &'static Info,
|
||||
) {
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(1);
|
||||
let lora = match LoRa::new(parts.driver, false, parts.delay).await {
|
||||
Ok(l) => {
|
||||
info!("radio initialized");
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(2);
|
||||
l
|
||||
}
|
||||
Err(e) => {
|
||||
error!("radio init failed: {}", e);
|
||||
#[cfg(feature = "debug-checkpoint")]
|
||||
crate::debug_blink::set(16);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -748,6 +870,8 @@ pub async fn radio_task(
|
||||
incoming_tag: 0,
|
||||
last_tx_tag: None,
|
||||
pending_rx_start_tag: None,
|
||||
resume_retries: 0,
|
||||
cad_retry_delay_us: 0,
|
||||
};
|
||||
|
||||
static RADIO_INTERNAL_CH: embassy_sync::channel::Channel<
|
||||
|
||||
Reference in new issue
Block a user