Files
mg4/SPECS.md
T
nilsandClaude Opus 5.5 9b5fcc0734 App 2.14.0: accent colour after the MG4 paints; car colour probe
The accent is selectable in Settings → Display: Dynamic Red (default),
Volcano Orange, Holborn Blue, Racing Green, Camden Grey, Monument
Silver, Arctic White and Black Pearl. White, silver and black cannot be
an accent on the black screen as they are, so they become light greys
and a pearl blue-grey.

The accent palette is now theme attributes (attrs_accent.xml) with one
overlay per paint (accents.xml), applied before the activity inflates;
layouts, colour lists, drawables and the views that draw in code use
them, including the red tints that were hard-coded. A change recreates
the activity on the same page. Android 9 crashes on gradients with
theme colours, so the battery card is built from layers.

Logs → "Look for the car colour" lists the head unit's system
properties that mention colour, paint or car model, to find out whether
the paint colour can be read from the car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-18 13:52:57 +02:00

16 KiB
Raw Blame History

SPECS — Nils' MG4

Journal for this fork of fatihdonmezdev/FatihsMG4: what has been changed, and what is still to do.

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/: 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 (RFC 8628-style; server/README.md). The shared INGEST_TOKEN still works.
  • Car app: Server tab (URL, pairing by QR, ingest token, installation id, config import); live telemetry every 15 s while driving or charging; consumption chunks and charging sessions as before.
  • 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

  • Go server 2.0.0: tests and govulncheck clean; image builds (about 11 MB).
  • End-to-end in podman (server + InfluxDB 2.7.12 + Grafana 13.2.3): every endpoint, retry idempotency, all 25 dashboard queries return data, dashboards rendered and checked.
  • Manifests validated offline (including Traefik CRDs). Not applied.
  • Car app 2.4.0 (pairing by QR): unit tests pass; not yet tried on the car.
  • Image mg4-server:2.1.0 (with pairing) pushed to git.stinnesbeck.com (public; pinned by digest sha256:e5f6e2d2… in k8s/server.yaml).
  • Check Grafana's host in k8s/ingress.yaml (placeholder car-grafana.k8s.stinnesbeck.com).
  • First pairing and drive with 2.4.0: telemetry arrives in InfluxDB, Grafana and ABRP.

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

  • About dialog link and QR code → https://git.stinnesbeck.com/nils/mg4. Both come from REPOSITORY_URL in app/src/main/java/com/evsuite/abrp/MainActivity.java and about_repository in app/src/main/res/values/strings.xml. They still point to Fatih's GitHub repository.
  • Re-enable OTA updates against our own releases. RELEASES_API in app/src/unstable/java/com/evsuite/abrp/OtaUpdater.java points at updates.invalid on purpose, so checks fail. The updater speaks the GitHub releases API and only allows GitHub hosts (ALLOWED_HOSTS). For git.stinnesbeck.com it needs the host on the allowlist, and the response parsing checked against that server's releases API. Release assets must be named NilsMG4-unstable-<version>.apk with a .sha256 sidecar (build-car writes both).
  • 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.
  • Telemetry queue survives restarts. Unsent live samples are kept in memory only.
  • Translate the rest. The docs (CHANGELOG.md, OTA.md's message table, comments in mise.toml/app/build.gradle) are still Turkish or French.
  • Move hardcoded UI strings into strings.xml. Many are inline in Java and layouts.

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 — App 2.14.0: accent colour selectable after the MG4 paints (Settings → Display): Dynamic Red (default), Volcano Orange, Holborn Blue, Racing Green, Camden Grey, Monument Silver, Arctic White, Black Pearl; white, silver and black become light greys and a pearl blue-grey, since they cannot be an accent on black. The accent palette is theme attributes (attrs_accent.xml, overlays in accents.xml); a change recreates the activity on the same page. Android 9 crashes on gradients with theme colours, so the battery card is layered. Logs tab: "Look for the car colour" lists system properties that mention colour, paint or car model, to find out whether the paint colour can be read (nothing on the emulator).
  • 2026-10-18 — App 2.13.0: tyre pressures in bar by default, kPa or psi selectable (Settings → Display; display only, the car and the server keep kPa). "Power, 5-min average" card under Power: time-weighted mean battery power, standing included.
  • 2026-10-18 — App 2.12.1: no scroll bars (the 2-second refresh kept them from ever fading); "Energy since start" tile (net kWh of the trip, recuperation counted back) left of "Usage since start".
  • 2026-10-18 — App 2.12.0, server 2.5.0: settings entered on a phone. "Enter on phone" shows a QR code; the phone form on the server (approval password) takes the ABRP credentials, charging price and usable capacity, and the car collects them once (RFC 8628-style, like pairing; values in server memory only, at most 10 minutes). The config file also takes charging_price and usable_kwh. The QR dialog is shared with pairing (PhoneQrDialog). Tested end to end with curl against the server image and InfluxDB; the app side on the emulator only up to the dialog.
  • 2026-10-18 — App 2.11.1, server 2.4.0: ABRP forwarding removed from the server. The car and the server use the same mobile connection, so a server fallback for ABRP bought nothing. The car no longer sends abrpSentUtc; the server ignores unknown fields, so older apps keep working.
  • 2026-10-18 — App 2.11.0, server 2.3.0:
    • Live data goes to ABRP straight from the car when ABRP credentials are set in the app (Live data tab or config file: abrp_api_key, abrp_token), so ABRP keeps getting it while the server is down. The car tells the server the newest sample ABRP took (abrpSentUtc); the server forwards only newer ones. Without credentials in the app the server forwards as before. This deliberately puts ABRP credentials in the car again (encrypted, never logged).
    • Settings regrouped: Live data (on/off, ABRP, frequency), Server, Settings (full screen, voice key, usable capacity and SoH from Consumption, charging price from Charging, Wi-Fi, which lost its own tab). Charging and Live data have their own icons.
  • 2026-10-18 — App 2.10.2: the range shows "∞" instead of "> 999 km" while recuperating.
  • 2026-10-18 — App 2.10.1: net regeneration (recuperating downhill) shows as negative usage, and the range as "> 999 km" with "while recuperating", instead of "needs a few minutes of driving".
  • 2026-10-18 — App 2.10.0: "Range since start" replaces the range at current power. It uses the consumption since the car was switched on, including standing with the climate on, and a new trip starts after 10 minutes without samples or when charging. Usage (kWh/100 km) for both estimates moved into cards of their own under the range tiles, which fit on the full-screen 1920x720 overview without scrolling.
  • 2026-10-18 — Code review fixes. App 2.9.0, server 2.2.0, Grafana token:
    • The server drops a bad value (or a sample without a plausible time) instead of rejecting the batch, and answers 200 with what it dropped; the app sends only values within the server's ranges (speed as a magnitude) and no samples before its clock is set. Before, one reversing sample cost the whole queued batch.
    • Pairing: the token is handed out and accepted only after the device is saved; wrong-password lockouts and open-request limits are per client address; a paired token only serves its own installation id (403 otherwise).
    • ABRP forwarding skips samples stamped ahead of the server's clock, which used to block it.
    • App: settings written in plaintext during a keystore failure move back into encrypted storage; the installation id is derived from ANDROID_ID, so a reinstall keeps its history; the imported config file is deleted; GPS stops when live telemetry is off and pauses while parked and idle; range estimates use the monotonic clock; one HTTP helper (ServerHttp) for all server calls; the last Turkish strings translated.
    • Grafana reads InfluxDB with its own read-only token (grafana-influx Secret, created by hand: k8s/README.md).
  • 2026-10-18 — README rewritten for Nils' MG4. OTA.md, mise.toml and docs/MG4_PLATFORM_INSTALL.md use the new package ID and NilsMG4-*.apk names; OTA.md notes that updates are switched off. CHANGELOG.md keeps the old ID as upstream history.
  • 2026-10-18 — App 2.8.2: full screen uses the whole display. The MG4 hid its bars but left a black band where they were: LAYOUT_STABLE kept reporting the bars' insets and the fitsSystemWindows root padded for them. Both are off in full screen now.
  • 2026-10-17 — App 2.8.1: three-part version names (2.8.1-unstable, NilsMG4-unstable-2.8.1.apk). Upstream's fourth part was its CI build number; the version code keeps the x100000 factor so it never falls below codes already installed.
  • 2026-10-17 — App 2.8.0: immersive full screen (switch in the Upload tab, default on). Hides the system bars on the emulator; whether SAIC's own bars on the MG4 can be hidden is only known once tried on the car.
  • 2026-10-17 — App 2.7.1: the voice key works. The car's key report, read off the on-screen log: android.intent.extra.hardkey.keycode (Integer), .down (Boolean, a report per press and per release), .longpress (Boolean). 2.7.0 looked only for short names and never found the code. Floating button removed (it did not show over the MG4 launcher).
  • 2026-10-17 — App 2.7.0: a double press of the steering wheel's voice key (code 287) opens the app, from the head unit's com.saic.keyevent.hardkey.report broadcast. Its extras are not documented; the usual names are tried and every report is listed raw on the Logs tab. The floating button (2.6.0) did not show over the MG4 launcher. Pages no longer jump back to the top on every refresh (StableScrollView), and the Vehicle tab shows all six value tiles, including both range estimates, in one row at the top.
  • 2026-10-17 — App 2.6.0: floating button to open the app, because the MG4 launcher does not list it. Drawn over other apps (needs the overlay permission, which the car build has through the system UID), draggable, hidden while the app is open, switch in the Upload tab. Tested on an Android 9 AOSP emulator with the car APK (installs as UID 1000 there too), at 1920x1080 and at the head unit's real 1920x720 / 160 dpi, where all tabs and the button were checked as well.
  • 2026-10-17 — App 2.5.0: range estimates at current and 5-minute average power (Vehicle tab).
  • 2026-10-17 — Live dashboard reworked around the signals the car sends: Climate setpoint, Last update and a computed Energy in pack replace the empty temperature tiles; Battery voltage and current replaces the Temperatures chart.
  • 2026-10-17 — Temporary on-screen diagnostics (b2a5b32) to debug blank values without adb; removed again (1002962) once values showed on the car. Revert 1002962 to bring them back.

Installing on the car

nix develop
build-car
adb push build/car/NilsMG4-unstable-<version>.apk /data/local/tmp/update.apk
adb shell "pm install -r -f -d -t /data/local/tmp/update.apk"

To remove the old upstream app: adb uninstall com.evsuite.abrp.unstable.