lesavka/server/src/bin/lesavka-uvc.real.inc

1071 lines
33 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
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;
#[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 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,
}
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<i32> = 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::<V4l2EventSubscription>(b'V', 90);
let vidioc_dqevent = ioctl_read::<V4l2Event>(b'V', 89);
let uvc_send_response = ioctl_write::<UvcRequestData>(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<PendingRequest> = None;
loop {
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");
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 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
)
})
}
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(),
_ 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<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> {
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<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(
_fd: i32,
_uvc_send_response: libc::c_ulong,
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 1byte 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"));
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<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)
}
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
}