uvc: log configfs snapshot
This commit is contained in:
parent
1dc3bab3aa
commit
dd69d7e378
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 1‑byte 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() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user