lesavka/server/src/usb_gadget.rs
2025-06-26 00:13:32 -05:00

154 lines
5.6 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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`)
fn find_controller() -> Result<String> {
Ok(fs::read_dir("/sys/class/udc")?
.next()
.transpose()?
.context("no UDC present")?
.file_name()
.to_string_lossy()
.into_owned())
}
/// Busyloop (≤ `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()))
}
/// 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!("/sys/class/udc/{ctrl}")).exists() {
return Ok(());
}
thread::sleep(Duration::from_millis(50));
}
Err(anyhow::anyhow!("⚠️ UDC {ctrl} did not reappear 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("/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 */
/// Hardreset the gadget → identical to a physical cable replug
pub fn cycle(&self) -> Result<()> {
/* 0ensure 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}");
Self::write_attr(self.udc_file, "")?;
Self::wait_state(&ctrl, "not attached", 3_000)?;
/* 2 reset driver */
Self::rebind_driver(&ctrl)?;
/* 3 wait UDC node to reappear */
Self::wait_udc_present(&ctrl, 3_000)?;
/* 4 reattach + pullup */
info!("🔌 reattaching gadget to {ctrl}");
Self::write_attr(self.udc_file, &ctrl)?;
let sc = format!("/sys/class/udc/{ctrl}/soft_connect");
Self::write_attr(&sc, "0")?;
thread::sleep(Duration::from_millis(50));
Self::write_attr(&sc, "1")?;
/* 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 6s continuing (state = {last:?})");
Ok(())
} else {
Err(e)
}
})?;
info!("✅ USBgadget cycle complete");
Ok(())
}
/// helper: unbind + 300ms 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 }
info!("🔧 unbinding UDC driver ({drv})");
let _ = Self::write_attr(format!("{root}/unbind"), ctrl); // ignore EINVAL
thread::sleep(Duration::from_millis(300));
info!("🔧 binding UDC driver ({drv})");
for attempt in 1..=10 {
match Self::write_attr(format!("{root}/bind"), ctrl) {
Ok(_) => return Ok(()),
Err(e) if attempt < 10 => {
trace!("bind busy (#{attempt}) retrying…");
thread::sleep(Duration::from_millis(100));
continue;
}
Err(e) => return Err(e)
.context(format!("bind failed after {attempt} tries")),
}
}
}
Err(anyhow::anyhow!("no dwc2/dwc3 driver nodes found"))
}
}