A new Overview style: the Grid over the roads round the car from
OpenStreetMap, drawn in the floor's perspective, heading up, the bigger
roads glowing brighter, the car as a chevron.
Server: POST /v1/map/roads {z:14,x,y} fetches a tile's drivable roads
from Overpass (OVERPASS_URL; one request at a time, at least a second
apart), simplifies them to tile-local points in four classes, caches
them for 30 days and never logs the tile.
App: MapRoads keeps the 3x3 tiles round the car from its own cache or
the server; GPS only while the style is shown; bearing from the GPS or
from successive positions. Without GPS, server or roads the plain floor
says why. OSM attribution on screen and in LICENSE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
147 lines
12 KiB
Markdown
147 lines
12 KiB
Markdown
# 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.27 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, and serves the roads for the Grid's map from
|
||
OpenStreetMap (Overpass). 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 drive mode is the one exception.** The app writes no vehicle
|
||
setting except the drive mode (owner's decision, 2026-10-25): a tap on Settings, or — with
|
||
"Set at start" on — once when the head unit starts, the mode last chosen there and confirmed
|
||
by the car, and only before the car first leaves P. Every write only in P at 0 km/h with both
|
||
read fresh (unreadable means no write), through EVHardware's standstill gate; the result
|
||
shown is what the car reports back (`DriveModeWrite`, `DriveModeSetter`). Nothing else is
|
||
written. 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`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
||
| 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 | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal, Grid (and Grid with map, sharing Grid's choice) and Visor add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About |
|
||
| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), 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
|
||
|
||
```sh
|
||
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 |
|
||
| `THEMES.md` | the Overview styles: two screenshots each (`screenshots/themes/`, sample values) and what inspired them |
|
||
| `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 |
|