feat: add direct car telemetry management
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user