uvc: log configfs snapshot

This commit is contained in:
Brad Stein 2026-01-27 04:45:14 -03:00
parent 1dc3bab3aa
commit dd69d7e378
2 changed files with 120 additions and 46 deletions

View File

@ -9,6 +9,11 @@ log() { printf '[lesavka-core] %s\n' "$*"; }
G=/sys/kernel/config/usb_gadget/lesavka G=/sys/kernel/config/usb_gadget/lesavka
if [[ -r /etc/lesavka/uvc.env ]]; then
# shellcheck disable=SC1091
source /etc/lesavka/uvc.env
fi
find_udc() { find_udc() {
ls /sys/class/udc 2>/dev/null | head -n1 || true ls /sys/class/udc 2>/dev/null | head -n1 || true
} }

View File

@ -24,6 +24,8 @@ const UVC_EVENT_DATA: u32 = V4L2_EVENT_PRIVATE_START + 5;
const UVC_STRING_CONTROL_IDX: u8 = 0; const UVC_STRING_CONTROL_IDX: u8 = 0;
const UVC_STRING_STREAMING_IDX: u8 = 1; 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 USB_DIR_IN: u8 = 0x80;
const UVC_SET_CUR: u8 = 0x01; const UVC_SET_CUR: u8 = 0x01;
@ -105,6 +107,7 @@ struct UvcState {
default: [u8; STREAM_CTRL_SIZE_MAX], default: [u8; STREAM_CTRL_SIZE_MAX],
probe: [u8; STREAM_CTRL_SIZE_MAX], probe: [u8; STREAM_CTRL_SIZE_MAX],
commit: [u8; STREAM_CTRL_SIZE_MAX], commit: [u8; STREAM_CTRL_SIZE_MAX],
cfg_snapshot: Option<ConfigfsSnapshot>,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -120,6 +123,16 @@ struct UvcInterfaces {
streaming: 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<()> { fn main() -> Result<()> {
let (dev, cfg) = parse_args()?; let (dev, cfg) = parse_args()?;
let interfaces = load_interfaces(); let interfaces = load_interfaces();
@ -219,10 +232,7 @@ fn main() -> Result<()> {
let data = parse_request_data(event_bytes(&ev)); let data = parse_request_data(event_bytes(&ev));
data_seen += 1; data_seen += 1;
if debug || data_seen <= 10 { if debug || data_seen <= 10 {
eprintln!( eprintln!("[lesavka-uvc] data #{data_seen} len={}", data.length);
"[lesavka-uvc] data #{data_seen} len={}",
data.length
);
} }
handle_data( handle_data(
fd, fd,
@ -302,7 +312,7 @@ impl UvcConfig {
); );
} }
if let Some(cfg_max) = read_u32_file( if let Some(cfg_max) = read_u32_file(
"/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/streaming_maxpacket", &format!("{CONFIGFS_UVC_BASE}/streaming_maxpacket"),
) { ) {
if max_packet > cfg_max { if max_packet > cfg_max {
eprintln!( eprintln!(
@ -350,13 +360,18 @@ impl UvcState {
default, default,
probe: default, probe: default,
commit: default, commit: default,
cfg_snapshot: None,
} }
} }
} }
fn load_interfaces() -> UvcInterfaces { fn load_interfaces() -> UvcInterfaces {
let control = env_u8("LESAVKA_UVC_CTRL_INTF") let control = env_u8("LESAVKA_UVC_CTRL_INTF")
.or_else(|| read_interface("/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/control/bInterfaceNumber")) .or_else(|| {
read_interface(
"/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/control/bInterfaceNumber",
)
})
.unwrap_or(UVC_STRING_CONTROL_IDX); .unwrap_or(UVC_STRING_CONTROL_IDX);
let streaming = env_u8("LESAVKA_UVC_STREAM_INTF") let streaming = env_u8("LESAVKA_UVC_STREAM_INTF")
.or_else(|| read_interface("/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/streaming/bInterfaceNumber")) .or_else(|| read_interface("/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0/streaming/bInterfaceNumber"))
@ -401,9 +416,8 @@ fn subscribe_event(fd: i32, req: libc::c_ulong, event: u32) -> Result<()> {
}; };
let rc = unsafe { libc::ioctl(fd, req, &mut sub) }; let rc = unsafe { libc::ioctl(fd, req, &mut sub) };
if rc < 0 { if rc < 0 {
return Err(std::io::Error::last_os_error()).with_context(|| { return Err(std::io::Error::last_os_error())
format!("subscribe event {event:#x} (fd={fd})") .with_context(|| format!("subscribe event {event:#x} (fd={fd})"));
});
} }
Ok(()) Ok(())
} }
@ -420,14 +434,14 @@ fn handle_setup(
let selector = (req.w_value >> 8) as u8; let selector = (req.w_value >> 8) as u8;
let interface_lo = (req.w_index & 0xff) as u8; let interface_lo = (req.w_index & 0xff) as u8;
let interface_hi = (req.w_index >> 8) as u8; let interface_hi = (req.w_index >> 8) as u8;
let interface_raw = if interface_hi == interfaces.streaming || interface_hi == interfaces.control let interface_raw =
{ if interface_hi == interfaces.streaming || interface_hi == interfaces.control {
interface_hi interface_hi
} else if interface_lo == interfaces.streaming || interface_lo == interfaces.control { } else if interface_lo == interfaces.streaming || interface_lo == interfaces.control {
interface_lo interface_lo
} else { } else {
interface_hi interface_hi
}; };
let is_in = (req.b_request_type & USB_DIR_IN) != 0; let is_in = (req.b_request_type & USB_DIR_IN) != 0;
if matches!(selector, UVC_VS_PROBE_CONTROL | UVC_VS_COMMIT_CONTROL) { if matches!(selector, UVC_VS_PROBE_CONTROL | UVC_VS_COMMIT_CONTROL) {
maybe_update_ctrl_len(state, req.w_length, debug); maybe_update_ctrl_len(state, req.w_length, debug);
@ -435,11 +449,23 @@ fn handle_setup(
let interface = map_interface(interface_raw, selector, interfaces, debug); let interface = map_interface(interface_raw, selector, interfaces, debug);
if !is_in && req.b_request == UVC_SET_CUR { 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 { if interface != interfaces.streaming {
let _ = send_stall(fd, uvc_send_response); let _ = send_stall(fd, uvc_send_response);
return; return;
} }
let len = req.w_length as usize;
if len > UVC_DATA_SIZE { if len > UVC_DATA_SIZE {
eprintln!( eprintln!(
"[lesavka-uvc] SET_CUR too large len={} (max={}); stalling", "[lesavka-uvc] SET_CUR too large len={} (max={}); stalling",
@ -568,10 +594,7 @@ fn handle_data(
let payload = read_le32(slice, 22); let payload = read_le32(slice, 22);
eprintln!( eprintln!(
"[lesavka-uvc] data ctrl fmt={} frame={} interval={} payload={}", "[lesavka-uvc] data ctrl fmt={} frame={} interval={} payload={}",
slice[2], slice[2], slice[3], interval, payload
slice[3],
interval,
payload
); );
} }
@ -586,9 +609,9 @@ fn handle_data(
let payload = read_le32(&state.probe, 22); let payload = read_le32(&state.probe, 22);
eprintln!( eprintln!(
"[lesavka-uvc] probe set interval={} payload={}", "[lesavka-uvc] probe set interval={} payload={}",
interval, interval, payload
payload
); );
log_configfs_snapshot(state, "probe");
} }
} else { } else {
state.commit = sanitized; state.commit = sanitized;
@ -597,9 +620,9 @@ fn handle_data(
let payload = read_le32(&state.commit, 22); let payload = read_le32(&state.commit, 22);
eprintln!( eprintln!(
"[lesavka-uvc] commit set interval={} payload={}", "[lesavka-uvc] commit set interval={} payload={}",
interval, interval, payload
payload
); );
log_configfs_snapshot(state, "commit");
} }
} }
} }
@ -624,11 +647,7 @@ fn build_in_response(
Some(adjust_length(payload, w_length)) Some(adjust_length(payload, w_length))
} }
fn build_streaming_response( fn build_streaming_response(state: &UvcState, selector: u8, request: u8) -> Option<Vec<u8>> {
state: &UvcState,
selector: u8,
request: u8,
) -> Option<Vec<u8>> {
let current = match selector { let current = match selector {
UVC_VS_PROBE_CONTROL => state.probe, UVC_VS_PROBE_CONTROL => state.probe,
UVC_VS_COMMIT_CONTROL => state.commit, UVC_VS_COMMIT_CONTROL => state.commit,
@ -636,7 +655,7 @@ fn build_streaming_response(
}; };
match request { match request {
UVC_GET_INFO => Some(vec![0x03]), 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_LEN => Some((state.ctrl_len as u16).to_le_bytes().to_vec()),
UVC_GET_CUR => Some(current[..state.ctrl_len].to_vec()), UVC_GET_CUR => Some(current[..state.ctrl_len].to_vec()),
UVC_GET_MIN | UVC_GET_MAX | UVC_GET_DEF | UVC_GET_RES => { UVC_GET_MIN | UVC_GET_MAX | UVC_GET_DEF | UVC_GET_RES => {
@ -648,12 +667,14 @@ fn build_streaming_response(
fn build_control_response(selector: u8, request: u8) -> Option<Vec<u8>> { fn build_control_response(selector: u8, request: u8) -> Option<Vec<u8>> {
match request { match request {
UVC_GET_INFO => Some(vec![0x01]), UVC_GET_INFO => Some(vec![0x03]), // indicate both GET/SET supported
UVC_GET_LEN => Some(1u16.to_le_bytes().to_vec()), 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 => { UVC_GET_CUR | UVC_GET_MIN | UVC_GET_MAX | UVC_GET_DEF | UVC_GET_RES => {
if selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL { if selector == UVC_VC_REQUEST_ERROR_CODE_CONTROL {
// reset error code to “no error”
Some(vec![0x00]) Some(vec![0x00])
} else { } else {
// simple 1byte placeholder for unhandled VC controls
Some(vec![0x00]) Some(vec![0x00])
} }
} }
@ -661,10 +682,7 @@ fn build_control_response(selector: u8, request: u8) -> Option<Vec<u8>> {
} }
} }
fn sanitize_streaming_control( fn sanitize_streaming_control(data: &[u8], state: &UvcState) -> [u8; STREAM_CTRL_SIZE_MAX] {
data: &[u8],
state: &UvcState,
) -> [u8; STREAM_CTRL_SIZE_MAX] {
let mut out = state.default; let mut out = state.default;
if data.len() >= STREAM_CTRL_SIZE_11 { if data.len() >= STREAM_CTRL_SIZE_11 {
let format_index = data[2]; let format_index = data[2];
@ -736,10 +754,10 @@ fn build_streaming_control(cfg: &UvcConfig, ctrl_len: usize) -> [u8; STREAM_CTRL
write_le32(&mut buf[22..26], cfg.max_packet); write_le32(&mut buf[22..26], cfg.max_packet);
if ctrl_len >= STREAM_CTRL_SIZE_15 { if ctrl_len >= STREAM_CTRL_SIZE_15 {
write_le32(&mut buf[26..30], 48_000_000); write_le32(&mut buf[26..30], 48_000_000);
buf[30] = 0; buf[30] = 0x03; // bmFramingInfo: FID + EOF supported
buf[31] = 0; buf[31] = 0x01; // bPreferedVersion
buf[32] = 0; buf[32] = 0x01; // bMinVersion
buf[33] = 0; buf[33] = 0x01; // bMaxVersion
} }
buf buf
@ -795,6 +813,52 @@ fn read_u32_file(path: &str) -> Option<u32> {
.and_then(|v| v.trim().parse::<u32>().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> { fn adjust_length(mut bytes: Vec<u8>, w_length: u16) -> Vec<u8> {
let want = (w_length as usize).min(UVC_DATA_SIZE); let want = (w_length as usize).min(UVC_DATA_SIZE);
if bytes.len() > want { if bytes.len() > want {
@ -820,7 +884,12 @@ fn write_le32(dst: &mut [u8], val: u32) {
} }
fn read_le32(src: &[u8], offset: usize) -> u32 { fn read_le32(src: &[u8], offset: usize) -> u32 {
u32::from_le_bytes([src[offset], src[offset + 1], src[offset + 2], src[offset + 3]]) u32::from_le_bytes([
src[offset],
src[offset + 1],
src[offset + 2],
src[offset + 3],
])
} }
fn compute_payload_cap(bulk: bool) -> Option<PayloadCap> { fn compute_payload_cap(bulk: bool) -> Option<PayloadCap> {
@ -834,10 +903,10 @@ fn compute_payload_cap(bulk: bool) -> Option<PayloadCap> {
}); });
} }
let mut periodic = read_fifo_min("/sys/module/dwc2/parameters/g_tx_fifo_size") let mut periodic =
.map(|v| (v, "dwc2.params")); 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") let mut non_periodic =
.map(|v| (v, "dwc2.params")); 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() { if periodic.is_none() || non_periodic.is_none() {
if let Some((p, np)) = read_debugfs_fifos() { if let Some((p, np)) = read_debugfs_fifos() {
if periodic.is_none() { if periodic.is_none() {