RE paperConfirmed

Reverse engineering paper #2 — the vehicle signals a normal app can read

Twenty-two live signals — battery, charging, tyres, motor, gear — reachable from an ordinary sideloaded app, and the two interfaces that hand them over.

BYD LabVerified 5 min read

22 live signals, one bind state of chargerange speedgear charging powertyre pressure HV bus V · Amotor torque CarAdapterService ten domain binders sensor · body · cabin … sideloaded app no platform signature needed — the vendor properties are all in the 0x21… group what is missing is heading: no yaw rate, no steering angle, no compass

Our own app. Sealion 7 Pilot is developed by BYD Lab. It is held to the same standards as every other app on this site — read with that in mind.

Tested on

Vehicle
BYD Sealion 7 · Morocco
Head-unit version
51.1.4.2606220.1
Android
11
Verified

The head unit has no map engine (paper #1). What it does have is the car — every signal the vehicle bus carries, sitting behind two interfaces an ordinary sideloaded app can reach.

Question#

Which live vehicle signals can an app read on an export Sealion 7 without being platform-signed, and through what interface?

Environment#

Decompiled read-only with jadx 1.5.5 from CarAdapterService.apk (com.ts.appservice.caradapter) and from the 277-class android.hardware.bydauto.* SDK bundled inside BydSpotifyMusic.apk. Verified by reading the signals continuously on the car from a sideloaded, debug-signed app over several months of driving.

Observation#

There are two paths to the same data, and they answer differently.

The ThunderSoft car adapter. com.ts.appservice.caradapter exports a bindable service, CarAdapterService, answering the action com.ts.lib.caradapter.ICarAdapterService. Its root binder has one method that matters — getCarAdapterService(String domain) — and it hands back a second binder per domain:

Log
"general"   -> com.ts.lib.caradapter.general.ICarGeneralService
"body"      -> com.ts.lib.caradapter.body.ICarBodyService
"cloud"     -> com.ts.lib.caradapter.cloud.ICarCloudService
"sensor"    -> com.ts.lib.caradapter.sensor.ICarSensorService
"hvac"      -> com.ts.lib.caradapter.hvac.IHvacAdapterService
"setting"   -> com.ts.lib.caradapter.setting.ISettingService
"cabin"     -> com.ts.lib.caradapter.cabin.ICabinService
"assist"    -> com.ts.lib.caradapter.assist.ICarAssistAdapterService
"selfstudy" -> com.ts.lib.caradapter.selfstudy.ISelfStudyService
"drivemode" -> com.ts.lib.caradapter.drivemode.IDriveModeService

Each domain binder carries named getters — getSocValue(), getTyrePressureValueByTypeLf(), getMotorMCUGeneratrixVolt(2) — which is a far more pleasant surface than raw property ids. The AIDL stubs live in the platform’s ts-framework.jar, already on the system classpath of any app on this unit.

Android Automotive’s own property reader. android.car.Car.createCar(context) then getCarManager(Car.PROPERTY_SERVICE) gives the standard AAOS CarPropertyManager, and the signals arrive as android.car.hardware.CarPropertyValue on vendor VHAL properties. Every id starts 0x21…, and the leading 0x2 is AOSP’s VehiclePropertyGroup.VENDOR (0x20000000) — these are BYD’s own properties, not the standard catalogue.

The two views agree. ID_CURRENT_SPEEDR in the BYD SDK’s HAL-id map is decimal 557 867 014, which is 0x21406006; ID_GEAR_R is 557 857 290, which is 0x21403a0a. The named getter and the raw property are the same signal reached two ways.

Evidence#

Property Signal Unit Reached by
0x21604402 State of charge % setting.getSocValue
0x21604421 Remaining battery energy kWh general.getEvRemainingBatteryPower
0x21404401 Remaining range km general.getElecDrivingRangeValue
0x21402037 State of health % cabin.getBatteryHealthStatus
0x21603408 Charging power kW body.getChargingPower
0x21406006 Speed km/h sensor.getCurrentSpeed
0x21403a0a Gear — cabin.getGear
0x21403974 Driving mode — cabin.getSportModeState
0x21604409 Odometer km general.getTotalMileageValue
0x21604405 Average consumption kWh/100 km general.getTotalElecConPHMValue
0x21601421 Last 50 km consumption kWh/100 km body.getTripLast50kmElectricConAvgValue
0x21407407 HV bus voltage V cloud.getMotorMCUGeneratrixVolt(2)
0x21407409 HV bus current A cloud.getMotorMCUGeneratrixCurrent(2)
0x21407403 Rear motor torque Nm cloud.getDriverMotorTorque(2)
0x21407405 Rear motor temperature °C cloud.getDriverMotorTemperature(2)
0x2140460c 12 V battery voltage V raw property 0x2140461e
0x2140460a Exterior temperature °C hvac.getTempratureOut
0x21401023 Cabin setpoint °C hvac.getTempratureMain
0x2160801d Tyre pressure, front left kPa sensor.getTyrePressureValueByTypeLf
0x2160801e Tyre pressure, front right kPa sensor.getTyrePressureValueByTypeRf
0x2160801f Tyre pressure, rear left kPa sensor.getTyrePressureValueByTypeLb
0x21608020 Tyre pressure, rear right kPa sensor.getTyrePressureValueByTypeRb

Four more appear in the SDK’s id map and are named here because they answer questions the table does not: 0x21604601 (ID_SPEED_VALUE_R) is the speed the adapter itself reads, 0x21600d04 is a rolling alive-counter for it, 0x21600d03 an IPB validity flag, and 0x21404b03 (ID_SLOPE) is road grade — pitch, from the BYD “sensor” device.

Interpretation#

Two things follow from the shape of this.

First, BYD’s signals are vendor properties, not the AAOS standard set. An app written against VehiclePropertyIds.PERF_VEHICLE_SPEED finds nothing here. Every useful value is in the 0x21…… vendor group, and the map from id to meaning exists only inside BYD’s own SDK.

Second, the named-getter path is the one to use. The domain binders are stable, self-describing and typed; the raw property path is a fallback for the handful of values with no getter — the 12 V battery among them. An app that binds CarAdapterService once and keeps the ten domain proxies is doing what the vendor’s own apps do.

The permissions are the surprise. Alongside the AAOS android.car.permission.* set, the platform declares a parallel android.permission.BYDAUTO_*_GET family — speed, gearbox, charging, power, sensor, tyre, engine, ADAS, location, bodywork, air conditioning, door lock, light, radar, PM2.5, instrument, version, OTA. Some of these are declared at protection level dangerous rather than signature, which means a normal app is granted them by a runtime prompt, the same way it is granted the camera. BYD’s vehicle data is not uniformly behind a platform signature.

Result#

Confirmed: twenty-two live vehicle signals are readable from an ordinary sideloaded app on this unit, through com.ts.appservice.caradapter’s domain binders, with CarPropertyManager on the vendor property group as a second path to the same values. No platform signature is needed.

What is not in the catalogue matters as much as what is. There is no heading, no yaw rate and no steering angle anywhere in the SDK — that absence is paper #3.

Limitations#

One car, one market, one head-unit build. The list is what returns live values on this unit, which is not the same as the complete set of properties the VHAL implements — a domain binder may carry getters nobody has called. Nothing here was written; every reading is a read.

Signals were sanity-checked against the car’s own displays (SOC, range, odometer, tyre pressures) rather than against a reference instrument, so scaling and units are BYD’s, taken at face value. The four ids named from the SDK map but not in the table were read from the id catalogue, not polled.

We have not tested which of the BYDAUTO permissions are actually enforced on a read. The app declares the whole family, so a signal that works may be working without needing the permission it declares — and the protection levels are read from the platform’s own permission list, not from a grant that was observed to be refused.

Reproduction#

Shell
adb shell dumpsys package com.ts.appservice.caradapter
adb shell pm list permissions -g -d | grep -i bydauto

The first shows the exported service and the action it answers; the second lists the BYDAUTO permission family with its protection levels, which is where the dangerous entries show up.

From an app on the unit, bind the service and take the domain binders:

java
Intent i = new Intent("com.ts.lib.caradapter.ICarAdapterService");
i.setComponent(new ComponentName("com.ts.appservice.caradapter",
                                 "com.ts.appservice.caradapter.CarAdapterService"));
bindService(i, connection, Context.BIND_AUTO_CREATE);
// then, on the root ICarAdapterService:
IBinder sensor = root.getCarAdapterService("sensor");

The ts-framework.jar stubs are on the system classpath of this unit, so the AIDL interfaces resolve at runtime without bundling anything. Off a BYD head unit they do not exist, and a build that assumes them will not start — reach them reflectively if the same APK has to run anywhere else.