# 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.38.0 (installed on the car over the air, 2026-10-26), server 2.6.2 (pinned by digest in `k8s/server.yaml`; applied and running since 2026-10-25). - [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.** - [x] Grafana's host in `k8s/ingress.yaml` is `car-grafana.k8s.stinnesbeck.com`, the one in use (owner, 2026-10-25). - [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. - [x] On the car (owner, confirmed 2026-10-25): 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`. - **Adaptive cruise control (set speed, distance to the car ahead, ACC state):** the head unit gets none of it (SWI68, checked 2026-10-25). The `AAD_*` properties of `CarAdvancedAssistedDrivingManager` exist and answer, but stay at their defaults with ACC engaged behind another car (`AAD_EPTCCTRGTSPD` `0x2160A15B` 0.0, `AAD_ACC_OBJ_*` 0.0 / -128.0, modes 0). A signal recording of all 1,194 values while the set speed was 101, 90 and 80 km/h shows none of those numbers (also tried as m/s, mph and ×2/×10/×16/×64) outside the actual speed, and the ACC buttons send no key broadcast. Most likely only the instrument cluster has it. ## 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. - [x] **EVHardware's comments are partly French.** Translated 2026-10-25 (see the done log). - [ ] **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-26 — App 2.39.0 and server 2.7.0: **Grid with map**, a new Overview style — the Grid over the roads round the car from OpenStreetMap (owner's choice of design: roads through the car's own server, drawn as the floor). Server: `POST /v1/map/roads` `{z:14, x, y}` (the tile, not the position, and in the body so it stays out of access logs) fetches the tile's drivable roads from Overpass (`OVERPASS_URL`, default the public instance, `off` disables; https only; one request at a time, ≥ 1 s apart, identifying User-Agent), turns them into tile-local points in 4 classes simplified with Douglas–Peucker (~1.5 m), caches them in memory (30 days, 3000 tiles; failures not cached), gzips when accepted, answers 503 when Overpass fails and never logs the tile. Tests with a fake Overpass (classes, split at missing points, simplification, cache, gzip, validation, 503 without caching, no tile in the log, config). App: `MapRoads` keeps the 3×3 tiles round the car (files/map, 30 days, 400 tiles; one at a time on a background thread; a failed tile waits a minute), `RoadTile` parses them failing closed, `MapGeometry` does the web-mercator arithmetic (both tested). GPS only while that style is shown and the app is in front; the bearing from the GPS, or worked out from positions 8 m apart when it gives none. `GridView` with a map draws the roads in the floor's perspective, heading up, 40 m floor steps, up to 900 m ahead, far roads fainter, the car as a chevron, its position run on with the speed between fixes, 20 fps while moving; the trail gives way to the map; "© OpenStreetMap contributors" shown; otherwise the plain floor saying why (waiting for GPS, set up the server, loading, not available). `OVERPASS_URL` in `k8s/server.yaml`; LICENSE.md notes the ODbL. Tested: server unit tests; one real fetch of 3×3 tiles in central Berlin through the server code (20–30 KB each; Overpass answered 502 a few times and the retry got them); on the emulator those tiles from the app's cache with emulator GPS, north up and turning with a simulated drive, both layouts, no GPS, no server; no crash. Not tried: the app fetching tiles from the deployed server (not deployed yet) and anything on the car. - 2026-10-26 — App 2.38.0 published (`car-2.38.0`) and installed on the car through the in-app updater, from 2.35.3 (so Grid, Time circuits, Quest, Visor and Empire are on the car). - 2026-10-26 — App 2.38.0: Overview styles **Visor** (after Halo's helmet display, `VisorView`) and **Empire** (after Age of Empires' interface, `EmpireView`). Visor: shield bar = battery (24 segments, red and blinking "BATTERY LOW" below 20 %), ammunition counter = range (number and rounds, one per 10 km), P R N D as grenade slots, a reticle with the speed and power round it (green and anticlockwise in regen), a motion tracker whose blips are the power trail (rim 60 kW, 40 kW regen) and whose sweep turns only while moving (~30 fps, on screen only), the clock and an objective panel; original blue or the accent colour (`hasOriginal`). Empire: a wooden resource bar (battery, range, power, 5-min usage, the age, the clock), an isometric world whose ground and trees move with the speed (6 m tiles, ~30 fps only while moving), the car as the selected unit with the battery as its health bar, the speed large over the world, a carved panel with P R N D as commands, the unit's portrait and stats, and a diamond minimap with the odometer and the way to the next age; ages by odometer in `EmpireAge` (pure, tested: Dark, Feudal from 10,000 km, Castle 30,000, Imperial 60,000); its own palette, the app in the player's blue. `stylePanes` is now built from the canvas list. THEMES.md with two screenshots each. Emulator: sample values, 15 % at a standstill, with and without the navigation, and no data (dashes); no crash. Not tried on the car. - 2026-10-26 — App 2.37.0: Overview styles **Time circuits** (after Back to the Future, `TimeCircuitsView`) and **Quest** (after World of Warcraft's interface, `QuestView`); both with their own palette, the app in red and gold with them. Time circuits: three LED rows in a metal panel with embossed tape — red "DESTINATION" (gear as DRV/REV/NTL/PRK, charge, range, power/regen lamps, usage), green "PRESENT TIME" (date and time from the head unit's clock), amber "LAST TIME DEPARTED" (when the car last left P, tracked from the readings, dashes until seen); the speed in red digits, a flux capacitor whose light runs inwards with the power and outwards in regen; at 88 km/h the speed burns white, the tape reads "88 KM/H — HOLD ON" and the capacitor flashes. Letters on fourteen segments (`Segments14`, pure, tested). Quest: player frame (gear as level, battery as health, power as energy), minimap with the clock, gold speed, a cast bar (Accelerating, Regenerating, Coasting, Resting in P), a road-trip quest tracker, system chat lines, P R N D action buttons, the odometer as experience (a level per 1000 km), a "Low battery!" raid warning below 20 %. Names avoid the trademarks; drawings and fonts are the app's own. THEMES.md with two screenshots each. Emulator: sample values (also 92 km/h and 15 %), with and without the navigation, and no data (dashes); no crash. Not tried on the car. - 2026-10-26 — App 2.36.0: Overview style **Grid**, after the Tron films (`GridView`): neon lines on black, a perspective grid floor that runs towards the driver with the speed (4 m tiles, ~30 fps only while moving and on screen), the speed large and thin, the battery as a 20-segment light wall standing on the floor (orange below 20 %, "LOW ENERGY"), power on a disc (zero at the top, clockwise drawing, anticlockwise regeneration; an inner ring that turns with the power), the last two minutes of power as a light-cycle trail of right angles (`ClusterValues.powerTrail`, the Cockpit's `PowerTrace`; gaps stay gaps), P R N D tiles, the clock, and five read-outs in cut-corner boxes. Glow from layered strokes (no blur filters). Original cyan, or the accent colour (`hasOriginal`; low battery then white); in Original the app turns cyan (`ThemeOverlay.EV.Style.Grid`). Named "Grid", not after the films. THEMES.md has it with two screenshots. Emulator: sample values (full screen, with navigation, accent, 15 %), the floor moving between frames, and no data (dashes, nothing lit, still floor); no crash. Tests and lint. Not tried on the car. - 2026-10-25 — Server 2.6.2: **the update relay logs what it hands out**, per car (installation ID, or "shared token"): `update check` with the number of releases and the newest pre-release, `update download started` with file and size, then `update handed out` with bytes and time, or `update download cut short`; refusals by the forge name the car too. No forge address or token in the log (tested: `TestUpdatesAreLogged`). Go 1.27.3 (build image and `go.mod` toolchain), up from 1.26.9: `govulncheck` had found GO-2026-6809 (compromised intermediate certificates, `crypto/x509`, reached through the server's TLS); now no vulnerabilities, in the source and in the built binary. go vet, tests, podman build. Pushed as `mg4-server:2.6.2` (`sha256:e4de1e75…`, checked in the registry) and pinned in `k8s/server.yaml`. Applied by the owner, together with the real `FORGE_TOKEN`: the car then updated itself to 2.35.3 over the air (owner, 2026-10-25). - 2026-10-25 — `k8s/influxdb.yaml`: the values Kubernetes fills in itself are written out — the NetworkPolicy port's `protocol: TCP`, and `apiVersion`, `kind` and `volumeMode: Filesystem` on the StatefulSet's volume claim template. Without them every `kubectl apply -k k8s/` reported both as "configured" while `kubectl diff -k k8s/` was empty (owner). Applied by the owner: both now "unchanged". Validated offline (kustomize + kubeconform). Also found: the in-app update check failed with an IOException because the cluster's `FORGE_TOKEN` was still the template placeholder — the forge refuses it and the server answers 502; `apply -k` does not include the Secrets, so a new token needs `kubectl apply -f k8s/secrets/secrets.yaml` and a restart. - 2026-10-25 — App 2.35.3: a car build with the English EVHardware (comments, log lines, the "Layer" names); no change in behaviour. Emulator: starts, no crash, the log reads "Layer1: …" to "Layer5: …". Not tried on the car. - 2026-10-25 — EVHardware in English: the French comments (and the half-translated ones like "l'screen", "registersment", "initialisesd") in `EVHardware.kt` and five test files, the French log messages ("aucun init() fonctionnel", "introuvable", "state initial", "ERREUR"), the French test names and assertion messages (`CatalogConsistencyTest`, `GlassEvidenceTest`, `GlassProbeTest`). Comments, log texts and test names only: no code changed (checked in the diff). Kept on purpose: the French translation `values-fr/strings.xml`, place names in test data, the quoted French sentence in `ChargeStopPlan` and the voice phrase in `SaicNavGuidance`, and "Katman" (Turkish for layer), which named the layers in function names and logs — renamed to "Layer" right after (`whenLayer1Ready`, `initLayer4Swi69`, `Layer5: …` in the log, and so on; nothing outside EVHardware used the names). EVHardware, app tests and lint pass. - 2026-10-25 — README.md brought up to date: the tab screenshots retaken with 2.35.2 on the emulator right after installing (`screenshots/nilsmg4-2.35.2-*.png`, the 2.18.1 ones removed), "read-only" now names the drive mode exception, the upload frequency matches the app (1 min by default, 15 s at low battery, 15 min parked), and the Settings and Logs rows list what is there now (Overview style, drive mode, signal recorder). - 2026-10-25 — App 2.35.2: the full-screen button is round. Its disc was an even inset in a 64 × 72 dp (narrow: 64 × 76) area, so an oval; the area is now a square 72 × 72 dp (inset 18 dp, a 36 dp disc), pulled 8 dp into the panel's padding in the wide layout so "MG4" keeps its line. **THEMES.md**: the twelve Overview styles, each with and without the navigation (`screenshots/themes/`, 1920 × 720 from the emulator with sample values from a throwaway build, never committed), with what inspired each and how it takes colour; linked from README.md (Screenshots, Contents, Project documents) and AGENTS.md. Emulator: the round button, the header with "MG4" on one line. Not tried on the car. - 2026-10-25 — App 2.35.1: **Aviation removed** (owner's choice: `AviationView`, its palette and string; a stored "AVIATION" opens Tiles). The Sci-fi palette for the app is now the lilac of the console frame's lower elbow (`scifi_lilac` #CC99CC) instead of the orange of the upper one. Tests and lint; emulator: Sci-fi with the lilac navigation. Not tried on the car. - 2026-10-25 — App 2.35.0: **the app takes on the style's own colours.** While the Overview style shows its own palette — Sci-fi, Cartoon, Aviation and Nixie always, Retro and Terminal with "Original" — the navigation, buttons, switches and titles take that palette instead of the accent colour: Retro turquoise (`retro_lit`), Terminal green phosphor, Sci-fi orange, Cartoon orange, Aviation cyan, Nixie neon orange (`StylePalette`, overlays in `res/values/style_palettes.xml`, laid over the accent overlay in `onCreate`; a style or colour choice that changes the overlay recreates the activity, as an accent change did). The accent colour stays stored for the styles that use it. **Blueprint and Mascot removed** (owner's choice): `BlueprintView`, `MascotView`, `MascotMood`, `MascotCharacter`, `ChibiBase`, `ChibiFigure`, `FoxFigure`, their strings and the Mascot field on Settings; a stored "BLUEPRINT" or "MASCOT" opens Tiles. Fix: the navigation (`nav_bar`) no longer saves view state — it is a ScrollView in the wide layout and a LinearLayout in the narrow one, and restoring one into the other crashed when the width changed (seen on the emulator only; the car's width does not change). Tests (`OverviewStyleTest`: palettes per style, removed styles) and lint; emulator: each palette, the switch to Retro recolouring at once, the width change. Not tried on the car. - 2026-10-25 — `styles-retro-future` (app 2.23.0–2.34.1) merged into `main` (merge commit `aff79cb`, the summary as its message). Work goes on `main` again. - 2026-10-25 — App 2.34.1 (branch `styles-retro-future`): the Overview's full screen without the app's full screen. With the car's bars shown, the car's own sidebar sits at the left edge, so a swipe from the edge lands on it, not on the app (owner, on the car). There a small tab now sits at the app's left edge instead of the faint handle (48 × 96 dp touch area, a dark tongue with ›): a tap brings the navigation back, and so does a swipe starting in the app's first 56 dp (`tabSwipe`). In the app's full screen nothing changed (handle and edge swipe, the owner's preference: least clutter). Switching Full screen on Settings updates it at once. Emulator: the tab shows, a tap and a swipe on the left strip both bring the navigation back. Not tried on the car. - 2026-10-25 — App 2.34.0 (branch `styles-retro-future`): **drive mode at start.** The car falls back to its own default mode when switched on; with "Set at start" (on by default, the switch in the drive mode card) the app sets the mode last chosen on Settings again. Only a choice the car confirmed counts (`PREF_CHOSEN`, saved on APPLIED). `AbrpUploadService` starts with the head unit and runs `DriveModeWrite.atStart` (pure, tested) once a second on its own thread: wait for gear, speed and mode; nothing when the car already reports the mode; one write, in P at 0 km/h, through the same `DriveModeSetter` as the buttons (write, read back up to 3 s); give up for this start as soon as the car is out of P or moving, or after 2 minutes. So a mode set with the car's own button after driving off stands. The result is kept with its time and shown under the buttons ("At start (25.10. 20:31): Sport set."). Assumes the head unit starts (and the service with it) when the car is switched on; a start without that (head unit kept running) is not caught. A service restart (app update, after a kill) counts as a start, still only in P at 0 km/h. Hints shortened to fit the page. Emulator: page fits, the start check waits and gives up (no readings), no crash. Not tried on the car. - 2026-10-25 — App 2.33.0 (branch `styles-retro-future`): **drive mode from Settings**, the first vehicle setting the app writes. The owner lifted rule 3 for the drive mode only (AGENTS.md). A card at the top of Settings' second column: Eco, Normal, Sport, Snow, Custom (codes 2, 3, 4, 6, 7 at `0x2140A17C`; the 2.32.0 recording read 2, Eco). The lit button is the mode the car reports, from every 0.2 s reading. A tap reads gear and speed fresh and writes only in P at 0 km/h (`DriveModeWrite`, pure and unit-tested; unreadable gear or speed is a refusal), through the new `EVHardware.writeDriveModeProperty`: the CarPropertyManager write behind `VehicleWriteGate` (0 km/h, supported firmware), without `setDriveMode`'s fallback binder transaction. Then the mode is read back for up to 3 s, and the status beside the title says what the car reports ("set", "not applied: the car still reports …", "could not be read back"). One change at a time, on the settings thread; each attempt goes to logcat. To fit the page, the wide layout's charging price row moved into the battery card ("Battery and charging price"). Emulator: the card fits, a tap is refused (no gear or speed there). Whether the car takes the write at all is untested: the first try on the car decides. - 2026-10-25 — App 2.32.1 (branch `styles-retro-future`): the cruise control work ends. The ACC recording of 2.32.0 (ACC set to 101, 90 and 80 km/h, a car ahead) found nothing: see "Signals the car does not provide". Removed: `CruiseLog` and its section on the Logs tab, and in EVHardware the set-speed read (`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`), `readAccDebug` and `ReadFailureLog.reason` — EVHardware is again as before 2.27.3. The old log files are deleted on start. The recorder stays as a general tool, renamed **Signal recorder** (`SignalRecorder`, `SignalRecording`, `signal-recording-