# SPECS — Nils' MG4 Journal for this fork of [fatihdonmezdev/FatihsMG4](https://github.com/fatihdonmezdev/FatihsMG4): what has been changed, and what is still to do. - Our repository: https://git.stinnesbeck.com/nils/mg4 - Upstream: https://github.com/fatihdonmezdev/FatihsMG4 (itself a fork of malys / EVABRPUploader and Leon Kernan / ABRP_Uploader). Credited in the app's About dialog. ## Architecture: car server at car.k8s.stinnesbeck.com The car sends everything to our own server, which stores it in InfluxDB 2, and its live data straight to ABRP with the ABRP credentials kept (encrypted) in the app. The server does not talk to ABRP. Grafana reads InfluxDB directly for the dashboards. ``` car ──live telemetry───▶ car.k8s.stinnesbeck.com (Go) │ ──daily chunks─────▶ │ │ ──charging sessions▶ ▼ │ InfluxDB 2 ◀── Grafana (car-grafana.k8s.stinnesbeck.com) └──live telemetry─────▶ ABRP api.iternio.com/1/tlm/send ``` - **Server:** `server/`, Go 1.26, API only — see `server/README.md` for endpoints, configuration and the storage layout. Replaced the Next.js app from upstream (`web/`, removed 2026-10-17). - **Dashboards:** Grafana, provisioned from `k8s/grafana/`, reading with a read-only token: Live (stats, SoC/range, power/speed, route map, temperatures, tyres), Consumption (period totals, lifetime, SoH, per-day energy, distance and efficiency) and Charging (totals, session table, curves). Prices in euro. - **Pairing:** the car shows a QR code, a phone approves it with the approval password, and the car gets its own token, bound to its installation id (RFC 8628-style; `server/README.md`). The shared `INGEST_TOKEN` still works. The same QR flow carries settings typed on the phone. - **Car app:** tabs Overview, Consumption, Charging, Live data (on/off, ABRP credentials, frequency), Server (URL, pairing, token, installation id, config import), Settings (display, voice key, tyre unit, accent colour, battery, charging price, Wi-Fi), Logs. Live telemetry every 15 s while driving or charging, to the server and straight to ABRP; consumption chunks and charging sessions to the server. - **Deployment:** `k8s/`, plain manifests. **Nothing is ever applied to the cluster by us** — Nils applies them (`k8s/README.md`). TLS comes from Traefik's default certificate. ### Status Current: app 2.14.2, server 2.5.0 (pinned by digest in `k8s/server.yaml`). - [x] On the car: QR pairing, values on the Overview, voice-key double press, full screen without black bands. - [x] Go server: tests and govulncheck clean; end to end in podman against InfluxDB 2.7.12 (telemetry, consumption, charging, pairing, settings from the phone) and Grafana 13.2.3 (all dashboard queries return data). - [x] Manifests validated offline (kubeconform, with Traefik CRDs). **Applied by Nils only.** - [ ] Check Grafana's host in `k8s/ingress.yaml` (placeholder `car-grafana.k8s.stinnesbeck.com`). - [x] Screen on the car: 1920×720 px at 160 dpi, app window 1778×720 px (Logs → SCREEN). - [x] Car-colour probe on the car: only `ro.build.mtk2712.vehicle_type = AS28`; the paint colour is not in the system properties. It may be in the VHAL (a debug-build VhalProbe listing would tell); until then the accent is chosen by hand. - [ ] Not yet tried on the car: live data straight to ABRP (2.11), Enter on phone (2.12), accent colours (2.14), right-aligned values (2.14.1). ### Signals the car does not provide Checked 2026-10-17 against what actually reached InfluxDB (`schema.measurementFieldKeys`). The car sends soc, est_battery_range, odometer, power, voltage, current, speed, hvac_setpoint, the four tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does **not** send: - **Battery temperature:** read from `EV_BATTERY_AVERAGE_TEMPERATURE`, an Android 14 property; the head unit runs Android 9, and the SAIC services offer none. - **Cabin temperature:** `HVAC_TEMPERATURE_CURRENT` is not published; only the setpoint is. - **Outside temperature:** not published by the vehicle (upstream noted the same). Upstream used the head unit's weather service as a stand-in (`HeadUnitWeather`, removed in `ea8032a`); restoring it was considered and declined. - **Pack energy (`soe`) and capacity:** not published. The Live dashboard computes energy in pack as SOC × usable capacity, from the hidden dashboard constant `usableKwh` (50.8 kWh for the 51 kWh battery) in `k8s/grafana/dashboards/live.json`. ## To do - [ ] **Grafana high availability needs another database.** Grafana keeps its own state (users, sessions, preferences, provisioned-dashboard records, alerting state) in SQLite at `/var/lib/grafana/grafana.db` on the `grafana-data` PVC. That is fine for one replica, and is why `k8s/grafana.yaml` uses `replicas: 1` and the `Recreate` strategy: only one Grafana may write the file. To run more than one replica, first move Grafana to PostgreSQL or MySQL (`GF_DATABASE_TYPE`, `GF_DATABASE_HOST`, …); the replicas then share that database instead of a volume. The car data itself is in InfluxDB and unaffected. - [ ] **EVHardware's comments are partly French.** About 120 comment lines in the vendored `EVHardware/` library (mostly `EVHardware.kt`, some tests), half-translated upstream. The app, the docs and the build files are English. - [ ] **Idea: climate energy on the Overview (motor vs climate).** Nothing the app reads splits energy by consumer: battery power (voltage × current) is the total of motor, climate, 12 V and the rest. SAIC's charging service, which EVHardware already binds, has counters that might: `TotalConsumption`, `TotalRegen` and `AccConsumption`, each since start and since the last charge (`SaicCharging.consumedKwhSinceStart()`, `regeneratedKwhSinceStart()`, `accCounterKwhSinceStart()` and the `…SinceCharge()` twins). Whether "Acc" means *accessory* (a measured climate/auxiliary share; motor ≈ total − acc) or *accumulated* (a duplicate of the total) is unknown — EVHardware deliberately does not call it climate — and whether SWI68 answers at all is untested. Plan: (1) a read-only diagnostic showing the raw counters since start; (2) two drives, climate off and on, with a photo of the counters after each; (3) if Acc grows only with the climate, an Overview card "Climate & accessories since start" with the motor's share beside it — otherwise drop the diagnostic and, at most, show a labelled estimate (power drawn at a standstill with the climate on). ## Done - 2026-10-17 — Translated the app UI from Turkish to English. - 2026-10-17 — Nix flake: `nix develop` provides JDK 21 and the Android SDK. `build-car` builds the unstable APK and signs it with the AOSP test platform key, which the MG4 head unit uses (cert SHA-256 `c8a2e9bc…192ab8`). - 2026-10-17 — OTA update checks disabled by pointing at an unresolvable host (see To do). - 2026-10-17 — Renamed to "Nils' MG4": package ID `com.stinnesbeck.nilsmg4` (`.unstable` for the car build), APK names `NilsMG4-*.apk`. It installs next to the old FatihsMG4 app instead of replacing it, and starts with empty settings and history. - 2026-10-17 — Server rewritten for InfluxDB 2 with live telemetry and ABRP forwarding; Dockerfile; Kubernetes manifests (not applied). - 2026-10-17 — App 2.3.0: Server tab (URL, ingest token, installation id, config import), live telemetry restored from before `ea8032a` and sent to the server, Upload-tab cadence controls reconnected, dead switches removed. The removed ABRP classes' unit tests compile and pass again (AbrpApi and HeadUnitWeather tests deleted with their classes). - 2026-10-17 — Accent colour: MG red instead of muted gold. Web app renamed to Nils' MG4. - 2026-10-17 — Dashboard translated to English (en-GB formatting); euro instead of lira in the dashboard and the car app's charging tab. - 2026-10-17 — Server rewritten in Go (API only); dashboards moved to Grafana with provisioned InfluxDB datasource and three dashboards; the Next.js app (`web/`) removed. Per-host cert-manager certificates dropped in favour of Traefik's default certificate. - 2026-10-17 — Pairing by QR code: server 2.1.0 issues per-car tokens after approval on a phone; app 2.4.0 shows the QR code and collects its token. Tested end to end against InfluxDB. - 2026-10-17 — Incident: a commit swept the filled-in `secrets.example.yaml` (InfluxDB admin token, INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside `kubectl apply -f k8s/`). Filled-in secrets live only there from now on. - 2026-10-18 — `english-translation` merged into the default branch (merge commit `f54e684`, the summary as its message), `master` renamed to `main`, the work branch deleted. Work goes on `main`. - 2026-10-18 — AGENTS.md rewritten as the entry point for agents: what the project is, rules, confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). - 2026-10-24 — App 2.22.0 (branch `dashboard-cockpit`, from `dashboard-gauges`): a third Overview style, "Cockpit", designed without a reference: three segmented rings (`RingView`, Canvas) — battery (0…100 %, range inside, below 20 % in the warning colour), power with the speed and the gear inside (zero at the top: drawing power clockwise to 150 kW in the accent colour, regeneration anticlockwise to −50 kW in green) and 5-minute usage (0…30 kWh/100 km, a white tick for the average since start). Segments brighten towards the leading one. Below, the power of the last two minutes as a trace (`TraceView`; `PowerTrace`, one slot per second, unit-tested; gaps for unread or missed readings, never zeros), the clock, climate setpoint and odometer. Settings → Overview style: Tiles / Dashboard / Cockpit. Checked on the emulator with sample values in a throwaway build (`screenshots/nilsmg4-2.22.0-cockpit.png`) and without data through the real setting (nothing lit, "—", the page fits 1778×640). Not tried on the car. - 2026-10-24 — App 2.21.0 (branch `dashboard-gauges`): Overview style "Dashboard", an instrument cluster after a reference picture of the owner's: power (−50…150 kW), speed (0…180 km/h, with TOTAL odometer and TRIP km since start) and battery (E…F, below 20 % in the warning colour, range underneath) as gauges (`GaugeView`, Canvas, accent-coloured arcs, white needles that ease to a new value), and a strip of gear (P/D), climate setpoint, clock, 5-minute usage and a battery bar (`BatteryBarView`). Unread values draw no needle and "—". Settings → Display → "Overview style: Tiles / Dashboard"; Tiles stays the default; both are fed from the same reading. Checked on the emulator with sample values in a throwaway build (`screenshots/nilsmg4-2.21.0-dashboard.png`) and without data through the real setting; the Overview and Settings fit 1778×640. Not tried on the car. - 2026-10-23 — App 2.20.0: the Overview's Vehicle info card shows the lease mileage: "Lease allowance today" (40,000 km over four years from 2023-05-01, accruing evenly per day, in `LeaseAllowance`, unit-tested) and "Versus allowance", the odometer against it ("… km over" / "… km under", "—" when the odometer is unread). The odometer now has a thousands separator, like the new rows. Checked on the emulator with a sample odometer of 25,000 km in a throwaway build (34,798 km allowed on 2026-10-23, 9,798 km under); the Overview still fits. - 2026-10-23 — App 2.19.9: the Foreground apps card is gone from the Logs tab (it found CarPlay's package; nothing left to find), with ForegroundProbe and ForegroundHistory. The Logs tab is one card again — update log, then uploads — with the other tabs' page padding. - 2026-10-23 — App 2.19.8: the charging curve's labels no longer overlap: "kW" has a row of its own above the plot, the power values are right-aligned against the axis, and the SOC ticks read "0 %" … "100 %" (the separate "SOC %" sat on top of "100"). The wide Charging tab splits 35/65, values | curve. Checked on the emulator with sample AC values and a curve in a throwaway build; the page still fits 1778×640. - 2026-10-23 — App 2.19.7: AC charging is metered too, while the app runs (owner's choice; Fatih had metered DC only, `e9bfa12`, because an overnight AC charge is slept through). A session runs whenever power flows into the pack (`ChargingState.isMeteredCharging`: inbound > 0.3 kW and a charge by `isCharging`), and its curve takes every inbound sample. Gaps where the head unit slept are dropped, not billed, so an overnight AC session shows the awake part only. The label says "AC charging" / "DC fast charging"; `isDcCharging` stays for it and ABRP's `is_dcfc`. Server comment, server README and the Grafana charging dashboard's texts no longer say DC only (the dashboard ConfigMap changes with them: re-apply `k8s/`). Unit tests; not tried on the car. - 2026-10-23 — App 2.19.6: the CarPlay full-screen switch and the "Export CarPlay data" button are removed (CarPlay's size is fixed, see below). On start, the car build takes the CarPlay entry out of `policy_control` if the switch was left on, and leaves anything else there alone — checked on the emulator. The Foreground apps card stays as a diagnostic. AGENTS.md lists the CarPlay and install findings among the confirmed car facts. - 2026-10-23 — CarPlay full screen: not possible on this firmware (SWI68). The export from the car showed CarPlay's window at 1920×720 with both SAIC bars hidden (policy_control on), and its video SurfaceView at (0,80), 1920×640, with a 1920×640 buffer: the grey strip is the window's own background above the video. Allgo's `/system/app/AllgoCarplay_EH32` hard-codes it twice: `CarPlayService` announces `SCREEN_HEIGHT = 640`, `SCREEN_WIDTH = 1920` to the iPhone (it reads `sys.screen.heightpixel/widthpixel`, then overwrites them with the constants), and `carplay_screen_layout.xml` places the SurfaceView at `640px` high with `80px` top margin. Both are in the read-only, signed system image. The export (`dump/`) is gitignored: it holds Allgo's software. - 2026-10-23 — App 2.19.5: installing from USB (and over the air) works on the car again, with a confirmation. The update log showed the car refusing the silent session: `Not allowed to install non-system apps on internal storage`. The fallback, Android's installer screen, then said "There was a problem parsing the package": as the system user, our FileProvider's read grant to the installer is refused (`not exported from UID 1000`). The car build now hands the installer a file URI to the verified copy in Downloads (StrictMode's file-URI check lifted for that one call). On the emulator, with the session forced to fail as on the car, the installer screen asked to update and installed it. Not tried on the car yet. OTA.md updated. - 2026-10-23 — App 2.19.4: "Export CarPlay data" on the Logs tab (Foreground apps card), for the question whether CarPlay's resolution can change. CarPlay with policy_control still leaves a grey strip, also after reconnecting: Allgo announces a smaller picture to the iPhone. 20 s after the press (time to open CarPlay) it writes to `/Android/data//files/ nilsmg4-carplay-