nilsandClaude Opus 5.5 f54e684ae1 Merge english-translation: Nils' MG4 (app 2.18.0, server 2.6.0)
Turns the FatihsMG4 fork into Nils' MG4: an English Android Automotive
app for the MG4 head unit, a Go car server with InfluxDB 2 and Grafana,
Kubernetes manifests, and updates over the air or from a USB stick.
AGENTS.md is the overview, SPECS.md the journal of every change.

App (com.stinnesbeck.nilsmg4, car build .unstable)
- English throughout; all UI text in strings.xml. Renamed with a new
  package ID, NilsMG4-*.apk names, three-part versions; About links to
  git.stinnesbeck.com/nils/mg4 and keeps the upstream credits.
- build-car (Nix flake) signs the car build with the AOSP platform key
  the head unit trusts; nix run .#car-emulator gives a matching Android
  9 emulator.
- Overview, fitting the car's 1778x720 window without scrolling:
  battery, range, range/usage/energy since start, 5-minute averages of
  range, usage, power and speed, vehicle info, tyre pressures in
  bar/kPa/psi. Read every 0.5 s on a background thread; values
  right-aligned with equal-width digits; recuperation as negative usage
  and an infinite range.
- Head unit: full screen (kept with drop-downs and dialogs), voice-key
  double press opens the app, selectable accent colour after the MG4
  paints (Dynamic Red by default), no scroll bars.
- Tabs: Overview, Consumption, Charging, Live data (on/off, ABRP
  credentials, frequency), Server (URL, QR pairing, token, read-only
  installation ID, config import), Settings (display, battery,
  charging price, update check, install from USB, About), Logs.
- Live data to the car server and straight to ABRP; only values within
  the server's ranges; the queue survives restarts; GPS pauses while
  parked and idle.
- Settings can be typed on a phone (QR code, form on the server) or
  imported from a config file, which is deleted afterwards.
- Security: credentials in encrypted storage (moved back after a
  keystore failure), installation ID derived from the head unit.
- Updates: from this repository's pre-releases through the car server,
  or from a USB stick; SHA-256, package and signature checked; the
  installer session ran silently on the emulator.
- Removed: Wi-Fi auto-connect (and its permissions), the floating
  button, upstream's diagnostics.

Server (server/, Go 1.26, replaces upstream's Next.js app)
- Telemetry, consumption chunks and charging sessions in InfluxDB 2;
  idempotent writes; a bad value or time drops only itself.
- Pairing by QR code approved on a phone; paired tokens bound to their
  installation; lockouts and limits per client address.
- Settings from a phone (in memory only, at most 10 minutes, handed out
  once); app releases relayed from the private forge with a read-only
  FORGE_TOKEN. No ABRP forwarding: the car sends to ABRP itself.

Infrastructure (k8s/, plain manifests, applied by hand)
- InfluxDB 2.7.12, the server pinned by digest, Grafana 13.2.3 with
  provisioned Live, Consumption and Charging dashboards (euro),
  Traefik traefik-internal routes.
- Grafana's read-only InfluxDB token is made by an init container at
  each start; the Grafana process never sees the admin token.
- Filled-in secrets only in the gitignored k8s/secrets/secrets.yaml.

Licence
- MIT throughout, like Leon Kernan's ABRP_Uploader (MIT since
  2026-07-29) and the forks in between; LICENSE names every copyright
  holder. EVHardware/ is PolyForm Noncommercial, so the built APK is
  non-commercial. EVProfile's MG4Control lineage is from MG4Control's
  MIT period (checked).

Deploying
1. Fill in k8s/secrets/secrets.yaml from the template (InfluxDB admin,
   ingest token, approval password, read-only FORGE_TOKEN, Grafana
   admin password).
2. kubectl apply -f k8s/namespace.yaml, then
   kubectl apply -f k8s/secrets/secrets.yaml, then kubectl apply -k k8s/
3. Car: install the car build once by hand; after that build-car, then
   publish-car (or copy the APK and .sha256 to a USB stick), then the
   buttons on the Settings tab.

Testing
- App unit tests (stable and car build) and lint pass; server tests
  pass and govulncheck is clean; manifests validate offline.
- End to end in podman: server with InfluxDB (telemetry, pairing, phone
  settings), Grafana with the init-container token; release relay
  against a fake Gitea.
- Emulator (Android 9, 1920x720 and 1778x640): layouts, tabs, accent
  colours, config import, queue restore, USB install.
