# Pi Car Companion

A local-first Raspberry Pi companion for the MG4. The Pi hosts a secure dashboard while the existing Android infotainment system remains the display and touch interface.

The current MVP slice includes:

- atomic first-run administrator creation with no default credentials;
- Argon2id password hashing and revocable, hashed server-side sessions;
- CSRF, trusted-origin, rate-limit, and secure-header protections;
- SQLite persistence and append-oriented authentication audit events;
- real host, CPU, memory, disk, network, uptime, and service status;
- field-level unavailable states when host data cannot be collected;
- authenticated Server-Sent Events for live status updates;
- allowlisted refresh-health, offline network-diagnostics, and sanitized support-bundle jobs;
- disabled-by-default, serial-pinned ADB status, APK installation, package inventory, and validated app shortcuts;
- live MG4 speed, gear, and steering-angle telemetry with a dedicated visual Car dashboard;
- a responsive 1920×720-oriented setup, login, and dashboard UI;
- an installable PWA shell with standalone landscape display and cached dashboard assets;
- a focused native Android companion with authenticated BLE status, car controls, and phone GPS uplink;
- an idempotent Raspberry Pi installer with systemd start-on-boot support;
- atomic, dashboard-triggered Git updates with automatic verification and rollback safety;
- optional dual-radio Wi-Fi failover with an always-recoverable offline car hotspot;
- authenticated live network diagnostics for hotspot clients, DNS query metadata, and active destination flows;
- tabbed Network, System, and restricted Shell settings for on-device administration;
- automated API security tests and reproducible quality commands.

The ADB transport, job UI, artifacts, Android companion, and remaining operations work are tracked in [PLAN.md](./PLAN.md).

## Requirements

- Node.js 22
- npm 10 or newer
- Linux, macOS, or Windows for development

Raspberry Pi OS Lite / Debian arm64 is the production target but has not yet been hardware-validated.

## Raspberry Pi installation

On a clean Raspberry Pi OS or Debian installation:

```bash
git clone https://git.molberg.cloud/alex/pi-car-companion.git
cd pi-car-companion
./install.sh
```

The script asks for sudo access and then:

- installs Node.js 22, Git, Avahi, SQLite, the complete hotspot/Realtek networking stack, and native build requirements;
- creates a dedicated `pi-companion` service account;
- builds and verifies the server and dashboard;
- installs an atomic release under `/opt/pi-car-companion`;
- stores application data under `/var/lib/pi-car-companion`;
- stores root-managed configuration under `/etc/pi-car-companion`;
- enables the companion and Avahi services at boot;
- starts the service and verifies `/healthz`.

The installer prints the final dashboard address, normally `http://<pi-hostname>.local:8787`. Open it and create the first administrator account.

It also installs the BlueZ GATT bridge used by the optional [Android companion](./android/README.md). Pair phones from **Settings → Mobile** after the Pi has Bluetooth enabled.

A ready-to-install development build is available at [`apks/pi-car-companion-debug.apk`](./apks/pi-car-companion-debug.apk). Android 8 or newer is required.

`install.sh` is idempotent. After pulling changes in the original clone, run it again to deploy that checkout:

```bash
git pull --ff-only
./install.sh
```

## Dashboard updates

Open **Settings → System**, select **Check for updates**, then choose **Install update**. The Pi will:

1. fetch the configured branch from `origin`;
2. refuse non-fast-forward history changes;
3. create an isolated release worktree;
4. install dependencies and run script checks, lint, type checks, tests, and builds;
5. atomically activate the release only after every check passes;
6. restart the background service and verify its health endpoint;
7. automatically restore the previous release if startup verification fails.

If any step fails, the current release stays active and Settings shows the failure. Updates use the Git credentials of the local user who ran `install.sh`; credentials are not copied into the web service or repository configuration. Non-interactive credentials such as an SSH deploy key or cached HTTPS credential work best.

Useful service commands:

```bash
sudo systemctl status pi-car-companion
sudo journalctl -u pi-car-companion -n 100 --no-pager
sudo journalctl -u pi-car-companion-update -n 100 --no-pager
```

