diff --git a/Cargo.lock b/Cargo.lock index ea49221..2a7d1ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.13.7" +version = "0.13.8" dependencies = [ "anyhow", "async-stream", @@ -1676,7 +1676,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.13.7" +version = "0.13.8" dependencies = [ "anyhow", "base64", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.13.7" +version = "0.13.8" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 6d8b315..8882e15 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.13.7" +version = "0.13.8" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index a901e20..1f74b42 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.13.7" +version = "0.13.8" edition = "2024" build = "build.rs" diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 5618ab0..6203022 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -408,6 +408,7 @@ sudo install -Dm755 "$SRC_DIR/target/release/lesavka-server" /usr/local/bin/lesa sudo install -Dm755 "$SRC_DIR/target/release/lesavka-uvc" /usr/local/bin/lesavka-uvc sudo install -Dm755 "$SRC_DIR/scripts/daemon/lesavka-core.sh" /usr/local/bin/lesavka-core.sh sudo install -Dm755 "$SRC_DIR/scripts/daemon/lesavka-uvc.sh" /usr/local/bin/lesavka-uvc.sh +sudo install -Dm755 "$SRC_DIR/scripts/manual/run_uac_output_sanity.sh" /usr/local/bin/lesavka-uac-sanity echo "==> 5b. Runtime environment defaults" sudo install -d -m 0755 /etc/lesavka diff --git a/scripts/manual/run_uac_output_sanity.sh b/scripts/manual/run_uac_output_sanity.sh new file mode 100644 index 0000000..4f4d356 --- /dev/null +++ b/scripts/manual/run_uac_output_sanity.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# scripts/manual/run_uac_output_sanity.sh - play a short tone into the Theia UAC sink +set -euo pipefail + +if [[ ${EUID:-$(id -u)} -ne 0 ]]; then + echo "❌ run this as root so it uses the same UAC leg as lesavka-server." >&2 + exit 1 +fi + +SERVER_ENV=${LESAVKA_SERVER_ENV:-/etc/lesavka/server.env} +if [[ -r $SERVER_ENV ]]; then + set -a + # shellcheck disable=SC1090 + source "$SERVER_ENV" + set +a +fi + +DURATION_SECONDS=${LESAVKA_UAC_SANITY_SECONDS:-4} +TONE_FREQ=${LESAVKA_UAC_SANITY_FREQ:-880} +TONE_VOLUME=${LESAVKA_UAC_SANITY_VOLUME:-0.45} +BUFFER_TIME_US=${LESAVKA_UAC_BUFFER_TIME_US:-20000} +LATENCY_TIME_US=${LESAVKA_UAC_LATENCY_TIME_US:-5000} +REQUESTED_DEV=${LESAVKA_UAC_SANITY_DEV:-${LESAVKA_UAC_DEV:-hw:UAC2Gadget,0}} + +declare -A SEEN_CANDIDATES=() +CANDIDATES=() + +append_candidate() { + local candidate="${1:-}" + candidate=${candidate//[$'\r\n']/} + candidate=${candidate#"${candidate%%[![:space:]]*}"} + candidate=${candidate%"${candidate##*[![:space:]]}"} + [[ -n $candidate ]] || return 0 + if [[ -z ${SEEN_CANDIDATES[$candidate]+x} ]]; then + CANDIDATES+=("$candidate") + SEEN_CANDIDATES["$candidate"]=1 + fi +} + +append_candidate_family() { + local candidate="${1:-}" + [[ -n ${candidate//[[:space:]]/} ]] || return 0 + append_candidate "$candidate" + if [[ $candidate == hw:* ]]; then + append_candidate "plughw:${candidate#hw:}" + elif [[ $candidate == plughw:* ]]; then + append_candidate "hw:${candidate#plughw:}" + fi +} + +discover_uac_numeric_candidates() { + python3 - <<'PY' +import re +import subprocess + +try: + output = subprocess.check_output(["aplay", "-l"], text=True, stderr=subprocess.DEVNULL) +except Exception: + raise SystemExit(0) + +seen = set() +for line in output.splitlines(): + match = re.search(r"^card\s+(\d+):.*device\s+(\d+):", line) + if not match: + continue + lowered = line.lower() + if not any(token in lowered for token in ("uac", "lesavka", "composite", "usb audio")): + continue + candidate = f"hw:{match.group(1)},{match.group(2)}" + if candidate not in seen: + print(candidate) + seen.add(candidate) +PY +} + +build_candidates() { + while IFS= read -r candidate; do + append_candidate_family "$candidate" + done < <(discover_uac_numeric_candidates) + + append_candidate_family "$REQUESTED_DEV" + + for alias in \ + "hw:UAC2Gadget,0" \ + "hw:UAC2_Gadget,0" \ + "hw:Composite,0" \ + "hw:Lesavka,0" + do + append_candidate_family "$alias" + done +} + +run_tone() { + local candidate="$1" + echo "🎧 trying UAC playback device: $candidate" >&2 + set +e + timeout --signal=INT "${DURATION_SECONDS}s" \ + gst-launch-1.0 -q \ + audiotestsrc wave=sine freq="$TONE_FREQ" volume="$TONE_VOLUME" is-live=true \ + ! audio/x-raw,format=S16LE,channels=2,rate=48000 \ + ! audioconvert \ + ! audioresample \ + ! alsasink \ + device="$candidate" \ + sync=false \ + async=false \ + provide-clock=false \ + enable-last-sample=false \ + buffer-time="$BUFFER_TIME_US" \ + latency-time="$LATENCY_TIME_US" + local rc=$? + set -e + if [[ $rc -eq 0 || $rc -eq 124 ]]; then + echo "✅ UAC tone completed on $candidate" >&2 + return 0 + fi + echo "⚠️ tone failed on $candidate (rc=$rc)" >&2 + return "$rc" +} + +build_candidates +if [[ ${#CANDIDATES[@]} -eq 0 ]]; then + echo "❌ no UAC playback candidates found. Check 'aplay -l' and /etc/lesavka/server.env." >&2 + exit 1 +fi + +echo "🎵 running Theia UAC sanity tone for ${DURATION_SECONDS}s at ${TONE_FREQ} Hz" >&2 +printf ' candidates: %s\n' "${CANDIDATES[*]}" >&2 +for candidate in "${CANDIDATES[@]}"; do + if run_tone "$candidate"; then + exit 0 + fi +done + +echo "❌ none of the UAC playback candidates accepted the sanity tone." >&2 +echo " requested device: $REQUESTED_DEV" >&2 +echo " current aplay -l:" >&2 +aplay -l >&2 || true +exit 1 diff --git a/server/Cargo.toml b/server/Cargo.toml index 2f4b86f..99edbf2 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.13.7" +version = "0.13.8" edition = "2024" autobins = false diff --git a/testing/tests/server_install_script_contract.rs b/testing/tests/server_install_script_contract.rs index 6c19772..b114a29 100644 --- a/testing/tests/server_install_script_contract.rs +++ b/testing/tests/server_install_script_contract.rs @@ -31,3 +31,15 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_HEIGHT:-1080}")); assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_SINK:-fbdevsink}")); } + +#[test] +fn server_install_keeps_uac_sanity_helper_available() { + assert!( + SERVER_INSTALL.contains("run_uac_output_sanity.sh"), + "install script should ship the root UAC sanity helper" + ); + assert!( + SERVER_INSTALL.contains("/usr/local/bin/lesavka-uac-sanity"), + "install script should install the UAC sanity helper to a stable path" + ); +}