use std::env; pub(crate) const STREAM_CTRL_SIZE_11: usize = 26; pub(crate) const STREAM_CTRL_SIZE_15: usize = 34; pub(crate) const STREAM_CTRL_SIZE_MAX: usize = STREAM_CTRL_SIZE_15; pub(crate) const UVC_DATA_SIZE: usize = 60; pub(crate) const V4L2_EVENT_PRIVATE_START: u32 = 0x0800_0000; pub(crate) const UVC_EVENT_CONNECT: u32 = V4L2_EVENT_PRIVATE_START + 0; pub(crate) const UVC_EVENT_DISCONNECT: u32 = V4L2_EVENT_PRIVATE_START + 1; pub(crate) const UVC_EVENT_STREAMON: u32 = V4L2_EVENT_PRIVATE_START + 2; pub(crate) const UVC_EVENT_STREAMOFF: u32 = V4L2_EVENT_PRIVATE_START + 3; pub(crate) const UVC_EVENT_SETUP: u32 = V4L2_EVENT_PRIVATE_START + 4; pub(crate) const UVC_EVENT_DATA: u32 = V4L2_EVENT_PRIVATE_START + 5; pub(crate) const UVC_STRING_CONTROL_IDX: u8 = 0; pub(crate) const UVC_STRING_STREAMING_IDX: u8 = 1; pub(crate) const CONFIGFS_UVC_BASE: &str = "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0"; pub(crate) const USB_DIR_IN: u8 = 0x80; pub(crate) const UVC_SET_CUR: u8 = 0x01; pub(crate) const UVC_GET_CUR: u8 = 0x81; pub(crate) const UVC_GET_MIN: u8 = 0x82; pub(crate) const UVC_GET_MAX: u8 = 0x83; pub(crate) const UVC_GET_RES: u8 = 0x84; pub(crate) const UVC_GET_LEN: u8 = 0x85; pub(crate) const UVC_GET_INFO: u8 = 0x86; pub(crate) const UVC_GET_DEF: u8 = 0x87; pub(crate) const UVC_VS_PROBE_CONTROL: u8 = 0x01; pub(crate) const UVC_VS_COMMIT_CONTROL: u8 = 0x02; pub(crate) const UVC_VC_REQUEST_ERROR_CODE_CONTROL: u8 = 0x02; #[repr(C)] pub(crate) struct V4l2EventSubscription { pub(crate) type_: u32, pub(crate) id: u32, pub(crate) flags: u32, pub(crate) reserved: [u32; 5], } #[repr(C)] pub(crate) union V4l2EventUnion { pub(crate) data: [u8; 64], pub(crate) _align: u64, } #[repr(C)] pub(crate) struct V4l2Event { pub(crate) type_: u32, pub(crate) u: V4l2EventUnion, pub(crate) pending: u32, pub(crate) sequence: u32, pub(crate) timestamp: libc::timespec, pub(crate) id: u32, pub(crate) reserved: [u32; 8], } #[repr(C)] #[derive(Clone, Copy)] pub(crate) struct UsbCtrlRequest { pub(crate) b_request_type: u8, pub(crate) b_request: u8, pub(crate) w_value: u16, pub(crate) w_index: u16, pub(crate) w_length: u16, } #[repr(C)] #[derive(Clone, Copy)] pub(crate) struct UvcRequestData { pub(crate) length: i32, pub(crate) data: [u8; UVC_DATA_SIZE], } #[derive(Clone, Copy)] pub(crate) struct UvcConfig { pub(crate) width: u32, pub(crate) height: u32, pub(crate) fps: u32, pub(crate) interval: u32, pub(crate) max_packet: u32, pub(crate) frame_size: u32, } pub(crate) struct PayloadCap { pub(crate) limit: u32, pub(crate) pct: u32, pub(crate) source: &'static str, pub(crate) periodic_dw: Option, pub(crate) non_periodic_dw: Option, } pub(crate) struct UvcState { pub(crate) cfg: UvcConfig, pub(crate) ctrl_len: usize, pub(crate) default: [u8; STREAM_CTRL_SIZE_MAX], pub(crate) probe: [u8; STREAM_CTRL_SIZE_MAX], pub(crate) commit: [u8; STREAM_CTRL_SIZE_MAX], pub(crate) cfg_snapshot: Option, } #[derive(Clone, Copy)] pub(crate) struct PendingRequest { pub(crate) interface: u8, pub(crate) selector: u8, pub(crate) expected_len: usize, } #[derive(Clone, Copy)] pub(crate) struct UvcInterfaces { pub(crate) control: u8, pub(crate) streaming: u8, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct ConfigfsSnapshot { pub(crate) width: u32, pub(crate) height: u32, pub(crate) default_interval: u32, pub(crate) frame_interval: u32, pub(crate) maxpacket: u32, pub(crate) maxburst: u32, } impl UvcConfig { pub(crate) fn from_env() -> Self { let width = env_u32("LESAVKA_UVC_WIDTH", 1280); let height = env_u32("LESAVKA_UVC_HEIGHT", 720); let fps = env_u32("LESAVKA_UVC_FPS", 30).max(1); let interval = env_u32("LESAVKA_UVC_INTERVAL", 0); let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024); let frame_size = env_u32("LESAVKA_UVC_FRAME_SIZE", width * height * 2); let bulk = env::var("LESAVKA_UVC_BULK").is_ok(); if let Some(cap) = compute_payload_cap(bulk) { if max_packet > cap.limit { eprintln!( "[lesavka-uvc] payload cap {}B ({}% from {}): clamp max_packet {} -> {} (periodic_dw={:?} non_periodic_dw={:?})", cap.limit, cap.pct, cap.source, max_packet, cap.limit, cap.periodic_dw, cap.non_periodic_dw ); max_packet = cap.limit; } else { eprintln!( "[lesavka-uvc] payload cap {}B ({}% from {}): max_packet {} (periodic_dw={:?} non_periodic_dw={:?})", cap.limit, cap.pct, cap.source, max_packet, cap.periodic_dw, cap.non_periodic_dw ); } } else { eprintln!( "[lesavka-uvc] payload cap unavailable; using max_packet {}", max_packet ); } if let Some(cfg_max) = read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming_maxpacket")) { if max_packet > cfg_max { eprintln!( "[lesavka-uvc] configfs maxpacket {}: clamp max_packet {} -> {}", cfg_max, max_packet, cfg_max ); max_packet = cfg_max; } else { eprintln!( "[lesavka-uvc] configfs maxpacket {}: max_packet {}", cfg_max, max_packet ); } } max_packet = if env::var("LESAVKA_UVC_BULK").is_ok() { max_packet.min(512) } else { max_packet.min(1024) }; let interval = if interval == 0 { 10_000_000 / fps } else { interval }; Self { width, height, fps, interval, max_packet, frame_size, } } } impl UvcState { pub(crate) fn new(cfg: UvcConfig) -> Self { let ctrl_len = stream_ctrl_len(); let default = build_streaming_control(&cfg, ctrl_len); Self { cfg, ctrl_len, default, probe: default, commit: default, cfg_snapshot: None, } } } pub(crate) fn stream_ctrl_len() -> usize { let value = env_u32("LESAVKA_UVC_CTRL_LEN", STREAM_CTRL_SIZE_15 as u32) as usize; match value { STREAM_CTRL_SIZE_11 | STREAM_CTRL_SIZE_15 => value, _ => STREAM_CTRL_SIZE_11, } } pub(crate) fn env_u32(name: &str, default: u32) -> u32 { env::var(name) .ok() .and_then(|value| value.parse::().ok()) .unwrap_or(default) } pub(crate) fn env_u8(name: &str) -> Option { env::var(name).ok().and_then(|value| value.parse::().ok()) } pub(crate) fn env_u32_opt(name: &str) -> Option { env::var(name).ok().and_then(|value| value.parse::().ok()) } pub(crate) fn read_u32_file(path: &str) -> Option { std::fs::read_to_string(path) .ok() .and_then(|value| value.trim().parse::().ok()) } pub(crate) fn read_u32_first(path: &str) -> Option { std::fs::read_to_string(path) .ok() .and_then(|value| value.split_whitespace().next()?.parse::().ok()) } pub(crate) fn read_configfs_snapshot() -> Option { let frame = configfs_frame_name_from_env(); let frame_root = format!("{CONFIGFS_UVC_BASE}/streaming/mjpeg/m/{frame}"); let width = read_u32_file(&format!("{frame_root}/wWidth"))?; let height = read_u32_file(&format!("{frame_root}/wHeight"))?; let default_interval = read_u32_file(&format!( "{frame_root}/dwDefaultFrameInterval" ))?; let frame_interval = read_u32_first(&format!("{frame_root}/dwFrameInterval")).unwrap_or(0); let maxpacket = read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming_maxpacket"))?; let maxburst = read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming_maxburst")).unwrap_or(0); Some(ConfigfsSnapshot { width, height, default_interval, frame_interval, maxpacket, maxburst, }) } fn configfs_frame_name_from_env() -> &'static str { match ( env_u32("LESAVKA_UVC_WIDTH", 1280), env_u32("LESAVKA_UVC_HEIGHT", 720), ) { (1920, 1080) => "1080p", _ => "720p", } } pub(crate) fn log_configfs_snapshot(state: &mut UvcState, label: &str) { let Some(current) = read_configfs_snapshot() else { eprintln!("[lesavka-uvc] configfs {label}: unavailable"); return; }; if state.cfg_snapshot == Some(current) { return; } eprintln!( "[lesavka-uvc] configfs {label}: {}x{} default_interval={} frame_interval={} maxpacket={} maxburst={}", current.width, current.height, current.default_interval, current.frame_interval, current.maxpacket, current.maxburst ); state.cfg_snapshot = Some(current); } pub(crate) fn adjust_length(mut bytes: Vec, w_length: u16) -> Vec { let want = (w_length as usize).min(UVC_DATA_SIZE); if bytes.len() > want { bytes.truncate(want); } else if bytes.len() < want { bytes.resize(want, 0); } bytes } pub(crate) fn write_le16(dst: &mut [u8], val: u16) { let bytes = val.to_le_bytes(); dst[0] = bytes[0]; dst[1] = bytes[1]; } pub(crate) fn write_le32(dst: &mut [u8], val: u32) { let bytes = val.to_le_bytes(); dst[0] = bytes[0]; dst[1] = bytes[1]; dst[2] = bytes[2]; dst[3] = bytes[3]; } pub(crate) fn read_le32(src: &[u8], offset: usize) -> u32 { u32::from_le_bytes([ src[offset], src[offset + 1], src[offset + 2], src[offset + 3], ]) } pub(crate) fn build_streaming_control( cfg: &UvcConfig, ctrl_len: usize, ) -> [u8; STREAM_CTRL_SIZE_MAX] { let mut buf = [0u8; STREAM_CTRL_SIZE_MAX]; write_le16(&mut buf[0..2], 1); buf[2] = 1; buf[3] = uvc_frame_index_for_mode(cfg.width, cfg.height); write_le32(&mut buf[4..8], cfg.interval); write_le16(&mut buf[8..10], 0); write_le16(&mut buf[10..12], 0); write_le16(&mut buf[12..14], 0); write_le16(&mut buf[14..16], 0); write_le16(&mut buf[16..18], 0); write_le32(&mut buf[18..22], cfg.frame_size); write_le32(&mut buf[22..26], cfg.max_packet); if ctrl_len >= STREAM_CTRL_SIZE_15 { write_le32(&mut buf[26..30], 48_000_000); buf[30] = 0x03; buf[31] = 0x01; buf[32] = 0x01; buf[33] = 0x01; } buf } pub(crate) fn uvc_frame_index_for_mode(width: u32, height: u32) -> u8 { match (width, height) { (1920, 1080) => 1, (1280, 720) => 2, _ => 2, } } pub(crate) fn uvc_frame_index_for_request(requested: u8, cfg: &UvcConfig) -> u8 { match requested { 1 | 2 => requested, _ => uvc_frame_index_for_mode(cfg.width, cfg.height), } } pub(crate) fn uvc_frame_size_for_index(frame_index: u8, fallback: u32) -> u32 { match frame_index { 1 => 1920 * 1080 * 2, 2 => 1280 * 720 * 2, _ => fallback, } } pub(crate) fn compute_payload_cap(bulk: bool) -> Option { if let Some(limit) = env_u32_opt("LESAVKA_UVC_MAXPAYLOAD_LIMIT") { return Some(PayloadCap { limit, pct: 100, source: "env", periodic_dw: None, non_periodic_dw: None, }); } let mut periodic = read_fifo_min("/sys/module/dwc2/parameters/g_tx_fifo_size").map(|value| (value, "dwc2.params")); let mut non_periodic = read_fifo_min("/sys/module/dwc2/parameters/g_np_tx_fifo_size") .map(|value| (value, "dwc2.params")); if periodic.is_none() || non_periodic.is_none() { if let Some((periodic_debug, non_periodic_debug)) = read_debugfs_fifos() { if periodic.is_none() { periodic = periodic_debug.map(|value| (value, "debugfs.params")); } if non_periodic.is_none() { non_periodic = non_periodic_debug.map(|value| (value, "debugfs.params")); } } } let periodic_dw = periodic.map(|(value, _)| value); let non_periodic_dw = non_periodic.map(|(value, _)| value); let (fifo_dw, source) = if bulk { if let Some((np, src)) = non_periodic { (np, src) } else if let Some((periodic_value, src)) = periodic { (periodic_value, src) } else { return None; } } else if let Some((periodic_value, src)) = periodic { (periodic_value, src) } else if let Some((np, src)) = non_periodic { (np, src) } else { return None; }; let pct = env_u32("LESAVKA_UVC_LIMIT_PCT", 95).clamp(1, 100); let fifo_bytes = fifo_dw.saturating_mul(4); let limit = fifo_bytes.saturating_mul(pct) / 100; if limit == 0 { return None; } Some(PayloadCap { limit, pct, source, periodic_dw, non_periodic_dw, }) } pub(crate) fn read_fifo_min(path: &str) -> Option { let raw = std::fs::read_to_string(path).ok()?; raw.split(|c: char| c == ',' || c.is_whitespace()) .filter_map(|value| value.trim().parse::().ok()) .filter(|value| *value > 0) .min() } pub(crate) fn read_debugfs_fifos() -> Option<(Option, Option)> { let udc = std::fs::read_dir("/sys/class/udc") .ok()? .filter_map(|entry| entry.ok()) .filter_map(|entry| entry.file_name().into_string().ok()) .next()?; let path = format!("/sys/kernel/debug/usb/{udc}/params"); let text = std::fs::read_to_string(path).ok()?; let mut periodic: Option = None; let mut non_periodic: Option = None; for line in text.lines() { let mut parts = line.splitn(2, ':'); let key = match parts.next() { Some(value) => value.trim(), None => continue, }; let value = match parts.next().and_then(|raw| raw.trim().parse::().ok()) { Some(value) => value, None => continue, }; if key == "g_np_tx_fifo_size" { non_periodic = Some(value); } else if key.starts_with("g_tx_fifo_size[") && value > 0 { periodic = Some(match periodic { Some(previous) => previous.min(value), None => value, }); } } if periodic.is_none() && non_periodic.is_none() { None } else { Some((periodic, non_periodic)) } } #[cfg(test)] #[path = "tests/model.rs"] mod tests;