Skip to content

End to end

End to end: one feature, all the way through

A complete worked example — a seat massage feature built from the physical actuator to both the cluster and the centre screen, naming every layer, every interface and every decision along the way.

Advanced7 minEnd to end · Worked example · Integration

Everything in this curriculum so far has been a layer at a time. This page builds one complete feature through all of them, so the pieces connect.

The feature: the driver adjusts seat massage intensity from the centre screen. The cluster shows a brief confirmation. Everything degrades sensibly on trims that do not have the hardware.

Simple enough to follow, and it touches every layer.

The accent layers are what “software-defined” actually addsVehicle applicationscockpit UI · comfort · charging · fleetApplication frameworksAndroid Automotive · AUTOSAR Adaptive · LinuxVehicle abstractionVSS · data broker · vehicle APICommunication middlewareSOME/IP · DDS · gRPC over Automotive EthernetOperating systemsAndroid · Linux · QNX · safety RTOSHypervisorpartitioning · freedom from interferenceHardwarecentral compute · zone controllers · sensorscloser to hardwareportableper-vehicle
The full stack this feature crossesThe accent layers are what SDV adds. This feature touches every one of them.

Step 1 — Define the signal, before writing anything#

The first artefact is not code. It is the signal definition, because that is what three teams will agree on.

vss-overlay/seat-massage.vspec
Vehicle.Cabin.Seat.Massage:
  datatype: uint8
  type: actuator          # it can be commanded, not just observed
  unit: percent
  min: 0
  max: 100
  description: Massage intensity for this seat. 0 is off.

Because Seat already carries instances, this expands automatically:

Vehicle.Cabin.Seat.Row1.DriverSide.Massage
Vehicle.Cabin.Seat.Row1.PassengerSide.Massage

Step 2 — The ECU end#

The seat module already drives the massage motors. It needs to accept a command and report actual state.

What the ECU offers, in its interface definition
SeatComfortService (1.0)
 
  method setMassage
    in  { uint8 seatId, uint8 intensity }
    out { bool accepted }
    error { INVALID_SEAT, NOT_FITTED, OCCUPANT_ABSENT, THERMAL_LIMIT }
 
  broadcast massageChanged
    out { uint8 seatId, uint8 intensity }

Step 3 — Bridge the service into VSS#

Something must connect the SOME/IP service to the VSS name.

feeder/seat_massage_feeder.py
from kuksa_client.grpc import VSSClient
 
SEATS = {
    0x01: 'Vehicle.Cabin.Seat.Row1.DriverSide.Massage',
    0x04: 'Vehicle.Cabin.Seat.Row1.PassengerSide.Massage',
}
 
def on_massage_changed(seat_id: int, intensity: int) -> None:
    # The ECU reported reality -> write the CURRENT value.
    path = SEATS.get(seat_id)
    if path:
        client.set_current_values({path: intensity})
 
def on_target_requested(path: str, intensity: int) -> None:
    # An application asked for a change -> call the service.
    # Do NOT update the current value here; wait for the ECU to confirm.
    seat_id = next(k for k, v in SEATS.items() if v == path)
    seat_service.setMassage(seat_id, intensity)

Step 4 — Access control#

The broker decides who may do what.

broker/policy.yaml
clients:
  cockpit-ui:
    read:  ['Vehicle.Cabin.**']
    write: ['Vehicle.Cabin.Seat.*.*.Massage']    # targets only
 
  cluster-service:
    read:  ['Vehicle.Cabin.Seat.*.*.Massage']
    write: []                                     # display only, never commands
 
  telemetry:
    read:  ['Vehicle.Cabin.Seat.*.*.Massage']
    write: []

Step 5 — The infotainment UI#

cockpit/SeatMassageViewModel.kt
class SeatMassageViewModel(private val vehicle: VehicleClient) : ViewModel() {
 
    private val path = "Vehicle.Cabin.Seat.Row1.DriverSide.Massage"
 
    // Discover, never assume. The base trim has no massage motors.
    private val available = vehicle.exists(path)
 
    val ui = MutableStateFlow(
        SeatMassageUi(visible = available, intensity = 0, pending = false)
    )
 
    init {
        if (available) {
            viewModelScope.launch {
                // The displayed value comes ONLY from the vehicle.
                vehicle.subscribe(path).collect { v ->
                    ui.update { it.copy(intensity = v.toInt(), pending = false) }
                }
            }
        }
    }
 
    fun onSliderChanged(target: Int) {
        // Send the request; show pending; do NOT move the displayed value.
        viewModelScope.launch {
            ui.update { it.copy(pending = true) }
            vehicle.setTarget(path, target)
        }
    }
 
    fun onServiceLost() {
        // Grey it out rather than freezing on a stale number.
        ui.update { it.copy(visible = false) }
    }
}

Four patterns from the IVI module, all in twenty lines: discover, render from confirmation, show pending, degrade visibly.

Step 6 — The cluster confirmation#

The cluster shows a brief card. Android sends a description, never pixels.

cluster/messages.proto
message ComfortCard {
  uint32 version   = 1;      // versioned from the first message
  Zone   zone      = 2;      // ZONE_3 — the infotainment region
  Icon   icon      = 3;      // ICON_SEAT_MASSAGE, an enum
  string primary   = 4;      // "Massage"
  uint32 value     = 5;      // 60
  uint32 ttl_ms    = 6;      // clear after this long with no update
}

Step 7 — Test the whole thing without a vehicle#

This is the payoff for all the layering.

Every case, from a command line
# 1. Normal operation
kuksa-client set Vehicle.Cabin.Seat.Row1.DriverSide.Massage 60
#    -> UI shows 60, cluster card appears
 
# 2. The vehicle refuses (target set, current never follows)
kuksa-client set-target Vehicle.Cabin.Seat.Row1.DriverSide.Massage 80
#    -> UI shows pending, then returns to 60. It must NOT show 80.
 
# 3. The trim has no massage hardware
docker stop seat-massage-feeder
#    -> the control is hidden entirely, no crash
 
# 4. The service disappears mid-session
docker stop seat-comfort-service
#    -> control greys out; it must not freeze on 60
 
# 5. Cluster time-to-live
#    stop the cockpit app -> the cluster card clears within ttl_ms

Step 8 — Ship it#

What goes where
VSS overlay              → generated artefacts, committed, checked in CI
Feeder                   → a container on central compute, updatable
Access policy            → broker configuration, reviewed like code
Cockpit UI               → the Android system image, or an OEM store app
Cluster message schema   → shared with the cluster supplier, versioned

The decisions that made this work#

Looking back at the whole feature, seven choices did the heavy lifting:

  1. The signal was named and typed before any code was written.
  2. The service returns meaningful errors, not a boolean.
  3. Target and current values are separate, so the UI cannot lie.
  4. Access control lives in one place.
  5. The UI discovers rather than assumes.
  6. The cluster receives a description with a time-to-live, not pixels.
  7. Every interface is versioned, so parts can change independently.

References & further reading

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