diff --git a/local/recipes/drivers/usb-core/source/src/spawn.rs b/local/recipes/drivers/usb-core/source/src/spawn.rs index cb4e3eff01..d8d1bd098a 100644 --- a/local/recipes/drivers/usb-core/source/src/spawn.rs +++ b/local/recipes/drivers/usb-core/source/src/spawn.rs @@ -2,14 +2,24 @@ /// binary path. Returns `None` if no driver is registered for this class. /// /// USB class codes from : +/// 0x01 — Audio (USB Audio Class) +/// 0x02 — Communications (CDC ACM/ECM) /// 0x03 — HID (Human Interface Device) /// 0x08 — Mass Storage /// 0x09 — Hub -pub fn class_driver_for_usb_class(class: u8, _subclass: u8, _protocol: u8) -> Option<&'static str> { - match class { - 0x03 => Some("/usr/bin/usbhidd"), - 0x08 => Some("/usr/bin/usbscsid"), - 0x09 => Some("/usr/bin/usbhubd"), +/// 0x0A — CDC Data (paired with CDC Comm) +/// 0xFF — Vendor-specific (FTDI, CP210x, etc.) +pub fn class_driver_for_usb_class(class: u8, subclass: u8, _protocol: u8) -> Option<&'static str> { + match (class, subclass) { + (0x01, 0x01) | (0x01, 0x02) => Some("/usr/bin/redbear-usbaudiod"), // Audio Control/Streaming + (0x02, 0x02) => Some("/usr/bin/redbear-acmd"), // CDC ACM (Abstract Control Model) + (0x02, 0x06) => Some("/usr/bin/redbear-ecmd"), // CDC ECM (Ethernet Control Model) + (0x02, _) => Some("/usr/bin/redbear-acmd"), // CDC generic → ACM fallback + (0x03, _) => Some("/usr/bin/usbhidd"), // HID + (0x08, _) => Some("/usr/bin/usbscsid"), // Mass Storage + (0x09, _) => Some("/usr/bin/usbhubd"), // Hub + (0x0A, _) => None, // CDC Data — paired with CDC Comm + (0xFF, _) => Some("/usr/bin/redbear-ftdi"), // Vendor-specific → FTDI _ => None, } } @@ -86,6 +96,8 @@ fn is_trusted_usb_driver(driver_binary: &str) -> bool { matches!( driver_binary, "/usr/bin/usbhubd" | "/usr/bin/usbhidd" | "/usr/bin/usbscsid" + | "/usr/bin/redbear-acmd" | "/usr/bin/redbear-ecmd" + | "/usr/bin/redbear-usbaudiod" | "/usr/bin/redbear-ftdi" ) } @@ -116,6 +128,10 @@ mod tests { assert!(is_trusted_usb_driver("/usr/bin/usbhubd")); assert!(is_trusted_usb_driver("/usr/bin/usbhidd")); assert!(is_trusted_usb_driver("/usr/bin/usbscsid")); + assert!(is_trusted_usb_driver("/usr/bin/redbear-acmd")); + assert!(is_trusted_usb_driver("/usr/bin/redbear-ecmd")); + assert!(is_trusted_usb_driver("/usr/bin/redbear-usbaudiod")); + assert!(is_trusted_usb_driver("/usr/bin/redbear-ftdi")); } #[test] diff --git a/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs b/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs index e0fc8296f9..7d07f3bb0e 100644 --- a/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs +++ b/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs @@ -1,13 +1,13 @@ -//! USB device hotplug daemon. +//! USB device hotplug daemon — comprehensive. //! //! Cross-referenced with Linux 7.1 `drivers/usb/core/hub.c` port event //! handling and `drivers/usb/core/driver.c` device-driver binding. //! -//! Watches USB controllers for device attachment/detachment events. -//! When a device connects, reads its class/subclass/protocol from the -//! device descriptor, maps it to the appropriate class driver binary, -//! and spawns the driver with the correct scheme + port arguments. -//! When a device disconnects, terminates the associated driver process. +//! Monitors all USB controllers for device connect/disconnect events. +//! When a device appears, reads its descriptors to determine class/subclass/ +//! protocol, maps to the correct class driver binary, and spawns it. +//! Handles both root hub ports and child hub ports (recursive scanning). +//! When a device disconnects or its driver exits, cleans up the process. use std::collections::HashMap; use std::fs; @@ -17,44 +17,51 @@ use std::time::Duration; use xhcid_interface::XhciClientHandle; -// USB class → driver binary mapping. -// Linux 7.1: usb_device_match() → usb_device_driver.id_table. -// Red Bear: static map in usb-core spawn.rs. Replicated here for the -// hotplug daemon which runs independently of usb-core. -const DRIVER_MAP: &[(u8, &str, &[&str])] = &[ - (0x03, "/usr/bin/usbhidd", &[]), // HID - (0x08, "/usr/bin/usbscsid", &["0x50"]), // Mass Storage (BOT) - (0x09, "/usr/bin/usbhubd", &[]), // Hub +// USB class → driver binary mapping, extended with all Red Bear class drivers. +// Cross-referenced with Linux 7.1 include/uapi/linux/usb/ch9.h and +// usb-core spawn.rs. Subclass-aware for CDC (ACM vs ECM) and Audio. +const DRIVER_MAP: &[(u8, u8, &str, &str)] = &[ + (0x01, 0x01, "/usr/bin/redbear-usbaudiod", "0"), + (0x01, 0x02, "/usr/bin/redbear-usbaudiod", "0"), + (0x02, 0x02, "/usr/bin/redbear-acmd", "0"), + (0x02, 0x06, "/usr/bin/redbear-ecmd", "0"), + (0x02, 0xFF, "/usr/bin/redbear-acmd", "0"), + (0x03, 0x00, "/usr/bin/usbhidd", "0"), + (0x03, 0x01, "/usr/bin/usbhidd", "0"), + (0x08, 0x06, "/usr/bin/usbscsid", "0x50"), + (0x09, 0x00, "/usr/bin/usbhubd", "0"), + (0xFF, 0x00, "/usr/bin/redbear-ftdi", "0"), + (0xFF, 0xFF, "/usr/bin/redbear-ftdi", "0"), ]; const POLL_INTERVAL_MS: u64 = 1000; struct TrackedDevice { - scheme: String, - port: String, child: Child, } -fn find_driver_for_class(class: u8, _subclass: u8, protocol: u8) -> Option<(&'static str, &'static str)> { - for &(cls, binary, _extra_args) in DRIVER_MAP { - if cls == class { - let extra = if protocol == 0x50 { "0x50" } else { "0" }; +fn find_driver(class: u8, subclass: u8, protocol: u8) -> Option<(&'static str, String)> { + for &(cls, subcls, binary, default_extra) in DRIVER_MAP { + if cls == class && (subcls == subclass || subcls == 0xFF) { + let extra = if cls == 0x08 { format!("0x{:02X}", protocol) } + else { default_extra.to_string() }; return Some((binary, extra)); } } None } -fn scan_controllers() -> Vec<(String, String)> { +fn scan_controllers() -> Vec { let mut controllers = Vec::new(); if let Ok(dir) = fs::read_dir("/scheme/usb") { for entry in dir.flatten() { let path = entry.path(); if path.is_dir() { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { - // Skip class driver scheme symlinks (not controller dirs) - if name.contains("_xhci") || name.contains("_ehci") || name.contains("_ohci") || name.contains("_uhci") { - controllers.push((name.to_string(), path.to_str().unwrap_or("").to_string())); + if name.contains("_xhci") || name.contains("_ehci") + || name.contains("_ohci") || name.contains("_uhci") + { + controllers.push(name.to_string()); } } } @@ -63,51 +70,62 @@ fn scan_controllers() -> Vec<(String, String)> { controllers } -fn scan_ports(_scheme: &str, controller_name: &str) -> Vec { +fn scan_ports(controller_name: &str) -> Vec { let mut ports = Vec::new(); let path = format!("/scheme/usb/{}", controller_name); - if let Ok(dir) = fs::read_dir(&path) { + scan_ports_recursive(&path, &mut ports); + ports +} + +fn scan_ports_recursive(base_path: &str, ports: &mut Vec) { + if let Ok(dir) = fs::read_dir(base_path) { for entry in dir.flatten() { let name = entry.file_name(); let name_str = name.to_string_lossy(); - if let Some(port_num) = name_str.strip_prefix("port") { - // Only root ports (no dots = root hub port) - if !port_num.contains('.') { - ports.push(format!("{}/{}", path, name_str)); + if name_str.starts_with("port") { + let full = format!("{}/{}", base_path, name_str); + ports.push(full.clone()); + if entry.path().is_dir() { + scan_ports_recursive(&full, ports); } } } } - ports } -fn try_read_descriptors(scheme: &str, _controller_name: &str, port_num: &str) -> Option<(u8, u8, u8)> { - // Try to open an XhciClientHandle to read device descriptors. - // Parse port number from the path. - let port_str = port_num.strip_prefix("port").unwrap_or(port_num); - let port_id: u8 = port_str.parse().ok()?; +fn extract_port_id(full_port_path: &str) -> Option<(String, String)> { + let parts: Vec<&str> = full_port_path.split('/').collect(); + if parts.len() < 4 { return None; } + let controller = parts.get(3)?; + let port_start = full_port_path.find(controller)? + controller.len() + 1; + let port_str = &full_port_path[port_start..]; + Some((controller.to_string(), port_str.to_string())) +} + +fn try_read_descriptors(scheme: &str, _controller: &str, port_str: &str) -> Option<(u8, u8, u8)> { + let port_id: u8 = if let Some(dot_pos) = port_str.find('.') { + port_str[4..dot_pos].parse().ok()? + } else { + port_str.strip_prefix("port")?.parse().ok()? + }; use xhcid_interface::PortId; let pid = PortId { root_hub_port_num: port_id, route_string: 0 }; match XhciClientHandle::new(scheme.to_string(), pid) { - Ok(handle) => { - match handle.get_standard_descs() { - Ok(desc) => { - // Find the first non-hub interface class (skip hubs, - // usbhubd handles its own children). - for conf in &desc.config_descs { - for ifd in &conf.interface_descs { - if ifd.class != 0x09 { - return Some((ifd.class, ifd.sub_class, ifd.protocol)); - } + Ok(handle) => match handle.get_standard_descs() { + Ok(desc) => { + for conf in &desc.config_descs { + for ifd in &conf.interface_descs { + if ifd.class != 0x09 && ifd.class != 0x0A { + return Some((ifd.class, ifd.sub_class, ifd.protocol)); } } - None } - Err(_) => None, + None } - } + Err(_) => None, + }, Err(_) => None, } } @@ -118,57 +136,45 @@ fn main() { common::setup_logging("usb", "system", "usb-hotplugd", common::output_level(), common::file_level()); - log::info!("USB hotplug daemon started — polling /scheme/usb/ every {}ms", POLL_INTERVAL_MS); + log::info!("USB hotplug daemon started — polling every {}ms", POLL_INTERVAL_MS); let mut tracked: HashMap = HashMap::new(); loop { let controllers = scan_controllers(); - for (ctrl_name, _ctrl_path) in &controllers { - // The scheme name passed to class drivers. xhci controllers - // use the full scheme path (e.g. "usb.0000:00:14.0_xhci"). + for ctrl_name in &controllers { let scheme_name = format!("usb.{}", ctrl_name); - - // Scan ports under this controller - let port_paths = scan_ports(&scheme_name, ctrl_name); + let port_paths = scan_ports(ctrl_name); for port_path in &port_paths { - let port_key = port_path.clone(); - - // Already tracking this port? - if tracked.contains_key(&port_key) { + if tracked.contains_key(port_path) { continue; } - // Extract port number from path - let port_num = port_path.rsplit('/').next().unwrap_or(""); - if port_num.is_empty() || !port_num.starts_with("port") { - continue; - } + let (ref ctrl, ref port_str) = match extract_port_id(port_path) { + Some(x) => x, + None => continue, + }; - // Try to read device descriptors - if let Some((class, subclass, protocol)) = try_read_descriptors(&scheme_name, ctrl_name, port_num) { - log::info!("New USB device on {}: class={:02X} subclass={:02X} protocol={:02X}", + if let Some((class, subclass, protocol)) = try_read_descriptors(&scheme_name, ctrl, port_str) { + log::info!("USB device on {}: class={:02X} subclass={:02X} protocol={:02X}", port_path, class, subclass, protocol); - if let Some((driver_binary, extra_arg)) = find_driver_for_class(class, subclass, protocol) { - let port_str = port_num.strip_prefix("port").unwrap_or(port_num); - log::info!("Spawning {} for {} on {} port {}", driver_binary, port_path, scheme_name, port_str); + if let Some((driver_binary, extra_arg)) = find_driver(class, subclass, protocol) { + let short_port = port_str.strip_prefix("port").unwrap_or(port_str); + log::info!("Spawning {} for {} (scheme={} port={} arg={})", + driver_binary, port_path, scheme_name, short_port, extra_arg); match Command::new(driver_binary) .arg(&scheme_name) - .arg(port_str) - .arg(extra_arg) + .arg(short_port) + .arg(&extra_arg) .stdin(std::process::Stdio::null()) .spawn() { Ok(child) => { - tracked.insert(port_key.clone(), TrackedDevice { - scheme: scheme_name.clone(), - port: port_str.to_string(), - child, - }); + tracked.insert(port_path.clone(), TrackedDevice { child }); } Err(e) => { log::warn!("Failed to spawn {} for {}: {}", driver_binary, port_path, e); @@ -179,35 +185,27 @@ fn main() { } } - // Check for disconnected devices: try to read port status. - // If a previously-tracked port no longer has a connected device, - // kill the driver and remove it from tracking. - let mut disconnected: Vec = Vec::new(); - for (port_key, tracked_dev) in &mut tracked { - match tracked_dev.child.try_wait() { + let mut removed: Vec = Vec::new(); + for (port_path, dev) in &mut tracked { + match dev.child.try_wait() { Ok(Some(status)) => { - log::info!("Driver for {} exited with status {:?}", port_key, status); - disconnected.push(port_key.clone()); + log::info!("Driver for {} exited ({:?})", port_path, status); + removed.push(port_path.clone()); } Ok(None) => { - // Driver still running. Check if device is still present. - // We use a simple heuristic: if the descriptors can no - // longer be read, the device is gone. - let scheme = tracked_dev.scheme.clone(); - let port_num = format!("port{}", tracked_dev.port); - let ctrl_name = port_key.split('/').nth(3).unwrap_or(""); - if try_read_descriptors(&scheme, ctrl_name, &port_num).is_none() { - log::info!("Device disconnected from {}", port_key); - let _ = tracked_dev.child.kill(); - disconnected.push(port_key.clone()); + if let Some((ref ctrl, ref port_str)) = extract_port_id(port_path) { + let scheme = format!("usb.{}", ctrl); + if try_read_descriptors(&scheme, ctrl, port_str).is_none() { + log::info!("Device disconnected from {}", port_path); + let _ = dev.child.kill(); + removed.push(port_path.clone()); + } } } - Err(e) => { - log::warn!("Error checking driver for {}: {}", port_key, e); - } + Err(e) => log::warn!("Driver check error for {}: {}", port_path, e), } } - for key in &disconnected { + for key in &removed { tracked.remove(key); }