All posts
Engineering · · 7 min read

From UDS to SOVD — what actually changes when diagnostics goes RESTful

In the CAN to DoIP post we changed the pipe: the same UDS payloads, carried over Ethernet instead of CAN. SOVD is a different kind of change. It does not swap the transport under UDS — it replaces the request/response byte protocol with HTTP resources and JSON. If DoIP was "UDS, but faster," SOVD is "diagnostics, but self-describing."

This post covers what SOVD actually changes, what it deliberately keeps, and how to run the same validation campaign against an ECU over CAN and over SOVD from one config file.


What SOVD is, in one paragraph

SOVD (Service-Oriented Vehicle Diagnostics, standardised by ASAM) exposes diagnostic capability as REST resources: data values, faults, and functions, addressed by URL and described in machine-readable form. A tester does not need an ODX file and a byte-level protocol stack to read a value — it needs an HTTP client. The intended home is the high-performance compute layer of a software-defined vehicle: remote diagnostics, cloud tooling, in-vehicle apps that need diagnostic data without a CAN driver.


What SOVD keeps: your ECU still speaks UDS

Here is the part that vendor slideware tends to blur: a Cortex-M ECU on a CAN bus is not going to serve HTTPS. SOVD deployments put a gateway — in OpenSOVD terms, a Classic Diagnostic Adapter (CDA) — between the REST world and the UDS world. The CDA needs a description of what sits behind it: which data elements map to which DIDs, which faults map to which DTCs, which functions map to which routines.

So the practical question for an embedded team is not "do we rewrite our diagnostics for SOVD?" It is "where does the CDA description come from, and does it stay in sync with the firmware?"


One YAML, both worlds

In EDS, the answer is: from the same diagnostics_config.yaml that generates your firmware. The code generator grew a --sovd flag:

python3 tools/codegen.py \
    --config diagnostics_config.yaml \
    --out    generated/ \
    --safety-wrappers --asil-level B \
    --sovd

Alongside the C sources, this produces generated/sovd_cda.json — an OpenSOVD 1.0 descriptor. The mapping is mechanical: DIDs become data elements, DTCs become fault entries, routines become functions, and sessions become access modes. If your ECU is DoIP-enabled (transport: doip or transport: both), the descriptor automatically carries the logicalAddress and port, so the CDA knows how to reach the ECU without a separate config file.

The point is drift prevention. The firmware, the test suite, and the SOVD descriptor are all generated from one source. Add a DID to the YAML, regenerate, and all three layers agree — verified in CI, because the code generator's output is deterministic.


Validating over SOVD with TestLab

TestLab added SovdTester — an async client for OpenSOVD endpoints — and a sovd interface type in the multi-ECU workspace. A mixed topology looks like this:

# testlab_workspace.yaml
ecus:
  - name: cgw            # UDS over CAN
    interface: vcan0
    rx_id: "0x7E8"
    tx_id: "0x7DF"

  - name: cluster        # SOVD via OpenSOVD CDA
    interface: sovd
    sovd_url: "http://192.168.1.30:20002"
    sovd_entity: "instrument_cluster"

Campaigns gain four SOVD actions — sovd_read_data, sovd_read_faults, sovd_clear_faults, and sovd_call_function — so a single job can exercise a CAN ECU and a SOVD entity side by side:

jobs:
  mixed_stack_check:
    on_failure: continue
    steps:
      # Classic path: UDS over CAN
      - action: session
        ecu:    cgw
        value:  extended
      - action: read_did
        ecu:    cgw
        did:    "0xF190"
        save_as: cgw_vin

      # SDV path: the same logical read over SOVD
      - action: sovd_read_data
        ecu:    cluster
        name:   "vin"
        save_as: cluster_vin
      - action: sovd_read_faults
        ecu:    cluster

The symmetry is the point. On the CAN side, read_did pulls the VIN by its identifier, 0xF190. On the SOVD side, sovd_read_data pulls the same value by its resource namevin — because SOVD addresses data by name, not by raw DID. Two transports, one logical check, both saved into campaign variables you can assert on later.

testlab-run --workspace testlab_workspace.yaml \
            --campaign  campaigns/mixed_stack.yaml \
            --job       mixed_stack_check \
            --json      reports/run.json

One JSON result, one HTML report, one regression diff — whether the step went over ISO-TP frames or HTTP. Clearing faults (sovd_clear_faults) and invoking functions (sovd_call_function, which takes a name and a params map) round out the set, addressed by the entity's resource names rather than raw DIDs and routine IDs.


When SOVD matters — and when it doesn't

An honest placement, because the hype cycle around SOVD is real.

SOVD matters if you are building on an SDV architecture with a Linux or HPC layer, you need remote or cloud diagnostics, or your diagnostic clients are apps rather than garage testers. The REST surface is genuinely easier to consume than raw UDS.

SOVD does not matter yet if you are shipping a standalone CAN ECU into a classic vehicle architecture. Nothing in SOVD reaches down to the microcontroller — your ECU speaks UDS either way, and it will for years. The workshops of the world run on ISO 14229 and will continue to.

The reason we built the bridge anyway: teams increasingly need both at once — classic UDS validation for the ECU itself, SOVD validation for the gateway layer above it. If that is your situation, the whole point of this post is that it can be one config file and one campaign run, not two toolchains.

EDS ships the --sovd code generation flag; TestLab ships SovdTester and the SOVD campaign actions. The runtime is open-source (GPL v2); the code generator and validation library are commercial tiers. See pricing →