From a569e09b42084a8fa49ca090a21fd42b4c53461d Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Fri, 27 Mar 2026 01:52:29 -0600 Subject: [PATCH] fix: reliable USB disconnect/reconnect display handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Poll DTR every 250ms via select3 timer arm (read_packet blocks forever after host closes port, so DTR check must not depend on it) - DTR drop → send DisplayCommand::Reset (blocking send, not try_send) - DTR rise → send DisplayCommand::On - Display task: disconnected flag ignores StatusWatch while set, preventing stale radio status from overriding the idle screen - On reconnect, display grabs current RadioStatus from watch and renders dashboard immediately (no waiting for next radio event) - Simplified clippy to use _cargo helper Co-Authored-By: Claude Opus 4.6 (1M context) --- Justfile | 9 +-------- src/display/task.rs | 12 +++++++++++- src/usb/task.rs | 46 ++++++++++++++++++++++++++++----------------- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/Justfile b/Justfile index 6e7ca411..cdd019b6 100644 --- a/Justfile +++ b/Justfile @@ -42,14 +42,7 @@ check board: # Run clippy on a single board clippy board: - @read -r feat target chip <<< "$(just _info {{board}})"; \ - env=""; extra=""; \ - case "$target" in xtensa-*) \ - just _require_esp_toolchain; \ - [ -f "$HOME/export-esp.sh" ] && . "$HOME/export-esp.sh"; \ - env="RUSTC=$(rustup which rustc --toolchain esp)"; extra="+nightly";; \ - esac; \ - eval $env cargo $extra clippy --target $target --features $feat -- -D warnings + @just _cargo {{board}} "clippy -- -D warnings" # Build release firmware and copy to firmware/ with a readable name build board profile="release": diff --git a/src/display/task.rs b/src/display/task.rs index 45c3bfb7..f5ac17d0 100644 --- a/src/display/task.rs +++ b/src/display/task.rs @@ -32,6 +32,7 @@ struct DisplayState { rssi_count: usize, current_slot_rssi: i16, display_on: bool, + disconnected: bool, last_status: RadioStatus, } @@ -42,6 +43,7 @@ impl DisplayState { rssi_count: 0, current_slot_rssi: NO_SIGNAL, display_on: true, + disconnected: true, last_status: RadioStatus::default(), } } @@ -98,7 +100,9 @@ pub async fn display_task( .await { Either3::First(radio_status) => { - // Record RSSI from new packets + if state.disconnected { + continue; + } if let Some(rssi) = radio_status.last_rssi { if radio_status.rx_count != state.last_status.rx_count { state.record_rssi(rssi); @@ -112,15 +116,21 @@ pub async fn display_task( } Either3::Second(cmd) => match cmd { DisplayCommand::Off => { + state.disconnected = false; state.display_on = false; render::blank(&mut display); let _ = display.flush().await; } DisplayCommand::On => { + state.disconnected = false; state.display_on = true; + if let Some(s) = receiver.try_get() { + state.last_status = s; + } render_and_flush(&mut display, &state).await; } DisplayCommand::Reset => { + state.disconnected = true; state.last_status = RadioStatus::default(); state.rssi_history = [NO_SIGNAL; RSSI_HISTORY_LEN]; state.rssi_count = 0; diff --git a/src/usb/task.rs b/src/usb/task.rs index 64235a12..785c6a53 100644 --- a/src/usb/task.rs +++ b/src/usb/task.rs @@ -1,6 +1,5 @@ use embassy_executor::task; use embassy_futures::join::join; -use embassy_futures::select::{select, Either}; use embassy_usb::class::cdc_acm::{CdcAcmClass, Receiver, Sender, State}; use embassy_usb::Builder; use postcard::accumulator::{CobsAccumulator, FeedResult}; @@ -74,28 +73,29 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>( display_commands: &'static DisplayCommandChannel, has_display: bool, ) { - let mut cobs_buf: CobsAccumulator = CobsAccumulator::new(); + use embassy_futures::select::select3; + use embassy_futures::select::Either3; + let mut read_buf = [0u8; MAX_PACKET]; let mut write_buf = [0u8; MAX_FRAME]; - - // Wait for host to open the port (DTR signal) - receiver.wait_connection().await; - if has_display { - // Could send a "connected" signal here in the future - } + let mut cobs_buf: CobsAccumulator = CobsAccumulator::new(); + let mut was_connected = false; loop { - match select(receiver.read_packet(&mut read_buf), responses.receive()).await { - Either::First(result) => { + match select3( + receiver.read_packet(&mut read_buf), + responses.receive(), + embassy_time::Timer::after_millis(250), + ) + .await + { + Either3::First(result) => { let n = match result { + Ok(0) => continue, Ok(n) => n, Err(_) => { - // USB disconnect — reset and wait for reconnection cobs_buf = CobsAccumulator::new(); - if has_display { - let _ = display_commands.try_send(DisplayCommand::Reset); - } - receiver.wait_connection().await; + embassy_time::Timer::after_millis(100).await; continue; } }; @@ -116,15 +116,27 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>( }; } } - Either::Second(response) => { + Either3::Second(response) => { if let Ok(buf) = postcard::to_slice_cobs(&response, &mut write_buf) { - // CDC-ACM has a max packet size — send in chunks for chunk in buf.chunks(MAX_PACKET) { let _ = sender.write_packet(chunk).await; } } } + Either3::Third(()) => { + // Poll DTR every 250ms to detect host disconnect + } } + + // Check DTR after every select wake + let connected = receiver.dtr(); + if was_connected && !connected && has_display { + display_commands.send(DisplayCommand::Reset).await; + } + if !was_connected && connected && has_display { + display_commands.send(DisplayCommand::On).await; + } + was_connected = connected; } }