#!/usr/bin/env bash set -Eeuo pipefail APP_NAME="pi-car-companion" SERVICE_USER="pi-companion" INSTALL_ROOT="/opt/${APP_NAME}" REPOSITORY="${INSTALL_ROOT}/repository" RELEASES="${INSTALL_ROOT}/releases" CURRENT="${INSTALL_ROOT}/current" CONFIG_DIR="/etc/${APP_NAME}" DATA_DIR="/var/lib/${APP_NAME}" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" if [[ ${EUID} -ne 0 ]]; then if ! command -v sudo >/dev/null 2>&1; then echo "This installer needs root access and sudo is not installed." >&2 exit 1 fi exec sudo -- bash "${SCRIPT_DIR}/install.sh" "$@" fi if [[ ! -d "${SCRIPT_DIR}/.git" ]]; then echo "Run install.sh from a Git clone of Pi Car Companion." >&2 exit 1 fi if [[ ! -f /etc/os-release ]]; then echo "This installer supports Raspberry Pi OS and Debian." >&2 exit 1 fi source /etc/os-release if [[ "${ID:-}" != "debian" && "${ID:-}" != "raspbian" && "${ID_LIKE:-}" != *debian* ]]; then echo "Unsupported operating system: ${PRETTY_NAME:-unknown}." >&2 exit 1 fi echo "[1/7] Installing operating-system packages" export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y \ android-tools-adb \ avahi-daemon \ bluez \ build-essential \ ca-certificates \ conntrack \ curl \ dnsmasq-base \ firmware-realtek \ git \ gnupg \ iproute2 \ network-manager \ nftables \ python3 \ python3-dbus \ python3-gi \ rfkill \ rsync \ sqlite3 \ sudo \ util-linux \ uuid-runtime \ wireless-regdb \ wpasupplicant for required_command in conntrack dnsmasq ip journalctl nmcli nft rfkill uuidgen wpa_passphrase; do if ! command -v "${required_command}" >/dev/null 2>&1; then echo "Required network command was not installed: ${required_command}" >&2 exit 1 fi done node_major="" if command -v node >/dev/null 2>&1; then node_major="$(node --version | sed -E 's/^v([0-9]+).*/\1/')" fi if [[ "${node_major}" != "22" ]]; then echo "[2/7] Installing Node.js 22" install -d -m 0755 /etc/apt/keyrings key_tmp="$(mktemp)" curl --fail --silent --show-error --location https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key --output "${key_tmp}" gpg --dearmor --yes --output /etc/apt/keyrings/nodesource.gpg "${key_tmp}" rm -f -- "${key_tmp}" chmod 0644 /etc/apt/keyrings/nodesource.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list apt-get update apt-get install -y nodejs else echo "[2/7] Node.js 22 is already installed" fi echo "[3/7] Creating the service account and directories" if ! id "${SERVICE_USER}" >/dev/null 2>&1; then useradd --system --home-dir "${DATA_DIR}" --shell /usr/sbin/nologin --user-group "${SERVICE_USER}" fi if getent group plugdev >/dev/null 2>&1; then usermod --append --groups plugdev "${SERVICE_USER}" fi install -d -m 0755 -o root -g root "${INSTALL_ROOT}" "${REPOSITORY}" "${RELEASES}" install -d -m 0750 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${DATA_DIR}" install -d -m 0750 -o root -g "${SERVICE_USER}" "${CONFIG_DIR}" echo "[4/7] Copying and building the application" if [[ "${SCRIPT_DIR}" != "${REPOSITORY}" ]]; then rsync -a --delete \ --exclude node_modules \ --exclude dist \ --exclude .env \ --exclude server/data \ "${SCRIPT_DIR}/" "${REPOSITORY}/" fi chown -R root:root "${REPOSITORY}" git config --system --add safe.directory "${REPOSITORY}" 2>/dev/null || true branch="$(git -C "${REPOSITORY}" branch --show-current)" if [[ -z "${branch}" ]]; then echo "The installation clone must be on a named Git branch." >&2 exit 1 fi printf '%s\n' "${branch}" > "${CONFIG_DIR}/update-branch" chmod 0644 "${CONFIG_DIR}/update-branch" update_user="${SUDO_USER:-$(stat -c '%U' "${SCRIPT_DIR}")}" if ! id "${update_user}" >/dev/null 2>&1; then echo "Could not determine the local user that owns Git update credentials." >&2 exit 1 fi printf '%s\n' "${update_user}" > "${CONFIG_DIR}/update-user" chmod 0600 "${CONFIG_DIR}/update-user" chown -R "${update_user}":"$(id -gn "${update_user}")" "${REPOSITORY}/.git" cd "${REPOSITORY}" npm ci npm run check revision="$(git rev-parse HEAD)" release="${RELEASES}/${revision}" install -d -m 0755 -o root -g root "${release}" rsync -a --delete --exclude .git "${REPOSITORY}/" "${release}/" printf '%s\n' "${revision}" > "${release}/REVISION" chown -R root:root "${release}" ln -sfn "releases/${revision}" "${INSTALL_ROOT}/current.next" mv -Tf "${INSTALL_ROOT}/current.next" "${CURRENT}" echo "[5/7] Installing system services" install -m 0755 "${SCRIPT_DIR}/scripts/pi-car-companion-network-setup" /usr/local/sbin/pi-car-companion-network-setup install -m 0755 "${SCRIPT_DIR}/scripts/pi-car-companion-power" /usr/local/sbin/pi-car-companion-power install -m 0755 "${SCRIPT_DIR}/scripts/pi-car-companion-bluetooth-control" /usr/local/sbin/pi-car-companion-bluetooth-control install -m 0755 "${SCRIPT_DIR}/scripts/pi-car-companion-update" /usr/local/sbin/pi-car-companion-update install -d -m 0755 /usr/local/libexec install -m 0755 "${SCRIPT_DIR}/scripts/pi-car-companion-bluetooth" /usr/local/libexec/pi-car-companion-bluetooth install -m 0644 "${SCRIPT_DIR}/systemd/pi-car-companion.service" /etc/systemd/system/pi-car-companion.service install -m 0644 "${SCRIPT_DIR}/systemd/pi-car-companion-update-check.service" /etc/systemd/system/pi-car-companion-update-check.service install -m 0644 "${SCRIPT_DIR}/systemd/pi-car-companion-update.service" /etc/systemd/system/pi-car-companion-update.service install -m 0644 "${SCRIPT_DIR}/systemd/pi-car-companion-network.service" /etc/systemd/system/pi-car-companion-network.service install -m 0644 "${SCRIPT_DIR}/systemd/pi-car-companion-network-configure.service" /etc/systemd/system/pi-car-companion-network-configure.service install -m 0644 "${SCRIPT_DIR}/systemd/pi-car-companion-network-observer.service" /etc/systemd/system/pi-car-companion-network-observer.service install -m 0644 "${SCRIPT_DIR}/systemd/pi-car-companion-bluetooth.service" /etc/systemd/system/pi-car-companion-bluetooth.service install -m 0440 "${SCRIPT_DIR}/systemd/pi-car-companion.sudoers" /etc/sudoers.d/pi-car-companion visudo -cf /etc/sudoers.d/pi-car-companion >/dev/null install -d -m 0755 /etc/avahi/services install -m 0644 "${SCRIPT_DIR}/config/pi-car-companion.service.xml" /etc/avahi/services/pi-car-companion.service install -d -m 0755 /etc/NetworkManager/dnsmasq-shared.d install -m 0644 "${SCRIPT_DIR}/config/dnsmasq-shared-pi-car-companion.conf" /etc/NetworkManager/dnsmasq-shared.d/pi-car-companion.conf if [[ ! -f "${CONFIG_DIR}/companion.env" ]]; then cat > "${CONFIG_DIR}/companion.env" <> "${CONFIG_DIR}/companion.env" fi } ensure_env_default NETWORK_MANAGER_ENABLED false ensure_env_default NETWORK_STATUS_PATH "${DATA_DIR}/network-status.json" ensure_env_default NETWORK_COMMAND_PATH "${DATA_DIR}/network-command.json" ensure_env_default NETWORK_CONFIGURATION_REQUEST_PATH "${DATA_DIR}/network-configuration-request.json" ensure_env_default NETWORK_ACTIVITY_PATH "${DATA_DIR}/network-activity.json" ensure_env_default NETWORK_HOTSPOT_INTERFACE wlan0 ensure_env_default NETWORK_UPSTREAM_INTERFACE wlan1 ensure_env_default NETWORK_HOTSPOT_CONNECTION pi-car-hotspot ensure_env_default NETWORK_PROBE_TIMEOUT_SECONDS 60 ensure_env_default NETWORK_STABILITY_SECONDS 10 ensure_env_default NETWORK_POLL_SECONDS 5 chown root:"${SERVICE_USER}" "${CONFIG_DIR}/companion.env" chmod 0640 "${CONFIG_DIR}/companion.env" if [[ ! -f "${DATA_DIR}/update-status.json" ]]; then printf '{"state":"idle","message":"Ready to check for updates.","fromRevision":null,"toRevision":null,"updatedAt":"%s"}\n' "$(date --iso-8601=seconds)" > "${DATA_DIR}/update-status.json" fi chown root:"${SERVICE_USER}" "${DATA_DIR}/update-status.json" chmod 0644 "${DATA_DIR}/update-status.json" echo "[6/7] Enabling start-on-boot services" systemctl daemon-reload systemctl enable --now avahi-daemon systemctl enable pi-car-companion.service systemctl enable --now bluetooth.service systemctl enable pi-car-companion-bluetooth.service systemctl enable pi-car-companion-network-observer.service if grep -qx 'NETWORK_MANAGER_ENABLED=true' "${CONFIG_DIR}/companion.env"; then systemctl enable --now pi-car-companion-network.service fi systemctl restart pi-car-companion.service systemctl restart pi-car-companion-bluetooth.service systemctl restart pi-car-companion-network-observer.service echo "[7/7] Verifying the service" for _attempt in {1..20}; do if curl --fail --silent --show-error http://127.0.0.1:8787/healthz >/dev/null; then break fi sleep 1 done if ! curl --fail --silent --show-error http://127.0.0.1:8787/healthz >/dev/null; then systemctl status --no-pager pi-car-companion.service || true echo "Installation finished, but the health check failed. Review the service status above." >&2 exit 1 fi host_name="$(hostname)" ip_addresses="$(hostname -I 2>/dev/null | xargs || true)" echo echo "Pi Car Companion is installed and running." echo "Dashboard: http://${host_name}.local:8787" if [[ -n "${ip_addresses}" ]]; then echo "Pi addresses: ${ip_addresses} (port 8787)" fi echo "Future updates can be installed from Settings, or by running git pull and install.sh again."