// lesavka-uvc - minimal UVC control handler for the gadget node. use anyhow::{Context, Result}; use std::env; use std::fs::{File, OpenOptions}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsRawFd, RawFd}; 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; 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 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; 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; const V4L2_BUF_TYPE_VIDEO_OUTPUT: u32 = 2; const V4L2_MEMORY_MMAP: u32 = 1; const V4L2_FIELD_NONE: u32 = 1; const V4L2_PIX_FMT_MJPEG: u32 = u32::from_le_bytes(*b"MJPG"); const MAX_MJPEG_FRAME_BYTES: usize = 1024 * 1024; const EMPTY_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9]; #[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], } #[repr(C)] #[derive(Clone, Copy)] struct V4l2PixFormat { width: u32, height: u32, pixelformat: u32, field: u32, bytesperline: u32, sizeimage: u32, colorspace: u32, priv_: u32, flags: u32, ycbcr_enc: u32, quantization: u32, xfer_func: u32, } #[repr(C)] union V4l2FormatUnion { pix: V4l2PixFormat, raw_data: [u8; 200], } #[repr(C)] struct V4l2Format { type_: u32, // v4l2_format's union is 8-byte aligned on aarch64; this pad keeps // our ioctl request size at the kernel's expected 208 bytes. _padding: u32, fmt: V4l2FormatUnion, } #[repr(C)] #[derive(Clone, Copy)] struct V4l2RequestBuffers { count: u32, type_: u32, memory: u32, capabilities: u32, flags: u8, reserved: [u8; 3], } #[repr(C)] #[derive(Clone, Copy)] struct V4l2Timecode { type_: u32, flags: u32, frames: u8, seconds: u8, minutes: u8, hours: u8, userbits: [u8; 4], } #[repr(C)] union V4l2BufferM { offset: u32, userptr: libc::c_ulong, planes: *mut libc::c_void, fd: i32, } #[repr(C)] union V4l2BufferRequest { request_fd: i32, reserved: u32, } #[repr(C)] struct V4l2Buffer { index: u32, type_: u32, bytesused: u32, flags: u32, field: u32, timestamp: libc::timeval, timecode: V4l2Timecode, sequence: u32, memory: u32, m: V4l2BufferM, length: u32, reserved2: u32, request: V4l2BufferRequest, } #[derive(Clone, Copy)] struct UvcConfig { width: u32, height: u32, fps: u32, interval: u32, max_packet: u32, frame_size: u32, } struct PayloadCap { limit: u32, pct: u32, source: &'static str, periodic_dw: Option, non_periodic_dw: Option, } 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], cfg_snapshot: Option, } #[derive(Clone, Copy)] struct PendingRequest { interface: u8, selector: u8, expected_len: usize, } #[derive(Clone, Copy)] struct UvcInterfaces { control: u8, streaming: u8, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct ConfigfsSnapshot { width: u32, height: u32, default_interval: u32, frame_interval: u32, maxpacket: u32, maxburst: u32, } struct MmapBuffer { ptr: *mut u8, len: usize, } struct UvcVideoStream { fd: RawFd, buffers: Vec, frame_path: std::path::PathBuf, latest_frame: Vec, streaming: bool, } impl UvcVideoStream { fn new(fd: RawFd) -> Self { Self { fd, buffers: Vec::new(), frame_path: frame_spool_path(), latest_frame: EMPTY_MJPEG_FRAME.to_vec(), streaming: false, } } fn start(&mut self, cfg: UvcConfig) -> Result<()> { self.stop(); self.set_format(cfg)?; self.request_buffers(4)?; for index in 0..self.buffers.len() { self.queue_buffer(index as u32)?; } let req = ioctl_write::(b'V', 18); let mut type_ = V4L2_BUF_TYPE_VIDEO_OUTPUT as libc::c_int; let rc = unsafe { libc::ioctl(self.fd, req, &mut type_) }; if rc < 0 { return Err(std::io::Error::last_os_error()).context("VIDIOC_STREAMON"); } self.streaming = true; eprintln!( "[lesavka-uvc] video stream started with {} buffers; frame_path={}", self.buffers.len(), self.frame_path.display() ); Ok(()) } fn stop(&mut self) { if self.streaming { let req = ioctl_write::(b'V', 19); let mut type_ = V4L2_BUF_TYPE_VIDEO_OUTPUT as libc::c_int; let rc = unsafe { libc::ioctl(self.fd, req, &mut type_) }; if rc < 0 { eprintln!( "[lesavka-uvc] VIDIOC_STREAMOFF failed: {}", std::io::Error::last_os_error() ); } self.streaming = false; } if !self.buffers.is_empty() { for buffer in self.buffers.drain(..) { unsafe { libc::munmap(buffer.ptr.cast(), buffer.len); } } let _ = self.request_buffers(0); } } fn pump(&mut self) -> Result<()> { if !self.streaming { return Ok(()); } loop { let mut buf = empty_v4l2_buffer(); let req = ioctl_readwrite::(b'V', 17); let rc = unsafe { libc::ioctl(self.fd, req, &mut buf) }; if rc < 0 { let err = std::io::Error::last_os_error(); match err.raw_os_error() { Some(libc::EAGAIN) | Some(libc::EINTR) => return Ok(()), _ => return Err(err).context("VIDIOC_DQBUF"), } } self.queue_buffer(buf.index)?; } } fn set_format(&self, cfg: UvcConfig) -> Result<()> { let mut fmt = V4l2Format { type_: V4L2_BUF_TYPE_VIDEO_OUTPUT, _padding: 0, fmt: V4l2FormatUnion { pix: V4l2PixFormat { width: cfg.width, height: cfg.height, pixelformat: V4L2_PIX_FMT_MJPEG, field: V4L2_FIELD_NONE, bytesperline: 0, sizeimage: cfg.frame_size.max(cfg.width * cfg.height), colorspace: 8, priv_: 0, flags: 0, ycbcr_enc: 0, quantization: 0, xfer_func: 0, }, }, }; let req = ioctl_readwrite::(b'V', 5); let rc = unsafe { libc::ioctl(self.fd, req, &mut fmt) }; if rc < 0 { return Err(std::io::Error::last_os_error()).context("VIDIOC_S_FMT"); } Ok(()) } fn request_buffers(&mut self, count: u32) -> Result<()> { let mut rb = V4l2RequestBuffers { count, type_: V4L2_BUF_TYPE_VIDEO_OUTPUT, memory: V4L2_MEMORY_MMAP, capabilities: 0, flags: 0, reserved: [0; 3], }; let req = ioctl_readwrite::(b'V', 8); let rc = unsafe { libc::ioctl(self.fd, req, &mut rb) }; if rc < 0 { return Err(std::io::Error::last_os_error()).context("VIDIOC_REQBUFS"); } if count == 0 { return Ok(()); } self.buffers.clear(); for index in 0..rb.count { let mut buf = empty_v4l2_buffer(); buf.index = index; let req = ioctl_readwrite::(b'V', 9); let rc = unsafe { libc::ioctl(self.fd, req, &mut buf) }; if rc < 0 { return Err(std::io::Error::last_os_error()).context("VIDIOC_QUERYBUF"); } let offset = unsafe { buf.m.offset }; let ptr = unsafe { libc::mmap( std::ptr::null_mut(), buf.length as usize, libc::PROT_READ | libc::PROT_WRITE, libc::MAP_SHARED, self.fd, offset as libc::off_t, ) }; if ptr == libc::MAP_FAILED { return Err(std::io::Error::last_os_error()).context("mmap UVC buffer"); } self.buffers.push(MmapBuffer { ptr: ptr.cast(), len: buf.length as usize, }); } Ok(()) } fn queue_buffer(&mut self, index: u32) -> Result<()> { self.refresh_latest_frame(); let Some(buffer) = self.buffers.get(index as usize) else { return Ok(()); }; let bytes = self.latest_frame.len().min(buffer.len); if bytes > 0 { unsafe { std::ptr::copy_nonoverlapping(self.latest_frame.as_ptr(), buffer.ptr, bytes); } } else { unsafe { std::ptr::write_bytes(buffer.ptr, 0, buffer.len); } } let mut buf = empty_v4l2_buffer(); buf.index = index; buf.bytesused = bytes as u32; buf.length = buffer.len as u32; let req = ioctl_readwrite::(b'V', 15); let rc = unsafe { libc::ioctl(self.fd, req, &mut buf) }; if rc < 0 { return Err(std::io::Error::last_os_error()).context("VIDIOC_QBUF"); } Ok(()) } fn refresh_latest_frame(&mut self) { if let Ok(frame) = std::fs::read(&self.frame_path) && !frame.is_empty() && frame.len() <= MAX_MJPEG_FRAME_BYTES { self.latest_frame = frame; } } } impl Drop for UvcVideoStream { fn drop(&mut self) { self.stop(); } } fn empty_v4l2_buffer() -> V4l2Buffer { V4l2Buffer { index: 0, type_: V4L2_BUF_TYPE_VIDEO_OUTPUT, bytesused: 0, flags: 0, field: V4L2_FIELD_NONE, timestamp: libc::timeval { tv_sec: 0, tv_usec: 0, }, timecode: V4l2Timecode { type_: 0, flags: 0, frames: 0, seconds: 0, minutes: 0, hours: 0, userbits: [0; 4], }, sequence: 0, memory: V4L2_MEMORY_MMAP, m: V4l2BufferM { offset: 0 }, length: 0, reserved2: 0, request: V4l2BufferRequest { reserved: 0 }, } } fn frame_spool_path() -> std::path::PathBuf { env::var("LESAVKA_UVC_FRAME_PATH") .map(std::path::PathBuf::from) .unwrap_or_else(|_| std::path::PathBuf::from("/run/lesavka-uvc-frame.mjpg")) } fn main() -> Result<()> { let (dev, cfg) = parse_args()?; let _singleton = acquire_singleton_lock()?; 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(); eprintln!("[lesavka-uvc] nonblock=1"); 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 = match open_with_retry(&dev) { Ok(file) => file, Err(err) => { eprintln!("[lesavka-uvc] open failed: {err:#}"); thread::sleep(Duration::from_millis(250)); continue; } }; 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); if let Err(err) = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_SETUP) .and_then(|_| subscribe_event(fd, vidioc_subscribe, UVC_EVENT_DATA)) { if should_retry_uvc_error(&err) { eprintln!("[lesavka-uvc] subscribe failed: {err:#}"); thread::sleep(Duration::from_millis(250)); continue; } return Err(err); } let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_CONNECT).map_err(|err| { eprintln!("[lesavka-uvc] connect-subscribe failed: {err:#}"); }); let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_DISCONNECT).map_err(|err| { eprintln!("[lesavka-uvc] disconnect-subscribe failed: {err:#}"); }); let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_STREAMON).map_err(|err| { eprintln!("[lesavka-uvc] streamon-subscribe failed: {err:#}"); }); let _ = subscribe_event(fd, vidioc_subscribe, UVC_EVENT_STREAMOFF).map_err(|err| { eprintln!("[lesavka-uvc] streamoff-subscribe failed: {err:#}"); }); let mut state = UvcState::new(cfg); let mut pending: Option = None; let mut video = UvcVideoStream::new(fd); loop { if let Err(err) = video.pump() { eprintln!("[lesavka-uvc] video pump failed: {err:#}"); } 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"); video.stop(); 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"); video.stop(); } UVC_EVENT_STREAMON => { eprintln!("[lesavka-uvc] stream on"); if let Err(err) = video.start(state.cfg) { eprintln!("[lesavka-uvc] stream start failed: {err:#}"); } } UVC_EVENT_STREAMOFF => { eprintln!("[lesavka-uvc] stream off"); video.stop(); } 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 should_retry_uvc_error(err: &anyhow::Error) -> bool { err.chain() .find_map(|cause| { cause .downcast_ref::() .and_then(|error| error.raw_os_error()) }) .is_some_and(|code| { matches!( code, libc::ENOENT | libc::ENODEV | libc::EIO | libc::EBADF | libc::EAGAIN ) }) } 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(), _ if arg.starts_with('/') => 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); 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 ); } } 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 _profile_hint = (cfg.width, cfg.height, cfg.fps); 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, } } } 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 read_only = uvc_control_read_only(); for attempt in 1..=200 { let mut opts = OpenOptions::new(); opts.read(true); if !read_only { opts.write(true); } opts.custom_flags(libc::O_NONBLOCK); match opts.open(path) { Ok(f) => { let mode = if read_only { "ro" } else { "rw" }; eprintln!("[lesavka-uvc] opened {path} mode={mode} (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 uvc_control_read_only() -> bool { env::var("LESAVKA_UVC_CONTROL_READ_ONLY") .ok() .map(|value| { let trimmed = value.trim(); !(trimmed.eq_ignore_ascii_case("0") || trimmed.eq_ignore_ascii_case("false") || trimmed.eq_ignore_ascii_case("no") || trimmed.eq_ignore_ascii_case("off")) }) .unwrap_or(true) } fn acquire_singleton_lock() -> Result { let path = env::var("LESAVKA_UVC_LOCK_PATH").unwrap_or_else(|_| "/run/lesavka-uvc.lock".to_string()); let file = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(&path) .with_context(|| format!("open singleton lock {path}"))?; let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }; if rc < 0 { let err = std::io::Error::last_os_error(); match err.raw_os_error() { Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN => { eprintln!("[lesavka-uvc] another helper already owns {path}; exiting"); std::process::exit(0); } _ => return Err(err).with_context(|| format!("lock singleton {path}")), } } Ok(file) } 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 { 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; } if len > UVC_DATA_SIZE { eprintln!( "[lesavka-uvc] SET_CUR too large len={} (max={}); stalling", len, UVC_DATA_SIZE ); let _ = send_stall(fd, uvc_send_response); return; } *pending = Some(PendingRequest { interface, selector, expected_len: len, }); let payload = vec![0u8; len]; 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 selector == UVC_VS_PROBE_CONTROL { interfaces.streaming } else if selector == UVC_VS_COMMIT_CONTROL || selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL { if raw == interfaces.control { interfaces.control } else { interfaces.streaming } } else { raw }; if debug && mapped != raw { eprintln!("[lesavka-uvc] remapped interface {raw} -> {mapped} for selector {selector}"); } 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 { if debug { eprintln!("[lesavka-uvc] DATA with no pending request; ignoring"); } return; }; if data.length < 0 { return; } let len = data.length as usize; if debug && p.expected_len != 0 && len != p.expected_len { eprintln!( "[lesavka-uvc] DATA len mismatch: expected={} got={}", p.expected_len, len ); } 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 ); log_configfs_snapshot(state, "probe"); } } 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 ); log_configfs_snapshot(state, "commit"); } } } } 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]), // 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 => { 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![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]) } } _ => 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] = 0x03; // bmFramingInfo: FID + EOF supported buf[31] = 0x01; // bPreferedVersion buf[32] = 0x01; // bMinVersion buf[33] = 0x01; // bMaxVersion } 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 env_u32_opt(name: &str) -> Option { env::var(name).ok().and_then(|v| v.parse::().ok()) } fn read_u32_file(path: &str) -> Option { std::fs::read_to_string(path) .ok() .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 { 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], ]) } 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(|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()) && let Some((p, np)) = read_debugfs_fifos() { if periodic.is_none() { periodic = p.map(|v| (v, "debugfs.params")); } if non_periodic.is_none() { non_periodic = np.map(|v| (v, "debugfs.params")); } } let periodic_dw = periodic.map(|(v, _)| v); let non_periodic_dw = non_periodic.map(|(v, _)| v); let (fifo_dw, source) = if bulk { if let Some((np, src)) = non_periodic { (np, src) } else if let Some((p, src)) = periodic { (p, src) } else { return None; } } else if let Some((p, src)) = periodic { (p, src) } else if let Some((np, src)) = non_periodic { (np, src) } else { return None; }; let mut pct = env_u32("LESAVKA_UVC_LIMIT_PCT", 95); if pct == 0 { pct = 1; } else if pct > 100 { pct = 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, }) } 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(|v| v.trim().parse::().ok()) .filter(|v| *v > 0) .min() } fn read_debugfs_fifos() -> Option<(Option, Option)> { let udc = std::fs::read_dir("/sys/class/udc") .ok()? .filter_map(|e| e.ok()) .filter_map(|e| e.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(v) => v.trim(), None => continue, }; let val = match parts.next().and_then(|v| v.trim().parse::().ok()) { Some(v) => v, None => continue, }; if key == "g_np_tx_fifo_size" { non_periodic = Some(val); } else if key.starts_with("g_tx_fifo_size[") && val > 0 { periodic = Some(match periodic { Some(prev) => prev.min(val), None => val, }); } } if periodic.is_none() && non_periodic.is_none() { None } else { Some((periodic, non_periodic)) } } const IOC_NRBITS: u8 = 8; const IOC_TYPEBITS: u8 = 8; const IOC_SIZEBITS: u8 = 14; 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 ioctl_readwrite(type_: u8, nr: u8) -> libc::c_ulong { ioc( IOC_READ | 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 }