lesavka/server/src/bin/lesavka_uvc/coverage_model.rs

239 lines
5.6 KiB
Rust

use anyhow::{Context, Result};
#[cfg(coverage)]
use std::env;
#[cfg(coverage)]
use std::fs::OpenOptions;
#[cfg(coverage)]
use std::os::unix::fs::{FileTypeExt, OpenOptionsExt};
#[cfg(coverage)]
const STREAM_CTRL_SIZE_11: usize = 26;
#[cfg(coverage)]
const STREAM_CTRL_SIZE_15: usize = 34;
#[cfg(coverage)]
const STREAM_CTRL_SIZE_MAX: usize = STREAM_CTRL_SIZE_15;
#[cfg(coverage)]
const UVC_DATA_SIZE: usize = 60;
#[cfg(coverage)]
const UVC_STRING_CONTROL_IDX: u8 = 0;
#[cfg(coverage)]
const UVC_STRING_STREAMING_IDX: u8 = 1;
#[cfg(coverage)]
const CONFIGFS_UVC_BASE: &str = "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0";
#[cfg(coverage)]
const USB_DIR_IN: u8 = 0x80;
#[cfg(coverage)]
const UVC_SET_CUR: u8 = 0x01;
#[cfg(coverage)]
const UVC_GET_CUR: u8 = 0x81;
#[cfg(coverage)]
const UVC_GET_MIN: u8 = 0x82;
#[cfg(coverage)]
const UVC_GET_MAX: u8 = 0x83;
#[cfg(coverage)]
const UVC_GET_RES: u8 = 0x84;
#[cfg(coverage)]
const UVC_GET_LEN: u8 = 0x85;
#[cfg(coverage)]
const UVC_GET_INFO: u8 = 0x86;
#[cfg(coverage)]
const UVC_GET_DEF: u8 = 0x87;
#[cfg(coverage)]
const UVC_VS_PROBE_CONTROL: u8 = 0x01;
#[cfg(coverage)]
const UVC_VS_COMMIT_CONTROL: u8 = 0x02;
#[cfg(coverage)]
const UVC_VC_REQUEST_ERROR_CODE_CONTROL: u8 = 0x02;
#[cfg(coverage)]
const DEFAULT_UVC_BUFFER_COUNT: u32 = 2;
#[cfg(coverage)]
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
#[cfg(coverage)]
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
#[cfg(coverage)]
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
#[cfg(coverage)]
const DEFAULT_UVC_STATS_INTERVAL_MS: u64 = 5_000;
#[cfg(coverage)]
const DEFAULT_UVC_STATS_PATH: &str = "/run/lesavka-uvc-video-stats.json";
#[cfg(coverage)]
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
#[cfg(coverage)]
const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
#[cfg(coverage)]
const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("idle_1280x720_black.jpg");
#[cfg(coverage)]
#[repr(C)]
#[derive(Clone, Copy)]
struct V4l2Event {
_bytes: [u8; 64],
}
#[cfg(coverage)]
#[repr(C)]
#[derive(Clone, Copy)]
struct UsbCtrlRequest {
b_request_type: u8,
b_request: u8,
w_value: u16,
w_index: u16,
w_length: u16,
}
#[cfg(coverage)]
#[repr(C)]
#[derive(Clone, Copy)]
struct UvcRequestData {
length: i32,
data: [u8; UVC_DATA_SIZE],
}
#[cfg(coverage)]
#[derive(Clone, Copy)]
struct UvcConfig {
width: u32,
height: u32,
fps: u32,
interval: u32,
max_packet: u32,
frame_size: u32,
}
#[cfg(coverage)]
struct PayloadCap {
limit: u32,
pct: u32,
source: &'static str,
periodic_dw: Option<u32>,
non_periodic_dw: Option<u32>,
}
#[cfg(coverage)]
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>,
}
#[cfg(coverage)]
#[derive(Clone, Copy)]
struct PendingRequest {
interface: u8,
selector: u8,
expected_len: usize,
}
#[cfg(coverage)]
#[derive(Clone, Copy)]
struct UvcInterfaces {
control: u8,
streaming: u8,
}
#[cfg(coverage)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ConfigfsSnapshot {
width: u32,
height: u32,
default_interval: u32,
frame_interval: u32,
maxpacket: u32,
maxburst: u32,
}
#[cfg(coverage)]
struct MmapBuffer {
ptr: *mut u8,
len: usize,
}
#[cfg(coverage)]
struct UvcVideoStream {
buffers: Vec<MmapBuffer>,
frame_path: std::path::PathBuf,
latest_frame: Vec<u8>,
frame_max_bytes: usize,
}
#[cfg(coverage)]
#[derive(Default)]
struct UvcVideoStats {
queued: u64,
reloaded: u64,
replayed_stale: u64,
rejected_oversize: u64,
rejected_invalid: u64,
fallback_idle: u64,
latest_bytes: usize,
}
#[cfg(coverage)]
impl UvcVideoStream {
fn new(_fd: i32) -> Self {
Self {
buffers: Vec::new(),
frame_path: frame_spool_path(),
latest_frame: IDLE_MJPEG_FRAME.to_vec(),
frame_max_bytes: MAX_MJPEG_FRAME_BYTES,
}
}
fn refresh_latest_frame(&mut self) {
let stale = frame_spool_is_stale(&self.frame_path, frame_spool_max_age());
if stale && looks_like_mjpeg_frame(&self.latest_frame) {
return;
}
let max_frame_bytes = self.frame_payload_limit();
if let Ok(frame) = std::fs::read(&self.frame_path)
&& frame.len() <= max_frame_bytes
&& looks_like_mjpeg_frame(&frame)
{
self.latest_frame = frame;
} else if !looks_like_mjpeg_frame(&self.latest_frame) {
self.latest_frame = IDLE_MJPEG_FRAME.to_vec();
}
}
fn frame_payload_limit(&self) -> usize {
self.buffers
.iter()
.map(|buffer| buffer.len)
.min()
.unwrap_or(MAX_MJPEG_FRAME_BYTES)
.min(self.frame_max_bytes)
}
fn frame_for_buffer(&self, buffer_len: usize) -> &[u8] {
if self.latest_frame.len() <= buffer_len && looks_like_mjpeg_frame(&self.latest_frame) {
&self.latest_frame
} else if IDLE_MJPEG_FRAME.len() <= buffer_len {
IDLE_MJPEG_FRAME
} else {
MINIMAL_MJPEG_FRAME
}
}
}
#[cfg(coverage)]
fn frame_spool_path() -> std::path::PathBuf {
env::var("LESAVKA_UVC_FRAME_PATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("/run/lesavka-uvc-frame.mjpg"))
}
#[cfg(coverage)]
fn looks_like_mjpeg_frame(frame: &[u8]) -> bool {
frame.len() > MINIMAL_MJPEG_FRAME.len()
&& frame.starts_with(&[0xff, 0xd8])
&& frame.ends_with(&[0xff, 0xd9])
}