2025-06-25 21:02:29 -05:00
|
|
|
|
// server/src/usb_gadget.rs
|
|
|
|
|
|
|
|
|
|
|
|
use std::{fs::{self, OpenOptions}, io::Write, thread, time::Duration};
|
2025-06-24 23:48:06 -05:00
|
|
|
|
use anyhow::{Result, Context};
|
2025-06-25 21:02:29 -05:00
|
|
|
|
use tracing::{debug, error, info, warn};
|
|
|
|
|
|
|
|
|
|
|
|
fn wait_for_detach(udc_path: &str) {
|
|
|
|
|
|
for _ in 0..20 {
|
|
|
|
|
|
// as soon as the file is **empty**, the detach succeeded
|
|
|
|
|
|
if fs::read_to_string(udc_path).map(|s| s.trim().is_empty()).unwrap_or(false) {
|
|
|
|
|
|
debug!("🔌 UDC is now *detached*");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
|
|
}
|
|
|
|
|
|
warn!("⏳ UDC did not detach within the expected time-out");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Like `wait_for_detach`, but the opposite condition.
|
|
|
|
|
|
fn wait_for_attach(udc_path: &str) {
|
|
|
|
|
|
for _ in 0..20 {
|
|
|
|
|
|
if fs::read_to_string(udc_path).map(|s| !s.trim().is_empty()).unwrap_or(false) {
|
|
|
|
|
|
debug!("🔌 UDC is *attached* again");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
|
|
}
|
|
|
|
|
|
warn!("⏳ UDC did not attach within the expected time-out");
|
|
|
|
|
|
}
|
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-24 23:48:06 -05:00
|
|
|
|
// /sys/kernel/config/usb_gadget/<name>/UDC
|
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 21:02:29 -05:00
|
|
|
|
/// Force the host to re-enumerate our HID gadget.
|
|
|
|
|
|
/// Retries on EBUSY so that a transient “resource busy” never kills the whole process.
|
2025-06-24 23:48:06 -05:00
|
|
|
|
pub fn cycle(&self) -> Result<()> {
|
2025-06-25 21:02:29 -05:00
|
|
|
|
info!("🔌 UDC-cycle: detaching gadget");
|
|
|
|
|
|
OpenOptions::new()
|
|
|
|
|
|
.write(true)
|
|
|
|
|
|
.open(self.udc_file)?
|
|
|
|
|
|
.write_all(b"")?; // empty string → detach
|
|
|
|
|
|
wait_for_detach(self.udc_file);
|
|
|
|
|
|
|
|
|
|
|
|
// Let the controller settle
|
2025-06-25 07:46:50 -05:00
|
|
|
|
thread::sleep(Duration::from_millis(200));
|
2025-06-24 23:48:06 -05:00
|
|
|
|
|
2025-06-25 07:46:50 -05:00
|
|
|
|
let udc_name = std::fs::read_dir("/sys/class/udc")?
|
|
|
|
|
|
.next()
|
|
|
|
|
|
.transpose()?
|
|
|
|
|
|
.context("no UDC present")?
|
2025-06-24 23:48:06 -05:00
|
|
|
|
.file_name();
|
2025-06-25 21:02:29 -05:00
|
|
|
|
|
|
|
|
|
|
// Retry loop for the notoriously fragile re-attach
|
|
|
|
|
|
for attempt in 0..5 {
|
|
|
|
|
|
info!("🔌 UDC-cycle: re-attaching to {} (try #{attempt})", udc_name.to_string_lossy());
|
|
|
|
|
|
match OpenOptions::new()
|
|
|
|
|
|
.write(true)
|
|
|
|
|
|
.open(self.udc_file)?
|
|
|
|
|
|
.write_all(udc_name.to_str().unwrap().as_bytes())
|
|
|
|
|
|
{
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
wait_for_attach(self.udc_file);
|
|
|
|
|
|
info!("🟢 USB-gadget cycled successfully");
|
|
|
|
|
|
return Ok(());
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) if e.raw_os_error() == Some(libc::EBUSY) => {
|
|
|
|
|
|
warn!("🚧 UDC busy on re-attach – retrying in 100 ms");
|
|
|
|
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
error!("💥 unexpected error while re-attaching UDC: {e:?}");
|
|
|
|
|
|
return Err(e).context("re-attaching gadget to UDC");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Err(anyhow::anyhow!(
|
|
|
|
|
|
"giving up after 5 attempts – UDC remained busy"
|
|
|
|
|
|
))
|
2025-06-24 23:48:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|