Fatih'sMG4 — ABRP telemetry for an MG4 EH32 on SWI69

A fork of malys/EVABRPUploader (itself a fork of Leon Kernan's ABRP_Uploader)
carrying the changes that make it work on this car. History starts here: the
tree is what matters, and the upstream projects are where the earlier work
belongs.

EVHardware is vendored rather than tracked as a submodule, so one clone
builds.

Telemetry. SOC, battery power and range read as unavailable on SWI69 because
the vendor fallbacks were gated to SWI68 alone, while the property ids they
use are the ones this car publishes — the gates now admit SWI69. Tyre
pressure, the odometer and charge status come from the vendor block too,
since the generic properties for all three are declared here and never
publish. Tyre pressure is confirmed on the vehicle; the other two are not
yet.

Vehicle page. Every signal the car answers, in a table. One that it does not
publish shows a dash: an unimplemented VHAL property returns zero, and
"0 °C" would read as a measurement.

Wi-Fi page. Joins a network the unit already has saved, so a phone hotspot
does not need a trip into Settings on every drive. Tried on the upload tick
and from a button, skipped while already associated. This leans on the unit
being API 28 and the build being platform-signed — getConfiguredNetworks and
enableNetwork are closed to ordinary apps from API 29 — and fails quietly
where that does not hold.

The OTA updater points at this repository. An APK from upstream is signed
with a different key, which Android will not install over a platform-signed
build.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TCFDONMEZ
2026-09-22 10:45:00 +03:00
co-authored by Claude
commit 2fd7b2dde1
245 changed files with 35594 additions and 0 deletions
@@ -0,0 +1,333 @@
package com.evsuite.abrp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.json.JSONObject;
import org.junit.Test;
/**
* [T-913] Telemetry payload construction — pure JVM, no car and no Android needed.
*
* The rule under test: a property that could not be read is OMITTED. Emitting 0 instead
* tells ABRP the car really is at 0 % / 0 km/h and corrupts the user's route plan.
*/
public class TelemetryPayloadTest {
/** Everything readable, no location. */
private TelemetryPayload fullCar() {
TelemetryPayload t = new TelemetryPayload(1_700_000_000L);
t.soc = 63;
t.speedKmh = 48.4f;
t.rangeKm = 210;
t.extTemp = 12f;
t.powerKw = -14.5f;
t.charging = false;
t.dcfc = false;
t.parked = false;
t.cabinTemp = 21.5f;
return t;
}
/** Nothing readable at all — the car link is down. */
private String noCar() {
return new TelemetryPayload(1_700_000_000L).build();
}
@Test
public void utcIsAlwaysPresent() throws Exception {
JSONObject json = new JSONObject(noCar());
assertEquals(1, json.length());
assertEquals(1_700_000_000L, json.getLong("utc"));
}
@Test
public void unreadablePropertiesAreOmittedNotZeroed() {
String json = noCar();
for (String field : new String[]{
"soc", "speed", "est_battery_range", "ext_temp", "power",
"is_charging", "is_dcfc", "is_parked", "cabin_temp",
"batt_temp", "capacity", "odometer", "hvac_setpoint",
"tire_pressure_fl", "tire_pressure_fr",
"tire_pressure_rl", "tire_pressure_rr"}) {
assertFalse("le champ " + field + " aurait dû être omis", json.contains(field));
}
}
@Test
public void aSingleUnreadablePropertyDoesNotSuppressTheOthers() throws Exception {
// SOC throws (null) but speed reads fine: speed must still be sent.
TelemetryPayload t = new TelemetryPayload(1L);
t.speedKmh = 48.4f;
JSONObject json = new JSONObject(t.build());
assertFalse("soc illisible aurait dû être omis", json.has("soc"));
assertEquals(48, json.getInt("speed"));
}
@Test
public void aGenuineZeroIsStillSent() throws Exception {
// 0 % battery is a real reading and must not be confused with "unreadable".
TelemetryPayload t = new TelemetryPayload(1L);
t.soc = 0;
t.speedKmh = 0f;
t.rangeKm = 0;
JSONObject json = new JSONObject(t.build());
assertEquals(0, json.getInt("soc"));
assertEquals(0, json.getInt("speed"));
assertEquals(0, json.getInt("est_battery_range"));
}
@Test
public void readablePropertiesAreEmitted() throws Exception {
JSONObject json = new JSONObject(fullCar().build());
assertEquals(63, json.getInt("soc"));
assertEquals(48, json.getInt("speed")); // rounded
assertEquals(210, json.getInt("est_battery_range"));
assertEquals(12, json.getInt("ext_temp"));
assertEquals(-14.5, json.getDouble("power"), 0.001);
assertEquals(0, json.getInt("is_charging"));
assertEquals(0, json.getInt("is_dcfc"));
assertEquals(0, json.getInt("is_parked"));
}
@Test
public void powerUsesDotDecimalSeparatorRegardlessOfDefaultLocale() throws Exception {
java.util.Locale previous = java.util.Locale.getDefault();
try {
java.util.Locale.setDefault(java.util.Locale.FRANCE); // comma locale
assertTrue(fullCar().build().contains("-14.5"));
} finally {
java.util.Locale.setDefault(previous);
}
}
@Test
public void implausibleCabinTempIsOmitted() {
// 0.0 means the property is unsupported on this VHAL.
TelemetryPayload zero = new TelemetryPayload(1L);
zero.cabinTemp = 0f;
assertFalse(zero.build().contains("cabin_temp"));
TelemetryPayload hot = new TelemetryPayload(1L);
hot.cabinTemp = 120f;
assertFalse(hot.build().contains("cabin_temp"));
}
@Test
public void plausibleCabinTempIsSent() throws Exception {
assertEquals(22, new JSONObject(fullCar().build()).getInt("cabin_temp")); // 21.5 rounds up
}
// ---------- Fields added for the ABRP lower-priority set ----------
@Test
public void batteryTemperatureIsSentWhenPlausible() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.battTempC = 24.6f;
assertEquals(25, new JSONObject(t.build()).getInt("batt_temp"));
}
@Test
public void unsupportedBatteryTemperatureIsOmitted() {
// A VHAL without the property answers 0.0; "pack at 0 °C" changes ABRP's plan.
TelemetryPayload t = new TelemetryPayload(1L);
t.battTempC = 0f;
assertFalse(t.build().contains("batt_temp"));
}
@Test
public void capacityIsSentInKwhWithTwoDecimals() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.capacityKwh = 61.734f;
assertEquals(61.73, new JSONObject(t.build()).getDouble("capacity"), 0.001);
}
@Test
public void zeroCapacityIsOmitted() {
TelemetryPayload t = new TelemetryPayload(1L);
t.capacityKwh = 0f;
assertFalse(t.build().contains("capacity"));
}
@Test
public void odometerIsRoundedToWholeKm() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.odometerKm = 24518.7f;
assertEquals(24519, new JSONObject(t.build()).getInt("odometer"));
}
@Test
public void zeroOdometerIsOmitted() {
TelemetryPayload t = new TelemetryPayload(1L);
t.odometerKm = 0f;
assertFalse(t.build().contains("odometer"));
}
@Test
public void hvacSetpointIsSentWhenSet() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.hvacSetpointC = 21f;
assertEquals(21, new JSONObject(t.build()).getInt("hvac_setpoint"));
}
@Test
public void unsupportedHvacSetpointIsOmitted() {
TelemetryPayload t = new TelemetryPayload(1L);
t.hvacSetpointC = 0f;
assertFalse(t.build().contains("hvac_setpoint"));
}
@Test
public void stateOfEnergyIsSentInKwh() throws Exception {
TelemetryPayload t = fullCar();
t.soeKwh = 41.256f;
assertEquals(41.26, new JSONObject(t.build()).getDouble("soe"), 0.001);
}
@Test
public void unsupportedStateOfEnergyIsOmitted() {
TelemetryPayload t = fullCar();
t.soeKwh = 0f;
assertFalse(t.build().contains("soe"));
}
@Test
public void chargedEnergyIsSentIncludingZeroAtSessionStart() throws Exception {
TelemetryPayload started = fullCar();
started.kwhCharged = 0.0;
assertEquals(0.0, new JSONObject(started.build()).getDouble("kwh_charged"), 0.001);
TelemetryPayload running = fullCar();
running.kwhCharged = 12.34;
assertEquals(12.34, new JSONObject(running.build()).getDouble("kwh_charged"), 0.001);
}
@Test
public void chargedEnergyIsOmittedOutsideACharge() {
assertFalse(fullCar().build().contains("kwh_charged"));
}
@Test
public void tyrePressuresAreSentPerWheelInKpa() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.tirePressureFlKpa = 240f;
t.tirePressureFrKpa = 241.4f;
t.tirePressureRlKpa = 250.6f;
t.tirePressureRrKpa = 249f;
JSONObject json = new JSONObject(t.build());
assertEquals(240, json.getInt("tire_pressure_fl"));
assertEquals(241, json.getInt("tire_pressure_fr"));
assertEquals(251, json.getInt("tire_pressure_rl"));
assertEquals(249, json.getInt("tire_pressure_rr"));
}
@Test
public void aWheelWithoutASensorIsOmittedWithoutHidingTheOthers() throws Exception {
// 0 kPa is not a flat tyre, it is a missing sensor: a flat still reads well above 0.
TelemetryPayload t = new TelemetryPayload(1L);
t.tirePressureFlKpa = 0f;
t.tirePressureFrKpa = 240f;
JSONObject json = new JSONObject(t.build());
assertFalse(json.has("tire_pressure_fl"));
assertEquals(240, json.getInt("tire_pressure_fr"));
}
// ---------- Location ----------
@Test
public void locationIsIndependentOfTheCarLink() throws Exception {
// No car data at all, but a GPS fix: ABRP should still see the vehicle online.
TelemetryPayload t = new TelemetryPayload(1L);
t.lat = 48.85;
t.lon = 2.35;
JSONObject json = new JSONObject(t.build());
assertEquals(48.85, json.getDouble("lat"), 0.0001);
assertEquals(2.35, json.getDouble("lon"), 0.0001);
assertFalse(json.has("soc"));
}
@Test
public void missingLocationOmitsAllLocationFields() {
String json = fullCar().build();
assertFalse(json.contains("lat"));
assertFalse(json.contains("lon"));
assertFalse(json.contains("elevation"));
assertFalse(json.contains("heading"));
}
@Test
public void elevationAndHeadingAreOptional() throws Exception {
TelemetryPayload flat = new TelemetryPayload(1L);
flat.lat = 48.85;
flat.lon = 2.35;
JSONObject flatJson = new JSONObject(flat.build());
assertFalse(flatJson.has("elevation"));
assertFalse(flatJson.has("heading"));
TelemetryPayload full = new TelemetryPayload(1L);
full.lat = 48.85;
full.lon = 2.35;
full.elevation = 35.4;
full.heading = 271.6f;
JSONObject fullJson = new JSONObject(full.build());
assertEquals(35, fullJson.getInt("elevation"));
assertEquals(272, fullJson.getInt("heading"));
}
@Test
public void booleanFlagsAreEmittedAsIntegers() throws Exception {
TelemetryPayload t = new TelemetryPayload(1L);
t.soc = 50;
t.powerKw = 62f;
t.charging = true;
t.dcfc = true;
t.parked = true;
JSONObject json = new JSONObject(t.build());
assertEquals(1, json.getInt("is_charging"));
assertEquals(1, json.getInt("is_dcfc"));
assertEquals(1, json.getInt("is_parked"));
}
@Test
public void thePayloadIsValidJson() throws Exception {
// The hand-built string could not guarantee this; JSONObject can.
new JSONObject(fullCar().build());
new JSONObject(noCar());
}
@Test
public void unimplementedOutsideTempIsOmittedNotSentAsZero() {
// A VHAL without ENV_OUTSIDE_TEMPERATURE answers 0.0. Sent as ext_temp:0 it is a
// freezing day as far as ABRP's consumption model is concerned.
TelemetryPayload t = fullCar();
t.extTemp = 0f;
assertFalse(t.build().contains("ext_temp"));
}
@Test
public void impossibleOutsideTempIsOmitted() {
TelemetryPayload t = fullCar();
t.extTemp = -273f;
assertFalse(t.build().contains("ext_temp"));
}
@Test
public void summaryListsSentValuesAndOmittedFields() {
String summary = TelemetryPayload.summarize(fullCar().build());
assertTrue(summary, summary.contains("soc=63"));
assertTrue(summary, summary.contains("ext_temp=12"));
assertTrue(summary, summary.contains("cabin_temp=22"));
// Nothing gave us a position, so lat/lon must show up as omitted, not as 0.
assertFalse(summary, summary.contains("lat="));
assertTrue(summary, summary.contains("| omitted: "));
assertTrue(summary, summary.substring(summary.indexOf("| omitted: ")).contains("lat"));
}
@Test
public void summaryOfAnEmptyPayloadReportsEverythingOmitted() {
String summary = TelemetryPayload.summarize(noCar());
assertTrue(summary, summary.startsWith("utc=1700000000"));
assertTrue(summary, summary.contains("| omitted: soc,speed,"));
}
}