From c0d34bd836e0ac6333d3aa78367e2d108e4e31f1 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 26 Jul 2026 08:05:42 -0400 Subject: [PATCH] fix(mesh): serialize serial-port opens between listener and RPC probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Observed live on .116 after the dedup/backoff fixes: the kiosk browser's hot-swap auto-probe (mesh.probe-device) still interleaved its handshakes with the listener's open sequence on the same tty every backoff window — Linux double-opens ttys silently, both handshakes corrupted each other, and every collision's open() DTR/RTS-reset the board again. The retry heuristic from 5f01ec31 narrowed but couldn't close the race. A static PORT_OPEN_LOCK now covers the listener's whole device-open sequence and each probe attempt, so exactly one prober touches the port at a time. With this + the settle/backoff/dedup fixes, the .116 radio completed its first MeshCore handshake in days (node-fa161601, 8 contacts) and the session has been stable since. Co-Authored-By: Claude Fable 5 --- core/archipelago/src/mesh/listener/session.rs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/core/archipelago/src/mesh/listener/session.rs b/core/archipelago/src/mesh/listener/session.rs index a5d3a92a..f85d9170 100644 --- a/core/archipelago/src/mesh/listener/session.rs +++ b/core/archipelago/src/mesh/listener/session.rs @@ -372,6 +372,16 @@ pub struct DeviceProbe { pub max_contacts: Option, } +/// Serializes serial-port open sequences between the listener's session +/// opens and the RPC probe (`mesh.probe-device`). Linux happily double-opens +/// a tty, and two concurrent handshakes corrupt each other into silence — +/// observed live on .116 (2026-07-26): the kiosk browser's hot-swap +/// auto-probe collided with the listener's cycle on every backoff window, so +/// neither ever succeeded, and each collision's open() DTR/RTS-reset the +/// board again. The probe's retry-across-idle-gaps heuristic (5f01ec31) +/// narrowed but could not close the race; this closes it. +static PORT_OPEN_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + /// Probe a serial port for a mesh radio WITHOUT provisioning it: identify the /// firmware (same strict Reticulum→Meshcore→Meshtastic order as auto-detect, /// for the same RNode-wedging reason) and read what's currently configured on @@ -379,11 +389,10 @@ pub struct DeviceProbe { /// loop can pick the device up afterwards. Reticulum uses the bare KISS /// DETECT probe — no daemon spawn just to identify a stick. pub(crate) async fn probe_device(path: &str) -> Result { - // The listener's reconnect loop may hold this port for ~10s of every - // backoff cycle, and Linux happily double-opens a tty — two concurrent - // handshakes corrupt each other into silence (observed on framework-pt: - // every firmware "failed" while the listener was mid-cycle). Retry across - // the listener's idle gaps instead of failing on first collision. + // Retries kept even with PORT_OPEN_LOCK closing the double-open race: + // the board may still be mid-boot from a previous open's DTR/RTS reset, + // and a later attempt after a quiet gap can succeed where the first + // couldn't. let mut last_err = None; for attempt in 0..3u32 { if attempt > 0 { @@ -398,6 +407,7 @@ pub(crate) async fn probe_device(path: &str) -> Result { } async fn probe_device_once(path: &str) -> Result { + let _port_guard = PORT_OPEN_LOCK.lock().await; if super::super::reticulum::probe_rnode(path).await.is_ok() { return Ok(DeviceProbe { path: path.to_string(), @@ -976,6 +986,11 @@ pub(super) async fn run_mesh_session( // set, otherwise try the preferred serial path, falling back to // auto-detect. TCP mode is additive/dev-only; it never changes behavior // for existing serial/RNode deployments where `reticulum_tcp` is None. + // + // The whole open sequence runs under PORT_OPEN_LOCK so an RPC probe + // can't interleave its own handshakes on the same tty (see the lock's + // doc comment). Held only until the device is opened, then released. + let port_guard = PORT_OPEN_LOCK.lock().await; let (device_path, mut device, device_info) = if let Some(tcp_cfg) = &reticulum_tcp { open_reticulum_tcp(tcp_cfg, data_dir, our_ed_pubkey_hex, our_x25519_pubkey_hex).await? } else if let Some(path) = preferred_path { @@ -1014,6 +1029,7 @@ pub(super) async fn run_mesh_session( ) .await? }; + drop(port_guard); // Update status {