Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
267 lines
12 KiB
Markdown
267 lines
12 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
|
||
|
||
- [Screenshots](#screenshots)
|
||
- [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)
|
||
|
||
## Screenshots
|
||
|
||
Version 2.18.1 in the car's app window (1778×640 dp next to the MG4's own sidebar), full screen,
|
||
on the Android 9 emulator. There is no vehicle there, so every value shows as a dash. Every tab
|
||
fits the window without scrolling.
|
||
|
||
<p align="center">
|
||
<img src="screenshots/nilsmg4-2.18.1-overview.png" width="900" alt="Overview tab: battery, range, power and speed tiles with since-start and 5-minute averages, vehicle info and tyre pressures">
|
||
</p>
|
||
|
||
<table>
|
||
<tr>
|
||
<td><img src="screenshots/nilsmg4-2.18.1-consumption.png" width="440" alt="Consumption tab: period totals beside the daily log"></td>
|
||
<td><img src="screenshots/nilsmg4-2.18.1-charging.png" width="440" alt="Charging tab: live charging values beside the SOC/power curve"></td>
|
||
</tr>
|
||
<tr>
|
||
<td><img src="screenshots/nilsmg4-2.18.1-live-data.png" width="440" alt="Live data tab: on/off, ABRP credentials, upload frequency"></td>
|
||
<td><img src="screenshots/nilsmg4-2.18.1-server.png" width="440" alt="Server tab: URL, pairing, ingest token, installation ID"></td>
|
||
</tr>
|
||
<tr>
|
||
<td><img src="screenshots/nilsmg4-2.18.1-settings.png" width="440" alt="Settings tab: display, battery, charging price, phone entry, app updates"></td>
|
||
<td><img src="screenshots/nilsmg4-2.18.1-logs.png" width="440" alt="Logs tab: recent uploads"></td>
|
||
</tr>
|
||
</table>
|
||
|
||
## 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, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, climate setpoint) and tyre pressures. Read every half second |
|
||
| **Consumption** | Lifetime, last 7 and 30 days, Trip A and B (distance, energy, average consumption and speed, SOC change), and a daily log of the last four months |
|
||
| **Charging** | The current session (SOC, voltage, current, power, energy into the battery and from the grid, time, cost) and its SOC/power curve |
|
||
| **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 (manual SoH, usable capacity), charging price, entry on the phone, version, update check, install from USB stick, 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`](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, two ways (details in [`OTA.md`](OTA.md)):
|
||
|
||
- **Over the air:** publish a build with `publish-car`, then press **Check for updates** on the
|
||
Settings tab. The car fetches it through your car server, which relays the forge's release, so
|
||
the car never holds a forge credential.
|
||
- **From a USB stick:** copy `NilsMG4-unstable-<version>.apk` and its `.sha256` from `build/car/`
|
||
to the stick's top folder and press **Install from USB stick** on the Settings tab.
|
||
|
||
Either way the app checks the SHA-256, the package and the signature before installing.
|
||
|
||
## 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 → On your phone) 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
|
||
```
|
||
|
||
A head-unit emulator for checking layouts and installs: `nix run .#car-emulator` (add
|
||
`-no-window` for headless) starts Android 9 at 1920×720 and 160 dpi. It is signed with the same
|
||
platform key as the car, so the car build installs there as the system user;
|
||
`adb shell wm size 1778x640` gives the car's app window. It has no vehicle, so values stay "—".
|
||
|
||
[`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, settings entered on a phone, and the release relay for the
|
||
in-app updater. 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 (via the car server) and USB install
|
||
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, build-car, publish-car, car-emulator
|
||
```
|
||
|
||
## Project documents
|
||
|
||
| Document | What it covers |
|
||
|---|---|
|
||
| [SPECS.md](SPECS.md) | Journal of this fork: architecture, status, to-dos, done log |
|
||
| [OTA.md](OTA.md) | Publishing releases, the updater's checks, USB install |
|
||
| [FIRMWARE.md](FIRMWARE.md) | MG4 firmware generations and how the app reads each |
|
||
| [AGENTS.md](AGENTS.md) | Overview of the repository for AI agents and new contributors |
|
||
| [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).
|