From 0ac62c088c2f355d44b9679bbccbfac294a9c37c Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Fri, 23 Oct 2026 17:58:23 -0700 Subject: [PATCH] fix(udev): tag tty/usb rules ID_MM_DEVICE_IGNORE so ModemManager skips boards (#1653) Co-Authored-By: Claude --- crates/fbuild-cli/src/cli/udev.rs | 32 ++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/fbuild-cli/src/cli/udev.rs b/crates/fbuild-cli/src/cli/udev.rs index e4ba0005c..5a3b0d234 100644 --- a/crates/fbuild-cli/src/cli/udev.rs +++ b/crates/fbuild-cli/src/cli/udev.rs @@ -38,6 +38,12 @@ pub const LEGACY_UDEV_RULES_FILENAME: &str = "99-fbuild.rules"; /// raw USB device (probe-rs, picotool, DFU) and HID loaders. pub const RULE_SUBSYSTEMS: [&str; 3] = ["tty", "usb", "hidraw"]; +/// Subsystems ModemManager probes. Tagging them `ID_MM_DEVICE_IGNORE` keeps +/// it from sending AT commands to a board that just re-enumerated after a +/// flash; the tag must be set before `80-mm-candidate.rules` +/// (FastLED/fbuild#1653). +pub const MM_IGNORE_SUBSYSTEMS: [&str; 2] = ["tty", "usb"]; + /// `group` if it exists in `group_file` (normally `/etc/group`), else None. /// /// Distros differ: Debian/Ubuntu ship `plugdev`, while NixOS, Arch and Fedora @@ -128,9 +134,14 @@ pub fn render_udev_rules(vids: &[u16], group: Option<&str>) -> Option { .map(|g| format!("GROUP=\"{g}\", ")) .unwrap_or_default(); for subsystem in RULE_SUBSYSTEMS { + let mm_field = if MM_IGNORE_SUBSYSTEMS.contains(&subsystem) { + ", ENV{ID_MM_DEVICE_IGNORE}=\"1\"" + } else { + "" + }; out.push_str(&format!( "SUBSYSTEM==\"{subsystem}\", ATTRS{{idVendor}}==\"{vid:04x}\", \ - MODE=\"0660\", {group_field}TAG+=\"uaccess\"\n" + MODE=\"0660\", {group_field}TAG+=\"uaccess\"{mm_field}\n" )); } } @@ -284,4 +295,23 @@ mod tests { "{UDEV_RULES_FILENAME} sorts after 73-seat-late.rules" ); } + + /// ModemManager probes fresh CDC/USB devices with AT commands; tagged + /// devices are skipped. The tag must land before 80-mm-candidate.rules. + #[test] + fn tags_tty_and_usb_for_modemmanager_ignore() { + let out = render_udev_rules(&[0x16c0], None).unwrap(); + for line in out.lines().filter(|l| l.starts_with("SUBSYSTEM==")) { + let tagged = line.contains(r#"ENV{ID_MM_DEVICE_IGNORE}="1""#); + let probed = MM_IGNORE_SUBSYSTEMS + .iter() + .any(|s| line.starts_with(&format!(r#"SUBSYSTEM=="{s}""#))); + assert_eq!(tagged, probed, "{line}"); + } + let prio: u32 = UDEV_RULES_FILENAME[..2].parse().unwrap(); + assert!( + prio < 80, + "{UDEV_RULES_FILENAME} sorts after 80-mm-candidate" + ); + } }