diff --git a/scripts/daemon/lesavka-core.sh b/scripts/daemon/lesavka-core.sh index 7c4a83e..75993c1 100644 --- a/scripts/daemon/lesavka-core.sh +++ b/scripts/daemon/lesavka-core.sh @@ -118,6 +118,79 @@ UVC_FPS=${LESAVKA_UVC_FPS:-25} UVC_DISABLE_IRQ=${LESAVKA_UVC_DISABLE_IRQ:-} UVC_BULK=${LESAVKA_UVC_BULK:-} UVC_CODEC=${LESAVKA_UVC_CODEC:-yuyv} + +uvc_fifo_min() { + local path="$1" + local raw="" + raw="$(cat "$path" 2>/dev/null || true)" + if [[ -z $raw ]]; then + return 0 + fi + echo "$raw" | tr ', ' '\n' | awk 'NF{print $1}' | awk ' + $1 > 0 { if (min == "" || $1 < min) min = $1 } + END { if (min != "") print min }' +} + +compute_uvc_payload_cap() { + UVC_PAYLOAD_CAP="" + UVC_PAYLOAD_SRC="" + UVC_PAYLOAD_PCT="" + UVC_FIFO_PERIODIC="$(uvc_fifo_min /sys/module/dwc2/parameters/g_tx_fifo_size)" + UVC_FIFO_NP="$(uvc_fifo_min /sys/module/dwc2/parameters/g_np_tx_fifo_size)" + + if [[ -n ${LESAVKA_UVC_MAXPAYLOAD_LIMIT:-} ]]; then + UVC_PAYLOAD_CAP="${LESAVKA_UVC_MAXPAYLOAD_LIMIT}" + UVC_PAYLOAD_SRC="env" + UVC_PAYLOAD_PCT=100 + return + fi + + local chosen="" + if [[ -n $UVC_BULK ]]; then + if [[ -n $UVC_FIFO_NP ]]; then + chosen="$UVC_FIFO_NP" + UVC_PAYLOAD_SRC="dwc2.g_np_tx_fifo_size" + elif [[ -n $UVC_FIFO_PERIODIC ]]; then + chosen="$UVC_FIFO_PERIODIC" + UVC_PAYLOAD_SRC="dwc2.g_tx_fifo_size" + fi + else + if [[ -n $UVC_FIFO_PERIODIC ]]; then + chosen="$UVC_FIFO_PERIODIC" + UVC_PAYLOAD_SRC="dwc2.g_tx_fifo_size" + elif [[ -n $UVC_FIFO_NP ]]; then + chosen="$UVC_FIFO_NP" + UVC_PAYLOAD_SRC="dwc2.g_np_tx_fifo_size" + fi + fi + + if [[ -z $chosen ]]; then + return + fi + + local pct=${LESAVKA_UVC_LIMIT_PCT:-95} + if ((pct < 1)); then + pct=1 + elif ((pct > 100)); then + pct=100 + fi + UVC_PAYLOAD_PCT=$pct + local bytes=$((chosen * 4)) + UVC_PAYLOAD_CAP=$((bytes * pct / 100)) +} + +compute_uvc_payload_cap +if [[ -n $UVC_PAYLOAD_CAP && $UVC_PAYLOAD_CAP -gt 0 ]]; then + log "UVC fifo periodic=${UVC_FIFO_PERIODIC:-?} np=${UVC_FIFO_NP:-?} cap=${UVC_PAYLOAD_CAP}B pct=${UVC_PAYLOAD_PCT:-?} src=${UVC_PAYLOAD_SRC:-?}" + if ((UVC_MAXPACKET > UVC_PAYLOAD_CAP)); then + log "clamping UVC maxpacket $UVC_MAXPACKET -> $UVC_PAYLOAD_CAP" + UVC_MAXPACKET=$UVC_PAYLOAD_CAP + fi +fi +if [[ -n $UVC_BULK && $UVC_MAXPACKET -gt 512 ]]; then + log "clamping UVC maxpacket $UVC_MAXPACKET -> 512 (bulk)" + UVC_MAXPACKET=512 +fi if [[ -n ${LESAVKA_UVC_MJPEG:-} ]]; then UVC_CODEC=mjpeg fi diff --git a/server/src/bin/lesavka-uvc.rs b/server/src/bin/lesavka-uvc.rs index 8299a2c..3eef994 100644 --- a/server/src/bin/lesavka-uvc.rs +++ b/server/src/bin/lesavka-uvc.rs @@ -91,6 +91,14 @@ struct UvcConfig { 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, @@ -103,6 +111,7 @@ struct UvcState { struct PendingRequest { interface: u8, selector: u8, + expected_len: usize, } #[derive(Clone, Copy)] @@ -261,6 +270,32 @@ impl UvcConfig { 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 + ); + } + } if env::var("LESAVKA_UVC_BULK").is_ok() { max_packet = max_packet.min(512); } else { @@ -383,9 +418,21 @@ fn handle_setup( let _ = send_stall(fd, uvc_send_response); return; } - *pending = Some(PendingRequest { interface, selector }); let len = req.w_length as usize; - let payload = vec![0u8; len.min(UVC_DATA_SIZE)]; + 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!( @@ -477,6 +524,9 @@ fn handle_data( debug: bool, ) { let Some(p) = pending.take() else { + if debug { + eprintln!("[lesavka-uvc] DATA with no pending request; ignoring"); + } return; }; @@ -485,6 +535,12 @@ fn handle_data( } 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); @@ -524,9 +580,6 @@ fn handle_data( payload ); } - if std::env::var("LESAVKA_UVC_ACK_AFTER_DATA").is_ok() { - let _ = send_response(fd, uvc_send_response, &[]); - } } } } @@ -711,6 +764,10 @@ 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 adjust_length(mut bytes: Vec, w_length: u16) -> Vec { let want = (w_length as usize).min(UVC_DATA_SIZE); if bytes.len() > want { @@ -739,6 +796,65 @@ 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 periodic_dw = read_fifo_min("/sys/module/dwc2/parameters/g_tx_fifo_size"); + let non_periodic_dw = read_fifo_min("/sys/module/dwc2/parameters/g_np_tx_fifo_size"); + + let (fifo_dw, source) = if bulk { + if let Some(np) = non_periodic_dw { + (np, "dwc2.g_np_tx_fifo_size") + } else if let Some(p) = periodic_dw { + (p, "dwc2.g_tx_fifo_size") + } else { + return None; + } + } else if let Some(p) = periodic_dw { + (p, "dwc2.g_tx_fifo_size") + } else if let Some(np) = non_periodic_dw { + (np, "dwc2.g_np_tx_fifo_size") + } 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() +} + const IOC_NRBITS: u8 = 8; const IOC_TYPEBITS: u8 = 8; const IOC_SIZEBITS: u8 = 14;