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-25 21:11:59 -05:00
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
|
use tracing::{info, warn};
|
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 21:11:59 -05:00
|
|
|
|
/// Read the controller name currently written to `<gadget>/UDC`
|
|
|
|
|
|
fn current_udc(&self) -> Result<String> {
|
|
|
|
|
|
Ok(fs::read_to_string(self.udc_file)?.trim().to_owned())
|
|
|
|
|
|
}
|
2025-06-25 21:02:29 -05:00
|
|
|
|
|
2025-06-25 21:11:59 -05:00
|
|
|
|
/// Helper: busy‑loop (max 10s) until the UDC `state` file equals `wanted`.
|
|
|
|
|
|
fn wait_state(udc: &str, wanted: &str) -> Result<()> {
|
|
|
|
|
|
let state_file = format!("/sys/class/udc/{udc}/state");
|
|
|
|
|
|
for _ in 0..100 {
|
|
|
|
|
|
let state = fs::read_to_string(&state_file)
|
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.to_owned();
|
|
|
|
|
|
if state == wanted {
|
|
|
|
|
|
return Ok(());
|
|
|
|
|
|
}
|
|
|
|
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(anyhow::anyhow!("🚧 UDC state did not reach '{wanted}' in time"))
|
|
|
|
|
|
.with_context(|| format!("waiting for {wanted} in {state_file}"))
|
|
|
|
|
|
}
|
2025-06-24 23:48:06 -05:00
|
|
|
|
|
2025-06-25 21:11:59 -05:00
|
|
|
|
/// Force the host to re‑enumerate our HID gadget *safely*.
|
|
|
|
|
|
pub fn cycle(&self) -> Result<()> {
|
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-25 21:11:59 -05:00
|
|
|
|
.file_name()
|
|
|
|
|
|
.to_string_lossy()
|
|
|
|
|
|
.into_owned();
|
2025-06-25 21:02:29 -05:00
|
|
|
|
|
2025-06-25 21:11:59 -05:00
|
|
|
|
/*–––– DETACH ––––*/
|
|
|
|
|
|
if !self.current_udc()?.is_empty() {
|
|
|
|
|
|
info!("🔌 UDC‑cycle: detaching gadget");
|
|
|
|
|
|
OpenOptions::new()
|
2025-06-25 21:02:29 -05:00
|
|
|
|
.write(true)
|
|
|
|
|
|
.open(self.udc_file)?
|
2025-06-25 21:11:59 -05:00
|
|
|
|
.write_all(b"")?;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
info!("🔌 Gadget already detached");
|
2025-06-25 21:02:29 -05:00
|
|
|
|
}
|
2025-06-25 21:11:59 -05:00
|
|
|
|
Self::wait_state(&udc_name, "not attached")?;
|
|
|
|
|
|
|
|
|
|
|
|
/*–––– ATTACH ––––*/
|
|
|
|
|
|
info!("🔌 UDC‑cycle: re‑attaching to {udc_name}");
|
|
|
|
|
|
OpenOptions::new()
|
|
|
|
|
|
.write(true)
|
|
|
|
|
|
.open(self.udc_file)?
|
|
|
|
|
|
.write_all(udc_name.as_bytes())?;
|
2025-06-25 21:02:29 -05:00
|
|
|
|
|
2025-06-25 21:11:59 -05:00
|
|
|
|
Self::wait_state(&udc_name, "configured")?;
|
|
|
|
|
|
info!("🔌 USB‑gadget cycled successfully 🎉");
|
|
|
|
|
|
Ok(())
|
2025-06-24 23:48:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|