fix: reliable USB disconnect/reconnect display handling
- 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) <noreply@anthropic.com>
This commit is contained in:
3 files changed
+41
-26
No files matched your search
@@ -42,14 +42,7 @@ check board:
|
|||||||
|
|
||||||
# Run clippy on a single board
|
# Run clippy on a single board
|
||||||
clippy board:
|
clippy board:
|
||||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
@just _cargo {{board}} "clippy -- -D warnings"
|
||||||
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
|
|
||||||
|
|
||||||
# Build release firmware and copy to firmware/ with a readable name
|
# Build release firmware and copy to firmware/ with a readable name
|
||||||
build board profile="release":
|
build board profile="release":
|
||||||
|
|||||||
+11
-1
@@ -32,6 +32,7 @@ struct DisplayState {
|
|||||||
rssi_count: usize,
|
rssi_count: usize,
|
||||||
current_slot_rssi: i16,
|
current_slot_rssi: i16,
|
||||||
display_on: bool,
|
display_on: bool,
|
||||||
|
disconnected: bool,
|
||||||
last_status: RadioStatus,
|
last_status: RadioStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ impl DisplayState {
|
|||||||
rssi_count: 0,
|
rssi_count: 0,
|
||||||
current_slot_rssi: NO_SIGNAL,
|
current_slot_rssi: NO_SIGNAL,
|
||||||
display_on: true,
|
display_on: true,
|
||||||
|
disconnected: true,
|
||||||
last_status: RadioStatus::default(),
|
last_status: RadioStatus::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +100,9 @@ pub async fn display_task(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Either3::First(radio_status) => {
|
Either3::First(radio_status) => {
|
||||||
// Record RSSI from new packets
|
if state.disconnected {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(rssi) = radio_status.last_rssi {
|
if let Some(rssi) = radio_status.last_rssi {
|
||||||
if radio_status.rx_count != state.last_status.rx_count {
|
if radio_status.rx_count != state.last_status.rx_count {
|
||||||
state.record_rssi(rssi);
|
state.record_rssi(rssi);
|
||||||
@@ -112,15 +116,21 @@ pub async fn display_task(
|
|||||||
}
|
}
|
||||||
Either3::Second(cmd) => match cmd {
|
Either3::Second(cmd) => match cmd {
|
||||||
DisplayCommand::Off => {
|
DisplayCommand::Off => {
|
||||||
|
state.disconnected = false;
|
||||||
state.display_on = false;
|
state.display_on = false;
|
||||||
render::blank(&mut display);
|
render::blank(&mut display);
|
||||||
let _ = display.flush().await;
|
let _ = display.flush().await;
|
||||||
}
|
}
|
||||||
DisplayCommand::On => {
|
DisplayCommand::On => {
|
||||||
|
state.disconnected = false;
|
||||||
state.display_on = true;
|
state.display_on = true;
|
||||||
|
if let Some(s) = receiver.try_get() {
|
||||||
|
state.last_status = s;
|
||||||
|
}
|
||||||
render_and_flush(&mut display, &state).await;
|
render_and_flush(&mut display, &state).await;
|
||||||
}
|
}
|
||||||
DisplayCommand::Reset => {
|
DisplayCommand::Reset => {
|
||||||
|
state.disconnected = true;
|
||||||
state.last_status = RadioStatus::default();
|
state.last_status = RadioStatus::default();
|
||||||
state.rssi_history = [NO_SIGNAL; RSSI_HISTORY_LEN];
|
state.rssi_history = [NO_SIGNAL; RSSI_HISTORY_LEN];
|
||||||
state.rssi_count = 0;
|
state.rssi_count = 0;
|
||||||
|
|||||||
+29
-17
@@ -1,6 +1,5 @@
|
|||||||
use embassy_executor::task;
|
use embassy_executor::task;
|
||||||
use embassy_futures::join::join;
|
use embassy_futures::join::join;
|
||||||
use embassy_futures::select::{select, Either};
|
|
||||||
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 postcard::accumulator::{CobsAccumulator, FeedResult};
|
use postcard::accumulator::{CobsAccumulator, FeedResult};
|
||||||
@@ -74,28 +73,29 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
|
|||||||
display_commands: &'static DisplayCommandChannel,
|
display_commands: &'static DisplayCommandChannel,
|
||||||
has_display: bool,
|
has_display: bool,
|
||||||
) {
|
) {
|
||||||
let mut cobs_buf: CobsAccumulator<MAX_FRAME> = CobsAccumulator::new();
|
use embassy_futures::select::select3;
|
||||||
|
use embassy_futures::select::Either3;
|
||||||
|
|
||||||
let mut read_buf = [0u8; MAX_PACKET];
|
let mut read_buf = [0u8; MAX_PACKET];
|
||||||
let mut write_buf = [0u8; MAX_FRAME];
|
let mut write_buf = [0u8; MAX_FRAME];
|
||||||
|
let mut cobs_buf: CobsAccumulator<MAX_FRAME> = CobsAccumulator::new();
|
||||||
// Wait for host to open the port (DTR signal)
|
let mut was_connected = false;
|
||||||
receiver.wait_connection().await;
|
|
||||||
if has_display {
|
|
||||||
// Could send a "connected" signal here in the future
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match select(receiver.read_packet(&mut read_buf), responses.receive()).await {
|
match select3(
|
||||||
Either::First(result) => {
|
receiver.read_packet(&mut read_buf),
|
||||||
|
responses.receive(),
|
||||||
|
embassy_time::Timer::after_millis(250),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Either3::First(result) => {
|
||||||
let n = match result {
|
let n = match result {
|
||||||
|
Ok(0) => continue,
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// USB disconnect — reset and wait for reconnection
|
|
||||||
cobs_buf = CobsAccumulator::new();
|
cobs_buf = CobsAccumulator::new();
|
||||||
if has_display {
|
embassy_time::Timer::after_millis(100).await;
|
||||||
let _ = display_commands.try_send(DisplayCommand::Reset);
|
|
||||||
}
|
|
||||||
receiver.wait_connection().await;
|
|
||||||
continue;
|
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) {
|
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) {
|
for chunk in buf.chunks(MAX_PACKET) {
|
||||||
let _ = sender.write_packet(chunk).await;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user