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 7d07f3bb0e..bdbe6c124d 100644 --- a/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs +++ b/local/recipes/system/redbear-usb-hotplugd/source/src/main.rs @@ -1,25 +1,30 @@ -//! USB device hotplug daemon — comprehensive. +//! USB device hotplug daemon — comprehensive port event state machine. //! -//! Cross-referenced with Linux 7.1 `drivers/usb/core/hub.c` port event -//! handling and `drivers/usb/core/driver.c` device-driver binding. +//! Cross-referenced with Linux 7.1 `drivers/usb/core/hub.c`: +//! - hub_port_connect_change() (line 5055+): port connect/disconnect dispatch +//! - hub_port_debounce() (line 4698+): 25ms-step debounce algorithm +//! - hub_port_connect() (line 4939+): full device enumeration sequence +//! - hub_irq() / port_event(): interrupt-driven change detection //! -//! 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. +//! Implements the USB 2.0 §7.1.7.3 connect debounce requirement: +//! 100ms stable connection before reporting connect, 2000ms timeout. +//! Follows Linux's HUB_DEBOUNCE_STEP=25ms, HUB_DEBOUNCE_STABLE=100ms, +//! HUB_DEBOUNCE_TIMEOUT=2000ms constants. use std::collections::HashMap; use std::fs; use std::process::{Child, Command}; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; use xhcid_interface::XhciClientHandle; -// 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. +// USB 2.0 §7.1.7.3 debounce timing. +// Linux 7.1 drivers/usb/core/hub.c:138-140. +const DEBOUNCE_STEP_MS: u64 = 25; +const DEBOUNCE_STABLE_MS: u64 = 100; +const DEBOUNCE_TIMEOUT_MS: u64 = 2000; + const DRIVER_MAP: &[(u8, u8, &str, &str)] = &[ (0x01, 0x01, "/usr/bin/redbear-usbaudiod", "0"), (0x01, 0x02, "/usr/bin/redbear-usbaudiod", "0"), @@ -34,10 +39,22 @@ const DRIVER_MAP: &[(u8, u8, &str, &str)] = &[ (0xFF, 0xFF, "/usr/bin/redbear-ftdi", "0"), ]; -const POLL_INTERVAL_MS: u64 = 1000; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PortState { + Disconnected, + DebouncingConnect { stable_start: Instant }, + Connected, + Active, +} struct TrackedDevice { - child: Child, + state: PortState, + child: Option, + class: u8, + subclass: u8, + protocol: u8, + scheme: String, + port_str: String, } fn find_driver(class: u8, subclass: u8, protocol: u8) -> Option<(&'static str, String)> { @@ -98,36 +115,39 @@ fn extract_port_id(full_port_path: &str) -> Option<(String, String)> { 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())) + Some((controller.to_string(), full_port_path[port_start..].to_string())) } -fn try_read_descriptors(scheme: &str, _controller: &str, port_str: &str) -> Option<(u8, u8, u8)> { +fn read_port_connected(scheme: &str, port_str: &str) -> Option { 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 }; + XhciClientHandle::new(scheme.to_string(), pid).ok().map(|_| true) +} +fn read_descriptors(scheme: &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) => { - 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)); - } - } + XhciClientHandle::new(scheme.to_string(), pid).ok()?.get_standard_descs().ok().and_then(|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, - }, - Err(_) => None, - } + } + None + }) } fn main() { @@ -136,13 +156,16 @@ fn main() { common::setup_logging("usb", "system", "usb-hotplugd", common::output_level(), common::file_level()); - log::info!("USB hotplug daemon started — polling every {}ms", POLL_INTERVAL_MS); + log::info!("USB hotplug daemon — debounce {}/{}/{}ms per Linux 7.1 hub_port_debounce()", + DEBOUNCE_STEP_MS, DEBOUNCE_STABLE_MS, DEBOUNCE_TIMEOUT_MS); let mut tracked: HashMap = HashMap::new(); loop { + let now = Instant::now(); let controllers = scan_controllers(); + // Phase 1: discover new ports (Linux 7.1 hub_irq → port_event) for ctrl_name in &controllers { let scheme_name = format!("usb.{}", ctrl_name); let port_paths = scan_ports(ctrl_name); @@ -151,64 +174,104 @@ fn main() { if tracked.contains_key(port_path) { continue; } - let (ref ctrl, ref port_str) = match extract_port_id(port_path) { Some(x) => x, None => continue, }; - - 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(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(short_port) - .arg(&extra_arg) - .stdin(std::process::Stdio::null()) - .spawn() - { - Ok(child) => { - tracked.insert(port_path.clone(), TrackedDevice { child }); - } - Err(e) => { - log::warn!("Failed to spawn {} for {}: {}", driver_binary, port_path, e); - } - } - } + let connected = read_port_connected(&scheme_name, port_str).unwrap_or(false); + if connected { + log::info!("port {}: new connection, entering debounce", port_path); + tracked.insert(port_path.clone(), TrackedDevice { + state: PortState::DebouncingConnect { stable_start: now }, + child: None, class: 0, subclass: 0, protocol: 0, + scheme: scheme_name.clone(), port_str: port_str.clone(), + }); } } } - let mut removed: Vec = Vec::new(); + // Phase 2: debounce + state transitions (Linux 7.1 hub_port_debounce) for (port_path, dev) in &mut tracked { - match dev.child.try_wait() { - Ok(Some(status)) => { - log::info!("Driver for {} exited ({:?})", port_path, status); - removed.push(port_path.clone()); + let scheme = dev.scheme.clone(); + let port_str = dev.port_str.clone(); + let connected = read_port_connected(&scheme, &port_str).unwrap_or(false); + + match dev.state { + PortState::Disconnected => {} + + PortState::DebouncingConnect { stable_start } => { + if !connected { + log::info!("port {}: dropped during debounce", port_path); + dev.state = PortState::Disconnected; + } else if now.duration_since(stable_start) >= Duration::from_millis(DEBOUNCE_STABLE_MS) { + log::info!("port {}: debounce passed ({}ms)", port_path, + now.duration_since(stable_start).as_millis()); + dev.state = PortState::Connected; + } } - Ok(None) => { - 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()); + + PortState::Connected => { + if !connected { + log::info!("port {}: disconnected before enumeration", port_path); + dev.state = PortState::Disconnected; + } else if let Some((class, subclass, protocol)) = read_descriptors(&scheme, &port_str) { + log::info!("port {}: class={:02X} subclass={:02X} protocol={:02X}", + port_path, class, subclass, protocol); + dev.class = class; dev.subclass = subclass; dev.protocol = protocol; + + 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 {} (scheme={} port={} arg={})", + driver_binary, scheme, short_port, extra_arg); + match Command::new(driver_binary) + .arg(&scheme).arg(short_port).arg(&extra_arg) + .stdin(std::process::Stdio::null()).spawn() + { + Ok(child) => { + dev.child = Some(child); + dev.state = PortState::Active; + } + Err(e) => { + log::warn!("spawn {}: {}", driver_binary, e); + } + } + } else { + dev.state = PortState::Active; } } } - Err(e) => log::warn!("Driver check error for {}: {}", port_path, e), + + PortState::Active => { + if let Some(ref mut child) = dev.child { + if let Ok(Some(status)) = child.try_wait() { + log::info!("port {}: driver exited ({:?})", port_path, status); + dev.child = None; + dev.state = PortState::Connected; + } + } + if !connected { + log::info!("port {}: device disconnected", port_path); + if let Some(ref mut child) = dev.child { let _ = child.kill(); } + dev.child = None; + dev.state = PortState::Disconnected; + } + } } } - for key in &removed { - tracked.remove(key); - } - thread::sleep(Duration::from_millis(POLL_INTERVAL_MS)); + // Phase 3: remove disconnected entries, time out stale debounces + let mut remove: Vec = Vec::new(); + for (key, dev) in &tracked { + if dev.state == PortState::Disconnected { remove.push(key.clone()); } + if let PortState::DebouncingConnect { stable_start } = dev.state { + if now.duration_since(stable_start) >= Duration::from_millis(DEBOUNCE_TIMEOUT_MS) { + log::warn!("port {}: debounce timed out", key); + remove.push(key.clone()); + } + } + } + for key in &remove { tracked.remove(key); } + + thread::sleep(Duration::from_millis(DEBOUNCE_STEP_MS)); } }