Merge pull request #12 from legop3/laptoprover

Laptoprover
This commit is contained in:
legop3
2026-07-04 20:24:17 -04:00
committed by GitHub
26 changed files with 1071 additions and 385 deletions
BIN
View File
Binary file not shown.
Vendored
BIN
View File
Binary file not shown.
Vendored Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+88
View File
@@ -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
}
+143
View File
@@ -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
+79
View File
@@ -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"
}
+42
View File
@@ -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
}
+89
View File
@@ -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
}
+160
View File
@@ -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
}
+105
View File
@@ -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 <name> Install profile: pi or debian-laptop (default: pi)
--debian-laptop Shortcut for --profile debian-laptop
-b, --binary <path> Path to the roverd binary (default depends on profile)
-c, --config <path> 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
}
+38
View File
@@ -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" <<ENV
# Managed by pi/install_roverd.sh.
# This path is intentionally captured from the installer working directory so
# admin-triggered rover updates always operate on the same full repository that
# was used for the manual install.
ROVERD_REPO_DIR=$quoted_repo_root
ENV
log "Registered roverd update repository at $REPO_ROOT"
# The helper is root-owned and argument-free. sudoers grants the roverd
# service user exactly this command and nothing broader, which is important
# because update requests arrive over the rover websocket.
install -D -o root -g root -m 0755 pi/bin/roverd-self-update.sh /usr/local/sbin/roverd-self-update
cat > "$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"
}
+43
View File
@@ -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
}
+38 -363
View File
@@ -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> Path to the roverd binary (default: dist/roverd)
-c, --config <path> 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" <<ENV
# Managed by pi/install_roverd.sh.
# This path is intentionally captured from the installer working directory so
# admin-triggered rover updates always operate on the same full repository that
# was used for the manual install.
ROVERD_REPO_DIR=$quoted_repo_root
ENV
log "Registered roverd update repository at $REPO_ROOT"
# The helper is root-owned and argument-free. sudoers grants the roverd
# service user exactly this command and nothing broader, which is important
# because update requests arrive over the rover websocket.
install -D -o root -g root -m 0755 pi/bin/roverd-self-update.sh /usr/local/sbin/roverd-self-update
cat > "$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 "$@"
+10 -6
View File
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !dummy
//go:build !dummy && !debian_laptop
package roverd
+25
View File
@@ -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) {}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !dummy
//go:build !dummy && !debian_laptop
package roverd
+38
View File
@@ -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
}
+16 -11
View File
@@ -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
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !dummy
//go:build !dummy && !debian_laptop
package roverd
+32
View File
@@ -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
}
+1
View File
@@ -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)
@@ -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
@@ -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
+6 -2
View File
@@ -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]: ^