Skip to content

Infotainment in an SDV

IVI as a service consumer

Writing infotainment software against vehicle services rather than raw signals — discovery, absence, version skew, and rendering honestly when the vehicle disagrees with you.

Intermediate6 minIVI · Services · Patterns

This page is the practical one for an infotainment engineer: the patterns that work when your data comes from services you do not own, on a vehicle whose configuration you cannot assume.

Pattern 1 — Discover, never assume#

Absence is a normal state, not an error
class ComfortViewModel(private val vehicle: VehicleServices) {
 
    init {
        // Discover once, at startup, and shape the UI from what is there.
        val seat = vehicle.discover<SeatService>(minVersion = 1)
        val massage = seat?.supports("massage") == true
 
        _uiState.value = ComfortUi(
            showSeatControls = seat != null,
            showMassage = massage,
            showAmbientLight = vehicle.discover<LightingService>() != null,
        )
    }
}

Pattern 2 — Render from confirmation, never from the request#

This is the rule that appears throughout this site, and it matters most in IVI because IVI is where the driver looks.

Ask, then wait to be told
fun onTemperatureSliderChanged(target: Float) {
    // Send the request. Do NOT update the displayed value.
    climate.setTargetTemperature(target)
    showPending()
}
 
// The displayed value only ever comes from here.
private val onTemperatureChanged = { actual: Float ->
    _displayed.value = actual
    clearPending()
}

Pattern 3 — Tolerate version skew in both directions#

IVI ships monthly; the service layer may ship yearly. Both directions of mismatch will occur in the field.

Negotiate, do not assume
val seat = vehicle.discover<SeatService>(minVersion = 1)
 
// A capability added in 1.3 — the vehicle in front of you may be on 1.1.
if (seat != null && seat.version >= Version(1, 3)) {
    enableMemoryPresets()
} else {
    hideMemoryPresets()
}

Pattern 4 — Subscribe at the rate the pixel needs#

Match the subscription to the display, not the data
// A gauge redrawing at 60 fps does not need 60 Hz of data.
vehicle.subscribe(SPEED, rate = Rate.UI)      // ~5 Hz
    .collect { speed -> gauge.setTarget(speed) }   // interpolate between samples

Pattern 5 — Handle the service disappearing#

Services restart. Processors reboot. Links drop.

Degrade visibly, recover automatically
vehicle.onServiceLost<ClimateService> {
    _uiState.update { it.copy(climateAvailable = false) }   // grey it out
}
 
vehicle.onServiceFound<ClimateService> { service ->
    _uiState.update { it.copy(climateAvailable = true) }
    resubscribe(service)
}

Pattern 6 — Do not process on the callback thread#

Receive and hand off
override fun onValueChanged(signal: String, value: Any) {
    // This is a transport thread. Do nothing expensive here.
    pending[signal] = value
    scheduleRender()      // coalesce and render on your own thread
}

Testing all of this without a vehicle#

The patterns above are exactly what a data broker makes testable.

Exercise every case from a command line
# The value your UI should display
kuksa-client set Vehicle.Cabin.HVAC.AmbientAirTemperature 22.5
 
# Simulate a refusal — target set, current never follows
kuksa-client set-target Vehicle.Cabin.HVAC.TargetTemperature 19
# ... and deliberately do not update the current value
 
# Simulate the service disappearing
docker stop climate-service

Next#

Getting applications onto the vehicle in the first place.

References & further reading

Code links target the main branch on cs.android.com. AOSP moves — if a path 404s, search the symbol instead.