|
| 1 | +#!/usr/bin/env bash |
| 2 | +# setup.sh — Full first-time setup for the car/base RPi |
| 3 | +# |
| 4 | +# Covers: system packages, GStreamer/gi bindings, CAN kernel modules, |
| 5 | +# MCP2517FD device tree overlay (20 MHz crystal), can0 boot service, |
| 6 | +# uv install, Python venv, car-telemetry systemd service, |
| 7 | +# static IP on eth0, WiFi routing priority, Tailscale, NoMachine. |
| 8 | +# |
| 9 | +# Usage: sudo ./setup.sh [--car | --base] |
| 10 | +# --car non-interactive car setup (10.71.1.10, remote 10.71.1.20) |
| 11 | +# --base non-interactive base setup (10.71.1.20, remote 10.71.1.10) |
| 12 | +# (omit flag for interactive role prompt) |
| 13 | + |
| 14 | +set -euo pipefail |
| 15 | + |
| 16 | +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BOLD='\033[1m'; NC='\033[0m' |
| 17 | + |
| 18 | +ok() { echo -e "${GREEN}✓ $*${NC}"; } |
| 19 | +warn() { echo -e "${YELLOW} $*${NC}"; } |
| 20 | +die() { echo -e "${RED}✗ $*${NC}"; exit 1; } |
| 21 | +hdr() { echo -e "\n${BOLD}── $* ──${NC}"; } |
| 22 | + |
| 23 | +if [[ $EUID -ne 0 ]]; then |
| 24 | + die "Run as root: sudo $0 $*" |
| 25 | +fi |
| 26 | + |
| 27 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 28 | + |
| 29 | +# ── Role selection ──────────────────────────────────────────────────────────── |
| 30 | +ROLE="" |
| 31 | +for arg in "$@"; do |
| 32 | + case "$arg" in |
| 33 | + --car) ROLE=car ;; |
| 34 | + --base) ROLE=base ;; |
| 35 | + esac |
| 36 | +done |
| 37 | + |
| 38 | +if [[ -z "$ROLE" ]]; then |
| 39 | + echo -e "${YELLOW}Which RPi is this?${NC}" |
| 40 | + echo " 1) Car — 10.71.1.10 (remote: 10.71.1.20)" |
| 41 | + echo " 2) Base — 10.71.1.20 (remote: 10.71.1.10)" |
| 42 | + echo "" |
| 43 | + read -rp "Enter 1 or 2: " choice |
| 44 | + case "$choice" in |
| 45 | + 1) ROLE=car ;; |
| 46 | + 2) ROLE=base ;; |
| 47 | + *) die "Invalid choice." ;; |
| 48 | + esac |
| 49 | +fi |
| 50 | + |
| 51 | +if [[ "$ROLE" == "car" ]]; then |
| 52 | + LOCAL_IP="10.71.1.10" |
| 53 | + REMOTE_IP="10.71.1.20" |
| 54 | +else |
| 55 | + LOCAL_IP="10.71.1.20" |
| 56 | + REMOTE_IP="10.71.1.10" |
| 57 | +fi |
| 58 | + |
| 59 | +# Derive the real user who will run the service (the one who called sudo) |
| 60 | +SERVICE_USER="${SUDO_USER:-$(logname 2>/dev/null || echo car)}" |
| 61 | +SERVICE_HOME="$(getent passwd "$SERVICE_USER" | cut -d: -f6)" |
| 62 | +UV="$SERVICE_HOME/.local/bin/uv" |
| 63 | + |
| 64 | +echo "" |
| 65 | +echo -e "${BOLD}Role:${NC} $ROLE" |
| 66 | +echo -e "${BOLD}Local IP:${NC} $LOCAL_IP" |
| 67 | +echo -e "${BOLD}Remote IP:${NC} $REMOTE_IP" |
| 68 | +echo -e "${BOLD}Service user:${NC} $SERVICE_USER ($SERVICE_HOME)" |
| 69 | +echo "" |
| 70 | + |
| 71 | +# ── 1. System packages ──────────────────────────────────────────────────────── |
| 72 | +hdr "System packages" |
| 73 | +apt-get update -qq |
| 74 | +apt-get install -y \ |
| 75 | + can-utils \ |
| 76 | + python3-gi \ |
| 77 | + python3-gst-1.0 \ |
| 78 | + gstreamer1.0-tools \ |
| 79 | + gstreamer1.0-plugins-base \ |
| 80 | + gstreamer1.0-plugins-good \ |
| 81 | + curl |
| 82 | +ok "System packages installed" |
| 83 | + |
| 84 | +# ── 2. CAN kernel modules ───────────────────────────────────────────────────── |
| 85 | +hdr "CAN kernel modules" |
| 86 | +for mod in can can_raw mcp251xfd; do |
| 87 | + modprobe "$mod" |
| 88 | + if ! grep -qx "$mod" /etc/modules 2>/dev/null; then |
| 89 | + echo "$mod" >> /etc/modules |
| 90 | + fi |
| 91 | +done |
| 92 | +ok "Modules loaded and persisted in /etc/modules" |
| 93 | + |
| 94 | +# ── 3. MCP2517FD device tree overlay (20 MHz crystal) ──────────────────────── |
| 95 | +hdr "Boot config — MCP2517FD overlay" |
| 96 | +CONFIG=/boot/firmware/config.txt |
| 97 | +for line in \ |
| 98 | + "dtoverlay=mcp251xfd,oscillator=20000000,interrupt=25" \ |
| 99 | + "dtoverlay=spi-bcm2835" |
| 100 | +do |
| 101 | + if grep -qF "$line" "$CONFIG" 2>/dev/null; then |
| 102 | + warn "Already present: $line" |
| 103 | + else |
| 104 | + echo "$line" >> "$CONFIG" |
| 105 | + ok "Added: $line" |
| 106 | + fi |
| 107 | +done |
| 108 | + |
| 109 | +# ── 4. can0 boot service ────────────────────────────────────────────────────── |
| 110 | +hdr "can0 systemd service" |
| 111 | +cat > /etc/systemd/system/can0.service <<'EOF' |
| 112 | +[Unit] |
| 113 | +Description=CAN bus interface can0 |
| 114 | +After=network.target |
| 115 | +
|
| 116 | +[Service] |
| 117 | +Type=oneshot |
| 118 | +ExecStart=/sbin/ip link set can0 up type can bitrate 500000 |
| 119 | +RemainAfterExit=yes |
| 120 | +
|
| 121 | +[Install] |
| 122 | +WantedBy=multi-user.target |
| 123 | +EOF |
| 124 | +systemctl daemon-reload |
| 125 | +systemctl enable can0 |
| 126 | +ok "can0.service installed and enabled" |
| 127 | + |
| 128 | +if ip link set can0 up type can bitrate 500000 2>/dev/null; then |
| 129 | + ok "can0 is UP at 500 kbps" |
| 130 | +else |
| 131 | + warn "can0 not yet available — overlay activates after reboot" |
| 132 | +fi |
| 133 | + |
| 134 | +# ── 5. Install uv ───────────────────────────────────────────────────────────── |
| 135 | +hdr "uv" |
| 136 | +if [[ -x "$UV" ]]; then |
| 137 | + ok "uv already installed at $UV" |
| 138 | +else |
| 139 | + sudo -u "$SERVICE_USER" env HOME="$SERVICE_HOME" \ |
| 140 | + sh -c 'curl -LsSf https://astral.sh/uv/install.sh | sh' |
| 141 | + ok "uv installed at $UV" |
| 142 | +fi |
| 143 | + |
| 144 | +# ── 6. Python venv + dependencies ──────────────────────────────────────────── |
| 145 | +hdr "Python venv (system-site-packages for gi/GStreamer)" |
| 146 | +cd "$SCRIPT_DIR" |
| 147 | +sudo -u "$SERVICE_USER" env HOME="$SERVICE_HOME" \ |
| 148 | + "$UV" venv --system-site-packages --clear .venv |
| 149 | +sudo -u "$SERVICE_USER" env HOME="$SERVICE_HOME" \ |
| 150 | + "$UV" sync |
| 151 | +ok "venv ready at $SCRIPT_DIR/.venv" |
| 152 | + |
| 153 | +# ── 7. car-telemetry systemd service ───────────────────────────────────────── |
| 154 | +hdr "car-telemetry systemd service" |
| 155 | +SERVICE_SRC="$SCRIPT_DIR/deploy/car-telemetry.service" |
| 156 | +GIT_HASH=$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown) |
| 157 | + |
| 158 | +sed \ |
| 159 | + -e "s|User=.*|User=$SERVICE_USER|" \ |
| 160 | + -e "s|WorkingDirectory=.*|WorkingDirectory=$SCRIPT_DIR|" \ |
| 161 | + -e "s|ExecStart=.*uv run|ExecStart=$UV run|" \ |
| 162 | + -e "s|REMOTE_IP=.*|REMOTE_IP=$REMOTE_IP|" \ |
| 163 | + -e "s|GIT_HASH=.*|GIT_HASH=$GIT_HASH|" \ |
| 164 | + "$SERVICE_SRC" > /etc/systemd/system/car-telemetry.service |
| 165 | + |
| 166 | +systemctl daemon-reload |
| 167 | +systemctl enable car-telemetry |
| 168 | +ok "car-telemetry.service installed and enabled (hash=$GIT_HASH, remote=$REMOTE_IP)" |
| 169 | + |
| 170 | +if [[ "$ROLE" == "car" ]]; then |
| 171 | + systemctl restart car-telemetry |
| 172 | + ok "car-telemetry started" |
| 173 | +else |
| 174 | + warn "Base role — skipping car-telemetry start" |
| 175 | +fi |
| 176 | + |
| 177 | +# ── 8. Static IP on eth0 + WiFi routing priority ───────────────────────────── |
| 178 | +# eth0 gets a high metric (low priority for default route) so WiFi remains the |
| 179 | +# default gateway for internet traffic even when the radio link is connected. |
| 180 | +# eth0 is still reachable at 10.71.1.x for the telemetry link. |
| 181 | +hdr "Static IP — eth0 (metric 200, WiFi stays default route)" |
| 182 | +NETPLAN_FILE=/etc/netplan/10-eth0-static.yaml |
| 183 | +cat > "$NETPLAN_FILE" <<EOF |
| 184 | +network: |
| 185 | + version: 2 |
| 186 | + ethernets: |
| 187 | + eth0: |
| 188 | + addresses: |
| 189 | + - ${LOCAL_IP}/24 |
| 190 | + routes: |
| 191 | + - to: 10.71.1.0/24 |
| 192 | + via: 0.0.0.0 |
| 193 | + metric: 200 |
| 194 | + on-link: true |
| 195 | + dhcp4: false |
| 196 | + dhcp6: false |
| 197 | +EOF |
| 198 | +chmod 600 "$NETPLAN_FILE" |
| 199 | +ok "Written $NETPLAN_FILE" |
| 200 | + |
| 201 | +# WiFi remains the default route automatically — eth0's netplan has no gateway, |
| 202 | +# so it only adds a host route to 10.71.1.0/24 and never competes for default. |
| 203 | + |
| 204 | +echo -e "${YELLOW}Applying netplan...${NC}" |
| 205 | +netplan apply |
| 206 | +ok "netplan applied" |
| 207 | + |
| 208 | +ASSIGNED=$(ip addr show eth0 2>/dev/null | grep -oP "inet \K[0-9.]+" || echo "") |
| 209 | +if [[ "$ASSIGNED" == "$LOCAL_IP" ]]; then |
| 210 | + ok "eth0 is now ${LOCAL_IP}/24" |
| 211 | +else |
| 212 | + warn "eth0 shows '${ASSIGNED:-none}' — expected '${LOCAL_IP}' (cable may not be plugged in yet)" |
| 213 | +fi |
| 214 | + |
| 215 | +# ── 9. Tailscale ───────────────────────────────────────────────────────────── |
| 216 | +hdr "Tailscale" |
| 217 | +if command -v tailscale &>/dev/null; then |
| 218 | + ok "Tailscale already installed ($(tailscale version | head -1))" |
| 219 | +else |
| 220 | + curl -fsSL https://tailscale.com/install.sh | sh |
| 221 | + ok "Tailscale installed" |
| 222 | +fi |
| 223 | +systemctl enable --now tailscaled |
| 224 | +warn "Run 'sudo tailscale up' to authenticate this node" |
| 225 | + |
| 226 | +# ── 10. NoMachine ───────────────────────────────────────────────────────────── |
| 227 | +hdr "NoMachine" |
| 228 | +if command -v nxserver &>/dev/null || dpkg -l nomachine &>/dev/null 2>&1; then |
| 229 | + ok "NoMachine already installed" |
| 230 | +else |
| 231 | + NX_URL="https://web9001.nomachine.com/download/9.4/Raspberry/nomachine_9.4.14_1_arm64.deb" |
| 232 | + NX_DEB="/tmp/nomachine.deb" |
| 233 | + echo " Downloading NoMachine for $ARCH..." |
| 234 | + curl -L "$NX_URL" -o "$NX_DEB" |
| 235 | + dpkg -i "$NX_DEB" |
| 236 | + rm -f "$NX_DEB" |
| 237 | + ok "NoMachine installed" |
| 238 | +fi |
| 239 | + |
| 240 | +# ── Done ────────────────────────────────────────────────────────────────────── |
| 241 | +echo "" |
| 242 | +echo -e "${GREEN}${BOLD}=========================================${NC}" |
| 243 | +echo -e "${GREEN}${BOLD} Setup complete${NC}" |
| 244 | +echo -e "${GREEN}${BOLD}=========================================${NC}" |
| 245 | +echo -e " Role: $ROLE" |
| 246 | +echo -e " This Pi: ${LOCAL_IP}/24 (eth0)" |
| 247 | +echo -e " Remote: $REMOTE_IP" |
| 248 | +echo -e " WiFi: default route (eth0 has no gateway)" |
| 249 | +echo -e " Git hash: $GIT_HASH" |
| 250 | +echo "" |
| 251 | +echo -e " ${YELLOW}Next steps:${NC}" |
| 252 | +echo -e " sudo tailscale up # authenticate Tailscale" |
| 253 | +echo -e " sudo reboot # activates MCP2517FD overlay" |
| 254 | +echo "" |
| 255 | +echo -e " After reboot:" |
| 256 | +echo -e " ip link show can0" |
| 257 | +echo -e " systemctl status car-telemetry" |
| 258 | +echo -e " journalctl -u car-telemetry -f" |
| 259 | +echo -e "${GREEN}${BOLD}=========================================${NC}" |
0 commit comments