It is not 25 °C outside: giving the emulated car a real thermometer
It hit 38 °C in Germany this summer. The window was open, the fan was losing, and on the second monitor an Android Automotive build was telling me, with total confidence, what the weather was like outside the car:
$ adb shell cmd car_service get-property-value 0x11600703
HalPropValue{Property ID: ENV_OUTSIDE_TEMPERATURE(0x11600703), ... Value: 25.0 CELSIUS}
Twenty-five degrees. It had been twenty-five degrees in that car since the day I first booted it in the spring. It was twenty-five degrees during the June heat wave, twenty-five during the July one, twenty-five on the day the Rhine ferry stopped running because the river was too low. The car lives in a virtual machine and the virtual machine lives in a perpetual, pleasant late-May afternoon.
I decided this was a bug. Not in the software — the software is doing
exactly what it’s told — but in the premise. A property called
ENV_OUTSIDE_TEMPERATURE should have some relationship, however loose, with
the environment. So I set out to give the car a thermometer, and the
closest to reality I have is the one inside the CPU.
Where the 25 lives
The first surprise is that the 25 isn’t in the car at all.
On a phone, a HAL is a process on the device that talks to hardware on the
device. The automotive Cuttlefish target does something more interesting
with its vehicle HAL. The VHAL process that runs in the guest — the one
car_service talks to over binder as IVehicle/default — knows nothing
about cars. It’s a stock DefaultVehicleHal, the generic AIDL front door
that every VHAL implementation shares, sitting on top of a pluggable
“hardware” object. On a real head unit that object would speak CAN. On
Cuttlefish it’s GRPCVehicleHardware, and every question it’s asked, it
forwards over gRPC to a process on the host called vhal_proxy_server.
That host process is where the fake car is. It runs FakeVehicleHardware,
the reference implementation, and the reference implementation gets its
opinions from a JSON file:
{
"property": "VehicleProperty::ENV_OUTSIDE_TEMPERATURE",
"defaultValue": { "floatValues": [ 25.0 ] },
"maxSampleRate": 2.0,
"minSampleRate": 1.0
}
So the weather is a constant in a config file, on a machine the car can’t see, behind two process boundaries and a VM. I find this genuinely elegant — the same guest image can be pointed at a different fake, or a real vehicle bus simulator, without rebuilding anything — but it does mean the obvious fix, “just make the fake say something else”, puts the change in the wrong place. I didn’t want a different constant. I wanted the car to read a sensor.
Can’t I just set it?
The second surprise is how thoroughly you’re prevented from cheating.
ENV_OUTSIDE_TEMPERATURE is declared in the VHAL’s AIDL with access
READ and change mode CONTINUOUS. Continuous means the hardware pushes it
at a subscribed rate, one to two hertz according to the config above.
Read-only means nobody sets it. Not an app, not car_service, not a shell
with root: DefaultVehicleHal checks the property’s declared access on
every setValues call and answers ACCESS_DENIED before the request gets
anywhere near the hardware object. There’s a debug back door — the fake
hardware accepts --set through dumpsys on userdebug builds — but a
debug back door is not a thermometer.
Which settles the architecture. If the value can’t be pushed into the VHAL, it has to originate inside it. The vehicle HAL needs to be the thing that reads the sensor. That’s actually how it works on real cars, where the VHAL implementation is the vendor’s and does whatever it takes to get numbers off the bus; it’s just that on Cuttlefish “the bus” is a gRPC channel to a JSON file.
Choosing a thermometer
The workstation, it turns out, has eighteen of them. The kernel’s hwmon
subsystem lists the Ryzen die sensors, a real NTC thermistor on the
motherboard, two more on the water-cooling headers, the NVMe drive, all four
DDR5 modules, the GPU, and — this one made me laugh — the Ethernet
controller.
I picked the CPU die, k10temp’s Tctl, for a reason that’s only half a
joke: it’s the one temperature that responds to building Android. Kick
off m and it climbs ten degrees in a minute. The car would be reporting,
as its outside temperature, the thermal cost of its own existence. There’s
something honest about that.
It is also, in September, more than twice the actual outside temperature. We’ll come back to that.
Getting a number across the VM boundary
A host-to-guest channel on Cuttlefish means vsock, the address family
purpose-built for talking to the hypervisor’s other side. The guest connects
to CID 2 — “the host”, always — on a port of my choosing, and the host
listens. Every Cuttlefish HAL that reaches host-side hardware does this:
GNSS, sensors, lights. Ten lines of Python on the host, a connect() in the
guest, done.
Except that when I did exactly that, my temperature came back as
%CTZV: 26/09/09:12:56:10+4:1:Europe!Berlin+CREG: 0+CGREG: 0. The car was
reading the weather off a modem.
The lesson is small but worth writing down: port 9600 looked free in the
instance config but is the modem simulator’s vsock port, and the modem
simulator will happily greet any caller with unsolicited AT results. Pick a
port nothing else in cuttlefish_config.json has claimed. With that sorted,
the host side of my thermometer is a Python script that listens on the
vsock port, finds the k10temp device by name (the hwmonN numbering
reshuffles on every boot), and answers each connection with one line —
67625 — before closing. Milli-degrees, one reading per connection, no
protocol to get out of sync.
car_service asks the same IVehicle/default it always did. Inside the VHAL process, a decorator sits between the stock front door and the stock gRPC shim, answers exactly one property over vsock, and forwards the other several hundred to the host proxy — where the fake car, and its 25.0, live on untouched.An alternative VHAL, without forking the VHAL
Here is where the pluggable-hardware design pays off. IVehicleHardware is
a small C++ interface — get configs, get values, set values, subscribe,
unsubscribe, register a change callback, dump. GRPCVehicleHardware
implements it. So does FakeVehicleHardware. So can I, and the cheapest
way to do it is to write a decorator: an IVehicleHardware that owns a
GRPCVehicleHardware and forwards every call to it, except when the
property is 0x11600703 (ENV_OUTSIDE_TEMPERATURE).
For that one property, four methods do something:
getValuessplits the request batch. Ours gets answered immediately with a fresh vsock read, stamped withelapsedRealtimeNano(); the rest go to the gRPC shim.DefaultVehicleHalmatches results to requests by ID, so two partial answers arriving at different times are fine.subscribe/updateSampleRate/unsubscribeare swallowed rather than forwarded — the host must never be asked to stream this property — and they start, retune or park a ticker thread that reads the sensor at the requested rate and emits change events through the callback the front door registered.registerOnPropertyChangeEventwraps the callback before handing it down, so that if the inner hardware ever does emit a 25.0 for our property, it’s filtered out. The ticker owns that number now.
Everything else — the several hundred other properties, HVAC, gear, speed, seat heaters — is a one-line passthrough. If the host bridge is down, the decorator forwards our request too, and the car quietly goes back to twenty-five. Graceful degradation to eternal spring.
Selecting the new VHAL was one product-makefile variable, plus the
discovery that the Cuttlefish auto product only applies its default VHAL
when that variable is empty, which makes the order of your
inherit-product lines suddenly matter. The SELinux side was a single rule
letting the VHAL domain open a vsock socket — and one afternoon of the
kernel denying a rule that was demonstrably present in the policy file on
the device. That one turned out to be a precompiled policy on the /odm
partition that init prefers over the text policy whenever its hashes
match, which they did, because I had only synced /vendor. File under
“Treble has more partitions than you remember”.
What the car says now
$ adb shell cmd car_service get-property-value 0x11600703
... Value: 67.625 CELSIUS}
$ adb shell cmd car_service get-property-value 0x11600703
... Value: 68.0 CELSIUS}
$ adb shell cmd car_service get-property-value 0x11600703
... Value: 67.5 CELSIUS}
Between sixty and seventy degrees, and it moves. Start a build on the
host and the car watches the outside world heat up in real time; let the
machine idle and it cools back down; kill the bridge and it snaps back to
25.0, which is now recognisably a fallback rather than a fact. Every other
property still comes from the untouched fake on the host, car_service
never knew anything changed, and the whole thing is one decorator class, one
sepolicy line, and a hundred lines of Python on the host.
Is 65 °C the outside temperature? No. Even this summer it never got there. But the two things a heat wave teaches you about weather — that it’s hotter than the brochure said, and that it changes — are both true of the car’s new reading and neither was true of the old one. As a model of the environment, a live thermometer pointed at the wrong thing beats a constant pointed at nothing. And when the autumn comes and the number finally drops into the fifties, I’ll know it wasn’t the weather. It was that I stopped building Android.
Reading list
Everything below is in an AOSP 15 tree; the VHAL reference implementation is small enough to read in an evening.
hardware/interfaces/automotive/vehicle/aidl/impl/current/default_config/config/DefaultProperties.json— where the 25.0 lives, along with the rest of the fake car’s opinions.hardware/interfaces/automotive/vehicle/aidl_property/android/hardware/automotive/vehicle/VehicleProperty.aidl—ENV_OUTSIDE_TEMPERATURE, whose doc comment (“the temperature reading of the environment outside the vehicle”) the fake takes some liberty with, and its@access VehiclePropertyAccess.READ/@change_mode VehiclePropertyChangeMode.CONTINUOUSannotations, the two lines that rule out cheating.hardware/interfaces/automotive/vehicle/aidl/impl/current/hardware/include/IVehicleHardware.h— the interface a “hardware” object implements; a decorator is nine forwarding methods and four interesting ones.hardware/interfaces/automotive/vehicle/aidl/impl/current/vhal/src/DefaultVehicleHal.cpp— the front door;checkWritePermission()is where asetValueson a read-only property ends.hardware/interfaces/automotive/vehicle/aidl/impl/current/grpc/GRPCVehicleHardware.h— the shim that makes the guest VHAL a client of the host.device/google/cuttlefish/guest/hals/vehicle/VehicleService.cppanddevice/google/cuttlefish/host/commands/vhal_proxy_server/— the two ends of that gRPC link, and where the fake car actually runs.device/google/cuttlefish/shared/auto/device_vendor.mk—LOCAL_VHAL_PRODUCT_PACKAGE, the one variable that swaps the VHAL, and theifeqthat makes inherit order matter.- Vehicle HAL — the official overview of properties, areas, change modes and access.
The previous post on Treble’s tripwires
explains why replacing a HAL is a product-makefile decision and nothing
more — and why the SELinux label on the new binary is the first thing that
bites when you skip the build system and adb sync it into place.