impl UsbGadget { fn sysfs_root() -> String { env::var("LESAVKA_GADGET_SYSFS_ROOT").unwrap_or_else(|_| "/sys".to_string()) } fn configfs_root() -> String { env::var("LESAVKA_GADGET_CONFIGFS_ROOT") .unwrap_or_else(|_| "/sys/kernel/config/usb_gadget".to_string()) } pub fn new(name: &'static str) -> Self { Self { udc_file: Box::leak( format!("{}/{}{}", Self::configfs_root(), name, "/UDC").into_boxed_str(), ), } } pub fn state(ctrl: &str) -> anyhow::Result { let p = format!("{}/class/udc/{ctrl}/state", Self::sysfs_root()); Ok(std::fs::read_to_string(p)?.trim().to_owned()) } pub fn current_controller_state() -> anyhow::Result<(String, String)> { let ctrl = Self::find_controller()?; let state = Self::state(&ctrl)?; Ok((ctrl, state)) } pub fn host_attached_state(state: &str) -> bool { matches!( state, "configured" | "addressed" | "default" | "suspended" | "unknown" ) } pub fn host_enumerated_state(state: &str) -> bool { matches!(state, "configured" | "addressed" | "default" | "suspended") } pub fn current_state_detail() -> String { match Self::current_controller_state() { Ok((ctrl, state)) => format!("UDC {ctrl} state={state}"), Err(err) => format!("UDC state unavailable: {err:#}"), } } /*---- helpers ----*/ /// Find the first controller in /sys/class/udc (e.g. `1000480000.usb`) pub fn find_controller() -> Result { Ok(fs::read_dir(format!("{}/class/udc", Self::sysfs_root()))? .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!("{}/class/udc/{ctrl}/state", Self::sysfs_root()); 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!("{}/class/udc/{ctrl}/state", Self::sysfs_root()); 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!("{}/class/udc/{ctrl}", Self::sysfs_root())).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(format!("{}/bus/platform/devices", Self::sysfs_root()))? { let p = entry?.file_name().into_string().unwrap(); if p.ends_with(".usb") { return Ok(Some(p)); } } Ok(None) } fn platform_driver(ctrl: &str) -> Option { fs::read_link(format!( "{}/bus/platform/devices/{ctrl}/driver", Self::sysfs_root() )) .ok() .and_then(|path| path.file_name().map(|name| name.to_string_lossy().into_owned())) } fn soft_connect_path(ctrl: &str) -> Option { let path = format!("{}/class/udc/{ctrl}/soft_connect", Self::sysfs_root()); if !Path::new(&path).exists() { return None; } if env::var("LESAVKA_FORCE_SOFT_CONNECT").ok().as_deref() == Some("1") { return Some(path); } if Self::platform_driver(ctrl).as_deref() == Some("dwc2") { return None; } Some(path) } }