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"), } }