// server/src/usb_gadget.rs use std::{fs::{self, OpenOptions}, io::Write, thread, time::Duration}; use anyhow::{Context, Result}; use tracing::{info, warn}; #[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(), ), } } /// Read the controller name currently written to `/UDC` fn current_udc(&self) -> Result { Ok(fs::read_to_string(self.udc_file)?.trim().to_owned()) } /// 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}")) } /// Force the host to re‑enumerate our HID gadget *safely*. pub fn cycle(&self) -> Result<()> { let udc_name = std::fs::read_dir("/sys/class/udc")? .next() .transpose()? .context("no UDC present")? .file_name() .to_string_lossy() .into_owned(); /*–––– DETACH ––––*/ if !self.current_udc()?.is_empty() { info!("🔌 UDC‑cycle: detaching gadget"); OpenOptions::new() .write(true) .open(self.udc_file)? .write_all(b"")?; } else { info!("🔌 Gadget already detached"); } 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())?; Self::wait_state(&udc_name, "configured")?; info!("🔌 USB‑gadget cycled successfully 🎉"); Ok(()) } }