lesavka/scripts/install/server.sh

724 lines
24 KiB
Bash
Raw Normal View History

2025-06-01 13:31:22 -05:00
#!/usr/bin/env bash
2025-06-27 22:51:50 -05:00
# scripts/install/server.sh - install and setup all server related apps and environments
2025-06-01 13:31:22 -05:00
set -euo pipefail
2025-06-01 21:30:55 -05:00
ORIG_USER=${SUDO_USER:-$(id -un)}
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
SCRIPT_REPO_ROOT=$(cd -- "$SCRIPT_DIR/../.." && pwd)
DEFAULT_REPO_URL=ssh://git@scm.bstein.dev:2242/bstein/lesavka.git
2026-04-16 13:54:25 -03:00
export TMPDIR=${TMPDIR:-/var/tmp}
2025-06-01 21:30:55 -05:00
2025-07-04 18:24:48 -05:00
REF=${LESAVKA_REF:-master} # fallback
REPO_URL=${LESAVKA_REPO_URL:-}
USER_HOME=$(getent passwd "$ORIG_USER" | cut -d: -f6)
INSTALL_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-mjpeg}
2025-07-04 18:24:48 -05:00
manifest_package_version() {
local manifest=$1
[[ -f $manifest ]] || return 1
awk -F'"' '
$0 ~ /^\[package\]/ { in_package=1; next }
in_package && $0 ~ /^\[/ { exit }
in_package && $0 ~ /^[[:space:]]*version[[:space:]]*=/ { print $2; exit }
' "$manifest"
}
render_uvc_env_file() {
cat <<EOF
# generated by lesavka/scripts/install/server.sh
# Edit only for local UVC hardware overrides; rerunning the installer refreshes defaults.
LESAVKA_UVC_DEBUG=${LESAVKA_UVC_DEBUG:-1}
LESAVKA_UVC_MAXPACKET=${LESAVKA_UVC_MAXPACKET:-256}
LESAVKA_UVC_LIMIT_PCT=${LESAVKA_UVC_LIMIT_PCT:-100}
LESAVKA_UVC_FPS=${LESAVKA_UVC_FPS:-20}
LESAVKA_UVC_INTERVAL=${LESAVKA_UVC_INTERVAL:-500000}
LESAVKA_UVC_WIDTH=${LESAVKA_UVC_WIDTH:-640}
LESAVKA_UVC_HEIGHT=${LESAVKA_UVC_HEIGHT:-480}
LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}
LESAVKA_UVC_BLOCKING=${LESAVKA_UVC_BLOCKING:-1}
LESAVKA_UVC_MAXBURST=${LESAVKA_UVC_MAXBURST:-0}
EOF
}
find_uvc_output_node() {
local by_path_root=/dev/v4l/by-path
local ctrl=""
ctrl=$(ls /sys/class/udc 2>/dev/null | head -n1 || true)
if [[ -n $ctrl && -e "$by_path_root/platform-$ctrl-video-index0" ]]; then
printf '%s\n' "$by_path_root/platform-$ctrl-video-index0"
return 0
fi
local candidate
shopt -s nullglob
for candidate in "$by_path_root"/platform-*-video-index0; do
printf '%s\n' "$candidate"
shopt -u nullglob
return 0
done
shopt -u nullglob
return 1
}
wait_for_uvc_output_node() {
local node=""
for _ in {1..50}; do
if node=$(find_uvc_output_node); then
printf '%s\n' "$node"
return 0
fi
sleep 0.1
done
return 1
}
validate_uvc_gadget_ready() {
if [[ -n ${LESAVKA_DISABLE_UVC:-} ]]; then
return 0
fi
if [[ ! -d /sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0 ]]; then
echo "❌ UVC gadget function is missing after rebuild; refusing to continue with a half-applied install." >&2
return 1
fi
local node=""
if ! node=$(wait_for_uvc_output_node); then
echo "❌ UVC gadget video-output node did not appear after rebuild; refusing to continue." >&2
return 1
fi
echo "✅ UVC gadget output ready at ${node}"
}
2026-01-08 23:58:19 -03:00
udc_state() {
local udc=""
udc=$(ls /sys/class/udc 2>/dev/null | head -n1 || true)
if [[ -z $udc ]]; then
echo "unknown"
return 0
fi
cat "/sys/class/udc/$udc/state" 2>/dev/null || echo "unknown"
}
is_attached_state() {
case "$1" in
configured|addressed|default|suspended|unknown)
return 0
;;
esac
return 1
}
normalize_hdmi_connector() {
local name="$1"
if [[ $name =~ (HDMI-A-[0-9]+)$ ]]; then
printf '%s\n' "${BASH_REMATCH[1]}"
else
printf '%s\n' "$name"
fi
}
read_existing_hdmi_connector() {
local line value
line=$(grep -E '^LESAVKA_HDMI_CONNECTOR=' /etc/lesavka/server.env 2>/dev/null | tail -n1 || true)
[[ -n $line ]] || return 0
value=${line#*=}
value=${value%\"}
value=${value#\"}
value=${value%\'}
value=${value#\'}
normalize_hdmi_connector "$value"
}
boot_config_path() {
for path in /boot/config.txt /boot/firmware/config.txt; do
if [[ -e $path ]]; then
printf '%s\n' "$path"
return 0
fi
done
return 0
}
boot_cmdline_path() {
for path in /boot/cmdline.txt /boot/firmware/cmdline.txt; do
if [[ -e $path ]]; then
printf '%s\n' "$path"
return 0
fi
done
return 0
}
runtime_forced_hdmi_connectors() {
[[ -r /proc/cmdline ]] || return 0
tr ' ' '\n' </proc/cmdline |
sed -n 's/^video=\(HDMI-A-[0-9]\+\):.*e$/\1/p' |
sort -u
}
remove_lesavka_hdmi_config_force() {
local cfg tmp
cfg=$(boot_config_path)
[[ -n $cfg ]] || return 0
tmp=$(mktemp)
awk '
/^# lesavka: force HDMI/ {next}
/^hdmi_force_hotplug:[0-9]+=1$/ {next}
/^hdmi_group:[0-9]+=2$/ {next}
/^hdmi_mode:[0-9]+=82$/ {next}
{print}
' "$cfg" >"$tmp"
sudo install -m 0644 "$tmp" "$cfg"
rm -f "$tmp"
}
write_hdmi_cmdline_force() {
local connector="${1:-}" cmd token tmp line
cmd=$(boot_cmdline_path)
[[ -n $cmd ]] || return 0
tmp=$(mktemp)
line=$(cat "$cmd")
for token in $line; do
case "$token" in
video=HDMI-A-*:1920x1080@60e)
continue
;;
esac
printf '%s ' "$token" >>"$tmp"
done
if [[ -n $connector ]]; then
printf 'video=%s:1920x1080@60e ' "$connector" >>"$tmp"
fi
sed -i 's/[[:space:]]*$//' "$tmp"
printf '\n' >>"$tmp"
sudo install -m 0644 "$tmp" "$cmd"
rm -f "$tmp"
}
clear_lesavka_hdmi_boot_force() {
remove_lesavka_hdmi_config_force
write_hdmi_cmdline_force ""
}
write_hdmi_boot_force() {
local connector="$1" idx cfg
connector=$(normalize_hdmi_connector "$connector")
if [[ ! $connector =~ ^HDMI-A-([0-9]+)$ ]]; then
echo "⚠️ cannot write HDMI boot force for unexpected connector '$connector'." >&2
return 0
fi
idx=$((BASH_REMATCH[1] - 1))
cfg=$(boot_config_path)
remove_lesavka_hdmi_config_force
if [[ -n $cfg ]]; then
{
echo
echo "# lesavka: force HDMI mode for capture dongle ($connector)"
printf 'hdmi_force_hotplug:%s=1\n' "$idx"
printf 'hdmi_group:%s=2\n' "$idx"
printf 'hdmi_mode:%s=82\n' "$idx"
} | sudo tee -a "$cfg" >/dev/null
fi
write_hdmi_cmdline_force "$connector"
}
prepare_hdmi_detection_boot_state() {
local -a forced=()
if [[ -n ${LESAVKA_HDMI_CONNECTOR:-} ]]; then
return 0
fi
mapfile -t forced < <(runtime_forced_hdmi_connectors)
if (( ${#forced[@]} <= 1 )); then
return 0
fi
echo "⚠️ current boot forces multiple HDMI connectors: ${forced[*]}" >&2
echo " Linux will report every forced connector as connected, so auto-detection cannot be trusted yet." >&2
clear_lesavka_hdmi_boot_force
echo "✅ removed Lesavka-managed dual-HDMI force settings from boot config." >&2
echo " Reboot Theia, then rerun this installer so it can see the real connected HDMI port." >&2
echo " If both ports are physically connected after reboot, rerun once with LESAVKA_HDMI_CONNECTOR=HDMI-A-N." >&2
exit 1
}
describe_hdmi_connectors() {
local dev name status id
for dev in /sys/class/drm/card*-HDMI-A-*; do
[[ -e $dev/status ]] || continue
name=$(basename "$dev")
status=$(cat "$dev/status" 2>/dev/null || true)
id=$(cat "$dev/connector_id" 2>/dev/null || true)
printf ' %s (%s): %s\n' "$(normalize_hdmi_connector "$name")" "${id:-no-id}" "${status:-unknown}" >&2
done
}
detect_connected_hdmi_connector() {
local dev name stable_name status score
local best="" best_score=0 best_count=0
declare -A scores=()
# HDMI status can briefly flap during gadget/display bring-up, so sample a
# few times. Store the logical connector suffix, not the DRM card prefix, so
# this stays valid if Linux renumbers cardN across boots.
for _ in 1 2 3 4 5; do
for dev in /sys/class/drm/card*-HDMI-A-*; do
[[ -e $dev/status ]] || continue
name=$(basename "$dev")
stable_name=$(normalize_hdmi_connector "$name")
status=$(cat "$dev/status" 2>/dev/null || true)
if [[ $status == connected ]]; then
scores[$stable_name]=$(( ${scores[$stable_name]:-0} + 1 ))
fi
done
sleep 0.2
done
for name in "${!scores[@]}"; do
score=${scores[$name]}
if (( score > best_score )); then
best=$name
best_score=$score
fi
done
(( best_score > 0 )) || return 0
for name in "${!scores[@]}"; do
if (( scores[$name] == best_score )); then
best_count=$((best_count + 1))
fi
done
if (( best_count == 1 )); then
printf '%s\n' "$best"
return 0
fi
echo "❌ multiple HDMI connectors are equally connected; refusing to guess." >&2
describe_hdmi_connectors
echo " Disconnect the non-target HDMI output or run once with LESAVKA_HDMI_CONNECTOR=HDMI-A-N." >&2
return 1
}
resolve_hdmi_connector() {
local detected existing
if [[ -n ${LESAVKA_HDMI_CONNECTOR:-} ]]; then
normalize_hdmi_connector "$LESAVKA_HDMI_CONNECTOR"
return 0
fi
if ! detected=$(detect_connected_hdmi_connector); then
return 1
fi
if [[ -n $detected ]]; then
printf '%s\n' "$detected"
return 0
fi
existing=$(read_existing_hdmi_connector)
if [[ -n $existing ]]; then
echo "⚠️ no connected HDMI connector detected; preserving existing LESAVKA_HDMI_CONNECTOR=$existing." >&2
printf '%s\n' "$existing"
fi
}
run_as_user() {
sudo -u "$ORIG_USER" env HOME="$USER_HOME" SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-}" "$@"
}
CAPTURE_DISCOVERY_RELAY_PRESENT=0
CAPTURE_DISCOVERY_RELAY_WAS_ACTIVE=0
CAPTURE_DISCOVERY_POWER_BORROWED=0
prepare_capture_power_for_discovery() {
if ! systemctl list-unit-files | grep -q '^relay.service'; then
return 0
fi
CAPTURE_DISCOVERY_RELAY_PRESENT=1
if systemctl is-active --quiet relay.service; then
CAPTURE_DISCOVERY_RELAY_WAS_ACTIVE=1
return 0
fi
echo " ↪ borrowing relay GPIO power for capture discovery"
sudo systemctl start relay.service
CAPTURE_DISCOVERY_POWER_BORROWED=1
sudo udevadm settle --timeout=10 || true
sleep 2
}
restore_capture_power_after_discovery() {
if [ "${CAPTURE_DISCOVERY_RELAY_PRESENT:-0}" -eq 0 ]; then
return 0
fi
if [ "${CAPTURE_DISCOVERY_POWER_BORROWED:-0}" -eq 0 ]; then
return 0
fi
if [ "${CAPTURE_DISCOVERY_RELAY_WAS_ACTIVE:-0}" -ne 0 ]; then
return 0
fi
echo " ↪ returning relay GPIO power control to Lesavka auto mode"
sudo systemctl stop relay.service || true
CAPTURE_DISCOVERY_POWER_BORROWED=0
}
2025-07-04 18:24:48 -05:00
while [[ $# -gt 0 ]]; do
case $1 in
-r|--ref) REF="$2"; shift 2 ;;
-h|--help)
echo "Usage: $0 [--ref <branch|commit>]"; exit 0 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
echo "==> Using git ref: $REF"
2026-04-16 13:54:25 -03:00
mkdir -p "$TMPDIR"
2025-07-04 18:24:48 -05:00
if [[ -z $REPO_URL ]] && [[ -d $SCRIPT_REPO_ROOT/.git ]]; then
REPO_URL=$(git -C "$SCRIPT_REPO_ROOT" config --get remote.origin.url || true)
fi
REPO_URL=${REPO_URL:-$DEFAULT_REPO_URL}
2025-06-23 00:26:02 -05:00
echo "==> 1a. Base packages"
2026-04-16 13:54:25 -03:00
sudo pacman -Sq --needed --noconfirm git \
2025-06-29 22:57:54 -05:00
rustup \
protobuf \
abseil-cpp \
2025-06-29 22:57:54 -05:00
gcc \
alsa-utils \
2025-06-29 22:57:54 -05:00
pipewire \
pipewire-pulse \
tailscale \
base-devel \
2025-11-30 16:16:03 -03:00
v4l-utils \
2025-06-29 22:57:54 -05:00
gstreamer \
gst-plugins-base \
2025-06-30 02:03:12 -05:00
gst-plugins-base-libs \
2025-06-29 22:57:54 -05:00
gst-plugins-good \
gst-plugins-bad \
2025-06-30 02:03:12 -05:00
gst-plugins-bad-libs \
2025-06-29 22:57:54 -05:00
gst-plugins-ugly \
2025-06-30 22:27:49 -05:00
gst-libav \
2025-07-01 10:23:51 -05:00
tcpdump \
lsof
2025-06-01 21:30:55 -05:00
if ! command -v yay >/dev/null 2>&1; then
2025-06-23 00:26:02 -05:00
echo "==> 1b. installing yay from AUR ..."
run_as_user env TMPDIR="$TMPDIR" bash -c '
2026-04-16 13:54:25 -03:00
rm -rf "$TMPDIR/yay" &&
cd "$TMPDIR" && git clone --depth 1 https://aur.archlinux.org/yay.git &&
2025-06-01 21:30:55 -05:00
cd yay && makepkg -si --noconfirm'
2025-06-01 16:04:00 -05:00
fi
2025-06-28 00:05:13 -05:00
# yay -S --noconfirm grpcurl-bin
2025-06-01 14:18:42 -05:00
2025-11-30 18:01:54 -03:00
echo "==> 1c. GPIO permissions for relay"
echo 'z /dev/gpiochip* 0660 root gpio -' | sudo tee /etc/tmpfiles.d/gpiochip.conf >/dev/null
sudo systemd-tmpfiles --create /etc/tmpfiles.d/gpiochip.conf || true
2026-04-24 22:43:25 -03:00
echo "==> 1d. Audio permissions for diagnostics"
if getent group audio >/dev/null 2>&1 && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
sudo usermod -aG audio "${SUDO_USER}" || true
fi
2025-06-28 15:45:35 -05:00
echo "==> 2a. Kernel-driver tweaks"
2025-06-23 00:26:02 -05:00
cat <<'EOF' | sudo tee /etc/modprobe.d/gc311-stream.conf >/dev/null
options uvcvideo quirks=0x200 timeout=10000
EOF
prepare_hdmi_detection_boot_state
2025-06-23 00:26:02 -05:00
echo "==> 2b. Predictable /dev names for each capture card"
prepare_capture_power_for_discovery
trap restore_capture_power_after_discovery EXIT
2025-11-30 16:16:03 -03:00
discover_gc_capture_pairs() {
for dev in /dev/video*; do
props=$(sudo udevadm info -q property -n "$dev" 2>/dev/null || true)
index=$(cat "/sys/class/video4linux/$(basename "$dev")/index" 2>/dev/null || true)
if echo "$props" | grep -q 'ID_VENDOR_ID=07ca' \
&& echo "$props" | grep -q 'ID_MODEL_ID=3311' \
&& [ "$index" = "0" ]; then
tag=$(printf '%s\n' "$props" | awk -F= '/^ID_PATH_TAG=/{print $2}')
if [ -n "$tag" ]; then
printf '%s %s\n' "$tag" "$dev"
2025-11-30 16:16:03 -03:00
fi
fi
done | sort -u
}
mapfile -t GC_CAPTURE_PAIRS < <(discover_gc_capture_pairs)
if [ "${#GC_CAPTURE_PAIRS[@]}" -ne 2 ]; then
for _ in {1..20}; do
sleep 0.5
mapfile -t GC_CAPTURE_PAIRS < <(discover_gc_capture_pairs)
[ "${#GC_CAPTURE_PAIRS[@]}" -eq 2 ] && break
done
fi
2025-11-30 16:16:03 -03:00
if [ "${#GC_CAPTURE_PAIRS[@]}" -ne 2 ]; then
echo "⚠️ GC311 capture cards not fully present; skipping udev eye-link refresh." >&2
if [ "${#GC_CAPTURE_PAIRS[@]}" -eq 0 ]; then
echo " Detected: none" >&2
else
printf ' Detected: %s\n' "${GC_CAPTURE_PAIRS[@]}" >&2
fi
echo " The server install will continue, and existing /dev/lesavka_* links stay untouched." >&2
else
LEFT_TAG=${GC_CAPTURE_PAIRS[0]%% *}
LEFT_DEV=${GC_CAPTURE_PAIRS[0]#* }
RIGHT_TAG=${GC_CAPTURE_PAIRS[1]%% *}
RIGHT_DEV=${GC_CAPTURE_PAIRS[1]#* }
if [ -z "$LEFT_TAG" ] || [ -z "$RIGHT_TAG" ] || [ "$LEFT_TAG" = "$RIGHT_TAG" ]; then
echo "⚠️ GC311 cards were detected, but the capture path tags are incomplete or duplicated." >&2
printf ' Left candidate: %s\n' "${GC_CAPTURE_PAIRS[0]:-missing}" >&2
printf ' Right candidate: %s\n' "${GC_CAPTURE_PAIRS[1]:-missing}" >&2
echo " Skipping udev eye-link refresh and preserving any existing /dev/lesavka_* links." >&2
else
printf ' ↪ Left card: %s (%s)\n' "$LEFT_DEV" "$LEFT_TAG"
printf ' ↪ Right card: %s (%s)\n' "$RIGHT_DEV" "$RIGHT_TAG"
2025-06-25 07:46:50 -05:00
sudo tee /etc/udev/rules.d/85-gc311.rules >/dev/null <<EOF
2025-06-27 22:51:50 -05:00
# auto-generated by lesavka/scripts/daemon/install-server.sh - DO NOT EDIT
2025-06-28 00:24:23 -05:00
SUBSYSTEM=="video4linux", ATTR{index}=="0", ENV{ID_PATH_TAG}=="$LEFT_TAG", SYMLINK+="lesavka_l_eye"
SUBSYSTEM=="video4linux", ATTR{index}=="0", ENV{ID_PATH_TAG}=="$RIGHT_TAG", SYMLINK+="lesavka_r_eye"
2025-06-25 07:46:50 -05:00
EOF
2025-06-25 08:25:13 -05:00
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=video4linux
sudo udevadm settle
fi
fi
restore_capture_power_after_discovery
trap - EXIT
2025-06-23 00:26:02 -05:00
echo "==> 3. Rust toolchain"
2025-06-01 21:30:55 -05:00
sudo rustup default stable
run_as_user rustup default stable
2025-06-01 13:31:22 -05:00
2025-06-23 00:26:02 -05:00
echo "==> 4a. Source checkout"
SRC_DIR=/var/src/lesavka
2025-06-01 21:30:55 -05:00
if [[ ! -d $SRC_DIR ]]; then
sudo mkdir -p /var/src
sudo chown "$ORIG_USER":"$ORIG_USER" /var/src
2025-06-01 14:18:42 -05:00
fi
2025-06-01 21:30:55 -05:00
if [[ -d $SRC_DIR/.git ]]; then
run_as_user git -C "$SRC_DIR" fetch --all --tags --prune
2025-06-01 14:18:42 -05:00
else
run_as_user git clone "$REPO_URL" "$SRC_DIR"
2025-07-04 18:30:28 -05:00
fi
2025-07-04 18:24:48 -05:00
if run_as_user git -C "$SRC_DIR" rev-parse --verify --quiet "origin/$REF" >/dev/null; then
run_as_user git -C "$SRC_DIR" checkout -B "$REF" "origin/$REF"
2025-07-04 18:24:48 -05:00
else
run_as_user git -C "$SRC_DIR" checkout --force "$REF"
2025-06-01 14:18:42 -05:00
fi
2025-06-01 13:31:22 -05:00
echo "==> 4b. Kernel upgrade (optional)"
2026-04-16 13:54:25 -03:00
if [[ "${LESAVKA_KERNEL_UPDATE:-0}" != "0" ]]; then
sudo LESAVKA_KERNEL_BUILD_USER="$ORIG_USER" bash "$SRC_DIR/scripts/kernel/build-linux-rpi.sh"
else
echo "⚠️ skipping kernel upgrade (LESAVKA_KERNEL_UPDATE=0)"
fi
echo "==> 4c. Source build"
run_as_user env TMPDIR="$TMPDIR" bash -c "cd '$SRC_DIR/server' && cargo clean && cargo build --release --bins"
2025-06-01 13:31:22 -05:00
2025-06-23 00:26:02 -05:00
echo "==> 5. Install binaries"
sudo install -Dm755 "$SRC_DIR/target/release/lesavka-server" /usr/local/bin/lesavka-server
sudo install -Dm755 "$SRC_DIR/target/release/lesavka-uvc" /usr/local/bin/lesavka-uvc
2025-06-27 22:51:50 -05:00
sudo install -Dm755 "$SRC_DIR/scripts/daemon/lesavka-core.sh" /usr/local/bin/lesavka-core.sh
2026-01-08 00:59:14 -03:00
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
2025-06-01 14:18:42 -05:00
echo "==> 5b. Runtime environment defaults"
sudo install -d -m 0755 /etc/lesavka
HDMI_CONNECTOR=$(resolve_hdmi_connector)
if [[ -n $HDMI_CONNECTOR ]]; then
echo " ↪ HDMI connector: $HDMI_CONNECTOR"
write_hdmi_boot_force "$HDMI_CONNECTOR"
else
echo "⚠️ no connected HDMI connector detected; leaving LESAVKA_HDMI_CONNECTOR unset." >&2
fi
{
echo "# generated by lesavka/scripts/install/server.sh"
echo "# Edit only for local hardware overrides; rerunning the installer refreshes defaults."
if [[ -n $HDMI_CONNECTOR ]]; then
printf 'LESAVKA_HDMI_CONNECTOR=%s\n' "$HDMI_CONNECTOR"
fi
printf 'LESAVKA_CAM_OUTPUT=%s\n' "${LESAVKA_INSTALL_CAM_OUTPUT:-uvc}"
printf 'LESAVKA_CAM_WIDTH=%s\n' "${LESAVKA_CAM_WIDTH:-1920}"
printf 'LESAVKA_CAM_HEIGHT=%s\n' "${LESAVKA_CAM_HEIGHT:-1080}"
printf 'LESAVKA_CAM_FPS=%s\n' "${LESAVKA_CAM_FPS:-30}"
printf 'LESAVKA_HDMI_WIDTH=%s\n' "${LESAVKA_HDMI_WIDTH:-1920}"
printf 'LESAVKA_HDMI_HEIGHT=%s\n' "${LESAVKA_HDMI_HEIGHT:-1080}"
printf 'LESAVKA_HDMI_SINK=%s\n' "${LESAVKA_HDMI_SINK:-fbdevsink}"
printf 'LESAVKA_HDMI_FBDEV=%s\n' "${LESAVKA_HDMI_FBDEV:-/dev/fb0}"
printf 'LESAVKA_HDMI_DRIVER=%s\n' "${LESAVKA_HDMI_DRIVER:-vc4}"
2026-04-24 17:44:11 -03:00
printf 'LESAVKA_HDMI_PRESENTATION_DELAY_US=%s\n' "${LESAVKA_HDMI_PRESENTATION_DELAY_US:-180000}"
printf 'LESAVKA_UAC_DEV=%s\n' "${LESAVKA_UAC_DEV:-hw:UAC2Gadget,0}"
printf 'LESAVKA_ALSA_DEV=%s\n' "${LESAVKA_ALSA_DEV:-hw:UAC2Gadget,0}"
printf 'LESAVKA_UAC_HDMI_COMPENSATION_US=%s\n' "${LESAVKA_UAC_HDMI_COMPENSATION_US:-205000}"
printf 'LESAVKA_UAC_SESSION_CLOCK_ALIGN=%s\n' "${LESAVKA_UAC_SESSION_CLOCK_ALIGN:-0}"
printf 'LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS=%s\n' "${LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS:-1000}"
printf 'LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-0}"
printf 'LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-0}"
printf 'LESAVKA_UPSTREAM_PAIR_SLACK_US=%s\n' "${LESAVKA_UPSTREAM_PAIR_SLACK_US:-20000}"
printf 'LESAVKA_UPSTREAM_STALE_DROP_MS=%s\n' "${LESAVKA_UPSTREAM_STALE_DROP_MS:-80}"
printf 'LESAVKA_UVC_CODEC=%s\n' "${INSTALL_UVC_CODEC}"
} | sudo tee /etc/lesavka/server.env >/dev/null
UVC_ENV_TMP=$(mktemp)
render_uvc_env_file >"$UVC_ENV_TMP"
UVC_ENV_CHANGED=1
if sudo test -f /etc/lesavka/uvc.env && sudo cmp -s "$UVC_ENV_TMP" /etc/lesavka/uvc.env; then
UVC_ENV_CHANGED=0
fi
sudo install -m 0644 "$UVC_ENV_TMP" /etc/lesavka/uvc.env
rm -f "$UVC_ENV_TMP"
2025-06-23 00:26:02 -05:00
echo "==> 6a. Systemd units - lesavka-core"
cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-core.service >/dev/null
2025-06-01 13:31:22 -05:00
[Unit]
2025-06-23 00:26:02 -05:00
Description=lesavka USB gadget bring-up
2025-06-06 00:04:55 -05:00
After=sys-kernel-config.mount
Requires=sys-kernel-config.mount
2025-06-23 00:26:02 -05:00
2025-06-01 13:31:22 -05:00
[Service]
2025-06-01 14:18:42 -05:00
Type=oneshot
2025-06-23 00:26:02 -05:00
ExecStart=/usr/local/bin/lesavka-core.sh
2025-06-01 14:18:42 -05:00
RemainAfterExit=yes
Environment=LESAVKA_UVC_FALLBACK=0
EnvironmentFile=-/etc/lesavka/server.env
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE
AmbientCapabilities=CAP_SYS_MODULE
2025-06-06 00:41:32 -05:00
MountFlags=slave
2025-06-23 00:26:02 -05:00
2025-06-01 13:31:22 -05:00
[Install]
WantedBy=multi-user.target
2025-06-01 21:30:55 -05:00
UNIT
2025-06-01 13:31:22 -05:00
2025-06-23 00:26:02 -05:00
echo "==> 6b. Systemd units - lesavka-server"
cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-server.service >/dev/null
2025-06-01 13:31:22 -05:00
[Unit]
2025-06-23 00:26:02 -05:00
Description=lesavka gRPC relay
After=network.target lesavka-core.service
2026-01-08 00:42:32 -03:00
StartLimitIntervalSec=30
StartLimitBurst=10
2025-06-23 00:26:02 -05:00
2025-06-01 13:31:22 -05:00
[Service]
2026-01-07 04:47:39 -03:00
ExecStartPre=/usr/local/bin/lesavka-core.sh --attach
2025-06-23 00:26:02 -05:00
ExecStart=/usr/local/bin/lesavka-server
2026-01-07 04:47:39 -03:00
TimeoutStopSec=10
KillSignal=SIGTERM
2026-01-08 01:53:22 -03:00
KillMode=process
2025-06-01 14:18:42 -05:00
Restart=always
2025-07-04 18:00:49 -05:00
Environment=RUST_LOG=lesavka_server=info,lesavka_server::audio=info,lesavka_server::video=debug,lesavka_server::gadget=info
2025-06-25 20:00:34 -05:00
Environment=RUST_BACKTRACE=1
2025-07-01 10:23:51 -05:00
Environment=GST_DEBUG="*:2,alsasink:6,alsasrc:6"
2026-01-08 00:59:14 -03:00
Environment=LESAVKA_UVC_EXTERNAL=1
Environment=LESAVKA_EYE_ADAPTIVE=1
Environment=LESAVKA_EYE_MIN_FPS=12
Environment=LESAVKA_EYE_FPS=20
Environment=LESAVKA_MIC_INIT_ATTEMPTS=5
Environment=LESAVKA_MIC_INIT_DELAY_MS=250
Environment=LESAVKA_ALLOW_GADGET_CYCLE=1
EnvironmentFile=-/etc/lesavka/uvc.env
EnvironmentFile=-/etc/lesavka/server.env
2025-06-25 20:00:34 -05:00
Restart=always
2025-06-25 22:24:58 -05:00
RestartSec=5
2025-06-28 19:40:48 -05:00
StandardError=append:/tmp/lesavka-server.stderr
2025-06-01 14:18:42 -05:00
User=root
2025-06-23 00:26:02 -05:00
2025-06-01 13:31:22 -05:00
[Install]
WantedBy=multi-user.target
2025-06-01 21:30:55 -05:00
UNIT
2025-06-01 13:31:22 -05:00
2025-06-23 00:26:02 -05:00
echo "==> 6c. Systemd units - initialization"
2025-06-25 20:36:42 -05:00
sudo truncate -s 0 /tmp/lesavka-server.log
2025-06-01 13:31:22 -05:00
sudo systemctl daemon-reload
2026-04-16 13:54:25 -03:00
sudo systemctl enable lesavka-core lesavka-server
2026-01-08 23:58:19 -03:00
UDC_STATE=$(udc_state)
FORCE_GADGET_REBUILD=0
if [[ "$UVC_ENV_CHANGED" == "1" ]] && is_attached_state "$UDC_STATE"; then
FORCE_GADGET_REBUILD=1
echo "⚠️ UVC runtime settings changed while the host is attached; forcing a gadget rebuild so the new descriptors take effect."
fi
if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || [[ "$FORCE_GADGET_REBUILD" == "1" ]] || ! is_attached_state "$UDC_STATE"; then
echo "⚠️ UDC state is '$UDC_STATE' - forcing a Lesavka gadget rebuild before server start."
sudo env \
LESAVKA_ALLOW_GADGET_RESET=1 \
LESAVKA_ATTACH_WRITE_UDC=1 \
LESAVKA_DETACH_CLEAR_UDC=1 \
LESAVKA_RELOAD_UVCVIDEO=1 \
LESAVKA_UVC_FALLBACK=0 \
LESAVKA_UVC_CODEC="${INSTALL_UVC_CODEC}" \
/usr/local/bin/lesavka-core.sh
2026-01-08 23:58:19 -03:00
sudo systemctl restart lesavka-core
echo "✅ lesavka-core installed and restarted..."
else
echo "⚠️ UDC state is '$UDC_STATE' - skipping lesavka-core restart."
echo " Set LESAVKA_ALLOW_GADGET_RESET=1 to force."
fi
2025-06-06 20:45:09 -05:00
2026-01-08 00:59:14 -03:00
cat <<'UNIT' | sudo tee /etc/systemd/system/lesavka-uvc.service >/dev/null
[Unit]
Description=lesavka UVC control helper
After=lesavka-core.service
Requires=lesavka-core.service
[Service]
ExecStart=/usr/local/bin/lesavka-uvc.sh
Restart=always
RestartSec=2
KillSignal=SIGTERM
2026-01-08 21:37:30 -03:00
KillMode=process
2026-01-08 00:59:14 -03:00
TimeoutStopSec=10
StandardError=append:/tmp/lesavka-uvc.stderr
User=root
EnvironmentFile=-/etc/lesavka/uvc.env
2026-01-08 00:59:14 -03:00
[Install]
WantedBy=multi-user.target
UNIT
2026-04-16 13:54:25 -03:00
sudo systemctl daemon-reload
sudo systemctl enable lesavka-uvc
echo "==> 6d. Systemd units - remove legacy reboot watchdog"
sudo systemctl stop lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true
sudo systemctl disable lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true
sudo systemctl unmask lesavka-watchdog.timer lesavka-watchdog.service >/dev/null 2>&1 || true
sudo rm -f /etc/systemd/system/lesavka-watchdog.timer \
/etc/systemd/system/lesavka-watchdog.service \
/usr/local/bin/lesavka-watchdog.sh \
/etc/lesavka/watchdog.touch
2026-01-09 17:23:27 -03:00
sudo systemctl daemon-reload
if [[ "$UVC_ENV_CHANGED" == "1" ]] && systemctl is-active --quiet lesavka-uvc; then
sudo systemctl restart lesavka-uvc
echo "✅ lesavka-uvc restarted with the refreshed UVC runtime settings."
elif systemctl is-active --quiet lesavka-uvc; then
echo "✅ lesavka-uvc already active; runtime settings unchanged."
2026-01-08 23:58:19 -03:00
else
echo "⚠️ lesavka-uvc is not active; start via lesavka-core dependency path."
2026-01-08 23:58:19 -03:00
fi
2026-01-08 00:59:14 -03:00
validate_uvc_gadget_ready
2025-06-23 00:26:02 -05:00
sudo systemctl restart lesavka-server
INSTALLED_VERSION=$(manifest_package_version "$SRC_DIR/server/Cargo.toml" 2>/dev/null || true)
INSTALLED_SHA=$(git -C "$SCRIPT_REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true)
2025-06-23 00:26:02 -05:00
echo "✅ lesavka-server installed and restarted..."
if [[ -n $INSTALLED_VERSION || -n $INSTALLED_SHA ]]; then
echo "➡️ Installed: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"
fi
echo "➡️ Status: sudo systemctl status lesavka-server --no-pager"
echo "➡️ Logs: sudo journalctl -u lesavka-server -f --no-pager"
echo "✅ Installed version: lesavka-server ${INSTALLED_VERSION:-unknown}${INSTALLED_SHA:+ ($INSTALLED_SHA)}"