Start with the three layers
Do not implement UDS as one large block of CAN code. Keep the layers separate:
UDS services
|
ISO-TP transport
|
CAN interface
- CAN moves individual frames.
- ISO-TP splits and reassembles long payloads.
- UDS defines services, sessions, DIDs, DTCs, routines, and responses.
This separation makes the software easier to test and prevents multi-frame logic from leaking into every diagnostic service.
Decide whether you are building a tester or ECU server
A UDS client is the diagnostic tester. It sends requests.
A UDS server runs in the ECU. It validates requests, performs allowed actions, and returns responses.
The examples below show a working Python tester pattern and a small server-dispatch pattern. A production ECU server needs much more validation and project integration.
Python UDS client requirements
On Linux, install:
python -m pip install udsoncan can-isotp python-can
Configure SocketCAN:
sudo ip link set can0 down
sudo ip link set can0 type can bitrate 500000
sudo ip link set can0 up
You also need the request and response IDs. This example uses common physical-addressing example IDs:
tester -> ECU: 0x7E0
ECU -> tester: 0x7E8
Use the actual IDs from your diagnostic specification.
Python example: read the VIN DID
import isotp
import udsoncan
import udsoncan.configs
from udsoncan import AsciiCodec
from udsoncan.client import Client
from udsoncan.connections import IsoTPSocketConnection
from udsoncan.exceptions import (
InvalidResponseException,
NegativeResponseException,
TimeoutException,
UnexpectedResponseException,
)
from udsoncan.services import DiagnosticSessionControl
address = isotp.Address(
isotp.AddressingMode.Normal_11bits,
txid=0x7E0,
rxid=0x7E8,
)
connection = IsoTPSocketConnection("can0", address)
config = udsoncan.configs.default_client_config.copy()
config["data_identifiers"] = {
0xF190: AsciiCodec(17), # VIN is 17 ASCII characters
}
try:
with Client(
connection,
request_timeout=2.0,
config=config,
) as client:
client.change_session(
DiagnosticSessionControl.Session.extendedDiagnosticSession
)
vin = client.read_data_by_identifier_first(0xF190)
print(f"VIN: {vin}")
except NegativeResponseException as error:
print(
"ECU rejected the request: "
f"{error.response.code_name} "
f"(0x{error.response.code:02X})"
)
except TimeoutException:
print("No UDS response before the timeout")
except (InvalidResponseException, UnexpectedResponseException) as error:
print(f"Invalid UDS response: {error}")
What the library handles
The example uses:
- SocketCAN for the Linux CAN interface
- the kernel ISO-TP socket through the Python ISO-TP package
- udsoncan for UDS request and response handling
The UDS request itself is conceptually:
22 F1 90
The VIN response is longer than one classical CAN frame. ISO-TP handles the first frame, flow control, and consecutive frames for you.
Add Tester Present for a long operation
An ECU may leave a non-default session after an inactivity timeout. During a longer diagnostic workflow, send Tester Present at the interval required by the ECU specification.
Udsoncan can manage a tester-present thread:
with Client(connection, request_timeout=2.0, config=config) as client:
client.change_session(3)
client.start_tester_present()
try:
# Run approved diagnostic work here.
vin = client.read_data_by_identifier_first(0xF190)
print(vin)
finally:
client.stop_tester_present()
Use the project's required session and timing values. Do not assume every ECU accepts the same behavior.
Handle negative responses clearly
Your code should report at least:
- requested service
- negative response code
- active session
- request bytes
- elapsed time
Important negative response codes include:
0x11 Service Not Supported
0x13 Incorrect Message Length Or Invalid Format
0x22 Conditions Not Correct
0x31 Request Out Of Range
0x33 Security Access Denied
0x35 Invalid Key
0x36 Exceed Number Of Attempts
0x37 Required Time Delay Not Expired
0x78 Response Pending
Do not retry every error automatically. Some codes require a state change, a delay, or human investigation.
ECU server implementation pattern
On the ECU side, the ISO-TP layer delivers a complete UDS payload. The UDS dispatcher validates it and calls a service handler.
This simplified C++ pattern shows the structure:
#include <cstdint>
#include <span>
#include <vector>
constexpr std::uint8_t kNegativeResponse = 0x7F;
constexpr std::uint8_t kIncorrectLength = 0x13;
constexpr std::uint8_t kServiceNotSupported = 0x11;
std::vector<std::uint8_t> negative_response(
std::uint8_t request_sid,
std::uint8_t code
) {
return {kNegativeResponse, request_sid, code};
}
std::vector<std::uint8_t> handle_read_did(
std::span<const std::uint8_t> request
) {
if (request.size() != 3) {
return negative_response(0x22, kIncorrectLength);
}
const std::uint16_t did =
(static_cast<std::uint16_t>(request[1]) << 8) | request[2];
if (did == 0xF190) {
const char vin[] = "EXAMPLEVIN1234567";
std::vector<std::uint8_t> response = {0x62, 0xF1, 0x90};
response.insert(response.end(), vin, vin + 17);
return response;
}
return negative_response(0x22, 0x31); // Request Out Of Range
}
std::vector<std::uint8_t> handle_uds(
std::span<const std::uint8_t> request
) {
if (request.empty()) {
return negative_response(0x00, kIncorrectLength);
}
switch (request[0]) {
case 0x22:
return handle_read_did(request);
default:
return negative_response(request[0], kServiceNotSupported);
}
}
This is only a dispatcher example. The vin string is a placeholder, and the code omits sessions, timing, access control, concurrency, persistence, and transport integration.
What a production UDS server must add
Session state
Track the current diagnostic session and reset it after the defined timeout.
Service access rules
Check whether each service, sub-function, DID, and routine is allowed in the active session and ECU state.
Security
Use the approved security or authentication design. Rate-limit attempts and enforce delays defined by the project.
Timing
Meet P2 and P2-star response timing. Use Response Pending correctly when an approved operation needs more time.
Data ownership
Read and write ECU data through safe application interfaces. Do not let diagnostic code write arbitrary memory.
Programming safety
Flashing needs download sequencing, memory checks, power-loss handling, integrity verification, rollback strategy, and bootloader integration.
Testing
Test positive paths, every supported negative response, malformed lengths, unsupported services, timeouts, repeated frames, sequence errors, session changes, resets, and bus interruption.
A sensible implementation order
- Bring up CAN.
- Verify ISO-TP with one short and one multi-frame message.
- Implement Diagnostic Session Control.
- Implement Tester Present and timeouts.
- Add one read-only DID.
- Add negative responses and malformed-request tests.
- Add DTCs, routines, writes, or programming only as required.
- Run conformance and project-specific tests.
The simple summary
Implement UDS as separate CAN, ISO-TP, and service layers. A Python tester can use udsoncan and ISO-TP instead of manually splitting frames. An ECU server should dispatch complete requests, validate state and length, and return precise negative responses. Start with one read-only DID before adding security, routines, or programming.