diff --git a/scripts/daemon/lesavka-core.sh b/scripts/daemon/lesavka-core.sh index e7f87db..2cb37f0 100644 --- a/scripts/daemon/lesavka-core.sh +++ b/scripts/daemon/lesavka-core.sh @@ -9,6 +9,11 @@ log() { printf '[lesavka-core] %s\n' "$*"; } G=/sys/kernel/config/usb_gadget/lesavka +if [[ -r /etc/lesavka/uvc.env ]]; then + # shellcheck disable=SC1091 + source /etc/lesavka/uvc.env +fi + find_udc() { ls /sys/class/udc 2>/dev/null | head -n1 || true } diff --git a/server/src/bin/lesavka-uvc.rs b/server/src/bin/lesavka-uvc.rs index db72314..31ae21b 100644 --- a/server/src/bin/lesavka-uvc.rs +++ b/server/src/bin/lesavka-uvc.rs @@ -24,6 +24,8 @@ const UVC_EVENT_DATA: u32 = V4L2_EVENT_PRIVATE_START + 5; const UVC_STRING_CONTROL_IDX: u8 = 0; const UVC_STRING_STREAMING_IDX: u8 = 1; +const CONFIGFS_UVC_BASE: &str = "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0"; + const USB_DIR_IN: u8 = 0x80; const UVC_SET_CUR: u8 = 0x01; @@ -105,6 +107,7 @@ struct UvcState { default: [u8; STREAM_CTRL_SIZE_MAX], probe: [u8; STREAM_CTRL_SIZE_MAX], commit: [u8; STREAM_CTRL_SIZE_MAX], + cfg_snapshot: Option, } #[derive(Clone, Copy)] @@ -120,6 +123,16 @@ struct UvcInterfaces { streaming: u8, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct ConfigfsSnapshot { + width: u32, + height: u32, + default_interval: u32, + frame_interval: u32, + maxpacket: u32, + maxburst: u32, +} + fn main() -> Result<()> { let (dev, cfg) = parse_args()?; let interfaces = load_interfaces(); @@ -219,10 +232,7 @@ fn main() -> Result<()> { let data = parse_request_data(event_bytes(&ev)); data_seen += 1; if debug || data_seen <= 10 { - eprintln!( - "[lesavka-uvc] data #{data_seen} len={}", - data.length - ); + eprintln!("[lesavka-uvc] data #{data_seen} len={}", data.length); } handle_data( fd, @@ -302,7 +312,7 @@ impl UvcConfig { ); } if let Some(cfg_max) = read_u32_file( - "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/streaming_maxpacket", + &format!("{CONFIGFS_UVC_BASE}/streaming_maxpacket"), ) { if max_packet > cfg_max { eprintln!( @@ -350,13 +360,18 @@ impl UvcState { default, probe: default, commit: default, + cfg_snapshot: None, } } } fn load_interfaces() -> UvcInterfaces { let control = env_u8("LESAVKA_UVC_CTRL_INTF") - .or_else(|| read_interface("/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/control/bInterfaceNumber")) + .or_else(|| { + read_interface( + "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/control/bInterfaceNumber", + ) + }) .unwrap_or(UVC_STRING_CONTROL_IDX); let streaming = env_u8("LESAVKA_UVC_STREAM_INTF") .or_else(|| read_interface("/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/streaming/bInterfaceNumber")) @@ -401,9 +416,8 @@ fn subscribe_event(fd: i32, req: libc::c_ulong, event: u32) -> Result<()> { }; let rc = unsafe { libc::ioctl(fd, req, &mut sub) }; if rc < 0 { - return Err(std::io::Error::last_os_error()).with_context(|| { - format!("subscribe event {event:#x} (fd={fd})") - }); + return Err(std::io::Error::last_os_error()) + .with_context(|| format!("subscribe event {event:#x} (fd={fd})")); } Ok(()) } @@ -420,14 +434,14 @@ fn handle_setup( let selector = (req.w_value >> 8) as u8; let interface_lo = (req.w_index & 0xff) as u8; let interface_hi = (req.w_index >> 8) as u8; - let interface_raw = if interface_hi == interfaces.streaming || interface_hi == interfaces.control - { - interface_hi - } else if interface_lo == interfaces.streaming || interface_lo == interfaces.control { - interface_lo - } else { - interface_hi - }; + let interface_raw = + if interface_hi == interfaces.streaming || interface_hi == interfaces.control { + interface_hi + } else if interface_lo == interfaces.streaming || interface_lo == interfaces.control { + interface_lo + } else { + interface_hi + }; let is_in = (req.b_request_type & USB_DIR_IN) != 0; if matches!(selector, UVC_VS_PROBE_CONTROL | UVC_VS_COMMIT_CONTROL) { maybe_update_ctrl_len(state, req.w_length, debug); @@ -435,11 +449,23 @@ fn handle_setup( let interface = map_interface(interface_raw, selector, interfaces, debug); if !is_in && req.b_request == UVC_SET_CUR { + let len = req.w_length as usize; + if interface == interfaces.control { + // Accept SET_CUR on VC controls by replying with a zeroed payload. + let payload = vec![0u8; len.min(UVC_DATA_SIZE)]; + let _ = send_response(fd, uvc_send_response, &payload); + if debug { + eprintln!( + "[lesavka-uvc] VC SET_CUR ack len={} iface={} sel={}", + req.w_length, interface, selector + ); + } + return; + } if interface != interfaces.streaming { let _ = send_stall(fd, uvc_send_response); return; } - let len = req.w_length as usize; if len > UVC_DATA_SIZE { eprintln!( "[lesavka-uvc] SET_CUR too large len={} (max={}); stalling", @@ -568,10 +594,7 @@ fn handle_data( let payload = read_le32(slice, 22); eprintln!( "[lesavka-uvc] data ctrl fmt={} frame={} interval={} payload={}", - slice[2], - slice[3], - interval, - payload + slice[2], slice[3], interval, payload ); } @@ -586,9 +609,9 @@ fn handle_data( let payload = read_le32(&state.probe, 22); eprintln!( "[lesavka-uvc] probe set interval={} payload={}", - interval, - payload + interval, payload ); + log_configfs_snapshot(state, "probe"); } } else { state.commit = sanitized; @@ -597,9 +620,9 @@ fn handle_data( let payload = read_le32(&state.commit, 22); eprintln!( "[lesavka-uvc] commit set interval={} payload={}", - interval, - payload + interval, payload ); + log_configfs_snapshot(state, "commit"); } } } @@ -624,11 +647,7 @@ fn build_in_response( Some(adjust_length(payload, w_length)) } -fn build_streaming_response( - state: &UvcState, - selector: u8, - request: u8, -) -> Option> { +fn build_streaming_response(state: &UvcState, selector: u8, request: u8) -> Option> { let current = match selector { UVC_VS_PROBE_CONTROL => state.probe, UVC_VS_COMMIT_CONTROL => state.commit, @@ -636,7 +655,7 @@ fn build_streaming_response( }; match request { - UVC_GET_INFO => Some(vec![0x03]), + UVC_GET_INFO => Some(vec![0x03]), // support GET/SET UVC_GET_LEN => Some((state.ctrl_len as u16).to_le_bytes().to_vec()), UVC_GET_CUR => Some(current[..state.ctrl_len].to_vec()), UVC_GET_MIN | UVC_GET_MAX | UVC_GET_DEF | UVC_GET_RES => { @@ -648,12 +667,14 @@ fn build_streaming_response( fn build_control_response(selector: u8, request: u8) -> Option> { match request { - UVC_GET_INFO => Some(vec![0x01]), + UVC_GET_INFO => Some(vec![0x03]), // indicate both GET/SET supported UVC_GET_LEN => Some(1u16.to_le_bytes().to_vec()), UVC_GET_CUR | UVC_GET_MIN | UVC_GET_MAX | UVC_GET_DEF | UVC_GET_RES => { if selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL { + // reset error code to “no error” Some(vec![0x00]) } else { + // simple 1‑byte placeholder for unhandled VC controls Some(vec![0x00]) } } @@ -661,10 +682,7 @@ fn build_control_response(selector: u8, request: u8) -> Option> { } } -fn sanitize_streaming_control( - data: &[u8], - state: &UvcState, -) -> [u8; STREAM_CTRL_SIZE_MAX] { +fn sanitize_streaming_control(data: &[u8], state: &UvcState) -> [u8; STREAM_CTRL_SIZE_MAX] { let mut out = state.default; if data.len() >= STREAM_CTRL_SIZE_11 { let format_index = data[2]; @@ -736,10 +754,10 @@ fn build_streaming_control(cfg: &UvcConfig, ctrl_len: usize) -> [u8; STREAM_CTRL 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] = 0; - buf[31] = 0; - buf[32] = 0; - buf[33] = 0; + buf[30] = 0x03; // bmFramingInfo: FID + EOF supported + buf[31] = 0x01; // bPreferedVersion + buf[32] = 0x01; // bMinVersion + buf[33] = 0x01; // bMaxVersion } buf @@ -795,6 +813,52 @@ fn read_u32_file(path: &str) -> Option { .and_then(|v| v.trim().parse::().ok()) } +fn read_u32_first(path: &str) -> Option { + std::fs::read_to_string(path) + .ok() + .and_then(|v| v.split_whitespace().next()?.parse::().ok()) +} + +fn read_configfs_snapshot() -> Option { + let width = read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming/mjpeg/m/720p/wWidth"))?; + let height = read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming/mjpeg/m/720p/wHeight"))?; + let default_interval = + read_u32_file(&format!("{CONFIGFS_UVC_BASE}/streaming/mjpeg/m/720p/dwDefaultFrameInterval"))?; + let frame_interval = + read_u32_first(&format!("{CONFIGFS_UVC_BASE}/streaming/mjpeg/m/720p/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 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); +} + fn adjust_length(mut bytes: Vec, w_length: u16) -> Vec { let want = (w_length as usize).min(UVC_DATA_SIZE); if bytes.len() > want { @@ -820,7 +884,12 @@ fn write_le32(dst: &mut [u8], val: u32) { } fn read_le32(src: &[u8], offset: usize) -> u32 { - u32::from_le_bytes([src[offset], src[offset + 1], src[offset + 2], src[offset + 3]]) + u32::from_le_bytes([ + src[offset], + src[offset + 1], + src[offset + 2], + src[offset + 3], + ]) } fn compute_payload_cap(bulk: bool) -> Option { @@ -834,10 +903,10 @@ fn compute_payload_cap(bulk: bool) -> Option { }); } - let mut periodic = read_fifo_min("/sys/module/dwc2/parameters/g_tx_fifo_size") - .map(|v| (v, "dwc2.params")); - let mut non_periodic = read_fifo_min("/sys/module/dwc2/parameters/g_np_tx_fifo_size") - .map(|v| (v, "dwc2.params")); + let mut periodic = + read_fifo_min("/sys/module/dwc2/parameters/g_tx_fifo_size").map(|v| (v, "dwc2.params")); + let mut non_periodic = + read_fifo_min("/sys/module/dwc2/parameters/g_np_tx_fifo_size").map(|v| (v, "dwc2.params")); if periodic.is_none() || non_periodic.is_none() { if let Some((p, np)) = read_debugfs_fifos() { if periodic.is_none() {