docs: mark APK distribution as deployed

This commit is contained in:
Mathieu Moran
2026-08-15 21:10:27 -04:00
parent a050f3577a
commit 4801508415
7 changed files with 479 additions and 5 deletions
+3 -2
View File
@@ -2237,8 +2237,9 @@ The app reuses the existing Pi REST API surface with no changes:
(`/api/v1/app/latest` + `/app/yaris-obd.apk` + a portal upload page), the Pi mirrors it
on a systemd timer (`deploy/yaris_app_updater.py` + `yaris-app-updater.{service,timer}`),
and the phone installs from `http://10.42.0.1/app`.
- Deployment: run `install_app_updater.sh` on the Pi; publish a build via the portal upload
page (or drop the APK in the server's mounted `dashcam/app/` volume). Play Store out of scope.
- ✅ Deployed 2026-08-15: server endpoints live (`/api/v1/app/latest` 200, `/app/yaris-obd.apk`
200), Pi updater timer active and mirroring the APK, phone downloads from
`http://10.42.0.1/app`. Play Store out of scope.
#### Open questions (resolve during 28.1)
+3 -2
View File
@@ -272,8 +272,9 @@ android/
mirrors the latest APK from the server on a systemd timer
(`deploy/yaris-app-updater.{service,timer}` + `deploy/install_app_updater.sh`)
- Phone: connect to hotspot → `http://10.42.0.1/app` → download → install
- [ ] Deploy: run `install_app_updater.sh` on the Pi; publish a build via the portal (or scp
the APK into the server's mounted `dashcam/app/` volume)
- [x] Deployed 2026-08-15: `install_app_updater.sh` run on the Pi (timer active, APK 0.1.0
mirrored), server rebuilt with the app endpoints, and the release APK published to the
server's mounted `dashcam/app/` volume.
## Working notes
+449
View File
@@ -0,0 +1,449 @@
#!/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 "$@"
+4
View File
@@ -23,6 +23,10 @@ sudo rm -f /usr/local/bin/yaris-netwatch.sh
sudo install -m 755 "$REPO_DIR/deploy/yaris_uplink.py" /usr/local/bin/yaris-uplink
sudo install -m 440 "$REPO_DIR/deploy/yaris-uplink.sudoers" /etc/sudoers.d/yaris-uplink
sudo visudo -cf /etc/sudoers.d/yaris-uplink
# Passwordless nmcli on the Hotspot-local profile so the Settings page can
# change the in-car hotspot SSID/password at runtime (FEAT-11).
sudo install -m 440 "$REPO_DIR/deploy/yaris-hotspot.sudoers" /etc/sudoers.d/yaris-hotspot
sudo visudo -cf /etc/sudoers.d/yaris-hotspot
sudo install -m 644 "$REPO_DIR/deploy/yaris-hotspot-dns.conf" \
/etc/NetworkManager/dnsmasq-shared.d/yaris-hotspot-dns.conf
sudo install -m 755 "$REPO_DIR/deploy/90-yaris-uplink" \
+5
View File
@@ -8,6 +8,7 @@ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SERVICE_FILE="$REPO_DIR/deploy/yaris-obd.service"
SERVICE_NAME="yaris-obd.service"
STATUS_SUDOERS_FILE="$REPO_DIR/deploy/yaris-status-actions.sudoers"
DASHCAM_SUDOERS_FILE="$REPO_DIR/deploy/yaris-dashcam.sudoers"
if [ ! -d "$REPO_DIR/venv" ]; then
echo "No venv found at $REPO_DIR/venv - creating one and installing requirements..."
@@ -19,6 +20,10 @@ fi
sudo cp "$SERVICE_FILE" "/etc/systemd/system/$SERVICE_NAME"
sudo install -m 440 "$STATUS_SUDOERS_FILE" /etc/sudoers.d/yaris-status-actions
sudo visudo -cf /etc/sudoers.d/yaris-status-actions
# Passwordless sudo for the dashcam mount/umount/delete operations the app runs
# at runtime (modules/app_dash.py) as the unprivileged `yaris` user.
sudo install -m 440 "$DASHCAM_SUDOERS_FILE" /etc/sudoers.d/yaris-dashcam
sudo visudo -cf /etc/sudoers.d/yaris-dashcam
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME"
sudo systemctl restart "$SERVICE_NAME"
+13
View File
@@ -0,0 +1,13 @@
# Dashcam USB storage operations performed by the yaris-obd service while it
# runs as the unprivileged `yaris` user (modules/app_dash.py). The dashcam is
# mounted read-only by default and only remounted read-write for the brief
# duration of a validated delete operation, then restored.
#
# The mount point is fixed at /mnt/dashcam, so the mount/umount grants below are
# scoped to that directory. The `rm` grant is scoped to the `rm --` form the app
# uses after its own extension/realpath/mount-boundary validation.
yaris ALL=(root) NOPASSWD: /usr/bin/mkdir -p /mnt/dashcam
yaris ALL=(root) NOPASSWD: /usr/bin/mount -o r[ow] *
yaris ALL=(root) NOPASSWD: /usr/bin/mount -o * /mnt/dashcam
yaris ALL=(root) NOPASSWD: /usr/bin/umount /mnt/dashcam
yaris ALL=(root) NOPASSWD: /usr/bin/rm -- *
+2 -1
View File
@@ -7,4 +7,5 @@ pyserial
RPi.GPIO
obd
requests
requests-toolbeltreverse_geocoder
requests-toolbelt
reverse_geocoder