Skip to content

Latest commit

 

History

History
1169 lines (1026 loc) · 88.2 KB

CHANGELOG.md

File metadata and controls

1169 lines (1026 loc) · 88.2 KB

Changelog

All notable changes follow Keep a Changelog.

[Unreleased]

Added

  • 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's EcoDrivingMonitor, 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 carries vehicle_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 the minHeight each 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 own describe(): 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-settings loads 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 run and run-unstable do 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 -hXXXdp qualifier 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 rolling unstable 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 — https and 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 into Download is 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 of goTo calls would not do it — goTo takes 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; and DiagnosticUsbStorage's write fallback, which already carried an internal seam 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. NavLegs grew one seam to allow it: an internal 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. PowerHistoryPolicy and the handoff assembly below are covered too. ProvenanceText and DriftCompanion are deliberately still not: both are Context-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. ChargeStopHandoff holds it — the geo:/goTo single point, the route channel's pathway and destination, and the legs in the order they are driven. Nothing in it needed an Activity, 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. removesStop moved 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 kWh and %.1f %% were spelled out again in four screens and the recording notification, next to the PATTERN_* 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 = value text 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 write evchargepilot-settings.json, which carries all of it under routing, vehicle and destinations, 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 no vehicle section 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 the key = value file 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 a note field 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: https with 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 own versionName were computed by two different formulas: the workflow named the file EVChargePilot-unstable-0.2.0.85.apk while the APK inside it reported 0.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 becomes 0.2.0.0-unstable — older than any release, which is what a local build is.

  • A 403 is no longer always read as a spent quota. HeiGIT answers 403 both 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: 0 closed RoutingQuota with 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 a 0 sitting 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 from onCreate, 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 the Download folder 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 cancelled samplingTask outside the lock ensureSampling takes, 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 and assembleStableRelease produced 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=true is 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. DestinationFavorites validated 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": null on a community-added site, "StatusTypeID": null on 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 min for 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 first onProviderDisabled ended 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 takes IGeneralService transaction 23 and does nothing with it, and transaction 48 is what starts guidance. The tap now sends the route first and keeps goTo and geo: 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. goTo was accepted and MG4 Navigator still did not guide, so the tap now asks the car instead of the driver: IGeneralService transaction 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 the geo: 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. IGeneralService transaction 23, goTo(address, latitude, longitude), is the other command — GeneralService turns it into MapService.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 to geo:, 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 boundary VehicleWriteGate 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: IGeneralService transaction 48, startNavFromEVRout(pathway, destination), on the same GeneralService this app already binds for guidance. It is a call in — the adapter forwards to MapService, 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. The geo: path stays behind it, unchanged, for a car whose adapter is not there or refuses.

    It says what it did and not more. MapService swallows 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 not oneway and 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,latitude lines inside evchargepilot-routing.txt, beside the four keys, and DestinationFavoritesExport/DestinationFavoritesImport are 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.parse is 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_DESTINATIONS is 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 be com.telenav.app.arp, not com.saicmotor.navigation — the package list copied from EVTasker was right, the assumption about which entry would match was not. An explicit-component ACTION_VIEW carrying the geo: URI is now attempted before the ACTION_MAIN/CATEGORY_HOME fallback: an explicit component skips the intent-filter matching that stopped every implicit send, so if that activity reads its Intent data the destination arrives. It is still reported as MAP_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. LocationSource read getLastKnownLocation and nothing else, and on a head unit where no other app subscribes to the receiver that cache is empty — EVTasker's CarLocation had the same failure and the same note about it. requestCurrent now 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 for geo: or google.navigation: on this head unit, so startActivity had nothing to reach and the button announced a failure. EVTasker's NAVIGATE_TO works on the same car by falling back to an explicit component — ACTION_MAIN + CATEGORY_HOME on the vendor's navigation package, which is what the head unit's own launcher sends — and MapApps now 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 matching XXXX-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. writableTarget no 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 the LOCATION group 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 own Android/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. DestinationActivity now 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, so MapApps.entryPoints lists their class names — a SearchResultActivity is a lead where MainActivity is 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 as destination.<label> = longitude,latitude lines inside evchargepilot-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's PlaceStore hit 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 a geo: 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, goHome and setFastestRoute are 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=0 on the same line as status=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 same key = value file 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 single validation-*.json in 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.md is 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 is ChargeStopPlan 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 a key = value file 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. https on 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 an Authorization header 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. An https → http Location is 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 from RouteGrade, 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.md now 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: INTERNET opens no socket yet and a granted position is compared to nothing yet. FOREGROUND_SERVICE_LOCATION stays out, because no service reads a position; ACCESS_COARSE_LOCATION had 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_SPEED is in permission group LOCATION, 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_SPEED and CAR_ENERGY as dangerous permissions, 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 into EnergySnapshot or 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 for adb 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's onStart and onStop, 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's Provenanced carrier, 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 through mise.

  • 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).