The Overview read the car every 2 s, on the main thread. It now reads every 0.5 s on a background thread (one reader; a read still running makes the next tick skip rather than queue) and shows the values on the main thread. The consumption and charging trackers stay on the 2-second tick with the newest reading: they integrate over time, and the charging tracker writes its session to storage on every sample. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
248 lines
18 KiB
Markdown
248 lines
18 KiB
Markdown
# 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`).
|
||
- [ ] 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.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`.
|