The car refuses the installer session, and only a refusal thrown at once opened the installer screen; one that came back through the session's result ended in a toast, so the update downloaded and then nothing seemed to happen. Now any result but success or the driver's cancel opens the installer screen with the verified APK, as does a confirmation that cannot be shown, and a session silent for 20 s is abandoned for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
921 lines
78 KiB
Markdown
921 lines
78 KiB
Markdown
# SPECS — Nils' MG4
|
||
|
||
Journal for this fork of [fatihdonmezdev/FatihsMG4](https://github.com/fatihdonmezdev/FatihsMG4):
|
||
what has been changed, and what is still to do.
|
||
|
||
- Our repository: https://git.stinnesbeck.com/nils/mg4
|
||
- Upstream: https://github.com/fatihdonmezdev/FatihsMG4 (itself a fork of malys / EVABRPUploader
|
||
and Leon Kernan / ABRP_Uploader). Credited in the app's About dialog.
|
||
|
||
## Architecture: car server at car.k8s.stinnesbeck.com
|
||
|
||
The car sends everything to our own server, which stores it in InfluxDB 2, and its live data
|
||
straight to ABRP with the ABRP credentials kept (encrypted) in the app. The server does not talk
|
||
to ABRP. Grafana reads InfluxDB directly for the dashboards.
|
||
|
||
```
|
||
car ──live telemetry───▶ car.k8s.stinnesbeck.com (Go)
|
||
│ ──daily chunks─────▶ │
|
||
│ ──charging sessions▶ ▼
|
||
│ InfluxDB 2 ◀── Grafana (car-grafana.k8s.stinnesbeck.com)
|
||
└──live telemetry─────▶ ABRP api.iternio.com/1/tlm/send
|
||
```
|
||
|
||
- **Server:** `server/`, Go 1.26, API only — see `server/README.md` for endpoints, configuration
|
||
and the storage layout. Replaced the Next.js app from upstream (`web/`, removed 2026-10-17).
|
||
- **Dashboards:** Grafana, provisioned from `k8s/grafana/`, reading with a read-only token: Live
|
||
(stats, SoC/range, power/speed, route map, temperatures, tyres), Consumption (period totals, lifetime, SoH, per-day energy,
|
||
distance and efficiency) and Charging (totals, session table, curves). Prices in euro.
|
||
- **Pairing:** the car shows a QR code, a phone approves it with the approval password, and the car
|
||
gets its own token, bound to its installation id (RFC 8628-style; `server/README.md`). The
|
||
shared `INGEST_TOKEN` still works. The same QR flow carries settings typed on the phone.
|
||
- **Car app:** tabs Overview, Consumption, Charging, Live data (on/off, ABRP credentials,
|
||
frequency), Server (URL, pairing, token, installation id, config import), Settings (display,
|
||
voice key, tyre unit, accent colour, battery, charging price, Wi-Fi), Logs. Live telemetry every
|
||
15 s while driving or charging, to the server and straight to ABRP; consumption chunks and
|
||
charging sessions to the server.
|
||
- **Deployment:** `k8s/`, plain manifests. **Nothing is ever applied to the cluster by us** —
|
||
Nils applies them (`k8s/README.md`). TLS comes from Traefik's default certificate.
|
||
|
||
### Status
|
||
|
||
Current: app 2.38.0 (installed on the car over the air, 2026-10-26), server 2.6.2 (pinned by digest in
|
||
`k8s/server.yaml`; applied and running since 2026-10-25).
|
||
|
||
- [x] On the car: QR pairing, values on the Overview, voice-key double press, full screen
|
||
without black bands.
|
||
- [x] Go server: tests and govulncheck clean; end to end in podman against InfluxDB 2.7.12
|
||
(telemetry, consumption, charging, pairing, settings from the phone) and Grafana 13.2.3 (all
|
||
dashboard queries return data).
|
||
- [x] Manifests validated offline (kubeconform, with Traefik CRDs). **Applied by Nils only.**
|
||
- [x] Grafana's host in `k8s/ingress.yaml` is `car-grafana.k8s.stinnesbeck.com`, the one in use
|
||
(owner, 2026-10-25).
|
||
- [x] Screen on the car: 1920×720 px at 160 dpi, app window 1778×720 px (Logs → SCREEN).
|
||
- [x] Car-colour probe on the car: only `ro.build.mtk2712.vehicle_type = AS28`; the paint colour
|
||
is not in the system properties. It may be in the VHAL (a debug-build VhalProbe listing
|
||
would tell); until then the accent is chosen by hand.
|
||
- [x] On the car (owner, confirmed 2026-10-25): live data straight to ABRP (2.11), Enter on
|
||
phone (2.12), accent colours (2.14), right-aligned values (2.14.1).
|
||
|
||
### Signals the car does not provide
|
||
|
||
Checked 2026-10-17 against what actually reached InfluxDB (`schema.measurementFieldKeys`). The car
|
||
sends soc, est_battery_range, odometer, power, voltage, current, speed, hvac_setpoint, the four
|
||
tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does **not** send:
|
||
|
||
- **Battery temperature:** read from `EV_BATTERY_AVERAGE_TEMPERATURE`, an Android 14 property; the
|
||
head unit runs Android 9, and the SAIC services offer none.
|
||
- **Cabin temperature:** `HVAC_TEMPERATURE_CURRENT` is not published; only the setpoint is.
|
||
- **Outside temperature:** not published by the vehicle (upstream noted the same). Upstream used
|
||
the head unit's weather service as a stand-in (`HeadUnitWeather`, removed in `ea8032a`);
|
||
restoring it was considered and declined.
|
||
- **Pack energy (`soe`) and capacity:** not published. The Live dashboard computes energy in pack
|
||
as SOC × usable capacity, from the hidden dashboard constant `usableKwh` (50.8 kWh for the
|
||
51 kWh battery) in `k8s/grafana/dashboards/live.json`.
|
||
- **Adaptive cruise control (set speed, distance to the car ahead, ACC state):** the head unit gets
|
||
none of it (SWI68, checked 2026-10-25). The `AAD_*` properties of `CarAdvancedAssistedDrivingManager`
|
||
exist and answer, but stay at their defaults with ACC engaged behind another car
|
||
(`AAD_EPTCCTRGTSPD` `0x2160A15B` 0.0, `AAD_ACC_OBJ_*` 0.0 / -128.0, modes 0). A signal recording
|
||
of all 1,194 values while the set speed was 101, 90 and 80 km/h shows none of those numbers
|
||
(also tried as m/s, mph and ×2/×10/×16/×64) outside the actual speed, and the ACC buttons send
|
||
no key broadcast. Most likely only the instrument cluster has it.
|
||
|
||
## To do
|
||
|
||
- [ ] **Grafana high availability needs another database.** Grafana keeps its own state (users,
|
||
sessions, preferences, provisioned-dashboard records, alerting state) in SQLite at
|
||
`/var/lib/grafana/grafana.db` on the `grafana-data` PVC. That is fine for one replica, and is
|
||
why `k8s/grafana.yaml` uses `replicas: 1` and the `Recreate` strategy: only one Grafana may
|
||
write the file. To run more than one replica, first move Grafana to PostgreSQL or MySQL
|
||
(`GF_DATABASE_TYPE`, `GF_DATABASE_HOST`, …); the replicas then share that database instead
|
||
of a volume. The car data itself is in InfluxDB and unaffected.
|
||
- [x] **EVHardware's comments are partly French.** Translated 2026-10-25 (see the done log).
|
||
- [ ] **Idea: climate energy on the Overview (motor vs climate).** Nothing the app reads splits
|
||
energy by consumer: battery power (voltage × current) is the total of motor, climate, 12 V
|
||
and the rest. SAIC's charging service, which EVHardware already binds, has counters that
|
||
might: `TotalConsumption`, `TotalRegen` and `AccConsumption`, each since start and since the
|
||
last charge (`SaicCharging.consumedKwhSinceStart()`, `regeneratedKwhSinceStart()`,
|
||
`accCounterKwhSinceStart()` and the `…SinceCharge()` twins). Whether "Acc" means
|
||
*accessory* (a measured climate/auxiliary share; motor ≈ total − acc) or *accumulated* (a
|
||
duplicate of the total) is unknown — EVHardware deliberately does not call it climate — and
|
||
whether SWI68 answers at all is untested. Plan: (1) a read-only diagnostic showing the raw
|
||
counters since start; (2) two drives, climate off and on, with a photo of the counters
|
||
after each; (3) if Acc grows only with the climate, an Overview card "Climate &
|
||
accessories since start" with the motor's share beside it — otherwise drop the diagnostic
|
||
and, at most, show a labelled estimate (power drawn at a standstill with the climate on).
|
||
|
||
## Done
|
||
|
||
- 2026-10-17 — Translated the app UI from Turkish to English.
|
||
- 2026-10-17 — Nix flake: `nix develop` provides JDK 21 and the Android SDK. `build-car` builds the
|
||
unstable APK and signs it with the AOSP test platform key, which the MG4 head unit uses
|
||
(cert SHA-256 `c8a2e9bc…192ab8`).
|
||
- 2026-10-17 — OTA update checks disabled by pointing at an unresolvable host (see To do).
|
||
- 2026-10-17 — Renamed to "Nils' MG4": package ID `com.stinnesbeck.nilsmg4` (`.unstable` for the car
|
||
build), APK names `NilsMG4-*.apk`. It installs next to the old FatihsMG4 app instead of replacing
|
||
it, and starts with empty settings and history.
|
||
- 2026-10-17 — Server rewritten for InfluxDB 2 with live telemetry and ABRP forwarding; Dockerfile;
|
||
Kubernetes manifests (not applied).
|
||
- 2026-10-17 — App 2.3.0: Server tab (URL, ingest token, installation id, config import), live
|
||
telemetry restored from before `ea8032a` and sent to the server, Upload-tab cadence controls
|
||
reconnected, dead switches removed. The removed ABRP classes' unit tests compile and pass again
|
||
(AbrpApi and HeadUnitWeather tests deleted with their classes).
|
||
- 2026-10-17 — Accent colour: MG red instead of muted gold. Web app renamed to Nils' MG4.
|
||
- 2026-10-17 — Dashboard translated to English (en-GB formatting); euro instead of lira in the
|
||
dashboard and the car app's charging tab.
|
||
- 2026-10-17 — Server rewritten in Go (API only); dashboards moved to Grafana with provisioned
|
||
InfluxDB datasource and three dashboards; the Next.js app (`web/`) removed. Per-host
|
||
cert-manager certificates dropped in favour of Traefik's default certificate.
|
||
- 2026-10-17 — Pairing by QR code: server 2.1.0 issues per-car tokens after approval on a phone;
|
||
app 2.4.0 shows the QR code and collects its token. Tested end to end against InfluxDB.
|
||
- 2026-10-17 — Incident: a commit swept the filled-in `secrets.example.yaml` (InfluxDB admin token,
|
||
INGEST_TOKEN) into git and pushed it. Commit amended and force-pushed; both values regenerated in
|
||
the now gitignored `k8s/secrets.yaml` (since moved to `k8s/secrets/secrets.yaml`, outside
|
||
`kubectl apply -f k8s/`). Filled-in secrets live only there from now on.
|
||
- 2026-10-18 — `english-translation` merged into the default branch (merge commit `f54e684`, the
|
||
summary as its message), `master` renamed to `main`, the work branch deleted. Work goes on `main`.
|
||
- 2026-10-18 — AGENTS.md rewritten as the entry point for agents: what the project is, rules,
|
||
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
||
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
||
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||
- 2026-10-27 — App 2.40.1: **an update that downloaded but never reached the installer** (owner:
|
||
"sometimes … downloads the update but then doesn't start the installer"). The car refuses the
|
||
installer session ("Not allowed to install non-system apps on internal storage"); only a
|
||
refusal thrown at once led to the installer screen. A refusal that came back through the
|
||
session's result was a toast and nothing else. Now any answer but success, the confirmation and
|
||
the driver's own cancel opens the installer screen with the verified APK (path and session id
|
||
ride on the result intent; `goAsync` for the copy), a confirmation that cannot be shown does
|
||
too, and a session silent for 20 s is abandoned for it. Emulator, with a throwaway build whose
|
||
session bytes were damaged (refused later, status 4 `INSTALL_PARSE_FAILED_NOT_APK`) and which
|
||
read the update from Downloads: the installer screen opened by itself, Install, now 2.40.1; the
|
||
update log shows every step. The 20 s watch is untested (no way to make a session stay silent).
|
||
Not tried on the car. OTA.md updated.
|
||
- 2026-10-27 — App 2.40.0 published (`car-2.40.0`).
|
||
- 2026-10-27 — App 2.40.0: **Grid with map straight from above, full height** (owner: 2.39.2
|
||
"does not look right"; wanted the map over the whole height and fully top-down). The map is
|
||
the whole background in map mode: orthographic, heading up, 1.2 layout units per metre (about
|
||
400 m ahead, 125 m behind, 450 m to each side on the car's screen), the car 150 units above
|
||
the bottom edge; ground grid every 50 m fixed to the ground; roads from 250 m ahead fainter;
|
||
the camera, the sky and the floor are gone there. The speed moves to a panel under the gears
|
||
(120 units), and the battery wall, the clock, the gears and the disc get dark panels. The
|
||
plain Grid is unchanged. Emulator with the Berlin tiles, both layouts. Not tried on the car.
|
||
- 2026-10-27 — App 2.39.2 published (`car-2.39.2`).
|
||
- 2026-10-27 — App 2.39.2: **Grid with map seen from higher up** (owner: the map looked like a
|
||
fish-eye; wanted it more top-down). The map has its own pinhole camera now: 45° down, 300 m
|
||
up, the car on its axis just above the read-outs, the top of the floor about 420 m ahead
|
||
(scale there ~60 % of the car's, before ~1/20 at 900 m); the floor starts at half height in
|
||
map mode and the speed is 170 instead of 210 units; the floor's grid becomes a ground grid
|
||
(a line every 50 m, fixed to the ground, turning with the roads); roads from 250 m ahead
|
||
fainter; the power disc gets a dark backing over the map. Emulator with the Berlin tiles,
|
||
both layouts. Not tried on the car.
|
||
- 2026-10-26 — Server 2.8.0 image pushed and pinned in `k8s/server.yaml` by digest
|
||
`sha256:8b8d94e0562833d196162866b7fdc6910d5dd219f6eb84572d4d5c91b74f7ff1` (checked with skopeo);
|
||
app 2.39.1 published (`car-2.39.1`). Not applied yet: the owner applies it.
|
||
- 2026-10-26 — App 2.39.1 and server 2.8.0: **the map's roads come from OpenFreeMap, on
|
||
demand.** In the car, server 2.7.0 logged `overpass answered 502`, `overpass could not be
|
||
reached`, and the owner saw 429s: the public Overpass limits per IP, and a tile takes it ~40 s
|
||
while the car gives up after 8 s (`ServerHttp.TIMEOUT_MS`) — the server then cancelled the
|
||
query, threw the work away, and the car's retries added to the load. Owner's choice: fetch
|
||
only the tiles the car asks for (a self-hosted Overpass would need a whole region imported).
|
||
Server: the zoom-14 OpenStreetMap vector tile (OpenMapTiles schema) from the TileJSON at
|
||
`MAP_TILES_URL` (default OpenFreeMap, free, no key, no limits; its template re-read every 6 h,
|
||
https only), decoded by our own minimal protobuf/MVT reader (`mvt.go`, the `transportation`
|
||
layer; classes motorway/trunk, primary/secondary, tertiary, minor), simplified as before. A
|
||
fetch runs to its end without the car (requests wait ≤ 6 s, then 503 with `Retry-After: 10`),
|
||
at most two at once, one per tile; tiles kept 30 days in `MAP_CACHE_DIR` (a 1 Gi volume
|
||
`mg4-server-maps`, pod `fsGroup` 65532) and the last 300 in memory; `OVERPASS_URL` is gone.
|
||
Tests with an encoded vector tile and a fake tile host (classes, split lines, scaled extent,
|
||
malformed tiles, gzip from the host, fetched once and kept across a restart, a slow fetch
|
||
finished for the next try, failures not cached and no tile in the log, http template refused,
|
||
config). Real: the 9 Berlin tiles in 0.7 s (9–15 KB each). App 2.39.1: "OpenFreeMap ©
|
||
OpenMapTiles · © OpenStreetMap contributors" on the map, a tile the server did not have yet is
|
||
asked for again after 15 s (was a minute). Emulator with those tiles: same roads as before.
|
||
Not tried: the deployed server and the car.
|
||
- 2026-10-26 — Server 2.7.0 image pushed (`git.stinnesbeck.com/nils/mg4-server:2.7.0`, Go 1.27.3,
|
||
govulncheck clean) and pinned in `k8s/server.yaml` by digest
|
||
`sha256:e5da9452a9dbdfabe3340766cc51f18bb1418b2588ab534714550470abe1ce29` (checked with skopeo);
|
||
manifests valid (kubeconform). Not applied yet: the owner applies it.
|
||
- 2026-10-26 — App 2.39.0 and server 2.7.0: **Grid with map**, a new Overview style — the Grid
|
||
over the roads round the car from OpenStreetMap (owner's choice of design: roads through the
|
||
car's own server, drawn as the floor). Server: `POST /v1/map/roads` `{z:14, x, y}` (the tile,
|
||
not the position, and in the body so it stays out of access logs) fetches the tile's drivable
|
||
roads from Overpass (`OVERPASS_URL`, default the public instance, `off` disables; https only;
|
||
one request at a time, ≥ 1 s apart, identifying User-Agent), turns them into tile-local points
|
||
in 4 classes simplified with Douglas–Peucker (~1.5 m), caches them in memory (30 days, 3000
|
||
tiles; failures not cached), gzips when accepted, answers 503 when Overpass fails and never logs
|
||
the tile. Tests with a fake Overpass (classes, split at missing points, simplification, cache,
|
||
gzip, validation, 503 without caching, no tile in the log, config). App: `MapRoads` keeps the
|
||
3×3 tiles round the car (files/map, 30 days, 400 tiles; one at a time on a background thread;
|
||
a failed tile waits a minute), `RoadTile` parses them failing closed, `MapGeometry` does the
|
||
web-mercator arithmetic (both tested). GPS only while that style is shown and the app is in
|
||
front; the bearing from the GPS, or worked out from positions 8 m apart when it gives none.
|
||
`GridView` with a map draws the roads in the floor's perspective, heading up, 40 m floor steps,
|
||
up to 900 m ahead, far roads fainter, the car as a chevron, its position run on with the speed
|
||
between fixes, 20 fps while moving; the trail gives way to the map; "© OpenStreetMap
|
||
contributors" shown; otherwise the plain floor saying why (waiting for GPS, set up the server,
|
||
loading, not available). `OVERPASS_URL` in `k8s/server.yaml`; LICENSE.md notes the ODbL. Tested:
|
||
server unit tests; one real fetch of 3×3 tiles in central Berlin through the server code
|
||
(20–30 KB each; Overpass answered 502 a few times and the retry got them); on the emulator those
|
||
tiles from the app's cache with emulator GPS, north up and turning with a simulated drive, both
|
||
layouts, no GPS, no server; no crash. Not tried: the app fetching tiles from the deployed
|
||
server (not deployed yet) and anything on the car.
|
||
- 2026-10-26 — App 2.38.0 published (`car-2.38.0`) and installed on the car through the in-app
|
||
updater, from 2.35.3 (so Grid, Time circuits, Quest, Visor and Empire are on the car).
|
||
- 2026-10-26 — App 2.38.0: Overview styles **Visor** (after Halo's helmet display, `VisorView`)
|
||
and **Empire** (after Age of Empires' interface, `EmpireView`). Visor: shield bar = battery (24
|
||
segments, red and blinking "BATTERY LOW" below 20 %), ammunition counter = range (number and
|
||
rounds, one per 10 km), P R N D as grenade slots, a reticle with the speed and power round it
|
||
(green and anticlockwise in regen), a motion tracker whose blips are the power trail (rim 60 kW,
|
||
40 kW regen) and whose sweep turns only while moving (~30 fps, on screen only), the clock and an
|
||
objective panel; original blue or the accent colour (`hasOriginal`). Empire: a wooden resource
|
||
bar (battery, range, power, 5-min usage, the age, the clock), an isometric world whose ground
|
||
and trees move with the speed (6 m tiles, ~30 fps only while moving), the car as the selected
|
||
unit with the battery as its health bar, the speed large over the world, a carved panel with
|
||
P R N D as commands, the unit's portrait and stats, and a diamond minimap with the odometer and
|
||
the way to the next age; ages by odometer in `EmpireAge` (pure, tested: Dark, Feudal from
|
||
10,000 km, Castle 30,000, Imperial 60,000); its own palette, the app in the player's blue.
|
||
`stylePanes` is now built from the canvas list. THEMES.md with two screenshots each. Emulator:
|
||
sample values, 15 % at a standstill, with and without the navigation, and no data (dashes); no
|
||
crash. Not tried on the car.
|
||
- 2026-10-26 — App 2.37.0: Overview styles **Time circuits** (after Back to the Future,
|
||
`TimeCircuitsView`) and **Quest** (after World of Warcraft's interface, `QuestView`); both
|
||
with their own palette, the app in red and gold with them. Time circuits: three LED rows in a
|
||
metal panel with embossed tape — red "DESTINATION" (gear as DRV/REV/NTL/PRK, charge, range,
|
||
power/regen lamps, usage), green "PRESENT TIME" (date and time from the head unit's clock),
|
||
amber "LAST TIME DEPARTED" (when the car last left P, tracked from the readings, dashes until
|
||
seen); the speed in red digits, a flux capacitor whose light runs inwards with the power and
|
||
outwards in regen; at 88 km/h the speed burns white, the tape reads "88 KM/H — HOLD ON" and the
|
||
capacitor flashes. Letters on fourteen segments (`Segments14`, pure, tested). Quest: player
|
||
frame (gear as level, battery as health, power as energy), minimap with the clock, gold speed,
|
||
a cast bar (Accelerating, Regenerating, Coasting, Resting in P), a road-trip quest tracker,
|
||
system chat lines, P R N D action buttons, the odometer as experience (a level per 1000 km), a
|
||
"Low battery!" raid warning below 20 %. Names avoid the trademarks; drawings and fonts are the
|
||
app's own. THEMES.md with two screenshots each. Emulator: sample values (also 92 km/h and
|
||
15 %), with and without the navigation, and no data (dashes); no crash. Not tried on the car.
|
||
- 2026-10-26 — App 2.36.0: Overview style **Grid**, after the Tron films (`GridView`): neon lines
|
||
on black, a perspective grid floor that runs towards the driver with the speed (4 m tiles,
|
||
~30 fps only while moving and on screen), the speed large and thin, the battery as a
|
||
20-segment light wall standing on the floor (orange below 20 %, "LOW ENERGY"), power on a disc
|
||
(zero at the top, clockwise drawing, anticlockwise regeneration; an inner ring that turns with
|
||
the power), the last two minutes of power as a light-cycle trail of right angles
|
||
(`ClusterValues.powerTrail`, the Cockpit's `PowerTrace`; gaps stay gaps), P R N D tiles, the
|
||
clock, and five read-outs in cut-corner boxes. Glow from layered strokes (no blur filters).
|
||
Original cyan, or the accent colour (`hasOriginal`; low battery then white); in Original the
|
||
app turns cyan (`ThemeOverlay.EV.Style.Grid`). Named "Grid", not after the films. THEMES.md has
|
||
it with two screenshots. Emulator: sample values (full screen, with navigation, accent, 15 %),
|
||
the floor moving between frames, and no data (dashes, nothing lit, still floor); no crash.
|
||
Tests and lint. Not tried on the car.
|
||
- 2026-10-25 — Server 2.6.2: **the update relay logs what it hands out**, per car (installation
|
||
ID, or "shared token"): `update check` with the number of releases and the newest
|
||
pre-release, `update download started` with file and size, then `update handed out` with
|
||
bytes and time, or `update download cut short`; refusals by the forge name the car too. No
|
||
forge address or token in the log (tested: `TestUpdatesAreLogged`). Go 1.27.3 (build image
|
||
and `go.mod` toolchain), up from 1.26.9: `govulncheck` had found GO-2026-6809 (compromised
|
||
intermediate certificates, `crypto/x509`, reached through the server's TLS); now no
|
||
vulnerabilities, in the source and in the built binary. go vet, tests, podman build. Pushed
|
||
as `mg4-server:2.6.2` (`sha256:e4de1e75…`, checked in the registry) and pinned in
|
||
`k8s/server.yaml`. Applied by the owner, together with the real `FORGE_TOKEN`: the car then
|
||
updated itself to 2.35.3 over the air (owner, 2026-10-25).
|
||
- 2026-10-25 — `k8s/influxdb.yaml`: the values Kubernetes fills in itself are written out — the
|
||
NetworkPolicy port's `protocol: TCP`, and `apiVersion`, `kind` and `volumeMode: Filesystem` on
|
||
the StatefulSet's volume claim template. Without them every `kubectl apply -k k8s/` reported
|
||
both as "configured" while `kubectl diff -k k8s/` was empty (owner). Applied by the owner:
|
||
both now "unchanged". Validated offline (kustomize + kubeconform). Also found: the in-app update
|
||
check failed with an IOException because the cluster's `FORGE_TOKEN` was still the template
|
||
placeholder — the forge refuses it and the server answers 502; `apply -k` does not include
|
||
the Secrets, so a new token needs `kubectl apply -f k8s/secrets/secrets.yaml` and a restart.
|
||
- 2026-10-25 — App 2.35.3: a car build with the English EVHardware (comments, log lines, the
|
||
"Layer" names); no change in behaviour. Emulator: starts, no crash, the log reads
|
||
"Layer1: …" to "Layer5: …". Not tried on the car.
|
||
- 2026-10-25 — EVHardware in English: the French comments (and the half-translated ones like
|
||
"l'screen", "registersment", "initialisesd") in `EVHardware.kt` and five test files, the French
|
||
log messages ("aucun init() fonctionnel", "introuvable", "state initial", "ERREUR"), the French
|
||
test names and assertion messages (`CatalogConsistencyTest`, `GlassEvidenceTest`,
|
||
`GlassProbeTest`). Comments, log texts and test names only: no code changed (checked in the
|
||
diff). Kept on purpose: the French translation `values-fr/strings.xml`, place names in test
|
||
data, the quoted French sentence in `ChargeStopPlan` and the voice phrase in `SaicNavGuidance`,
|
||
and "Katman" (Turkish for layer), which named the layers in function names and logs —
|
||
renamed to "Layer" right after (`whenLayer1Ready`, `initLayer4Swi69`, `Layer5: …` in the log,
|
||
and so on; nothing outside EVHardware used the names). EVHardware, app tests and lint pass.
|
||
- 2026-10-25 — README.md brought up to date: the tab screenshots retaken with 2.35.2 on the
|
||
emulator right after installing (`screenshots/nilsmg4-2.35.2-*.png`, the 2.18.1 ones removed),
|
||
"read-only" now names the drive mode exception, the upload frequency matches the app (1 min by
|
||
default, 15 s at low battery, 15 min parked), and the Settings and Logs rows list what is there
|
||
now (Overview style, drive mode, signal recorder).
|
||
- 2026-10-25 — App 2.35.2: the full-screen button is round. Its disc was an even inset in a
|
||
64 × 72 dp (narrow: 64 × 76) area, so an oval; the area is now a square 72 × 72 dp (inset
|
||
18 dp, a 36 dp disc), pulled 8 dp into the panel's padding in the wide layout so "MG4" keeps
|
||
its line. **THEMES.md**: the twelve Overview styles, each with and without the navigation
|
||
(`screenshots/themes/`, 1920 × 720 from the emulator with sample values from a throwaway build,
|
||
never committed), with what inspired each and how it takes colour; linked from README.md
|
||
(Screenshots, Contents, Project documents) and AGENTS.md. Emulator: the round button, the
|
||
header with "MG4" on one line. Not tried on the car.
|
||
- 2026-10-25 — App 2.35.1: **Aviation removed** (owner's choice: `AviationView`, its palette and
|
||
string; a stored "AVIATION" opens Tiles). The Sci-fi palette for the app is now the lilac of
|
||
the console frame's lower elbow (`scifi_lilac` #CC99CC) instead of the orange of the upper one.
|
||
Tests and lint; emulator: Sci-fi with the lilac navigation. Not tried on the car.
|
||
- 2026-10-25 — App 2.35.0: **the app takes on the style's own colours.** While the Overview style
|
||
shows its own palette — Sci-fi, Cartoon, Aviation and Nixie always, Retro and Terminal with
|
||
"Original" — the navigation, buttons, switches and titles take that palette instead of the
|
||
accent colour: Retro turquoise (`retro_lit`), Terminal green phosphor, Sci-fi orange, Cartoon
|
||
orange, Aviation cyan, Nixie neon orange (`StylePalette`, overlays in
|
||
`res/values/style_palettes.xml`, laid over the accent overlay in `onCreate`; a style or colour
|
||
choice that changes the overlay recreates the activity, as an accent change did). The accent
|
||
colour stays stored for the styles that use it. **Blueprint and Mascot removed** (owner's
|
||
choice): `BlueprintView`, `MascotView`, `MascotMood`, `MascotCharacter`, `ChibiBase`,
|
||
`ChibiFigure`, `FoxFigure`, their strings and the Mascot field on Settings; a stored
|
||
"BLUEPRINT" or "MASCOT" opens Tiles. Fix: the navigation (`nav_bar`) no longer saves view state —
|
||
it is a ScrollView in the wide layout and a LinearLayout in the narrow one, and restoring one
|
||
into the other crashed when the width changed (seen on the emulator only; the car's width does
|
||
not change). Tests (`OverviewStyleTest`: palettes per style, removed styles) and lint;
|
||
emulator: each palette, the switch to Retro recolouring at once, the width change. Not tried
|
||
on the car.
|
||
- 2026-10-25 — `styles-retro-future` (app 2.23.0–2.34.1) merged into `main` (merge commit
|
||
`aff79cb`, the summary as its message). Work goes on `main` again.
|
||
- 2026-10-25 — App 2.34.1 (branch `styles-retro-future`): the Overview's full screen without the
|
||
app's full screen. With the car's bars shown, the car's own sidebar sits at the left edge, so a
|
||
swipe from the edge lands on it, not on the app (owner, on the car). There a small tab now sits
|
||
at the app's left edge instead of the faint handle (48 × 96 dp touch area, a dark tongue with
|
||
›): a tap brings the navigation back, and so does a swipe starting in the app's first 56 dp
|
||
(`tabSwipe`). In the app's full screen nothing changed (handle and edge swipe, the owner's
|
||
preference: least clutter). Switching Full screen on Settings updates it at once. Emulator:
|
||
the tab shows, a tap and a swipe on the left strip both bring the navigation back. Not tried
|
||
on the car.
|
||
- 2026-10-25 — App 2.34.0 (branch `styles-retro-future`): **drive mode at start.** The car falls
|
||
back to its own default mode when switched on; with "Set at start" (on by default, the switch
|
||
in the drive mode card) the app sets the mode last chosen on Settings again. Only a choice the
|
||
car confirmed counts (`PREF_CHOSEN`, saved on APPLIED). `AbrpUploadService` starts with the
|
||
head unit and runs `DriveModeWrite.atStart` (pure, tested) once a second on its own thread:
|
||
wait for gear, speed and mode; nothing when the car already reports the mode; one write, in P
|
||
at 0 km/h, through the same `DriveModeSetter` as the buttons (write, read back up to 3 s); give
|
||
up for this start as soon as the car is out of P or moving, or after 2 minutes. So a mode set
|
||
with the car's own button after driving off stands. The result is kept with its time and
|
||
shown under the buttons ("At start (25.10. 20:31): Sport set."). Assumes the head unit starts
|
||
(and the service with it) when the car is switched on; a start without that (head unit kept
|
||
running) is not caught. A service restart (app update, after a kill) counts as a start, still
|
||
only in P at 0 km/h. Hints shortened to fit the page. Emulator: page fits, the start check
|
||
waits and gives up (no readings), no crash. Not tried on the car.
|
||
- 2026-10-25 — App 2.33.0 (branch `styles-retro-future`): **drive mode from Settings**, the first
|
||
vehicle setting the app writes. The owner lifted rule 3 for the drive mode only (AGENTS.md).
|
||
A card at the top of Settings' second column: Eco, Normal, Sport, Snow, Custom (codes 2, 3, 4,
|
||
6, 7 at `0x2140A17C`; the 2.32.0 recording read 2, Eco). The lit button is the mode the car
|
||
reports, from every 0.2 s reading. A tap reads gear and speed fresh and writes only in P at
|
||
0 km/h (`DriveModeWrite`, pure and unit-tested; unreadable gear or speed is a refusal), through
|
||
the new `EVHardware.writeDriveModeProperty`: the CarPropertyManager write behind
|
||
`VehicleWriteGate` (0 km/h, supported firmware), without `setDriveMode`'s fallback binder
|
||
transaction. Then the mode is read back for up to 3 s, and the status beside the title says
|
||
what the car reports ("set", "not applied: the car still reports …", "could not be read
|
||
back"). One change at a time, on the settings thread; each attempt goes to logcat. To fit the
|
||
page, the wide layout's charging price row moved into the battery card ("Battery and charging
|
||
price"). Emulator: the card fits, a tap is refused (no gear or speed there). Whether the car
|
||
takes the write at all is untested: the first try on the car decides.
|
||
- 2026-10-25 — App 2.32.1 (branch `styles-retro-future`): the cruise control work ends. The ACC
|
||
recording of 2.32.0 (ACC set to 101, 90 and 80 km/h, a car ahead) found nothing: see "Signals
|
||
the car does not provide". Removed: `CruiseLog` and its section on the Logs tab, and in
|
||
EVHardware the set-speed read (`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`),
|
||
`readAccDebug` and `ReadFailureLog.reason` — EVHardware is again as before 2.27.3. The old log
|
||
files are deleted on start. The recorder stays as a general tool, renamed **Signal recorder**
|
||
(`SignalRecorder`, `SignalRecording`, `signal-recording-<time>.txt`). What the recording did
|
||
show, for later: `0x2140A15A` (`AAD_ELECVEHSYSMD`) flips 3/7 with accelerating and slowing,
|
||
`0x21409326` is the turn indicator (0 off, 1 and 2 the sides), `0x2140159D` (unnamed) shows
|
||
values like 300–1425 only while slowing down, `0x2140B970`/`0x2140B97B` are the parking sensors
|
||
in R. A full capture takes about 5 s on the car. Tests and lint; emulator: the recorder starts,
|
||
stops and saves under its new name. Not tried on the car.
|
||
- 2026-10-25 — App 2.32.0 (branch `styles-retro-future`): the **ACC recorder** on the Logs tab.
|
||
The 2.31.1 log on the car (a drive with ACC engaged behind another car) showed every assist
|
||
property constant: `cc 0.00`, `ica 0`, `tja 1`, `sas 0`, `sam 0`, `obj 0.00`, `dist 0.00`,
|
||
`rel -128.00` (the field's minimum, i.e. no data). The `AAD_*` properties named in the head
|
||
unit's manager are not fed on this car. The recorder looks everywhere instead: for 3 minutes it
|
||
reads every value the head unit offers (`VendorSurfaceProbe.capture`: every CarPropertyManager
|
||
property and every value getter of the vendor objects, read only), waiting at least 2 s and at
|
||
least as long as the last capture took (at most half the time busy), and every
|
||
`com.saic.keyevent.hardkey.report` broadcast. `AccRecording` (pure, unit-tested) keeps the
|
||
first capture whole, then what changed and when, and per value how often and through which
|
||
values; the Logs tab shows the keys, then the changed values, the rarest first (the leads).
|
||
At the end the whole recording goes to `files/acc_recording.txt` and to the app's folder on
|
||
every removable volume (`Android/data/<package>/files/acc-recording-<time>.txt`). The ACC
|
||
buttons do nothing on the head unit's screen (owner). Emulator: the recorder starts, stops,
|
||
records a sent key broadcast and saves (its CarPropertyManager is not connected there, so no
|
||
values); the USB stick copy was not tried. Not tried on the car.
|
||
- 2026-10-25 — App 2.31.1 (branch `styles-retro-future`): the cruise control log now logs while
|
||
driving. 2.27.3 wrote a line only when the target speed changed, so on the car it showed one
|
||
line at the start. Now `CruiseLog` writes a line at once when a mode or the target changes and
|
||
every 2 s while driving (D or R, or moving), with every driver-assistance property that may
|
||
describe the ACC, raw (`EVHardware.readAccDebug`, read once a second on the reader thread):
|
||
the target speed (`cc`, `0x2160A15B`), the traffic-jam / intelligent cruise assist operation
|
||
mode (`ica`, `AAD_LANE_TJAICAOPMD` `0x2140A12A`), the traffic-jam assist (`tja`, `0x2140A107`),
|
||
the speed assist system and mode (`sas`/`sam`, `0x2140A101`/`0x2140A102`) and the object the
|
||
ACC follows (`obj`/`dist`/`rel`, `AAD_ACC_OBJ_*` `0x2160A12F`/`0x2160A12C`/`0x2160A12E`; these
|
||
alone never write a line). An unread value carries the reason the car gave
|
||
(`ReadFailureLog.reason`). The last 600 lines live in `files/cruise_log.txt`, appended to; the
|
||
Logs tab shows the newest 150. The old change-only log in preferences is dropped. Only while the
|
||
app is open (the reader runs with the screen). Emulator: every value unread, the file written
|
||
and kept over a restart. Not tried on the car.
|
||
- 2026-10-25 — App 2.31.0 (branch `styles-retro-future`): the Overview's full screen moved into
|
||
the navigation: its button now sits beside the MG4 logo (at the end of the bottom bar on narrow
|
||
screens) instead of over the page's top-right corner, and only shows on the Overview. A swipe
|
||
in from the left edge brings the navigation back (`EdgeSwipe`, pure and unit-tested: starts
|
||
within 48 dp of the edge, 64 dp to the right, more sideways than up or down; the page under it
|
||
gets a cancel). A faint handle at the left edge marks it in full screen. Swiping sideways no
|
||
longer changes the page (`ViewPager2.setUserInputEnabled(false)`); the navigation does. The
|
||
exit icon and its string went. Checked on the emulator: the button enters full screen, a
|
||
swipe in the middle does nothing, a swipe from the edge brings the navigation back, sideways
|
||
swipes keep the page, the narrow layout opens. Not tried on the car.
|
||
- 2026-10-24 — App 2.30.0 (branch `styles-retro-future`): a third Mascot, the **chibi fox**
|
||
(`FoxFigure`): a sitting fox with a big head, pointed ears, cream cheeks, chest and tail tip,
|
||
dark paws and a scarf in the accent colour. Ears and tail act the mood with the face: the
|
||
tail wags while content and fast while cheering, stands stiff when determined and hangs low
|
||
when worried or asleep; ears droop when worried or asleep and lie back when fast, when the
|
||
scarf also flies. Its own lines (`fox_*`, "Kon!"). The chibi's shared parts (frame, eyes,
|
||
mouths, limbs by mood, effects) moved into `ChibiBase`, which both figures extend. Settings →
|
||
Mascot: Classic / Chibi / Fox. Emulator: all moods checked with sample values in a throwaway
|
||
build; the tail's swing is kept to about 18° so it stays clear of the values. Not tried on the
|
||
car.
|
||
- 2026-10-24 — App 2.29.0 (branch `styles-retro-future`): a second Mascot, the **chibi**
|
||
(`ChibiFigure`): small and full-figure, a big round head with a coral bob and a star clip,
|
||
a hoodie in the accent colour, dark outlines like a sticker; an adult character in chibi
|
||
proportions, clothed. Her whole body acts the mood: waves when content, both arms up and
|
||
hopping with star eyes when cheering, fist at her chest when determined, hands on her cheeks
|
||
with a tear when worried, a nose bubble asleep in P, glancing and pointing back in R, leaning
|
||
into the wind when fast, "?" while waiting. Her own lines (`chibi_*`). Settings → Mascot
|
||
(Classic / Chibi, `MascotCharacter`, pref `mascot_character`) sits beside Tyre pressure and
|
||
shows only while the Mascot style is chosen. Emulator: all moods checked with sample values in
|
||
a throwaway build, and the Settings drop-down. Not tried on the car.
|
||
- 2026-10-24 — App 2.28.0 (branch `styles-retro-future`): Overview style **Mascot**
|
||
(`MascotView` on `ClusterCanvasView`): an anime co-pilot, head and shoulders, drawn on a
|
||
100 × 100 grid — an adult woman, fully dressed, with a headset and a racing jacket in the
|
||
accent colour (so the style takes the accent). Her face follows `MascotMood`, chosen from the
|
||
reading in this order: waiting (nothing read), reverse (glances back), worried under 20 %
|
||
(sweat drop), asleep in P (drifting Zs), cheering while regenerating more than 5 kW
|
||
(sparkles), determined over 60 kW, enjoying 100 km/h and more (wind lines), else content. A
|
||
new mood shows after it lasts 1.5 s (`MascotMood.Holder`), so values hovering at a threshold
|
||
do not make her flicker; R and "waiting" show at once. Unread values are never a reason for a
|
||
mood. She says a line per mood in a speech bubble (`mascot_*` string arrays, one picked every
|
||
30 s, shrunk to fit). Beside her: speed, the gear in a ring, battery bar with percent and
|
||
range, power, 5-minute usage, trip, and the clock. She blinks and the Zs, sparkles and wind
|
||
move with the 0.2 s readings; no animation loop of its own. Emulator: all eight moods checked
|
||
with sample values in a throwaway build, and "waiting" with no data. Not tried on the car.
|
||
- 2026-10-24 — App 2.27.3 (branch `styles-retro-future`): first step to the cruise control (ACC)
|
||
set speed. The head unit's `CarAdvancedAssistedDrivingManager` names a vendor float
|
||
`AAD_EPTCCTRGTSPD` = `0x2160A15B`, by its name the powertrain's cruise control target speed;
|
||
nothing else in the decompiled SDK looks like a set speed (the other ACC properties are
|
||
objects ahead: distances, relative speeds). EVHardware reads it raw
|
||
(`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`; read-only), and the Logs
|
||
tab lists every change with time, speed and gear (`CruiseLog`, last 12). Unit and the value
|
||
while cruise control is off are unknown until read on the car; the dashboards show nothing of
|
||
it yet. Emulator: the property is absent there, one "unread" line, no crash. Not tried on the
|
||
car.
|
||
- 2026-10-24 — App 2.27.2 (branch `styles-retro-future`): Terminal can be drawn in the accent
|
||
colour too (text in its light shade, the command line in the accent, plain black; the low
|
||
battery stays amber). Instead of separate "(accent colour)" entries, Retro and Terminal are one
|
||
entry each and the accent colour drop-down beside the style offers "Original" first (their own
|
||
palette, the default) and then the accent colours; the choice is kept per style
|
||
(`original_colours_<STYLE>`). Picking "Original" leaves the app's accent as it is. The
|
||
"Retro (accent colour)" entry is gone: a stored `RETRO_ACCENT` becomes Retro in the accent
|
||
colour (`OverviewStyle.hasOriginal`, `wasAccentEntry`; unit-tested). Fourteen entries in the
|
||
style list. Checked on the emulator: Terminal original → Dynamic Red, Retro still original,
|
||
and a stored `RETRO_ACCENT` migrated to Retro in Dynamic Red
|
||
(`screenshots/nilsmg4-2.27.2-terminal-accent.png`); no crash. Not tried on the car.
|
||
- 2026-10-24 — App 2.27.1 (branch `styles-retro-future`): Settings → Display rearranged:
|
||
Overview style first, the accent colour beside it, tyre pressure on its own row below. The
|
||
accent colour is only offered for styles drawn in it (Tiles, Gauges, Cockpit, Retro (accent
|
||
colour), Futuristic, MG, Minimal, Chronograph); for the styles with their own palette (Retro,
|
||
Sci-fi, Cartoon, Blueprint, Terminal, Aviation, Nixie tubes) the field is greyed out and
|
||
ignores taps, keeping the colour chosen before, and a line says "… has its own colours. To
|
||
change the accent colour, pick a style that uses it." The styles are now an enum,
|
||
`OverviewStyle` (stored names, `usesAccent`, the old `DASHBOARD` value; unit-tested), in place
|
||
of the string array. The style list is wider than its field so "Retro (accent colour)" fits.
|
||
Checked on the emulator: Tiles (accent open), Cartoon (greyed, tap ignored, note), back to
|
||
Tiles; the page fits without scrolling
|
||
(`screenshots/nilsmg4-2.27.1-settings-own-colours.png`). Not tried on the car.
|
||
- 2026-10-24 — App 2.27.0 (branch `styles-retro-future`): the Overview can go full width. A
|
||
small round button in its top-right corner (40 dp disc in a 72 dp touch area, over the pages)
|
||
hides the app's navigation (sidebar, or the bottom bar on narrow screens); its icon turns to
|
||
"shrink" and a second tap brings the navigation back. Only on the Overview: other pages
|
||
always show the navigation and no button, swiping between pages still works, and coming back
|
||
to the Overview restores full width. Remembered (`overview_fullscreen`). The top-right corner
|
||
was chosen after looking at all fifteen styles (top left covered Tiles' "Battery"); Minimal's
|
||
clock moved left of it. Checked on the emulator: toggle, swipe away and back, restart, Tiles
|
||
and MG at full width (`screenshots/nilsmg4-2.27.0-overview-fullscreen.png`); no crash. Not
|
||
tried on the car.
|
||
- 2026-10-24 — App 2.26.0 (branch `styles-retro-future`): five more Overview styles, chosen to
|
||
differ in arrangement as much as in look, on a new base class `ClusterCanvasView` (values,
|
||
easing, null-safe formatting, animating only while something moves and the view is shown).
|
||
**Minimal** (`MinimalView`): the speed alone, very large and thin; power as a short line
|
||
growing left (green, regenerating) or right from the centre; the battery as a hairline along
|
||
the bottom edge. **Terminal** (`TerminalView`): text only, green monospace with scan lines —
|
||
rows with ASCII bars, the gear as [D], tyres, a prompt with a cursor that blinks as readings
|
||
redraw it; a low battery row turns amber. **Aviation** (`AviationView`): a primary flight
|
||
display — speed and battery on scrolling tapes with readout boxes (blank, not centred on 0,
|
||
when unread), a horizon that pitches with power (up drawing, down regenerating) with a kW
|
||
ladder and the aircraft symbol, annunciators on top, data boxes below. **Nixie tubes**
|
||
(`NixieView`): orange glowing digits in glass tubes with the unlit numerals behind and a mesh,
|
||
brass name plates on walnut; speed, gear and battery large, power, range, usage and clock
|
||
smaller (`SevenSegment` lays the text out in tubes). **Chronograph** (`ChronographView`): a
|
||
watch face — the speed on the main hand over a bezel scale, sub-dials for battery, power and
|
||
usage, the gear in a window, engraved columns either side (its radial gradient is a Canvas
|
||
shader made in `onSizeChanged`). Checked on the emulator with a throwaway build cycling the
|
||
five styles with sample values (drawing and regenerating;
|
||
`screenshots/nilsmg4-2.26.0-minimal.png`, `-terminal`, `-aviation`, `-nixie`, `-chronograph`)
|
||
and without data; no crash. Not tried on the car.
|
||
- 2026-10-24 — App 2.25.0 (branch `styles-retro-future`): Overview style "MG" (`OctagonView`),
|
||
after the classic MG dashboards whose dials sat in octagonal bezels — not a copy of the MG4's
|
||
own driver display, and without MG's octagon logo. Three octagonal dials with chrome bezels,
|
||
cream scales and needles in the accent colour: speed in the middle (digits and P R N D under
|
||
the hub), battery E…F on the left (red zone below 20 %, range), power −50…150 kW on the right
|
||
(green charge zone below zero). A cream line below: odometer, trip, 5-minute usage, climate,
|
||
clock, separated by small accent octagons. Needles ease and redraw only until settled, only
|
||
on screen; unread values draw no needle and "—". Palette `mg_*`. Checked on the emulator with
|
||
sample values (`screenshots/nilsmg4-2.25.0-mg.png`) and without data; no crash. Not tried on
|
||
the car.
|
||
- 2026-10-24 — App 2.24.1 (branch `styles-retro-future`): the line of text above every page
|
||
("Your drive at a glance.", "Settings. Display, battery…" and so on) is gone, at the owner's
|
||
request; the pages start at the top, level with the sidebar, and gain about 45 dp. Its 14
|
||
strings and the title arrays are removed. Checked on the emulator: no crash, and every tab
|
||
still fits 1778×640 without scrolling. Not tried on the car.
|
||
- 2026-10-24 — App 2.24.0 (branch `styles-retro-future`): three more Overview styles, each one
|
||
Canvas view fed the shared `ClusterValues` (now with the tyre pressures, formatted in the
|
||
chosen unit). **Sci-fi** (`SciFiView`): a starship console — rounded elbow frames in orange
|
||
and lilac, pill labels, condensed capitals; velocity large, power as blocks (regeneration
|
||
blue), rows for reserves, range, efficiency, since start, trip and climate, the reserves as a
|
||
stack of blocks, gears as blocks in the frame; below 20 % the frames turn red ("red alert").
|
||
**Cartoon** (`CartoonView`): comic panels with ink outlines and hard shadows on night blue; a
|
||
battery with a face (happy, unsure, worried and sweating), a speedometer whose needle springs
|
||
into place (animated only until it settles, only on screen), the speed in a speech bubble,
|
||
the gear on a sticker, a "zap-o-meter" with a sound word (PSST, PURR, VROOM, ZOOM!,
|
||
RECHARGE!), stats on pills. **Blueprint** (`BlueprintView`): a top-view drawing of the car on a
|
||
gridded navy sheet — the battery hatched as far as it is charged, leader lines to each tyre's
|
||
pressure, power as a force arrow (dashed and reversed while regenerating), range and trip as
|
||
dimension lines, the cabin setpoint as a note, and a title block with speed, power, gear,
|
||
usage, odometer and time. Palettes `scifi_*`, `cartoon_*`, `blueprint_*` in colors.xml.
|
||
`MainActivity` now maps each style to its pane in a table (`stylePanes`); the new panes are
|
||
built in code (`drawnPane`). Checked on the emulator with sample values in a throwaway build
|
||
(`screenshots/nilsmg4-2.24.0-scifi.png`, `-cartoon.png`, `-blueprint.png`) and without data
|
||
through the real setting; no crash. Not tried on the car.
|
||
- 2026-10-24 — App 2.23.1 (branch `styles-retro-future`): "Retro (accent colour)", the Retro
|
||
style in the chosen paint colour — display in `evAccent`, labels in `evAccentLight`;
|
||
regeneration stays green, a low battery turns amber (not red, so it never looks like Dynamic
|
||
Red). Same `RetroView`, `setAccentPalette`; listed under Retro, stored as `RETRO_ACCENT`.
|
||
Checked on the emulator in Dynamic Red with sample values
|
||
(`screenshots/nilsmg4-2.23.1-retro-accent.png`). Not tried on the car.
|
||
- 2026-10-24 — App 2.23.0 (branch `styles-retro-future`): two more Overview styles, each one
|
||
Canvas view fed a `ClusterValues` per reading. **Retro** (`RetroView`): an 80s digital cluster
|
||
as a vacuum fluorescent display — slanted seven-segment digits (`SevenSegment`, unit-tested)
|
||
with the unlit segments faintly visible, amber labels, a lit P R N D selector, the speed large,
|
||
power as a −50…150 kW bar (regeneration green), charge as a vertical bar (red below 20 %),
|
||
range, 5-minute and since-start usage, trip, climate, clock and odometer; its fixed palette is
|
||
`retro_*` in colors.xml (a style's own colours, not the accent). **Futuristic** (`FlowView`): a
|
||
hairline HUD in the accent colour — a battery capsule of scan lines, an energy channel whose
|
||
particles flow to the wheel while drawing and back in green while regenerating (faster with
|
||
more power), the speed inside a ring of dashes that turns with the speed and a power arc,
|
||
P R N D in a column, and a readout line. It animates at about 30 frames a second only while
|
||
power or speed is non-zero and the view is shown. Unread values show dashes and light or move
|
||
nothing in both. The Overview now reads the car every 0.2 s instead of 0.5 s (a read still
|
||
running skips the tick, as before). Checked on the emulator with sample values in a throwaway
|
||
build (`screenshots/nilsmg4-2.23.0-retro.png`, `…-futuristic.png`) and without data through
|
||
the real setting; no crash. Not tried on the car, including the 0.2 s reading rate.
|
||
- 2026-10-24 — App 2.22.3: the Overview style "Dashboard" is now called "Gauges" (stored as
|
||
`GAUGES`; the old value `DASHBOARD` still selects it, so the choice survives the update;
|
||
`pane_dashboard.xml` is `pane_gauges.xml`). The branches `dashboard-gauges` and
|
||
`dashboard-cockpit`, merged into main, are deleted. Tests and lint.
|
||
- 2026-10-24 — App 2.22.2 (branch `dashboard-cockpit`): the gear badge names all four gears.
|
||
The owner shifted through the lever with 2.22.1 and read the codes off the Logs tab: 1 P,
|
||
2 R, 3 N, 4 D (firmware SWI68-29958-1300R69). `Gear.label` maps them (unit-tested); any
|
||
other code shows no letter, unread "—". The gear list on the Logs tab, which existed only for
|
||
this, is gone again. Codes recorded in AGENTS.md and EVHardware's comments. Unit tests only;
|
||
the mapping itself comes from the car.
|
||
- 2026-10-24 — App 2.22.1 (branch `dashboard-cockpit`): the gear badge on the Dashboard and
|
||
Cockpit no longer guesses. It showed "D" for anything not in P, so N and R read as D. The app
|
||
only knew "in P or not"; EVHardware now passes on the car's raw gear code
|
||
(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`; `isVehicleInPark` is built on it).
|
||
`Gear.label`: 1 → "P" (the only established code), any other code → no letter, unread → "—".
|
||
To map R, N and D, the Logs tab lists every change of the code with time and speed
|
||
(`GearLog`, last 12, kept in preferences). Checked on the emulator with a throwaway build
|
||
cycling codes 1–4 (pill shows P on 1, nothing on 2–4; the Logs tab lists each change). Not
|
||
tried on the car; the codes for R, N and D are still to be read there.
|
||
- 2026-10-24 — App 2.22.0 (branch `dashboard-cockpit`, from `dashboard-gauges`): a third Overview
|
||
style, "Cockpit", designed without a reference: three segmented rings (`RingView`, Canvas) —
|
||
battery (0…100 %, range inside, below 20 % in the warning colour), power with the speed and the
|
||
gear inside (zero at the top: drawing power clockwise to 150 kW in the accent colour,
|
||
regeneration anticlockwise to −50 kW in green) and 5-minute usage (0…30 kWh/100 km, a white tick
|
||
for the average since start). Segments brighten towards the leading one. Below, the power of the
|
||
last two minutes as a trace (`TraceView`; `PowerTrace`, one slot per second, unit-tested; gaps
|
||
for unread or missed readings, never zeros), the clock, climate setpoint and odometer. Settings
|
||
→ Overview style: Tiles / Dashboard / Cockpit. Checked on the emulator with sample values in a
|
||
throwaway build (`screenshots/nilsmg4-2.22.0-cockpit.png`) and without data through the real
|
||
setting (nothing lit, "—", the page fits 1778×640). Not tried on the car.
|
||
- 2026-10-24 — App 2.21.0 (branch `dashboard-gauges`): Overview style "Dashboard", an instrument
|
||
cluster after a reference picture of the owner's: power (−50…150 kW), speed (0…180 km/h, with
|
||
TOTAL odometer and TRIP km since start) and battery (E…F, below 20 % in the warning colour,
|
||
range underneath) as gauges (`GaugeView`, Canvas, accent-coloured arcs, white needles that ease
|
||
to a new value), and a strip of gear (P/D), climate setpoint, clock, 5-minute usage and a
|
||
battery bar (`BatteryBarView`). Unread values draw no needle and "—". Settings → Display →
|
||
"Overview style: Tiles / Dashboard"; Tiles stays the default; both are fed from the same
|
||
reading. Checked on the emulator with sample values in a throwaway build
|
||
(`screenshots/nilsmg4-2.21.0-dashboard.png`) and without data through the real setting; the
|
||
Overview and Settings fit 1778×640. Not tried on the car.
|
||
- 2026-10-23 — App 2.20.0: the Overview's Vehicle info card shows the lease mileage: "Lease
|
||
allowance today" (40,000 km over four years from 2023-05-01, accruing evenly per day, in
|
||
`LeaseAllowance`, unit-tested) and "Versus allowance", the odometer against it ("… km over" /
|
||
"… km under", "—" when the odometer is unread). The odometer now has a thousands separator,
|
||
like the new rows. Checked on the emulator with a sample odometer of 25,000 km in a throwaway
|
||
build (34,798 km allowed on 2026-10-23, 9,798 km under); the Overview still fits.
|
||
- 2026-10-23 — App 2.19.9: the Foreground apps card is gone from the Logs tab (it found CarPlay's
|
||
package; nothing left to find), with ForegroundProbe and ForegroundHistory. The Logs tab is one
|
||
card again — update log, then uploads — with the other tabs' page padding.
|
||
- 2026-10-23 — App 2.19.8: the charging curve's labels no longer overlap: "kW" has a row of its
|
||
own above the plot, the power values are right-aligned against the axis, and the SOC ticks read
|
||
"0 %" … "100 %" (the separate "SOC %" sat on top of "100"). The wide Charging tab splits 35/65,
|
||
values | curve. Checked on the emulator with sample AC values and a curve in a throwaway build;
|
||
the page still fits 1778×640.
|
||
- 2026-10-23 — App 2.19.7: AC charging is metered too, while the app runs (owner's choice; Fatih
|
||
had metered DC only, `e9bfa12`, because an overnight AC charge is slept through). A session runs
|
||
whenever power flows into the pack (`ChargingState.isMeteredCharging`: inbound > 0.3 kW and a
|
||
charge by `isCharging`), and its curve takes every inbound sample. Gaps where the head unit
|
||
slept are dropped, not billed, so an overnight AC session shows the awake part only. The label
|
||
says "AC charging" / "DC fast charging"; `isDcCharging` stays for it and ABRP's `is_dcfc`. Server
|
||
comment, server README and the Grafana charging dashboard's texts no longer say DC only (the
|
||
dashboard ConfigMap changes with them: re-apply `k8s/`). Unit tests; not tried on the car.
|
||
- 2026-10-23 — App 2.19.6: the CarPlay full-screen switch and the "Export CarPlay data" button are
|
||
removed (CarPlay's size is fixed, see below). On start, the car build takes the CarPlay entry out
|
||
of `policy_control` if the switch was left on, and leaves anything else there alone — checked on
|
||
the emulator. The Foreground apps card stays as a diagnostic. AGENTS.md lists the CarPlay and
|
||
install findings among the confirmed car facts.
|
||
- 2026-10-23 — CarPlay full screen: not possible on this firmware (SWI68). The export from the
|
||
car showed CarPlay's window at 1920×720 with both SAIC bars hidden (policy_control on), and its
|
||
video SurfaceView at (0,80), 1920×640, with a 1920×640 buffer: the grey strip is the window's own
|
||
background above the video. Allgo's `/system/app/AllgoCarplay_EH32` hard-codes it twice:
|
||
`CarPlayService` announces `SCREEN_HEIGHT = 640`, `SCREEN_WIDTH = 1920` to the iPhone (it reads
|
||
`sys.screen.heightpixel/widthpixel`, then overwrites them with the constants), and
|
||
`carplay_screen_layout.xml` places the SurfaceView at `640px` high with `80px` top margin. Both
|
||
are in the read-only, signed system image. The export (`dump/`) is gitignored: it holds Allgo's
|
||
software.
|
||
- 2026-10-23 — App 2.19.5: installing from USB (and over the air) works on the car again, with a
|
||
confirmation. The update log showed the car refusing the silent session: `Not allowed to install
|
||
non-system apps on internal storage`. The fallback, Android's installer screen, then said "There
|
||
was a problem parsing the package": as the system user, our FileProvider's read grant to the
|
||
installer is refused (`not exported from UID 1000`). The car build now hands the installer a
|
||
file URI to the verified copy in Downloads (StrictMode's file-URI check lifted for that one
|
||
call). On the emulator, with the session forced to fail as on the car, the installer screen
|
||
asked to update and installed it. Not tried on the car yet. OTA.md updated.
|
||
- 2026-10-23 — App 2.19.4: "Export CarPlay data" on the Logs tab (Foreground apps card), for the
|
||
question whether CarPlay's resolution can change. CarPlay with policy_control still leaves a grey
|
||
strip, also after reconnecting: Allgo announces a smaller picture to the iPhone. 20 s after the
|
||
press (time to open CarPlay) it writes to `<stick>/Android/data/<package>/files/
|
||
nilsmg4-carplay-<time>/` — the only place on a removable volume Android 9 lets an app write:
|
||
`dumpsys window windows`, `SurfaceFlinger`, `display`, `activity activities`, `wm size/density`,
|
||
the display-related system properties (identifiers filtered out), Allgo's APK and native
|
||
libraries, and CarPlay/Allgo/iAP2-named files under /system, /vendor, /odm, /product. It reads
|
||
the car and writes only to the stick. Owner's choice to export Allgo's software for analysis
|
||
(2026-10-23). On the emulator (no Allgo there) every dump was written, without permission
|
||
errors. XDA (leonkernan, page 30) found SAIC's SystemUI hides its top bar itself for a whitelist
|
||
that includes com.allgo.carplay.service — compiled into SystemUI, not changeable from the app.
|
||
- 2026-10-23 — App 2.19.3: an update log on the Logs tab, above the uploads. A USB install on the
|
||
car ended in an installer message about a broken package, while the same APK installed through
|
||
the Bluetooth-settings route; the message was only a toast and logcat, so its exact words are
|
||
lost. Now every step is kept (last 10, survives the restart an install causes): file found with
|
||
its size, SHA-256 match, the package manager refusing to parse, a failed installer session with
|
||
its exception, bytes handed to the installer, the platform's result in its own words, and "Now
|
||
running <version>" on the first start of a new version. On the emulator, with a FAT image as a
|
||
removable volume (like the car's stick), the USB install worked silently and the log showed the
|
||
whole sequence; a truncated APK with a matching hash was refused at the package check, logged.
|
||
The car's failure is not reproduced or explained yet.
|
||
- 2026-10-23 — App 2.19.2: Settings → "Full screen for CarPlay (experiment)". The Logs tab showed
|
||
CarPlay on the car as `com.allgo.carplay.service/.CarPlayActivity`, an ordinary activity. The
|
||
switch adds that package to `immersive.full` in Android 9's `policy_control` global setting
|
||
(system user only; elsewhere it is disabled) and, off, removes just that entry; it shows what the
|
||
setting holds. On the emulator: policy_control hid the status bar of another app and its content
|
||
took the space; the switch wrote and removed its entry and left an existing one alone. Not tried
|
||
on the car: whether SAIC's top bar is the status bar, and whether CarPlay's picture grows or
|
||
leaves a black strip until the phone reconnects. Tyre pressure and accent colour now sit side by
|
||
side in the wide Settings layout, so the page still fits.
|
||
- 2026-10-23 — App 2.19.1: "Foreground apps" card on the Logs tab, step 1 of the CarPlay
|
||
full-screen experiment. It reads which apps came to the front in the last 30 minutes (usage
|
||
events, open to the system user without a declared permission), the open tasks, the
|
||
`policy_control` global setting and the status bar height — on opening the tab, on returning to
|
||
it and on Refresh, never in the background. It changes nothing. On the emulator (car build,
|
||
system user) it named Settings, the clock, the calculator and the launcher with times, and the
|
||
page still fits 1778×640 with every line filled. The stable build says it cannot read them. Not
|
||
tried on the car; the CarPlay package name is what this is for. Next: a switch that sets
|
||
`policy_control` to `immersive.full=<that package>`, on approval.
|
||
- 2026-10-19 — `k8s/agent-access/`: a service account `mg4-agent` for a coding agent, limited to
|
||
the `mg4` namespace. Read-only role by default; an optional deploy role. Neither may touch
|
||
Secrets, exec, port-forward or RBAC; the deploy role cannot delete PVCs. `make-kubeconfig.sh`
|
||
writes a kubeconfig with a `kubectl create token` token that expires (default 24 h). Validated
|
||
with kubeconform and shellcheck, the script against a stub kubectl; not applied.
|
||
- 2026-10-19 — App 2.19.0: EVHardware updated from malys/EVHardware `b595ccb` (2026-09-17, the
|
||
base Fatih copied) to `e4e1158` (2026-10-18, after 1.18.0), 42 commits, merged three ways so Fatih's
|
||
changes stay (listed in `LICENSE.md`). Upstream brings among others: trip, charge and eco
|
||
telemetry fixes, a daily battery digest, the snapshot's widest window and raw drive mode and
|
||
regen, probes for the vendor charge status, plug and energy ids, read-only vendor-surface probes,
|
||
exterior-light conditions and actions, and a diagnostic inbox. The app uses only
|
||
`EnergyTelemetryReader` and its snapshot, so the new reads (windows, drive mode, regen) are the
|
||
only new calls it makes; they are reads. The new actions and the inbox are not called. Conflicts
|
||
were only where both sides added fields; the charge-status id is now upstream's
|
||
`PROP_BMS_CHRG_STS`. EVHardware's 491 tests and the app's pass; the app ran on the emulator
|
||
without errors. Not tried on the car.
|
||
- 2026-10-19 — App 2.18.3: the Battery tile's value is 40 sp too. The first row of tiles now
|
||
shares one height, since the Battery tile, with its charging line, grew taller than the rest.
|
||
Checked on the emulator with sample values: the Overview still fits 1778×640. Not tried on the car.
|
||
- 2026-10-19 — App 2.18.2: Overview's Range, Power and Speed tiles (the ones without a line under
|
||
the value) show it at 40 sp instead of 28; auto-size still shrinks a long value. Checked on the
|
||
emulator with sample values ("-142.5 kW", "160 km/h") in a throwaway build: they fit at full
|
||
size and the row keeps its height. Not tried on the car.
|
||
- 2026-10-19 — Server 2.6.1: the code field on the phone pages (`/pair`, `/settings`, when opened
|
||
without the code in the link) upper-cases what is typed and puts the dash in after the third
|
||
character. That takes the pages' first script: one inline script, allowed by its SHA-256 hash in
|
||
the CSP (`script-src 'sha256-…'`, still `default-src 'none'`); a test checks the served script
|
||
against the header. Without it the server still accepts the code in any case, with or without
|
||
the dash. Image pinned by digest in `k8s/server.yaml` (not applied).
|
||
- 2026-10-18 — README brought up to date: screenshots of all seven tabs from 2.18.1 at the car's
|
||
1778×640 window (the upstream and 2.8.2 screenshots are gone), tab descriptions, updating over
|
||
the air and from a USB stick, the emulator, the release relay. OTA.md: updates are checked from
|
||
Settings, no longer from a top-bar button.
|
||
- 2026-10-18 — App 2.18.1: every tab fits the car's window (1778×640 dp) without scrolling — checked
|
||
on the emulator by swiping each page and comparing screenshots. Charging and Consumption get wide
|
||
layouts with two columns (values | curve; period totals | daily log and storage note); the curve
|
||
table scrolls inside its card, so a long session no longer stretches the page. Live data: ABRP key
|
||
and token side by side, Save / Test / Enter on phone in one row. Settings: save buttons beside
|
||
their fields, charging price under Battery, "Enter on phone" in a card of its own above App. Wide
|
||
pages use 12 dp top padding. With real data the value texts are longer; not tried on the car.
|
||
- 2026-10-18 — App 2.18.0: Settings → App → "Install from USB stick" (car build only): the newest
|
||
`NilsMG4-unstable-<version>.apk` newer than the installed one in a stick's top folder, checked
|
||
against its `.sha256` and the package, installed with the OTA installer session. Tested on the
|
||
emulator: 2.18.0 installed a 2.18.1 from the storage's top folder silently, as the system user —
|
||
so the OTA install step should not need a confirmation either.
|
||
- 2026-10-18 — App 2.17.5: Server tab fits the car's window. The "Your private connection" card is
|
||
gone (its text was out of date); Import from file sits with Save and Test in one row; the
|
||
installation ID is shown as text, not edited, since it is derived from the head unit (a config
|
||
file can still carry another one over). The subtitle no longer says the server forwards to ABRP.
|
||
- 2026-10-18 — App 2.17.3: the Overview panel "Energy and temperature" is "Vehicle info" (2.17.4; "Odometer and climate" in 2.17.3):
|
||
it only holds those two, since the car publishes no pack energy or temperatures.
|
||
- 2026-10-18 — App 2.17.2: Wi-Fi auto-connect removed with its two permissions (ACCESS_ and
|
||
CHANGE_WIFI_STATE). Upstream used it to keep the head unit on a phone hotspot; this car's head
|
||
unit has its own connection.
|
||
- 2026-10-18 — App 2.17.1: Settings without the Wi-Fi and saved-networks cards, in three columns
|
||
(display and steering wheel | battery | charging price and app). Logs keeps only the upload log: the steering-key
|
||
reports, the car-colour probe and the screen line are gone, their questions answered.
|
||
- 2026-10-18 — App 2.17.0: "Speed, 5-min average" card under Speed. The top bar is one compact line
|
||
(title and subtitle); version, update check and About moved to an App card on the Settings tab.
|
||
The Overview fits the car's window without scrolling: checked on the emulator at 1778×640, the
|
||
layout height the car reports (less page padding, a lower tyre card).
|
||
- 2026-10-18 — App 2.16.4: drop-downs again, as Material exposed dropdown menus: their list leaves
|
||
the focus with the activity, so full screen stays (a Spinner's list is a focusable window of its
|
||
own without the full-screen flags). 2.16.3 had replaced them with buttons.
|
||
- 2026-10-18 — App 2.16.3: full screen stays while choosing or reading a dialog. Drop-downs (upload
|
||
frequency, tyre unit, accent colour) became buttons on the page: a drop-down is a window of its
|
||
own without the full-screen flags, so the car showed its bars while it was open. Dialogs are
|
||
shown unfocusable, given the activity's full-screen flags, then allowed focus
|
||
(`showKeepingFullScreen`); none of them takes typing.
|
||
- 2026-10-18 — From the car (2.16.2): screen 1920×720 px at 160 dpi with a 1778×720 px app
|
||
window, so EVProfile's 1280×480 does not apply; colour probe found only `vehicle_type = AS28`.
|
||
FIRMWARE.md, mise.toml and SPECS.md say so.
|
||
- 2026-10-18 — App 2.16.2: Logs → SCREEN shows the panel's pixels and density and the app
|
||
window in px and dp. The MG4's display figures disagree (1920×720 px used here; EVProfile lists
|
||
1280×480, which is 1920×720 at 240 dpi in dp; upstream reported a 1778×720 window); FIRMWARE.md
|
||
lists all three until a photo of that line settles it.
|
||
- 2026-10-18 — EVProfile's licence checked: MIT, forked from SliDeeN/MG4Control and last merged
|
||
from it on 2026-08-02, inside MG4Control's MIT period (MIT up to 2.6.5 of 2026-08-05, GPL v3
|
||
from 2.6.6 of 2026-09-02). Recorded in `LICENSE.md`. History scanned for secrets before the
|
||
repository went public: none.
|
||
- 2026-10-18 — Licence: Leon Kernan's `ABRP_Uploader` has been MIT since 2026-07-29 (its commit
|
||
`75d8f2bc`), so the "unresolved" notes are gone. `LICENSE` names every copyright holder in the
|
||
fork chain; `LICENSE.md` explains that `EVHardware/` is PolyForm Noncommercial, which makes the
|
||
built APK non-commercial. README, AGENTS.md and CONTRIBUTING.md updated.
|
||
- 2026-10-18 — App 2.16.1: the Overview reads the car every 0.5 s (was 2 s), on a background
|
||
thread; the consumption and charging trackers stay on the 2-second tick (they integrate over
|
||
time, and the charging tracker writes its session to storage on every sample). Note for
|
||
emulator tests: uiautomator can no longer dump the screen while the Overview is open, since
|
||
the UI is never idle.
|
||
- 2026-10-18 — Grafana's read-only InfluxDB token without a manual step: an init container in
|
||
the Grafana pod creates it with the admin token at every start, deletes the previous one and
|
||
leaves only the read-only token in a memory volume; the datasource reads it with
|
||
`$__file{/run/influx/token}`. InfluxDB ignores chosen token values (tested), so it cannot come
|
||
from the Secret. The `grafana-influx` Secret is gone. Tested in podman: InfluxDB 2.7.12, the
|
||
script as the Grafana user (twice: rotates, no pile-up), Grafana 13.2.3 datasource healthy.
|
||
- 2026-10-18 — App 2.16.0, server 2.6.0: over-the-air updates from this repository. `publish-car`
|
||
creates the pre-release `car-<version>` on the private forge with the APK and its `.sha256`;
|
||
the server relays releases and attachments to the car (`/v1/app/…`, read with a read-only
|
||
`FORGE_TOKEN`); the app checks them through its server with its own token, allows exactly the
|
||
server's host, downloads itself (no DownloadManager, so the token stays in the app), verifies
|
||
SHA-256 and package, and tries a PackageInstaller session before falling back to Downloads.
|
||
`OTA.md` rewritten for this flow. Not yet tried on the car.
|
||
- 2026-10-18 — App 2.15.0: unsent live samples survive a restart. The queue is saved to
|
||
`files/telemetry_queue.json` (AtomicFile) at most every 5 minutes and only after a change, and
|
||
at once when the service stops; it is restored, in order and within its 2000-sample cap, when
|
||
the service starts. A killed process loses at most 5 minutes. Tested on the emulator across a
|
||
reboot (6 queued samples back).
|
||
- 2026-10-18 — App 2.14.4: every UI text is in `strings.xml` (Charging, Consumption and Settings
|
||
labels, toasts, the trip-reset dialog, charging and Wi-Fi states, the daily summary, the "—"
|
||
placeholder); lint reports no hard-coded text. Upload-log entries stay in code as log text.
|
||
- 2026-10-18 — Translated the rest: `mise.toml`, the comments in `app/build.gradle`, `OTA.md`'s
|
||
message table (now the updater's current English messages), the Turkish messages quoted in
|
||
`CHANGELOG.md` and `OTA.md` (glossed), two test messages, the ignition-state comment in
|
||
`EVHardware.kt`.
|
||
- 2026-10-18 — About dialog: link and QR code point to https://git.stinnesbeck.com/nils/mg4
|
||
(were Fatih's GitHub). Status section brought up to date.
|
||
- 2026-10-18 — App 2.14.2: "Range, 5-min average", like the usage and power cards (was "Range at 5-min average").
|
||
- 2026-10-18 — App 2.14.1: Overview values and their notes right-aligned, with tabular
|
||
(equal-width) digits, so 9 → 10 → 100 no longer shifts the text; labels stay left.
|
||
- 2026-10-18 — App 2.14.0: accent colour selectable after the MG4 paints (Settings → Display):
|
||
Dynamic Red (default), Volcano Orange, Holborn Blue, Racing Green, Camden Grey, Monument Silver,
|
||
Arctic White, Black Pearl; white, silver and black become light greys and a pearl blue-grey,
|
||
since they cannot be an accent on black. The accent palette is theme attributes
|
||
(`attrs_accent.xml`, overlays in `accents.xml`); a change recreates the activity on the same
|
||
page. Android 9 crashes on gradients with theme colours, so the battery card is layered.
|
||
Logs tab: "Look for the car colour" lists system properties that mention colour, paint or car
|
||
model, to find out whether the paint colour can be read (nothing on the emulator).
|
||
- 2026-10-18 — App 2.13.0: tyre pressures in bar by default, kPa or psi selectable (Settings →
|
||
Display; display only, the car and the server keep kPa). "Power, 5-min average" card under
|
||
Power: time-weighted mean battery power, standing included.
|
||
- 2026-10-18 — App 2.12.1: no scroll bars (the 2-second refresh kept them from ever fading);
|
||
"Energy since start" tile (net kWh of the trip, recuperation counted back) left of "Usage since
|
||
start".
|
||
- 2026-10-18 — App 2.12.0, server 2.5.0: settings entered on a phone. "Enter on phone" shows a QR
|
||
code; the phone form on the server (approval password) takes the ABRP credentials, charging
|
||
price and usable capacity, and the car collects them once (RFC 8628-style, like pairing; values
|
||
in server memory only, at most 10 minutes). The config file also takes `charging_price` and
|
||
`usable_kwh`. The QR dialog is shared with pairing (`PhoneQrDialog`). Tested end to end with
|
||
curl against the server image and InfluxDB; the app side on the emulator only up to the dialog.
|
||
- 2026-10-18 — App 2.11.1, server 2.4.0: ABRP forwarding removed from the server. The car and the
|
||
server use the same mobile connection, so a server fallback for ABRP bought nothing. The car no
|
||
longer sends `abrpSentUtc`; the server ignores unknown fields, so older apps keep working.
|
||
- 2026-10-18 — App 2.11.0, server 2.3.0:
|
||
- Live data goes to ABRP straight from the car when ABRP credentials are set in the app
|
||
(Live data tab or config file: `abrp_api_key`, `abrp_token`), so ABRP keeps getting it
|
||
while the server is down. The car tells the server the newest sample ABRP took
|
||
(`abrpSentUtc`); the server forwards only newer ones. Without credentials in the app the
|
||
server forwards as before. This deliberately puts ABRP credentials in the car again
|
||
(encrypted, never logged).
|
||
- Settings regrouped: Live data (on/off, ABRP, frequency), Server, Settings (full screen, voice
|
||
key, usable capacity and SoH from Consumption, charging price from Charging, Wi-Fi, which
|
||
lost its own tab). Charging and Live data have their own icons.
|
||
- 2026-10-18 — App 2.10.2: the range shows "∞" instead of "> 999 km" while recuperating.
|
||
- 2026-10-18 — App 2.10.1: net regeneration (recuperating downhill) shows as negative usage, and
|
||
the range as "> 999 km" with "while recuperating", instead of "needs a few minutes of driving".
|
||
- 2026-10-18 — App 2.10.0: "Range since start" replaces the range at current power. It uses
|
||
the consumption since the car was switched on, including standing with the climate on, and a
|
||
new trip starts after 10 minutes without samples or when charging. Usage (kWh/100 km) for both
|
||
estimates moved into cards of their own under the range tiles, which fit on the full-screen
|
||
1920x720 overview without scrolling.
|
||
- 2026-10-18 — Code review fixes. App 2.9.0, server 2.2.0, Grafana token:
|
||
- The server drops a bad value (or a sample without a plausible time) instead of rejecting
|
||
the batch, and answers 200 with what it dropped; the app sends only values within the
|
||
server's ranges (speed as a magnitude) and no samples before its clock is set. Before, one
|
||
reversing sample cost the whole queued batch.
|
||
- Pairing: the token is handed out and accepted only after the device is saved; wrong-password
|
||
lockouts and open-request limits are per client address; a paired token only serves its
|
||
own installation id (403 otherwise).
|
||
- ABRP forwarding skips samples stamped ahead of the server's clock, which used to block it.
|
||
- App: settings written in plaintext during a keystore failure move back into encrypted
|
||
storage; the installation id is derived from ANDROID_ID, so a reinstall keeps its history;
|
||
the imported config file is deleted; GPS stops when live telemetry is off and pauses while
|
||
parked and idle; range estimates use the monotonic clock; one HTTP helper (`ServerHttp`)
|
||
for all server calls; the last Turkish strings translated.
|
||
- Grafana reads InfluxDB with its own read-only token (`grafana-influx` Secret, created by
|
||
hand: `k8s/README.md`).
|
||
- 2026-10-18 — README rewritten for Nils' MG4. `OTA.md`, `mise.toml` and
|
||
`docs/MG4_PLATFORM_INSTALL.md` use the new package ID and `NilsMG4-*.apk` names; `OTA.md` notes
|
||
that updates are switched off. `CHANGELOG.md` keeps the old ID as upstream history.
|
||
- 2026-10-18 — App 2.8.2: full screen uses the whole display. The MG4 hid its bars but left a black
|
||
band where they were: LAYOUT_STABLE kept reporting the bars' insets and the fitsSystemWindows root
|
||
padded for them. Both are off in full screen now.
|
||
- 2026-10-17 — App 2.8.1: three-part version names (`2.8.1-unstable`, `NilsMG4-unstable-2.8.1.apk`).
|
||
Upstream's fourth part was its CI build number; the version code keeps the x100000 factor so it
|
||
never falls below codes already installed.
|
||
- 2026-10-17 — App 2.8.0: immersive full screen (switch in the Upload tab, default on). Hides the
|
||
system bars on the emulator; whether SAIC's own bars on the MG4 can be hidden is only known once
|
||
tried on the car.
|
||
- 2026-10-17 — App 2.7.1: the voice key works. The car's key report, read off the on-screen log:
|
||
`android.intent.extra.hardkey.keycode` (Integer), `.down` (Boolean, a report per press and per
|
||
release), `.longpress` (Boolean). 2.7.0 looked only for short names and never found the code.
|
||
Floating button removed (it did not show over the MG4 launcher).
|
||
- 2026-10-17 — App 2.7.0: a double press of the steering wheel's voice key (code 287) opens the
|
||
app, from the head unit's `com.saic.keyevent.hardkey.report` broadcast. Its extras are not
|
||
documented; the usual names are tried and every report is listed raw on the Logs tab. The
|
||
floating button (2.6.0) did not show over the MG4 launcher. Pages no longer jump back to the
|
||
top on every refresh (`StableScrollView`), and the Vehicle tab shows all six value tiles,
|
||
including both range estimates, in one row at the top.
|
||
- 2026-10-17 — App 2.6.0: floating button to open the app, because the MG4 launcher does not list
|
||
it. Drawn over other apps (needs the overlay permission, which the car build has through the
|
||
system UID), draggable, hidden while the app is open, switch in the Upload tab. Tested on an
|
||
Android 9 AOSP emulator with the car APK (installs as UID 1000 there too), at 1920x1080 and at the
|
||
head unit's real 1920x720 / 160 dpi, where all tabs and the button were checked as well.
|
||
- 2026-10-17 — App 2.5.0: range estimates at current and 5-minute average power (Vehicle tab).
|
||
- 2026-10-17 — Live dashboard reworked around the signals the car sends: Climate setpoint, Last
|
||
update and a computed Energy in pack replace the empty temperature tiles; Battery voltage and
|
||
current replaces the Temperatures chart.
|
||
- 2026-10-17 — Temporary on-screen diagnostics (b2a5b32) to debug blank values without adb;
|
||
removed again (1002962) once values showed on the car. Revert 1002962 to bring them back.
|
||
|
||
## Installing on the car
|
||
|
||
```
|
||
nix develop
|
||
build-car
|
||
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"
|
||
```
|
||
|
||
To remove the old upstream app: `adb uninstall com.evsuite.abrp.unstable`.
|