USB: comprehensive hotplug with Linux 7.1 port event state machine
Cross-referenced with Linux 7.1 drivers/usb/core/hub.c:
- hub_port_debounce() (line 4698): 25ms-step debounce algorithm
with 100ms stable threshold, 2000ms timeout per USB 2.0 §7.1.7.3
- hub_port_connect_change() (line 5055): connect/disconnect dispatch
- hub_irq() / port_event(): interrupt-driven change detection
PortState enum (replaces ad-hoc bool tracking):
Disconnected → DebouncingConnect → Connected → Active → Disconnected
Debounce: stable_start timestamp, 4x 25ms checks for 100ms
stable window. Connection drop during debounce → reset.
Timeout after 2000ms → warn + remove.
Phase-based main loop:
Phase 1: scan controllers/ports → detect new connections
Phase 2: debounce + state transitions per port
- DebouncingConnect: check stability against stable_start
- Connected: read descriptors → find_driver → spawn
- Active: check driver exit (try_wait) + disconnect detection
Phase 3: cleanup (remove disconnected, time out stale debounces)
Driver re-enumeration on crash: Active → Connected transition
on driver exit re-triggers descriptor read + spawn.
read_port_connected() uses XhciClientHandle open success as
connectivity check (lighter than full descriptor read).
This commit is contained in:
@@ -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<Child>,
|
||||
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<bool> {
|
||||
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<String, TrackedDevice> = 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<String> = 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<String> = 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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user