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-26 20:38:55 -05:00
|
|
|
|
pub fn state(ctrl: &str) -> anyhow::Result<String> {
|
|
|
|
|
|
let p = format!("/sys/class/udc/{ctrl}/state");
|
|
|
|
|
|
Ok(std::fs::read_to_string(p)?.trim().to_owned())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-28 15:45:11 -05:00
|
|
|
|
/*---- helpers ----*/
|
2025-06-25 22:24:58 -05:00
|
|
|
|
|
|
|
|
|
|
/// Find the first controller in /sys/class/udc (e.g. `1000480000.usb`)
|
2025-06-26 18:29:14 -05:00
|
|
|
|
pub fn find_controller() -> Result<String> {
|
2025-06-25 22:24:58 -05:00
|
|
|
|
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-28 15:45:11 -05:00
|
|
|
|
/// Busy-loop (≤ `limit_ms`) until `state` matches `wanted`
|
2025-06-25 22:24:58 -05:00
|
|
|
|
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-26 18:29:14 -05:00
|
|
|
|
pub fn wait_state_any(ctrl: &str, limit_ms: u64) -> anyhow::Result<String> {
|
|
|
|
|
|
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"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
2025-06-28 15:45:11 -05:00
|
|
|
|
Err(anyhow::anyhow!("⚠️ UDC {ctrl} did not re-appear within {limit_ms} ms"))
|
2025-06-25 23:51:30 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
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-28 15:45:11 -05:00
|
|
|
|
/*---- public API ----*/
|
2025-06-25 21:02:29 -05:00
|
|
|
|
|
2025-06-28 15:45:11 -05:00
|
|
|
|
/// Hard-reset the gadget → identical to a physical cable re-plug
|
2025-06-25 22:24:58 -05:00
|
|
|
|
pub fn cycle(&self) -> Result<()> {
|
2025-06-28 15:45:11 -05:00
|
|
|
|
/* 0 - ensure we *know* the controller even after a previous crash */
|
2025-06-27 14:01:29 -05:00
|
|
|
|
let ctrl = Self::find_controller()
|
2025-06-26 00:13:32 -05:00
|
|
|
|
.or_else(|_| Self::probe_platform_udc()?
|
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("no UDC present")))?;
|
2025-06-25 21:52:52 -05:00
|
|
|
|
|
2025-06-28 15:45:11 -05:00
|
|
|
|
/* 1 - detach gadget */
|
2025-06-25 22:24:58 -05:00
|
|
|
|
info!("🔌 detaching gadget from {ctrl}");
|
2025-06-28 15:45:11 -05:00
|
|
|
|
// a) drop pull-ups (if the controller offers the switch)
|
2025-06-26 01:11:14 -05:00
|
|
|
|
let sc = format!("/sys/class/udc/{ctrl}/soft_connect");
|
2025-06-28 15:45:11 -05:00
|
|
|
|
let _ = Self::write_attr(&sc, "0"); // ignore errors - not all HW has it
|
2025-06-26 01:11:14 -05:00
|
|
|
|
|
|
|
|
|
|
// 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::<std::io::Error>()
|
|
|
|
|
|
.and_then(|io| io.raw_os_error())
|
|
|
|
|
|
== Some(libc::EBUSY) && attempt < 10
|
|
|
|
|
|
} => {
|
2025-06-28 15:45:11 -05:00
|
|
|
|
trace!("⏳ UDC busy (attempt {attempt}/10) - retrying…");
|
2025-06-26 01:11:14 -05:00
|
|
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(err) => return Err(err),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-25 22:24:58 -05:00
|
|
|
|
Self::wait_state(&ctrl, "not attached", 3_000)?;
|
2025-06-25 21:52:52 -05:00
|
|
|
|
|
2025-06-28 15:45:11 -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-28 15:45:11 -05:00
|
|
|
|
/* 3 - wait UDC node to re-appear */
|
2025-06-26 00:13:32 -05:00
|
|
|
|
Self::wait_udc_present(&ctrl, 3_000)?;
|
|
|
|
|
|
|
2025-06-28 15:45:11 -05:00
|
|
|
|
/* 4 - re-attach + pull-up */
|
|
|
|
|
|
info!("🔌 re-attaching gadget to {ctrl}");
|
2025-06-25 22:24:58 -05:00
|
|
|
|
Self::write_attr(self.udc_file, &ctrl)?;
|
2025-06-26 00:45:52 -05:00
|
|
|
|
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::<std::io::Error>() {
|
|
|
|
|
|
match io.raw_os_error() {
|
|
|
|
|
|
// EINVAL | EPERM | ENOENT
|
|
|
|
|
|
Some(libc::EINVAL) | Some(libc::EPERM) | Some(libc::ENOENT) => {
|
2025-06-26 01:11:14 -05:00
|
|
|
|
warn!("⚠️ soft_connect unsupported ({io}); continuing");
|
2025-06-26 00:45:52 -05:00
|
|
|
|
}
|
|
|
|
|
|
_ => return Err(err), // propagate all other errors
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return Err(err); // non-IO errors: propagate
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(_) => { /* success */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-25 23:27:51 -05:00
|
|
|
|
|
2025-06-28 15:45:11 -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)
|
2025-06-28 15:45:11 -05:00
|
|
|
|
// we allow 'not attached' and continue - we can still
|
2025-06-25 23:27:51 -05:00
|
|
|
|
// 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" {
|
2025-06-28 15:45:11 -05:00
|
|
|
|
warn!("⚠️ host did not enumerate within 6 s - continuing (state = {last:?})");
|
2025-06-25 23:27:51 -05:00
|
|
|
|
Ok(())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Err(e)
|
|
|
|
|
|
}
|
|
|
|
|
|
})?;
|
2025-06-25 21:02:29 -05:00
|
|
|
|
|
2025-06-28 15:45:11 -05:00
|
|
|
|
info!("✅ USB-gadget 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 + 300 ms 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-26 01:59:53 -05:00
|
|
|
|
|
|
|
|
|
|
/*----------- unbind ------------------------------------------------*/
|
2025-06-25 22:24:58 -05:00
|
|
|
|
info!("🔧 unbinding UDC driver ({drv})");
|
2025-06-26 01:59:53 -05:00
|
|
|
|
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) => {
|
2025-06-28 15:45:11 -05:00
|
|
|
|
trace!("unbind in-progress (#{attempt}) - waiting…");
|
2025-06-26 01:59:53 -05:00
|
|
|
|
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
|
2025-06-25 22:24:58 -05:00
|
|
|
|
|
2025-06-26 01:59:53 -05:00
|
|
|
|
/*----------- bind --------------------------------------------------*/
|
2025-06-25 22:24:58 -05:00
|
|
|
|
info!("🔧 binding UDC driver ({drv})");
|
2025-06-26 01:59:53 -05:00
|
|
|
|
for attempt in 1..=20 {
|
2025-06-26 00:13:32 -05:00
|
|
|
|
match Self::write_attr(format!("{root}/bind"), ctrl) {
|
2025-06-26 01:59:53 -05:00
|
|
|
|
Ok(_) => return Ok(()), // success 🎉
|
|
|
|
|
|
Err(err) if attempt < 20 && Self::is_still_detaching(&err) => {
|
2025-06-28 15:45:11 -05:00
|
|
|
|
trace!("bind busy (#{attempt}) - retrying…");
|
2025-06-26 00:13:32 -05:00
|
|
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
|
|
}
|
2025-06-26 01:59:53 -05:00
|
|
|
|
Err(err) => return Err(err)
|
|
|
|
|
|
.context("UDC bind failed irrecoverably"),
|
2025-06-26 00:13:32 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-25 22:24:58 -05:00
|
|
|
|
}
|
|
|
|
|
|
Err(anyhow::anyhow!("no dwc2/dwc3 driver nodes found"))
|
|
|
|
|
|
}
|
2025-06-26 01:59:53 -05:00
|
|
|
|
|
|
|
|
|
|
fn is_still_detaching(err: &anyhow::Error) -> bool {
|
|
|
|
|
|
err.downcast_ref::<std::io::Error>()
|
|
|
|
|
|
.and_then(|io| io.raw_os_error())
|
|
|
|
|
|
.map_or(false, |code| {
|
|
|
|
|
|
matches!(code, libc::EBUSY | libc::ENOENT | libc::ENODEV)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-06-24 23:48:06 -05:00
|
|
|
|
}
|