Files
mg4/SPECS.md
T
nilsandClaude Opus 5.5 bfa33d51bd App 2.16.2: show the car's screen size; display figures documented
The MG4's display is given as 1920x720 (this project), 1280x480
(EVProfile's README) and a 1778x720 app window (upstream). 1280x480 is
exactly 1920x720 px at 240 dpi expressed in dp, so the figures may
describe one panel at different densities, which this platform lets be
changed per app.

The Logs tab's new SCREEN line shows the panel's pixels and density and
the app window in px and dp, to settle it from the car. FIRMWARE.md
lists the three figures and what the emulator shows at 240 dpi;
mise.toml, README and SPECS.md no longer state 160 dpi as fact for the
car.

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

261 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).
- [ ] Screen size and density on the car: Logs → SCREEN (2.16.2), see FIRMWARE.md.
- [ ] Not yet tried on the car: live data straight to ABRP (2.11), Enter on phone (2.12), accent
colours and the car-colour probe on the Logs tab (2.14; a photo of its list decides whether
the accent can follow the paint automatically), 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.
## 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.16.2: Logs → SCREEN shows the panel's pixels and density and the app
window in px and dp. The MG4's display figures disagree (1920×720 px used here; EVProfile lists
1280×480, which is 1920×720 at 240 dpi in dp; upstream reported a 1778×720 window); FIRMWARE.md
lists all three until a photo of that line settles it.
- 2026-10-18 — EVProfile's licence checked: MIT, forked from SliDeeN/MG4Control and last merged
from it on 2026-08-02, inside MG4Control's MIT period (MIT up to 2.6.5 of 2026-08-05, GPL v3
from 2.6.6 of 2026-09-02). Recorded in `LICENSE.md`. History scanned for secrets before the
repository went public: none.
- 2026-10-18 — Licence: Leon Kernan's `ABRP_Uploader` has been MIT since 2026-07-29 (its commit
`75d8f2bc`), so the "unresolved" notes are gone. `LICENSE` names every copyright holder in the
fork chain; `LICENSE.md` explains that `EVHardware/` is PolyForm Noncommercial, which makes the
built APK non-commercial. README, AGENTS.md and CONTRIBUTING.md updated.
- 2026-10-18 — App 2.16.1: the Overview reads the car every 0.5 s (was 2 s), on a background
thread; the consumption and charging trackers stay on the 2-second tick (they integrate over
time, and the charging tracker writes its session to storage on every sample). Note for
emulator tests: uiautomator can no longer dump the screen while the Overview is open, since
the UI is never idle.
- 2026-10-18 — Grafana's read-only InfluxDB token without a manual step: an init container in
the Grafana pod creates it with the admin token at every start, deletes the previous one and
leaves only the read-only token in a memory volume; the datasource reads it with
`$__file{/run/influx/token}`. InfluxDB ignores chosen token values (tested), so it cannot come
from the Secret. The `grafana-influx` Secret is gone. Tested in podman: InfluxDB 2.7.12, the
script as the Grafana user (twice: rotates, no pile-up), Grafana 13.2.3 datasource healthy.
- 2026-10-18 — App 2.16.0, server 2.6.0: over-the-air updates from this repository. `publish-car`
creates the pre-release `car-<version>` on the private forge with the APK and its `.sha256`;
the server relays releases and attachments to the car (`/v1/app/…`, read with a read-only
`FORGE_TOKEN`); the app checks them through its server with its own token, allows exactly the
server's host, downloads itself (no DownloadManager, so the token stays in the app), verifies
SHA-256 and package, and tries a PackageInstaller session before falling back to Downloads.
`OTA.md` rewritten for this flow. Not yet tried on the car.
- 2026-10-18 — App 2.15.0: unsent live samples survive a restart. The queue is saved to
`files/telemetry_queue.json` (AtomicFile) at most every 5 minutes and only after a change, and
at once when the service stops; it is restored, in order and within its 2000-sample cap, when
the service starts. A killed process loses at most 5 minutes. Tested on the emulator across a
reboot (6 queued samples back).
- 2026-10-18 — App 2.14.4: every UI text is in `strings.xml` (Charging, Consumption and Settings
labels, toasts, the trip-reset dialog, charging and Wi-Fi states, the daily summary, the "—"
placeholder); lint reports no hard-coded text. Upload-log entries stay in code as log text.
- 2026-10-18 — Translated the rest: `mise.toml`, the comments in `app/build.gradle`, `OTA.md`'s
message table (now the updater's current English messages), the Turkish messages quoted in
`CHANGELOG.md` and `OTA.md` (glossed), two test messages, the ignition-state comment in
`EVHardware.kt`.
- 2026-10-18 — About dialog: link and QR code point to https://git.stinnesbeck.com/nils/mg4
(were Fatih's GitHub). Status section brought up to date.
- 2026-10-18 — App 2.14.2: "Range, 5-min average", like the usage and power cards (was "Range at 5-min average").
- 2026-10-18 — App 2.14.1: Overview values and their notes right-aligned, with tabular
(equal-width) digits, so 9 → 10 → 100 no longer shifts the text; labels stay left.
- 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`.