diff --git a/dist/hornverifier b/dist/hornverifier index 92561340..2b3ee771 100755 Binary files a/dist/hornverifier and b/dist/hornverifier differ diff --git a/dist/roverd b/dist/roverd index 9312e05a..4032c1de 100755 Binary files a/dist/roverd and b/dist/roverd differ diff --git a/dist/roverd-debian-laptop b/dist/roverd-debian-laptop new file mode 100755 index 00000000..c0493c2f Binary files /dev/null and b/dist/roverd-debian-laptop differ diff --git a/dist/servoverifier b/dist/servoverifier index d7605719..2216711b 100755 Binary files a/dist/servoverifier and b/dist/servoverifier differ diff --git a/pi/asound.debian-laptop.conf b/pi/asound.debian-laptop.conf new file mode 100644 index 00000000..3795ff1e --- /dev/null +++ b/pi/asound.debian-laptop.conf @@ -0,0 +1,88 @@ +# ALSA routing for the Debian laptop rover profile. +# +# This file intentionally mirrors the logical device names used by the Pi rover +# audio setup. roverd can keep sending horn audio to "horn", forwarded browser +# audio to "forward", and TTS to ALSA's default playback path without caring +# which physical sound card is underneath the profile. + +pcm.roverd_playback { + type plug + + # Use the system's first normal ALSA playback device as the physical sink. + # This avoids referencing "default" here, because this file replaces + # pcm.!default below and using it as a slave would recurse. + slave.pcm "sysdefault" +} + +pcm.roverd_capture { + type plug + + # The media publisher records from "default"; with pcm.!default below that + # capture side resolves here. Keeping capture separate from playback lets the + # asym default expose ordinary microphone input while playback goes through + # the TTS softvol path. + slave.pcm "sysdefault" +} + +pcm.tts_softvol { + type softvol + slave.pcm "roverd_playback" + control { + name "TTSMaster" + card 0 + } + min_dB -60.0 + max_dB 12.0 +} + +pcm.horn_softvol { + type softvol + slave.pcm "roverd_playback" + control { + name "HornMaster" + card 0 + } + min_dB -60.0 + max_dB 12.0 +} + +pcm.forward_softvol { + type softvol + slave.pcm "roverd_playback" + control { + name "ForwardMaster" + card 0 + } + min_dB -60.0 + max_dB 12.0 +} + +pcm.tts { + type plug + slave.pcm "tts_softvol" +} + +pcm.horn { + type plug + slave.pcm "horn_softvol" +} + +pcm.forward { + type plug + slave.pcm "forward_softvol" +} + +pcm.!default { + type asym + + # Existing TTS engines play to their default ALSA output, so default playback + # is intentionally the TTS path. This preserves the current TTS execution + # model while still making the TTS volume control meaningful on laptops. + playback.pcm "tts" + capture.pcm "roverd_capture" +} + +ctl.!default { + type hw + card 0 +} diff --git a/pi/bin/debian-laptop-video-publisher.sh b/pi/bin/debian-laptop-video-publisher.sh new file mode 100755 index 00000000..3038a010 --- /dev/null +++ b/pi/bin/debian-laptop-video-publisher.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail +set +H + +ENV_FILE="${ROVERD_MEDIA_ENV_FILE:-/var/lib/roverd/media.env}" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "Environment file ${ENV_FILE} missing; cannot publish laptop video" >&2 + exit 1 +fi + +# Load roverd's generated media.env as data instead of sourcing it as shell. +# The SRT publish URL contains normal query-string characters like '&' and '#!', +# so evaluating the file would be both fragile and unnecessary. +load_env_file() { + local content="" + + if [[ -r "$ENV_FILE" ]]; then + content="$(cat "$ENV_FILE")" + elif command -v sudo >/dev/null 2>&1; then + content="$(sudo -n cat "$ENV_FILE" 2>/dev/null || true)" + fi + + if [[ -z "$content" ]]; then + echo "Cannot read ${ENV_FILE} (permission denied). Run as a user that can read it, or allow sudo -n for cat." >&2 + exit 1 + fi + + local line key val + while IFS= read -r line || [[ -n "$line" ]]; do + [[ "$line" =~ ^[[:space:]]*$ ]] && continue + [[ "$line" =~ ^[[:space:]]*# ]] && continue + + if [[ "$line" =~ ^[[:space:]]*export[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + val="${BASH_REMATCH[2]}" + elif [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + val="${BASH_REMATCH[2]}" + else + continue + fi + + val="${val#${val%%[![:space:]]*}}" + val="${val%${val##*[![:space:]]}}" + if [[ "$val" =~ ^\".*\"$ ]]; then + val="${val:1:${#val}-2}" + elif [[ "$val" =~ ^\'.*\'$ ]]; then + val="${val:1:${#val}-2}" + fi + + printf -v "$key" '%s' "$val" + export "$key" + done <<< "$content" +} + +load_env_file +: "${ROVERD_VIDEO_ENABLE:?ROVERD_VIDEO_ENABLE not set in ${ENV_FILE}}" +: "${ROVERD_VIDEO_PUBLISH_URL:?ROVERD_VIDEO_PUBLISH_URL not set in ${ENV_FILE}}" +: "${ROVERD_VIDEO_DEVICE:?ROVERD_VIDEO_DEVICE not set in ${ENV_FILE}}" +: "${ROVERD_VIDEO_INPUT_FORMAT:?ROVERD_VIDEO_INPUT_FORMAT not set in ${ENV_FILE}}" +: "${ROVERD_VIDEO_WIDTH:?ROVERD_VIDEO_WIDTH not set in ${ENV_FILE}}" +: "${ROVERD_VIDEO_HEIGHT:?ROVERD_VIDEO_HEIGHT not set in ${ENV_FILE}}" +: "${ROVERD_VIDEO_FPS:?ROVERD_VIDEO_FPS not set in ${ENV_FILE}}" +: "${ROVERD_VIDEO_BITRATE:?ROVERD_VIDEO_BITRATE not set in ${ENV_FILE}}" +: "${ROVERD_VIDEO_INVERT:?ROVERD_VIDEO_INVERT not set in ${ENV_FILE}}" + +if [[ "${ROVERD_VIDEO_ENABLE}" -ne 1 ]]; then + echo "Laptop video publisher disabled by roverd media config; skipping" >&2 + exit 0 +fi + +if [[ -z "${ROVERD_VIDEO_DEVICE}" ]]; then + echo "ROVERD_VIDEO_DEVICE is required for the debian-laptop V4L2 publisher" >&2 + exit 1 +fi + +INPUT_FORMAT_ARGS=() +if [[ -n "${ROVERD_VIDEO_INPUT_FORMAT}" ]]; then + # Many laptop webcams expose both compressed MJPEG and raw YUYV modes. The + # wrong negotiated format can still produce a decodable stream, but the + # picture appears as green/noisy mush because ffmpeg is interpreting the + # frame bytes with the wrong pixel format. Passing input_format pins V4L2 to + # the camera mode selected in roverd.yaml. + INPUT_FORMAT_ARGS=(-input_format "${ROVERD_VIDEO_INPUT_FORMAT}") +fi + +if [[ -n "${FFMPEG_BIN:-}" ]]; then + FFMPEG_BIN_PATH="$FFMPEG_BIN" +elif command -v ffmpeg >/dev/null 2>&1; then + FFMPEG_BIN_PATH="$(command -v ffmpeg)" +else + echo "ffmpeg not found; install it via apt install ffmpeg." >&2 + exit 1 +fi + +VIDEO_FILTER_ARGS=() +if [[ "${ROVERD_VIDEO_INVERT}" -ne 0 ]]; then + # The Pi publisher uses libcamera rotation. With a generic webcam, ffmpeg's + # transpose pair gives the same 180-degree correction without assuming a + # camera-specific driver feature. + VIDEO_FILTER_ARGS=(-vf "transpose=2,transpose=2") +fi + +run_pipeline() { + "${FFMPEG_BIN_PATH}" \ + -hide_banner \ + -loglevel warning \ + -fflags nobuffer \ + -flags low_delay \ + -thread_queue_size 4096 \ + -f v4l2 \ + "${INPUT_FORMAT_ARGS[@]}" \ + -framerate "${ROVERD_VIDEO_FPS}" \ + -video_size "${ROVERD_VIDEO_WIDTH}x${ROVERD_VIDEO_HEIGHT}" \ + -i "${ROVERD_VIDEO_DEVICE}" \ + "${VIDEO_FILTER_ARGS[@]}" \ + -an \ + -c:v libx264 \ + -preset veryfast \ + -tune zerolatency \ + -profile:v baseline \ + -pix_fmt yuv420p \ + -b:v "${ROVERD_VIDEO_BITRATE}" \ + -maxrate "${ROVERD_VIDEO_BITRATE}" \ + -bufsize "$((ROVERD_VIDEO_BITRATE / 2))" \ + -g "${ROVERD_VIDEO_FPS}" \ + -keyint_min "${ROVERD_VIDEO_FPS}" \ + -sc_threshold 0 \ + -flush_packets 1 \ + -muxdelay 0 \ + -muxpreload 0 \ + -f mpegts \ + "${ROVERD_VIDEO_PUBLISH_URL}" +} + +while true; do + if run_pipeline; then + exit 0 + fi + echo "Debian laptop video publisher exited, restarting in 2s..." >&2 + sleep 2 +done diff --git a/pi/install/common.sh b/pi/install/common.sh new file mode 100644 index 00000000..37e8b689 --- /dev/null +++ b/pi/install/common.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +CONFIG_DEST="/etc/roverd.yaml" + +log() { + echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" +} + +ensure_user() { + local user="$1" + local groups="${2:-}" + local existing_groups="" + local group + + if [[ -n "$groups" ]]; then + IFS=',' read -ra group_list <<< "$groups" + for group in "${group_list[@]}"; do + if getent group "$group" >/dev/null 2>&1; then + existing_groups="${existing_groups:+$existing_groups,}$group" + else + # Different Debian-family installs expose slightly different + # hardware groups. Skipping missing optional groups lets one + # profile script cover ordinary laptops and Raspberry Pi OS. + log "Skipping missing system group '$group' for $user" + fi + done + fi + + if ! id -u "$user" >/dev/null 2>&1; then + if [[ -n "$existing_groups" ]]; then + useradd -r -s /usr/sbin/nologin -G "$existing_groups" "$user" + else + useradd -r -s /usr/sbin/nologin "$user" + fi + elif [[ -n "$existing_groups" ]]; then + usermod -a -G "$existing_groups" "$user" + fi +} + +require_root() { + if [[ "${EUID}" -ne 0 ]]; then + echo "Please run as root (sudo)" >&2 + exit 1 + fi +} + +validate_install_inputs() { + if [[ ! -f "$BINARY_SRC" ]]; then + echo "Binary not found at $BINARY_SRC" >&2 + exit 1 + fi + + if [[ ! -f "$CONFIG_SRC" ]]; then + echo "Config source not found at $CONFIG_SRC" >&2 + exit 1 + fi +} + +install_roverd_binary() { + ensure_user roverd "$ROVERD_GROUPS" + install -o roverd -g roverd -m 0755 "$BINARY_SRC" /usr/local/bin/roverd + log "Installed roverd binary for profile $PROFILE" +} + +install_roverd_config() { + CONFIG_EXISTS=0 + if [[ -f "$CONFIG_DEST" ]]; then + CONFIG_EXISTS=1 + log "Existing $CONFIG_DEST found; leaving it in place" + else + install -D -o roverd -g roverd -m 0640 "$CONFIG_SRC" "$CONFIG_DEST" + log "Installed sample config to $CONFIG_DEST (edit before starting service)" + fi +} + +install_roverd_unit() { + install -m 0644 pi/systemd/roverd.service /etc/systemd/system/roverd.service + log "Installed roverd systemd unit" +} diff --git a/pi/install/debian_laptop_profile.sh b/pi/install/debian_laptop_profile.sh new file mode 100644 index 00000000..3276bd00 --- /dev/null +++ b/pi/install/debian_laptop_profile.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +install_debian_laptop_deps() { + # This profile is deliberately Debian-only. Using apt directly is simpler + # than adding a fake cross-distro layer, and it keeps the installed package + # set easy to inspect on the actual rover laptop. + if command -v ffmpeg >/dev/null 2>&1 \ + && command -v arecord >/dev/null 2>&1 \ + && command -v aplay >/dev/null 2>&1 \ + && command -v amixer >/dev/null 2>&1 \ + && command -v v4l2-ctl >/dev/null 2>&1 \ + && command -v flite >/dev/null 2>&1 \ + && command -v espeak >/dev/null 2>&1; then + log "Debian laptop media/audio dependencies already installed; skipping apt install" + return + fi + + log "Installing Debian laptop dependencies (ffmpeg, ALSA tools, V4L2 tools, flite, espeak)..." + apt-get update + apt-get install -y --no-install-recommends ffmpeg alsa-utils v4l-utils ca-certificates flite espeak +} + +install_debian_laptop_audio_support() { + if [[ ! -f pi/asound.debian-laptop.conf ]]; then + log "WARNING: pi/asound.debian-laptop.conf missing; skipping Debian laptop ALSA config install" + return + fi + + # The laptop profile still uses roverd's existing audio contract: TTS plays + # to ALSA's default output, horn plays to the named "horn" device, and + # forwarded web audio plays to the named "forward" device. Installing one + # profile-specific asound.conf gives those paths independent softvol mixer + # controls without changing the TTS runtime code. + install -m 0644 pi/asound.debian-laptop.conf /etc/asound.conf + log "Installed Debian laptop ALSA config to /etc/asound.conf" + log "ALSA config updated; restarting audio clients or rebooting is recommended before testing laptop audio" +} + +install_debian_laptop_profile() { + install_debian_laptop_deps + install_debian_laptop_audio_support +} diff --git a/pi/install/media_env.sh b/pi/install/media_env.sh new file mode 100644 index 00000000..8c56d206 --- /dev/null +++ b/pi/install/media_env.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +write_media_env_placeholder() { + install -d -o roverd -g roverd /var/lib/roverd + + case "$PROFILE" in + pi) + # roverd rewrites media.env after loading /etc/roverd.yaml. These + # values only make the services syntactically usable before the first + # successful roverd run, so they mirror the Pi defaults instead of + # trying to become a second source of truth. + cat > /var/lib/roverd/media.env <<'ENV' +# Managed by roverd; placeholder values will be overwritten at runtime. +ROVERD_VIDEO_ENABLE=1 +ROVERD_VIDEO_PUBLISHER=pi-libcamera +ROVERD_VIDEO_PUBLISH_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316 +ROVERD_VIDEO_DEVICE= +ROVERD_VIDEO_INPUT_FORMAT= +ROVERD_VIDEO_WIDTH=640 +ROVERD_VIDEO_HEIGHT=480 +ROVERD_VIDEO_FPS=30 +ROVERD_VIDEO_BITRATE=2000000 +ROVERD_VIDEO_INVERT=1 +ROVERD_VIDEO_SENSOR_MODE=1296:972 +ROVERD_AUDIO_CAPTURE_ENABLE=0 +ROVERD_AUDIO_CAPTURE_PUBLISH_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-audio,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316 +ROVERD_AUDIO_CAPTURE_DEVICE=hw:0,0 +ROVERD_AUDIO_CAPTURE_SAMPLE_RATE=48000 +ROVERD_AUDIO_CAPTURE_CHANNELS=2 +ROVERD_AUDIO_CAPTURE_BITRATE=510000 +ROVERD_AUDIO_PLAYBACK_ENABLE=1 +ROVERD_AUDIO_PLAYBACK_FORWARD_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-fwd,m=request&latency=10&mode=caller&transtype=live&pkt_size=1316 +ROVERD_AUDIO_PLAYBACK_DEVICE=forward +ROVERD_AUDIO_PLAYBACK_NORMALIZE=1 +ROVERD_AUDIO_PLAYBACK_NORMALIZE_FILTER=dynaudnorm=f=75:g=15:m=10:p=0.9,alimiter=limit=0.85:level=disabled +ENV + ;; + debian-laptop) + # The laptop placeholder uses the same logical ALSA names as the + # installed laptop asound.conf. That keeps forwarded audio under the + # ForwardMaster mixer control while TTS and horn keep their own paths. + cat > /var/lib/roverd/media.env <<'ENV' +# Managed by roverd; placeholder values will be overwritten at runtime. +ROVERD_VIDEO_ENABLE=1 +ROVERD_VIDEO_PUBLISHER=debian-laptop-v4l2 +ROVERD_VIDEO_PUBLISH_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316 +ROVERD_VIDEO_DEVICE=/dev/video0 +ROVERD_VIDEO_INPUT_FORMAT=mjpeg +ROVERD_VIDEO_WIDTH=640 +ROVERD_VIDEO_HEIGHT=480 +ROVERD_VIDEO_FPS=30 +ROVERD_VIDEO_BITRATE=2000000 +ROVERD_VIDEO_INVERT=0 +ROVERD_VIDEO_SENSOR_MODE= +ROVERD_AUDIO_CAPTURE_ENABLE=1 +ROVERD_AUDIO_CAPTURE_PUBLISH_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-audio,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316 +ROVERD_AUDIO_CAPTURE_DEVICE=default +ROVERD_AUDIO_CAPTURE_SAMPLE_RATE=48000 +ROVERD_AUDIO_CAPTURE_CHANNELS=2 +ROVERD_AUDIO_CAPTURE_BITRATE=510000 +ROVERD_AUDIO_PLAYBACK_ENABLE=1 +ROVERD_AUDIO_PLAYBACK_FORWARD_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-fwd,m=request&latency=10&mode=caller&transtype=live&pkt_size=1316 +ROVERD_AUDIO_PLAYBACK_DEVICE=forward +ROVERD_AUDIO_PLAYBACK_NORMALIZE=1 +ROVERD_AUDIO_PLAYBACK_NORMALIZE_FILTER=dynaudnorm=f=75:g=15:m=10:p=0.9,alimiter=limit=0.85:level=disabled +ENV + ;; + esac + + chown roverd:roverd /var/lib/roverd/media.env + chmod 0640 /var/lib/roverd/media.env +} + +install_audio_fifo() { + local fifo_path="/var/lib/roverd/audio.pcm" + + # The capture service and publisher service meet at this FIFO. Recreating + # it only when it is missing or the path is not a FIFO preserves a working + # service pipe while still fixing accidental regular-file leftovers. + if [[ -p "$fifo_path" ]]; then + chown roverd:audio "$fifo_path" + chmod 0660 "$fifo_path" + else + rm -f "$fifo_path" + mkfifo "$fifo_path" + chown roverd:audio "$fifo_path" + chmod 0660 "$fifo_path" + fi +} diff --git a/pi/install/pi_profile.sh b/pi/install/pi_profile.sh new file mode 100644 index 00000000..599f114c --- /dev/null +++ b/pi/install/pi_profile.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +install_pi_video_deps() { + if command -v ffmpeg >/dev/null 2>&1 && (command -v rpicam-vid >/dev/null 2>&1 || command -v libcamera-vid >/dev/null 2>&1); then + log "Video dependencies already installed; skipping apt install" + return + fi + log "Installing video dependencies (libcamera-apps, ffmpeg)..." + apt-get update + apt-get install -y --no-install-recommends libcamera-apps ffmpeg +} + +find_boot_config() { + if [[ -f /boot/firmware/config.txt ]]; then + printf "/boot/firmware/config.txt" + return 0 + fi + if [[ -f /boot/config.txt ]]; then + printf "/boot/config.txt" + return 0 + fi + return 1 +} + +ensure_pwm_overlay() { + local boot_config + if ! boot_config="$(find_boot_config)"; then + log "WARNING: unable to locate /boot config.txt; please ensure dtoverlay=pwm-2chan is added manually for servo support" + return + fi + if grep -Eq '^\s*dtoverlay=pwm(-2chan)?' "$boot_config"; then + log "PWM overlay already present in $boot_config" + return + fi + local backup="${boot_config}.roverd.$(date +%Y%m%d%H%M%S).bak" + cp "$boot_config" "$backup" + { + echo "" + echo "# Added by roverd installer to expose PWM hardware for camera servo control on GPIO12/13 (leaves GPIO18/19 free for I2S)" + echo "dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4" + } >> "$boot_config" + log "Enabled dtoverlay=pwm-2chan on GPIO12/13 in $boot_config (backup at $backup). Reboot required for changes to apply." +} + +install_pi_audio_support() { + local boot_config + if ! boot_config="$(find_boot_config)"; then + log "WARNING: unable to locate /boot config.txt; please enable googlevoicehat-soundcard overlay manually" + else + # Ensure onboard audio is disabled (prevents card index flapping) + if grep -Eq '^\s*dtparam=audio=on\b' "$boot_config"; then + log "Disabling onboard audio (dtparam=audio=on -> off) in $boot_config" + sed -i 's/^\s*dtparam=audio=on\b/# roverd disabled onboard audio\ndtparam=audio=off/' "$boot_config" + fi + if ! grep -Eq '^\s*dtparam=audio=off\b' "$boot_config"; then + log "Adding dtparam=audio=off to $boot_config" + echo "dtparam=audio=off" >> "$boot_config" + fi + if ! grep -Eq '^\s*dtparam=i2s=on\b' "$boot_config"; then + log "Adding dtparam=i2s=on to $boot_config" + echo "dtparam=i2s=on" >> "$boot_config" + fi + if ! grep -Eq '^\s*dtoverlay=googlevoicehat-soundcard\b' "$boot_config"; then + local backup="${boot_config}.roverd.$(date +%Y%m%d%H%M%S).bak" + cp "$boot_config" "$backup" + { + echo "" + echo "# Added by roverd installer to enable Google AIY v1 sound card" + echo "dtoverlay=googlevoicehat-soundcard" + } >> "$boot_config" + log "Enabled googlevoicehat-soundcard overlay in $boot_config (backup at $backup). Reboot required." + else + log "googlevoicehat-soundcard overlay already present in $boot_config" + fi + fi + if [[ -f pi/asound.conf ]]; then + install -m 0644 pi/asound.conf /etc/asound.conf + log "Installed ALSA config to /etc/asound.conf" + alsa_reload_notice=1 + else + log "WARNING: pi/asound.conf missing; skipping ALSA config install" + fi + + if [[ "${alsa_reload_notice:-0}" -eq 1 ]]; then + log "ALSA config updated; reboot recommended for overlay + audio changes" + fi + + log "Installing TTS/audio packages (flite, espeak, Chrome TTS runtime deps)..." + if command -v flite >/dev/null 2>&1 \ + && command -v espeak >/dev/null 2>&1 \ + && command -v python3 >/dev/null 2>&1 \ + && command -v curl >/dev/null 2>&1 \ + && command -v xz >/dev/null 2>&1 \ + && command -v unzip >/dev/null 2>&1 \ + && command -v aplay >/dev/null 2>&1 \ + && ldconfig -p 2>/dev/null | grep -q 'libc++\.so\.1' \ + && ldconfig -p 2>/dev/null | grep -q 'libc++abi\.so\.1'; then + log "Core TTS packages already installed; skipping apt install" + else + apt-get update + apt-get install -y --no-install-recommends flite espeak python3 curl xz-utils unzip alsa-utils libc++1 libc++abi1 \ + || apt-get install -y --no-install-recommends flite espeak python3 curl xz-utils unzip alsa-utils libc++1-14 libc++abi1-14 + fi + + install -D -o root -g root -m 0755 pi/bin/chromegtts-daemon.py /usr/local/bin/chromegtts-daemon + log "Installed chromegtts daemon" + + install_google_tts_assets +} + +install_google_tts_assets() { + local asset_dir="/opt/roverd/googletts" + local voice_dir="${asset_dir}/en-us-x-multi-r30" + local dist_url="https://storage.googleapis.com/chromeos-localmirror/distfiles/googletts-26.5.tar.xz" + local tmp_dir + local lib_member + + case "$(uname -m)" in + aarch64|arm64) + lib_member="libchrometts_arm64.so" + ;; + armv7l|armhf) + lib_member="libchrometts_armv7.so" + ;; + *) + log "WARNING: unsupported Chrome TTS architecture $(uname -m); skipping Google TTS assets" + return + ;; + esac + + if [[ -f "${asset_dir}/libchrometts.so" && -f "${voice_dir}/pipeline.pb" ]]; then + log "Google Chrome TTS assets already installed; skipping download" + return + fi + + tmp_dir="$(mktemp -d)" + log "Downloading Google Chrome TTS assets..." + curl -L -o "${tmp_dir}/googletts-26.5.tar.xz" "$dist_url" + tar -xf "${tmp_dir}/googletts-26.5.tar.xz" -C "$tmp_dir" en-us-x-multi.zvoice "$lib_member" + + install -d -o root -g root -m 0755 "$asset_dir" + install -o root -g root -m 0644 "${tmp_dir}/${lib_member}" "${asset_dir}/libchrometts.so" + rm -rf "$voice_dir" + install -d -o root -g root -m 0755 "$voice_dir" + unzip -q "${tmp_dir}/en-us-x-multi.zvoice" -d "$voice_dir" + chown -R root:root "$asset_dir" + find "$asset_dir" -type d -exec chmod 0755 {} + + find "$asset_dir" -type f -exec chmod 0644 {} + + rm -rf "$tmp_dir" + log "Installed Google Chrome TTS assets to $asset_dir" +} + +install_pi_profile() { + if ! command -v rpicam-vid >/dev/null 2>&1 && ! command -v libcamera-vid >/dev/null 2>&1; then + log "WARNING: neither rpicam-vid nor libcamera-vid found in PATH; install libcamera-apps." + fi + install_pi_video_deps + ensure_pwm_overlay + install_pi_audio_support +} diff --git a/pi/install/profiles.sh b/pi/install/profiles.sh new file mode 100644 index 00000000..7245974a --- /dev/null +++ b/pi/install/profiles.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +PROFILE="pi" +BINARY_SRC="" +CONFIG_SRC="" + +usage() { + cat <<'USAGE' +Usage: sudo ./pi/install_roverd.sh [options] + +Options: + --profile Install profile: pi or debian-laptop (default: pi) + --debian-laptop Shortcut for --profile debian-laptop + -b, --binary Path to the roverd binary (default depends on profile) + -c, --config Source config to install if /etc/roverd.yaml is missing + (default depends on profile) + -h, --help Show this help text + +The script must run from the repository root and as root (sudo). It will: + * create system users/groups if needed + * install /usr/local/bin/roverd and /etc/roverd.yaml + * install profile-specific video helpers and shared audio helpers + * install profile-specific ALSA/system audio setup + * install the fixed-command self-update helper used by admin-triggered updates + * enable roverd.service and media publisher/listener services +USAGE +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --profile) + PROFILE="${2:-}" + shift 2 + ;; + --debian-laptop) + PROFILE="debian-laptop" + shift + ;; + -b|--binary) + BINARY_SRC="${2:-}" + shift 2 + ;; + -c|--config) + CONFIG_SRC="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + esac + done +} + +select_profile() { + case "$PROFILE" in + pi) + BINARY_SRC="${BINARY_SRC:-dist/roverd}" + CONFIG_SRC="${CONFIG_SRC:-pi/roverd/roverd.sample.yaml}" + VIDEO_HELPER_SRC="pi/bin/video-publisher.sh" + VIDEO_HELPER_DEST="/usr/local/bin/video-publisher" + VIDEO_SERVICE_SRC="pi/systemd/video-publisher.service" + VIDEO_SERVICE_NAME="video-publisher.service" + ROVERD_GROUPS="dialout,gpio,video,render,audio" + ;; + debian-laptop) + BINARY_SRC="${BINARY_SRC:-dist/roverd-debian-laptop}" + CONFIG_SRC="${CONFIG_SRC:-pi/roverd/roverd.debian-laptop.sample.yaml}" + VIDEO_HELPER_SRC="pi/bin/debian-laptop-video-publisher.sh" + VIDEO_HELPER_DEST="/usr/local/bin/debian-laptop-video-publisher" + VIDEO_SERVICE_SRC="pi/systemd/debian-laptop-video-publisher.service" + VIDEO_SERVICE_NAME="debian-laptop-video-publisher.service" + ROVERD_GROUPS="dialout,video,render,audio" + ;; + *) + echo "Unknown profile: $PROFILE" >&2 + usage + exit 1 + ;; + esac +} + +install_selected_profile() { + case "$PROFILE" in + pi) + # The Pi profile owns the board-specific camera, PWM, and AIY audio + # setup. Keeping that hardware work behind this dispatcher prevents + # the laptop profile from accidentally inheriting Pi overlays or + # card-index assumptions. + install_pi_profile + ;; + debian-laptop) + # The Debian laptop profile only installs ordinary Debian packages + # and a laptop ALSA routing file. Anything that depends on Pi GPIO, + # overlays, or the Google Voice HAT stays out of this path. + install_debian_laptop_profile + ;; + esac +} diff --git a/pi/install/self_update.sh b/pi/install/self_update.sh new file mode 100644 index 00000000..0dc8276a --- /dev/null +++ b/pi/install/self_update.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +install_self_update_support() { + local sudoers_file="/etc/sudoers.d/roverd-self-update" + local update_env="/etc/roverd-update.env" + local quoted_repo_root + + # The self-update helper must know which checkout should receive the git + # pull. Recording the repository root during the normal installer run keeps + # the runtime websocket command simple and prevents the rover from accepting + # a caller-controlled path. + printf -v quoted_repo_root '%q' "$REPO_ROOT" + install -D -o root -g root -m 0644 /dev/null "$update_env" + cat > "$update_env" < "$sudoers_file" <<'SUDOERS' +# Managed by pi/install_roverd.sh. +# Allow only the roverd service account to run the fixed self-update helper. +roverd ALL=(root) NOPASSWD: /usr/local/sbin/roverd-self-update +SUDOERS + chown root:root "$sudoers_file" + chmod 0440 "$sudoers_file" + if command -v visudo >/dev/null 2>&1; then + visudo -cf "$sudoers_file" >/dev/null + fi + log "Installed roverd self-update helper and sudoers rule" +} diff --git a/pi/install/systemd.sh b/pi/install/systemd.sh new file mode 100644 index 00000000..a39bcb0f --- /dev/null +++ b/pi/install/systemd.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +install_media_units() { + # Video publishing is the only profile-specific media service right now: + # Pi uses libcamera/rpicam, while Debian laptops publish from a V4L2 webcam. + install -D -o root -g root -m 0755 "$VIDEO_HELPER_SRC" "$VIDEO_HELPER_DEST" + log "Installed $PROFILE video publisher helper" + install -m 0644 "$VIDEO_SERVICE_SRC" "/etc/systemd/system/$VIDEO_SERVICE_NAME" + log "Installed $PROFILE video publisher systemd unit" + + # Audio capture publishing and forwarded-audio playback use the same ffmpeg + # and ALSA contract on both profiles, so they stay as shared installer work. + install -D -o root -g root -m 0755 pi/bin/audio-only-publisher.sh /usr/local/bin/audio-only-publisher + install -m 0644 pi/systemd/audio-only-publisher.service /etc/systemd/system/audio-only-publisher.service + log "Installed audio-only publisher helper + systemd unit" + + install -D -o root -g root -m 0755 pi/bin/audio-forward-listener.sh /usr/local/bin/audio-forward-listener + install -m 0644 pi/systemd/audio-forward-listener.service /etc/systemd/system/audio-forward-listener.service + log "Installed audio-forward listener helper + systemd unit" +} + +enable_and_restart_units() { + systemctl daemon-reload + systemctl enable roverd.service + systemctl enable "$VIDEO_SERVICE_NAME" + systemctl enable audio-only-publisher.service + systemctl enable audio-forward-listener.service + + if [[ $CONFIG_EXISTS -eq 1 ]]; then + # An existing config means this host was already configured, so restart + # immediately to pick up the new binary, helpers, units, and media env. + systemctl restart roverd.service + systemctl restart "$VIDEO_SERVICE_NAME" + systemctl restart audio-only-publisher.service + systemctl restart audio-forward-listener.service + log "Restarted roverd + media publishers/listener" + else + # A freshly installed sample config usually still has placeholder names + # and URLs. Enabling without starting avoids connecting a half-configured + # rover to the control server by accident. + log "Skipped auto-start because config is the sample; edit $CONFIG_DEST then run: sudo systemctl restart roverd ${VIDEO_SERVICE_NAME%.service} audio-only-publisher audio-forward-listener" + fi +} diff --git a/pi/install_roverd.sh b/pi/install_roverd.sh index 27a71770..321b15df 100755 --- a/pi/install_roverd.sh +++ b/pi/install_roverd.sh @@ -1,373 +1,48 @@ #!/usr/bin/env bash # -# Installer for the roverd agent on Raspberry Pi +# Installer entry point for roverd hosts. +# +# The detailed work lives in pi/install/*.sh so platform-specific setup stays +# local to the profile that needs it. This file should read like the install +# order: choose a profile, validate inputs, install shared roverd pieces, +# install profile support, then enable the services. set -euo pipefail -BINARY_SRC="dist/roverd" -CONFIG_SRC="pi/roverd/roverd.sample.yaml" -REPO_ROOT="$(pwd -P)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" -usage() { - cat <<'USAGE' -Usage: sudo ./pi/install_roverd.sh [options] +# The helper scripts use repository-relative paths because those same paths are +# shown in docs and logs. Moving to the repository root once keeps every module +# simple and prevents each function from needing its own path resolver. +cd "$REPO_ROOT" -Options: - -b, --binary Path to the roverd binary (default: dist/roverd) - -c, --config Source config to install if /etc/roverd.yaml is missing - (default: pi/roverd/roverd.sample.yaml) - -h, --help Show this help text +source pi/install/common.sh +source pi/install/profiles.sh +source pi/install/self_update.sh +source pi/install/pi_profile.sh +source pi/install/debian_laptop_profile.sh +source pi/install/media_env.sh +source pi/install/systemd.sh -The script must run from the repository root and as root (sudo). It will: - * create system users/groups if needed - * install /usr/local/bin/roverd and /etc/roverd.yaml - * install /usr/local/bin/video/audio helpers and systemd units - * install fixed-location Google Chrome TTS assets for roverd - * install the fixed-command self-update helper used by admin-triggered updates - * enable roverd.service and media publisher/listener services -USAGE +main() { + parse_args "$@" + select_profile + require_root + validate_install_inputs + + install_roverd_binary + install_self_update_support + install_roverd_config + install_roverd_unit + + install_selected_profile + install_media_units + write_media_env_placeholder + install_audio_fifo + enable_and_restart_units + + log "Install complete for profile $PROFILE" } -while [[ $# -gt 0 ]]; do - case "$1" in - -b|--binary) - BINARY_SRC="${2:-}" - shift 2 - ;; - -c|--config) - CONFIG_SRC="${2:-}" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - usage - exit 1 - ;; - esac -done - -if [[ "${EUID}" -ne 0 ]]; then - echo "Please run as root (sudo)" >&2 - exit 1 -fi - -if [[ ! -f "$BINARY_SRC" ]]; then - echo "Binary not found at $BINARY_SRC" >&2 - exit 1 -fi - -if [[ ! -f "$CONFIG_SRC" ]]; then - echo "Config source not found at $CONFIG_SRC" >&2 - exit 1 -fi - -ensure_user() { - local user="$1" - local groups="${2:-}" - if ! id -u "$user" >/dev/null 2>&1; then - if [[ -n "$groups" ]]; then - useradd -r -s /usr/sbin/nologin -G "$groups" "$user" - else - useradd -r -s /usr/sbin/nologin "$user" - fi - elif [[ -n "$groups" ]]; then - usermod -a -G "$groups" "$user" - fi -} - -log() { - echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" -} - -if ! command -v rpicam-vid >/dev/null 2>&1 && ! command -v libcamera-vid >/dev/null 2>&1; then - log "WARNING: neither rpicam-vid nor libcamera-vid found in PATH; install libcamera-apps." -fi - -install_video_deps() { - if command -v ffmpeg >/dev/null 2>&1 && (command -v rpicam-vid >/dev/null 2>&1 || command -v libcamera-vid >/dev/null 2>&1); then - log "Video dependencies already installed; skipping apt install" - return - fi - log "Installing video dependencies (libcamera-apps, ffmpeg)..." - apt-get update - apt-get install -y --no-install-recommends libcamera-apps ffmpeg -} - -find_boot_config() { - if [[ -f /boot/firmware/config.txt ]]; then - printf "/boot/firmware/config.txt" - return 0 - fi - if [[ -f /boot/config.txt ]]; then - printf "/boot/config.txt" - return 0 - fi - return 1 -} - -ensure_pwm_overlay() { - local boot_config - if ! boot_config="$(find_boot_config)"; then - log "WARNING: unable to locate /boot config.txt; please ensure dtoverlay=pwm-2chan is added manually for servo support" - return - fi - if grep -Eq '^\s*dtoverlay=pwm(-2chan)?' "$boot_config"; then - log "PWM overlay already present in $boot_config" - return - fi - local backup="${boot_config}.roverd.$(date +%Y%m%d%H%M%S).bak" - cp "$boot_config" "$backup" - { - echo "" - echo "# Added by roverd installer to expose PWM hardware for camera servo control on GPIO12/13 (leaves GPIO18/19 free for I2S)" - echo "dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4" - } >> "$boot_config" - log "Enabled dtoverlay=pwm-2chan on GPIO12/13 in $boot_config (backup at $backup). Reboot required for changes to apply." -} - -ensure_user roverd "dialout,gpio,video,render,audio" -install -o roverd -g roverd -m 0755 "$BINARY_SRC" /usr/local/bin/roverd -log "Installed roverd binary" - -install_self_update_support() { - local sudoers_file="/etc/sudoers.d/roverd-self-update" - local update_env="/etc/roverd-update.env" - local quoted_repo_root - - # The self-update helper must know which checkout should receive the git - # pull. Recording the repository root during the normal installer run keeps - # the runtime websocket command simple and prevents the rover from accepting - # a caller-controlled path. - printf -v quoted_repo_root '%q' "$REPO_ROOT" - install -D -o root -g root -m 0644 /dev/null "$update_env" - cat > "$update_env" < "$sudoers_file" <<'SUDOERS' -# Managed by pi/install_roverd.sh. -# Allow only the roverd service account to run the fixed self-update helper. -roverd ALL=(root) NOPASSWD: /usr/local/sbin/roverd-self-update -SUDOERS - chown root:root "$sudoers_file" - chmod 0440 "$sudoers_file" - if command -v visudo >/dev/null 2>&1; then - visudo -cf "$sudoers_file" >/dev/null - fi - log "Installed roverd self-update helper and sudoers rule" -} - -install_self_update_support - -CONFIG_DEST="/etc/roverd.yaml" -CONFIG_EXISTS=0 -if [[ -f "$CONFIG_DEST" ]]; then - CONFIG_EXISTS=1 - log "Existing $CONFIG_DEST found; leaving it in place" -else - install -D -o roverd -g roverd -m 0640 "$CONFIG_SRC" "$CONFIG_DEST" - log "Installed sample config to $CONFIG_DEST (edit before starting service)" -fi - -install -m 0644 pi/systemd/roverd.service /etc/systemd/system/roverd.service -log "Installed roverd systemd unit" - -install_video_deps -ensure_pwm_overlay - -# Enable Google AIY v1 sound card, ALSA defaults, and TTS engines -install_audio_support() { - local boot_config - if ! boot_config="$(find_boot_config)"; then - log "WARNING: unable to locate /boot config.txt; please enable googlevoicehat-soundcard overlay manually" - else - # Ensure onboard audio is disabled (prevents card index flapping) - if grep -Eq '^\s*dtparam=audio=on\b' "$boot_config"; then - log "Disabling onboard audio (dtparam=audio=on -> off) in $boot_config" - sed -i 's/^\s*dtparam=audio=on\b/# roverd disabled onboard audio\ndtparam=audio=off/' "$boot_config" - fi - if ! grep -Eq '^\s*dtparam=audio=off\b' "$boot_config"; then - log "Adding dtparam=audio=off to $boot_config" - echo "dtparam=audio=off" >> "$boot_config" - fi - if ! grep -Eq '^\s*dtparam=i2s=on\b' "$boot_config"; then - log "Adding dtparam=i2s=on to $boot_config" - echo "dtparam=i2s=on" >> "$boot_config" - fi - if ! grep -Eq '^\s*dtoverlay=googlevoicehat-soundcard\b' "$boot_config"; then - local backup="${boot_config}.roverd.$(date +%Y%m%d%H%M%S).bak" - cp "$boot_config" "$backup" - { - echo "" - echo "# Added by roverd installer to enable Google AIY v1 sound card" - echo "dtoverlay=googlevoicehat-soundcard" - } >> "$boot_config" - log "Enabled googlevoicehat-soundcard overlay in $boot_config (backup at $backup). Reboot required." - else - log "googlevoicehat-soundcard overlay already present in $boot_config" - fi - fi - if [[ -f pi/asound.conf ]]; then - install -m 0644 pi/asound.conf /etc/asound.conf - log "Installed ALSA config to /etc/asound.conf" - alsa_reload_notice=1 - else - log "WARNING: pi/asound.conf missing; skipping ALSA config install" - fi - - if [[ "${alsa_reload_notice:-0}" -eq 1 ]]; then - log "ALSA config updated; reboot recommended for overlay + audio changes" - fi - - log "Installing TTS/audio packages (flite, espeak, Chrome TTS runtime deps)..." - # check for flite and espeak before installing, and then install them if either is missing - if command -v flite >/dev/null 2>&1 \ - && command -v espeak >/dev/null 2>&1 \ - && command -v python3 >/dev/null 2>&1 \ - && command -v curl >/dev/null 2>&1 \ - && command -v xz >/dev/null 2>&1 \ - && command -v unzip >/dev/null 2>&1 \ - && command -v aplay >/dev/null 2>&1 \ - && ldconfig -p 2>/dev/null | grep -q 'libc++\.so\.1' \ - && ldconfig -p 2>/dev/null | grep -q 'libc++abi\.so\.1'; then - log "Core TTS packages already installed; skipping apt install" - else - apt-get update - apt-get install -y --no-install-recommends flite espeak python3 curl xz-utils unzip alsa-utils libc++1 libc++abi1 \ - || apt-get install -y --no-install-recommends flite espeak python3 curl xz-utils unzip alsa-utils libc++1-14 libc++abi1-14 - fi - - install -D -o root -g root -m 0755 pi/bin/chromegtts-daemon.py /usr/local/bin/chromegtts-daemon - log "Installed chromegtts daemon" - - install_google_tts_assets -} - -install_google_tts_assets() { - local asset_dir="/opt/roverd/googletts" - local voice_dir="${asset_dir}/en-us-x-multi-r30" - local dist_url="https://storage.googleapis.com/chromeos-localmirror/distfiles/googletts-26.5.tar.xz" - local tmp_dir - local lib_member - - case "$(uname -m)" in - aarch64|arm64) - lib_member="libchrometts_arm64.so" - ;; - armv7l|armhf) - lib_member="libchrometts_armv7.so" - ;; - *) - log "WARNING: unsupported Chrome TTS architecture $(uname -m); skipping Google TTS assets" - return - ;; - esac - - if [[ -f "${asset_dir}/libchrometts.so" && -f "${voice_dir}/pipeline.pb" ]]; then - log "Google Chrome TTS assets already installed; skipping download" - return - fi - - tmp_dir="$(mktemp -d)" - log "Downloading Google Chrome TTS assets..." - curl -L -o "${tmp_dir}/googletts-26.5.tar.xz" "$dist_url" - tar -xf "${tmp_dir}/googletts-26.5.tar.xz" -C "$tmp_dir" en-us-x-multi.zvoice "$lib_member" - - install -d -o root -g root -m 0755 "$asset_dir" - install -o root -g root -m 0644 "${tmp_dir}/${lib_member}" "${asset_dir}/libchrometts.so" - rm -rf "$voice_dir" - install -d -o root -g root -m 0755 "$voice_dir" - unzip -q "${tmp_dir}/en-us-x-multi.zvoice" -d "$voice_dir" - chown -R root:root "$asset_dir" - find "$asset_dir" -type d -exec chmod 0755 {} + - find "$asset_dir" -type f -exec chmod 0644 {} + - rm -rf "$tmp_dir" - log "Installed Google Chrome TTS assets to $asset_dir" -} - -# Install video publisher assets -install -D -o root -g root -m 0755 pi/bin/video-publisher.sh /usr/local/bin/video-publisher -log "Installed video-publisher helper" -install -m 0644 pi/systemd/video-publisher.service /etc/systemd/system/video-publisher.service -log "Installed video-publisher systemd unit" -# Install audio-only publisher assets -install -D -o root -g root -m 0755 pi/bin/audio-only-publisher.sh /usr/local/bin/audio-only-publisher -install -m 0644 pi/systemd/audio-only-publisher.service /etc/systemd/system/audio-only-publisher.service -log "Installed audio-only publisher helper + systemd unit" -# Install audio-forward listener assets -install -D -o root -g root -m 0755 pi/bin/audio-forward-listener.sh /usr/local/bin/audio-forward-listener -install -m 0644 pi/systemd/audio-forward-listener.service /etc/systemd/system/audio-forward-listener.service -log "Installed audio-forward listener helper + systemd unit" -install -d -o roverd -g roverd /var/lib/roverd -cat > /var/lib/roverd/media.env <<'ENV' -# Managed by roverd; placeholder values will be overwritten at runtime. -ROVERD_VIDEO_ENABLE=1 -ROVERD_VIDEO_PUBLISHER=pi-libcamera -ROVERD_VIDEO_PUBLISH_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316 -ROVERD_VIDEO_DEVICE= -ROVERD_VIDEO_WIDTH=640 -ROVERD_VIDEO_HEIGHT=480 -ROVERD_VIDEO_FPS=30 -ROVERD_VIDEO_BITRATE=2000000 -ROVERD_VIDEO_INVERT=1 -ROVERD_VIDEO_SENSOR_MODE=1296:972 -ROVERD_AUDIO_CAPTURE_ENABLE=0 -ROVERD_AUDIO_CAPTURE_PUBLISH_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-audio,m=publish&latency=10&mode=caller&transtype=live&pkt_size=1316 -ROVERD_AUDIO_CAPTURE_DEVICE=hw:0,0 -ROVERD_AUDIO_CAPTURE_SAMPLE_RATE=48000 -ROVERD_AUDIO_CAPTURE_CHANNELS=2 -ROVERD_AUDIO_CAPTURE_BITRATE=510000 -ROVERD_AUDIO_PLAYBACK_ENABLE=1 -ROVERD_AUDIO_PLAYBACK_FORWARD_URL=srt://192.168.0.86:9000?streamid=#!::r=CHANGE_ME-fwd,m=request&latency=10&mode=caller&transtype=live&pkt_size=1316 -ROVERD_AUDIO_PLAYBACK_DEVICE=forward -ROVERD_AUDIO_PLAYBACK_NORMALIZE=1 -ROVERD_AUDIO_PLAYBACK_NORMALIZE_FILTER=dynaudnorm=f=75:g=15:m=10:p=0.9,alimiter=limit=0.85:level=disabled -ENV -chown roverd:roverd /var/lib/roverd/media.env -chmod 0640 /var/lib/roverd/media.env -# Create persistent audio FIFO for capture -> publisher -FIFO_PATH="/var/lib/roverd/audio.pcm" -if [[ -p "$FIFO_PATH" ]]; then - chown roverd:audio "$FIFO_PATH" - chmod 0660 "$FIFO_PATH" -else - rm -f "$FIFO_PATH" - mkfifo "$FIFO_PATH" - chown roverd:audio "$FIFO_PATH" - chmod 0660 "$FIFO_PATH" -fi -# Ensure ALSA config is in place for rovermic device -install -m 0644 pi/asound.conf /etc/asound.conf -log "Installed ALSA config (/etc/asound.conf)" - -install_audio_support - -systemctl daemon-reload -systemctl enable roverd.service -systemctl enable video-publisher.service -systemctl enable audio-only-publisher.service -systemctl enable audio-forward-listener.service -if [[ $CONFIG_EXISTS -eq 1 ]]; then - systemctl restart roverd.service - systemctl restart video-publisher.service - systemctl restart audio-only-publisher.service - systemctl restart audio-forward-listener.service - log "Restarted roverd + media publishers/listener" -else - log "Skipped auto-start because config is the sample; edit $CONFIG_DEST then run: sudo systemctl restart roverd video-publisher audio-only-publisher audio-forward-listener" -fi - -log "Install complete" +main "$@" diff --git a/pi/roverd/Makefile b/pi/roverd/Makefile index 4df4d7fb..d85f788a 100644 --- a/pi/roverd/Makefile +++ b/pi/roverd/Makefile @@ -3,18 +3,22 @@ GOOS ?= linux GOARCH ?= arm GOARM ?= 6 -.PHONY: build pi-build dummy clean +.PHONY: build pi-build debian-laptop dummy verifier-tools clean -build: - go build -o $(BIN_DIR)/roverd ./cmd/roverd +build: pi-build debian-laptop dummy -pi-build: - GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -trimpath -ldflags="-s -w" -o $(BIN_DIR)/roverd ./cmd/roverd +verifier-tools: GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -trimpath -ldflags="-s -w" -o $(BIN_DIR)/servoverifier ./cmd/servoverifier GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -trimpath -ldflags="-s -w" -o $(BIN_DIR)/hornverifier ./cmd/hornverifier +pi-build: verifier-tools + GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) go build -trimpath -ldflags="-s -w" -o $(BIN_DIR)/roverd ./cmd/roverd + +debian-laptop: + GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -tags debian_laptop -o $(BIN_DIR)/roverd-debian-laptop ./cmd/roverd + dummy: GOOS=linux GOARCH=amd64 go build -tags dummy -o $(BIN_DIR)/roverd-dummy ./cmd/roverd clean: - rm -f $(BIN_DIR)/roverd $(BIN_DIR)/servoverifier $(BIN_DIR)/hornverifier + rm -f $(BIN_DIR)/roverd $(BIN_DIR)/roverd-debian-laptop $(BIN_DIR)/roverd-dummy $(BIN_DIR)/servoverifier $(BIN_DIR)/hornverifier diff --git a/pi/roverd/brc.go b/pi/roverd/brc.go index 005ffdf2..df96319e 100644 --- a/pi/roverd/brc.go +++ b/pi/roverd/brc.go @@ -1,4 +1,4 @@ -//go:build !dummy +//go:build !dummy && !debian_laptop package roverd diff --git a/pi/roverd/brc_debian_laptop.go b/pi/roverd/brc_debian_laptop.go new file mode 100644 index 00000000..9e49e9ff --- /dev/null +++ b/pi/roverd/brc_debian_laptop.go @@ -0,0 +1,25 @@ +//go:build debian_laptop + +package roverd + +import ( + "context" + "fmt" + "log" +) + +type BRCPulser struct{} + +func NewBRCPulser(_ BRCConfig, _ *log.Logger) (*BRCPulser, error) { + /* + The first Debian laptop profile keeps BRC disabled instead of pretending a + USB serial modem-control line has already been selected and tested. The + config can set brc.gpioPin: -1 to skip construction entirely; if someone + enables it, fail loudly so the rover does not silently miss wake pulses. + */ + return nil, fmt.Errorf("brc is not supported in the debian-laptop build yet") +} + +func (b *BRCPulser) Close() {} + +func (b *BRCPulser) Start(ctx context.Context) {} diff --git a/pi/roverd/camera_servo.go b/pi/roverd/camera_servo.go index d1b2b067..83382ca5 100644 --- a/pi/roverd/camera_servo.go +++ b/pi/roverd/camera_servo.go @@ -1,4 +1,4 @@ -//go:build !dummy +//go:build !dummy && !debian_laptop package roverd diff --git a/pi/roverd/camera_servo_debian_laptop.go b/pi/roverd/camera_servo_debian_laptop.go new file mode 100644 index 00000000..16dc3f5f --- /dev/null +++ b/pi/roverd/camera_servo_debian_laptop.go @@ -0,0 +1,38 @@ +//go:build debian_laptop + +package roverd + +import ( + "fmt" + "log" +) + +type CameraServo struct{} + +func NewCameraServo(_ CameraServoConfig, _ *log.Logger) (*CameraServo, error) { + /* + The Debian laptop profile starts with the laptop's built-in webcam and no + Pi PWM servo. If a laptop rover eventually grows an external servo board, + it should get its own implementation instead of reusing Raspberry Pi GPIO + assumptions. + */ + return nil, fmt.Errorf("camera servo not supported in the debian-laptop build") +} + +func (c *CameraServo) Close() {} + +func (c *CameraServo) SetAngle(angle float64) error { + return fmt.Errorf("camera servo disabled") +} + +func (c *CameraServo) Nudge(delta float64) error { + return fmt.Errorf("camera servo disabled") +} + +func (c *CameraServo) SetPulseWidth(micros int) error { + return fmt.Errorf("camera servo disabled") +} + +func (c *CameraServo) CurrentAngle() float64 { + return 0 +} diff --git a/pi/roverd/config.go b/pi/roverd/config.go index caf1b153..e2b81030 100644 --- a/pi/roverd/config.go +++ b/pi/roverd/config.go @@ -93,17 +93,18 @@ type VideoMediaConfig struct { // Publisher selects the installed publisher script/pipeline family. The // first pass uses pi-libcamera for current rovers; laptop-v4l2 can be added // without changing the server-facing media shape again. - Enabled bool `yaml:"enabled" json:"enabled"` - Service string `yaml:"service" json:"service,omitempty"` - Publisher string `yaml:"publisher" json:"publisher,omitempty"` - PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"` - Device string `yaml:"device" json:"device,omitempty"` - Width int `yaml:"width" json:"-"` - Height int `yaml:"height" json:"-"` - FPS int `yaml:"fps" json:"-"` - Bitrate int `yaml:"bitrate" json:"-"` - Inverted bool `yaml:"inverted" json:"-"` - SensorMode string `yaml:"sensorMode" json:"-"` + Enabled bool `yaml:"enabled" json:"enabled"` + Service string `yaml:"service" json:"service,omitempty"` + Publisher string `yaml:"publisher" json:"publisher,omitempty"` + PublishURL string `yaml:"publishUrl" json:"publishUrl,omitempty"` + Device string `yaml:"device" json:"device,omitempty"` + InputFormat string `yaml:"inputFormat" json:"-"` + Width int `yaml:"width" json:"-"` + Height int `yaml:"height" json:"-"` + FPS int `yaml:"fps" json:"-"` + Bitrate int `yaml:"bitrate" json:"-"` + Inverted bool `yaml:"inverted" json:"-"` + SensorMode string `yaml:"sensorMode" json:"-"` } type AudioCaptureConfig struct { @@ -450,6 +451,10 @@ func validateVideoMediaConfig(cfg *VideoMediaConfig, serverURL string, roverName if cfg.Publisher == "" { cfg.Publisher = "pi-libcamera" } + // V4L2 input formats are consumed by ffmpeg as lowercase names such as + // mjpeg or yuyv422. Normalizing here keeps the publisher script simple and + // makes hand-edited Debian laptop configs less sensitive to capitalization. + cfg.InputFormat = strings.ToLower(strings.TrimSpace(cfg.InputFormat)) if cfg.Width <= 0 { cfg.Width = 640 } diff --git a/pi/roverd/gpio_toggle.go b/pi/roverd/gpio_toggle.go index 52557cc3..43d1c9f7 100644 --- a/pi/roverd/gpio_toggle.go +++ b/pi/roverd/gpio_toggle.go @@ -1,4 +1,4 @@ -//go:build !dummy +//go:build !dummy && !debian_laptop package roverd diff --git a/pi/roverd/gpio_toggle_debian_laptop.go b/pi/roverd/gpio_toggle_debian_laptop.go new file mode 100644 index 00000000..71bfabed --- /dev/null +++ b/pi/roverd/gpio_toggle_debian_laptop.go @@ -0,0 +1,32 @@ +//go:build debian_laptop + +package roverd + +import ( + "fmt" + "log" +) + +type GPIOToggle struct { + name string +} + +func NewGPIOToggle(name string, _ GPIOToggleConfig, _ *log.Logger) (*GPIOToggle, error) { + /* + A Debian laptop has no Raspberry Pi GPIO character-device contract for + headlights or lasers. Returning an error when enabled makes bad laptop + configs fail during startup instead of advertising controls that cannot + change any hardware. + */ + return nil, fmt.Errorf("%s not supported in the debian-laptop build", name) +} + +func (g *GPIOToggle) Close() {} + +func (g *GPIOToggle) HandleAction(action string) error { + return fmt.Errorf("%s not supported in the debian-laptop build", g.name) +} + +func (g *GPIOToggle) On() bool { + return false +} diff --git a/pi/roverd/media_env.go b/pi/roverd/media_env.go index 81b35eae..a72dba03 100644 --- a/pi/roverd/media_env.go +++ b/pi/roverd/media_env.go @@ -36,6 +36,7 @@ func UpdatePublisherEnv(media MediaConfig) error { fmt.Fprintf(&buf, "ROVERD_VIDEO_PUBLISHER=%s\n", media.Video.Publisher) fmt.Fprintf(&buf, "ROVERD_VIDEO_PUBLISH_URL=%s\n", media.Video.PublishURL) fmt.Fprintf(&buf, "ROVERD_VIDEO_DEVICE=%s\n", media.Video.Device) + fmt.Fprintf(&buf, "ROVERD_VIDEO_INPUT_FORMAT=%s\n", media.Video.InputFormat) fmt.Fprintf(&buf, "ROVERD_VIDEO_WIDTH=%d\n", media.Video.Width) fmt.Fprintf(&buf, "ROVERD_VIDEO_HEIGHT=%d\n", media.Video.Height) fmt.Fprintf(&buf, "ROVERD_VIDEO_FPS=%d\n", media.Video.FPS) diff --git a/pi/roverd/roverd.debian-laptop.sample.yaml b/pi/roverd/roverd.debian-laptop.sample.yaml new file mode 100644 index 00000000..060224e5 --- /dev/null +++ b/pi/roverd/roverd.debian-laptop.sample.yaml @@ -0,0 +1,98 @@ +# Sample configuration for roverd on a Debian laptop rover. +name: laptop-rover +description: "Debian laptop rover using USB serial, webcam, mic, and speakers." +color: "#4DB6AC" +serverUrl: ws://control-server.local:8080/rover + +serial: + device: /dev/ttyUSB0 + baud: 115200 + +# BRC is disabled in the first Debian laptop profile because USB serial +# RTS/DTR wake-pulse support needs adapter-specific testing before it should be +# trusted to keep a Roomba awake. +brc: + gpioPin: -1 + +battery: + full: 2068 + warn: 1700 + urgent: 1650 + +maxWheelSpeed: 350 + +media: + publishPort: 9000 + manage: true + healthUrl: "" + healthInterval: 30s + video: + enabled: true + service: debian-laptop-video-publisher.service + publisher: debian-laptop-v4l2 + device: /dev/video0 + # Check supported values with: + # v4l2-ctl --device=/dev/video0 --list-formats-ext + # Common working values are mjpeg and yuyv422. + inputFormat: mjpeg + width: 640 + height: 480 + fps: 30 + bitrate: 2000000 + inverted: false + audioCapture: + enabled: true + service: audio-only-publisher.service + device: default + sampleRate: 48000 + channels: 2 + bitrate: 510000 + audioPlayback: + enabled: true + service: audio-forward-listener.service + # The Debian laptop installer creates a named "forward" ALSA device so + # browser-forwarded audio has its own ForwardMaster mixer control instead + # of sharing the TTS default output path. + device: forward + normalize: true + +cameraServo: + enabled: false + +audio: + # TTS does not need Pi-specific hardware. The laptop ALSA profile maps the + # normal default playback device through the TTSMaster softvol control, so + # the existing flite/espeak execution path can stay unchanged. + ttsEnabled: true + defaultEngine: flite + +horn: + # Horn audio is generated locally by roverd and plays through the named + # "horn" ALSA device by default, which the laptop installer wires to a + # separate HornMaster softvol control. + enabled: true + +headlight: + enabled: false + +laser: + enabled: false + +autoSideBrush: + enabled: true + speed: 20 + +private: + enabled: false + safety: + speedLimitEnabled: false + speedLimitMaxWheelSpeed: 250 + hardOvercurrentEnabled: false + overcurrentStopMs: 300 + hardBumpEnabled: false + bumpBackoffSpeed: 250 + bumpBackoffMs: 350 + cliffEnabled: false + cliffBackoffSpeed: 250 + cliffBackoffMs: 500 + triggerCooldownMs: 800 diff --git a/pi/systemd/debian-laptop-video-publisher.service b/pi/systemd/debian-laptop-video-publisher.service new file mode 100644 index 00000000..6a9d61a1 --- /dev/null +++ b/pi/systemd/debian-laptop-video-publisher.service @@ -0,0 +1,17 @@ +[Unit] +Description=Rover Debian Laptop Video Publisher (V4L2 -> SRT) +After=network-online.target roverd.service +Wants=network-online.target + +[Service] +Type=simple +User=roverd +Group=roverd +WorkingDirectory=/var/lib/roverd +EnvironmentFile=/var/lib/roverd/media.env +ExecStart=/usr/local/bin/debian-laptop-video-publisher +Restart=always +RestartSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/to-do.md b/to-do.md index ecbb890b..4a73c93b 100644 --- a/to-do.md +++ b/to-do.md @@ -1,6 +1,10 @@ 1. improve spectator page, options on what to see and what not to see -2. fix private rover requests not working..... -3. fix this: +2. pagewide system for "why was i removed from a rover", instead of the link to spectator page thing +3. admin command for kicking people off of rovers. just a kick, nothing persistent +4. kick people off rover after 3 consecurtive bump-off attempts +5. add rs admin command for controlling light lock +6. make signaling more clear for idle skips and idle skip kicks +7. fix this: `Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92 Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message }); Jun 18 15:14:18 roombaserver.local node[216731]: ^