// lesavka-uvc - minimal UVC control handler for the gadget node. use anyhow::{Context, Result}; use std::env; use std::fs::OpenOptions; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::AsRawFd; use std::thread; use std::time::Duration; const STREAM_CTRL_SIZE_11: usize = 26; const STREAM_CTRL_SIZE_15: usize = 34; const STREAM_CTRL_SIZE_MAX: usize = STREAM_CTRL_SIZE_15; const UVC_DATA_SIZE: usize = 60; const V4L2_EVENT_PRIVATE_START: u32 = 0x0800_0000; const UVC_EVENT_CONNECT: u32 = V4L2_EVENT_PRIVATE_START + 0; const UVC_EVENT_DISCONNECT: u32 = V4L2_EVENT_PRIVATE_START + 1; const UVC_EVENT_STREAMON: u32 = V4L2_EVENT_PRIVATE_START + 2; const UVC_EVENT_STREAMOFF: u32 = V4L2_EVENT_PRIVATE_START + 3; const UVC_EVENT_SETUP: u32 = V4L2_EVENT_PRIVATE_START + 4; 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 USB_DIR_IN: u8 = 0x80; const UVC_SET_CUR: u8 = 0x01; const UVC_GET_CUR: u8 = 0x81; const UVC_GET_MIN: u8 = 0x82; const UVC_GET_MAX: u8 = 0x83; const UVC_GET_RES: u8 = 0x84; const UVC_GET_LEN: u8 = 0x85; const UVC_GET_INFO: u8 = 0x86; const UVC_GET_DEF: u8 = 0x87; const UVC_VS_PROBE_CONTROL: u8 = 0x01; const UVC_VS_COMMIT_CONTROL: u8 = 0x02; const UVC_VC_REQUEST_ERROR_CODE_CONTROL: u8 = 0x02; #[repr(C)] struct V4l2EventSubscription { type_: u32, id: u32, flags: u32, reserved: [u32; 5], } #[repr(C)] union V4l2EventUnion { data: [u8; 64], _align: u64, } #[repr(C)] struct V4l2Event { type_: u32, u: V4l2EventUnion, pending: u32, sequence: u32, timestamp: libc::timespec, id: u32, reserved: [u32; 8], } #[repr(C)] #[derive(Clone, Copy)] struct UsbCtrlRequest { b_request_type: u8, b_request: u8, w_value: u16, w_index: u16, w_length: u16, } #[repr(C)] #[derive(Clone, Copy)] struct UvcRequestData { length: i32, data: [u8; UVC_DATA_SIZE], } #[derive(Clone, Copy)] struct UvcConfig { width: u32, height: u32, fps: u32, interval: u32, max_packet: u32, frame_size: u32, } struct UvcState { cfg: UvcConfig, ctrl_len: usize, default: [u8; STREAM_CTRL_SIZE_MAX], probe: [u8; STREAM_CTRL_SIZE_MAX], commit: [u8; STREAM_CTRL_SIZE_MAX], } #[derive(Clone, Copy)] struct PendingRequest { interface: u8, selector: u8, } #[derive(Clone, Copy)] struct UvcInterfaces { control: u8, streaming: u8, } fn main() -> Result<()> { let (dev, cfg) = parse_args()?; let interfaces = load_interfaces(); eprintln!("[lesavka-uvc] starting (dev={dev})"); eprintln!( "[lesavka-uvc] interfaces control={} streaming={}", interfaces.control, interfaces.streaming ); let debug = env::var("LESAVKA_UVC_DEBUG").is_ok(); let nonblock = env::var("LESAVKA_UVC_BLOCKING").is_err(); eprintln!("[lesavka-uvc] nonblock={}", if nonblock { 1 } else { 0 }); let mut setup_seen: u64 = 0; let mut data_seen: u64 = 0; let mut dq_err_seen: u64 = 0; let mut dq_err_last: Option = None; loop { let file = open_with_retry(&dev)?; let fd = file.as_raw_fd(); let vidioc_subscribe = ioctl_write::(b'V', 90); let vidioc_dqevent = ioctl_read::(b'V', 89); let uvc_send_response = ioctl_write::(b'U', 1); subscribe_event(fd, vidioc_subscribe, UVC_EVENT_SETUP)?; subscribe_event(fd, vidioc_subscribe, UVC_EVENT_DATA)?; let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_CONNECT); let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_DISCONNECT); let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_STREAMON); let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_STREAMOFF); let mut state = UvcState::new(cfg); let mut pending: Option = None; loop { let mut ev = unsafe { std::mem::zeroed::() }; let rc = unsafe { libc::ioctl(fd, vidioc_dqevent, &mut ev) }; if rc < 0 { let err = std::io::Error::last_os_error(); match err.raw_os_error() { Some(libc::EAGAIN) | Some(libc::EINTR) | Some(libc::ENOENT) => { if debug { let code = err.raw_os_error(); if dq_err_seen < 10 || code != dq_err_last { eprintln!("[lesavka-uvc] dqevent idle: {err}"); dq_err_seen += 1; dq_err_last = code; } } thread::sleep(Duration::from_millis(10)); continue; } Some(libc::ENODEV) | Some(libc::EBADF) | Some(libc::EIO) => { eprintln!("[lesavka-uvc] device reset ({err}); reopening"); break; } _ => { eprintln!("[lesavka-uvc] dqevent failed: {err}"); thread::sleep(Duration::from_millis(100)); continue; } } } match ev.type_ { UVC_EVENT_CONNECT => { let speed = u32::from_le_bytes(event_bytes(&ev)[0..4].try_into().unwrap()); eprintln!("[lesavka-uvc] UVC connect (speed={speed})"); } UVC_EVENT_DISCONNECT => eprintln!("[lesavka-uvc] UVC disconnect"), UVC_EVENT_STREAMON => eprintln!("[lesavka-uvc] stream on"), UVC_EVENT_STREAMOFF => eprintln!("[lesavka-uvc] stream off"), UVC_EVENT_SETUP => { let req = parse_ctrl_request(event_bytes(&ev)); setup_seen += 1; if debug || setup_seen <= 10 { eprintln!( "[lesavka-uvc] setup #{setup_seen} rt=0x{:02x} rq=0x{:02x} val=0x{:04x} idx=0x{:04x} len={}", req.b_request_type, req.b_request, req.w_value, req.w_index, req.w_length ); } handle_setup( fd, uvc_send_response, &mut state, &mut pending, interfaces, req, debug, ); } UVC_EVENT_DATA => { 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 ); } handle_data( fd, uvc_send_response, &mut state, &mut pending, interfaces, data, debug, ); } _ => { if debug { eprintln!("[lesavka-uvc] event type=0x{:08x}", ev.type_); } } } } } } fn parse_args() -> Result<(String, UvcConfig)> { let mut args = env::args().skip(1); let mut dev: Option = None; while let Some(arg) = args.next() { match arg.as_str() { "--device" | "-d" => dev = args.next(), _ => dev = Some(arg), } } let dev = dev .or_else(|| env::var("LESAVKA_UVC_DEV").ok()) .context("missing --device (or LESAVKA_UVC_DEV)")?; Ok((dev, UvcConfig::from_env())) } impl UvcConfig { 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", 25).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); if env::var("LESAVKA_UVC_BULK").is_ok() { max_packet = max_packet.min(512); } else { max_packet = 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 { 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, } } } 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")) .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")) .unwrap_or(UVC_STRING_STREAMING_IDX); UvcInterfaces { control, streaming } } fn read_interface(path: &str) -> Option { std::fs::read_to_string(path) .ok() .and_then(|v| v.trim().parse::().ok()) } fn open_with_retry(path: &str) -> Result { let nonblock = env::var("LESAVKA_UVC_BLOCKING").is_err(); for attempt in 1..=200 { let mut opts = OpenOptions::new(); opts.read(true).write(true); if nonblock { opts.custom_flags(libc::O_NONBLOCK); } match opts.open(path) { Ok(f) => { eprintln!("[lesavka-uvc] opened {path} (attempt {attempt})"); return Ok(f); } Err(err) if err.raw_os_error() == Some(libc::ENOENT) => { thread::sleep(Duration::from_millis(50)); } Err(err) => return Err(err).with_context(|| format!("open {path}")), } } Err(anyhow::anyhow!("timeout opening {path}")) } fn subscribe_event(fd: i32, req: libc::c_ulong, event: u32) -> Result<()> { let mut sub = V4l2EventSubscription { type_: event, id: 0, flags: 0, reserved: [0; 5], }; 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})") }); } Ok(()) } fn handle_setup( fd: i32, uvc_send_response: libc::c_ulong, state: &mut UvcState, pending: &mut Option, interfaces: UvcInterfaces, req: UsbCtrlRequest, debug: bool, ) { 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 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); } let interface = map_interface(interface_raw, selector, interfaces, debug); if !is_in && req.b_request == UVC_SET_CUR { if interface != interfaces.streaming { let _ = send_stall(fd, uvc_send_response); return; } *pending = Some(PendingRequest { interface, selector }); let len = req.w_length as usize; let payload = vec![0u8; len.min(UVC_DATA_SIZE)]; let _ = send_response(fd, uvc_send_response, &payload); if debug { eprintln!( "[lesavka-uvc] SET_CUR queued len={} iface={} sel={}", req.w_length, interface, selector ); } return; } if !is_in { let _ = send_stall(fd, uvc_send_response); return; } let payload = build_in_response( state, interfaces, interface, selector, req.b_request, req.w_length, ); match payload { Some(bytes) => { if debug { eprintln!( "[lesavka-uvc] send IN response rq=0x{:02x} sel={} len={}", req.b_request, selector, bytes.len() ); } let _ = send_response(fd, uvc_send_response, &bytes); } None => { let _ = send_stall(fd, uvc_send_response); } } } fn map_interface(raw: u8, selector: u8, interfaces: UvcInterfaces, debug: bool) -> u8 { let mapped = if matches!(selector, UVC_VS_PROBE_CONTROL | UVC_VS_COMMIT_CONTROL) { interfaces.streaming } else if selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL { interfaces.control } else if raw == interfaces.streaming || raw == interfaces.control { raw } else { raw }; if debug && mapped != raw { eprintln!( "[lesavka-uvc] remapped interface {} -> {} for selector {selector}", raw, mapped ); } mapped } fn maybe_update_ctrl_len(state: &mut UvcState, w_length: u16, debug: bool) { let want = w_length as usize; if !(want == STREAM_CTRL_SIZE_11 || want == STREAM_CTRL_SIZE_15) { return; } if state.ctrl_len == want { return; } state.ctrl_len = want; state.default = build_streaming_control(&state.cfg, state.ctrl_len); state.probe = state.default; state.commit = state.default; if debug { eprintln!("[lesavka-uvc] ctrl_len set to {}", state.ctrl_len); } } fn handle_data( fd: i32, uvc_send_response: libc::c_ulong, state: &mut UvcState, pending: &mut Option, interfaces: UvcInterfaces, data: UvcRequestData, debug: bool, ) { let Some(p) = pending.take() else { return; }; if data.length < 0 { return; } let len = data.length as usize; let slice = &data.data[..len.min(data.data.len())]; if debug && slice.len() >= STREAM_CTRL_SIZE_11 { let interval = read_le32(slice, 4); let payload = read_le32(slice, 22); eprintln!( "[lesavka-uvc] data ctrl fmt={} frame={} interval={} payload={}", slice[2], slice[3], interval, payload ); } if p.interface == interfaces.streaming && matches!(p.selector, UVC_VS_PROBE_CONTROL | UVC_VS_COMMIT_CONTROL) { let sanitized = sanitize_streaming_control(slice, state); if p.selector == UVC_VS_PROBE_CONTROL { state.probe = sanitized; if debug { let interval = read_le32(&state.probe, 4); let payload = read_le32(&state.probe, 22); eprintln!( "[lesavka-uvc] probe set interval={} payload={}", interval, payload ); } } else { state.commit = sanitized; if debug { let interval = read_le32(&state.commit, 4); let payload = read_le32(&state.commit, 22); eprintln!( "[lesavka-uvc] commit set interval={} payload={}", interval, payload ); } } // No extra response required; ep0 data stage completion ends the control transfer. } } fn build_in_response( state: &UvcState, interfaces: UvcInterfaces, interface: u8, selector: u8, request: u8, w_length: u16, ) -> Option> { let payload = match interface { _ if interface == interfaces.streaming => { build_streaming_response(state, selector, request) } _ if interface == interfaces.control => build_control_response(selector, request), _ => None, }?; Some(adjust_length(payload, w_length)) } 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, _ => return None, }; match request { UVC_GET_INFO => Some(vec![0x03]), 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 => { Some(state.default[..state.ctrl_len].to_vec()) } _ => None, } } fn build_control_response(selector: u8, request: u8) -> Option> { match request { UVC_GET_INFO => Some(vec![0x01]), 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 { Some(vec![0x00]) } else { Some(vec![0x00]) } } _ => None, } } 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]; let frame_index = data[3]; let interval = read_le32(data, 4); let host_payload = read_le32(data, 22); if format_index == 1 { out[2] = 1; } if frame_index == 1 { out[3] = 1; } if interval != 0 { write_le32(&mut out[4..8], interval); } if host_payload > 0 { let payload = host_payload.min(state.cfg.max_packet); write_le32(&mut out[22..26], payload); } } out } fn send_response(fd: i32, req: libc::c_ulong, payload: &[u8]) -> Result<()> { let mut resp = UvcRequestData { length: payload.len() as i32, data: [0u8; UVC_DATA_SIZE], }; let n = payload.len().min(UVC_DATA_SIZE); resp.data[..n].copy_from_slice(&payload[..n]); let rc = unsafe { libc::ioctl(fd, req, &resp) }; if rc < 0 { let err = std::io::Error::last_os_error(); eprintln!("[lesavka-uvc] send_response failed: {err}"); return Err(err).context("UVCIOC_SEND_RESPONSE"); } Ok(()) } fn send_stall(fd: i32, req: libc::c_ulong) -> Result<()> { let resp = UvcRequestData { length: -1, data: [0u8; UVC_DATA_SIZE], }; let rc = unsafe { libc::ioctl(fd, req, &resp) }; if rc < 0 { let err = std::io::Error::last_os_error(); eprintln!("[lesavka-uvc] send_stall failed: {err}"); return Err(err).context("UVCIOC_SEND_RESPONSE(stall)"); } Ok(()) } 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); // bmHint: dwFrameInterval buf[2] = 1; // bFormatIndex buf[3] = 1; // bFrameIndex 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] = 0; buf[31] = 0; buf[32] = 0; buf[33] = 0; } buf } fn event_bytes(ev: &V4l2Event) -> [u8; 64] { unsafe { ev.u.data } } fn parse_ctrl_request(data: [u8; 64]) -> UsbCtrlRequest { UsbCtrlRequest { b_request_type: data[0], b_request: data[1], w_value: u16::from_le_bytes([data[2], data[3]]), w_index: u16::from_le_bytes([data[4], data[5]]), w_length: u16::from_le_bytes([data[6], data[7]]), } } fn parse_request_data(data: [u8; 64]) -> UvcRequestData { let length = i32::from_le_bytes([data[0], data[1], data[2], data[3]]); let mut out = [0u8; UVC_DATA_SIZE]; out.copy_from_slice(&data[4..64]); UvcRequestData { length, data: out } } 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, } } fn env_u32(name: &str, default: u32) -> u32 { env::var(name) .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(default) } fn env_u8(name: &str) -> Option { env::var(name).ok().and_then(|v| v.parse::().ok()) } 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 } fn write_le16(dst: &mut [u8], val: u16) { let bytes = val.to_le_bytes(); dst[0] = bytes[0]; dst[1] = bytes[1]; } 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]; } fn read_le32(src: &[u8], offset: usize) -> u32 { u32::from_le_bytes([src[offset], src[offset + 1], src[offset + 2], src[offset + 3]]) } const IOC_NRBITS: u8 = 8; const IOC_TYPEBITS: u8 = 8; const IOC_SIZEBITS: u8 = 14; const IOC_DIRBITS: u8 = 2; const IOC_NRSHIFT: u8 = 0; const IOC_TYPESHIFT: u8 = IOC_NRSHIFT + IOC_NRBITS; const IOC_SIZESHIFT: u8 = IOC_TYPESHIFT + IOC_TYPEBITS; const IOC_DIRSHIFT: u8 = IOC_SIZESHIFT + IOC_SIZEBITS; const IOC_READ: u8 = 2; const IOC_WRITE: u8 = 1; fn ioctl_read(type_: u8, nr: u8) -> libc::c_ulong { ioc(IOC_READ, type_, nr, std::mem::size_of::() as u16) } fn ioctl_write(type_: u8, nr: u8) -> libc::c_ulong { ioc(IOC_WRITE, type_, nr, std::mem::size_of::() as u16) } fn ioc(dir: u8, type_: u8, nr: u8, size: u16) -> libc::c_ulong { let dir = (dir as u32) << IOC_DIRSHIFT; let ty = (type_ as u32) << IOC_TYPESHIFT; let nr = (nr as u32) << IOC_NRSHIFT; let size = (size as u32) << IOC_SIZESHIFT; (dir | ty | nr | size) as libc::c_ulong }