- On the car: pairing, live values, voice key, full screen, screen
  size. Not yet on the car: direct ABRP sending, Enter on phone, accent
  colours, 0.5 s refresh, over-the-air and USB updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
2026-10-18 21:22:13 +02:00
2026-10-18 20:59:45 +02:00
2026-10-18 00:09:52 +02:00
2026-10-18 00:08:14 +02:00
2026-10-18 20:59:45 +02:00
2026-10-18 20:32:01 +02:00
2026-09-29 14:32:25 +03:00

Nils' MG4

An Android Automotive app for the MG4 (SAIC eh32) head unit, plus a small server: the car sends its telemetry to my own server at car.k8s.stinnesbeck.com, which stores it in InfluxDB 2 for the Grafana dashboards, and its live data straight to A Better Route Planner.

No OBD dongle and no phone in the loop: the app runs on the head unit and reads the car's own APIs.

⚠️ No warranty, no liability. This software is provided "as is" and runs on a vehicle. Installing it is your decision and your risk; see DISCLAIMER.md. Not affiliated with SAIC, MG Motor or Iternio. MG and MG4 are third-party marks used only to identify compatibility.

Licence. MIT, like Leon Kernan's original ABRP_Uploader and the forks in between — except the bundled EVHardware/ library, which is PolyForm Noncommercial, so the built app may only be used and shared non-commercially. See LICENSE.md.


Contents

Screenshot

Version 2.8.2 at the head unit's 1920x720 pixels and 160 dpi, in full screen (Android 9 emulator; no vehicle there, so every value shows as a dash).

Overview tab: battery, range, range estimates, power and speed tiles above the energy and tyre-pressure cards

How it fits together

car ──live telemetry───▶ car.k8s.stinnesbeck.com (Go)
 │  ──daily chunks─────▶          │
 │  ──charging sessions▶          ▼
 │                            InfluxDB 2 ◀── Grafana
 └──live telemetry─────▶ ABRP

The car sends live data to ABRP itself, with the ABRP credentials set on the Live data tab, so ABRP and the server do not depend on each other. The server only stores. The car authenticates with the server using a token of its own, obtained by pairing with a phone (below).

The app

Tab What it shows
Overview Battery, range, range since start and at the 5-minute average with their usage, energy since start, power and speed with their 5-minute averages; odometer, climate setpoint and tyre pressures
Consumption Daily and period consumption, efficiency
Charging Charging sessions and curves, cost in euro
Live data Live data on/off, ABRP credentials for sending directly from the car, upload frequency
Server Server URL, pairing by QR code, ingest token, installation ID, config import
Settings Full screen, voice key, tyre pressure unit, accent colour, battery (usable capacity, manual SoH), charging price, version, update check, About
Logs Upload log
  • Opening the app: the MG4 launcher does not list third-party apps. Double-press the voice key on the steering wheel and the app comes to the front (switch on the Settings tab, on by default).
  • Full screen: hides SAIC's top and bottom bars while the app is open; a swipe from the edge shows them briefly (Settings tab, on by default).
  • Live telemetry: every 15 s while driving or charging, much less often while parked. A value the car cannot read is left out, never sent as zero, so ABRP is never told the battery is empty because a read failed.
  • Read-only: the app never writes a vehicle setting.

Signals the MG4 does not publish (battery, cabin and outside temperature, pack energy and capacity) are listed in SPECS.md.

Install on the car

The car build (NilsMG4-unstable-<version>.apk) runs with the system UID and is signed with the AOSP test platform key that the MG4 head unit uses. It installs next to the old FatihsMG4 app (package com.stinnesbeck.nilsmg4.unstable), not over it.

Without adb, through the head unit's hidden file manager:

  1. Copy the APK to a USB stick and plug it in.
  2. Open Bluetooth Settings and select the car name.
  3. Long-press the comma key until the keyboard settings appear.
  4. Android Keyboard Settings → Languages, then search for file in the box at the top.
  5. That opens the Files app: open the USB stick, tap the APK and install.

With adb:

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"

⚠️ Do this parked, and only with an APK you trust. See DISCLAIMER.md.

After that first install, the app updates itself: publish a build with publish-car, then press the refresh button in the app's top bar (OTA.md). The car fetches it through your car server, so the repository stays private.

