fix(mesh): plug-and-play radio detection in all situations
Three root causes from the 2026-08-16 framework-pt incident where a replugged radio detected but never connected: - detect_serial_devices scanned a hardcoded ttyUSB0-2/ttyACM0-2 list, so a radio enumerating at index 3+ was permanently invisible. Now scans /dev for all ttyUSB*/ttyACM* nodes (deterministic order, /dev/mesh-radio alias still first and still wins the dedup). - An operator rnode-rf-settings.json port override silently outranked the device_path the user just chose in the detection modal. mesh.configure now clears a stale override when a different device is configured (symlink-resolved compare keeps /dev/mesh-radio aliases intact). - Espressif native-USB boards (303a, ESP32-S2/S3/C3 RNodes) had no udev rule, so they never got the stable /dev/mesh-radio alias and a persisted alias path dangled after a port move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c5cd751bcf
commit
809f7649a4
@@ -531,6 +531,18 @@ pub async fn save_config(data_dir: &Path, config: &MeshConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Two /dev paths refer to the same serial device if their symlink-resolved
|
||||
/// targets match (e.g. `/dev/mesh-radio` vs the `/dev/ttyUSBn` it points at).
|
||||
/// Paths that fail to resolve fall back to a plain string comparison.
|
||||
async fn same_serial_device(a: &str, b: &str) -> bool {
|
||||
if a == b {
|
||||
return true;
|
||||
}
|
||||
let ra = fs::canonicalize(a).await.unwrap_or_else(|_| PathBuf::from(a));
|
||||
let rb = fs::canonicalize(b).await.unwrap_or_else(|_| PathBuf::from(b));
|
||||
ra == rb
|
||||
}
|
||||
|
||||
pub async fn load_ignored_radio_contacts(data_dir: &Path) -> Vec<String> {
|
||||
let path = data_dir.join(MESH_IGNORED_RADIO_FILE);
|
||||
if !path.exists() {
|
||||
@@ -2268,6 +2280,30 @@ impl MeshService {
|
||||
pub async fn configure(&mut self, config: MeshConfig) -> Result<()> {
|
||||
save_config(&self.data_dir, &config).await?;
|
||||
|
||||
// An operator-set RNode serial-port override (rnode-rf-settings.json)
|
||||
// outranks `device_path` when the Reticulum session opens the radio.
|
||||
// When a *different* device path is being configured (hot-swap, or
|
||||
// "Keep As Is" on a newly detected radio), a stale override pinned to
|
||||
// the old port would silently veto the choice the user just made —
|
||||
// clear it so the explicit device selection wins. Same-device aliases
|
||||
// (/dev/mesh-radio vs its ttyUSBn target) are left alone.
|
||||
if let Some(new_path) = config.device_path.as_deref() {
|
||||
let mut rf = rnode_settings::RNodeRfSettings::load(&self.data_dir).await;
|
||||
if let Some(port) = rf.port.clone() {
|
||||
if !same_serial_device(&port, new_path).await {
|
||||
info!(
|
||||
old_port = %port,
|
||||
new_path = %new_path,
|
||||
"Clearing stale RNode serial-port override — configured device path changed"
|
||||
);
|
||||
rf.port = None;
|
||||
if let Err(e) = rf.save(&self.data_dir).await {
|
||||
warn!("Failed to clear stale RNode port override: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let was_enabled = self.config.enabled;
|
||||
let needs_session_restart = session_config_changed(&self.config, &config);
|
||||
self.config = config.clone();
|
||||
|
||||
Reference in New Issue
Block a user