diff --git a/scripts/manual/eval_lesavka.sh b/scripts/manual/eval_lesavka.sh new file mode 100755 index 0000000..60bddba --- /dev/null +++ b/scripts/manual/eval_lesavka.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# scripts/manual/eval_lesavka.sh - iterative health check for lesavka client/server/gadget +# - Locally: probes TCP + gRPC handshake on LESAVKA_SERVER_ADDR +# - Optional: if TETHYS_HOST is set, ssh to run lsusb + dmesg tail (enumeration check) +# - Optional: if THEIA_HOST is set, ssh to show core/server status + hidg/uvc presence +# +# Env: +# LESAVKA_SERVER_ADDR (default http://38.28.125.112:50051) +# ITER=0 (loop forever) or number of iterations +# SLEEP=10 (seconds between iterations) +# TETHYS_HOST=host (ssh target for target machine; requires key auth) +# THEIA_HOST=host (ssh target for server/gadget Pi) +# SSH_OPTS="-o ConnectTimeout=5" (optional extra ssh flags) + +set -euo pipefail + +SERVER=${LESAVKA_SERVER_ADDR:-http://38.28.125.112:50051} +# default to a few iterations instead of infinite to avoid unintentional long runs +ITER=${ITER:-5} +SLEEP=${SLEEP:-10} +SSH_OPTS=${SSH_OPTS:-"-o ConnectTimeout=5 -o BatchMode=yes"} +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +PROTO_DIR="${PROTO_DIR:-${SCRIPT_DIR}/../../common/proto}" + +hostport=${SERVER#http://} +hostport=${hostport#https://} +host=${hostport%%:*} +port=${hostport##*:} + +has_nc() { command -v nc >/dev/null 2>&1; } +has_grpc() { command -v grpcurl >/dev/null 2>&1; } + +probe_server() { + echo "==> [local] $(date -Is) probing $SERVER" + if has_nc; then + if nc -zw3 "$host" "$port"; then + echo " tcp: OK (port reachable)" + else + echo " tcp: FAIL (port unreachable)" + fi + else + echo " tcp: skipped (nc not present)" + fi + + if has_grpc; then + if [[ -f "${PROTO_DIR}/lesavka.proto" ]]; then + if out=$(grpcurl -plaintext -max-time 5 \ + -import-path "${PROTO_DIR}" -proto lesavka.proto \ + "$host:$port" lesavka.Handshake/GetCapabilities 2>&1); then + echo " gRPC Handshake (proto): OK → $out" + else + echo " gRPC Handshake (proto): FAIL → $out" + fi + else + if out=$(grpcurl -plaintext -max-time 5 "$host:$port" list 2>&1); then + echo " gRPC list (reflection): $out" + else + echo " gRPC list (reflection) FAIL → $out" + fi + fi + else + echo " gRPC: skipped (grpcurl not present)" + fi +} + +probe_tethys() { + [[ -z "${TETHYS_HOST:-}" ]] && return + echo "==> [tethys] $(date -Is) checking lsusb + dmesg tail on $TETHYS_HOST" + ssh $SSH_OPTS "$TETHYS_HOST" ' + lsusb; + echo "--- /dev/hidraw* ---"; + ls /dev/hidraw* 2>/dev/null || true; + echo "--- dmesg (USB tail) ---"; + dmesg | tail -n 20 + ' || echo " ssh to $TETHYS_HOST failed" +} + +probe_theia() { + [[ -z "${THEIA_HOST:-}" ]] && return + echo "==> [theia] $(date -Is) checking services/device nodes on $THEIA_HOST" + ssh $SSH_OPTS "$THEIA_HOST" ' + systemctl --no-pager --quiet is-active lesavka-core && echo "lesavka-core: active" || echo "lesavka-core: INACTIVE"; + systemctl --no-pager --quiet is-active lesavka-server && echo "lesavka-server: active" || echo "lesavka-server: INACTIVE"; + echo "--- hidg nodes ---"; + ls -l /dev/hidg0 /dev/hidg1 2>/dev/null || true; + echo "--- video nodes ---"; + ls -l /dev/video* 2>/dev/null | head; + echo "--- recent server log ---"; + journalctl -u lesavka-server -n 20 --no-pager + ' || echo " ssh to $THEIA_HOST failed" +} + +count=0 +while :; do + probe_server + probe_theia + probe_tethys + + count=$((count + 1)) + if [[ "$ITER" -gt 0 && "$count" -ge "$ITER" ]]; then + break + fi + echo "==> sleeping ${SLEEP}s (iteration $count complete)" + sleep "$SLEEP" +done + +echo "Done." diff --git a/scripts/manual/kde-start-tethys.sh b/scripts/manual/kde-start-tethys.sh new file mode 100755 index 0000000..d73999a --- /dev/null +++ b/scripts/manual/kde-start-tethys.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# scripts/manual/kde-start-tethys.sh +# +# Start/restart SDDM on tethys and set display geometry over :0. +# Intended for remote use after SSH-ing into tethys. +# +# Env overrides: +# MODE=1920x1080 (preferred mode) +# RATE=60 (refresh rate) +# OUTPUTS="HDMI-1 DP-1" (space-separated outputs to try) +# DISPLAY=:0 (X display; default :0) +# XAUTHORITY=... (override cookie; otherwise auto-detected from SDDM) + +set -euo pipefail + +MODE=${MODE:-1920x1080} +RATE=${RATE:-60} +OUTPUTS=${OUTPUTS:-"HDMI-1 DP-1"} +DISPLAY=${DISPLAY:-:0} + +log() { printf "[kde-start] %s\n" "$*"; } + +log "restarting sddm.service" +sudo systemctl restart sddm +sleep 2 + +# find SDDM Xauthority if not provided +if [[ -z "${XAUTHORITY:-}" ]]; then + XAUTHORITY=$(ls /var/run/sddm/*/xauth_* 2>/dev/null | head -n1 || true) +fi + +if [[ -z "${XAUTHORITY:-}" ]]; then + log "warning: no XAUTHORITY found; xrandr may fail" +fi + +# wait for X to come up +for attempt in {1..15}; do + if DISPLAY=$DISPLAY XAUTHORITY=${XAUTHORITY:-} xrandr --query >/dev/null 2>&1; then + break + fi + sleep 1 +done + +log "setting mode ${MODE}@${RATE} on outputs: ${OUTPUTS}" +for out in $OUTPUTS; do + if DISPLAY=$DISPLAY XAUTHORITY=${XAUTHORITY:-} xrandr --output "$out" --mode "$MODE" --rate "$RATE" --primary >/dev/null 2>&1; then + log "set $out to ${MODE}@${RATE}" + else + log "skip $out (xrandr failed)" + fi +done + +log "current xrandr:" +DISPLAY=$DISPLAY XAUTHORITY=${XAUTHORITY:-} xrandr --query || true + +log "done." diff --git a/scripts/manual/usb-reset.sh b/scripts/manual/usb-reset.sh index a9b832a..4eb0c83 100755 --- a/scripts/manual/usb-reset.sh +++ b/scripts/manual/usb-reset.sh @@ -1,10 +1,14 @@ #!/usr/bin/env bash -# scripts/manual/usb-reset.sh +# scripts/manual/usb-reset.sh - trigger USB reset RPC on the server +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +PROTO_DIR="${SCRIPT_DIR}/../../common/proto" grpcurl \ -plaintext \ - -import-path ./../../common/proto \ + -import-path "${PROTO_DIR}" \ -proto lesavka.proto \ -d '{}' \ - 64.25.10.31:50051 \ + 38.28.125.112:50051 \ lesavka.Relay/ResetUsb diff --git a/server/src/main.rs b/server/src/main.rs index e4b4e63..9a774a0 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -89,6 +89,37 @@ fn next_minute() -> SystemTime { UNIX_EPOCH + Duration::from_secs(next) } +/// Pick the UVC gadget video node. +/// Priority: 1) `LESAVKA_UVC_DEV` override; 2) first `video_output` node. +/// Returns an error when nothing matches instead of guessing a capture card. +fn pick_uvc_device() -> anyhow::Result { + if let Ok(path) = std::env::var("LESAVKA_UVC_DEV") { + return Ok(path); + } + + // walk /dev/video* via udev and look for an output‑capable node (gadget exposes one) + if let Ok(mut en) = udev::Enumerator::new() { + let _ = en.match_subsystem("video4linux"); + if let Ok(devs) = en.scan_devices() { + for dev in devs { + let caps = dev + .property_value("ID_V4L_CAPABILITIES") + .and_then(|v| v.to_str()) + .unwrap_or_default(); + if caps.contains(":video_output:") { + if let Some(node) = dev.devnode() { + return Ok(node.to_string_lossy().into_owned()); + } + } + } + } + } + + Err(anyhow::anyhow!( + "no video_output v4l2 node found; set LESAVKA_UVC_DEV" + )) +} + /*──────────────── Handler ───────────────────*/ struct Handler { kb: Arc>, @@ -186,7 +217,9 @@ impl Relay for Handler { req: Request>, ) -> Result, Status> { // 1 ─ build once, early - let mut sink = audio::Voice::new("hw:UAC2Gadget,0") + let uac_dev = std::env::var("LESAVKA_UAC_DEV").unwrap_or_else(|_| "hw:UAC2Gadget,0".into()); + info!(%uac_dev, "🎤 stream_microphone using UAC sink"); + let mut sink = audio::Voice::new(&uac_dev) .await .map_err(|e| Status::internal(format!("{e:#}")))?; @@ -218,7 +251,8 @@ impl Relay for Handler { req: Request>, ) -> Result, Status> { // map gRPC camera id → UVC device - let uvc = std::env::var("LESAVKA_UVC_DEV").unwrap_or_else(|_| "/dev/video4".into()); + let uvc = pick_uvc_device().map_err(|e| Status::internal(format!("{e:#}")))?; + info!(%uvc, "🎥 stream_camera using UVC sink"); // build once let relay =