#!/usr/bin/env bash # # yaris-obd — one-shot installer for a freshly flashed Raspberry Pi OS. # # Takes a blank Raspberry Pi OS Bookworm (Lite or desktop) install all the way # to a running yaris-obd deployment: # * base packages + NetworkManager # * the `yaris` service user (with sudo + serial/GPIO groups) # * repo clone at /home/yaris/app/yaris-obd # * RTL8812BU driver for the two TP-Link Archer T3U dongles (wlan1/wlan2) # * NetworkManager profiles: home-wifi, Hotspot-local, optional phone-hotspot # * every deploy/install_*.sh in the correct order # # Run ON THE PI as root (or via sudo): # sudo bash auto_install.sh # # The only questions it asks are for the WiFi credentials it needs. You can # instead pass them through the environment for fully non-interactive runs: # # HOME_WIFI_SSID="MyWiFi" \ # HOME_WIFI_PSK="secret" \ # HOTSPOT_SSID="yaris-obd" \ # HOTSPOT_PSK="in-car-password" \ # sudo -E bash auto_install.sh --non-interactive # # Optional flags: # --non-interactive fail instead of prompting for missing credentials # --skip-t3u-driver do not build the RTL8812BU driver (wlan1/wlan2) # --no-watchdog do not enable the hardware watchdog # --no-app-updater do not install the Android APK mirror timer # --with-rtc configure the DS3231 RTC (I2C + overlay + fake-hwclock off) # --with-sim868 configure the SIM868 UART (enable_uart, disable-bt, # no serial login shell, hciuart off) # -h|--help print this help # # Env vars honoured (all optional unless marked required): # GIT_URL / GIT_BRANCH repo source (defaults to GitHub default branch) # HOME_WIFI_SSID / HOME_WIFI_PSK home WiFi (required for netwatch) # HOTSPOT_SSID / HOTSPOT_PSK in-car AP (SSID defaults to "yaris-obd") # PHONE_HOTSPOT_SSID / PHONE_HOTSPOT_PSK optional phone hotspot uplink # YARIS_PASSWORD set a password for the yaris user (else disabled) # OWNER_NAME / CAR_MODEL / DEVICE_NAME prefill config/device.json # YARIS_SERVER_URL / YARIS_DEVICE_NAME / YARIS_API_KEY prefill db/upload_creds.env set -euo pipefail export DEBIAN_FRONTEND=noninteractive REPO_DIR="/home/yaris/app/yaris-obd" REPO_URL="${GIT_URL:-https://github.com/quoije/yaris-obd.git}" REPO_BRANCH="${GIT_BRANCH:-}" # ---- flag defaults (env-overridable) --------------------------------------- NONINTERACTIVE=${NONINTERACTIVE:-0} SKIP_T3U_DRIVER=${SKIP_T3U_DRIVER:-0} INSTALL_WATCHDOG=${INSTALL_WATCHDOG:-1} INSTALL_APP_UPDATER=${INSTALL_APP_UPDATER:-1} WITH_RTC=${WITH_RTC:-0} WITH_SIM868=${WITH_SIM868:-0} # ---- logging ---------------------------------------------------------------- info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m!!>\033[0m %s\n' "$*" >&2; } err() { printf '\033[1;31m!!>\033[0m %s\n' "$*" >&2; } usage() { sed -n '2,40p' "$0" } # ---- small helpers ---------------------------------------------------------- config_txt_path() { if [ -f /boot/firmware/config.txt ]; then printf '%s' /boot/firmware/config.txt elif [ -f /boot/config.txt ]; then printf '%s' /boot/config.txt else printf '%s' /boot/firmware/config.txt fi } # Idempotently set a KEY=... style line in config.txt, replacing any existing # (possibly commented) occurrence of the key, appending otherwise. set_config_line() { local file="$1" key="$2" line="$3" if [ ! -f "$file" ]; then warn "$file does not exist; skipping '$line'." return 0 fi if grep -qE "^[[:space:]]*#?[[:space:]]*${key}" "$file"; then sed -i -E "s|^[[:space:]]*#?[[:space:]]*${key}.*|${line}|" "$file" else printf '%s\n' "$line" >> "$file" fi } # Return the value of the named env var, an explicit default, or prompt (unless # non-interactive). Prints the value to stdout. prompt_value() { local var="$1" prompt="$2" default="${3:-}" local val="${!var:-}" if [ -n "$val" ]; then printf '%s' "$val" return 0 fi if [ -n "$default" ]; then printf '%s' "$default" return 0 fi if [ "$NONINTERACTIVE" = "1" ]; then err "Missing required value: $var ($prompt). Pass it via the environment or drop --non-interactive." exit 1 fi read -rp "$prompt: " val printf '%s' "$val" } # Same as prompt_value but reads the answer without echoing it. prompt_secret() { local var="$1" prompt="$2" default="${3:-}" local val="${!var:-}" if [ -n "$val" ]; then printf '%s' "$val" return 0 fi if [ -n "$default" ]; then printf '%s' "$default" return 0 fi if [ "$NONINTERACTIVE" = "1" ]; then err "Missing required value: $var ($prompt). Pass it via the environment or drop --non-interactive." exit 1 fi read -rsp "$prompt: " val printf '\n' >&2 printf '%s' "$val" } # ---- install steps ---------------------------------------------------------- install_base_packages() { info "Installing base packages..." apt-get update apt-get install -y \ git curl python3 python3-venv python3-pip \ network-manager nftables avahi-daemon dnsmasq-base iw systemctl unmask NetworkManager 2>/dev/null || true systemctl enable --now NetworkManager } create_yaris_user() { info "Creating the 'yaris' service user..." if ! id -u yaris >/dev/null 2>&1; then if [ -n "${YARIS_PASSWORD:-}" ]; then useradd --create-home --shell /bin/bash \ --password "$(openssl passwd -6 "$YARIS_PASSWORD")" yaris else useradd --create-home --shell /bin/bash --disabled-password yaris fi fi usermod -aG sudo,dialout,gpio,i2c yaris info "yaris user ready (groups: $(id -nG yaris | tr ' ' ','))" } clone_repo() { info "Cloning $REPO_URL into $REPO_DIR..." mkdir -p "$(dirname "$REPO_DIR")" chown yaris:yaris "$(dirname "$REPO_DIR")" if [ ! -d "$REPO_DIR/.git" ]; then local branch_args=() if [ -n "$REPO_BRANCH" ]; then branch_args+=(--branch "$REPO_BRANCH") fi sudo -u yaris git clone "${branch_args[@]}" "$REPO_URL" "$REPO_DIR" else warn "$REPO_DIR already exists — attempting git pull." sudo -u yaris git -C "$REPO_DIR" pull --ff-only || warn "git pull failed; continuing with the existing checkout." fi } install_t3u_driver() { if [ "$SKIP_T3U_DRIVER" = "1" ]; then warn "Skipping RTL8812BU driver build (--skip-t3u-driver). wlan1/wlan2 will not exist." return 0 fi info "Building the RTL8812BU driver for the TP-Link Archer T3U dongles..." apt-get install -y dkms build-essential bc raspberrypi-kernel-headers rm -rf /tmp/88x2bu-20210702 git clone --depth 1 https://github.com/morrownr/88x2bu-20210702.git /tmp/88x2bu-20210702 ( cd /tmp/88x2bu-20210702 && printf 'n\n' | ./install-driver.sh ) || { warn "RTL8812BU driver build failed. The in-car AP/uplink on wlan1/wlan2 will be unavailable until fixed." return 0 } if [ -e /sys/class/net/wlan1 ]; then info "wlan1 is present." else warn "wlan1 still not present after the driver build — check the dongle / USB ports." fi } create_home_wifi() { local ssid psk ssid="$(prompt_value HOME_WIFI_SSID "Home WiFi SSID")" psk="$(prompt_secret HOME_WIFI_PSK "Home WiFi password")" info "Configuring NetworkManager profile 'home-wifi'..." if nmcli connection show home-wifi >/dev/null 2>&1; then nmcli connection modify home-wifi \ 802-11-wireless.ssid "$ssid" \ wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$psk" \ connection.autoconnect yes connection.autoconnect-priority 30 else nmcli connection add type wifi con-name home-wifi ifname wlan2 \ ssid "$ssid" \ wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$psk" \ connection.autoconnect yes connection.autoconnect-priority 30 fi } create_hotspot() { local ssid psk ssid="$(prompt_value HOTSPOT_SSID "In-car hotspot SSID" "yaris-obd")" psk="$(prompt_secret HOTSPOT_PSK "In-car hotspot password")" info "Configuring NetworkManager profile 'Hotspot-local' (5GHz AP on wlan1)..." if nmcli connection show Hotspot-local >/dev/null 2>&1; then nmcli connection modify Hotspot-local \ 802-11-wireless.ssid "$ssid" \ 802-11-wireless.mode ap \ 802-11-wireless.band a \ wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$psk" \ ipv4.method shared ipv4.addresses 10.42.0.1/24 \ connection.autoconnect yes connection.autoconnect-priority 10 else nmcli connection add type wifi con-name Hotspot-local ifname wlan1 \ ssid "$ssid" \ 802-11-wireless.mode ap \ 802-11-wireless.band a \ wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$psk" \ ipv4.method shared ipv4.addresses 10.42.0.1/24 \ connection.autoconnect yes connection.autoconnect-priority 10 fi } create_phone_hotspot() { local ssid="${PHONE_HOTSPOT_SSID:-}" if [ -z "$ssid" ]; then if [ "$NONINTERACTIVE" = "1" ]; then info "No phone-hotspot credentials provided; skipping phone hotspot." return 0 fi read -rp "Phone hotspot SSID (leave empty to skip): " ssid if [ -z "$ssid" ]; then info "Skipping phone hotspot." return 0 fi fi local psk psk="$(prompt_secret PHONE_HOTSPOT_PSK "Phone hotspot password")" info "Configuring NetworkManager profile 'phone-hotspot'..." if nmcli connection show phone-hotspot >/dev/null 2>&1; then nmcli connection modify phone-hotspot \ 802-11-wireless.ssid "$ssid" \ wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$psk" \ connection.autoconnect yes connection.autoconnect-priority 20 \ ipv4.dhcp-send-hostname yes ipv4.dhcp-hostname yaris \ 802-11-wireless.mtu 1400 else nmcli connection add type wifi con-name phone-hotspot ifname wlan2 \ ssid "$ssid" \ wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$psk" \ connection.autoconnect yes connection.autoconnect-priority 20 \ ipv4.dhcp-send-hostname yes ipv4.dhcp-hostname yaris \ 802-11-wireless.mtu 1400 fi } enable_rtc() { if [ "$WITH_RTC" != "1" ]; then return 0 fi info "Configuring the DS3231 RTC (--with-rtc)..." apt-get install -y i2c-tools local cfg cfg="$(config_txt_path)" set_config_line "$cfg" "dtparam=i2c_arm" "dtparam=i2c_arm=on" set_config_line "$cfg" "dtoverlay=i2c-rtc,ds3231" "dtoverlay=i2c-rtc,ds3231" printf '%s\n' "i2c-dev" > /etc/modules-load.d/i2c-dev.conf systemctl disable --now fake-hwclock 2>/dev/null || true warn "RTC overlay enabled — reboot for it to take effect, then verify with: i2cdetect -y 1 (expect UU at 0x68)." } enable_sim868() { if [ "$WITH_SIM868" != "1" ]; then return 0 fi info "Configuring the SIM868 UART (--with-sim868)..." local cfg cfg="$(config_txt_path)" set_config_line "$cfg" "enable_uart" "enable_uart=1" set_config_line "$cfg" "dtoverlay=disable-bt" "dtoverlay=disable-bt" if command -v raspi-config >/dev/null 2>&1; then raspi-config nonint do_serial_cons 1 || true fi systemctl disable --now hciuart 2>/dev/null || true warn "SIM868 UART enabled — reboot for it to take effect (serial0 frees the full PL011 UART)." } prefill_device_config() { if [ -z "${OWNER_NAME:-}${CAR_MODEL:-}${DEVICE_NAME:-}" ]; then return 0 fi info "Prefilling config/device.json..." python3 - "$REPO_DIR/config/device.json" "${OWNER_NAME:-}" "${CAR_MODEL:-}" "${DEVICE_NAME:-}" <<'PY' import json, sys path, owner, car, device = sys.argv[1:5] with open(path) as f: data = json.load(f) if owner: data["owner_name"] = owner if car: data["car_model"] = car if device: data["device_name"] = device with open(path, "w") as f: json.dump(data, f, indent=2) f.write("\n") PY } prefill_upload_creds() { if [ -z "${YARIS_SERVER_URL:-}${YARIS_DEVICE_NAME:-}${YARIS_API_KEY:-}" ]; then return 0 fi info "Prefilling db/upload_creds.env..." local creds="$REPO_DIR/db/upload_creds.env" python3 - "$creds" \ "YARIS_SERVER_URL" "${YARIS_SERVER_URL:-}" \ "YARIS_DEVICE_NAME" "${YARIS_DEVICE_NAME:-}" \ "YARIS_API_KEY" "${YARIS_API_KEY:-}" <<'PY' import re, sys path = sys.argv[1] pairs = list(zip(sys.argv[2::2], sys.argv[3::2])) with open(path) as f: text = f.read() for key, value in pairs: if value == "": continue pattern = re.compile(rf"^{re.escape(key)}\s*=.*$", re.M) replacement = f'{key} = "{value}"' if pattern.search(text): text = pattern.sub(replacement, text, count=1) else: text += replacement + "\n" with open(path, "w") as f: f.write(text) PY } run_installers() { info "Running deploy installers (as root; sudo inside them is a no-op)..." info "install_service.sh" bash "$REPO_DIR/deploy/install_service.sh" info "install_port_redirect.sh" bash "$REPO_DIR/deploy/install_port_redirect.sh" info "install_netwatch.sh" bash "$REPO_DIR/deploy/install_netwatch.sh" info "install_video_tools.sh" bash "$REPO_DIR/deploy/install_video_tools.sh" if [ "$INSTALL_WATCHDOG" = "1" ]; then info "install_watchdog.sh" bash "$REPO_DIR/deploy/install_watchdog.sh" || warn "watchdog install failed (non-fatal)." else warn "Skipping watchdog install (--no-watchdog)." fi if [ "$INSTALL_APP_UPDATER" = "1" ]; then info "install_app_updater.sh" bash "$REPO_DIR/deploy/install_app_updater.sh" || warn "APK updater install failed (non-fatal)." else warn "Skipping APK updater install (--no-app-updater)." fi } fix_ownership_and_data_dirs() { info "Fixing repo ownership and creating writable data directories..." mkdir -p "$REPO_DIR/db/sql" "$REPO_DIR/dash/videos" "$REPO_DIR/static/app" chown -R yaris:yaris "$REPO_DIR" } print_summary() { echo info "Installation complete." echo echo "What was set up:" echo " - yaris user (sudo, dialout, gpio, i2c)" echo " - repo at $REPO_DIR" echo " - yaris-obd.service (enabled + running on boot)" echo " - nftables port 80 -> 5000 redirect" echo " - yaris-netwatch.timer (home-wifi / phone-hotspot / Hotspot-local switching)" echo " - ffmpeg thumbnails, hardware watchdog, APK mirror timer" echo echo "Verify:" echo " sudo systemctl status yaris-obd.service --no-pager" echo " curl -I http://127.0.0.1/" echo if [ "$WITH_RTC" = "1" ] || [ "$WITH_SIM868" = "1" ]; then echo "A reboot is required for the RTC / SIM868 boot-config changes to apply." fi echo "After reboot, open the web UI at http://yaris.local (or http://10.42.0.1 from the car hotspot)." } main() { while [ $# -gt 0 ]; do case "$1" in --non-interactive) NONINTERACTIVE=1 ;; --skip-t3u-driver) SKIP_T3U_DRIVER=1 ;; --no-watchdog) INSTALL_WATCHDOG=0 ;; --no-app-updater) INSTALL_APP_UPDATER=0 ;; --with-rtc) WITH_RTC=1 ;; --with-sim868) WITH_SIM868=1 ;; -h|--help) usage; exit 0 ;; *) err "Unknown option: $1"; usage; exit 1 ;; esac shift done if [ "$(id -u)" -ne 0 ]; then err "This script must be run as root (or via sudo)." exit 1 fi install_base_packages create_yaris_user clone_repo enable_rtc enable_sim868 install_t3u_driver create_home_wifi create_hotspot create_phone_hotspot run_installers prefill_device_config prefill_upload_creds fix_ownership_and_data_dirs print_summary } main "$@"