feat: add direct car telemetry management

This commit is contained in:
2026-07-31 23:14:40 +02:00
parent 530162cbf7
commit e347a581e1
16 changed files with 447 additions and 57 deletions
+10
View File
@@ -0,0 +1,10 @@
# Direct ADB car telemetry helper
`CarTelemetryProbe` is a small, read-only DEX program run by Android's `app_process`
through ADB. It is pushed to `/data/local/tmp` and is never installed as an Android
application. It binds directly to the factory SAIC vehicle service and performs only
the known getter transactions for SOC, range, speed, voltage, consumption, and charging
status.
The checked-in `server/assets/car-telemetry-probe.dex` is built from this source for
Android API 28. It has no MG Utility dependency and sends no vehicle-control calls.
@@ -0,0 +1,124 @@
package cloud.molberg.picarprobe;
import android.app.ActivityThread;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
/** Read-only shell process that samples the factory SAIC vehicle Binder service. */
public final class CarTelemetryProbe {
private static final String HUB_TOKEN = "com.saicmotor.sdk.vehiclesettings.IHubService";
private static final String CHARGING_TOKEN = "com.saicmotor.sdk.vehiclesettings.IVehicleChargingService";
private static final String CONDITION_TOKEN = "com.saicmotor.sdk.vehiclesettings.IVehicleConditionService";
private static final AtomicBoolean finished = new AtomicBoolean(false);
private CarTelemetryProbe() {}
public static void main(String[] arguments) {
try {
Looper.prepareMainLooper();
Context context = ActivityThread.systemMain().getSystemContext();
Intent intent = new Intent("com.saicmotor.service.vehicle.VehicleService")
.setPackage("com.saicmotor.service.vehicle");
boolean binding = context.bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder hub) {
try {
IBinder charging = service(hub, "vehiclecharging");
IBinder condition = service(hub, "vehiclecondition");
float soc = readFloat(charging, CHARGING_TOKEN, 3);
int rangeKm = readInt(charging, CHARGING_TOKEN, 4);
float speedKph = readFloat(condition, CONDITION_TOKEN, 3);
float batteryVolts = readFloat(charging, CHARGING_TOKEN, 64);
float totalConsumptionKwh = readFloat(charging, CHARGING_TOKEN, 75);
int chargingStatus = readInt(charging, CHARGING_TOKEN, 9);
succeed(soc, rangeKm, speedKph, batteryVolts, totalConsumptionKwh, chargingStatus);
} catch (Throwable error) {
fail("binder_read_failed");
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
fail("service_disconnected");
}
}, Context.BIND_AUTO_CREATE);
if (!binding) fail("service_unavailable");
Thread timeout = new Thread(() -> {
try { Thread.sleep(5000); } catch (InterruptedException ignored) { return; }
fail("service_timeout");
}, "car-telemetry-timeout");
timeout.setDaemon(true);
timeout.start();
Looper.loop();
} catch (Throwable error) {
fail("probe_failed");
}
}
private static IBinder service(IBinder hub, String name) throws Exception {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
data.writeInterfaceToken(HUB_TOKEN);
data.writeString(name);
hub.transact(1, data, reply, 0);
reply.readException();
IBinder binder = reply.readStrongBinder();
if (binder == null) throw new IllegalStateException("Missing " + name);
return binder;
} finally {
data.recycle();
reply.recycle();
}
}
private static float readFloat(IBinder binder, String token, int code) throws Exception {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
data.writeInterfaceToken(token);
binder.transact(code, data, reply, 0);
reply.readException();
return reply.readFloat();
} finally {
data.recycle();
reply.recycle();
}
}
private static int readInt(IBinder binder, String token, int code) throws Exception {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
data.writeInterfaceToken(token);
binder.transact(code, data, reply, 0);
reply.readException();
return reply.readInt();
} finally {
data.recycle();
reply.recycle();
}
}
private static void succeed(float soc, int rangeKm, float speedKph, float batteryVolts, float consumption, int charging) {
if (!finished.compareAndSet(false, true)) return;
System.out.printf(Locale.US,
"{\"status\":\"ok\",\"soc\":%.3f,\"rangeKm\":%d,\"speedKph\":%.3f,\"batteryVolts\":%.3f,\"totalConsumptionKwh\":%.3f,\"chargingStatus\":%d}%n",
soc, rangeKm, speedKph, batteryVolts, consumption, charging);
System.exit(0);
}
private static void fail(String reason) {
if (!finished.compareAndSet(false, true)) return;
System.out.println("{\"status\":\"error\",\"reason\":\"" + reason + "\"}");
System.exit(0);
}
}