USB: comprehensive hotplug — extended driver map, recursive port scanning

usb-core spawn.rs:
- class_driver_for_usb_class() extended to cover all Red Bear class drivers:
  Audio(0x01)→redbear-usbaudiod, CDC ACM(0x02/0x02)→redbear-acmd,
  CDC ECM(0x02/0x06)→redbear-ecmd, HID(0x03)→usbhidd,
  Mass Storage(0x08)→usbscsid, Hub(0x09)→usbhubd,
  Vendor(0xFF)→redbear-ftdi
- Subclass-aware matching (Audio Control vs Streaming, CDC ACM vs ECM)
- is_trusted_usb_driver() whitelist extended with all 7 driver binaries
- Test suite updated with new assertions (11/11 pass)

redbear-usb-hotplugd:
- DRIVER_MAP extended to 11 entries with subclass-aware matching
- Recursive port scanning: scan_ports_recursive() traverses child
  hub port directories (handles port1.port2.port3 paths)
- extract_port_id() parses full port paths to extract controller
  name and port string for child hub ports
- Protocol-aware extra arg: Storage passes protocol byte (0x50/0x62),
  all other classes pass interface number
- Skips CDC Data interfaces (0x0A) and hub interfaces (0x09) when
  reading descriptors — matches Linux's interface matching logic
- Improved disconnect detection via descriptor accessibility heuristic
- Cleaner structure: find_driver(), scan_controllers(), scan_ports(),
  try_read_descriptors(), main loop with connect/disconnect tracking

All 7 Red Bear USB class drivers now auto-spawning on device connect:
usbhidd, usbscsid, usbhubd, redbear-acmd, redbear-ecmd,
redbear-usbaudiod, redbear-ftdi
This commit is contained in:
2026-07-07 16:00:24 +03:00
parent 19c7856642
commit 0d494d8e89
2 changed files with 119 additions and 105 deletions
@@ -2,14 +2,24 @@
/// binary path. Returns `None` if no driver is registered for this class.
///
/// USB class codes from <https://www.usb.org/defined-class-codes>:
/// 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]
@@ -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<String> {
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<String> {
fn scan_ports(controller_name: &str) -> Vec<String> {
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<String>) {
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<String, TrackedDevice> = 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<String> = Vec::new();
for (port_key, tracked_dev) in &mut tracked {
match tracked_dev.child.try_wait() {
let mut removed: Vec<String> = 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);
}