refactor(radio): chart-first restructure on hsmc 0.3 + soak hardening
Migrate from hsmc 0.2 (local-patched) to hsmc 0.3.0 (crates.io, with embassy + trace-defmt features). Restructure radio chart and bundle several adjacent stability changes that need to land together for soak runs. Radio chart: - Booting (default, no handlers) -> Operational (default Unconfigured; super-state owning CmdPing/CmdGetInfo/Reset/WedgeDetected handlers + Recovering + CheckRecoveryBudget) -> Dead (terminal). - Cold init's during is structurally un-cancellable. - radio_task is a 30-line shim that panics if run() returns (chart is non-terminating). - Internal queue capacity bumped 8 -> 32 to cover action-emit bursts. Host chart: - Connected splits into WaitingForFirstFrame (default, no timer) + Active (chart-owned 1s inactivity timer via on(after) + self- transition on FrameReceived). Manual Timer::after + last_frame_at removed. Soak hardening: - ld/nrf52840-memory.x: app start 0x1000 -> 0x26000 to leave the factory s140 SoftDevice dormant in flash. - hal/nrf52840.rs: dump POWER.RESETREAS on boot -- WARN on DOG/LOCKUP, INFO on RESETPIN/SREQ/wake-from-OFF. - host/usb.rs: read_packet errors discriminated -- Disabled is debug (normal during USB enum), BufferOverflow stays warn. - COBS encode warns now carry type/tag/payload-length for diagnosis. - RADIO_THROTTLED static initializes to true (no proof of host drain at boot). - driver/sh1106.rs: init retries 3x with 100ms backoff (intermittent first-write race on power-up). - Cargo.toml: unexpected_cfgs allowlist for hsmc's journal/trace-* features. docs/semtech_lora_cad_lbt_guidebook.md: vendor the Semtech CAD/LBT reference for offline lookup.
This commit is contained in:
16 files changed
+2417
-262
No files matched your search
Generated
+13
-17
@@ -506,12 +506,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defmt-rtt"
|
name = "defmt-rtt"
|
||||||
version = "1.1.0"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93d5a25c99d89c40f5676bec8cefe0614f17f0f40e916f98e345dae941807f9e"
|
checksum = "c6eca0aae8aa2cf8333200ecbd236274697bc0a394765c858b3d9372eb1abcfa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"defmt 1.0.1",
|
"defmt 0.3.100",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -592,7 +592,7 @@ dependencies = [
|
|||||||
"cortex-m",
|
"cortex-m",
|
||||||
"cortex-m-rt",
|
"cortex-m-rt",
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"defmt 1.0.1",
|
"defmt 0.3.100",
|
||||||
"defmt-rtt",
|
"defmt-rtt",
|
||||||
"display-interface",
|
"display-interface",
|
||||||
"donglora-protocol",
|
"donglora-protocol",
|
||||||
@@ -1550,10 +1550,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc"
|
name = "hsmc"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "960effec6ded897c013d5cde8fb7f09bf0137e783efc4c2e0346092a2941c46e"
|
checksum = "ebed0d04633829a1e8319efcb038684dff8c01d06e4e50a4669691fb7c706f9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"defmt 0.3.100",
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
"embassy-sync 0.8.0",
|
"embassy-sync 0.8.0",
|
||||||
"embassy-time",
|
"embassy-time",
|
||||||
@@ -1563,9 +1564,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc-macros"
|
name = "hsmc-macros"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e1933eea1e95edb4fa03688f969f0a1ffabc4f16fc396b40ead6bedd5c4bdff"
|
checksum = "e4de168c4459a45a99c71d51be8af79d94eb09ecd62d6f25baee6ec174297db2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1574,9 +1575,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.10"
|
version = "0.4.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214"
|
checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
@@ -1733,7 +1734,6 @@ 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=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
]
|
]
|
||||||
@@ -1741,7 +1741,6 @@ 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=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"document-features",
|
"document-features",
|
||||||
@@ -1755,7 +1754,6 @@ 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=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"cmac",
|
"cmac",
|
||||||
@@ -1767,7 +1765,6 @@ 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=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"document-features",
|
"document-features",
|
||||||
@@ -1782,7 +1779,6 @@ 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=fix%2Fsx126x-busy-race#f9ebb303a7029cbc3f19f0760ae164946000b152"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2747,9 +2743,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ucobs"
|
name = "ucobs"
|
||||||
|
|||||||
+48
-10
@@ -19,7 +19,7 @@ categories = ["embedded", "hardware-support", "no-std"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
critical-section = "1.2"
|
critical-section = "1.2"
|
||||||
defmt = "1.0"
|
defmt = "0.3"
|
||||||
embedded-hal = "1.0"
|
embedded-hal = "1.0"
|
||||||
embedded-hal-async = "1.0"
|
embedded-hal-async = "1.0"
|
||||||
embassy-embedded-hal = "0.6"
|
embassy-embedded-hal = "0.6"
|
||||||
@@ -31,12 +31,11 @@ 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.2", default-features = false, features = ["embassy"] }
|
hsmc = { version = "0.3", default-features = false, features = ["embassy", "trace-defmt"] }
|
||||||
# Pulled from swaits/lora-rs fork until lora-rs/lora-rs#428 (pre-command
|
# Local path while iterating on CAD IRQ-clear / standby-after-CAD fixes.
|
||||||
# BUSY check) is merged + released on crates.io. Switch back to a normal
|
# Switch back to git fork (then crates.io) once changes are upstreamed.
|
||||||
# crates.io version then.
|
lora-phy = { path = "../../lora-rs/lora-phy", features = ["defmt-03"] }
|
||||||
lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fix/sx126x-busy-race", features = ["defmt-03"] }
|
portable-atomic = { version = "1", default-features = false, features = ["critical-section"] }
|
||||||
portable-atomic = { version = "1", optional = true, default-features = false, features = ["critical-section"] }
|
|
||||||
static_cell = "2.1"
|
static_cell = "2.1"
|
||||||
|
|
||||||
# Display rendering
|
# Display rendering
|
||||||
@@ -70,12 +69,22 @@ cortex-m = { version = "0.7", optional = true, features = [
|
|||||||
"critical-section-single-core",
|
"critical-section-single-core",
|
||||||
] }
|
] }
|
||||||
cortex-m-rt = { version = "0.7", optional = true }
|
cortex-m-rt = { version = "0.7", optional = true }
|
||||||
defmt-rtt = { version = "1.0", optional = true }
|
defmt-rtt = { version = "0.4", optional = true }
|
||||||
embassy-nrf = { version = "0.10", optional = true, features = [
|
embassy-nrf = { version = "0.10", optional = true, features = [
|
||||||
"nrf52840",
|
"nrf52840",
|
||||||
"gpiote",
|
"gpiote",
|
||||||
"time-driver-rtc1",
|
"time-driver-rtc1",
|
||||||
"defmt",
|
"defmt",
|
||||||
|
# UICR pin-config flags — must match what the Adafruit nRF52 bootloader
|
||||||
|
# actually programmed into UICR on this board. Verified empirically on
|
||||||
|
# RAK4631 via `probe-rs read b32 0x10001200 4`:
|
||||||
|
# PSELRESET[0/1] = 0x12 (P0.18 connected as RESET — NOT gpio)
|
||||||
|
# NFCPINS = 0xFFFFFFFE (bit 0 = 0 → NFC pins as GPIO)
|
||||||
|
# So `nfc-pins-as-gpio` matches; `reset-pin-as-gpio` would NOT match
|
||||||
|
# (would request GPIO when UICR has RESET enabled). The bootloader
|
||||||
|
# uses NRESET for its physical button-reset path, so leaving the pin
|
||||||
|
# as RESET is the right state.
|
||||||
|
"nfc-pins-as-gpio",
|
||||||
] }
|
] }
|
||||||
panic-probe = { version = "1.0", optional = true, features = ["print-defmt"] }
|
panic-probe = { version = "1.0", optional = true, features = ["print-defmt"] }
|
||||||
|
|
||||||
@@ -105,6 +114,21 @@ tokio = []
|
|||||||
# a human watches the pattern and reports N to determine where execution
|
# a human watches the pattern and reports N to determine where execution
|
||||||
# reached / stopped.
|
# reached / stopped.
|
||||||
debug-checkpoint = []
|
debug-checkpoint = []
|
||||||
|
# NOTE: chart auto-tracing is wired in unconditionally via the
|
||||||
|
# `trace;` keyword in each `statechart!` and the `hsmc/trace-defmt`
|
||||||
|
# feature on the dependency. Whether the trace lines actually emit at
|
||||||
|
# runtime is controlled by defmt's standard compile-time level filter
|
||||||
|
# (`DEFMT_LOG` env var). Release/shipping builds with `DEFMT_LOG`
|
||||||
|
# unset (or `off`) compile every chart trace call out to nothing —
|
||||||
|
# zero overhead, no separate cargo feature needed.
|
||||||
|
# When enabled, the SH1106 OLED driver emits a defmt::info! line for
|
||||||
|
# every I2C transaction it issues — bytes, lengths, addresses. Used to
|
||||||
|
# debug the embassy_nrf::twim assertion panic at twim.rs:541 (cannot
|
||||||
|
# happen via the documented call path; instrumenting the SH1106 path
|
||||||
|
# captures the actual transaction sequence preceding the panic so we
|
||||||
|
# can root-cause). Off by default; opt in via
|
||||||
|
# `just flash-attach rak_wisblock_4631 debug-twim`.
|
||||||
|
debug-twim = []
|
||||||
heltec_v3 = [
|
heltec_v3 = [
|
||||||
"embassy",
|
"embassy",
|
||||||
"dep:esp-backtrace",
|
"dep:esp-backtrace",
|
||||||
@@ -200,7 +224,6 @@ waveshare_rp2040_lora = [
|
|||||||
"dep:defmt-rtt",
|
"dep:defmt-rtt",
|
||||||
"dep:embassy-rp",
|
"dep:embassy-rp",
|
||||||
"dep:panic-probe",
|
"dep:panic-probe",
|
||||||
"dep:portable-atomic",
|
|
||||||
"embassy-executor/platform-cortex-m",
|
"embassy-executor/platform-cortex-m",
|
||||||
"embassy-executor/executor-thread",
|
"embassy-executor/executor-thread",
|
||||||
]
|
]
|
||||||
@@ -215,6 +238,17 @@ 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-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" }
|
esp-println = { git = "https://github.com/esp-rs/esp-hal.git", branch = "main" }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
# hsmc 0.3's `statechart!` macro emits `#[cfg(feature = "journal")]`
|
||||||
|
# attributes (and others). check-cfg evaluates those in the CALLER's
|
||||||
|
# crate context, so unless donglora declares the same feature names
|
||||||
|
# (we don't — they're hsmc's, not ours), rustc lints them. Per hsmc's
|
||||||
|
# README, the documented fix is to allow the lint with an explicit
|
||||||
|
# check-cfg list so other unexpected cfgs still surface as warnings.
|
||||||
|
unexpected_cfgs = { level = "allow", check-cfg = [
|
||||||
|
'cfg(feature, values("tokio", "embassy", "journal", "trace-defmt", "trace-log", "trace-tracing"))',
|
||||||
|
] }
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
undocumented_unsafe_blocks = "deny"
|
undocumented_unsafe_blocks = "deny"
|
||||||
|
|
||||||
@@ -223,4 +257,8 @@ codegen-units = 1
|
|||||||
lto = "fat"
|
lto = "fat"
|
||||||
opt-level = 3 # "s"/"z" break Xtensa (l32r literal range exceeded)
|
opt-level = 3 # "s"/"z" break Xtensa (l32r literal range exceeded)
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
strip = true
|
# `strip = true` was killing defmt's `_defmt_version_` / `_defmt_encoding_`
|
||||||
|
# symbols, breaking RTT log decoding via probe-rs. Keep symbols + DWARF
|
||||||
|
# so probe-rs can show source locations for log lines.
|
||||||
|
strip = false
|
||||||
|
debug = 2
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
workers = ["app.routes"]
|
||||||
|
|
||||||
|
[settings]
|
||||||
|
cache = ".script"
|
||||||
|
address = "127.0.0.1"
|
||||||
|
port = 3500
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -82,12 +82,48 @@ flash board extra_features="":
|
|||||||
|
|
||||||
# Flash ARM board via debug probe (requires J-Link or similar).
|
# Flash ARM board via debug probe (requires J-Link or similar).
|
||||||
# `extra_features` is passed through to the build.
|
# `extra_features` is passed through to the build.
|
||||||
|
# Sets DEFMT_LOG at build time so defmt::info!/warn!/error! are actually
|
||||||
|
# compiled in (default filter is ERROR-only — strips info logs).
|
||||||
|
# `embassy_usb=error` silences the benign Linux modem-manager
|
||||||
|
# `SET_INTERFACE: alt setting out of range` WARN that fires once per
|
||||||
|
# enumeration; it's a normal STALL response (correct USB behavior),
|
||||||
|
# the WARN is just embassy_usb noise. Other crates inherit the `info`
|
||||||
|
# default so chart traces, our own info!/warn!/error! all still fire.
|
||||||
flash-probe board extra_features="":
|
flash-probe board extra_features="":
|
||||||
@just _ensure_tools
|
@just _ensure_tools
|
||||||
@just build {{board}} release "{{extra_features}}"
|
@DEFMT_LOG=info,embassy_usb=error just build {{board}} release "{{extra_features}}"
|
||||||
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
||||||
probe-rs run --chip $chip "{{builds_dir}}/donglora-{{board}}-v{{version}}.elf"
|
probe-rs run --chip $chip "{{builds_dir}}/donglora-{{board}}-v{{version}}.elf"
|
||||||
|
|
||||||
|
# UF2-flash via the bootloader, then probe-rs attach for RTT log streaming.
|
||||||
|
# Required on boards (e.g. RAK4631) where the bootloader gates the app
|
||||||
|
# behind a "valid app flag" in its settings page that only the bootloader
|
||||||
|
# itself can set — `probe-rs run`'s direct SWD writes bypass that flag and
|
||||||
|
# leave the bootloader stuck in DFU mode. UF2 path goes through the
|
||||||
|
# bootloader, sets the flag, then we attach for RTT (no reset, no reflash).
|
||||||
|
flash-attach board extra_features="":
|
||||||
|
@just _ensure_tools
|
||||||
|
@DEFMT_LOG=info,embassy_usb=error just build {{board}} release "{{extra_features}}"
|
||||||
|
@just _flash_uf2 {{board}}
|
||||||
|
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
||||||
|
sleep 1; \
|
||||||
|
probe-rs attach --chip $chip --no-catch-reset \
|
||||||
|
"{{builds_dir}}/donglora-{{board}}-v{{version}}.elf"
|
||||||
|
|
||||||
|
# Attach probe-rs for live RTT/defmt streaming WITHOUT flashing or
|
||||||
|
# resetting the chip. Logs are tee'd to a UTC-timestamped file in /tmp
|
||||||
|
# so soak captures don't clobber each other across runs. Use this with
|
||||||
|
# the dongle already running the desired firmware (flash via `just flash`
|
||||||
|
# or `just flash-attach` first).
|
||||||
|
attach board:
|
||||||
|
@just _ensure_tools
|
||||||
|
@set -- $(just _info {{board}}); feat=$1; target=$2; chip=$3; \
|
||||||
|
log="/tmp/donglora-soak-$(date -u +%Y%m%dT%H%M%SZ).log"; \
|
||||||
|
echo "Logging to $log"; \
|
||||||
|
probe-rs attach --chip $chip --no-catch-reset \
|
||||||
|
"{{builds_dir}}/donglora-{{board}}-v{{version}}.elf" 2>&1 \
|
||||||
|
| tee "$log"
|
||||||
|
|
||||||
# Show binary size for a release build
|
# Show binary size for a release build
|
||||||
size board:
|
size board:
|
||||||
@just _cargo {{board}} "size --release"
|
@just _cargo {{board}} "size --release"
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
/* nRF52840 memory layout — Adafruit UF2 bootloader
|
/* nRF52840 memory layout — Adafruit UF2 bootloader + s140 SoftDevice
|
||||||
*
|
*
|
||||||
* 0x00000 – 0x01000 MBR (Master Boot Record)
|
* 0x00000 – 0x01000 MBR (Master Boot Record)
|
||||||
* 0x01000 – 0xEA000 Application ← we live here
|
* 0x01000 – 0x26000 s140 SoftDevice (dormant — we don't use BLE)
|
||||||
|
* 0x26000 – 0xEA000 Application ← we live here
|
||||||
* 0xEA000 – 0xF4000 Reserved (app data)
|
* 0xEA000 – 0xF4000 Reserved (app data)
|
||||||
* 0xF4000 – 0xFC000 UF2 Bootloader
|
* 0xF4000 – 0xFC000 UF2 Bootloader
|
||||||
* 0xFF000 – 0x100000 Bootloader settings
|
* 0xFF000 – 0x100000 Bootloader settings
|
||||||
*
|
*
|
||||||
* RAM: first 8 bytes reserved for MBR interrupt forwarding.
|
* RAM: first 8 bytes reserved for MBR interrupt forwarding. SoftDevice
|
||||||
|
* RAM (typically 0x20000008–0x20002000) is NOT reserved here because we
|
||||||
|
* never call sd_softdevice_enable — SD sits dormant in flash and never
|
||||||
|
* touches RAM. cortex-m-rt's set-vtor feature handles relocating VTOR
|
||||||
|
* to the app's vector table at 0x26000 at runtime.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
MEMORY
|
MEMORY
|
||||||
{
|
{
|
||||||
FLASH : ORIGIN = 0x00001000, LENGTH = 0xE9000
|
FLASH : ORIGIN = 0x00026000, LENGTH = 0xCE000
|
||||||
RAM : ORIGIN = 0x20000008, LENGTH = 255K
|
RAM : ORIGIN = 0x20000008, LENGTH = 255K
|
||||||
}
|
}
|
||||||
@@ -22,11 +22,18 @@ pub type UsbDriver = mcu::UsbNrfDriver;
|
|||||||
pub type DisplayI2c = mcu::I2cBus;
|
pub type DisplayI2c = mcu::I2cBus;
|
||||||
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
|
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
|
||||||
|
|
||||||
pub type DisplayDriver = ssd1306::Ssd1306Async<
|
// SH1106 hand-rolled driver instead of the ssd1306 crate. The RAK1921
|
||||||
ssd1306::prelude::I2CInterface<DisplayI2c>,
|
// OLED ships under various Chinese-OEM packaging (e.g. JMD0.96C) that
|
||||||
ssd1306::size::DisplaySize128x64,
|
// can be either SSD1306 or SH1106 silicon — physically and pin
|
||||||
ssd1306::mode::BufferedGraphicsModeAsync<ssd1306::size::DisplaySize128x64>,
|
// identical, but use different addressing modes. The bigger reason
|
||||||
>;
|
// though: ssd1306 0.10's display-interface-i2c emits I2C transaction
|
||||||
|
// patterns (consecutive writes, 3+ ops) that embassy_nrf::twim's
|
||||||
|
// `unreachable!()` rejects, so even on real SSD1306 silicon the panic
|
||||||
|
// would fire. Our hand-rolled SH1106 driver issues only single writes
|
||||||
|
// per logical op and is twim-safe. SH1106 init opcodes are similar
|
||||||
|
// enough that real SSD1306 silicon mostly works (worst case: mirrored
|
||||||
|
// display until proper runtime detection lands).
|
||||||
|
pub type DisplayDriver = crate::driver::sh1106::Sh1106<DisplayI2c>;
|
||||||
|
|
||||||
// ── Peripheral bundles ──────────────────────────────────────────────
|
// ── Peripheral bundles ──────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -46,22 +53,29 @@ pub struct DisplayParts {
|
|||||||
// ── Display init ────────────────────────────────────────────────────
|
// ── Display init ────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
pub async fn create_display(i2c: DisplayI2c) -> Option<DisplayDriver> {
|
||||||
use ssd1306::mode::DisplayConfigAsync;
|
let mut display = crate::driver::sh1106::Sh1106::new(i2c, 0x3C);
|
||||||
use ssd1306::prelude::{Brightness, DisplayRotation};
|
|
||||||
use ssd1306::size::DisplaySize128x64;
|
|
||||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
|
||||||
|
|
||||||
let interface = I2CDisplayInterface::new(i2c);
|
// Retry 3× with 100ms backoff. The SH1106's first transaction
|
||||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
// after power-on can return Err if the chip's internal voltage
|
||||||
.into_buffered_graphics_mode();
|
// converter hasn't fully settled — observed empirically on
|
||||||
|
// RAK4631/RAK1921. Each attempt is independent (full init
|
||||||
embassy_time::Timer::after_millis(100).await;
|
// sequence). Display works visibly even if the FIRST attempt
|
||||||
if display.init().await.is_err() {
|
// failed, because subsequent attempts succeed.
|
||||||
defmt::error!("SSD1306 display init failed");
|
for attempt in 0..3u8 {
|
||||||
return None;
|
embassy_time::Timer::after_millis(100).await;
|
||||||
|
if display.init().await.is_ok() {
|
||||||
|
let _ = display.set_brightness(0xFF).await;
|
||||||
|
return Some(display);
|
||||||
|
}
|
||||||
|
if attempt < 2 {
|
||||||
|
defmt::warn!(
|
||||||
|
"SH1106 display init attempt {=u8}/3 failed; retrying",
|
||||||
|
attempt + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let _ = display.set_brightness(Brightness::BRIGHTEST).await;
|
defmt::error!("SH1106 display init failed after 3 attempts");
|
||||||
Some(display)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Board init ──────────────────────────────────────────────────────
|
// ── Board init ──────────────────────────────────────────────────────
|
||||||
|
|||||||
+67
-3
@@ -9,9 +9,66 @@
|
|||||||
|
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
use embassy_sync::channel::Channel;
|
use embassy_sync::channel::Channel;
|
||||||
|
use portable_atomic::AtomicBool;
|
||||||
|
|
||||||
use crate::protocol::{Command, DeviceMessage, LoRaConfig};
|
use crate::protocol::{Command, DeviceMessage, LoRaConfig};
|
||||||
|
|
||||||
|
/// True while a Reset has been queued in `CommandChannel` but not yet
|
||||||
|
/// consumed by the radio task. Dedupes injections from any host chart
|
||||||
|
/// (USB DTR-driven OR UART inactivity-driven) that fires `reset_radio`
|
||||||
|
/// repeatedly during quiet/inactive periods. Without this dedup, a slow
|
||||||
|
/// or wedged radio causes the host chart's exit action to inject Reset
|
||||||
|
/// every cycle, filling the commands channel and producing a "queue
|
||||||
|
/// full" warn flood. Cleared by the radio chart's Reset handler
|
||||||
|
/// (`reset_to_unconfigured`).
|
||||||
|
///
|
||||||
|
/// Uses `portable_atomic::AtomicBool` (not `core::sync::atomic`) because
|
||||||
|
/// Cortex-M0+ targets (RP2040) lack native CAS for `swap`. portable-atomic
|
||||||
|
/// transparently uses native atomics on M3+/M4 and critical-section
|
||||||
|
/// emulation on M0/M0+.
|
||||||
|
pub static RESET_PENDING: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// True while the host transport's USB IN endpoint is observed-dead
|
||||||
|
/// (write_packet returned Err). Set by `host_task::protocol_loop` on the
|
||||||
|
/// first failed write; cleared on the next successful write. Producers of
|
||||||
|
/// **async** D→H events — `radio_task::record_packet` (RX) and
|
||||||
|
/// `host::framing::emit_async_err` — check this flag before pushing into
|
||||||
|
/// `OutboundChannel` and silently skip when set, so the queue does not
|
||||||
|
/// fill while the host is gone.
|
||||||
|
///
|
||||||
|
/// Tag-correlated responses (OK / sync ERR / TX_DONE) are NOT gated by
|
||||||
|
/// this flag: they're a small, bounded volume and the protocol contract
|
||||||
|
/// requires them to be delivered if we ever reach the host again.
|
||||||
|
///
|
||||||
|
/// Same `portable_atomic::AtomicBool` rationale as `RESET_PENDING`.
|
||||||
|
///
|
||||||
|
/// **Initial value `true`**: at boot we have no proof the host can
|
||||||
|
/// drain the outbound queue. Until host_task observes its first
|
||||||
|
/// successful `write_packet` (and clears this), async-RX producers
|
||||||
|
/// silently skip pushing. Otherwise — at boot, with no client
|
||||||
|
/// attached or with mux-rs idle — RX events would fill the queue
|
||||||
|
/// faster than host_task could drain it (which it can't: `write_packet`
|
||||||
|
/// blocks until the host actually polls), and the depth-64 ring would
|
||||||
|
/// flood `[WARN] RX dropped: outbound queue full` hundreds of times
|
||||||
|
/// per second. Pre-throttle is the structural fix.
|
||||||
|
pub static RADIO_THROTTLED: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
|
/// Set by `radio::lora_setup` / `lora_setup_with` when a SHORT-DURATION
|
||||||
|
/// lora-phy op blew through its wall-clock budget (almost always BUSY
|
||||||
|
/// stuck high — chip is wedged). Read by chart actions and `during:`
|
||||||
|
/// activities that called the helper: when they observe a downstream
|
||||||
|
/// `Err(_)` AND see this flag set, they emit `WedgeDetected` (which
|
||||||
|
/// drives the radio chart into `Recovering` for an in-place NRESET +
|
||||||
|
/// `lora.init()` retry) instead of the per-action failure event. The
|
||||||
|
/// flag is consumed (`swap(false)`) by whoever reads it so the next
|
||||||
|
/// independent op gets a fresh signal.
|
||||||
|
///
|
||||||
|
/// This is the cleanest plumbing path for "wedge timeout vs ordinary
|
||||||
|
/// driver error" given the helper sits below the chart-action layer
|
||||||
|
/// (free function, no chart-emit access). One atomic, one concern,
|
||||||
|
/// matching the established `RESET_PENDING` / `RADIO_THROTTLED` pattern.
|
||||||
|
pub static LORA_WEDGED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
// ── Host → Radio: tag-correlated commands ───────────────────────────
|
// ── Host → Radio: tag-correlated commands ───────────────────────────
|
||||||
|
|
||||||
/// An event flowing from the host task into the radio task.
|
/// An event flowing from the host task into the radio task.
|
||||||
@@ -42,9 +99,16 @@ pub struct OutboundFrame {
|
|||||||
pub msg: DeviceMessage,
|
pub msg: DeviceMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Radio-to-host outbound channel (depth 32: enough to hold a burst of
|
/// Radio-to-host outbound channel.
|
||||||
/// RX events + queued command responses without stalling the radio ISR).
|
///
|
||||||
pub type OutboundChannel = Channel<CriticalSectionRawMutex, OutboundFrame, 32>;
|
/// Depth 64: absorbs bursts of RX events plus queued command responses
|
||||||
|
/// plus async ERRs while the host transport drains. Each slot is ~280 B
|
||||||
|
/// (max RxPayload + envelope), so 64 slots is ~18 KB — well within RAM
|
||||||
|
/// budget on every supported MCU. The previous depth-32 was provoking
|
||||||
|
/// "outbound queue full" floods during host stalls; the larger queue
|
||||||
|
/// plus `RADIO_THROTTLED` backpressure flag (set by host_task on a
|
||||||
|
/// failed write_packet) together stop the cascade.
|
||||||
|
pub type OutboundChannel = Channel<CriticalSectionRawMutex, OutboundFrame, 64>;
|
||||||
|
|
||||||
// ── Internal: display commands (no longer wire-visible) ────────────
|
// ── Internal: display commands (no longer wire-visible) ────────────
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ statechart! {
|
|||||||
Display {
|
Display {
|
||||||
context: DisplayContext;
|
context: DisplayContext;
|
||||||
events: DisplayEvent;
|
events: DisplayEvent;
|
||||||
|
// Auto-trace — backend selected by `hsmc/trace-*` cargo feature.
|
||||||
|
trace;
|
||||||
default(On);
|
default(On);
|
||||||
|
|
||||||
state Off {
|
state Off {
|
||||||
|
|||||||
+4
-2
@@ -1,7 +1,8 @@
|
|||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "wio_tracker_l1",
|
feature = "wio_tracker_l1",
|
||||||
feature = "elecrow_thinknode_m2",
|
feature = "elecrow_thinknode_m2",
|
||||||
feature = "lilygo_tbeam_supreme"
|
feature = "lilygo_tbeam_supreme",
|
||||||
|
feature = "rak_wisblock_4631"
|
||||||
))]
|
))]
|
||||||
pub mod sh1106;
|
pub mod sh1106;
|
||||||
|
|
||||||
@@ -67,7 +68,8 @@ where
|
|||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "wio_tracker_l1",
|
feature = "wio_tracker_l1",
|
||||||
feature = "elecrow_thinknode_m2",
|
feature = "elecrow_thinknode_m2",
|
||||||
feature = "lilygo_tbeam_supreme"
|
feature = "lilygo_tbeam_supreme",
|
||||||
|
feature = "rak_wisblock_4631"
|
||||||
))]
|
))]
|
||||||
impl<I> DisplayBrightness for sh1106::Sh1106<I>
|
impl<I> DisplayBrightness for sh1106::Sh1106<I>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ impl<I: I2c> Sh1106<I> {
|
|||||||
/// Send a sequence of commands (each prefixed with the I2C command byte 0x00).
|
/// Send a sequence of commands (each prefixed with the I2C command byte 0x00).
|
||||||
async fn cmd(&mut self, commands: &[u8]) -> Result<(), I::Error> {
|
async fn cmd(&mut self, commands: &[u8]) -> Result<(), I::Error> {
|
||||||
for &c in commands {
|
for &c in commands {
|
||||||
|
#[cfg(feature = "debug-twim")]
|
||||||
|
defmt::info!(
|
||||||
|
"[sh1106] cmd write: addr=0x{=u8:02X} bytes=[0x00, 0x{=u8:02X}]",
|
||||||
|
self.addr,
|
||||||
|
c,
|
||||||
|
);
|
||||||
self.i2c.write(self.addr, &[0x00, c]).await?;
|
self.i2c.write(self.addr, &[0x00, c]).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -44,6 +50,8 @@ impl<I: I2c> Sh1106<I> {
|
|||||||
|
|
||||||
/// Initialize the display with sensible defaults.
|
/// Initialize the display with sensible defaults.
|
||||||
pub async fn init(&mut self) -> Result<(), I::Error> {
|
pub async fn init(&mut self) -> Result<(), I::Error> {
|
||||||
|
#[cfg(feature = "debug-twim")]
|
||||||
|
defmt::info!("[sh1106] init: starting");
|
||||||
self.cmd(&[
|
self.cmd(&[
|
||||||
0xAE, // display off
|
0xAE, // display off
|
||||||
0xD5, 0x80, // clock div: default
|
0xD5, 0x80, // clock div: default
|
||||||
@@ -71,6 +79,8 @@ impl<I: I2c> Sh1106<I> {
|
|||||||
page_buf[0] = 0x40;
|
page_buf[0] = 0x40;
|
||||||
|
|
||||||
for page in 0..PAGES {
|
for page in 0..PAGES {
|
||||||
|
#[cfg(feature = "debug-twim")]
|
||||||
|
defmt::info!("[sh1106] flush page {=usize}: addr cmds (3 bytes)", page);
|
||||||
// Set page address and column start (with SH1106 2-column offset).
|
// Set page address and column start (with SH1106 2-column offset).
|
||||||
self.cmd(&[
|
self.cmd(&[
|
||||||
0xB0 | page as u8, // page address
|
0xB0 | page as u8, // page address
|
||||||
@@ -81,6 +91,12 @@ impl<I: I2c> Sh1106<I> {
|
|||||||
|
|
||||||
let start = page * WIDTH;
|
let start = page * WIDTH;
|
||||||
page_buf[1..].copy_from_slice(&self.buffer[start..start + WIDTH]);
|
page_buf[1..].copy_from_slice(&self.buffer[start..start + WIDTH]);
|
||||||
|
#[cfg(feature = "debug-twim")]
|
||||||
|
defmt::info!(
|
||||||
|
"[sh1106] flush page {=usize}: data write {=usize} bytes (0x40 prefix + 128 pixels)",
|
||||||
|
page,
|
||||||
|
page_buf.len(),
|
||||||
|
);
|
||||||
self.i2c.write(self.addr, &page_buf).await?;
|
self.i2c.write(self.addr, &page_buf).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -88,6 +104,8 @@ impl<I: I2c> Sh1106<I> {
|
|||||||
|
|
||||||
/// Set display brightness (contrast).
|
/// Set display brightness (contrast).
|
||||||
pub async fn set_brightness(&mut self, value: u8) -> Result<(), I::Error> {
|
pub async fn set_brightness(&mut self, value: u8) -> Result<(), I::Error> {
|
||||||
|
#[cfg(feature = "debug-twim")]
|
||||||
|
defmt::info!("[sh1106] set_brightness: 0x{=u8:02X}", value);
|
||||||
self.cmd(&[0x81, value]).await
|
self.cmd(&[0x81, value]).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,76 @@ pub fn alloc_vbus_detect(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Reset reason ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Read the POWER.RESETREAS register, log a human-readable summary,
|
||||||
|
/// then clear it so the next boot reflects only the next reset cause.
|
||||||
|
///
|
||||||
|
/// This is the post-mortem diagnostic for soak runs: if the firmware
|
||||||
|
/// crashes and the chip resets, the next boot's RESETREAS tells us
|
||||||
|
/// what happened (cortex-m lockup, watchdog, soft reset, debugger,
|
||||||
|
/// pin reset, etc.). Without this, a self-rebooting firmware just
|
||||||
|
/// looks like log restarts with no signal as to why.
|
||||||
|
///
|
||||||
|
/// nRF52840 `POWER.RESETREAS` (PS §15.4.1): bits are write-1-to-clear.
|
||||||
|
/// The register accumulates causes across resets until cleared, so
|
||||||
|
/// always clear after reading.
|
||||||
|
pub fn dump_reset_reason() {
|
||||||
|
// POWER.RESETREAS lives at 0x40000400 (POWER base 0x40000000 +
|
||||||
|
// offset 0x400). Direct memory access avoids requiring embassy_nrf's
|
||||||
|
// `unstable-pac` feature. Bit layout per nRF52840 PS §15.4.1:
|
||||||
|
// 0 RESETPIN - external NRESET pin asserted
|
||||||
|
// 1 DOG - watchdog reset
|
||||||
|
// 2 SREQ - software-requested reset (cortex SYSRESETREQ)
|
||||||
|
// 3 LOCKUP - cortex-m core lockup
|
||||||
|
// 16 OFF - wake from System OFF via DETECT
|
||||||
|
// 17 LPCOMP - wake from System OFF via ANADETECT
|
||||||
|
// 18 DIF - wake from System OFF via debug interface
|
||||||
|
// 24 NFC - wake from System OFF via NFC field
|
||||||
|
const RESETREAS_ADDR: *mut u32 = 0x4000_0400 as *mut u32;
|
||||||
|
// SAFETY: 0x40000400 is the POWER.RESETREAS register on nRF52840;
|
||||||
|
// read/write_volatile is correct for memory-mapped hardware.
|
||||||
|
let raw = unsafe { core::ptr::read_volatile(RESETREAS_ADDR) };
|
||||||
|
let dog = (raw & (1 << 1)) != 0;
|
||||||
|
let lockup = (raw & (1 << 3)) != 0;
|
||||||
|
if raw == 0 {
|
||||||
|
defmt::info!("boot: cold POR (RESETREAS=0)");
|
||||||
|
} else if dog || lockup {
|
||||||
|
// Real problems: watchdog timeout (hung task) OR CPU lockup
|
||||||
|
// (HardFault → escalated). Worth a WARN — these are bugs.
|
||||||
|
defmt::warn!(
|
||||||
|
"boot: ABNORMAL reset RESETREAS=0x{=u32:08x} resetpin={=bool} dog={=bool} sreq={=bool} lockup={=bool} off={=bool} lpcomp={=bool} dif={=bool} nfc={=bool}",
|
||||||
|
raw,
|
||||||
|
(raw & (1 << 0)) != 0,
|
||||||
|
dog,
|
||||||
|
(raw & (1 << 2)) != 0,
|
||||||
|
lockup,
|
||||||
|
(raw & (1 << 16)) != 0,
|
||||||
|
(raw & (1 << 17)) != 0,
|
||||||
|
(raw & (1 << 18)) != 0,
|
||||||
|
(raw & (1 << 24)) != 0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Normal reset paths: RESETPIN (UF2 bootloader pulses NRESET on
|
||||||
|
// app start; user reset button), SREQ (deliberate sys_reset),
|
||||||
|
// OFF/LPCOMP/DIF/NFC (wake from System OFF). All expected
|
||||||
|
// operational reasons; INFO not WARN.
|
||||||
|
defmt::info!(
|
||||||
|
"boot: RESETREAS=0x{=u32:08x} resetpin={=bool} sreq={=bool} off={=bool} lpcomp={=bool} dif={=bool} nfc={=bool}",
|
||||||
|
raw,
|
||||||
|
(raw & (1 << 0)) != 0,
|
||||||
|
(raw & (1 << 2)) != 0,
|
||||||
|
(raw & (1 << 16)) != 0,
|
||||||
|
(raw & (1 << 17)) != 0,
|
||||||
|
(raw & (1 << 18)) != 0,
|
||||||
|
(raw & (1 << 24)) != 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Clear all bits — write-1-to-clear per nRF52840 PS §15.4.1.
|
||||||
|
// SAFETY: same justification as the read above.
|
||||||
|
unsafe { core::ptr::write_volatile(RESETREAS_ADDR, 0xFFFF_FFFF) };
|
||||||
|
}
|
||||||
|
|
||||||
// ── MAC address ─────────────────────────────────────────────────────
|
// ── MAC address ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Read the factory-programmed device address from FICR registers.
|
/// Read the factory-programmed device address from FICR registers.
|
||||||
|
|||||||
+28
-3
@@ -54,7 +54,16 @@ pub async fn dispatch_frame(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Emit an async framing error (tag = 0x0000) per spec §2.5.
|
/// Emit an async framing error (tag = 0x0000) per spec §2.5.
|
||||||
|
///
|
||||||
|
/// Backpressure: if the host transport's IN endpoint is observed-dead
|
||||||
|
/// (RADIO_THROTTLED set by host_task), drop silently. Without this gate,
|
||||||
|
/// a bad-frame burst from a wedged host fills `OutboundChannel` faster
|
||||||
|
/// than host_task can drain it (because it can't drain it at all when
|
||||||
|
/// write_packet keeps failing), producing the warn flood seen in soak.
|
||||||
pub async fn emit_async_err(outbound: &OutboundChannel, code: ErrorCode) {
|
pub async fn emit_async_err(outbound: &OutboundChannel, code: ErrorCode) {
|
||||||
|
if crate::channel::RADIO_THROTTLED.load(portable_atomic::Ordering::Acquire) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let frame = OutboundFrame {
|
let frame = OutboundFrame {
|
||||||
tag: 0,
|
tag: 0,
|
||||||
msg: DeviceMessage::Err(code),
|
msg: DeviceMessage::Err(code),
|
||||||
@@ -106,15 +115,31 @@ pub fn encode_outbound(frame: &OutboundFrame, out: &mut [u8; MAX_WIRE_FRAME]) ->
|
|||||||
) {
|
) {
|
||||||
Ok(n) => Some(n),
|
Ok(n) => Some(n),
|
||||||
Err(FrameEncodeError::BufferTooSmall) => {
|
Err(FrameEncodeError::BufferTooSmall) => {
|
||||||
warn!("encode_frame: output buffer too small");
|
warn!(
|
||||||
|
"encode_frame: output buffer too small (type={=u8}, tag={=u16}, payload={=usize}B, out_buf={=usize}B)",
|
||||||
|
frame.msg.type_id(),
|
||||||
|
frame.tag,
|
||||||
|
payload_len,
|
||||||
|
out.len(),
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Err(FrameEncodeError::PayloadTooLarge) => {
|
Err(FrameEncodeError::PayloadTooLarge) => {
|
||||||
warn!("encode_frame: payload exceeds MAX_PAYLOAD_FIELD");
|
warn!(
|
||||||
|
"encode_frame: payload exceeds MAX_PAYLOAD_FIELD (type={=u8}, tag={=u16}, payload={=usize}B)",
|
||||||
|
frame.msg.type_id(),
|
||||||
|
frame.tag,
|
||||||
|
payload_len,
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Err(FrameEncodeError::CobsEncode) => {
|
Err(FrameEncodeError::CobsEncode) => {
|
||||||
warn!("encode_frame: COBS encode refused");
|
warn!(
|
||||||
|
"encode_frame: COBS encode refused (type={=u8}, tag={=u16}, payload={=usize}B)",
|
||||||
|
frame.msg.type_id(),
|
||||||
|
frame.tag,
|
||||||
|
payload_len,
|
||||||
|
);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+204
-41
@@ -2,24 +2,35 @@
|
|||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! Host
|
//! Host
|
||||||
//! ├── Disconnected (default) no DTR, or inactivity timer fired
|
//! ├── Disconnected (default) no DTR
|
||||||
//! └── Connected host attached, liveness OK
|
//! └── Connected host attached
|
||||||
//! entry: wake_display, start_liveness_timer
|
//! ├── WaitingForFirstFrame (default) DTR up, no traffic yet
|
||||||
//! exit: reset_radio, reset_display, stop_liveness_timer
|
//! │ NO inactivity timer (boot quiet
|
||||||
|
//! │ is normal — chart structurally
|
||||||
|
//! │ blocks the spurious 1s fire).
|
||||||
|
//! └── Active ≥1 frame seen since connect
|
||||||
|
//! inactivity timer (1s) armed and
|
||||||
|
//! restarted by every frame via
|
||||||
|
//! self-transition.
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! The statechart tracks one state bit: "is a host present?". DTR drives
|
//! Two chart-level invariants made structurally impossible by the shape:
|
||||||
//! connect edges; the 1-second inactivity watchdog (spec §3.4) drives
|
//! 1. **No spurious inactivity at boot.** The 1-second timer only exists
|
||||||
//! the disconnect edge when DTR is held high but no bytes arrive.
|
//! in `Active`, which requires a `FrameReceived` event to enter.
|
||||||
|
//! A freshly-connected host that holds DTR high but sends nothing
|
||||||
|
//! sits in `WaitingForFirstFrame` indefinitely — no timer, no warn,
|
||||||
|
//! no Reset cascade.
|
||||||
|
//! 2. **Chart-owned timer scope.** Per hsmc README rule §7 (timers
|
||||||
|
//! armed by state lifecycle), the inactivity timer is cancelled on
|
||||||
|
//! every exit of `Active`, restarted on every entry. No global
|
||||||
|
//! `last_frame_at` instant, no manual elapsed check in the loop.
|
||||||
//!
|
//!
|
||||||
//! The data plane (frame decode, command dispatch, frame encode) stays
|
//! The data plane (frame decode, command dispatch, frame encode) stays
|
||||||
//! imperative in `protocol_loop` for the same reason the radio's
|
//! imperative in `protocol_loop` because `read_packet` blocks.
|
||||||
//! `rx_once` stayed out of its chart: `read_packet` blocks.
|
|
||||||
|
|
||||||
use defmt::warn;
|
|
||||||
use embassy_executor::task;
|
use embassy_executor::task;
|
||||||
use embassy_futures::join::join;
|
use embassy_futures::join::join;
|
||||||
use embassy_time::{Duration, Instant};
|
use embassy_time::Duration;
|
||||||
use embassy_usb::class::cdc_acm::{CdcAcmClass, Receiver, Sender, State};
|
use embassy_usb::class::cdc_acm::{CdcAcmClass, Receiver, Sender, State};
|
||||||
use embassy_usb::Builder;
|
use embassy_usb::Builder;
|
||||||
use hsmc::{statechart, Duration as HsmcDuration};
|
use hsmc::{statechart, Duration as HsmcDuration};
|
||||||
@@ -35,17 +46,24 @@ const MAX_USB_PACKET: usize = 64;
|
|||||||
|
|
||||||
/// Spec §3.4: inactivity timer period. The device must see at least one
|
/// Spec §3.4: inactivity timer period. The device must see at least one
|
||||||
/// frame (good or bad) every 1000 ms or the session is considered lost.
|
/// frame (good or bad) every 1000 ms or the session is considered lost.
|
||||||
|
/// Declared in the chart on `Active` — chart owns timer lifecycle.
|
||||||
const INACTIVITY_MS: u64 = 1000;
|
const INACTIVITY_MS: u64 = 1000;
|
||||||
|
|
||||||
/// Poll interval for the DTR edge detector / inactivity check.
|
/// Poll interval for the DTR edge detector. Chart owns the inactivity
|
||||||
|
/// timer; this is only the cadence at which protocol_loop wakes when
|
||||||
|
/// no other event is pending, to re-check DTR.
|
||||||
const POLL_INTERVAL_MS: u64 = 100;
|
const POLL_INTERVAL_MS: u64 = 100;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum HostInput {
|
pub enum HostInput {
|
||||||
DtrRaised,
|
DtrRaised,
|
||||||
DtrDropped,
|
DtrDropped,
|
||||||
/// Host went silent for >1 s; kick the radio back to Unconfigured.
|
/// A USB read returned ≥1 byte. Drives `WaitingForFirstFrame →
|
||||||
InactivityTimeout,
|
/// Active` (first frame after connect) and the `Active → Active`
|
||||||
|
/// self-transition that restarts the inactivity timer per
|
||||||
|
/// hsmc README rule §4 (self-transition exits + re-enters target,
|
||||||
|
/// re-arming its timer).
|
||||||
|
FrameReceived,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HostContext {
|
pub struct HostContext {
|
||||||
@@ -68,8 +86,22 @@ impl HostContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send_radio_reset(&self) {
|
fn send_radio_reset(&self) {
|
||||||
|
use portable_atomic::Ordering;
|
||||||
|
// Dedupe: if a Reset is already queued (and not yet consumed by
|
||||||
|
// the radio task), don't queue another. Without this, a slow or
|
||||||
|
// wedged radio + repeated chart exits (DTR drop, inactivity,
|
||||||
|
// both transports) fill the commands channel and produce the
|
||||||
|
// "queue full" warn flood. The radio chart's reset_to_unconfigured
|
||||||
|
// clears the flag once it processes the queued Reset.
|
||||||
|
if crate::channel::RESET_PENDING.swap(true, Ordering::AcqRel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.commands.try_send(InboundEvent::Reset).is_err() {
|
if self.commands.try_send(InboundEvent::Reset).is_err() {
|
||||||
warn!("radio Reset inject: command queue full");
|
// Channel genuinely full of non-Reset commands; this is the
|
||||||
|
// edge case worth knowing about. Clear the flag so future
|
||||||
|
// Reset attempts can re-try once the channel drains.
|
||||||
|
defmt::warn!("radio Reset inject: command queue full");
|
||||||
|
crate::channel::RESET_PENDING.store(false, Ordering::Release);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +110,8 @@ statechart! {
|
|||||||
Host {
|
Host {
|
||||||
context: HostContext;
|
context: HostContext;
|
||||||
events: HostInput;
|
events: HostInput;
|
||||||
|
// Auto-trace — backend selected by `hsmc/trace-*` cargo feature.
|
||||||
|
trace;
|
||||||
default(Disconnected);
|
default(Disconnected);
|
||||||
|
|
||||||
state Disconnected {
|
state Disconnected {
|
||||||
@@ -88,7 +122,31 @@ statechart! {
|
|||||||
entry: wake_display;
|
entry: wake_display;
|
||||||
exit: reset_radio, reset_display;
|
exit: reset_radio, reset_display;
|
||||||
on(DtrDropped) => Disconnected;
|
on(DtrDropped) => Disconnected;
|
||||||
on(InactivityTimeout) => Disconnected;
|
default(WaitingForFirstFrame);
|
||||||
|
|
||||||
|
// Default child of Connected. No inactivity timer yet —
|
||||||
|
// a freshly-connected host that hasn't sent its first byte
|
||||||
|
// is normal (mux-rs holds DTR high before any client
|
||||||
|
// attaches). Stays here indefinitely until the first
|
||||||
|
// frame arrives.
|
||||||
|
state WaitingForFirstFrame {
|
||||||
|
on(FrameReceived) => Active;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inactivity timer is chart-scoped: armed on every entry to
|
||||||
|
// Active (whether from WaitingForFirstFrame or self-loop
|
||||||
|
// after a frame), cancelled on every exit (per hsmc README
|
||||||
|
// rule §7). The self-transition `on(FrameReceived) =>
|
||||||
|
// Active` re-enters Active per rule §4, re-arming the
|
||||||
|
// timer — equivalent to the "reset last_frame_at on each
|
||||||
|
// byte" pattern but expressed in chart shape.
|
||||||
|
//
|
||||||
|
// hsmc's `after` triggers want `hsmc::Duration` (re-export
|
||||||
|
// of `core::time::Duration`), not `embassy_time::Duration`.
|
||||||
|
state Active {
|
||||||
|
on(after HsmcDuration::from_millis(INACTIVITY_MS)) => Disconnected;
|
||||||
|
on(FrameReceived) => Active;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,20 +191,34 @@ pub async fn host_task(
|
|||||||
}
|
}
|
||||||
config.serial_number = Some(core::str::from_utf8(serial_buf).expect("MAC hex is valid UTF-8"));
|
config.serial_number = Some(core::str::from_utf8(serial_buf).expect("MAC hex is valid UTF-8"));
|
||||||
|
|
||||||
|
// CTRL_BUF was 64 bytes; bumped to 128 because the previous size
|
||||||
|
// matches the panic site "control buffer too small" in
|
||||||
|
// embassy_usb/src/lib.rs:758. CdcAcmClass only requires >= 7 for
|
||||||
|
// SET_LINE_CODING, but composite descriptor handlers and string
|
||||||
|
// descriptor responses may overrun a 64 B buffer for some host
|
||||||
|
// enumerations. 128 is safely above the largest CDC-ACM control
|
||||||
|
// request and well within RAM budget.
|
||||||
static DESC_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
static DESC_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||||
static CONF_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
static CONF_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||||
static BOS_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
static BOS_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||||
static CTRL_BUF: StaticCell<[u8; 64]> = StaticCell::new();
|
static CTRL_BUF: StaticCell<[u8; 128]> = StaticCell::new();
|
||||||
static CDC_STATE: StaticCell<State> = StaticCell::new();
|
static CDC_STATE: StaticCell<State> = StaticCell::new();
|
||||||
|
|
||||||
let desc_buf = DESC_BUF.init([0; 256]);
|
let desc_buf = DESC_BUF.init([0; 256]);
|
||||||
let conf_buf = CONF_BUF.init([0; 256]);
|
let conf_buf = CONF_BUF.init([0; 256]);
|
||||||
let bos_buf = BOS_BUF.init([0; 256]);
|
let bos_buf = BOS_BUF.init([0; 256]);
|
||||||
let ctrl_buf = CTRL_BUF.init([0; 64]);
|
let ctrl_buf = CTRL_BUF.init([0; 128]);
|
||||||
let cdc_state = CDC_STATE.init(State::new());
|
let cdc_state = CDC_STATE.init(State::new());
|
||||||
|
|
||||||
let mut builder = Builder::new(parts.driver, config, desc_buf, conf_buf, bos_buf, ctrl_buf);
|
let mut builder = Builder::new(parts.driver, config, desc_buf, conf_buf, bos_buf, ctrl_buf);
|
||||||
|
|
||||||
|
// Note: when this device enumerates on Linux, modem-manager's
|
||||||
|
// CDC-ACM probe issues SET_INTERFACE with alt settings beyond what
|
||||||
|
// we declare, triggering a "SET_INTERFACE: alt setting out of
|
||||||
|
// range" WARN from embassy_usb. The probe is normal Linux
|
||||||
|
// behavior; embassy_usb's STALL response is correct USB negotiation.
|
||||||
|
// The WARN is benign noise — do NOT add dummy alt settings just to
|
||||||
|
// silence it (risks breaking Windows/macOS enumeration).
|
||||||
let class = CdcAcmClass::new(&mut builder, cdc_state, MAX_USB_PACKET as u16);
|
let class = CdcAcmClass::new(&mut builder, cdc_state, MAX_USB_PACKET as u16);
|
||||||
let mut usb_dev = builder.build();
|
let mut usb_dev = builder.build();
|
||||||
let (sender, receiver) = class.split();
|
let (sender, receiver) = class.split();
|
||||||
@@ -186,8 +258,6 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
|
|||||||
let mut write_buf: [u8; MAX_WIRE_FRAME] = [0u8; MAX_WIRE_FRAME];
|
let mut write_buf: [u8; MAX_WIRE_FRAME] = [0u8; MAX_WIRE_FRAME];
|
||||||
let mut decoder = FrameDecoder::new();
|
let mut decoder = FrameDecoder::new();
|
||||||
|
|
||||||
let mut last_frame_at = Instant::now();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match select3(
|
match select3(
|
||||||
receiver.read_packet(&mut read_buf),
|
receiver.read_packet(&mut read_buf),
|
||||||
@@ -196,52 +266,145 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Either3::First(Ok(0)) => {}
|
Either3::First(Ok(0)) => {
|
||||||
|
defmt::info!("host_task: read_packet returned 0 bytes");
|
||||||
|
}
|
||||||
Either3::First(Ok(n)) => {
|
Either3::First(Ok(n)) => {
|
||||||
last_frame_at = Instant::now();
|
defmt::info!(
|
||||||
|
"host_task: read_packet got {=usize}B (DTR={=bool})",
|
||||||
|
n,
|
||||||
|
receiver.dtr(),
|
||||||
|
);
|
||||||
|
// Drives WaitingForFirstFrame → Active on first byte
|
||||||
|
// and Active → Active (timer-restart) on every byte
|
||||||
|
// after. The chart owns inactivity from here.
|
||||||
|
let _ = machine.dispatch(HostInput::FrameReceived).await;
|
||||||
dispatch_bytes(&mut decoder, &read_buf[..n], commands, outbound).await;
|
dispatch_bytes(&mut decoder, &read_buf[..n], commands, outbound).await;
|
||||||
}
|
}
|
||||||
Either3::First(Err(_)) => {
|
Either3::First(Err(e)) => {
|
||||||
|
// Per embassy_usb_driver::EndpointError, `read_packet`
|
||||||
|
// returns one of two errors:
|
||||||
|
// - `Disabled`: OUT endpoint not enabled by the host
|
||||||
|
// (USB enumeration in progress, host
|
||||||
|
// suspended, or interface not yet
|
||||||
|
// configured). Completely normal during
|
||||||
|
// bring-up + every host disconnect.
|
||||||
|
// - `BufferOverflow`: incoming packet larger than read_buf.
|
||||||
|
// read_buf is sized to MAX_USB_PACKET
|
||||||
|
// (64) which matches the CDC-ACM EP
|
||||||
|
// max packet size, so this should
|
||||||
|
// never happen in practice.
|
||||||
|
//
|
||||||
|
// `Disabled` floods at boot before USB enumeration finishes
|
||||||
|
// (the OS hasn't sent SET_CONFIGURATION yet) — was previously
|
||||||
|
// logging dozens of "read_packet error" lines per second.
|
||||||
|
// Demote to debug-level since it's expected; reserve WARN
|
||||||
|
// for BufferOverflow which IS a real bug.
|
||||||
|
use embassy_usb_driver::EndpointError;
|
||||||
|
match e {
|
||||||
|
EndpointError::Disabled => {
|
||||||
|
defmt::debug!(
|
||||||
|
"host_task: read_packet returned Disabled (USB endpoint not yet enabled by host — normal during enumeration)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EndpointError::BufferOverflow => {
|
||||||
|
defmt::warn!(
|
||||||
|
"host_task: read_packet BufferOverflow — host sent a packet larger than {=usize}B (max EP packet size); decoder reset",
|
||||||
|
MAX_USB_PACKET,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
decoder.reset();
|
decoder.reset();
|
||||||
embassy_time::Timer::after_millis(100).await;
|
embassy_time::Timer::after_millis(100).await;
|
||||||
}
|
}
|
||||||
Either3::Second(frame) => {
|
Either3::Second(frame) => {
|
||||||
|
let frame_tag = frame.tag;
|
||||||
if let Some(n) = encode_outbound(&frame, &mut write_buf) {
|
if let Some(n) = encode_outbound(&frame, &mut write_buf) {
|
||||||
for chunk in write_buf[..n].chunks(MAX_USB_PACKET) {
|
defmt::info!(
|
||||||
|
"host_task: sending {=usize}B frame tag={=u16}",
|
||||||
|
n,
|
||||||
|
frame_tag,
|
||||||
|
);
|
||||||
|
// Per embassy_usb 0.6: write_packet only returns
|
||||||
|
// EndpointError::Disabled when the IN endpoint is
|
||||||
|
// dead (host gone or USB suspended). If a chunk
|
||||||
|
// mid-frame fails, the partial bytes already sent
|
||||||
|
// are unrecoverable — host's framing layer will see
|
||||||
|
// corruption until a clean frame arrives. Bail on
|
||||||
|
// the rest of THIS frame and set RADIO_THROTTLED so
|
||||||
|
// the radio task stops generating async events
|
||||||
|
// until the host comes back. Without the throttle,
|
||||||
|
// every subsequent frame would also fail mid-write
|
||||||
|
// and produce more wire corruption.
|
||||||
|
let mut frame_failed = false;
|
||||||
|
for (chunk_idx, chunk) in
|
||||||
|
write_buf[..n].chunks(MAX_USB_PACKET).enumerate()
|
||||||
|
{
|
||||||
|
defmt::info!(
|
||||||
|
"host_task: write_packet chunk {=usize} ({=usize}B) tag={=u16}",
|
||||||
|
chunk_idx,
|
||||||
|
chunk.len(),
|
||||||
|
frame_tag,
|
||||||
|
);
|
||||||
if sender.write_packet(chunk).await.is_err() {
|
if sender.write_packet(chunk).await.is_err() {
|
||||||
warn!("USB write failed, frame dropped");
|
defmt::warn!(
|
||||||
|
"USB write failed (host stalled or gone); frame dropped tag={=u16}, throttling radio",
|
||||||
|
frame_tag,
|
||||||
|
);
|
||||||
|
crate::channel::RADIO_THROTTLED
|
||||||
|
.store(true, portable_atomic::Ordering::Release);
|
||||||
|
frame_failed = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
defmt::info!(
|
||||||
|
"host_task: write_packet OK chunk {=usize} tag={=u16}",
|
||||||
|
chunk_idx,
|
||||||
|
frame_tag,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
if !frame_failed
|
||||||
|
&& crate::channel::RADIO_THROTTLED
|
||||||
|
.load(portable_atomic::Ordering::Acquire)
|
||||||
|
{
|
||||||
|
defmt::info!(
|
||||||
|
"USB write recovered (tag={=u16}); clearing radio throttle",
|
||||||
|
frame_tag,
|
||||||
|
);
|
||||||
|
crate::channel::RADIO_THROTTLED
|
||||||
|
.store(false, portable_atomic::Ordering::Release);
|
||||||
|
}
|
||||||
|
defmt::info!("host_task: frame done tag={=u16}", frame_tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Either3::Third(()) => {}
|
Either3::Third(()) => {
|
||||||
|
// Heartbeat tick — drives DTR edge polling + inactivity
|
||||||
|
// watchdog below. No log: it would fire every
|
||||||
|
// POLL_INTERVAL_MS and bury the interesting traffic.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DTR edge detection — drives Connected / Disconnected transitions.
|
// DTR edge detection — drives Connected / Disconnected
|
||||||
let connected = receiver.dtr();
|
// transitions. The chart's `current_state()` may be
|
||||||
let was_connected = matches!(machine.current_state(), HostState::Connected);
|
// Disconnected, Connected/WaitingForFirstFrame, or
|
||||||
match (was_connected, connected) {
|
// Connected/Active per the new hierarchy. We're connected if
|
||||||
|
// current state is anywhere inside Connected.
|
||||||
|
let dtr_high = receiver.dtr();
|
||||||
|
let was_connected = !matches!(
|
||||||
|
machine.current_state(),
|
||||||
|
HostState::Disconnected
|
||||||
|
);
|
||||||
|
match (was_connected, dtr_high) {
|
||||||
(false, true) => {
|
(false, true) => {
|
||||||
|
defmt::info!("host_task: DTR raised");
|
||||||
let _ = machine.dispatch(HostInput::DtrRaised).await;
|
let _ = machine.dispatch(HostInput::DtrRaised).await;
|
||||||
last_frame_at = Instant::now();
|
|
||||||
}
|
}
|
||||||
(true, false) => {
|
(true, false) => {
|
||||||
|
defmt::info!("host_task: DTR dropped");
|
||||||
decoder.reset();
|
decoder.reset();
|
||||||
let _ = machine.dispatch(HostInput::DtrDropped).await;
|
let _ = machine.dispatch(HostInput::DtrDropped).await;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inactivity watchdog (spec §3.4): while Connected, if no frame
|
|
||||||
// arrived in the last INACTIVITY_MS, drive the session back to
|
|
||||||
// Disconnected.
|
|
||||||
if matches!(machine.current_state(), HostState::Connected)
|
|
||||||
&& last_frame_at.elapsed() >= Duration::from_millis(INACTIVITY_MS)
|
|
||||||
{
|
|
||||||
warn!("host inactivity timeout fired");
|
|
||||||
let _ = machine.dispatch(HostInput::InactivityTimeout).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ cfg_if::cfg_if! {
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
async fn run(spawner: Spawner) {
|
async fn run(spawner: Spawner) {
|
||||||
let board = <board::Board as LoRaBoard>::init();
|
let board = <board::Board as LoRaBoard>::init();
|
||||||
|
|
||||||
|
// Read + log + clear POWER.RESETREAS before any other boot output so
|
||||||
|
// the cause of the *previous* reset (if any) lands at the very top
|
||||||
|
// of the RTT log. Critical for soak-debugging silent crash-reboots:
|
||||||
|
// a self-rebooting firmware otherwise looks like the log just
|
||||||
|
// restarted with no explanation. nRF52840-only.
|
||||||
|
#[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))]
|
||||||
|
crate::hal::nrf52840::dump_reset_reason();
|
||||||
|
|
||||||
let parts = board.into_parts();
|
let parts = board.into_parts();
|
||||||
|
|
||||||
// Build the GET_INFO struct once at boot. This is the authoritative
|
// Build the GET_INFO struct once at boot. This is the authoritative
|
||||||
|
|||||||
+828
-162
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user