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, and picks one at run time:
| Path | Used | Notes |
|---|---|---|
Kernel can_isotp | whenever the kernel provides it | a SOCK_DGRAM/CAN_ISOTP socket from libmuxenuds, non-blocking, poll() with the command's timeout |
| In-process | only where the kernel does not | a state machine on a raw CAN socket, driven by a GLib main loop |
The choice is a probe, not a build option: muxen-uds tries to create a CAN_ISOTP socket and falls back to the in-process implementation when that fails.
Both request a separation time (STmin) of 10 ms between consecutive frames from the sender and advertise a block size of 0 — send everything, no intermediate flow control.
One socket per exchange, except for a firmware upload
A command opens an ISO-TP socket for each request and closes it once the answer is in. On the kernel path that close is slow — the kernel waits before releasing a bound can_isotp socket, tens of milliseconds — which nobody notices on a request or two.
A firmware upload is thousands of exchanges, one per 32-byte record, so firmware keeps one socket for the whole transfer instead. It gives that socket up only when an exchange fails: an attempt that got no answer is retried on a fresh socket, exactly as it would be by any other command. Between two blocks, anything that reached the socket in the meantime is discarded before the next request is sent.
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 read. 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.