diff --git a/README.md b/README.md index d723b58..6b1084f 100644 --- a/README.md +++ b/README.md @@ -1,261 +1,214 @@ -# FatihsMG4 +# Nils' MG4 -

FatihsMG4

+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) -[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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).

- OLED vehicle overview with energy and tyre panels above compact metrics - Dark ABRP account configuration + Overview tab: battery, range, range estimates, power and speed tiles above the energy and tyre-pressure cards

---- +## 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-.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-.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-.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. diff --git a/screenshots/nilsmg4-2.8.2-overview.png b/screenshots/nilsmg4-2.8.2-overview.png new file mode 100644 index 0000000..1bfb9bd Binary files /dev/null and b/screenshots/nilsmg4-2.8.2-overview.png differ