lesavka/server/src/gadget/sysfs_state.rs

151 lines
5.0 KiB
Rust
Raw Normal View History

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<String> {
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<String> {
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<String> {
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: AsRef<Path>>(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/<ctrl>` 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<Option<String>> {
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)
}
2026-04-28 04:30:48 -03:00
fn platform_driver(ctrl: &str) -> Option<String> {
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<String> {
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)
}
}