Leon Kernan's ABRP_Uploader has carried the MIT Licence since 2026-07-29 (lkernan/ABRP_Uploader commit 75d8f2bc), so the inherited "no licence upstream, status unresolved" notes no longer hold. - LICENSE: MIT, now naming every copyright holder in the fork chain (Leon Kernan, EVABRPUploader contributors, FatihsMG4 contributors, Nils Stinnesbeck). - LICENSE.md: rewritten. The repository is MIT except EVHardware/, which is PolyForm Noncommercial 1.0.0; the app links it into the APK, so the built app is non-commercial. EVHardware's own caveat about EVProfile stays with it. - README, AGENTS.md, CONTRIBUTING.md and SPECS.md say the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
238 lines
10 KiB
Markdown
238 lines
10 KiB
Markdown
# 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](https://abetterrouteplanner.com).
|
|
|
|
No OBD dongle and no phone in the loop: the app runs on the head unit and reads the car's own
|
|
APIs.
|
|
|
|
- Repository: https://git.stinnesbeck.com/nils/mg4
|
|
- Fork of [fatihdonmezdev/FatihsMG4](https://github.com/fatihdonmezdev/FatihsMG4), itself built on malys' EVABRPUploader and Leon Kernan's `ABRP_Uploader`.
|
|
- What changed and what is still to do: [`SPECS.md`](SPECS.md).
|
|
|
|
> ⚠️ **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`](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`](LICENSE.md).
|
|
|
|
---
|
|
|
|
## Contents
|
|
|
|
- [Screenshot](#screenshot)
|
|
- [How it fits together](#how-it-fits-together)
|
|
- [The app](#the-app)
|
|
- [Install on the car](#install-on-the-car)
|
|
- [Setup](#setup)
|
|
- [Building](#building)
|
|
- [Server and dashboards](#server-and-dashboards)
|
|
- [Project layout](#project-layout)
|
|
- [Project documents](#project-documents)
|
|
- [Credits](#credits)
|
|
|
|
## Screenshot
|
|
|
|
Version 2.8.2 on the head unit's 1920x720 screen, in full screen (Android 9 emulator; no
|
|
vehicle there, so every value shows as a dash).
|
|
|
|
<p align="center">
|
|
<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">
|
|
</p>
|
|
|
|
## 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, power, speed; 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, battery (usable capacity, manual SoH), charging price, Wi-Fi |
|
|
| **Logs** | Upload log and the last steering-wheel key reports |
|
|
|
|
- **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`](SPECS.md#signals-the-car-does-not-provide).
|
|
|
|
## 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:
|
|
|
|
```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"
|
|
```
|
|
|
|
> ⚠️ Do this **parked**, and only with an APK you trust. See [DISCLAIMER.md](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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
./gradlew testStableDebugUnitTest testUnstableDebugUnitTest lintStableDebug
|
|
```
|
|
|
|
[`mise`](https://mise.jdx.dev) tasks from upstream (`mise run check`, `mise run build`) still
|
|
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. 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
|
|
|
|
```
|
|
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](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 |
|
|
| [DESIGN.md](DESIGN.md) | The EVSuite design system (the app overrides its palette) |
|
|
| [SECURITY.md](SECURITY.md) | Threat model |
|
|
| [DISCLAIMER.md](DISCLAIMER.md) | Vehicle-safety disclaimer; read before installing |
|
|
| [CHANGELOG.md](CHANGELOG.md) | Upstream release history |
|
|
| [LICENSE.md](LICENSE.md) | Licences: MIT, and PolyForm Noncommercial for `EVHardware/` |
|
|
|
|
## Credits
|
|
|
|
- **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).
|