lesavka/server/src/usb_gadget.rs

154 lines
5.6 KiB
Rust
Raw Normal View History

2025-06-25 21:02:29 -05:00
// server/src/usb_gadget.rs
2025-06-25 22:24:58 -05:00
use std::{fs::{self, OpenOptions}, io::Write, path::Path, thread, time::Duration};
2025-06-25 21:11:59 -05:00
use anyhow::{Context, Result};
2025-06-25 21:52:52 -05:00
use tracing::{info, warn, trace};
2025-06-24 23:48:06 -05:00
#[derive(Clone)]
pub struct UsbGadget {
udc_file: &'static str,
}
2025-06-25 07:46:50 -05:00
impl UsbGadget {
pub fn new(name: &'static str) -> Self {
2025-06-25 21:02:29 -05:00
Self {
udc_file: Box::leak(
format!("/sys/kernel/config/usb_gadget/{name}/UDC").into_boxed_str(),
),
2025-06-24 23:48:06 -05:00
}
}
2025-06-25 22:24:58 -05:00
/* 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")?
2025-06-25 21:52:52 -05:00
.next()
.transpose()?
.context("no UDC present")?
.file_name()
.to_string_lossy()
.into_owned())
}
2025-06-25 22:24:58 -05:00
/// 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 {
2025-06-25 21:11:59 -05:00
return Ok(());
}
2025-06-25 21:52:52 -05:00
thread::sleep(Duration::from_millis(50));
2025-06-25 21:11:59 -05:00
}
2025-06-25 22:24:58 -05:00
Err(anyhow::anyhow!("UDC never reached '{wanted}' (last = {:?})",
fs::read_to_string(&path).unwrap_or_default()))
2025-06-25 21:11:59 -05:00
}
2025-06-24 23:48:06 -05:00
2025-06-25 22:24:58 -05:00
/// 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(())
}
2025-06-25 21:52:52 -05:00
2025-06-25 23:51:30 -05:00
// 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"))
}
2025-06-26 00:13:32 -05:00
/// 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)
}
2025-06-25 22:24:58 -05:00
/* public API */
2025-06-25 21:02:29 -05:00
2025-06-25 22:24:58 -05:00
/// Hardreset the gadget → identical to a physical cable replug
pub fn cycle(&self) -> Result<()> {
/* 0ensure we *know* the controller even after a previous crash */
2025-06-26 00:13:32 -05:00
let mut ctrl = Self::find_controller()
.or_else(|_| Self::probe_platform_udc()?
.ok_or_else(|| anyhow::anyhow!("no UDC present")))?;
2025-06-25 21:52:52 -05:00
2025-06-25 22:24:58 -05:00
/* 1 detach gadget */
info!("🔌 detaching gadget from {ctrl}");
Self::write_attr(self.udc_file, "")?;
Self::wait_state(&ctrl, "not attached", 3_000)?;
2025-06-25 21:52:52 -05:00
2025-06-26 00:13:32 -05:00
/* 2 reset driver */
2025-06-25 22:24:58 -05:00
Self::rebind_driver(&ctrl)?;
2025-06-25 21:52:52 -05:00
2025-06-26 00:13:32 -05:00
/* 3 wait UDC node to reappear */
Self::wait_udc_present(&ctrl, 3_000)?;
/* 4 reattach + pullup */
2025-06-25 21:52:52 -05:00
info!("🔌 reattaching gadget to {ctrl}");
2025-06-25 22:24:58 -05:00
Self::write_attr(self.udc_file, &ctrl)?;
2025-06-25 23:27:51 -05:00
let sc = format!("/sys/class/udc/{ctrl}/soft_connect");
2025-06-26 00:13:32 -05:00
Self::write_attr(&sc, "0")?;
2025-06-25 23:27:51 -05:00
thread::sleep(Duration::from_millis(50));
Self::write_attr(&sc, "1")?;
2025-06-25 23:51:30 -05:00
/* 5 wait for host (but tolerate sleep) */
2025-06-25 23:27:51 -05:00
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)
}
})?;
2025-06-25 21:02:29 -05:00
2025-06-25 22:24:58 -05:00
info!("✅ USBgadget cycle complete");
2025-06-25 21:11:59 -05:00
Ok(())
2025-06-24 23:48:06 -05:00
}
2025-06-25 22:24:58 -05:00
/// helper: unbind + 300ms reset + bind
fn rebind_driver(ctrl: &str) -> Result<()> {
2025-06-26 00:13:32 -05:00
let cand = ["dwc2", "dwc3"];
2025-06-25 22:24:58 -05:00
for drv in cand {
let root = format!("/sys/bus/platform/drivers/{drv}");
2025-06-26 00:13:32 -05:00
if !Path::new(&root).exists() { continue }
2025-06-25 22:24:58 -05:00
info!("🔧 unbinding UDC driver ({drv})");
2025-06-26 00:13:32 -05:00
let _ = Self::write_attr(format!("{root}/unbind"), ctrl); // ignore EINVAL
thread::sleep(Duration::from_millis(300));
2025-06-25 22:24:58 -05:00
info!("🔧 binding UDC driver ({drv})");
2025-06-26 00:13:32 -05:00
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")),
}
}
2025-06-25 22:24:58 -05:00
}
Err(anyhow::anyhow!("no dwc2/dwc3 driver nodes found"))
}
2025-06-24 23:48:06 -05:00
}