fix(sync): rebuild incomplete uvc gadgets
This commit is contained in:
parent
cd57ee0f9a
commit
8b5dc220ad
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.14.29"
|
version = "0.14.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1676,7 +1676,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.14.29"
|
version = "0.14.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1688,7 +1688,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.14.29"
|
version = "0.14.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.14.29"
|
version = "0.14.30"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -472,3 +472,52 @@ async fn runtime_probe_video_packets_change_across_a_pulse_boundary() {
|
|||||||
"expected decoded pulse frame to be much brighter than decoded dark frame, got dark={dark_mean} pulse={pulse_mean}"
|
"expected decoded pulse frame to be much brighter than decoded dark frame, got dark={dark_mean} pulse={pulse_mean}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn runtime_probe_dark_video_packets_do_not_alternate_frame_to_frame() {
|
||||||
|
let capture = SyncProbeCapture::new(
|
||||||
|
CameraConfig {
|
||||||
|
codec: CameraCodec::Mjpeg,
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
fps: 20,
|
||||||
|
},
|
||||||
|
PulseSchedule::new(
|
||||||
|
Duration::from_secs(4),
|
||||||
|
Duration::from_secs(1),
|
||||||
|
Duration::from_millis(120),
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
Duration::from_secs(3),
|
||||||
|
)
|
||||||
|
.expect("runtime capture");
|
||||||
|
|
||||||
|
let video_queue = capture.video_queue();
|
||||||
|
let mut dark_means = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let next = video_queue.pop_fresh().await;
|
||||||
|
let Some(packet) = next.packet else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if packet.pts >= 1_000_000 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dark_means.push(decode_mjpeg_packet_mean_luma(&packet));
|
||||||
|
if dark_means.len() >= 8 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
dark_means.len() >= 4,
|
||||||
|
"expected several dark packets before the first pulse, got {dark_means:?}"
|
||||||
|
);
|
||||||
|
let min = *dark_means.iter().min().expect("dark min");
|
||||||
|
let max = *dark_means.iter().max().expect("dark max");
|
||||||
|
assert!(
|
||||||
|
max.saturating_sub(min) <= 2,
|
||||||
|
"expected consecutive dark MJPEG packets to stay visually stable, got {dark_means:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.14.29"
|
version = "0.14.30"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,18 @@ attach_gadget() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gadget_has_expected_functions() {
|
||||||
|
[[ -d $G/functions/hid.usb0 ]] || return 1
|
||||||
|
[[ -d $G/functions/hid.usb1 ]] || return 1
|
||||||
|
if [[ -z ${DISABLE_UAC:-} ]]; then
|
||||||
|
[[ -d $G/functions/uac2.usb0 ]] || return 1
|
||||||
|
fi
|
||||||
|
if [[ -z ${DISABLE_UVC:-} ]]; then
|
||||||
|
[[ -d $G/functions/uvc.usb0 ]] || return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
--detach)
|
--detach)
|
||||||
detach_gadget
|
detach_gadget
|
||||||
@ -314,10 +326,13 @@ log "✅ UDC detected: $UDC"
|
|||||||
# If a gadget is already configured, avoid tearing it down unless forced.
|
# If a gadget is already configured, avoid tearing it down unless forced.
|
||||||
if [[ -d $G && -z $ALLOW_RESET ]]; then
|
if [[ -d $G && -z $ALLOW_RESET ]]; then
|
||||||
if [[ -s $G/UDC || -d $G/configs/c.1 ]]; then
|
if [[ -s $G/UDC || -d $G/configs/c.1 ]]; then
|
||||||
log "🔒 gadget already configured; skipping reset."
|
if gadget_has_expected_functions; then
|
||||||
log " Set LESAVKA_ALLOW_GADGET_RESET=1 to force rebuild."
|
log "🔒 gadget already configured; skipping reset."
|
||||||
attach_gadget || true
|
log " Set LESAVKA_ALLOW_GADGET_RESET=1 to force rebuild."
|
||||||
exit 0
|
attach_gadget || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
log "⚠️ gadget is configured but missing expected functions; continuing toward rebuild."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -90,6 +90,10 @@ validate_uvc_gadget_ready() {
|
|||||||
echo "✅ UVC gadget output ready at ${node}"
|
echo "✅ UVC gadget output ready at ${node}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uvc_gadget_present() {
|
||||||
|
[[ -d /sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
udc_state() {
|
udc_state() {
|
||||||
local udc=""
|
local udc=""
|
||||||
udc=$(ls /sys/class/udc 2>/dev/null | head -n1 || true)
|
udc=$(ls /sys/class/udc 2>/dev/null | head -n1 || true)
|
||||||
@ -644,6 +648,10 @@ sudo systemctl enable lesavka-core lesavka-server
|
|||||||
|
|
||||||
UDC_STATE=$(udc_state)
|
UDC_STATE=$(udc_state)
|
||||||
FORCE_GADGET_REBUILD=0
|
FORCE_GADGET_REBUILD=0
|
||||||
|
if [[ -z ${LESAVKA_DISABLE_UVC:-} ]] && ! uvc_gadget_present; then
|
||||||
|
FORCE_GADGET_REBUILD=1
|
||||||
|
echo "⚠️ UVC function is missing from the live gadget; forcing a rebuild before server start."
|
||||||
|
fi
|
||||||
if [[ "$UVC_ENV_CHANGED" == "1" ]] && is_attached_state "$UDC_STATE"; then
|
if [[ "$UVC_ENV_CHANGED" == "1" ]] && is_attached_state "$UDC_STATE"; then
|
||||||
FORCE_GADGET_REBUILD=1
|
FORCE_GADGET_REBUILD=1
|
||||||
echo "⚠️ UVC runtime settings changed while the host is attached; forcing a gadget rebuild so the new descriptors take effect."
|
echo "⚠️ UVC runtime settings changed while the host is attached; forcing a gadget rebuild so the new descriptors take effect."
|
||||||
@ -676,7 +684,7 @@ ExecStart=/usr/local/bin/lesavka-uvc.sh
|
|||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
KillSignal=SIGTERM
|
KillSignal=SIGTERM
|
||||||
KillMode=process
|
KillMode=control-group
|
||||||
TimeoutStopSec=10
|
TimeoutStopSec=10
|
||||||
StandardError=append:/tmp/lesavka-uvc.stderr
|
StandardError=append:/tmp/lesavka-uvc.stderr
|
||||||
User=root
|
User=root
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.14.29"
|
version = "0.14.30"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,14 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
|||||||
SERVER_INSTALL.contains("validate_uvc_gadget_ready"),
|
SERVER_INSTALL.contains("validate_uvc_gadget_ready"),
|
||||||
"install script should verify that the UVC gadget comes back before declaring success"
|
"install script should verify that the UVC gadget comes back before declaring success"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
SERVER_INSTALL.contains("uvc_gadget_present"),
|
||||||
|
"install script should detect when the live gadget is missing the expected UVC function"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
SERVER_INSTALL.contains("UVC function is missing from the live gadget; forcing a rebuild before server start."),
|
||||||
|
"install script should force a rebuild when the live gadget is attached but missing UVC"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
SERVER_INSTALL.contains("video-output node did not appear after rebuild"),
|
SERVER_INSTALL.contains("video-output node did not appear after rebuild"),
|
||||||
"install script should explain why it refuses a half-applied UVC install"
|
"install script should explain why it refuses a half-applied UVC install"
|
||||||
@ -78,6 +86,10 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
|||||||
!SERVER_INSTALL.contains("RefuseManualStop=yes"),
|
!SERVER_INSTALL.contains("RefuseManualStop=yes"),
|
||||||
"install script should not generate a UVC unit that blocks legitimate refreshes"
|
"install script should not generate a UVC unit that blocks legitimate refreshes"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
SERVER_INSTALL.contains("KillMode=control-group"),
|
||||||
|
"install script should stop the whole UVC helper cgroup instead of leaving child processes behind"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
SERVER_INSTALL.contains("lesavka-uvc already active; runtime settings unchanged."),
|
SERVER_INSTALL.contains("lesavka-uvc already active; runtime settings unchanged."),
|
||||||
"install script should avoid unnecessary UVC restarts when nothing changed"
|
"install script should avoid unnecessary UVC restarts when nothing changed"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user