Files
mg4/AGENTS.md
T
nilsandClaude Opus 5.5 1c85fd2d9f App 2.27.2: Terminal in the accent colour; Original as a colour choice
Retro and Terminal are one entry each; the accent colour drop-down
offers "Original" first for them, remembered per style. The old
"Retro (accent colour)" entry is folded in and its stored value
migrated. Emulator only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-10-24 21:38:01 +02:00

9.7 KiB
Raw Blame History

AGENTS.md — Nils' MG4

Start here. What this repository is, how its parts fit, how to build, test and ship, and the rules for changing it. Details live in the documents linked below; SPECS.md is the journal (status, to-dos, a dated log of every change) and is updated with each change.

What this is

An app for the MG4 (SAIC eh32) head unit plus the backend it talks to:

MG4 head unit (Android 9, AAOS, MT2712)
  app/  "Nils' MG4" ──live data, consumption, charging──▶ server/ (Go) ──▶ InfluxDB 2 ◀── Grafana
        │                                                   │ relays app releases from the forge
        └──live data, straight, with the driver's own key──▶ ABRP (A Better Route Planner)
  • app/ — Android app (Java, minSdk 27, runs on API 28). Reads the car through the EVHardware/ library, shows it (Overview, Consumption, Charging), sends live data to the server and to ABRP, and updates itself.
  • server/ — Go 1.26 API at https://car.k8s.stinnesbeck.com: stores telemetry, consumption and charging in InfluxDB, pairs cars by QR code, carries settings typed on a phone, relays app releases from the private forge. It does not talk to ABRP. See server/README.md.
  • k8s/ — plain Kubernetes manifests (InfluxDB, server, Grafana with three provisioned dashboards, Traefik routes). See k8s/README.md.
  • EVHardware/ — vendored shared vehicle layer (PolyForm Noncommercial, see LICENSE.md).

Repository: ssh://git@git.stinnesbeck.com:2222/nils/mg4.git (remote origin), branch main. Upstream fork: fatih → github.com/fatihdonmezdev/FatihsMG4.

Rules

  1. Nothing touches the Kubernetes cluster. Write and validate manifests only; the owner applies them. No kubectl against the cluster, not even read-only, unless asked.
  2. Secrets. Filled-in values only in k8s/secrets/secrets.yaml (gitignored); secrets.example.yaml holds CHANGE-ME placeholders only. Stage files explicitly, check the index for changes that are not yours (the owner works in parallel), and scan the staged diff for secrets before every commit. Never print a token.
  3. Telemetry is read-only. The app never writes a vehicle setting. The only system setting it touches is policy_control, and only to remove the CarPlay entry the withdrawn full-screen experiment (2.19.2–2.19.5) may have left.
  4. A failed read is not a zero. Unreadable values are null, omitted from payloads and shown as "—". Sending 0 for SOC tells ABRP the battery is empty.
  5. Fail closed. Updates (origin, SHA-256, package, signature), credentials, unknown state.
  6. No credential in a URL, a log line or a crash report. Tokens go in headers or bodies.
  7. The head unit must stay stable. Every car call is reflection with try/catch; no blocking car reads on the main thread.
  8. Power. No wake locks or alarms; GPS pauses while parked and idle.
  9. Honesty. Most behaviour cannot be verified off the car. Say what was tested (unit tests, emulator, podman) and what was not. "Not tried on the car" is an acceptable answer.

