From f68e5d9be54e1c10b298f17533eb1ec90aaf3d31 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 28 Apr 2026 15:28:01 -0600 Subject: [PATCH] fix(host/uart): deliver RX to host and survive sustained bidirectional load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions on UART boards (heltec_v3_uart, etc.): 1. RADIO_THROTTLED was never cleared on UART. The throttle gate at src/radio.rs:2017 (added in 276f941, "chart-first restructure ... soak hardening") silently drops async RX while the flag is true. USB host_task clears it on first successful write_packet (src/host/usb.rs:354/373), but the UART path was never updated -- every RX event was dropped from boot. The OLED bar lit up regardless because record_packet announces PacketRx before the gate. Mirror the USB pattern: clear on successful tx.write_all, re-arm on failure. 2. Once RX flowed, sustained bidirectional spam tripped the §3.4 inactivity watchdog spuriously. The select3-based loop serialised read and write; tx.write_all (~25 ms per max frame at 115_200 baud) blocked the read poll long enough for last_frame_at to age past 1000 ms under a saturated outbound queue. The Reset cascaded to radio (Unconfigured) and display (splash), leaving subsequent host TX commands timing out waiting for TX_DONE. Restructure host_task into two sub-loops joined in the same task: read_loop owns rx + watchdog, write_loop owns tx + throttle. The read future re-polls on every executor wake regardless of write progress. Same executor, disjoint borrows on the split rx/tx halves, no Mutex needed. Also reset the FrameDecoder when the watchdog fires so a host that resumes mid-frame resyncs cleanly on the next 0x00 sentinel (USB gets this for free via DTR-drop reset; UART has no DTR). Build: bump esp-hal/esp-rtos/esp-backtrace/esp-println version requirements (1.0->1.1, 0.2->0.3, 0.18->0.19, 0.16->0.17) to match what the upstream esp-rs/esp-hal main branch now publishes. cargo update had invalidated the [patch.crates-io] entries since the patched versions were no longer semver-compatible with the old top-level requirements, which silently dropped the patches and fell back to crates.io releases that pin embassy-executor 0.9 (mismatched with our 0.10). --- Cargo.lock | 166 +++++++++++++++++++++++------------------------ Cargo.toml | 69 +++++++++++++------- src/host/uart.rs | 118 +++++++++++++++++++++++---------- 3 files changed, 212 insertions(+), 141 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e8e4a1..adc0cc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,9 +125,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -167,9 +167,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", @@ -274,9 +274,9 @@ dependencies = [ [[package]] name = "cpubits" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" [[package]] name = "cpufeatures" @@ -743,7 +743,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b715380574504335c40ec3016bc44598fac3c385f82959b26c41b25c30b7648d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cortex-m", "cortex-m-rt", @@ -880,12 +880,12 @@ dependencies = [ [[package]] name = "embassy-time-queue-utils" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454" +checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693" dependencies = [ "embassy-executor-timer-queue", - "heapless 0.8.0", + "heapless 0.9.2", ] [[package]] @@ -894,7 +894,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a25746d8b152b72fbf2a217f489a083dbbe243f281f09184a1f2cfbe9bbb245f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "defmt 1.0.1", "embassy-futures", "embassy-net-driver-channel", @@ -1084,8 +1084,8 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "esp-backtrace" -version = "0.18.1" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.19.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "cfg-if", "defmt 1.0.1", @@ -1101,8 +1101,8 @@ dependencies = [ [[package]] name = "esp-config" -version = "0.6.1" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.7.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "document-features", "esp-metadata-generated", @@ -1113,11 +1113,11 @@ dependencies = [ [[package]] name = "esp-hal" -version = "1.0.0" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "1.1.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "bitfield 0.19.4", - "bitflags 2.11.0", + "bitflags 2.11.1", "bytemuck", "cfg-if", "critical-section", @@ -1156,7 +1156,7 @@ dependencies = [ "nb 1.1.0", "paste", "portable-atomic", - "rand_core 0.10.0", + "rand_core 0.10.1", "rand_core 0.6.4", "rand_core 0.9.5", "riscv", @@ -1170,8 +1170,8 @@ dependencies = [ [[package]] name = "esp-hal-procmacros" -version = "0.21.0" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.22.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "document-features", "object", @@ -1184,13 +1184,13 @@ dependencies = [ [[package]] name = "esp-metadata-generated" -version = "0.3.0" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.4.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" [[package]] name = "esp-println" -version = "0.16.1" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.17.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "defmt 1.0.1", "document-features", @@ -1202,8 +1202,8 @@ dependencies = [ [[package]] name = "esp-riscv-rt" -version = "0.13.0" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.14.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "document-features", "riscv", @@ -1212,8 +1212,8 @@ dependencies = [ [[package]] name = "esp-rom-sys" -version = "0.1.3" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.1.4" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "cfg-if", "document-features", @@ -1224,8 +1224,8 @@ dependencies = [ [[package]] name = "esp-rtos" -version = "0.2.0" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.3.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "cfg-if", "document-features", @@ -1246,8 +1246,8 @@ dependencies = [ [[package]] name = "esp-sync" -version = "0.1.1" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.2.1" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "cfg-if", "document-features", @@ -1274,64 +1274,64 @@ dependencies = [ [[package]] name = "esp32" -version = "0.39.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=fc3e6d4#fc3e6d4b34cfc885f750641d27e6fe3e63bad5d2" +version = "0.40.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5726e07689249d1a2cb7c492077bc424837fb68a64f7eb5d46569325352e9428" dependencies = [ - "critical-section", "vcell", ] [[package]] name = "esp32c2" -version = "0.28.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=fc3e6d4#fc3e6d4b34cfc885f750641d27e6fe3e63bad5d2" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef0b623533bbaa37e348c18b6b41cfd5b47c3cb64a4b9e44f0295941d62aa2e" dependencies = [ - "critical-section", "vcell", ] [[package]] name = "esp32c3" -version = "0.31.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=fc3e6d4#fc3e6d4b34cfc885f750641d27e6fe3e63bad5d2" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e89ed62cf6c043a6d29c520b02a13b359ec8a75d67b65d4330ed717d15fe97" dependencies = [ - "critical-section", "vcell", ] [[package]] name = "esp32c6" -version = "0.22.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=fc3e6d4#fc3e6d4b34cfc885f750641d27e6fe3e63bad5d2" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f34ff2633968c12125efc7f4f8f101078d5d34c7cb60eab82268db20986f9" dependencies = [ - "critical-section", "vcell", ] [[package]] name = "esp32h2" -version = "0.18.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=fc3e6d4#fc3e6d4b34cfc885f750641d27e6fe3e63bad5d2" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5bab026020ed4606ce113b6fde598dbc48f7eefcc46e9469ece77cc2b1aa4be" dependencies = [ - "critical-section", "vcell", ] [[package]] name = "esp32s2" -version = "0.30.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=fc3e6d4#fc3e6d4b34cfc885f750641d27e6fe3e63bad5d2" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ad6f21cdf6ec7b06b7f7e0fbe51f0d975fd6a5fa67c3f8a5a910d3981af531" dependencies = [ - "critical-section", "vcell", ] [[package]] name = "esp32s3" -version = "0.34.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=fc3e6d4#fc3e6d4b34cfc885f750641d27e6fe3e63bad5d2" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4b8c4e4d9f187553ecdb7173edec7b2deb2beea106eedefecdb1654b8ee25a" dependencies = [ - "critical-section", "vcell", ] @@ -1511,9 +1511,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heapless" @@ -1550,9 +1550,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hsmc" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebed0d04633829a1e8319efcb038684dff8c01d06e4e50a4669691fb7c706f9e" +checksum = "adbdeff427849daf4d03395027c8538dcdd02a0a26c6e59745f8647f5e657527" dependencies = [ "defmt 0.3.100", "embassy-futures", @@ -1564,9 +1564,9 @@ dependencies = [ [[package]] name = "hsmc-macros" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4de168c4459a45a99c71d51be8af79d94eb09ecd62d6f25baee6ec174297db2" +checksum = "66ae4d525e3047234d69597f616c89b9ad49ac6a851e4522322a9d74fc2cc1bf" dependencies = [ "proc-macro2", "quote", @@ -1590,12 +1590,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", ] [[package]] @@ -1693,9 +1693,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "litrs" @@ -1827,9 +1827,9 @@ checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" [[package]] name = "minijinja" -version = "2.18.0" +version = "2.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328251e58ad8e415be6198888fc207502727dc77945806421ab34f35bf012e7d" +checksum = "805bfd7352166bae857ee569628b52bcd85a1cecf7810861ebceb1686b72b75d" dependencies = [ "memo-map", "serde", @@ -2129,7 +2129,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "memchr", "unicase", ] @@ -2172,9 +2172,9 @@ checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "redox_syscall" @@ -2182,7 +2182,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -2434,9 +2434,9 @@ checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -2661,9 +2661,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.9+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da053d28fe57e2c9d21b48261e14e7b4c8b670b54d2c684847b91feaf4c7dac5" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "toml_datetime", @@ -2673,9 +2673,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] @@ -2912,9 +2912,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -2922,15 +2922,15 @@ dependencies = [ [[package]] name = "xtensa-lx" version = "0.13.0" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "critical-section", ] [[package]] name = "xtensa-lx-rt" -version = "0.21.0" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +version = "0.22.0" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "document-features", "xtensa-lx", @@ -2940,7 +2940,7 @@ dependencies = [ [[package]] name = "xtensa-lx-rt-proc-macros" version = "0.5.0" -source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#70930f20d9f681a30ff20aced39d0bd5363126d5" +source = "git+https://github.com/esp-rs/esp-hal.git?branch=main#54a62c57f1f005020777a4e0843ce03c68312aba" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index e4cc414..2034afe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,10 @@ homepage = "https://github.com/donglora/firmware" keywords = ["lora", "radio", "embedded", "usb", "firmware"] categories = ["embedded", "hardware-support", "no-std"] -# NOTE: esp-rtos patched from GitHub main (see [patch.crates-io] below). -# Remove patches once esp-rtos publishes with embassy-executor 0.10 support. +# 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] cfg-if = "1.0" critical-section = "1.2" @@ -31,7 +33,7 @@ embassy-usb-driver = "0.2" ucobs = { version = "0.3", features = ["legacy-msrv"] } heapless = "0.9" donglora-protocol = "1" -hsmc = { version = "0.3", default-features = false, features = ["embassy", "trace-defmt"] } +hsmc = { version = "0.4", default-features = false, features = ["embassy", "trace-defmt"] } # Local path while iterating on CAD IRQ-clear / standby-after-CAD fixes. # Switch back to git fork (then crates.io) once changes are upstreamed. lora-phy = { path = "../../lora-rs/lora-phy", features = ["defmt-03"] } @@ -50,17 +52,17 @@ embassy-futures = "0.1" # Xtensa ESP crates — requires esp toolchain (espup). The chip feature # (esp32 / esp32s3) is selected per board via the board-level feature flag # below, so the same dep lines support both classic ESP32 and ESP32-S3. -esp-backtrace = { version = "0.18", optional = true, features = [ +esp-backtrace = { version = "0.19", optional = true, features = [ "panic-handler", "defmt", ] } -esp-hal = { version = "1.0", optional = true, features = [ +esp-hal = { version = "1.1", optional = true, features = [ "unstable", ] } -esp-println = { version = "0.16", optional = true, features = [ +esp-println = { version = "0.17", optional = true, features = [ "defmt-espflash", ] } -esp-rtos = { version = "0.2", optional = true, features = [ +esp-rtos = { version = "0.3", optional = true, features = [ "embassy", ] } @@ -100,13 +102,22 @@ minijinja = "2.18" owo-colors = "4.3" [features] -# The `embassy` feature gates the async-actions codepath inside the `hsmc` -# statechart macro. Its `#[cfg(feature = "embassy")]` attributes are -# evaluated in *this* crate's context (a proc-macro cfg quirk), so every -# board feature must enable it. `tokio` is declared only to silence the -# macro's `#[cfg(feature = "tokio")]` checks. +# Bridge features for hsmc's macro-expanded `#[cfg(feature = "...")]` +# attributes. Macros expand into the *call-site* crate, so any cfg the +# macro emits is evaluated against THIS crate's feature set, not hsmc's. +# That means enabling `hsmc/embassy` or `hsmc/trace-defmt` via the dep +# only activates them inside hsmc's own compilation — for the cfg gates +# inside expanded macro bodies to fire here, we need same-named features +# in this Cargo.toml. `tokio` is declared just to silence the macro's +# `#[cfg(feature = "tokio")]` checks. +# +# `trace-defmt` must be in every board feature so chart trace lines +# (entries, exits, transitions, etc.) actually compile in. Without it, +# hsmc 0.4's `__chart_observe!` macro expands to nothing for the defmt +# arm and you get silent statecharts. embassy = [] tokio = [] +trace-defmt = [] # When enabled, replaces the display task with a LED-based diagnostic # blinker driven by `crate::debug_blink::CHECKPOINT`. Used for diagnosing # the firmware on UART-only boards where defmt output would corrupt the @@ -114,13 +125,15 @@ tokio = [] # a human watches the pattern and reports N to determine where execution # reached / stopped. 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. +# NOTE: chart auto-tracing in hsmc 0.4 emits `__chart_observe!` calls +# whose defmt arm is gated on `#[cfg(feature = "trace-defmt")]`. That +# cfg is evaluated against THIS crate (proc-macro cfg quirk above), so +# every board feature includes `trace-defmt` to keep the arm live. The +# `trace;` keyword in `statechart!` blocks is deprecated in 0.4 and +# no longer gates emission. Whether trace lines actually emit at +# runtime is still controlled by defmt's standard compile-time level +# filter (`DEFMT_LOG` env var) -- release builds with `DEFMT_LOG` +# unset compile the calls out to nothing. # 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 @@ -131,6 +144,7 @@ debug-checkpoint = [] debug-twim = [] heltec_v3 = [ "embassy", + "trace-defmt", "dep:esp-backtrace", "dep:esp-hal", "dep:esp-println", @@ -142,6 +156,7 @@ heltec_v3 = [ ] heltec_v3_uart = [ "embassy", + "trace-defmt", "dep:esp-backtrace", "dep:esp-hal", "dep:esp-println", @@ -153,6 +168,7 @@ heltec_v3_uart = [ ] heltec_v4 = [ "embassy", + "trace-defmt", "dep:esp-backtrace", "dep:esp-hal", "dep:esp-println", @@ -164,6 +180,7 @@ heltec_v4 = [ ] elecrow_thinknode_m2 = [ "embassy", + "trace-defmt", "dep:esp-backtrace", "dep:esp-hal", "dep:esp-println", @@ -175,6 +192,7 @@ elecrow_thinknode_m2 = [ ] lilygo_tbeam_supreme = [ "embassy", + "trace-defmt", "dep:esp-backtrace", "dep:esp-hal", "dep:esp-println", @@ -186,6 +204,7 @@ lilygo_tbeam_supreme = [ ] lilygo_tbeam = [ "embassy", + "trace-defmt", "dep:esp-backtrace", "dep:esp-hal", "dep:esp-println", @@ -197,6 +216,7 @@ lilygo_tbeam = [ ] rak_wisblock_4631 = [ "embassy", + "trace-defmt", "dep:cortex-m", "dep:cortex-m-rt", "cortex-m-rt/set-vtor", @@ -208,6 +228,7 @@ rak_wisblock_4631 = [ ] wio_tracker_l1 = [ "embassy", + "trace-defmt", "dep:cortex-m", "dep:cortex-m-rt", "cortex-m-rt/set-vtor", @@ -219,6 +240,7 @@ wio_tracker_l1 = [ ] waveshare_rp2040_lora = [ "embassy", + "trace-defmt", "dep:cortex-m", "dep:cortex-m-rt", "dep:defmt-rtt", @@ -228,10 +250,11 @@ waveshare_rp2040_lora = [ "embassy-executor/executor-thread", ] -# 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. Track: -# https://github.com/esp-rs/esp-hal/tree/main/esp-rtos +# 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" } diff --git a/src/host/uart.rs b/src/host/uart.rs index 361a4bd..38f9abb 100644 --- a/src/host/uart.rs +++ b/src/host/uart.rs @@ -4,7 +4,7 @@ //! rely entirely on the spec §3.4 inactivity watchdog (1 s of silence //! → Disconnected → drop radio to Unconfigured). -use defmt::warn; +use defmt::{info, warn}; use embassy_executor::task; use embassy_time::{Duration, Instant}; use embedded_io_async::Write; @@ -34,25 +34,32 @@ pub async fn host_task( display_commands.send(DisplayCommand::On).await; } - let mut read_buf = [0u8; READ_BUF]; - let mut write_buf: [u8; MAX_WIRE_FRAME] = [0u8; MAX_WIRE_FRAME]; - let mut decoder = FrameDecoder::new(); - let mut last_frame_at = Instant::now(); - let mut connected = true; + // Two sub-loops joined in the same task. Splitting read and write + // is what stops a long-running tx.write_all (~25 ms for a max-size + // frame at 115_200 baud, multiplied across a saturated outbound + // queue) from starving the inbound poll. While write_all sits in + // its FIFO-drain await, join re-polls the read future on the next + // wake, so last_frame_at keeps refreshing and the §3.4 watchdog + // does not spuriously fire under sustained bidirectional load. + // Same task = single executor + disjoint borrows on the split + // rx/tx halves, so no Mutex is needed. + let read_loop = async { + let mut read_buf = [0u8; READ_BUF]; + let mut decoder = FrameDecoder::new(); + let mut last_frame_at = Instant::now(); + let mut connected = true; - loop { - use embassy_futures::select::{select3, Either3}; + loop { + use embassy_futures::select::{select, Either}; - match select3( - embedded_io_async::Read::read(&mut rx, &mut read_buf), - outbound.receive(), - embassy_time::Timer::after(Duration::from_millis(100)), - ) - .await - { - Either3::First(result) => match result { - Ok(0) => continue, - Ok(n) => { + match select( + embedded_io_async::Read::read(&mut rx, &mut read_buf), + embassy_time::Timer::after(Duration::from_millis(100)), + ) + .await + { + Either::First(Ok(0)) => {} + Either::First(Ok(n)) => { last_frame_at = Instant::now(); if !connected { if has_display { @@ -62,31 +69,72 @@ pub async fn host_task( } dispatch_bytes(&mut decoder, &read_buf[..n], commands, outbound).await; } - Err(_) => { + Either::First(Err(_)) => { + // Byte-level error (FIFO overrun, UART framing, + // parity). Per PROTOCOL.md §3.4: only "byte stream + // activity that results in a complete frame + // arriving" resets the inactivity timer, so we do + // NOT touch last_frame_at here. The 100 ms sleep + // is anti-spin: a stuck driver pumping continuous + // Errs would otherwise burn the executor. decoder.reset(); embassy_time::Timer::after_millis(100).await; } - }, - Either3::Second(frame) => { - if let Some(n) = encode_outbound(&frame, &mut write_buf) { - if tx.write_all(&write_buf[..n]).await.is_err() { - warn!("UART write failed, frame dropped"); + Either::Second(()) => {} + } + + // Inactivity watchdog (PROTOCOL.md §3.4, 1000 ms). + if connected && last_frame_at.elapsed() >= Duration::from_millis(INACTIVITY_MS) { + warn!("UART host inactivity timeout fired"); + let _ = commands.try_send(InboundEvent::Reset); + if has_display { + let _ = display_commands.try_send(DisplayCommand::Reset); + } + // Drop in-flight frame state. When the host resumes it + // may pick up mid-frame; without resetting the decoder + // we'd chew through garbage until the next 0x00 + // sentinel. USB gets the equivalent for free via + // DTR-drop reset (host/usb.rs Disconnected entry); UART + // has no DTR. + decoder.reset(); + connected = false; + } + } + }; + + let write_loop = async { + let mut write_buf: [u8; MAX_WIRE_FRAME] = [0u8; MAX_WIRE_FRAME]; + + loop { + let frame = outbound.receive().await; + let Some(n) = encode_outbound(&frame, &mut write_buf) else { + continue; + }; + // Mirror src/host/usb.rs:349-374. The throttle gate at + // src/radio.rs:2017 silently drops async RX into the + // outbound queue while RADIO_THROTTLED is true; clear it + // on every successful write and re-arm it on failure, + // same contract as USB. + match tx.write_all(&write_buf[..n]).await { + Ok(()) => { + if crate::channel::RADIO_THROTTLED + .load(portable_atomic::Ordering::Acquire) + { + info!("UART write recovered; clearing radio throttle"); + crate::channel::RADIO_THROTTLED + .store(false, portable_atomic::Ordering::Release); } } + Err(_) => { + warn!("UART write failed, frame dropped, throttling radio"); + crate::channel::RADIO_THROTTLED + .store(true, portable_atomic::Ordering::Release); + } } - Either3::Third(()) => {} } + }; - // Inactivity watchdog. - if connected && last_frame_at.elapsed() >= Duration::from_millis(INACTIVITY_MS) { - warn!("UART host inactivity timeout fired"); - let _ = commands.try_send(InboundEvent::Reset); - if has_display { - let _ = display_commands.try_send(DisplayCommand::Reset); - } - connected = false; - } - } + embassy_futures::join::join(read_loop, write_loop).await; } async fn dispatch_bytes(