Compare commits

...

2 Commits

Author SHA1 Message Date
da6aa6c425 core: fallback when UVC fails 2026-01-05 14:09:52 -03:00
953f29dec7 nextcloud: integration with mailu 2025-12-14 14:20:56 -03:00
2 changed files with 130 additions and 117 deletions

View File

@ -1,38 +1,31 @@
# Repository Guidelines
## Project Structure & Module Organization
Rust is split into three crates: `server/` (gRPC relay and device control with async tests in `server/tests`), `client/` (GTK + GStreamer desktop agent; `src/input/` and `src/output/` hold HID and media pipelines), and `common/` (shared protobuf schema in `common/proto/lesavka.proto`, compiled via `build.rs`). `scripts/install/*.sh` provision Arch-based hosts, `scripts/manual/*.sh` run hardware diagnostics, and `scripts/daemon/lesavka-core.sh` handles USB gadget bring-up.
- `common/`: tonic/prost definitions; edit `proto/lesavka.proto` and let `build.rs` regenerate bindings.
- `server/`: gRPC relay and media handling; entrypoint `src/main.rs`; integration tests in `server/tests`.
- `client/`: GTK + GStreamer desktop agent; input adapters live in `src/input`, UVC output in `src/output`, helpers/tests in `src/tests`.
- `scripts/`: `install/` provisioning for Arch-based hosts, `daemon/lesavka-core.sh` systemd entrypoint, `manual/` diagnostics (VPN, USB reset, media capture).
## Build, Test, and Development Commands
- `cargo fmt --all` — run rustfmt across every crate before committing.
- `cargo clippy --workspace --all-targets -D warnings` — enforce lint cleanliness against desktop, server, and shared code.
- `cargo build --workspace --all-targets` — compile every binary/lib in debug mode; prefer `--release` for deployment artifacts referenced by install scripts.
- `cargo test --workspace` — executes unit tests plus async scenarios such as `server/tests/hid.rs`.
- `cargo run -p lesavka_server` / `cargo run -p lesavka_client` — start each side locally; set `LESAVKA_SERVER_ADDR` in the environment when pointing the client at a remote relay.
- Formatting: `cargo fmt --all --manifest-path client/Cargo.toml` (repeat for `server`/`common`).
- Linting: `cargo clippy --all-targets --manifest-path server/Cargo.toml -D warnings` (run for client too).
- Build: `cargo build --all-targets --manifest-path server/Cargo.toml` and `cargo build --all-targets --manifest-path client/Cargo.toml`; building either pulls `common`.
- Tests: `cargo test --manifest-path server/Cargo.toml` runs async RPC tests; `cargo test --manifest-path client/Cargo.toml` covers keymap/unit cases.
- Runtime: `cargo run --manifest-path server/Cargo.toml` to start the relay; `LESAVKA_SERVER_ADDR=<host:port> cargo run --manifest-path client/Cargo.toml` to point the client at a server.
- Provisioning: `scripts/install/server.sh` and `scripts/install/client.sh` install dependencies and systemd units; rerun after changing installers or binary names.
## Coding Style & Naming Conventions
Follow Rust 2024 idioms: four-space indentation, `snake_case` for modules/functions, and `CamelCase` for types and protobuf messages. Keep gRPC traits in `common` authoritative and prefer `tonic` streams over manual channels. Never hand-edit generated files in `target/` or the `OUT_DIR`; update `.proto` files and rebuild instead. Document hardware constants inline.
- Rust 2024 with four-space indentation; `snake_case` for functions/modules, `CamelCase` for types and Protobuf messages.
- Keep RPC shapes defined in `common/proto/lesavka.proto`; never hand-edit generated code in `target/` or `OUT_DIR`.
- Prefer tonic streaming APIs over manual channels; keep GStreamer/GTK wiring in small helpers to avoid sprawling `main.rs`.
- Use `tracing` consistently; default `RUST_LOG=info,lesavka=debug` for local runs.
## Testing Guidelines
Async tests should use `#[tokio::test(flavor = "multi_thread")]` when they spawn background servers, mirroring `hid_roundtrip`. Give tests descriptive names and keep them near the code they exercise (`client/src/tests/`, `server/tests/`). Cover new RPCs end-to-end by instantiating the tonic server in-process and asserting on gRPC clients.
- Async integration tests mirror `server/tests/hid.rs` using `#[tokio::test]` and an in-process tonic server/client pair.
- Place client unit tests next to modules (`client/src/tests`); name cases after behavior (`keycode_to_usage_maps_letters`, `hid_roundtrip`).
- For new RPCs, add end-to-end assertions for request/response, stream termination, and error paths; prefer deterministic timeouts over sleeps.
## Commit & Pull Request Guidelines
Recent history favors terse, imperative summaries (“usb fix”), so keep titles under ~50 characters and describe the subsystem. Commits should stay focused (proto update plus regeneration in one commit, client UI tweaks in another). Pull requests must include context, manual verification steps (`cargo test --workspace`, notable hardware checks), relevant logs or screenshots, and linked issues. Call out script or deployment changes so operators know when to re-run `scripts/install/server.sh` or update systemd units.
## Deployment Notes
For reproducible installs, prefer `scripts/install/server.sh --ref <branch>` to provision capture nodes (udev rules, GStreamer stack) and drop `lesavka-core.service`, while `scripts/install/client.sh` handles desktop prerequisites and systemd activation. When editing these scripts, test on an Arch VM. Keep secret material (VPN credentials, Tailscale auth) out of git and load them via environment variables or systemd drop-ins.
Install scripts must stay idempotent and self-contained: add any new runtime dependencies (e.g., audio/ALSA tools needed for mic bring-up) to the respective install script so a rerun on an arbitrary server/client brings the box to a ready state.
## Current Setup
- Server runs on Raspberry Pi 5 host `titan-jh` (ssh alias configured) and is already provisioned; client setup happens on this machine.
- HDMI capture uses two USB AVerMedia/GC311 devices with power/data split; AC relays on GPIO keep them off unless needed (control scripts already on the Pi).
- USB gadget exposes HID/UAC/UVC; webcam feed is expected from the client into the gadget UVC node (default `LESAVKA_UVC_DEV=/dev/video4`).
- Client ↔ server address: `http://38.28.125.112:50051` (was `64.25.10.31`).
- Webcam uplink is the remaining piece to confirm end-to-end after client install; server-side audio/video capture is in place.
## Session Notes (Dec 1, 2025)
- Server change pending deployment: `server/src/main.rs` now auto-picks UVC sink via `LESAVKA_UVC_DEV` or first `:video_output:` node (titan-jh gadget is `/dev/video0`); errors if none. `stream_microphone` honors `LESAVKA_UAC_DEV`.
- Client change: mic capture falls back to first non-`.monitor` Pulse source if `LESAVKA_MIC_SOURCE` missing/not found.
- Action: rerun `scripts/install/server.sh --ref feature/webcam-caps` on titan-jh (optionally set `LESAVKA_UVC_DEV=/dev/video0`, `LESAVKA_UAC_DEV=plughw:UAC2Gadget,0`), then restart service. Check `/tmp/lesavka-server.log` for “stream_camera using UVC sink”.
- Tethys display: SDDM running but greeter not; capture shows black+cursor. Need greeter/desktop on HDMI head (GC311 input) once display/cable is correct; investigate sddm greeter crash after reboot.
- Git history favors short, imperative subjects (`"install mic deps"`, `"client: improve mic defaults"`); keep each commit focused.
- Before opening a PR, run fmt, clippy, and tests for client and server; note any manual steps or env vars used (`LESAVKA_SERVER_ADDR`, `LESAVKA_UVC_DEV`, `LESAVKA_UAC_DEV`).
- PR descriptions should summarize intent, attach logs/screenshots when touching media paths, and flag installer/systemd changes so operators can re-run the relevant scripts.

