Repository navigation
Unstable
Pre-releaseUnstable build 2026-10-29 20:06 UTC
Version 0.2.0.160 (run 160).
Automated build from master. Not tested on a vehicle.
This channel does not self-update: EVChargePilot declares no network permission
in any channel, so a newer build is always a manual install, parked.
The unstable id is com.evsuite.chargepilot.unstable, so it installs beside a
stable build and keeps its own trip history.
Changelog
Changed
- Split screen (CP-094): the companion is drawn over the map, the map is no longer embedded.
The 2026-10-25 drive heard two guidance voices and found a frozen map behind Back: handing the
embedded map back started a second MG4 Navigator. Android 9 resumes one activity, so a map on
the companion's virtual display froze whenever the companion had the focus. Navigator now stays
full screen, alone and live; the companion is a non-focusable window over its right quarter,
in place only while Navigator is the top task and guiding, and gone under the reverse camera.
Its button closes it. TheActivityViewpane and its frame watch are removed.
Validation question Q12 records where the window landed, every show and hide with the top
task, and how many Navigator tasks and activities exist, so a second copy shows as 2.
Fixed
- Eco score measures smoothness again, and ignores crawls. Smoothness read 97–100 on every
recorded drive (scale ending at 25 % of moving time hard, measured 0–0,85 %); it ends at 1 %
now. Trips under 2 km get no score, so a 600 m move no longer says "Note 100" or feeds the
average, record and run (EVHardwareEcoScore). - Battery page, charging line: no more "no charge has crossed 80–90 %" over the whole answer.
The car had charged to 99,9 %, overnight with the head unit asleep, so the app never timed it.
The line now gives the time to the last band it did time ("80 % at 18:10") and says the rest is
unknown because no charge was timed there with the screen on. Six languages. - One trip-end line per drive. A 19 m creep across the car park, joined onto the drive nine
minutes later, said the same score again (2026-10-29, and 10-27). A joined leg under 2 km is
saved and not re-advised. - Charge history and Battery page: a DC charge is no longer a measured 0 kWh. The pack pair
stays flat through SWI68 fast charges (2026-10-26 and 10-29, +49,8 and +30,9 points), so both
rows read "0,0 kWh at 0,0 kW", their columns were empty and four SOC bands showed 0 kW. Such a
charge is now estimated from its points, like one the app slept through, and leaves the curve
and the measured-energy line alone (EVHardwareChargeEnergy). - Battery notifications wait for P. A charge line posted 12 s into a trip on 2026-10-29: the
gate read 0 km/h, the car was stopped in D. The gear must now read P; an unread gear holds the
notification back and the line stays in the history.
Fixed (regression guards)
- Embedded map: a covered companion (the reverse camera, 2026-10-24) no longer hands the map
back full screen after eight seconds; the frame watch pauses while it is stopped and resumes
on return. After a route sent bygoToit comes back over the map only when the map is the
focused task (REAL_GET_TASKS, read-only), and themap pane:lines name the top tasks. - Lock emulator validation to the measured 1778×640 app window, adjusting for API 28 and
Automotive system bars. Reject a local app checkout older than the workspace gitlink. - Render every manifest activity and About in six languages, both palettes and 100%/130%
fonts. Fail CI on cropped children or text and retain failure screenshots; stable and
unstable publication now wait for this gate. - Let charge-row energy details and the companion's range label wrap; let the small arc
grow vertically for larger text, keeping its narrow width and the full-size trace. - Keep the embedded-map watch active through unreadable surfaces; hand back full screen
after eight seconds without a readable frame and attempt display release after failed
launches. Monotonic-time tests cover recovery, later black frames and a static map. - A host resumed inside the three-second re-front window now sends the map to the front once
the window ends, instead of leaving it paused behind the companion. - Joined trips keep power/speed provenance only when both halves had it, so legacy samples no
longer enter model training (EVHardware096d85d). - The save at P no longer joins a drive across a Trip A reset, which dropped its km from Trip A.
- The 15 km consumption curve reads the live drive's samples, not its overall average spread flat.
Added (navigation)
- The map beside the companion (CP-094). When "Aller à" hands a route to the car, the car's
map opens on the left two thirds of an EVChargePilot screen. The right third shows charge,
range, remaining distance and time, and the drift line. "Full-screen map", Back and any
failure of the pane all give the map back full screen, as before. The 2026-10-18 bundle read
split_screen_verdict=MAP_EMBEDDABLEfrom the platform signature. The pane needs
INTERNAL_SYSTEM_WINDOW, newly declared: see SECURITY.md.[split_screen]now reports
embedding_permission_grantedandmap_pane_possible. Not yet tried on the car. - The split screen's right third, as gauges (CP-094). A ring for the charge with the range
beside it. A destination card with the km and time left, a bar for the trip's share of the
range, and the range left on arrival. The live eco score in a ring, with its band word and its
three parts. The roads the plan offered, one bar each, whose length is the travel time. The
drift line stays on the dashboard only.
Changed (design)
-
One "Details" screen per trip. The energy-breakdown and speed-comparison screens are
merged and drawn: bars for consumed, regenerated and net energy, the gauge drop placed on the
0–100 % track, one bar per speed band (all your trips) with a mark for this trip, and the
modelled motorway speeds as bands either side of a centre line. Every group still names its
claim — derived, measured, estimated — and every row says it to TalkBack. The modelled
comparison stays parked-only. In the ledger, "Export trip" and "Delete trip" are icons; the
attribution line moved into Details. -
Battery page as four cards. State of health with its "use this figure" action under it;
the last charge as a span of the gauge; calibration; exposure. Charge and advice histories
are page actions in the top bar, like Trips and Settings. -
Charges screen. Each charge is its own card, and the analysis draws the energy every
charge put back as columns — solid where measured, light where estimated. -
Settings fit the panel. Save joined Import and Export in the top bar, the vehicle figures
became a 2 × 3 grid, and the coach and language took a third column. Language is one button
opening the list, as in EVTasker. -
Split screen: consumption card (CP-094). The last 15 km against your reference line and
their average, the trip screen's own trace, after Tesla's energy graph. -
Trips resume after a short stop. The save at P now joins the previous trip when the stop
lasted ten minutes or less with no charge in between. In the ledger, a join icon does the same
for any stop, parked only and confirmed first: a join cannot be split again. -
Energy, more graphic. The time and, while the car guides a route, the arrival time in the
top bar; the charge as a fill of 0–100 % (amber under 20 %); on a route, now → on arrival as
one bar; the last 15 km of consumption drawn under the instant figure. -
Split screen (CP-094), after Tesla's cluster. The map takes three quarters, the companion a
quarter, always in the night palette. Consumption is a small arc filled to the 15 km average
with a tick at the reference and the figure inside, and the trace beside it on the rest of the
card (the 2026-10-22 drive: the arc filled the card, the trace was a thumbnail); the arrival
time is on the destination line; the time is in the corner; "Full-screen map" is an icon.
Everything fits without scrolling: the trace shrinks first on a shorter panel.
mise run split-previewshows it on the emulator. -
Layouts checked in the car's real box. The 2026-10-23 bundle's
[panel]says an app gets
w1778dp h640dp on the car, not 1920×720: the vendor's top strip and left dock keep the rest.
The emulator profile was sized for h720dp and hid every overflow in the bottom 80dp, so the
screens reworked on 2026-10-22 cut their last lines on the car.mise run emulator-screennow
sizes the app box to 1778×640. The battery page and the split screen's companion column scroll
when their content outgrows that box, and fill it as before when it fits. -
Split screen kept while driving, watched (CP-094). The 2026-10-22 22:39 drive: the map
was live on the pane and its first frame was seen, but the driver saw black from D until P.
The pane now stays up in D and keeps sampling its own picture every 5 s, logging each change
between map, black and frozen, and each change of gear. Black for 8 s in a row, at any time,
gives the map back full screen. Replaces the parked-only rule of the same day. -
Charging stop in two columns. The plan's sentence over a strip of the trip — departure
charge, each stop with its uncertainty bracketed under it, arrival charge — and the charger;
the road choices and speed what-if on the right.mise run charge-stop-previewplans a real
route on the emulator with a charge handed in (debug builds only). -
Diagnostics has no tab. Three quick taps on the app name open it.
-
The charge model fits this car (EVHardware 394ce73). The 2026-10-22 bundle refused it:
143 segments,fit=refused(aero). The model now carries the speed-independent load, so the eco
verdict, the trip eco review, the motorway comparison, the charge-based energy breakdown and
"Slowing down" can come back once the car refits it. "Slowing down" no longer blames the
firmware when there is no model. The unstable bundle now carries the fit's own segments
(socSegments), so a refusal can be replayed off the car.
Added (languages)
- Deutsch, Español, Italiano, Português, alongside English and Français: EVTasker's five.
locales_config.xmllists them for the head unit's per-app language setting.
Fixed (navigation)
- The map pane never got a surface (CP-094). On the 2026-10-22 drive the pane gave up after
5 s withTrying to start activity before ActivityView is ready. The car was already guiding,
sogoTowent out after the route the map had just taken. That sent the destination a second
time, and the map most likely came forward full screen over the split screen. A covered
screen never gets the surface the pane is built on. NowgoTois never sent once the route
is taken by a car that was already guiding. A pane due while the screen is covered waits for
it to come back, andrelease()is skipped for a pane that never started. Not yet tried on
the car. - A trip saved while one is deleted no longer undoes the other. The service saves a trip
while the history screen can delete one, each through its own copy oftrips.json. The last
write won: a drive lost, or a deleted trip back. EVHardware now holds one lock for both. - The map pane is black, and only the full-screen map moves (CP-094). After "Aller à" on
the 2026-10-21 drive, the left two thirds stayed black until the driver chose the full-screen
map. Waiting for guidance did not fix it, so timing was not the cause. Android 9 lets only one
activity be resumed on the whole head unit. The map on the pane was therefore paused while the
split screen had the focus, and a paused map does not draw. Now, each time the split screen
takes the focus back, the map is brought to the front of the pane again. If the pane still
shows no map frame 8 s after the start, the map goes back to full screen by itself. The log
records the pause, each re-front and the first map frame. Not yet tried on the car. - The map pane waits for the car to be guiding (CP-094). On the 2026-10-19 drive, "Aller à"
turned the screen black and then left a frozen MG4 Navigator while guidance carried on. The
likely cause is that the pane moved the map while the car was still routing. The pane now
starts onceisMapNavigatingis true, plus 2 s, and logs how long it waited. - The route handoff sends the charging stops only (CP-060). The 8 shape points made Telenav
guide to the next point ("900 m" on a 7 km trip), and guidance dropped about 2 km in. With a
stop, the split screen computes the km to the destination as the road's length less the
distance driven since the tap.
Changed (charges)
- A charge the screen slept through gets an estimated energy instead of "Partial". Points
gained × the declared usable capacity (±5 %), and a power derived from it over the session's
span — MG4Control's method (behaviour reference, v2.6.8, GPL: nothing copied), written as
estimated and derived on the charge list and the battery page. A watched charge keeps
its measured pack integral.
Added (diagnostics)
- Diagnostics → Send to server. The USB bundle, posted to an HTTPS address typed into the
dialog (your own inbox, such as the Node-RED flow in the workspace'stools/nodered/). Parked
only, like the USB export; the address is shown again before every send; no new permission.
Fixed
- The trip-end line says one thing per sentence. "Note 24, -40 sur votre moyenne. Trajet
habituel, dans votre propre moyenne." used moyenne for two different averages. The score now
reads "Note 24 sur 100, moyenne 64."; consumption is named as such and said only when it is away
from the usual; "not enough comparable trips" is said only when nothing else is; tips are shorter. - Hard stops are counted from 2.5 m/s² (EVHardware): lifting off into regeneration no longer
scores a calm town drive at zero for anticipation. - The passenger setpoint of a single-zone car is shown as derived from the driver's, no longer as
measured by the vehicle. [split_screen]answers per route (option.map_system_window,option.map_allowed_embedding,
option.split_screen,option.guidance). It now counts the INTERNAL_SYSTEM_WINDOW route a
platform-signed build has, checks that secondary-display activities are on, and stops calling
an idle guidance service "answering": with no route running the verdict isUNPROVEN.- Leftover quarantine copies (more than 400 on the car, from the R8 ledger bug fixed on
2026-10-11) and writes cut by a power-off are swept at start: the newest five copies per file
stay, and any copy over 4 KB is never touched.
Added
- Split-screen probe (CP-094).
diagnostic.txtgains[split_screen]: whether the car's
map could be embedded beside the companion (hiddenActivityViewpresent, platform signature,
launcher holding the embedding permission, map activities that allow embedding, displays added
while the launcher's map card is up) or only its guidance answers (map navigating, road,
remaining distance and time, next manoeuvre). Ends onsplit_screen_verdict=. Read-only, no
new permission, in stable builds. - Eco score out of 100, with a record, a run and a weekly challenge (CP-093). Anticipation
(hard stops per 10 km), smoothness and efficiency against your own expectation. The Trips page
shows the score, your best and this week's target — a fifth fewer hard stops than last week;
the trip detail shows the three parts; the voice in P starts with the score and a record. - A new lever, anticipate braking, when hard stops reach two per 10 km.
Changed
- The eco coach is less kind (CP-093). The verdict's "typical" zone is one residual wide
instead of the 95 % band; a trip is no longer part of the fit it is judged against; hard
driving is counted at 1 Hz instead of replayed from the 5 s track; a heavy trip is called heavy
out loud; the drive mode advice uses the mode the trip was driven in, recorded with it.
Fixed
- Automatic trips are saved before the head unit powers off (CP-080). The car switches the
screen off about twelve seconds after P, and the trip end waited two minutes; EVHardware
1a05d5b now ends a trip two seconds after P. The monitor journal also recordstrip start. - The consumption model trains on SWI68. The 2026-10-16 bundle refused it for a negative
weather term alone; the fit now drops that term instead, so the eco verdict, speed what-if,
route what-if and energy breakdown can fill in. - Route choices can be chosen (CP-057). The second road the routing service sends back is a
row of the comparison, with its stops and its charge against the routed road; one button per
road re-plans the screen on it, and Go hands that road to the car. Past 100 km the second
road is no longer asked for, which saves a refused request and its retry. - "This vehicle never publishes battery power" is gone on SWI68. The pack fallback reads it;
the screens now say it is not validated yet.
Added
-
Read-only evidence probes for the open on-car tickets (CP-055, CP-078–CP-081, CP-083,
CP-086, CP-087, CP-090). Nothing new is read from or written to the car.- Monitor journal (unstable, CP-078/079/080). A persistent, bounded (200 lines), atomically
writtenmonitor-journal.jsonrecords process start, boot received (with detection on/off),
monitor start/suspension/self-stop/destroy, every trip end (automatic, km, stored, history
revision), every battery ledger entry, and dashboard / battery page reloads, each with
wall clock, uptime, odometer and charge. It reaches the existing validation file as block
Q11; the bundle gains no file. - Q1 on the proven guidance signal (CP-055). When
isMapNavigatingturns true the
destination probe fires, instead of waiting for a guidance flag never seen on the car. - Voice outcomes (CP-081/083). The last 20 voice dispatches (origin, voice path, bind state,
wait, process importance) are listed asvoice_event=in the diagnostic[eco_driving]. - Windows factor (CP-086).
[eco_trip_review]always prints the newest trip's open-window
samples, open distance above 60 km/h, window residual and whether it was distinguishable. - Notification time (CP-087). Advice history entries record when their notification was
posted (notified_at_ms=in[battery_advice]); older files read unchanged. - Driving stress per trip (unstable, CP-090). The trips artifact (schema 3) carries
drivingStressfor each exported trip with a track.
- Monitor journal (unstable, CP-078/079/080). A persistent, bounded (200 lines), atomically
-
Energy page warms when driving is not smooth. With eco advice on, the page background
between the cards blends gradually towards the warning colour as the harsh-acceleration share
rises: none up to 6 %, full at 24 % (the advice mark is 12 %). Eased over a few seconds,
never flashed; the cards keep their own surface, so no figure loses contrast, and the gauge's
word still carries the meaning. -
"Validation results complete" notice (unstable, CP-055). Once every validation question
has recorded at least one answer, a notification asks the driver to export the diagnostic to
USB; tapping it opens Diagnostics. Posted once per process, only with the car read as stopped. -
Battery stress while driving (CP-090). When a trip spent 2 minutes or more pulling hard
(above 60 % of its own peak) at a low charge (under 20 %) or below 0 °C outside, the trip-end
line gains one clause, through the same voice, gate and advice history. The Battery page's
exposure card adds this month's stressed minutes over the trips that could measure it, and the
energy regenerated below 0 °C; a month with none says so instead of showing zero. -
The charge in progress and when it ends (CP-089). While the car stands still and its charge
rises, the Battery page shows "80 % at 21:40, 100 % at 23:10", with the range the driver's own
watched charges allow, to the car's limit (else 100 %). The time is recomputed each time the
charge enters a new 10-point band, not every sample. A band no watched charge crossed gives no
time and says which band is missing. The journal gets the opening line (with the last stretch
above 80 % when it takes as long as the rest), a note when a whole band took longer than the
slowest of at least three past charges, and a closing line: points added, duration, and whether
the curve was usual. The app reads no charging state it trusts, so a charge is the ledger's: a
rise with the car still. It closes when the car moves or after 45 minutes without a rise. -
Advice on how the car is left parked (CP-088). The daily round now proposes a charge limit
from the driver's own days — the 95th-percentile driving day in charge points, measured from
the ledger's odometer and charge steps, plus a 15-point reserve, rounded up to 5, floor 60 % —
and says so when the car's limit sits 10 points above it, or below what the longer days need.
It also says a stand of 12 h or more at 90 % or more, and a day parked at 80 % or more with
30 °C or more outside. Each is said once while its cause is unchanged. The limit is read,
never written. -
A battery companion that speaks up once a day (CP-087). At the first sample of each local
day the service recomputes the battery analyses in both flavors and stores the day's digest.
What moved past its own band since the day before — a first health estimate, a band that
narrowed, health that moved, a calibration verdict that changed — becomes advice. Every
advice, the trip-end line included, is kept word for word in an advice history (Battery page,
"Advice history", newest first) and in the diagnostic bundle ([battery_advice]). With the car
parked, a "Battery advice" notification carries it, at most one per kind a day; a Settings
switch turns it off. No new permission: the head unit's API 28 needs none, and on API 33+ the
advice stays in the history only. -
Open windows, measured and counted (CP-086). A Windows tile in the Details climate grid
shows the widest opening in percent. The trip review adds a Windows finding when the window-open
residual is distinguishable and above the cabin's minimum share: its kWh, its share, and the
distance driven open above 60 km/h. The trip-end advice in P and the general advice card count
it, and the energy breakdown, the trip attribution, the diagnostic report and the trip export
carry it. -
Charges — a list of every charge, and what the watched ones measured (CP-082). The battery
page gains a Charge history button. The screen lists every charge newest first — start and
end, charge before and after, duration, mean points per hour, outside temperature range and,
for a charge the app watched, the energy into the pack with its mean and peak power. The
analysis pane adds the median charge, the fastest one, totals, the pack-current sign and the
car-counter verdicts, and the charge curve: measured pack power per 10-point band of charge,
which is where the taper above 80 % shows. No cost, no charger type, no capacity from the charge
side — measured only. The exported[charge_energy]section carries the same per-charge peak,
step count and bands.
Changed
-
The energy page is a driving companion (CP-083). Three large figures — battery charge, the
charge on arrival while the car's navigation guides a route (adaptive range otherwise), and the
instantaneous consumption — over the eco verdict, the trip average and battery power. Speed,
the manufacturer range, the climate values and the trip controls moved to the second page,
now called Details, next to the arrival explanation. -
The eco coach speaks behind the map (CP-083). The coach lives in the trip recorder instead of
the energy screen, so its voice carries on while MG4 Navigator or any other app is in front, as
long as a trip records or automatic detection is on. -
A gauge instead of "nothing to say yet" (CP-084). When no advice applies, the energy page
draws the share of hard acceleration as a bar, with a mark where the steadiness advice starts
and one word beside it ("Smooth · 4 %", "Coach listening" before a minute of driving). -
The trips summary reads at a glance (CP-084). Estimated range and consumption side by side
in large figures, the gap to the reference on one line; the explanation of the estimate and of
the chart moved behind an ⓘ. -
The battery page draws what it says (CP-085). The health figure is large over a bar with its
± band lighter and a mark at your declared figure; calibration is a state and one line; the
exposure is a bar of time below 10 %, between and above 80 %, with cycles, km per cycle and mean
charge as figures. Charge history is a filled button. -
Charge history as rows, not paragraphs (CP-085). Three figures (charges, points put back,
median charge) and the charge curve as bars; each charge is a date, a 0–100 % track with the
charged segment, "+32 pts · 81 h" and its energy or a "Partial" badge, explained once under the
list. The pack-sign and counter verdicts stay in the diagnostic report only.
Fixed
-
The charge history stays empty after a real charge (release builds). R8 stripped the
battery ledger's stored classes, so every read saw schema 0, quarantined the file and came back
empty: the ledger never held more than one entry on the car. Keep rules added. EVHardware also
drops ledger readings taken on the head unit's factory clock, which split a night charge. -
The eco coach's smoothness gauge was too optimistic. It counted hard acceleration only,
while the trip summary counted hard braking too; the live gauge, the steadiness line and its voice
now count both, through EVHardware'sEcoDrivingMonitor, and their wording says so. -
The Diagnostics page opens again on a release build. The battery digest and the advice
history (CP-087) were saved with Gson but had no keep rule, so the minified build read their
entries back as maps and the report crashed as soon as a digest existed. Files written by the
broken build are set aside once and a new history starts. -
The eco voice can be heard, and checked. The coach now waits for the vehicle voice service
to bind before speaking instead of asking it in the same instant it starts binding, and speaks
on its own worker thread. Turning the voice on says a test line at once; if neither the car's
voice nor an Android engine answers, the settings screen says so under the switch, and the
diagnostic report carriesvehicle_voice_bound. -
An overnight charge now appears the next morning (CP-082). Two causes. EVHardware
3417b30: a rise across a night the head unit slept through was filed as regeneration whenever
the odometer differed between the evening and morning entries, which it does as soon as the
drive home went unrecorded; a rise is now a charge when the odometer stood still, the car
reported a non-zero charging status, or it is at least 10 points and no one watched the car
move. And the battery page loaded the ledger before the sampler it had just started wrote the
morning reading that closes the charge; stable now reloads on a new ledger entry the way
unstable already did.
Added
-
Eco advice when the trip ends in P. With advice and voice on, the car says what the trip
cost against the driver's own average and what would have made it cheaper: "You could have
saved about 12 % (1.1 kWh) on this trip by accelerating and braking less sharply for 6 min or
by driving in Eco mode." Hard braking now counts beside hard acceleration; Eco mode is named
only when the car was not already in it, Snow mode only for a sharp drive at 3 °C or below. -
General eco advice beside the trip graphs. The trips overview adds a card over the last 20
trips: how many used more than the driver's own average and what the excess added up to, time
spent accelerating and braking sharply, the fast-road saving at 10 km/h less, and the cabin's
mean share. -
The eco coach speaks on the car. Voice coaching went through the Android text-to-speech
engine only, and the MG4 has none: it talks through the SAIC vehicle voice service. The coach
now uses that service first, queued behind any announcement in progress, and keeps the Android
engine as the fallback for the emulator. -
The eco coach no longer waits forever on a calm drive. It showed "a few minutes of driving
are needed" for a whole drive whenever nothing crossed an advice threshold. After a minute of
movement it now states the measured share of hard acceleration. The trip review keeps that
share as a finding too, so a calm drive without a trained model is no longer "nothing
measurable". -
Full-screen consumption graphs carry their axes and legend. The trace draws both axes, the
kWh/100 km and "km before now" units, and a legend for above, below and the reference line,
since full screen leaves the text beside the graph behind.
Added
-
Speed comparison and energy breakdown answer on urban trips. When no model can answer, both
screens now show what the power line measured instead of only a refusal. Speed comparison
lists net kWh/100 km for each speed band over the retained trips (and for this trip), plus
the mean draw while standing still. Energy breakdown gives consumed, regenerated and net kWh,
kWh/100 km and the energy spent standing still. -
Unstable automatic history now keeps model-worthy drives. Automatically detected trips are
persisted only when their measured distance exceeds 5 km; manual recordings remain explicit and
are always kept. Existing storage bounds remain unchanged: 200 summaries and 512 KiB, with old
sample tracks evicted before trip summaries. At the same trip boundary, Q6 records the app's
gross and regeneration-adjusted averages beside the vehicle's raw published average, so the
next drive can establish why they diverge without assuming equivalent units or reset rules. -
Unstable trip-history artifacts now include
socModelFit, computed from full stored tracks
during export. CP-055 Q6 is recorded before validation artifact is written, so model evidence
no longer depends on reopening Charging stop after a drive. -
The energy screen says whether this drive is a careful one. A sixth readout tile reads
better than usual, usual or worse than usual with the signed distance from the driver's
own fitted expectation for this speed and this outside temperature — their history, not a
brochure figure, and not a kilowatt-hour: the verdict is built on the charge gauge and the
speed, because the sign of the battery-power channel is still unvalidated on this firmware. The
band is the fit's own residual, so a model that knows little says usual more often instead of
accusing the driver. Advice is a separate opt-in — one line, one lever, never an imperative —
and the voice is a second opt-in on top of it, speaking only when the advice becomes different
advice, at most once every two minutes, and never while the car is stopped. -
A finished trip now answers whether it was an efficient one. Under the trip readouts: the
same verdict replayed over the stored track, then up to three findings, each naming the
measurement it came from — what a lower motorway speed would have saved, in the unit the
what-if produced it in; the cabin's fitted share, and only where the attribution could tell it
apart from its own error; and the share of the drive spent accelerating hard. Findings are
ordered by how strong their evidence is rather than by size, because a kilowatt-hour, a fitted
share and a percentage of a drive have no exchange rate. Nothing measurable is a stated reason,
which is the normal answer for a first short trip. The same review travels in the JSON export
(schema version 3) and in the diagnostic bundle's new[eco_trip_review]section. -
The driver picks the app's language. Settings offers system, English or French, applied on
the tap and stored by the platform rather than by us — so the app can be read in a language the
head unit is not set to, and the configuration file copied to another car does not carry it.
The eco tile, the trip review and every refusal reason follow the same choice, and the coach's
voice now speaks the language the line was written in: where the engine has no voice for it the
coach stays silent rather than reading French aloud with English phonemes (CP-077).
Fixed
-
The consumption models learn from town driving. EVHardware now fits the power model on
one point per kilometre instead of per 5 s sample, so stop-and-go trips train it, and the
charge model cuts segments at the SWI68 gauge's 0.1 % step instead of waiting for 2 %. Twelve
kilometres at two speeds are enough for the speed comparison and the energy breakdown to
answer from a model. -
Finished-trip eco advice no longer drops persisted samples. Trip tracks keep the first
telemetry frame after each five-second boundary, commonly around 5.3 seconds apart. Replay used
the stricter five-second live gap and discarded those intervals; it now uses the stored-track
cadence while live monitoring keeps its original sleep-gap guard. -
The advice line said nothing about having nothing to say. With the switch on and no lever
measurable — no fitted model yet, or too little movement in the window — the line simply was
not drawn, which reads exactly like a switch that did nothing. It now states which it is, the
same rule every other unavailable value on these screens follows. The voice is unchanged: it
speaks advice, never the waiting line. -
The routing keys looked erased after any configuration change. A password field does not
restore its own text, so a day/night switch, a density change — and now a language change, on
the very page that holds the keys — left both key boxes blank while the keys were still stored
and still in use. Nothing was ever lost, since a blank box has always meant "leave it alone" on
save, but the page contradicted its own status line. The boxes no longer restore state and are
filled from the store on every start. -
The trip screen's numbers were clipped on the car. Both readout rows were weighted children
of the detail column, so theminHeighteach carried could never apply — a weighted child is
measured exactly — and at the driver-set density the car actually runs, the figures lost their
descenders. The rows now wrap their content and keep the floor they always meant to have; the
track plot takes whatever height is left. -
The battery page says what the last charge actually cost. The pack's health is measured
from discharges, so the charge that ends each window was a boundary marker and nothing more —
on a page about energy, in an app about energy, the driver was told nothing about their own
charging. It now reads "+34 points in 2,1 h — about 12,3 kWh at 5,9 kW", with the source named
inline and the energy stated as approximate, because it is an integral of a pack
voltage-current pair whose sign no drive has yet confirmed. Only a session the odometer says
stood still is offered: a long descent raises the charge too, and that is regeneration rather
than a plug. A charge this app slept through says so and drops the energy instead of
understating it. The exported diagnostic gains a[charge_energy]section carrying the same
measurements plus the two verdicts RI-002 has been blocked on — which way the pack pair signs a
charge, and whether the car's own kWh counters reset across one — so a bundle brought back from
one watched charge settles the ticket with no probe to arm and no drive to plan. Nothing new is
sampled and nothing is written to the car (CP-073). -
The charge-stop probe says which gate refused the fit.
fit=unavailable(INSUFFICIENT_SAMPLES)
was the same line for a history of short drives and for a history with no motorway leg in it,
and the two ask the driver for opposite things. The validation line now carries the library's
owndescribe(): used trips, segments and speed span, each against its threshold. Evaluated
inside the probe lambda, so the second collection pass costs nothing unless validation is armed. -
A battery page: what the pack is worth, what its gauge is anchored on, what it has been
through. The health figure is measured rather than declared — the energy that left the pack
over the charge it cost, from the car's own counters where it keeps any and from the pack
voltage-current pair otherwise — and it leads with the trend, because the absolute inherits
whichever node the energy was counted at. Beside it: how long since the pack saw a full charge,
any charge step that nothing explains, and the dwell, cycles and charge sessions the ledger has
accumulated. The sampler now feeds that ledger, which is what makes windows spanning the
parking between two drives possible at all. The estimate can be adopted as the declared state of
health in the vehicle settings — parked only, one tap, never silently, and never offered for a
figure outside the band those settings accept. Nothing on the page writes to the car: no charge
limit, no schedule, no preheat. With too little history it says exactly what it is waiting for. -
The trip trace opens at full size. In the ledger it is a thumbnail a couple of hundred
pixels tall, and on a panel shorter than the emulator's it is the first thing squeezed. Tap it
and it gets a screen of its own: speed and charge on labelled axes, and a cursor dragged along
the trace that reads back what the car actually held at that moment — elapsed time, speed,
charge, outside temperature, battery power, climate. With no finger on it the row shows the
trip's own extremes instead of six dashes. Nothing here is computed: a gap in the record reads
as a gap, never as a zero. -
mise run seed-settingsloads the keys into the emulator. Validating a routing change cost
a USB stick or a key typed on a touch keyboard. A debug build now applies a settings file
dropped in its own files directory, once, at the next start; the task pushes
evchargepilot-settings.local.json(gitignored — it holds keys in clear text) there over adb.
mise run runandrun-unstabledo it on every install, and launch without it when the file
is absent. Release builds have no such path: the only way in stays the stick the driver browsed
to. -
A search result can be kept without going there. Every row in the destination results now
carries a star beside it: tap the place to drive to it, tap the star to put it in the favourites
and keep searching. Saving used to be a long press, which is an affordance nobody finds on a
panel; the long press still works for a hand that learned it. -
The diagnostics capture reports the panel. A
[panel]section records what the resource
system and the window manager actually see — the dp the layouts are written in, the density, the
font scale, the pixels, and the insets the platform reserves — so the next layout question about
this screen is settled by a capture instead of by a guess from a desk. -
The probe asks the property channel, the charging counters and the platform's diagnostics.
Three unblocked tickets' worth of questions in one capture: the typed property channel reached
by name rather than by reflection, asked for eight ids including the two the energy screen has
no source for; the charging service's own energy counters with the validity flag the vehicle
publishes beside each one, which is what can finally settle the unit nobody could prove; and
whether the platform's own diagnostic service is reachable here at all, so the OBD question has
a recorded answer rather than an assumed one. Reads only — no transaction that starts, stops or
limits a charge, or writes a property, is named anywhere in the probe, and a test asserts it
against the full write set harvested from each interface. -
The probe bundle asks three more questions. The pack's voltage and current are reported by
name, so a capture says which of the two answered rather than only that their product did not;
and the one remaining in-car temperature candidate the firmware declares is probed rather than
read, because guessing wrong would put the outside temperature in the cabin tile.
Changed
-
The trip screen no longer assumes the emulator's height. The car has no system bars but its
per-app DPI is the driver's to set, and a taller density buys fewer dp — so the real panel is
shorter than the emulator and the trip readouts came out squashed. The readout rows now take a
share of whatever height the panel has, with the tile size as a floor rather than a fixed slice,
and the trace keeps the largest share. No height bucket decides it: a-hXXXdpqualifier only
moves the guess, and a panel on the wrong side of the threshold gets the layout meant for the
other one. -
The speed comparison and the energy breakdown can train again. Both rest on a local model,
and the model refused to train because no drive has ever confirmed a battery-power conversion on
this firmware — so on the only car this project runs on, they were empty by construction. The
model now trains on the pack pair the dashboard already derives its power from, and carries a
separate interpretation stamp saying so. Nothing is relabelled as measured: the figure on the
dashboard stays derived, the model's own predictions stay estimates with a band, and the day a
validated conversion arrives the stamp changes and the derived model is retrained from scratch
rather than quietly surviving underneath it. -
Battery power is shown where the car publishes it, labelled for what it is. On this firmware
the standard power property never answers, so instantaneous power, consumption, the trip's
energy and the adaptive range were all blank. The pack's own voltage and current are readable
and multiply out to watts, so they are read and published as a derived figure — arithmetic,
drawn differently from a vehicle reading, and still barred from training any model: a trip
recorded this way carries no power evidence, which is what the models filter on. -
A trip with no power reading is priced from the pack you declared. The charge dropped by a
measured number of points and the settings say what a point is worth, so the trip's energy and
its average consumption are estimated from that instead of left empty — with the band that a
declared capacity and a state of health deserve. Regeneration keeps its dash: the gauge cannot
tell energy that came back from energy never spent. The same figure closes the energy breakdown
when neither model can produce one, instead of a screen that only says no. -
Unstable builds now tell you when a newer one exists. A tester's only way to find out was
to go and look at the releases page. An unstable build now asks the rollingunstable
pre-release at start, downloads a newer APK into the download folder and names the file and the
folder on screen. It stops there on purpose: this app holds no install capability in any
channel, so the install stays a tap the driver makes in the head unit's own package installer,
parked. What it downloads is proven first —httpsand an exact-match host allowlist on the
initial URL and every redirect, a size ceiling on both the declared and the transferred length,
and the same signing certificate as the running app or the file is deleted rather than offered.
Writing intoDownloadis the one reason an unstable build asks for storage access, and it
asks on the dashboard rather than assuming; say no and the APK lands in the app's own folder
instead, which the dialog then names. One APK is kept, and stable contains none of the code —
not the permission, not the release URL, not a line of the updater. -
A destination now suggests itself while you type. Typing an address on this head unit's
keyboard was the slowest part of planning a trip, and it ended on a button: nothing happened
until the driver tapped Search with a finished address. Places now arrive on the way —
every pause in typing asks the geocoder's own type-ahead endpoint what the prefix might mean,
and the saved list beside it narrows at the same time to the favourites that match, matched
inside the car with accents folded away so a keyboard without an É still finds Écully. The
keyboard's own search key finishes the job; it said "search" all along and did nothing. The
restraint is the point: one request per pause rather than per keystroke, none under three
letters, none twice for the same text, and a slow answer to an older prefix is dropped rather
than shown over a newer one — a screen that suggests is a screen that spends a quota, and the
geocoding allowance is only assumed. Refusals are still announced; a quota that has run out
says so instead of looking like a place that does not exist. -
A plan with a charging stop is now driven all the way, one leg at a time. The command that
starts guidance on this head unit carries a single point, so the tap sends the charging stop and
the destination used to be the driver's problem again. It no longer is: the trip is held in
memory as an ordered list of legs, and when the car's own guidance flag goes false after having
been true — the stop reached — the next point goes out on its own, and so on to the end. Nothing
is written to disk: legs are place names and coordinates, and the followed-plan store on the
other side of this trip deliberately keeps neither. A leg whose guidance never starts is
abandoned rather than chained onto, so a refused handoff cannot send the car onward from a
charger it is still standing at, and a leg needs three consecutive readings of "not guiding"
before it counts as over. Every change of that flag after a handoff is now recorded, so the
next drive says whether it can be trusted for a whole route rather than leaving it assumed.
Changed
-
"Y aller" now hands the car the road, not only the end of it. The handoff named where the
driver was going and left the car to plan its own way there, so every figure on the screen — the
arrival charge, the stop, the drift the companion watches — described a route the car might not
be driving. The route handoff's pathway list now carries the shape of the planned road as well:
points sampled evenly by distance along the route rather than by vertex, since a router puts
hundreds of vertices in a roundabout and four in a motorway straight, each named by the road the
router said it sits on, with the charging stop keeping its own place in the order. They are
waypoints and never legs: a place the car passes is not an arrival, and a chain that read one as
one would hand the next destination over in the middle of a motorway. Sending the same points as
a series ofgoTocalls would not do it —goTotakes one point and starts guidance, so a
second replaces the first and the car would drive to the last one sent. -
Starting guidance now gets the screen out of the way. Tapping "Y aller" left the charge-stop
screen sitting over the map that had just been told where to go. It now finishes, which returns
the driver to the dashboard behind it, and only on the one outcome the car confirmed: guidance
actually running. A refusal keeps the screen and its message, because the driver has something
left to do there. -
The only socket, the leg chain and the USB fallback now have tests around them. Three of the
app's decisions ran unpinned:RoutingTransport, which is the single HTTP entry point the whole
network argument rests on;NavLegs, which is the one thing here that asks the car to act with
nobody touching the screen; andDiagnosticUsbStorage's write fallback, which already carried
aninternalseam for exactly this and had never been called through it. The transport's cases
all sit in front of the socket — that is what makes them testable and it is also the claim worth
testing, that nothing reaches the wire unchecked — and the two that must spend the quota aim at
https://127.0.0.1:1, refused by the loopback stack without a DNS lookup or a packet leaving the
machine.NavLegsgrew one seam to allow it: aninternal tick(guidingNow, handOver)taking the
two binder calls as parameters, named apart from the private members they default to because a
parameter of function type loses name resolution to a member function of the same name and the
test's own sender would have been silently ignored.PowerHistoryPolicyand the handoff
assembly below are covered too.ProvenanceTextandDriftCompanionare deliberately still
not: both areContext-bound, the arithmetic under them is EVHardware's and tested there, and
a Robolectric dependency is a larger thing to own than the two classes are worth. -
Which point the car is sent to is no longer assembled inside an activity.
ChargeStopHandoffholds it — the geo:/goTosingle point, the route channel's pathway and
destination, and the legs in the order they are driven. Nothing in it needed anActivity, and
the mistake it guards costs nothing to make and is invisible until someone arrives under their
reserve: send the destination where the charging stop was meant, and the car picks its own road
while every figure on the screen goes on describing a different trip.removesStopmoved with
it and now takes the reserve rather than the whole settings object, since that is the only field
it read. -
The units are declared once.
%.1f km,%.2f kWhand%.1f %%were spelled out again in
four screens and the recording notification, next to thePATTERN_*constants that exist so the
dashboard and the diagnostics report print the same figure the same way. They all read the
constants now, and the two the arrival screen needed — a charge and a reach without a decimal —
are declared beside the others rather than inline. -
Every setting the driver owns now travels as one JSON file. Import and export wrote
key = valuetext carrying the two API keys, their base URLs and the saved destinations; the
car's own figures — usable capacity when new, state of health, minimum charger power, reserve —
travelled nowhere, so a second car or the unstable channel meant retyping them on a head unit
keyboard. Both screens now read and writeevchargepilot-settings.json, which carries all of
it underrouting,vehicleanddestinations, and both go through the same apply, so
neither can configure half the car. Sections the file leaves out are left alone rather than
reset: a file carrying only a key does not blank a self-hosted base URL, and a car still on the
specification-sheet defaults exports novehiclesection rather than writing the sheet over a
pack that has been measured. The file is still identified by its contents rather than its name,
and thekey = valuefile earlier versions exported is still imported, because the sticks
carrying it are in gloveboxes already. It still holds the keys in clear text — that is what a
file this app can import back has to be — so the file says so in anotefield a driver
opening it on a laptop cannot miss, and both screens say so when they write it. Decoding is
read field by field rather than mapped by reflection, and every value goes through the checks
the app already relied on:httpswith no credentials and no query for a base URL, finite
coordinates within ±180/±90 and a label to tap for a destination, and the same clamp the
vehicle preferences already applied for the four figures.
Fixed
-
The key boxes on the Configuration page can be read, not only written. They opened empty, so
the reveal button on them showed an empty box and the only way to see the key a car was actually
using was to export the settings to a stick and open the file on a laptop. They now open holding
what is stored, masked, the way EVABRPUploader's have always done — a key that cannot be read
cannot be checked against the one on the service dashboard, and that check is what tells a wrong
key from a spent quota. -
An unstable build no longer offers the release it was cut from. The published asset name and
the APK's ownversionNamewere computed by two different formulas: the workflow named the file
EVChargePilot-unstable-0.2.0.85.apkwhile the APK inside it reported0.2.0-unstable. The
update check compares one against the other, so every launch found an update that was already
installed, and installing it changed nothing. The build number now reaches both, and a local
build with no run number becomes0.2.0.0-unstable— older than any release, which is what a
local build is. -
A
403is no longer always read as a spent quota. HeiGIT answers403both when the day's
allowance is gone and when the key is simply not served on that API, and the screen said "the
daily quota for this key is spent" for both. A driver whose key was refused outright was sent
away to wait until tomorrow for a key that would never work, however long they waited. The body
now decides, and a refused key gets its own message naming the key and the service address. -
The server's own remaining count no longer locks the local gate shut for good. A
x-ratelimit-remaining: 0closedRoutingQuotawith no way back: refusing locally meant no
request went out, and only a request could bring back the header that would reopen it. The count
is now believed for five minutes, and a0sitting beside an answer the server actually served
is not kept at all — a limiter describing a window this app never asked about reads exactly the
same as an allowance this app has spent. -
Geocoding is no longer throttled at a third of what the key allows. The local limits were
third-party guesses and the guesses were low: 1000 a day and 40 a minute against the 3000 and 100
the key dashboard actually lists. A local throttle refusing requests the service would have
answered is the same broken screen as one that spends a quota, minus the evidence. -
The unstable update check asks again when the network was not there yet. The check latched
itself for the process after three attempts spanning about forty seconds, so a head unit whose
Wi-Fi came up a minute after launch left the tester on an old build until they killed the app.
Giving up now releases the latch, and returning to the dashboard checks again. -
The unstable update check no longer loses its storage question to the vehicle one. Both were
asked fromonCreate, and an activity holds one permission request at a time — the second was
dropped by the framework, so on the first launch of a fresh unstable install the driver was never
asked for theDownloadfolder and the APK quietly landed in the app's own directory instead.
The update check now starts once the vehicle dialog has been answered, or straight away when
there is no vehicle dialog to collide with. -
The telemetry sampler's handle is cleared under the same lock that sets it.
onDestroy
cancelledsamplingTaskoutside the lockensureSamplingtakes, so a task the sampler's own
thread was in the middle of scheduling could be cancelled before it was published — which
cancels nothing. -
A release APK is no longer built unsigned in silence. With no keystore configured the
signing config simply did not exist andassembleStableReleaseproduced an APK that installs on
nothing and looks in the build output exactly like one that would. The keystore stays optional,
so debug builds, unit tests and lint still work on a machine that has none; only a release
assembly insists on it, and-Pevsuite.allowUnsignedRelease=trueis the deliberate way to ask
for a bare APK. -
A saved destination is checked on the way out of storage, not only on the way in.
DestinationFavoritesvalidated coordinates when it wrote them and trusted them when it read
them back, so a preferences file that had been downgraded, restored or hand-edited could hand a
route origin a pair of numbers that is not a place on Earth. Reading now goes through
RoutingConfig.place, the same gate every other door to a destination already used. -
One charger record with a null field no longer empties the whole charger list. Open Charge
Map returns members as explicit JSON nulls —"OperatorInfo": nullon a community-added site,
"StatusTypeID": nullon one whose status nobody has confirmed — and Gson answers those with
JsonNull, which throws on every typed accessor including the object cast. The parse caught
that around the whole response, so a single such site discarded every other charger in the
answer and the screen said there was nowhere to stop on a motorway lined with chargers. Each
record is now read behind accessors that treat a null member as absent, and a record that still
fails to parse is skipped on its own instead of taking the answer with it. -
The arrival screen no longer prints
0 minfor a time the car did not give. The head unit
publishes the remaining distance and the remaining time as two separate signals and sometimes
sends only the first; the missing one was rendered as zero, which reads as "you have arrived" —
the one substitution this app promises never to make. It renders as—now, like every other
unavailable reading. -
A route no longer gives up on the GPS because the network provider went down. Both
providers are subscribed for one fix and the firstonProviderDisabledended the request for
both, so on a head unit with no usable network provider a GPS fix that was still coming was
abandoned and the route refused for want of an origin. A provider going down now removes only
itself, and only the last one left ends the wait. -
The telemetry sampler cannot be started twice. Automatic detection starts a trip from
inside the sampler thread while the binder callbacks arrive on the main one; both call the same
"start sampling if it is not running" check, which was unsynchronised. Two callers could each
find nothing running and schedule a reader, leaving the vehicle polled at 2 Hz with one handle
to cancel it by. -
The car is asked to drive there through the channel that drives. The vehicle answered on
2026-09-09:goTo taken=true guiding=false, route taken=true guiding=true, and the navigation
app's own flag confirmed the second one. Telenav takesIGeneralServicetransaction 23 and does
nothing with it, and transaction 48 is what starts guidance. The tap now sends the route first
and keepsgoToandgeo:as the rungs below, which cost nothing on a tap that works because
they are never reached. The leg chain sends through the same ladder in the same order. -
The button that starts the drive was below the fold, and the one the driver could see reopened
the destination chooser. On the vehicle the tap "did nothing at all and looped back to the
destination window", which is exactly what happens when the plan renders seven cards above an
action that sits some four hundred dp past the bottom of a 648 dp panel: the only button in
sight was the destination chooser, and tapping it reopens the chooser. The action and its caveat
are now pinned in a bar at the bottom of the screen and the cards scroll behind them, so the one
thing the screen exists to offer is the one thing that is always on it. -
The screen no longer claims a handoff worked when it did not.
goTowas accepted and MG4
Navigator still did not guide, so the tap now asks the car instead of the driver:
IGeneralServicetransaction 18 returns a flag the navigation app sets itself, and the ladder
sends a channel, waits up to four seconds for that flag, and moves to the next one if it stays
false. What the screen says is what the car answered — guiding, handed over and not guiding
yet, start it on the map, or thegeo:fallback's type it in there. One probe line records
every rung, so a drive where nothing happens still says which rung refused. A route already
running before the tap is the one case that cannot be judged, and it is reported as asked
rather than as started. -
The car is now asked to drive there, not only to draw it. The route handoff reached MG4
Navigator on the third drive and stopped half-way: the destination appeared on the map and no
guidance ever started.IGeneralServicetransaction 23,goTo(address, latitude, longitude),
is the other command —GeneralServiceturns it intoMapService.goToPoi, which is what the
head unit's own voice assistant calls when someone says "take me there". The tap now sends
that first and falls back to the route handoff, then togeo:, so a car that refuses one still
gets the next. It carries a single point, so a plan with a charging stop guides to the stop —
the leg this screen forecasts — and the screen says the final destination is set after
charging. -
"Naviguer vers" now hands over the route, not just the map. The read-only rule was scoped
by the owner to the car's driving and safety settings — the same boundaryVehicleWriteGate
already draws in code, where AEB, ELK, ACC/TJA and the drive mode are gated on standstill and
the comfort writes are exempt. A destination is on the exempt side of it, so the adapter path
CP-056 left as an open question is now built:IGeneralServicetransaction 48,
startNavFromEVRout(pathway, destination), on the sameGeneralServicethis app already binds
for guidance. It is a call in — the adapter forwards toMapService, which fans the points
out to whichever navigation app registered — not a registration on the channel the head unit
uses to command that app. And it has a waypoint list, so the plan goes over as it was planned:
the destination, with the charging stop on the way to it, instead of the one-or-the-other a
geo:URI forced. Thegeo:path stays behind it, unchanged, for a car whose adapter is not
there or refuses.It says what it did and not more.
MapServiceswallows whatever a listener throws while
fanning out, so an app that refused the points looks like one that followed them. The screen
announces that the route was handed over and asks the driver to check the map picked it up.
The call runs on the screen's worker thread, because the transaction is notonewayand the
adapter fans out synchronously under a lock. (CP-060) -
Two files to carry on a stick, and each of them half a working app. The owner's report:
« Import et l'export des favoris de navigation ne sont pas un fichier à part mais inclus dans
un seul fichier de configuration ». The saved destinations now travel as
destination.<label> = longitude,latitudelines insideevchargepilot-routing.txt, beside the
four keys, andDestinationFavoritesExport/DestinationFavoritesImportare deleted rather than
kept as a second path. Both screens that import the file apply all of it — the routing key
screen saves the destinations, the destination screen applies the keys — so one file and one
import configure a car.RoutingConfig.parseis the trust boundary the hand-editable format
needs: a coordinate that is not finite, is outside ±180/±90, or has no label to tap is skipped
one line at a time and the rest of the file still loads, and a file longer than
MAX_DESTINATIONSis truncated rather than refused. The file carries the API keys in clear
text — it has to, to be importable back — so the export announcement on both screens says so
in the same words. (CP-059) -
"Naviguer vers" opened MG4 Navigator without the destination. The 2026-09-08 drive: the map
came to the foreground and the driver still typed the address in. The map on this car turned out
to becom.telenav.app.arp, notcom.saicmotor.navigation— the package list copied from
EVTasker was right, the assumption about which entry would match was not. An explicit-component
ACTION_VIEWcarrying thegeo:URI is now attempted before theACTION_MAIN/CATEGORY_HOME
fallback: an explicit component skips the intent-filter matching that stopped every implicit
send, so if that activity reads itsIntentdata the destination arrives. It is still reported
asMAP_ONLY, and the screen now says that whether the map took the destination is what the
driver has to say, because nothing on this car reports it back — a driver reading sent would
stop looking. (CP-056) -
Every route refused with "no recent position", even with location granted. The vehicle
session of 2026-09-07 recorded it fifteen times in eighteen seconds: the driver granted fine
location at the prompt, and the origin was refused anyway.LocationSourceread
getLastKnownLocationand nothing else, and on a head unit where no other app subscribes to
the receiver that cache is empty — EVTasker'sCarLocationhad the same failure and the same
note about it.requestCurrentnow asks the receiver itself, on every enabled provider, and
drops the subscription on the first fix or after twenty seconds. The screen says it is looking,
a second tap no longer starts a second search, and the refusal, when it comes, says the
receiver was asked rather than blaming a two-minute-old cache. -
"Navigate to this" never opened MG4 Navigator. The 2026-09-05 drive recorded no handler
forgeo:orgoogle.navigation:on this head unit, sostartActivityhad nothing to reach
and the button announced a failure. EVTasker'sNAVIGATE_TOworks on the same car by falling
back to an explicit component —ACTION_MAIN+CATEGORY_HOMEon the vendor's navigation
package, which is what the head unit's own launcher sends — andMapAppsnow does the same
here. The destination does not travel on that path: the map opens where it is, and the screen
says so and prints the coordinates to type. A handoff that carried the destination and a map
merely brought to the foreground are announced, and recorded in the CP-056 probe, as the
different things they are. -
Importing and exporting the routing configuration found no USB stick. Storage discovery
demanded positive proof that a volume was removable —Environment.isExternalStorageRemovable,
StorageVolume.isRemovable, or a mount name matchingXXXX-XXXX/usb/udisk. This head unit
gives none of the three for a stick it mounts itself, so the scan returned an empty list, the
import announced "no configuration file found" and the export announced "no USB stick that can
be written to" with a stick plugged in. Discovery now follows EVTasker's browser, which has
worked on this car for a year: every mounted volume the app can already list, sorted with the
ones that say they are removable first. The rule that mattered is kept and stated the other
way round — primary emulated storage is excluded outright, so neither the keys nor a diagnostic
bundle can land in storage the driver cannot unplug.writableTargetno longer refuses a
directory just because discovery had not listed it. -
Tapping a destination did nothing when location was refused. The search listed the places,
and the tap that should have routed to one asked for the fine-location grant and stopped there:
a refusal — or this head unit answering without ever showing a dialog, which it does when
another permission in theLOCATIONgroup is already held — landed in a branch that only
re-rendered the screen. Nothing on it changed, so a missing grant read as a dead button. That
branch now says what is missing and where to grant it. -
The routing configuration was guessed at instead of chosen. Import scanned the stick and
offered a list of volume paths — including this app's ownAndroid/data/…folder on the stick,
which is not where a driver puts a file — and a config any deeper than one folder was never
found at all. Import and export now browse, the way EVTasker has on this car for a year: pick
the stick at its root, walk into folders, tap the file to import or Save here for an
export. The file is still identified by parsing it and not by its name, and the diagnostic
bundle uses the same browser, so it lands in the folder the driver will go looking in.
Added
-
Choosing a destination is its own screen, and it uses the whole panel. The owner's report
after the 2026-09-08 drive: « La sélection de la destination se fait dans une liste en bas trop
petite (pas ergonomique) ». It was a text field and two short lists stacked under the charging
plan, on a panel 1920 dp wide and about 648 dp tall, so the results landed below the fold of a
screen whose top half was a plan that did not exist yet.DestinationActivitynow gets the
panel: searched on the left, saved on the right, each column scrolling on its own, import,
export and back on the top bar. The charge-stop screen keeps one button whose label becomes the
chosen place, and plans as soon as the screen returns one — coming back without a choice plans
nothing, because three requests are not spent on a driver who went to look. (CP-059) -
The map's exported activities are recorded on the Q9 probe line. Two drives have now proved
that no destination reaches this car through a published intent filter, and guessing a vendor
action from a laptop is how a build ships a button that does nothing. An exported activity is
the only thing another app can start, soMapApps.entryPointslists their class names — a
SearchResultActivityis a lead whereMainActivityis not. Class names only: no coordinate,
no place, no key. (CP-056) -
Favourite destinations, importable and exportable on a USB stick. A place searched once had
to be typed again on every trip, on a head unit whose keyboard makes that the reason the screen
gets abandoned. Long-pressing a search result keeps it; a favourite is one tap to route to, and
a long press to forget. The list travels asdestination.<label> = longitude,latitudelines
insideevchargepilot-routing.txt, browsed to with the same picker the routing key already
uses, so a second car or a factory reset does not mean researching every address again. The=
separator is what lets a label carry its own commas — an address usually does.It is not synchronised with MG4 Navigator, and cannot be. CP-056's drive on SWI68 found no
activity on this head unit declaring a navigation intent, and the SAIC map app publishes no
provider for its own favourites either — EVTasker'sPlaceStorehit the same wall first. There
is no channel to read that list from or write to it, so this is EVChargePilot's own list and
the file is how it moves between cars. -
The drift companion says at the tap whether it armed. CP-058's probe line only appeared
once fifteen kilometres had been driven, so a drive that stopped short left it blank — which
reads the same as a companion that never armed at all. The handoff now records the leg, the
planned rate and the section count the moment the plan is frozen, or the reason there was
nothing to freeze. Distances and charges only, as before. -
The charge-stop screen says, in the artifact, what it was missing. Two vehicle sessions
came back with Q3 to Q8 and Q10 blank and no way to tell why: every one of those probes fires
downstream of a route request, and no route was ever asked for. Opening the screen now records
the fine grant, whether a position is held, and whether each key is configured — four booleans,
never a key and never a place — so the next bundle names the blocker instead of being silent. -
The guidance capture carries the size of the last parcel on each transaction code. The
2026-09-07 capture had the car's own navigation guiding, and still decoded a remaining distance
and a remaining time of zero, while an undecoded code 11 carried 98 callbacks in the same
window. A size tells a genuine zero from a payload this build reads as the wrong shape. -
A companion that notices the plan stopped being true. CP-058. A plan chosen at the kerb is
a prediction, and predictions go wrong on the road — a headwind, a cold pack, traffic, a driver
going faster than the row they picked. The dashboard now carries one line: what the drive is
actually spending, what that leaves on arrival, and the mildest speed on the road ahead that
gives the shortfall back.It costs nothing. Charge and the odometer are already read once a second and the chosen plan
was written down when the driver tapped Y aller, so detecting drift is a division — no request,
no quota, no network. Only a re-proposal costs a request and that stayed where it was, on the
charge-stop screen, behind a tap, parked. The proof is structural rather than a promise: neither
the calculator nor the companion imports a transport, so neither can reach one.The alarm is hard to trip on purpose. Both instruments quantise — a gauge that steps a whole
percent, an odometer that answers whole kilometres — so the measured rate arrives with a band of
its own, and drift is that band failing to overlap the plan's. On top of that the arrival has to
actually fall under the reserve: a driver going faster than the row they chose and still arriving
at 30 % does not need telling, and a companion that speaks then is one nobody reads when it
matters. Under 15 km of road it says nothing at all, because the gauge's own step is a third of
what this car spends over that distance.One line, stating a consequence. No dialog, no sound, no countdown, nothing that wants a
decision at 130 km/h. "You arrive at 4 %, 6 under your reserve; 110 km/h over the 140 km of fast
road ahead gives it back, for 12 minutes" is a fact about the road. "Slow down" is an
instruction, and this app does not give them. Parked, the same line offers to forget the plan.What is watched is the leg, not the route: where the plan has a charging stop the comparison
ends at the charger, because arriving under the reserve at a destination the plan says to charge
before is the plan working. And what is stored is a distance, a charge, two rates and a list of
number pairs — no destination, no coordinate, no place name, no road name. The plan expires by
itself after twelve hours. -
Three ways to get there, on one card. CP-057. Every part of the answer to "how should I
drive to Alès" was already computed and none of it was a choice: the plan, the charging stop,
the speed what-if and the road profile sat on four cards and the driver did the comparison in
their head, parked, before setting off. The comparison is the decision, so it is now stated —
the route as calculated with its stop, the mildest speed on that same road that removes the
stop, and the same trip avoiding motorways, each with a distance, a time and a charge.Both figures on every row, or neither. A charge saved without the time it costs is advice,
and this screen does not give advice; a row that cannot produce both says which is missing.
The charge after charging is deliberately absent everywhere — it depends on how long the
driver plugs in, which this app does not model and must not appear to.The motorway-free road costs one request, and only when it could change the answer. It is
fetched only where the planned road needs a charging stop, because an hour of departmental
roads to arrive with the same charge is not a choice. No charger search is spent on a road the
driver has not picked. The card states how many route requests the plan took: the allowance
belongs to the driver's key, not to this app. -
The plan can go to the car's navigation instead of being retyped. CP-056. A route worked
out here used to end on this screen: the driver turned to MG4 Navigator and entered it again,
which is where the feature gets abandoned and, worse, where it goes wrong without saying so —
a plan the car is not following still shows an arrival charge. One button, while parked, sends
ageo:destination to whatever navigation app answers.It sends the charging stop, not the destination, whenever there is one.
geo:carries a
single point and no waypoints, so a route planned through somewhere cannot be pinned by its
endpoint. The stop is the leg the forecast is about; the driver sets the rest after charging.
The screen says which one went, and says plainly that the car picks its own road — a caveat on
its own line, never inside the button's label, because a button is read as a promise.This is the first thing EVChargePilot asks a vehicle system to do rather than tell it, and
the boundary that used to say "read-only" now says so with its exception written out. Parked
gate re-read at the tap rather than trusted from the render. One method, one tap, one
destination.stopNav,goHomeandsetFastestRouteare not called anywhere in the tree.
Nothing about where the driver is going reaches a log or the USB bundle. -
The unstable build arms its own validation. Two drives came back with
validationModeOn=false— eleven questions still open, because the toggle sits three taps
from the dashboard and a driver about to leave has no reason to go looking for it. An unstable
build exists to answer those questions, so it now arms itself and the driver's decision is the
one that turns it off. The stable channel is unchanged: none of this is compiled into it. -
Q9: does anything on this head unit accept a destination? The route this app plans is
copied into MG4 Navigator by hand, which is where the feature gets abandoned and, worse, where
it gets silently wrong — a plan the car is not following still displays an arrival charge. The
probe resolves the three shapes of Android navigation intent and records which packages claim
them. It sends nothing, needs no drive and no network, and it aims at a public landmark in
another city rather than anywhere the driver has been, because the answer leaves the car on a
USB stick. -
The destination probe now fires on the firmware it was written for. CP-051's question —
do IMapService transactions 38 and 39 carry the destination — is worth asking at exactly one
moment: while the car is guiding to somewhere. That moment was detected by counting guidance
callbacks, and the capture of 2026-09-04 shows this head unit sending none of them:n=0on
the same line asstatus=1 dist=9788 road=Rue de la Fontaine, the whole state coming from the
synchronous getters. The probe would never have fired on a validation drive, and the bundle
would have carried the process-start sample twice over. A trip is now recognised by a distance
to a destination, a road name or a direction — each of which needs a trip to exist — and a
status code on its own is still not one, because no value is known to mean idle. -
The routing key goes back onto the stick it came from. Configuring this car meant a key on
a USB stick, because typing sixty characters on this head unit is not something anyone does
twice — and there was no way back out, so a second car, a factory reset or the unstable
channel, which is a separate app with its own storage, meant typing it anyway. Export to USB
writes the samekey = valuefile Import from USB reads, under one fixed name so a stale
export cannot be picked up later, and to a removable volume only: the keys are not copied into
a second unencrypted place on the car. The file carries them in clear text, which is what a
file this app can import has to be — so the file says so in its own header, and the screen says
so when the export lands. Parked only, like everything else on that screen. -
One drive answers eight questions, on the unstable channel only. Eleven tickets ended
"verified on a laptop, never on the car", and each named a different question; answered one
at a time they would cost eight evenings. There is now one toggle on the evidence screen
that arms every remaining question at once, and each of them records itself from the
decision it is about — the permission result, the routing response and its sections, the
charger search, the what-if rows or its refusal, the model fit, and the car's own navigation
transactions. Nothing needs a tap while driving, because an instrument that needs one at
130 km/h is a hazard. The diagnostic export carries a singlevalidation-*.jsonin which
every question has a named block, including the questions nothing was recorded for: those
say what would have made them fire, since "no chargers came back" and "the search never ran"
close different tickets. The file carries no API key, no typed destination and no origin
coordinate; it does carry road names and the navigation parcel bytes, and it says so in its
own notes.analysis/CP-055_validation_drive.mdis the checklist, written to be followed by
someone who has not read the source. Stable contains none of it. -
The car's numbers are the driver's now, not a specification sheet's. Usable capacity when
new, state of health, the charger power floor and the reserve to arrive with were constants in
the source, each with a comment admitting it should not be. There is a parked-only screen for
all four: the climb cost, which chargers are offered and where the stop lands all follow what
is entered there. The defaults are unchanged and still the MG4 Long Range sheet — the
difference is that the screen says so, instead of a 61,7 kWh assumption sitting invisibly
inside every forecast. -
"On your trip of 12 March, 110 km/h would have saved about 4 % of charge." The post-trip
speed comparison used to integrate battery power, so on this car it had nothing to integrate.
Where a trip carries no power it is now measured on the gauge the driver watched fall: the
charge spent over the recorded motorway stretch, against what CP-052's fit expects at 90, 100,
110, 120 and 130 km/h, with the equivalent kilometres from that trip's own percent per
kilometre — no pack capacity anywhere. A stretch the gauge moved a step or less on is refused
rather than divided by its own resolution, and the two units never share a table. -
The speed what-if now works on the car it was written for. This MG4 declares battery power
and never publishes it — three diagnostic bundles, 51 samples, not one reading — so the kWh
model behind the what-if was never going to train, and the screen would have said "not yet"
forever. Consumption is now fitted in percent of charge per 100 km straight from the gauge and
the speed, which needs no battery power and no pack capacity from a specification sheet. The
saving comes out in the unit the driver's own gauge is in. -
The two screens that stay blank now say why, and stop promising. Battery power is what the
energy breakdown and the post-trip speed comparison are made of, and this vehicle never sends
it. They said "not validated yet", which invites coming back to a screen that will never fill
in. They now say the vehicle does not publish it, now or later. -
"If you drive at 110 you will not need to charge" — with the twenty minutes it costs.
The charge-stop screen now says what driving differently would change on the route it just
planned. Where the road ahead is fast — which comes from the router's own duration over its
own distance, not from a speed-limit dataset — it shows what 130, 120, 110, 100 and 90 km/h
would save and what each would cost in delay, and puts first the mildest slowdown that
removes the charging stop altogether. A saving is never shown without its delay: that would
be advice, and this app does not give advice. Consumption comes from the same fitted model as
the post-trip comparison, so the two screens cannot disagree, and outside its trained
envelope this refuses rather than inventing a number. The same request also brings back a
second road, compared on distance, time and charge. ORS bills one request either way, so the
alternative costs the driver's quota nothing. -
"Do I need to charge on this trip, and in how many kilometres" — the sentence this app was
described by, now on a screen. Type a destination while parked, choose which of the answers
you meant, and the app plans to a reserve rather than to zero: either the charge left on
arrival with the margin above that reserve, or the distance at which to stop. The stop
distance is computed at the top of the rate band, not its middle — being told to charge
earlier than necessary costs minutes, being told to charge later costs a tow truck — and a
plan too uncertain to act on is refused instead of shown. The arithmetic isChargeStopPlan
in EVHardware, seven JVM tests, and it has no idea a network exists. -
A routing key that is yours. No key ships in the APK, so there is a screen to enter one
and a USB import for the realistic case: this head unit's picker answers "no apps can perform
this action", so akey = valuefile is found by scanning the stick and parsing it, the same
format and the same reasoning as EVABRPUploader. The key is stored through the Android
keystore, is never displayed back, never logged, and never reaches a diagnostic export — the
exporter reads no preferences at all, which is a stronger guarantee than a filter. -
One place, and only one, where this app opens a socket.
httpson the first URL and on
every redirect hop, checked against the host the driver configured, with redirects not
followed by the platform; the key travels in anAuthorizationheader and never in a query,
even where the service documents a query; timeouts, a 2 MB cap, single flight, and a local
quota counter so the app refuses before the server does. Anhttps→httpLocationis a
man in the middle and is refused, not followed. -
Elevation, which the car will never give. The route comes back as a 3D geometry, so climb
and descent are shown with the distance and duration, and CP-050 has something to model with.
OpenStreetMap under ODbL: the service's attribution is displayed wherever a route is. -
And where the stop is, not only how far away. With a second key of the driver's own, the
screen names the last charger reachable before the reserve is reached — the last, not the
nearest, because stopping earlier than necessary costs a driver time they came for — with its
power, its connector as this car can use it, and the charge it would be reached with. Open
Charge Map was chosen over the routing engine's own POI service for the two things that
service cannot answer: which connector, and how old the record is. Every charger is shown
with its data provider and the date the record was last confirmed, because a charger dataset
is wrong the day it is published and hiding that turns a suggestion into a promise
(CC BY 4.0 for contributed records, the provider's own licence for imported ones, attributed
on screen either way). What leaves the car is a 40 km stretch of road around the planned stop
and a 5 km corridor — never the route, never the origin, never the destination. -
The Cévennes are on the screen, in charge rather than in metres. The route already showed
climb and descent; now they cost something. The stop distance and the arrival figure carry a
grade term fromRouteGrade, on its own line, labelled as modelled and never as measured —
and absent entirely when no profile came back, because a zero would be a claim that the road
is flat. The profile itself is filtered first: elevation behind these geometries is a sampled
terrain model that wobbles a metre or two at every one of thousands of points, and summing
that raw turns a flat road into hundreds of metres of imaginary climb. A 10 m threshold
discards the noise and keeps the col.
Changed
-
The dashboard stopped introducing itself and started reading larger. The top bar spent
80dp of a 720dp screen on the app's own name — which the driver had just tapped to get here —
and on a permanent "vehicle data available", a line that said what the numbers underneath
were already saying. Both are gone. The state of charge, the one Display element on this
screen, is now 56sp instead of 32sp, and every value on it moved up a step of the type scale;
the battery card grew with them. When the car stops answering, the line under the state of
charge says so in red instead of a banner saying it in orange all day: text and colour
together, in the place where the missing number is, and nothing appears, moves or asks for a
tap while driving. Sub-screens keep their title bars, where a title and a way back are the
point of the bar. -
The app says which version it is, and how to find its source from a car with no keyboard.
A version badge sits at the top of the trip rail, where the app's name used to be in the bar,
and opens an About with the version, the author and the repository as a QR code — encoded on
the device, no network involved, modelled on EVTasker's. The address is deliberately not a
link: this head unit has no browser to hand it to, and a tap that does nothing is worse than
a code a phone can read off the glass. -
"Works offline" stopped being true in one direction only, and the documents say so.
Network and location were approved for route planning, which retires the first line of this
app's safety argument — an app that cannot open a socket cannot exfiltrate a trip history, and
that property came free from not declaring a permission.PRODUCT.mdnow states the boundary
as it is rather than as it was: everything shipped so far is local, a route request is the one
thing that will ever leave, and it carries an origin, a destination and a road profile to a
host the driver configures — never a trip, an evidence capture, a diagnostic bundle, a charge
or an identifier. The three permissions are in the allowlist with a security note each and in
the manifest, by the owner's decision, ahead of the routing client that will use them:
INTERNETopens no socket yet and a granted position is compared to nothing yet.
FOREGROUND_SERVICE_LOCATIONstays out, because no service reads a position;
ACCESS_COARSE_LOCATIONhad to come in, because Android 12+ refuses a fine-only declaration,
and a coarse-only grant will have to be treated as a refusal rather than as an origin. Engine chosen
(OpenRouteService, key entered by the driver, because a key inside a published APK is not a
secret), transport rules written down, security gate re-run green twice.One thing to confirm in the car:
CAR_SPEEDis in permission groupLOCATION, and on API 28
the system grants a permission without a dialog when the app already holds another from the
same group. A driver who granted speed at startup may therefore be handed location with no
prompt. The speed request and the location request are kept in separate call sites so the
coupling stays visible rather than merged into one silent grant.
Fixed
- Trip distances were 3.6× too long. Speed is read through EVHardware, which converted a
property this firmware already reports in km/h, so every integrated distance carried the same
factor: a 2.4 km town route was recorded as 8.46 km. Fixed in the library and picked up here;
trips already stored keep the distances they were recorded with.
Added
-
The dashboard forecasts the charge you will arrive with. The screen the whole trip
companion was supposed to lead to could not be built the obvious way: CP-003 proved this
firmware declares battery power and never publishes it, so nothing can integrate kWh. Charge
per kilometre needs no energy unit, and both its inputs read. The new "Arrival charge" screen
takes the remaining distance from the car's own guidance — read through synchronous getters,
never by registering on the adapter's callback fan-out — and spends it at a rate taken from
your recorded trips, or from the vehicle's own range estimate until three trips exist.
It refuses to show a figure whose band is wider than fifteen points, because a forecast that
says "somewhere between 20 and 60" is not a forecast. Trips recorded before the 3.6× speed fix
are excluded by the estimator rather than silently averaged in. -
The bundle answers the speed-unit question by itself. A trip distance is integrated from
speed, so a wrong speed unit scales it by the same factor and nothing in the number says so —
a 2.4 km drive recorded as more than 7 km was the only symptom, and it needed somebody to
notice. The unstable signal recorder now also reads the navigation adapter's odometer, which
is not where the snapshot's odometer comes from and which nothing derives from speed. The trip
artifact carries the ratio between the two distances and names what it means: consistent, the
m/s factor applied twice, a disagreement that is neither, or too short a drive to tell. -
The unstable channel records its own evidence and the export carries all of it. Validation
had to be driven by hand: open a screen, arm a probe, remember a save button, then export —
and the run that mattered most, the one where a probe heard nothing, was the one nobody
thought worth saving. The guidance listener and a session-long signal recorder now arm when
the application starts, and one "Export to USB" writes the guidance trace, the signal
statistics and the recorded trip summaries into the bundle by itself. The probe screen became
a window onto the recorder rather than its owner, so closing it no longer ends a capture.
Trip summaries travel because a distance integrated from a wrong speed scale shows up there
first. Stable arms nothing and contains none of it.
Fixed
- The dashboard now asks for the vehicle signals it already declared. AAOS classifies
CAR_SPEEDandCAR_ENERGYasdangerouspermissions, so declaring them in the manifest
granted nothing and every matching property read failed silently. The first on-vehicle test
showed the consequence: no speed anywhere, trip start and automatic detection permanently
disabled, and the USB diagnostic export refusing because it could not prove the car was
stopped — all of it indistinguishable from a firmware that publishes nothing. The main screen
now requests the two at launch and logs a denial. No permission was added: the manifest is
unchanged, and the app still holds no location, network, overlay or vehicle-write capability.
Added
- A route source exists after all, and the unstable channel can prove it. The head unit's
adapter service fans remaining distance, remaining time, guidance status and road name out to
any registered listener, so a route no longer requires a network, a permission or an account.
A parked-armed probe in the unstable channel records that fan-out over a drive and saves it
as a trace file the diagnostic export carries to USB. Nothing is displayed as a value: the
distance callbacks carry no proven unit and the status codes no documented meaning, so every
reading stays raw pending the on-vehicle run. The stable channel is unchanged and gains no
capability.
Changed
- Every probe now leaves the car on a USB stick, not through the clipboard. The guidance
probe kept its trace in memory and offered a copy button, so the only way off the head unit
was a paste into some other app — which the read-only, offline boundary makes pointless, and
which loses the census and the caveats a reading has to be judged against. Both probes now
write bounded JSON into the folder the diagnostic export bundles, so one Export to USB
carries the signal captures and the guidance trace out together, each hashed in the bundle
manifest. The copy actions are gone, and the capture screen no longer points at adb.
Security
- The application declares an empty task affinity, reducing launcher-task hijacking exposure on
the target Android 9 platform without adding a permission or changing vehicle access.
Added
-
A two-column post-trip breakdown separates facts from model claims. Integrated consumed,
regenerated and net battery energy plus the unmodelled remainder sit beside banded traction
and residual estimates. Plain-language summaries call out model noise, negative error and the
lack of a vehicle consumer meter. Unsupported battery-heating and accessory categories never
appear; missing evidence gets a complete explanatory state. -
Trip detail exposes honest energy attribution. Integrated consumption is reconciled with
modelled traction, measured regeneration, climate-conditioned residual groups and a visible
unmodelled discrepancy. Every attributed number has a band; model-noise residuals say they are
indistinguishable from zero, negative residuals remain model error, and the copy explicitly
says no vehicle per-consumer measurement exists. -
Completed trips can show a parked-only motorway speed comparison. The panel offers only
90–130 km/h reference speeds inside the exact trained speed/temperature envelope, labels every
result as estimated, and propagates the model band into energy and equivalent-range intervals.
Missing model evidence, motorway samples, power, temperature, range baseline or fresh parked
speed produces an explanation instead of a number. Training remains bounded and fail-closed
until CP-003 validates exact firmware evidence. -
Unstable telemetry evidence now has a hard retention ceiling. Captures remain
app-private and atomic, but only the eight newest JSON summaries are kept; stale temporary
files are removed before a write. Repeated validation runs therefore cannot grow AAOS storage
without bound. -
Thermal and climate telemetry is grouped without implying energy use. Outside, cabin and
battery temperatures now sit with HVAC, AC, auto, econ and recirculation states, fan
level/maximum and both target temperatures. Every field stays independently nullable, so a
partial firmware snapshot remains useful and every missing value keeps an explained em dash.
Battery temperature is evidence-gated per firmware and therefore remains explicitly
unvalidated until CP-003 proves it. The block states that it shows climate state only and that
no HVAC power is measured; it adds no control path and retains no additional history. -
Diagnostics and vehicle-test evidence export as one bounded USB bundle. The parked-only ZIP
carries runtime/APK/signer identity, exact firmware, service and latest-sample state, property
probes, provenance, bounded logs, previous crash, and up to eight scenario-named CP-003 JSON
captures. Its manifest provides byte counts and SHA-256 per artifact. Report, capture and bundle
limits are 128/64/768 KiB; the atomic temporary file lives on USB, no archive accumulates on
AAOS, and no network or storage permission is added. Unstable diagnostics now require choosing
the physical test scenario before capture so evidence cannot be mistaken for another segment. -
Battery power flow is directional, static and evidence-gated. The signed kW value is paired
with a centred, non-animated scale and explicit output/regeneration text, so colour never
carries direction alone. EVHardware's per-firmware evidence catalogue starts empty: until
CP-003 validates a generation, raw power is not integrated into normal trips and every
dependent dashboard calculation renders as unavailable instead of confidently applying an
assumed scale or sign. Trusted trip totals store only a small exact firmware/conversion tag;
legacy and mismatched totals are excluded from models, while CP-003's bounded unstable
evidence recorder remains the separate path for proving the raw signal. CSV and JSON schema 2
exports preserve that tag explicitly; legacy exports carry empty/null evidence instead of
silently presenting the totals as validated. -
Automatic trip detection is fail-closed and parked-configurable. A small persisted switch
enables the existing foreground sampler, while EVHardware alone decides start/end boundaries.
Five seconds of motion starts a trip; a confirmed two-minute standstill ends it. Missing speed
or a telemetry gap can do neither, and the dashboard states whether it is waiting, confirming,
or unavailable. Ten consecutive unavailable speed reads suspend idle background polling until
the next foreground launch, keeping unsupported firmware quiet and bounded. -
Vehicle range stays authoritative beside a visibly modelled adaptive estimate.
Up to eight recent trip averages feed EVHardware's estimate after three usable observations;
the dashboard prefixes it with≈and shows the observed-spread uncertainty in kilometres.
Missing current power, usable energy, or history leaves an explained gap instead of a guess. -
The dashboard now separates immediate and trip consumption without competing with SOC.
Instant kWh/100 km uses EVHardware's five-second smoothing and stays unavailable below
5 km/h; regeneration remains negative. The adjacent trip average keeps the accumulator's
integrated consumed-energy/distance definition, and both numbers are labelled as arithmetic
derived from vehicle readings rather than measurements or predictions. -
Completed trips can now leave the head unit without granting broad storage access. The
ledger exports either one trip or all retained trips as a documented CSV summary or a
schema-versioned JSON document with sample tracks. Files are bounded, written atomically to
app-private storage on a worker thread, and shared only through a user-chosen app with
temporary read access; unavailable readings stay empty or null rather than becoming zero. -
A parked-only trip ledger now exposes the complete local record. Up to 200 trips appear
newest first with recorded totals, SOC change, consumed/regenerated energy and a lightweight
speed/power trace when samples exist. Missing values remain explicit and explainable; single
and bulk deletion require a fresh readable speed at or below 0.1 km/h and a full-screen
confirmation. -
The unstable evidence capture records usable battery capacity as an unvalidated candidate.
The public post-AAOS 9 property is shown beside normal snapshot fields, remains null when the
runtime does not publish it, and is never copied intoEnergySnapshotor a stable screen. -
Unstable builds can capture firmware-specific telemetry evidence from one parked-only
diagnostics screen. The screen shows per-signal availability and statistics, records at the
dashboard's 1 Hz cadence plus a 200 ms burst for the first minute, and saves schema-versioned
JSON atomically in app-private storage. Start and stop both fail closed until speed is readable
and at or below 0.1 km/h. The completed file path stays visible foradb pull, while its
Markdown table can be copied without storage or network permission. Stable builds contain no
capture activity, recorder controller or file store. -
A trip keeps recording while the driver looks at something else. Sampling used to live
between the dashboard'sonStartandonStop, so switching to the media app for ten minutes
left a hole in the middle of the drive: the accumulator refuses to integrate across a gap
longer than five seconds, correctly, since it has no idea what the car did in between, and the
trip came back missing the distance and energy of the part nobody was watching. A foreground
service now owns the sampler and the trip history, and the dashboard is one of its readers. It
samples while a dashboard is bound or a trip is recording, and stops itself when neither is
true, so nothing reads vehicle properties for nobody. The service claims only the foreground
type its held permissions back, and the notification stays a low-importance status line — it
never interrupts a moving car. -
Every figure on the dashboard now says what kind of claim it is. The energy work ahead is
mostly inference — the MG4 publishes no per-consumer counters, so the climate share, the speed
comparison and the arrival state of charge will all be models — and a model drawn in the same
typeface as a vehicle reading is a vehicle reading as far as the driver is concerned. Readings
are mapped into EVHardware'sProvenancedcarrier, which cannot hold an estimate without its
uncertainty band and cannot hold a number for a value it calls unavailable. An estimate wears a
≈and its band; a missing figure stays an em dash whose reason — unsupported generation,
signal not published, not enough samples — is spoken by the reading's description and spelled
out in the diagnostics report. The distinction is carried by text, not colour, so it survives
daylight and reaches a screen reader. -
The diagnostics dialog lists what each energy property actually answers. A field showing
an em dash says a signal is unusable but not why, and unsupported, declared-but-never-published
and unreachable all look identical from the driver's seat. The report names the status, the raw
value and the publication timestamp per property, so a firmware generation's telemetry surface
can be recorded from the car. -
Initial read-only energy dashboard.
-
Nullable vehicle snapshot backed by EVHardware.
-
Parked-only local trip recording with tested distance and energy integration.
-
Atomic bounded trip history and in-app crash diagnostics.
-
Shared EVHardware energy snapshot with battery power, pack temperature, charging and
climate fallbacks; shared trip integration and persistence replace app-local copies. -
Complementary Automotive API 33 and screen-accurate API 28 emulator profiles, with setup,
launch, install, logs and stop commands throughmise. -
The suite's CI and security workflows: blocking permission-drift and secret gates,
mobsfscan/semgrep SARIF, OWASP dependency scan, a tag-driven stable release and a rolling
unstable pre-release. Unlike the launcher apps, neither channel self-updates — this app
declares no network permission at all. -
Issue, security and pull-request templates matching the rest of the suite.
Changed
-
A recorded trip now stores its sample track alongside its totals. The recorder hands the
track to EVHardware's versioned history file, which keeps one sample per five seconds and
drops the oldest tracks — never a whole trip — when the file approaches its size ceiling.
Nothing on screen changes yet; the tracks are what the consumption, climate-share and
arrival-state-of-charge models will be fitted from, and they have to exist before those
models can. -
The diagnostics report is built on the sampler thread. It reads ten vehicle properties over
binder and the crash file off disk, neither of which belongs on the thread drawing the dialog.
Fixed
-
The battery temperature no longer reads 0 °C on a warm car. The reading came from a
property SWI68 declares and never publishes, which answers a read with a plain zero that looks
exactly like a measurement. EVHardware now checks the status the vehicle attaches to each
property, so an unpublished signal arrives as unknown and the dashboard shows an em dash —
the rule this app already stated and could not previously enforce. -
A trip's duration now counts only the time covered by usable samples. Hiding the dashboard
for ten minutes used to add ten minutes to a trip whose distance and energy had not moved,
which made the consumption average divide numbers that did not belong together. -
The vehicle snapshot read by a control tap is published across threads (
@Volatile).
Commit: 3ef6a2f