## Development

Install dependencies:

```bash
npm install
```

Start the API in one terminal:

```bash
npm run dev
```

Start the Vite dashboard in another:

```bash
npm run dev:web
```

Open `http://127.0.0.1:5173`. The development server proxies API requests to `http://127.0.0.1:8787`.

The production build includes a web app manifest and service worker. PWA installation and offline shell caching require a secure browser context: HTTPS in production, or `localhost` during local use. The dashboard still requires the Pi connection for live data and authenticated actions.

Local development data is stored in `server/data/companion.db` and ignored by Git.

## Quality gate

Run every host-runnable check:

```bash
npm run check
```

This runs ESLint, strict TypeScript checks, tests, and both production builds.

Audit production dependencies separately from development tooling:

```bash
npm audit --omit=dev
```

## Production-mode preview

Build and start the combined server/dashboard:

```bash
npm run build
NODE_ENV=production \
DATABASE_PATH=./server/data/companion.db \
ALLOWED_ORIGINS=http://127.0.0.1:8787 \
npm start --workspace @pi-car/server
```

Then open `http://127.0.0.1:8787`.

The development default remains loopback-only. `install.sh` explicitly enables LAN access so the car can reach the dashboard.

## Configuration

See [.env.example](./.env.example). Configuration is validated at startup.

Important values:

- `HOST`: listen address, defaults to `127.0.0.1`
- `PORT`: HTTP port, defaults to `8787`
- `DATABASE_PATH`: SQLite database location
- `SESSION_TTL_HOURS`: session lifetime, defaults to 24 hours
- `COOKIE_SECURE`: enable for HTTPS deployments
- `ALLOWED_ORIGINS`: comma-separated exact browser origins accepted for state changes
- `UPDATE_STATUS_PATH`: update state shared with the systemd update unit
- `VERSION_FILE`: revision metadata for the active atomic release
- `ADB_ENABLED`: enables the read-only head-unit collector; defaults to `false`
- `ADB_SERIAL`: optional exact serial; when empty, discovery requires exactly one attached device
- `ADB_PATH` and `ADB_TIMEOUT_MS`: executable path and per-command timeout
- `NETWORK_MANAGER_ENABLED`: enables the separately supervised NetworkManager controller after explicit setup
- `NETWORK_STATUS_PATH` and `NETWORK_COMMAND_PATH`: validated state and fixed-action request files
- `NETWORK_CONFIGURATION_REQUEST_PATH`: mode-0600 one-time hotspot configuration request consumed by the root service
- `NETWORK_ACTIVITY_PATH`: bounded observer snapshot consumed by the authenticated dashboard
- `NETWORK_HOTSPOT_INTERFACE` / `NETWORK_UPSTREAM_INTERFACE`: default to built-in `wlan0` and USB `wlan1`
- `NETWORK_PROBE_TIMEOUT_SECONDS`: maximum built-in upstream attempt before restoring the offline hotspot

### Dual-radio car network

The optional network controller prefers a USB Wi-Fi adapter for upstream internet and keeps the built-in adapter on a stable car hotspot. If the USB adapter disappears, the built-in adapter tries its saved upstream profiles for 60 seconds. When none connect, the controller restores the offline hotspot at `http://10.42.0.1:8787`.

Automatic switching is disabled by default so installation cannot interrupt an existing SSH connection. First connect the USB adapter to a saved upstream profile:

```bash
sudo nmcli --ask device wifi connect "HOME_SSID" ifname wlan1 name upstream-home
```

Then create the recovery hotspot and enable its controller:

```bash
sudo pi-car-companion-network-setup
```

The helper requires `wlan1` to be connected before it enables switching, asks interactively for a unique hotspot password, and never places that password in the application configuration. Alternatively, an authenticated administrator can configure or rotate the hotspot SSID and password directly in **Settings → Network**. The dashboard uses a narrowly scoped root service; the one-time password request is deleted before processing and NetworkManager stores only the derived WPA key.

Settings also shows the current mode and offers **Try upstream again** and **Restore hotspot** controls. Applying new hotspot credentials can disconnect the dashboard, after which devices must reconnect using the new values.

Behavior by mode:

- `wlan1` connected: `wlan0` hosts `MG4-Companion` and shares upstream access.
- `wlan1` unavailable: `wlan0` becomes upstream when a saved network connects.
- no upstream after 60 seconds: `wlan0` returns to the offline hotspot automatically.
- `wlan1` returns: it must connect and remain stable before `wlan0` returns to hotspot mode.

Add phone hotspots as additional saved `wlan1` profiles. Upstream actions only select existing NetworkManager profiles; the dashboard credential form configures the isolated car hotspot, not arbitrary upstream networks.

### Network activity diagnostics

The authenticated **Network** page refreshes every two seconds and reports:

- current and recently leased hotspot clients from NetworkManager DHCP leases and the neighbor table;
- up to 250 DNS query names/types from the previous 15 minutes;
- up to 150 active TCP/UDP destination IP, port, state, packet, and byte counters from conntrack.

The observer runs read-only as a separate root service and publishes a validated snapshot to the unprivileged application. It does not capture packet payloads, passwords, cookies, HTTP bodies, or HTTPS paths. DNS metadata comes from the system journal and follows its configured retention; the dashboard snapshot is overwritten rather than appended.

### ADB setup

ADB integration is optional and disabled by default. It starts the ADB server when the
companion service boots. With no configured serial it automatically selects exactly one
attached device and refuses ambiguous multi-device connections; after selection every
command is serial-pinned. Commands use fixed server-side argument lists with timeouts
and bounded output. The ADB authorization key persists in the companion data
directory, so an already-authorized car reconnects after Pi reboot or USB reattachment.
The dashboard can read identity and display information, list installed packages,
install a selected APK of at most 256 MB, and launch validated package or component
targets. It never accepts shell text or arbitrary ADB arguments.

On a deployed Pi, rerun `install.sh` once so `android-tools-adb` is present and the
service account receives the standard `plugdev` device-access group. Connect the head
unit, then establish authorization as the service account:

```bash
sudo -u pi-companion -H adb devices
```

Accept the authorization prompt on the parked infotainment screen. Set these values in
`/etc/pi-car-companion/companion.env` and restart the service:

```text
ADB_ENABLED=true
# Optional when only the MG4 is attached:
ADB_SERIAL=
```

If the device is disconnected, offline, unauthorized, partially readable, or ADB is
missing, the dashboard reports that state without substituting generated data. APK
installation and app launches are administrator actions intended for a parked vehicle.

The Car page refreshes the verified live properties `arcsoft.avm.mCurCarSpeed`,
`arcsoft.avm.mCurCarGear`, and `arcsoft.avm.mCurCarWheelAngle`. Traction-battery and
range values are queried directly from the factory SAIC vehicle service using a small
read-only DEX probe run through ADB. The probe is pushed to `/data/local/tmp` and run
with `app_process`; it is not installed as an Android app and does not depend on MG
Utility. Android's head-unit battery status is deliberately not presented as the car
battery.

### System controls and shell

**Settings → System** provides confirmed reboot and power-off actions through a fixed,
sudo-allowlisted helper. It also separates remote update checks from installation.

**Settings → Shell** opens an authenticated WebSocket and a real local pseudo-terminal.
The shell runs as the restricted `pi-companion` service account inside the companion
service sandbox; it is not a root shell and does not request or store SSH credentials.
The WebSocket requires the active session, a trusted browser origin, and the session's
CSRF token. A terminal closes when its tab is left and expires after 30 minutes.

## Safety boundary

This project does not modify the MG4 firmware, launcher, vehicle controls, CAN bus, or safety systems. The authenticated dashboard includes a restricted `pi-companion` shell for local diagnostics, but it does not expose a root shell, SSH credentials, the Docker socket, unrestricted filesystem access, or arbitrary ADB commands.
