mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
334 lines
13 KiB
Bash
Executable File
334 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
MEDIAMTX_VERSION="1.15.3"
|
|
NEOLINK_VERSION="0.6.2"
|
|
MEDIAMTX_BASE_URL="https://github.com/bluenviron/mediamtx/releases/download/v${MEDIAMTX_VERSION}"
|
|
NEOLINK_BASE_URL="https://github.com/QuantumEntangledAndy/neolink/releases/download/v${NEOLINK_VERSION}"
|
|
MEDIAMTX_BIN="/usr/local/bin/mediamtx"
|
|
NEOLINK_BIN="/usr/local/bin/neolink"
|
|
CHROMEGTTS_WAV_BIN="/usr/local/bin/chromegtts-wav"
|
|
ROVER_SNAPSHOT_WRITER_BIN="/usr/local/bin/rover-snapshot-writer.sh"
|
|
MEDIAMTX_SERVICE="/etc/systemd/system/mediamtx.service"
|
|
MULTIROVER_SERVICE="/etc/systemd/system/multirover.service"
|
|
KINECT_UDEV_RULE="/etc/udev/rules.d/99-kinect-world.rules"
|
|
BLUETOOTH_OVERRIDE_DIR="/etc/systemd/system/bluetooth.service.d"
|
|
BLUETOOTH_OVERRIDE="$BLUETOOTH_OVERRIDE_DIR/20-multirover-balance-board.conf"
|
|
|
|
if [[ $EUID -ne 0 ]]; then
|
|
echo "This installer must be run with sudo/root." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${SUDO_USER:-}" || "${SUDO_USER}" == "root" ]]; then
|
|
echo "Run this script via 'sudo' from the normal user that owns the repo." >&2
|
|
exit 1
|
|
fi
|
|
|
|
TARGET_USER="$SUDO_USER"
|
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
SERVER_DIR="$SCRIPT_DIR"
|
|
DATA_DIR="$SERVER_DIR/data"
|
|
SNAPSHOT_DIR="$DATA_DIR/rover-snapshots"
|
|
BALANCE_BOARD_NATIVE_DIR="$SCRIPT_DIR/src/services/balanceBoardService/native"
|
|
BALANCE_BOARD_WORKER="$BALANCE_BOARD_NATIVE_DIR/balance_board_worker"
|
|
ROVER_SNAPSHOT_WRITER_TEMPLATE="$SERVER_DIR/mediamtx/rover-snapshot-writer.sh"
|
|
CHROMEGTTS_WAV_TEMPLATE="$SERVER_DIR/bin/chromegtts-wav.py"
|
|
|
|
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 lib_member=""
|
|
local arch_name
|
|
arch_name=$(uname -m)
|
|
|
|
# The PTZ camera is not a rover, so Google speech must be synthesized on the
|
|
# server before neolink sends a WAV to the camera. These assets are the same
|
|
# offline ChromeOS local TTS assets that rover installers already use; keeping
|
|
# the layout identical lets the server helper and rover daemon share loader
|
|
# assumptions.
|
|
if [[ -f "${asset_dir}/libchrometts.so" && -f "${voice_dir}/pipeline.pb" ]]; then
|
|
echo " Google TTS assets already installed"
|
|
return
|
|
fi
|
|
|
|
case "$arch_name" in
|
|
x86_64|amd64)
|
|
lib_member="libchrometts_x86_64.so"
|
|
;;
|
|
aarch64)
|
|
lib_member="libchrometts_arm64.so"
|
|
;;
|
|
armv7l|armv6l)
|
|
lib_member="libchrometts_armv7.so"
|
|
;;
|
|
*)
|
|
echo "Unsupported Google TTS architecture: $arch_name" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
echo " Installing Google TTS assets -> $asset_dir"
|
|
curl -L -o "$tmpdir/googletts-26.5.tar.xz" "$dist_url"
|
|
tar -xf "$tmpdir/googletts-26.5.tar.xz" -C "$tmpdir" en-us-x-multi.zvoice "$lib_member"
|
|
install -d -o root -g root -m 0755 "$asset_dir"
|
|
install -o root -g root -m 0644 "$tmpdir/$lib_member" "${asset_dir}/libchrometts.so"
|
|
rm -rf "$voice_dir"
|
|
install -d -o root -g root -m 0755 "$voice_dir"
|
|
# The .zvoice member is a zip archive inside the outer tar.xz. Match the
|
|
# rover installers here; trying to untar it fails after the large download.
|
|
unzip -q "$tmpdir/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 {} +
|
|
}
|
|
|
|
verify_google_tts_helper() {
|
|
local smoke_wav="$tmpdir/chromegtts-smoke.wav"
|
|
|
|
echo " Verifying Chrome Google TTS helper"
|
|
# libchrometts is a native ChromeOS library. Rendering one tiny WAV during
|
|
# install catches missing shared-library dependencies, bad asset extraction,
|
|
# and helper path mistakes before multirover.service starts accepting PTZ TTS
|
|
# requests that would fail later in logs.
|
|
if ! "$CHROMEGTTS_WAV_BIN" \
|
|
--text "test" \
|
|
--voice tpf \
|
|
--pitch 1 \
|
|
--speed 1 \
|
|
--output "$smoke_wav"; then
|
|
echo "Chrome Google TTS helper smoke render failed." >&2
|
|
return 1
|
|
fi
|
|
if [[ ! -s "$smoke_wav" ]]; then
|
|
echo "Chrome Google TTS helper did not create a WAV file." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
echo "[1/6] Installing dependencies..."
|
|
# The Kinect tooling uses a native libfreenect worker/probe rather than a
|
|
# Python wrapper. Install both runtime and development headers here so a fresh
|
|
# Fedora server can build the worker locally and then run it under the same
|
|
# normal user that owns the rover service.
|
|
dnf install -y \
|
|
nodejs \
|
|
npm \
|
|
curl \
|
|
tar \
|
|
unzip \
|
|
xz \
|
|
gcc-c++ \
|
|
make \
|
|
pkgconf-pkg-config \
|
|
flite \
|
|
espeak \
|
|
python3 \
|
|
libcxx \
|
|
libcxxabi \
|
|
gstreamer1 \
|
|
gstreamer1-plugins-base \
|
|
gstreamer1-plugins-good \
|
|
gstreamer1-plugins-bad-free \
|
|
gstreamer1-rtsp-server \
|
|
libfreenect \
|
|
libfreenect-devel \
|
|
libusb1-devel \
|
|
bluez \
|
|
wiiuse \
|
|
wiiuse-devel \
|
|
libcap >/dev/null
|
|
NODE_BIN="$(command -v node)"
|
|
|
|
echo " Installing Kinect udev rule -> $KINECT_UDEV_RULE"
|
|
cat > "$KINECT_UDEV_RULE" <<'EOF'
|
|
# Xbox 360 / Kinect v1 exposes motor, audio, and camera as separate Microsoft
|
|
# USB devices. Fedora/OpenNI PrimeSense rules can leave the camera node as
|
|
# root:primesense 0660, which makes libfreenect fail with LIBUSB_ERROR_ACCESS
|
|
# when the rover server runs as the normal service user. This late 99-* rule is
|
|
# intentionally broad for local rover hardware: every Microsoft Kinect sibling
|
|
# gets world read/write access so the native libfreenect worker can open the
|
|
# camera without running the whole server as root.
|
|
SUBSYSTEM=="usb", ATTR{idVendor}=="045e", MODE="0666", GROUP="root", TAG+="uaccess"
|
|
EOF
|
|
chmod 644 "$KINECT_UDEV_RULE"
|
|
udevadm control --reload-rules
|
|
|
|
if [[ ! -f "$CHROMEGTTS_WAV_TEMPLATE" ]]; then
|
|
echo "Chrome Google TTS WAV helper missing at $CHROMEGTTS_WAV_TEMPLATE" >&2
|
|
exit 1
|
|
fi
|
|
echo " Installing Chrome Google TTS WAV helper -> $CHROMEGTTS_WAV_BIN"
|
|
install -m 0755 "$CHROMEGTTS_WAV_TEMPLATE" "$CHROMEGTTS_WAV_BIN"
|
|
|
|
echo "[2/6] Installing Node production deps..."
|
|
runuser -u "$TARGET_USER" -- bash -c "cd '$SERVER_DIR' && npm install --production"
|
|
|
|
if [[ -f "$SERVER_DIR/src/services/kinectService/native/Makefile" ]]; then
|
|
echo " Building native Kinect worker..."
|
|
runuser -u "$TARGET_USER" -- bash -c "cd '$SERVER_DIR/src/services/kinectService/native' && make"
|
|
fi
|
|
|
|
if [[ -f "$BALANCE_BOARD_NATIVE_DIR/Makefile" ]]; then
|
|
echo " Building native Balance Board bridge..."
|
|
runuser -u "$TARGET_USER" -- bash -c "cd '$BALANCE_BOARD_NATIVE_DIR' && make"
|
|
if [[ ! -x "$BALANCE_BOARD_WORKER" ]]; then
|
|
echo "Balance Board worker build did not create $BALANCE_BOARD_WORKER" >&2
|
|
exit 1
|
|
fi
|
|
# Only this small audited bridge needs the management socket used for the
|
|
# board's raw six-byte pairing PIN and the two reserved HID PSMs used by
|
|
# front-button reconnects. Never grant either capability to node or the full
|
|
# multirover service executable.
|
|
setcap cap_net_admin,cap_net_bind_service+ep "$BALANCE_BOARD_WORKER"
|
|
fi
|
|
|
|
# Bluetoothd remains responsible for discovery and the one-time bond, but its
|
|
# generic input plugin otherwise reserves control PSM 0x11 and interrupt PSM
|
|
# 0x13 before the Balance Board worker can listen for the board's front-button
|
|
# reconnect. This dedicated rover server gives those two HID listeners to the
|
|
# worker; every other BlueZ profile is left enabled. Clearing ExecStart is
|
|
# required by systemd before replacing the vendor unit's command in a drop-in.
|
|
install -d -m 0755 "$BLUETOOTH_OVERRIDE_DIR"
|
|
cat > "$BLUETOOTH_OVERRIDE" <<'EOF'
|
|
[Service]
|
|
ExecStart=
|
|
ExecStart=/usr/libexec/bluetooth/bluetoothd --noplugin=input
|
|
EOF
|
|
chmod 0644 "$BLUETOOTH_OVERRIDE"
|
|
systemctl daemon-reload
|
|
systemctl enable bluetooth.service
|
|
systemctl restart bluetooth.service
|
|
|
|
tmpdir=$(mktemp -d)
|
|
trap 'rm -rf "$tmpdir"' EXIT
|
|
|
|
arch=$(uname -m)
|
|
case "$arch" in
|
|
x86_64|amd64)
|
|
mediamtx_pkg="mediamtx_v${MEDIAMTX_VERSION}_linux_amd64.tar.gz"
|
|
neolink_pkg="neolink_linux_x86_64_ubuntu.zip"
|
|
;;
|
|
aarch64)
|
|
mediamtx_pkg="mediamtx_v${MEDIAMTX_VERSION}_linux_arm64.tar.gz"
|
|
neolink_pkg="neolink_linux_arm64.zip"
|
|
;;
|
|
armv7l)
|
|
mediamtx_pkg="mediamtx_v${MEDIAMTX_VERSION}_linux_armv7.tar.gz"
|
|
neolink_pkg="neolink_linux_armhf.zip"
|
|
;;
|
|
*)
|
|
echo "Unsupported architecture: $arch" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
echo "[3/6] Installing mediaMTX ${MEDIAMTX_VERSION}..."
|
|
curl -L "$MEDIAMTX_BASE_URL/$mediamtx_pkg" -o "$tmpdir/mediamtx.tgz"
|
|
tar -xzf "$tmpdir/mediamtx.tgz" -C "$tmpdir" mediamtx
|
|
install -m 0755 "$tmpdir/mediamtx" "$MEDIAMTX_BIN"
|
|
|
|
echo " Installing neolink ${NEOLINK_VERSION} -> $NEOLINK_BIN"
|
|
curl -L "$NEOLINK_BASE_URL/$neolink_pkg" -o "$tmpdir/neolink.zip"
|
|
unzip -q "$tmpdir/neolink.zip" -d "$tmpdir/neolink"
|
|
neolink_extracted=$(find "$tmpdir/neolink" -type f -name neolink -perm /111 | head -n 1)
|
|
if [[ -z "$neolink_extracted" ]]; then
|
|
neolink_extracted=$(find "$tmpdir/neolink" -type f -name neolink | head -n 1)
|
|
fi
|
|
if [[ -z "$neolink_extracted" ]]; then
|
|
echo "neolink binary missing from $neolink_pkg" >&2
|
|
exit 1
|
|
fi
|
|
install -m 0755 "$neolink_extracted" "$NEOLINK_BIN"
|
|
install_google_tts_assets
|
|
if ! verify_google_tts_helper; then
|
|
echo " Reinstalling Google TTS assets after failed verification"
|
|
rm -rf /opt/roverd/googletts
|
|
install_google_tts_assets
|
|
verify_google_tts_helper
|
|
fi
|
|
|
|
if [[ ! -f "$ROVER_SNAPSHOT_WRITER_TEMPLATE" ]]; then
|
|
echo "Snapshot writer template missing at $ROVER_SNAPSHOT_WRITER_TEMPLATE" >&2
|
|
exit 1
|
|
fi
|
|
echo " Installing rover snapshot writer -> $ROVER_SNAPSHOT_WRITER_BIN"
|
|
install -m 0755 "$ROVER_SNAPSHOT_WRITER_TEMPLATE" "$ROVER_SNAPSHOT_WRITER_BIN"
|
|
|
|
# Validate database-backed MediaMTX inputs before disabling a working legacy
|
|
# service. This performs the same build and serialization as startup without
|
|
# opening listeners or leaving a process behind.
|
|
runuser -u "$TARGET_USER" -- env \
|
|
SERVER_DATA_DIR="$DATA_DIR" \
|
|
ROVER_SNAPSHOT_WRITER_BIN="$ROVER_SNAPSHOT_WRITER_BIN" \
|
|
"$NODE_BIN" "$SERVER_DIR/scripts/validateMediaMtxConfig.js"
|
|
|
|
# MediaMTX used to run as its own systemd service with a hand-maintained config in
|
|
# /etc/mediamtx. Stop it before multirover starts the new child process, otherwise the two
|
|
# processes race for every media listener. Both commands are deliberately idempotent so an
|
|
# already-migrated server and a first-time installation follow the same path.
|
|
echo " Disabling legacy mediamtx.service"
|
|
systemctl disable --now mediamtx.service 2>/dev/null || true
|
|
rm -f "$MEDIAMTX_SERVICE"
|
|
rm -f /etc/mediamtx/mediamtx.yml
|
|
|
|
echo "[4/6] Writing systemd units..."
|
|
# The repository data directory is the legacy deployment's single persistence
|
|
# root and becomes the one bind-mounted /data directory during containerization.
|
|
# Create only the snapshot child eagerly because MediaMTX's hook writes there;
|
|
# the other services already create their own children when those features run.
|
|
mkdir -p "$DATA_DIR" "$SNAPSHOT_DIR"
|
|
chown "$TARGET_USER":"$TARGET_USER" "$DATA_DIR" "$SNAPSHOT_DIR"
|
|
|
|
# Previous installers used these two /var/lib directories. Replay code already
|
|
# stopped reading its old location, and snapshots regenerate immediately, so do
|
|
# not merge possibly stale runtime media over the new canonical data tree. Keep
|
|
# an existing directory untouched and report it for deliberate cleanup after the
|
|
# operator verifies the upgraded server.
|
|
for legacy_dir in /var/lib/rover-snapshots /var/lib/replay-segments; do
|
|
if [[ -d "$legacy_dir" ]]; then
|
|
echo " Legacy runtime directory is no longer used: $legacy_dir"
|
|
fi
|
|
done
|
|
cat > "$MULTIROVER_SERVICE" <<EOF
|
|
[Unit]
|
|
Description=Multi-Roomba Rover control server
|
|
After=network-online.target bluetooth.service
|
|
Wants=network-online.target bluetooth.service
|
|
|
|
[Service]
|
|
User=$TARGET_USER
|
|
Group=$TARGET_USER
|
|
WorkingDirectory=$SERVER_DIR
|
|
Environment=NODE_ENV=production
|
|
Environment=SERVER_DATA_DIR=$DATA_DIR
|
|
Environment=ROVER_SNAPSHOT_WRITER_BIN=$ROVER_SNAPSHOT_WRITER_BIN
|
|
ExecStart=$NODE_BIN $SERVER_DIR/index.js
|
|
Restart=on-failure
|
|
RestartSec=2
|
|
SuccessExitStatus=130 143
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
chmod 644 "$MULTIROVER_SERVICE"
|
|
|
|
echo "[5/6] Enabling services..."
|
|
systemctl daemon-reload
|
|
systemctl enable --now multirover.service
|
|
systemctl restart multirover.service
|
|
|
|
echo "[6/6] Done."
|
|
echo
|
|
echo "Services installed:"
|
|
echo " multirover.service (Node.js control server with MediaMTX child)"
|
|
echo
|
|
echo "Open /setup to initialize the installation, then use /admin for administration."
|
|
echo "For fresh setup, read the one-time code from $DATA_DIR/setup-code.txt."
|
|
echo "Kinect/libfreenect packages and udev permissions were installed."
|
|
echo "If a Kinect is already plugged in, unplug/replug its USB/power before testing so the new udev rule applies."
|
|
echo "Wii Balance Board direct Bluetooth bridge and front-button listener were installed."
|
|
echo "Enable Balance Board support in /admin, press red Sync once, then use the front button for later wakes."
|