Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
688938d8e8
|
||
|
|
e9360c28ba
|
||
|
|
d7a1ee86d0
|
||
|
|
0b9f9ee882
|
||
|
|
04dc58c943
|
||
|
|
948589ae5d
|
||
|
|
aca4c3cffd
|
||
|
|
188e445e80
|
||
|
|
333dc1bb3b
|
No files matched your search
+151
@@ -7,6 +7,157 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.5.2] - 2026-05-11
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refreshed `Cargo.lock` to pull in the latest compatible patch/minor
|
||||||
|
releases of dependencies. Notable bumps:
|
||||||
|
- `donglora-protocol` 1.1.0 → 1.2.0
|
||||||
|
- `esp-hal` 1.1.0 → 1.1.1
|
||||||
|
- `heapless` 0.9.2 → 0.9.3 (propagates through embassy + esp crates)
|
||||||
|
- `embassy-usb-driver` 0.2.0 → 0.2.1
|
||||||
|
- `embassy-usb-synopsys-otg` 0.3.2 → 0.3.3
|
||||||
|
- `enumset` 1.1.10 → 1.1.12
|
||||||
|
- `digest` 0.11.2 → 0.11.3, `hashbrown` 0.17.0 → 0.17.1,
|
||||||
|
`hybrid-array` 0.4.11 → 0.4.12, `cc` 1.2.61 → 1.2.62
|
||||||
|
No source changes; all 10 board targets compile clean and host-side
|
||||||
|
protocol tests pass.
|
||||||
|
|
||||||
|
## [1.5.1] - 2026-05-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`LoRaConfig.sync_word` is now actually applied to the SX126x.**
|
||||||
|
`reconfigure_radio` accepted the host's `sync_word` field in name
|
||||||
|
only — the value was stored in `self.config` but never written to
|
||||||
|
the chip's `LoRaSyncwordMSB`/`LoRaSyncwordLSB` register pair
|
||||||
|
(0x0740/0x0741, SX1261/2 datasheet Rev 2.2 §13.4.4). The chip sat
|
||||||
|
on whatever `LoRa::new(_, false, _)` set at cold init —
|
||||||
|
`LORAWAN_PRIVATE_SYNCWORD` (byte `0x12`, register pair `0x1424`).
|
||||||
|
Everyone got byte `0x12` regardless of the requested value. Worked
|
||||||
|
by accident for MeshCore (which uses byte `0x12`); silently broke
|
||||||
|
any host trying to talk to a non-MeshCore network. Fix wires
|
||||||
|
`cfg.sync_word` through to `lora.set_lora_sync_word(byte)` after
|
||||||
|
`enter_standby`, using the new lora-phy API from
|
||||||
|
[lora-rs/lora-rs#433](https://github.com/lora-rs/lora-rs/pull/433).
|
||||||
|
The `u16` protocol field is decoded back to its underlying byte
|
||||||
|
(`byte = (hi & 0xF0) | (lo >> 4)` after `to_be_bytes()`), the
|
||||||
|
inverse of lora-phy's `convert_sync_word` nibble-expansion. Honors
|
||||||
|
SX1261/2 §8.3.1 (BUSY-low precondition) via the existing
|
||||||
|
`ensure_ready + conditional set_standby` preamble in
|
||||||
|
`LoRa::set_lora_sync_word`.
|
||||||
|
- **First TX after `SET_CONFIG` no longer fails with `ERR(ERadio)` on
|
||||||
|
a freshly-booted chip.** The chart's `prep_for_cad_act`
|
||||||
|
(`TxCadPrep` entry) and `prep_for_tx_act` (`TxPrep` entry) both
|
||||||
|
read `ctx.mdltn` and emit `TransmitFailed` → `ERR(ERadio)` if it's
|
||||||
|
`None`. But `ctx.mdltn` was only ever assigned on the RX path —
|
||||||
|
`try_start_rx_hw` (`RxArming` entry) and `cache_rx_pkt`
|
||||||
|
(`RxSession` entry). `reconfigure_radio` already called
|
||||||
|
`to_lora_mod_params(lora, cfg)` during `SET_CONFIG` but discarded
|
||||||
|
the result via `let _ = …?;` (the dropped comment claimed image
|
||||||
|
calibration; the function is pure compute on SX126x). Hidden in
|
||||||
|
practice by any prior `RX_START` (ai-bot, `ping_pong --role rx`,
|
||||||
|
etc.) leaving `ctx.mdltn` populated until the next reflash. Fix
|
||||||
|
threads the `ModulationParams` out of `reconfigure_radio` as its
|
||||||
|
return value, and `apply_set_config` caches it in `self.mdltn`
|
||||||
|
alongside `self.config`. First TX on a fresh boot now works
|
||||||
|
without a preceding `RX_START`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`lora-phy` `swaits/lora-rs#fixed` bumped from `e8a52ebd` →
|
||||||
|
`043d7659`.** The fork's `fixed` merge now includes a new
|
||||||
|
`feat/set-sync-word` branch alongside the three pre-existing
|
||||||
|
`fix/*` branches.
|
||||||
|
[lora-rs/lora-rs#433](https://github.com/lora-rs/lora-rs/pull/433)
|
||||||
|
adds the runtime `LoRa::set_lora_sync_word(u8)` API the radio fix
|
||||||
|
above depends on (was unavailable in stock lora-phy; the only
|
||||||
|
public sync-word entry points were `with_syncword` /
|
||||||
|
`new(_, public_network: bool, _)` at construction time). Strictly
|
||||||
|
additive in lora-phy: pure addition, no behavioral change to
|
||||||
|
`init_lora` / `with_syncword` / any existing path.
|
||||||
|
- **`hsmc` 0.5 → 0.6.** Patch bump tracked via `cargo update -p
|
||||||
|
hsmc`; no behavior change in this crate's usage. Pulled in
|
||||||
|
separately from the radio fixes so the dep churn doesn't blur the
|
||||||
|
fix commits.
|
||||||
|
|
||||||
|
## [1.5.0] - 2026-04-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Heltec Mesh Node T114 board support** (nRF52840 + SX1262 + 1.14"
|
||||||
|
240×135 ST7789 color TFT). The first color display board in the
|
||||||
|
project, and the third nRF52840 + SX1262 board alongside the
|
||||||
|
RAK4631 and Wio Tracker L1.
|
||||||
|
- `VEXT_ENABLE` (P0.21) driven HIGH at boot to power the SX1262 +
|
||||||
|
TFT rail. Without this, lora-phy init succeeds at the SPI level
|
||||||
|
but TX_DONE never fires.
|
||||||
|
- `VTFT_CTRL` on P0.03 (active-LOW); PWM backlight on P0.15
|
||||||
|
(active-LOW, inverted) for smooth dim/bright transitions.
|
||||||
|
- TFT SPI2 (SCK P1.08 / MOSI P1.09) at 8 MHz over EasyDMA, async
|
||||||
|
`ExclusiveDevice` from `embedded-hal-bus`.
|
||||||
|
- Linker uses the shared `ld/nrf52840-memory.x` so the S140
|
||||||
|
SoftDevice region (0x01000–0x26000) is preserved per project
|
||||||
|
policy.
|
||||||
|
|
||||||
|
- **`BoardDisplay` trait** (`src/display.rs`) bundles `render_*`,
|
||||||
|
`present()`, `set_bright()`, and `set_dim()`. Replaces the previous
|
||||||
|
`DrawTarget<Color = BinaryColor>` + `DisplayBrightness` combo as
|
||||||
|
the `LoRaBoard::DisplayDriver` bound, so each board renders in its
|
||||||
|
native pixel format and resolution without aliasing or
|
||||||
|
color-quantization round-trips. Existing mono OLED drivers
|
||||||
|
(`Sh1106`, `Ssd1306Async`) get adapter impls that delegate to the
|
||||||
|
existing `display::render` module — no behavior change for any
|
||||||
|
previously supported board.
|
||||||
|
|
||||||
|
- **Async ST7789 driver** (`src/driver/st7789.rs`) wrapping
|
||||||
|
`lcd-async` (async fork of `mipidsi`) with a 64,800-byte raw-byte
|
||||||
|
framebuffer in `.bss`. Renders via
|
||||||
|
`lcd_async::raw_framebuf::RawFrameBuf<Rgb565, _>` and presents via
|
||||||
|
a single async DMA blit (`panel.show_raw_data(...).await`); the
|
||||||
|
await yields the executor for the ~65 ms SPI transfer so the radio
|
||||||
|
task isn't starved during repaints.
|
||||||
|
|
||||||
|
- **Native 240×135 RGB565 renderer** (`src/display/render_color.rs`)
|
||||||
|
with splash, dashboard, and sparkline parity with the mono path,
|
||||||
|
plus a shared QR-code blit (`src/display/qr.rs`) generic over
|
||||||
|
`PixelColor`. The blit re-centers the 64×64 bitmap (cells in cols
|
||||||
|
0..50 with ~14 px right-side padding), so both mono and color
|
||||||
|
splash now show the QR with 7 px quiet zones on all four sides
|
||||||
|
(was visibly left-shifted on mono).
|
||||||
|
|
||||||
|
- **Shared content types** (`BoardInfo`, `DashboardCtx`) decouple
|
||||||
|
*what* to draw from *how* each board renders it. `create_display()`
|
||||||
|
now takes the board's full `DisplayParts` bundle (was just the I2C
|
||||||
|
bus), so non-I2C displays can carry SPI + control pins through.
|
||||||
|
|
||||||
|
- **IDF app descriptor on every ESP image.** New
|
||||||
|
`esp-bootloader-esp-idf` 0.5 dependency (chip-feature-gated like
|
||||||
|
the rest of the `esp-*` stack) plus `esp_app_desc!()` in
|
||||||
|
`src/main.rs`, which emits the `.flash.appdesc` section that
|
||||||
|
`espflash` v4 requires in every ESP binary.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **ESP build modernized to upstream crates.io.** Removed the
|
||||||
|
`[patch.crates-io]` block that pinned `esp-hal`, `esp-rtos`,
|
||||||
|
`esp-backtrace`, and `esp-println` to `esp-rs/esp-hal#main`. The
|
||||||
|
versions tracked there (`esp-hal 1.1`, `esp-rtos 0.3`,
|
||||||
|
`esp-backtrace 0.19`, `esp-println 0.17`) have all reached
|
||||||
|
crates.io with the `embassy-executor` 0.10 compatibility we needed,
|
||||||
|
so the local fork pins are no longer required. `cargo:espflash`
|
||||||
|
bumped to v4 in `mise.toml` to match.
|
||||||
|
|
||||||
|
- **`hsmc` 0.5.0 → 0.5.1.** Patch bump pulled in transparently with
|
||||||
|
the lockfile refresh; no behavior change.
|
||||||
|
|
||||||
|
- **Documentation consolidated.** `PROTOCOL.md` removed from the
|
||||||
|
firmware repo (it now lives canonically in the spec repo, linked
|
||||||
|
from `README.md`). `BLE_MUX_DESIGN_DOC.md` moved to
|
||||||
|
`docs/ble_mux_design.md` so all firmware design docs sit under
|
||||||
|
`docs/`.
|
||||||
|
|
||||||
## [1.4.1] - 2026-04-29
|
## [1.4.1] - 2026-04-29
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+148
-56
@@ -167,9 +167,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.61"
|
version = "1.2.62"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -210,7 +210,7 @@ checksum = "ac78aa94ce13e432b332a4d1bf2eff167d3a2520188ee05b337180a42fd2e62e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cipher",
|
"cipher",
|
||||||
"dbl",
|
"dbl",
|
||||||
"digest 0.11.2",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -537,9 +537,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.11.2"
|
version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.12.0",
|
"block-buffer 0.12.0",
|
||||||
"crypto-common 0.2.1",
|
"crypto-common 0.2.1",
|
||||||
@@ -586,7 +586,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donglora"
|
name = "donglora"
|
||||||
version = "1.4.1"
|
version = "1.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cortex-m",
|
"cortex-m",
|
||||||
@@ -608,13 +608,16 @@ dependencies = [
|
|||||||
"embedded-graphics",
|
"embedded-graphics",
|
||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
"embedded-hal-async",
|
"embedded-hal-async",
|
||||||
|
"embedded-hal-bus",
|
||||||
"embedded-io-async 0.7.0",
|
"embedded-io-async 0.7.0",
|
||||||
"esp-backtrace",
|
"esp-backtrace",
|
||||||
|
"esp-bootloader-esp-idf",
|
||||||
"esp-hal",
|
"esp-hal",
|
||||||
"esp-println",
|
"esp-println",
|
||||||
"esp-rtos",
|
"esp-rtos",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"hsmc",
|
"hsmc",
|
||||||
|
"lcd-async",
|
||||||
"lora-phy",
|
"lora-phy",
|
||||||
"minijinja",
|
"minijinja",
|
||||||
"owo-colors",
|
"owo-colors",
|
||||||
@@ -627,12 +630,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donglora-protocol"
|
name = "donglora-protocol"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bae9e9e5a8cebc6b4167f9ef696b61ce25b94c6d8283dbe1b2e97075a481b9d"
|
checksum = "0eea2a87a5ad31629140accc2bae6c931434bf1946706f2184dec9b3d356505d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"ucobs",
|
"ucobs",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -850,7 +853,7 @@ dependencies = [
|
|||||||
"embedded-io-async 0.7.0",
|
"embedded-io-async 0.7.0",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -885,7 +888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693"
|
checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"embassy-executor-timer-queue",
|
"embassy-executor-timer-queue",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -902,30 +905,32 @@ dependencies = [
|
|||||||
"embassy-time",
|
"embassy-time",
|
||||||
"embassy-usb-driver",
|
"embassy-usb-driver",
|
||||||
"embedded-io-async 0.7.0",
|
"embedded-io-async 0.7.0",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"ssmarshal",
|
"ssmarshal",
|
||||||
"usbd-hid",
|
"usbd-hid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embassy-usb-driver"
|
name = "embassy-usb-driver"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17119855ccc2d1f7470a39756b12068454ae27a3eabb037d940b5c03d9c77b7a"
|
checksum = "6a189cbf1d8ab3c591f88632f85c1df892b6ab04116037cbee39dc37cbdcd79f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"embedded-io-async 0.6.1",
|
"embedded-io-async 0.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embassy-usb-synopsys-otg"
|
name = "embassy-usb-synopsys-otg"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cbe46f4083109c7ea12a03ca61095d1e87c76fec52c7ca9ee06a42935606dacb"
|
checksum = "fb205e26d59e483e8c48f91f637ec217ec7e2cfb0b61d50482301b991b4ff431"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"embassy-sync 0.8.0",
|
"embassy-sync 0.8.0",
|
||||||
|
"embassy-time",
|
||||||
"embassy-usb-driver",
|
"embassy-usb-driver",
|
||||||
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -985,6 +990,17 @@ dependencies = [
|
|||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-hal-bus"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "513e0b3a8fb7d3013a8ae17a834283f170deaf7d0eeab0a7c1a36ad4dd356d22"
|
||||||
|
dependencies = [
|
||||||
|
"critical-section",
|
||||||
|
"embedded-hal 1.0.0",
|
||||||
|
"embedded-hal-async",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embedded-hal-nb"
|
name = "embedded-hal-nb"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -1057,18 +1073,18 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enumset"
|
name = "enumset"
|
||||||
version = "1.1.10"
|
version = "1.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634"
|
checksum = "7f96a4a12fe60ac746ae295a1a4ecb5bb02debc20856506c8635288065f142de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"enumset_derive",
|
"enumset_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enumset_derive"
|
name = "enumset_derive"
|
||||||
version = "0.14.0"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce"
|
checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.21.3",
|
"darling 0.21.3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1085,7 +1101,8 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-backtrace"
|
name = "esp-backtrace"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37950e24b2dfd98f1581102d1798281d4d9547af881e6bffc2c2b534c026ec8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
@@ -1093,16 +1110,35 @@ dependencies = [
|
|||||||
"esp-config",
|
"esp-config",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated",
|
||||||
"esp-println",
|
"esp-println",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"riscv",
|
"riscv",
|
||||||
"semihosting",
|
"semihosting",
|
||||||
"xtensa-lx",
|
"xtensa-lx",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esp-bootloader-esp-idf"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35ffc117c3a9859835d89d0e90f5ee9886ce2264a71a849a7a22ab5308f6653c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"defmt 1.0.1",
|
||||||
|
"document-features",
|
||||||
|
"embedded-storage",
|
||||||
|
"esp-config",
|
||||||
|
"esp-hal-procmacros",
|
||||||
|
"esp-metadata-generated",
|
||||||
|
"esp-rom-sys",
|
||||||
|
"jiff",
|
||||||
|
"strum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-config"
|
name = "esp-config"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d9b92fd9cfb0b4f8f1b6219b9763269a335571e307b014903b8201619374b80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"document-features",
|
"document-features",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated",
|
||||||
@@ -1113,8 +1149,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-hal"
|
name = "esp-hal"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bfcf2a0842903717f4663f6a08512c32b0f6b2d7fb7db3c8a6895d2e6d49f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitfield 0.19.4",
|
"bitfield 0.19.4",
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
@@ -1171,7 +1208,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-hal-procmacros"
|
name = "esp-hal-procmacros"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6aebfabb2c21bec45e575e4f6cb6bb7aa8e1b33e7ac45b5dffa0f9d33ff59105"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"document-features",
|
"document-features",
|
||||||
"object",
|
"object",
|
||||||
@@ -1185,12 +1223,14 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-metadata-generated"
|
name = "esp-metadata-generated"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42c2ee95b945a4780796e4359e72c033aed3b45073880e8029458f538532db8a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-println"
|
name = "esp-println"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42dee1e9ac7c3539bf6464db1707b0edd7557168f98278cf3c84fe70e63c6ce6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
@@ -1203,7 +1243,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-riscv-rt"
|
name = "esp-riscv-rt"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66a814ae91452de56a5e74f69aebfee40579511756837d3774a56fd24cf0ab79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"document-features",
|
"document-features",
|
||||||
"riscv",
|
"riscv",
|
||||||
@@ -1213,7 +1254,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-rom-sys"
|
name = "esp-rom-sys"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eae852ccb08971155023d1371c96d5490cbc26860f06aee2d629ef73f1a890c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"document-features",
|
"document-features",
|
||||||
@@ -1225,7 +1267,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-rtos"
|
name = "esp-rtos"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "551f90766e1527edaa0c91e8d559e9e2a60397b545e93357ac61fb31845e5712"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"document-features",
|
"document-features",
|
||||||
@@ -1247,7 +1290,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-sync"
|
name = "esp-sync"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4736bfbbb9e3f6353344e14fc61b6d18d3b877c3286914cf8c0a037be0ed224"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"document-features",
|
"document-features",
|
||||||
@@ -1511,9 +1555,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.0"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heapless"
|
name = "heapless"
|
||||||
@@ -1527,9 +1571,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heapless"
|
name = "heapless"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
|
checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"hash32",
|
"hash32",
|
||||||
@@ -1550,9 +1594,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc"
|
name = "hsmc"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3951a3170ce7aa0f0bf4627ac4cc650857f15449c98e97647fa29fcd19c6d33"
|
checksum = "18bb06fda289e60e17bc7483a222fc6d9d6865b92648f836242b7a79115cc91d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
@@ -1564,9 +1608,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc-macros"
|
name = "hsmc-macros"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b6d97dd1d31fba9e2f96c65c0ce38d7bc1ff59abe775a0ae18e14dfae01d1d1"
|
checksum = "289a7c9f1b677168229c8ad4e992380c3951d0908b52b1d2bfae8ff3c980c35d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1575,9 +1619,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.11"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5"
|
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
@@ -1595,7 +1639,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.17.0",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1644,6 +1688,30 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff"
|
||||||
|
version = "0.2.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-static",
|
||||||
|
"log",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-static"
|
||||||
|
version = "0.2.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keccak"
|
name = "keccak"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -1691,6 +1759,18 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lcd-async"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "359805d2afdff45f3abad226ae27a61ee980309562d14084842c542af29b422a"
|
||||||
|
dependencies = [
|
||||||
|
"embedded-graphics",
|
||||||
|
"embedded-graphics-core",
|
||||||
|
"embedded-hal 1.0.0",
|
||||||
|
"embedded-hal-async",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.186"
|
||||||
@@ -1734,7 +1814,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lora-modulation"
|
name = "lora-modulation"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
]
|
]
|
||||||
@@ -1742,7 +1822,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lora-phy"
|
name = "lora-phy"
|
||||||
version = "3.0.2-alpha"
|
version = "3.0.2-alpha"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"document-features",
|
"document-features",
|
||||||
@@ -1756,7 +1836,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lorawan"
|
name = "lorawan"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"cmac",
|
"cmac",
|
||||||
@@ -1768,13 +1848,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lorawan-device"
|
name = "lorawan-device"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"document-features",
|
"document-features",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"futures",
|
"futures",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"lora-modulation",
|
"lora-modulation",
|
||||||
"lorawan",
|
"lorawan",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
@@ -1783,7 +1863,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lorawan-macros"
|
name = "lorawan-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2058,6 +2138,15 @@ dependencies = [
|
|||||||
"critical-section",
|
"critical-section",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2464,9 +2553,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
@@ -2927,7 +3016,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "xtensa-lx"
|
name = "xtensa-lx"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e012d667b0aa6d2592ace8ef145a98bff3e76cca7a644f4181ecd7a916ed289b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"critical-section",
|
"critical-section",
|
||||||
]
|
]
|
||||||
@@ -2935,7 +3025,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "xtensa-lx-rt"
|
name = "xtensa-lx-rt"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "409a9b4629d429e995cde4dfbd9fe562ccae66f7624514e200733fc5d0ea8905"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"document-features",
|
"document-features",
|
||||||
"xtensa-lx",
|
"xtensa-lx",
|
||||||
@@ -2945,7 +3036,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "xtensa-lx-rt-proc-macros"
|
name = "xtensa-lx-rt-proc-macros"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96fb42cd29c42f8744c74276e9f5bee7b06685bbe5b88df891516d72cb320450"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
+49
-17
@@ -3,7 +3,7 @@ members = ["."]
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "donglora"
|
name = "donglora"
|
||||||
version = "1.4.1"
|
version = "1.5.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
@@ -14,10 +14,6 @@ homepage = "https://github.com/donglora/firmware"
|
|||||||
keywords = ["lora", "radio", "embedded", "usb", "firmware"]
|
keywords = ["lora", "radio", "embedded", "usb", "firmware"]
|
||||||
categories = ["embedded", "hardware-support", "no-std"]
|
categories = ["embedded", "hardware-support", "no-std"]
|
||||||
|
|
||||||
# NOTE: esp-{hal,rtos,backtrace,println} patched from GitHub main (see
|
|
||||||
# [patch.crates-io] below). The patch versions must stay semver-compatible
|
|
||||||
# with the version requirements above, or Cargo silently drops the patch
|
|
||||||
# and falls back to the (older) crates.io release.
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
critical-section = "1.2"
|
critical-section = "1.2"
|
||||||
@@ -33,7 +29,7 @@ embassy-usb-driver = "0.2"
|
|||||||
ucobs = { version = "0.3", features = ["legacy-msrv"] }
|
ucobs = { version = "0.3", features = ["legacy-msrv"] }
|
||||||
heapless = "0.9"
|
heapless = "0.9"
|
||||||
donglora-protocol = "1"
|
donglora-protocol = "1"
|
||||||
hsmc = { version = "0.5", default-features = false, features = ["embassy", "trace-defmt"] }
|
hsmc = { version = "0.6", default-features = false, features = ["embassy", "trace-defmt"] }
|
||||||
# Fork branch carrying CAD IRQ-clear / standby-after-CAD fixes awaiting upstream PRs.
|
# Fork branch carrying CAD IRQ-clear / standby-after-CAD fixes awaiting upstream PRs.
|
||||||
# Switch back to crates.io once changes are upstreamed.
|
# Switch back to crates.io once changes are upstreamed.
|
||||||
lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fixed", features = ["defmt-03"] }
|
lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fixed", features = ["defmt-03"] }
|
||||||
@@ -44,6 +40,21 @@ static_cell = "2.1"
|
|||||||
display-interface = "0.5"
|
display-interface = "0.5"
|
||||||
embedded-graphics = "0.8"
|
embedded-graphics = "0.8"
|
||||||
ssd1306 = { version = "0.10", features = ["async"] }
|
ssd1306 = { version = "0.10", features = ["async"] }
|
||||||
|
# T114 ST7789 TFT (color, async SPI). Optional — only pulled in by the
|
||||||
|
# heltec_mesh_node_t114 feature. Mono OLED boards keep their existing
|
||||||
|
# display-interface + ssd1306/sh1106 stack and don't pay for these.
|
||||||
|
#
|
||||||
|
# `lcd-async` is an async fork of mipidsi: same model/builder shape but
|
||||||
|
# `init()` and pixel writes are `async fn`s. Combined with embassy-nrf's
|
||||||
|
# DMA-backed async Spim, the dashboard repaint yields the executor for
|
||||||
|
# the duration of the SPI transfer instead of busy-looping for ~65 ms,
|
||||||
|
# so the radio task isn't starved.
|
||||||
|
#
|
||||||
|
# embedded-hal-bus needs the `async` feature for `ExclusiveDevice` to
|
||||||
|
# implement `embedded_hal_async::spi::SpiDevice` (in addition to the
|
||||||
|
# blocking impl).
|
||||||
|
lcd-async = { version = "0.1", optional = true }
|
||||||
|
embedded-hal-bus = { version = "0.3", optional = true, features = ["async"] }
|
||||||
|
|
||||||
# Async IO traits and combinators
|
# Async IO traits and combinators
|
||||||
embedded-io-async = "0.7"
|
embedded-io-async = "0.7"
|
||||||
@@ -65,6 +76,12 @@ esp-println = { version = "0.17", optional = true, features = [
|
|||||||
esp-rtos = { version = "0.3", optional = true, features = [
|
esp-rtos = { version = "0.3", optional = true, features = [
|
||||||
"embassy",
|
"embassy",
|
||||||
] }
|
] }
|
||||||
|
# Emits the IDF app descriptor (`.flash.appdesc`) that espflash v4
|
||||||
|
# requires in every ESP binary. The chip subfeature is selected per
|
||||||
|
# board below alongside the other esp-* chip features.
|
||||||
|
esp-bootloader-esp-idf = { version = "0.5", optional = true, features = [
|
||||||
|
"defmt",
|
||||||
|
] }
|
||||||
|
|
||||||
# nRF52840 (RAK 4631)
|
# nRF52840 (RAK 4631)
|
||||||
cortex-m = { version = "0.7", optional = true, features = [
|
cortex-m = { version = "0.7", optional = true, features = [
|
||||||
@@ -146,10 +163,12 @@ heltec_v3 = [
|
|||||||
"embassy",
|
"embassy",
|
||||||
"trace-defmt",
|
"trace-defmt",
|
||||||
"dep:esp-backtrace",
|
"dep:esp-backtrace",
|
||||||
|
"dep:esp-bootloader-esp-idf",
|
||||||
"dep:esp-hal",
|
"dep:esp-hal",
|
||||||
"dep:esp-println",
|
"dep:esp-println",
|
||||||
"dep:esp-rtos",
|
"dep:esp-rtos",
|
||||||
"esp-backtrace/esp32s3",
|
"esp-backtrace/esp32s3",
|
||||||
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
@@ -158,10 +177,12 @@ heltec_v3_uart = [
|
|||||||
"embassy",
|
"embassy",
|
||||||
"trace-defmt",
|
"trace-defmt",
|
||||||
"dep:esp-backtrace",
|
"dep:esp-backtrace",
|
||||||
|
"dep:esp-bootloader-esp-idf",
|
||||||
"dep:esp-hal",
|
"dep:esp-hal",
|
||||||
"dep:esp-println",
|
"dep:esp-println",
|
||||||
"dep:esp-rtos",
|
"dep:esp-rtos",
|
||||||
"esp-backtrace/esp32s3",
|
"esp-backtrace/esp32s3",
|
||||||
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
@@ -170,10 +191,12 @@ heltec_v4 = [
|
|||||||
"embassy",
|
"embassy",
|
||||||
"trace-defmt",
|
"trace-defmt",
|
||||||
"dep:esp-backtrace",
|
"dep:esp-backtrace",
|
||||||
|
"dep:esp-bootloader-esp-idf",
|
||||||
"dep:esp-hal",
|
"dep:esp-hal",
|
||||||
"dep:esp-println",
|
"dep:esp-println",
|
||||||
"dep:esp-rtos",
|
"dep:esp-rtos",
|
||||||
"esp-backtrace/esp32s3",
|
"esp-backtrace/esp32s3",
|
||||||
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
@@ -182,10 +205,12 @@ elecrow_thinknode_m2 = [
|
|||||||
"embassy",
|
"embassy",
|
||||||
"trace-defmt",
|
"trace-defmt",
|
||||||
"dep:esp-backtrace",
|
"dep:esp-backtrace",
|
||||||
|
"dep:esp-bootloader-esp-idf",
|
||||||
"dep:esp-hal",
|
"dep:esp-hal",
|
||||||
"dep:esp-println",
|
"dep:esp-println",
|
||||||
"dep:esp-rtos",
|
"dep:esp-rtos",
|
||||||
"esp-backtrace/esp32s3",
|
"esp-backtrace/esp32s3",
|
||||||
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
@@ -194,10 +219,12 @@ lilygo_tbeam_supreme = [
|
|||||||
"embassy",
|
"embassy",
|
||||||
"trace-defmt",
|
"trace-defmt",
|
||||||
"dep:esp-backtrace",
|
"dep:esp-backtrace",
|
||||||
|
"dep:esp-bootloader-esp-idf",
|
||||||
"dep:esp-hal",
|
"dep:esp-hal",
|
||||||
"dep:esp-println",
|
"dep:esp-println",
|
||||||
"dep:esp-rtos",
|
"dep:esp-rtos",
|
||||||
"esp-backtrace/esp32s3",
|
"esp-backtrace/esp32s3",
|
||||||
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
@@ -206,10 +233,12 @@ lilygo_tbeam = [
|
|||||||
"embassy",
|
"embassy",
|
||||||
"trace-defmt",
|
"trace-defmt",
|
||||||
"dep:esp-backtrace",
|
"dep:esp-backtrace",
|
||||||
|
"dep:esp-bootloader-esp-idf",
|
||||||
"dep:esp-hal",
|
"dep:esp-hal",
|
||||||
"dep:esp-println",
|
"dep:esp-println",
|
||||||
"dep:esp-rtos",
|
"dep:esp-rtos",
|
||||||
"esp-backtrace/esp32",
|
"esp-backtrace/esp32",
|
||||||
|
"esp-bootloader-esp-idf/esp32",
|
||||||
"esp-hal/esp32",
|
"esp-hal/esp32",
|
||||||
"esp-println/esp32",
|
"esp-println/esp32",
|
||||||
"esp-rtos/esp32",
|
"esp-rtos/esp32",
|
||||||
@@ -238,6 +267,20 @@ wio_tracker_l1 = [
|
|||||||
"embassy-executor/platform-cortex-m",
|
"embassy-executor/platform-cortex-m",
|
||||||
"embassy-executor/executor-thread",
|
"embassy-executor/executor-thread",
|
||||||
]
|
]
|
||||||
|
heltec_mesh_node_t114 = [
|
||||||
|
"embassy",
|
||||||
|
"trace-defmt",
|
||||||
|
"dep:cortex-m",
|
||||||
|
"dep:cortex-m-rt",
|
||||||
|
"cortex-m-rt/set-vtor",
|
||||||
|
"dep:defmt-rtt",
|
||||||
|
"dep:embassy-nrf",
|
||||||
|
"dep:panic-probe",
|
||||||
|
"dep:lcd-async",
|
||||||
|
"dep:embedded-hal-bus",
|
||||||
|
"embassy-executor/platform-cortex-m",
|
||||||
|
"embassy-executor/executor-thread",
|
||||||
|
]
|
||||||
waveshare_rp2040_lora = [
|
waveshare_rp2040_lora = [
|
||||||
"embassy",
|
"embassy",
|
||||||
"trace-defmt",
|
"trace-defmt",
|
||||||
@@ -250,17 +293,6 @@ waveshare_rp2040_lora = [
|
|||||||
"embassy-executor/executor-thread",
|
"embassy-executor/executor-thread",
|
||||||
]
|
]
|
||||||
|
|
||||||
# REVISIT: pulling esp-{hal,rtos,backtrace,println} from esp-rs/esp-hal main
|
|
||||||
# to ride ahead of crates.io releases (notably for embassy-executor 0.10
|
|
||||||
# support in esp-rtos). When bumping the upstream rev, also bump the version
|
|
||||||
# requirements above to stay semver-compatible — Cargo silently ignores a
|
|
||||||
# patch whose version falls outside the requested range.
|
|
||||||
[patch.crates-io]
|
|
||||||
esp-hal = { git = "https://github.com/esp-rs/esp-hal.git", branch = "main" }
|
|
||||||
esp-rtos = { git = "https://github.com/esp-rs/esp-hal.git", branch = "main" }
|
|
||||||
esp-backtrace = { git = "https://github.com/esp-rs/esp-hal.git", branch = "main" }
|
|
||||||
esp-println = { git = "https://github.com/esp-rs/esp-hal.git", branch = "main" }
|
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
# hsmc 0.3's `statechart!` macro emits `#[cfg(feature = "journal")]`
|
# hsmc 0.3's `statechart!` macro emits `#[cfg(feature = "journal")]`
|
||||||
# attributes (and others). check-cfg evaluates those in the CALLER's
|
# attributes (and others). check-cfg evaluates those in the CALLER's
|
||||||
|
|||||||
-1923
File diff suppressed because it is too large.
Load diff
@@ -20,6 +20,7 @@ All commands are run from the `firmware/` directory.
|
|||||||
|
|
||||||
| Board | Feature | MCU | Radio | USB | Target |
|
| Board | Feature | MCU | Radio | USB | Target |
|
||||||
|-------|---------|-----|-------|-----|--------|
|
|-------|---------|-----|-------|-----|--------|
|
||||||
|
| Heltec Mesh Node T114 | `heltec_mesh_node_t114` | nRF52840 | SX1262 | Native CDC-ACM | thumbv7em-none-eabihf |
|
||||||
| Heltec V3 | `heltec_v3` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf |
|
| Heltec V3 | `heltec_v3` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf |
|
||||||
| Heltec V3 (UART) | `heltec_v3_uart` | ESP32-S3 | SX1262 | CP2102 bridge | xtensa-esp32s3-none-elf |
|
| 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 |
|
| Heltec V4 | `heltec_v4` | ESP32-S3 | SX1262 | Native CDC-ACM | xtensa-esp32s3-none-elf |
|
||||||
@@ -62,4 +63,8 @@ See [src/board/PORTING.md](src/board/PORTING.md).
|
|||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
See [PROTOCOL.md](PROTOCOL.md) for the complete wire format specification.
|
See [PROTOCOL.md](https://github.com/donglora/protocol-rs/blob/main/PROTOCOL.md)
|
||||||
|
in the [donglora-protocol](https://github.com/donglora/protocol-rs) repo
|
||||||
|
(crates.io: [`donglora-protocol`](https://crates.io/crates/donglora-protocol))
|
||||||
|
for the complete wire format specification — that is the canonical home;
|
||||||
|
this firmware just consumes it.
|
||||||
@@ -33,142 +33,5 @@ Hardware impact: large stability gain on every board — the sx126x
|
|||||||
BUSY-race fix lands on every nRF/RP2040 board, and the sx127x IRQ-clear
|
BUSY-race fix lands on every nRF/RP2040 board, and the sx127x IRQ-clear
|
||||||
fixes unblock TBeam classic in particular. Hardware-verified on Wio
|
fixes unblock TBeam classic in particular. Hardware-verified on Wio
|
||||||
Tracker L1 (sx126x): 20/20 clean-boot TX after the fix (vs ~50%
|
Tracker L1 (sx126x): 20/20 clean-boot TX after the fix (vs ~50%
|
||||||
before).
|
before). LilyGo T-Beam classic (sx127x): TX confirmed working
|
||||||
|
2026-04-30 on the post-esp-hal-1.1 build.
|
||||||
---
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
File renamed without changes.
@@ -13,7 +13,7 @@ version := `sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml`
|
|||||||
# Xtensa targets are built with `+esp` + `-Zbuild-std=core,alloc` (the
|
# Xtensa targets are built with `+esp` + `-Zbuild-std=core,alloc` (the
|
||||||
# self-contained esp toolchain ships its own rust-src; the public
|
# self-contained esp toolchain ships its own rust-src; the public
|
||||||
# nightly drifts ahead and breaks core-stdlib compilation).
|
# 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"
|
boards := "heltec_mesh_node_t114 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
|
# Install all required tools and toolchains
|
||||||
setup:
|
setup:
|
||||||
@@ -246,6 +246,7 @@ _flash_uf2 board:
|
|||||||
[private]
|
[private]
|
||||||
_info name:
|
_info name:
|
||||||
@case "{{name}}" in \
|
@case "{{name}}" in \
|
||||||
|
heltec_mesh_node_t114) echo "heltec_mesh_node_t114 thumbv7em-none-eabihf nRF52840_xxAA" ;; \
|
||||||
heltec_v3) echo "heltec_v3 xtensa-esp32s3-none-elf esp32s3" ;; \
|
heltec_v3) echo "heltec_v3 xtensa-esp32s3-none-elf esp32s3" ;; \
|
||||||
heltec_v3_uart) echo "heltec_v3_uart 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" ;; \
|
heltec_v4) echo "heltec_v4 xtensa-esp32s3-none-elf esp32s3" ;; \
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ just = "latest"
|
|||||||
"cargo:cargo-binutils" = "latest" # rust-objcopy, rust-size
|
"cargo:cargo-binutils" = "latest" # rust-objcopy, rust-size
|
||||||
"cargo:cargo-hex-to-uf2" = "latest" # ELF → UF2 for nRF52 / RP2040 DFU drives
|
"cargo:cargo-hex-to-uf2" = "latest" # ELF → UF2 for nRF52 / RP2040 DFU drives
|
||||||
"cargo:espup" = "0.17.1" # ESP Xtensa toolchain installer (pinned: [env] paths below match this version's defaults)
|
"cargo:espup" = "0.17.1" # ESP Xtensa toolchain installer (pinned: [env] paths below match this version's defaults)
|
||||||
"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:espflash" = "4" # flash + save-image for ESP targets
|
||||||
"cargo:probe-rs-tools" = "latest" # debug-probe flash/run for ARM targets
|
"cargo:probe-rs-tools" = "latest" # debug-probe flash/run for ARM targets
|
||||||
|
|
||||||
# Stable Rust + cross-compile targets. Targets land under $RUSTUP_HOME for
|
# Stable Rust + cross-compile targets. Targets land under $RUSTUP_HOME for
|
||||||
|
|||||||
@@ -70,8 +70,10 @@ impl LoRaBoard for Board {
|
|||||||
### 5. Add display init
|
### 5. Add display init
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
// Construct and initialize your display driver (SSD1306, SH1106, etc.)
|
// Construct and initialize your display driver (SSD1306, SH1106,
|
||||||
|
// ST7789, etc.) using whatever pins/buses you bundled into
|
||||||
|
// DisplayParts in `into_parts()`.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ pub struct DisplayParts {
|
|||||||
|
|
||||||
// ── Display init ────────────────────────────────────────────────────
|
// ── Display init ────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
let mut display = Sh1106::new(i2c, 0x3C);
|
let mut display = Sh1106::new(parts.i2c, 0x3C);
|
||||||
|
|
||||||
embassy_time::Timer::after_millis(100).await;
|
embassy_time::Timer::after_millis(100).await;
|
||||||
if display.init().await.is_err() {
|
if display.init().await.is_err() {
|
||||||
|
|||||||
+2
-2
@@ -47,13 +47,13 @@ pub type DisplayDriver = ssd1306::Ssd1306Async<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
/// Construct and initialize an SSD1306 display from raw I²C.
|
/// Construct and initialize an SSD1306 display from raw I²C.
|
||||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
use ssd1306::mode::DisplayConfigAsync;
|
use ssd1306::mode::DisplayConfigAsync;
|
||||||
use ssd1306::prelude::{Brightness, DisplayRotation};
|
use ssd1306::prelude::{Brightness, DisplayRotation};
|
||||||
use ssd1306::size::DisplaySize128x64;
|
use ssd1306::size::DisplaySize128x64;
|
||||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
||||||
|
|
||||||
let interface = I2CDisplayInterface::new(i2c);
|
let interface = I2CDisplayInterface::new(parts.i2c);
|
||||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
||||||
.into_buffered_graphics_mode();
|
.into_buffered_graphics_mode();
|
||||||
|
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ pub type DisplayDriver = ssd1306::Ssd1306Async<
|
|||||||
feature = "heltec_v3_uart",
|
feature = "heltec_v3_uart",
|
||||||
feature = "heltec_v4"
|
feature = "heltec_v4"
|
||||||
))]
|
))]
|
||||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
use ssd1306::mode::DisplayConfigAsync;
|
use ssd1306::mode::DisplayConfigAsync;
|
||||||
use ssd1306::prelude::{Brightness, DisplayRotation};
|
use ssd1306::prelude::{Brightness, DisplayRotation};
|
||||||
use ssd1306::size::DisplaySize128x64;
|
use ssd1306::size::DisplaySize128x64;
|
||||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
||||||
|
|
||||||
let interface = I2CDisplayInterface::new(i2c);
|
let interface = I2CDisplayInterface::new(parts.i2c);
|
||||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
||||||
.into_buffered_graphics_mode();
|
.into_buffered_graphics_mode();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
//! Heltec Mesh Node T114 (nRF52840 + SX1262 + 1.14" ST7789 TFT).
|
||||||
|
//!
|
||||||
|
//! Hardware reference: cross-verified across MeshCore, Meshtastic, and
|
||||||
|
//! RNode firmwares. Pin maps quoted from
|
||||||
|
//! `meshcore-spec/references/MeshCore/variants/heltec_t114/variant.h` and
|
||||||
|
//! `meshtastic/firmware/variants/nrf52840/heltec_mesh_node_t114/variant.h`.
|
||||||
|
//!
|
||||||
|
//! Stage 2: panel comes up with static colors via the `St7789Color`
|
||||||
|
//! driver. Stage 3 swaps in the proper color renderer module.
|
||||||
|
|
||||||
|
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||||
|
use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull};
|
||||||
|
use embassy_nrf::pwm::{Prescaler, SimpleConfig, SimplePwm};
|
||||||
|
use embassy_nrf::spim::{self, Spim};
|
||||||
|
use embassy_nrf::usb::Driver;
|
||||||
|
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||||
|
use embassy_time::{Delay, Timer};
|
||||||
|
|
||||||
|
use embedded_hal_bus::spi::ExclusiveDevice;
|
||||||
|
use lcd_async::interface::SpiInterface;
|
||||||
|
use lcd_async::models::ST7789;
|
||||||
|
use lcd_async::options::{ColorInversion, Orientation, Rotation};
|
||||||
|
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||||
|
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||||
|
use static_cell::ConstStaticCell;
|
||||||
|
|
||||||
|
use super::traits::{BoardParts, LoRaBoard};
|
||||||
|
use crate::driver::inverted_pin::InvertedPin;
|
||||||
|
use crate::driver::simple_led::SimpleLed;
|
||||||
|
use crate::driver::st7789::{St7789Color, FB_BYTES};
|
||||||
|
use crate::hal::nrf52840 as mcu;
|
||||||
|
|
||||||
|
// ── Concrete peripheral types ───────────────────────────────────────
|
||||||
|
|
||||||
|
type Nss = Output<'static>;
|
||||||
|
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
||||||
|
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
|
||||||
|
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||||
|
pub type UsbDriver = mcu::UsbNrfDriver;
|
||||||
|
// Green LED on P1.03, active LOW.
|
||||||
|
pub type LedDriver = SimpleLed<InvertedPin<Output<'static>>>;
|
||||||
|
|
||||||
|
// ── TFT bus and driver type aliases ─────────────────────────────────
|
||||||
|
|
||||||
|
type TftSpim = Spim<'static>;
|
||||||
|
type TftSpiDevice = ExclusiveDevice<TftSpim, Output<'static>, Delay>;
|
||||||
|
type TftInterface = SpiInterface<TftSpiDevice, Output<'static>>;
|
||||||
|
pub type DisplayDriver = St7789Color<TftInterface, Output<'static>>;
|
||||||
|
|
||||||
|
// ── Peripheral bundles ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct RadioParts {
|
||||||
|
pub driver: RadioDriver,
|
||||||
|
pub delay: Delay,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UsbParts {
|
||||||
|
pub driver: UsbDriver,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assembled-but-not-yet-initialized TFT plumbing. Real initialization
|
||||||
|
/// (power gate, RST cycle, mipidsi init) happens in `create_display`.
|
||||||
|
pub struct DisplayParts {
|
||||||
|
interface: TftInterface,
|
||||||
|
rst_pin: Output<'static>,
|
||||||
|
vdd_en: Output<'static>,
|
||||||
|
backlight: SimplePwm<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Display init ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
|
let DisplayParts {
|
||||||
|
interface,
|
||||||
|
rst_pin,
|
||||||
|
mut vdd_en,
|
||||||
|
mut backlight,
|
||||||
|
} = parts;
|
||||||
|
|
||||||
|
// 1. Drive TFT VDD enable LOW (active LOW per Meshtastic) and let
|
||||||
|
// the rail settle. Heltec's schematic has the panel power gated
|
||||||
|
// by P0.03 — touching SPI before VDD is stable produces snow.
|
||||||
|
vdd_en.set_low();
|
||||||
|
Timer::after_millis(50).await;
|
||||||
|
|
||||||
|
// 2. Build the lcd-async display.
|
||||||
|
// The T114 panel is wired natively portrait (135w × 240h) at
|
||||||
|
// chip framebuffer offset (52, 40) — the standard 1.14" 135×240
|
||||||
|
// ST7789 die mapping. Deg90 rotation rotates the user-facing
|
||||||
|
// surface to landscape (240w × 135h). Meshtastic's variant.h
|
||||||
|
// sets TFT_OFFSET_X=0/Y=0 because their fork of TFT_eSPI applies
|
||||||
|
// these offsets internally; lcd-async expects them in display_offset.
|
||||||
|
// If the image comes up mirrored or upside-down, swap
|
||||||
|
// Deg90 ↔ Deg270 or set `mirrored = true`.
|
||||||
|
let mut delay = Delay;
|
||||||
|
let panel = match lcd_async::Builder::new(ST7789, interface)
|
||||||
|
.reset_pin(rst_pin)
|
||||||
|
.display_size(135, 240)
|
||||||
|
.display_offset(52, 40)
|
||||||
|
.orientation(Orientation::new().rotate(Rotation::Deg90))
|
||||||
|
.invert_colors(ColorInversion::Inverted)
|
||||||
|
.init(&mut delay)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
defmt::error!("ST7789 init failed");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
defmt::info!("ST7789 init OK");
|
||||||
|
|
||||||
|
// 3. Bring the backlight up at full brightness. Stage 2 uses the
|
||||||
|
// full-on duty; the splash dimming logic handled by the display
|
||||||
|
// statechart will hand off to set_dim/set_bright shortly.
|
||||||
|
backlight.set_duty(0, embassy_nrf::pwm::DutyCycle::inverted(0));
|
||||||
|
|
||||||
|
// Forget vdd_en so it stays driven for the lifetime of the program.
|
||||||
|
// We took it by move so we could call set_low() during init.
|
||||||
|
core::mem::forget(vdd_en);
|
||||||
|
|
||||||
|
// Take the static framebuffer (240×135 × 2 bytes Rgb565 BE
|
||||||
|
// = 64,800 bytes zero-initialized in .bss). Stored as raw bytes
|
||||||
|
// so present() can DMA-blit the slice straight to the panel
|
||||||
|
// without an iterator-conversion pass — the lcd-async
|
||||||
|
// `RawFrameBuf<Rgb565, _>` wrapper handles BE byte packing on
|
||||||
|
// each pixel write.
|
||||||
|
static FRAMEBUFFER: ConstStaticCell<[u8; FB_BYTES]> =
|
||||||
|
ConstStaticCell::new([0u8; FB_BYTES]);
|
||||||
|
let fb = FRAMEBUFFER.take();
|
||||||
|
|
||||||
|
Some(St7789Color::new(panel, backlight, fb))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Board init ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct Board {
|
||||||
|
p: embassy_nrf::Peripherals,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoRaBoard for Board {
|
||||||
|
const NAME: &'static str = "Heltec Mesh Node T114";
|
||||||
|
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 {
|
||||||
|
// 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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mac_address() -> [u8; 6] {
|
||||||
|
mcu::mac_address()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
||||||
|
let p = self.p;
|
||||||
|
|
||||||
|
// ── VEXT_ENABLE (P0.21, active HIGH) ────────────────────
|
||||||
|
// Powers the SX1262 *and* the TFT/peripheral 3V3 rail. Must be
|
||||||
|
// driven HIGH before any SPI traffic to the radio or panel —
|
||||||
|
// without this, lora-phy init can succeed (SPI bus works
|
||||||
|
// electrically) but TX_DONE never fires because the PA is
|
||||||
|
// unpowered. Held forever via core::mem::forget. Mirrors
|
||||||
|
// Meshtastic's `VEXT_ENABLE = P0.21, VEXT_ON_VALUE = HIGH`.
|
||||||
|
let vext_en = Output::new(p.P0_21, Level::High, OutputDrive::Standard);
|
||||||
|
core::mem::forget(vext_en);
|
||||||
|
|
||||||
|
// ── SPI3 bus for SX1262 ─────────────────────────────────
|
||||||
|
// Per Heltec T114 schematic + Meshtastic variant.h:
|
||||||
|
// SCK=P0.19, MOSI=P0.22, MISO=P0.23
|
||||||
|
// NSS=P0.24, RST=P0.25, BUSY=P0.17, DIO1=P0.20
|
||||||
|
// No external RXEN/TXEN — antenna switch is driven internally
|
||||||
|
// by SX1262 DIO2. TCXO is on DIO3 at 1.8 V.
|
||||||
|
let mut spi_cfg = spim::Config::default();
|
||||||
|
spi_cfg.frequency = spim::Frequency::M1;
|
||||||
|
let spi = Spim::new(p.SPI3, mcu::Irqs, p.P0_19, p.P0_23, p.P0_22, spi_cfg);
|
||||||
|
let spi_bus = mcu::share_spi_bus(spi);
|
||||||
|
|
||||||
|
let nss = Output::new(p.P0_24, Level::High, OutputDrive::Standard);
|
||||||
|
let spi_device = SpiDevice::new(spi_bus, nss);
|
||||||
|
|
||||||
|
let reset = Output::new(p.P0_25, Level::High, OutputDrive::Standard);
|
||||||
|
let dio1 = Input::new(p.P0_20, Pull::Down);
|
||||||
|
// BUSY has an internal 20 kΩ pull-up on the SX1262 die (datasheet
|
||||||
|
// p. 53); an MCU-side Pull fights it. See lora-rs/lora-rs#260.
|
||||||
|
let busy = Input::new(p.P0_17, Pull::None);
|
||||||
|
|
||||||
|
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None)
|
||||||
|
.expect("SX1262 interface init");
|
||||||
|
|
||||||
|
let sx_config = sx126x::Config {
|
||||||
|
chip: Sx1262,
|
||||||
|
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V8),
|
||||||
|
use_dcdc: true,
|
||||||
|
rx_boost: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let radio = RadioParts {
|
||||||
|
driver: Sx126x::new(spi_device, iv, sx_config),
|
||||||
|
delay: Delay,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── USB ─────────────────────────────────────────────────
|
||||||
|
let vbus = mcu::alloc_vbus_detect(true, true);
|
||||||
|
let host = UsbParts {
|
||||||
|
driver: Driver::new(p.USBD, mcu::Irqs, vbus),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Battery divider control (P0.06) LOW disables the divider —
|
||||||
|
// saves a few tens of µA. Battery sense (P0.04 / AIN2) deferred
|
||||||
|
// until the protocol gets a battery field.
|
||||||
|
let bat_ctl = Output::new(p.P0_06, Level::Low, OutputDrive::Standard);
|
||||||
|
core::mem::forget(bat_ctl);
|
||||||
|
|
||||||
|
// ── SPI2 bus for ST7789 TFT ─────────────────────────────
|
||||||
|
// Per Meshtastic variant.h:
|
||||||
|
// SCK=P1.08, MOSI=P1.09, CS=P0.11, DC=P0.12, RST=P0.02
|
||||||
|
// VDD enable=P0.03 (active HIGH), Backlight=P0.15 (active LOW PWM)
|
||||||
|
// Write-only bus — embassy_nrf::Spim still requires a MISO slot,
|
||||||
|
// so we wire a free unused pin (P1.06) as a no-op input.
|
||||||
|
let mut tft_spi_cfg = spim::Config::default();
|
||||||
|
// 8 MHz lets us repaint the 240×135 panel comfortably. Higher
|
||||||
|
// rates work with mipidsi but are platform-dependent (board
|
||||||
|
// routing, pull strengths). 8 MHz is the safe default.
|
||||||
|
tft_spi_cfg.frequency = spim::Frequency::M8;
|
||||||
|
let tft_spi = Spim::new(p.SPI2, mcu::Irqs, p.P1_08, p.P1_06, p.P1_09, tft_spi_cfg);
|
||||||
|
|
||||||
|
let tft_cs = Output::new(p.P0_11, Level::High, OutputDrive::Standard);
|
||||||
|
let tft_dc = Output::new(p.P0_12, Level::Low, OutputDrive::Standard);
|
||||||
|
let tft_rst = Output::new(p.P0_02, Level::High, OutputDrive::Standard);
|
||||||
|
// VTFT_CTRL is active LOW per Meshtastic
|
||||||
|
// (`#define TFT_BACKLIGHT_ON LOW`, `digitalWrite(VTFT_CTRL, LOW)`
|
||||||
|
// at boot). Start HIGH (panel rail off); create_display drives
|
||||||
|
// LOW to enable the panel.
|
||||||
|
let tft_vdd = Output::new(p.P0_03, Level::High, OutputDrive::Standard);
|
||||||
|
|
||||||
|
let tft_spi_device = ExclusiveDevice::new(tft_spi, tft_cs, Delay)
|
||||||
|
.expect("TFT ExclusiveDevice init");
|
||||||
|
|
||||||
|
// lcd-async's SpiInterface streams pixel data directly from
|
||||||
|
// the caller's slice — no scratch buffer required (mipidsi's
|
||||||
|
// sync version needed one for batching).
|
||||||
|
let tft_interface = SpiInterface::new(tft_spi_device, tft_dc);
|
||||||
|
|
||||||
|
// ── PWM backlight (active-LOW on P0.15) ─────────────────
|
||||||
|
// Default SimpleConfig is fine — countertop=1000 → ~1 kHz
|
||||||
|
// (well above flicker threshold), prescaler default. Channel 0
|
||||||
|
// idle level matches the active-LOW wiring.
|
||||||
|
let mut bl_cfg = SimpleConfig::default();
|
||||||
|
bl_cfg.max_duty = crate::driver::st7789::BL_MAX;
|
||||||
|
bl_cfg.prescaler = Prescaler::Div128;
|
||||||
|
bl_cfg.ch0_idle_level = Level::High;
|
||||||
|
let backlight = SimplePwm::new_1ch(p.PWM0, p.P0_15, &bl_cfg);
|
||||||
|
|
||||||
|
let display_parts = DisplayParts {
|
||||||
|
interface: tft_interface,
|
||||||
|
rst_pin: tft_rst,
|
||||||
|
vdd_en: tft_vdd,
|
||||||
|
backlight,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Green LED (P1.03, active LOW) ───────────────────────
|
||||||
|
// Idle level HIGH = LED off. InvertedPin flips polarity so
|
||||||
|
// SimpleLed can use normal "set_high = on" semantics.
|
||||||
|
let led_pin = Output::new(p.P1_03, Level::High, OutputDrive::Standard);
|
||||||
|
let led = SimpleLed(InvertedPin(led_pin));
|
||||||
|
|
||||||
|
BoardParts {
|
||||||
|
radio,
|
||||||
|
host,
|
||||||
|
display: Some(display_parts),
|
||||||
|
led,
|
||||||
|
mac: Self::mac_address(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,8 +50,8 @@ pub struct DisplayParts {
|
|||||||
|
|
||||||
// ── Display init ────────────────────────────────────────────────────
|
// ── Display init ────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
let mut display = Sh1106::new(i2c, 0x3C);
|
let mut display = Sh1106::new(parts.i2c, 0x3C);
|
||||||
|
|
||||||
embassy_time::Timer::after_millis(100).await;
|
embassy_time::Timer::after_millis(100).await;
|
||||||
if display.init().await.is_err() {
|
if display.init().await.is_err() {
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ pub struct DisplayParts {
|
|||||||
|
|
||||||
// ── Display init ────────────────────────────────────────────────────
|
// ── Display init ────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
let mut display = crate::driver::sh1106::Sh1106::new(i2c, 0x3C);
|
let mut display = crate::driver::sh1106::Sh1106::new(parts.i2c, 0x3C);
|
||||||
|
|
||||||
// Retry 3× with 100ms backoff. The SH1106's first transaction
|
// Retry 3× with 100ms backoff. The SH1106's first transaction
|
||||||
// after power-on can return Err if the chip's internal voltage
|
// after power-on can return Err if the chip's internal voltage
|
||||||
|
|||||||
+5
-7
@@ -6,10 +6,7 @@
|
|||||||
//!
|
//!
|
||||||
//! See `src/board/PORTING.md` for a step-by-step guide to adding a new board.
|
//! See `src/board/PORTING.md` for a step-by-step guide to adding a new board.
|
||||||
|
|
||||||
use embedded_graphics::draw_target::DrawTarget;
|
use crate::display::BoardDisplay;
|
||||||
use embedded_graphics::pixelcolor::BinaryColor;
|
|
||||||
|
|
||||||
use crate::driver::DisplayBrightness;
|
|
||||||
|
|
||||||
/// Async RGB LED control. Boards without an LED use `()` which is a no-op.
|
/// Async RGB LED control. Boards without an LED use `()` which is a no-op.
|
||||||
pub trait RgbLed {
|
pub trait RgbLed {
|
||||||
@@ -37,9 +34,10 @@ pub trait LoRaBoard: Sized {
|
|||||||
/// Display peripheral bundle (I2C bus for display init).
|
/// Display peripheral bundle (I2C bus for display init).
|
||||||
type DisplayParts;
|
type DisplayParts;
|
||||||
|
|
||||||
/// Concrete display driver type (must implement DrawTarget for rendering
|
/// Concrete display driver type. Each board's driver implements
|
||||||
/// and DisplayBrightness for the two-level dim control).
|
/// [`BoardDisplay`] to render the splash + dashboard content into its
|
||||||
type DisplayDriver: DrawTarget<Color = BinaryColor> + DisplayBrightness;
|
/// native pixel format and resolution and to flush / dim the panel.
|
||||||
|
type DisplayDriver: BoardDisplay;
|
||||||
|
|
||||||
/// RGB LED driver. Boards without an LED use `()`.
|
/// RGB LED driver. Boards without an LED use `()`.
|
||||||
type LedDriver: RgbLed;
|
type LedDriver: RgbLed;
|
||||||
|
|||||||
@@ -41,12 +41,16 @@ pub struct UsbParts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct DisplayParts {
|
pub struct DisplayParts {
|
||||||
|
// No display on this board, but the trait still wants concrete
|
||||||
|
// peripheral types. The i2c bus type is reserved for the future
|
||||||
|
// possibility of an external OLED add-on.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub i2c: DisplayI2c,
|
pub i2c: DisplayI2c,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Display init ───────────────────────────────────────────────────
|
// ── Display init ───────────────────────────────────────────────────
|
||||||
|
|
||||||
pub async fn create_display(_i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(_parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ pub struct DisplayParts {
|
|||||||
|
|
||||||
// ── Display init ────────────────────────────────────────────────────
|
// ── Display init ────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
||||||
let mut display = crate::driver::sh1106::Sh1106::new(i2c, 0x3D);
|
let mut display = crate::driver::sh1106::Sh1106::new(parts.i2c, 0x3D);
|
||||||
|
|
||||||
embassy_time::Timer::after_millis(100).await;
|
embassy_time::Timer::after_millis(100).await;
|
||||||
if display.init().await.is_err() {
|
if display.init().await.is_err() {
|
||||||
|
|||||||
+232
-107
@@ -38,27 +38,29 @@ use hsmc::statechart;
|
|||||||
|
|
||||||
use crate::board::{self, Board, DisplayDriver, DisplayParts, LedDriver, LoRaBoard, RgbLed};
|
use crate::board::{self, Board, DisplayDriver, DisplayParts, LedDriver, LoRaBoard, RgbLed};
|
||||||
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioEvent, RadioEventChannel};
|
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioEvent, RadioEventChannel};
|
||||||
use crate::driver::DisplayBrightness;
|
|
||||||
use crate::protocol::LoRaConfig;
|
use crate::protocol::LoRaConfig;
|
||||||
|
|
||||||
use render::{BoardInfo, RSSI_HISTORY_LEN};
|
|
||||||
|
|
||||||
const BOARD_NAME: &str = Board::NAME;
|
const BOARD_NAME: &str = Board::NAME;
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
// Splash board-name font selection. Every OLED we drive is 128 px wide;
|
/// Number of slots in the dashboard's RSSI/TX sparkline ring buffer.
|
||||||
// the primary font (FONT_6X10, 6 px advance) fits up to 21 ASCII chars
|
pub const RSSI_HISTORY_LEN: usize = 128;
|
||||||
// across the screen, the fallback (FONT_5X8, 5 px advance) fits up to
|
|
||||||
// 25. If even the fallback can't fit, fail at compile time so the board
|
// Mono splash board-name font selection. The mono OLEDs we drive are
|
||||||
// porter shortens NAME rather than silently overflowing the splash.
|
// 128 px wide; the primary font (FONT_6X10, 6 px advance) fits up to 21
|
||||||
|
// ASCII chars across, the fallback (FONT_5X8, 5 px advance) fits up to
|
||||||
|
// 25. If even the fallback can't fit on a mono OLED, fail at compile
|
||||||
|
// time so the board porter shortens NAME rather than silently
|
||||||
|
// overflowing the splash. Color renderers have their own size budget
|
||||||
|
// and simply use the same NAME with their own font choice.
|
||||||
const SPLASH_SCREEN_W_PX: u32 = 128;
|
const SPLASH_SCREEN_W_PX: u32 = 128;
|
||||||
const SPLASH_FONT_PRIMARY_W_PX: u32 = 6;
|
const SPLASH_FONT_PRIMARY_W_PX: u32 = 6;
|
||||||
const SPLASH_FONT_FALLBACK_W_PX: u32 = 5;
|
const SPLASH_FONT_FALLBACK_W_PX: u32 = 5;
|
||||||
const BOARD_NAME_FITS_PRIMARY_FONT: bool =
|
pub(crate) const BOARD_NAME_FITS_PRIMARY_FONT: bool =
|
||||||
(BOARD_NAME.len() as u32) * SPLASH_FONT_PRIMARY_W_PX <= SPLASH_SCREEN_W_PX;
|
(BOARD_NAME.len() as u32) * SPLASH_FONT_PRIMARY_W_PX <= SPLASH_SCREEN_W_PX;
|
||||||
const _: () = assert!(
|
const _: () = assert!(
|
||||||
(BOARD_NAME.len() as u32) * SPLASH_FONT_FALLBACK_W_PX <= SPLASH_SCREEN_W_PX,
|
(BOARD_NAME.len() as u32) * SPLASH_FONT_FALLBACK_W_PX <= SPLASH_SCREEN_W_PX,
|
||||||
"Board::NAME is too long for the 128 px splash screen even with the FONT_5X8 fallback \
|
"Board::NAME is too long for the 128 px mono splash screen even with the FONT_5X8 fallback \
|
||||||
(max 25 ASCII chars). Shorten the NAME constant in src/board/<board>.rs.",
|
(max 25 ASCII chars). Shorten the NAME constant in src/board/<board>.rs.",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -66,6 +68,49 @@ const _: () = assert!(
|
|||||||
/// floor (-120 dBm), so it cannot be confused with a real RSSI value.
|
/// floor (-120 dBm), so it cannot be confused with a real RSSI value.
|
||||||
const NO_SIGNAL: i16 = -121;
|
const NO_SIGNAL: i16 = -121;
|
||||||
|
|
||||||
|
/// Hand-authored 64 × 64 pixel-perfect QR bitmap for the splash
|
||||||
|
/// "learn more" screen. 1 bit/pixel, MSB-first, row-major. Bit `1` is
|
||||||
|
/// "light" (OLED pixel on), bit `0` is "dark" (OLED pixel off), so when
|
||||||
|
/// drawn with `BinaryColor::On` = lit, the QR appears with the
|
||||||
|
/// traditional dark-on-light look including a proper quiet zone. The
|
||||||
|
/// color renderer remaps bit 1 → white, bit 0 → black for the same
|
||||||
|
/// visual contrast on a black-background TFT.
|
||||||
|
pub(crate) const QR_WIDTH_PX: u32 = 64;
|
||||||
|
pub(crate) const QR_BITMAP: [u8; 512] = [
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x07, 0x9F, 0xFE, 0x60, 0x00, 0x7F, 0xFE,
|
||||||
|
0x00, 0x07, 0x9F, 0xFE, 0x60, 0x00, 0x7F, 0xFE, 0x7F, 0xE6, 0x18, 0x19, 0xE7, 0xFE, 0x7F, 0xFE,
|
||||||
|
0x7F, 0xE6, 0x18, 0x19, 0xE7, 0xFE, 0x7F, 0xFE, 0x60, 0x67, 0x81, 0xE6, 0x66, 0x06, 0x7F, 0xFE,
|
||||||
|
0x60, 0x67, 0x81, 0xE6, 0x66, 0x06, 0x7F, 0xFE, 0x60, 0x66, 0x19, 0x87, 0xE6, 0x06, 0x7F, 0xFE,
|
||||||
|
0x60, 0x66, 0x19, 0x87, 0xE6, 0x06, 0x7F, 0xFE, 0x60, 0x67, 0xE6, 0x1F, 0xE6, 0x06, 0x7F, 0xFE,
|
||||||
|
0x60, 0x67, 0xE6, 0x1F, 0xE6, 0x06, 0x7F, 0xFE, 0x7F, 0xE6, 0x67, 0x80, 0x67, 0xFE, 0x7F, 0xFE,
|
||||||
|
0x7F, 0xE6, 0x67, 0x80, 0x67, 0xFE, 0x7F, 0xFE, 0x00, 0x06, 0x66, 0x66, 0x60, 0x00, 0x7F, 0xFE,
|
||||||
|
0x00, 0x06, 0x66, 0x66, 0x60, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFE, 0x61, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFE, 0x61, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x60, 0x06, 0x01, 0x99, 0x99, 0xFF, 0xFE,
|
||||||
|
0x00, 0x60, 0x06, 0x01, 0x99, 0x99, 0xFF, 0xFE, 0x01, 0x98, 0x1F, 0x9E, 0x79, 0xF9, 0xFF, 0xFE,
|
||||||
|
0x01, 0x98, 0x1F, 0x9E, 0x79, 0xF9, 0xFF, 0xFF, 0xE0, 0x66, 0x18, 0x61, 0x9F, 0x98, 0x7F, 0xFF,
|
||||||
|
0xE0, 0x66, 0x18, 0x61, 0x9F, 0x98, 0x7F, 0xFF, 0x98, 0x1E, 0x01, 0xE7, 0x99, 0xFE, 0x7F, 0xFF,
|
||||||
|
0x98, 0x1E, 0x01, 0xE7, 0x99, 0xFE, 0x7F, 0xFF, 0xE1, 0xE6, 0x19, 0xF8, 0x06, 0x60, 0x7F, 0xFF,
|
||||||
|
0xE1, 0xE6, 0x19, 0xF8, 0x06, 0x60, 0x7F, 0xFE, 0x19, 0xF8, 0x06, 0x1E, 0x79, 0x99, 0xFF, 0xFE,
|
||||||
|
0x19, 0xF8, 0x06, 0x1E, 0x79, 0x99, 0xFF, 0xFE, 0x7E, 0x01, 0xFF, 0x80, 0x18, 0x18, 0x7F, 0xFE,
|
||||||
|
0x7E, 0x01, 0xFF, 0x80, 0x18, 0x18, 0x7F, 0xFE, 0x7E, 0x19, 0x9E, 0x1E, 0x78, 0x7E, 0x7F, 0xFE,
|
||||||
|
0x7E, 0x19, 0x9E, 0x1E, 0x78, 0x7E, 0x7F, 0xFE, 0x60, 0x06, 0x66, 0x66, 0x00, 0x67, 0xFF, 0xFE,
|
||||||
|
0x60, 0x06, 0x66, 0x66, 0x00, 0x67, 0xFF, 0xFF, 0xFF, 0xFE, 0x67, 0xF8, 0x7E, 0x1F, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFE, 0x67, 0xF8, 0x7E, 0x1F, 0xFF, 0xFE, 0x00, 0x06, 0x00, 0x7E, 0x66, 0x60, 0x7F, 0xFE,
|
||||||
|
0x00, 0x06, 0x00, 0x7E, 0x66, 0x60, 0x7F, 0xFE, 0x7F, 0xE7, 0x99, 0xE6, 0x7E, 0x1E, 0x7F, 0xFE,
|
||||||
|
0x7F, 0xE7, 0x99, 0xE6, 0x7E, 0x1E, 0x7F, 0xFE, 0x60, 0x66, 0x61, 0xF8, 0x00, 0x60, 0x7F, 0xFE,
|
||||||
|
0x60, 0x66, 0x61, 0xF8, 0x00, 0x60, 0x7F, 0xFE, 0x60, 0x66, 0x06, 0x01, 0x86, 0x00, 0x7F, 0xFE,
|
||||||
|
0x60, 0x66, 0x06, 0x01, 0x86, 0x00, 0x7F, 0xFE, 0x60, 0x66, 0x67, 0x86, 0x7F, 0x86, 0x7F, 0xFE,
|
||||||
|
0x60, 0x66, 0x67, 0x86, 0x7F, 0x86, 0x7F, 0xFE, 0x7F, 0xE6, 0x66, 0x18, 0x00, 0x1E, 0x7F, 0xFE,
|
||||||
|
0x7F, 0xE6, 0x66, 0x18, 0x00, 0x1E, 0x7F, 0xFE, 0x00, 0x06, 0x66, 0x67, 0x98, 0x00, 0x7F, 0xFE,
|
||||||
|
0x00, 0x06, 0x66, 0x67, 0x98, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
];
|
||||||
|
|
||||||
/// Which dashboard badge is in view (drives the RX/TX indicator only).
|
/// Which dashboard badge is in view (drives the RX/TX indicator only).
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Badge {
|
pub enum Badge {
|
||||||
@@ -73,6 +118,136 @@ pub enum Badge {
|
|||||||
Tx,
|
Tx,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Display content types (shared across all renderers) ───────────────
|
||||||
|
//
|
||||||
|
// `BoardInfo` and `DashboardCtx` describe *what* to draw on the splash
|
||||||
|
// and dashboard screens. Each board's `BoardDisplay` impl decides *how*
|
||||||
|
// to render that content for its native pixel format and resolution.
|
||||||
|
|
||||||
|
/// Static-ish board identity rendered on the splash + dashboard.
|
||||||
|
pub struct BoardInfo<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub version: &'a str,
|
||||||
|
pub mac: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Live dashboard data at the moment of a render call.
|
||||||
|
pub struct DashboardCtx<'a> {
|
||||||
|
pub badge: Badge,
|
||||||
|
pub config: Option<LoRaConfig>,
|
||||||
|
pub rx_count: u32,
|
||||||
|
pub tx_count: u32,
|
||||||
|
pub last_rssi: Option<i16>,
|
||||||
|
pub last_snr: Option<i16>,
|
||||||
|
pub rssi_history: &'a [i16; RSSI_HISTORY_LEN],
|
||||||
|
pub tx_history: &'a [bool; RSSI_HISTORY_LEN],
|
||||||
|
pub rssi_count: usize,
|
||||||
|
pub current_slot_rssi: i16,
|
||||||
|
pub current_slot_tx: bool,
|
||||||
|
pub board: &'a BoardInfo<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BoardDisplay trait ────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// One logical display surface, four content states (splash×2, dashboard,
|
||||||
|
// blank), plus brightness control and a flush hook. Each board's display
|
||||||
|
// driver implements this directly — color depth, resolution, dimming
|
||||||
|
// strategy, and any framebuffer flushing live behind the trait.
|
||||||
|
|
||||||
|
/// Single trait every board's display driver implements. The display
|
||||||
|
/// task talks to the trait; the per-driver impl handles native format.
|
||||||
|
///
|
||||||
|
/// `present()` is named distinctly from any inherent `flush()` method on
|
||||||
|
/// concrete display types so method resolution doesn't get ambiguous.
|
||||||
|
pub trait BoardDisplay {
|
||||||
|
fn present(&mut self) -> impl core::future::Future<Output = ()>;
|
||||||
|
fn set_bright(&mut self) -> impl core::future::Future<Output = ()>;
|
||||||
|
fn set_dim(&mut self) -> impl core::future::Future<Output = ()>;
|
||||||
|
|
||||||
|
fn render_splash_learn_more(&mut self);
|
||||||
|
fn render_splash_info(&mut self, info: &BoardInfo<'_>);
|
||||||
|
fn render_dashboard(&mut self, ctx: &DashboardCtx<'_>);
|
||||||
|
fn render_blank(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color renderer module — used by `crate::driver::st7789::St7789Color` on
|
||||||
|
// the Heltec T114 to draw native 240×135 Rgb565 dashboards/splash screens.
|
||||||
|
// Gated to the T114 feature so other boards don't pay for the imports.
|
||||||
|
#[cfg(feature = "heltec_mesh_node_t114")]
|
||||||
|
pub mod render_color;
|
||||||
|
|
||||||
|
// Shared QR-code blit, generic over color. Used by both the mono and
|
||||||
|
// color splash screens to render the bitmap with proper quiet zones.
|
||||||
|
mod qr;
|
||||||
|
|
||||||
|
// ── Mono-OLED adapters ────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// SH1106 + SSD1306Async both render `BinaryColor` content into a 128×64
|
||||||
|
// framebuffer flushed via I2C. Their `BoardDisplay` impls all delegate
|
||||||
|
// to `mod render` — the existing pixel-perfect mono renderer.
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "wio_tracker_l1",
|
||||||
|
feature = "elecrow_thinknode_m2",
|
||||||
|
feature = "lilygo_tbeam_supreme",
|
||||||
|
feature = "rak_wisblock_4631"
|
||||||
|
))]
|
||||||
|
impl<I> BoardDisplay for crate::driver::sh1106::Sh1106<I>
|
||||||
|
where
|
||||||
|
I: embedded_hal_async::i2c::I2c,
|
||||||
|
{
|
||||||
|
async fn present(&mut self) {
|
||||||
|
let _ = crate::driver::sh1106::Sh1106::flush(self).await;
|
||||||
|
}
|
||||||
|
async fn set_bright(&mut self) {
|
||||||
|
crate::driver::DisplayBrightness::set_bright(self).await;
|
||||||
|
}
|
||||||
|
async fn set_dim(&mut self) {
|
||||||
|
crate::driver::DisplayBrightness::set_dim(self).await;
|
||||||
|
}
|
||||||
|
fn render_splash_learn_more(&mut self) {
|
||||||
|
render::learn_more(self);
|
||||||
|
}
|
||||||
|
fn render_splash_info(&mut self, info: &BoardInfo<'_>) {
|
||||||
|
render::info(self, info);
|
||||||
|
}
|
||||||
|
fn render_dashboard(&mut self, ctx: &DashboardCtx<'_>) {
|
||||||
|
render::dashboard(self, ctx);
|
||||||
|
}
|
||||||
|
fn render_blank(&mut self) {
|
||||||
|
render::blank(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DI, SIZE> BoardDisplay
|
||||||
|
for ssd1306::Ssd1306Async<DI, SIZE, ssd1306::mode::BufferedGraphicsModeAsync<SIZE>>
|
||||||
|
where
|
||||||
|
DI: display_interface::AsyncWriteOnlyDataCommand,
|
||||||
|
SIZE: ssd1306::size::DisplaySizeAsync,
|
||||||
|
{
|
||||||
|
async fn present(&mut self) {
|
||||||
|
let _ = ssd1306::Ssd1306Async::flush(self).await;
|
||||||
|
}
|
||||||
|
async fn set_bright(&mut self) {
|
||||||
|
crate::driver::DisplayBrightness::set_bright(self).await;
|
||||||
|
}
|
||||||
|
async fn set_dim(&mut self) {
|
||||||
|
crate::driver::DisplayBrightness::set_dim(self).await;
|
||||||
|
}
|
||||||
|
fn render_splash_learn_more(&mut self) {
|
||||||
|
render::learn_more(self);
|
||||||
|
}
|
||||||
|
fn render_splash_info(&mut self, info: &BoardInfo<'_>) {
|
||||||
|
render::info(self, info);
|
||||||
|
}
|
||||||
|
fn render_dashboard(&mut self, ctx: &DashboardCtx<'_>) {
|
||||||
|
render::dashboard(self, ctx);
|
||||||
|
}
|
||||||
|
fn render_blank(&mut self) {
|
||||||
|
render::blank(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fine-grained events driving the `Display` statechart.
|
/// Fine-grained events driving the `Display` statechart.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DisplayEvent {
|
pub enum DisplayEvent {
|
||||||
@@ -180,14 +355,14 @@ impl DisplayContext {
|
|||||||
version: VERSION,
|
version: VERSION,
|
||||||
mac: mac_str,
|
mac: mac_str,
|
||||||
};
|
};
|
||||||
render::info(display, &info);
|
display.render_splash_info(&info);
|
||||||
let _ = display.flush().await;
|
display.present().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn flush_learn_more(&mut self) {
|
async fn flush_learn_more(&mut self) {
|
||||||
let display = &mut self.peripherals.display;
|
let display = &mut self.peripherals.display;
|
||||||
render::learn_more(display);
|
display.render_splash_learn_more();
|
||||||
let _ = display.flush().await;
|
display.present().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn flush_dashboard(&mut self, badge: Badge) {
|
async fn flush_dashboard(&mut self, badge: Badge) {
|
||||||
@@ -199,27 +374,28 @@ impl DisplayContext {
|
|||||||
version: VERSION,
|
version: VERSION,
|
||||||
mac: mac_str,
|
mac: mac_str,
|
||||||
};
|
};
|
||||||
render::dashboard(
|
let ctx = DashboardCtx {
|
||||||
display,
|
|
||||||
badge,
|
badge,
|
||||||
self.run.config,
|
config: self.run.config,
|
||||||
self.run.rx_count,
|
rx_count: self.run.rx_count,
|
||||||
self.run.tx_count,
|
tx_count: self.run.tx_count,
|
||||||
self.run.last_rssi,
|
last_rssi: self.run.last_rssi,
|
||||||
self.run.last_snr,
|
last_snr: self.run.last_snr,
|
||||||
&self.run.rssi_history,
|
rssi_history: &self.run.rssi_history,
|
||||||
&self.run.tx_history,
|
tx_history: &self.run.tx_history,
|
||||||
self.run.rssi_count,
|
rssi_count: self.run.rssi_count,
|
||||||
self.run.current_slot_rssi,
|
current_slot_rssi: self.run.current_slot_rssi,
|
||||||
self.run.current_slot_tx,
|
current_slot_tx: self.run.current_slot_tx,
|
||||||
&info,
|
board: &info,
|
||||||
);
|
};
|
||||||
let _ = display.flush().await;
|
display.render_dashboard(&ctx);
|
||||||
|
display.present().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn flush_blank(&mut self) {
|
async fn flush_blank(&mut self) {
|
||||||
render::blank(&mut self.peripherals.display);
|
let display = &mut self.peripherals.display;
|
||||||
let _ = self.peripherals.display.flush().await;
|
display.render_blank();
|
||||||
|
display.present().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boards without an LED use `LedDriver = ()`; the `RgbLed for ()` impl
|
// Boards without an LED use `LedDriver = ()`; the `RgbLed for ()` impl
|
||||||
@@ -453,7 +629,7 @@ pub async fn display_task(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
let Some(display) = board::create_display(parts.i2c).await else {
|
let Some(display) = board::create_display(parts).await else {
|
||||||
defmt::error!("display init failed, running LED-only loop");
|
defmt::error!("display init failed, running LED-only loop");
|
||||||
loop {
|
loop {
|
||||||
match display_commands.receive().await {
|
match display_commands.receive().await {
|
||||||
@@ -516,10 +692,8 @@ mod render {
|
|||||||
use embedded_graphics::text::{Alignment, Text};
|
use embedded_graphics::text::{Alignment, Text};
|
||||||
use heapless::String;
|
use heapless::String;
|
||||||
|
|
||||||
use super::Badge;
|
use super::{Badge, BoardInfo, DashboardCtx, QR_WIDTH_PX, RSSI_HISTORY_LEN};
|
||||||
use crate::protocol::{LoRaBandwidth, LoRaConfig};
|
use crate::protocol::LoRaBandwidth;
|
||||||
|
|
||||||
pub const RSSI_HISTORY_LEN: usize = 128;
|
|
||||||
|
|
||||||
const CHAR_W: i32 = 6;
|
const CHAR_W: i32 = 6;
|
||||||
const FONT_H: i32 = 10;
|
const FONT_H: i32 = 10;
|
||||||
@@ -528,72 +702,20 @@ mod render {
|
|||||||
const RSSI_MIN: i16 = -120;
|
const RSSI_MIN: i16 = -120;
|
||||||
const RSSI_MAX: i16 = 0;
|
const RSSI_MAX: i16 = 0;
|
||||||
|
|
||||||
/// Hand-authored 64 × 64 pixel-perfect QR bitmap for the splash
|
|
||||||
/// "learn more" screen. 1 bit/pixel, MSB-first, row-major. Bit `1` is
|
|
||||||
/// "light" (OLED pixel on), bit `0` is "dark" (OLED pixel off), so
|
|
||||||
/// when drawn with `BinaryColor::On` = lit, the QR appears with the
|
|
||||||
/// traditional dark-on-light look including a proper quiet zone.
|
|
||||||
const QR_WIDTH_PX: u32 = 64;
|
|
||||||
const QR_BITMAP: [u8; 512] = [
|
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x07, 0x9F,
|
|
||||||
0xFE, 0x60, 0x00, 0x7F, 0xFE, 0x00, 0x07, 0x9F, 0xFE, 0x60, 0x00, 0x7F, 0xFE, 0x7F, 0xE6,
|
|
||||||
0x18, 0x19, 0xE7, 0xFE, 0x7F, 0xFE, 0x7F, 0xE6, 0x18, 0x19, 0xE7, 0xFE, 0x7F, 0xFE, 0x60,
|
|
||||||
0x67, 0x81, 0xE6, 0x66, 0x06, 0x7F, 0xFE, 0x60, 0x67, 0x81, 0xE6, 0x66, 0x06, 0x7F, 0xFE,
|
|
||||||
0x60, 0x66, 0x19, 0x87, 0xE6, 0x06, 0x7F, 0xFE, 0x60, 0x66, 0x19, 0x87, 0xE6, 0x06, 0x7F,
|
|
||||||
0xFE, 0x60, 0x67, 0xE6, 0x1F, 0xE6, 0x06, 0x7F, 0xFE, 0x60, 0x67, 0xE6, 0x1F, 0xE6, 0x06,
|
|
||||||
0x7F, 0xFE, 0x7F, 0xE6, 0x67, 0x80, 0x67, 0xFE, 0x7F, 0xFE, 0x7F, 0xE6, 0x67, 0x80, 0x67,
|
|
||||||
0xFE, 0x7F, 0xFE, 0x00, 0x06, 0x66, 0x66, 0x60, 0x00, 0x7F, 0xFE, 0x00, 0x06, 0x66, 0x66,
|
|
||||||
0x60, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFE, 0x61, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
|
|
||||||
0x61, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x60, 0x06, 0x01, 0x99, 0x99, 0xFF, 0xFE, 0x00, 0x60,
|
|
||||||
0x06, 0x01, 0x99, 0x99, 0xFF, 0xFE, 0x01, 0x98, 0x1F, 0x9E, 0x79, 0xF9, 0xFF, 0xFE, 0x01,
|
|
||||||
0x98, 0x1F, 0x9E, 0x79, 0xF9, 0xFF, 0xFF, 0xE0, 0x66, 0x18, 0x61, 0x9F, 0x98, 0x7F, 0xFF,
|
|
||||||
0xE0, 0x66, 0x18, 0x61, 0x9F, 0x98, 0x7F, 0xFF, 0x98, 0x1E, 0x01, 0xE7, 0x99, 0xFE, 0x7F,
|
|
||||||
0xFF, 0x98, 0x1E, 0x01, 0xE7, 0x99, 0xFE, 0x7F, 0xFF, 0xE1, 0xE6, 0x19, 0xF8, 0x06, 0x60,
|
|
||||||
0x7F, 0xFF, 0xE1, 0xE6, 0x19, 0xF8, 0x06, 0x60, 0x7F, 0xFE, 0x19, 0xF8, 0x06, 0x1E, 0x79,
|
|
||||||
0x99, 0xFF, 0xFE, 0x19, 0xF8, 0x06, 0x1E, 0x79, 0x99, 0xFF, 0xFE, 0x7E, 0x01, 0xFF, 0x80,
|
|
||||||
0x18, 0x18, 0x7F, 0xFE, 0x7E, 0x01, 0xFF, 0x80, 0x18, 0x18, 0x7F, 0xFE, 0x7E, 0x19, 0x9E,
|
|
||||||
0x1E, 0x78, 0x7E, 0x7F, 0xFE, 0x7E, 0x19, 0x9E, 0x1E, 0x78, 0x7E, 0x7F, 0xFE, 0x60, 0x06,
|
|
||||||
0x66, 0x66, 0x00, 0x67, 0xFF, 0xFE, 0x60, 0x06, 0x66, 0x66, 0x00, 0x67, 0xFF, 0xFF, 0xFF,
|
|
||||||
0xFE, 0x67, 0xF8, 0x7E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFE, 0x67, 0xF8, 0x7E, 0x1F, 0xFF, 0xFE,
|
|
||||||
0x00, 0x06, 0x00, 0x7E, 0x66, 0x60, 0x7F, 0xFE, 0x00, 0x06, 0x00, 0x7E, 0x66, 0x60, 0x7F,
|
|
||||||
0xFE, 0x7F, 0xE7, 0x99, 0xE6, 0x7E, 0x1E, 0x7F, 0xFE, 0x7F, 0xE7, 0x99, 0xE6, 0x7E, 0x1E,
|
|
||||||
0x7F, 0xFE, 0x60, 0x66, 0x61, 0xF8, 0x00, 0x60, 0x7F, 0xFE, 0x60, 0x66, 0x61, 0xF8, 0x00,
|
|
||||||
0x60, 0x7F, 0xFE, 0x60, 0x66, 0x06, 0x01, 0x86, 0x00, 0x7F, 0xFE, 0x60, 0x66, 0x06, 0x01,
|
|
||||||
0x86, 0x00, 0x7F, 0xFE, 0x60, 0x66, 0x67, 0x86, 0x7F, 0x86, 0x7F, 0xFE, 0x60, 0x66, 0x67,
|
|
||||||
0x86, 0x7F, 0x86, 0x7F, 0xFE, 0x7F, 0xE6, 0x66, 0x18, 0x00, 0x1E, 0x7F, 0xFE, 0x7F, 0xE6,
|
|
||||||
0x66, 0x18, 0x00, 0x1E, 0x7F, 0xFE, 0x00, 0x06, 0x66, 0x67, 0x98, 0x00, 0x7F, 0xFE, 0x00,
|
|
||||||
0x06, 0x66, 0x67, 0x98, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
||||||
0xFF, 0xFF,
|
|
||||||
];
|
|
||||||
|
|
||||||
pub struct BoardInfo<'a> {
|
pub fn dashboard(target: &mut impl DrawTarget<Color = BinaryColor>, ctx: &DashboardCtx<'_>) {
|
||||||
pub name: &'a str,
|
let badge = ctx.badge;
|
||||||
pub version: &'a str,
|
let config = ctx.config;
|
||||||
pub mac: &'a str,
|
let rx_count = ctx.rx_count;
|
||||||
}
|
let tx_count = ctx.tx_count;
|
||||||
|
let last_rssi = ctx.last_rssi;
|
||||||
#[allow(clippy::too_many_arguments)]
|
let last_snr = ctx.last_snr;
|
||||||
pub fn dashboard(
|
let rssi_history = ctx.rssi_history;
|
||||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
let tx_history = ctx.tx_history;
|
||||||
badge: Badge,
|
let rssi_count = ctx.rssi_count;
|
||||||
config: Option<LoRaConfig>,
|
let current_slot_rssi = ctx.current_slot_rssi;
|
||||||
rx_count: u32,
|
let current_slot_tx = ctx.current_slot_tx;
|
||||||
tx_count: u32,
|
let board = ctx.board;
|
||||||
last_rssi: Option<i16>,
|
|
||||||
last_snr: Option<i16>,
|
|
||||||
rssi_history: &[i16; RSSI_HISTORY_LEN],
|
|
||||||
tx_history: &[bool; RSSI_HISTORY_LEN],
|
|
||||||
rssi_count: usize,
|
|
||||||
current_slot_rssi: i16,
|
|
||||||
current_slot_tx: bool,
|
|
||||||
board: &BoardInfo,
|
|
||||||
) {
|
|
||||||
let bb = target.bounding_box();
|
let bb = target.bounding_box();
|
||||||
let w = bb.size.width as i32;
|
let w = bb.size.width as i32;
|
||||||
let h = bb.size.height as i32;
|
let h = bb.size.height as i32;
|
||||||
@@ -833,10 +955,9 @@ mod render {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Alternate splash screen: 64 × 64 pixel-perfect QR bitmap on the
|
/// Alternate splash screen: 64 × 64 pixel-perfect QR bitmap on the
|
||||||
/// right (with its own quiet zone baked in), centered text column on
|
/// right (centered within its quiet zone via `super::qr::draw`),
|
||||||
/// the left inviting the user to visit the URL.
|
/// centered text column on the left inviting the user to visit the URL.
|
||||||
pub fn learn_more(target: &mut impl DrawTarget<Color = BinaryColor>) {
|
pub fn learn_more(target: &mut impl DrawTarget<Color = BinaryColor>) {
|
||||||
use embedded_graphics::image::{Image, ImageRaw};
|
|
||||||
use embedded_graphics::mono_font::ascii::{FONT_4X6, FONT_7X14_BOLD};
|
use embedded_graphics::mono_font::ascii::{FONT_4X6, FONT_7X14_BOLD};
|
||||||
|
|
||||||
let bb = target.bounding_box();
|
let bb = target.bounding_box();
|
||||||
@@ -844,10 +965,14 @@ mod render {
|
|||||||
|
|
||||||
let _ = target.clear(BinaryColor::Off);
|
let _ = target.clear(BinaryColor::Off);
|
||||||
|
|
||||||
// ── QR code: flush to the right edge, 1:1 pixel rendering ───
|
// ── QR code: rightmost 64 × 64 region, centered cells ───────
|
||||||
let qr_x = w - QR_WIDTH_PX as i32;
|
let qr_x = w - QR_WIDTH_PX as i32;
|
||||||
let qr_raw: ImageRaw<BinaryColor> = ImageRaw::new(&QR_BITMAP, QR_WIDTH_PX);
|
super::qr::draw(
|
||||||
Image::new(&qr_raw, Point::new(qr_x, 0)).draw(target).ok();
|
target,
|
||||||
|
Point::new(qr_x, 0),
|
||||||
|
BinaryColor::On,
|
||||||
|
BinaryColor::Off,
|
||||||
|
);
|
||||||
|
|
||||||
// ── Text column (everything left of the QR) ─────────────────
|
// ── Text column (everything left of the QR) ─────────────────
|
||||||
let col_w = qr_x;
|
let col_w = qr_x;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
//! Shared QR-code blit for splash screens.
|
||||||
|
//!
|
||||||
|
//! The 512-byte QR bitmap in the parent module is laid out asymmetrically
|
||||||
|
//! within its 64×64 frame: rows 0-6 and 57-63 are top/bottom quiet zones,
|
||||||
|
//! but the QR cells start at col 0 (no left quiet zone) and extend to
|
||||||
|
//! col ~49, leaving ~14 px of white padding on the right plus one stray
|
||||||
|
//! black pixel at col 63. Rendering the bitmap as-is leaves the QR
|
||||||
|
//! visually shifted toward the left of its rendering area, which looks
|
||||||
|
//! wrong on any layout that doesn't tuck the bitmap flush against a
|
||||||
|
//! screen edge.
|
||||||
|
//!
|
||||||
|
//! [`draw`] re-centers the QR data: it skips the bitmap's right padding
|
||||||
|
//! (cols 50-63), fills the rendering area's left and right 7 px with
|
||||||
|
//! the foreground "quiet zone" color, and renders the actual QR cells
|
||||||
|
//! (bitmap cols 0..50) at +7 horizontal offset. Rows are rendered as-is
|
||||||
|
//! so the bitmap's existing top/bottom quiet zones land where expected.
|
||||||
|
//!
|
||||||
|
//! Generic over color so the mono renderer (`BinaryColor`) and the
|
||||||
|
//! T114's color renderer (`Rgb565`) share a single implementation —
|
||||||
|
//! they pass their own foreground (white / lit) and background (black /
|
||||||
|
//! unlit) values.
|
||||||
|
|
||||||
|
use embedded_graphics::draw_target::DrawTarget;
|
||||||
|
use embedded_graphics::geometry::{Point, Size};
|
||||||
|
use embedded_graphics::pixelcolor::PixelColor;
|
||||||
|
use embedded_graphics::primitives::Rectangle;
|
||||||
|
use embedded_graphics::Pixel;
|
||||||
|
|
||||||
|
use super::{QR_BITMAP, QR_WIDTH_PX};
|
||||||
|
|
||||||
|
/// Width of the actual QR data within the bitmap (cols 0..QR_DATA_W).
|
||||||
|
const QR_DATA_W: i32 = 50;
|
||||||
|
/// White padding on left + right to center the QR cells. With
|
||||||
|
/// QR_DATA_W=50 and total area=64 (= QR_WIDTH_PX), padding = 7 each side.
|
||||||
|
const PAD_X: i32 = (QR_WIDTH_PX as i32 - QR_DATA_W) / 2;
|
||||||
|
|
||||||
|
/// Render the 64×64 QR with proper quiet zones on all four sides.
|
||||||
|
///
|
||||||
|
/// `fg` is the "quiet zone / light cell" color (white on a TFT, "on"
|
||||||
|
/// on a mono OLED). `bg` is the "dark cell" color. Bitmap rows 0-6 and
|
||||||
|
/// 57-63 already encode top/bottom quiet zones in `fg`; this function
|
||||||
|
/// fills the missing left/right padding columns explicitly.
|
||||||
|
pub fn draw<D, C>(target: &mut D, origin: Point, fg: C, bg: C)
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = C>,
|
||||||
|
C: PixelColor,
|
||||||
|
{
|
||||||
|
// Fill left padding (cols 0..PAD_X) with fg.
|
||||||
|
let _ = target.fill_solid(
|
||||||
|
&Rectangle::new(origin, Size::new(PAD_X as u32, QR_WIDTH_PX)),
|
||||||
|
fg,
|
||||||
|
);
|
||||||
|
// Fill right padding (cols PAD_X+QR_DATA_W..QR_WIDTH_PX) with fg.
|
||||||
|
let right_pad_w = QR_WIDTH_PX as i32 - PAD_X - QR_DATA_W;
|
||||||
|
let _ = target.fill_solid(
|
||||||
|
&Rectangle::new(
|
||||||
|
Point::new(origin.x + PAD_X + QR_DATA_W, origin.y),
|
||||||
|
Size::new(right_pad_w as u32, QR_WIDTH_PX),
|
||||||
|
),
|
||||||
|
fg,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render bitmap cols 0..QR_DATA_W at +PAD_X horizontal offset.
|
||||||
|
let bytes_per_row = (QR_WIDTH_PX / 8) as usize;
|
||||||
|
let pixels = (0..QR_WIDTH_PX as i32).flat_map(move |y| {
|
||||||
|
(0..QR_DATA_W).map(move |x| {
|
||||||
|
let byte = QR_BITMAP[y as usize * bytes_per_row + (x as usize) / 8];
|
||||||
|
let bit = (byte >> (7 - (x as u8 & 7))) & 1;
|
||||||
|
let color = if bit == 1 { fg } else { bg };
|
||||||
|
Pixel(Point::new(origin.x + PAD_X + x, origin.y + y), color)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let _ = target.draw_iter(pixels);
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
//! Native 240×135 Rgb565 renderer for the Heltec T114 ST7789 panel.
|
||||||
|
//!
|
||||||
|
//! Mirrors the content shape of the mono OLED renderer in `display::render`
|
||||||
|
//! (same `BoardInfo` / `DashboardCtx` inputs) but lays things out for the
|
||||||
|
//! T114's bigger color screen with native font sizes and colored badges.
|
||||||
|
//! Each board's `BoardDisplay` impl picks the renderer that matches its
|
||||||
|
//! native pixel format — no per-pixel color quantization, no scaling,
|
||||||
|
//! no aliasing.
|
||||||
|
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use embedded_graphics::mono_font::ascii::{
|
||||||
|
FONT_6X10, FONT_8X13, FONT_8X13_BOLD, FONT_9X15_BOLD, FONT_10X20,
|
||||||
|
};
|
||||||
|
use embedded_graphics::mono_font::MonoTextStyle;
|
||||||
|
use embedded_graphics::pixelcolor::Rgb565;
|
||||||
|
use embedded_graphics::prelude::*;
|
||||||
|
use embedded_graphics::primitives::{Line, PrimitiveStyle, Rectangle};
|
||||||
|
use embedded_graphics::text::renderer::CharacterStyle;
|
||||||
|
use embedded_graphics::text::{Alignment, Text};
|
||||||
|
use heapless::String;
|
||||||
|
|
||||||
|
use super::{qr, Badge, BoardInfo, DashboardCtx, QR_WIDTH_PX, RSSI_HISTORY_LEN};
|
||||||
|
use crate::protocol::LoRaBandwidth;
|
||||||
|
|
||||||
|
// ── Panel dimensions ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const W: i32 = 240;
|
||||||
|
const H: i32 = 135;
|
||||||
|
|
||||||
|
// ── Palette ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const BG: Rgb565 = Rgb565::BLACK;
|
||||||
|
const FG: Rgb565 = Rgb565::WHITE;
|
||||||
|
const DIM: Rgb565 = Rgb565::new(12, 24, 12); // muted gray
|
||||||
|
const ACCENT: Rgb565 = Rgb565::new(8, 30, 31); // bright cyan
|
||||||
|
const RX_COLOR: Rgb565 = Rgb565::new(8, 60, 8); // bright green
|
||||||
|
const TX_COLOR: Rgb565 = Rgb565::new(31, 16, 8); // amber-red
|
||||||
|
const SPARK_COLOR: Rgb565 = Rgb565::new(20, 50, 24); // soft green
|
||||||
|
|
||||||
|
// ── Sparkline ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const RSSI_MIN: i16 = -120;
|
||||||
|
const RSSI_MAX: i16 = 0;
|
||||||
|
/// Sentinel: no packet received in this slot. Mirrors `display::NO_SIGNAL`.
|
||||||
|
const NO_SIGNAL: i16 = -121;
|
||||||
|
|
||||||
|
// ── Public render entry points ────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn blank<D: DrawTarget<Color = Rgb565>>(target: &mut D) {
|
||||||
|
let _ = target.clear(BG);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn splash_learn_more<D: DrawTarget<Color = Rgb565>>(target: &mut D) {
|
||||||
|
let _ = target.clear(BG);
|
||||||
|
|
||||||
|
// QR pinned to the right side, vertically centered. 64×64 fits with
|
||||||
|
// ~36 px headroom top/bottom on the 135-tall panel.
|
||||||
|
let qr_w = QR_WIDTH_PX as i32;
|
||||||
|
let qr_x = W - qr_w - 8;
|
||||||
|
let qr_y = (H - qr_w) / 2;
|
||||||
|
qr::draw(target, Point::new(qr_x, qr_y), FG, BG);
|
||||||
|
|
||||||
|
// Text column to the left of the QR.
|
||||||
|
let col_w = qr_x - 8;
|
||||||
|
let col_center = col_w / 2 + 4;
|
||||||
|
|
||||||
|
let title_style = MonoTextStyle::new(&FONT_10X20, ACCENT);
|
||||||
|
let url_style = MonoTextStyle::new(&FONT_8X13, FG);
|
||||||
|
let caption_style = MonoTextStyle::new(&FONT_6X10, DIM);
|
||||||
|
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
"DongLoRa",
|
||||||
|
Point::new(col_center, 45),
|
||||||
|
title_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
"donglora.com",
|
||||||
|
Point::new(col_center, 73),
|
||||||
|
url_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
"Scan for docs",
|
||||||
|
Point::new(col_center, 95),
|
||||||
|
caption_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn splash_info<D: DrawTarget<Color = Rgb565>>(target: &mut D, info: &BoardInfo<'_>) {
|
||||||
|
let _ = target.clear(BG);
|
||||||
|
|
||||||
|
let title_style = MonoTextStyle::new(&FONT_10X20, ACCENT);
|
||||||
|
let board_style = MonoTextStyle::new(&FONT_9X15_BOLD, FG);
|
||||||
|
let mac_style = MonoTextStyle::new(&FONT_8X13, FG);
|
||||||
|
let small_style = MonoTextStyle::new(&FONT_6X10, DIM);
|
||||||
|
|
||||||
|
// Title at top.
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
"DongLoRa",
|
||||||
|
Point::new(W / 2, 22),
|
||||||
|
title_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
// Board name.
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
info.name,
|
||||||
|
Point::new(W / 2, 50),
|
||||||
|
board_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
// MAC.
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
info.mac,
|
||||||
|
Point::new(W / 2, 75),
|
||||||
|
mac_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
// Version.
|
||||||
|
let mut version_buf: String<24> = String::new();
|
||||||
|
let _ = write!(version_buf, "v{}", info.version);
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
&version_buf,
|
||||||
|
Point::new(W / 2, 95),
|
||||||
|
small_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
// Waiting line.
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
"Waiting for host...",
|
||||||
|
Point::new(W / 2, 120),
|
||||||
|
small_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dashboard<D: DrawTarget<Color = Rgb565>>(target: &mut D, ctx: &DashboardCtx<'_>) {
|
||||||
|
let _ = target.clear(BG);
|
||||||
|
|
||||||
|
// ── Top bar (badge + radio config) ──────────────────────────
|
||||||
|
let top_h = 24;
|
||||||
|
let badge_w = 38;
|
||||||
|
let (badge_color, badge_text) = match ctx.badge {
|
||||||
|
Badge::Rx => (RX_COLOR, "RX"),
|
||||||
|
Badge::Tx => (TX_COLOR, "TX"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let badge_rect = Rectangle::new(Point::zero(), Size::new(badge_w as u32, top_h as u32));
|
||||||
|
let _ = badge_rect
|
||||||
|
.into_styled(PrimitiveStyle::with_fill(badge_color))
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
let mut badge_style = MonoTextStyle::new(&FONT_8X13_BOLD, FG);
|
||||||
|
badge_style.set_background_color(Some(badge_color));
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
badge_text,
|
||||||
|
Point::new(badge_w / 2, 16),
|
||||||
|
badge_style,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
// Radio config text right of the badge.
|
||||||
|
let cfg_style = MonoTextStyle::new(&FONT_8X13, FG);
|
||||||
|
let mut buf: String<48> = String::new();
|
||||||
|
if let Some(cfg) = ctx.config {
|
||||||
|
let bw_str = bw_label(cfg.bw);
|
||||||
|
let cr_str = cr_label(cfg.cr);
|
||||||
|
let freq_mhz = cfg.freq_hz / 1_000_000;
|
||||||
|
let freq_khz = (cfg.freq_hz % 1_000_000) / 1_000;
|
||||||
|
let _ = write!(
|
||||||
|
buf,
|
||||||
|
"{}.{:03} {} SF{} CR{}",
|
||||||
|
freq_mhz, freq_khz, bw_str, cfg.sf, cr_str
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = write!(buf, "Awaiting config");
|
||||||
|
}
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
&buf,
|
||||||
|
Point::new(W - 4, 16),
|
||||||
|
cfg_style,
|
||||||
|
Alignment::Right,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
// Separator line under top bar.
|
||||||
|
let _ = Line::new(Point::new(0, top_h + 1), Point::new(W - 1, top_h + 1))
|
||||||
|
.into_styled(PrimitiveStyle::with_stroke(DIM, 1))
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
// ── Bottom bar (counters + RSSI) ───────────────────────────
|
||||||
|
let bottom_h = 30;
|
||||||
|
let bottom_top = H - bottom_h;
|
||||||
|
let _ = Line::new(
|
||||||
|
Point::new(0, bottom_top - 1),
|
||||||
|
Point::new(W - 1, bottom_top - 1),
|
||||||
|
)
|
||||||
|
.into_styled(PrimitiveStyle::with_stroke(DIM, 1))
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
let stat_style = MonoTextStyle::new(&FONT_8X13, FG);
|
||||||
|
|
||||||
|
let mut rxtx_buf: String<24> = String::new();
|
||||||
|
let _ = write!(
|
||||||
|
rxtx_buf,
|
||||||
|
"RX {} TX {}",
|
||||||
|
compact_count(ctx.rx_count).as_str(),
|
||||||
|
compact_count(ctx.tx_count).as_str()
|
||||||
|
);
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
&rxtx_buf,
|
||||||
|
Point::new(4, bottom_top + 11),
|
||||||
|
stat_style,
|
||||||
|
Alignment::Left,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
let mut rssi_buf: String<32> = String::new();
|
||||||
|
match (ctx.last_rssi, ctx.last_snr) {
|
||||||
|
(Some(rssi), Some(snr)) => {
|
||||||
|
let _ = write!(rssi_buf, "{} dBm SNR {}", rssi, snr);
|
||||||
|
}
|
||||||
|
(Some(rssi), None) => {
|
||||||
|
let _ = write!(rssi_buf, "{} dBm", rssi);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let _ = write!(rssi_buf, "no signal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
&rssi_buf,
|
||||||
|
Point::new(W - 4, bottom_top + 11),
|
||||||
|
stat_style,
|
||||||
|
Alignment::Right,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
let secondary = MonoTextStyle::new(&FONT_6X10, DIM);
|
||||||
|
let _ = Text::with_alignment(
|
||||||
|
ctx.board.mac,
|
||||||
|
Point::new(W / 2, bottom_top + 25),
|
||||||
|
secondary,
|
||||||
|
Alignment::Center,
|
||||||
|
)
|
||||||
|
.draw(target);
|
||||||
|
|
||||||
|
// ── Sparkline ──────────────────────────────────────────────
|
||||||
|
let spark_top = top_h + 4;
|
||||||
|
let spark_h = (bottom_top - 2) - spark_top;
|
||||||
|
if spark_h > 0 {
|
||||||
|
// 240 wide / 2 px per bar = 120 visible bars (subset of the
|
||||||
|
// 128-slot ring). Matches the mono renderer's
|
||||||
|
// `visible_bars = w / 2` heuristic.
|
||||||
|
let visible_bars = ((W / 2) as usize).min(RSSI_HISTORY_LEN);
|
||||||
|
rssi_sparkline(
|
||||||
|
target,
|
||||||
|
ctx.rssi_history,
|
||||||
|
ctx.tx_history,
|
||||||
|
ctx.rssi_count,
|
||||||
|
ctx.current_slot_rssi,
|
||||||
|
ctx.current_slot_tx,
|
||||||
|
spark_top,
|
||||||
|
spark_h,
|
||||||
|
visible_bars,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn bw_label(bw: LoRaBandwidth) -> &'static str {
|
||||||
|
match bw {
|
||||||
|
LoRaBandwidth::Khz7 => "7.8k",
|
||||||
|
LoRaBandwidth::Khz10 => "10.4k",
|
||||||
|
LoRaBandwidth::Khz15 => "15.6k",
|
||||||
|
LoRaBandwidth::Khz20 => "20.8k",
|
||||||
|
LoRaBandwidth::Khz31 => "31.2k",
|
||||||
|
LoRaBandwidth::Khz41 => "41.7k",
|
||||||
|
LoRaBandwidth::Khz62 => "62.5k",
|
||||||
|
LoRaBandwidth::Khz125 => "125k",
|
||||||
|
LoRaBandwidth::Khz250 => "250k",
|
||||||
|
LoRaBandwidth::Khz500 => "500k",
|
||||||
|
LoRaBandwidth::Khz200 => "200k",
|
||||||
|
LoRaBandwidth::Khz400 => "400k",
|
||||||
|
LoRaBandwidth::Khz800 => "800k",
|
||||||
|
LoRaBandwidth::Khz1600 => "1.6M",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cr_label(cr: crate::protocol::LoRaCodingRate) -> &'static str {
|
||||||
|
match cr {
|
||||||
|
crate::protocol::LoRaCodingRate::Cr4_5 => "5",
|
||||||
|
crate::protocol::LoRaCodingRate::Cr4_6 => "6",
|
||||||
|
crate::protocol::LoRaCodingRate::Cr4_7 => "7",
|
||||||
|
crate::protocol::LoRaCodingRate::Cr4_8 => "8",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compact_count(n: u32) -> String<10> {
|
||||||
|
let mut s: String<10> = String::new();
|
||||||
|
if n > 9_999_999 {
|
||||||
|
let _ = write!(s, "{}M", n / 1_000_000);
|
||||||
|
} else if n > 999_999 {
|
||||||
|
let _ = write!(s, "{}k", n / 1_000);
|
||||||
|
} else {
|
||||||
|
let _ = write!(s, "{}", n);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn rssi_sparkline<D: DrawTarget<Color = Rgb565>>(
|
||||||
|
target: &mut D,
|
||||||
|
history: &[i16; RSSI_HISTORY_LEN],
|
||||||
|
tx_history: &[bool; RSSI_HISTORY_LEN],
|
||||||
|
count: usize,
|
||||||
|
current_rssi: i16,
|
||||||
|
current_tx: bool,
|
||||||
|
spark_top: i32,
|
||||||
|
spark_h: i32,
|
||||||
|
visible_bars: usize,
|
||||||
|
) {
|
||||||
|
let live = current_rssi > RSSI_MIN || current_tx;
|
||||||
|
let committed = count.min(RSSI_HISTORY_LEN);
|
||||||
|
let hist_slots = if live {
|
||||||
|
committed.min(visible_bars.saturating_sub(1))
|
||||||
|
} else {
|
||||||
|
committed.min(visible_bars)
|
||||||
|
};
|
||||||
|
let total = hist_slots + if live { 1 } else { 0 };
|
||||||
|
|
||||||
|
if total == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..hist_slots {
|
||||||
|
let idx = if count <= RSSI_HISTORY_LEN {
|
||||||
|
let effective_bars = if live { visible_bars - 1 } else { visible_bars };
|
||||||
|
i + committed.saturating_sub(effective_bars)
|
||||||
|
} else {
|
||||||
|
let start = count - RSSI_HISTORY_LEN;
|
||||||
|
let skip = RSSI_HISTORY_LEN.saturating_sub(if live {
|
||||||
|
visible_bars - 1
|
||||||
|
} else {
|
||||||
|
visible_bars
|
||||||
|
});
|
||||||
|
(start + skip + i) % RSSI_HISTORY_LEN
|
||||||
|
};
|
||||||
|
let is_tx = tx_history[idx];
|
||||||
|
let rssi = history[idx];
|
||||||
|
|
||||||
|
if let Some(bar_h) = bar_height(rssi, is_tx, spark_h) {
|
||||||
|
let x = (visible_bars - total + i) as i32 * 2;
|
||||||
|
draw_bar(target, x, bar_h, is_tx, spark_top, spark_h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if live {
|
||||||
|
if let Some(bar_h) = bar_height(current_rssi, current_tx, spark_h) {
|
||||||
|
let x = (visible_bars - 1) as i32 * 2;
|
||||||
|
draw_bar(target, x, bar_h, current_tx, spark_top, spark_h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bar_height(rssi: i16, is_tx: bool, spark_h: i32) -> Option<i32> {
|
||||||
|
let h = if rssi <= RSSI_MIN || rssi == NO_SIGNAL {
|
||||||
|
if is_tx {
|
||||||
|
spark_h / 3
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let clamped = rssi.clamp(RSSI_MIN, RSSI_MAX);
|
||||||
|
((clamped - RSSI_MIN) as i32 * spark_h) / (RSSI_MAX - RSSI_MIN) as i32
|
||||||
|
};
|
||||||
|
if h == 0 { None } else { Some(h) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_bar<D: DrawTarget<Color = Rgb565>>(
|
||||||
|
target: &mut D,
|
||||||
|
x: i32,
|
||||||
|
bar_h: i32,
|
||||||
|
is_tx: bool,
|
||||||
|
spark_top: i32,
|
||||||
|
spark_h: i32,
|
||||||
|
) {
|
||||||
|
let y = spark_top + spark_h - bar_h;
|
||||||
|
let color = if is_tx { TX_COLOR } else { SPARK_COLOR };
|
||||||
|
if is_tx {
|
||||||
|
// Hatched bar — every other pixel row — mirrors the mono
|
||||||
|
// renderer's TX visual cue.
|
||||||
|
for row in 0..bar_h {
|
||||||
|
if row % 2 == 0 {
|
||||||
|
let _ = Rectangle::new(Point::new(x, y + row), Size::new(2, 1))
|
||||||
|
.into_styled(PrimitiveStyle::with_fill(color))
|
||||||
|
.draw(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = Rectangle::new(Point::new(x, y), Size::new(2, bar_h as u32))
|
||||||
|
.into_styled(PrimitiveStyle::with_fill(color))
|
||||||
|
.draw(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -9,10 +9,14 @@ pub mod sh1106;
|
|||||||
#[cfg(any(feature = "lilygo_tbeam_supreme", feature = "lilygo_tbeam"))]
|
#[cfg(any(feature = "lilygo_tbeam_supreme", feature = "lilygo_tbeam"))]
|
||||||
pub mod axp;
|
pub mod axp;
|
||||||
|
|
||||||
#[cfg(feature = "lilygo_tbeam")]
|
#[cfg(any(feature = "lilygo_tbeam", feature = "heltec_mesh_node_t114"))]
|
||||||
pub mod inverted_pin;
|
pub mod inverted_pin;
|
||||||
|
|
||||||
|
#[cfg(feature = "heltec_mesh_node_t114")]
|
||||||
|
pub mod st7789;
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
|
feature = "heltec_mesh_node_t114",
|
||||||
feature = "heltec_v3",
|
feature = "heltec_v3",
|
||||||
feature = "heltec_v3_uart",
|
feature = "heltec_v3_uart",
|
||||||
feature = "heltec_v4",
|
feature = "heltec_v4",
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
//! ST7789 240×135 IPS TFT driver wrapper for the Heltec Mesh Node T114.
|
||||||
|
//!
|
||||||
|
//! Wraps `lcd_async::Display<…, models::ST7789, …>` (async fork of
|
||||||
|
//! mipidsi) with an in-RAM raw byte framebuffer (240×135 × 2 bytes
|
||||||
|
//! Rgb565 BE = 64,800 bytes) and an active-LOW PWM backlight on PWM0.
|
||||||
|
//!
|
||||||
|
//! Renderers draw into the framebuffer via `lcd_async::raw_framebuf::RawFrameBuf`;
|
||||||
|
//! `present()` awaits a single async DMA blit of the entire buffer to
|
||||||
|
//! the panel via `panel.show_raw_data(...).await`. The await lets
|
||||||
|
//! embassy-nrf's DMA-backed Spim yield the executor for the duration
|
||||||
|
//! of the ~65 ms SPI transfer, so the radio task can run alongside
|
||||||
|
//! and the SX1262 IRQ handler isn't starved during repaints.
|
||||||
|
|
||||||
|
use embassy_nrf::pwm::{DutyCycle, SimplePwm};
|
||||||
|
use embedded_graphics::pixelcolor::Rgb565;
|
||||||
|
use lcd_async::interface::Interface;
|
||||||
|
use lcd_async::models::ST7789;
|
||||||
|
use lcd_async::raw_framebuf::RawFrameBuf;
|
||||||
|
use lcd_async::Display;
|
||||||
|
|
||||||
|
use crate::display::{render_color, BoardDisplay, BoardInfo, DashboardCtx};
|
||||||
|
|
||||||
|
/// PWM countertop. Drives the backlight at ~1 kHz (well above flicker).
|
||||||
|
pub const BL_MAX: u16 = 1000;
|
||||||
|
|
||||||
|
fn bl_bright() -> DutyCycle {
|
||||||
|
// Inverted polarity: HIGH for 0 counts → always LOW → full bright.
|
||||||
|
DutyCycle::inverted(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bl_dim() -> DutyCycle {
|
||||||
|
// ~80% HIGH → ~20% LOW → 20% brightness.
|
||||||
|
DutyCycle::inverted((BL_MAX * 4) / 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn bl_off() -> DutyCycle {
|
||||||
|
DutyCycle::inverted(BL_MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Framebuffer ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub const FB_W: usize = 240;
|
||||||
|
pub const FB_H: usize = 135;
|
||||||
|
/// Bytes for one full Rgb565 frame (2 bytes/pixel, big-endian — matches
|
||||||
|
/// what ST7789 expects in 16-bit color mode).
|
||||||
|
pub const FB_BYTES: usize = FB_W * FB_H * 2;
|
||||||
|
|
||||||
|
// ── St7789Color ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct St7789Color<DI, RST>
|
||||||
|
where
|
||||||
|
DI: Interface,
|
||||||
|
RST: embedded_hal::digital::OutputPin,
|
||||||
|
{
|
||||||
|
panel: Display<DI, ST7789, RST>,
|
||||||
|
backlight: SimplePwm<'static>,
|
||||||
|
fb: &'static mut [u8; FB_BYTES],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DI, RST> St7789Color<DI, RST>
|
||||||
|
where
|
||||||
|
DI: Interface,
|
||||||
|
RST: embedded_hal::digital::OutputPin,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
panel: Display<DI, ST7789, RST>,
|
||||||
|
backlight: SimplePwm<'static>,
|
||||||
|
fb: &'static mut [u8; FB_BYTES],
|
||||||
|
) -> Self {
|
||||||
|
Self { panel, backlight, fb }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap the raw byte buffer as a `RawFrameBuf<Rgb565, _>` so
|
||||||
|
/// `embedded-graphics` primitives can draw into it, then run `f`.
|
||||||
|
/// The borrow ends when this returns, freeing `self.fb` for the
|
||||||
|
/// subsequent blit in `present()`.
|
||||||
|
fn with_fb<F: FnOnce(&mut RawFrameBuf<Rgb565, &mut [u8]>)>(&mut self, f: F) {
|
||||||
|
let mut fbuf = RawFrameBuf::<Rgb565, _>::new(&mut self.fb[..], FB_W, FB_H);
|
||||||
|
f(&mut fbuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DI, RST> BoardDisplay for St7789Color<DI, RST>
|
||||||
|
where
|
||||||
|
DI: Interface<Word = u8>,
|
||||||
|
RST: embedded_hal::digital::OutputPin,
|
||||||
|
{
|
||||||
|
/// Blit the entire framebuffer to the panel via the async SPI
|
||||||
|
/// interface. embassy-nrf's Spim runs the transfer over EasyDMA;
|
||||||
|
/// the `.await` yields the executor for the duration so the radio
|
||||||
|
/// task can process SX1262 IRQs concurrently.
|
||||||
|
async fn present(&mut self) {
|
||||||
|
let _ = self
|
||||||
|
.panel
|
||||||
|
.show_raw_data(0, 0, FB_W as u16, FB_H as u16, self.fb.as_ref())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_bright(&mut self) {
|
||||||
|
self.backlight.set_duty(0, bl_bright());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_dim(&mut self) {
|
||||||
|
self.backlight.set_duty(0, bl_dim());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_splash_learn_more(&mut self) {
|
||||||
|
self.with_fb(|fb| render_color::splash_learn_more(fb));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_splash_info(&mut self, info: &BoardInfo<'_>) {
|
||||||
|
self.with_fb(|fb| render_color::splash_info(fb, info));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_dashboard(&mut self, ctx: &DashboardCtx<'_>) {
|
||||||
|
self.with_fb(|fb| render_color::dashboard(fb, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_blank(&mut self) {
|
||||||
|
self.with_fb(|fb| render_color::blank(fb));
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -15,7 +15,11 @@ pub mod esp32s3;
|
|||||||
#[cfg(feature = "lilygo_tbeam")]
|
#[cfg(feature = "lilygo_tbeam")]
|
||||||
pub mod esp32;
|
pub mod esp32;
|
||||||
|
|
||||||
#[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))]
|
#[cfg(any(
|
||||||
|
feature = "heltec_mesh_node_t114",
|
||||||
|
feature = "rak_wisblock_4631",
|
||||||
|
feature = "wio_tracker_l1"
|
||||||
|
))]
|
||||||
pub mod nrf52840;
|
pub mod nrf52840;
|
||||||
|
|
||||||
#[cfg(feature = "waveshare_rp2040_lora")]
|
#[cfg(feature = "waveshare_rp2040_lora")]
|
||||||
|
|||||||
@@ -13,11 +13,23 @@ bind_interrupts!(pub struct Irqs {
|
|||||||
USBD => embassy_nrf::usb::InterruptHandler<embassy_nrf::peripherals::USBD>;
|
USBD => embassy_nrf::usb::InterruptHandler<embassy_nrf::peripherals::USBD>;
|
||||||
SPIM3 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI3>;
|
SPIM3 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI3>;
|
||||||
TWISPI0 => embassy_nrf::twim::InterruptHandler<embassy_nrf::peripherals::TWISPI0>;
|
TWISPI0 => embassy_nrf::twim::InterruptHandler<embassy_nrf::peripherals::TWISPI0>;
|
||||||
|
// SPIM2 is the second free SPI peripheral on nRF52840. Only used
|
||||||
|
// today by the Heltec T114 (ST7789 TFT panel on a write-only bus
|
||||||
|
// that can't share with the SX1262 on SPIM3 because the pins
|
||||||
|
// differ). Boards that don't use SPI2 don't construct anything
|
||||||
|
// against it and this binding is a no-op for them.
|
||||||
|
SPI2 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI2>;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── MCU-level types ─────────────────────────────────────────────────
|
// ── MCU-level types ─────────────────────────────────────────────────
|
||||||
|
|
||||||
pub type SpiBus = embassy_nrf::spim::Spim<'static>;
|
pub type SpiBus = embassy_nrf::spim::Spim<'static>;
|
||||||
|
// I2C is used by some nRF boards (RAK4631 / Wio Tracker L1 OLED) but not
|
||||||
|
// others (T114 uses SPI for the TFT). The `#[allow(dead_code)]` keeps
|
||||||
|
// the type available for any board that wants it without per-feature
|
||||||
|
// gating; rustc otherwise fires a dead-code lint when the active board
|
||||||
|
// doesn't construct one.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub type I2cBus = embassy_nrf::twim::Twim<'static>;
|
pub type I2cBus = embassy_nrf::twim::Twim<'static>;
|
||||||
pub type UsbNrfDriver =
|
pub type UsbNrfDriver =
|
||||||
embassy_nrf::usb::Driver<'static, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>;
|
embassy_nrf::usb::Driver<'static, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>;
|
||||||
@@ -37,6 +49,8 @@ pub fn share_spi_bus(spi: SpiBus) -> &'static embassy_sync::mutex::Mutex<NoopRaw
|
|||||||
///
|
///
|
||||||
/// Every nRF52840 Twim user needs a `&'static mut [u8]` buffer.
|
/// Every nRF52840 Twim user needs a `&'static mut [u8]` buffer.
|
||||||
/// This provides one via StaticCell so board files don't repeat the pattern.
|
/// This provides one via StaticCell so board files don't repeat the pattern.
|
||||||
|
/// Unused on nRF boards that don't expose I2C (e.g. T114 uses SPI for the TFT).
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn alloc_i2c_buffer() -> &'static mut [u8; 256] {
|
pub fn alloc_i2c_buffer() -> &'static mut [u8; 256] {
|
||||||
static TWIM_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
static TWIM_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||||
TWIM_BUF.init([0u8; 256])
|
TWIM_BUF.init([0u8; 256])
|
||||||
|
|||||||
+6
-3
@@ -46,12 +46,15 @@ use crate::protocol::{cap, Info, MAX_MCU_UID_LEN, MAX_RADIO_UID_LEN};
|
|||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] {
|
if #[cfg(any(feature = "heltec_mesh_node_t114", feature = "rak_wisblock_4631", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] {
|
||||||
use defmt_rtt as _;
|
use defmt_rtt as _;
|
||||||
use panic_probe as _;
|
use panic_probe as _;
|
||||||
} 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"))] {
|
} 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_backtrace as _;
|
||||||
use esp_println as _;
|
use esp_println as _;
|
||||||
|
// Emits the IDF app descriptor into `.flash.appdesc`. espflash v4
|
||||||
|
// refuses any ESP image that lacks it.
|
||||||
|
esp_bootloader_esp_idf::esp_app_desc!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ static mut INFO: Option<Info> = None;
|
|||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] {
|
if #[cfg(any(feature = "heltec_mesh_node_t114", feature = "rak_wisblock_4631", feature = "wio_tracker_l1", feature = "waveshare_rp2040_lora"))] {
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
async fn main(spawner: Spawner) {
|
async fn main(spawner: Spawner) {
|
||||||
run(spawner).await;
|
run(spawner).await;
|
||||||
@@ -90,7 +93,7 @@ async fn run(spawner: Spawner) {
|
|||||||
// of the RTT log. Critical for soak-debugging silent crash-reboots:
|
// of the RTT log. Critical for soak-debugging silent crash-reboots:
|
||||||
// a self-rebooting firmware otherwise looks like the log just
|
// a self-rebooting firmware otherwise looks like the log just
|
||||||
// restarted with no explanation. nRF52840-only.
|
// restarted with no explanation. nRF52840-only.
|
||||||
#[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))]
|
#[cfg(any(feature = "heltec_mesh_node_t114", feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))]
|
||||||
crate::hal::nrf52840::dump_reset_reason();
|
crate::hal::nrf52840::dump_reset_reason();
|
||||||
|
|
||||||
let parts = board.into_parts();
|
let parts = board.into_parts();
|
||||||
|
|||||||
+46
-26
@@ -1252,29 +1252,33 @@ impl RadioActions for RadioActionContext<'_> {
|
|||||||
Some(lora) => reconfigure_radio(lora, &cfg).await,
|
Some(lora) => reconfigure_radio(lora, &cfg).await,
|
||||||
None => Err(RadioError::Busy),
|
None => Err(RadioError::Busy),
|
||||||
};
|
};
|
||||||
if recfg_result.is_err() {
|
let mdltn = match recfg_result {
|
||||||
// Hardware failure: drop to Unconfigured (spec §6.3).
|
Ok(m) => m,
|
||||||
// Clear all per-session state — including any in-flight
|
Err(_) => {
|
||||||
// RxStart tag — so the next CmdRxStart in Unconfigured
|
// Hardware failure: drop to Unconfigured (spec §6.3).
|
||||||
// is rejected cleanly (rather than racing a stale tag
|
// Clear all per-session state — including any in-flight
|
||||||
// through respond_err_start_rx on an unrelated path).
|
// RxStart tag — so the next CmdRxStart in Unconfigured
|
||||||
self.config = None;
|
// is rejected cleanly (rather than racing a stale tag
|
||||||
self.cad_retry_delay_us = 0;
|
// through respond_err_start_rx on an unrelated path).
|
||||||
self.pending_rx_start_tag = None;
|
self.config = None;
|
||||||
info!("config cleared (apply_set_config failure tag={=u16})", tag);
|
self.cad_retry_delay_us = 0;
|
||||||
self.send(tag, DeviceMessage::Err(ErrorCode::ERadio)).await;
|
self.pending_rx_start_tag = None;
|
||||||
// Wedge takes precedence over ConfigFailedRadio so the
|
info!("config cleared (apply_set_config failure tag={=u16})", tag);
|
||||||
// chart enters Recovering (NRESET + lora.init()) instead
|
self.send(tag, DeviceMessage::Err(ErrorCode::ERadio)).await;
|
||||||
// of bouncing back to Unconfigured with a still-wedged
|
// Wedge takes precedence over ConfigFailedRadio so the
|
||||||
// chip that the next SET_CONFIG would also fail on.
|
// chart enters Recovering (NRESET + lora.init()) instead
|
||||||
if take_wedge_flag() {
|
// of bouncing back to Unconfigured with a still-wedged
|
||||||
let _ = self.emit(RadioInput::WedgeDetected);
|
// chip that the next SET_CONFIG would also fail on.
|
||||||
} else {
|
if take_wedge_flag() {
|
||||||
let _ = self.emit(RadioInput::ConfigFailedRadio);
|
let _ = self.emit(RadioInput::WedgeDetected);
|
||||||
|
} else {
|
||||||
|
let _ = self.emit(RadioInput::ConfigFailedRadio);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
self.config = Some(cfg);
|
self.config = Some(cfg);
|
||||||
|
self.mdltn = Some(mdltn);
|
||||||
info!("config set (apply_set_config success tag={=u16})", tag);
|
info!("config set (apply_set_config success tag={=u16})", tag);
|
||||||
self.cad_retry_delay_us = toa_us(&cfg, CAD_ASSUMED_PEER_PAYLOAD_B);
|
self.cad_retry_delay_us = toa_us(&cfg, CAD_ASSUMED_PEER_PAYLOAD_B);
|
||||||
if self.cad_retry_delay_us > CAD_BUDGET_US {
|
if self.cad_retry_delay_us > CAD_BUDGET_US {
|
||||||
@@ -2094,12 +2098,28 @@ fn to_lora_mod_params(
|
|||||||
lora.create_modulation_params(sf, bw, cr, cfg.freq_hz)
|
lora.create_modulation_params(sf, bw, cr, cfg.freq_hz)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reconfigure_radio(lora: &mut LoRaDriver, cfg: &LoRaConfig) -> Result<(), RadioError> {
|
async fn reconfigure_radio(
|
||||||
|
lora: &mut LoRaDriver,
|
||||||
|
cfg: &LoRaConfig,
|
||||||
|
) -> Result<lora_phy::mod_params::ModulationParams, RadioError> {
|
||||||
lora_setup("reconfigure_radio.enter_standby", lora.enter_standby()).await?;
|
lora_setup("reconfigure_radio.enter_standby", lora.enter_standby()).await?;
|
||||||
// Calibrate image etc. via a throwaway create_modulation_params call
|
// Apply the host-supplied LoRa sync word. The protocol field is a
|
||||||
// — inside the SET_CONFIG OK window so first TX/RX is fast.
|
// u16 holding the SX126x register pair in big-endian order
|
||||||
let _ = to_lora_mod_params(lora, cfg)?;
|
// (RadioLib convention: 0x1424 == byte 0x12 == LoRa "private" /
|
||||||
Ok(())
|
// MeshCore; 0x3444 == byte 0x34 == LoRaWAN "public"). lora-phy's
|
||||||
|
// set_lora_sync_word takes the underlying single-byte form, so
|
||||||
|
// undo the nibble encoding: convert_sync_word(byte) puts the
|
||||||
|
// chip's mandatory 0x4 in the low nibble of each register byte,
|
||||||
|
// and the high nibble of each register byte holds the original
|
||||||
|
// byte's high/low nibble respectively.
|
||||||
|
let [hi, lo] = cfg.sync_word.to_be_bytes();
|
||||||
|
let sync_byte = (hi & 0xF0) | (lo >> 4);
|
||||||
|
lora_setup(
|
||||||
|
"reconfigure_radio.set_lora_sync_word",
|
||||||
|
lora.set_lora_sync_word(sync_byte),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
to_lora_mod_params(lora, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_lora(cfg: &LoRaConfig, info: &Info) -> Result<(), ErrorCode> {
|
fn validate_lora(cfg: &LoRaConfig, info: &Info) -> Result<(), ErrorCode> {
|
||||||
|
|||||||
Reference in new issue
Block a user