diff --git a/core/archipelago/src/mesh/listener/session.rs b/core/archipelago/src/mesh/listener/session.rs index 9db19b43..a5d3a92a 100644 --- a/core/archipelago/src/mesh/listener/session.rs +++ b/core/archipelago/src/mesh/listener/session.rs @@ -269,10 +269,23 @@ async fn auto_detect_and_open( our_ed_pubkey_hex: &str, our_x25519_pubkey_hex: &str, device_kind: Option, + skip_path: Option<&str>, ) -> Result<(String, MeshRadioDevice, DeviceInfo)> { - let paths = super::super::serial::detect_serial_devices().await; + let mut paths = super::super::serial::detect_serial_devices().await; + // When falling back from a just-failed preferred path, don't probe that + // same device again in the same cycle — every open() toggles DTR/RTS, + // which resets ESP32-family boards, and back-to-back re-probes are what + // keeps a mid-boot board from ever finishing its boot. + if let Some(skip) = skip_path { + let canon = |p: &str| std::fs::canonicalize(p).unwrap_or_else(|_| p.into()); + let skip_canon = canon(skip); + paths.retain(|p| canon(p) != skip_canon); + } if paths.is_empty() { - anyhow::bail!("No serial devices found in /dev"); + anyhow::bail!(match skip_path { + Some(skip) => format!("No serial devices found in /dev besides {skip}, which was already probed this cycle"), + None => "No serial devices found in /dev".to_string(), + }); } for path in &paths { debug!(path = %path, "Probing for mesh radio device"); @@ -986,6 +999,7 @@ pub(super) async fn run_mesh_session( our_ed_pubkey_hex, our_x25519_pubkey_hex, device_kind, + Some(path), ) .await? } @@ -996,6 +1010,7 @@ pub(super) async fn run_mesh_session( our_ed_pubkey_hex, our_x25519_pubkey_hex, device_kind, + None, ) .await? }; diff --git a/core/archipelago/src/mesh/mod.rs b/core/archipelago/src/mesh/mod.rs index ae2d9389..54191ee7 100644 --- a/core/archipelago/src/mesh/mod.rs +++ b/core/archipelago/src/mesh/mod.rs @@ -1063,8 +1063,18 @@ impl MeshService { /// with the reconnect loop (whichever loses just retries). pub async fn probe_device(&self, path: &str) -> Result { let status = self.state.status.read().await; - if status.device_connected && status.device_path.as_deref() == Some(path) { - anyhow::bail!("{path} is the active mesh radio — already connected"); + if status.device_connected { + if let Some(active) = status.device_path.as_deref() { + // Compare canonical paths: /dev/mesh-radio is a symlink to the + // ttyUSB*/ttyACM* node, and a probe through the alias would + // still open the very tty the live session is holding. + let canon = |p: &str| { + std::fs::canonicalize(p).unwrap_or_else(|_| std::path::PathBuf::from(p)) + }; + if canon(active) == canon(path) { + anyhow::bail!("{path} is the active mesh radio — already connected"); + } + } } drop(status); listener::probe_device(path).await diff --git a/core/archipelago/src/mesh/serial.rs b/core/archipelago/src/mesh/serial.rs index 32f0451b..dd7634bf 100644 --- a/core/archipelago/src/mesh/serial.rs +++ b/core/archipelago/src/mesh/serial.rs @@ -556,14 +556,30 @@ fn likely_non_mesh_serial_device(path: &str) -> bool { /// Scan for serial devices that could be Meshcore radios. /// Returns paths to existing serial device files. +/// +/// Candidates are deduplicated by canonical path: /dev/mesh-radio is a udev +/// symlink to a ttyUSB*/ttyACM* node that is ALSO in the candidate list, so +/// without this one physical board shows up (and gets probed, and gets its +/// DTR/RTS reset toggled) twice per cycle. The first candidate wins, which +/// keeps the stable /dev/mesh-radio name when the symlink exists. pub async fn detect_serial_devices() -> Vec { let mut devices = Vec::new(); + let mut seen_canonical: Vec = Vec::new(); for path in SERIAL_CANDIDATES { if tokio::fs::metadata(path).await.is_ok() { if likely_non_mesh_serial_device(path) { debug!(path = %path, "Skipping known non-mesh serial device"); continue; } + let canonical = tokio::fs::canonicalize(path) + .await + .unwrap_or_else(|_| std::path::PathBuf::from(path)); + if seen_canonical.contains(&canonical) { + debug!(path = %path, canonical = %canonical.display(), + "Skipping alias of an already-detected serial device"); + continue; + } + seen_canonical.push(canonical); devices.push(path.to_string()); } }