Setup

  1. Open the app (voice key double press, or "Open" right after installing).
  2. On the Server tab the URL is already https://car.k8s.stinnesbeck.com.
  3. Tap Pair with phone. Scan the QR code with a phone (or open the address shown and enter the code), and approve with the server's approval password. The car receives its token.
  4. On the Live data tab, turn live data on. The service starts with the car from then on.
  5. Enter your ABRP API key and user token on the same tab, so the car sends live data to ABRP.

On your phone

Once the car is paired, Enter on phone (Live data tab, and Settings → Charging price) shows a QR code. It opens a form on your server where you type the ABRP API key and user token, the charging price and the usable capacity, and send them with the approval password. The car collects them within a few seconds and saves them through the same checks as the fields on screen. The server only passes them on: they are held in memory until the car has them (at most ten minutes), never stored or logged.

Config file

Instead of typing, put the settings in a text file and tap Import file on the Server tab. The head unit has no file picker, so copy the file to

Android/data/com.stinnesbeck.nilsmg4.unstable/files/

on the USB stick or the internal storage. The app scans that folder on every mounted volume and takes the first file that parses. Format, one key = value per line, # for comments; keys left out keep their current value:

server_url      = https://car.k8s.stinnesbeck.com
token           = the server's INGEST_TOKEN (not needed after pairing)
installation_id = only to carry over history from an install older than 2.9.0
interval_sec    = 60      # 15 / 30 / 60 / 120 / 300
boost_low_soc   = true
low_soc_percent = 20
abrp_api_key    = your ABRP API key
abrp_token      = your ABRP user token
charging_price  = 0.39    # €/kWh
usable_kwh      = 50.8    # usable battery capacity, for the range estimates

After a successful import the app deletes the file, since it holds the token; credentials live only in the app's encrypted store.

The installation ID is derived from the head unit, so a reinstall finds its history again on its own. installation_id is only needed to carry history over from an install made before 2.9.0. A paired token belongs to one installation ID: after changing the ID, pair again.

Power

The app holds no wake lock and schedules no alarms, so it cannot wake a sleeping head unit and costs nothing while the car is off.

Building

With Nix:

nix develop                    # JDK 21, Android SDK, Go
./gradlew assembleStableDebug  # debug APK for a phone or tablet
build-car                      # car APK, signed with the AOSP test platform key

build-car writes build/car/NilsMG4-unstable-<version>.apk and a .sha256 next to it, and checks the signing certificate before it finishes.

Tests and lint:

./gradlew testStableDebugUnitTest testUnstableDebugUnitTest lintStableDebug

mise tasks from upstream (mise run check, mise run build) still work for the plain Gradle builds.

Server and dashboards

  • Server (server/): Go, API only. Live telemetry, daily consumption chunks, charging sessions, pairing. Image at git.stinnesbeck.com/nils/mg4-server.
  • Kubernetes (k8s/): plain manifests for the server, InfluxDB 2 and Grafana, applied by hand with kubectl apply -k k8s/. Secrets go in the gitignored k8s/secrets/secrets.yaml, from the template next to it.
  • Grafana dashboards (k8s/grafana/dashboards/): Live, Consumption and Charging, provisioned with the InfluxDB datasource.

Project layout

app/src/main      app: upload service, server client, pairing, UI
app/src/stable    no-op update hook (no updater in the APK)
app/src/unstable  car build: system UID, updater (currently pointed nowhere)
app/src/debug     VHAL probe tools, absent from release builds
app/src/test      JVM unit tests
EVHardware        shared vehicle reads per firmware generation
server/           Go server
k8s/              Kubernetes manifests and Grafana provisioning
flake.nix         dev shell and build-car

Project documents

Document What it covers
SPECS.md Journal of this fork: architecture, status, to-dos, done log
FIRMWARE.md MG4 firmware generations and how the app reads each
AGENTS.md Context for AI agents working in this repository
DESIGN.md The EVSuite design system (the app overrides its palette)
SECURITY.md Threat model
DISCLAIMER.md Vehicle-safety disclaimer; read before installing
CHANGELOG.md Upstream release history
LICENSE.md Licences: MIT, and PolyForm Noncommercial for EVHardware/

Credits

  • fatihdonmezdev: FatihsMG4, the app this fork starts from.
  • malys: EVABRPUploader.
  • Leon Kernan: the original ABRP_Uploader, the car-API approach and the first working uploader.
  • ABRP telemetry API: Iternio.
S
Description
No description provided
Readme MIT
4.1 MiB
Languages
Kotlin 64.2%
Java 27.3%
Go 7.6%
Nix 0.8%
Shell 0.1%