2026-04-13 02:52:32 -03:00
|
|
|
|
// lesavka-uvc - minimal UVC control handler for the gadget node.
|
|
|
|
|
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
|
use std::env;
|
2026-04-28 21:00:56 -03:00
|
|
|
|
use std::fs::{File, OpenOptions};
|
2026-04-13 02:52:32 -03:00
|
|
|
|
use std::os::unix::fs::OpenOptionsExt;
|
2026-04-28 21:39:46 -03:00
|
|
|
|
use std::os::unix::io::{AsRawFd, RawFd};
|
|
|
|
|
|
use std::path::PathBuf;
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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;
|
2026-04-23 07:00:06 -03:00
|
|
|
|
const UVC_EVENT_CONNECT: u32 = V4L2_EVENT_PRIVATE_START;
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-04-28 21:39:46 -03:00
|
|
|
|
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];
|
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
|
#[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],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 21:39:46 -03:00
|
|
|
|
#[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,
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
|
#[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<u32>,
|
|
|
|
|
|
non_periodic_dw: Option<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],
|
|
|
|
|
|
cfg_snapshot: Option<ConfigfsSnapshot>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 21:39:46 -03:00
|
|
|
|
struct MmapBuffer {
|
|
|
|
|
|
ptr: *mut u8,
|
|
|
|
|
|
len: usize,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct UvcVideoStream {
|
|
|
|
|
|
fd: RawFd,
|
|
|
|
|
|
buffers: Vec<MmapBuffer>,
|
|
|
|
|
|
frame_path: PathBuf,
|
|
|
|
|
|
latest_frame: Vec<u8>,
|
|
|
|
|
|
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::<libc::c_int>(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::<libc::c_int>(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::<V4l2Buffer>(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,
|
|
|
|
|
|
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::<V4l2Format>(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::<V4l2RequestBuffers>(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::<V4l2Buffer>(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::<V4l2Buffer>(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) {
|
|
|
|
|
|
if !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() -> PathBuf {
|
|
|
|
|
|
env::var("LESAVKA_UVC_FRAME_PATH")
|
|
|
|
|
|
.map(PathBuf::from)
|
|
|
|
|
|
.unwrap_or_else(|_| PathBuf::from("/run/lesavka-uvc-frame.mjpg"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
|
fn main() -> Result<()> {
|
|
|
|
|
|
let (dev, cfg) = parse_args()?;
|
2026-04-28 21:00:56 -03:00
|
|
|
|
let _singleton = acquire_singleton_lock()?;
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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();
|
2026-04-28 21:39:46 -03:00
|
|
|
|
eprintln!("[lesavka-uvc] nonblock=1");
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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<i32> = None;
|
|
|
|
|
|
|
|
|
|
|
|
loop {
|
2026-04-20 02:28:16 -03:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-04-13 02:52:32 -03:00
|
|
|
|
let fd = file.as_raw_fd();
|
|
|
|
|
|
let vidioc_subscribe = ioctl_write::<V4l2EventSubscription>(b'V', 90);
|
|
|
|
|
|
let vidioc_dqevent = ioctl_read::<V4l2Event>(b'V', 89);
|
|
|
|
|
|
let uvc_send_response = ioctl_write::<UvcRequestData>(b'U', 1);
|
|
|
|
|
|
|
2026-04-20 02:28:16 -03:00
|
|
|
|
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:#}");
|
|
|
|
|
|
});
|
2026-04-13 02:52:32 -03:00
|
|
|
|
|
|
|
|
|
|
let mut state = UvcState::new(cfg);
|
|
|
|
|
|
let mut pending: Option<PendingRequest> = None;
|
2026-04-28 21:39:46 -03:00
|
|
|
|
let mut video = UvcVideoStream::new(fd);
|
2026-04-13 02:52:32 -03:00
|
|
|
|
|
|
|
|
|
|
loop {
|
2026-04-28 21:39:46 -03:00
|
|
|
|
if let Err(err) = video.pump() {
|
|
|
|
|
|
eprintln!("[lesavka-uvc] video pump failed: {err:#}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
|
let mut ev = unsafe { std::mem::zeroed::<V4l2Event>() };
|
|
|
|
|
|
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");
|
2026-04-28 21:39:46 -03:00
|
|
|
|
video.stop();
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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})");
|
|
|
|
|
|
}
|
2026-04-28 21:39:46 -03:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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_);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 02:28:16 -03:00
|
|
|
|
fn should_retry_uvc_error(err: &anyhow::Error) -> bool {
|
|
|
|
|
|
err.chain()
|
|
|
|
|
|
.find_map(|cause| {
|
|
|
|
|
|
cause
|
|
|
|
|
|
.downcast_ref::<std::io::Error>()
|
|
|
|
|
|
.and_then(|error| error.raw_os_error())
|
|
|
|
|
|
})
|
|
|
|
|
|
.is_some_and(|code| {
|
|
|
|
|
|
matches!(
|
|
|
|
|
|
code,
|
|
|
|
|
|
libc::ENOENT | libc::ENODEV | libc::EIO | libc::EBADF | libc::EAGAIN
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
|
fn parse_args() -> Result<(String, UvcConfig)> {
|
|
|
|
|
|
let mut args = env::args().skip(1);
|
|
|
|
|
|
let mut dev: Option<String> = None;
|
|
|
|
|
|
|
|
|
|
|
|
while let Some(arg) = args.next() {
|
|
|
|
|
|
match arg.as_str() {
|
|
|
|
|
|
"--device" | "-d" => dev = args.next(),
|
2026-04-20 02:28:16 -03:00
|
|
|
|
_ if arg.starts_with('/') => dev = Some(arg),
|
|
|
|
|
|
_ => {}
|
2026-04-13 02:52:32 -03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-04-23 07:00:06 -03:00
|
|
|
|
let _profile_hint = (cfg.width, cfg.height, cfg.fps);
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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<u8> {
|
|
|
|
|
|
std::fs::read_to_string(path)
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.and_then(|v| v.trim().parse::<u8>().ok())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn open_with_retry(path: &str) -> Result<std::fs::File> {
|
2026-04-28 21:00:56 -03:00
|
|
|
|
let read_only = uvc_control_read_only();
|
2026-04-13 02:52:32 -03:00
|
|
|
|
for attempt in 1..=200 {
|
|
|
|
|
|
let mut opts = OpenOptions::new();
|
2026-04-28 21:00:56 -03:00
|
|
|
|
opts.read(true);
|
|
|
|
|
|
if !read_only {
|
|
|
|
|
|
opts.write(true);
|
|
|
|
|
|
}
|
2026-04-28 21:39:46 -03:00
|
|
|
|
opts.custom_flags(libc::O_NONBLOCK);
|
2026-04-13 02:52:32 -03:00
|
|
|
|
match opts.open(path) {
|
|
|
|
|
|
Ok(f) => {
|
2026-04-28 21:00:56 -03:00
|
|
|
|
let mode = if read_only { "ro" } else { "rw" };
|
|
|
|
|
|
eprintln!("[lesavka-uvc] opened {path} mode={mode} (attempt {attempt})");
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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}"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 21:00:56 -03:00
|
|
|
|
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"))
|
|
|
|
|
|
})
|
2026-04-28 21:39:46 -03:00
|
|
|
|
.unwrap_or(false)
|
2026-04-28 21:00:56 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn acquire_singleton_lock() -> Result<File> {
|
|
|
|
|
|
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)
|
|
|
|
|
|
.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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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<PendingRequest>,
|
|
|
|
|
|
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(
|
2026-04-23 07:00:06 -03:00
|
|
|
|
_fd: i32,
|
|
|
|
|
|
_uvc_send_response: libc::c_ulong,
|
2026-04-13 02:52:32 -03:00
|
|
|
|
state: &mut UvcState,
|
|
|
|
|
|
pending: &mut Option<PendingRequest>,
|
|
|
|
|
|
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<Vec<u8>> {
|
|
|
|
|
|
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<Vec<u8>> {
|
|
|
|
|
|
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<Vec<u8>> {
|
|
|
|
|
|
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::<u32>().ok())
|
|
|
|
|
|
.unwrap_or(default)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn env_u8(name: &str) -> Option<u8> {
|
|
|
|
|
|
env::var(name).ok().and_then(|v| v.parse::<u8>().ok())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn env_u32_opt(name: &str) -> Option<u32> {
|
|
|
|
|
|
env::var(name).ok().and_then(|v| v.parse::<u32>().ok())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn read_u32_file(path: &str) -> Option<u32> {
|
|
|
|
|
|
std::fs::read_to_string(path)
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.and_then(|v| v.trim().parse::<u32>().ok())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn read_u32_first(path: &str) -> Option<u32> {
|
|
|
|
|
|
std::fs::read_to_string(path)
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.and_then(|v| v.split_whitespace().next()?.parse::<u32>().ok())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn read_configfs_snapshot() -> Option<ConfigfsSnapshot> {
|
|
|
|
|
|
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<u8>, w_length: u16) -> Vec<u8> {
|
|
|
|
|
|
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<PayloadCap> {
|
|
|
|
|
|
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"));
|
2026-04-23 07:00:06 -03:00
|
|
|
|
if (periodic.is_none() || non_periodic.is_none())
|
2026-04-28 21:00:56 -03:00
|
|
|
|
&& 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"));
|
2026-04-13 02:52:32 -03:00
|
|
|
|
}
|
2026-04-28 21:00:56 -03:00
|
|
|
|
}
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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<u32> {
|
|
|
|
|
|
let raw = std::fs::read_to_string(path).ok()?;
|
|
|
|
|
|
raw.split(|c: char| c == ',' || c.is_whitespace())
|
|
|
|
|
|
.filter_map(|v| v.trim().parse::<u32>().ok())
|
|
|
|
|
|
.filter(|v| *v > 0)
|
|
|
|
|
|
.min()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn read_debugfs_fifos() -> Option<(Option<u32>, Option<u32>)> {
|
|
|
|
|
|
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<u32> = None;
|
|
|
|
|
|
let mut non_periodic: Option<u32> = 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::<u32>().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<T>(type_: u8, nr: u8) -> libc::c_ulong {
|
|
|
|
|
|
ioc(IOC_READ, type_, nr, std::mem::size_of::<T>() as u16)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn ioctl_write<T>(type_: u8, nr: u8) -> libc::c_ulong {
|
|
|
|
|
|
ioc(IOC_WRITE, type_, nr, std::mem::size_of::<T>() as u16)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-28 21:39:46 -03:00
|
|
|
|
fn ioctl_readwrite<T>(type_: u8, nr: u8) -> libc::c_ulong {
|
|
|
|
|
|
ioc(
|
|
|
|
|
|
IOC_READ | IOC_WRITE,
|
|
|
|
|
|
type_,
|
|
|
|
|
|
nr,
|
|
|
|
|
|
std::mem::size_of::<T>() as u16,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
|
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
|
|
|
|
|
|
}
|