App 2.19.0: EVHardware updated from upstream (malys/EVHardware e4e1158)
Three-way merge from b595ccb, the commit Fatih copied, to upstream's current state; Fatih's vendor reads (tyres, mileage, charge status, SWI69) stay. LICENSE.md records the base for the next update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -8,6 +8,258 @@ All notable changes to this project are documented here. Format follows
|
||||
|
||||
### Added
|
||||
|
||||
- `diag.DiagnosticInbox`: posts one diagnostic artifact (a bundle zip or a text report) to an
|
||||
HTTPS inbox the driver runs at home, as one JSON object shared by every app. No redirect
|
||||
followed; an address with credentials or a query is refused; 1 MiB cap.
|
||||
|
||||
### Changed
|
||||
|
||||
- Hard acceleration and hard braking start at 2.5 m/s², up from 1.5. On the car, ordinary town
|
||||
drives counted 7 to 16 hard stops every 10 km, and regeneration on High alone comes close to
|
||||
the old line. Trips recorded before keep the counts they were given.
|
||||
- `EnergySnapshot.climate.passengerTargetCelsius` is null when the car publishes no passenger
|
||||
setpoint, instead of a copy of the driver's. A screen that shows one zone for both seats says so.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Screen brightness rounds both ways: 10 % was written as native 25 and read back as 9 %.
|
||||
- A trip that started on the head unit's factory clock is dated from the corrected one.
|
||||
- `RuntimeInterfaceProbe` records ABSENT, not DENIED, when the hub hands over another interface:
|
||||
ICarPowerService and IVehiclePropertyService answer as IVehicleSettingService on SWI68.
|
||||
- The charge diagnostic line shows `pack_delta_kwh=unavailable` for an unwatched charge.
|
||||
- The ACC/TJA and speed-limiter getters log the value they read.
|
||||
|
||||
## [1.18.0] - 2026-10-16
|
||||
|
||||
### Added
|
||||
|
||||
- `EnergySnapshot`, `TripSample` and `EnergyTripSummary` carry the raw drive mode and regen
|
||||
codes (`getDriveModeRaw`, `getRegenLevelRaw`; 6 is one-pedal on SWI68). Raw, so an unknown code
|
||||
stays unknown instead of reading NORMAL or MEDIUM; `DriveMode.ofRaw` and `RegenLevel.ofRaw`
|
||||
decode exactly.
|
||||
- `EnergyTripSummary` counts driving style at the live 1 Hz rate: moving and hard time, hard
|
||||
accelerations and hard brakings as episodes. The 5 s track had smoothed short hard stops away.
|
||||
- `EcoScore`: a trip out of 100 from anticipation (hard brakings per 10 km), smoothness (hard
|
||||
share) and efficiency (charge against the driver's own expectation). Unmeasured parts drop out.
|
||||
|
||||
### Changed
|
||||
|
||||
- The eco verdict's TYPICAL zone is one residual either side of the expectation, half the
|
||||
prediction band. At the full 1.96 band almost every drive read TYPICAL.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `TripDetector` ends an automatic trip two seconds after the car reports P or a plugged cable,
|
||||
instead of after the two-minute standstill. The SWI68 head unit powers off about twelve
|
||||
seconds after P, so an automatic trip died with the process before the timer ran out.
|
||||
- `SocConsumptionFitter` falls back to two coefficients when the thermal term comes out
|
||||
negative, instead of refusing the fit. The 2026-10-16 bundle refused 87 segments for this
|
||||
reason alone.
|
||||
- `CarPropertyEvidence` no longer lists SWI68 battery power as never published: the vendor pack
|
||||
fallback reads −32,9 to +32,4 kW on a town drive. Screens now say "not validated yet" instead
|
||||
of "this vehicle never publishes battery power".
|
||||
|
||||
## [1.17.0] - 2026-10-14
|
||||
|
||||
### Changed
|
||||
|
||||
- `SET_CHARGE_LIMIT` is `writeProven = false`: SWI68 reads the limit back as `5`, a code, while
|
||||
the write sends a percent. Hidden from editors and refused by executors until one write is
|
||||
seen to set the limit on the car's screen.
|
||||
|
||||
### Added
|
||||
|
||||
- Probe-only vendor charge readings for the telemetry evidence: `BMS_CHRG_STS`, the AC and DC
|
||||
plug states and the kWh-since-plug-in counter, ids from MG4Control's SWI133 readings and
|
||||
unproven on SWI68. Recorded as `candidate.vendor*`; nothing reads them as values yet.
|
||||
- `RuntimeInterfaceProbe.componentMap()`, part of `survey()`: every service the
|
||||
`com.saicmotor.*` packages declare, with its exported flag and permission, from
|
||||
PackageManager metadata only. Nothing is bound or called; capped at 64 packages and
|
||||
40 services per package, with the cut recorded.
|
||||
- `windows.widestPercent` in the telemetry evidence, from `EnergySnapshot.widestWindowPercent`.
|
||||
- `SocConsumptionFitter.describe()` ends with the solve's outcome once both gates pass:
|
||||
`fit=ready rmse=…` or `fit=refused(<guard>)`. The existing prefix is unchanged.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`SaicCharging.chargeLimitPercent()` answers null outside 40–100 %.** SWI68 answered `5`,
|
||||
a code rather than a percent; callers now take their "limit unreadable" path. The setter
|
||||
is unchanged.
|
||||
- **The battery ledger ignores readings taken on the factory clock.** A head unit woken by a
|
||||
night charge runs on its late-2018 clock until it syncs; one such entry sorted first in the
|
||||
ledger and split the charge. `BatteryLedgerRecorder` now drops samples before 2025-01-01.
|
||||
|
||||
## [1.16.0] - 2026-10-11
|
||||
|
||||
### Added
|
||||
|
||||
- `EVHardware.probeFrontDoors()`: front-door evidence on firmwares the door feature does not
|
||||
claim. Connects and subscribes to DLOCK_DOOR_OPEN_STS whatever `hasDoorVolumeFeature()` says
|
||||
(MG4Control reads it through the change subscription on SWI68/69/131/165) and reports the
|
||||
managers, the subscriptions, the on-demand read and the last event per front door.
|
||||
`frontDoorOpenOrNull()` stays gated until a car confirms.
|
||||
- `SaicClimate.passengerTempState()`: the passenger target as the service answers it, -1
|
||||
included, for the diagnostic.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `SET_PASSENGER_TEMP` no longer claims SWI68: the passenger target reads -1 there, so nothing
|
||||
shows the write lands.
|
||||
|
||||
## [1.15.1] - 2026-10-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`PASSENGER_TEMP` and `DOORS_LOCKED` no longer claim SWI68.** Both were declared from the
|
||||
getters the SAIC services expose, never from a read on the car, and a SWI68 MG4 reported
|
||||
both `NOT_READABLE` (2026-10-11). The passenger target reads a zone a single-zone car does not
|
||||
have; the service answers -1. `getDoorLock` reads back the lock *request* property, and the
|
||||
car answered 0, which is neither of the two named values. The editor now hides both there.
|
||||
SWI165 keeps them until a car says otherwise.
|
||||
|
||||
## [1.15.0] - 2026-10-08
|
||||
|
||||
### Added
|
||||
|
||||
- **Exterior-light conditions and standstill actions (CR-046).** Conditions `HEADLIGHTS`
|
||||
(off / on / daytime running), `HIGH_BEAM_ON` and `FOG_LIGHTS_ON`, plus gated actions
|
||||
`SET_HEADLIGHTS` (ON / AUTO), `SET_HIGH_BEAM` (OFF / AUTO) and `SET_FOG_LIGHTS`. They use the
|
||||
standard AAOS `HEADLIGHTS` / `HIGH_BEAM_LIGHTS` / `FOG_LIGHTS` `_STATE` and `_SWITCH` ids.
|
||||
Writes happen only at 0 km/h. A rule can never switch the low beams OFF, and can never force
|
||||
the high beam ON (`ExteriorLights`). **Proven on no generation yet**: `ExteriorLights.PROVEN`
|
||||
and `@SupportedOn` are empty and `writeProven` is false, so the entries stay hidden and inert
|
||||
until a capture proves a generation.
|
||||
- **Read-only vendor-surface tools (CR-044, CR-045)** for an app's unstable capture.
|
||||
`VendorSurfaceProbe.socSourcesReport()` reads every candidate source of state of charge side by
|
||||
side. `VendorSurfaceProbe.capture()` together with `VendorSurfaceRules.diff()` reads every
|
||||
`CarPropertyManager` property and every value getter of the vendor setting objects, before and
|
||||
after a setting is changed on the car's own screen, and lists what changed, naming the light
|
||||
properties. No setter is called: getter selection is pinned by `VendorSurfaceRulesTest`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`EcoDrivingMonitor.harshSharePercent` counts hard braking as well as hard acceleration.**
|
||||
The live steadiness share read only positive steps past `HARSH_ACCELERATION_MS2`, while
|
||||
EVChargePilot's trip review (`DrivingStyle`) has always counted both signs at the same threshold,
|
||||
so the live gauge read lower than the summary of the same drive — roughly half in stop-and-go.
|
||||
Thresholds are unchanged.
|
||||
- **The trip history keeps four times as many tracks.** `EnergyTripHistoryStore`'s default
|
||||
`maxBytes` goes from 512 KiB to 2 MiB. At about 145 KB per hour of driving, 512 KiB held roughly
|
||||
fourteen short trips' tracks, so each new drive evicted older tracks and the consumption model's
|
||||
segment count fell as trips were added (51 to 47 on SWI68, 2026-09-30). One long trip's
|
||||
4 096-sample track alone did not fit. Every reader parses the file off the main thread and the
|
||||
write stays atomic, so the cost is parse time and memory per read, not a new failure mode.
|
||||
|
||||
### Added
|
||||
|
||||
- **CUSTOM drive mode settings (CR-040).** `CustomDrive` reads and writes powertrain response,
|
||||
steering weight and brake-pedal feel by index (0 Eco/Comfort, 1 Normal, 2 Sport) on the VSM
|
||||
generations: `…Level` methods on SWI68/SWI165, `…Mode` methods on SWI69/SWI131/SWI132. The
|
||||
index↔code tables live here only — the pedal's Normal is `0`, the A9 powertrain uses the
|
||||
drive-mode scale. Writes are standstill-gated. SWI133 answers unavailable: its route is a raw
|
||||
property id this project has not verified. Codes beyond RI-006 are leads until a car confirms.
|
||||
- **`DrivingProfile` carries the CUSTOM settings and an optional `ProfileClimate` (CR-041).**
|
||||
All new fields are nullable and default to null, so every saved profile and backup reads as
|
||||
"not configured" and writes nothing new.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **An overnight charge the head unit slept through is a charge.** `ChargeEnergy.plugged` required
|
||||
the odometer to stand still between the two ledger entries around a rise; when the drive home
|
||||
was never recorded either, the evening entry predates it, the odometer "moved", and a night's
|
||||
fifty points were filed as regeneration and hidden from the battery page. A rise is now a charge
|
||||
when the odometer stood still, **or** the car reported a non-zero charging status, **or** it is
|
||||
at least 10 points and nobody watched the car move during it.
|
||||
- **Each charge carries its curve.** `ChargeEnergy.steps` is pack power per watched ledger step
|
||||
(one step per point of charge), with `peakPowerKw` and the outside temperature range;
|
||||
`ChargeEnergyReport.powerBySocBand()` aggregates the steps of every charge into 10-point bands,
|
||||
which is where the taper shows. Measured only; an unwatched charge has no steps. Both reach
|
||||
`describe()`.
|
||||
|
||||
- **A calm drive is measured, not waiting.** `EcoDrivingMonitor.harshSharePercent()` returns the
|
||||
share of hard acceleration once the window holds a minute of movement, below the advice
|
||||
threshold included. `steadiness()` still answers only above it; callers can now tell "not
|
||||
measured yet" from "measured, and calm".
|
||||
- **The power model trains on town driving.** `EnergyModelTrainer` fitted every 5 s sample,
|
||||
so an acceleration costing several times a cruise counted as model error and stop-and-go
|
||||
history never passed the residual gate. It now fits one point per kilometre of driving, a
|
||||
point per charge made rather than per instant, and `sampleCount` counts those kilometres
|
||||
(twelve at least). A history without 5 °C of temperature spread fits speed alone, with no
|
||||
weather term, instead of refusing.
|
||||
- **The charge model trains on short drives.** `SocConsumptionFitter` waited for 2 % of charge
|
||||
before cutting a segment; SWI68 publishes tenths of a percent, so every drive under about
|
||||
ten kilometres gave nothing. The drop needed is now two steps of the track's own gauge,
|
||||
capped at 2 %, and a stretch shorter than 2 km keeps accumulating instead of being dropped.
|
||||
- **Hard keys are decoded per broadcast.** `com.saic.keyevent.hardkey.report` carries Android
|
||||
key codes and `com.android.systemui.ACTION_HARD_KEY_EVENT` carries SAIC's own numbering, and
|
||||
the same press arrives on both. Read as one space, SystemUI's next-track (5) decoded as the
|
||||
report's phone (5), and a phone press (5 and 16) read as a double. `accept()` now takes a
|
||||
`Source`; SystemUI contributes only the cluster pad (6–10), which the report does not carry.
|
||||
A key neither table names is reported with an id of its own (`unknownKeyId`), so it can still
|
||||
be bound. `Event` carries `keyId`; `Button.id` keeps the numbers rules already store.
|
||||
|
||||
- **Eco-driving trip replay now accepts persisted sample cadence.** Live monitoring still rejects
|
||||
gaps over five seconds, while replay allows the expected delay between five-second track samples
|
||||
and the next telemetry frame. Stored trips can therefore report measured steadiness again.
|
||||
|
||||
### Added
|
||||
|
||||
- **What a drive asked of the pack at the moments it minds most (CP-090).** `DrivingStress.of`
|
||||
measures, per trip, the minutes of hard discharge (above 60 % of the trip's own peak) below
|
||||
20 % charge or below 0 °C outside, and the energy regenerated below 0 °C. The pack power sign
|
||||
is unvalidated (RI-002), so each trip orients itself from its falling charge; a trip that
|
||||
cannot, or with more than a tenth of its time undecidable, gives null rather than zero.
|
||||
`DrivingStress.monthly` sums the trips that could say, per local month. Measured quantities,
|
||||
never a wear figure.
|
||||
- **How long the rest of a charge takes (CP-089).** `ChargeDuration.estimate` sums, band by
|
||||
10-point band, the minutes per point the driver's own watched charges took: the median, and the
|
||||
fastest and slowest charge as the range. Charges at a similar outside temperature (±8 °C) are
|
||||
tried first, all charges after. A band on the path that no watched charge crossed is refused
|
||||
(`MISSING_BAND`), never extrapolated. `lastStretchMinutes` is the part above 80 %.
|
||||
`ChargeDuration.curveCheck` compares one finished band with the same band in at least three
|
||||
past charges; `slower` only when it took longer than the slowest of them.
|
||||
- **A daily battery digest (CP-087).** `BatteryDigest` copies one day of the health, calibration,
|
||||
exposure and charge analyses; `BatteryDigestStore` keeps a year of them, one per local day, with
|
||||
the same atomic write and quarantine as the ledger. `BatteryDigest.changes` reports only what
|
||||
moved past its own band from one day to the next — health beyond its uncertainty, a band that
|
||||
narrowed by at least a point (or a first estimate), a calibration verdict that changed — so a
|
||||
health figure wandering inside its band is never news.
|
||||
|
||||
- **Open windows are a consumption factor, like the climate (CP-086).** `EnergySnapshot` and
|
||||
`TripSample` carry `widestWindowPercent`, read from `SaicVehicleControl.widestWindowPercent()`;
|
||||
null where unread, never 0, and absent from older stored trips. The attribution gains
|
||||
`ResidualContext.WINDOWS_OPEN`: an interval with a window open at either end gets its own
|
||||
residual instead of joining a climate group, so the cabin share no longer absorbs the drag.
|
||||
|
||||
- **`EcoDriving`: the band, the levers and the refusals behind the eco coach.** `EcoDrivingMonitor`
|
||||
keeps a bounded window of the speed channel and answers one question — is this drive cheaper or
|
||||
dearer than what this driver's own fit expects at this mean speed and outside temperature —
|
||||
against the fit's own residual rather than a threshold invented for the purpose. It offers at
|
||||
most one lever: a cruising speed quoted from `SocConsumptionModel.predict`, or the share of
|
||||
moving time spent accelerating hard, chosen by precedence because the two have no common unit.
|
||||
Every silence carries its `UnavailableReason`: no fit, a speed outside the fitted envelope, a
|
||||
distance recorded under the superseded speed conversion, too little distance, too little charge
|
||||
drop, no outside temperature. `forReplay()` gives the same class an unbounded window, so a
|
||||
finished track is judged whole rather than on its last three minutes.
|
||||
|
||||
### Added
|
||||
|
||||
- **The charge side of the battery ledger, which nothing read.** Phase 5 measures the pack from
|
||||
its discharges alone: a rise in charge is only a boundary marker, so the energy going back in
|
||||
was being recorded and never looked at — and the two facts RI-002 has been blocked on since
|
||||
2026-09-13 live in exactly that gap. `ChargeEnergyAnalyzer` reads it, taking `BatteryExposure`'s
|
||||
sessions rather than cutting its own so the two can never disagree about what a charge is. Per
|
||||
charge: energy and mean power from the pack-pair integral, which way that integral moved
|
||||
(`ENERGY_FELL` is `EnergySnapshot.batteryPowerKw`'s declared convention holding, `ENERGY_ROSE`
|
||||
is it inverted, `CONTRADICTORY` when two charges disagree — never averaged), what the car's own
|
||||
counters did (`RESET` is what "since the last charge" predicts, and one observed reset outranks
|
||||
a charge that ended before the car clears them), and the odometer, because a long descent raises
|
||||
the charge too and only a stationary session is a plug. A charge the app slept through keeps its
|
||||
points and loses its power: the recorder earns an entry every quarter of an hour, so a longer
|
||||
silence is this app having been down, and the integral does not move while nobody is
|
||||
integrating. `describe()` puts the verdicts in a diagnostic bundle. Nothing is corrected and
|
||||
nothing new is sampled — no new property, no new transaction, no new permission (CP-073).
|
||||
|
||||
- **A probe that says when it was never able to ask.** `RuntimeInterfaceProbe.adapterClientMap()`
|
||||
swept nineteen service codes and recorded ABSENT for every one of them on a firmware where
|
||||
`sCarAdapter` is never assigned — nineteen answers to a question that never reached a binder,
|
||||
|
||||
Reference in New Issue
Block a user