Appearance
CAN transport
Below the commands sits one exchange pattern: a request goes out on the MUXEN CAN bus addressed to one device, and one answer comes back. This chapter describes how that exchange is framed, in case you are reading a candump capture, writing a device firmware, or trying to work out why a bus behaves the way it does.
Nothing here is needed to use muxen-uds.
Addressing on the wire
MUXEN uses 29-bit (extended) CAN identifiers. A UDS exchange uses two of them, one per direction:
request (client → device): 0x01000000 | (deviceId << 12) | clientId
response (device → client): 0x01000000 | (clientId << 12) | deviceIdBoth device and client addresses are 12 bits. The client address defaults to 0x27F and is settable with -c/--client-id.
For device 0x280 (640) and the default client, that is request id 0x01280 27F and response id 0x0127F 280, both with the extended flag set. A receiver filters on the response id with the mask 0x03FFFFFF.
Three broadcast frames are used outside the UDS pair:
| Purpose | Identifier | Payload |
|---|---|---|
| UID request | 0x00F00FFF, RTR, DLC 0 | — |
| UID answer (from devices) | 0x00F00nnn, matched with mask 0x03FFF000 | 8 bytes of UID |
| Enable UDS by UID | 0x00F01000 | deviceId | the target's 8-byte UID |
| Communication off | broadcast, sent before and during a firmware transfer | encoded by the shared MUXEN library |
A UID answer shorter than 8 bytes is discarded rather than padded — a truncated frame would otherwise contribute whatever followed it in the buffer as UID bytes, and that UID would end up in the scan output and in deployed.json.
ISO-TP
Requests and responses larger than a single CAN frame are segmented with ISO-TP (ISO 15765-2): a Single Frame when the payload fits, or a First Frame / Flow Control / Consecutive Frame sequence when it does not.
The source carries two implementations:
| Path | Availability | Notes |
|---|---|---|
| In-process | always | a state machine on a raw CAN socket, driven by a GLib main loop |
Kernel can_isotp | only when the build defines HAVE_KERNEL_ISOTP | one SOCK_DGRAM/CAN_ISOTP socket per exchange, non-blocking, poll() with the command's timeout |
The kernel path is not compiled in to the shipped build, so in the packaged muxen-uds the in-process implementation is the one that runs, whether or not the kernel provides can_isotp. The runtime probe for kernel support is still performed; it simply falls through.
Both request a separation time (STmin) of 10 ms between consecutive frames from the sender, and the in-process path advertises a block size of 0 — send everything, no intermediate flow control.
The in-process path is defensive about malformed input: a frame with a DLC of 0 or above 8 is ignored, a Single Frame claiming more bytes than it carries is ignored, a Consecutive Frame out of sequence aborts the transfer with an overflow flow-control frame, and a payload larger than the receive buffer is refused rather than truncated.
The receive buffer is 5000 bytes, which bounds any single UDS response and any routine --payload.
Timeouts and retries
| Operation | Timeout | Attempts |
|---|---|---|
| Most commands | 2000 ms | 3 |
Most commands, --speedy | 500 ms | 1 |
writeconfig — table read | 2000 ms | 3 (1 with --speedy) |
writeconfig — the write itself | 2000 ms | 3 |
firmware — download request, transfer exit | 30000 ms | 5 |
firmware — a transfer block | 1500 ms | 10 |
firmware — the last transfer block | 5000 ms | 10 |
Retries are 200 ms apart on the firmware path. Re-sending a request is safe: UDS is request/response, and re-sending the same transfer block sequence is the standard ISO 14229 recovery for a lost frame.
The scans have their own windows: scan listens for --timeout seconds; uid collects for 4000 ms while re-broadcasting the request every 1000 ms (2000/500 with --speedy).
Why answers are matched by identifier
A fresh ISO-TP socket is opened per exchange. An answer that arrives after its own request timed out is therefore delivered on the socket of the next request and reads as a perfectly valid response to it.
Service 0x22 (ReadDataByIdentifier) echoes the requested identifier in its positive response, and that echo is the only thing pairing an answer with a request. muxen-uds checks it: an answer carrying another identifier is dropped and the request re-sent, never handed back as the value of the one that was asked for.
That check is what stands between a busy bus and a configuration dump in which three parameters all claim to be #000 ProductId — and which would then be restored, or deployed, as if it were correct.
The services used
| Service | Code | Command |
|---|---|---|
| ECU Reset | 0x11 | reset (hard reset, with a 16-bit delay in milliseconds) |
| Read Data By Identifier | 0x22 | readconfig and every command that reads a table |
| Write Data By Identifier | 0x2E | writeconfig |
| Routine Control | 0x31 | locate, factory, routine |
| Request Download | 0x34 | firmware, announcing address and size |
| Transfer Data | 0x36 | firmware, one block per SREC S3 record |
| Transfer Exit | 0x37 | firmware |
Routine control actions are 1 start, 2 stop, 3 request results. The vendor routine identifiers muxen-uds uses:
| Routine | Id | Used by |
|---|---|---|
| Reset parameters | 0x0001 | factory --reset-device-id, --reset-device-configuration |
| Locate | 0x0002 | locate |
| Allocate configuration | 0x0004 | factory --allocate-device-configuration |
factory encodes its choice as a 16-bit flag word appended to the routine request: bit 0 resets the CAN address, 0xFFFE resets every other configuration section.
A negative response is a frame beginning with 0x7F; its third byte is the error code. readconfig treats a negative response to identifier n as "the table ends here" rather than as a failure, which is how it knows where to stop.
Reading a parameter answer
A positive 0x22 response carries, after the 0x62 service echo and the 16-bit identifier:
<name>\n <typeSize> <type> <value…>The name is terminated by a line feed (0x0A), type is 0 signed, 1 unsigned, 2 string, and typeSize is the width in bytes — the length of the string for a string. A name longer than 256 characters makes the whole answer be refused rather than truncated, because a partial name could collide with another parameter.
