2026-04-23 07:00:06 -03:00
|
|
|
|
impl UsbGadget {
|
|
|
|
|
|
fn sysfs_root() -> String {
|
|
|
|
|
|
env::var("LESAVKA_GADGET_SYSFS_ROOT").unwrap_or_else(|_| "/sys".to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn configfs_root() -> String {
|
|
|
|
|
|
env::var("LESAVKA_GADGET_CONFIGFS_ROOT")
|
|
|
|
|
|
.unwrap_or_else(|_| "/sys/kernel/config/usb_gadget".to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn new(name: &'static str) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
udc_file: Box::leak(
|
|
|
|
|
|
format!("{}/{}{}", Self::configfs_root(), name, "/UDC").into_boxed_str(),
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn state(ctrl: &str) -> anyhow::Result<String> {
|
|
|
|
|
|
let p = format!("{}/class/udc/{ctrl}/state", Self::sysfs_root());
|
|
|
|
|
|
Ok(std::fs::read_to_string(p)?.trim().to_owned())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn current_controller_state() -> anyhow::Result<(String, String)> {
|
|
|
|
|
|
let ctrl = Self::find_controller()?;
|
|
|
|
|
|
let state = Self::state(&ctrl)?;
|
|
|
|
|
|
Ok((ctrl, state))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn host_attached_state(state: &str) -> bool {
|
|
|
|
|
|
matches!(
|
|
|
|
|
|
state,
|
|
|
|
|
|
"configured" | "addressed" | "default" | "suspended" | "unknown"
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn host_enumerated_state(state: &str) -> bool {
|
|
|
|
|
|
matches!(state, "configured" | "addressed" | "default" | "suspended")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn current_state_detail() -> String {
|
|
|
|
|
|
match Self::current_controller_state() {
|
|
|
|
|
|
Ok((ctrl, state)) => format!("UDC {ctrl} state={state}"),
|
|
|
|
|
|
Err(err) => format!("UDC state unavailable: {err:#}"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*---- helpers ----*/
|
|
|
|
|
|
|
|
|
|
|
|
/// Find the first controller in /sys/class/udc (e.g. `1000480000.usb`)
|
|
|
|
|
|
pub fn find_controller() -> Result<String> {
|
|
|
|
|
|
Ok(fs::read_dir(format!("{}/class/udc", Self::sysfs_root()))?
|
|
|
|
|
|
.next()
|
|
|
|
|
|
.transpose()?
|
|
|
|
|
|
.context("no UDC present")?
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.to_string_lossy()
|
|
|
|
|
|
.into_owned())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Busy-loop (≤ `limit_ms`) until `state` matches `wanted`
|
|
|
|
|
|
fn wait_state(ctrl: &str, wanted: &str, limit_ms: u64) -> Result<()> {
|
|
|
|
|
|
let path = format!("{}/class/udc/{ctrl}/state", Self::sysfs_root());
|
|
|
|
|
|
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 {
|
|
|
|
|
|
return Ok(());
|
|
|
|
|
|
}
|
|
|
|
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(anyhow::anyhow!(
|
|
|
|
|
|
"UDC never reached '{wanted}' (last = {:?})",
|
|
|
|
|
|
fs::read_to_string(&path).unwrap_or_default()
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn wait_state_any(ctrl: &str, limit_ms: u64) -> anyhow::Result<String> {
|
|
|
|
|
|
let path = format!("{}/class/udc/{ctrl}/state", Self::sysfs_root());
|
|
|
|
|
|
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"
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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!("{}/class/udc/{ctrl}", Self::sysfs_root())).exists() {
|
|
|
|
|
|
return Ok(());
|
|
|
|
|
|
}
|
|
|
|
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(anyhow::anyhow!(
|
|
|
|
|
|
"⚠️ UDC {ctrl} did not re-appear within {limit_ms} ms"
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scan platform devices when /sys/class/udc is empty
|
|
|
|
|
|
fn probe_platform_udc() -> Result<Option<String>> {
|
|
|
|
|
|
for entry in fs::read_dir(format!("{}/bus/platform/devices", Self::sysfs_root()))? {
|
|
|
|
|
|
let p = entry?.file_name().into_string().unwrap();
|
|
|
|
|
|
if p.ends_with(".usb") {
|
|
|
|
|
|
return Ok(Some(p));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(None)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 04:30:48 -03:00
|
|
|
|
fn platform_driver(ctrl: &str) -> Option<String> {
|
|
|
|
|
|
fs::read_link(format!(
|
|
|
|
|
|
"{}/bus/platform/devices/{ctrl}/driver",
|
|
|
|
|
|
Self::sysfs_root()
|
|
|
|
|
|
))
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.and_then(|path| path.file_name().map(|name| name.to_string_lossy().into_owned()))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn soft_connect_path(ctrl: &str) -> Option<String> {
|
|
|
|
|
|
let path = format!("{}/class/udc/{ctrl}/soft_connect", Self::sysfs_root());
|
|
|
|
|
|
if !Path::new(&path).exists() {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
if env::var("LESAVKA_FORCE_SOFT_CONNECT").ok().as_deref() == Some("1") {
|
|
|
|
|
|
return Some(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Self::platform_driver(ctrl).as_deref() == Some("dwc2") {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
|
}
|