fix(mesh): ride out the radio restart during apply — no false errors, no setup modal
Demo images / Build & push demo images (push) Successful in 3m54s

Applying RF settings deliberately restarts the radio daemon (~15-20s).
Two things treated that healthy, expected gap as a fault (operator,
2026-08-06):

- radio_state was single-shot: a query landing inside the restart
  window reported "The radio daemon did not answer the state query"
  for a restart that was working correctly. It now retries for ~30s
  and says the radio is restarting while it waits. A real device-level
  refusal (not an RNode) still returns immediately.
- The device-setup modal auto-opens for any detected-but-unconnected
  port, so the restart looked like a newly plugged stick and
  interrupted the apply. Apply and Reboot now suppress auto-detect for
  90s via mesh.suppressDeviceDetect().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-06 09:55:52 -04:00
co-authored by Claude Fable 5
parent 53753687a5
commit f394c02055
3 changed files with 60 additions and 15 deletions
+44 -15
View File
@@ -2155,22 +2155,51 @@ impl MeshService {
/// interface including the radio-confirmed r_* parameters. The LoRa
/// settings panel's source for "what is the device actually running".
pub async fn radio_state(&self) -> Result<serde_json::Value> {
let status = self.state.status.read().await;
if !status.device_connected {
anyhow::bail!("No mesh device connected. Check USB connection.");
// Retry across a reconnect window. Applying settings deliberately
// restarts the radio daemon (~15s), and the session is legitimately
// absent while it comes back — a single-shot query inside that window
// reported "the daemon did not answer" for what is a healthy,
// in-progress restart (operator, 2026-08-06).
const ATTEMPTS: u32 = 6;
let mut last_err = anyhow::anyhow!("No mesh device connected. Check USB connection.");
for attempt in 0..ATTEMPTS {
if attempt > 0 {
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
}
if !self.state.status.read().await.device_connected {
last_err = anyhow::anyhow!(
"The radio is not connected right now — if settings were just applied it \
is restarting and comes back within about 20 seconds."
);
continue;
}
let (tx, rx) = tokio::sync::oneshot::channel();
if self
.state
.send_cmd(listener::MeshCommand::QueryRadioState { reply: tx })
.await
.is_err()
{
last_err = anyhow::anyhow!("Mesh listener not running");
continue;
}
match tokio::time::timeout(std::time::Duration::from_secs(10), rx).await {
Ok(Ok(Ok(state))) => return Ok(state),
Ok(Ok(Err(e))) => {
// A real device-level refusal (e.g. not an RNode radio) —
// retrying cannot change it.
return Err(anyhow::anyhow!(e));
}
Ok(Err(_)) => {
last_err =
anyhow::anyhow!("Mesh session ended before the state query completed")
}
Err(_) => {
last_err = anyhow::anyhow!("The radio daemon did not answer the state query")
}
}
}
drop(status);
let (tx, rx) = tokio::sync::oneshot::channel();
self.state
.send_cmd(listener::MeshCommand::QueryRadioState { reply: tx })
.await
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
let state = tokio::time::timeout(std::time::Duration::from_secs(10), rx)
.await
.map_err(|_| anyhow::anyhow!("The radio daemon did not answer the state query"))?
.map_err(|_| anyhow::anyhow!("Mesh session ended before the state query completed"))?;
state.map_err(|e| anyhow::anyhow!(e))
Err(last_err)
}
/// Current mesh-AI assistant settings (issue #50).