Codes read on the owner's car with 2.22.1: 1 P, 2 R, 3 N, 4 D. Gear
maps them (unit-tested); other codes show no letter, unread "—". The
temporary gear list on the Logs tab is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
The badge said "D" for every gear that was not P, so N and R read as D.
EVHardware now passes on the raw gear code; the badge shows P for 1,
no letter for codes not yet mapped, "—" when unread. The Logs tab lists
each change of the code with time and speed, to match R, N and D on
the car. Emulator only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
A third Overview style beside Tiles and Dashboard: rings for battery,
power (zero at the top, regeneration anticlockwise in green) with the
speed and gear inside, and 5-minute usage with a tick for the average
since start; below, the power of the last two minutes, the clock,
climate and odometer. Unread values light nothing and show "—"; the
trace leaves gaps (PowerTrace, unit-tested). Emulator only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
The car refuses the silent session for a non-system app; the fallback
installer screen could not read our FileProvider URI because grants
from the system user are refused. The car build now hands it a file
URI to the verified copy in Downloads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
Read-only diagnostic for CarPlay's resolution: window, SurfaceFlinger
and display state captured while CarPlay is in front, display-related
properties without identifiers, Allgo's APK and libraries and
CarPlay-named system files, written to the app's folder on the stick.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
Read-only: recent foreground apps (usage events), open tasks,
policy_control and the status bar height, for finding the CarPlay
package before a full-screen experiment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three-way merge from b595ccb, the commit Fatih copied, to upstream's
current state; Fatih's vendor reads (tyres, mileage, charge status,
SWI69) stay. LICENSE.md records the base for the next update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The phone pages' code field gets one inline script, allowed by its
SHA-256 hash in the CSP; a test checks the served script against the
header. Image pinned by digest in k8s/server.yaml (not applied).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Charging and Consumption get two-column wide layouts; Live data and
Settings are tightened (fields beside their buttons, button rows,
16 dp vertical card padding). Checked on the emulator at 1778x640.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Turns the FatihsMG4 fork into Nils' MG4: an English Android Automotive
app for the MG4 head unit, a Go car server with InfluxDB 2 and Grafana,
Kubernetes manifests, and updates over the air or from a USB stick.
AGENTS.md is the overview, SPECS.md the journal of every change.
App (com.stinnesbeck.nilsmg4, car build .unstable)
- English throughout; all UI text in strings.xml. Renamed with a new
package ID, NilsMG4-*.apk names, three-part versions; About links to
git.stinnesbeck.com/nils/mg4 and keeps the upstream credits.
- build-car (Nix flake) signs the car build with the AOSP platform key
the head unit trusts; nix run .#car-emulator gives a matching Android
9 emulator.
- Overview, fitting the car's 1778x720 window without scrolling:
battery, range, range/usage/energy since start, 5-minute averages of
range, usage, power and speed, vehicle info, tyre pressures in
bar/kPa/psi. Read every 0.5 s on a background thread; values
right-aligned with equal-width digits; recuperation as negative usage
and an infinite range.
- Head unit: full screen (kept with drop-downs and dialogs), voice-key
double press opens the app, selectable accent colour after the MG4
paints (Dynamic Red by default), no scroll bars.
- Tabs: Overview, Consumption, Charging, Live data (on/off, ABRP
credentials, frequency), Server (URL, QR pairing, token, read-only
installation ID, config import), Settings (display, battery,
charging price, update check, install from USB, About), Logs.
- Live data to the car server and straight to ABRP; only values within
the server's ranges; the queue survives restarts; GPS pauses while
parked and idle.
- Settings can be typed on a phone (QR code, form on the server) or
imported from a config file, which is deleted afterwards.
- Security: credentials in encrypted storage (moved back after a
keystore failure), installation ID derived from the head unit.
- Updates: from this repository's pre-releases through the car server,
or from a USB stick; SHA-256, package and signature checked; the
installer session ran silently on the emulator.
- Removed: Wi-Fi auto-connect (and its permissions), the floating
button, upstream's diagnostics.
Server (server/, Go 1.26, replaces upstream's Next.js app)
- Telemetry, consumption chunks and charging sessions in InfluxDB 2;
idempotent writes; a bad value or time drops only itself.
- Pairing by QR code approved on a phone; paired tokens bound to their
installation; lockouts and limits per client address.
- Settings from a phone (in memory only, at most 10 minutes, handed out
once); app releases relayed from the private forge with a read-only
FORGE_TOKEN. No ABRP forwarding: the car sends to ABRP itself.
Infrastructure (k8s/, plain manifests, applied by hand)
- InfluxDB 2.7.12, the server pinned by digest, Grafana 13.2.3 with
provisioned Live, Consumption and Charging dashboards (euro),
Traefik traefik-internal routes.
- Grafana's read-only InfluxDB token is made by an init container at
each start; the Grafana process never sees the admin token.
- Filled-in secrets only in the gitignored k8s/secrets/secrets.yaml.
Licence
- MIT throughout, like Leon Kernan's ABRP_Uploader (MIT since
2026-07-29) and the forks in between; LICENSE names every copyright
holder. EVHardware/ is PolyForm Noncommercial, so the built APK is
non-commercial. EVProfile's MG4Control lineage is from MG4Control's
MIT period (checked).
Deploying
1. Fill in k8s/secrets/secrets.yaml from the template (InfluxDB admin,
ingest token, approval password, read-only FORGE_TOKEN, Grafana
admin password).
2. kubectl apply -f k8s/namespace.yaml, then
kubectl apply -f k8s/secrets/secrets.yaml, then kubectl apply -k k8s/
3. Car: install the car build once by hand; after that build-car, then
publish-car (or copy the APK and .sha256 to a USB stick), then the
buttons on the Settings tab.
Testing
- App unit tests (stable and car build) and lint pass; server tests
pass and govulncheck is clean; manifests validate offline.
- End to end in podman: server with InfluxDB (telemetry, pairing, phone
settings), Grafana with the init-container token; release relay
against a fake Gitea.
- Emulator (Android 9, 1920x720 and 1778x640): layouts, tabs, accent
colours, config import, queue restore, USB install.
- On the car: pairing, live values, voice key, full screen, screen
size. Not yet on the car: direct ABRP sending, Enter on phone, accent
colours, 0.5 s refresh, over-the-air and USB updates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
AGENTS.md still described upstream's EVABRPUploader (GitHub updates,
the old architecture and tabs). It is now the entry point for agents:
what the project is (app, Go server, k8s, EVHardware), the rules
(nothing touches the cluster, secrets, read-only telemetry, null is not
zero, fail closed, honesty), the facts confirmed on the car, the app's
structure and UI conventions, how to build, test, publish and install,
and which document holds what. SPECS.md stays the journal.
The Android 9 test emulator, so far a file under the ignored build/,
is now reproducible: nix run .#car-emulator creates an AVD at
1920x720/160 dpi with the AOSP image that shares the car's platform key
and starts it on port 5560. Checked from scratch: the car build
installs as the system user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Settings -> App -> "Install from USB stick" (car build only). It looks
in the top folder of every mounted volume (found from the app's own
folder on it, since the head unit has no file picker) for
NilsMG4-unstable-<version>.apk, takes the newest one newer than the
installed version, checks it against its .sha256 sidecar and the
package name, and installs it with the same PackageInstaller session
as the over-the-air update. The stable build gets a stub and no button.
Tested on the emulator: 2.18.0 installed a 2.18.1 from the storage's
top folder without a confirmation, as the system user, so the OTA
install step should not need one either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
- The "Your private connection" card is gone; its text was out of date
(the ABRP credentials are no longer on the server).
- Import from file moved next to Save and Test, in one row.
- The installation ID is shown as text instead of an editable field:
it is derived from the head unit, and changing it by hand only risked
splitting the history. A config file can still carry an older one
over, as before.
- The page fits the car's 1778x640 window without scrolling (checked
on the emulator), and the subtitle no longer says the server passes
data on to ABRP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The Overview panel's title came from upstream, where it also showed
pack energy and temperatures. The MG4 publishes none of those, so the
panel only holds the odometer and the climate setpoint; its title now
says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Upstream kept the head unit on the driver's phone hotspot by re-joining
a saved network every few minutes. This car's head unit has its own
connection and the Wi-Fi is not used, so the feature goes entirely:
WifiAutoConnect, its call on the service tick, the preference key, and
the ACCESS_WIFI_STATE / CHANGE_WIFI_STATE permissions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
- Settings: the Wi-Fi and saved-networks cards are gone, and the rest
sits in three columns (display and steering wheel | battery |
charging price and app) instead of one tall column. The background
Wi-Fi auto-connect stays on, without a switch.
- Logs: only the upload log. The steering-key reports, the car-colour
probe and the screen line answered their questions and are removed,
with their code (CarColorProbe, the key-report log) and strings.
- README lists the current tabs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
- "Speed, 5-min average" card under Speed: time-weighted mean speed,
standing included, from the same samples as the other averages.
- The top bar is one compact line (title and subtitle side by side).
Version, update check and About moved from it to an App card on the
Settings tab.
- The Overview no longer scrolls in the car's window: checked on the
emulator at 1778x640, the layout height the car reports, after
trimming the page's padding and the tyre card's height.
- The capacity hint says "Overview" instead of "Vehicle tab".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Back to drop-downs for upload frequency, tyre pressure unit and accent
colour, as Material exposed dropdown menus instead of Spinners. A
Spinner's list is a focusable window of its own without the
full-screen flags, so the car showed its bars while it was open; an
exposed dropdown's list leaves the focus with the activity, so the app
stays full screen. Styled like the other input fields, with full-height
rows. Replaces the buttons of 2.16.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Opening a drop-down or a dialog brought the car's bars back until it
closed: each is a window of its own, and the system shows the bars when
a window without the full-screen flags takes focus.
- The drop-downs (upload frequency, tyre pressure unit, accent colour)
are now single-choice buttons on the page: no extra window, one tap
instead of two, full touch targets, the choice in the accent colour.
- Dialogs (About, update, pairing, phone settings, trip reset, date
picker) are shown unfocusable, given the activity's full-screen
flags, and only then allowed focus. None of them takes typing, which
an unfocusable window would block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Read on the car with 2.16.2: panel 1920x720 px at 160 dpi, app window
1778x720 px (the car's own sidebar takes the rest), so EVProfile's
1280x480 (1920x720 at 240 dpi, in dp) does not apply here. The colour
probe found only ro.build.mtk2712.vehicle_type = AS28: the paint colour
is not among the system properties. FIRMWARE.md, mise.toml and
SPECS.md updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The MG4's display is given as 1920x720 (this project), 1280x480
(EVProfile's README) and a 1778x720 app window (upstream). 1280x480 is
exactly 1920x720 px at 240 dpi expressed in dp, so the figures may
describe one panel at different densities, which this platform lets be
changed per app.
The Logs tab's new SCREEN line shows the panel's pixels and density and
the app window in px and dp, to settle it from the car. FIRMWARE.md
lists the three figures and what the emulator shows at 240 dpi;
mise.toml, README and SPECS.md no longer state 160 dpi as fact for the
car.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
EVHardware's notes call the licence of EVProfile's upstream unresolved.
Checked: malys/EVProfile is MIT, forked from SliDeeN/MG4Control and last
merged from it on 2026-08-02. MG4Control is MIT up to 2.6.5
(2026-08-05) and GPL v3 from 2.6.6 (2026-09-02), with the earlier
versions staying MIT, so the code EVProfile took is from the MIT
period. Recorded with links; not an audit of every EVHardware line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Leon Kernan's ABRP_Uploader has carried the MIT Licence since
2026-07-29 (lkernan/ABRP_Uploader commit 75d8f2bc), so the inherited
"no licence upstream, status unresolved" notes no longer hold.
- LICENSE: MIT, now naming every copyright holder in the fork chain
(Leon Kernan, EVABRPUploader contributors, FatihsMG4 contributors,
Nils Stinnesbeck).
- LICENSE.md: rewritten. The repository is MIT except EVHardware/,
which is PolyForm Noncommercial 1.0.0; the app links it into the APK,
so the built app is non-commercial. EVHardware's own caveat about
EVProfile stays with it.
- README, AGENTS.md, CONTRIBUTING.md and SPECS.md say the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The Overview read the car every 2 s, on the main thread. It now reads
every 0.5 s on a background thread (one reader; a read still running
makes the next tick skip rather than queue) and shows the values on the
main thread. The consumption and charging trackers stay on the 2-second
tick with the newest reading: they integrate over time, and the
charging tracker writes its session to storage on every sample.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The read-only token for Grafana needed a manual influx command and a
Secret. InfluxDB generates every token but the admin one and ignores a
chosen value (tested on 2.7.12), so it cannot be preset in a Secret.
Instead an init container in the Grafana pod creates it at every start
with the admin token, deletes the one from the previous start, and
writes only the read-only token to a memory-backed volume; the
provisioned datasource reads it with $__file{/run/influx/token}. The
Grafana process never sees the admin token. The grafana-influx Secret
is gone from the template and the README.
Tested in podman: InfluxDB 2.7.12, the script run twice as the Grafana
user (the old token is replaced, not piled up), and Grafana 13.2.3
reporting the datasource healthy with the file-based token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The car updates itself again, from this fork's own releases, while the
repository stays private:
- publish-car (flake) creates the pre-release car-<version> on the
forge and attaches the APK and its .sha256. It needs a forge token
that may write releases, from ~/.config/nilsmg4/forge-token or
$FORGE_TOKEN; it is sent in a header and never printed.
- The server relays releases and attachments (/v1/app/releases,
/v1/app/assets/...), read with a read-only FORGE_TOKEN from its
Secret, GitHub-shaped with download URLs back at itself. Redirects
away from the forge are not followed; off (404) without a token.
- The app asks its server with its own paired token, allows exactly
the server's host, downloads the APK itself instead of through the
DownloadManager (so the token stays in the app), verifies SHA-256 and
package name, and tries a PackageInstaller session (it may install
silently as the system user) before falling back to Downloads.
OTA.md is rewritten for this flow; the k8s and server READMEs explain
the new token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Unsent live samples were kept in memory only, so a reboot or a killed
process during a dead spot lost them. The queue is now saved to
files/telemetry_queue.json with AtomicFile, at most every 5 minutes and
only after a change (the flash is not written every 15 s), and at once
when the service stops; it is restored, oldest first and within the
2000-sample cap, when the service starts. A damaged file restores
nothing rather than failing. Tested on the emulator across a reboot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The Charging, Consumption and Settings labels, toasts, the trip-reset
dialog, the charging and Wi-Fi states, the daily summary and the "—"
placeholder move from layouts and code into strings.xml; lint now
reports no hard-coded text. Upload-log entries stay in code, as log
text. No visible change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
mise.toml, the comments in app/build.gradle (corrected where they no
longer described the build: the car build is signed by build-car, and
the updater reads the version from build-car's file name), OTA.md's
message table (now the updater's current English messages), the Turkish
messages quoted in CHANGELOG.md and OTA.md (glossed in English), two
test assertion messages, an icon comment, and the ignition-state comment
in EVHardware.kt. The vendored EVHardware library still has about 120
half-French comment lines; noted in SPECS.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The Overview tiles' values (and the notes under them) are right-aligned
and use tabular figures, so a value going from 9 to 10 to 100 grows to
the left instead of shifting the whole text, and digits no longer
change width as they tick. The labels stay left; tyre pressures stay
centred but use tabular figures too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki