Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
1080 lines
104 KiB
Markdown
1080 lines
104 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. Not yet tried: listening
|
||
(subscribing) instead of polling, as the head unit's own manager does — the recorder does it
|
||
since 2.40.3; a drive with ACC will tell.
|
||
|
||
## 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-11-07 — App 2.49.1 published (`car-2.49.1`).
|
||
- 2026-11-07 — App 2.49.1: **the Caterpillar looks more like the one in the book** (owner: "Make it more like the original caterpillar", with the German edition's cover as the reference). It arches over the middle like the cover's: overlapping chunks of mottled green and teal paper, a yellow fringe along the back, olive feet, and a red-orange head with a yellow face, green eyes and purple antennae; the speed is on the face. Power still lights the segments (blue when regenerating) and the back humps while driving. Still my own drawing of it, not the book's artwork. Two screenshots redone. Tested: unit tests, lint, emulator with sample values. Not tried on the car.
|
||
- 2026-11-07 — App 2.49.0 published (`car-2.49.0`).
|
||
- 2026-11-07 — App 2.49.0: **the Caterpillar style** (owner: "a theme based on The Very Hungry Caterpillar"; design agreed first, defaults taken: dark night page, named "Caterpillar" not the book's title, fruit as range). `CaterpillarView`, an original cut-paper collage, not the book's illustrations: the caterpillar's red head is the speed and its ten segments the power (blue while regenerating, wiggling while driving); the sun is the battery (a grey moon below 20 %); a leaf holds trip, usage, climate and odometer; a clock in a paper circle; fruit in the story's order (1 apple, 2 pears, 3 plums, 4 strawberries, 5 oranges) is the range, one per 50 km, used ones with a bite out; P R N D are an egg, the caterpillar, a cocoon and a butterfly; hills are the last two minutes of power. The music light (now also for this style) bounces the sun's rays and the segments on the kick drum. What plays: a butterfly and, on the next pass, a ladybird tow a strip of tissue paper (colour fixed by the song); both in `SongCarriers`. Own palette (green). Two screenshots in THEMES.md. Tested: unit tests, lint, emulator with sample values and with the throwaway fake song. Not tried on the car.
|
||
- 2026-11-07 — App 2.48.0 published (`car-2.48.0`).
|
||
- 2026-11-07 — App 2.48.0: **the Christmas style** (owner: "let's also do a christmas based theme", with music and something that pulsates; design agreed first). `ChristmasView`: a tree with a big red bauble (speed), fairy lights arching over it (power: gold left to right while drawing, ice blue and right to left while regenerating), a glass mug of cocoa filling with the charge (cold blue and "BATTERY LOW" below 20 %), gift tags (trip, usage, climate, odometer), a wreath clock, a signboard (range) with a candy cane per 50 km, P R N D as baubles, and a snow drift of the last two minutes of power; snow falls and the lights twinkle while driving. Own palette (red); always in the drop-down; English only. The music light is now **"Light follows the music"** for Halloween and Christmas (same `PREF_MUSIC_LIGHT`, off by default): the tree's lights and the arch flare on the kick drum. What plays: Santa's sleigh with a reindeer and, on the next pass, an angel with a trumpet tow a strip of wrapping paper (colour fixed by the song); both in `SongCarriers`. Two screenshots in THEMES.md. Tested: unit tests, lint, emulator with sample values (panel and full screen, and with no data), with the throwaway fake song (both carriers), and the music-tap thread runs only with the switch on. Not tried on the emulator: the pulse itself with real sound (the code path is Halloween's `MusicTap`). Not tried on the car.
|
||
- 2026-11-02 — App 2.47.0 published (`car-2.47.0`).
|
||
- 2026-11-02 — App 2.47.0: **now playing in four more styles** (owner: "let's implement Now playing in more styles"; Spanish labels declined). Quest: a red dragon tows a waving scroll across the top of the sky, behind the frames. Empire: an eagle flies a scroll across the top of the world, behind the speed. Terminal: a PLAYING row above the prompt. Sci-fi: the song in black on the top bar. The creatures are in the new `SongCarriers` (dragon, eagle) beside `SongBanner`. Same switch as before (Settings → "Show what's playing"). Not yet in the rest: Gauges, Cockpit, Retro, Futuristic, MG, Minimal, Nixie, Chronograph, Grid, Grid with map, Time circuits, Visor, Cartoon. Tested: unit tests, lint, emulator with the throwaway fake session. Not tried on the car.
|
||
- 2026-11-02 — App 2.46.0 published (`car-2.46.0`).
|
||
- 2026-11-02 — App 2.46.0: **Día de Muertos shows what plays** (owner: "Can we also put that into the dia de muertos theme?", then "switch between monarch and alebrije", "one color per song"). A strip of papel picado (zigzag-cut edges) with "♪ title — artist" waves across the sky between the flags and the ring, behind the ring and the clock; a monarch butterfly and an alebrije (a painted horned creature with feathered wings and a curled tail) tow it on alternate passes. The paper's colour is one of the flags', fixed by the song (hash of title and artist, never logged). The banner moved out of `HalloweenView` into the shared `SongBanner`; Halloween looks as before. Same switch as Tiles and Halloween (Settings → "Show what's playing"). Tested: unit tests, lint, emulator with the throwaway fake session. Not tried on the car.
|
||
- 2026-11-02 — App 2.45.2 published (`car-2.45.2`).
|
||
- 2026-11-02 — App 2.45.2: Halloween's song banner **waves** (owner: "Can it also be a bit wavy?"): waves run from the bat to the tail, still where the bat holds it and up to 7 units at the tail, and the text follows them (`drawTextOnPath`). Tested: emulator with the throwaway fake session, unit tests, lint. Not tried on the car.
|
||
- 2026-11-02 — App 2.45.1 published (`car-2.45.1`).
|
||
- 2026-11-02 — App 2.45.1: **Halloween's song flies through the sky** (owner: "Instead of taking up screen space on the bottom, can we somehow include the title in the theme itself? ... have text fly through the air"). A bat tows a parchment banner with "♪ title — artist" from left to right across the strip of sky above the sign, behind the moon, one pass about every half minute, again and again while something plays (the title is cut first so the artist stays); the line at the foot of the hill is gone. While a banner flies the view draws about 30 frames a second, as it does while driving. Tested: emulator with the throwaway fake session (banner clear of the sign along the whole pass), unit tests, lint. Not tried on the car.
|
||
- 2026-11-02 — App 2.45.0 published (`car-2.45.0`).
|
||
- 2026-11-02 — App 2.45.0: **now playing on the Overview** (owner: "can we also implement music titles etc in our app?"; display only, Tiles and Halloween first). `NowPlayingWatcher` follows the active media sessions as the music-sources check reads them (`MediaSessionManager`, MEDIA_CONTENT_CONTROL as the system user), with a change listener and a callback per session, so reading costs no call into another app; it never connects to a media browser service (2.42.0) and sends no command. It runs only while the Overview is on screen in Tiles or Halloween with Settings → "Show what's playing" on (**on by default**, `PREF_NOW_PLAYING`). `NowPlaying` (pure, unit-tested) picks the session that plays, the one updated last of several, else the one paused last, passing over sessions without text (Bluetooth's empty one); CarPlay's YouTube titles lose their `[videoID]`; the radio shows its text (the artist field, e.g. "BLIND MELON - NO RAIN") with the station below. Tiles: the empty slot under the battery shows source ("· paused" when paused), title, artist, a cover (the session's picture, or Bluetooth's `ALBUM_ART_URI` file, read small off the main thread) and how far the song is; invisible when nothing plays; the narrow layout has no room and shows none. Halloween: "♪ title — artist" along the foot of the hill, the title cut first. Titles stay on screen: not logged, not sent. Tested: unit tests, lint, emulator with a throwaway build publishing a fake session (CarPlay-like title with ID removed, cover, bar; Halloween line; the switch hides and brings back both). With Halloween chosen the Settings card has two switches more and scrolls further. Not tried on the car: whether the car's sessions, Bluetooth's cover file and the radio text show as in the music-sources checks.
|
||
- 2026-11-02 — App 2.44.0 published (`car-2.44.0`).
|
||
- 2026-11-02 — App 2.44.0: **the pumpkin light follows the bass, and only when switched on** (owner: "more focussed on the bass", "optional via a settings flag"). `MusicPulse` now reads the kick drum's band (40–120 Hz, was 20–250 Hz) and lights only on a jump since the last reading, against the biggest jumps of the last 4 s; a jump under a fifth of that, or under half the bass's average level, lights nothing. So kicks flash, and a held note, a wobbling bass line or a swelling note stay dark (unit tests with made-up patterns). It flares at once and falls in 0.2 s. New switch under the Overview style, shown only for Halloween: "Pumpkin light follows the music" (`PREF_MUSIC_LIGHT`), **off by default**; off, the analyser never starts. Tuned on made-up signals only: no Bluetooth or CarPlay sound report from the car has arrived yet. Next: now playing (title, artist, source) from the media sessions, in its own version. Tested: unit tests, lint, emulator (switch off by default and the analyser thread absent; on, it runs only on the Overview; a throwaway build playing a pulsing test tone: the glow flares with the switch on, and stays exactly still with it off). With Halloween chosen, the Settings card now scrolls a little to reach the tyre unit. Not tried on the car.
|
||
- 2026-11-01 — App 2.43.5 published (`car-2.43.5`).
|
||
- 2026-11-01 — App 2.43.5: **update checks log what happens** (`UpdateNetworkCheck`, `UpdateDiagnosis`; owner: updating from the car fails, "Can we log what happens? Maybe my SIM card has no data left"). Each check on Settings writes to the Logs tab: the networks Android sees (type, in use, internet, validated, roaming), whether the server's name resolves, how a failure happened, in words from its type or status only, and how many reports wait. On a failure it also asks `https://www.gstatic.com/generate_204`, the one request to anyone but the car server, to tell no data from a server that is down. A report goes to a USB stick and, when online, the server (`update-check-*.txt`; only the newest waits, so it cannot push out a recording). A check at start logs only a failure. The update log keeps 40 lines, not 10. An out-of-data SIM would also explain why the Bluetooth and CarPlay sound reports did not arrive; not confirmed. Tested: unit tests, lint, emulator with a fake server (online: the name fails, the internet test says it works; airplane mode: both fail, "none in use"; the real build installs and starts, its check not run there, having no server). Not tried on the car.
|
||
- 2026-11-01 — App 2.43.4 published (`car-2.43.4`).
|
||
- 2026-11-01 — App 2.43.4: **the Halloween pumpkin's light follows the music** (`MusicTap`, `MusicPulse`; owner: "make the light in the pumpkin react to the equalizer/sound of the music"). The bass, 25 times a second off the main thread, drives the glow and brightens the carving on the beat; yellow and green still mean drawing and regenerating, the embers still the power, and no signal leaves the light as it was. It listens only with the app in front, on the Overview, in Halloween. The owner reports the sound check worked with Bluetooth and CarPlay; those two reports never reached the server (why not is unexplained), so their levels are unseen and the pulse's feel is untuned for the car. Tested: unit tests, lint, emulator (a tone already playing when the analyser starts: the carving and glow move with it; no sound: unchanged; the analyser is released on another tab and when the app leaves the front; no crash). Not tried on the car; how it looks there, and its effect on the head unit over a long drive, are unknown.
|
||
- 2026-10-31 — Sound check on the car (2.43.3), radio: `com.saicmotor.radio` playing, `isMusicActive` false, analyser enabled without error and silent for all 10 seconds (peak 0.00, -9600 mB). The audio analyser does not hear the radio. The owner ran Bluetooth and CarPlay too; those reports have not reached the server yet (the outbox sends oldest first and stops at the first failure, retrying while the service runs).
|
||
- 2026-10-31 — App 2.43.3 published (`car-2.43.3`).
|
||
- 2026-10-31 — Music checks from the car: the radio publishes a session while it plays (`com.saicmotor.radio`, 29 October, DAB: TITLE the station, ARTIST the programme text, AUTHOR the band), with `isMusicActive` false, so its sound may not pass through Android's mix and the audio analyser may not hear it. The sound check with the radio playing will tell. CarPlay playing (31 October, 2.43.2) has `isMusicActive` true, like Bluetooth on 28 October. The sound check itself has not run on the car yet: 2.43.3 was not published and those reports came from 2.43.2's music-sources check.
|
||
- 2026-10-31 — App 2.43.3: **sound check** on the Logs tab (`SoundProbe`, `SoundLevels`), asked for before the Halloween pumpkin's light follows the music (owner: "make the light in the pumpkin react to the equalizer/sound of the music"). One tap: 10 seconds of levels from `Visualizer` on session 0 (peak, bass, mid, treble, rms), saved and sent like a recording; no sound kept, no microphone. New in the car build's manifest: RECORD_AUDIO and MODIFY_AUDIO_SETTINGS. Grounds for trying: Bluetooth played with `isMusicActive` true (28 October check), `libCarPlayPlatformAudio.so` uses AudioTrack and no direct PCM. Whether the analyser hears anything on the car is what this is to find out. Tested: unit tests, lint, emulator (silence says nothing was playing; a pulsing 110 Hz + 1 kHz test tone from another app is heard, peak 0.27–0.91, bass strongest, the tone kept playing, no prompt, no crash). Not tried on the car.
|
||
- 2026-10-31 — App 2.43.2 published (`car-2.43.2`).
|
||
- 2026-10-31 — App 2.43.2: Halloween's teeth are triangles hanging from the grin's upper lip, each in the colour of the rib behind it (the right one was the darker orange on the lighter rib; owner: "the color on the right side is off"). Two screenshots redone. Tested: unit tests, lint, emulator with sample values (drawing in panel and full screen, regenerating in full screen) and with no data. Not tried on the car.
|
||
- 2026-10-31 — App 2.43.1 published (`car-2.43.1`).
|
||
- 2026-10-31 — App 2.43.1: Halloween's bats can be seen. They were near-black and small on the darkest part of the sky, and the owner did not spot them; now a moonlit purple-grey with a light edge, a third larger. Two screenshots redone. Tested: unit tests, lint, emulator with sample values (panel and full screen) and with no data (no bats). Not tried on the car.
|
||
- 2026-10-31 — App 2.43.0 published (`car-2.43.0`).
|
||
- 2026-10-31 — App 2.43.0: Overview styles **Día de Muertos** (`MuertosView`) and **Halloween**
|
||
(`HalloweenView`; owner: "let's design a 'dia de muertos' and a 'halloween' style"), each with
|
||
its own palette (the papel picado's pink, the pumpkin's orange). Día de Muertos, an ofrenda and
|
||
kept a celebration: papel picado = battery (one flag per 10 %), calavera with the speed on its
|
||
forehead, a ring of marigolds = power (turquoise and anticlockwise regenerating), candles =
|
||
range (one per 50 km), a path of petals = the last two minutes of power. Halloween: a
|
||
jack-o'-lantern with the speed in its grin, burning yellow or, regenerating, green; an arch of
|
||
embers = power; a potion flask = battery; a tombstone and bats = range; the hill = the last two
|
||
minutes of power. What a value lights is `Tally` (pure, unit-tested: an unread value lights
|
||
nothing). The static scene is drawn once per size into a bitmap (about 4.5 MB while the view is
|
||
attached), so a frame only draws the readings; flags, flames, bats, bubbles and the ghost move
|
||
only while the car moves. Tests and lint pass. Emulator with sample values, panel and full
|
||
screen: normal, 12 % with regeneration, 40 % (the flask's words over the potion's surface); real
|
||
build with no data: "—" and nothing lit. No crash of the app. Not tried on the car, so the frame
|
||
rate on the head unit is unmeasured.
|
||
- 2026-10-28 — App 2.42.1 published (`car-2.42.1`).
|
||
- 2026-10-28 — App 2.42.1: **the music-sources check no longer connects to other apps' media
|
||
services.** In the car, 2.42.0's check (14:26, Bluetooth music playing) made the head unit ask
|
||
whether to allow access to media (the owner agreed) and **stopped the Bluetooth music**: it
|
||
connected to every media browser service, and among them the AOSP car radio's
|
||
(`com.android.car.radio/.RadioService`, which answered "connecting", most likely taking the
|
||
audio) and the Local Media Player (`com.android.car.media.localmediaplayer`, whose permission
|
||
request is most likely the question; it may now read media files). My claim that the check only
|
||
read was wrong: binding another app's service starts it. Now the services are only listed.
|
||
Nothing lost: the active sessions alone carried everything — **Bluetooth**
|
||
(`com.android.bluetooth`: title, artist, album, duration, track, cover as a file path, playing)
|
||
and **CarPlay** (`com.allgo.remoteui.mediabrowserservice`: title, artist, duration, paused) —
|
||
readable with MEDIA_CONTENT_CONTROL, no connection needed. Services on the car: CarPlay's,
|
||
Bluetooth A2DP, Local Media Player, the AOSP car radio, SAIC's radio (DAB, "Brillux Radio"),
|
||
SAIC's USB 1/2 and online media, and SAIC's video browser. Tests and lint pass; not run on the
|
||
emulator (stopped for low memory; the change only removes code); not tried on the car.
|
||
- 2026-10-28 — App 2.42.0 published (`car-2.42.0`).
|
||
- 2026-10-28 — App 2.42.0: **music-sources check** on the Logs tab (`MediaProbe`,
|
||
`MediaProbeText`; owner: "is it possible to show playing music in our dashboard as well?", then
|
||
"yes start the probe"). Before a now-playing display, it finds where the head unit tells what is
|
||
playing, read only: the players Android's audio service reports (use, app, state), the active
|
||
media sessions (`MediaSessionManager.getActiveSessions`, MEDIA_CONTENT_CONTROL, which the system
|
||
user has) with their song, and every media browser service, each connected to, its session read
|
||
and disconnected. CarPlay's is tried even when not listed:
|
||
`com.allgo.remoteui.mediabrowserservice/.RemoteUIMediaBrowserService`, which the car's own media
|
||
library (`com.saicmotor.sdk.media…MediaPlayControlManager` in Allgo's APK) reads CarPlay's title,
|
||
artist, album and cover from through a plain `MediaBrowser`. Nothing of Allgo's is changed and
|
||
nothing is played, paused or skipped. The report (`media-probe-YYYYMMDD-HHMMSS.txt`) goes where
|
||
a recording goes, through the shared `SignalRecorder.saveCopies`: the app, a USB stick, the
|
||
server (`fetch-logs`). Unit tests for the text; tests and lint pass. Emulator (Android 9): the
|
||
players are read with their app and state (the system APIs answer the system user), the
|
||
sessions are readable (none there, no SecurityException), the AOSP music app's browser service
|
||
connects and is read, CarPlay's (not installed there) is "refused or failed", and the report
|
||
lands in the outbox. **Not tried on the car.** Unknown until then: whether CarPlay's service
|
||
lets the app connect, and whether the radio, Bluetooth and USB sources publish a session.
|
||
- 2026-10-28 — First recordings over the server (14:19 and 14:23, app 2.41.0, ACC on, the set
|
||
speed raised a few times): the server path works, and listening works in the car (249
|
||
assistance properties, 0 refused, 76 and 81 change events). `0x2160A15B` (`AAD_EPTCCTRGTSPD`)
|
||
stays 0.0 even while listened to: the car does not fill it. `0x2140A15A` (3/7, a few 0s) and
|
||
`0x2160A1A3`/`A1A4` (always equal, 1.4–5.3, now and then 0.1–0.2 down) do not move with the
|
||
key presses. New lead: steering-wheel keys **297** (2× at 14:20:09, 2× at 14:20:12) and **300**
|
||
(14:20:05, 2× at 14:20:40), the car speeding up 27 → 49 km/h after them; key 3 is Home. The
|
||
14:23 recording has no key at all. Not confirmed: which buttons these are. Also seen:
|
||
`0x21401553`–`1556` ≈ 256 are tyre pressures and `0x2140155B`–`155E` (18–24) tyre
|
||
temperatures, not the set speed.
|
||
- 2026-10-27 — Skill `car-logs` (`.claude/skills/car-logs/SKILL.md`; owner: "can we write a skill for downloading the logs?"): fetch with `fetch-logs` without ever showing the token, what its errors mean, how a recording is laid out, the cuts that find a lead, the leads so far, and that recordings are never committed. Its commands tried on the 08:52 recording.
|
||
- 2026-10-27 — Server 2.9.0 pushed (`sha256:c68837f0e53cfca150644987be535e04d5e999bd4c91064d40973db67235df48`, pinned in `k8s/server.yaml`); app 2.41.0 published (`car-2.41.0`).
|
||
- 2026-10-27 — App 2.41.0, server 2.9.0: **recordings reach the server** (owner: "can we make
|
||
it so that we can upload the logs to an endpoint in the server, and you can get the logs from
|
||
there? that way i don't have to carry a usb stick"). A finished signal recording goes to
|
||
`files/log-outbox` (`LogOutbox`, at most 10 waiting) and is sent at once, gzipped, with the car's
|
||
token: `PUT /v1/logs/{name}`. Offline, 401 (not paired yet), 404 (a server without logs), 408,
|
||
429 or 5xx keep it, and the service's tick sends it again, at most every 2 minutes; any other
|
||
refusal drops it. The Logs tab says "Sent to the server." or how many wait and why. Server
|
||
(`logs.go`): `LOGS_DIR` on a new 1Gi volume `mg4-server-logs`, the newest 200 files and none
|
||
older than 180 days, names as the app's only, up to 2 MB sent and 8 MB unpacked, UTF-8 only; the
|
||
same name replaces itself. Reading (`GET /v1/logs`, `GET /v1/logs/{name}`) takes `LOGS_TOKEN`, a
|
||
new optional Secret value that must differ from `INGEST_TOKEN`; the car's token cannot read, the
|
||
read token cannot write. `fetch-logs` in the dev shell fetches the new ones into `logs/`
|
||
(gitignored, as are the recordings copied into the checkout's top folder), token from
|
||
`~/.config/nilsmg4/logs-token`. Tested: server unit tests (both tokens, refusals, pruning,
|
||
config); the real server code in a harness on this machine took the 08:52 recording gzipped as
|
||
the app sends it (204), refused the car's token for reading (401), and `fetch-logs` brought it
|
||
back byte for byte, once. The app's own sending could not be pointed at that harness (the app
|
||
takes https only); on the emulator the recording lands in the outbox and the Logs tab says it
|
||
waits. Not tried on the car.
|
||
- 2026-10-27 — App 2.40.4 published (`car-2.40.4`).
|
||
- 2026-10-27 — App 2.40.4: **the recorder's listening works on Android 9**. Both recordings
|
||
from the car (08:49 and 08:52, 2.40.3) said "listening to the assistance properties: not
|
||
possible": `listenToProperties` looked only for `registerCallback`, Android 10's name; the car's
|
||
car library (Android 9) has `registerListener(CarPropertyEventListener, int, float)` and
|
||
`unregisterListener`, as the app's `CarPropertyAdapter` already knew. It now takes either, and
|
||
unsubscribes property by property (Android 9's one-argument `unregisterListener` removes
|
||
entries from the list it walks and would leave every other property subscribed). A failure now
|
||
says why (`EVHardware.listenProblem`: no car service, no way to subscribe, or the exception).
|
||
Tested with a stand-in manager of Android 9's shape (`PropertyListeningTest`). Correction to
|
||
2.40.3: the ignition listener it was compared with also looks only for `registerCallback`, so
|
||
it most likely never subscribes on the car either (it reads the state once at start); left as
|
||
it is. What the polled part of the two recordings shows of the block: `0x2160A15B` (by name
|
||
the target speed) 0.0 throughout; `0x2140A15A` between 3 and 7 every few captures;
|
||
`0x2160A1A3`/`0x2160A1A4` counting up by 0.1; speeds 8–33 km/h. Not tried on the car.
|
||
- 2026-10-27 — App 2.40.3 published (`car-2.40.3`; also carries 2.40.1 and 2.40.2).
|
||
- 2026-10-27 — App 2.40.3: **the signal recorder listens** (owner: "is there really nothing we
|
||
can do about the readout of the cruise control set point"). The 2.32.0 recording only polled.
|
||
The head unit's `CarAdvancedAssistedDrivingManager` never polls its 108 `AAD_*` properties: it
|
||
registers a listener for each (`registerListener(…, 0.0f)`, on change), and a vendor HAL may
|
||
fill a value from the car network only while someone listens — which would read as the
|
||
defaults we saw. EVHardware gains `listenToProperties` (read only: the CarPropertyManager's
|
||
`registerCallback` per admitted property, rate 0 for ON_CHANGE, 1 Hz within limits for
|
||
CONTINUOUS, one proxy, `unregisterCallback` on close) and `AssistBlock` (global vendor
|
||
properties `0x21x0A1xx`, which holds all 108 `AAD_*` ids; tested). The recorder subscribes to
|
||
that block for its 3 minutes; `SignalRecording.event` counts every event and puts value changes
|
||
on the timeline; the file and the Logs tab list the properties heard, those with changing
|
||
values first ("heard …"). Emulator (no car service): the recorder notes that listening is not
|
||
possible, records, saves the new section; no crash. The subscription itself is the same
|
||
reflection as the ignition listener that works on the car, but untried on the car.
|
||
- 2026-10-27 — App 2.40.2: **the map moves on the GPU** (owner: "is the map screen gpu
|
||
accelerated? it looks a bit slow on update"). It was hardware-rendered, but every frame (20 a
|
||
second) recomputed every road point on the main thread, rebuilt all paths, and drew the big
|
||
roads three times for the glow; and the car jumped with each GPS fix (1 Hz). Now the map is
|
||
its own view under the Grid (`MapLayerView`, `LAYER_TYPE_HARDWARE`): drawn once into a GPU
|
||
texture round an anchor and only redrawn when tiles come or go or the car is ~280 layout units
|
||
(≈230 m) from the anchor; each display frame (`postOnAnimation`, only while that style is on
|
||
screen and the map shows) sets its translation, pivot and rotation, which the GPU composites.
|
||
The car glides towards each fix (eased, snaps beyond 200 m), the heading eases; GridView over
|
||
the map no longer animates itself (only the readings redraw it), the "far roads fainter" sort
|
||
is a fixed dark gradient towards the top. Layer size on the car about 2500 px square (~25 MB).
|
||
Measured on the emulator: with SwiftShader (software GPU) old and new both ~57 ms a frame, and
|
||
with the host's Intel GPU (`-gpu host`) both run at the display rate (~18 ms) — the emulator
|
||
is too fast to show the car's slowness, so the gain on the head unit is reasoned, not
|
||
measured. Not tried on the car.
|
||
- 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`.
|