"Retro (accent colour)" draws the display in the chosen paint colour
and the labels in its light shade; regeneration stays green, a low
battery turns amber. Listed under Retro. Emulator only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
Retro: an 80s vacuum fluorescent cluster with seven-segment digits,
bar graphs and a lit P R N D selector. Futuristic: a hairline HUD with
an animated energy flow between battery and wheel, a turning speed ring
and a power arc. Both show unread values as dashes. The Overview now
reads the car five times a second. Emulator only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
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
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
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
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
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 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
The accent is selectable in Settings → Display: Dynamic Red (default),
Volcano Orange, Holborn Blue, Racing Green, Camden Grey, Monument
Silver, Arctic White and Black Pearl. White, silver and black cannot be
an accent on the black screen as they are, so they become light greys
and a pearl blue-grey.
The accent palette is now theme attributes (attrs_accent.xml) with one
overlay per paint (accents.xml), applied before the activity inflates;
layouts, colour lists, drawables and the views that draw in code use
them, including the red tints that were hard-coded. A change recreates
the activity on the same page. Android 9 crashes on gradients with
theme colours, so the battery card is built from layers.
Logs → "Look for the car colour" lists the head unit's system
properties that mention colour, paint or car model, to find out whether
the paint colour can be read from the car.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Tyre pressures show in bar by default; Settings → Display offers kPa
and psi as well. Display only: the car reports kPa and the server keeps
storing kPa.
A "Power, 5-min average" card under Power: the time-weighted mean
battery power over the last five minutes, standing included, from the
same samples as the 5-minute range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The pages' scroll bars never faded on the car: a ScrollView shows its
bar on every layout, and the live values lay the page out every two
seconds. StableScrollView, the root of every page, now has none.
A new "Energy since start" tile, left of "Usage since start", shows the
net kWh taken from the pack since the trip began, recuperation counted
back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The ABRP credentials are long to type on the car. "Enter on phone"
(Live data tab, and Settings next to the charging price) shows a QR
code; it opens a form on the server where the phone sends the ABRP API
key and user token, charging price and usable capacity, confirmed with
the approval password. The car polls with its own token and collects
the values once, then saves them through the same checks as the fields
on screen.
Same device-flow shape as pairing. The server keeps the values in memory
only, at most ten minutes, hands them out once with no-store, and never
logs them or puts them back into the page; a paired token only reaches
its own installation's request. Pairing's QR dialog is now shared
(PhoneQrDialog), and the phone pages share one stylesheet.
The config file also takes charging_price and usable_kwh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
The car sends live data to ABRP itself, and the car and the server go
over the same mobile connection: when the car cannot reach ABRP, it
cannot reach the server either, so the server's fallback bought
nothing. The forwarder, its ABRP_* settings and abrpSentUtc are gone;
the server only stores. The app stops sending abrpSentUtc (the server
ignores unknown fields, so 2.11.0 keeps working) and its texts no longer
promise a server fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
ABRP keeps getting live data while the car server is down: with an ABRP
API key and user token set in the app (Live data tab, or abrp_api_key /
abrp_token in the config file), the car sends the newest sample to ABRP
itself at every upload. It tells the server the newest sample ABRP took
(abrpSentUtc), and the server forwards only newer ones, so ABRP gets
each sample once. Without credentials in the app the server forwards
everything, as before. The credentials sit in encrypted storage, go in
the request body, and neither they nor ABRP's answer are logged.
Settings are grouped by what they are for:
- Live data (was Upload): on/off, ABRP credentials, upload frequency.
- Settings (replaces the Wi-Fi tab): full screen and voice key,
usable capacity and manual SoH (were on Consumption), charging price
(was on Charging), Wi-Fi.
Charging and Live data get icons of their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Net regeneration over the window (or the trip) used to hide both
estimates behind "needs a few minutes of driving". It is a real
reading: the usage now shows as negative kWh/100 km, and the range as
"> 999 km" with "while recuperating" beneath it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
"Range since start" replaces the range at current power: remaining
energy over the consumption since the car was switched on, standing
with the climate on included. A trip starts again after 10 minutes
without samples (the car was off) or when charging.
The kWh/100 km for both estimates moved out of the range tiles into
two cards of their own, directly below them; the overview still fits
the full-screen 1920x720 display without scrolling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
Telemetry no longer loses queued batches. 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 inside
the server's ranges (speed as a magnitude, so reversing is fine) and no
samples before the head unit's clock is set.
Server:
- Pairing hands out and accepts a token only after the device is saved;
a failed save leaves the request pending.
- A paired token serves only the installation id it was paired with
(403 otherwise); the shared INGEST_TOKEN still serves any.
- Wrong-password lockouts and open pairing requests are limited per
client address (last X-Forwarded-For entry), so a stranger cannot
lock the owner out; a car starting again replaces its own request.
- ABRP forwarding skips samples stamped ahead of the server's clock,
which used to stop it until a restart.
App:
- Settings written to plaintext during a keystore failure move back
into encrypted storage; the stored installation id wins.
- The installation id is derived from ANDROID_ID on first start, so a
reinstall finds its history; a stored or imported id still wins.
- The scanned config file is deleted after import (it holds the token).
- GPS stops while live telemetry is off or the server unconfigured, and
pauses while parked and idle, keeping the fix the car parked with.
- Range estimates use the monotonic clock.
- One HTTP helper (ServerHttp) for telemetry, sync and pairing.
- Last Turkish strings translated; mise logs shows the NilsMG4 tags.
Grafana reads InfluxDB with its own read-only token (new grafana-influx
Secret, created by hand as described in k8s/README.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
On the MG4 the immersive flags hid SAIC's bars but left a black band
where they had been. SYSTEM_UI_FLAG_LAYOUT_STABLE reports the bars'
insets even while they are hidden, and the fitsSystemWindows root
padded itself by them. In full screen the flag is dropped and the root
no longer fits system windows; switching full screen off restores both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki