fix: resolve display stuck after disconnect and radio task deadlock

- Set disconnected flag on DisplayCommand::Reset to block stale radio
  status updates from re-rendering dashboard over splash screen
- Send StopRx to radio when USB DTR drops so radio stops generating
  packets after host disconnects
- Use try_send for unsolicited responses (RxPacket, error recovery,
  StopRx Ok) to prevent radio task deadlock on full response channel
- Fall back to LED-only event loop when display init fails instead of
  returning early and orphaning the LED

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-04-07 19:49:15 -06:00
1 parent 0c9094dcbd
commit de38278d7b
6 files changed
+114 -12

No files matched your search

+10 -2
View File
@@ -131,8 +131,15 @@ pub async fn display_task(
};
let Some(mut display) = crate::board::create_display(parts.i2c).await else {
defmt::error!("display init failed, giving up");
return;
defmt::error!("display init failed, running LED-only loop");
loop {
match display_commands.receive().await {
DisplayCommand::Reset | DisplayCommand::Off => {
led.set_rgb(0, 0, 0).await;
}
DisplayCommand::On => {}
}
}
};
let mut state = DisplayState::new();
@@ -207,6 +214,7 @@ pub async fn display_task(
}
DisplayCommand::Reset => {
state = DisplayState::new();
state.disconnected = true;
render::splash(&mut display, &board_info);
let _ = display.flush().await;
led.set_rgb(0, 0, 0).await;
+6 -2
View File
@@ -8,6 +8,7 @@ use embassy_usb::Builder;
use static_cell::StaticCell;
use crate::channel::{CommandChannel, DisplayCommand, DisplayCommandChannel, ResponseChannel};
use crate::protocol::Command;
use super::framing::{self, CobsDecoder, MAX_FRAME};
const MAX_PACKET: usize = 64;
@@ -126,8 +127,11 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
}
let connected = receiver.dtr();
if was_connected && !connected && has_display {
display_commands.send(DisplayCommand::Reset).await;
if was_connected && !connected {
let _ = commands.try_send(Command::StopRx);
if has_display {
display_commands.send(DisplayCommand::Reset).await;
}
}
if !was_connected && connected && has_display {
display_commands.send(DisplayCommand::On).await;
+11 -6
View File
@@ -113,19 +113,22 @@ pub async fn radio_task(
// copy_len <= MAX_PAYLOAD == Vec capacity, so this cannot fail.
let _ = payload.extend_from_slice(&rx_buf[..copy_len]);
responses
.send(Response::RxPacket {
if responses
.try_send(Response::RxPacket {
rssi: pkt_status.rssi,
snr: pkt_status.snr,
payload,
})
.await;
.is_err()
{
warn!("RX packet dropped: response channel full");
}
if let Err(e) = start_rx(&mut lora, &cfg).await {
warn!("restart RX failed: {}", e);
state.state = RadioState::Idle;
status.sender().send(state.clone());
responses.send(Response::Error(ErrorCode::RadioBusy)).await;
let _ = responses.try_send(Response::Error(ErrorCode::RadioBusy));
}
}
Err(e) => {
@@ -133,7 +136,7 @@ pub async fn radio_task(
if start_rx(&mut lora, &cfg).await.is_err() {
state.state = RadioState::Idle;
status.sender().send(state.clone());
responses.send(Response::Error(ErrorCode::RadioBusy)).await;
let _ = responses.try_send(Response::Error(ErrorCode::RadioBusy));
}
}
},
@@ -210,7 +213,9 @@ async fn handle_cmd(
let _ = lora.enter_standby().await;
state.state = RadioState::Idle;
status.sender().send(state.clone());
responses.send(Response::Ok).await;
// try_send: StopRx may be sent internally on USB disconnect
// when the host isn't draining responses.
let _ = responses.try_send(Response::Ok);
}
Command::DisplayOn | Command::DisplayOff | Command::GetMac => {}
Command::Transmit { config, payload } => {