Leon Kernan's ABRP_Uploader has carried the MIT Licence since 2026-07-29 (lkernan/ABRP_Uploader commit 75d8f2bc), so the inherited "no licence upstream, status unresolved" notes no longer hold. - LICENSE: MIT, now naming every copyright holder in the fork chain (Leon Kernan, EVABRPUploader contributors, FatihsMG4 contributors, Nils Stinnesbeck). - LICENSE.md: rewritten. The repository is MIT except EVHardware/, which is PolyForm Noncommercial 1.0.0; the app links it into the APK, so the built app is non-commercial. EVHardware's own caveat about EVProfile stays with it. - README, AGENTS.md, CONTRIBUTING.md and SPECS.md say the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
6.3 KiB
AGENTS.md — EVABRPUploader
Context for AI agents and new contributors working in this repository.
EVABRPUploader is part of EVSuite. The workspace AGENTS.md and normative
workspace DESIGN.md apply; this file contains only telemetry-specific additions.
App-specific visual direction (2.2.6)
The user explicitly requested a permanent OLED-black theme, charcoal glass-like cards,
an accent the user picks from the MG4 paint colours (Dynamic Red #E0424C by default; the palette
is theme attributes in res/values/attrs_accent.xml and accents.xml — use ?attr/evAccent…,
never a hard-coded accent hex). These app resources intentionally override the shared
DESIGN.md palette, DayNight preference and flat-surface rules. Do not overwrite them
with the shared token generator. Use static gradients rather than continuous effects;
keep large touch targets, readable text and null telemetry shown as a dash.
Wide layouts start at 1000dp; narrower windows use a bottom navigation bar.
What this is
An Android Automotive app for the MG4 (SAIC eh32) that reads vehicle telemetry and pushes it to A Better Route Planner. It runs on the car's head unit (AAOS 9, MT2712).
Fork of Leon Kernan's ABRP_Uploader (MIT, like every fork after it). See LICENSE.md: the
repository is MIT except EVHardware/, which is PolyForm Noncommercial — so the built APK may
only be used and shared non-commercially. Keep every copyright line in LICENSE.
Non-negotiables
- Telemetry is read-only. No vehicle settings may be written. Window automation was removed in 2.2.5 at the user's request, including its service and door listener.
- A failed read is not a zero. Getters return
Integer/Float/Booleanandnullmeans "could not read". Null fields are omitted from the payload. Sending 0 for SOC tells ABRP the battery is empty and wrecks a live route plan. - Fail closed. Update origin, APK signature, unknown vehicle state — refuse rather than assume.
- Never let a credential reach a URL, a log line, or a crash report.
- AAOS 9 must stay stable. A third-party app crashing the head unit is unacceptable. Every car call goes through reflection with try/catch; keep it that way.
- Power matters. The head unit is awake whenever the car is on. No wake locks, no alarms. The GPS subscription and the upload tick both cost power — keep them in step.
Architecture
AbrpUploadService foreground service; scheduler thread does all IPC and HTTP
CarPropertyAdapter reflection bridge to android.car (not on the compile classpath)
TelemetryPayload pure JSON building — no Android types, unit-tested
UploadCadence when a tick actually uploads — pure, unit-tested
UploadSettings user cadence configuration
UploadLog last 20 attempts + derived service state — pure, unit-tested
SecurePrefs credentials in EncryptedSharedPreferences, migrates plaintext
AbrpApi HTTP; credentials in the POST body, never the query string
MainActivity configuration, service state, upload log
Put logic in the pure classes. Anything Android-free can be unit-tested on the JVM; anything inside the service effectively cannot be tested without a car.
Source sets
| Set | Contents | Ships in |
|---|---|---|
main |
everything shared | all builds |
stable |
UpdateHook no-op |
stable channel |
unstable |
active GitHub pre-release updater and PackageInstaller result receiver | unstable channel |
debug |
VhalProbe — enumerates the whole VHAL |
debug builds only |
testUnstable |
OTA policy tests | test only |
The stable channel does not merely disable self-update — the updater is not in the APK.
Firmware generations
MG4 infotainment ships as SWI68 / SWI69 / SWI131 / SWI132 / SWI133 / SWI165, and they differ. Property IDs confirmed on one generation are not universal. Anything vehicle-facing must tolerate a property simply not existing.
Two read paths, split by whether the signal is firmware-agnostic:
| Signal | Read path | Firmwares |
|---|---|---|
| Speed, outside temp, park | EVHardware getters (getVehicleSpeedKmh, getOutsideTempCelsius, isVehicleInPark) |
all six — EVHardware branches per generation internally |
| SOC, range | CarPropertyAdapter vendor IDs (0x2160F404, 0x2140F41C) |
SWI68 only |
| Charge rate/port, cabin temp | CarPropertyAdapter standard-AAOS IDs |
wherever the VHAL implements them |
EVHardware.init() is called in AbrpUploadService.onCreate() (idempotent); it detects
the generation and picks the right underlying API. Fields it covers therefore work on every
supported firmware.
Reference firmware: SWI68-29958-1300R69. The vendor EV IDs still in
CarPropertyAdapter are supported only on SWI68 firmware. EVHardware
has no EV-battery abstraction for any generation, so SOC and range cannot go through it;
until those vendor IDs are confirmed on the other generations they stay SWI68-only, and on
another firmware they may silently return nothing (handled safely — the field is omitted, but
SOC/range would be missing). Re-confirm with VhalProbe before trusting the vendor IDs on any
other generation.
Working here
mise run check # what CI runs: permission gate + lint + unit tests
mise run build
mise run logs # adb logcat, filtered to this app
Gradle needs JDK 17. If a build fails in JdkImageTransform complaining about jlink,
a stale Gradle daemon started by VS Code's Java extension is being reused —
pkill -f GradleDaemon and retry.
Things that will bite you
android.caris not on the compile classpath. Everything car-related is reflection. R8 cannot see those uses, soproguard-rules.prokeeps them by name.- Unit tests get a stub
org.jsonfromandroid.jarthat returns null. The real implementation is added as a test dependency; do not remove it. service_runningin preferences goes stale-true after a force-kill. UseAbrpUploadService.isRunning().- Permission changes fail CI until allowlisted with a justification.
Honesty requirements
Most of this cannot be verified off a vehicle. When reporting work, state plainly what was tested and what was not. "Unit tests pass, not tried on the car" is the expected and acceptable answer. Do not imply road-testing that did not happen.