fix(mesh): serialize serial-port opens between listener and RPC probe
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 <noreply@anthropic.com>
This commit is contained in:
parent
c8d0dda656
commit
c0d34bd836
@ -372,6 +372,16 @@ pub struct DeviceProbe {
|
|||||||
pub max_contacts: Option<u16>,
|
pub max_contacts: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// Probe a serial port for a mesh radio WITHOUT provisioning it: identify the
|
||||||
/// firmware (same strict Reticulum→Meshcore→Meshtastic order as auto-detect,
|
/// firmware (same strict Reticulum→Meshcore→Meshtastic order as auto-detect,
|
||||||
/// for the same RNode-wedging reason) and read what's currently configured on
|
/// 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
|
/// loop can pick the device up afterwards. Reticulum uses the bare KISS
|
||||||
/// DETECT probe — no daemon spawn just to identify a stick.
|
/// DETECT probe — no daemon spawn just to identify a stick.
|
||||||
pub(crate) async fn probe_device(path: &str) -> Result<DeviceProbe> {
|
pub(crate) async fn probe_device(path: &str) -> Result<DeviceProbe> {
|
||||||
// The listener's reconnect loop may hold this port for ~10s of every
|
// Retries kept even with PORT_OPEN_LOCK closing the double-open race:
|
||||||
// backoff cycle, and Linux happily double-opens a tty — two concurrent
|
// the board may still be mid-boot from a previous open's DTR/RTS reset,
|
||||||
// handshakes corrupt each other into silence (observed on framework-pt:
|
// and a later attempt after a quiet gap can succeed where the first
|
||||||
// every firmware "failed" while the listener was mid-cycle). Retry across
|
// couldn't.
|
||||||
// the listener's idle gaps instead of failing on first collision.
|
|
||||||
let mut last_err = None;
|
let mut last_err = None;
|
||||||
for attempt in 0..3u32 {
|
for attempt in 0..3u32 {
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
@ -398,6 +407,7 @@ pub(crate) async fn probe_device(path: &str) -> Result<DeviceProbe> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn probe_device_once(path: &str) -> Result<DeviceProbe> {
|
async fn probe_device_once(path: &str) -> Result<DeviceProbe> {
|
||||||
|
let _port_guard = PORT_OPEN_LOCK.lock().await;
|
||||||
if super::super::reticulum::probe_rnode(path).await.is_ok() {
|
if super::super::reticulum::probe_rnode(path).await.is_ok() {
|
||||||
return Ok(DeviceProbe {
|
return Ok(DeviceProbe {
|
||||||
path: path.to_string(),
|
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
|
// set, otherwise try the preferred serial path, falling back to
|
||||||
// auto-detect. TCP mode is additive/dev-only; it never changes behavior
|
// auto-detect. TCP mode is additive/dev-only; it never changes behavior
|
||||||
// for existing serial/RNode deployments where `reticulum_tcp` is None.
|
// 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 {
|
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?
|
open_reticulum_tcp(tcp_cfg, data_dir, our_ed_pubkey_hex, our_x25519_pubkey_hex).await?
|
||||||
} else if let Some(path) = preferred_path {
|
} else if let Some(path) = preferred_path {
|
||||||
@ -1014,6 +1029,7 @@ pub(super) async fn run_mesh_session(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
drop(port_guard);
|
||||||
|
|
||||||
// Update status
|
// Update status
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user