mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
installer splitup and laptop alsa stuff
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+35
-500
@@ -1,513 +1,48 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Installer for the roverd agent on Raspberry Pi and Debian laptop rover hosts.
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
PROFILE="pi"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
BINARY_SRC=""
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
CONFIG_SRC=""
|
|
||||||
REPO_ROOT="$(pwd -P)"
|
|
||||||
|
|
||||||
usage() {
|
# The helper scripts use repository-relative paths because those same paths are
|
||||||
cat <<'USAGE'
|
# shown in docs and logs. Moving to the repository root once keeps every module
|
||||||
Usage: sudo ./pi/install_roverd.sh [options]
|
# simple and prevents each function from needing its own path resolver.
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
Options:
|
source pi/install/common.sh
|
||||||
--profile <name> Install profile: pi or debian-laptop (default: pi)
|
source pi/install/profiles.sh
|
||||||
--debian-laptop Shortcut for --profile debian-laptop
|
source pi/install/self_update.sh
|
||||||
-b, --binary <path> Path to the roverd binary (default: dist/roverd)
|
source pi/install/pi_profile.sh
|
||||||
-c, --config <path> Source config to install if /etc/roverd.yaml is missing
|
source pi/install/debian_laptop_profile.sh
|
||||||
(default: pi/roverd/roverd.sample.yaml)
|
source pi/install/media_env.sh
|
||||||
-h, --help Show this help text
|
source pi/install/systemd.sh
|
||||||
|
|
||||||
The script must run from the repository root and as root (sudo). It will:
|
main() {
|
||||||
* create system users/groups if needed
|
parse_args "$@"
|
||||||
* install /usr/local/bin/roverd and /etc/roverd.yaml
|
select_profile
|
||||||
* install profile-specific video helpers and shared audio helpers
|
require_root
|
||||||
* install Pi-only Google Chrome TTS assets when using the pi profile
|
validate_install_inputs
|
||||||
* install the fixed-command self-update helper used by admin-triggered updates
|
|
||||||
* enable roverd.service and media publisher/listener services
|
|
||||||
USAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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:-}"
|
|
||||||
local existing_groups=""
|
|
||||||
if ! id -u "$user" >/dev/null 2>&1; then
|
|
||||||
if [[ -n "$groups" ]]; then
|
|
||||||
local group
|
|
||||||
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
|
|
||||||
log "Skipping missing system group '$group' for $user"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
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 "$groups" ]]; then
|
|
||||||
local group
|
|
||||||
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
|
|
||||||
log "Skipping missing system group '$group' for $user"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ -n "$existing_groups" ]]; then
|
|
||||||
usermod -a -G "$existing_groups" "$user"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$PROFILE" == "pi" ]] && ! 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
|
|
||||||
}
|
|
||||||
|
|
||||||
install_debian_laptop_deps() {
|
|
||||||
# The debian-laptop profile is intentionally Debian-specific, so using apt
|
|
||||||
# and systemd directly is simpler than pretending this installer is a
|
|
||||||
# cross-distro abstraction. v4l-utils is included for local camera inspection
|
|
||||||
# with tools such as v4l2-ctl while ffmpeg owns publishing.
|
|
||||||
if command -v ffmpeg >/dev/null 2>&1 \
|
|
||||||
&& command -v arecord >/dev/null 2>&1 \
|
|
||||||
&& command -v aplay >/dev/null 2>&1 \
|
|
||||||
&& command -v v4l2-ctl >/dev/null 2>&1; then
|
|
||||||
log "Debian laptop media dependencies already installed; skipping apt install"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
log "Installing Debian laptop media dependencies (ffmpeg, ALSA tools, V4L2 tools)..."
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --no-install-recommends ffmpeg alsa-utils v4l-utils ca-certificates
|
|
||||||
}
|
|
||||||
|
|
||||||
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_profile_dependencies() {
|
|
||||||
case "$PROFILE" in
|
|
||||||
pi)
|
|
||||||
install_video_deps
|
|
||||||
ensure_pwm_overlay
|
|
||||||
;;
|
|
||||||
debian-laptop)
|
|
||||||
install_debian_laptop_deps
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
install_profile_audio_support() {
|
|
||||||
case "$PROFILE" in
|
|
||||||
pi)
|
|
||||||
install_audio_support
|
|
||||||
;;
|
|
||||||
debian-laptop)
|
|
||||||
# Laptop audio uses the distro's normal ALSA "default" device by
|
|
||||||
# default. Avoid installing the Raspberry Pi AIY asound.conf because
|
|
||||||
# that would break ordinary laptop sound routing.
|
|
||||||
log "Skipping Pi AIY audio setup for debian-laptop profile"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
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_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_roverd_binary
|
||||||
install_self_update_support
|
install_self_update_support
|
||||||
|
install_roverd_config
|
||||||
|
install_roverd_unit
|
||||||
|
|
||||||
CONFIG_DEST="/etc/roverd.yaml"
|
install_selected_profile
|
||||||
CONFIG_EXISTS=0
|
install_media_units
|
||||||
if [[ -f "$CONFIG_DEST" ]]; then
|
write_media_env_placeholder
|
||||||
CONFIG_EXISTS=1
|
install_audio_fifo
|
||||||
log "Existing $CONFIG_DEST found; leaving it in place"
|
enable_and_restart_units
|
||||||
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_profile_dependencies
|
|
||||||
|
|
||||||
# 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 the profile-specific video publisher. The audio publisher/listener are
|
|
||||||
# shared because both Pi and Debian laptop profiles use the same media.env audio
|
|
||||||
# contract and ffmpeg/ALSA command shape.
|
|
||||||
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"
|
|
||||||
# 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
|
|
||||||
case "$PROFILE" in
|
|
||||||
pi)
|
|
||||||
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)
|
|
||||||
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=default
|
|
||||||
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
|
|
||||||
# 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
|
|
||||||
if [[ "$PROFILE" == "pi" ]]; then
|
|
||||||
# Ensure ALSA config is in place for the Raspberry Pi Google Voice HAT
|
|
||||||
# rovermic/forward devices. Debian laptop installs should keep their normal
|
|
||||||
# distro sound configuration and use device: default in roverd.yaml.
|
|
||||||
install -m 0644 pi/asound.conf /etc/asound.conf
|
|
||||||
log "Installed ALSA config (/etc/asound.conf)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
install_profile_audio_support
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
log "Install complete for profile $PROFILE"
|
log "Install complete for profile $PROFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|||||||
@@ -50,17 +50,27 @@ media:
|
|||||||
audioPlayback:
|
audioPlayback:
|
||||||
enabled: true
|
enabled: true
|
||||||
service: audio-forward-listener.service
|
service: audio-forward-listener.service
|
||||||
device: default
|
# 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
|
normalize: true
|
||||||
|
|
||||||
cameraServo:
|
cameraServo:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
audio:
|
audio:
|
||||||
ttsEnabled: false
|
# 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:
|
||||||
enabled: false
|
# 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:
|
headlight:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
1. improve spectator page, options on what to see and what not to see
|
1. improve spectator page, options on what to see and what not to see
|
||||||
2. fix private rover requests not working.....
|
2. pagewide system for "why was i removed from a rover", instead of the link to spectator page thing
|
||||||
3. fix this:
|
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. make signaling more clear for idle skips and idle skip kicks
|
||||||
|
6. 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]: /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]: cb({ error: err.message });
|
||||||
Jun 18 15:14:18 roombaserver.local node[216731]: ^
|
Jun 18 15:14:18 roombaserver.local node[216731]: ^
|
||||||
|
|||||||
Reference in New Issue
Block a user