From 893508b7002f6c586962ce8f5b04336599adb8a9 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 4 Jul 2026 02:16:07 +0000 Subject: [PATCH] fix(mesh): probe Reticulum/RNode before Meshcore/Meshtastic during auto-detect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed on real hardware (Heltec V4, RNode firmware): the board answers the exact KISS detect probe correctly and instantly on a fresh port open, but auto_detect_and_open (and open_preferred_path's unpinned branch) tried Meshcore (~5s timeout) then Meshtastic (~5s timeout) first, leaving the RNode firmware unresponsive by the time Reticulum's turn came ~10.6s later. ReticulumLink::open() already gates its expensive daemon-spawn behind a cheap ~1s probe_rnode check, so trying it first only costs ~1s extra when the device turns out to be Meshcore/Meshtastic instead — the previous comment's "most expensive, so goes last" reasoning only applies to a successful match, not a failed one. Co-Authored-By: Claude Sonnet 5 --- core/archipelago/src/mesh/listener/session.rs | 92 ++++++++++--------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/core/archipelago/src/mesh/listener/session.rs b/core/archipelago/src/mesh/listener/session.rs index 4e094c48..c1312dbc 100644 --- a/core/archipelago/src/mesh/listener/session.rs +++ b/core/archipelago/src/mesh/listener/session.rs @@ -261,7 +261,7 @@ impl MeshRadioDevice { /// `MeshConfig.device_kind` — see the plan's §2c reflashable-board note): only /// that one device's probe runs, so a non-matching firmware's init bytes are /// never injected into the port. `None` keeps the strict -/// Meshcore→Meshtastic→Reticulum probe order. +/// Reticulum→Meshcore→Meshtastic probe order. async fn auto_detect_and_open( data_dir: &Path, our_ed_pubkey_hex: &str, @@ -274,6 +274,34 @@ async fn auto_detect_and_open( } for path in &paths { debug!(path = %path, "Probing for mesh radio device"); + // Tried FIRST: `ReticulumLink::open()` gates its expensive daemon + // spawn behind a cheap (~1s worst case) RNode KISS-detect probe, so a + // failed match here costs about as much as the Meshcore/Meshtastic + // probes below. Trying it first matters in practice: on a real RNode + // board, Meshcore's and Meshtastic's handshake bytes (~5s timeout + // each, ~10.6s combined) sitting on the wire before Reticulum ever + // gets a turn was observed to leave the RNode firmware unresponsive + // by the time its turn came — confirmed on hardware that responds + // correctly to the same KISS probe on a truly fresh port. + if device_kind.is_none_or(|k| k == DeviceType::Reticulum) { + match ReticulumLink::open( + path, + data_dir, + Some(our_ed_pubkey_hex), + Some(our_x25519_pubkey_hex), + ) + .await + { + Ok(mut dev) => match dev.initialize().await { + Ok(info) => { + info!(path = %path, "Found Reticulum (RNode) device via auto-detect"); + return Ok((path.clone(), MeshRadioDevice::Reticulum(dev), info)); + } + Err(e) => debug!(path = %path, error = %e, "Reticulum daemon failed to initialize"), + }, + Err(e) => debug!(path = %path, error = %e, "Not a Reticulum RNode"), + } + } if device_kind.is_none_or(|k| k == DeviceType::Meshcore) { match MeshcoreDevice::open(path).await { Ok(mut dev) => match dev.initialize().await { @@ -298,30 +326,6 @@ async fn auto_detect_and_open( Err(e) => debug!(path = %path, error = %e, "Could not open serial port for Meshtastic"), } } - // Tried LAST: the same reflashable board (e.g. Heltec V3) can run - // Meshcore, Meshtastic, or RNode firmware, so each probe must fail - // strictly before the next is attempted. The RNode KISS-detect probe - // is the most expensive (spawns the supervised daemon on a match), so - // it goes after the two cheap firmware-specific handshakes above. - if device_kind.is_none_or(|k| k == DeviceType::Reticulum) { - match ReticulumLink::open( - path, - data_dir, - Some(our_ed_pubkey_hex), - Some(our_x25519_pubkey_hex), - ) - .await - { - Ok(mut dev) => match dev.initialize().await { - Ok(info) => { - info!(path = %path, "Found Reticulum (RNode) device via auto-detect"); - return Ok((path.clone(), MeshRadioDevice::Reticulum(dev), info)); - } - Err(e) => debug!(path = %path, error = %e, "Reticulum daemon failed to initialize"), - }, - Err(e) => debug!(path = %path, error = %e, "Not a Reticulum RNode"), - } - } } anyhow::bail!( "No supported mesh radio found on {} candidate ports: {:?}", @@ -381,20 +385,10 @@ async fn open_preferred_path( }; } - match MeshcoreDevice::open(path).await { - Ok(mut dev) => match dev.initialize().await { - Ok(info) => return Ok((MeshRadioDevice::Meshcore(dev), info)), - Err(e) => debug!(path = %path, error = %e, "Preferred path is not Meshcore"), - }, - Err(e) => debug!(path = %path, error = %e, "Could not open preferred path as Meshcore"), - } - match MeshtasticDevice::open(path).await { - Ok(mut dev) => match dev.initialize().await { - Ok(info) => return Ok((MeshRadioDevice::Meshtastic(dev), info)), - Err(e) => debug!(path = %path, error = %e, "Preferred path is not Meshtastic"), - }, - Err(e) => debug!(path = %path, error = %e, "Could not open preferred path as Meshtastic"), - } + // Reticulum first — see the matching comment on auto_detect_and_open: + // its cheap probe_rnode gate fails in ~1s for non-RNode firmware, while + // trying Meshcore/Meshtastic first was observed leaving a real RNode + // board unresponsive by the time Reticulum's turn came. match ReticulumLink::open( path, data_dir, @@ -404,10 +398,24 @@ async fn open_preferred_path( .await { Ok(mut dev) => match dev.initialize().await { - Ok(info) => Ok((MeshRadioDevice::Reticulum(dev), info)), - Err(e) => Err(e).context("Preferred path is not a working Reticulum RNode"), + Ok(info) => return Ok((MeshRadioDevice::Reticulum(dev), info)), + Err(e) => debug!(path = %path, error = %e, "Preferred path is not a working Reticulum RNode"), }, - Err(e) => Err(e).context("Could not open preferred path as Reticulum"), + Err(e) => debug!(path = %path, error = %e, "Could not open preferred path as Reticulum"), + } + match MeshcoreDevice::open(path).await { + Ok(mut dev) => match dev.initialize().await { + Ok(info) => return Ok((MeshRadioDevice::Meshcore(dev), info)), + Err(e) => debug!(path = %path, error = %e, "Preferred path is not Meshcore"), + }, + Err(e) => debug!(path = %path, error = %e, "Could not open preferred path as Meshcore"), + } + match MeshtasticDevice::open(path).await { + Ok(mut dev) => match dev.initialize().await { + Ok(info) => Ok((MeshRadioDevice::Meshtastic(dev), info)), + Err(e) => Err(e).context("Preferred path is not a working Meshtastic device"), + }, + Err(e) => Err(e).context("Could not open preferred path as Meshtastic"), } }