fix(mesh): stop probing one physical radio as two devices
/dev/mesh-radio is a udev symlink to a ttyUSB*/ttyACM* node that is also in SERIAL_CANDIDATES, so one board was detected, probed and DTR/RTS-reset twice per reconnect cycle (and shown twice in the UI): - detect_serial_devices() dedupes candidates by canonical path, keeping the stable /dev/mesh-radio name - auto-detect fallback skips the preferred path it just probed this cycle instead of immediately resetting the same board again - probe_device's active-session guard compares canonical paths, so a probe via the alias can no longer open the tty the live session holds Together with the 2s boot-settle and stable-session backoff gate, this takes a plugged-in CP2102/ESP32 board from up to 9 reset events per cycle at a permanent 5s retry floor down to one probe sequence per backoff window — enough for the radio to actually finish booting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
449ed7c1f7
commit
a32e40da31
@ -269,10 +269,23 @@ async fn auto_detect_and_open(
|
|||||||
our_ed_pubkey_hex: &str,
|
our_ed_pubkey_hex: &str,
|
||||||
our_x25519_pubkey_hex: &str,
|
our_x25519_pubkey_hex: &str,
|
||||||
device_kind: Option<DeviceType>,
|
device_kind: Option<DeviceType>,
|
||||||
|
skip_path: Option<&str>,
|
||||||
) -> Result<(String, MeshRadioDevice, DeviceInfo)> {
|
) -> 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() {
|
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 {
|
for path in &paths {
|
||||||
debug!(path = %path, "Probing for mesh radio device");
|
debug!(path = %path, "Probing for mesh radio device");
|
||||||
@ -986,6 +999,7 @@ pub(super) async fn run_mesh_session(
|
|||||||
our_ed_pubkey_hex,
|
our_ed_pubkey_hex,
|
||||||
our_x25519_pubkey_hex,
|
our_x25519_pubkey_hex,
|
||||||
device_kind,
|
device_kind,
|
||||||
|
Some(path),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
@ -996,6 +1010,7 @@ pub(super) async fn run_mesh_session(
|
|||||||
our_ed_pubkey_hex,
|
our_ed_pubkey_hex,
|
||||||
our_x25519_pubkey_hex,
|
our_x25519_pubkey_hex,
|
||||||
device_kind,
|
device_kind,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1063,8 +1063,18 @@ impl MeshService {
|
|||||||
/// with the reconnect loop (whichever loses just retries).
|
/// with the reconnect loop (whichever loses just retries).
|
||||||
pub async fn probe_device(&self, path: &str) -> Result<listener::DeviceProbe> {
|
pub async fn probe_device(&self, path: &str) -> Result<listener::DeviceProbe> {
|
||||||
let status = self.state.status.read().await;
|
let status = self.state.status.read().await;
|
||||||
if status.device_connected && status.device_path.as_deref() == Some(path) {
|
if status.device_connected {
|
||||||
anyhow::bail!("{path} is the active mesh radio — already 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);
|
drop(status);
|
||||||
listener::probe_device(path).await
|
listener::probe_device(path).await
|
||||||
|
|||||||
@ -556,14 +556,30 @@ fn likely_non_mesh_serial_device(path: &str) -> bool {
|
|||||||
|
|
||||||
/// Scan for serial devices that could be Meshcore radios.
|
/// Scan for serial devices that could be Meshcore radios.
|
||||||
/// Returns paths to existing serial device files.
|
/// 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<String> {
|
pub async fn detect_serial_devices() -> Vec<String> {
|
||||||
let mut devices = Vec::new();
|
let mut devices = Vec::new();
|
||||||
|
let mut seen_canonical: Vec<std::path::PathBuf> = Vec::new();
|
||||||
for path in SERIAL_CANDIDATES {
|
for path in SERIAL_CANDIDATES {
|
||||||
if tokio::fs::metadata(path).await.is_ok() {
|
if tokio::fs::metadata(path).await.is_ok() {
|
||||||
if likely_non_mesh_serial_device(path) {
|
if likely_non_mesh_serial_device(path) {
|
||||||
debug!(path = %path, "Skipping known non-mesh serial device");
|
debug!(path = %path, "Skipping known non-mesh serial device");
|
||||||
continue;
|
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());
|
devices.push(path.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user