The car (confirmed on the owner's car, firmware SWI68-29958-1300R69)

  • Display 1920×720 px at 160 dpi; the app window is 1778×720 px (the car's own sidebar), about 1778×640 dp for layouts. Pages must fit that without scrolling where possible.
  • Signed with the public AOSP test platform key; the car build runs as android.uid.system.
  • Gear codes (EVHardware.getGearRaw): 1 P, 2 R, 3 N, 4 D (read off the lever).
  • Steering-wheel keys arrive as the broadcast com.saic.keyevent.hardkey.report (android.intent.extra.hardkey.keycode / .down / .longpress); the voice key is 287.
  • Not published by the car: battery, cabin and outside temperature, pack energy and capacity, paint colour (only ro.build.mtk2712.vehicle_type = AS28). See SPECS.md, FIRMWARE.md.
  • No third-party apps in the MG launcher: the app opens by a double press of the voice key.
  • Its internet comes from the car itself (no Wi-Fi needed).
  • CarPlay is Allgo's com.allgo.carplay.service (/system/app/AllgoCarplay_EH32). Its picture is hard-coded at 1920×640, 80 px below the top (constants in its code and layout), so it cannot be made full screen from an app. Installs from an app's own session are refused ("Not allowed to install non-system apps on internal storage"): updates end on the installer screen.

The app

Tab Contents
Overview value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (GaugeView needles), Cockpit (RingView rings, TraceView power trace), Retro (RetroView, seven-segment VFD), Futuristic (FlowView, animated energy flow), Sci-fi (SciFiView, starship console), Cartoon (CartoonView), Blueprint (BlueprintView, top-view drawing with tyre pressures), MG (OctagonView, octagonal dials after the classic MG dashboards), and on ClusterCanvasView: Minimal, Terminal, Aviation, Nixie tubes, Chronograph; the drawn styles share ClusterValues. A button in the top-right corner hides the app's navigation for a full-width Overview (PREF_OVERVIEW_FULLSCREEN)
Consumption / Charging period totals, daily log, charging sessions and curve
Live data on/off, ABRP API key and user token, upload frequency
Server URL, pairing by QR, ingest token, installation ID (read-only), config import
Settings full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, OverviewStyle.usesAccent; Retro and Terminal add "Original" for their own palette, hasOriginal), tyre unit, battery, charging price, version, update check, install from USB, About
Logs update log (every install step, with the platform's words), upload log

Key classes (app/src/main/java/com/evsuite/abrp/): AbrpUploadService (foreground service, 15 s tick), TelemetryUploader (+ TelemetryQueue, persisted), TelemetryPayload (server limits), AbrpApi/AbrpConfig, ServerHttp/ServerConfig/PairingClient/ PhoneSettingsClient, ConsumptionTracker/ConsumptionCloudClient, ChargeSessionTracker, RangeEstimator, SecurePrefs, ConfigImport, SteeringKeyListener, MainActivity (UI). Updater (car build only): app/src/unstable/ OtaUpdater, UpdateHook, UsbUpdate.

Put logic in pure, unit-tested classes (no Android types); the service and activity are hard to test without a car.

UI conventions: OLED black, glass cards, an accent the user picks from the MG4 paints — always ?attr/evAccent… (res/values/attrs_accent.xml, overlays in accents.xml), never a hard-coded accent hex. All UI text in strings.xml. 72 dp touch targets. Values right-aligned with tabular digits. Drop-downs are Material exposed dropdown menus and dialogs go through showKeepingFullScreen — a Spinner or a plain dialog brings the car's bars back. Android 9 crashes on gradients with theme colours: use layers.

Flavours: stable (no updater at all) and unstable (the car build: system UID, OTA and USB install). The car always gets unstable.

Build, test, ship

nix develop                                   # JDK 21, Android SDK, Go
./gradlew testStableDebugUnitTest testUnstableDebugUnitTest lintStableDebug
build-car                                     # build/car/NilsMG4-unstable-<version>.apk + .sha256
publish-car                                   # pre-release car-<version> on the forge (needs a token)
cd server && go vet ./... && go test ./... && govulncheck ./...
  • Versions: three-part versionName in app/build.gradle; raise versionCode by one with every build that goes to the car (a lower code is refused as a downgrade).
  • Getting it to the car: in-app update (Settings → Check for updates, via the server), or Install from USB stick (APK + .sha256 in the stick's top folder), or the first time through the Bluetooth-settings file manager (README.md). OTA.md has the whole flow.
  • Server image: podman build in server/, push to git.stinnesbeck.com/nils/mg4-server:<v>, pin the pushed digest in k8s/server.yaml. Validate manifests offline with kustomize build k8s | kubeconform (see k8s/README.md).
  • Emulator: nix run .#car-emulator (add -no-window for headless) starts an Android 9 AOSP emulator at 1920×720, 160 dpi on port 5560 (state in build/emulator/). Its image shares the car's platform key, so the car build installs as the system user. adb shell wm size 1778x640 gives the car's app window (taps then use those coordinates; screenshots stay 1920×720). It has no car data (every value is "—"). While the app is open, uiautomator dump never sees an idle screen; use screenshots. A USB stick: a FAT image (mkfs.vfat, files in with mcopy) passed as -sdcard <image> mounts as a removable volume like the car's stick; adb push to /storage/<id>/ changes it while running.
  • After each change: a SPECS.md done-log entry, docs that the change affects, explicit staging, commit, push.

Documents

File What
SPECS.md journal: architecture, status, signals the car lacks, to-dos, dated done-log
README.md user-facing overview, install, setup, config file
OTA.md releases, the updater's checks, USB install
FIRMWARE.md the head unit, firmware generations, display
server/README.md, k8s/README.md API, configuration, deployment
LICENSE.md MIT, EVHardware's PolyForm Noncommercial, the fork chain
DESIGN.md, CHANGELOG.md upstream's design system and release history