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:
@@ -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.
|
||||
|
||||
[](https://github.com/fatihdonmezdev/FatihsMG4/releases)
|
||||
[](LICENSE)
|
||||
No OBD dongle and no phone in the loop: the app runs on the head unit and reads the car's own
|
||||
APIs.
|
||||
|
||||
Sends live telemetry from an **MG4 (SAIC eh32)** to
|
||||
[A Better Route Planner](https://abetterrouteplanner.com) — battery level, speed, range,
|
||||
temperature, charging state and position — straight from the car's own APIs.
|
||||
|
||||
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.
|
||||
- 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
|
||||
> **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; no official origin
|
||||
> or approval is claimed.
|
||||
> MG and MG4 are third-party marks used only to identify compatibility.
|
||||
|
||||
> **Fork notice.** This is a fork of Leon Kernan's `ABRP_Uploader`, substantially
|
||||
> reworked. **Read [`LICENSE.md`](LICENSE.md) before redistributing anything** — the
|
||||
> upstream project carries no licence, which limits what may legally be published.
|
||||
> **Licence.** The original `ABRP_Uploader` carries no licence, which limits what may legally
|
||||
> be published. Read [`LICENSE.md`](LICENSE.md) before redistributing anything.
|
||||
|
||||
---
|
||||
|
||||
## Contents
|
||||
|
||||
- [Screenshots](#screenshots)
|
||||
- [Overview](#overview)
|
||||
- [Install](#install)
|
||||
- [Configuration](#configuration)
|
||||
- [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)
|
||||
- [Security](#security)
|
||||
- [Contributing](#contributing)
|
||||
- [Legal](#legal)
|
||||
- [Credits](#credits)
|
||||
|
||||
## Screenshots
|
||||
## Screenshot
|
||||
|
||||
Version 2.2.6 introduces an OLED-black interface with charcoal cards, muted gold and
|
||||
cyan accents, a vehicle overview, and responsive side or bottom navigation. This
|
||||
app-specific theme intentionally overrides the shared EVSuite visual tokens.
|
||||
Tablet UI previews at 1280×720; vehicle readings are unavailable on this device.
|
||||
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/evabrp-2.2.6-overview.png" width="760" alt="OLED vehicle overview with energy and tyre panels above compact metrics">
|
||||
<img src="screenshots/evabrp-2.2.6-abrp.png" width="760" alt="Dark ABRP account configuration">
|
||||
<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
|
||||
|
||||
## Overview
|
||||
| Signal | Source | Firmwares |
|
||||
|---|---|---|
|
||||
| State of charge | `EVHardware` vendor charging service, with a SWI68 property fallback | all supported (fallback SWI68 only) |
|
||||
| Range | `EVHardware` vendor charging service, standard AAOS property, then SWI68 fallback | all supported |
|
||||
| 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 | — |
|
||||
```
|
||||
car ──live telemetry───▶ car.k8s.stinnesbeck.com (Go) ──▶ ABRP
|
||||
──daily chunks─────▶ │
|
||||
──charging sessions▶ ▼
|
||||
InfluxDB 2 ◀── Grafana
|
||||
```
|
||||
|
||||
All vehicle telemetry goes through **EVHardware**, which detects the generation
|
||||
(SWI68/69/131/132/133/165), owns property ids and units, and applies the shared nullable
|
||||
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).
|
||||
The ABRP credentials live only on the server, never in the APK. The car authenticates with a
|
||||
token of its own, obtained by pairing with a phone (below).
|
||||
|
||||
Odometer and tyre pressures sit behind privileged AAOS permissions: a platform-signed
|
||||
install sends them, any other install simply omits them.
|
||||
## The app
|
||||
|
||||
A property the car will not give up is **omitted** from the payload, never sent as zero —
|
||||
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
|
||||
interval)` is dropped rather than resent, so a stale fix cannot report the car back at the
|
||||
place it set off from.
|
||||
| Tab | What it shows |
|
||||
|---|---|
|
||||
| **Overview** | Battery, range, range estimates at current power and at the 5-minute average, power, speed; odometer, climate setpoint and tyre pressures |
|
||||
| **Consumption** | Daily and period consumption, efficiency |
|
||||
| **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
|
||||
is no window service, door listener or window command path in the app.
|
||||
- **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 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
|
||||
The MG4 head unit hides Settings and APK install. The known route in:
|
||||
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:
|
||||
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).
|
||||
|
||||
1. Open any app with a text field and tap it to raise the on-screen keyboard — e.g. the
|
||||
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.
|
||||
## Install on the car
|
||||
|
||||
> ⚠️ You are enabling developer options and sideloading on a car. Do this **parked**, and
|
||||
> only with an APK you trust. See [DISCLAIMER.md](DISCLAIMER.md).
|
||||
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.
|
||||
|
||||
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 |
|
||||
|---|---|---|
|
||||
| **Stable** | No. Contains no updater at all. | You want the car to run what you put on it |
|
||||
| **Unstable** | Yes, from GitHub pre-releases | You are testing and want fixes as they land |
|
||||
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.
|
||||
|
||||
Grab the APK from [Releases](https://github.com/fatihdonmezdev/FatihsMG4/releases). Stable builds are the tagged ones; unstable builds
|
||||
are marked pre-release.
|
||||
With adb:
|
||||
|
||||
### 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.
|
||||
2. Long-press the **comma** key until the keyboard settings appear.
|
||||
3. **Android Keyboard Settings → Languages**.
|
||||
4. Search for `file` in the search box at the top.
|
||||
5. That gives you the Files app.
|
||||
Self-updating (OTA) is switched off: the updater points at an unresolvable host until it is
|
||||
pointed at this repository's releases (see `SPECS.md`).
|
||||
|
||||
From there, open your USB stick and tap the APK.
|
||||
## Setup
|
||||
|
||||
### First run
|
||||
|
||||
1. Open the app.
|
||||
2. By default, the app prefills the open-source telemetry API key published by the
|
||||
[SAIC Python MQTT Gateway](https://github.com/SAIC-iSmart-API/saic-python-mqtt-gateway#abrp-api-integration).
|
||||
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.
|
||||
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 **Upload** tab, turn live telemetry on. The service starts with the car from then on.
|
||||
|
||||
### Config file
|
||||
|
||||
To avoid typing on the head unit, put your settings in a plain text file and tap **Import file**
|
||||
on the **ABRP** tab. The app fills in whatever the file contains. Nothing is sent — press
|
||||
**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
|
||||
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.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
|
||||
scans that folder on every mounted volume and takes the first file that parses — the name does
|
||||
not matter. If nothing is found it shows the exact path to copy to. On a phone or tablet, where
|
||||
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:
|
||||
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:
|
||||
|
||||
```
|
||||
# ABRP Uploader config
|
||||
api_key = your-abrp-api-key
|
||||
token = your-abrp-user-token
|
||||
|
||||
# optional — the cadence controls, same values as the UI
|
||||
interval_sec = 60 # snapped to 15 / 30 / 60 / 120 / 300
|
||||
server_url = https://car.k8s.stinnesbeck.com
|
||||
token = the server's INGEST_TOKEN (not needed after pairing)
|
||||
installation_id = keep the old one when reinstalling, so history stays together
|
||||
interval_sec = 60 # 15 / 30 / 60 / 120 / 300
|
||||
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 —
|
||||
your credentials live only in the app's encrypted store afterwards.
|
||||
The file is read once and not kept; credentials live only in the app's encrypted store.
|
||||
|
||||
### Power
|
||||
|
||||
The app holds **no wake lock** and schedules **no alarms**, so it cannot wake a sleeping
|
||||
head unit; it costs nothing while the car is off. While the car is on, both the upload tick
|
||||
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**.
|
||||
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
|
||||
Requires JDK 17 and the Android SDK. With [mise](https://mise.jdev):
|
||||
|
||||
```bash
|
||||
mise install # JDK
|
||||
mise run bootstrap # Android SDK + local.properties
|
||||
mise run check # permission gate + lint + tests
|
||||
mise run build # debug APK
|
||||
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
|
||||
```
|
||||
|
||||
Without mise: set `JAVA_HOME` to a JDK 17, write `sdk.dir` into `local.properties`, then
|
||||
`./gradlew assembleStableDebug`.
|
||||
`build-car` writes `build/car/NilsMG4-unstable-<version>.apk` and a `.sha256` next to it, and
|
||||
checks the signing certificate before it finishes.
|
||||
|
||||
Release builds are signed from environment variables. These sign the **APK** and have
|
||||
nothing to do with your ABRP API key, which you type into the app itself:
|
||||
Tests and lint:
|
||||
|
||||
| Variable | What it is |
|
||||
|---|---|
|
||||
| `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 |
|
||||
```sh
|
||||
./gradlew testStableDebugUnitTest testUnstableDebugUnitTest lintStableDebug
|
||||
```
|
||||
|
||||
**Never commit a keystore or a password.** Put them in `mise.local.toml`, which is
|
||||
gitignored, or in GitHub Actions secrets.
|
||||
[`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; 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
|
||||
|
||||
```
|
||||
app/src/main upload service, ABRP payload and UI
|
||||
EVHardware shared vehicle reads, energy snapshot and integration
|
||||
app/src/stable no-op update hook — the stable channel cannot self-update
|
||||
app/src/unstable audited updater policy; runtime trigger currently suspended
|
||||
app/src/debug VHAL probe tools, absent from every release build
|
||||
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 |
|
||||
|---|---|
|
||||
| [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 |
|
||||
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to build, test and submit a change |
|
||||
| [SECURITY.md](SECURITY.md) | Threat model and vulnerability disclosure |
|
||||
| [DISCLAIMER.md](DISCLAIMER.md) | Vehicle-safety disclaimer — read before installing |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | Release history |
|
||||
| [LICENSE.md](LICENSE.md) | Licence text |
|
||||
|
||||
## 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.
|
||||
| [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) | Licence status |
|
||||
|
||||
## 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).
|
||||
- Sibling project: **EVProfile**, whose security and CI patterns this repo reuses.
|
||||
|
||||
Reference in New Issue
Block a user