README for Nils' MG4

Rewritten for this fork: the car server, InfluxDB and Grafana setup,
pairing by QR code, the app's tabs, voice-key opening and full screen,
installing the platform-signed car build, the config file under the new
package ID, building with Nix, and credits. Adds a current screenshot
(2.8.2 at 1920x720, emulator).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
2026-10-18 00:08:14 +02:00
co-authored by Claude Opus 5.5
parent 1b918e3c39
commit 4b858e3c35
2 changed files with 140 additions and 187 deletions
+140 -187
View File
@@ -1,261 +1,214 @@
# FatihsMG4 # Nils' MG4
<p align="center"><img src="docs/logo.svg" width="440" alt="FatihsMG4"></p> 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,
forwards live data to [A Better Route Planner](https://abetterrouteplanner.com) and feeds the
Grafana dashboards.
[![Release](https://img.shields.io/github/v/release/fatihdonmezdev/FatihsMG4?include_prereleases&sort=semver)](https://github.com/fatihdonmezdev/FatihsMG4/releases) No OBD dongle and no phone in the loop: the app runs on the head unit and reads the car's own
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) APIs.
Sends live telemetry from an **MG4 (SAIC eh32)** to - Repository: https://git.stinnesbeck.com/nils/mg4
[A Better Route Planner](https://abetterrouteplanner.com) — battery level, speed, range, - Fork of [fatihdonmezdev/FatihsMG4](https://github.com/fatihdonmezdev/FatihsMG4), itself built on malys' EVABRPUploader and Leon Kernan's `ABRP_Uploader`.
temperature, charging state and position — straight from the car's own APIs. - What changed and what is still to do: [`SPECS.md`](SPECS.md).
No OBD dongle. No Home Assistant. No phone in the loop. The app runs on the car's head
unit and talks to ABRP directly.
> ⚠️ **No warranty, no liability.** This software is provided "as is" and runs on a > ⚠️ **No warranty, no liability.** This software is provided "as is" and runs on a
> **vehicle**. Installing it is your decision and your risk — see > **vehicle**. Installing it is your decision and your risk; see
> [`DISCLAIMER.md`](DISCLAIMER.md). Not affiliated with SAIC, MG Motor or Iternio. > [`DISCLAIMER.md`](DISCLAIMER.md). Not affiliated with SAIC, MG Motor or Iternio.
> MG and MG4 are third-party marks used only to identify compatibility; no official origin > MG and MG4 are third-party marks used only to identify compatibility.
> or approval is claimed.
> **Fork notice.** This is a fork of Leon Kernan's `ABRP_Uploader`, substantially > **Licence.** The original `ABRP_Uploader` carries no licence, which limits what may legally
> reworked. **Read [`LICENSE.md`](LICENSE.md) before redistributing anything** — the > be published. Read [`LICENSE.md`](LICENSE.md) before redistributing anything.
> upstream project carries no licence, which limits what may legally be published.
--- ---
## Contents ## Contents
- [Screenshots](#screenshots) - [Screenshot](#screenshot)
- [Overview](#overview) - [How it fits together](#how-it-fits-together)
- [Install](#install) - [The app](#the-app)
- [Configuration](#configuration) - [Install on the car](#install-on-the-car)
- [Setup](#setup)
- [Building](#building) - [Building](#building)
- [Server and dashboards](#server-and-dashboards)
- [Project layout](#project-layout) - [Project layout](#project-layout)
- [Project documents](#project-documents) - [Project documents](#project-documents)
- [Security](#security)
- [Contributing](#contributing)
- [Legal](#legal)
- [Credits](#credits) - [Credits](#credits)
## Screenshots ## Screenshot
Version 2.2.6 introduces an OLED-black interface with charcoal cards, muted gold and Version 2.8.2 on the head unit's 1920x720 screen, in full screen (Android 9 emulator; no
cyan accents, a vehicle overview, and responsive side or bottom navigation. This vehicle there, so every value shows as a dash).
app-specific theme intentionally overrides the shared EVSuite visual tokens.
Tablet UI previews at 1280×720; vehicle readings are unavailable on this device.
<p align="center"> <p align="center">
<img src="screenshots/evabrp-2.2.6-overview.png" width="760" alt="OLED vehicle overview with energy and tyre panels above compact metrics"> <img src="screenshots/nilsmg4-2.8.2-overview.png" width="900" alt="Overview tab: battery, range, range estimates, power and speed tiles above the energy and tyre-pressure cards">
<img src="screenshots/evabrp-2.2.6-abrp.png" width="760" alt="Dark ABRP account configuration">
</p> </p>
--- ## How it fits together
## Overview ```
| Signal | Source | Firmwares | car ──live telemetry───▶ car.k8s.stinnesbeck.com (Go) ──▶ ABRP
|---|---|---| ──daily chunks─────▶ │
| State of charge | `EVHardware` vendor charging service, with a SWI68 property fallback | all supported (fallback SWI68 only) | ──charging sessions▶ ▼
| Range | `EVHardware` vendor charging service, standard AAOS property, then SWI68 fallback | all supported | InfluxDB 2 ◀── Grafana
| Speed, parked state | `EVHardware` shared energy snapshot | all supported | ```
| Outside temperature | The head unit's weather broadcast, falling back to its map-service query; the vehicle sensor first where one exists | all supported (the vehicle sensor is absent on MG4) |
| Charging, DC fast charging | Charge port state + charge rate heuristic | where the VHAL implements them |
| Charged energy this session | `EVHardware` charging-session integration | where the charge rate is readable |
| State of energy, pack capacity, pack temperature | `EVHardware` typed standard-AAOS reads | where the VHAL implements them |
| Odometer, tyre pressures | Standard AAOS properties (privileged) | platform-signed installs only |
| Cabin temperature | `EVHardware` typed HVAC read | where the VHAL implements it |
| Climate setpoint | `EVHardware` shared energy snapshot | all supported |
| Position, elevation, heading | GPS | — |
All vehicle telemetry goes through **EVHardware**, which detects the generation The ABRP credentials live only on the server, never in the APK. The car authenticates with a
(SWI68/69/131/132/133/165), owns property ids and units, and applies the shared nullable token of its own, obtained by pairing with a phone (below).
fallback order. `CarPropertyAdapter` remains only as transport for the debug-only VHAL probe
and is never instantiated by the production service. See [`FIRMWARE.md`](FIRMWARE.md).
Odometer and tyre pressures sit behind privileged AAOS permissions: a platform-signed ## The app
install sends them, any other install simply omits them.
A property the car will not give up is **omitted** from the payload, never sent as zero — | Tab | What it shows |
so ABRP is never told the battery is empty because a read failed. Position is held to the |---|---|
same rule in time as well as in value: a GPS fix older than `max(5 min, 3 x your upload | **Overview** | Battery, range, range estimates at current power and at the 5-minute average, power, speed; odometer, climate setpoint and tyre pressures |
interval)` is dropped rather than resent, so a stale fix cannot report the car back at the | **Consumption** | Daily and period consumption, efficiency |
place it set off from. | **Charging** | Charging sessions and curves, cost in euro |
| **Upload** | Live telemetry on/off, full screen, voice key, upload cadence |
| **Server** | Server URL, pairing by QR code, ingest token, installation ID, config import |
| **Wi-Fi** | Joins a network the head unit already knows (e.g. the phone hotspot) |
| **Logs** | Upload log and the last steering-wheel key reports |
Telemetry only reads the car. Version 2.2.5 removes window automation entirely; there - **Opening the app:** the MG4 launcher does not list third-party apps. **Double-press the
is no window service, door listener or window command path in the app. voice key** on the steering wheel and the app comes to the front (switch on the Upload 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 (Upload 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.
## Install Signals the MG4 does not publish (battery, cabin and outside temperature, pack energy and
The MG4 head unit hides Settings and APK install. The known route in: capacity) are listed in [`SPECS.md`](SPECS.md#signals-the-car-does-not-provide).
The MG4 head unit has no visible way to open Settings or install an APK. The known route
in (via the on-screen keyboard) is:
1. Open any app with a text field and tap it to raise the on-screen keyboard — e.g. the ## Install on the car
Amazon Music app's email/login field.
2. **Long-press** the comma `,` (or the `@`) key on the keyboard.
3. Tap **"Language settings"**.
4. Tap the **search** icon in the top bar and type **`backup`**. It opens an empty page —
now press the **back** arrow, and you land in Android's Settings panel.
5. Enable **Developer options**, and turn on **"Install unknown apps"** (unknown sources).
6. In Settings, search **`storage`** — you now have access to internal storage and the USB
key. Navigate to the APK and tap it to install.
> ⚠️ You are enabling developer options and sideloading on a car. Do this **parked**, and The car build (`NilsMG4-unstable-<version>.apk`) runs with the system UID and is signed with the
> only with an APK you trust. See [DISCLAIMER.md](DISCLAIMER.md). 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.
Two channels. Pick one — they install side by side. Without adb, through the head unit's hidden file manager:
| Channel | Auto-update | Use it if | 1. Copy the APK to a USB stick and plug it in.
|---|---|---| 2. Open **Bluetooth Settings** and select the car name.
| **Stable** | No. Contains no updater at all. | You want the car to run what you put on it | 3. Long-press the **comma** key until the keyboard settings appear.
| **Unstable** | Yes, from GitHub pre-releases | You are testing and want fixes as they land | 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.
Grab the APK from [Releases](https://github.com/fatihdonmezdev/FatihsMG4/releases). Stable builds are the tagged ones; unstable builds With adb:
are marked pre-release.
### Getting the APK onto the car ```sh
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"
```
The MG4 has no visible file manager. To reach it: > ⚠️ Do this **parked**, and only with an APK you trust. See [DISCLAIMER.md](DISCLAIMER.md).
1. Open **Bluetooth Settings** and select the car name. Self-updating (OTA) is switched off: the updater points at an unresolvable host until it is
2. Long-press the **comma** key until the keyboard settings appear. pointed at this repository's releases (see `SPECS.md`).
3. **Android Keyboard Settings → Languages**.
4. Search for `file` in the search box at the top.
5. That gives you the Files app.
From there, open your USB stick and tap the APK. ## Setup
### First run 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`.
1. Open the app. 3. Tap **Pair with phone**. Scan the QR code with a phone (or open the address shown and enter
2. By default, the app prefills the open-source telemetry API key published by the the code), and approve with the server's approval password. The car receives its token.
[SAIC Python MQTT Gateway](https://github.com/SAIC-iSmart-API/saic-python-mqtt-gateway#abrp-api-integration). 4. On the **Upload** tab, turn live telemetry on. The service starts with the car from then on.
You can replace it with your own key. Get a token from your ABRP account.
3. Paste the token, press **Test** (read-only — it does not send telemetry), then **Save**.
4. Turn the switch on. The service restarts with the car from then on.
Typing a long API key and token on the car's on-screen keyboard is painful. Instead you can
put them in a text file and tap **Import file** — see [Config file](#config-file) below.
## Configuration
| Setting | Default | Notes |
|---|---|---|
| Upload frequency | 60 s | 15 / 30 / 60 / 120 / 300 s. Applies while driving |
| More often at low battery | On | Tightens to 15 s below the threshold |
| Low battery threshold | 20 % | Editable, 1–99 |
| Service on/off | Off | Also the "stop uploading" control |
Parked and unplugged, the app drops to **one upload every 15 minutes** on its own,
whatever the setting. A state change — parked, plugged in, unplugged — always uploads
immediately.
### Config file ### Config file
To avoid typing on the head unit, put your settings in a plain text file and tap **Import file** Instead of typing, put the settings in a text file and tap **Import file** on the Server tab.
on the **ABRP** tab. The app fills in whatever the file contains. Nothing is sent — press The head unit has no file picker, so copy the file to
**Test**, then **Save** as usual.
The head unit has no file picker, so the file is not chosen by hand: copy it from a PC into
``` ```
Android/data/com.evsuite.abrp/files/ Android/data/com.stinnesbeck.nilsmg4.unstable/files/
``` ```
on the USB stick (or on the head unit's internal storage), then tap **Import file**. The app on the USB stick or the internal storage. The app scans that folder on every mounted volume and
scans that folder on every mounted volume and takes the first file that parses — the name does takes the first file that parses. Format, one `key = value` per line, `#` for comments; keys left
not matter. If nothing is found it shows the exact path to copy to. On a phone or tablet, where out keep their current value:
a document picker exists, the button falls back to letting you pick any file.
The file is a `key = value` list, one per line. Blank lines and lines starting with `#` are
ignored, keys are case-insensitive, and any key you leave out keeps its current value:
``` ```
# ABRP Uploader config server_url = https://car.k8s.stinnesbeck.com
api_key = your-abrp-api-key token = the server's INGEST_TOKEN (not needed after pairing)
token = your-abrp-user-token installation_id = keep the old one when reinstalling, so history stays together
interval_sec = 60 # 15 / 30 / 60 / 120 / 300
# optional — the cadence controls, same values as the UI
interval_sec = 60 # snapped to 15 / 30 / 60 / 120 / 300
boost_low_soc = true boost_low_soc = true
low_soc_percent = 20 # clamped to 1–99 low_soc_percent = 20
``` ```
Only `api_key` and `token` are usually needed. The file is read once on import and not kept — The file is read once and not kept; credentials live only in the app's encrypted store.
your credentials live only in the app's encrypted store afterwards.
### Power ### Power
The app holds **no wake lock** and schedules **no alarms**, so it cannot wake a sleeping The app holds **no wake lock** and schedules **no alarms**, so it cannot wake a sleeping head
head unit; it costs nothing while the car is off. While the car is on, both the upload tick unit and costs nothing while the car is off.
and the GPS subscription cost power, and the GPS interval follows your configured
frequency.
At defaults that is roughly **59 uploads/hour driving** and **3/hour parked**.
## Building ## Building
Requires JDK 17 and the Android SDK. With [mise](https://mise.jdev):
```bash With Nix:
mise install # JDK
mise run bootstrap # Android SDK + local.properties ```sh
mise run check # permission gate + lint + tests nix develop # JDK 21, Android SDK, Go
mise run build # debug APK ./gradlew assembleStableDebug # debug APK for a phone or tablet
build-car # car APK, signed with the AOSP test platform key
``` ```
Without mise: set `JAVA_HOME` to a JDK 17, write `sdk.dir` into `local.properties`, then `build-car` writes `build/car/NilsMG4-unstable-<version>.apk` and a `.sha256` next to it, and
`./gradlew assembleStableDebug`. checks the signing certificate before it finishes.
Release builds are signed from environment variables. These sign the **APK** and have Tests and lint:
nothing to do with your ABRP API key, which you type into the app itself:
| Variable | What it is | ```sh
|---|---| ./gradlew testStableDebugUnitTest testUnstableDebugUnitTest lintStableDebug
| `SIGNING_KEYSTORE` | Path to the keystore file (CI decodes `SIGNING_KEYSTORE_BASE64` into one) | ```
| `SIGNING_STORE_PASSWORD` | Opens the keystore container |
| `SIGNING_KEY_ALIAS` | Which key inside it to sign with (default `platform`) |
| `SIGNING_KEY_PASSWORD` | Opens that particular key — a keystore protects each key separately |
**Never commit a keystore or a password.** Put them in `mise.local.toml`, which is [`mise`](https://mise.jdx.dev) tasks from upstream (`mise run check`, `mise run build`) still
gitignored, or in GitHub Actions secrets. work for the plain Gradle builds.
## Server and dashboards
- **Server** ([`server/`](server/README.md)): Go, API only. Live telemetry, daily consumption
chunks, charging sessions, pairing; forwards live data to ABRP. Image at
`git.stinnesbeck.com/nils/mg4-server`.
- **Kubernetes** ([`k8s/`](k8s/README.md)): 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 ## Project layout
``` ```
app/src/main upload service, ABRP payload and UI app/src/main app: upload service, server client, pairing, UI
EVHardware shared vehicle reads, energy snapshot and integration app/src/stable no-op update hook (no updater in the APK)
app/src/stable no-op update hook — the stable channel cannot self-update app/src/unstable car build: system UID, updater (currently pointed nowhere)
app/src/unstable audited updater policy; runtime trigger currently suspended app/src/debug VHAL probe tools, absent from release builds
app/src/debug VHAL probe tools, absent from every release build
app/src/test JVM unit tests 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 ## Project documents
| Document | What it covers | | Document | What it covers |
|---|---| |---|---|
| [DESIGN.md](DESIGN.md) | The EVSuite design system — colour, type, touch targets, icons | | [SPECS.md](SPECS.md) | Journal of this fork: architecture, status, to-dos, done log |
| [FIRMWARE.md](FIRMWARE.md) | MG4 firmware generations and how the app reads each |
| [AGENTS.md](AGENTS.md) | Context for AI agents working in this repository | | [AGENTS.md](AGENTS.md) | Context for AI agents working in this repository |
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to build, test and submit a change | | [DESIGN.md](DESIGN.md) | The EVSuite design system (the app overrides its palette) |
| [SECURITY.md](SECURITY.md) | Threat model and vulnerability disclosure | | [SECURITY.md](SECURITY.md) | Threat model |
| [DISCLAIMER.md](DISCLAIMER.md) | Vehicle-safety disclaimer — read before installing | | [DISCLAIMER.md](DISCLAIMER.md) | Vehicle-safety disclaimer; read before installing |
| [CHANGELOG.md](CHANGELOG.md) | Release history | | [CHANGELOG.md](CHANGELOG.md) | Upstream release history |
| [LICENSE.md](LICENSE.md) | Licence text | | [LICENSE.md](LICENSE.md) | Licence status |
## Security
See [SECURITY.md](SECURITY.md) for the threat model and how to report a vulnerability
privately.
## Contributing
See [`CONTRIBUTING.md`](CONTRIBUTING.md). Short version: this code runs on a moving
vehicle, so changes need tests and a clear account of what you verified on a car and what
you did not.
## Legal
- [`DISCLAIMER.md`](DISCLAIMER.md) — no warranty, no liability, not affiliated with the
carmaker or with ABRP.
- [`LICENSE.md`](LICENSE.md) — unresolved licence status inherited from the fork source.
Read it before redistributing.
- [`SECURITY.md`](SECURITY.md) — how to report a vulnerability privately.
## Credits ## Credits
- Original app: **Leon Kernan** — the car-API approach and the first working uploader.
- **fatihdonmezdev**: [FatihsMG4](https://github.com/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](https://documenter.getpostman.com/view/7396339/SWTK5a8w). - ABRP telemetry API: [Iternio](https://documenter.getpostman.com/view/7396339/SWTK5a8w).
- Sibling project: **EVProfile**, whose security and CI patterns this repo reuses.
Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB