2026-04-23 07:00:06 -03:00
|
|
|
fn main() -> Result<()> {
|
|
|
|
|
let (dev, _cfg) = parse_args()?;
|
|
|
|
|
let _ = load_interfaces();
|
|
|
|
|
let _ = UvcConfig::from_env();
|
|
|
|
|
|
|
|
|
|
let _ = open_with_retry(&dev)?;
|
|
|
|
|
anyhow::bail!("coverage harness: control loop disabled");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
fn parse_args() -> Result<(String, UvcConfig)> {
|
|
|
|
|
let args: Vec<String> = env::args().skip(1).collect();
|
|
|
|
|
let dev = parse_device_arg(&args)
|
|
|
|
|
.or_else(|| env::var("LESAVKA_UVC_DEV").ok())
|
|
|
|
|
.context("missing --device (or LESAVKA_UVC_DEV)")?;
|
|
|
|
|
|
|
|
|
|
Ok((dev, UvcConfig::from_env()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
fn parse_device_arg(args: &[String]) -> Option<String> {
|
|
|
|
|
args
|
|
|
|
|
.windows(2)
|
|
|
|
|
.find_map(|pair| (pair[0] == "--device" || pair[0] == "-d").then(|| pair[1].clone()))
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
args.iter()
|
|
|
|
|
.rev()
|
|
|
|
|
.find(|arg| {
|
|
|
|
|
arg.as_str() != "--device" && arg.as_str() != "-d" && arg.starts_with('/')
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
impl UvcConfig {
|
|
|
|
|
fn from_env() -> Self {
|
|
|
|
|
let width = env_u32("LESAVKA_UVC_WIDTH", 1280);
|
|
|
|
|
let height = env_u32("LESAVKA_UVC_HEIGHT", 720);
|
2026-05-02 22:59:18 -03:00
|
|
|
let fps = env_u32("LESAVKA_UVC_FPS", 30).max(1);
|
2026-04-23 07:00:06 -03:00
|
|
|
let frame_size = env_u32("LESAVKA_UVC_FRAME_SIZE", width * height * 2);
|
|
|
|
|
let interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
|
|
|
|
|
let bulk = env::var("LESAVKA_UVC_BULK").is_ok();
|
|
|
|
|
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
|
|
|
|
|
|
|
|
|
|
if let Some(limit) = compute_payload_cap(bulk).map(|cap| cap.limit) {
|
|
|
|
|
max_packet = max_packet.min(limit);
|
|
|
|
|
}
|
|
|
|
|
max_packet = if bulk {
|
|
|
|
|
max_packet.min(512)
|
|
|
|
|
} else {
|
|
|
|
|
max_packet.min(1024)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let interval = if interval == 0 {
|
|
|
|
|
10_000_000 / fps
|
|
|
|
|
} else {
|
|
|
|
|
interval
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
fps,
|
|
|
|
|
interval,
|
|
|
|
|
max_packet,
|
|
|
|
|
frame_size,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
impl UvcState {
|
|
|
|
|
fn new(cfg: UvcConfig) -> Self {
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
fn load_interfaces() -> UvcInterfaces {
|
|
|
|
|
let control = env_u8("LESAVKA_UVC_CTRL_INTF").unwrap_or(UVC_STRING_CONTROL_IDX);
|
|
|
|
|
let streaming = env_u8("LESAVKA_UVC_STREAM_INTF").unwrap_or(UVC_STRING_STREAMING_IDX);
|
|
|
|
|
UvcInterfaces { control, streaming }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
fn read_interface(path: &str) -> Option<u8> {
|
|
|
|
|
std::fs::read_to_string(path)
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|v| v.trim().parse::<u8>().ok())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
fn open_with_retry(path: &str) -> Result<std::fs::File> {
|
2026-04-29 01:25:06 -03:00
|
|
|
let read_only = uvc_control_read_only();
|
2026-04-23 07:00:06 -03:00
|
|
|
let mut opts = OpenOptions::new();
|
2026-04-29 01:25:06 -03:00
|
|
|
opts.read(true);
|
|
|
|
|
if !read_only {
|
|
|
|
|
opts.write(true);
|
|
|
|
|
}
|
2026-04-23 07:00:06 -03:00
|
|
|
if env::var("LESAVKA_UVC_BLOCKING").is_err() {
|
|
|
|
|
opts.custom_flags(libc::O_NONBLOCK);
|
|
|
|
|
}
|
|
|
|
|
opts.open(path).with_context(|| format!("open {path}"))
|
|
|
|
|
}
|
2026-04-29 01:25:06 -03:00
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
2026-04-30 08:16:57 -03:00
|
|
|
/// Keep coverage-mode UVC control opens read-only unless a test opts into writes.
|
2026-04-29 01:25:06 -03:00
|
|
|
fn uvc_control_read_only() -> bool {
|
|
|
|
|
env::var("LESAVKA_UVC_CONTROL_READ_ONLY")
|
|
|
|
|
.ok()
|
|
|
|
|
.map(|value| {
|
|
|
|
|
let trimmed = value.trim();
|
|
|
|
|
!(trimmed.eq_ignore_ascii_case("0")
|
|
|
|
|
|| trimmed.eq_ignore_ascii_case("false")
|
|
|
|
|
|| trimmed.eq_ignore_ascii_case("no")
|
|
|
|
|
|| trimmed.eq_ignore_ascii_case("off"))
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(true)
|
|
|
|
|
}
|
2026-05-09 11:34:13 -03:00
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
/// Returns the bounded UVC request-buffer count used by the helper.
|
|
|
|
|
///
|
|
|
|
|
/// Inputs: `LESAVKA_UVC_BUFFER_COUNT`. Outputs: a value clamped to the safe
|
|
|
|
|
/// range accepted by the helper. Why: coverage contracts need the same backlog
|
|
|
|
|
/// bound as the real UVC binary without opening a gadget device.
|
|
|
|
|
fn uvc_buffer_count() -> u32 {
|
|
|
|
|
env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
/// Returns the idle frame-pump sleep interval used when no fresh frame is ready.
|
|
|
|
|
///
|
|
|
|
|
/// Inputs: `LESAVKA_UVC_IDLE_PUMP_MS`. Outputs: a duration in milliseconds.
|
|
|
|
|
/// Why: this value controls how quickly the UVC helper retries the freshness
|
|
|
|
|
/// spool when browsers are already streaming.
|
|
|
|
|
fn uvc_idle_pump_sleep() -> std::time::Duration {
|
|
|
|
|
std::time::Duration::from_millis(env_u64(
|
|
|
|
|
"LESAVKA_UVC_IDLE_PUMP_MS",
|
|
|
|
|
DEFAULT_UVC_IDLE_PUMP_MS,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 21:48:57 -03:00
|
|
|
#[cfg(coverage)]
|
|
|
|
|
/// Bound MJPEG frames before they enter the USB UVC helper.
|
|
|
|
|
///
|
|
|
|
|
/// Inputs: UVC mode plus optional `LESAVKA_UVC_FRAME_MAX_BYTES` or
|
|
|
|
|
/// `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC`. Output: maximum accepted JPEG
|
|
|
|
|
/// byte length. Why: coverage tests should lock the artifact-prevention budget
|
|
|
|
|
/// that turns oversized UVC frames into freezes instead of grey half-frames.
|
|
|
|
|
fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
|
|
|
|
if let Some(limit) = env_u32_opt("LESAVKA_UVC_FRAME_MAX_BYTES") {
|
|
|
|
|
return if limit == 0 {
|
|
|
|
|
MAX_MJPEG_FRAME_BYTES
|
|
|
|
|
} else {
|
|
|
|
|
limit as usize
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let fps = cfg.fps.max(1);
|
|
|
|
|
let budget_per_sec = env_u32(
|
|
|
|
|
"LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
|
|
|
|
|
DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
|
|
|
|
|
)
|
|
|
|
|
.max(1);
|
|
|
|
|
let per_frame = (budget_per_sec / fps).max(64 * 1024);
|
|
|
|
|
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 11:34:13 -03:00
|
|
|
#[cfg(coverage)]
|
|
|
|
|
/// Returns the optional maximum age for a spooled MJPEG frame.
|
|
|
|
|
///
|
|
|
|
|
/// Inputs: `LESAVKA_UVC_FRAME_MAX_AGE_MS`. Outputs: `None` when the TTL is
|
|
|
|
|
/// disabled. Why: stale-frame replay must be explicit because it trades
|
|
|
|
|
/// smoothness against freshness during browser stream recovery.
|
|
|
|
|
fn frame_spool_max_age() -> Option<std::time::Duration> {
|
|
|
|
|
match env_u64(
|
|
|
|
|
"LESAVKA_UVC_FRAME_MAX_AGE_MS",
|
|
|
|
|
DEFAULT_UVC_FRAME_MAX_AGE_MS,
|
|
|
|
|
) {
|
|
|
|
|
0 => None,
|
|
|
|
|
value => Some(std::time::Duration::from_millis(value)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
/// Determines whether a spooled MJPEG frame is too old to replay.
|
|
|
|
|
///
|
|
|
|
|
/// Inputs: frame path and optional age limit. Outputs: true when the frame is
|
|
|
|
|
/// missing or older than the limit. Why: the coverage harness should guard the
|
|
|
|
|
/// same freshness cut-off that prevents seconds-old video from re-entering UVC.
|
|
|
|
|
fn frame_spool_is_stale(path: &std::path::Path, max_age: Option<std::time::Duration>) -> bool {
|
|
|
|
|
let Some(max_age) = max_age else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
let Ok(metadata) = std::fs::metadata(path) else {
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
let Ok(modified) = metadata.modified() else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
std::time::SystemTime::now()
|
|
|
|
|
.duration_since(modified)
|
|
|
|
|
.map(|age| age > max_age)
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
/// Parses an unsigned 64-bit environment value with a fallback.
|
|
|
|
|
///
|
|
|
|
|
/// Inputs: variable name and default value. Outputs: parsed value or default.
|
|
|
|
|
/// Why: UVC freshness settings use millisecond durations that should not be
|
|
|
|
|
/// truncated to 32-bit just because the coverage harness is lightweight.
|
|
|
|
|
fn env_u64(name: &str, default: u64) -> u64 {
|
|
|
|
|
env::var(name)
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|value| value.parse::<u64>().ok())
|
|
|
|
|
.unwrap_or(default)
|
|
|
|
|
}
|