Skip to content

Unstable

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Oct 20:06

Unstable 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. The ActivityView pane 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 (EVHardware EcoScore).
  • 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 (EVHardware ChargeEnergy).
  • 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 by goTo it comes back over the map only when the map is the
    focused task (REAL_GET_TASKS, read-only), and the map 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 (EVHardware 096d85d).
  • 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_EMBEDDABLE from the platform signature. The pane needs
    INTERNAL_SYSTEM_WINDOW, newly declared: see SECURITY.md. [split_screen] now reports
    embedding_permission_granted and map_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-preview shows 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-screen now
    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-preview plans 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.xml lists 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 with Trying to start activity before ActivityView is ready. The car was already guiding,
    so goTo went 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. Now goTo is 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, and release() 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 of trips.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 once isMapNavigating is 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's tools/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 is UNPROVEN.
  • 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.txt gains [split_screen]: whether the car's
    map could be embedded beside the companion (hidden ActivityView present, 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 on split_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 records trip 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
      written monitor-journal.json records 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 isMapNavigating turns 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 as voice_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
      drivingStress for each exported trip with a track.
  • 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).

Commit: 3ef6a2f