The MG4's display is given as 1920x720 (this project), 1280x480 (EVProfile's README) and a 1778x720 app window (upstream). 1280x480 is exactly 1920x720 px at 240 dpi expressed in dp, so the figures may describe one panel at different densities, which this platform lets be changed per app. The Logs tab's new SCREEN line shows the panel's pixels and density and the app window in px and dp, to settle it from the car. FIRMWARE.md lists the three figures and what the emulator shows at 240 dpi; mise.toml, README and SPECS.md no longer state 160 dpi as fact for the car. 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 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).
|
|
|
|
<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).
|