// server/src/usb_gadget.rs use std::{fs::{self, OpenOptions}, io::Write, path::Path, thread, time::Duration}; use anyhow::{Context, Result}; use tracing::{info, warn, trace}; #[derive(Clone)] pub struct UsbGadget { udc_file: &'static str, } impl UsbGadget { pub fn new(name: &'static str) -> Self { Self { udc_file: Box::leak( format!("/sys/kernel/config/usb_gadget/{name}/UDC").into_boxed_str(), ), } } /*–––– helpers ––––*/ /// Find the first controller in /sys/class/udc (e.g. `1000480000.usb`) pub fn find_controller() -> Result { Ok(fs::read_dir("/sys/class/udc")? .next() .transpose()? .context("no UDC present")? .file_name() .to_string_lossy() .into_owned()) } /// Busy‑loop (≤ `limit_ms`) until `state` matches `wanted` fn wait_state(ctrl: &str, wanted: &str, limit_ms: u64) -> Result<()> { let path = format!("/sys/class/udc/{ctrl}/state"); for _ in 0..=limit_ms / 50 { let s = fs::read_to_string(&path).unwrap_or_default(); trace!("⏳ state={s:?}, want={wanted}"); if s.trim() == wanted { return Ok(()); } thread::sleep(Duration::from_millis(50)); } Err(anyhow::anyhow!("UDC never reached '{wanted}' (last = {:?})", fs::read_to_string(&path).unwrap_or_default())) } pub fn wait_state_any(ctrl: &str, limit_ms: u64) -> anyhow::Result { let path = format!("/sys/class/udc/{ctrl}/state"); for _ in 0..=limit_ms / 50 { if let Ok(s) = std::fs::read_to_string(&path) { let s = s.trim(); if matches!(s, "configured" | "not attached") { return Ok(s.to_owned()); } } std::thread::sleep(std::time::Duration::from_millis(50)); } Err(anyhow::anyhow!("UDC state did not settle within {limit_ms} ms")) } /// Write `value` (plus “\n”) into a sysfs attribute fn write_attr>(p: P, value: &str) -> Result<()> { OpenOptions::new() .write(true) .open(p)? .write_all(format!("{value}\n").as_bytes())?; Ok(()) } // Wait (≤ `limit_ms`) until `/sys/class/udc/` exists again. fn wait_udc_present(ctrl: &str, limit_ms: u64) -> Result<()> { for _ in 0..=limit_ms / 50 { if Path::new(&format!("/sys/class/udc/{ctrl}")).exists() { return Ok(()); } thread::sleep(Duration::from_millis(50)); } Err(anyhow::anyhow!("⚠️ UDC {ctrl} did not re‑appear within {limit_ms} ms")) } /// Scan platform devices when /sys/class/udc is empty fn probe_platform_udc() -> Result> { for entry in fs::read_dir("/sys/bus/platform/devices")? { let p = entry?.file_name().into_string().unwrap(); if p.ends_with(".usb") { return Ok(Some(p)); } } Ok(None) } /*–––– public API ––––*/ /// Hard‑reset the gadget → identical to a physical cable re‑plug pub fn cycle(&self) -> Result<()> { /* 0 – ensure we *know* the controller even after a previous crash */ let mut ctrl = Self::find_controller() .or_else(|_| Self::probe_platform_udc()? .ok_or_else(|| anyhow::anyhow!("no UDC present")))?; /* 1 – detach gadget */ info!("🔌 detaching gadget from {ctrl}"); // a) drop pull‑ups (if the controller offers the switch) let sc = format!("/sys/class/udc/{ctrl}/soft_connect"); let _ = Self::write_attr(&sc, "0"); // ignore errors – not all HW has it // b) clear the UDC attribute; the kernel may transiently answer EBUSY for attempt in 1..=10 { match Self::write_attr(self.udc_file, "") { Ok(_) => break, Err(err) if { // only swallow EBUSY err.downcast_ref::() .and_then(|io| io.raw_os_error()) == Some(libc::EBUSY) && attempt < 10 } => { trace!("⏳ UDC busy (attempt {attempt}/10) – retrying…"); thread::sleep(Duration::from_millis(100)); } Err(err) => return Err(err), } } Self::wait_state(&ctrl, "not attached", 3_000)?; /* 2 – reset driver */ Self::rebind_driver(&ctrl)?; /* 3 – wait UDC node to re‑appear */ Self::wait_udc_present(&ctrl, 3_000)?; /* 4 – re‑attach + pull‑up */ info!("🔌 re‑attaching gadget to {ctrl}"); Self::write_attr(self.udc_file, &ctrl)?; if Path::new(&sc).exists() { // try to set the pull-up; ignore if the kernel rejects it match Self::write_attr(&sc, "1") { Err(err) => { // only swallow specific errno values if let Some(io) = err.downcast_ref::() { match io.raw_os_error() { // EINVAL | EPERM | ENOENT Some(libc::EINVAL) | Some(libc::EPERM) | Some(libc::ENOENT) => { warn!("⚠️ soft_connect unsupported ({io}); continuing"); } _ => return Err(err), // propagate all other errors } } else { return Err(err); // non-IO errors: propagate } } Ok(_) => { /* success */ } } } /* 5 – wait for host (but tolerate sleep) */ Self::wait_state(&ctrl, "configured", 6_000) .or_else(|e| { // If the host is physically absent (sleep / KVM paused) // we allow 'not attached' and continue – we can still // accept keyboard/mouse data and the host will enumerate // later without another reset. let last = fs::read_to_string(format!("/sys/class/udc/{ctrl}/state")) .unwrap_or_default(); if last.trim() == "not attached" { warn!("⚠️ host did not enumerate within 6 s – continuing (state = {last:?})"); Ok(()) } else { Err(e) } })?; info!("✅ USB‑gadget cycle complete"); Ok(()) } /// helper: unbind + 300 ms reset + bind fn rebind_driver(ctrl: &str) -> Result<()> { let cand = ["dwc2", "dwc3"]; for drv in cand { let root = format!("/sys/bus/platform/drivers/{drv}"); if !Path::new(&root).exists() { continue } /*----------- unbind ------------------------------------------------*/ info!("🔧 unbinding UDC driver ({drv})"); for attempt in 1..=20 { match Self::write_attr(format!("{root}/unbind"), ctrl) { Ok(_) => break, Err(err) if attempt < 20 && Self::is_still_detaching(&err) => { trace!("unbind in‑progress (#{attempt}) – waiting…"); thread::sleep(Duration::from_millis(100)); } Err(err) => return Err(err) .context("UDC unbind failed irrecoverably"), } } thread::sleep(Duration::from_millis(150)); // let the core quiesce /*----------- bind --------------------------------------------------*/ info!("🔧 binding UDC driver ({drv})"); for attempt in 1..=20 { match Self::write_attr(format!("{root}/bind"), ctrl) { Ok(_) => return Ok(()), // success 🎉 Err(err) if attempt < 20 && Self::is_still_detaching(&err) => { trace!("bind busy (#{attempt}) – retrying…"); thread::sleep(Duration::from_millis(100)); } Err(err) => return Err(err) .context("UDC bind failed irrecoverably"), } } } Err(anyhow::anyhow!("no dwc2/dwc3 driver nodes found")) } fn is_still_detaching(err: &anyhow::Error) -> bool { err.downcast_ref::() .and_then(|io| io.raw_os_error()) .map_or(false, |code| { matches!(code, libc::EBUSY | libc::ENOENT | libc::ENODEV) }) } }