View File

@ -8,6 +8,25 @@ set -euo pipefail
log() { printf '[lesavka-core] %s\n' "$*"; }
cleanup() { echo "" >"$G/UDC" 2>/dev/null || true; }
DISABLE_UAC=${LESAVKA_DISABLE_UAC:-}
DISABLE_UVC=${LESAVKA_DISABLE_UVC:-}
UVC_FALLBACK=${LESAVKA_UVC_FALLBACK:-1}
wait_for_enum() {
local tries=${1:-50} # 50 x 100ms = 5s
UDC_STATE="unknown"
UDC_SPEED="unknown"
for ((i=0; i<tries; i++)); do
UDC_STATE=$(cat "/sys/class/udc/$UDC/state" 2>/dev/null || echo "unknown")
UDC_SPEED=$(cat "/sys/class/udc/$UDC/current_speed" 2>/dev/null || echo "unknown")
if [[ "$UDC_STATE" != "not attached" && "$UDC_STATE" != "unknown" ]]; then
return 0
fi
sleep 0.1
done
return 1
}
exec 2> >(tee -a /tmp/lesavka-core.debug.$(date +%s).log)
set -x
echo "[lesavka-core] running: $0 (sha1sum=$(sha1sum "$0" | cut -d' ' -f1))"
@ -103,114 +122,102 @@ printf '\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00'\
'\x05\x01\x09\x30\x09\x31\x09\x38\x15\x81\x25\x7f\x75\x08\x95\x03\x81\x06'\
'\xc0\xc0' >"$G/functions/hid.usb1/report_desc"
# ---------- UAC2 function - speaker + mic, 2×48kHz stereo ---------
mkdir -p "$G/functions/uac2.usb0"
U="$G/functions/uac2.usb0"
# Playback (speaker)
echo 0x3 >"$U/p_chmask" # L+R
echo 48000 >"$U/p_srate"
echo 2 >"$U/p_ssize" # 16bit
# Capture (microphone)
echo 0x3 >"$U/c_chmask"
echo 48000 >"$U/c_srate"
echo 2 >"$U/c_ssize"
# Optional: allocate a few extra request buffers
echo 32 >"$U/req_number" 2>/dev/null || true
if [[ -z $DISABLE_UAC ]]; then
# ---------- UAC2 function - speaker + mic, 2×48kHz stereo ---------
mkdir -p "$G/functions/uac2.usb0"
U="$G/functions/uac2.usb0"
# Playback (speaker)
echo 0x3 >"$U/p_chmask" # L+R
echo 48000 >"$U/p_srate"
echo 2 >"$U/p_ssize" # 16bit
# Capture (microphone)
echo 0x3 >"$U/c_chmask"
echo 48000 >"$U/c_srate"
echo 2 >"$U/c_ssize"
# Optional: allocate a few extra request buffers
echo 32 >"$U/req_number" 2>/dev/null || true
else
log "🔇 UAC2 disabled (LESAVKA_DISABLE_UAC set)"
fi
# ----------------------- UVC function (usbvideo) ------------------
mkdir -p "$G/functions/uvc.usb0"
F="$G/functions/uvc.usb0"
if [[ -z $DISABLE_UVC ]]; then
# ----------------------- UVC function (usbvideo) ------------------
mkdir -p "$G/functions/uvc.usb0"
F="$G/functions/uvc.usb0"
# ── 1. FORMAT DESCRIPTOR (uncompressed YUY2, 16bpp) ──────────────
mkdir -p "$F/streaming/uncompressed/u"
# GUID = {59555932-0000-0010-8000-00aa00389b71} (“YUY2”) littleendian
printf '\x59\x55\x59\x32\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71' \
>"$F/streaming/uncompressed/u/guidFormat"
echo 16 >"$F/streaming/uncompressed/u/bBitsPerPixel"
# ── 1. FORMAT DESCRIPTOR (uncompressed YUY2, 16 bpp) ──────────────
mkdir -p "$F/streaming/uncompressed/yuyv"
# GUID = {59555932-0000-0010-8000-00aa00389b71} (“YUY2”) little-endian
printf '\x59\x55\x59\x32\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71' \
>"$F/streaming/uncompressed/yuyv/guidFormat"
echo 16 >"$F/streaming/uncompressed/yuyv/bBitsPerPixel"
# ── 2. FRAME DESCRIPTOR (index1 @30fps) ────────────────────────
mkdir -p "$F/streaming/uncompressed/u/f1"
echo 1280 >"$F/streaming/uncompressed/u/f1/wWidth"
echo 720 >"$F/streaming/uncompressed/u/f1/wHeight"
echo 1843200 >"$F/streaming/uncompressed/u/f1/dwMaxVideoFrameBufferSize"
echo 333333 >"$F/streaming/uncompressed/u/f1/dwDefaultFrameInterval" # 30fps
echo 333333 >"$F/streaming/uncompressed/u/f1/dwFrameInterval"
# ── 2. FRAME DESCRIPTOR (720p @ 30 fps) ───────────────────────────
mkdir -p "$F/streaming/uncompressed/yuyv/720p"
echo 1280 >"$F/streaming/uncompressed/yuyv/720p/wWidth"
echo 720 >"$F/streaming/uncompressed/yuyv/720p/wHeight"
echo 1843200 >"$F/streaming/uncompressed/yuyv/720p/dwMaxVideoFrameBufferSize"
echo 333333 >"$F/streaming/uncompressed/yuyv/720p/dwDefaultFrameInterval"
cat <<'EOF' >"$F/streaming/uncompressed/yuyv/720p/dwFrameInterval"
333333
EOF
# ── 3. REQUIRED HEADER LINKS (absolutepaths, no “1”) ──────────────
header_h="$F/streaming/header/h" # convenience variables
fmt_dir="$F/streaming/uncompressed/u"
# ── 3. REQUIRED HEADER LINKS (per UVC gadget docs) ────────────────
mkdir -p "$F/streaming/header/h"
pushd "$F/streaming/header/h" >/dev/null
ln -s ../../uncompressed/yuyv yuyv
popd >/dev/null
mkdir -p "$header_h"
# wait until the kernel has rebuilt the directory and added its attribute
# files (bmInfo is always created by the driver)
for _ in {1..50}; do
[ -e "$header_h/bmInfo" ] && break
sleep 0.010 # max 0.5s total
done
# ABSOLUTE symlink → no relative elements, name is “fmt” (not “1”)
ln -sf "$fmt_dir" "$header_h/fmt"
# echo 1 >"$header_h/bNumFormats"
# echo 0 >"$header_h/bmInfo"
# perspeed class directories (absolute links)
for s in fs hs ss; do
mkdir -p "$F/streaming/class/$s"
ln -sf "$header_h" "$F/streaming/class/$s/h"
done
for s in fs hs ss; do
mkdir -p "$F/streaming/class/$s"
pushd "$F/streaming/class/$s" >/dev/null
ln -s ../../header/h h
popd >/dev/null
done
# ── 4. VideoControl interface ─────────────────────────────────────
set +e # relax errors for configfs quirks
mkdir -p "$F/control/header/h" # real dir mandatory
mkdir -p "$F/control/class" # parent once
mkdir -p "$F/control/class/fs" "$F/control/class/hs" "$F/control/class/ss" 2>/dev/null || true
echo "[lesavka-core] ★ directory tree just before links:"
tree -L 3 "$F/control" | sed 's/^/[lesavka-core] /'
for s in fs hs ss; do
# best-effort: some UDCs reject certain speeds; skip on failure
if mkdir -p "$F/control/class/$s" 2>/dev/null; then
ln -snf "$F/control/header/h" "$F/control/class/$s/h" 2>/dev/null || \
log "⚠️ control/class/$s/h link missing (continuing)"
else
log "⚠️ skipping control/class/$s (mkdir failed)"
fi
done
for s in fs hs ss; do
[ -L "$F/control/class/$s/h" ] || log "⚠️ $s/h link missing (continuing)"
done
echo "[lesavka-core] ★ directory tree just before bind:"
tree -L 3 "$F/control" | sed 's/^/[lesavka-core] /'
for s in fs hs ss; do
[ -L "$F/control/class/$s" ] || log "⚠️ $s link missing (continuing)"
done
set -e # back to strict mode
mkdir -p "$F/control/header/h"
set +e
for s in fs hs ss; do
mkdir -p "$F/control/class/$s" 2>/dev/null || continue
pushd "$F/control/class/$s" >/dev/null
ln -s ../../header/h h 2>/dev/null || true
popd >/dev/null
done
set -e
# optional: hide unsupported controls
echo 0 >"$F/control/terminal/camera/default/bmControls" 2>/dev/null || true
echo 0 >"$F/control/processing/default/bmControls" 2>/dev/null || true
echo 0 >"$F/control/terminal/camera/default/bmControls" 2>/dev/null || true
echo 0 >"$F/control/processing/default/bmControls" 2>/dev/null || true
# friendly label
set +e
mkdir -p "$F/control/header/strings/0x409" 2>/dev/null || log "⚠️ skipping control/header strings (mkdir failed)"
echo "Lesavka UVC" >"$F/control/header/strings/0x409/label" 2>/dev/null || log "⚠️ unable to set UVC label (continuing)"
set -e
mkdir -p "$F/control/header/h/strings/0x409" 2>/dev/null || true
echo "Lesavka UVC" >"$F/control/header/h/strings/0x409/label" 2>/dev/null || true
else
log "📷 UVC disabled (LESAVKA_DISABLE_UVC set)"
fi
# ----------------------- configuration -----------------------------
mkdir -p "$G/configs/c.1/strings/0x409"
echo 500 > "$G/configs/c.1/MaxPower"
# echo "Config 1" > "$G/configs/c.1/strings/0x409/configuration"
echo "Config 1: HID + UAC2" >"$G/configs/c.1/strings/0x409/configuration"
config_label="Config 1: HID"
if [[ -z $DISABLE_UAC ]]; then
config_label+=" + UAC2"
fi
if [[ -z $DISABLE_UVC ]]; then
config_label+=" + UVC"
fi
echo "$config_label" >"$G/configs/c.1/strings/0x409/configuration"
ln -s $G/functions/hid.usb0 $G/configs/c.1/
ln -s $G/functions/hid.usb1 $G/configs/c.1/
ln -s $U $G/configs/c.1/
ln -s $G/functions/uvc.usb0 $G/configs/c.1/
if [[ -z $DISABLE_UAC ]]; then
ln -s $U $G/configs/c.1/
fi
if [[ -z $DISABLE_UVC ]]; then
ln -s $G/functions/uvc.usb0 $G/configs/c.1/
fi
# mkdir -p $G/functions/hid.usb0/os_desc
# mkdir -p $G/functions/hid.usb1/os_desc
@ -233,6 +240,19 @@ ln -s $G/functions/uvc.usb0 $G/configs/c.1/
# 4. Bind gadget
#──────────────────────────────────────────────────
echo "$UDC" >"$G/UDC"
log "🎉 gadget bound on $UDC (hidg0, hidg1, UAC2 L+R, UVC)"
parts="hidg0,hidg1"
[[ -z $DISABLE_UAC ]] && parts+=",UAC2"
[[ -z $DISABLE_UVC ]] && parts+=",UVC"
log "🎉 gadget bound on $UDC ($parts)"
if wait_for_enum 50; then
log "✅ UDC state is '$UDC_STATE' (speed=$UDC_SPEED)"
else
log "⚠️ UDC state is '$UDC_STATE' (speed=$UDC_SPEED). Host not enumerated."
if [[ -z $DISABLE_UVC && "$UVC_FALLBACK" != "0" ]]; then
log "♻️ retrying without UVC (LESAVKA_UVC_FALLBACK=0 to disable)"
exec env LESAVKA_DISABLE_UVC=1 LESAVKA_UVC_FALLBACK=0 "$0"
fi
fi
exit 0