Demonstrates supplying your own backend for the xcplib raw Ethernet transport
(OPTION_ENABLE_UDP_RAW) from outside the library. xcplite is used as installed and
unmodified: it builds plain Ethernet/IPv4/UDP frames and hands them to the six eth_hal_*
functions, which this project implements.
ASAM CMP (Capture Module Protocol) serves testing of XCP tools that communicate through capture modules. It is not an ECU developer feature, so nothing about it lives in libxcplite — that separation is the point of this example.
Implements ASAM CMP 1.1.0: Ethernet Data Message payloads (0x08) over UDP (§6.4.2), in
both directions, plus the read-only part of the REST interface.
The demo emulates a capture module with one interface, behind which sits one XCP ECU. The tool never addresses the ECU over IP — everything is tunnelled:
XCP tool ============ CMP over UDP ============ cmp_demo
(Data Sink) (Capture Module)
|
<--- CAP_DATA_MSG (0x01) -- captured frames -------+--- emulated ECU
---- TX_DATA_MSG (0x04) -- injected frames ------>+ (xcplib)
Transmission — TX_DATA_MSG, message type 0x04 — is what CMP 1.1 added, and it is what
makes XCP communication with an ECU possible.
| Direction | What happens |
|---|---|
| ECU → tool | The frame xcplib hands to eth_hal_send is what the capture module just captured, so it is wrapped as a Captured Data Message and sent to the Data Sink |
| tool → ECU | A Transmit Data Message is unwrapped and its inner frame handed to eth_hal_recv; xcplib parses it as ordinary Ethernet/IPv4/UDP and answers the XCP command inside |
All CMP fields are big endian (§6.2). Sizes are fixed by the specification.
CMP header (8 B, §6.2.1) version=1, reserved, DeviceId, MessageType,
StreamId, StreamSequenceCounter
Captured Data Message hdr (16 B) Timestamp, InterfaceId, CommonFlags,
PayloadType=0x08, PayloadLength §7.2.1
Transmit Data Message hdr (24 B) Timestamp, Deadline, InterfaceId,
TransmissionOptions, CommonFlags,
PayloadType=0x08, PayloadLength §7.2.2
Ethernet payload (6 B + data) Flags, Reserved, DataLength, DATA §7.3.8
DATA runs from the destination MAC through the FCS, but xcplib's HAL contract passes
frames without FCS — so the codec appends four zero bytes on wrap and strips four on
unwrap. FCS_SUPPORT is reported as 0, which is what §7.3.8 prescribes for a module that
cannot compute a real FCS. Payload type 0x0D RAW_ETHERNET is deliberately not used: it
additionally carries the preamble and SFD, which a synthetic ECU has nothing useful to put in.
Not used, and advertised as unsupported over REST: aggregation (§6.3.2), segmentation (§6.3.3), status messages (§8) and control messages (§9).
libxcplite's socket_raw.c calls six eth_hal_* functions. This project defines all six in
src/socket_raw_hal_cmp.c. Because libxcplite is a static library, the linker only pulls an
archive member in to resolve an undefined symbol — and these are already defined here, so a
built-in backend is never pulled in and there is no clash.
| File | Purpose |
|---|---|
src/cmp.h / src/cmp.c |
The CMP envelope codec — the only place that knows about CMP. Pure: no sockets, no I/O, no global state |
src/cmp_transport.h |
The outer transport seam |
src/cmp_transport_udp.c |
CMP over UDP (§6.4.2) — one ordinary datagram socket |
src/socket_raw_hal_cmp.c |
The six eth_hal_* functions, joining codec and transport |
src/cmp_backend.h |
Backend configuration and status |
src/cmp_rest.h / src/cmp_rest.c |
The read-only REST interface (§12.3) |
src/main.c |
Demo application — command line, server setup, calibration segment, event |
test/cmp_codec_test.c |
Codec unit test against the specification's own sample files |
src/cmp_discovery.c |
CMP_CM_DISCOVERY responder (12.1.1), multicast |
test/fake_sink.py |
A minimal Data Sink — plays the tool's half |
test/discovery_probe.py |
A Data Sink looking for capture modules |
test.sh |
On-target test: syncs to the target, builds, runs and checks it |
test/test_local.sh |
The same end-to-end check, on this machine over loopback |
xcplite has to be installed from the raw configuration first:
cd <xcplite>
cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=$HOME/xcplite-install
cmake --build build-raw --target installOn macOS or Windows add -DCMAKE_C_FLAGS="-DOPTION_UDP_RAW_HAL_EXTERNAL" to that first
command.
cd <xcplite>
cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw -DCMAKE_C_FLAGS="-DOPTION_UDP_RAW_HAL_EXTERNAL" -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=$HOME/xcplite-install
cmake --build build-raw --target installThen build this project against the install:
cd examples/cmp_demo
cmake -B build -S . -Dxcplite_DIR=$HOME/xcplite-install/lib/cmake/xcplite
cmake --build buildThis project only needs an installed xcplite, not a source checkout. If you move it into a repository of its own, that install is the whole dependency.
./build/cmp_demo --sink 192.168.0.10:55555--sink may be omitted, in which case the Data Sink address is learned from the first CMP
message received, the same way socket_raw.c learns the peer MAC.
| Option | Meaning |
|---|---|
--sink <a.b.c.d:port> |
Data Sink address for captured data. Default: learned |
--listen <port> |
UDP port for incoming CMP messages (default 55555) |
--mtu <bytes> |
MTU of the path to the Data Sink (default 1500, max 9000) |
--rest-port <port> |
REST interface port (default 8080, 0 disables) |
--device-id, --stream-id, --interface-id |
CMP identity |
--ecu-mac <xx:…:xx> |
MAC of the emulated ECU. Default: derived from --device-id |
--ip, --port |
Address of the emulated ECU, seen only inside the CMP payload |
Because the ECU address only ever appears inside the payload, it does not have to be free
on any real network — unlike the plain raw Ethernet transport. ping to it does not work
either: there is no IP route to the ECU, it lives behind the tunnel. Send an ICMP echo as a
TX_DATA_MSG instead if you want to exercise that path.
§6.4.2: "CMP messages shall not be sent over IP fragmented packets." The envelope adds 34 bytes to every captured frame, and the outer IPv4/UDP headers add 28 more, so on a 1500-byte path the largest inner frame is 1438 bytes.
This is why xcplite's raw configuration sets OPTION_MTU 1420 rather than the 1500 a standard Ethernet link allows:
OPTION_MTU |
1420 |
XCPTL_MAX_SEGMENT_SIZE |
1392 (OPTION_MTU - 32, %8) |
| Largest inner Ethernet frame | 1434 (42 + segment) |
| As a CMP message | 1468 (+ 34 envelope) |
| As an IP packet | 1496 (+ 28) — fits 1500 with 4 bytes to spare |
At the full link MTU a segment is 1472 bytes and the frame 1514, filling a 1500-byte path on its own, leaving
the envelope nothing: small transfers still work, but a saturated DAQ stream hits the limit
and reports SOCKET_ERROR_MSGSIZE. The demo checks this at startup and warns, naming the
budget and the remedy, so a mismatched configuration is visible before it bites:
CMP frame budget: 1438 bytes per inner frame (1472 byte CMP message - 34 byte envelope)
If you raise OPTION_MTU again, either lower the envelope's share with a jumbo-capable path
(--mtu 9000, allowed explicitly by §6.4) or expect that warning back.
Either way an oversized frame is refused with ETH_HAL_ERROR_SIZE rather than fragmented.
That is exactly what the HAL contract designed that error for: whether a frame fits is a
runtime property only the backend knows.
# Test MTU 1501 — just above standard Ethernet MTU
ping -D -c 3 -s 1473 192.168.0.206§12 requires a capture module to support at least one of three approaches to address configuration and discovery — and one of them is "static configuration without Capture Module Discovery", so implementing none of it conforms. This demo implements the second:
| Approach | § | Here |
|---|---|---|
| Static, no discovery | 12 | still works, --no-discovery |
| XCP-based, IP multicast | 12.1 | implemented, src/cmp_discovery.c |
| mDNS / DNS-SD | 12.2 | not implemented — needs a full mDNS responder or a dependency on Avahi |
§12.1 is titled "XCP-based approach" and means it literally: the request is an ordinary XCP
packet in the ordinary XCP-on-Ethernet transport header, with command code 0xF2
(CC_TRANSPORT_LAYER_CMD) and sub-command 0x10, multicast to 239.255.0.0:5556. XCP uses
the same mechanism for its own discovery — see the repository
docs/XCP_DISCOVERY.md for what XCPlite has there today and the
options for it. Nothing about XCP discovery is decided by this demo.
./test/discovery_probe.py reply to the group (12.1.1) : 0 answer(s)
reply to us directly : 1 answer(s)
capture module cmp_demo-0001
description XCPlite cmp_demo, emulated ASAM CMP capture module
MAC 2C:CF:67:EF:F6:78
reachable at 192.168.0.206 -> http://192.168.0.206:8080/asam-cmp/version-info
prefix /24 gateway 0.0.0.0
answered via unicast
The point of the exchange is the HTTP port: discovery hands the tool the REST interface it then configures the module through. The reply address and port come from the request, so the responder never needs to know anything about the network it is on.
It runs on the REST thread, not a thread of its own — that thread is already in a poll()
loop and already knows the HTTP port the response has to advertise.
The multicast return path is filtered more often than the request path. In the run above
the request reached the Pi over Wi-Fi and was answered, and the multicast answer never came
back: an access point does not normally forward group traffic to a wireless client. §12.1 is
explicit that "the IP destination address and UDP destination port of the response are given
by the request", so discovery_probe.py makes two passes — one asking to be answered on the
group as §12.1.1 describes, one asking to be answered directly — and reports which worked.
The responder simply honours whatever address the request carries. On loopback both work; on
Wi-Fi typically only the direct one does.
Two things worth knowing if you touch the socket code. Joining with
imr_interface = INADDR_ANY does not mean "all interfaces": it lets the stack pick one, and
on macOS it receives nothing at all. cmpDiscoveryStart() therefore enumerates interfaces and
joins the group on each — loopback included, which is what makes the same-host test work,
since a process does not see its own multicast sent via a LAN interface. And a multicast reply
needs IP_MULTICAST_IF set per response, or it leaves through the default route and the tool
that asked never sees it.
§12.3 calls the REST interface mandatory for a capture module, and §7.2.2 says a Data Sink uses it to detect whether transmission is supported. If a tool gates injection on that, no REST means no XCP — which is why the read-only part is implemented here.
| Endpoint | § |
|---|---|
GET /asam-cmp/version-info |
12.3.1 |
GET /asam-cmp/v1/identification |
12.3.2 |
GET /asam-cmp/v1/interfaces |
12.3.4 — advertises transmission support |
GET /asam-cmp/v1/measurement |
12.3.6 |
The decisive part is the Transmitter object of /interfaces (§12.3.4, Table 88):
"Transmitter": { "TransmissionSupportBitmask": 1, "FeatureSupportBitmask": 0,
"AggregationMtu": 1472, "AggregationCount": 1 }TransmissionSupportBitmask bit 0 is TIMESTAMP_IMMEDIATE: we send every request straight
away and support neither absolute nor relative scheduling, no deadline and no segmentation.
§7.2.2 allows that — "If the CM does not support Timestamp, it shall always send
immediately". AggregationMtu is where the MTU budget above is advertised to the tool.
§12.3 says the interface should run on port 80; the demo defaults to 8080 so it needs no
privileges. Everything that would change configuration (the PUT methods), mDNS/DNS-SD
discovery (§12.2.2) and XCP-based discovery (§12.1) are not implemented — §12 permits
"Static configuration without Capture Module Discovery", which is what this demo uses.
Two scripts, same checks, different place to run them:
./test.sh # on the target: sync, build, run and check over the network
./test/test_local.sh # on this machine, over loopbackNeither needs veth, a network namespace or root, unlike the plain raw Ethernet transport:
the outer transport is an ordinary UDP socket, and the emulated ECU address only ever appears
inside the CMP payload.
test.sh is the on-target one, modelled on
[udp_raw_demo/test.sh]. Set TARGET_USER
and TARGET_HOST at the top of it, then it:
- rsyncs the library sources and this example to the target;
- builds and installs the
rawconfiguration of xcplite there, then buildscmp_demoagainst that install — the two-stage build a standalone project needs; - checks that the built-in AF_PACKET backend was not linked into the binary, by looking
for a string only it contains. Checking the archive would prove nothing: on Linux
socket_raw_hal_linux.ois inlibxcplite.aeither way, and it is the static-library link rule that keeps it out of the executable; - runs the codec unit test on the target, which is also the check that the big-endian packing is right on aarch64;
- starts the capture module and queries all four REST endpoints, asserting that the
Transmitterobject advertises transmission; - reports the CMP endpoint status and confirms the UDP port is open;
- sends a hardcoded XCP CONNECT tunnelled through CMP and decodes the response, printing
the exact bytes of the
TX_DATA_MSGit puts on the wire.
The CONNECT frame is assembled from the parameters at the top of the script rather than
pasted in as a fixed hex blob, so that changing e.g. ECU_IP cannot leave a stale IPv4
header checksum behind. The XCP command itself — FF 00 — is the hardcoded part.
test/test_local.sh runs the codec test and then drives cmp_demo with fake_sink.py
over loopback, for a full CONNECT / GET_STATUS / DISCONNECT exchange. It leaves cmp.pcap in
the folder it was started from — open it in Wireshark, whose ASAM CMP dissector keys on
EtherType 0x99FE. Everything else the demo writes (demo.log, the .a2l and the .bin)
stays in a temporary directory and is discarded.
test/cmp_codec_test.c is the strongest check. Its golden vectors are lifted byte for byte
from the sample PCAPNG files shipped with the specification, so it pins the wire format against
the standard itself rather than against one reading of it:
CMP_1.0/asam_cmp_cap_0x08_Ethernet.pcapng— a Captured Data Message with an Ethernet payload, exactly the shape this backend emits.cmpWrapCaptured()must reproduce it byte for byte (with the sample's FCS zeroed, since we reportFCS_SUPPORT = 0).CMP_1.1/asam_cmp_tx_0x01_can_29bit_0x12345678.pcapng— a real Transmit Data Message. Its payload is CAN, so it must be rejected — but only after the 24-byte transmit header has been parsed correctly. A wrong header length surfaces asMALFORMEDinstead ofPAYLOAD_TYPE, so this vector pins the transmit header layout too.
There is no sample of a transmit message carrying an Ethernet payload — the 1.1 samples cover CAN, CAN FD and LIN only — so the happy path uses a message built from those two pinned layouts.
test/fake_sink.py plays the tool's half with nothing but the Python standard library: it
queries the REST endpoints, checks that transmission is advertised, and tunnels XCP CONNECT /
GET_STATUS / DISCONNECT, verifying the responses and the StreamSequenceCounter continuity.
--pcap <file> writes every CMP message to a capture file for Wireshark, whose built-in
ASAM CMP dissector keys on EtherType 0x99FE — so the messages are framed for the Ethernet
transport option there and dissect automatically. The CMP bytes are identical under both
transport options; only the outer framing differs.
The openDAQ ASAM-CMP-Library can serve as an
independent decoder for the capture direction, but not for transmission: its MessageType enum
stops at 0x03, so it predates CMP 1.1 and does not know TX_DATA_MSG.
Runs on Linux and macOS. Threads go through the library's own abstraction; sockets do not:
| Threads | platform.h — THREAD_HANDLE, create_thread, join_thread, THREAD_FUNC_RETURN |
| Mutexes | none needed |
| Sockets | direct POSIX, in cmp_transport_udp.c, cmp_rest.c and cmp_discovery.c |
The sockets are deliberately not routed through sockets.h, and that is not an oversight —
it is not possible. In the raw configuration sockets.c is compiled out entirely
(#if (TCP || UDP) && !defined(OPTION_ENABLE_UDP_RAW)), and sockets.h does not even
declare socketJoin, socketListen, socketAccept or socketRecv when
OPTION_ENABLE_UDP_RAW is set, so discovery has no multicast call and the REST interface has
no TCP listener to use. socket_raw.c implements a UDP-datagram subset and has no TCP at all.
There is a deeper reason. In the raw configuration the socket API is the emulated
ECU-side stack — UDP/IPv4 over the Ethernet HAL. This project's sockets are real host
sockets on the other side of that HAL. Different layer, different network: calling
socketOpen() for the REST listener would open it on the emulated ECU's network.
On Windows the port is about 54 lines of socket code across those three files, plus
getifaddrs → GetAdaptersAddresses and WSAStartup (the raw transport's socketStartup()
does not call it). All of it additive #ifdef, no effect on Linux. It is not currently done:
a Windows XCP tool can already drive this on a Linux target over the network, which is what
test.sh does. Before attempting it, check that Windows' IP_MULTICAST_LOOP — a
receive-side option there, send-side on BSD and Linux — still allows a same-machine discovery
test, since "everything on one box" is the only thing the port would buy.
- CMP over Ethernet, EtherType 0x99FE (§6.4.1). This is the transport option the
specification makes mandatory for a capture module, so it should not be deferred
indefinitely. It needs the AF_PACKET plumbing — preserved in git commit
01e7f40, which used it for the pass-through version of this backend — plus an outer Ethernet header (dst = sink MAC, src = our MAC, EtherType0x99FE) and padding to the 60-byte minimum. The envelope codec itself is unchanged: onlycmp_transport.hgains an implementation. - Status messages (§8), control messages (§9), aggregation, segmentation, time synchronisation and the configuration-changing REST methods.
- Nothing CMP-specific belongs in libxcplite. If a change there appears necessary, that is the
signal that the encapsulation has leaked — fix it in the backend. No such change was
needed:
socket_raw_hal.his untouched, including for the capture timestamp, which the backend takes itself ineth_hal_sendbecause that is the moment the emulated module sees the frame. - The envelope is applied into the backend's own buffer, never into the transmit queue
headroom. CMP must not participate in the zero-copy path or influence
XCPTL_TX_HEADROOM. - The extra copy that costs is accepted — this is a test-bench path, not a performance one.
See [docs/SOCKET_RAW.md] for the transport design and the HAL contract.