diff --git a/AGENTS.md b/AGENTS.md index 786414c..e1b5a3f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,9 +35,15 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`), `secrets.example.yaml` holds `CHANGE-ME` placeholders only. Stage files explicitly, check the index for changes that are not yours (the owner works in parallel), and scan the staged diff for secrets before every commit. Never print a token. -3. **Telemetry is read-only.** The app never writes a vehicle setting. The only system setting it - touches is `policy_control`, and only to remove the CarPlay entry the withdrawn full-screen - experiment (2.19.2–2.19.5) may have left. +3. **Telemetry is read-only; the drive mode is the one exception.** The app writes no vehicle + setting except the drive mode (owner's decision, 2026-10-25): a tap on Settings, or — with + "Set at start" on — once when the head unit starts, the mode last chosen there and confirmed + by the car, and only before the car first leaves P. Every write only in P at 0 km/h with both + read fresh (unreadable means no write), through EVHardware's standstill gate; the result + shown is what the car reports back (`DriveModeWrite`, `DriveModeSetter`). Nothing else is + written. The only system + setting it touches is `policy_control`, and only to remove the CarPlay entry the withdrawn + full-screen experiment (2.19.2–2.19.5) may have left. 4. **A failed read is not a zero.** Unreadable values are `null`, omitted from payloads and shown as "—". Sending 0 for SOC tells ABRP the battery is empty. 5. **Fail closed.** Updates (origin, SHA-256, package, signature), credentials, unknown state. @@ -69,12 +75,12 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`), | Tab | Contents | |---|---| -| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.5 s on a background thread. Three styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace) | +| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), Blueprint (`BlueprintView`, top-view drawing with tyre pressures), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Aviation, Nixie tubes, Chronograph, Mascot (`MascotView`, an anime co-pilot whose face follows `MascotMood`; classic, `ChibiFigure` or `FoxFigure` on `ChibiBase`, chosen by `MascotCharacter`); the drawn styles share `ClusterValues`. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) | | Consumption / Charging | period totals, daily log, charging sessions and curve | | Live data | on/off, ABRP API key and user token, upload frequency | | Server | URL, pairing by QR, ingest token, installation ID (read-only), config import | -| Settings | full screen, voice key, tyre unit, accent colour, Overview style, battery, charging price, version, update check, install from USB, About | -| Logs | update log (every install step, with the platform's words), upload log | +| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it, the Mascot character (only for that style) (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro and Terminal add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About | +| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), update log (every install step, with the platform's words), upload log | Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service, 15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt index 90733a5..0b9e3a8 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt @@ -2432,6 +2432,16 @@ object EVHardware { return ok } + /** + * The drive mode write Nils' MG4 uses: the CarPropertyManager write alone, behind + * [VehicleWriteGate] like every write here, without [setDriveMode]'s fallback binder + * transaction or its remembered "last mode". True means the write call went through, not + * that the car took it: read [getDriveModeRaw] back. + */ + @RequiresStandstill + fun writeDriveModeProperty(mode: DriveMode): Boolean = + setIntPropertyCPM(PROP_DRIVE_MODE, AREA_GLOBAL, mode.value) + @RequiresStandstill fun setRegenLevel(level: RegenLevel): Boolean { if (logEnabled) AppLogger.i(TAG, "setRegenLevel → ${level.label} (${level.value})") diff --git a/LICENSE.md b/LICENSE.md index e3073fe..a4aa48e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -31,7 +31,8 @@ top. Those are Fatih's reads the car needs and upstream does not have — the ve ids `0x21401553`–`56`, the vendor mileage `0x21401566` for the odometer, the charging status from `0x2140F409`, SoC, range, power, pack voltage and current on SWI69 as on SWI68, and SWI69 speed already in km/h — plus translated comments and, from this fork, the gear's raw code -(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`). To take upstream again, merge three ways with +(`EVHardware.getGearRaw`, `EnergySnapshot.gearRaw`) and a drive mode write without the binder +fallback (`writeDriveModeProperty`). To take upstream again, merge three ways with `e4e1158` as the base. The car app links EVHardware into its APK, so the **built app** may only be used and passed on diff --git a/README.md b/README.md index 65e59a6..faa8959 100644 --- a/README.md +++ b/README.md @@ -79,12 +79,12 @@ with the server using a token of its own, obtained by pairing with a phone (belo | Tab | What it shows | |---|---| -| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, climate setpoint) and tyre pressures. Read every half second | +| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, Blueprint, MG, Minimal, Terminal, Aviation, Nixie tubes, Chronograph and Mascot (an anime co-pilot who reacts to the drive, classic, chibi or a chibi fox). The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways | | **Consumption** | Lifetime, last 7 and 30 days, Trip A and B (distance, energy, average consumption and speed, SOC change), and a daily log of the last four months | | **Charging** | The current session (SOC, voltage, current, power, energy into the battery and from the grid, time, cost) and its SOC/power curve | | **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency | | **Server** | Server URL, pairing by QR code, ingest token, installation ID, config import | -| **Settings** | Full screen, voice key, tyre pressure unit, accent colour, battery (manual SoH, usable capacity), charging price, entry on the phone, version, update check, install from USB stick, About | +| **Settings** | Drive mode (Eco, Normal, Sport, Snow, Custom: changes only in P at a standstill; the highlighted one is what the car reports; with "Set at start" the last confirmed choice is set again when the car starts, while it is still in P), full screen, voice key, tyre pressure unit, accent colour, battery (manual SoH, usable capacity), charging price, entry on the phone, version, update check, install from USB stick, About | | **Logs** | Update log (every install step), upload log | - **Opening the app:** the MG4 launcher does not list third-party apps. **Double-press the diff --git a/SPECS.md b/SPECS.md index 16f3a51..c1c68d6 100644 --- a/SPECS.md +++ b/SPECS.md @@ -70,6 +70,13 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * - **Pack energy (`soe`) and capacity:** not published. The Live dashboard computes energy in pack as SOC × usable capacity, from the hidden dashboard constant `usableKwh` (50.8 kWh for the 51 kWh battery) in `k8s/grafana/dashboards/live.json`. +- **Adaptive cruise control (set speed, distance to the car ahead, ACC state):** the head unit gets + none of it (SWI68, checked 2026-10-25). The `AAD_*` properties of `CarAdvancedAssistedDrivingManager` + exist and answer, but stay at their defaults with ACC engaged behind another car + (`AAD_EPTCCTRGTSPD` `0x2160A15B` 0.0, `AAD_ACC_OBJ_*` 0.0 / -128.0, modes 0). A signal recording + of all 1,194 values while the set speed was 101, 90 and 80 km/h shows none of those numbers + (also tried as m/s, mph and ×2/×10/×16/×64) outside the actual speed, and the ACC buttons send + no key broadcast. Most likely only the instrument cluster has it. ## To do @@ -131,6 +138,248 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). +- 2026-10-25 — App 2.34.1 (branch `styles-retro-future`): the Overview's full screen without the + app's full screen. With the car's bars shown, the car's own sidebar sits at the left edge, so a + swipe from the edge lands on it, not on the app (owner, on the car). There a small tab now sits + at the app's left edge instead of the faint handle (48 × 96 dp touch area, a dark tongue with + ›): a tap brings the navigation back, and so does a swipe starting in the app's first 56 dp + (`tabSwipe`). In the app's full screen nothing changed (handle and edge swipe, the owner's + preference: least clutter). Switching Full screen on Settings updates it at once. Emulator: + the tab shows, a tap and a swipe on the left strip both bring the navigation back. Not tried + on the car. +- 2026-10-25 — App 2.34.0 (branch `styles-retro-future`): **drive mode at start.** The car falls + back to its own default mode when switched on; with "Set at start" (on by default, the switch + in the drive mode card) the app sets the mode last chosen on Settings again. Only a choice the + car confirmed counts (`PREF_CHOSEN`, saved on APPLIED). `AbrpUploadService` starts with the + head unit and runs `DriveModeWrite.atStart` (pure, tested) once a second on its own thread: + wait for gear, speed and mode; nothing when the car already reports the mode; one write, in P + at 0 km/h, through the same `DriveModeSetter` as the buttons (write, read back up to 3 s); give + up for this start as soon as the car is out of P or moving, or after 2 minutes. So a mode set + with the car's own button after driving off stands. The result is kept with its time and + shown under the buttons ("At start (25.10. 20:31): Sport set."). Assumes the head unit starts + (and the service with it) when the car is switched on; a start without that (head unit kept + running) is not caught. A service restart (app update, after a kill) counts as a start, still + only in P at 0 km/h. Hints shortened to fit the page. Emulator: page fits, the start check + waits and gives up (no readings), no crash. Not tried on the car. +- 2026-10-25 — App 2.33.0 (branch `styles-retro-future`): **drive mode from Settings**, the first + vehicle setting the app writes. The owner lifted rule 3 for the drive mode only (AGENTS.md). + A card at the top of Settings' second column: Eco, Normal, Sport, Snow, Custom (codes 2, 3, 4, + 6, 7 at `0x2140A17C`; the 2.32.0 recording read 2, Eco). The lit button is the mode the car + reports, from every 0.2 s reading. A tap reads gear and speed fresh and writes only in P at + 0 km/h (`DriveModeWrite`, pure and unit-tested; unreadable gear or speed is a refusal), through + the new `EVHardware.writeDriveModeProperty`: the CarPropertyManager write behind + `VehicleWriteGate` (0 km/h, supported firmware), without `setDriveMode`'s fallback binder + transaction. Then the mode is read back for up to 3 s, and the status beside the title says + what the car reports ("set", "not applied: the car still reports …", "could not be read + back"). One change at a time, on the settings thread; each attempt goes to logcat. To fit the + page, the wide layout's charging price row moved into the battery card ("Battery and charging + price"). Emulator: the card fits, a tap is refused (no gear or speed there). Whether the car + takes the write at all is untested: the first try on the car decides. +- 2026-10-25 — App 2.32.1 (branch `styles-retro-future`): the cruise control work ends. The ACC + recording of 2.32.0 (ACC set to 101, 90 and 80 km/h, a car ahead) found nothing: see "Signals + the car does not provide". Removed: `CruiseLog` and its section on the Logs tab, and in + EVHardware the set-speed read (`getCruiseTargetSpeedRaw`, `EnergySnapshot.cruiseTargetSpeedRaw`), + `readAccDebug` and `ReadFailureLog.reason` — EVHardware is again as before 2.27.3. The old log + files are deleted on start. The recorder stays as a general tool, renamed **Signal recorder** + (`SignalRecorder`, `SignalRecording`, `signal-recording-