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.
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.
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.MassageStep 2 — The ECU end#
The seat module already drives the massage motors. It needs to accept a command and report actual state.
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.
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.
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#
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.
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.
# 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_msStep 8 — Ship it#
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, versionedThe decisions that made this work#
Looking back at the whole feature, seven choices did the heavy lifting:
- The signal was named and typed before any code was written.
- The service returns meaningful errors, not a boolean.
- Target and current values are separate, so the UI cannot lie.
- Access control lives in one place.
- The UI discovers rather than assumes.
- The cluster receives a description with a time-to-live, not pixels.
- Every interface is versioned, so parts can change independently.

