diff --git a/.gitignore b/.gitignore index a0a31d6b..759baf06 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ RecycleBin.ini test_bintool *.bak *.log +*.pcap # Other .vscode @@ -36,3 +37,4 @@ test_bintool /docs/SUBMODULE_GUIDE.md /shm_*.sh wlan.h +/TODO_old.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 39297e83..641c8425 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,5 +23,9 @@ "cmake.configureOnOpen": false, "rust-analyzer.linkedProjects": [ "${workspaceFolder}/tools/xcpclient/Cargo.toml" - ] + ], + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.formatOnSave": true + } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c652fe9e..9bb416ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,98 @@ All notable changes to XCPlite are documented in this file. +## [V2.2.1] + +- New macros `DaqTriggerEventCapture`, `DaqTriggerEventCaptureAt` and `DaqCreateAndTriggerEventCapture` in `xcplib.h` to measure local variables which the compiler keeps in registers, without marking them `volatile`. They copy the given variables into a capture struct on the stack when the event is triggered and pass its address as the base address of address extension 3, the originals stay in their registers. The offline A2L generator registers the members of the struct with the names of the captured variables (`.`), see `docs/OFFLINE_A2L.md`. A function which captures may be inlined, and asynchronous access (polling) works. Up to 16 plain identifiers per trigger, in C and C++, not available with MSVC. Bitfields can not be captured, `const` variables only in C. The unimplemented `DaqCapture` macro, which copied a variable into a hidden static, is removed +- xcpclient related changes (xcpclient version 4.0.0): + - The captured local variables of the event triggers are registered from the capture structs (`cap__`). A captured variable is not registered a second time as a stack frame relative variable, and the diagnostics about the stack frame of an inlined function are only reported when the function has stack frame relative variables which are not captured + - The addressing mode signature of the target (`XCPLITE__`) is read from the ELF symbol table instead of the DWARF variables, so it is found when the debug information of the XCPlite library is not parsed (`--elf-unit-limit`) or the library was built without `-g`. Without the signature xcpclient assumes absolute addressing of the calibration segments, which is wrong for a `CASDD` or `CXSDD` build. The `--elf-unit-limit=0` of `examples/no_a2l_demo/create_a2l.sh` had this effect, it is removed + - The compilers which built the ELF file are logged as `Compiler: ...` (`DW_AT_producer` of the compilation units) with their version and command line options + - Metadata markers (`XCP_COMMENT`, `XCP_UNIT`, `XCP_LIMITS`, `XCP_READ_WRITE`) of local variables in C compilation units built with GCC are applied again. GCC gives the marker constants inside a function no `DW_AT_location` and names their symbols `.`, which no symbol lookup matched, so every marker written inside a C function was silently dropped. The address now comes from these symbols, markers of the same name in several functions are told apart by their size + - A metadata marker at file scope is no longer applied to the local variables of the same name in functions. `XCP_COMMENT(counter, ...)` and `XCP_READ_WRITE(counter)` for a global variable `counter` also annotated `foo.counter` and `task.counter`, which made local variables writable in the A2L file. A marker in a function is no longer applied to a global variable of the same name when its own local variable was not registered, it is reported instead + - Functions with an event trigger which the compiler inlined are detected (abstract instance, inlined copies and the out of line copy). The stack frame relative variables of such a function are not registered, as no address is valid for all copies of the function, and a warning asks to mark the function `noinline`. Previously the variables of the inlined copy were registered as local variables of the calling function with the default event, which measured a reused stack slot, and the static variables of the out of line copy lost their function scope, comment and event (clang inlines a function called once already at `-O1`, `no_a2l_demo` built with clang) + - DWARF 5 location lists referenced by index (`DW_FORM_loclistx`, clang) are evaluated like the location lists of DWARF 4. A location list is accepted only if all its entries describe the same memory location, as the trigger point of the event is not known: previously the first memory location in the list was used, which may be valid only in a part of the function. Variables in registers or with changing locations are not measurable, they are not looked up in the symbol table + - The stack frame relative addresses of local variables are the offsets from the DWARF frame base of the function (`DW_AT_frame_base`) without correction, the call frame information parser is removed. The trigger macros pass this frame base to the target: `xcp_get_frame_addr()` in `xcplib.h` is `__builtin_dwarf_cfa()` under GCC (canonical frame address, on every architecture, the Xtensa special case is gone) and `__builtin_frame_address(0)` under clang (frame pointer register). xcpclient checks the frame base of the function of every event trigger and warns when it is neither, for example the stack pointer of a function without frame pointer under clang, the stack frame relative variables of such a function are not registered. **GCC targets must be rebuilt with the new header, the A2L file of an older build does not match**. Previously the distance between the frame pointer and the CFA was read from the call frame information with heuristics which were wrong for clang builds (the frame base is the frame pointer, no distance to add) and for GCC AArch64 functions whose frame record is not at the stack pointer + - Location expressions of variables which can not be evaluated (register locations, entry values, optimized code in other compilation units) and variables without a name are reported at debug level instead of error/warning level + - New option `--default-event ` (also `default_event` in the TOML config file): event used for the DAQ measurement of variables without a fixed event, such as global variables in an A2L file generated from an ELF file. When an A2L file is created from an ELF file, the event is assigned to global variables and to static variables in functions without an event trigger as their default event +- New attribute macro `XCP_NOINLINE` in `xcplib.h` for functions which trigger an event and measure their local variables, used by `foo()` in `no_a2l_demo` and `no_a2l_demo_cpp`. Such a function must not be inlined, see `docs/OFFLINE_A2L.md` +- Split `platform.c/.h` into `platform.c/.h` (threads, mutex, clock, sleep, memory, atomics) and `sockets.c/.h` (socket abstraction for all platforms). `sockets.h` includes `platform.h`; files that use both include both explicitly (IWYU). +- New raw Ethernet transport `OPTION_ENABLE_UDP_RAW`: XCP on UDP/IPv4 implemented inside xcplib on top of a thin raw Ethernet HAL, for targets without a TCP/IP stack. See `docs/SOCKET_RAW.md`. + - New build configuration `raw` (`src/xcplib_raw_cfg.h`, `build-raw/`) with the new example `udp_raw_demo` and the unit test `socket_raw_test` (Linux only) + - `src/socket_raw.c` — UDP/IPv4 layer, answer-only ARP, ICMP Echo responder, receive filter with an absolute-deadline loop + - `src/socket_raw_hal.h` — raw Ethernet HAL interface, `src/socket_raw_hal_linux.c` — AF_PACKET backend (requires `CAP_NET_RAW`) + - `test/test_socket_raw.sh` — isolated veth/netns test setup with ARP, ping and XCP CONNECT checks + - Mutually exclusive with `OPTION_ENABLE_UDP`/`OPTION_ENABLE_TCP` and requires `OPTION_QUEUE_32`; both enforced by `#error` in `xcptl_cfg.h`, together with the SHM, multicast and MTU restrictions + - The `rtos` configuration keeps using the lwIP socket API - the raw transport is a separate configuration, not an override +- Optional zero copy transmit for the raw Ethernet transport (`OPTION_UDP_RAW_ZERO_COPY`, on by default): headroom is reserved in front of every transmit queue3/queue32 segment so the Ethernet/IPv4/UDP header is written in place instead of copying the payload into a frame buffer. + - New generic queue concept `QUEUE_SEGMENT_HEADER_SIZE` in `queue.h` - reserved once per *segment*, as opposed to the existing per-*message* `QUEUE_ENTRY_USER_HEADER_SIZE` + - `queue32.c`/`queue32m.c` gain one guarded field; with the option off the queue entry layout is byte identical to before + - Command responses keep the copying path, they are built on the stack and are not hot +- New `OPTION_UDP_RAW_HAL_EXTERNAL`: an application can supply its own raw Ethernet HAL from outside the library. xcplib then selects no backend and the `eth_hal_*` symbols stay undefined in `libxcplite` until the application links its own implementation. Intended for backends which do not belong in the library, for example ASAM CMP for testing XCP tools through capture modules, or a vendor specific interface such as XLAPI. + - Also lifts the Linux-only restriction of the raw transport: without a built in backend there is nothing platform specific left in it + - Depends on `libxcplite` being a **static** library, so the undefined `eth_hal_*` resolve at application link time with no indirection in the transmit path. A shared build would resolve them internally and the override would silently not take + - `socket_raw_hal_linux.c` is excluded by the same option, so the built in AF_PACKET backend is not linked in +- `OPTION_MTU` in the `raw` configuration set to **1420**, below the 1500 of a standard Ethernet link, to leave headroom for a HAL backend which encapsulates the frame before putting it on the wire. `XCPTL_MAX_SEGMENT_SIZE` becomes 1392, the largest Ethernet frame 1434 and its IP packet 1420 bytes, i.e. `OPTION_MTU` exactly. An encapsulating backend then fits inside a 1500 byte path: `cmp_demo` wraps that 1434 byte frame in a 34 byte CMP envelope and 28 bytes of outer IPv4/UDP headers, reaching 1496 bytes. At the full link MTU a segment is 1472 bytes and the frame 1514, filling the path on its own, so an encapsulating backend has nothing left and the raw transport, which does not fragment, can only refuse the frame. + - The previous value was 1504, a leftover of the pre-V2.1.11 convention where `OPTION_MTU` was the link MTU rounded up to a multiple of 8. Since "Fix UDP segment size calculation from MTU" (V2.1.11) `XCPTL_MAX_SEGMENT_SIZE` is `(OPTION_MTU - 28) & ~7`, so `OPTION_MTU` is the true link MTU and 1504 gained nothing over 1500 + - **Behaviour change:** `udp_raw_demo` now sends segments of 1392 instead of 1472 bytes +- New example `cmp_demo`: an emulated **ASAM CMP** (Capture Module Protocol) capture module carrying XCP, for testing XCP tools which communicate through capture modules. Implemented against ASAM CMP Protocol Layer Specification V1.1.0. Nothing CMP specific is in `libxcplite` — the whole protocol lives in the example, behind the `eth_hal_*` interface. + - A standalone CMake project consuming an **installed** xcplite via `find_package(xcplite)`, not built from the root `CMakeLists.txt` + - CMP over UDP (6.4.2). CMP over raw Ethernet is a possible later step + - `src/cmp.c` — the envelope codec, verified byte exact against the sample PCAPNG files shipped with the specification + - Injection uses `TX_DATA_MSG` (message type `0x04`), which **CMP 1.1 introduced** and 1.0 does not have. Without it a capture module could only carry DAQ and XCP could never `CONNECT`. Note that a CMP 1.0 dissector reports it as an unknown message type + - `src/cmp_rest.c` — the mandatory REST interface (12.3), read only. Its `Transmitter` object is how a Data Sink detects that transmission is supported (7.2.2); without it a tool may never inject + - `src/cmp_discovery.c` — `CMP_CM_DISCOVERY` responder (12.1.1) on `239.255.0.0:5556`, serviced by the REST thread. Section 12 requires only one of three discovery approaches and permits static configuration with none, so this is optional + - `test/cmp_codec_test.c`, `test/fake_sink.py` (a minimal Data Sink), `test/discovery_probe.py`, `test/test_local.sh` (loopback) and `test.sh` (on target) +- New `docs/XCP_DISCOVERY.md`: records what xcplib has for XCP's own multicast discovery today — `GET_SERVER_ID_EXTENDED` implemented, `GET_SERVER_ID` stubbed, both behind `XCPTL_ENABLE_MULTICAST`, which no shipped configuration enables and which the raw transport excludes — and the options for it. Nothing is decided or changed. Also records that `XCPTL_MULTICAST_PORT` is 5557 while ASAM CMP 12.1 states XCP uses 5556, which needs checking against ASAM MCD-1 XCP. +- IPv4 fragmentation is now prevented on the socket transport: `socketOpen` sets the DF bit on UDP sockets (`IP_MTU_DISCOVER`/`IP_PMTUDISC_DO` on Linux, `IP_DONTFRAG` on macOS/BSD, `IP_DONTFRAGMENT` on Windows, no-op on lwIP). Fragmentation is harmful for DAQ - one lost fragment loses the whole datagram and reassembly adds jitter - and an `OPTION_MTU` larger than the path MTU previously degraded measurement silently. Oversized segments now fail with `EMSGSIZE` and a message naming the segment size and the `OPTION_MTU` to reduce. **Behaviour change:** a setup that relied on fragmentation will now report an error instead of silently fragmenting. +- The lwIP socket path now reports a segment that does not fit the link MTU. lwIP sets no DF option - it has no `IP_DONTFRAG` - so unlike Linux, macOS/BSD, QNX and Windows it does not refuse an oversized datagram: it fragments or drops it according to its own `IP_FRAG` build setting, silently either way. That made lwIP the one transport where an `OPTION_MTU` larger than the link MTU degraded measurement with no diagnostic at all. `socketSendTo` now compares the segment plus 28 bytes of IPv4/UDP headers against `netif_default->mtu` and warns once, with the same wording as the socket path. It still sends: this is a diagnostic, not a guard, since refusing a datagram lwIP may well deliver would change behaviour. `netif_default` is not necessarily the interface routing to the destination on a multi-homed target, so a false report is possible there. +- `create_thread()` on Windows now returns 0 on success, matching `pthread_create()`, instead of evaluating to the thread `HANDLE` where non-NULL meant success. The two conventions were inverted, so `if (create_thread(...) != 0)` read as an error on POSIX and as success on Windows. A *portable* check is still not possible - both FreeRTOS variants are `do {} while (0)` statements which assert - and `platform.h` now documents the contract per platform. No caller in the repository tests the result, so nothing changes in behaviour. +- Version aligned to 2.2.1 in `CMakeLists.txt` (what `find_package(xcplite)` reports) and in `OPTION_VERSION_MAJOR`/`_MINOR`/`_PATCH` in `xcplib_cfg.h` (what `XCP_DRIVER_VERSION` reports to the XCP client). The two had drifted apart at 2.1.2 and 2.1.10. +- Fixed `PLATFORM_32_BIT` typo in `xcplib_cfg.h`, which prevented the automatic selection of `OPTION_QUEUE_32` on 32 bit platforms +- Fixed missing `#include ` in `shm.c`, which broke the `shm` configuration build on macOS +- Window build workaround to disable section based event pre-registration +- Fixed older XCP protocol layer versions 0x0100 to 0x0102 + +## [V2.1.14] + +- Support static allocation and improve FreeRTOS robustness (#124) + * Support static allocation for FreeRTOS tasks and mutexes + * Use static storage when configSUPPORT_STATIC_ALLOCATION is enabled + * Preserve dynamic allocation and recursive mutex support + * Update FreeRTOS documentation and remove obsolete queue TODOs + * Fix FreeRTOS delay rounding and overflow + * Avoid unaligned IPv4 address access in FreeRTOS/lwIP + * Fix synchronization of FreeRTOS queue state + * Correct FreeRTOS queue and lwIP documentation + * Fix FreeRTOS emulator clock test build + + +## [V2.1.12] + +- xcpclient related changes (xcpclient version 3.0.10): + - Improved variable registration for global and static variables + - Global variables and local static variables in functions without event trigger are registered, but not associated to any specific default DAQ event + - Unique typedef names for struct/class types with the same name in different namespaces, classes or functions (`motor_control.Input`, `valve_control.Input`). Previously all variables of such types referenced the first registered typedef and were shown with the wrong type. + - Global variables with the same name in different namespaces are registered with namespace qualified names (`motor_control.input`, `valve_control.input`) instead of dropping all but the first one + - A struct type which is used for measurement and for calibration variables gets separate typedefs + - The GCC declaration and definition entries of a namespace scope variable are merged, they no longer produce duplicate instance errors + - Variables with internal linkage in a namespace and without DWARF location (e.g. metadata markers `XCP_COMMENT(motor_control__input, ...)` inside a namespace) are resolved by their mangled symbol name + - Metadata annotations placed in the same namespace or function as the variable no longer need a scope prefix (`XCP_COMMENT(input, ...)` in namespace `motor_control` annotates `motor_control.input`), explicit prefixes keep working. Metadata for the typedef fields of namespace qualified instances. + - Local variables without a DWARF location (optimized away) are no longer resolved to the address of a global variable with the same name, only symbols with local binding are considered for them + - no_a2l_demo, no_a2l_demo_cpp: more demo cases, no_a2l_demo_cpp demonstrates types and variables with the same name in different namespaces +- Documentation: new `docs/OFFLINE_A2L.md` for the offline A2L generation with xcpclient (workflow, naming rules, supported types, diagnostics), `docs/TECHNICAL.md` keeps the library topics and the instrumentation marker contract, xcpclient and no_a2l_demo READMEs updated + + + ## [V2.1.11] -* Allow custom memory attributes for queue32m -* Avoid mutex allocation when queue32m uses critical sections -* Support optional recursive FreeRTOS mutexes -* Fix typos in FreeRTOS documentation and comments -* Fix UDP segment size calculation from MTU -* Make FreeRTOS queue segment count configurable +- FreeRTOS related changes: + * Allow custom memory attributes for queue32m + * Avoid mutex allocation when queue32m uses critical sections + * Support optional recursive FreeRTOS mutexes + * Fix typos in FreeRTOS documentation and comments + * Fix UDP segment size calculation from MTU + * Make FreeRTOS queue segment count configurable ## [V2.1.10] @@ -139,7 +223,7 @@ All notable changes to XCPlite are documented in this file. ```c void XcpInit(const char *name, const char *epk, uint8_t mode); ``` -- The return value contract of `socketRecv` and `socketRecvFrom` has changed. Only code that uses these functions directly (i.e. code that includes `platform.h` is affected) +- The return value contract of `socketRecv` and `socketRecvFrom` has changed. Only code that uses these functions directly (i.e. code that includes `socket.h` is affected) ### Experimental diff --git a/CLAUDE.md b/CLAUDE.md index 94c4032e..c9be0bfa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,7 +89,10 @@ src/queue*.c Lock-free/mutex transmit queue implementations ( src/cal.c/.h Calibration segment RCU implementation (page switching, locks) src/a2l.c, src/a2l_writer.c Runtime A2L file generation src/persistence.c/.h Binary (.bin) parameter/event persistence across restarts -src/platform.c/.h OS abstraction (threads, sockets, clock, atomics) — Linux/macOS/QNX/Windows/FreeRTOS +src/platform.c/.h OS abstraction (threads, clock, mutex, atomics) — Linux/macOS/QNX/Windows/FreeRTOS +src/sockets.c/.h Socket abstraction over the OS socket API (all standard platforms) +src/socket_raw.c Raw-Ethernet UDP/IPv4 transport (OPTION_ENABLE_UDP_RAW), see docs/SOCKET_RAW.md +src/socket_raw_hal.h Raw Ethernet HAL interface; socket_raw_hal_linux.c is the AF_PACKET backend src/util.c/.h Shared helpers ``` @@ -119,7 +122,9 @@ XCPlite encodes *where* a measured/calibrated variable lives (global, stack, hea - `xcp_evts` section — `tXcpEventDescriptor` constants emitted by `DaqCreateEvent`/`DaqCreateAndTriggerEvent` - `xcp_cals` section — `tXcpCalSegDescriptor` constants emitted by `CalSegDecl`+`CalSegCreate` -and DWARF scope anchors named `trg____` (e.g. `trg__AAS__foo`, letter position = address-extension value [0..]: `A`=absolute, `C`=cal-segment-relative, `S`=stack-relative, `D`=dynamic/heap) emitted by the trigger macros, to reconstruct addressing without any runtime A2L calls. Full details, including what changes if you modify the trigger macros, are in `docs/TECHNICAL.md`. +and DWARF scope anchors named `trg____` (e.g. `trg__AAS__foo`, letter position = address-extension value [0..]: `A`=absolute, `C`=cal-segment-relative, `S`=stack-relative, `D`=dynamic/heap) emitted by the trigger macros, to reconstruct addressing without any runtime A2L calls. The marker contract (sections, marker names, trigger anchor naming) is in `docs/TECHNICAL.md`, the tool side (workflow, naming rules, symbol resolution, supported types) in `docs/OFFLINE_A2L.md`. + +The generator reads ELF files only. Executables built on macOS are Mach-O and carry no DWARF (the linker leaves it in the `.o` files / `.dSYM`), so `xcpclient` rejects them with an explicit "macOS is not supported" error and exit status 1 — A2L files for `no_a2l`/`rtos` builds must be generated from a Linux build, which is what the examples' `create_a2l.sh` scripts do via a remote build on a Linux target. ### Shared-memory (SHM) multi-application mode (`docs/SHM.md`) diff --git a/CMakeLists.txt b/CMakeLists.txt index 27909771..b513c222 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(xcplite VERSION 2.1.2 LANGUAGES C CXX) +project(xcplite VERSION 2.2.1 LANGUAGES C CXX) @@ -16,6 +16,7 @@ project(xcplite VERSION 2.1.2 LANGUAGES C CXX) # cmake -B build-ptp -S . -DXCPLITE_CONFIGURATION=ptp # cmake -B build-shm -S . -DXCPLITE_CONFIGURATION=shm # cmake -B build-rtos -S . -DXCPLITE_CONFIGURATION=rtos +# cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw # # Configuration descriptions (see src/xcplib__cfg.h for details): # default - 64-bit, on-target A2L generation, filesystem, Ethernet UDP/TCP @@ -23,15 +24,16 @@ project(xcplite VERSION 2.1.2 LANGUAGES C CXX) # ptp - Like default with socket hardware timestamps (ptptool, Linux only) # shm - Shared-memory multi-application mode (shmtool, xcpdaemon) # rtos - Embedded RTOS targets (FreeRTOS), reduced footprint, no filesystem +# raw - Raw Ethernet transport, UDP/IPv4 inside xcplib, no TCP/IP stack (Linux only) # ============================================================================= set(XCPLITE_CONFIGURATION "default" CACHE STRING - "Library build configuration: default | no_a2l | ptp | shm | rtos") -set_property(CACHE XCPLITE_CONFIGURATION PROPERTY STRINGS default no_a2l ptp shm rtos) + "Library build configuration: default | no_a2l | ptp | shm | rtos | raw") +set_property(CACHE XCPLITE_CONFIGURATION PROPERTY STRINGS default no_a2l ptp shm rtos raw) -if(NOT XCPLITE_CONFIGURATION MATCHES "^(default|no_a2l|ptp|shm|rtos)$") +if(NOT XCPLITE_CONFIGURATION MATCHES "^(default|no_a2l|ptp|shm|rtos|raw)$") message(FATAL_ERROR "Invalid XCPLITE_CONFIGURATION='${XCPLITE_CONFIGURATION}'. " - "Must be one of: default, no_a2l, ptp, shm, rtos") + "Must be one of: default, no_a2l, ptp, shm, rtos, raw") endif() # Resolve configuration override header (empty = use default xcplib_cfg.h) @@ -43,6 +45,8 @@ elseif(XCPLITE_CONFIGURATION STREQUAL "shm") set(_xcplib_cfg_override "xcplib_shm_cfg.h") elseif(XCPLITE_CONFIGURATION STREQUAL "rtos") set(_xcplib_cfg_override "xcplib_rtos_cfg.h") +elseif(XCPLITE_CONFIGURATION STREQUAL "raw") + set(_xcplib_cfg_override "xcplib_raw_cfg.h") else() set(_xcplib_cfg_override "") endif() @@ -176,7 +180,7 @@ elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC") endif() # xcplite sources -set(xcplite_SOURCES src/xcpappl.c src/xcplite.c src/xcpethserver.c src/xcpethtl.c src/queue32m.c src/queue32.c src/queue64v.c src/queue64f.c src/shm.c src/cal.c src/a2l.c src/a2l_writer.c src/persistence.c src/platform.c src/util.c ) +set(xcplite_SOURCES src/xcpappl.c src/xcplite.c src/xcpethserver.c src/xcpethtl.c src/queue32m.c src/queue32.c src/queue64v.c src/queue64f.c src/shm.c src/cal.c src/a2l.c src/a2l_writer.c src/persistence.c src/platform.c src/sockets.c src/socket_raw.c src/socket_raw_hal_linux.c src/util.c ) # Create xcplite library add_library(xcplite ${xcplite_SOURCES}) @@ -314,6 +318,19 @@ if(XCPLITE_BUILD_EXAMPLES) # Build it separately: cd examples/silkit_demo && cmake -B build -DSilKit_DIR=... -Dxcplite_DIR=... message(STATUS "silkit_demo: not built from root (standalone project, see examples/silkit_demo/)") + elseif(XCPLITE_CONFIGURATION STREQUAL "raw") + + # udp_raw_demo: XCP over the raw Ethernet transport (UDP/IPv4 inside xcplib). + # Linux only - the HAL backend uses AF_PACKET and needs CAP_NET_RAW. + # See docs/SOCKET_RAW.md for the network setup used to test it. + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_executable(udp_raw_demo examples/udp_raw_demo/src/main.c) + target_link_libraries(udp_raw_demo PRIVATE xcplite) + message(STATUS "udp_raw_demo: building for raw configuration (Linux)") + else() + message(STATUS "udp_raw_demo: skipped (Linux only, raw Ethernet HAL uses AF_PACKET)") + endif() + elseif(XCPLITE_CONFIGURATION STREQUAL "rtos") # freertos_demo: FreeRTOS POSIX simulator for testing FreeRTOS xcplite support on the host. @@ -373,6 +390,27 @@ if(XCPLITE_BUILD_TESTS) target_link_libraries(clock_test PRIVATE xcplite) message(STATUS "clock_test: building for ptp configuration") + elseif(XCPLITE_CONFIGURATION STREQUAL "raw") + + # Unit tests for the raw Ethernet transport: checksums, wire layout, frame build, + # receive filter, ARP and ICMP responders. No network and no HAL involved - + # src/socket_raw.c is compiled into the test with a fake Ethernet HAL (src/stubs.c), + # so this does not link against xcplite and needs no privileges. + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_executable(socket_raw_test test/socket_raw_test/src/main.c test/socket_raw_test/src/stubs.c) + target_include_directories(socket_raw_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/inc") + # This target deliberately does not link xcplite (it supplies its own fake + # Ethernet HAL), so it does not inherit the PUBLIC _GNU_SOURCE from that target + target_compile_definitions(socket_raw_test PRIVATE + "XCPLITE_CONFIGURATION=\"raw\"" + "XCPLIB_CFG_OVERRIDE=\"xcplib_raw_cfg.h\"" + $<$:_GNU_SOURCE> + ) + message(STATUS "socket_raw_test: building for raw configuration") + else() + message(STATUS "socket_raw_test: skipped (Linux only, raw Ethernet HAL uses AF_PACKET)") + endif() + else() message(STATUS "XCPLITE_BUILD_TESTS=ON: no test targets defined for configuration '${XCPLITE_CONFIGURATION}'") endif() @@ -483,6 +521,10 @@ install(DIRECTORY inc/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PA # Install xcplib_cfg.h (default configuration) and platform.h install(FILES src/xcplib_cfg.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(FILES src/platform.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES src/sockets.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +# Raw Ethernet HAL interface, needed by applications which provide their own backend +# (OPTION_UDP_RAW_HAL_EXTERNAL), e.g. an ASAM CMP implementation +install(FILES src/socket_raw_hal.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) # Install the active configuration override header (if not default) # This documents which configuration was compiled into the installed library. diff --git a/README.md b/README.md index 559d8a0a..cc9eaf9e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ For save operation, the client tool must respect fixed event definitions and han If a tool understands ABI details of complex data instances, it may still perform the usual address calculations to access individual fields of composite types, array elements, or merge memory ranges to optimize upload, download, and data acquisition. -General-purpose A2L editors/creators typically cannot reconstruct XCPlite-specific relative address encoding automatically. In this case, you are limited to use only global memory objects. For full featured offline A2L generation, you may use the XCPlite-aware xcpclient tool workflow described in the [technical documentation](docs/TECHNICAL.md). +General-purpose A2L editors/creators typically cannot reconstruct XCPlite-specific relative address encoding automatically. In this case, you are limited to use only global memory objects. For full featured offline A2L generation, use the XCPlite-aware xcpclient tool, see [Offline A2L Generation](docs/OFFLINE_A2L.md). Support for A2L TYPEDEF and shared axis references with `this.` is beneficial, but not strictly required. @@ -105,7 +105,8 @@ Details how to build for Linux, QNX, macOS, and Windows are in the [building doc - **[API Reference](docs/xcplib.md)** - XCP instrumentation API - **[Configuration](docs/xcplib_cfg.md)** - Configuration options - **[Examples](examples/README.md)** - Example applications and CANape setup -- **[Technical Details](docs/TECHNICAL.md)** - Addressing modes, A2L generation, instrumentation costs +- **[Technical Details](docs/TECHNICAL.md)** - Addressing modes, on-target A2L generation, instrumentation costs and markers +- **[Offline A2L Generation](docs/OFFLINE_A2L.md)** - A2L generation from the ELF file with xcpclient - **[Building](docs/BUILDING.md)** - Detailed build instructions - **[XCP Introduction](docs/XCP_INTRODUCTION.md)** - What is XCP? diff --git a/build.sh b/build.sh index 2019ebb1..64b5cc88 100755 --- a/build.sh +++ b/build.sh @@ -28,6 +28,7 @@ show_usage() { echo " ptp build-ptp/ Hardware socket timestamps for PTP (Linux, PTP-capable NIC)" echo " shm build-shm/ Shared-memory multi-application mode" echo " rtos build-rtos/ FreeRTOS POSIX simulator (Linux/macOS only)" + echo " raw build-raw/ Raw Ethernet transport, UDP/IPv4 inside xcplib (Linux only)" echo "" echo "Target (default: examples) — what to build within the configuration:" echo " lib Library only" @@ -64,6 +65,7 @@ show_usage() { echo " $0 shm tools # shm config: shmtool + xcpdaemon" echo " $0 ptp tools # ptp config: ptptool (Linux only)" echo " $0 no_a2l examples # no_a2l config: no_a2l_demo, no_a2l_demo_cpp" + echo " $0 raw examples # raw config: udp_raw_demo (Linux only)" echo " $0 rtos examples # rtos config: freertos_demo (Linux/macOS only)" echo " $0 lib install # Library only, install to build/install" echo " $0 release lib install=/usr/local # Release build, install to /usr/local" @@ -98,7 +100,7 @@ for arg in "$@"; do relwithdebinfo) BUILD_TYPE="RelWithDebInfo" ;; # Configuration - default|no_a2l|ptp|shm|rtos) CONFIGURATION="$arg_lower" ;; + default|no_a2l|ptp|shm|rtos|raw) CONFIGURATION="$arg_lower" ;; # Target (multiple targets may be combined, e.g. "tools examples") lib) _ANY_TARGET=true ;; # library only: all extras remain false @@ -113,7 +115,7 @@ for arg in "$@"; do clean) CLEAN_BUILD=true ;; cleanall) echo "Cleaning all build directories..." - rm -rf build build-no_a2l build-ptp build-shm build-rtos + rm -rf build build-no_a2l build-ptp build-shm build-rtos build-raw # Also clean legacy directory names from older builds rm -rf build_no_a2l_demo build_freertos build_ptptool build_shm rm -f ./*.bin ./*.hex ./*.log ./*.mf4 ./*.a2l @@ -262,10 +264,12 @@ if [[ "$RUN_CLANG_TIDY" == true && "$BUILD_SUCCESS" == true ]]; then if [[ ! -f "$BUILD_DIR/compile_commands.json" ]]; then cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -S . -B "$BUILD_DIR" fi + # Keep in sync with xcplite_SOURCES in CMakeLists.txt XCPLITE_SOURCES=( src/xcpappl.c src/xcplite.c src/xcpethserver.c src/xcpethtl.c - src/queue32.c src/queue64v.c src/queue64f.c src/shm.c - src/cal.c src/a2l.c src/a2l_writer.c src/persistence.c src/platform.c src/util.c + src/queue32.c src/queue32m.c src/queue64v.c src/queue64f.c src/shm.c + src/cal.c src/a2l.c src/a2l_writer.c src/persistence.c src/platform.c + src/sockets.c src/socket_raw.c src/socket_raw_hal_linux.c src/util.c ) for src in "${XCPLITE_SOURCES[@]}"; do [[ -f "$SCRIPT_DIR/$src" ]] || { echo "Warning: not found: $src"; continue; } @@ -324,6 +328,13 @@ case "$CONFIGURATION" in echo " Tests : (none for rtos configuration)" echo " Tools : (none for rtos configuration)" ;; + raw) + [[ "$CMAKE_BUILD_EXAMPLES" == "ON" ]] && echo " Examples : udp_raw_demo (Linux only, needs CAP_NET_RAW)" \ + || echo " Examples : (not built)" + [[ "$CMAKE_BUILD_TESTS" == "ON" ]] && echo " Tests : socket_raw_test" \ + || echo " Tests : (not built)" + echo " Tools : (none for raw configuration)" + ;; esac [[ "$CMAKE_BUILD_RUST" == "ON" ]] && echo " Rust tools : xcpclient, bintool" \ diff --git a/docs/BUILDING.md b/docs/BUILDING.md index add8893a..5bc0d2e6 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -19,6 +19,7 @@ The library has **mutually exclusive build configurations** selected via `XCPLIT | `ptp` | `build-ptp/` | `xcplib_ptp_cfg.h` | Like default with socket hardware timestamps; requires Linux and a PTP-capable NIC | | `shm` | `build-shm/` | `xcplib_shm_cfg.h` | Shared-memory multi-application mode (shmtool, xcpdaemon) | | `rtos` | `build-rtos/` | `xcplib_rtos_cfg.h` | FreeRTOS embedded targets: reduced footprint, no filesystem, 32-bit | +| `raw` | `build-raw/` | `xcplib_raw_cfg.h` | Raw Ethernet transport: UDP/IPv4 inside xcplib, no TCP/IP stack (Linux only) | Each `src/xcplib__cfg.h` header documents the exact overrides applied on top of the defaults in `src/xcplib_cfg.h`. @@ -45,6 +46,7 @@ Within a chosen configuration, the following options control what gets built: | `ptp` | ptp4l_demo¹ | clock_test | ptptool¹ | | `shm` | hello_xcp (SHM), hello_xcp_cpp (SHM) | *(none)* | shmtool, xcpdaemon³ | | `rtos` | freertos_emu_demo³ (downloads FreeRTOS-Kernel) | *(none)* | *(none)* | +| `raw` | udp_raw_demo (Linux only) | socket_raw_test | *(none)* | ¹ Linux only ² requires libbpf ³ not supported on Windows @@ -71,7 +73,7 @@ These examples have their own `CMakeLists.txt` and use `find_package(xcplite)` a | Argument group | Values | Default | |----------------|--------|---------| | Build type | `debug` \| `release` \| `relwithdebinfo` | `debug` | -| Configuration | `default` \| `no_a2l` \| `ptp` \| `shm` \| `rtos` | `default` | +| Configuration | `default` \| `no_a2l` \| `ptp` \| `shm` \| `rtos` \| `raw` | `default` | | Target | `lib` \| `examples` \| `tests` \| `tools` \| `rust_tools` \| `all` | `examples` | | Options | `clean` `cleanall` `install` `install=` `cargo_install` `tidy` | — | @@ -111,6 +113,9 @@ Examples: # rtos config: freertos_demo (Linux/macOS only) ./build.sh rtos examples +# raw config: udp_raw_demo, raw Ethernet transport (Linux only, needs CAP_NET_RAW) +./build.sh raw examples + # Library only, install to build/install ./build.sh lib install @@ -173,6 +178,10 @@ cmake --build build-shm --parallel cmake -B build-rtos -S . -DXCPLITE_CONFIGURATION=rtos -DXCPLITE_BUILD_EXAMPLES=ON cmake --build build-rtos --parallel +# raw configuration — udp_raw_demo, raw Ethernet transport (Linux only) +cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw -DXCPLITE_BUILD_EXAMPLES=ON +cmake --build build-raw --parallel + # Build a specific target cmake --build build --target hello_xcp @@ -341,5 +350,6 @@ To test all configurations: ./build.sh ptp tools # ptp config, tools (Linux only) ./build.sh no_a2l examples # no_a2l config ./build.sh rtos examples # rtos config (Linux/macOS only) +./build.sh raw examples # raw config (Linux only, see docs/SOCKET_RAW.md) ``` diff --git a/docs/OFFLINE_A2L.md b/docs/OFFLINE_A2L.md new file mode 100644 index 00000000..1df03fbf --- /dev/null +++ b/docs/OFFLINE_A2L.md @@ -0,0 +1,288 @@ +# Offline A2L Generation with xcpclient + +XCPlite can generate the A2L file on the target at runtime ([on-target A2L generation](TECHNICAL.md#on-target-a2l-file-generation)), +or the A2L file is generated offline from the ELF file of the application by the ELF/DWARF to A2L generator built into the +`xcpclient` tool (`tools/xcpclient/`). Offline generation is used by the `no_a2l` (Linux,MacOS) and `rtos` (FreeRTOS) build configurations: the library is built without A2L generator and without file system dependency, which reduces code size on microcontroller and RTOS targets. + +The generator is specific to XCPlite. It knows the markers the XCPlite instrumentation macros leave in the ELF file and the relative +addressing modes of XCPlite, so it creates a complete A2L file for measurement variables on the stack, calibration parameters in +segments and complex types, which a general purpose A2L creator can not reconstruct from the debug information alone. + +See `examples/no_a2l_demo`, `examples/no_a2l_demo_cpp` and `examples/freertos_demo` for complete examples with build scripts. + +## Concept + +The instrumentation macros place information in the ELF file at compile and link time. The library and the generator use it: + +| Source in the ELF file | Written by | Used for | +|---|---|---| +| `xcp_evts` section | `DaqCreateEvent`, `DaqCreateEventExt`, `DaqCreateAndTriggerEvent` | All events with name, cycle time and priority. The position of the descriptor in the section is the event id, on the target and in the A2L file | +| `xcp_cals` section | `CalSegDecl`, `CalSegDeclRef`, `CalSegCreate` | All calibration segments with the address and the size of their default page. The order of the descriptors is the segment number | +| `xcp_epk` section | `XcpCreateEpk` | The EPK software version string and its address | +| `xcp_meta` section | `XCP_UNIT`, `XCP_LIMITS`, `XCP_COMMENT`, `XCP_READ_WRITE` | Metadata of measurement and calibration objects | +| DWARF scope of the `trg____` anchor variables | `DaqTriggerEvent`, `DaqCreateAndTriggerEvent`, `DaqTriggerEventExt`, `DaqEventVar` | The function in which an event is triggered, its stack frame and the addressing modes available at the trigger point | +| `XCPLITE__` variable | libxcplite | The addressing scheme of the target (`CASDD`, `ACSDD`, `AXSDD`, `CXSDD`), see [addressing modes](TECHNICAL.md#addressing-modes) | +| DWARF type of the `cap__` capture structs | `DaqTriggerEventCapture`, `DaqCreateAndTriggerEventCapture` | The captured local variables of an event trigger, with their names, types and offsets in the capture struct | +| DWARF variables and types, ELF symbol table | Compiler and linker | Names, addresses, stack frame offsets and types of all global, static and local variables | + +The macro expansions and the naming conventions of the markers are the contract between the library and the generator, they are +described in [TECHNICAL.md — Instrumentation Markers for Offline A2L Tools](TECHNICAL.md#instrumentation-markers-for-offline-a2l-tools). + +The same link time information is used by the library itself: `XcpInit` registers the events and calibration segments from the +sections in a deterministic order, so the event ids and segment numbers in the A2L file stay valid independent of the code execution +order, without a persistence file. + +## Workflow + +1. Build the application with debug information: `-g`, `CMAKE_BUILD_TYPE=Debug` or `RelWithDebInfo`. Optimized builds work, see the + rules for the application code below. The generator reads ELF files: build on Linux or QNX, or for an embedded ELF target. + macOS is not supported: the macOS linker does not put the DWARF debug information into the executable (Mach-O), it stays in the + object files and in the `.dSYM` bundle. xcpclient rejects Mach-O files with an error message. The `no_a2l` and `rtos` + configurations build and run on macOS, but the A2L file has to be generated from a Linux build of the same sources, as the + `create_a2l.sh` scripts of the examples do with a remote build on a Linux target. +2. Generate the A2L file from the ELF file, offline or with the running target: + +```bash +# Offline: events and segments from the ELF file only, transport layer parameters for the A2L IF_DATA from the command line +xcpclient --offline --udp --dest-addr 192.168.0.206 --elf build-no_a2l/no_a2l_demo --create-a2l --a2l no_a2l_demo.a2l + +# Online: events and segments are checked against the running target (GET_EVENT_INFO, GET_SEGMENT_INFO), the EPK is checked +xcpclient --udp --dest-addr 192.168.0.206 --elf build-no_a2l/no_a2l_demo --create-a2l --a2l no_a2l_demo.a2l + +# The ELF file may also be uploaded from the target (OPTION_ENABLE_ELF_UPLOAD) +xcpclient --udp --dest-addr 192.168.0.206 --upload-elf --elf no_a2l_demo.elf --create-a2l --a2l no_a2l_demo.a2l + +# Restrict the variables to compilation units and names (regular expressions) +xcpclient --offline --elf build-no_a2l/no_a2l_demo --create-a2l --a2l no_a2l_demo.a2l --elf-unit-filter main --elf-var-filter "^(counter|params)" + +# Only annotated variables (XCP_UNIT, XCP_LIMITS, XCP_COMMENT) +xcpclient --offline --elf build-no_a2l/no_a2l_demo --create-a2l --a2l no_a2l_demo.a2l --elf-skip-no-metadata + +# A2L skeleton with events, segments and IF_DATA only, to be completed with other tools +xcpclient --offline --elf build-no_a2l/no_a2l_demo --create-a2l-template --a2l no_a2l_demo_template.a2l +``` + +3. Use the A2L file in the XCP tool. The xcpclient test client itself can work with the ELF file directly (`--elf` with `--mea`, + `--cal`, `--list-mea`), no A2L file is needed for it. Variables without a fixed event get the event given by `--default-event`, + in the generated A2L file and for the measurement with xcpclient. + +`examples/no_a2l_demo_cpp/create_a2l.sh` shows a complete round trip: sync the sources to the target, build there, download the ELF +file and generate the A2L file. The command line reference is in [tools/xcpclient/README.md](../tools/xcpclient/README.md). + +### Calibration segment addressing + +The addressing scheme of the target is read from the `XCPLITE__` variable of the XCPlite library and written as `PROJECT_NO` +into the A2L header. The variable is an exported global, so it is read from the symbol table and found even when the debug information of +the library is not parsed (`--elf-unit-limit`) or the library was built without `-g`: + +- `XCPLITE__ACSDD` (`OPTION_CAL_SEGMENTS_ABS` defined): calibration parameters are addressed by the absolute address of their default + page with address extension 0. This is the usual choice for microcontrollers. The default pages of all segments must be in the 32 bit + address range of the target and have static lifetime. +- `XCPLITE__CASDD` (default): calibration parameters are addressed by segment number and offset with address extension 0, the default + pages may be anywhere in a 64 bit address space. The segment numbers are read from the target when connected, or derived from the order + of the descriptors in the `xcp_cals` section. + +## Rules for the application code + +- Use the instrumentation macros (`DaqCreateEvent`, `DaqTriggerEvent`, `CalSegDecl`, `XcpCreateEpk`, ...), not the C API functions + (`XcpCreateEvent`, `XcpCreateCalSeg`). Only the macros emit the sections and the anchor variables. +- Mark local measurement variables `volatile` (or use the `XCP_MEA` attribute). Otherwise an optimizing compiler might keep them in registers, the DWARF entry has no location and the variable is skipped. +- Declare calibration segments with `CalSegDecl` or `CalSegDeclRef` and give the default page static lifetime. File scope is + recommended. The segment name and the name of the default page variable are identical by convention, the generator relies on it. +- Metadata macros name the object with `__` as path separator: `XCP_UNIT(params__delay_us, "us")` annotates the field `delay_us` of the + instance `params`. A macro placed in the same namespace as the variable, or in the same function as a local variable, does not need a + scope prefix: `XCP_COMMENT(input, ...)` in namespace `motor_control` annotates `motor_control.input`, `XCP_COMMENT(counter, ...)` in + function `foo` annotates `foo.counter`. Explicit prefixes (`foo__counter`) are possible anyway. + +## What the generator derives from the ELF file + +This section describes how `xcpclient --create-a2l` discovers events, calibration segments, variables and types, for contributors +and for developers who need to understand why a variable does or does not appear in the generated A2L file. + +### Events + +Every descriptor in the `xcp_evts` section is an event. The event id is the position of the descriptor in the section, which is also how +`XcpInit` assigns the ids on the target. If the same event is created at several places (a `DaqCreateEvent` in several functions or +compilation units), the descriptor with the lowest address wins, on the target and in the generator. Without an `xcp_evts` section (a +linker script may merge it into another output section) the linker symbols `__start_xcp_evts` and `__stop_xcp_evts` are used. Without +both, the events get placeholder ids, which are corrected from the event information of the target when connected. + +### Calibration segments + +Every descriptor in the `xcp_cals` section is a calibration segment or block. The default page variable of a segment has the same name +as the segment, its DWARF type gives the size and the layout: the parameters become a `TYPEDEF_STRUCTURE` with an `INSTANCE`, or +`CHARACTERISTIC` objects for basic types. Variables whose address lies within a segment are calibration parameters, all other variables +are measurements. + +### Trigger points and local variables + +The trigger macros emit a static variable `trg____` in the function in which the event is triggered. Its DWARF scope gives +the function and the addressing modes available there (the mode letters, see the marker contract). Local variables of that function are +registered with stack frame relative addresses (address extension 2) and the event as fixed event, static variables in the function get +the event as well. The DWARF locations of local variables are relative to the frame base of the function (`DW_AT_frame_base`), and the +trigger macros pass exactly this frame base to the target (`xcp_get_frame_addr()` in `inc/xcplib.h`): the canonical frame address +(`__builtin_dwarf_cfa()`) for GCC, which describes the locals relative to the CFA, and the frame pointer (`__builtin_frame_address(0)`) +for clang, which describes them relative to the frame pointer register. The generator checks the frame base of the function of every +trigger and uses the offsets from the DWARF as they are. A function whose frame base is something else, for example the stack pointer of +a function without frame pointer under clang, gets a warning and its stack frame relative variables are not registered. clang describes +a local variable relative to the stack pointer when that is closer to the variable than the frame pointer, such variables are not +measurable, they are reported at debug level. + +### Captured local variables + +A local variable which the compiler keeps in a register has no address and can not be measured. Marking it `volatile` gives it a memory +location for its whole lifetime. The alternative is to capture it: `DaqTriggerEventCapture(event, counter, ratio)` declares a struct +`cap__` in the function, copies the given variables into it and passes the address of the struct to the target as the base address +of address extension 3. The originals stay in their registers, the copy of a scalar is a single store instruction. + +The generator takes the DWARF type of `cap__`, which is a struct with one member per captured variable, and registers every member +as a measurement named `.`, with the event of the trigger as fixed event, address extension 3 and the offset of the +member in the struct as address. The member is named like the variable with one trailing underscore, which the generator removes again: +a member may not be named like the variable used in its own type expression, C++ forbids it. The measurements look exactly like the stack frame relative ones, the metadata markers of the captured +variables work unchanged. A variable which is captured is not registered a second time as a stack frame relative variable. + +The macros work in C and in C++. In C++ a reference variable is captured as the object it refers to, and the captured objects must be +trivially copyable, since they are copied byte wise. A `const` variable can only be captured in C, in C++ a const member would leave the +capture struct without a default constructor. A bitfield member can not be captured in either language. + +Captured variables do not depend on the stack frame of their function, so a function which only captures may be inlined. Asynchronous +access (polling) works like for any other event based relative address, the pending command is executed in the next trigger of the event, +while the capture struct is alive. One capture per event: if the same event is triggered with a capture in several functions, the first +one is used and the others are reported. + +A function with an event trigger must not be inlined. An inlined function has a copy at each call site and possibly an out of line copy, +each with its own stack frame layout, and the event may be triggered from any of them, so there is no stack frame relative address which +is valid for all copies. xcpclient warns when the trigger of an event is found in an inlined function (an abstract instance with +`DW_AT_inline`, an inlined copy `DW_TAG_inlined_subroutine` or an out of line copy referring to the abstract instance) and does not +register the stack frame relative variables of the function, its static variables keep the function scope and the event. Mark such +functions `XCP_NOINLINE` (`inc/xcplib.h`). GCC does not inline external functions at `-O1`, clang inlines a function which is called once +already at `-O1`. Global variables and static variables +in functions without an event trigger are registered without a fixed event, in this case it is in the responsibility of the XCP tool user to assign an event which allows correct visibilty and consistent capture of the associated variables. CANape usually defaults to polling in this case, and each available event may be selected for synchronous data acquisition. +With `--default-event `, xcpclient assigns this event to such variables when it creates the A2L file (`DAQ_EVENT VARIABLE` with a +`DEFAULT_EVENT_LIST`), and measures them with it. The event is given by its id or by its name (a C identifier, e.g. `--default-event mainloop`), +a name is looked up in the event list of the ELF file (and of the XCP server when connected), xcpclient aborts when it is not found. + +### Variables and symbols + +- The address of a variable comes from its `DW_AT_location`. Variables without a location (declarations, `static const` data in a + namespace, the metadata markers in optimized builds) are resolved from the ELF symbol table: by `DW_AT_linkage_name`, by name, by the + Itanium mangled name of a namespace scope variable (`_ZN13motor_controlL5inputE`) or by a unique name suffix (`_ZZ4mainE7counter` for a + static local). For variables inside a function only symbols with local binding are considered, a global symbol with the same name + belongs to a different variable. +- GCC describes a namespace scope variable with a declaration entry inside the namespace and a definition entry at compilation unit level + (`DW_AT_specification`), both are merged into one variable. +- Variables with the same name get distinct A2L names: static variables in functions are prefixed with the function (`foo.counter`), + global variables defined in several namespaces with their namespace (`motor_control.input`). + +### Type names + +The `DW_AT_name` of a `DW_TAG_structure_type` or `DW_TAG_class_type` entry is the unqualified type name (`Input` for +`motor_control::Input`). The enclosing namespace, class or function is only visible from the position of the entry in the DWARF tree: +it is a child of the `DW_TAG_namespace`, `DW_TAG_class_type` or `DW_TAG_subprogram` entry. Type entries have no `DW_AT_linkage_name`, +only variables and functions carry a mangled name. Every compilation unit which uses a type has its own copy of the type entry. + +A2L has one flat name space for `TYPEDEF_STRUCTURE`, so the generator records the enclosing scopes of the type entries while traversing +the tree and names the typedefs as follows: + +- The typedef is named after the type. If struct/class types with the same name exist in different scopes, all of them are qualified + with their scope (`motor_control.Input`, `valve_control.Input`, `MotorController.Params`), types with a unique name keep their plain name. + Type names which are not valid A2L identifiers (template instantiations such as `TplStruct`) are sanitized (`TplStruct_float_`). +- Typedefs with identical content are merged: the same type used by several variables, or the copies of a type from several compilation units. +- A name which is still used by a typedef with different content (types without a scope in different C files, or a type used for + measurement and for calibration variables) simply get a numeric suffix (`state_1`), which is reported as a warning. +- The `TYPEDEF_MEASUREMENT` or `TYPEDEF_CHARACTERISTIC` of a struct field is named after the field. If another structure has a field + with the same name but a different type or metadata, the name is qualified with the sanitized structure name (`TplStruct_float_.value`). + +### Metadata + +Each metadata macro emits a constant named `xcp_meta____` into the `xcp_meta` section, with `` one of `unit`, `min`, +`max`, `comment` or `read_write`. After the variables are registered, the constants are matched to their A2L objects: the name is looked +up qualified with the scope of the marker first (its namespace, or its function for a local variable), then unqualified. `__` in the name +is the path separator for the fields of typedef instances (`params__delay_us`, `motor_control__input__speed`). + +A marker in a function annotates a variable of this function, written plain (`XCP_COMMENT(counter, ...)` in `foo` annotates `foo.counter`) +or with the scope prefix (`XCP_COMMENT(foo__counter, ...)`). If the function has no variable of that name, the marker annotates the object +of that name outside the function, for example a global variable used there. A marker at file scope annotates the global variable and +never a local variable of the same name in a function, it reaches a typedef field only when no object has its plain name. + +GCC gives the `static const` marker constants inside a function no `DW_AT_location` and names their symbols `.`, so their +addresses come from the symbol table. Markers with the same name in several functions are told apart by their size, which differs as soon +as the annotation strings differ. Markers of the same name, function and size cannot be told apart, xcpclient warns and asks for the scope +prefixed form. Metadata never adds +objects, it only annotates variables which were registered from the sources above. With `--elf-skip-no-metadata` every variable without +any annotation is removed from the A2L file, a convenient way to publish only explicitly curated signals. + +## Supported types and limitations + +The DWARF type information is mapped to A2L objects as follows: + +| C/C++ type | A2L representation | +|---|---| +| `bool`, integer and floating point types | `MEASUREMENT` or `CHARACTERISTIC` of the matching A2L data type | +| `enum` | integer of the enum's size; for variables the enumerators become a verbal conversion table, enum struct members are plain integers | +| one- and two-dimensional arrays | `MEASUREMENT` / `CHARACTERISTIC` with `MATRIX_DIM` (`VAL_BLK`, `CURVE`, `MAP`); arrays of structs become arrays of typedef instances | +| `struct`, `class`, template instantiations | `TYPEDEF_STRUCTURE` + `INSTANCE`; nested structs and classes become nested typedefs; private members are included; base class members are flattened into the derived type for all combinations of `struct`/`class` bases; `static`/`constexpr` members are skipped | +| pointers as struct or class members | the address value as unsigned integer of the target's pointer size, the pointee is not followed | + +Not supported, future extensions, cases skipped and reported as warnings (log level 2 and above): + +- Variables of pointer type (measure the pointed-to variable instead). +- Unions, bitfields and function pointers. A struct member of such a type is written as a one byte `UBYTE` placeholder so that the + remaining members of the structure keep their offsets. +- Arrays with more than two dimensions (written as a byte array placeholder) (@@@@ TODO verify this claim). +- C++ pointer-to-member types (`DW_TAG_ptr_to_member_type`): a struct or class containing one cannot be read at all, so it and every + class deriving from it end up without members. This is a limitation of the a2ltool DWARF reader this code is based on. +- C++ library containers (`std::vector`, `std::string`, smart pointers, ...) are read as the structs they are; the heap data behind them is not reachable (@@@@ TODO: Maybe add a blacklist feature to remove these). +- Variables addressed relative to a base pointer (`DaqTriggerEventExt`, the dynamic slots of `DaqEventVar`, address extension 3 and + above) are not generated yet, only absolute and stack frame relative addressing is. + (@@@@ TODO: Create a concept how to handle this) +- Thread local variables are not evaluated yet. + (@@@@ TODO: future feature ?) +- Local variable in functions without events are skipped + (@@@@ TODO: future feature ?, trigger if called by ?, with stack unwinding check) + +## Diagnostics + +The compilers which built the ELF file are logged as `Compiler: ...` (the `DW_AT_producer` of the compilation units), with their +version and the command line options which matter here, in particular the optimization level and the frame pointer. + +Messages worth knowing when a variable is missing or looks wrong in the A2L file: + +| Message | Meaning | +|---|---| +| `Struct/class type 'x' in unit has a different definition or object type than the existing typedef 'x', registered as typedef 'x_1'` | Two different types with the same name and no scope to qualify them with, or one type used for measurement and for calibration. Both get their own typedef. | +| `Local variable 'x' in function 'f' skipped, could not find event for dyn addressing mode` | The function contains no event trigger, so there is no stack frame anchor for its local variables. | +| `Variable 'x' not registered, no address` (log level 4) | The variable has no DWARF location, typically a local variable held in a register. Make it `volatile`. | +| `Global variable 'x' not registered, address ... out of the 32 bit XCP address range` | The variable is outside the addressable range, see the addressing modes. | +| `Metadata 'xcp_meta__...': no matching registry entry for '...'` | The annotated variable was not registered, or the name does not match. Check the scope prefix and the `__` path. | +| `Metadata variable '...' address is 0` | The marker has no DWARF location and no resolveable symbol. | +| `Event '...' is triggered with a capture in N functions, only the one in function ... is used` | The same event is triggered with `DaqTriggerEventCapture` in several functions, whose capture structs have different layouts. Use one event per capture. | +| `No target signature found in ELF file` | The `XCPLITE__` variable of the XCPlite library is missing, absolute addressing of calibration segments is assumed. A build with segment relative addressing (`CASDD`, `CXSDD`) then gets wrong calibration addresses. | +| `New event '...' found, created with undefined event id ...` | No `xcp_evts` section and no linker symbols. Connect to the target to get the ids. | +| `Calibration segment reference page variable 'x' has N usable definitions, expected 1` | The name of the default page variable is ambiguous, restrict the compilation units with `--elf-unit-filter`. | +| `EPK mismatch: A2L file '...' has EPK '...', target reports EPK '...'` | The A2L file does not belong to the running build. `--yes` overrides the check. | +| `'...' is a Mach-O (macOS) binary, macOS is not supported` | The application was built on macOS. Executables built on macOS contain no DWARF debug information, build on Linux or for an embedded ELF target. | +| `... does not contain DWARF2+ debug info. The section .debug_info is missing.` | The application was built without `-g`, or the debug information was stripped. | + +xcpclient exits with status 1 when the A2L file could not be created or any other error occurred, scripts can rely on the exit +status. The `create_a2l.sh` scripts of the examples check the exit status and the existence of the A2L file, and print the error +lines of the xcpclient log when the generation failed. + +## Other tools + +The A2L template generated with `--create-a2l-template` contains the IF_DATA, the EPK, the memory segments and the events, and can be +completed with other tools: + +- **Vector CANape A2L editor with ELF support**: create a new XCP on Ethernet device from the template, enable the ELF file in the + device configuration and add measurement and calibration objects in the A2L editor. For calibration segments, add the default page + structure as an `INSTANCE` of a `TYPEDEF_STRUCTURE` or the parameters as `CHARACTERISTIC` objects. +- **Vector A2L-Toolset A2L-Creator**: the examples contain metadata annotations as comments (`@@ SYMBOL`, `@@ STRUCTURE`, ...) for + the commercial A2L Creator. +- **a2ltool** (open source), for example to add the calibration segment `params` and the measurement `counter` to the template: + +```bash +a2ltool --update --measurement-regex "counter" --characteristic-regex "params" --elffile no_a2l_demo.elf --enable-structures --output no_a2l_demo.a2l +``` + +These tools reconstruct absolute addresses only. Stack frame relative and segment relative addressing needs the XCPlite specific +generator. diff --git a/docs/README.md b/docs/README.md index e8f50c73..14675f69 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,8 @@ This directory contains detailed documentation for XCPlite. - **[xcplib_cfg.md](xcplib_cfg.md)** - Configuration options ### Advanced Topics -- **[Technical Details](TECHNICAL.md)** - Instrumentation costs, A2L generation, addressing modes, EPK +- **[Technical Details](TECHNICAL.md)** - Instrumentation costs, on-target A2L generation, instrumentation markers, addressing modes, EPK +- **[Offline A2L Generation](OFFLINE_A2L.md)** - A2L generation from the ELF file with xcpclient: workflow, naming rules, supported types - **[Building](BUILDING.md)** - Detailed build instructions and troubleshooting - **[SHM](SHM.md)** - Shared memory transport layer details and usage - **[RCU](CAL_RCU.md)** - Read-Copy-Update (RCU) mechanism details and usage diff --git a/docs/SOCKET_RAW.md b/docs/SOCKET_RAW.md new file mode 100644 index 00000000..873c7a03 --- /dev/null +++ b/docs/SOCKET_RAW.md @@ -0,0 +1,477 @@ +# OPTION_ENABLE_UDP_RAW — Raw-Ethernet XCP/UDP Transport + +## Motivation + +XCPlite supports XCP-on-Ethernet through the OS socket API (`OPTION_ENABLE_UDP` / +`OPTION_ENABLE_TCP`). Some embedded targets have no TCP/IP stack at all and expose +only a raw Ethernet send/receive interface — a direct EMAC driver, or an RTOS +Ethernet abstraction without lwIP. This build variant implements the XCP UDP/IPv4 +transport entirely inside XCPlite, directly on top of raw Ethernet frames. + +Two further backends motivate the HAL abstraction: + +- **Vector XLAPI** (Windows) — raw frame access on Vector VN interface hardware +- **ASAM CMP** (Capture Module Protocol) — for testing XCP tools which communicate + through capture modules + +--- + +## Compilation guard + +`OPTION_ENABLE_UDP_RAW` is **mutually exclusive** with `OPTION_ENABLE_UDP` and +`OPTION_ENABLE_TCP`, and **requires `OPTION_QUEUE_32`**. Both are enforced by +`#error` in `src/xcptl_cfg.h`: + +| Guard | Reason | +|---|---| +| not with `OPTION_ENABLE_UDP` / `OPTION_ENABLE_TCP` | exactly one transport per build | +| requires `OPTION_QUEUE_32` | the 64 bit queues transmit with `socketSendToV` (scatter-gather), which the raw transport does not implement. Without this guard a 64 bit build fails at link time with no hint about the cause | +| not with `OPTION_SHM_MODE` | SHM needs `queueInitFromMemory`, which exists only in `queue64v.c` / `queue64f.c` | +| not with `XCPTL_ENABLE_MULTICAST` | `socketJoin` is not provided | + +### MTU and frame size + +There is deliberately **no compile-time MTU guard**. The link MTU is a runtime property that only +the target knows, so hard-coding a limit would bake a "standard Ethernet" assumption into +`xcptl_cfg.h` and would wrongly forbid a jumbo-capable link. + +Note what `OPTION_MTU` means: it is the link MTU, and the 14 byte **Ethernet header is not part of +it**. `XCPTL_MAX_SEGMENT_SIZE = (OPTION_MTU - 28) & ~7` reserves 28 bytes for the IPv4 and UDP +headers and then aligns down as the transport layer requires, so the resulting IP packet is at most +`OPTION_MTU` bytes, and exactly `OPTION_MTU` when `OPTION_MTU - 28` is already a multiple of 8: +1500 gives a 1472 byte segment and a 1500 byte IP packet. The invariant is `OPTION_MTU <= link MTU`. + +Before V2.1.11 `OPTION_MTU` was the link MTU rounded *up* to a multiple of 8 (1504 for a 1500 byte +link), with the invariant `OPTION_MTU <= link MTU + 4`. A configuration still carrying 1504 is a +leftover of that convention: it works, but gains nothing over 1500. + +An `OPTION_MTU` too large for the link is reported at runtime, because neither of these +**fragments IPv4**: + +| Transport | Mechanism | +|---|---| +| socket (UDP) on Linux, macOS/BSD, QNX, Windows | `socketOpen` sets DF (`IP_PMTUDISC_DO` / `IP_DONTFRAG` / `IP_DONTFRAGMENT`), so `sendto` fails with `EMSGSIZE` | +| raw Ethernet | `eth_hal_send` returns `ETH_HAL_ERROR_SIZE`, mapped to `SOCKET_ERROR_MSGSIZE` | + +Both print the segment size and the `OPTION_MTU` to reduce; the raw HAL additionally names the +interface and its MTU, since only the backend knows that. Observed on a link forced to MTU 1000: + +``` +ERROR: eth_hal_send: frame of 1242 bytes is too large for interface veth1 (MTU 1000, so at most 1014 bytes per frame) +ERROR: socketSendTo: segment of 1200 bytes does not fit into one Ethernet frame on this link. + Reduce OPTION_MTU (currently 1420, giving XCPTL_MAX_SEGMENT_SIZE=1392), see the interface MTU reported above. +``` + +**lwIP does not refuse an oversized datagram.** The FreeRTOS/lwIP `socketOpen` is a separate +implementation in `sockets.c` and sets no DF option, because lwIP has no `IP_DONTFRAG`, so the +datagram is fragmented or dropped according to lwIP's own `IP_FRAG` build setting rather than +failing. To keep the misconfiguration visible, `socketSendTo` compares the segment plus 28 bytes of +IPv4/UDP headers against `netif_default->mtu` and warns once - it still sends, so this is a +diagnostic and not a guard. Two caveats: `netif_default` is not necessarily the interface routing +to the destination on a multi-homed target, so a false report is possible there, and the check +costs one comparison per datagram on the DAQ transmit path. On lwIP, `OPTION_MTU` has to be correct +by construction. + +The transport also asserts a **little endian host** (`src/socket_raw.c`) and the +availability of a **HAL backend** (`src/socket_raw_hal.h`). Only the Linux AF_PACKET +backend exists today, so a macOS or Windows build of the `raw` configuration stops +with a clear message rather than an obscure link error. + +--- + +## Build configuration + +RAW has its own configuration, `XCPLITE_CONFIGURATION=raw` → `src/xcplib_raw_cfg.h`, +building into `build-raw/`: + +```bash +./build.sh raw examples # library + udp_raw_demo (Linux only) +./build.sh raw tests # library + socket_raw_test (Linux only) +``` + +It is deliberately **not** an override inside the `rtos` configuration: `rtos` targets +use the lwIP socket API (`OPTION_FREERTOS_LWIP`) or host sockets in the POSIX +simulator, and both must keep working. An embedded target without an IP stack defines +`OPTION_ENABLE_UDP_RAW` in its own external build (PlatformIO, CubeMX) and supplies its +own HAL backend. + +Options in `src/xcplib_raw_cfg.h`: + +| Option | Default | Purpose | +|---|---|---| +| `OPTION_UDP_RAW_IFNAME` | `"eth0"` | default interface, overridden by `socketRawSetInterface()` | +| `OPTION_UDP_RAW_ENABLE_ICMP_ECHO` | on | answer ping — the single most useful bring-up aid | +| `OPTION_UDP_RAW_UDP_CHECKSUM_ZERO` | on | transmit UDP checksum 0, legal for IPv4 (RFC 768) | +| `OPTION_UDP_RAW_UDP_CHECKSUM_COMPUTE` | off | RFC 768 software checksum | +| `OPTION_UDP_RAW_UDP_CHECKSUM_HW` | off | leave 0, the EMAC inserts it | +| `OPTION_UDP_RAW_VERIFY_RX_CHECKSUM` | on | verify received IPv4 header checksums | +| `OPTION_UDP_RAW_GRATUITOUS_ARP` | off | announce our IP/MAC on bind | +| `OPTION_UDP_RAW_ZERO_COPY` | on | reserve header space in the queue, avoid copy payload | + +Note on the zero UDP checksum default: `tcpdump` and Wireshark can then not validate the +UDP framing. Switch to `_COMPUTE` temporarily while bringing up a new target. + +--- + +## API subset + +Only these functions from `sockets.h` exist in a RAW build; the rest are removed from +the header, so reaching for one is a compile error rather than a link error: + +| Function | Notes | +|---|---| +| `socketStartup` / `socketCleanup` | initialize / release the transport | +| `socketGetErrorString` / `socketGetLastError` | self contained error codes, no `` | +| `socketOpen` | opens the Ethernet HAL, reads the local MAC. UDP only, rejects `SOCKET_MODE_TCP` | +| `socketBind` | stores the local IP and UDP port. Rejects `0.0.0.0` | +| `socketRecvFrom` | receives one UDP datagram, answers ARP and ICMP on the way | +| `socketSendTo` | builds Ethernet/IPv4/UDP headers and transmits | +| `socketSetTimeout` | receive timeout, RX only | +| `socketShutdown` / `socketClose` | unblock a receive / release the HAL | +| `socketRawSetInterface` | RAW only: select the interface before `XcpEthServerInit()` | +| `socketRawGetLocalMac` | RAW only: local MAC, used for the A2L `IF_DATA` | + +Not provided: TCP (`socketListen`/`socketAccept`/`socketRecv`/`socketSend`), multicast +(`socketJoin`), scatter-gather (`socketSendToV`/`socketSendV`), hardware timestamps, +`socketGetMAC`, `socketGetLocalAddr`. + +--- + +## Architecture + +``` +xcpethtl.c / xcpethserver.c + │ sockets.h API (subset above) + ▼ + socket_raw.c + ├── UDP/IPv4 layer (header build and parse, checksums) + ├── ARP (answer requests for our IP) + ├── ICMP (answer Echo Requests) + └── receive filter + deadline loop + │ socket_raw_hal.h + ▼ + ┌──────────────────────────────────────────────────┐ + │ socket_raw_hal_linux.c AF_PACKET (implemented) │ + │ socket_raw_hal_xlapi.c Vector XLAPI (future) │ + │ socket_raw_hal_cmp.c ASAM CMP (future) │ + └──────────────────────────────────────────────────┘ +``` + +### Local address + +There is no IP stack and no DHCP, so `socketBind(0.0.0.0)` has no meaning. The +**application supplies the IPv4 address** through the `address` parameter of +`XcpEthServerInit()`, which already exists; `socketBind` rejects all-zero, broadcast, +multicast and loopback addresses with an explanatory error. The **MAC comes from the +HAL** (`eth_hal_get_mac`) — every EMAC and every AF_PACKET interface knows its own. + +Both values are also stored in `gXcpTl.server_addr` / `server_mac`, so `XcpEthTlGetInfo` +and the A2L `IF_DATA` report the real address without needing +`OPTION_ENABLE_GET_LOCAL_ADDR`. + +### ARP: answer only + +XCP is always master initiated — the tool sends `CONNECT` first — so the peer MAC, IP +and UDP port all arrive with that first frame. Consequently: + +1. **ARP Requests for our IP are answered.** This is mandatory: the IP stack of the XCP + client resolves us before it can send anything. +2. **The peer is learned from the accepted UDP datagram**, never from ARP. An unrelated + host asking for our IP must not be able to redirect the DAQ stream. +3. **No ARP Requests are ever sent**, and ARP Replies are ignored. +4. A gratuitous ARP announcement on bind is available but off by default. + +A consequence worth knowing: because the peer MAC is learned rather than resolved, +**no netmask and no default gateway are needed**. With a client behind a router, the +router MAC arrives as the frame source and the responses go back to it. + +### ICMP Echo + +Answering ping is enabled by default. It is the highest value bring-up milestone: a +successful `ping ` proves the Ethernet HAL, the MAC filter, the ARP responder, +the IPv4 header build and the header checksum all work, before any XCP tooling is +involved. + +### Receive: filter and deadline loop + +A raw socket sees every frame on the wire. `socketRecvFrom` therefore **loops +internally against an absolute deadline** computed once on entry, rather than returning +0 for every foreign frame — otherwise the caller would run its full background task +suite once per foreign frame, and that cadence would depend on link load instead of on +`XCPTL_RECV_TIMEOUT_MS`. + +Filter order, cheapest and most discriminating first: + +1. EtherType — ARP goes to the responder, VLAN (`0x8100`) is dropped with a warning so a + trunk port is diagnosable, anything else is dropped +2. destination MAC — ours or broadcast +3. IPv4 sanity, **fragments rejected with a warning** (there is no reassembly), + destination IP, optional header checksum verification +4. protocol — ICMP goes to the responder +5. destination UDP port — where almost every remaining frame on a busy link dies +6. payload size — **dropped, never truncated**: a truncated message would surface as a + confusing "Corrupt message received!" from the transport layer + +Return contract, identical to `sockets.c`: `> 0` bytes, `== 0` timeout (the caller does +background work and loops), `< 0` closed or error (the caller exits its loop). +`socketShutdown` sets a flag and wakes a blocked receive through the HAL. + +### Transmit serialization + +ARP and ICMP replies are generated in the **receive** thread, while command responses +and DAQ segments are transmitted from their own paths, so `eth_hal_send` is called from +two threads. `struct socket_raw` therefore owns a `tx_mutex` which covers header +construction **and** the HAL call, keeping the HAL contract simple: `eth_hal_send` does +not need to be reentrant. + +This mutex is deliberately **independent of `gXcpTl.ctr_mutex`**. That one happens to +serialize transmissions today, but it exists for a different reason — XCP requires the +message counter to increase monotonically across command responses and DAQ messages — +and removing it is a future goal. Nothing in `socket_raw.c` depends on it. + +--- + +## Raw Ethernet HAL + +`src/socket_raw_hal.h` — a port has to provide send and receive of complete Ethernet +frames plus the local MAC, nothing else: + +```c +bool eth_hal_open(const char *config, tEthHalCtx **ctx); +void eth_hal_close(tEthHalCtx *ctx); +bool eth_hal_get_mac(tEthHalCtx *ctx, uint8_t *mac); +int16_t eth_hal_send(tEthHalCtx *ctx, const uint8_t *frame, uint16_t len); +int16_t eth_hal_recv(tEthHalCtx *ctx, uint8_t *frame, uint16_t max_len, uint32_t timeout_ms); +void eth_hal_wakeup(tEthHalCtx *ctx); // optional, may be a no-op +``` + +`config` is backend specific and opaque to `socket_raw.c`: the interface name on Linux, +an application/channel selector for XLAPI, a device and stream id for CMP. + +**There is no per-frame channel parameter.** Every foreseeable backend would pass a +constant, the backends do not share a channel type (XLAPI channel index vs. CMP device +id + stream id + interface id vs. nothing at all for AF_PACKET), and a CMP concept must +not leak into the core. Backend identity is configuration, not a per-frame value. + +Frames are complete Ethernet frames **without FCS**. Frames as short as 50 bytes are +passed; a port whose MAC does not pad to the 60 byte Ethernet minimum must do it itself. + +### Linux backend (`socket_raw_hal_linux.c`) + +`AF_PACKET`/`SOCK_RAW`/`ETH_P_ALL` bound to one interface. Needs `CAP_NET_RAW`: + +```bash +sudo setcap cap_net_raw+ep ./build-raw/udp_raw_demo +``` + +Two details that matter: `PACKET_IGNORE_OUTGOING` (plus a `PACKET_OUTGOING` check as the +portable fallback) stops our own transmitted frames from coming straight back into the +receive path; and a blocked receive is unblocked with an `eventfd` and `poll()` rather +than `SO_RCVTIMEO`, so shutdown is immediate and an infinite timeout stays interruptible. + +### Providing a backend out of tree (`OPTION_UDP_RAW_HAL_EXTERNAL`) + +A backend does not have to live in this repository. Define `OPTION_UDP_RAW_HAL_EXTERNAL` in the +configuration override header and xcplib selects **no** backend: the `eth_hal_*` symbols stay +undefined in `libxcplite`, and the application links its own implementation against it. This is the +route for backends which do not belong in the library — ASAM CMP for testing XCP tools through +capture modules, or a vendor specific interface. + +Three things make it work: + +- `socket_raw_hal.h` is installed with the library, so the out of tree backend can implement the + interface without a source checkout +- `socket_raw_hal_linux.c` is excluded when the option is set, so the built in AF_PACKET backend + does not collide with the supplied one +- `libxcplite` is a **static** library, so the undefined `eth_hal_*` symbols resolve at application + link time with no indirection in the transmit path + +The option also lifts the "Linux only" restriction: with an external backend the raw transport +builds on any platform, since the `#error` only fires when no backend has been selected. + +See `examples/external_example/` for the `find_package(xcplite)` pattern to build against an +installed library. + +```c +// in your configuration override header +#undef OPTION_ENABLE_TCP +#undef OPTION_ENABLE_UDP +#define OPTION_ENABLE_UDP_RAW +#define OPTION_UDP_RAW_HAL_EXTERNAL // we supply socket_raw_hal_cmp.c ourselves +#define OPTION_QUEUE_32 +``` + +### ASAM CMP + +CMP is a **HAL backend, fully hidden**. It exists to test XCP tools which communicate +through capture modules, not as a feature for ECU developers, so nothing in the core is +optimized or parameterized for it: + +- the CMP backend receives a complete Ethernet/IPv4/UDP frame and applies its envelope + **into its own buffer**, inside the HAL +- it does not borrow the queue headroom and does not participate in the zero copy path +- device id, stream id and interface selection are parsed from the `config` string + +The extra copy is accepted — this is a test bench path. If a CMP driven change ever +appears to be needed in `socket_raw.c`, the queue or the config headers, that is the +signal that the encapsulation has leaked; fix it in the HAL. + +--- + +## Testing + +See `test/test_socket_raw.sh`. Development happens on Linux (a Raspberry Pi over ssh). + +### Phase A — isolated, one machine + +The kernel IP stack sees every frame on an interface, so if the target IP were a host +address the kernel would answer the ARP itself and send ICMP port unreachable for the XCP +UDP port. A network namespace avoids that. `lo` cannot be used — it is `ARPHRD_LOOPBACK` +and has no Ethernet header. + +```bash +./build.sh raw examples +sudo ./test/test_socket_raw.sh # ARP, ping and XCP CONNECT checks +sudo ./test/test_socket_raw.sh --keep # leave it running for manual tests +``` + +The script creates a veth pair with the target in namespace `xcpraw` and **no kernel IP +on the target side**, so `socket_raw.c` alone owns `192.168.90.2`. This phase needs only +`ping`, `arping` and `tcpdump` — no XCP tooling has to be built on the target machine. + +### Phase B — real LAN + +```bash +sudo setcap cap_net_raw+ep ./build-raw/udp_raw_demo +./build-raw/udp_raw_demo --if eth0 --ip 192.168.1.240 # spare, outside the DHCP pool +``` + +The address must be outside the DHCP pool and not otherwise in use. The kernel does not +own it, so it drops the datagrams at the IP layer while AF_PACKET still hands us a copy +at the link layer — which is why this works without a namespace. Drive it with +`xcpclient` and CANape from another machine. Expect `PACKET_IGNORE_OUTGOING` to matter +much more here, and the receive filter to be exercised by real background traffic. + +### Bring-up order + +1. `ping ` — HAL, MAC filter, ARP responder, IPv4 header and checksum, in one shot +2. `arping -I veth0 ` — isolates ARP from IP +3. `tcpdump -i veth0 -nn -e -vv` alongside everything: it prints `bad ip cksum` explicitly. + Build with `OPTION_UDP_RAW_UDP_CHECKSUM_COMPUTE` for this step so the UDP checksum can + be validated too. Confirm full segments are `OPTION_MTU + 10` bytes on the wire + (1434 with the `OPTION_MTU` of 1420 this configuration uses) +4. `xcpclient` CONNECT / GET_STATUS — source address and port extraction, peer MAC + learning, and the `socketSendTo` return value contract +5. UPLOAD / DOWNLOAD — larger command responses +6. DAQ measurement, long enough to wrap the transmit queue ring +7. Shutdown — the receive thread must exit within ~100 ms, and the idle receive loop must + not burn CPU (watch `top`; that is the symptom of a broken deadline loop) +8. Adversarial receive: `ping -s 3000` (fragments dropped, not parsed), UDP to the wrong + port, broadcast UDP, and an oversized datagram (dropped, not truncated) + +### Unit tests + +`test/socket_raw_test` covers everything that does not need a network — checksums against +the RFC 1071 reference vector, wire struct packing, the frame build, the receive filter +and the ARP and ICMP responders — by compiling `socket_raw.c` with a fake HAL that +captures the transmitted frame: + +```bash +./build.sh raw tests && ./build-raw/socket_raw_test +``` + +--- + +## Zero copy transmit (`OPTION_UDP_RAW_ZERO_COPY`) + +Optional, default on, headroom is reserved in front of every transmit queue +segment so `socketSendTo` writes the 42 byte Ethernet/IPv4/UDP header directly in place instead of +copying the payload into a separate frame buffer. + +### The queue concept + +`queue.h` defines two distinct header reservations, easy to confuse: + +| Constant | Scope | Purpose | +|---|---|---| +| `QUEUE_ENTRY_USER_HEADER_SIZE` | per **message** | the 4 byte XCP transport layer header (ctr+len) that every accumulated message carries | +| `QUEUE_SEGMENT_HEADER_SIZE` | per **segment** | reserved once, in front of the whole segment, for a consumer which prepends a header to the complete segment | + +Zero copy needs the second. A segment is one Ethernet frame, so its link header is needed exactly +once, in front. Reserving it per message instead would put the space *inside* the datagram payload +and multiply it by the number of accumulated messages — measured accumulation is ~61 messages per +1464 byte datagram, so a 42 byte per-message reservation would need 2562 bytes of header for a +1464 byte datagram and cut accumulation efficiency by about two thirds. + +`QUEUE_SEGMENT_HEADER_SIZE` is meaningful only for the segment accumulating queues +(`queue32.c`, `queue32m.c`); `xcptl_cfg.h` enforces that with an `#error` against `OPTION_QUEUE_32`. + +### Implementation + +| File | Change | +|---|---| +| `xcptl_cfg.h` | `XCPTL_TX_HEADROOM` (48, or 0 when the option is off) | +| `queue.h` | `QUEUE_SEGMENT_HEADER_SIZE` plus an alignment precondition | +| `queue32.c`, `queue32m.c` | one `#if` guarded `segment_header[]` field and a `static_assert` on payload alignment | +| `xcpethtl.c` | `has_headroom` parameter on the **static** `XcpEthTlSend`, set at its three call sites | +| `socket_raw.c` | `socketSendToReserved`, sharing the header build with `socketSendTo` | + +That is the whole change outside the socket layer. Every use of `tXcpSegmentBuffer` is `sizeof()`, +array indexing or member access, so no pointer arithmetic needed adjusting. With the option off, +`XCPTL_TX_HEADROOM` is 0 and the queue entry layout is byte identical to before. + +48 rather than 42 keeps the segment payload 8 byte aligned. The header is written **right +justified**, ending exactly where the payload starts, which also lands the IPv4 header on a 4 byte +boundary. Verified layout with the option on: `offsetof(msg_buffer)` = 56, frame start at offset 14, +IPv4 header at offset 28, entry stride 1528. + +The command response path keeps copying: `XcpTlSendCrm` builds its message on the stack, so there is +no headroom in front of it. That path is not hot (one response per request, at most +`XCPTL_MAX_CTO_SIZE` bytes), so a second in-place path there would not pay for itself. + +### Not done: preinitialized headers + +Preparing the header once per connection and only patching it per send was considered and rejected. +The length changes on every send and is embedded in the IPv4 `total_length`, the UDP `length` **and** +the IPv4 checksum, so even a prepared template still needs three fields written each time. The saving +would be a 42 byte header build reduced to a 6 byte patch — roughly 2% on top of the ~97% that +removing the payload copy already achieves. Not worth the complexity. + +### Where the benefit is + +The removed copy is up to `XCPTL_MAX_SEGMENT_SIZE` bytes per datagram. At a saturated 100 Mbit/s +(~8000 frames/s) that is ~12 MB/s of memory bandwidth: negligible on a Linux host, a meaningful +fraction of a core on a microcontroller. The optimization therefore pays off on the embedded targets +the raw transport exists for, not on the Linux test vehicles. + +--- + +## Not implemented yet + +### Command path latency + +`GET_DAQ_CLOCK` is used for time synchronization, so jitter in handling it degrades +synchronization quality. The current design optimizes for throughput, not latency. Known +raw specific jitter sources, for a later optimization pass: + +1. **inline ARP/ICMP replies** — an ARP burst or ping flood delays the command behind it. + Largest controllable source; could be deferred or rate limited +2. **filter work per foreign frame** — small, but scales with link load +3. **`tx_mutex` contention** — a command response can wait behind a DAQ segment send. + Should be short, but on AF_PACKET `eth_hal_send` is a `write()` syscall, so measure + +A further optimization would remove the header build from the lock entirely: almost the +whole 42 byte header is constant per client and could be prepared once on connect, with +only `total_length`, the UDP length and the IPv4 checksum patched per send. Fixing the +IPv4 `ident` at 0 (legal for atomic datagrams with DF set, RFC 6864) removes the last +shared mutable state. Note the lengths do vary: `queuePop` flushes partial segments. + +If the jitter turns out to be unsatisfactory, the better answer is not a transport +optimization at all — XCP allows telling the client that the `GET_DAQ_CLOCK` timestamp is +sampled close to **response transmission** rather than command reception, which makes the +receive path jitter largely irrelevant. That is not implemented today. + +### Other + +- VLAN (802.1Q) is out of scope; tagged frames are dropped with a warning +- Vector XLAPI and ASAM CMP backends diff --git a/docs/TECHNICAL.md b/docs/TECHNICAL.md index 7882e9f1..d8faf8fc 100644 --- a/docs/TECHNICAL.md +++ b/docs/TECHNICAL.md @@ -86,20 +86,62 @@ As a side effect, calibration segment persistence (freeze command) is supported. --- -## Offline A2L Generation +## Offline A2L Generation -Use the XCPlite specific A2L creator tool (xcpclient), which is aware of the different addressing schemes and static markers created by the code instrumentation macros. -See `no_a2l_demo` or `no_a2l_demo_cpp` and in particular `esp32_freertos_demo` for examples and instructions. +The A2L file can be generated offline from the ELF file of the application with the XCPlite specific ELF/DWARF to A2L generator in the +`xcpclient` tool. The workflow, the rules for the application code, the naming of types and variables, the supported types and the +diagnostics are described in [OFFLINE_A2L.md](OFFLINE_A2L.md). The information the instrumentation macros leave in the ELF file for +this purpose is described in the next section. -### xcpclient — ELF/DWARF Internals +## Instrumentation Markers for Offline A2L Tools -This section documents how `xcpclient --create-a2l` discovers events, calibration segments, -and local variables from the firmware ELF/DWARF. It is intended for contributors to xcpclient -or developers who need to understand why a particular variable does or does not appear in the -generated A2L file. For the user-facing rules (what you need to do in your application code), -see `examples/no_a2l_demo/README.md`. +The instrumentation macros leave static data and named variables in the ELF file, from which an A2L creator or an XCP tool can build the +A2L file from the linker map and the debug information only, without on-target A2L generation. The markers make calibration segments, +events, the EPK, metadata and the scope in which an event is triggered detectable in the ELF/DWARF file. The xcpclient tool reads them +for C and C++ applications, see [OFFLINE_A2L.md](OFFLINE_A2L.md). -#### `xcp_evts` section — event descriptors +This section is the contract between the macros and such tools: a changed section name, marker name or descriptor layout requires a +change of the tool. + +### ELF sections + +| Section | Content | Emitted by | +|---|---|---| +| `xcp_evts` | one `tXcpEventDescriptor` (16 bytes: name, cycle time, priority) per event in link order, the position is the event id | `DaqCreateEvent`, `DaqCreateEventExt`, `DaqCreateAndTriggerEvent` | +| `xcp_cals` | one `tXcpCalSegDescriptor` (32 bytes: name, default page address, index variable, size, type) per calibration segment or block | `CalSegDecl`, `CalSegDeclRef`, `CalSegCreate` and the calibration block macros | +| `xcp_epk` | the EPK software version string | `XcpCreateEpk` | +| `xcp_meta` | the metadata constants `xcp_meta____` | `XCP_UNIT`, `XCP_LIMITS`, `XCP_COMMENT`, `XCP_READ_WRITE` | + +On macOS the sections are named `__DATA,xcp_evts` etc. On platforms without section support (`XCP_EVENT_SECTION_ATTR` empty) the +events and segments are registered at runtime only. + +### Marker variables + +```c +// Addressing scheme signature (libxcplite), the value is the driver version, see Addressing Modes +const uint16_t XCPLITE__CASDD; // or XCPLITE__ACSDD, XCPLITE__AXSDD, XCPLITE__CXSDD + +// Calibration segment descriptor and index, from CalSegDecl(name), CalSegCreate(name) +static const tXcpCalSegDescriptor calseg__name; // in section xcp_cals, calblk__name for calibration blocks +static tXcpCalSegIndex calseg_id_name; + +// Event descriptor and id, from DaqCreateEvent(name), DaqCreateEventExt(name, cycle, prio), DaqCreateAndTriggerEvent(name) +static const tXcpEventDescriptor evt__name; // in section xcp_evts +static tXcpEventId evt_id_name; +static THREAD_LOCAL tXcpEventId evt__dynname; // DaqCreateEventInstance(name), one event instance per thread + +// Event trigger anchor, from DaqTriggerEvent(name), DaqTriggerEventExt(name, base), DaqEventVar(name, ...), DaqTriggerEventCapture(name, ...), see below +static tXcpEventId trg____name; // in the function which triggers the event + +// Metadata, from XCP_COMMENT(name, text), XCP_UNIT(name, unit), XCP_LIMITS(name, min, max), XCP_READ_WRITE(name) +static const char xcp_meta__comment__name[]; // in section xcp_meta, also xcp_meta__unit__, xcp_meta__min__, xcp_meta__max__, xcp_meta__read_write__ + +// Capture struct, from DaqTriggerEventCapture(event, var, ...), one member per captured variable, +// named like the variable with a trailing underscore, which an A2L tool removes again +struct { __typeof__(var) var_; ... } cap__event; +``` + +### `xcp_evts` section — event descriptors Every call to `DaqCreateEvent(name)` or `DaqCreateAndTriggerEvent(name)` emits a `tXcpEventDescriptor` constant into the `xcp_evts` section (`.rodata` on ELF targets, `__DATA,xcp_evts` on macOS): @@ -111,10 +153,10 @@ static const tXcpEventDescriptor evt__task ``` `tXcpEventDescriptor` contains the event name string, cycle time, and priority. -xcpclient iterates all entries in `xcp_evts` to discover **every event** defined in the +A tool iterates all entries in `xcp_evts` to discover **every event** defined in the firmware, regardless of whether that code path has executed at the time of A2L generation. -#### `xcp_cals` section — calibration segment descriptors +### `xcp_cals` section — calibration segment descriptors Every `CalSegDecl(name)` + `CalSegCreate(name)` pair (or `CalSegDecl(name)` at file scope) emits a `tXcpCalSegDescriptor` constant into the `xcp_cals` section: @@ -131,14 +173,18 @@ static const tXcpCalSegDescriptor calseg__params ``` `tXcpCalSegDescriptor` contains the segment name, the address of the default page, its size, and -the type (segment vs. block). xcpclient reads these to discover all calibration segments and +the type (segment vs. block). A tool reads these to discover all calibration segments and their exact layout in memory — without any A2L registration calls in the application code. -#### Trigger point DWARF scope anchors — `trg__` naming convention +### Trigger point DWARF scope anchors — `trg____` Every event trigger macro emits a **named static local variable** whose name encodes -the set of addressing modes active at that trigger point. xcpclient reads this name from -the DWARF to know how to decode the XCP address for each measurement variable. +the set of addressing modes active at that trigger point. An A2L tool reads this name from +the DWARF to know how to decode the XCP address for each measurement variable. The base address the macros pass for the stack +frame relative addressing mode is `xcp_get_frame_addr()`: the frame base the compiler uses in the DWARF locations of the local +variables (`DW_AT_frame_base`) minus `XCP_FRAME_ADDR_OFFSET`, see `inc/xcplib.h`. This is `__builtin_frame_address(0)` under clang, +which describes the local variables relative to the frame pointer register, and `__builtin_dwarf_cfa()` under GCC, which describes +them relative to the canonical frame address (CFA). The offsets from the DWARF are used without correction. #### Naming convention @@ -151,9 +197,10 @@ The letters between `trg__` and the trailing `__name` form a sequence where | `C` | any | **Calibration-segment relative** — offset within a named `CalSeg` | | `S` | 2 | **Stack frame relative** — offset from `xcp_get_frame_addr()` | | `D` | 3+ | **Dynamic** — offset from an individually supplied base pointer; supports both synchronous and asynchronous access | +| `R` | 3 | **Capture struct relative** — offset of a member in the capture struct `cap__` which the trigger passes as base pointer, a `D` slot with a known layout. The member names carry a trailing underscore | The trailing `__name` (double underscore) identifies the event and separates it from the -mode sequence so xcpclient can split them unambiguously. +mode sequence so a tool can split them unambiguously. #### Anchor variants in the codebase @@ -162,6 +209,7 @@ mode sequence so xcpclient can split them unambiguously. | `trg__AAS__name` | `DaqTriggerEvent`, `DaqCreateAndTriggerEvent`, `DaqEventVar` (C) | ext=0,1: Absolute — ext=2: Stack | | `trg__AASD__name` | `DaqTriggerEventExt` | ext=0,1: Absolute — ext=2: Stack — ext=3: Dynamic base pointer | | `trg__AASDD__name` | `DaqEventVar`, `DaqEventAtVar` (C++) | ext=0,1: Absolute — ext=2: Stack — ext=3+: Dynamic (one slot per measurement variable) | +| `trg__AASR__name` | `DaqTriggerEventCapture`, `DaqTriggerEventCaptureAt`, `DaqCreateAndTriggerEventCapture` | ext=0,1: Absolute — ext=2: Stack — ext=3: the capture struct `cap__name` | --- @@ -177,7 +225,7 @@ XcpEventExt_Var(trg__AAS__task, 1 /*base count*/, xcp_get_frame_addr()); `trg__AAS__task` is a **named static local variable**. The DWARF debug info records its address and the lexical scope it lives in — which is the same scope as the local variables -on the stack. xcpclient finds `trg__AAS__name` in the DWARF, walks all variables whose live +on the stack. A tool finds `trg__AAS__name` in the DWARF, walks all variables whose live range covers that location, and creates A2L entries for them with the correct addressing mode. `DaqCreateAndTriggerEvent(name)` does the same in one macro — it writes the @@ -218,21 +266,6 @@ static tXcpEventId trg__AASDD__calc = XCP_UNDEFINED_EVENT_ID; ``` -#### What xcpclient reads from the ELF - -| ELF / DWARF source | Populated by | xcpclient use | -|---|---|---| -| `xcp_evts` section | `DaqCreateEvent`, `DaqCreateEventInstance`, `DaqCreateAndTriggerEvent` | Discover all events, names, cycle times | -| `xcp_cals` section | `CalSegDecl` + `CalSegCreate` | Discover all calibration segments, default page addresses and sizes | -| DWARF scope of `trg__AAS__name` | `DaqTriggerEvent`, `DaqCreateAndTriggerEvent`, `DaqEventVar` (C) | Find stack-local and absolute variables — ext=0,1: Absolute, ext=2: Stack | -| DWARF scope of `trg__AASD__name` | `DaqTriggerEventExt` | Same plus a dynamic base pointer slot — ext=3: Dynamic | -| DWARF scope of `trg__AASDD__name` | `DaqEventVar` / `DaqEventAtVar` (C++) | Per-variable dynamic slots — ext=3+: one per measurement | -| DWARF global/static symbols | Linker output | Resolve absolute addresses of global measurement and calibration variables | -| DWARF type info (`DW_TAG_structure_type` etc.) | Compiler | Generate `TYPEDEF_STRUCTURE` / `RECORD_LAYOUT` entries in A2L | - -See no_a2l_demo or free_rtos_demo. - - ## Addressing Modes XCPlite makes intensive use of relative addressing. @@ -249,7 +282,7 @@ XCPlite absolute addressing: XCPLITE__CASDD (default) 0x03. - Pointer relative (Event based relative addressing mode with asynchronous access) ... 0x0F -0xFD - File download memory space (XCP_ADDR_EXT_FILE) +0xFD - File upload memory space (XCP_ADDR_EXT_FILE) 0xFE - MTA pointer address space (XCP_ADDR_EXT_PTR) 0xFF - Undefined address extension (XCP_UNDEFINED_ADDR_EXT) @@ -258,6 +291,11 @@ XCPlite relative addressing: XCPLITE__ACSDD (for use cases with external A2L gen 0x01 - Calibration segment relative addressing mode (XCP_ADDR_EXT_SEG) ... same as above +XCPlite absolute addressing without calibration segment management: XCPLITE__AXSDD (XCP_ENABLE_CALSEG_LIST not defined) +0x00 - Absolute addressing mode (XCP_ADDR_EXT_ABS) +0x01 - Memory access via application callbacks (XCP_ADDR_EXT_APP) +... same as above + XCPlite multi application absolute addressing: XCP_ADDRESS_MODE_XCPLITE__CXSDD (for SHM mode) 0x00 - Absolute addressing mode (XCP_ADDR_EXT_ABS) 0x01 - Memory access via application callbacks @@ -371,36 +409,3 @@ In XCPlite, the EPK may be specified with an API function or is generated from b - Configuration for begin/end atomic calibration user defined XCP command is not default. Must be set once in a new CANape project to 0x01F1 and 0x02F1 - EPK segment is defined with 2 readonly pages, because of CANape irritations with mixed mode calibration segment. CANape would not care for a single page EPK segment, reads active page always from segment 0 and uses only SET_CAL_PAGE ALL mode - CANape ignores address extension of `loop_histogram` in ccp_demo, when saving calibration values to a parameter file. `loop_histogram` is a CHARACTERISTIC array, but it is in a measurement group - - -## 5 · Appendix - -### Static Instrumentation Markers for A2L Updater/Creator Tools - -The code instrumentations creates static variables, to help an A2L Updater/Creator or an XCP tool to build an A2L file or its database from linker map and debug information only. -The markers make it possible to detect calibration segments, events, capture buffers and the scope where an event is triggered in the ELF/DWARF file. -Runtime A2L generation can be turned off. Measurement and calibration metadata may be added with the usual methods. - -This is currently in experimental state. -The xcpclient tool has support to read this information from an ELF/DWARF file. -CPP is not supported yet. - - -```c -//Create calibration segment macro segment index once pattern -static tXcpCalSegIndex calseg_id_##name; - -// Create measurement event macro event id once pattern -// From DaqCreateXxx(name), -static tXcpEventId evt_id_##name -static tXcpEventId evt__dynname - -// Daq capture macro (DaqCapture(event, var)) capture buffer -static __typeof__(var) daq__##event##__##var - -// Daq event trigger macro event id once pattern -// From C macros DaqCreateAndTriggerXxx(name), DaqEventVar(name, ...), ...) -static tXcpEventId trg__AAS__##name // For absloute and stack relative addressing [XCP_ADDR_EXT_ABS and XCP_ADDR_EXT_DYN] -static tXcpEventId trg__AASD__##name // For absolute, stack and relative addressing [XCP_ADDR_EXT_ABS, XCP_ADDR_EXT_DYN, XCP_ADDR_EXT_DYN+1] -static tXcpEventId trg__AASDD__##name // for multiple DYN address extensions [XCP_ADDR_EXT_DYN+1 ..= XCP_ADDR_EXT_DYN_MAX] -``` diff --git a/docs/XCP_DISCOVERY.md b/docs/XCP_DISCOVERY.md new file mode 100644 index 00000000..5ea74630 --- /dev/null +++ b/docs/XCP_DISCOVERY.md @@ -0,0 +1,198 @@ +# XCP discovery — findings and options + +Analysis by Claude + +**Status: nothing decided, nothing changed.** This records what is in the tree today, what +was learned while implementing the ASAM CMP equivalent in `examples/cmp_demo`, and what the +options are. XCPlite does not do XCP discovery today and this document does not change that. + +--- + +## 1. What "XCP discovery" is + +A tool that wants to talk to an XCP server has to know its IP address and port. Discovery is +how it finds them without being told. XCP does this with a **transport layer command sent to +a multicast group**, answered by every server that hears it: + +| | | +|---|---| +| Command | `0xF2` — `CC_TRANSPORT_LAYER_CMD` (`src/xcp.h:43`) | +| Sub-command | `0xFF` `GET_SERVER_ID`, or `0xFD` `GET_SERVER_ID_EXTENDED` | +| Carried in | the ordinary XCP-on-Ethernet transport header (`len`, `ctr`) | +| Answered to | the address and port **carried in the request**, not the sender's | + +The last row is the important design property: the responder needs to know nothing about the +network it sits on. The asking tool says where to reply. + +This is not an XCPlite invention and not a CANape quirk — it is the same mechanism ASAM CMP +adopted wholesale, see §3. + +## 2. What XCPlite has today + +| Piece | Where | State | +|---|---|---| +| `CC_TL_GET_SERVER_ID_EXTENDED` (`0xFD`) | `src/xcplite.c:2804` | **Implemented.** Reads the reply address/port from the request, fills in server IP, port, status, resource, ASCII id and MAC, answers with `XcpSendMulticastResponse()` | +| `CC_TL_GET_SERVER_ID` (`0xFF`) | `src/xcplite.c:2801` | **Stubbed.** `goto no_response; // Not supported, no response, response has atypical layout` | +| Multicast socket + thread | `src/xcpethtl.c:559`, `:682` | Present. Binds `XCPTL_MULTICAST_PORT` (**5557**), joins `239.255.`, one dedicated thread, hands each datagram to `XcpCommand()` | +| `XcpEthTlSetClusterId()` | `src/xcpethtl.c:550` | Empty — `// Not implemented` | +| Enablement | `src/xcptl_cfg.h:118` | `XCPTL_ENABLE_MULTICAST` is commented out and **no shipped configuration defines it** | + +Three consequences worth being explicit about: + +- **It is dead code as shipped.** Nothing in `src/xcplib_*_cfg.h` turns it on, so none of the + above is compiled into any build the project produces. +- **It cannot be used on the raw transport at all.** `docs/SOCKET_RAW.md:30` lists + `XCPTL_ENABLE_MULTICAST` as incompatible with `OPTION_ENABLE_UDP_RAW`, because `socketJoin` + is not implemented there — joining a group means IGMP and the raw stack has none. +- **The A2L never advertises it.** `src/a2l_writer.c:200` emits `OPTIONAL_TL_SUBCMD + GET_DAQ_CLOCK_MULTICAST` when multicast is on, but there is no corresponding line for + `GET_SERVER_ID`. A tool reading the A2L is not told the server can be discovered. + +### The multicast option is entangled with a feature that is not wanted + +`XCPTL_ENABLE_MULTICAST` currently exists to serve `GET_DAQ_CLOCK_MULTICAST`, not discovery. +`src/xcptl_cfg.h:117` calls the whole option *"Not recommended setting"* and explains that it +*"needs to create an additional thread and socket"* with *"no benefit if PTP time synchronized +is used or if there is only one XCP device"*. + +That verdict is about the **clock** feature. It is not a verdict on discovery, which happens +to sit behind the same switch. Any decision here should separate the two. + +## 3. ASAM CMP uses exactly this mechanism + +ASAM CMP 1.1.0 §12.1 is titled *"XCP-based approach"* and means it literally: *"The following +commands are based on the Ethernet Transport Layer of ASAM MCD-1 XCP."* `CMP_CM_DISCOVERY` is +`0xF2` with sub-command `0x10`, multicast to `239.255.0.0:5556`, answered to the address and +port from the request. + +So the two are the same shape, differing in payload, port and one byte: + +| | XCP | ASAM CMP | +|---|---|---| +| Command / sub | `0xF2` / `0xFD` | `0xF2` / `0x10` | +| Group | `239.255.` | `239.255.0.0` | +| Port | 5557 *(in XCPlite)* | 5556 | +| Answer says | server IP, XCP port, resource, id | module IP, prefix, gateway, MAC, **HTTP port**, description, serial | +| Next step for the tool | XCP `CONNECT` | the REST interface | + +`examples/cmp_demo/src/cmp_discovery.c` implements the CMP side. It is a working, tested +reference for the mechanism — roughly 200 lines including the interface enumeration — and +deliberately lives outside the library, because its answer advertises an HTTP port for a REST +interface, a concept `libxcplite` has no business knowing. + +### Port 5556 vs 5557 — an open question + +The CMP spec says XCP uses **5556**: *"While the port number was not registered with IANA, +XCP uses the already registered UDP port number 5556 because a private and closed network is +assumed."* XCPlite's `XCPTL_MULTICAST_PORT` is **5557**. + +This has not been checked against the XCP standard itself, and it matters: if 5556 is what +the XCP specification actually mandates, XCPlite's multicast socket is on the wrong port and +no standard tool would ever find it — which would explain why the feature has never been +exercised. **Verify against ASAM MCD-1 XCP before building anything on top of this.** + +Note also the group differs by default: `XCP_MULTICAST_CLUSTER_ID` is 1 (`src/xcp_cfg.h:448`), +giving `239.255.0.1`, while CMP uses `239.255.0.0` — i.e. cluster id 0. + +## 4. What the CMP implementation taught us + +These are portability findings from getting the CMP responder actually working, and they +apply unchanged to any XCP discovery responder: + +- **`imr_interface = INADDR_ANY` does not mean "all interfaces".** It lets the stack pick one + from the routing table. Measured on macOS 15: a join with `INADDR_ANY` receives **nothing at + all**. The fix is to enumerate interfaces with `getifaddrs()` and join the group on every + `IFF_UP | IFF_MULTICAST` IPv4 interface. A capture module — or an XCP server — cannot know + which interface a tool will appear on. +- **Loopback is a separate interface and must be joined too.** Also measured on macOS: a + process does not receive its own multicast sent via a LAN interface; only a loopback join + and a loopback send see each other. Including `lo0` is what makes a same-host test possible + at all, which matters because it is how CI would exercise this without two machines. +- **A multicast reply needs `IP_MULTICAST_IF` set per response.** Otherwise the reply leaves + through the default route and a tool on another interface never sees it. The correct value + is the local address facing the requester, obtainable by `connect()`ing a scratch UDP socket + to the requester and reading back `getsockname()` — no packet is sent. +- **The multicast RETURN path is filtered far more often than the request path.** Measured + between a Wi-Fi laptop and a wired Raspberry Pi on one subnet: the multicast request + reached the target and was answered, and the multicast answer never came back — an access + point normally does not forward group traffic to a wireless client. Discovery that only + ever answers to a multicast address will appear broken on exactly the setup an engineer + is most likely to use. The escape is already in the protocol: both `CMP_CM_DISCOVERY` and + `GET_SERVER_ID_EXTENDED` take the reply address **from the request**, so a tool can ask to + be answered directly, which ASAM CMP §12.1 permits in as many words — *"The IP destination + address and UDP destination port of the response are given by the request."* A responder + should therefore honour whatever address it is given rather than forcing the group, and a + tool should be prepared to ask for both. +- **`XcpTlMulticastThread` does none of this.** It calls `socketJoin(sock, maddr, addr, NULL)` + once with the server address. On a single-homed Linux box that is fine; anywhere else it is + the `INADDR_ANY` trap in a different shape. This is the most likely reason the feature would + fail if someone enabled it today. + +## 5. Options + +### A. Leave it alone +Discovery stays unimplemented, multicast stays off, the code stays as dead weight. +**Cost:** none. **Consequence:** the `GET_SERVER_ID` stub and the "not recommended" comment +keep implying the feature half-exists, and the next person re-derives all of §4. + +### B. Delete the clock feature, keep and fix discovery *(recommended)* +Remove `XCP_ENABLE_DAQ_CLOCK_MULTICAST` and its A2L line. What remains is a discovery +transport rather than a clock transport, which is a much easier thing to justify keeping. +Then fix the join per §4 and rename the option to say what it is. + +**Cost:** a deletion, plus ~40 lines in the join path. **Consequence:** `XCPTL_ENABLE_MULTICAST` +becomes an honest, defensible option; the raw-transport exclusion still applies. + +### C. Also implement plain `GET_SERVER_ID` (`0xFF`) +The stub says the response *"has atypical layout"* — it is not a normal CRM, which is why it +was skipped. Only worth doing if a real tool is found that sends `0xFF` and not `0xFD`. + +**Cost:** small, but needs the XCP spec open. **Consequence:** none until such a tool exists. +**Do not do this speculatively.** + +### D. Move discovery out of the library, as CMP did +Discovery is stateless, answered before any session exists, and touches neither the queue nor +DAQ. An application could own the socket and call a small library helper to format the +response, exactly as `cmp_demo` owns its CMP responder. + +**Cost:** a new public API. **Consequence:** keeps multicast, interface enumeration and IGMP +out of the library — attractive for FreeRTOS targets, where a multicast join may not exist. +**Downside:** every application that wants discovery re-implements the socket handling. + +### Summary + +| | Effort | Removes dead code | Works on `raw` | Needs the XCP spec | +|---|---|---|---|---| +| A. Leave alone | none | no | n/a | no | +| B. Delete clock, fix join | small | yes | no | to settle the port | +| C. Plain `GET_SERVER_ID` | small | no | no | yes | +| D. Move out of the library | medium | yes | yes | to settle the port | + +## 6. What to settle first + +Two questions gate everything above, and neither needs code: + +1. **Is the XCP multicast port 5556 or 5557?** Read ASAM MCD-1 XCP. If XCPlite is on the wrong + port, option A is not "leave it alone", it is "leave a bug in place". +2. **Does any tool you care about actually use XCP discovery?** CANape is normally pointed at + an address, and the A2L carries it. If nothing in the toolchain sends `GET_SERVER_ID`, then + B is a cleanup exercise, not a feature — still worth doing, but with no deadline. + +Until both are answered, `examples/cmp_demo/src/cmp_discovery.c` stands as the working +reference for how this mechanism behaves in practice. + +--- + +## Appendix: a spec ambiguity resolved in the CMP responder + +ASAM CMP §12.1.1 Table 79 encodes `DeviceDescription` and `SerialNumber` as an `A_UINT16` +length followed by a zero-terminated, `0x00`-padded-to-16-bit `A_UTF8` string. Its offset +column is self-inconsistent by one about where the next field begins (`47–47+N = L1`, then +the next field at `L1+1`, which makes the string `N+1` bytes while calling it `A_UTF8[N]`). + +`cmp_discovery.c` takes **N as the padded byte count** — `"Dev1"` → 6, `""` → 2. The +justification is the identical construction in the status message payload (§8.2.1), whose +wording is *"N is length before"* against a field typed `A_UTF8[N]`, plus the spec's own +worked example showing the padded form as what goes on the wire. A tool that interprets N as +the character count will mis-parse everything after `DeviceDescription`; if one is ever found +that does, this is the first place to look. diff --git a/docs/xcplib_cfg.md b/docs/xcplib_cfg.md index 3f33038d..f97b16d2 100644 --- a/docs/xcplib_cfg.md +++ b/docs/xcplib_cfg.md @@ -78,7 +78,7 @@ This section describes the transport layer configuration parameters in xcptl_cfg | `XCPTL_MAX_CTO_SIZE` | Maximum size of XCP command packets (CRO/CRM) in bytes. Must be divisible by 8 (default: 248) | | `XCPTL_MAX_DTO_SIZE` | Maximum size of XCP data packets (DAQ/STIM) in bytes. Must be divisible by 8 (default: 1024) | | `XCPTL_MAX_SEGMENT_SIZE` | Maximum data buffer size for socket send operations. For UDP, this is the UDP MTU. Calculated as OPTION_MTU - 32 (IP header) | -| `XCPTL_PACKET_ALIGNMENT` | Packet alignment for multiple XCP transport layer packets in a message (default: 4) | +| `XCPTL_PACKET_ALIGNMENT` | Size granularity of the protocol layer packet inside a transport layer message; the packet is padded to this alignment so that concatenated messages in a segment stay aligned. Only 4 is supported, enforced by an `#error` in `queue.h`. Note the fill is included in the message LEN — see "Transport Layer Message Padding" in `docs/TECHNICAL.md` (default: 4) | | `XCPTL_TRANSPORT_LAYER_HEADER_SIZE` | Transport layer message header size in bytes (fixed: 4) | ### Multicast Configuration diff --git a/examples/README.md b/examples/README.md index 0c5f9706..eadf0961 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,6 +28,7 @@ Note that examples targets may need different XCPlite library build configuratio cmake -B build-ptp -S . -DXCPLITE_CONFIGURATION=ptp # for ptp4l_demo cmake -B build-shm -S . -DXCPLITE_CONFIGURATION=shm # for silkit_demo cmake -B build-rtos -S . -DXCPLITE_CONFIGURATION=rtos # for freertos_demo with the FreeRTOS POSIX simulator + cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw # for udp_raw_demo with the raw Ethernet transport ``` @@ -81,6 +82,25 @@ Builds against a pre-built libxcplite and silkit library Demonstrates how to use a PTP (Precision Time Protocol) synchronized clock as XCP data acquisition timestamp source. +### [udp_raw_demo](udp_raw_demo/README.md) + +**Demonstrates XCP on UDP/IPv4 without any TCP/IP stack** - the transport is implemented inside xcplib on top of a thin raw Ethernet HAL. +- For targets with no IP stack at all: a bare metal EMAC driver, or an RTOS Ethernet abstraction without lwIP. +- xcplib answers ARP and ICMP itself, so the target is pingable without a stack. +- Needs an explicit local IPv4 address, there is no DHCP and `0.0.0.0` (ANY) has no meaning. +- Linux and FreeRTOS only, the HAL backend uses `AF_PACKET` and requires `CAP_NET_RAW`. See [docs/SOCKET_RAW.md](../docs/SOCKET_RAW.md). +- ASAM CMP driver planned, for all operating systems. + + +### [cmp_demo](cmp_demo/README.md) + +**Demonstrates supplying your own Ethernet HAL backend from outside the library** - a standalone project consuming an installed xcplite, like external_example. +- Prepares XCP over ASAM CMP, for testing XCP tools which communicate through capture modules. +- Implements the six `eth_hal_*` functions of `src/socket_raw_hal.h` in the application; the built in AF_PACKET backend is then never pulled from the static library. +- The CMP envelope itself is not implemented yet and is a pass through, so the HAL plumbing is testable on its own. +- Linux only, needs `CAP_NET_RAW`. Not built from the root CMakeLists. + + ### [external_example](external_example/README.md) **Demonstrates using libxcplite as a pre-built external library** - independent from the main build system. diff --git a/examples/cmp_demo/.gitignore b/examples/cmp_demo/.gitignore new file mode 100644 index 00000000..0564dc25 --- /dev/null +++ b/examples/cmp_demo/.gitignore @@ -0,0 +1,5 @@ +build/ +*.a2l +*.bin +*.log +*.pcap diff --git a/examples/cmp_demo/CMakeLists.txt b/examples/cmp_demo/CMakeLists.txt new file mode 100644 index 00000000..ccfa81b7 --- /dev/null +++ b/examples/cmp_demo/CMakeLists.txt @@ -0,0 +1,75 @@ +cmake_minimum_required(VERSION 3.10) + +project(cmp_demo VERSION 1.0.0 LANGUAGES C) + +# Standalone project, like examples/external_example: it is NOT built from the xcplite root +# CMakeLists, it consumes an INSTALLED xcplite. See README.md. +# +# xcplite must be installed from the "raw" configuration, which enables the raw Ethernet +# transport (OPTION_ENABLE_UDP_RAW): +# +# cd +# cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw -DCMAKE_INSTALL_PREFIX= +# cmake --build build-raw --target install +# +# then point this project at it: +# +# cmake -B build -S . -Dxcplite_DIR=/lib/cmake/xcplite +# cmake --build build + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +find_package(xcplite REQUIRED) +find_package(Threads REQUIRED) + +# The application supplies the Ethernet HAL backend itself. +# +# socket_raw_hal_cmp.c defines all six eth_hal_* functions that libxcplite's socket_raw.c +# needs. Because libxcplite is a STATIC library, the linker only pulls an archive member to +# resolve an undefined symbol - and these are already defined here, so a built in backend +# is never pulled in and there is no symbol clash. +# This would NOT hold for a shared libxcplite, which would resolve its own symbols internally. +add_executable(cmp_demo + src/main.c + src/socket_raw_hal_cmp.c + src/cmp_transport_udp.c + src/cmp_rest.c + src/cmp_discovery.c + src/cmp.c +) + +target_link_libraries(cmp_demo PRIVATE xcplite::xcplite Threads::Threads) + +# OPTION_UDP_RAW_HAL_EXTERNAL tells the installed socket_raw_hal.h that the application +# supplies the backend. On Linux the library has a built in AF_PACKET backend which the +# static archive rule above already keeps out, so the option only documents the intent. +# On macOS and Windows there is no built in backend and the header refuses to compile +# without it - and the LIBRARY has to be built with it too, for example: +# cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw \ +# -DCMAKE_C_FLAGS="-DOPTION_UDP_RAW_HAL_EXTERNAL" +target_compile_definitions(cmp_demo PRIVATE OPTION_UDP_RAW_HAL_EXTERNAL) + +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(cmp_demo PRIVATE -Wall -Wextra -pedantic) +endif() + +# Unit test for the CMP envelope codec. Links cmp.c only: no sockets, no libxcplite, so it +# runs anywhere and is the fastest way to check a change to the wire format. Its golden +# vectors come from the sample PCAPNG files of the ASAM CMP 1.1.0 specification. +add_executable(cmp_codec_test + test/cmp_codec_test.c + src/cmp.c +) +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(cmp_codec_test PRIVATE -Wall -Wextra -pedantic) +endif() + +enable_testing() +add_test(NAME cmp_codec_test COMMAND cmp_codec_test) + +message(STATUS "Building cmp_demo") +message(STATUS " xcplite version: ${xcplite_VERSION}") +message(STATUS " Ethernet HAL : supplied by this project (src/socket_raw_hal_cmp.c)") +message(STATUS " CMP envelope : ASAM CMP 1.1, Ethernet payload (0x08) over UDP") diff --git a/examples/cmp_demo/README.md b/examples/cmp_demo/README.md new file mode 100644 index 00000000..f8a2e4b5 --- /dev/null +++ b/examples/cmp_demo/README.md @@ -0,0 +1,422 @@ +# cmp_demo — XCP tunnelled through an emulated ASAM CMP capture module + +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. + +--- + +## What it does + +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 | + +### Wire format + +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). + +--- + +## How the override works + +`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. + + +### Files + +| 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 | + +--- + +## Building + +xcplite has to be installed from the `raw` configuration first: + +```bash +cd +cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=$HOME/xcplite-install +cmake --build build-raw --target install +``` + +On macOS or Windows add `-DCMAKE_C_FLAGS="-DOPTION_UDP_RAW_HAL_EXTERNAL"` to that first +command. + +```bash +cd +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 install +``` + +Then build this project against the install: + +```bash +cd examples/cmp_demo +cmake -B build -S . -Dxcplite_DIR=$HOME/xcplite-install/lib/cmake/xcplite +cmake --build build +``` + +This 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. + +--- + +## Running + +```bash +./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 ` | Data Sink address for captured data. Default: learned | +| `--listen ` | UDP port for incoming CMP messages (default 55555) | +| `--mtu ` | MTU of the path to the Data Sink (default 1500, max 9000) | +| `--rest-port ` | REST interface port (default 8080, 0 disables) | +| `--device-id`, `--stream-id`, `--interface-id` | CMP identity | +| `--ecu-mac ` | 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. + +--- + +## The MTU constraint + +§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. + +--- + +## Discovery + +§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](../../docs/XCP_DISCOVERY.md) for what XCPlite has there today and the +options for it. Nothing about XCP discovery is decided by this demo. + +```bash +./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. + +--- + +## The REST interface + +§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): + +```json +"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. + +--- + +## Testing + +Two scripts, same checks, different place to run them: + +```bash +./test.sh # on the target: sync, build, run and check over the network +./test/test_local.sh # on this machine, over loopback +``` + +Neither 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](https://github.com/RainerZ/XCPlite/blob/master/examples/udp_raw_demo/test.sh). Set `TARGET_USER` +and `TARGET_HOST` at the top of it, then it: + +1. rsyncs the library sources and this example to the target; +2. builds and installs the `raw` configuration of xcplite there, then builds `cmp_demo` + against that install — the two-stage build a standalone project needs; +3. 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.o` is in `libxcplite.a` either way, and it is the static-library link + rule that keeps it out of the executable; +4. runs the codec unit test on the target, which is also the check that the big-endian + packing is right on aarch64; +5. starts the capture module and queries all four REST endpoints, asserting that the + `Transmitter` object advertises transmission; +6. reports the CMP endpoint status and confirms the UDP port is open; +7. sends a hardcoded XCP CONNECT tunnelled through CMP and decodes the response, printing + the exact bytes of the `TX_DATA_MSG` it 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 report `FCS_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 as `MALFORMED` instead of + `PAYLOAD_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 ` 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](https://github.com/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`. + +--- + +## Platform dependencies + +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. + +--- + +## Verified against + +| | | +|---|---| +| xcplite | https://github.com/RainerZ/XCPlite, `raw` configuration | +| Library version | 2.2.1 (as reported by `find_package`) | +| Specification | ASAM CMP Protocol Layer Specification V1.1.0, 2026-01-31 | +| Target | Raspberry Pi 5 Model B Rev 1.1 (`pi6`), Debian, aarch64, GCC, `RelWithDebInfo` — via `./test.sh` | +| Host | macOS 15 / arm64 / Apple clang, library built with `OPTION_UDP_RAW_HAL_EXTERNAL` — via `./test/test_local.sh` | +| Checked on both | codec test 74/74 against the specification's sample files; XCP CONNECT tunnelled end to end through CMP; all four REST endpoints answered, with the `Transmitter` object advertising transmission; the built-in AF_PACKET backend confirmed absent from the linked binary | +| Checked on the host | GET_STATUS and DISCONNECT as well; emitted messages re-decoded from the pcap | +| **Not** yet checked | Wireshark dissection (no Wireshark on either machine); DAQ under load, which is where the MTU limit bites; CANape | + +--- + +## Not implemented + +- **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, EtherType `0x99FE`) and padding to the 60-byte minimum. + The envelope codec itself is unchanged: only `cmp_transport.h` gains an implementation. +- Status messages (§8), control messages (§9), aggregation, segmentation, time + synchronisation and the configuration-changing REST methods. + +--- + +## Constraints that should not be worked around + +- 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.h` is untouched, including for the capture timestamp, which the + backend takes itself in `eth_hal_send` because 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](https://github.com/RainerZ/XCPlite/blob/master/docs/SOCKET_RAW.md) for the transport design and +the HAL contract. diff --git a/examples/cmp_demo/src/cmp.c b/examples/cmp_demo/src/cmp.c new file mode 100644 index 00000000..17ec7e61 --- /dev/null +++ b/examples/cmp_demo/src/cmp.c @@ -0,0 +1,249 @@ +/*---------------------------------------------------------------------------- +| File: +| cmp.c +| +| Description: +| ASAM CMP envelope codec. Pure: no sockets, no I/O, no global state, so it can be +| unit tested against the sample frames shipped with the specification. +| See cmp.h for the wire layout and the section references. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include "cmp.h" + +#include + +//------------------------------------------------------------------------------- +// Big endian accessors +// +// All CMP header fields are big endian (6.2). Written out by hand rather than with +// htons/htonl: this file must stay free of platform headers so it can be linked into a +// standalone unit test, and on BSD/macOS the HTONS macros assign in place. + +static uint8_t *put16(uint8_t *p, uint16_t v) { + *p++ = (uint8_t)(v >> 8); + *p++ = (uint8_t)v; + return p; +} + +static uint8_t *put32(uint8_t *p, uint32_t v) { + *p++ = (uint8_t)(v >> 24); + *p++ = (uint8_t)(v >> 16); + *p++ = (uint8_t)(v >> 8); + *p++ = (uint8_t)v; + return p; +} + +static uint8_t *put64(uint8_t *p, uint64_t v) { + for (int i = 56; i >= 0; i -= 8) { + *p++ = (uint8_t)(v >> i); + } + return p; +} + +static uint16_t get16(const uint8_t *p) { return (uint16_t)(((uint16_t)p[0] << 8) | p[1]); } + +static uint32_t get32(const uint8_t *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; } + +//------------------------------------------------------------------------------- + +void cmpCodecInit(tCmpCodec *codec, const tCmpConfig *config) { + if (codec == NULL || config == NULL) { + return; + } + memset(codec, 0, sizeof(*codec)); + codec->config = *config; +} + +const char *cmpResultName(tCmpResult result) { + switch (result) { + case CMP_OK: + return "ok"; + case CMP_DROP_TOO_SHORT: + return "shorter than the CMP headers"; + case CMP_DROP_VERSION: + return "unsupported CMP version (0x00 would be TECMP/PLP)"; + case CMP_DROP_MESSAGE_TYPE: + return "not a Transmit Data Message"; + case CMP_DROP_PAYLOAD_TYPE: + return "no Ethernet Data Message payload"; + case CMP_DROP_INTERFACE_ID: + return "addressed to a different InterfaceId"; + case CMP_DROP_SEGMENTED: + return "segmented, not supported"; + case CMP_DROP_MALFORMED: + return "inconsistent length fields"; + case CMP_DROP_NO_FCS: + return "Ethernet payload too short to hold the FCS"; + case CMP_DROP_TOO_LARGE: + return "inner frame larger than the receive buffer"; + } + return "unknown"; +} + +//------------------------------------------------------------------------------- +// Capture direction: inner Ethernet frame -> Captured Data Message + +uint16_t cmpWrapCaptured(tCmpCodec *codec, const uint8_t *frame, uint16_t frame_len, uint64_t timestamp_ns, bool in_sync, uint8_t *out, uint16_t out_max) { + + if (codec == NULL || frame == NULL || out == NULL || frame_len == 0) { + return 0; + } + + // The Ethernet payload DATA runs from the destination MAC through the FCS (7.3.8), but + // xcplib's HAL contract passes frames WITHOUT FCS, so four bytes are appended here. + uint32_t data_length = (uint32_t)frame_len + CMP_FCS_LEN; + uint32_t payload_length = CMP_ETH_PAYLOAD_HDR_LEN + data_length; + uint32_t total = CMP_HDR_LEN + CMP_CAP_DATA_HDR_LEN + payload_length; + if (payload_length > 0xFFFFu || total > (uint32_t)out_max) { + return 0; // caller reports ETH_HAL_ERROR_SIZE + } + + uint8_t *p = out; + + // CMP header (6.2.1) + *p++ = CMP_VERSION; + *p++ = 0; // reserved + p = put16(p, codec->config.device_id); + *p++ = CMP_MSG_CAP_DATA; + *p++ = codec->config.stream_id; + p = put16(p, codec->tx_seq); + + // Captured Data Message header (7.2.1) + p = put64(p, timestamp_ns); + p = put32(p, codec->config.interface_id); + // DIR_ON_IF = 0: the emulated ECU sits behind the interface, so from the capture + // module's point of view this frame was RECEIVED on the interface (Table 11 bit 4). + // SEG = 00, unsegmented: one inner frame per CMP message, no aggregation (6.3.2/6.3.3). + *p++ = (uint8_t)(in_sync ? CMP_CAP_FLAG_INSYNC : 0); + *p++ = CMP_PAYLOAD_ETHERNET; + p = put16(p, (uint16_t)payload_length); + + // Ethernet Data Message payload (7.3.8) + // FCS_SUPPORT = 0: this capture module cannot compute a real FCS, so the four bytes + // below are the zero value the specification prescribes for that case. + p = put16(p, 0); + p = put16(p, 0); // reserved + p = put16(p, (uint16_t)data_length); + memcpy(p, frame, frame_len); + p += frame_len; + memset(p, 0, CMP_FCS_LEN); + p += CMP_FCS_LEN; + + codec->tx_seq++; // wraps 0xFFFF -> 0 naturally (6.2.1) + codec->n_wrapped++; + return (uint16_t)(p - out); +} + +//------------------------------------------------------------------------------- +// Transmit direction: Transmit Data Message -> inner Ethernet frame + +static uint16_t drop(tCmpCodec *codec, tCmpResult *result, tCmpResult reason) { + codec->n_dropped++; + if (result != NULL) { + *result = reason; + } + return 0; +} + +// StreamSequenceCounter monitoring (6.3.1): detects loss, duplication and reordering. +// A diagnostic only - a jump never causes a frame to be dropped. +static void checkPeerSeq(tCmpCodec *codec, uint16_t device_id, uint16_t seq) { + if (codec->peer_seq_valid && codec->peer_device_id == device_id && (uint16_t)(codec->peer_seq + 1u) != seq) { + codec->n_seq_jumps++; + } + codec->peer_device_id = device_id; + codec->peer_seq = seq; + codec->peer_seq_valid = true; +} + +uint16_t cmpUnwrapTransmit(tCmpCodec *codec, const uint8_t *in, uint16_t in_len, uint8_t *out, uint16_t out_max, tCmpResult *result) { + + if (codec == NULL || in == NULL || out == NULL) { + return 0; + } + if (in_len < CMP_HDR_LEN + CMP_TX_DATA_HDR_LEN + CMP_ETH_PAYLOAD_HDR_LEN) { + return drop(codec, result, CMP_DROP_TOO_SHORT); + } + // Version 0x00 is TECMP/PLP, which is not compatible with CMP (5.2) + if (in[0] < CMP_VERSION) { + return drop(codec, result, CMP_DROP_VERSION); + } + if (in[4] != CMP_MSG_TX_DATA) { + // Control and Status messages from the data sink land here too and are simply not + // ours to deliver. Not counted as an error by the caller. + return drop(codec, result, CMP_DROP_MESSAGE_TYPE); + } + + checkPeerSeq(codec, get16(in + 2), get16(in + 6)); + + // Walk the (Transmit Data Message header, payload) pairs. A conformant data sink sends + // exactly one - we advertise AggregationCount = 1 over REST - but aggregation (6.3.2) + // is legal, so extras are counted rather than silently ignored. + uint16_t delivered = 0; + tCmpResult reason = CMP_DROP_PAYLOAD_TYPE; + uint32_t off = CMP_HDR_LEN; + + while (off + CMP_TX_DATA_HDR_LEN <= (uint32_t)in_len) { + const uint8_t *h = in + off; + uint32_t interface_id = get32(h + 12); + uint8_t common_flags = h[20]; + uint8_t payload_type = h[21]; + uint16_t payload_length = get16(h + 22); + uint32_t body = off + CMP_TX_DATA_HDR_LEN; + + // Payload type INVALID means padding: discard it and the rest of the message (7.2) + if (payload_type == CMP_PAYLOAD_INVALID) { + break; + } + if (body + payload_length > (uint32_t)in_len) { + return drop(codec, result, CMP_DROP_MALFORMED); + } + + if (delivered != 0) { + codec->n_aggregated_ignored++; + } else if ((common_flags & CMP_TX_FLAG_SEG_MASK) != 0) { + reason = CMP_DROP_SEGMENTED; + } else if (interface_id != codec->config.interface_id) { + // Addressing first: a request for another interface is simply not ours, while an + // unsupported payload type ON our interface means the data sink is misconfigured. + reason = CMP_DROP_INTERFACE_ID; + } else if (payload_type != CMP_PAYLOAD_ETHERNET) { + reason = CMP_DROP_PAYLOAD_TYPE; + } else if (payload_length < CMP_ETH_PAYLOAD_HDR_LEN) { + reason = CMP_DROP_MALFORMED; + } else { + uint16_t data_length = get16(in + body + 4); + if ((uint32_t)data_length + CMP_ETH_PAYLOAD_HDR_LEN > (uint32_t)payload_length) { + reason = CMP_DROP_MALFORMED; + } else if (data_length < CMP_FCS_LEN) { + reason = CMP_DROP_NO_FCS; + } else { + // Strip the trailing FCS regardless of FCS_SENDING: with the flag clear the + // four bytes are dummies to be ignored (7.3.8 Table 35 bit 8), and with it + // set they are a real FCS - but xcplib's HAL contract wants neither. + uint16_t frame_len = (uint16_t)(data_length - CMP_FCS_LEN); + if (frame_len == 0) { + reason = CMP_DROP_MALFORMED; + } else if (frame_len > out_max) { + reason = CMP_DROP_TOO_LARGE; + } else { + memcpy(out, in + body + CMP_ETH_PAYLOAD_HDR_LEN, frame_len); + delivered = frame_len; + } + } + } + + off = body + payload_length; + } + + if (delivered == 0) { + return drop(codec, result, reason); + } + codec->n_unwrapped++; + if (result != NULL) { + *result = CMP_OK; + } + return delivered; +} diff --git a/examples/cmp_demo/src/cmp.h b/examples/cmp_demo/src/cmp.h new file mode 100644 index 00000000..ff60e485 --- /dev/null +++ b/examples/cmp_demo/src/cmp.h @@ -0,0 +1,159 @@ +#pragma once + +/*---------------------------------------------------------------------------- +| File: +| cmp.h +| +| Description: +| ASAM CMP (Capture Module Protocol) envelope codec for the cmp_demo backend. +| +| This file and cmp.c are the ONLY place in the demo that know about CMP, and they +| are pure: no sockets, no I/O, no global state. socket_raw_hal_cmp.c owns the +| transport and calls into here. xcplib itself knows nothing about CMP - it builds +| and parses plain Ethernet/IPv4/UDP frames and hands them to the HAL. +| See docs/SOCKET_RAW.md and the README. +| +| The demo emulates a Capture Module which tunnels one XCP ECU: +| ECU -> tool the frame xcplib hands to eth_hal_send becomes a Captured Data +| Message (CAP_DATA_MSG, 0x01) +| tool -> ECU a Transmit Data Message (TX_DATA_MSG, 0x04, new in CMP 1.1) is +| unwrapped and its inner frame handed to eth_hal_recv +| +| Reference: ASAM CMP Protocol Layer Specification V1.1.0. Section numbers below +| refer to it. All CMP header fields are BIG endian (6.2); the payload keeps the +| original byte order of the captured data. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include +#include + +// EtherType assigned to ASAM CMP (6.4.1). +// Shared with TECMP/PLP, which are distinguished by a first byte of 0x00 (5.2). +#define CMP_ETHERTYPE 0x99FE + +// CMP major version, first byte of every message. Must be >= 1 (5.2). +#define CMP_VERSION 0x01 + +// CMP_MESSAGE_TYPE (6.2.1, Table 6) +#define CMP_MSG_CAP_DATA 0x01 // captured data, capture module -> data sink +#define CMP_MSG_CTRL 0x02 // control +#define CMP_MSG_STATUS 0x03 // status, capture module -> data sink +#define CMP_MSG_TX_DATA 0x04 // transmit data, data sink -> capture module (CMP 1.1) +#define CMP_MSG_VENDOR 0xFF // vendor defined + +// DATA_MESSAGE_PAYLOAD_TYPE (7.2, Table 9), the subset this demo uses +#define CMP_PAYLOAD_INVALID 0x00 // padding: discard this and the rest of the message +#define CMP_PAYLOAD_ETHERNET 0x08 // Ethernet frame, dst MAC .. FCS +#define CMP_PAYLOAD_RAW_ETHERNET 0x0D // as 0x08 but with preamble and SFD, not used here + +// Header lengths, fixed by the specification +#define CMP_HDR_LEN 8 // CMP header (6.2.1) +#define CMP_CAP_DATA_HDR_LEN 16 // Captured Data Message header (7.2.1) +#define CMP_TX_DATA_HDR_LEN 24 // Transmit Data Message header (7.2.2) +#define CMP_ETH_PAYLOAD_HDR_LEN 6 // Ethernet payload flags/reserved/data_length (7.3.8) +#define CMP_FCS_LEN 4 // FCS, part of the Ethernet payload DATA + +// Bytes the envelope adds to one inner Ethernet frame, per direction. +// The HAL needs these to size its buffers and to compute the MTU budget: the CMP message +// must fit into one un-fragmented outer packet (6.4.2 forbids IP fragmentation). +#define CMP_CAP_OVERHEAD (CMP_HDR_LEN + CMP_CAP_DATA_HDR_LEN + CMP_ETH_PAYLOAD_HDR_LEN + CMP_FCS_LEN) // 34 +#define CMP_TX_OVERHEAD (CMP_HDR_LEN + CMP_TX_DATA_HDR_LEN + CMP_ETH_PAYLOAD_HDR_LEN + CMP_FCS_LEN) // 42 + +// Captured Data Message header common flags (7.2.1, Table 11) +#define CMP_CAP_FLAG_RECALC 0x01 // timestamp was recalculated before transmission +#define CMP_CAP_FLAG_INSYNC 0x02 // synchronized to the time provider +#define CMP_CAP_FLAG_SEG_MASK 0x0C // 00 unsegmented, 01 first, 10 intermediary, 11 last +#define CMP_CAP_FLAG_DIR_ON_IF 0x10 // 0 received on interface, 1 sent on interface +#define CMP_CAP_FLAG_OVERFLOW 0x20 // one or more messages were lost while capturing +#define CMP_CAP_FLAG_ERROR 0x40 // error detected in the captured message + +// Transmit Data Message header common flags (7.2.2, Table 13) +#define CMP_TX_FLAG_RELATIVE 0x01 // timestamp is a minimum distance to the previous frame +#define CMP_TX_FLAG_SEG_MASK 0x0C + +// Ethernet Data Message payload flags (7.3.8, Table 35) +#define CMP_ETH_FLAG_FCS_ERR 0x0001 +#define CMP_ETH_FLAG_FRAME_TOO_SHORT_ERR 0x0002 +#define CMP_ETH_FLAG_TX_PORT_DOWN 0x0004 +#define CMP_ETH_FLAG_COLLISION 0x0008 +#define CMP_ETH_FLAG_FRAME_TOO_LONG_ERR 0x0010 +#define CMP_ETH_FLAG_PHY_ERR 0x0020 +#define CMP_ETH_FLAG_FRAME_TRUNCATED 0x0040 +#define CMP_ETH_FLAG_FCS_SUPPORT 0x0080 // 1: the CM is able to fill in the FCS value +#define CMP_ETH_FLAG_FCS_SENDING 0x0100 // TX: 1: DATA carries a real FCS, 0: dummy, ignore + +// Configuration of the emulated capture module +typedef struct { + uint16_t device_id; // identifies this capture module, unique in the network (6.2.1) + uint8_t stream_id; // our outgoing stream (6.3.1) + uint32_t interface_id; // the emulated capture interface (7.2.1) +} tCmpConfig; + +// Why a received message was not delivered. Diagnostics only - a drop is never fatal. +typedef enum { + CMP_OK = 0, + CMP_DROP_TOO_SHORT, // shorter than the mandatory headers + CMP_DROP_VERSION, // version 0 (that is TECMP/PLP) or unknown + CMP_DROP_MESSAGE_TYPE, // not a TX_DATA_MSG + CMP_DROP_PAYLOAD_TYPE, // not an Ethernet payload + CMP_DROP_INTERFACE_ID, // addressed to a different interface + CMP_DROP_SEGMENTED, // segmentation not supported, see 6.3.3 + CMP_DROP_MALFORMED, // length fields inconsistent + CMP_DROP_NO_FCS, // Ethernet payload too short to hold the 4 byte FCS + CMP_DROP_TOO_LARGE, // inner frame does not fit the caller's buffer +} tCmpResult; + +// Codec state. One instance per link; not thread safe by itself, but the HAL contract +// serializes send (transmit mutex held) and receive (XCP receive thread only) separately, +// and the two directions touch disjoint fields. +typedef struct { + tCmpConfig config; + + uint16_t tx_seq; // our StreamSequenceCounter, wraps 0xFFFF -> 0 (6.2.1) + + // Sequence counter monitoring of the peer, purely a diagnostic (6.3.1) + bool peer_seq_valid; + uint16_t peer_device_id; + uint16_t peer_seq; + + // Counters, for the REST interface and the shutdown summary + uint64_t n_wrapped; + uint64_t n_unwrapped; + uint64_t n_dropped; + uint64_t n_seq_jumps; + uint64_t n_aggregated_ignored; // extra data messages in an aggregated TX message +} tCmpCodec; + +// Initialize the codec. config must not be NULL. +void cmpCodecInit(tCmpCodec *codec, const tCmpConfig *config); + +// Wrap one captured inner Ethernet frame as a Captured Data Message (CAP_DATA_MSG). +// +// frame/frame_len: complete Ethernet frame WITHOUT FCS, as delivered by xcplib's HAL +// contract (socket_raw_hal.h). A dummy zero FCS is appended here, +// because the Ethernet payload DATA runs from dst MAC through FCS (7.3.8). +// timestamp_ns: capture timestamp, nanoseconds +// in_sync: sets INSYNC; false when not synchronized to a time provider +// out/out_max: caller owned buffer for the complete CMP message +// +// Returns the CMP message length, or 0 if it does not fit into out_max. +// +// The envelope is applied into the CALLER's buffer, never into the transmit queue +// headroom: CMP must not participate in the zero copy path and must not influence +// XCPTL_TX_HEADROOM. The extra copy is accepted, this is a test bench path. +uint16_t cmpWrapCaptured(tCmpCodec *codec, const uint8_t *frame, uint16_t frame_len, uint64_t timestamp_ns, bool in_sync, uint8_t *out, uint16_t out_max); + +// Unwrap a received CMP message and extract the inner Ethernet frame of a Transmit Data +// Message (TX_DATA_MSG) addressed to our interface. The trailing FCS is stripped, so the +// result satisfies xcplib's "frames WITHOUT FCS" contract. +// +// Returns the inner frame length, or 0 if nothing was delivered; *result then says why. +// An aggregated message (6.3.2) delivers its first Ethernet payload and counts the rest +// in n_aggregated_ignored - the demo advertises AggregationCount = 1 over REST, so a +// conformant data sink does not aggregate. +uint16_t cmpUnwrapTransmit(tCmpCodec *codec, const uint8_t *in, uint16_t in_len, uint8_t *out, uint16_t out_max, tCmpResult *result); + +// Human readable form of a tCmpResult, for logging +const char *cmpResultName(tCmpResult result); diff --git a/examples/cmp_demo/src/cmp_backend.h b/examples/cmp_demo/src/cmp_backend.h new file mode 100644 index 00000000..27cad486 --- /dev/null +++ b/examples/cmp_demo/src/cmp_backend.h @@ -0,0 +1,74 @@ +#pragma once + +/*---------------------------------------------------------------------------- +| File: +| cmp_backend.h +| +| Description: +| Configuration and status of the CMP Ethernet HAL backend (socket_raw_hal_cmp.c). +| +| main.c configures the backend before XcpEthServerInit(), because that is what opens +| the transport. cmp_rest.c reads the status to answer the REST queries a Data Sink +| uses to find us and to discover that we support transmission. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include +#include + +// Largest CMP message the backend buffers. 6.4 allows jumbo frames up to a 9000 byte +// Ethernet MTU for both transport options; this is that plus a little slack. +#define CMP_MAX_MESSAGE 9216 +#define CMP_MAX_OUTER_MTU 9000 + +typedef struct { + // Capture module identity, see cmp.h + uint16_t device_id; + uint8_t stream_id; + uint32_t interface_id; + + // Outer transport, see cmp_transport.h + uint16_t local_port; // UDP port we listen on + const char *sink_ip; // Data Sink address, NULL or empty to learn it. Not copied. + uint16_t sink_port; + uint16_t outer_mtu; // MTU of the path to the Data Sink + + // MAC address of the emulated ECU, i.e. the source MAC of the frames xcplib builds. + // All zero: derive a locally administered one from device_id. + uint8_t ecu_mac[6]; +} tCmpBackendConfig; + +// Must be called before XcpEthServerInit(). config is copied except for sink_ip. +void cmpBackendConfigure(const tCmpBackendConfig *config); + +typedef struct { + bool open; // the transport is open + bool sink_known; // the Data Sink address is configured or has been learned + + char local_ip[16]; // empty while unknown + uint16_t local_port; + char sink_ip[16]; + uint16_t sink_port; + + uint16_t max_message; // largest CMP message the path carries un-fragmented + uint16_t max_inner_frame; // largest inner Ethernet frame that fits inside it + bool mtu_warning; // xcplib can produce frames larger than max_inner_frame + + uint8_t ecu_mac[6]; + uint16_t device_id; + uint8_t stream_id; + uint32_t interface_id; + + uint64_t n_wrapped; // captured frames sent to the Data Sink + uint64_t n_unwrapped; // transmit requests delivered to xcplib + uint64_t n_dropped; // CMP messages received but not for us + uint64_t n_seq_jumps; // gaps in the Data Sink's StreamSequenceCounter + uint64_t n_aggregated_ignored; // extra data messages in an aggregated request + uint64_t n_oversize; // frames refused because they exceed max_inner_frame +} tCmpBackendStatus; + +// Snapshot the backend status. Returns false before the transport is open. +// Counters are read without synchronisation: this is a test bench, and a torn count in a +// status page is not worth a lock on the transmit path. +bool cmpBackendGetStatus(tCmpBackendStatus *status); diff --git a/examples/cmp_demo/src/cmp_discovery.c b/examples/cmp_demo/src/cmp_discovery.c new file mode 100644 index 00000000..e80acf1c --- /dev/null +++ b/examples/cmp_demo/src/cmp_discovery.c @@ -0,0 +1,377 @@ +/*---------------------------------------------------------------------------- +| File: +| cmp_discovery.c +| +| Description: +| CMP_CM_DISCOVERY responder (12.1.1). See cmp_discovery.h. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include // struct sockaddr_ll, for the interface MAC +#else +#include // struct sockaddr_dl, LLADDR +#endif + +#include "cmp_discovery.h" + +//------------------------------------------------------------------------------- +// Wire format, 12.1.1 +// +// "All command fields are encoded in little endian byte order" (12.1), so the XCP header +// length, the ports and the string lengths are little endian. The address and MAC fields +// are byte arrays in "descending byte significance", i.e. 192.168.0.1 is 192,168,0,1. + +#define XCP_HEADER_LEN 4 // A_UINT16 length + A_UINT16 reserved +#define XCP_CMD_TL 0xF2 // CC_TRANSPORT_LAYER_CMD +#define XCP_SUB_DISCOVERY 0x10 +#define XCP_PID_RESPONSE 0xFF + +#define REQUEST_LEN 0x15 // 21 bytes after the XCP header, fixed by Table 78 +#define RESPONSE_FIXED 47 // bytes 0..46 of the response, before the two strings + +#define DISCOVERY_BUF_MAX 512 + +static int sFd = -1; +static tCmpDiscoveryConfig sConfig; +static uint64_t sCount = 0; + +//------------------------------------------------------------------------------- +// Little endian helpers. The demo already assumes a little endian host elsewhere, but +// these keep the wire format explicit rather than punning a struct over the buffer. + +static uint16_t get_u16_le(const uint8_t *p) { return (uint16_t)(p[0] | ((uint16_t)p[1] << 8)); } + +static void put_u16_le(uint8_t *p, uint16_t v) { + p[0] = (uint8_t)(v & 0xFF); + p[1] = (uint8_t)(v >> 8); +} + +// Append an A_UINT16 length followed by a zero terminated A_UTF8 string padded with 0x00 +// to a multiple of two bytes. +// +// The length counts the PADDED bytes, not the characters: 12.1.1's own example encodes +// "Dev1" as 44 65 76 31 00 00, six bytes, and the empty string as 00 00, two bytes. The +// offset column of Table 79 disagrees with itself by one about where the next field +// starts; the "N is length before" wording used for the identical construction in the +// status message payload (8.2.1) is what settles it. See the repository docs/XCP_DISCOVERY.md. +static size_t appendString(uint8_t *out, size_t out_max, size_t off, const char *s) { + if (s == NULL) { + s = ""; + } + size_t len = strlen(s) + 1; // include the zero termination + if (len % 2 != 0) { + len++; // pad to 16 bit + } + if (off + 2 + len > out_max) { + return 0; // does not fit + } + put_u16_le(out + off, (uint16_t)len); + off += 2; + memset(out + off, 0, len); + memcpy(out + off, s, strlen(s)); + return off + len; +} + +//------------------------------------------------------------------------------- +// Local address, prefix length and MAC +// +// The CMP transport binds INADDR_ANY, so the address a Data Sink should use is not known +// until somebody asks. Connecting a scratch UDP socket to the requester and reading back +// the local end is the portable way to ask the routing table "which of my addresses would +// you use to reach this peer" - no packet is sent by connect() on a datagram socket. + +static bool localAddrFor(const struct in_addr *peer, struct in_addr *local) { + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + return false; + } + struct sockaddr_in to; + memset(&to, 0, sizeof(to)); + to.sin_family = AF_INET; + to.sin_port = htons(CMP_DISCOVERY_PORT); + to.sin_addr = *peer; + bool ok = false; + if (connect(fd, (struct sockaddr *)&to, sizeof(to)) == 0) { + struct sockaddr_in me; + socklen_t len = sizeof(me); + if (getsockname(fd, (struct sockaddr *)&me, &len) == 0) { + *local = me.sin_addr; + ok = true; + } + } + close(fd); + return ok; +} + +// Prefix length and MAC of the interface that owns local_ip. Both are best effort: the +// prefix is informational and 12.1.1 makes the gateway explicitly optional. +static void interfaceInfoFor(const struct in_addr *local_ip, uint8_t *prefix_len, uint8_t *mac) { + *prefix_len = 0; + memset(mac, 0, 6); + + struct ifaddrs *ifa_list = NULL; + if (getifaddrs(&ifa_list) != 0) { + return; + } + + // First pass: find the interface carrying our address and take its netmask + char ifname[IF_NAMESIZE] = {0}; + for (struct ifaddrs *ifa = ifa_list; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET || ifa->ifa_netmask == NULL) { + continue; + } + const struct sockaddr_in *a = (const struct sockaddr_in *)(void *)ifa->ifa_addr; + if (a->sin_addr.s_addr != local_ip->s_addr) { + continue; + } + const struct sockaddr_in *m = (const struct sockaddr_in *)(void *)ifa->ifa_netmask; + uint32_t mask = ntohl(m->sin_addr.s_addr); + while (mask & 0x80000000u) { // count the leading ones + (*prefix_len)++; + mask <<= 1; + } + snprintf(ifname, sizeof(ifname), "%s", ifa->ifa_name); + break; + } + + // Second pass: the link layer address of that same interface + if (ifname[0] != '\0') { + for (struct ifaddrs *ifa = ifa_list; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL || strcmp(ifa->ifa_name, ifname) != 0) { + continue; + } +#if defined(__linux__) + if (ifa->ifa_addr->sa_family == AF_PACKET) { + const struct sockaddr_ll *ll = (const struct sockaddr_ll *)(void *)ifa->ifa_addr; + if (ll->sll_halen == 6) { + memcpy(mac, ll->sll_addr, 6); + break; + } + } +#else + if (ifa->ifa_addr->sa_family == AF_LINK) { + const struct sockaddr_dl *dl = (const struct sockaddr_dl *)(void *)ifa->ifa_addr; + if (dl->sdl_alen == 6) { + memcpy(mac, LLADDR(dl), 6); + break; + } + } +#endif + } + } + + freeifaddrs(ifa_list); +} + +//------------------------------------------------------------------------------- + +bool cmpDiscoveryStart(const tCmpDiscoveryConfig *config) { + + if (config == NULL) { + return false; + } + sConfig = *config; + + sFd = socket(AF_INET, SOCK_DGRAM, 0); + if (sFd < 0) { + printf("ERROR: cmpDiscoveryStart: socket failed (errno=%d, %s)\n", errno, strerror(errno)); + return false; + } + + int one = 1; + if (setsockopt(sFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) { + printf("WARNING: cmpDiscoveryStart: SO_REUSEADDR failed (errno=%d, %s)\n", errno, strerror(errno)); + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(CMP_DISCOVERY_PORT); + if (bind(sFd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + printf("ERROR: cmpDiscoveryStart: bind to UDP %u failed (errno=%d, %s)\n", CMP_DISCOVERY_PORT, errno, strerror(errno)); + close(sFd); + sFd = -1; + return false; + } + + // Join the group on EVERY multicast capable interface, not with imr_interface + // INADDR_ANY. "Any" does not mean "all": it lets the stack pick one interface from the + // routing table, and a capture module does not know which interface a Data Sink will + // appear on. On macOS an INADDR_ANY join receives nothing at all, and a join on a LAN + // interface does not see this host's own traffic - only a loopback join does, which is + // why loopback is deliberately included and is what makes a same-host test work. + int joined = 0; + struct ifaddrs *ifa_list = NULL; + if (getifaddrs(&ifa_list) == 0) { + for (struct ifaddrs *ifa = ifa_list; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET) { + continue; + } + if ((ifa->ifa_flags & IFF_UP) == 0 || (ifa->ifa_flags & IFF_MULTICAST) == 0) { + continue; + } + const struct sockaddr_in *a = (const struct sockaddr_in *)(void *)ifa->ifa_addr; + struct ip_mreq mreq; + memset(&mreq, 0, sizeof(mreq)); + mreq.imr_multiaddr.s_addr = inet_addr(CMP_DISCOVERY_GROUP); + mreq.imr_interface = a->sin_addr; + if (setsockopt(sFd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == 0) { + joined++; + } + // A failed join is normal: an interface may already be a member through + // another address, or may not route multicast. Only zero joins is fatal. + } + freeifaddrs(ifa_list); + } + + if (joined == 0) { + printf("ERROR: cmpDiscoveryStart: could not join %s on any interface\n", CMP_DISCOVERY_GROUP); + close(sFd); + sFd = -1; + return false; + } + + printf(" CMP discovery: listening on %s:%u (%d interface%s), advertising HTTP port %u\n", CMP_DISCOVERY_GROUP, CMP_DISCOVERY_PORT, joined, joined == 1 ? "" : "s", + sConfig.http_port); + return true; +} + +int cmpDiscoveryFd(void) { return sFd; } + +uint64_t cmpDiscoveryCount(void) { return sCount; } + +void cmpDiscoveryStop(void) { + if (sFd >= 0) { + close(sFd); + sFd = -1; + } +} + +//------------------------------------------------------------------------------- + +void cmpDiscoveryService(void) { + + if (sFd < 0) { + return; + } + + uint8_t in[DISCOVERY_BUF_MAX]; + struct sockaddr_in from; + socklen_t from_len = sizeof(from); + ssize_t n = recvfrom(sFd, in, sizeof(in), 0, (struct sockaddr *)&from, &from_len); + if (n < 0) { + return; + } + + // Ignore anything that is not a CMP_CM_DISCOVERY request. The group and port are + // shared with whatever else a tool multicasts, and 12.1.2 CMP_IP_ADDRESS_ASSIGNMENT + // arrives here too - we do not implement it, so it is dropped silently. + if ((size_t)n < XCP_HEADER_LEN + REQUEST_LEN) { + return; + } + if (get_u16_le(in) != REQUEST_LEN) { + return; + } + const uint8_t *cmd = in + XCP_HEADER_LEN; + if (cmd[0] != XCP_CMD_TL || cmd[1] != XCP_SUB_DISCOVERY) { + return; + } + if (cmd[20] & 0x01) { + printf("WARNING: cmpDiscoveryService: IPv6 requested, this demo is IPv4 only\n"); + return; + } + + // 12.1.1: "A Capture Module shall send its response to the multicast address and port + // given in the command request." The request carries both, so the responder never has + // to know anything about the network it sits on. + uint16_t reply_port = get_u16_le(cmd + 2); + struct sockaddr_in to; + memset(&to, 0, sizeof(to)); + to.sin_family = AF_INET; + to.sin_port = htons(reply_port); + memcpy(&to.sin_addr.s_addr, cmd + 4, 4); // already in network byte order + if (to.sin_addr.s_addr == 0 || reply_port == 0) { + // Not covered by the spec. Answering the sender is more useful than dropping the + // request, and it is what a tool that left the field empty most likely wants. + to.sin_addr = from.sin_addr; + if (reply_port == 0) { + to.sin_port = from.sin_port; + } + } + + // Our own address, as seen from the peer that asked + struct in_addr local; + if (!localAddrFor(&from.sin_addr, &local)) { + local.s_addr = 0; // 12.1.1: "shall be set to 0.0.0.0" when there is no valid address + } + uint8_t prefix_len = 0; + uint8_t mac[6]; + interfaceInfoFor(&local, &prefix_len, mac); + + // Build the positive response, Table 79 + uint8_t out[DISCOVERY_BUF_MAX]; + memset(out, 0, RESPONSE_FIXED + XCP_HEADER_LEN); + uint8_t *rsp = out + XCP_HEADER_LEN; + rsp[0] = XCP_PID_RESPONSE; + rsp[1] = XCP_SUB_DISCOVERY; + memcpy(rsp + 2, &local.s_addr, 4); // 2..17, IPv4 uses the first four bytes + rsp[18] = prefix_len; + // 19..34 gateway: "This value is optional. In such a case the address 0.0.0.0 is + // used." Reading the default route is not portable enough to be worth it here. + memcpy(rsp + 35, mac, 6); + rsp[41] = 0x00; // IP version: IPv4 + rsp[42] = 0x00; // reserved + put_u16_le(rsp + 43, sConfig.http_port); + + size_t off = appendString(rsp, sizeof(out) - XCP_HEADER_LEN, 45, sConfig.description); + if (off == 0) { + printf("WARNING: cmpDiscoveryService: DeviceDescription does not fit, not answering\n"); + return; + } + off = appendString(rsp, sizeof(out) - XCP_HEADER_LEN, off, sConfig.serial); + if (off == 0) { + printf("WARNING: cmpDiscoveryService: SerialNumber does not fit, not answering\n"); + return; + } + + // XCP header: length of everything after it, then the reserved word + put_u16_le(out, (uint16_t)off); + put_u16_le(out + 2, 0); + + // A multicast response has to leave through the interface the request came in on, + // otherwise the stack sends it out the default route and the asking tool never sees + // it. local is exactly that interface's address. + if (IN_MULTICAST(ntohl(to.sin_addr.s_addr)) && local.s_addr != 0) { + if (setsockopt(sFd, IPPROTO_IP, IP_MULTICAST_IF, &local, sizeof(local)) < 0) { + printf("WARNING: cmpDiscoveryService: IP_MULTICAST_IF failed (errno=%d, %s)\n", errno, strerror(errno)); + } + } + + if (sendto(sFd, out, XCP_HEADER_LEN + off, 0, (struct sockaddr *)&to, sizeof(to)) < 0) { + printf("WARNING: cmpDiscoveryService: sending the response failed (errno=%d, %s)\n", errno, strerror(errno)); + return; + } + + sCount++; + + char from_s[INET_ADDRSTRLEN] = {0}; + char to_s[INET_ADDRSTRLEN] = {0}; + char local_s[INET_ADDRSTRLEN] = {0}; + inet_ntop(AF_INET, &from.sin_addr, from_s, sizeof(from_s)); + inet_ntop(AF_INET, &to.sin_addr, to_s, sizeof(to_s)); + inet_ntop(AF_INET, &local, local_s, sizeof(local_s)); + printf(" CMP discovery: request from %s, answered to %s:%u with %s/%u, HTTP port %u\n", from_s, to_s, ntohs(to.sin_port), local_s, prefix_len, sConfig.http_port); +} diff --git a/examples/cmp_demo/src/cmp_discovery.h b/examples/cmp_demo/src/cmp_discovery.h new file mode 100644 index 00000000..341648fc --- /dev/null +++ b/examples/cmp_demo/src/cmp_discovery.h @@ -0,0 +1,64 @@ +#pragma once + +/*---------------------------------------------------------------------------- +| File: +| cmp_discovery.h +| +| Description: +| IP multicast based discovery of this emulated Capture Module (12.1.1). +| +| Section 12 requires a Capture Module to support AT LEAST ONE of three approaches to +| address configuration and discovery: +| - static configuration without any discovery (what this demo did before) +| - the XCP based approach of 12.1 <- implemented here +| - Multicast DNS / DNS-SD of 12.2 (not implemented, see the repository docs/XCP_DISCOVERY.md) +| +| 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. It is answered before, and independently +| of, any XCP session - a Data Sink uses it to learn our IP address and, decisively, the +| HTTP port of the REST interface it then configures us through. +| +| Deliberately NOT in libxcplite: the response advertises an HTTP port for a REST +| interface, which is a CMP concept the library has no business knowing. The library's +| own multicast code is also unusable here - docs/SOCKET_RAW.md excludes +| XCPTL_ENABLE_MULTICAST from the raw transport because socketJoin is not implemented +| there. This is its own socket, its own group, its own datagram. +| +| No thread of its own: the socket is serviced by the REST thread, which is already in a +| poll loop and already knows the HTTP port that the response has to carry. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include +#include + +// The group and port are fixed by 12.1: IPv4 destination 239.255.0.0, UDP destination +// port 5556. The spec notes that the port was never registered with IANA and that XCP +// uses it "because a private and closed network is assumed". +#define CMP_DISCOVERY_GROUP "239.255.0.0" +#define CMP_DISCOVERY_PORT 5556 + +typedef struct { + uint16_t http_port; // REST port to advertise (12.3), the point of the whole exchange + const char *description; // DeviceDescription, not copied, must outlive the responder + const char *serial; // SerialNumber, not copied +} tCmpDiscoveryConfig; + +// Open the discovery socket and join the multicast group. +// Returns false if the port cannot be bound or the group cannot be joined; the demo then +// runs without discovery, which 12 still permits as "static configuration". +bool cmpDiscoveryStart(const tCmpDiscoveryConfig *config); + +// The socket, for the caller's poll(). Returns -1 when discovery is not running. +int cmpDiscoveryFd(void); + +// Read and answer one datagram. Call when cmpDiscoveryFd() is readable. +void cmpDiscoveryService(void); + +// Close the socket. Safe to call if discovery was never started. +void cmpDiscoveryStop(void); + +// Number of discovery requests answered so far, for the test script and the log. +uint64_t cmpDiscoveryCount(void); diff --git a/examples/cmp_demo/src/cmp_rest.c b/examples/cmp_demo/src/cmp_rest.c new file mode 100644 index 00000000..fe9c052a --- /dev/null +++ b/examples/cmp_demo/src/cmp_rest.c @@ -0,0 +1,343 @@ +/*---------------------------------------------------------------------------- +| File: +| cmp_rest.c +| +| Description: +| Minimal HTTP/1.1 server for the read only part of the ASAM CMP REST interface (12.3). +| One thread, one connection at a time, Connection: close. See cmp_rest.h. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // for THREAD_HANDLE, create_thread, join_thread, sleepMs + +#include "cmp.h" +#include "cmp_backend.h" +#include "cmp_discovery.h" +#include "cmp_rest.h" + +#define REST_REQUEST_MAX 2048 +#define REST_BODY_MAX 4096 +#define REST_ACCEPT_POLL_MS 200 + +// Identification of this capture module (12.3.2, Table 84) +// VendorId 0 is not an ASAM registered vendor: this is an emulation, not a product. +#define REST_VENDOR_ID 0 +#define REST_DEVICE_DESCRIPTION "XCPlite cmp_demo, emulated ASAM CMP capture module" +#define REST_HARDWARE_VERSION "none (software emulation)" +#define REST_SOFTWARE_VERSION "cmp_demo 1.0.0" + +// Transport option for reception of Control and Transmit Data Messages (12.3.2, Table 84) +#define REST_TRANSPORT_UDP_IPV4 1 + +// Feature Support Bitmask for ETHERNET_DATA_MSG (8.3.1.2, Table 62). +// Capture: bits 0-3 and 7 are the mandatory flags and are fixed to 1; bits 4-6 +// (FRAME_TOO_LONG_ERR, PHY_ERR, FRAME_TRUNCATED) are optional and we detect none of them. +#define REST_ETH_FEATURES_CAP 0x0000008Fu +// Transmit: only bit 8 FCS_SENDING is definable and we do not support a tool defined FCS. +#define REST_ETH_FEATURES_TX 0x00000000u + +// Transmission Support Bitmask (12.3.4, Table 89). +// Bit 0 TIMESTAMP_IMMEDIATE only: we send every request straight away and support neither +// absolute nor relative scheduling, no deadline and no segmentation. 7.2.2 explicitly +// allows that - "If the CM does not support Timestamp, it shall always send immediately". +#define REST_TRANSMISSION_SUPPORT 0x00000001u + +#define REST_INTERFACE_STATUS_UP 0x01 // 7.3.16, Table 56 + +//------------------------------------------------------------------------------- + +static THREAD_HANDLE sThread; +static bool sRunning = false; +static volatile bool sThreadUp = false; +static volatile bool sStop = false; +static int sListenFd = -1; + +//------------------------------------------------------------------------------- +// Response bodies + +static int bodyVersionInfo(char *buf, size_t size) { + // 12.3.1: CmpVersion is the CMP major version, ApiVersion 0x01 means {apiVersion} = v1 + return snprintf(buf, size, "{\"CmpVersion\":%u,\"ApiVersion\":1}", CMP_VERSION); +} + +static int bodyIdentification(char *buf, size_t size, const tCmpBackendStatus *s) { + return snprintf(buf, size, + "{" + "\"VendorId\":%u," + "\"DeviceDescription\":\"%s\"," + "\"SerialNumber\":\"cmp_demo-%04X\"," + "\"HardwareVersion\":\"%s\"," + "\"SoftwareVersion\":\"%s\"," + "\"DeviceId\":%u," + "\"CmpListeningTransportOption\":%u," + "\"CmpListeningMac\":\"\"," + "\"CmpListeningIP\":\"%s\"," + "\"CmpListeningPort\":%u" + "}", + REST_VENDOR_ID, REST_DEVICE_DESCRIPTION, s->device_id, REST_HARDWARE_VERSION, REST_SOFTWARE_VERSION, s->device_id, REST_TRANSPORT_UDP_IPV4, s->local_ip, + s->local_port); +} + +static int bodyInterfaces(char *buf, size_t size, const tCmpBackendStatus *s) { + // One interface carrying one stream. DataMessagePayloadType 0x08 is ETHERNET_DATA_MSG: + // the interface captures and transmits complete Ethernet frames. + // + // The Transmitter object is what tells the Data Sink that injection is possible + // (7.2.2). AggregationMtu is the largest CMP message we can accept, which on this path + // is bounded by the outer MTU because 6.4.2 forbids IP fragmentation; AggregationCount + // is 1 because one CMP message carries exactly one frame. + return snprintf(buf, size, + "{\"Interfaces\":[{" + "\"InterfaceId\":%u," + "\"DataMessagePayloadType\":%u," + "\"InterfaceStatus\":%u," + "\"InterfaceDescription\":\"Emulated XCP ECU link\"," + "\"FeatureSupportBitmask\":%u," + "\"Streams\":[{" + "\"StreamId\":%u," + "\"StreamDescription\":\"Captured ECU traffic\"," + "\"SupportForDataSinkReadyToReceive\":false," + "\"SinkDeviceId\":0," + "\"TransportOption\":%u," + "\"DestinationMac\":\"\"," + "\"DestinationIp\":\"%s\"," + "\"DestinationPort\":%u," + "\"Mtu\":%u" + "}]," + "\"Transmitter\":{" + "\"TransmissionSupportBitmask\":%u," + "\"FeatureSupportBitmask\":%u," + "\"AggregationMtu\":%u," + "\"AggregationCount\":1" + "}" + "}]}", + s->interface_id, CMP_PAYLOAD_ETHERNET, REST_INTERFACE_STATUS_UP, REST_ETH_FEATURES_CAP, s->stream_id, REST_TRANSPORT_UDP_IPV4, s->sink_ip, s->sink_port, + s->max_message, REST_TRANSMISSION_SUPPORT, REST_ETH_FEATURES_TX, s->max_message); +} + +static int bodyMeasurement(char *buf, size_t size, const tCmpBackendStatus *s) { + // 12.3.6, Table 96/97. The demo starts capturing on its own, so the stream is + // "transmitting" as soon as a Data Sink address is known and "inactive" before that - + // there is nowhere to send yet. We never enter inactive_error. + return snprintf(buf, size, + "{" + "\"CaptureModuleState\":\"active\"," + "\"Message\":\"captured %llu, transmitted %llu, dropped %llu\"," + "\"StateOfStreams\":[{\"StreamId\":%u,\"State\":\"%s\"}]" + "}", + (unsigned long long)s->n_wrapped, (unsigned long long)s->n_unwrapped, (unsigned long long)s->n_dropped, s->stream_id, + s->sink_known ? "transmitting" : "inactive"); +} + +//------------------------------------------------------------------------------- + +static void sendResponse(int fd, int status, const char *reason, const char *content_type, const char *body, size_t body_len) { + char header[256]; + int n = snprintf(header, sizeof(header), + "HTTP/1.1 %d %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %zu\r\n" + "Cache-Control: no-store\r\n" + "Connection: close\r\n" + "\r\n", + status, reason, content_type, body_len); + if (n <= 0) { + return; + } + if (write(fd, header, (size_t)n) < 0) { + return; + } + if (body_len > 0 && write(fd, body, body_len) < 0) { + return; + } +} + +static void sendJson(int fd, const char *body, int body_len) { + if (body_len < 0) { + sendResponse(fd, 500, "Internal Server Error", "text/plain", "response too large\n", 19); + return; + } + sendResponse(fd, 200, "OK", "application/json", body, (size_t)body_len); +} + +static void handleRequest(int fd) { + + char request[REST_REQUEST_MAX]; + ssize_t n = read(fd, request, sizeof(request) - 1); + if (n <= 0) { + return; + } + request[n] = 0; + + // Only the request line matters: " HTTP/1.x" + char method[16] = {0}; + char path[256] = {0}; + if (sscanf(request, "%15s %255s", method, path) != 2) { + sendResponse(fd, 400, "Bad Request", "text/plain", "malformed request line\n", 23); + return; + } + // Ignore a query string, none of these methods take parameters + char *query = strchr(path, '?'); + if (query != NULL) { + *query = 0; + } + + if (strcmp(method, "GET") != 0) { + // Everything that would change configuration is deliberately absent, see cmp_rest.h + sendResponse(fd, 405, "Method Not Allowed", "text/plain", "this capture module is read only\n", 33); + return; + } + + tCmpBackendStatus status; + if (!cmpBackendGetStatus(&status)) { + sendResponse(fd, 503, "Service Unavailable", "text/plain", "capture module not started yet\n", 31); + return; + } + + char body[REST_BODY_MAX]; + if (strcmp(path, "/asam-cmp/version-info") == 0) { + sendJson(fd, body, bodyVersionInfo(body, sizeof(body))); + } else if (strcmp(path, "/asam-cmp/v1/identification") == 0) { + sendJson(fd, body, bodyIdentification(body, sizeof(body), &status)); + } else if (strcmp(path, "/asam-cmp/v1/interfaces") == 0) { + sendJson(fd, body, bodyInterfaces(body, sizeof(body), &status)); + } else if (strcmp(path, "/asam-cmp/v1/measurement") == 0) { + sendJson(fd, body, bodyMeasurement(body, sizeof(body), &status)); + } else { + sendResponse(fd, 404, "Not Found", "text/plain", + "Implemented: /asam-cmp/version-info, /asam-cmp/v1/identification,\n" + " /asam-cmp/v1/interfaces, /asam-cmp/v1/measurement\n", + 125); + } +} + +// This thread services the whole control plane of the capture module: the HTTP listener +// and, when it is running, the CMP discovery socket (12.1.1). Discovery gets no thread of +// its own because it is stateless, answers one datagram at a time, and has to advertise +// the very HTTP port this thread serves. +static THREAD_FUNC_RETURN restThread(void *arg) { + (void)arg; + sThreadUp = true; + while (!sStop) { + struct pollfd pfd[2]; + pfd[0] = (struct pollfd){.fd = sListenFd, .events = POLLIN, .revents = 0}; + nfds_t nfds = 1; + int discovery_fd = cmpDiscoveryFd(); + if (discovery_fd >= 0) { + pfd[1] = (struct pollfd){.fd = discovery_fd, .events = POLLIN, .revents = 0}; + nfds = 2; + } + int r = poll(pfd, nfds, REST_ACCEPT_POLL_MS); + if (r < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (r == 0) { + continue; // timeout, re-check sStop + } + if (nfds == 2 && (pfd[1].revents & POLLIN) != 0) { + cmpDiscoveryService(); + } + if ((pfd[0].revents & POLLIN) == 0) { + continue; // nothing to accept + } + int fd = accept(sListenFd, NULL, NULL); + if (fd < 0) { + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) { + continue; + } + break; + } + handleRequest(fd); + close(fd); + } + THREAD_FUNC_END; +} + +//------------------------------------------------------------------------------- + +bool cmpRestStart(uint16_t port) { + + if (sRunning) { + return true; + } + sStop = false; + + sListenFd = socket(AF_INET, SOCK_STREAM, 0); + if (sListenFd < 0) { + printf("ERROR: cmpRestStart: socket failed (errno=%d, %s)\n", errno, strerror(errno)); + return false; + } + int one = 1; + if (setsockopt(sListenFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) { + printf("WARNING: cmpRestStart: SO_REUSEADDR failed (errno=%d, %s)\n", errno, strerror(errno)); + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(port); + if (bind(sListenFd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + printf("ERROR: cmpRestStart: bind to TCP port %u failed (errno=%d, %s)%s\n", port, errno, strerror(errno), + (errno == EACCES) ? "\n Ports below 1024 need privileges; 12.3 only says the REST interface SHOULD use port 80." : ""); + close(sListenFd); + sListenFd = -1; + return false; + } + if (listen(sListenFd, 4) < 0) { + printf("ERROR: cmpRestStart: listen failed (errno=%d, %s)\n", errno, strerror(errno)); + close(sListenFd); + sListenFd = -1; + return false; + } + + // create_thread() returns 0 on success on POSIX and Windows alike, but it still cannot + // be tested PORTABLY: both FreeRTOS variants are statements which assert, so + // "if (create_thread(...))" does not compile there. Wait for the thread to announce + // itself instead - portable, and it proves the thread is running rather than merely + // created. That matters here because the listen socket is already bound at this point, + // so a thread that never starts would still accept connections at the kernel backlog + // and then answer none of them, which looks like a hang rather than an error. + sThreadUp = false; + create_thread(&sThread, NULL, restThread, NULL); + for (int i = 0; i < 100 && !sThreadUp; i++) { + sleepMs(2); + } + if (!sThreadUp) { + printf("ERROR: cmpRestStart: the REST thread did not start\n"); + close(sListenFd); + sListenFd = -1; + return false; + } + + sRunning = true; + printf(" CMP REST interface: http://:%u/asam-cmp/version-info\n", port); + return true; +} + +void cmpRestStop(void) { + if (!sRunning) { + return; + } + sStop = true; + join_thread(sThread); + if (sListenFd >= 0) { + close(sListenFd); + sListenFd = -1; + } + sRunning = false; +} diff --git a/examples/cmp_demo/src/cmp_rest.h b/examples/cmp_demo/src/cmp_rest.h new file mode 100644 index 00000000..47c16e3d --- /dev/null +++ b/examples/cmp_demo/src/cmp_rest.h @@ -0,0 +1,40 @@ +#pragma once + +/*---------------------------------------------------------------------------- +| File: +| cmp_rest.h +| +| Description: +| Minimal read only REST interface of the emulated Capture Module (12.3). +| +| A Data Sink uses it to find us and, decisively for this demo, to discover that we +| support transmission: 7.2.2 says "Support for transmission is optional in the Capture +| Module, the data sink can use the REST API to detect if transmission is supported or +| not". That detection is the Transmitter object of GET /asam-cmp/v1/interfaces +| (12.3.4, Table 88) - without it a tool may never send us a Transmit Data Message and +| XCP could never connect through the tunnel. +| +| Implemented (all read only): +| 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 +| +| Not implemented: everything that changes configuration (PUT), time synchronisation +| (12.3.3, we are never synchronised), mDNS/DNS-SD discovery (12.2.2) and the XCP based +| discovery of 12.1. Section 12 permits "Static configuration without Capture Module +| Discovery", which is what this demo uses. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include +#include + +// Start the REST server on its own thread. 12.3 says the interface "should run on +// standard HTTP port 80"; the demo defaults to 8080 so it needs no privileges. +// Returns false if the port cannot be bound. +bool cmpRestStart(uint16_t port); + +// Stop the REST server and join its thread. Safe to call if it was never started. +void cmpRestStop(void); diff --git a/examples/cmp_demo/src/cmp_transport.h b/examples/cmp_demo/src/cmp_transport.h new file mode 100644 index 00000000..dcdcb8f4 --- /dev/null +++ b/examples/cmp_demo/src/cmp_transport.h @@ -0,0 +1,68 @@ +#pragma once + +/*---------------------------------------------------------------------------- +| File: +| cmp_transport.h +| +| Description: +| Outer transport for CMP messages, i.e. how a complete CMP message reaches the +| Data Sink and back. Kept behind this seam so the envelope codec in cmp.c stays +| independent of it. +| +| The specification defines two transport options (6.4): +| 6.4.1 IEEE 802.3 Ethernet frames, EtherType 0x99FE - MANDATORY for a Capture Module +| 6.4.2 UDP, destination IP and port configurable - OPTIONAL +| +| Only the UDP option is implemented (cmp_transport_udp.c). It needs no AF_PACKET, no +| CAP_NET_RAW and no root, and it is portable. The Ethernet option is the next step; +| the AF_PACKET plumbing it needs is preserved in git commit 01e7f40 +| (examples/cmp_demo/src/socket_raw_hal_cmp.c), which used it for the pass through +| version of this backend. It additionally needs an outer Ethernet header +| (dst = sink MAC, src = our MAC, EtherType 0x99FE) and padding to the 60 byte +| Ethernet minimum (6.4.1). +| +| Note 6.4.2: "CMP messages shall not be sent over IP fragmented packets." The largest +| CMP message the outer path can carry is therefore a hard limit, not a soft one - see +| cmpTransportMaxMessage(). +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include +#include +#include + +// Opaque transport instance +typedef struct cmp_transport tCmpTransport; + +typedef struct { + uint16_t local_port; // UDP port to listen on for Transmit Data and Control Messages + const char *sink_ip; // Data Sink IPv4 address, dotted quad. NULL or empty: learn it + // from the source of the first CMP message we receive. + uint16_t sink_port; // Data Sink UDP port + uint16_t outer_mtu; // MTU of the path to the Data Sink, for the size budget +} tCmpTransportConfig; + +// Open the transport. Returns true on success, *transport is then valid until close. +bool cmpTransportOpen(const tCmpTransportConfig *config, tCmpTransport **transport); +void cmpTransportClose(tCmpTransport *transport); + +// Send one complete CMP message to the Data Sink. +// Returns len on success, 0 if the sink address is not known yet (not an error: the demo +// simply has nowhere to send until the sink announces itself), or -1 on error. +int32_t cmpTransportSend(tCmpTransport *transport, const uint8_t *msg, uint16_t len); + +// Receive one complete CMP message, blocking with timeout. +// Returns > 0 bytes received, 0 on timeout or wakeup, -1 on a fatal error. +int32_t cmpTransportRecv(tCmpTransport *transport, uint8_t *msg, uint16_t max_len, uint32_t timeout_ms); + +// Abort a blocked cmpTransportRecv(). May be called from any thread. +void cmpTransportWakeup(tCmpTransport *transport); + +// Largest CMP message this path can carry without IP fragmentation (6.4.2). +uint16_t cmpTransportMaxMessage(const tCmpTransport *transport); + +// Endpoint information for the REST identification response (12.3.2) and logging. +// ip must be at least 16 bytes. Returns false if the endpoint is not known yet. +bool cmpTransportGetLocal(const tCmpTransport *transport, char *ip, size_t ip_size, uint16_t *port); +bool cmpTransportGetSink(const tCmpTransport *transport, char *ip, size_t ip_size, uint16_t *port); diff --git a/examples/cmp_demo/src/cmp_transport_udp.c b/examples/cmp_demo/src/cmp_transport_udp.c new file mode 100644 index 00000000..47839ac2 --- /dev/null +++ b/examples/cmp_demo/src/cmp_transport_udp.c @@ -0,0 +1,324 @@ +/*---------------------------------------------------------------------------- +| File: +| cmp_transport_udp.c +| +| Description: +| CMP over UDP (6.4.2). One ordinary AF_INET/SOCK_DGRAM socket: the kernel does the +| outer IPv4/UDP and the ARP for the path to the Data Sink, so this needs no AF_PACKET, +| no CAP_NET_RAW and no root. +| +| Do not confuse the two UDP layers. The INNER Ethernet/IPv4/UDP frame is built by +| xcplib's raw transport and is what the Data Sink decodes out of the CMP payload - it +| is the traffic of the emulated ECU. The OUTER UDP datagram implemented here is the +| measurement network between capture module and Data Sink. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include // for inet_pton, inet_ntop, htons +#include // for errno +#include // for fcntl, O_NONBLOCK +#include // for sockaddr_in +#include // for poll +#include // for printf, snprintf +#include // for malloc, free +#include // for memset, memcpy, strerror +#include // for socket, bind, recvfrom, sendto +#include // for close, read, write + +#include "cmp_transport.h" + +// IPv4 header + UDP header. Subtracted from the path MTU to get the largest CMP message +// that fits into one un-fragmented datagram (6.4.2). +#define IP4_UDP_HDR_LEN 28 + +struct cmp_transport { + int fd; // the UDP socket + int wakeup_rd; // self pipe, portable equivalent of an eventfd + int wakeup_wr; + uint16_t local_port; + uint16_t max_message; // largest CMP message this path can carry + + bool sink_known; + bool sink_learned; // adopted from a received message rather than configured + struct sockaddr_in sink; +}; + +//------------------------------------------------------------------------------- + +static bool setNonBlocking(int fd) { + int flags = fcntl(fd, F_GETFL, 0); + return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) >= 0; +} + +static void formatAddr(const struct sockaddr_in *addr, char *ip, size_t ip_size, uint16_t *port) { + if (ip != NULL && ip_size > 0) { + if (inet_ntop(AF_INET, &addr->sin_addr, ip, (socklen_t)ip_size) == NULL) { + ip[0] = 0; + } + } + if (port != NULL) { + *port = ntohs(addr->sin_port); + } +} + +//------------------------------------------------------------------------------- + +bool cmpTransportOpen(const tCmpTransportConfig *config, tCmpTransport **transportp) { + + if (config == NULL || transportp == NULL) { + return false; + } + *transportp = NULL; + + if (config->outer_mtu <= IP4_UDP_HDR_LEN) { + printf("ERROR: cmpTransportOpen: outer MTU %u is too small\n", config->outer_mtu); + return false; + } + + tCmpTransport *t = (tCmpTransport *)malloc(sizeof(tCmpTransport)); + if (t == NULL) { + printf("ERROR: cmpTransportOpen: out of memory\n"); + return false; + } + memset(t, 0, sizeof(*t)); + t->fd = -1; + t->wakeup_rd = -1; + t->wakeup_wr = -1; + t->local_port = config->local_port; + t->max_message = (uint16_t)(config->outer_mtu - IP4_UDP_HDR_LEN); + + if (config->sink_ip != NULL && config->sink_ip[0] != 0) { + t->sink.sin_family = AF_INET; + t->sink.sin_port = htons(config->sink_port); + if (inet_pton(AF_INET, config->sink_ip, &t->sink.sin_addr) != 1) { + printf("ERROR: cmpTransportOpen: '%s' is not a valid IPv4 address\n", config->sink_ip); + goto error; + } + if (config->sink_port == 0) { + printf("ERROR: cmpTransportOpen: a Data Sink port is required with a Data Sink address\n"); + goto error; + } + t->sink_known = true; + } + + t->fd = socket(AF_INET, SOCK_DGRAM, 0); + if (t->fd < 0) { + printf("ERROR: cmpTransportOpen: socket failed (errno=%d, %s)\n", errno, strerror(errno)); + goto error; + } + + int one = 1; + if (setsockopt(t->fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) { + printf("WARNING: cmpTransportOpen: SO_REUSEADDR failed (errno=%d, %s)\n", errno, strerror(errno)); + } + + struct sockaddr_in local; + memset(&local, 0, sizeof(local)); + local.sin_family = AF_INET; + local.sin_addr.s_addr = htonl(INADDR_ANY); + local.sin_port = htons(config->local_port); + if (bind(t->fd, (struct sockaddr *)&local, sizeof(local)) < 0) { + printf("ERROR: cmpTransportOpen: bind to UDP port %u failed (errno=%d, %s)\n", config->local_port, errno, strerror(errno)); + goto error; + } + if (config->local_port == 0) { // ephemeral, find out which one we got + socklen_t len = sizeof(local); + if (getsockname(t->fd, (struct sockaddr *)&local, &len) == 0) { + t->local_port = ntohs(local.sin_port); + } + } + + // Self pipe for eth_hal_wakeup(). A pipe rather than an eventfd so this file stays + // portable; the write end is only ever poked with a single byte. + int pipefd[2]; + if (pipe(pipefd) < 0) { + printf("ERROR: cmpTransportOpen: pipe failed (errno=%d, %s)\n", errno, strerror(errno)); + goto error; + } + t->wakeup_rd = pipefd[0]; + t->wakeup_wr = pipefd[1]; + if (!setNonBlocking(t->wakeup_rd) || !setNonBlocking(t->wakeup_wr)) { + printf("WARNING: cmpTransportOpen: could not make the wakeup pipe non blocking\n"); + } + + *transportp = t; + return true; + +error: + cmpTransportClose(t); + return false; +} + +void cmpTransportClose(tCmpTransport *t) { + if (t == NULL) { + return; + } + if (t->fd >= 0) { + close(t->fd); + } + if (t->wakeup_rd >= 0) { + close(t->wakeup_rd); + } + if (t->wakeup_wr >= 0) { + close(t->wakeup_wr); + } + free(t); +} + +//------------------------------------------------------------------------------- + +int32_t cmpTransportSend(tCmpTransport *t, const uint8_t *msg, uint16_t len) { + + if (t == NULL || msg == NULL) { + return -1; + } + if (!t->sink_known) { + // Nothing to send to yet. Not an error: with a learned sink address the capture + // direction simply stays quiet until the Data Sink first speaks to us. + return 0; + } + + for (;;) { + ssize_t n = sendto(t->fd, msg, len, 0, (struct sockaddr *)&t->sink, sizeof(t->sink)); + if (n < 0) { + if (errno == EINTR) { + continue; + } + if (errno == EMSGSIZE) { + printf("ERROR: cmpTransportSend: CMP message of %u bytes exceeds the path MTU\n", len); + return -1; + } + printf("ERROR: cmpTransportSend: sendto failed (errno=%d, %s)\n", errno, strerror(errno)); + return -1; + } + if (n != (ssize_t)len) { + printf("ERROR: cmpTransportSend: partial send %zd of %u bytes\n", n, len); + return -1; + } + return (int32_t)len; + } +} + +int32_t cmpTransportRecv(tCmpTransport *t, uint8_t *msg, uint16_t max_len, uint32_t timeout_ms) { + + if (t == NULL || msg == NULL) { + return -1; + } + + struct pollfd pfd[2]; + pfd[0].fd = t->fd; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + pfd[1].fd = t->wakeup_rd; + pfd[1].events = POLLIN; + pfd[1].revents = 0; + + int r = poll(pfd, 2, (int)timeout_ms); + if (r < 0) { + if (errno == EINTR) { + return 0; // the caller re-evaluates its own deadline + } + printf("ERROR: cmpTransportRecv: poll failed (errno=%d, %s)\n", errno, strerror(errno)); + return -1; + } + if (r == 0) { + return 0; // timeout + } + + if ((pfd[1].revents & POLLIN) != 0) { // wakeup requested, drain and report no message + uint8_t drain[64]; + while (read(t->wakeup_rd, drain, sizeof(drain)) > 0) { + } + return 0; + } + if ((pfd[0].revents & POLLIN) == 0) { + return 0; + } + + struct sockaddr_in from; + socklen_t fromlen = sizeof(from); + memset(&from, 0, sizeof(from)); + ssize_t n = recvfrom(t->fd, msg, max_len, 0, (struct sockaddr *)&from, &fromlen); + if (n < 0) { + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + printf("ERROR: cmpTransportRecv: recvfrom failed (errno=%d, %s)\n", errno, strerror(errno)); + return -1; + } + + // Learn where the Data Sink lives, if it was not configured. Transmission requests are + // sent unicast to the Capture Module (7.2.2), so the source of one is a usable reply + // address - the same trick socket_raw.c uses to learn the peer MAC. + if (!t->sink_known && from.sin_family == AF_INET) { + t->sink = from; + t->sink_known = true; + t->sink_learned = true; + char ip[16] = {0}; + uint16_t port = 0; + formatAddr(&t->sink, ip, sizeof(ip), &port); + printf(" CMP: Data Sink learned as %s:%u\n", ip, port); + } + + return (int32_t)n; +} + +void cmpTransportWakeup(tCmpTransport *t) { + if (t == NULL || t->wakeup_wr < 0) { + return; + } + uint8_t one = 1; + ssize_t nw = write(t->wakeup_wr, &one, 1); + (void)nw; // a full pipe already means a wakeup is pending +} + +//------------------------------------------------------------------------------- + +uint16_t cmpTransportMaxMessage(const tCmpTransport *t) { return (t != NULL) ? t->max_message : 0; } + +bool cmpTransportGetSink(const tCmpTransport *t, char *ip, size_t ip_size, uint16_t *port) { + if (t == NULL || !t->sink_known) { + return false; + } + formatAddr(&t->sink, ip, ip_size, port); + return true; +} + +bool cmpTransportGetLocal(const tCmpTransport *t, char *ip, size_t ip_size, uint16_t *port) { + + if (t == NULL) { + return false; + } + if (port != NULL) { + *port = t->local_port; + } + if (ip == NULL || ip_size == 0) { + return true; + } + ip[0] = 0; + + // We bind to INADDR_ANY, so there is no single local address to report. Ask the routing + // table which source address would be used towards the Data Sink: connecting a throwaway + // datagram socket sends nothing, it only fixes the source. Without a known sink there is + // nothing sensible to report and CmpListeningIP stays empty. + if (!t->sink_known) { + return false; + } + int s = socket(AF_INET, SOCK_DGRAM, 0); + if (s < 0) { + return false; + } + bool ok = false; + if (connect(s, (const struct sockaddr *)&t->sink, sizeof(t->sink)) == 0) { + struct sockaddr_in local; + socklen_t len = sizeof(local); + memset(&local, 0, sizeof(local)); + if (getsockname(s, (struct sockaddr *)&local, &len) == 0) { + formatAddr(&local, ip, ip_size, NULL); + ok = ip[0] != 0; + } + } + close(s); + return ok; +} diff --git a/examples/cmp_demo/src/main.c b/examples/cmp_demo/src/main.c new file mode 100644 index 00000000..b384a51d --- /dev/null +++ b/examples/cmp_demo/src/main.c @@ -0,0 +1,369 @@ +// cmp_demo - XCP tunnelled through an emulated ASAM CMP capture module +// +// Demonstrates supplying your own backend for the xcplib raw Ethernet transport +// (OPTION_ENABLE_UDP_RAW) from OUTSIDE the library. xcplib 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 in socket_raw_hal_cmp.c. +// +// CMP serves testing of XCP tools which communicate through capture modules. It is not an +// ECU developer feature, so nothing about it lives in libxcplite. +// +// The demo emulates a Capture Module with one interface, behind which sits one XCP ECU: +// +// ECU -> tool frames xcplib builds leave as CMP Captured Data Messages (0x01) +// tool -> ECU CMP Transmit Data Messages (0x04, new in CMP 1.1) are unwrapped and +// their inner frame handed to xcplib, which answers the XCP command +// +// The tool therefore never talks IP to the ECU directly - everything is tunnelled inside +// CMP over UDP. See README.md. +// +// Build and run (xcplite must be installed with the raw configuration first, see README): +// cmake -B build -S . -Dxcplite_DIR=/lib/cmake/xcplite +// cmake --build build +// ./build/cmp_demo --sink 192.168.0.10:55555 + +#include // for assert +#include // for signal handling +#include // for bool +#include // for uintxx_t +#include // for printf, sscanf +#include // for strtoul +#include // for strcmp + +// Include XCPlite/libxcplite C headers +#include // for A2l generation +#include // for application programming interface + +#include "cmp_backend.h" // for the CMP backend configuration +#include "cmp_discovery.h" // for the multicast discovery responder +#include "cmp_rest.h" // for the REST interface of the emulated capture module + +//----------------------------------------------------------------------------------------------------- +// XCP params + +#define OPTION_PROJECT_NAME "cmp_demo" +#define OPTION_PROJECT_VERSION "V1.0.0" +#define OPTION_SERVER_PORT 5555 +#define OPTION_QUEUE_SIZE (1024 * 32) +#define OPTION_LOG_LEVEL 4 + +#define OPTION_XCP_MODE (XCP_MODE_PERSISTENCE | XCP_MODE_LOCAL) +#define OPTION_A2L_MODE (A2L_MODE_WRITE_ONCE | A2L_MODE_FINALIZE_ON_CONNECT | A2L_MODE_AUTO_GROUPS) + +// Address of the emulated ECU. It only ever appears INSIDE the CMP payload, so unlike the +// plain raw transport it does not have to be free on any real network - but it must not +// collide with the address the Data Sink uses to reach us. +#define DEFAULT_ECU_IP {192, 168, 0, 220} + +// UDP port we listen on for CMP Transmit Data and Control Messages. 55555 is the port the +// specification uses in its DNS-SD examples (12.2.2.2). +#define DEFAULT_CMP_PORT 55555 + +#define DEFAULT_REST_PORT 8080 // 12.3 says "should" be 80, which would need privileges +#define DEFAULT_OUTER_MTU 1500 + +#define DEFAULT_DEVICE_ID 1 +#define DEFAULT_STREAM_ID 0 +#define DEFAULT_INTERFACE_ID 1 + +//----------------------------------------------------------------------------------------------------- +// Demo calibration parameters + +typedef struct params { + uint32_t delay_us; // Mainloop delay time in us + uint16_t counter_max; // Maximum value for the counter + float amplitude; // Amplitude of the demo signal +} params_t; + +const params_t params = {.delay_us = 1000, .counter_max = 1024, .amplitude = 100.0f}; + +tXcpCalSegIndex params_calseg = XCP_UNDEFINED_CALSEG; + +//----------------------------------------------------------------------------------------------------- +// Demo global measurement values + +uint32_t global_counter = 0; +double demo_signal = 0.0; + +//----------------------------------------------------------------------------------------------------- +// Command line + +static void usage(const char *argv0) { + printf("\nUsage: %s [options]\n" + "\n" + "Data Sink (the XCP tool) and the CMP transport:\n" + " --sink Data Sink address for Captured Data Messages.\n" + " Default: learned from the first CMP message received.\n" + " --listen UDP port to listen on for CMP messages (default: %u)\n" + " --mtu MTU of the path to the Data Sink (default: %u, max %u).\n" + " CMP messages must not be IP fragmented, so this bounds\n" + " the largest ECU frame that can be carried.\n" + " --rest-port REST interface port (default: %u, 0 disables it)\n" + " --no-discovery Do not answer CMP_CM_DISCOVERY on %s:%u (12.1.1).\n" + " Without it the module has to be configured statically,\n" + " which section 12 permits.\n" + "\n" + "Capture module identity:\n" + " --device-id CMP DeviceId (default: %u)\n" + " --stream-id CMP StreamId (default: %u)\n" + " --interface-id CMP InterfaceId of the emulated ECU link (default: %u)\n" + " --ecu-mac MAC of the emulated ECU (default: derived from DeviceId)\n" + "\n" + "Emulated ECU, seen only inside the CMP payload:\n" + " --ip IPv4 address of the ECU (default: 192.168.0.220)\n" + " --port XCP UDP port of the ECU (default: %u)\n" + "\n" + "Needs no privileges: the CMP transport is an ordinary UDP socket.\n\n", + argv0, (unsigned)DEFAULT_CMP_PORT, (unsigned)DEFAULT_OUTER_MTU, (unsigned)CMP_MAX_OUTER_MTU, (unsigned)DEFAULT_REST_PORT, CMP_DISCOVERY_GROUP, + (unsigned)CMP_DISCOVERY_PORT, (unsigned)DEFAULT_DEVICE_ID, (unsigned)DEFAULT_STREAM_ID, (unsigned)DEFAULT_INTERFACE_ID, (unsigned)OPTION_SERVER_PORT); +} + +// Parse "a.b.c.d" into 4 bytes, returns false on a malformed address +static bool parseIp(const char *s, uint8_t *addr) { + unsigned v[4]; + if (sscanf(s, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]) != 4) + return false; + for (int i = 0; i < 4; i++) { + if (v[i] > 255) + return false; + addr[i] = (uint8_t)v[i]; + } + return true; +} + +// Parse "a.b.c.d:port" into a dotted quad string and a port +static bool parseEndpoint(const char *s, char *ip, size_t ip_size, uint16_t *port) { + unsigned v[4], p; + if (sscanf(s, "%u.%u.%u.%u:%u", &v[0], &v[1], &v[2], &v[3], &p) != 5) + return false; + for (int i = 0; i < 4; i++) { + if (v[i] > 255) + return false; + } + if (p == 0 || p > 65535) + return false; + snprintf(ip, ip_size, "%u.%u.%u.%u", v[0], v[1], v[2], v[3]); + *port = (uint16_t)p; + return true; +} + +// Parse "xx:xx:xx:xx:xx:xx", returns false on a malformed or unusable address +static bool parseMac(const char *s, uint8_t *mac) { + unsigned v[6]; + if (sscanf(s, "%x:%x:%x:%x:%x:%x", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5]) != 6) + return false; + for (int i = 0; i < 6; i++) { + if (v[i] > 255) + return false; + mac[i] = (uint8_t)v[i]; + } + // socket_raw.c rejects both of these when it reads the MAC back from the HAL + if ((mac[0] & 0x01) != 0) { + printf("ERROR: '%s' is a multicast MAC address\n", s); + return false; + } + if ((mac[0] | mac[1] | mac[2] | mac[3] | mac[4] | mac[5]) == 0) { + printf("ERROR: the MAC address must not be all zero\n"); + return false; + } + return true; +} + +//----------------------------------------------------------------------------------------------------- +// Demo main + +static volatile bool running = true; +static void sig_handler(int sig) { + (void)sig; + running = false; +} + +int main(int argc, char *argv[]) { + + // Line buffer stdout so the log stays readable and in order when it is redirected to a + // file or a pipe, which is how the test script and any CI run it. + setvbuf(stdout, NULL, _IOLBF, 0); + + uint8_t addr[4] = DEFAULT_ECU_IP; + uint16_t port = OPTION_SERVER_PORT; + uint16_t rest_port = DEFAULT_REST_PORT; + bool discovery = true; + + char sink_ip[16] = {0}; + uint16_t sink_port = 0; + + tCmpBackendConfig cmp = { + .device_id = DEFAULT_DEVICE_ID, + .stream_id = DEFAULT_STREAM_ID, + .interface_id = DEFAULT_INTERFACE_ID, + .local_port = DEFAULT_CMP_PORT, + .sink_ip = NULL, + .sink_port = 0, + .outer_mtu = DEFAULT_OUTER_MTU, + .ecu_mac = {0, 0, 0, 0, 0, 0}, + }; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--sink") && i + 1 < argc) { + if (!parseEndpoint(argv[++i], sink_ip, sizeof(sink_ip), &sink_port)) { + printf("Invalid Data Sink endpoint '%s', expected a.b.c.d:port\n", argv[i]); + return 1; + } + } else if (!strcmp(argv[i], "--listen") && i + 1 < argc) { + cmp.local_port = (uint16_t)strtoul(argv[++i], NULL, 10); + } else if (!strcmp(argv[i], "--mtu") && i + 1 < argc) { + unsigned long mtu = strtoul(argv[++i], NULL, 10); + if (mtu < 576 || mtu > CMP_MAX_OUTER_MTU) { + printf("Invalid MTU '%s', expected 576..%u\n", argv[i], (unsigned)CMP_MAX_OUTER_MTU); + return 1; + } + cmp.outer_mtu = (uint16_t)mtu; + } else if (!strcmp(argv[i], "--rest-port") && i + 1 < argc) { + rest_port = (uint16_t)strtoul(argv[++i], NULL, 10); + } else if (!strcmp(argv[i], "--no-discovery")) { + discovery = false; + } else if (!strcmp(argv[i], "--device-id") && i + 1 < argc) { + cmp.device_id = (uint16_t)strtoul(argv[++i], NULL, 0); + } else if (!strcmp(argv[i], "--stream-id") && i + 1 < argc) { + cmp.stream_id = (uint8_t)strtoul(argv[++i], NULL, 0); + } else if (!strcmp(argv[i], "--interface-id") && i + 1 < argc) { + cmp.interface_id = (uint32_t)strtoul(argv[++i], NULL, 0); + } else if (!strcmp(argv[i], "--ecu-mac") && i + 1 < argc) { + if (!parseMac(argv[++i], cmp.ecu_mac)) { + printf("Invalid MAC address '%s', expected xx:xx:xx:xx:xx:xx\n", argv[i]); + return 1; + } + } else if (!strcmp(argv[i], "--ip") && i + 1 < argc) { + if (!parseIp(argv[++i], addr)) { + printf("Invalid IPv4 address '%s'\n", argv[i]); + return 1; + } + } else if (!strcmp(argv[i], "--port") && i + 1 < argc) { + port = (uint16_t)strtoul(argv[++i], NULL, 10); + } else { + usage(argv[0]); + return (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) ? 0 : 1; + } + } + + if (sink_ip[0] != 0) { + cmp.sink_ip = sink_ip; // not copied by the backend, and sink_ip outlives it + cmp.sink_port = sink_port; + } + + printf("\nXCP over ASAM CMP - cmp_demo %uBit %s\n", (uint32_t)(sizeof(void *) * 8), OPTION_PROJECT_VERSION); + printf(" Emulated ECU: %u.%u.%u.%u:%u (inside the CMP payload only)\n", addr[0], addr[1], addr[2], addr[3], port); + + signal(SIGINT, sig_handler); + signal(SIGTERM, sig_handler); + + XcpSetLogLevel(OPTION_LOG_LEVEL); + + if (!XcpInit(OPTION_PROJECT_NAME, OPTION_PROJECT_VERSION, OPTION_XCP_MODE)) { + printf("Failed to initialize XCP\n"); + return 1; + } + XcpSetElfName(argv[0]); + + // Configure our own HAL backend before starting the server, which is what opens it. + // socketRawSetInterface() is deliberately not called: its opaque string cannot carry + // this much, so the backend takes a typed configuration instead. + cmpBackendConfigure(&cmp); + + // XCP: Initialize the XCP Server. + // The address is mandatory here: the raw transport rejects 0.0.0.0 (ANY), there is no + // IP stack which could resolve it. useTCP is false, the raw transport is UDP only. + if (!XcpEthServerInit(addr, port, false, OPTION_QUEUE_SIZE)) { + printf("Failed to start the XCP server.\n" + " Check that UDP port %u is free for the CMP transport.\n", + cmp.local_port); + return 1; + } + + // Discovery must be opened BEFORE the REST thread starts, because that thread is what + // polls its socket. It exists to advertise the REST port, so it is pointless without one. + static char serial_number[32]; // static: cmpDiscoveryStart keeps the pointer + snprintf(serial_number, sizeof(serial_number), "cmp_demo-%04X", cmp.device_id); + if (discovery && rest_port != 0) { + tCmpDiscoveryConfig discovery_config = { + .http_port = rest_port, + .description = "XCPlite cmp_demo, emulated ASAM CMP capture module", + .serial = serial_number, + }; + if (!cmpDiscoveryStart(&discovery_config)) { + printf("WARNING: discovery is not available, the capture module has to be configured\n" + " statically. Section 12 permits exactly that.\n"); + } + } + + // The REST interface is how a Data Sink discovers that this capture module supports + // transmission (7.2.2), so start it once the backend can report its status. + if (rest_port != 0 && !cmpRestStart(rest_port)) { + printf("WARNING: the REST interface is not available. A Data Sink which relies on it\n" + " to detect transmission support will not send us anything.\n"); + } + + if (!A2lInit(addr, port, false, OPTION_A2L_MODE)) { + return 1; + } + + params_calseg = XcpCreateCalSeg("params", ¶ms, sizeof(params)); + assert(params_calseg != XCP_UNDEFINED_CALSEG); + + A2lSetSegmentAddrMode(params_calseg, params); + A2lCreateParameter(params.counter_max, "Maximum counter value", "", 0, 65535); + A2lCreateParameter(params.delay_us, "Mainloop delay time in us", "us", 0, 500000); + A2lCreateParameter(params.amplitude, "Amplitude of the demo signal", "", 0.0, 1000.0); + + uint16_t counter = 0; + + // XCP: Create a measurement event and register the measurement variables + DaqCreateEvent(mainloop); + A2lOnce() { + A2lSetAbsoluteAddrMode(mainloop); + A2lCreateMeasurement(global_counter, "Global free running counter"); + A2lCreatePhysMeasurement(demo_signal, "Demo signal", "", -1000.0, 1000.0); + A2lSetStackAddrMode(mainloop); + A2lCreateMeasurement(counter, "Mainloop counter"); + } + + A2lFinalize(); // @@@@ TEST: finalize now, before the first connect + + printf("\nCapture module running.\n"); + printf(" The XCP tool is a CMP Data Sink: it reaches the ECU by sending Transmit Data\n"); + printf(" Messages to our CMP port, not by addressing %u.%u.%u.%u directly.\n", addr[0], addr[1], addr[2], addr[3]); + if (cmp.sink_ip == NULL) { + printf(" No --sink given, so nothing is captured until the Data Sink speaks first.\n"); + } + printf("\n"); + + // Mainloop + uint32_t delay_us = 1000; + while (running) { + + const params_t *p = (params_t *)XcpLockCalSeg(params_calseg); + delay_us = p->delay_us; + + counter++; + if (counter > p->counter_max) { + counter = 0; + } + global_counter++; + demo_signal = (double)p->amplitude * (double)counter / 1000.0; + + XcpUnlockCalSeg(params_calseg); + + // XCP: Trigger the measurement event + DaqTriggerEvent(mainloop); + + sleepUs(delay_us); + } + + printf("\nShutting down...\n"); + cmpRestStop(); // Stop the REST interface, joins the thread that polls discovery + cmpDiscoveryStop(); // Safe only once that thread is gone + XcpDisconnect(); // Force disconnect the XCP client + A2lFinalize(); // Finalize A2L generation, if not done yet + XcpEthServerShutdown(); // Stop the XCP server + return 0; +} diff --git a/examples/cmp_demo/src/socket_raw_hal_cmp.c b/examples/cmp_demo/src/socket_raw_hal_cmp.c new file mode 100644 index 00000000..33f20baa --- /dev/null +++ b/examples/cmp_demo/src/socket_raw_hal_cmp.c @@ -0,0 +1,325 @@ +/*---------------------------------------------------------------------------- +| File: +| socket_raw_hal_cmp.c +| +| Description: +| Ethernet HAL backend for cmp_demo, implementing src/socket_raw_hal.h of xcplib. +| +| This file lives in the demo, NOT in the library: CMP serves testing of XCP tools +| through capture modules, it is not an ECU developer feature, so nothing about it +| belongs in libxcplite. The library is used as installed and unmodified. Because +| libxcplite is a static library and this object defines all six eth_hal_* symbols, +| the linker never pulls the built in backend out of the archive. +| +| The demo emulates a Capture Module which tunnels one XCP ECU: +| +| eth_hal_send the frame xcplib built is what the capture module just captured on +| its interface, so it is wrapped as a Captured Data Message and sent +| to the Data Sink +| eth_hal_recv a Transmit Data Message from the Data Sink is unwrapped and its +| inner frame handed to xcplib, which parses it as ordinary +| Ethernet/IPv4/UDP and answers the XCP command inside +| +| Layering: +| cmp.c the envelope codec, pure, no I/O +| cmp_transport_udp.c the outer transport, CMP over UDP (6.4.2) +| here the six eth_hal_* functions that join the two +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include // for printf +#include // for malloc, free +#include // for memset, memcpy + +#include "cmp.h" +#include "cmp_backend.h" +#include "cmp_transport.h" +#include "socket_raw_hal.h" // the interface this file implements, installed with xcplib +// socket_raw_hal.h pulls in platform.h, which provides clockGet() and OPTION_MTU + +//------------------------------------------------------------------------------- +// Configuration, set by main.c before XcpEthServerInit() + +static tCmpBackendConfig sConfig = { + .device_id = 1, + .stream_id = 0, + .interface_id = 1, + .local_port = 55555, + .sink_ip = NULL, + .sink_port = 0, + .outer_mtu = 1500, + .ecu_mac = {0, 0, 0, 0, 0, 0}, +}; + +void cmpBackendConfigure(const tCmpBackendConfig *config) { + if (config != NULL) { + sConfig = *config; + } +} + +//------------------------------------------------------------------------------- + +struct eth_hal_ctx { + tCmpTransport *transport; + tCmpCodec codec; + uint8_t mac[6]; // MAC of the emulated ECU + uint16_t max_inner_frame; // largest frame that fits into one CMP message + bool mtu_warning; // xcplib's segment size does not fit that budget + uint64_t n_oversize; // frames refused for that reason + uint64_t n_drop_logged; // how many receive drops have been logged so far + uint8_t tx[CMP_MAX_MESSAGE]; + uint8_t rx[CMP_MAX_MESSAGE]; +}; + +// The single instance, so cmpBackendGetStatus() can reach it from the REST thread +static tEthHalCtx *sCtx = NULL; + +//------------------------------------------------------------------------------- + +// The frames xcplib produces are at most 42 + XCPTL_MAX_SEGMENT_SIZE bytes, which is +// OPTION_MTU + 10 (socket_raw_hal.h). xcptl_cfg.h is not installed, but OPTION_MTU is +// visible because XCPLIB_CFG_OVERRIDE is a PUBLIC compile definition of the library +// target and the override header is installed alongside it. +#define XCPLIB_MAX_FRAME (OPTION_MTU + 10) + +static void deriveMac(uint8_t *mac, uint16_t device_id) { + // Locally administered unicast: bit 0 of the first byte clear, bit 1 set. socket_raw.c + // rejects a multicast or all zero MAC, so both properties are load bearing. + mac[0] = 0x02; + mac[1] = 0x00; + mac[2] = 0x00; + mac[3] = 0x00; + mac[4] = (uint8_t)(device_id >> 8); + mac[5] = (uint8_t)(device_id & 0xFF); + if (mac[4] == 0 && mac[5] == 0) { + mac[5] = 1; // never hand out an all zero MAC + } +} + +//------------------------------------------------------------------------------- + +bool eth_hal_open(const char *config, tEthHalCtx **ctxp) { + + if (ctxp == NULL) { + return false; + } + *ctxp = NULL; + + // The opaque HAL config string is not used: this backend is configured through + // cmpBackendConfigure() from main.c, which is typed and carries more than a name. + (void)config; + + if (sConfig.outer_mtu > CMP_MAX_OUTER_MTU) { + printf("ERROR: eth_hal_open: outer MTU %u exceeds the %u byte maximum of 6.4\n", sConfig.outer_mtu, CMP_MAX_OUTER_MTU); + return false; + } + + tEthHalCtx *ctx = (tEthHalCtx *)malloc(sizeof(tEthHalCtx)); + if (ctx == NULL) { + printf("ERROR: eth_hal_open: out of memory\n"); + return false; + } + memset(ctx, 0, sizeof(tEthHalCtx)); + + tCmpConfig codec_config = { + .device_id = sConfig.device_id, + .stream_id = sConfig.stream_id, + .interface_id = sConfig.interface_id, + }; + cmpCodecInit(&ctx->codec, &codec_config); + + static const uint8_t zero_mac[6] = {0, 0, 0, 0, 0, 0}; + if (memcmp(sConfig.ecu_mac, zero_mac, 6) == 0) { + deriveMac(ctx->mac, sConfig.device_id); + } else { + memcpy(ctx->mac, sConfig.ecu_mac, 6); + } + + tCmpTransportConfig transport_config = { + .local_port = sConfig.local_port, + .sink_ip = sConfig.sink_ip, + .sink_port = sConfig.sink_port, + .outer_mtu = sConfig.outer_mtu, + }; + if (!cmpTransportOpen(&transport_config, &ctx->transport)) { + free(ctx); + return false; + } + + uint16_t max_message = cmpTransportMaxMessage(ctx->transport); + if (max_message > CMP_MAX_MESSAGE) { + max_message = CMP_MAX_MESSAGE; + } + ctx->max_inner_frame = (max_message > CMP_CAP_OVERHEAD) ? (uint16_t)(max_message - CMP_CAP_OVERHEAD) : 0; + ctx->mtu_warning = XCPLIB_MAX_FRAME > ctx->max_inner_frame; + + char local_ip[16] = {0}; + uint16_t local_port = 0; + cmpTransportGetLocal(ctx->transport, local_ip, sizeof(local_ip), &local_port); + + printf(" CMP capture module: DeviceId %u (0x%04X), StreamId %u, InterfaceId %u\n", sConfig.device_id, sConfig.device_id, sConfig.stream_id, sConfig.interface_id); + printf(" CMP transport: UDP, listening on %s:%u, outer MTU %u\n", local_ip[0] != 0 ? local_ip : "0.0.0.0", local_port, sConfig.outer_mtu); + char sink_ip[16] = {0}; + uint16_t sink_port = 0; + if (cmpTransportGetSink(ctx->transport, sink_ip, sizeof(sink_ip), &sink_port)) { + printf(" CMP Data Sink: %s:%u\n", sink_ip, sink_port); + } else { + printf(" CMP Data Sink: not configured, will be learned from the first message received\n"); + } + printf(" CMP emulated ECU MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", ctx->mac[0], ctx->mac[1], ctx->mac[2], ctx->mac[3], ctx->mac[4], ctx->mac[5]); + printf(" CMP frame budget: %u bytes per inner frame (%u byte CMP message - %u byte envelope)\n", ctx->max_inner_frame, max_message, CMP_CAP_OVERHEAD); + + if (ctx->mtu_warning) { + // 6.4.2 forbids sending CMP messages in IP fragmented packets, so an oversized + // frame cannot be split - it has to be refused. Say so once, with the remedy, + // instead of only reporting it per frame later. + printf("WARNING: xcplib can produce frames of up to %u bytes, but only %u fit into one\n" + " un-fragmented CMP message on this path. Small transfers work, saturated DAQ\n" + " will report SOCKET_ERROR_MSGSIZE. Remedies:\n" + " - raise the outer MTU (--mtu, up to %u, needs a jumbo capable path), or\n" + " - build xcplite with OPTION_MTU <= %u (currently %u) in xcplib_raw_cfg.h\n", + (unsigned)XCPLIB_MAX_FRAME, ctx->max_inner_frame, CMP_MAX_OUTER_MTU, (unsigned)(ctx->max_inner_frame - 10), (unsigned)OPTION_MTU); + } + + sCtx = ctx; + *ctxp = ctx; + return true; +} + +void eth_hal_close(tEthHalCtx *ctx) { + if (ctx == NULL) { + return; + } + printf(" CMP: %llu frames captured, %llu transmit requests delivered, %llu messages dropped", (unsigned long long)ctx->codec.n_wrapped, + (unsigned long long)ctx->codec.n_unwrapped, (unsigned long long)ctx->codec.n_dropped); + if (ctx->codec.n_seq_jumps != 0) { + printf(", %llu sequence counter gaps", (unsigned long long)ctx->codec.n_seq_jumps); + } + if (ctx->n_oversize != 0) { + printf(", %llu frames refused as oversized", (unsigned long long)ctx->n_oversize); + } + printf("\n"); + + sCtx = NULL; + cmpTransportClose(ctx->transport); + free(ctx); +} + +bool eth_hal_get_mac(tEthHalCtx *ctx, uint8_t *mac) { + if (ctx == NULL || mac == NULL) { + return false; + } + // This is the MAC of the emulated ECU behind the capture module, not of the capture + // module itself: it becomes the source MAC of the frames xcplib builds, which is what + // the Data Sink sees inside the CMP payload. + memcpy(mac, ctx->mac, 6); + return true; +} + +int16_t eth_hal_send(tEthHalCtx *ctx, const uint8_t *frame, uint16_t len) { + + if (ctx == NULL || frame == NULL) { + return ETH_HAL_ERROR; + } + + if (len > ctx->max_inner_frame) { + ctx->n_oversize++; + if (ctx->n_oversize == 1) { + printf("ERROR: eth_hal_send: frame of %u bytes exceeds the %u byte CMP budget of this path.\n" + " CMP messages must not be IP fragmented (6.4.2), so it cannot be sent.\n", + len, ctx->max_inner_frame); + } + return ETH_HAL_ERROR_SIZE; + } + + // Apply the envelope into our own buffer. Never into the transmit queue headroom: CMP + // must not participate in the zero copy path and must not influence XCPTL_TX_HEADROOM. + // + // The timestamp is taken here rather than passed in from xcplib: this is the moment the + // emulated capture module sees the frame, and it is the only capture time that exists. + // INSYNC is false because nothing synchronises us to a time provider (11.1.3). + uint16_t msg_len = cmpWrapCaptured(&ctx->codec, frame, len, clockGet(), false, ctx->tx, (uint16_t)sizeof(ctx->tx)); + if (msg_len == 0) { + ctx->n_oversize++; + return ETH_HAL_ERROR_SIZE; + } + + int32_t sent = cmpTransportSend(ctx->transport, ctx->tx, msg_len); + if (sent < 0) { + return ETH_HAL_ERROR; + } + + // Report the length xcplib handed us, not the wrapped length: the envelope is invisible + // above this layer. A return of 0 from the transport means the Data Sink is not known + // yet - the frame is discarded, which is what a capture module with no configured sink + // does, and reporting success keeps the XCP transmit path from treating it as an error. + return (int16_t)len; +} + +int16_t eth_hal_recv(tEthHalCtx *ctx, uint8_t *frame, uint16_t max_len, uint32_t timeout_ms) { + + if (ctx == NULL || frame == NULL) { + return ETH_HAL_ERROR; + } + + int32_t n = cmpTransportRecv(ctx->transport, ctx->rx, (uint16_t)sizeof(ctx->rx), timeout_ms); + if (n < 0) { + return ETH_HAL_ERROR; + } + if (n == 0) { + return 0; // timeout or wakeup + } + + tCmpResult result = CMP_OK; + uint16_t inner = cmpUnwrapTransmit(&ctx->codec, ctx->rx, (uint16_t)n, frame, max_len, &result); + if (inner == 0) { + // Not for us. Returning 0 means "no frame", so the caller keeps waiting against its + // own deadline. Log the first few and then every 1000th, so a misconfigured Data + // Sink is visible without the log becoming the bottleneck. + if (ctx->codec.n_dropped <= 5 || (ctx->codec.n_dropped % 1000) == 0) { + printf("WARNING: eth_hal_recv: dropped a %d byte CMP message: %s (%llu so far)\n", (int)n, cmpResultName(result), (unsigned long long)ctx->codec.n_dropped); + } + return 0; + } + + return (int16_t)inner; +} + +void eth_hal_wakeup(tEthHalCtx *ctx) { + if (ctx != NULL) { + cmpTransportWakeup(ctx->transport); + } +} + +//------------------------------------------------------------------------------- + +bool cmpBackendGetStatus(tCmpBackendStatus *status) { + + tEthHalCtx *ctx = sCtx; + if (status == NULL || ctx == NULL) { + return false; + } + memset(status, 0, sizeof(*status)); + + status->open = true; + status->sink_known = cmpTransportGetSink(ctx->transport, status->sink_ip, sizeof(status->sink_ip), &status->sink_port); + cmpTransportGetLocal(ctx->transport, status->local_ip, sizeof(status->local_ip), &status->local_port); + + status->max_message = cmpTransportMaxMessage(ctx->transport); + status->max_inner_frame = ctx->max_inner_frame; + status->mtu_warning = ctx->mtu_warning; + + memcpy(status->ecu_mac, ctx->mac, 6); + status->device_id = ctx->codec.config.device_id; + status->stream_id = ctx->codec.config.stream_id; + status->interface_id = ctx->codec.config.interface_id; + + status->n_wrapped = ctx->codec.n_wrapped; + status->n_unwrapped = ctx->codec.n_unwrapped; + status->n_dropped = ctx->codec.n_dropped; + status->n_seq_jumps = ctx->codec.n_seq_jumps; + status->n_aggregated_ignored = ctx->codec.n_aggregated_ignored; + status->n_oversize = ctx->n_oversize; + return true; +} diff --git a/examples/cmp_demo/test.sh b/examples/cmp_demo/test.sh new file mode 100755 index 00000000..ab417ea2 --- /dev/null +++ b/examples/cmp_demo/test.sh @@ -0,0 +1,509 @@ +#!/bin/bash + +# On-target test for the cmp_demo example project +# +# Syncs the sources to the target, builds xcplite and the demo there, starts the emulated +# capture module and checks it from this machine: +# 1. the CMP envelope codec unit test, on the target +# 2. the REST interface (12.3), including that transmission is advertised +# 3. the status of the CMP endpoint +# 4. a hardcoded XCP CONNECT tunnelled through CMP, with the response decoded +# +# Unlike examples/udp_raw_demo/test.sh this needs NO setcap and NO free IP address on the +# network: the CMP transport is an ordinary UDP socket, and the emulated ECU address only +# ever appears inside the CMP payload. +# +# For a purely local run on this machine, without a target, use test/test_local.sh. +# +# Prerequisites: +# - The target must be reachable via SSH, with rsync, cmake and a C compiler installed +# - This machine must have rsync, curl and python3 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +#====================================================================================================================== +# Parameters +#====================================================================================================================== + +# Target connection details. +# All parameters in this block can be overridden from the environment, so a target on a +# different address needs no edit here: +# TARGET_HOST=192.168.0.205 ./test.sh +TARGET_USER="${TARGET_USER:-rainer}" +TARGET_HOST="${TARGET_HOST:-192.168.0.206}" +TARGET_PATH="${TARGET_PATH:-~/XCPlite-CMP}" +TARGET_BINARY="cmp_demo" + +# Where the xcplite library is installed on the target, and where the demo is built +TARGET_INSTALL_DIR="xcplite-install" +TARGET_DEMO_DIR="examples/cmp_demo" + +# Build type for the target executables: Release, RelWithDebInfo or Debug +BUILD_TYPE="${BUILD_TYPE:-RelWithDebInfo}" + +# Ports the capture module serves on the target +CMP_PORT="${CMP_PORT:-55555}" # UDP, CMP messages (12.2.2.2 uses this port in its examples) +REST_PORT="${REST_PORT:-8080}" # TCP, REST interface. 12.3 says "should" be 80, which would need root + +# Identity of the emulated capture module. The XCP CONNECT below is built for exactly +# these values, so change them here and nowhere else. +DEVICE_ID="1" +INTERFACE_ID="1" + +# The emulated ECU behind the capture module. This address lives ONLY inside the CMP +# payload, so it does not have to be free on the network and must not be pinged. +ECU_IP="192.168.0.220" +ECU_PORT="5555" +ECU_MAC="02:00:00:00:00:01" # derived by the demo from DEVICE_ID + +# Us, as the Data Sink, inside the tunnelled frame +SINK_IP="192.168.0.10" +SINK_MAC="02:00:00:00:FF:01" +SINK_PORT="50000" +SINK_DEVICE_ID="8738" # 0x2222, our own CMP DeviceId + +FAILURES=0 + +#====================================================================================================================== +# Helpers +#====================================================================================================================== + +step_failed() { + echo "❌ FAILED: $*" + FAILURES=$((FAILURES + 1)) +} + +# Stop the demo on the target, however this script ends +SSH_PID="" +cleanup() { + ssh "$TARGET_USER@$TARGET_HOST" "pkill -x $TARGET_BINARY" 2> /dev/null + if [ -n "$SSH_PID" ]; then + wait "$SSH_PID" 2> /dev/null + SSH_PID="" + fi +} +trap cleanup EXIT + +for tool in rsync ssh curl python3; do + if ! command -v "$tool" > /dev/null 2>&1; then + echo "❌ FAILED: '$tool' is required on this machine" + exit 1 + fi +done + +echo "========================================================================================================" +echo "cmp_demo on-target test" +echo " target $TARGET_USER@$TARGET_HOST:$TARGET_PATH" +echo " capture module CMP on UDP $TARGET_HOST:$CMP_PORT, REST on $TARGET_HOST:$REST_PORT" +echo " emulated ECU $ECU_IP:$ECU_PORT, MAC $ECU_MAC (inside the CMP payload only)" +echo "========================================================================================================" + +#====================================================================================================================== +# Sync target +#====================================================================================================================== + +echo "" +echo "Sync target ..." +# Exclusions come FIRST: rsync applies the first matching rule, so these have to precede the +# --include patterns below. Build artifacts must not be synced - a CMakeCache.txt carried over +# from this machine records the local source and build paths, and cmake on the target then +# refuses to configure. This is the same list as examples/cmp_demo/.gitignore. +rsync -avz --delete \ + --exclude='build/' \ + --exclude='build-*/' \ + --exclude='*.a2l' \ + --exclude='*.bin' \ + --exclude='*.log' \ + --include='/build.sh' \ + --include='/CMakeLists.txt' \ + --include='/cmake/***' \ + --include='/inc/***' \ + --include='/src/***' \ + --include='/examples/' \ + --include='/examples/cmp_demo/***' \ + --exclude='*' \ + "$REPO_ROOT/" "$TARGET_USER@$TARGET_HOST:$TARGET_PATH/" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Rsync with target" + exit 1 +fi + +#====================================================================================================================== +# Build on target +#====================================================================================================================== + +# Resolve TARGET_PATH to an absolute path on the target, once. +# A leading ~ is expanded by the remote shell only at the start of a word. After an '=' it is +# not: bash expands install=~/... because that looks like an assignment, but leaves +# -Dxcplite_DIR=~/... alone because -Dxcplite_DIR is not a valid identifier, and dash expands +# neither. Every remote path below therefore uses TARGET_ABS, never the ~ form. +TARGET_ABS=$(ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && pwd") +if [ $? -ne 0 ] || [ -z "$TARGET_ABS" ]; then + echo "❌ FAILED: cannot resolve $TARGET_PATH on the target" + exit 1 +fi + +# cmp_demo is a STANDALONE cmake project: it consumes an INSTALLED xcplite rather than being +# built from the root CMakeLists. So the library is built and installed first, then the demo +# is pointed at that install - exactly as the README describes for a manual build. + +echo "Build and install the xcplite library (raw configuration) on the target ..." +ssh "$TARGET_USER@$TARGET_HOST" \ + "cd $TARGET_ABS && ./build.sh $BUILD_TYPE raw lib install=$TARGET_ABS/$TARGET_INSTALL_DIR" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Build/install of libxcplite on the target" + exit 1 +fi + +echo "Build cmp_demo on the target ..." +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_ABS/$TARGET_DEMO_DIR \ + && cmake -B build -S . -DCMAKE_BUILD_TYPE=$BUILD_TYPE \ + -Dxcplite_DIR=$TARGET_ABS/$TARGET_INSTALL_DIR/lib/cmake/xcplite \ + && cmake --build build --parallel" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Build of cmp_demo on the target" + exit 1 +fi + +# The override works because libxcplite is a STATIC library: the linker only pulls an archive +# member in to resolve an UNDEFINED symbol, and this project already defines all six eth_hal_* +# functions. On Linux socket_raw_hal_linux.o is present in the archive, so looking at the +# archive proves nothing - the LINKED BINARY is what has to be checked. The built in backend +# is the only place that mentions cap_net_raw, so its absence means it was not pulled in. +echo "Check that the built in AF_PACKET backend is not linked in ..." +ssh "$TARGET_USER@$TARGET_HOST" \ + "grep -a -q 'cap_net_raw+ep' $TARGET_ABS/$TARGET_DEMO_DIR/build/$TARGET_BINARY" +if [ $? -eq 0 ]; then + step_failed "the built in AF_PACKET backend is linked into $TARGET_BINARY - check that libxcplite is a STATIC library" +else + echo "✅ the built in AF_PACKET backend is not linked in, this project supplies the HAL" +fi + +#====================================================================================================================== +# 1. Envelope codec unit test, on the target +#====================================================================================================================== + +echo "" +echo "========================================================================================================" +echo "1. CMP envelope codec on the target, against the ASAM CMP 1.1.0 sample files" +echo "========================================================================================================" +ssh "$TARGET_USER@$TARGET_HOST" "$TARGET_ABS/$TARGET_DEMO_DIR/build/cmp_codec_test" +if [ $? -ne 0 ]; then + step_failed "the codec unit test did not pass on the target" +fi + +#====================================================================================================================== +# Start the capture module on the target +#====================================================================================================================== + +echo "" +echo "Start $TARGET_BINARY on the target ..." + +# Clear any leftover from an aborted run first. Both sockets use SO_REUSEADDR, so a stale +# process does not reliably make the bind fail - it can instead answer in place of the one +# started here, and every check below would then be testing the wrong process. +ssh "$TARGET_USER@$TARGET_HOST" "pkill -x $TARGET_BINARY" 2> /dev/null + +# No --sink: the Data Sink address is learned from the first CMP message we send, which +# avoids having to know this machine's address on the target's network. +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_ABS/$TARGET_DEMO_DIR \ + && ./build/$TARGET_BINARY --listen $CMP_PORT --rest-port $REST_PORT \ + --ip $ECU_IP --port $ECU_PORT \ + --device-id $DEVICE_ID --interface-id $INTERFACE_ID > cmp_demo.log 2>&1" & +SSH_PID=$! + +# Note: pgrep/pkill -x matches the process NAME. Do NOT use -f here: it matches the full +# command line, and the ssh command line on the target contains "$TARGET_BINARY" itself, so +# -f would match the ssh session as well and terminate it. +ssh "$TARGET_USER@$TARGET_HOST" "for i in \$(seq 1 20); do pgrep -x $TARGET_BINARY > /dev/null && exit 0; sleep 0.5; done; exit 1" +if [ $? -ne 0 ]; then + echo "❌ FAILED: $TARGET_BINARY is not running on the target" + ssh "$TARGET_USER@$TARGET_HOST" "cat $TARGET_ABS/$TARGET_DEMO_DIR/cmp_demo.log" 2> /dev/null + exit 1 +fi + +# Being in the process table is not the same as serving: wait for the REST port to answer. +echo "Waiting for the REST interface on $TARGET_HOST:$REST_PORT ..." +for i in $(seq 1 20); do + curl -s -m 2 -o /dev/null "http://$TARGET_HOST:$REST_PORT/asam-cmp/version-info" && break + sleep 0.5 +done +curl -s -m 2 -o /dev/null "http://$TARGET_HOST:$REST_PORT/asam-cmp/version-info" +if [ $? -ne 0 ]; then + echo "❌ FAILED: the REST interface did not come up on $TARGET_HOST:$REST_PORT" + ssh "$TARGET_USER@$TARGET_HOST" "cat $TARGET_ABS/$TARGET_DEMO_DIR/cmp_demo.log" 2> /dev/null + exit 1 +fi + +#====================================================================================================================== +# 2. REST interface +#====================================================================================================================== + +echo "" +echo "========================================================================================================" +echo "2. REST interface (12.3)" +echo "========================================================================================================" + +for path in "/asam-cmp/version-info" \ + "/asam-cmp/v1/identification" \ + "/asam-cmp/v1/interfaces" \ + "/asam-cmp/v1/measurement"; do + echo "" + echo "GET $path" + body=$(curl -s -m 5 -w '\n%{http_code}' "http://$TARGET_HOST:$REST_PORT$path") + code=$(echo "$body" | tail -1) + json=$(echo "$body" | sed '$d') + if [ "$code" = "000" ]; then + step_failed "GET $path: could not connect to $TARGET_HOST:$REST_PORT at all" + continue + elif [ "$code" != "200" ]; then + step_failed "GET $path returned HTTP $code" + continue + fi + echo "$json" | python3 -m json.tool 2> /dev/null || echo "$json" +done + +# 7.2.2: "Support for transmission is optional in the Capture Module, the data sink can use +# the REST API to detect if transmission is supported or not". The Transmitter object of +# /interfaces is that signal - without it a tool may never inject and XCP cannot connect. +echo "" +echo "Checking that transmission is advertised ..." +curl -s -m 5 "http://$TARGET_HOST:$REST_PORT/asam-cmp/v1/interfaces" | python3 -c ' +import json, sys +try: + interfaces = json.load(sys.stdin).get("Interfaces", []) +except ValueError as exc: + print(" cannot parse the response: %s" % exc); sys.exit(1) +if not interfaces: + print(" no interfaces reported"); sys.exit(1) +transmitter = interfaces[0].get("Transmitter") +if transmitter is None: + print(" no Transmitter object: a Data Sink would conclude that this capture module") + print(" cannot transmit, and would never inject (7.2.2)"); sys.exit(1) +bitmask = transmitter.get("TransmissionSupportBitmask", 0) +if not bitmask & 1: + print(" TransmissionSupportBitmask=0x%02X has TIMESTAMP_IMMEDIATE clear" % bitmask); sys.exit(1) +mtu = transmitter.get("AggregationMtu", 0) +print(" transmission supported, TransmissionSupportBitmask=0x%02X (TIMESTAMP_IMMEDIATE)" % bitmask) +print(" AggregationMtu=%u, so the largest inner Ethernet frame is %u bytes" % (mtu, mtu - 34)) +' +if [ $? -ne 0 ]; then + step_failed "the REST interface does not advertise transmission support" +else + echo "✅ transmission is advertised" +fi + +#====================================================================================================================== +# 3. Status of the CMP endpoint +#====================================================================================================================== + +echo "" +echo "========================================================================================================" +echo "3. Status of the CMP endpoint" +echo "========================================================================================================" +curl -s -m 5 "http://$TARGET_HOST:$REST_PORT/asam-cmp/v1/measurement" | python3 -c ' +import json, sys +try: + status = json.load(sys.stdin) +except ValueError as exc: + print(" cannot parse the response: %s" % exc); sys.exit(1) +print(" CaptureModuleState : %s" % status.get("CaptureModuleState")) +print(" Counters : %s" % status.get("Message")) +for stream in status.get("StateOfStreams", []): + print(" Stream %-3s : %s" % (stream.get("StreamId"), stream.get("State"))) +sys.exit(0 if status.get("CaptureModuleState") == "active" else 1) +' +if [ $? -ne 0 ]; then + step_failed "the capture module does not report itself as active" +fi + +# The endpoint itself: a CMP message sent to a closed UDP port would be answered with an +# ICMP port unreachable, which the CONNECT below would surface as a confusing timeout. +echo "" +echo "CMP endpoint: UDP $TARGET_HOST:$CMP_PORT" +LISTENING=$(ssh "$TARGET_USER@$TARGET_HOST" \ + "if command -v ss > /dev/null; then ss -lun; elif command -v netstat > /dev/null; then netstat -lun; else echo NO_TOOL; fi" 2> /dev/null) +if echo "$LISTENING" | grep -q "NO_TOOL"; then + echo "ℹ️ neither ss nor netstat on the target, skipping the port check" +elif echo "$LISTENING" | grep -q ":$CMP_PORT"; then + echo "✅ UDP port $CMP_PORT is open" +else + step_failed "UDP port $CMP_PORT is not open on the target" +fi + +#====================================================================================================================== +# 4. Hardcoded XCP CONNECT, tunnelled through CMP +#====================================================================================================================== + +echo "" +echo "========================================================================================================" +echo "4. Multicast discovery (12.1.1)" +echo "========================================================================================================" + +# Sends CMP_CM_DISCOVERY to 239.255.0.0:5556 from THIS machine and decodes the answer, so it +# also proves multicast crosses the link to the target. Unlike the local run, the module +# reports a real LAN address, prefix length and MAC here. +"$SCRIPT_DIR/test/discovery_probe.py" --expect-http "$REST_PORT" +if [ $? -ne 0 ]; then + step_failed "the capture module did not answer CMP_CM_DISCOVERY" + echo " If every other check passes, multicast is most likely not forwarded between this" + echo " machine and the target, rather than the responder being broken." +fi + +echo "" +echo "========================================================================================================" +echo "5. XCP CONNECT through the capture module" +echo "========================================================================================================" + +# The XCP command is the hardcoded constant FF 00 (CONNECT, normal mode), wrapped in the XCP +# on Ethernet transport header and then in a complete Ethernet/IPv4/UDP frame, which is what +# a capture module transmits on behalf of the tool. The frame is assembled here from the +# parameters at the top of this script rather than pasted in as a fixed hex blob, so that +# changing e.g. ECU_IP cannot silently leave a stale IPv4 header checksum behind. The exact +# bytes that go on the wire are printed below. +# +# For the fuller exchange - CONNECT, GET_STATUS, DISCONNECT, sequence counter checks and a +# Wireshark capture file - use test/fake_sink.py, which this check is a cut down version of. + +CMP_TARGET="$TARGET_HOST" \ +CMP_PORT="$CMP_PORT" \ +ECU_IP="$ECU_IP" ECU_PORT="$ECU_PORT" ECU_MAC="$ECU_MAC" \ +SINK_IP="$SINK_IP" SINK_PORT="$SINK_PORT" SINK_MAC="$SINK_MAC" \ +SINK_DEVICE_ID="$SINK_DEVICE_ID" INTERFACE_ID="$INTERFACE_ID" \ +python3 - <<'PY' +import os, socket, struct, sys + +env = os.environ +target = (env["CMP_TARGET"], int(env["CMP_PORT"])) +ecu_ip, ecu_port, ecu_mac = env["ECU_IP"], int(env["ECU_PORT"]), env["ECU_MAC"] +sink_ip, sink_port, sink_mac = env["SINK_IP"], int(env["SINK_PORT"]), env["SINK_MAC"] +device_id, interface_id = int(env["SINK_DEVICE_ID"]), int(env["INTERFACE_ID"]) + + +def checksum16(data): + total = 0 + for i in range(0, len(data), 2): + total += (data[i] << 8) | data[i + 1] + while total >> 16: + total = (total & 0xFFFF) + (total >> 16) + return (~total) & 0xFFFF + + +# --- the XCP command, hardcoded --------------------------------------------------- +xcp_packet = bytes([0xFF, 0x00]) # CONNECT, mode 0 +xcp = struct.pack("HHHH", sink_port, ecu_port, 8 + len(xcp), 0) + xcp +ip = struct.pack(">BBHHHBBH4s4s", 0x45, 0x00, 20 + len(udp), 1, 0x4000, 64, 17, 0, + socket.inet_aton(sink_ip), socket.inet_aton(ecu_ip)) +ip = ip[:10] + struct.pack(">H", checksum16(ip)) + ip[12:] +frame = bytes.fromhex(ecu_mac.replace(":", "")) + bytes.fromhex(sink_mac.replace(":", "")) \ + + struct.pack(">H", 0x0800) + ip + udp + +# --- the CMP Transmit Data Message (7.2.2) with an Ethernet payload (7.3.8) -------- +data = frame + b"\x00\x00\x00\x00" # dummy FCS, FCS_SENDING = 0 +payload = struct.pack(">HHH", 0, 0, len(data)) + data +message = (struct.pack(">BBHBBH", 0x01, 0, device_id, 0x04, 0, 0) # CMP header + + struct.pack(">QIIIBBH", 0, 0, interface_id, 0, 0, 0x08, len(payload)) + + payload) + +print(" TX_DATA_MSG, %u bytes, carrying a %u byte Ethernet frame with XCP CONNECT:" + % (len(message), len(frame))) +for off in range(0, len(message), 24): + print(" %04x %s" % (off, message[off:off + 24].hex(" "))) + +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.settimeout(5.0) +try: + sock.sendto(message, target) +except OSError as exc: + # EHOSTUNREACH/ENETUNREACH here mean the datagram never left this machine: the route + # lookup or the ARP for the target failed. That is a plain connectivity problem, not a + # CMP one, so say so rather than letting it surface as a traceback. + print(" -> could not send to %s:%u: %s" % (target[0], target[1], exc)) + print(" The datagram never left this machine. Check that the target is reachable") + print(" (ping %s) and that nothing filters UDP to port %u." % (target[0], target[1])) + sys.exit(1) +print(" -> sent to %s:%u from UDP port %u" % (target[0], target[1], sock.getsockname()[1])) + +# --- the response, a Captured Data Message (7.2.1) -------------------------------- +try: + response, sender = sock.recvfrom(65535) +except socket.timeout: + print(" <- TIMEOUT: no CMP message came back within 5s") + sys.exit(1) +except OSError as exc: + # A cached ICMP port unreachable arrives here: the target answered, but nothing is + # listening on the CMP port. + print(" <- no response: %s" % exc) + print(" The target is reachable but did not accept the message on UDP port %u." % target[1]) + sys.exit(1) + +if len(response) < 8 + 16: + print(" <- %u bytes, too short to be a CMP data message" % len(response)) + sys.exit(1) +version, _res, dev, msg_type, stream_id, seq = struct.unpack_from(">BBHBBH", response, 0) +if version < 1 or msg_type != 0x01: + print(" <- version %u message type 0x%02X, expected a Captured Data Message" + % (version, msg_type)) + sys.exit(1) +timestamp, iface, flags, ptype, plen = struct.unpack_from(">QIBBH", response, 8) +print(" <- CAP_DATA_MSG from %s:%u, %u bytes" % (sender[0], sender[1], len(response))) +print(" DeviceId 0x%04X, StreamId %u, StreamSequenceCounter %u" % (dev, stream_id, seq)) +print(" InterfaceId %u, PayloadType 0x%02X, capture timestamp %u ns, INSYNC=%u" + % (iface, ptype, timestamp, (flags >> 1) & 1)) +if ptype != 0x08: + print(" payload is not an Ethernet Data Message") + sys.exit(1) + +body = response[24:24 + plen] +_pflags, _pres, dlen = struct.unpack_from(">HHH", body, 0) +inner = body[6:6 + dlen][:-4] # strip the FCS +ihl = (inner[14] & 0x0F) * 4 +udp_off = 14 + ihl +udp_len = struct.unpack_from(">H", inner, udp_off + 4)[0] +xcp_payload = inner[udp_off + 8: udp_off + udp_len] +xlen, xctr = struct.unpack_from("%u" + % (len(inner), *struct.unpack_from(">HH", inner, udp_off))) +if packet[0] != 0xFF: + print(" <- XCP error, PID 0x%02X" % packet[0]) + sys.exit(1) +resource, comm_mode, max_cto, max_dto, proto, transport = struct.unpack_from(" /dev/null + +echo "" +echo "========================================================================================================" +if [ "$FAILURES" -eq 0 ]; then + echo "✅ SUCCESS: all checks passed" + echo "========================================================================================================" + exit 0 +fi +echo "❌ FAILED: $FAILURES check(s) did not pass" +echo "========================================================================================================" +exit 1 diff --git a/examples/cmp_demo/test/cmp_codec_test.c b/examples/cmp_demo/test/cmp_codec_test.c new file mode 100644 index 00000000..2189fb03 --- /dev/null +++ b/examples/cmp_demo/test/cmp_codec_test.c @@ -0,0 +1,422 @@ +/*---------------------------------------------------------------------------- +| File: +| cmp_codec_test.c +| +| Description: +| Unit test for the ASAM CMP envelope codec (src/cmp.c). +| +| Pure: links cmp.c only, no sockets, no libxcplite, no network. Run it as +| ./build/cmp_codec_test - it returns non zero if anything fails. +| +| The golden vectors are taken byte for byte from the sample PCAPNG files shipped +| with the ASAM CMP 1.1.0 specification (Sample_Files/), so this test pins the wire +| format against the standard itself rather than against our own reading of it: +| +| GOLDEN_CAP_* CMP_1.0/asam_cmp_cap_0x08_Ethernet.pcapng +| a Captured Data Message with an Ethernet payload - exactly the +| shape this backend emits +| GOLDEN_TX_CAN CMP_1.1/asam_cmp_tx_0x01_can_29bit_0x12345678.pcapng +| a real Transmit Data Message. Its payload is CAN, not Ethernet, so +| the codec must reject it - but only after parsing the 24 byte +| Transmit Data Message header correctly. Getting that header's length +| or field offsets wrong yields MALFORMED instead of PAYLOAD_TYPE, so +| this vector pins the TX header layout against a real 1.1 message. +| +| There is no sample of a Transmit Data Message carrying an Ethernet payload - the +| 1.1 samples cover CAN, CAN FD and LIN only - so the happy path uses a message built +| by buildTxEthernet() below from the two layouts the vectors above have pinned. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include +#include + +#include "../src/cmp.h" + +//------------------------------------------------------------------------------- + +static int sChecks = 0; +static int sFailures = 0; + +#define CHECK(cond, ...) \ + do { \ + sChecks++; \ + if (!(cond)) { \ + sFailures++; \ + printf(" FAIL %s:%d: ", __FILE__, __LINE__); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + } \ + } while (0) + +static void hexdiff(const char *what, const uint8_t *got, const uint8_t *want, uint16_t len) { + printf(" %s mismatch:\n", what); + for (uint16_t i = 0; i < len; i++) { + if (got[i] != want[i]) { + printf(" offset %3u: got 0x%02x want 0x%02x\n", i, got[i], want[i]); + } + } +} + +//------------------------------------------------------------------------------- +// Golden vectors + +// The 42 byte inner Ethernet frame (an ARP request), dst MAC .. end, no FCS +static const uint8_t GOLDEN_CAP_FRAME[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, + 0x01, 0x60, 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0, 0xa8, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x02, +}; + +// The complete 76 byte CMP message from the sample, with one deliberate change: the +// sample's FCS bytes 0x11 0x22 0x00 0x00 are zeroed here. This capture module reports +// FCS_SUPPORT = 0 and the specification prescribes an all zero FCS for that case (7.3.8). +static const uint8_t GOLDEN_CAP_MSG[] = { + 0x01, 0x00, 0x47, 0x11, 0x01, 0x00, 0x00, 0x00, // CMP header: v1, device 0x4711, CAP_DATA, stream 0, seq 0 + 0x18, 0x88, 0x66, 0xbc, 0xae, 0x60, 0x8e, 0xe4, // timestamp + 0x00, 0x00, 0x00, 0x08, // InterfaceId 8 + 0x00, // common flags: INSYNC = 0, SEG = 00, DIR_ON_IF = 0 + 0x08, // payload type ETHERNET_DATA_MSG + 0x00, 0x34, // payload length 52 + 0x00, 0x00, // Ethernet payload flags: FCS_SUPPORT = 0 + 0x00, 0x00, // reserved + 0x00, 0x2e, // data length 46 = 42 frame + 4 FCS + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x01, 0xc0, 0xa8, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, // FCS, zeroed +}; + +#define GOLDEN_CAP_DEVICE_ID 0x4711 +#define GOLDEN_CAP_STREAM_ID 0 +#define GOLDEN_CAP_INTERFACE_ID 8 +#define GOLDEN_CAP_TIMESTAMP 1767775814308368100u + +// A real CMP 1.1 Transmit Data Message, verbatim. Device 0x4711, stream 0, seq 0, +// deadline 1e9 ns, InterfaceId 1, payload type 0x01 (CAN), payload length 24. +static const uint8_t GOLDEN_TX_CAN[] = { + 0x01, 0x00, 0x47, 0x11, 0x04, 0x00, 0x00, 0x00, // CMP header: TX_DATA_MSG + 0x18, 0x88, 0x66, 0xbc, 0xae, 0xfe, 0x47, 0x10, // timestamp + 0x3b, 0x9a, 0xca, 0x00, // deadline 1000000000 ns + 0x00, 0x00, 0x00, 0x01, // InterfaceId 1 + 0x00, 0x00, 0x00, 0x00, // transmission options + 0x00, // common flags: SEG = 00 + 0x01, // payload type CAN_DATA_MSG + 0x00, 0x18, // payload length 24 + 0x00, 0x00, 0x00, 0x00, 0x92, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, +}; + +//------------------------------------------------------------------------------- +// Test only: build a Transmit Data Message carrying an Ethernet payload, i.e. play the +// part of the data sink. Layout per 7.2.2 and 7.3.8, pinned by the vectors above. + +static uint16_t buildTxEthernet(uint8_t *out, uint16_t device_id, uint8_t stream_id, uint16_t seq, uint32_t interface_id, uint8_t common_flags, uint8_t payload_type, + const uint8_t *frame, uint16_t frame_len) { + uint16_t data_length = (uint16_t)(frame_len + CMP_FCS_LEN); + uint16_t payload_length = (uint16_t)(CMP_ETH_PAYLOAD_HDR_LEN + data_length); + uint8_t *p = out; + + *p++ = CMP_VERSION; + *p++ = 0; + *p++ = (uint8_t)(device_id >> 8); + *p++ = (uint8_t)device_id; + *p++ = CMP_MSG_TX_DATA; + *p++ = stream_id; + *p++ = (uint8_t)(seq >> 8); + *p++ = (uint8_t)seq; + + memset(p, 0, 8); // timestamp 0 = send immediately + p += 8; + memset(p, 0, 4); // deadline 0 = none + p += 4; + *p++ = (uint8_t)(interface_id >> 24); + *p++ = (uint8_t)(interface_id >> 16); + *p++ = (uint8_t)(interface_id >> 8); + *p++ = (uint8_t)interface_id; + memset(p, 0, 4); // transmission options, 0 for Ethernet payloads + p += 4; + *p++ = common_flags; + *p++ = payload_type; + *p++ = (uint8_t)(payload_length >> 8); + *p++ = (uint8_t)payload_length; + + *p++ = 0; // Ethernet payload flags: FCS_SENDING = 0, the FCS below is a dummy + *p++ = 0; + *p++ = 0; // reserved + *p++ = 0; + *p++ = (uint8_t)(data_length >> 8); + *p++ = (uint8_t)data_length; + memcpy(p, frame, frame_len); + p += frame_len; + memset(p, 0, CMP_FCS_LEN); + p += CMP_FCS_LEN; + + return (uint16_t)(p - out); +} + +static void initCodec(tCmpCodec *codec, uint32_t interface_id) { + tCmpConfig config = {.device_id = GOLDEN_CAP_DEVICE_ID, .stream_id = GOLDEN_CAP_STREAM_ID, .interface_id = interface_id}; + cmpCodecInit(codec, &config); +} + +//------------------------------------------------------------------------------- + +static void testWrapGolden(void) { + printf("wrap: golden Captured Data Message\n"); + tCmpCodec codec; + initCodec(&codec, GOLDEN_CAP_INTERFACE_ID); + + uint8_t out[256]; + uint16_t n = cmpWrapCaptured(&codec, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME), GOLDEN_CAP_TIMESTAMP, false, out, sizeof(out)); + + CHECK(n == sizeof(GOLDEN_CAP_MSG), "length %u, want %zu", n, sizeof(GOLDEN_CAP_MSG)); + if (n == sizeof(GOLDEN_CAP_MSG)) { + bool equal = memcmp(out, GOLDEN_CAP_MSG, n) == 0; + CHECK(equal, "wrapped message differs from the specification sample"); + if (!equal) { + hexdiff("CAP message", out, GOLDEN_CAP_MSG, n); + } + } + CHECK(n == sizeof(GOLDEN_CAP_FRAME) + CMP_CAP_OVERHEAD, "overhead %u, want %u", (unsigned)(n - sizeof(GOLDEN_CAP_FRAME)), CMP_CAP_OVERHEAD); + CHECK(codec.n_wrapped == 1, "n_wrapped %llu", (unsigned long long)codec.n_wrapped); +} + +static void testWrapInSyncFlag(void) { + printf("wrap: INSYNC flag\n"); + tCmpCodec codec; + initCodec(&codec, GOLDEN_CAP_INTERFACE_ID); + uint8_t out[256]; + cmpWrapCaptured(&codec, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME), 0, true, out, sizeof(out)); + CHECK(out[20] == CMP_CAP_FLAG_INSYNC, "common flags 0x%02x, want 0x%02x", out[20], CMP_CAP_FLAG_INSYNC); +} + +static void testWrapSequenceCounter(void) { + printf("wrap: StreamSequenceCounter increments and wraps\n"); + tCmpCodec codec; + initCodec(&codec, GOLDEN_CAP_INTERFACE_ID); + uint8_t out[256]; + + for (uint16_t i = 0; i < 4; i++) { + cmpWrapCaptured(&codec, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME), 0, false, out, sizeof(out)); + uint16_t seq = (uint16_t)((out[6] << 8) | out[7]); + CHECK(seq == i, "message %u carries seq %u", i, seq); + } + + codec.tx_seq = 0xFFFF; + cmpWrapCaptured(&codec, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME), 0, false, out, sizeof(out)); + CHECK(out[6] == 0xFF && out[7] == 0xFF, "seq at wrap boundary"); + CHECK(codec.tx_seq == 0, "seq wrapped to %u, want 0", codec.tx_seq); +} + +static void testWrapSizeLimit(void) { + printf("wrap: refuses to overflow the caller's buffer\n"); + tCmpCodec codec; + initCodec(&codec, GOLDEN_CAP_INTERFACE_ID); + uint8_t out[256]; + + uint16_t need = (uint16_t)(sizeof(GOLDEN_CAP_FRAME) + CMP_CAP_OVERHEAD); + CHECK(cmpWrapCaptured(&codec, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME), 0, false, out, need) == need, "exact fit must succeed"); + CHECK(cmpWrapCaptured(&codec, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME), 0, false, out, (uint16_t)(need - 1)) == 0, "one byte short must fail"); + CHECK(cmpWrapCaptured(&codec, GOLDEN_CAP_FRAME, 0, 0, false, out, sizeof(out)) == 0, "empty frame must fail"); +} + +static void testUnwrapRealTxMessage(void) { + printf("unwrap: real CMP 1.1 Transmit Data Message (CAN payload)\n"); + tCmpCodec codec; + uint8_t out[2048]; + tCmpResult result = CMP_OK; + + // InterfaceId 1 matches the sample, so the rejection must be about the payload type. + // A wrong Transmit Data Message header length would surface as MALFORMED instead. + initCodec(&codec, 1); + CHECK(cmpUnwrapTransmit(&codec, GOLDEN_TX_CAN, sizeof(GOLDEN_TX_CAN), out, sizeof(out), &result) == 0, "CAN payload must not be delivered"); + CHECK(result == CMP_DROP_PAYLOAD_TYPE, "result %s, want a payload type rejection", cmpResultName(result)); + + // The sequence counter of the sending data sink is tracked even for a dropped message + CHECK(codec.peer_seq_valid && codec.peer_device_id == 0x4711 && codec.peer_seq == 0, "peer stream state not tracked"); + + // A different InterfaceId must be rejected on that ground, which proves the codec + // reads InterfaceId from the right offset in the 24 byte header. + initCodec(&codec, 99); + CHECK(cmpUnwrapTransmit(&codec, GOLDEN_TX_CAN, sizeof(GOLDEN_TX_CAN), out, sizeof(out), &result) == 0, "foreign interface must not be delivered"); + CHECK(result == CMP_DROP_INTERFACE_ID, "result %s, want an InterfaceId rejection", cmpResultName(result)); +} + +static void testUnwrapEthernetHappyPath(void) { + printf("unwrap: Transmit Data Message with an Ethernet payload\n"); + tCmpCodec codec; + initCodec(&codec, 7); + + uint8_t msg[256]; + uint16_t msg_len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + CHECK(msg_len == sizeof(GOLDEN_CAP_FRAME) + CMP_TX_OVERHEAD, "built length %u, want %u", msg_len, (unsigned)(sizeof(GOLDEN_CAP_FRAME) + CMP_TX_OVERHEAD)); + + uint8_t out[2048]; + tCmpResult result = CMP_DROP_MALFORMED; + uint16_t n = cmpUnwrapTransmit(&codec, msg, msg_len, out, sizeof(out), &result); + + CHECK(result == CMP_OK, "result %s", cmpResultName(result)); + CHECK(n == sizeof(GOLDEN_CAP_FRAME), "inner frame %u bytes, want %zu (FCS must be stripped)", n, sizeof(GOLDEN_CAP_FRAME)); + if (n == sizeof(GOLDEN_CAP_FRAME)) { + bool equal = memcmp(out, GOLDEN_CAP_FRAME, n) == 0; + CHECK(equal, "inner frame differs from the original"); + if (!equal) { + hexdiff("inner frame", out, GOLDEN_CAP_FRAME, n); + } + } + CHECK(codec.n_unwrapped == 1 && codec.n_dropped == 0, "counters: unwrapped %llu dropped %llu", (unsigned long long)codec.n_unwrapped, (unsigned long long)codec.n_dropped); +} + +static void testUnwrapRejections(void) { + printf("unwrap: rejections\n"); + tCmpCodec codec; + uint8_t msg[256]; + uint8_t out[2048]; + tCmpResult result; + uint16_t len; + + // Too short + initCodec(&codec, 7); + CHECK(cmpUnwrapTransmit(&codec, GOLDEN_TX_CAN, 8, out, sizeof(out), &result) == 0, "truncated message"); + CHECK(result == CMP_DROP_TOO_SHORT, "result %s, want too short", cmpResultName(result)); + + // Version 0x00 is TECMP/PLP, not CMP (5.2) + initCodec(&codec, 7); + len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + msg[0] = 0x00; + CHECK(cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), &result) == 0, "version 0 message"); + CHECK(result == CMP_DROP_VERSION, "result %s, want version rejection", cmpResultName(result)); + + // A Captured Data Message must not be mistaken for a transmit request + initCodec(&codec, 7); + len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + msg[4] = CMP_MSG_CAP_DATA; + CHECK(cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), &result) == 0, "captured data message"); + CHECK(result == CMP_DROP_MESSAGE_TYPE, "result %s, want message type rejection", cmpResultName(result)); + + // Segmentation is not supported (6.3.3) and is advertised as such over REST + initCodec(&codec, 7); + len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0x04 /* SEG = first segment */, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + CHECK(cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), &result) == 0, "segmented message"); + CHECK(result == CMP_DROP_SEGMENTED, "result %s, want segmentation rejection", cmpResultName(result)); + + // Payload type INVALID is padding: discard it and the rest of the message (7.2) + initCodec(&codec, 7); + len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0, CMP_PAYLOAD_INVALID, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + CHECK(cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), &result) == 0, "padding message"); + + // Inconsistent length fields + initCodec(&codec, 7); + len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + msg[31] = 0xFF; // payload length low byte, now past the end of the message + CHECK(cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), &result) == 0, "payload length past the end"); + CHECK(result == CMP_DROP_MALFORMED, "result %s, want malformed", cmpResultName(result)); + + // Ethernet payload with no room for the FCS + initCodec(&codec, 7); + len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + msg[36] = 0x00; // data length high byte + msg[37] = 0x03; // data length 3, less than the 4 byte FCS + CHECK(cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), &result) == 0, "payload shorter than the FCS"); + CHECK(result == CMP_DROP_NO_FCS, "result %s, want no FCS", cmpResultName(result)); + + // Inner frame larger than the caller's buffer + initCodec(&codec, 7); + len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + CHECK(cmpUnwrapTransmit(&codec, msg, len, out, 8, &result) == 0, "receive buffer too small"); + CHECK(result == CMP_DROP_TOO_LARGE, "result %s, want too large", cmpResultName(result)); +} + +static void testRoundTrip(void) { + printf("round trip: wrap -> unwrap over a range of frame sizes\n"); + uint8_t frame[1600]; + for (size_t i = 0; i < sizeof(frame); i++) { + frame[i] = (uint8_t)(i * 31u + 7u); + } + + static const uint16_t sizes[] = {1, 14, 42, 60, 64, 512, 1434, 1500}; + for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++) { + uint16_t frame_len = sizes[s]; + + // Capture direction, then parse it back with the test's own reader by re-wrapping + // it as a transmit message: this checks that the Ethernet payload the capture + // direction produces is exactly what the transmit direction expects. + tCmpCodec sender; + initCodec(&sender, 5); + uint8_t cap[2048]; + uint16_t cap_len = cmpWrapCaptured(&sender, frame, frame_len, 0x0123456789ABCDEFu, true, cap, sizeof(cap)); + CHECK(cap_len == frame_len + CMP_CAP_OVERHEAD, "size %u: wrapped %u", frame_len, cap_len); + + tCmpCodec receiver; + initCodec(&receiver, 5); + uint8_t msg[2048]; + uint16_t msg_len = buildTxEthernet(msg, 0x1234, 0, (uint16_t)s, 5, 0, CMP_PAYLOAD_ETHERNET, frame, frame_len); + uint8_t back[2048]; + tCmpResult result = CMP_DROP_MALFORMED; + uint16_t n = cmpUnwrapTransmit(&receiver, msg, msg_len, back, sizeof(back), &result); + CHECK(n == frame_len && result == CMP_OK, "size %u: unwrapped %u (%s)", frame_len, n, cmpResultName(result)); + CHECK(n == frame_len && memcmp(back, frame, frame_len) == 0, "size %u: payload corrupted", frame_len); + } +} + +static void testPeerSequenceMonitoring(void) { + printf("unwrap: peer StreamSequenceCounter monitoring\n"); + tCmpCodec codec; + initCodec(&codec, 7); + uint8_t msg[256]; + uint8_t out[2048]; + + for (uint16_t seq = 0; seq < 3; seq++) { + uint16_t len = buildTxEthernet(msg, 0x1234, 0, seq, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), NULL); + } + CHECK(codec.n_seq_jumps == 0, "contiguous counters reported %llu jumps", (unsigned long long)codec.n_seq_jumps); + + // Skip 3, i.e. a lost message + uint16_t len = buildTxEthernet(msg, 0x1234, 0, 4, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + uint16_t n = cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), NULL); + CHECK(codec.n_seq_jumps == 1, "gap reported %llu jumps, want 1", (unsigned long long)codec.n_seq_jumps); + CHECK(n == sizeof(GOLDEN_CAP_FRAME), "a counter gap must never drop the frame"); + + // Wrap around 0xFFFF -> 0 is not a jump + codec.peer_seq = 0xFFFF; + len = buildTxEthernet(msg, 0x1234, 0, 0, 7, 0, CMP_PAYLOAD_ETHERNET, GOLDEN_CAP_FRAME, sizeof(GOLDEN_CAP_FRAME)); + cmpUnwrapTransmit(&codec, msg, len, out, sizeof(out), NULL); + CHECK(codec.n_seq_jumps == 1, "wrap around counted as a jump"); +} + +static void testOverheadConstants(void) { + printf("constants: overhead matches the specified header lengths\n"); + CHECK(CMP_HDR_LEN == 8, "CMP header length"); + CHECK(CMP_CAP_DATA_HDR_LEN == 16, "Captured Data Message header length"); + CHECK(CMP_TX_DATA_HDR_LEN == 24, "Transmit Data Message header length"); + CHECK(CMP_CAP_OVERHEAD == 34, "capture direction overhead"); + CHECK(CMP_TX_OVERHEAD == 42, "transmit direction overhead"); + // The golden vector is the arithmetic check: 42 byte frame -> 76 byte message + CHECK(sizeof(GOLDEN_CAP_MSG) == sizeof(GOLDEN_CAP_FRAME) + CMP_CAP_OVERHEAD, "golden vector sizes"); + CHECK(sizeof(GOLDEN_TX_CAN) == 56, "golden transmit vector size"); +} + +//------------------------------------------------------------------------------- + +int main(void) { + printf("ASAM CMP envelope codec test\n"); + printf("golden vectors from the ASAM CMP 1.1.0 sample PCAPNG files\n\n"); + + testOverheadConstants(); + testWrapGolden(); + testWrapInSyncFlag(); + testWrapSequenceCounter(); + testWrapSizeLimit(); + testUnwrapRealTxMessage(); + testUnwrapEthernetHappyPath(); + testUnwrapRejections(); + testPeerSequenceMonitoring(); + testRoundTrip(); + + printf("\n%d checks, %d failures\n", sChecks, sFailures); + if (sFailures != 0) { + printf("FAILED\n"); + return 1; + } + printf("PASSED\n"); + return 0; +} diff --git a/examples/cmp_demo/test/discovery_probe.py b/examples/cmp_demo/test/discovery_probe.py new file mode 100755 index 00000000..0d4ec75a --- /dev/null +++ b/examples/cmp_demo/test/discovery_probe.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +discovery_probe.py - a Data Sink looking for capture modules (ASAM CMP 1.1.0, 12.1.1) + +Multicasts a CMP_CM_DISCOVERY request to 239.255.0.0:5556 and decodes every response. +That request is an ordinary XCP packet: transport header, command 0xF2 +(CC_TRANSPORT_LAYER_CMD), sub command 0x10. + + ./test/discovery_probe.py discover on every interface + ./test/discovery_probe.py --expect-http 8080 + +Exit code 0 when at least one capture module answered and every response parsed. +""" + +import argparse +import re +import socket +import subprocess +import struct +import sys + +GROUP = "239.255.0.0" +PORT = 5556 + +XCP_CMD_TL = 0xF2 +XCP_SUB_DISCOVERY = 0x10 +XCP_PID_RESPONSE = 0xFF + + +def local_interfaces(): + """IPv4 address of every interface, loopback included, best effort and stdlib only.""" + ips = [] + try: + out = subprocess.run(["ifconfig"], capture_output=True, text=True, timeout=5).stdout + except (OSError, subprocess.SubprocessError): + try: + out = subprocess.run(["ip", "-4", "addr"], capture_output=True, text=True, timeout=5).stdout + except (OSError, subprocess.SubprocessError): + return ["127.0.0.1"] + for match in re.finditer(r"inet (?:addr:)?(\d+\.\d+\.\d+\.\d+)", out): + ip = match.group(1) + if ip not in ips: + ips.append(ip) + return ips or ["127.0.0.1"] + + +def build_request(reply_addr, reply_port): + """Table 78. Little endian scalars, address as a 16 byte array in network order.""" + body = struct.pack(" %s:%u, via %u interface(s): %s" + % (GROUP, PORT, len(interfaces), ", ".join(interfaces))) + print(" " + build_request(GROUP, args.reply_port).hex(" ")) + + n_mcast = sweep(reply_to_group=True) + print(" reply to the group (12.1.1) : %u answer(s)" % n_mcast) + n_ucast = sweep(reply_to_group=False) + print(" reply to us directly : %u answer(s)" % n_ucast) + + for info in modules.values(): + print("\n capture module %s" % info["serial"]) + print(" description %s" % info["description"]) + print(" MAC %s" % info["mac"]) + for ip in info["addresses"]: + print(" reachable at %s -> http://%s:%u/asam-cmp/version-info" + % (ip, ip, info["http_port"])) + print(" prefix /%u gateway %s" % (info["prefix_len"], info["gateway"])) + print(" answered via %s" % ", ".join(sorted(info["via"]))) + + print() + if not modules: + print("FAILED: no capture module answered within %.1fs" % args.timeout) + return 1 + if args.expect_http is not None and not any(m["http_port"] == args.expect_http for m in modules.values()): + print("FAILED: no module advertised HTTP port %u" % args.expect_http) + return 1 + if problems: + print("FAILED (%u malformed response(s))" % problems) + return 1 + if not any("multicast" in m["via"] for m in modules.values()): + print("Note: no module answered on the multicast group, only directly.") + print(" The requests clearly arrived, so the return multicast is being filtered") + print(" somewhere in between - a Wi-Fi AP will normally not forward group traffic") + print(" to a wireless client. Not a fault of the capture module.") + print() + print("PASSED (%u capture module(s))" % len(modules)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/cmp_demo/test/fake_sink.py b/examples/cmp_demo/test/fake_sink.py new file mode 100755 index 00000000..bdb06f37 --- /dev/null +++ b/examples/cmp_demo/test/fake_sink.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +""" +fake_sink.py - a minimal ASAM CMP Data Sink, i.e. the half that CANape will play. + +It talks to cmp_demo over CMP on UDP (ASAM CMP 1.1.0, section 6.4.2): + + sink -> module Transmit Data Messages (TX_DATA_MSG, 0x04) carrying an Ethernet frame + with an XCP command inside, so the emulated ECU can be reached at all + module -> sink Captured Data Messages (CAP_DATA_MSG, 0x01) carrying the ECU's frames + +Nothing but the Python standard library is needed: the Ethernet, IPv4 and UDP headers of +the inner frame are built by hand, which is also the point - it proves the capture module +really does tunnel an ordinary frame rather than something bespoke. + +With --pcap it also writes every CMP message it sends and receives to a capture file. +Wireshark has a built in ASAM CMP dissector keyed on EtherType 0x99FE, so the messages are +framed for the Ethernet transport option (6.4.1) in that file and dissect automatically. +The CMP message bytes are identical under both transport options - only the outer framing +differs - so this validates the envelope, it is not a literal recording of the wire. + +Usage: + ./fake_sink.py --target 127.0.0.1:55555 [--ecu-ip 192.168.0.220] [--rest 127.0.0.1:8080] + ./fake_sink.py --target 127.0.0.1:55555 --pcap cmp.pcap +""" + +import argparse +import json +import socket +import struct +import sys +import time +import urllib.error +import urllib.request + +# ---------------------------------------------------------------------------- CMP + +CMP_VERSION = 0x01 +CMP_MSG_CAP_DATA = 0x01 +CMP_MSG_TX_DATA = 0x04 +CMP_PAYLOAD_ETHERNET = 0x08 + +MSG_TYPE_NAMES = {0x01: "CAP_DATA_MSG", 0x02: "CTRL_MSG", 0x03: "STATUS_MSG", + 0x04: "TX_DATA_MSG", 0xFF: "VENDOR_MSG"} + + +def cmp_header(device_id, message_type, stream_id, seq): + """CMP header, 8 bytes, big endian (6.2.1).""" + return struct.pack(">BBHBBH", CMP_VERSION, 0, device_id, message_type, stream_id, seq) + + +def cmp_wrap_transmit(device_id, stream_id, seq, interface_id, frame): + """Transmit Data Message (7.2.2) with an Ethernet payload (7.3.8).""" + data = frame + b"\x00\x00\x00\x00" # dummy FCS, FCS_SENDING = 0 + payload = struct.pack(">HHH", 0, 0, len(data)) + data + tx_header = struct.pack( + ">QIIIBBH", + 0, # Timestamp 0: send immediately + 0, # Deadline 0: none + interface_id, + 0, # Transmission Options: 0 for Ethernet payloads + 0, # Common Flags: SEG = 00, absolute mode + CMP_PAYLOAD_ETHERNET, + len(payload), + ) + return cmp_header(device_id, CMP_MSG_TX_DATA, stream_id, seq) + tx_header + payload + + +def cmp_parse(msg): + """Decode one CMP message. Returns a dict, or None if it is not one we understand.""" + if len(msg) < 8: + return None + version, _res, device_id, msg_type, stream_id, seq = struct.unpack_from(">BBHBBH", msg, 0) + if version < CMP_VERSION: + return None + out = {"version": version, "device_id": device_id, "message_type": msg_type, + "stream_id": stream_id, "seq": seq, "frame": None} + if msg_type == CMP_MSG_CAP_DATA and len(msg) >= 8 + 16: + ts, iface, flags, ptype, plen = struct.unpack_from(">QIBBH", msg, 8) + out.update(timestamp=ts, interface_id=iface, flags=flags, payload_type=ptype) + body = msg[24:24 + plen] + if ptype == CMP_PAYLOAD_ETHERNET and len(body) >= 6: + _pflags, _pres, dlen = struct.unpack_from(">HHH", body, 0) + data = body[6:6 + dlen] + if len(data) >= 4: + out["frame"] = data[:-4] # strip the FCS + return out + + +# -------------------------------------------------------------------------- pcap + +CMP_ETHERTYPE = 0x99FE +PCAP_LINKTYPE_ETHERNET = 1 + + +class PcapWriter: + """Classic libpcap writer, microsecond resolution.""" + + def __init__(self, path, module_mac, sink_mac): + self.file = open(path, "wb") + # magic, version 2.4, no timezone/sigfigs, snaplen, linktype + self.file.write(struct.pack("H", CMP_ETHERTYPE) + cmp_message + if len(frame) < 60: # Ethernet minimum, zero padded (6.4.1) + frame += b"\x00" * (60 - len(frame)) + now = time.time() + self.file.write(struct.pack("> 16: + total = (total & 0xFFFF) + (total >> 16) + return (~total) & 0xFFFF + + +def build_frame(src_mac, dst_mac, src_ip, dst_ip, src_port, dst_port, payload, ident): + """One complete Ethernet/IPv4/UDP frame, without FCS.""" + udp = struct.pack(">HHHH", src_port, dst_port, 8 + len(payload), 0) + payload + + ip_no_csum = struct.pack( + ">BBHHHBBH4s4s", + 0x45, 0x00, 20 + len(udp), ident, + 0x4000, # Don't Fragment, as socket_raw.c also sets + 64, 17, 0, + socket.inet_aton(src_ip), socket.inet_aton(dst_ip), + ) + csum = checksum16(ip_no_csum) + ip = ip_no_csum[:10] + struct.pack(">H", csum) + ip_no_csum[12:] + + eth = bytes.fromhex(dst_mac.replace(":", "")) + bytes.fromhex(src_mac.replace(":", "")) \ + + struct.pack(">H", 0x0800) + return eth + ip + udp + + +def parse_frame(frame): + """Pull the UDP payload out of an Ethernet/IPv4/UDP frame. None if it is not one.""" + if len(frame) < 14 + 20 + 8 or struct.unpack_from(">H", frame, 12)[0] != 0x0800: + return None + ihl = (frame[14] & 0x0F) * 4 + if frame[14 + 9] != 17: + return None + udp_off = 14 + ihl + udp_len = struct.unpack_from(">H", frame, udp_off + 4)[0] + return frame[udp_off + 8: udp_off + udp_len] + + +# --------------------------------------------------------------------------- XCP + +XCP_CONNECT = 0xFF +XCP_DISCONNECT = 0xFE +XCP_GET_STATUS = 0xFD +PID_RES, PID_ERR = 0xFF, 0xFE + + +def xcp_message(counter, packet): + """XCP on Ethernet transport layer: WORD len + WORD ctr + packet, little endian.""" + return struct.pack(" len(payload): + break + out.append((counter, payload[off + 4: off + 4 + length])) + off += 4 + length + return out + + +# -------------------------------------------------------------------------- REST + +def query_rest(endpoint): + base = "http://%s" % endpoint + paths = ["/asam-cmp/version-info", "/asam-cmp/v1/identification", + "/asam-cmp/v1/interfaces", "/asam-cmp/v1/measurement"] + print("REST interface at %s" % base) + ok = True + for path in paths: + try: + with urllib.request.urlopen(base + path, timeout=3) as response: + body = json.loads(response.read().decode()) + print(" GET %-32s %s" % (path, json.dumps(body))) + except (urllib.error.URLError, OSError, ValueError) as exc: + print(" GET %-32s FAILED: %s" % (path, exc)) + ok = False + continue + if path.endswith("/interfaces"): + interfaces = body.get("Interfaces", []) + transmitter = interfaces[0].get("Transmitter") if interfaces else None + if transmitter is None: + print(" -> no Transmitter object: a Data Sink would conclude that this") + print(" capture module cannot transmit, and would never inject (7.2.2)") + ok = False + else: + bitmask = transmitter.get("TransmissionSupportBitmask", 0) + print(" -> transmission supported, TransmissionSupportBitmask=0x%02X%s" + % (bitmask, " (TIMESTAMP_IMMEDIATE)" if bitmask & 1 else "")) + print(" -> AggregationMtu=%s, so the largest inner frame is %s bytes" + % (transmitter.get("AggregationMtu"), + (transmitter.get("AggregationMtu") or 34) - 34)) + return ok + + +# -------------------------------------------------------------------------- main + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--target", default="127.0.0.1:55555", + help="CMP port of the capture module (default: %(default)s)") + parser.add_argument("--rest", default="127.0.0.1:8080", + help="REST interface of the capture module, '' to skip") + parser.add_argument("--ecu-ip", default="192.168.0.220") + parser.add_argument("--ecu-port", type=int, default=5555) + parser.add_argument("--ecu-mac", default="02:00:00:00:00:01", + help="default matches DeviceId 1 of cmp_demo") + parser.add_argument("--sink-ip", default="192.168.0.10", + help="source address inside the tunnelled frame") + parser.add_argument("--sink-mac", default="02:00:00:00:FF:01") + parser.add_argument("--device-id", type=int, default=0x2222, help="our own CMP DeviceId") + parser.add_argument("--interface-id", type=int, default=1) + parser.add_argument("--timeout", type=float, default=3.0) + parser.add_argument("--pcap", default=None, + help="write the CMP messages to this capture file for Wireshark") + args = parser.parse_args() + + host, _, port = args.target.rpartition(":") + target = (host, int(port)) + + failures = 0 + if args.rest: + if not query_rest(args.rest): + failures += 1 + print() + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(("0.0.0.0", 0)) + sock.settimeout(args.timeout) + print("Data Sink on UDP port %u, capture module at %s:%u" + % (sock.getsockname()[1], target[0], target[1])) + + pcap = PcapWriter(args.pcap, args.ecu_mac, args.sink_mac) if args.pcap else None + + seq = 0 + expected_cap_seq = None + + def send_xcp(packet, name): + nonlocal seq + frame = build_frame(args.sink_mac, args.ecu_mac, args.sink_ip, args.ecu_ip, + 50000, args.ecu_port, xcp_message(seq, packet), ident=seq + 1) + msg = cmp_wrap_transmit(args.device_id, 0, seq, args.interface_id, frame) + sock.sendto(msg, target) + if pcap: + pcap.write(msg, from_module=False) + print(" -> %-12s TX_DATA_MSG seq=%u, %u byte inner frame, %u byte CMP message" + % (name, seq, len(frame), len(msg))) + seq += 1 + + def recv_xcp(name): + nonlocal expected_cap_seq + try: + while True: + data, _ = sock.recvfrom(65535) + if pcap: + pcap.write(data, from_module=True) + parsed = cmp_parse(data) + if parsed is None: + print(" <- %u bytes that are not a CMP message" % len(data)) + continue + kind = MSG_TYPE_NAMES.get(parsed["message_type"], "0x%02X" % parsed["message_type"]) + if parsed["message_type"] != CMP_MSG_CAP_DATA: + print(" <- %s, ignored" % kind) + continue + if expected_cap_seq is not None and parsed["seq"] != expected_cap_seq: + print(" !! StreamSequenceCounter gap: expected %u, got %u" + % (expected_cap_seq, parsed["seq"])) + expected_cap_seq = (parsed["seq"] + 1) & 0xFFFF + frame = parsed["frame"] + if frame is None: + print(" <- %s without an Ethernet payload" % kind) + continue + payload = parse_frame(frame) + if payload is None: + print(" <- %s: inner frame is not Ethernet/IPv4/UDP" % kind) + continue + return parsed, xcp_packets(payload) + except socket.timeout: + print(" <- %s: TIMEOUT after %.1fs" % (name, args.timeout)) + return None, [] + + print("\nXCP through the capture module:") + send_xcp(bytes([XCP_CONNECT, 0x00]), "CONNECT") + parsed, packets = recv_xcp("CONNECT") + if not packets: + failures += 1 + for counter, packet in packets: + if packet[0] == PID_RES and len(packet) >= 8: + resource, comm_mode, max_cto, max_dto, proto, transport = struct.unpack_from( + "> 1) & 1)) + elif packet[0] == PID_ERR: + print(" <- XCP error 0x%02X" % packet[1]) + failures += 1 + else: + print(" <- unexpected XCP packet 0x%02X (ctr %u)" % (packet[0], counter)) + failures += 1 + + if packets: + send_xcp(bytes([XCP_GET_STATUS]), "GET_STATUS") + _, packets = recv_xcp("GET_STATUS") + for _counter, packet in packets: + if packet[0] == PID_RES: + print(" <- GET_STATUS ok: session status 0x%02X" % packet[1]) + else: + print(" <- GET_STATUS unexpected packet 0x%02X" % packet[0]) + failures += 1 + if not packets: + failures += 1 + + send_xcp(bytes([XCP_DISCONNECT]), "DISCONNECT") + _, packets = recv_xcp("DISCONNECT") + for _counter, packet in packets: + print(" <- DISCONNECT %s" % ("ok" if packet[0] == PID_RES else "error")) + + if pcap: + pcap.close() + print("\nWrote %u CMP messages to %s" % (pcap.count, args.pcap)) + print("Open it in Wireshark: the ASAM CMP dissector keys on EtherType 0x99FE.") + + print("\n%s" % ("FAILED (%u problems)" % failures if failures else "PASSED")) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/cmp_demo/test/test_local.sh b/examples/cmp_demo/test/test_local.sh new file mode 100755 index 00000000..8783a27f --- /dev/null +++ b/examples/cmp_demo/test/test_local.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# +# test_local.sh - end to end test of the cmp_demo capture module, on this machine +# +# Runs the envelope codec unit test, then starts cmp_demo and drives it with fake_sink.py, +# which plays the part of the XCP tool: it queries the REST interface and tunnels XCP +# CONNECT / GET_STATUS / DISCONNECT through CMP. +# +# No veth pair and no network namespace are needed, unlike the plain raw Ethernet +# transport: with CMP over UDP (6.4.2) the outer transport is an ordinary UDP socket, the +# emulated ECU address only ever appears inside the CMP payload, and loopback is enough. +# Nothing here needs root. +# +# For the on-target (Raspberry Pi) variant see ../test.sh in the example root. +# +# Usage: ./test/test_local.sh [build_dir] (default: build) + +set -u + +DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BUILD_DIR="${1:-$DEMO_DIR/build}" +DEMO="$BUILD_DIR/cmp_demo" +CODEC_TEST="$BUILD_DIR/cmp_codec_test" + +CMP_PORT=55555 +REST_PORT=8080 +WORK_DIR="$(mktemp -d)" +# The pcap goes to the folder the script was started from, not into WORK_DIR: it is the +# one artifact meant to be opened afterwards. Everything else the demo writes - demo.log, +# the .a2l and the .bin - stays in WORK_DIR and is thrown away with it. +OUT_DIR="$(pwd)" +PCAP="$OUT_DIR/cmp.pcap" +DEMO_PID="" + +cleanup() { + if [ -n "$DEMO_PID" ] && kill -0 "$DEMO_PID" 2>/dev/null; then + kill -TERM "$DEMO_PID" 2>/dev/null + wait "$DEMO_PID" 2>/dev/null + fi +} +trap cleanup EXIT + +fail() { echo "FAILED: $*"; exit 1; } + +for binary in "$DEMO" "$CODEC_TEST"; do + [ -x "$binary" ] || fail "$binary not found. Build first: + cmake -B build -S . -Dxcplite_DIR=/lib/cmake/xcplite + cmake --build build" +done + +echo "==============================================================" +echo "1. CMP envelope codec, against the ASAM CMP 1.1.0 sample files" +echo "==============================================================" +"$CODEC_TEST" || fail "the codec unit test did not pass" + +echo +echo "==============================================================" +echo "2. cmp_demo end to end, driven by fake_sink.py" +echo "==============================================================" + +# Refuse to start when something already holds the ports. Both sockets are opened with +# SO_REUSEADDR, so a leftover cmp_demo from an aborted run does not necessarily make the +# bind fail - it can instead leave the exchange talking to the STALE process while the one +# started here has already exited. That produces a confusing failure much further down. +for port in "$CMP_PORT" "$REST_PORT"; do + holder=$(lsof -nP -iTCP:"$port" -iUDP:"$port" 2>/dev/null | awk 'NR>1 {print $2" ("$1")"}' | sort -u | tr '\n' ' ') + [ -z "$holder" ] || fail "port $port is already in use by: $holder + A cmp_demo from an earlier run is probably still alive. Stop it with: + pkill -x cmp_demo" +done + +cd "$WORK_DIR" || fail "cannot enter $WORK_DIR" + +"$DEMO" --listen "$CMP_PORT" --rest-port "$REST_PORT" > demo.log 2>&1 & +DEMO_PID=$! + +# Wait for the REST port to accept connections rather than grepping the log: it is the +# only readiness signal that does not depend on how stdout happens to be buffered. +wait_for_port() { + python3 - "$1" <<'PY' +import socket, sys, time +port = int(sys.argv[1]) +deadline = time.time() + 10.0 +while time.time() < deadline: + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.2): + sys.exit(0) + except OSError: + time.sleep(0.1) +sys.exit(1) +PY +} +wait_for_port "$REST_PORT" || { + echo "--- cmp_demo log ---"; cat demo.log + fail "cmp_demo did not start listening on port $REST_PORT within 10s" +} + +# The port answering does not prove OUR process is the one answering it. +kill -0 "$DEMO_PID" 2>/dev/null || { + echo "--- cmp_demo log ---"; cat demo.log + fail "the cmp_demo started here has already exited, yet port $REST_PORT answers. + Something else is serving these ports - see the log above." +} + +grep -E "CMP capture module:|CMP transport:|CMP frame budget:" demo.log +echo + +python3 "$DEMO_DIR/test/fake_sink.py" \ + --target "127.0.0.1:$CMP_PORT" \ + --rest "127.0.0.1:$REST_PORT" \ + --pcap "$PCAP" || fail "fake_sink.py reported a problem" + +echo +echo "==============================================================" +echo "3. Multicast discovery (12.1.1)" +echo "==============================================================" +"$DEMO_DIR/test/discovery_probe.py" --expect-http "$REST_PORT" \ + || fail "the capture module did not answer CMP_CM_DISCOVERY" + +cleanup +DEMO_PID="" + +echo +echo "==============================================================" +echo "4. Capture module counters" +echo "==============================================================" +grep -E "^ CMP: " demo.log || fail "no summary line in the cmp_demo log" + +# Anything dropped or refused means the two sides disagree about the wire format +if grep -qE "dropped [1-9]|refused" demo.log; then + echo "--- cmp_demo log ---"; cat demo.log + fail "the capture module dropped or refused messages" +fi + +echo +echo "Capture file for Wireshark: $PCAP" +echo " (its ASAM CMP dissector keys on EtherType 0x99FE)" +echo +echo "PASSED" diff --git a/examples/freertos_demo/README.md b/examples/freertos_demo/README.md index dc963240..48ce1878 100644 --- a/examples/freertos_demo/README.md +++ b/examples/freertos_demo/README.md @@ -51,6 +51,7 @@ The following files are required: xcplite_sources = [ "cal.c", "platform.c", + "sockets.c", "queue32m.c", "xcpappl.c", "xcpethserver.c", @@ -60,7 +61,7 @@ xcplite_sources = [ ``` The FreeRTOS build of XCPlite: -- Uses the FreeRTOS/lwIP socket, thread, mutex and clock platform abstractions in `src/platform.c`. +- Uses the FreeRTOS/lwIP socket, thread, mutex and clock platform abstractions in `src/platform.c` and `src/sockets.c`. - Uses `src/queue32m.c` with mutex or critical-section synchronization. @@ -89,9 +90,9 @@ xcpclient --offline --udp --dest-addr --elf --elf-unit-fil # Automatically add all possible measurement variables and calibration parameters in calibration parameter segments from compilation unit 'xcp_demo' # Example freertos_stm32_demo: -xcpclient --offline --udp --dest-addr 192.168.0.207 --elf build/Debug/STM32H753EthDemo.elf --a2l CANape/stm32_freertos_demo.a2l --elf-unit-filter xcp_demo -# Example freertos_emu_demo: -xcpclient --offline --udp --dest-addr 127.0.0.1 --elf build-rtos/Debug/freertos_emu_demo --a2l examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.a2l --elf-unit-filter xcp_demo +xcpclient --offline --udp --dest-addr 192.168.0.207 --elf build/Debug/STM32H753EthDemo.elf --a2l CANape/stm32_freertos_demo.a2l --default-event=mainloop --elf-unit-filter xcp_demo +# Example freertos_emu_demo (Linux build only: an executable built on macOS contains no DWARF debug information and is rejected by xcpclient): +xcpclient --offline --udp --dest-addr 127.0.0.1 --elf build-rtos/Debug/freertos_emu_demo --a2l examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.a2l --default-event=mainloop --elf-unit-filter xcp_demo ``` See below how to obtain the xcpclient tool. @@ -185,7 +186,7 @@ DWARF type or location data. For the complete technical details — ELF section layouts, `trg__` anchor naming convention, and `AddrExt` encoding — see -[docs/TECHNICAL.md — Offline A2L Generation](../../docs/TECHNICAL.md#offline-a2l-generation--elfdwarf-internals). +[docs/OFFLINE_A2L.md — Offline A2L Generation](../../docs/OFFLINE_A2L.md). @@ -295,7 +296,8 @@ It has been tested with ELF files from Linux gcc and clang tool chains. For more information on offline A2L generation see: - [tools/xcpclient/README.md](../../tools/xcpclient/README.md) — xcpclient documentation and all command-line options - [examples/no_a2l_demo/README.md](../no_a2l_demo/README.md) — dedicated no-A2L / offline A2L workflow example -- [docs/TECHNICAL.md — Offline A2L Generation](../../docs/TECHNICAL.md#offline-a2l-generation) — ELF/DWARF internals and design details of the offline A2L generation approach +- [docs/OFFLINE_A2L.md — Offline A2L Generation](../../docs/OFFLINE_A2L.md) — offline A2L generation with xcpclient: workflow, naming rules, supported types, diagnostics +- [docs/TECHNICAL.md — Instrumentation Markers](../../docs/TECHNICAL.md#instrumentation-markers-for-offline-a2l-tools) — the markers the instrumentation macros leave in the ELF file - [examples/freertos_demo/README.md](../freertos_demo/README.md) — Linux FreeRTOS demo with offline A2L generation @@ -352,9 +354,9 @@ target_compile_definitions(freertos_config INTERFACE projCOVERAGE_TEST=0) ### Step 3 — Implement the socket layer When `_FREE_RTOS` is defined **without** `FREE_RTOS_POSIX_SIM`, the socket functions in -`platform.c` use the lwIP socket API. +`sockets.c` use the lwIP socket API. Replace them with another implementation if required. -The required interface is documented in `src/platform.h` (search for `SOCKET_HANDLE`). +The required interface is documented in `src/sockets.h`. ### Step 4 — Implement the clock (bare-metal) diff --git a/examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.a2l b/examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.a2l index 41861bbd..7c4399fa 100644 --- a/examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.a2l +++ b/examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.a2l @@ -1,5 +1,5 @@ - -/* Created by xcp_client with ELF/DWARF information only, offline mode - 2026-06-16 13:54:54 */ + +/* Created by xcp_client with ELF/DWARF information only, offline mode - 2026-09-08 10:16:19 */ ASAP2_VERSION 1 71 /begin PROJECT project_name "" /begin HEADER "" VERSION "1.0" PROJECT_NO XCPLITE__ACSDD /end HEADER @@ -82,8 +82,8 @@ ASAP2_VERSION 1 71 /begin TYPEDEF_CHARACTERISTIC C_F32 "" VALUE F32 0 NO_COMPU_METHOD -1e12 1e12 /end TYPEDEF_CHARACTERISTIC /begin MOD_PAR "" -EPK "V102" ADDR_EPK 0x24000714 -/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x24000714 4 -1 -1 -1 -1 -1 +EPK "V102" ADDR_EPK 0x00030278 +/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x30278 4 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 0 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -92,7 +92,7 @@ EPK "V102" ADDR_EPK 0x24000714 /end SEGMENT /end IF_DATA /end MEMORY_SEGMENT -/begin MEMORY_SEGMENT parameters "" DATA FLASH INTERN 0x803B0B0 16 -1 -1 -1 -1 -1 +/begin MEMORY_SEGMENT parameters "" DATA FLASH INTERN 0xFD80 32 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 1 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -105,7 +105,7 @@ EPK "V102" ADDR_EPK 0x24000714 /begin IF_DATA XCP /begin PROTOCOL_LAYER - 0x0104 1000 2000 0 0 0 0 0 252 1468 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE + 0x0104 1000 2000 0 0 0 0 0 248 1024 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE OPTIONAL_CMD GET_COMM_MODE_INFO OPTIONAL_CMD GET_ID OPTIONAL_CMD SET_REQUEST @@ -143,56 +143,65 @@ EPK "V102" ADDR_EPK 0x24000714 0x1 SIZE_DWORD UNIT_1US TIMESTAMP_FIXED /end TIMESTAMP_SUPPORTED - /* compilation unit = 80, function = fastTask, CFA = 0 */ - /begin EVENT "fastTask" "fastTask" 0 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT - /* compilation unit = 80, function = slowTask, CFA = 0 */ - /begin EVENT "slowTask" "slowTask" 1 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT + /* compilation unit = 1, function = slowTask, CFA = 112 */ + /begin EVENT "slowTask" "slowTask" 0 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT + /* compilation unit = 1, function = fastTask, CFA = 96 */ + /begin EVENT "fastTask" "fastTask" 1 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT /end DAQ - /begin XCP_ON_UDP_IP 0x0104 5555 ADDRESS "192.168.0.207" /end XCP_ON_UDP_IP + /begin XCP_ON_UDP_IP 0x0104 5555 ADDRESS "192.168.0.206" /end XCP_ON_UDP_IP /end IF_DATA /* TypeDefs */ -/begin TYPEDEF_CHARACTERISTIC fast_task_period_ms "" VALUE U32 0 IDENTITY 0 4294967295 /end TYPEDEF_CHARACTERISTIC -/begin TYPEDEF_CHARACTERISTIC slow_task_period_ms "" VALUE U32 0 IDENTITY 0 4294967295 /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC fast_task_period_ms "" VALUE U32 0 IDENTITY 0 4294967295 PHYS_UNIT "ms" /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC slow_task_period_ms "" VALUE U32 0 IDENTITY 0 4294967295 PHYS_UNIT "ms" /end TYPEDEF_CHARACTERISTIC /begin TYPEDEF_CHARACTERISTIC counter_max "" VALUE U16 0 IDENTITY 0 65535 /end TYPEDEF_CHARACTERISTIC -/begin TYPEDEF_CHARACTERISTIC amplitude "" VALUE F32 0 NO_COMPU_METHOD -100000000000000000000000000000000 100000000000000000000000000000000 /end TYPEDEF_CHARACTERISTIC -/begin TYPEDEF_STRUCTURE parameters "" 16 +/begin TYPEDEF_CHARACTERISTIC amplitude "" VALUE F32 0 NO_COMPU_METHOD -100000000000000000000000000000000 100000000000000000000000000000000 PHYS_UNIT "bar" /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC sensor_voltage_point1 "Pressure sensor voltage at two-point calibration point 1" VALUE F32 0 NO_COMPU_METHOD -100000000000000000000000000000000 100000000000000000000000000000000 PHYS_UNIT "V" /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC pressure_point1 "Pressure at two-point calibration point 1" VALUE F32 0 NO_COMPU_METHOD -100000000000000000000000000000000 100000000000000000000000000000000 PHYS_UNIT "bar" /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC sensor_voltage_point2 "Pressure sensor voltage at two-point calibration point 2" VALUE F32 0 NO_COMPU_METHOD -100000000000000000000000000000000 100000000000000000000000000000000 PHYS_UNIT "V" /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC pressure_point2 "Pressure at two-point calibration point 2" VALUE F32 0 NO_COMPU_METHOD -100000000000000000000000000000000 100000000000000000000000000000000 PHYS_UNIT "bar" /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_STRUCTURE parameters "" 32 /begin STRUCTURE_COMPONENT fast_task_period_ms fast_task_period_ms 0 /end STRUCTURE_COMPONENT /begin STRUCTURE_COMPONENT slow_task_period_ms slow_task_period_ms 4 /end STRUCTURE_COMPONENT /begin STRUCTURE_COMPONENT counter_max counter_max 8 /end STRUCTURE_COMPONENT /begin STRUCTURE_COMPONENT amplitude amplitude 12 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT sensor_voltage_point1 sensor_voltage_point1 16 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT pressure_point1 pressure_point1 20 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT sensor_voltage_point2 sensor_voltage_point2 24 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT pressure_point2 pressure_point2 28 /end STRUCTURE_COMPONENT /end TYPEDEF_STRUCTURE /* Measurements */ -/* Measurements for event 'fastTask' */ -/begin MEASUREMENT fastTask.counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0xFFFA ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 0 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x2401A33C /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 0 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT channel1 "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x2401A340 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 0 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT calseg_id_parameters "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x24000034 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 0 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT evt_id_fastTask "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x24000036 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 0 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT fastTaskOverruns "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x2401A34C /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 0 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT slowTaskOverruns "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x2401A350 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 0 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT - /* Measurements for event 'slowTask' */ -/begin MEASUREMENT slowTask.counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x40FFFA ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT evt_id_slowTask "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x2400003A /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_counter "Global measurement variable, incremented in fastTask" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x302B2 READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT channel1 "Pressure measured on analog channel 1 or generated sine wave, updated in slowTask" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x302A8 PHYS_UNIT "bar" /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT fastTaskOverruns "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x302B4 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT slowTaskOverruns "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x302AC /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT calseg_id_parameters "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30228 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT slowTask.counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x1006E ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT slowTask.lastWakeTime "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x10060 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT + +/* Measurements for event 'fastTask' */ +/begin MEASUREMENT fastTask.counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x41005E ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT fastTask.lastWakeTime "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x410050 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT static_counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x302B0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT /* Measurements without fixed event */ -/begin GROUP Measurements "" ROOT /begin SUB_GROUP fastTask slowTask /end SUB_GROUP /end GROUP -/begin GROUP fastTask "" /begin REF_MEASUREMENT fastTask.counter global_counter channel1 calseg_id_parameters evt_id_fastTask fastTaskOverruns slowTaskOverruns /end REF_MEASUREMENT /end GROUP -/begin GROUP slowTask "" /begin REF_MEASUREMENT slowTask.counter evt_id_slowTask /end REF_MEASUREMENT /end GROUP +/begin GROUP Measurements "" ROOT /begin SUB_GROUP slowTask fastTask /end SUB_GROUP /end GROUP +/begin GROUP slowTask "" /begin REF_MEASUREMENT global_counter channel1 fastTaskOverruns slowTaskOverruns calseg_id_parameters slowTask.counter slowTask.lastWakeTime /end REF_MEASUREMENT /end GROUP +/begin GROUP fastTask "" /begin REF_MEASUREMENT fastTask.counter fastTask.lastWakeTime static_counter /end REF_MEASUREMENT /end GROUP /* Axis */ /* Characteristics */ -/begin INSTANCE parameters "" parameters 0x803B0B0 /end INSTANCE +/begin INSTANCE parameters "" parameters 0xFD80 /end INSTANCE /* Characteristic and Axis Groups */ /begin GROUP Characteristics "" ROOT /begin SUB_GROUP parameters /end SUB_GROUP /end GROUP diff --git a/examples/freertos_demo/freertos_emu_demo/CMakeLists.txt b/examples/freertos_demo/freertos_emu_demo/CMakeLists.txt index 51cfab32..da484137 100644 --- a/examples/freertos_demo/freertos_emu_demo/CMakeLists.txt +++ b/examples/freertos_demo/freertos_emu_demo/CMakeLists.txt @@ -75,8 +75,8 @@ endif() target_compile_definitions(xcplite PRIVATE - _FREE_RTOS # Force FreeRTOS code paths in platform.h/platform.c, xcpappl.c - FREE_RTOS_POSIX_SIM # Use Linux sockets and clock code in platform.c when _FREE_RTOS is defined + _FREE_RTOS # Force FreeRTOS code paths in platform.h/c and sockets.h/c, xcpappl.c + FREE_RTOS_POSIX_SIM # Use Linux sockets and clock code in platform.c/sockets.c when _FREE_RTOS is defined "XCPLIB_CFG_OVERRIDE=\"xcplib_rtos_cfg.h\"" ) diff --git a/examples/freertos_demo/freertos_emu_demo/README.md b/examples/freertos_demo/freertos_emu_demo/README.md index 5b401dbc..2199fb6a 100644 --- a/examples/freertos_demo/freertos_emu_demo/README.md +++ b/examples/freertos_demo/freertos_emu_demo/README.md @@ -1,6 +1,7 @@ # freertos_emu_demo — XCPlite on FreeRTOS POSIX emulator -This example uses the **FreeRTOS POSIX simulator** port so the demo builds and runs on macOS or Linux. +This example uses the **FreeRTOS POSIX simulator** port so the demo builds and runs on macOS or Linux. +A2L generation works on Linux only, ## Notes diff --git a/examples/freertos_demo/freertos_emu_demo/create_a2l.sh b/examples/freertos_demo/freertos_emu_demo/create_a2l.sh new file mode 100755 index 00000000..bd3b224b --- /dev/null +++ b/examples/freertos_demo/freertos_emu_demo/create_a2l.sh @@ -0,0 +1,209 @@ +#!/bin/bash + +# A2L file creator for the freertos_emu_demo example project + +# The script syncs the example project to the target, builds it there, runs it with XCP on Ethernet, +# downloads the ELF file to the local machine and creates an A2L file. +# Prerequisites: +# - The target machine must be Linux +# - The target must be reachable via SSH and have rsync installed +# - The local machine must have rsync and scp installed +# - The local machine must have xcpclient installed + + +echo "========================================================================================================" +echo "A2L file creator for the freertos_emu_demo example project" +echo "========================================================================================================" + + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 1 +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" || exit 1 + +if [ ! -f "$REPO_ROOT/build.sh" ] || [ ! -f "$REPO_ROOT/CMakeLists.txt" ]; then + echo "❌ FAILED: Repository root not found at $REPO_ROOT" >&2 + exit 1 +fi + +#====================================================================================================================== +# Parameters +#====================================================================================================================== + +# Build on the remote Linux target (true) or on the local machine (false) +# A local build is possible on Linux only: executables built on macOS (Mach-O) contain no DWARF debug information, +# the xcpclient A2L generator can not create an A2L file from them +REMOTE=true + +# Run a short calibration and measurement test +TEST=false +CSVFILE="$REPO_ROOT/examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.csv" + + + +LOGFILE="$REPO_ROOT/examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.log" +#LOGFILE='/dev/stdout' +#LOGFILE="/dev/null" + +# A2L file path on local machine +A2LFILE="$REPO_ROOT/examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.a2l" + +# ELF file path on local machine +ELFFILE="$REPO_ROOT/examples/freertos_demo/freertos_emu_demo/CANape/freertos_demo.elf" + +# Build type for target executable: Release, RelWithDebInfo or Debug +# RelWithDebInfo is default to demonstrate operation with with -O1 and NDEBUG +# Optimization level >= -O1 keeps variables in registers whenever possible, so these local variables cannot be measured +# Debug mode is the least efficient but keeps all variables and stack frames intact +BUILD_TYPE="RelWithDebInfo" +# -O0 +#BUILD_TYPE="Debug" +# -O2 no debug symbols +#BUILD_TYPE="Release" + + +# Target connection details +#TARGET_USER="parallels" +#TARGET_HOST="10.211.55.4" +TARGET_USER="rainer" +TARGET_HOST="192.168.0.206" +TARGET_PATH="~/XCPlite-rtos" +TARGET_BUILD_DIR="build-rtos" +TARGET_BINARY="freertos_emu_demo" + +# Path to xcpclient tool executable (assuming cargo installed it to ~/.cargo/bin) +XCPCLIENT="xcpclient" + + +#====================================================================================================================== +# Sync Target, Build Application on Target, Download ELF, Start ECU, ... +#====================================================================================================================== + +mkdir -p "$(dirname "$LOGFILE")" +echo "Logging to $LOGFILE enabled" +echo "" > "$LOGFILE" + +#====================================================================================================================== +# Remote build +# Sync target, build, upload ELF, start application on target +#====================================================================================================================== + +if [ "$REMOTE" = true ]; then + +# Sync target +echo "Sync $REPO_ROOT/ to $TARGET_USER@$TARGET_HOST:$TARGET_PATH/ ..." +rsync -avz --delete \ + --include='/build.sh' \ + --include='/CMakeLists.txt' \ + --include='/cmake/***' \ + --include='/inc/***' \ + --include='/src/***' \ + --include='/examples/' \ + --include='/examples/freertos_demo/***' \ + --exclude='*' \ + "$REPO_ROOT/" "$TARGET_USER@$TARGET_HOST:$TARGET_PATH/" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Rsync with target" + exit 1 +fi + + +# Build on target +# Always a clean build: if the target has no NTP and its clock may skew, +echo "Clean build executable on Target ..." +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && ./build.sh $BUILD_TYPE rtos examples clean" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Build on target" + exit 1 +fi + + +# Download the target executable for the local A2L generation process +echo "Downloading ELF file from target $TARGET_PATH/$TARGET_BUILD_DIR/$TARGET_BINARY to $ELFFILE ..." +scp "$TARGET_USER@$TARGET_HOST:$TARGET_PATH/$TARGET_BUILD_DIR/$TARGET_BINARY" "$ELFFILE" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Download $TARGET_PATH/$TARGET_BUILD_DIR/$TARGET_BINARY" + exit 1 +fi + +else + +# Build local +# Linux only: the macOS linker does not put the DWARF debug information into the executable (Mach-O), +# xcpclient can not create an A2L file from it +if [ "$(uname -s)" = "Darwin" ]; then + echo "❌ FAILED: A local build on macOS creates a Mach-O executable without DWARF debug information, xcpclient can not create an A2L file from it" + echo " Build on a Linux target instead: set REMOTE=true in $SCRIPT_DIR/create_a2l.sh" + exit 1 +fi +echo "Build ..." +"$REPO_ROOT/build.sh" $BUILD_TYPE rtos examples 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Build" + exit 1 +fi + +cp "$REPO_ROOT/$TARGET_BUILD_DIR/$TARGET_BINARY" "$ELFFILE" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Copy $REPO_ROOT/$TARGET_BUILD_DIR/$TARGET_BINARY to $ELFFILE" + exit 1 +fi + +fi + + +#====================================================================================================================== +# Create A2L file +# Create the A2L from ELF file with xcpclient tool +#====================================================================================================================== + +echo "" +echo "========================================================================================================" +echo "Creating A2L file from XCPlite ELF file ..." +echo "========================================================================================================" +echo "" +# --log-level is program flow verbosity +# --verbose is information detail level +# Remove the A2L file of a previous run, so a failed generation can not leave a stale A2L file behind +rm -f "$A2LFILE" +XCPCLIENT_ARGS=(--log-level=3 --verbose=5 --dest-addr="$TARGET_HOST" --udp --offline --elf "$ELFFILE" --elf-unit-filter xcp_demo --default-event=0 --create-a2l --a2l "$A2LFILE") +echo "Command: $XCPCLIENT ${XCPCLIENT_ARGS[*]}" +"$XCPCLIENT" "${XCPCLIENT_ARGS[@]}" >> "$LOGFILE" +if [ $? -ne 0 ] || [ ! -f "$A2LFILE" ]; then + echo "❌ FAILED: xcpclient could not create the A2L file $A2LFILE, see $LOGFILE" + grep "\[ERROR\]" "$LOGFILE" + exit 1 +fi + + + +echo "" +echo "✅ SUCCESS:" +echo "Created a new A2L file $A2LFILE" +echo "" + + +#====================================================================================================================== +# Test +#====================================================================================================================== + +if [ "$TEST" = true ]; then + +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && ./$TARGET_BUILD_DIR/$TARGET_BINARY" & +sleep 1 + +echo "========================================================================================================" +echo "Test connect" +echo "========================================================================================================" +read -p "Press any key to continue..." -n1 -s +$XCPCLIENT --log-level=3 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --list-mea . --list-cal . +sleep 1 + +echo "========================================================================================================" +echo "Test measurement" +echo "========================================================================================================" +read -p "Press any key to continue..." -n1 -s +$XCPCLIENT --log-level=3 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --mea counter --time 3 --csv "$CSVFILE" +sleep 1 + +ssh "$TARGET_USER@$TARGET_HOST" "pkill -f freertos_emu_demo" + +fi diff --git a/examples/freertos_demo/freertos_esp32_demo/CANape/freertos_demo.a2l b/examples/freertos_demo/freertos_esp32_demo/CANape/freertos_demo.a2l index 95458468..9662a2c7 100644 --- a/examples/freertos_demo/freertos_esp32_demo/CANape/freertos_demo.a2l +++ b/examples/freertos_demo/freertos_esp32_demo/CANape/freertos_demo.a2l @@ -1,5 +1,5 @@  -/* Created by xcp_client with ELF/DWARF information only, offline mode - 2026-08-17 12:48:59 */ +/* Created by xcp_client with ELF/DWARF information only, offline mode - 2026-09-09 21:31:05 */ ASAP2_VERSION 1 71 /begin PROJECT project_name "" /begin HEADER "" VERSION "1.0" PROJECT_NO XCPLITE__ACSDD /end HEADER @@ -82,8 +82,8 @@ ASAP2_VERSION 1 71 /begin TYPEDEF_CHARACTERISTIC C_F32 "" VALUE F32 0 NO_COMPU_METHOD -1e12 1e12 /end TYPEDEF_CHARACTERISTIC /begin MOD_PAR "" -EPK "V102" ADDR_EPK 0x3C0E2810 -/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x3C0E2810 4 -1 -1 -1 -1 -1 +EPK "V102" ADDR_EPK 0x3C0E28D0 +/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x3C0E28D0 4 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 0 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -92,7 +92,7 @@ EPK "V102" ADDR_EPK 0x3C0E2810 /end SEGMENT /end IF_DATA /end MEMORY_SEGMENT -/begin MEMORY_SEGMENT parameters "" DATA FLASH INTERN 0x3C0C8D28 32 -1 -1 -1 -1 -1 +/begin MEMORY_SEGMENT parameters "" DATA FLASH INTERN 0x3C0C8E04 32 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 1 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -105,7 +105,7 @@ EPK "V102" ADDR_EPK 0x3C0E2810 /begin IF_DATA XCP /begin PROTOCOL_LAYER - 0x0104 1000 2000 0 0 0 0 0 248 248 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE + 0x0104 1000 2000 0 0 0 0 0 248 1024 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE OPTIONAL_CMD GET_COMM_MODE_INFO OPTIONAL_CMD GET_ID OPTIONAL_CMD SET_REQUEST @@ -138,19 +138,21 @@ EPK "V102" ADDR_EPK 0x3C0E2810 /end PROTOCOL_LAYER /begin DAQ - DYNAMIC 0 2 0 OPTIMISATION_TYPE_DEFAULT ADDRESS_EXTENSION_FREE IDENTIFICATION_FIELD_TYPE_RELATIVE_BYTE GRANULARITY_ODT_ENTRY_SIZE_DAQ_BYTE 0xF8 OVERLOAD_INDICATION_PID + DYNAMIC 0 3 0 OPTIMISATION_TYPE_DEFAULT ADDRESS_EXTENSION_FREE IDENTIFICATION_FIELD_TYPE_RELATIVE_BYTE GRANULARITY_ODT_ENTRY_SIZE_DAQ_BYTE 0xF8 OVERLOAD_INDICATION_PID /begin TIMESTAMP_SUPPORTED 0x1 SIZE_DWORD UNIT_1US TIMESTAMP_FIXED /end TIMESTAMP_SUPPORTED - /* compilation unit = 85, function = slowTask, CFA = 0 */ + /* compilation unit = 86, function = slowTask, CFA = 0 */ /begin EVENT "slowTask" "slowTask" 0 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT - /* compilation unit = 85, function = fastTask, CFA = 0 */ + /* compilation unit = 86, function = fastTask, CFA = 0 */ /begin EVENT "fastTask" "fastTask" 1 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT + /* compilation unit = 86, function = foo, CFA = 0 */ + /begin EVENT "foo" "foo" 2 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT /end DAQ - /begin XCP_ON_UDP_IP 0x0104 5555 ADDRESS "192.168.0.154" /end XCP_ON_UDP_IP + /begin XCP_ON_UDP_IP 0x0104 5555 ADDRESS "192.168.0.146" /end XCP_ON_UDP_IP /end IF_DATA @@ -174,39 +176,71 @@ EPK "V102" ADDR_EPK 0x3C0E2810 /begin STRUCTURE_COMPONENT sensor_voltage_point2 sensor_voltage_point2 24 /end STRUCTURE_COMPONENT /begin STRUCTURE_COMPONENT pressure_point2 pressure_point2 28 /end STRUCTURE_COMPONENT /end TYPEDEF_STRUCTURE +/begin TYPEDEF_MEASUREMENT index_ "" UWORD IDENTITY 0 0 0 65535 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT params_ptr_ "" ULONG IDENTITY 0 0 0 4294967295 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE CalSegGuard "" 8 + /begin STRUCTURE_COMPONENT index_ index_ 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT params_ptr_ params_ptr_ 4 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE +/begin TYPEDEF_MEASUREMENT a "" UWORD IDENTITY 0 0 0 65535 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT b "" SWORD IDENTITY 0 0 -32768 32767 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT f "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT d "" UBYTE IDENTITY 0 0 0 255 MATRIX_DIM 3 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE test_struct "" 12 + /begin STRUCTURE_COMPONENT a a 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT b b 2 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT f f 4 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT d d 8 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE /* Measurements */ /* Measurements for event 'slowTask' */ -/begin MEASUREMENT MOSI "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x42014BD8 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT MISO "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x42014B90 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT slowTask.counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0xFFD2 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_counter "Global measurement variable, incremented in fastTask" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3FCA4C98 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT channel1 "Pressure measured on analog channel 1 or generated sine wave, updated in slowTask" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x3FCA4C94 PHYS_UNIT "bar" /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT pressure_sensor_voltage "Raw pressure sensor voltage measured on analog channel 1" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x3FC96F28 PHYS_UNIT "V" /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT fastTaskOverruns "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x3FCA4C90 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT slowTaskOverruns "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x3FCA4C8C /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT calseg_id_parameters "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3FC96F24 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT slowTask.phase "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0xFFBC ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT slowTask.fast_task_period_ms "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0xFFC0 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT slowTask.counter "Local measurement variable in `slowTask`" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0xFFD2 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE slowTask.params "" CalSegGuard 0xFFD8 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE /begin MEASUREMENT slowTask.lastWakeTime "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0xFFD4 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT /* Measurements for event 'fastTask' */ -/begin MEASUREMENT fastTask.counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x40FFD2 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT fastTask.lastWakeTime "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x40FFD4 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT static_counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3FCA4C88 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT MOSI "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x420149C4 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT MISO "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x4201497C /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT fastTask.counter "Local measurement variable in `fastTask`" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x40FFD2 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE fastTask.params "" CalSegGuard 0x40FFD8 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT global_counter "Global measurement variable, incremented in fastTask" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3FCA7054 READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT channel1 "Pressure measured on analog channel 1 or generated sine wave, updated in slowTask" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x3FCA7050 PHYS_UNIT "bar" /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT fastTaskOverruns "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x3FCA704C /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT slowTaskOverruns "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x3FCA7048 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT calseg_id_parameters "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3FC96F24 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT fastTask.lastWakeTime "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x40FFD4 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT fastTask.static_counter "Local static measurement variable in `fastTask`" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3FCA7044 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT + +/* Measurements for event 'foo' */ +/begin MEASUREMENT foo.static_counter "Local static measurement variable in function `foo`" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3FCA7046 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x80FF8D ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x80FF8E ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0x80FF94 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x80FF98 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.counter "Local captured measurement variable in function `foo`" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x800000 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x800004 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x800008 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x800010 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x800012 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x800014 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x800018 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE foo.test_struct "" test_struct 0x800020 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT foo.test_array "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x80002C ECU_ADDRESS_EXTENSION 3 MATRIX_DIM 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT /* Measurements without fixed event */ -/begin GROUP Measurements "" ROOT /begin SUB_GROUP slowTask fastTask /end SUB_GROUP /end GROUP -/begin GROUP slowTask "" /begin REF_MEASUREMENT MOSI MISO slowTask.counter global_counter channel1 pressure_sensor_voltage fastTaskOverruns slowTaskOverruns calseg_id_parameters slowTask.phase slowTask.fast_task_period_ms slowTask.lastWakeTime /end REF_MEASUREMENT /end GROUP -/begin GROUP fastTask "" /begin REF_MEASUREMENT fastTask.counter fastTask.lastWakeTime static_counter /end REF_MEASUREMENT /end GROUP +/begin GROUP Measurements "" ROOT /begin SUB_GROUP slowTask fastTask foo /end SUB_GROUP /end GROUP +/begin GROUP slowTask "" /begin REF_MEASUREMENT slowTask.counter slowTask.params slowTask.lastWakeTime /end REF_MEASUREMENT /end GROUP +/begin GROUP fastTask "" /begin REF_MEASUREMENT MOSI MISO fastTask.counter fastTask.params global_counter channel1 fastTaskOverruns slowTaskOverruns calseg_id_parameters fastTask.lastWakeTime fastTask.static_counter /end REF_MEASUREMENT /end GROUP +/begin GROUP foo "" /begin REF_MEASUREMENT foo.static_counter foo.test_int8 foo.test_int16 foo.test_int32 foo.test_int64 foo.counter foo.test_float foo.test_double foo.test_uint8 foo.test_uint16 foo.test_uint32 foo.test_uint64 foo.test_struct foo.test_array /end REF_MEASUREMENT /end GROUP /* Axis */ /* Characteristics */ -/begin INSTANCE parameters "" parameters 0x3C0C8D28 /end INSTANCE +/begin INSTANCE parameters "" parameters 0x3C0C8E04 /end INSTANCE /* Characteristic and Axis Groups */ /begin GROUP Characteristics "" ROOT /begin SUB_GROUP parameters /end SUB_GROUP /end GROUP diff --git a/examples/freertos_demo/freertos_esp32_demo/README.md b/examples/freertos_demo/freertos_esp32_demo/README.md index 5547679d..bcd4b7a1 100644 --- a/examples/freertos_demo/freertos_esp32_demo/README.md +++ b/examples/freertos_demo/freertos_esp32_demo/README.md @@ -91,11 +91,9 @@ continues to generate the original sine signal. Disable `OPTION_ANALOG` in ```bash pio device monitor ``` -4. Generate the A2L file from the firmware ELF. The current linker limitation - described under [Offline A2L generation](#offline-a2l-generation) must first - be resolved so the ELF retains the `xcp_evts` section name: +4. Generate the A2L file from the firmware ELF: ```bash - xcpclient --offline --udp --dest-addr --elf .pio/build/lilygo-t-display-s3/firmware.elf --a2l CANape/freertos_demo.a2l --elf-unit-filter xcp_demo --log-level=3 + xcpclient --offline --udp --dest-addr --elf .pio/build/lilygo-t-display-s3/firmware.elf --a2l CANape/freertos_demo.a2l --default-event=fastTask --elf-unit-filter xcp_demo --log-level=3 ``` 5. Connect with CANape using `CANape_Project`, or run a basic xcpclient measurement test: ```bash @@ -298,6 +296,7 @@ src/xcpethtl.c src/queue32m.c src/cal.c src/platform.c +src/sockets.c ``` The XCPlite source files remain in the repository `src/` folder and are not diff --git a/examples/freertos_demo/freertos_esp32_demo/create_a2l.sh b/examples/freertos_demo/freertos_esp32_demo/create_a2l.sh new file mode 100755 index 00000000..01cab8d1 --- /dev/null +++ b/examples/freertos_demo/freertos_esp32_demo/create_a2l.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# A2L file creator for the freertos_esp32_demo example project +# Creates the A2L file offline from the ELF file built with PlatformIO +# Transport layer UDP, the given IP address and port 5555 are written to the A2L file +# Prerequisites: +# - The firmware has been built with PlatformIO (pio run) +# - The local machine must have xcpclient installed: +# cd XCPlite +# ./build.sh rust_tools cargo_install + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 1 +cd "$SCRIPT_DIR" || exit 1 + +# IP address of the ESP32, written to the A2L file +TARGET_HOST="192.168.0.146" + +# ELF file built by PlatformIO +ELFFILE=".pio/build/lilygo-t-display-s3/firmware.elf" + +# A2L file +A2LFILE="CANape/freertos_demo.a2l" + +# LOG file +LOGFILE="CANape/freertos_demo.log" + +# Path to xcpclient tool executable (assuming cargo installed it to ~/.cargo/bin) +XCPCLIENT="xcpclient" + +if [ ! -f "$ELFFILE" ]; then + echo "❌ FAILED: ELF file $ELFFILE not found, build the firmware with PlatformIO first" + exit 1 +fi + +# Remove the A2L file of a previous run, so a failed generation can not leave a stale A2L file behind +rm -f "$A2LFILE" +XCPCLIENT_ARGS=(--offline --udp --dest-addr "$TARGET_HOST" --elf "$ELFFILE" --a2l "$A2LFILE" --elf-unit-limit=100 --elf-unit-filter xcp_demo --default-event=fastTask --log-level=3 --verbose=1) +echo "Command: $XCPCLIENT ${XCPCLIENT_ARGS[*]}" +"$XCPCLIENT" "${XCPCLIENT_ARGS[@]}" >$LOGFILE +if [ $? -ne 0 ] || [ ! -f "$A2LFILE" ]; then + echo "❌ FAILED: xcpclient could not create the A2L file $A2LFILE" + exit 1 +fi + +echo "✅ SUCCESS: Created a new A2L file $A2LFILE" diff --git a/examples/freertos_demo/freertos_esp32_demo/extra_script.py b/examples/freertos_demo/freertos_esp32_demo/extra_script.py index 5f8d098b..3bba9522 100644 --- a/examples/freertos_demo/freertos_esp32_demo/extra_script.py +++ b/examples/freertos_demo/freertos_esp32_demo/extra_script.py @@ -13,6 +13,7 @@ xcplite_sources = [ "cal.c", "platform.c", + "sockets.c", "queue32m.c", "xcpappl.c", "xcpethserver.c", diff --git a/examples/freertos_demo/freertos_esp32_demo/platformio.ini b/examples/freertos_demo/freertos_esp32_demo/platformio.ini index 90a0b54b..9fae5ab1 100644 --- a/examples/freertos_demo/freertos_esp32_demo/platformio.ini +++ b/examples/freertos_demo/freertos_esp32_demo/platformio.ini @@ -24,8 +24,10 @@ build_flags = -DXCPLIB_CFG_OVERRIDE=\"xcplib_rtos_cfg.h\" ; Enable this on LilyGo T-Display-S3 compatible boards: -DOPTION_DISPLAY - -DOPTION_IO - -DOPTION_ANALOG + ; Enable the 2 scope pins to observer the task activities + -DOPTION_IO + ; Enable this with an ADS1115 connected + ; -DOPTION_ANALOG ; Set these locally, for example: ; -DWIFI_SSID=\"your-ssid\" ; -DWIFI_PASSWORD=\"your-password\" diff --git a/examples/freertos_demo/freertos_esp32_demo/src/main.cpp b/examples/freertos_demo/freertos_esp32_demo/src/main.cpp index 5c8bf64a..98413103 100644 --- a/examples/freertos_demo/freertos_esp32_demo/src/main.cpp +++ b/examples/freertos_demo/freertos_esp32_demo/src/main.cpp @@ -350,7 +350,7 @@ static void initAnalogConverter() { ads1115Present = ads1115.begin(ADS1115_I2C_ADDRESS, &Wire); if (!ads1115Present) { - Serial.printf("ADS1115 not found at I2C address 0x%02X; using sine signal\n", ADS1115_I2C_ADDRESS); + Serial.printf("ADS1115 not found at I2C address 0x%02X\n", ADS1115_I2C_ADDRESS); return; } diff --git a/examples/freertos_demo/freertos_stm32_demo/CMakeLists.txt b/examples/freertos_demo/freertos_stm32_demo/CMakeLists.txt index eb40daf2..017dc4f5 100644 --- a/examples/freertos_demo/freertos_stm32_demo/CMakeLists.txt +++ b/examples/freertos_demo/freertos_stm32_demo/CMakeLists.txt @@ -59,6 +59,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE /git/XCPlite-RainerZ/src/queue32m.c /git/XCPlite-RainerZ/src/cal.c /git/XCPlite-RainerZ/src/platform.c + /git/XCPlite-RainerZ/src/sockets.c ) @@ -81,7 +82,7 @@ target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE # Add user defined symbols # for XCPlite library - _FREE_RTOS # Enable FreeRTOS code paths in platform.h/platform.c, xcpappl.c + _FREE_RTOS # Enable FreeRTOS code paths in platform.h/.c, sockets.h/c, xcpappl.c "XCPLIB_CFG_OVERRIDE=\"xcplib_rtos_cfg.h\"" # Enable configuration override for FreeRTOS # OPTION_CMSIS OPTION_IO diff --git a/examples/freertos_demo/xcp_demo.c b/examples/freertos_demo/xcp_demo.c index f870906e..fa09bd0b 100644 --- a/examples/freertos_demo/xcp_demo.c +++ b/examples/freertos_demo/xcp_demo.c @@ -116,7 +116,7 @@ uint16_t global_counter = 0; XCP_COMMENT(global_counter, "Global measurement variable, incremented in fastTask"); XCP_READ_WRITE(global_counter); -// Platform analog input when available, otherwise a generated sine signal +// Platform analog input when available #define SLOWTASK_PHASE_STEP_RAD 0.001f #define SINE_PERIOD_RAD 6.28318530717958647692f float channel1 = 0.0f; @@ -163,8 +163,8 @@ XCP_UNIT(parameters__pressure_point2, "bar"); // ¶meters is the A2l file address of the calibration parameter segment 'parameters' // Typename and variable name must be identical const struct parameters parameters = { - .fast_task_period_ms = 1, // 1 ms = 1000 Hz - .slow_task_period_ms = 2, // 2 ms = 500 Hz + .fast_task_period_ms = 1, // 1 ms = 1000 Hz + .slow_task_period_ms = 10, // 10 ms = 100 Hz .counter_max = 1000, .amplitude = 1.0f, .sensor_voltage_point1 = 0.0f, @@ -196,6 +196,50 @@ CalSegDecl(parameters); (x) = (y); \ } while (0) +//---------------------------------------------------------------------------------------------------- +// Functions + +XCP_NOINLINE void foo(void) { + + struct test_struct { + uint16_t a; + int16_t b; + float f; + uint8_t d[3]; + }; + + // Static local scope measurement variable + XCP_COMMENT(static_counter, "Local static measurement variable in function `foo`"); + volatile static uint16_t static_counter = 0; + + // Local measurement variable + XCP_COMMENT(counter, "Local captured measurement variable in function `foo`"); + uint32_t counter = 0; + + // More local measurement variables + + // Measured via capture + float test_float = 0.001f * static_counter; + double test_double = 0.002 * static_counter; + uint8_t test_uint8 = 1; + uint16_t test_uint16 = static_counter + 2; + uint32_t test_uint32 = static_counter + 3; + uint64_t test_uint64 = static_counter + 4; + struct test_struct test_struct = {1, -2, 0.003f * static_counter, {1, 2, 3}}; + uint8_t test_array[3] = {1, 2, static_counter & 0xff}; + + // Measure directly from stack, registers spilled to stack + XCP_MEAS int8_t test_int8 = static_counter - 1; + XCP_MEAS int16_t test_int16 = static_counter -2; + XCP_MEAS int32_t test_int32 = static_counter -3; + XCP_MEAS uint64_t test_int64 = static_counter -4; + + static_counter = static_counter + 1; + counter = static_counter; + + DaqCreateAndTriggerEventCapture(foo, counter, test_float, test_double, test_uint8, test_uint16, test_uint32, test_uint64, test_struct, test_array); +} + //---------------------------------------------------------------------------------------------------- // Tasks @@ -205,7 +249,10 @@ static void fastTask(void *parameter) { // Volatile keeps this local measurement variable visible in optimized builds, // The offline A2L generator can discover it in the ELF file and associate it to the functions DAQ event trigger. + XCP_COMMENT(counter, "Local measurement variable in `fastTask`"); volatile uint16_t counter = 0; + + XCP_COMMENT(static_counter, "Local static measurement variable in `fastTask`"); static volatile uint16_t static_counter = 0; printf("fastTask started\n"); @@ -238,8 +285,8 @@ static void fastTask(void *parameter) { // Save the task period parameter, don't delay during the lock to give XCP a chance to modify the parameters. clamp_parameter(period_ms, params->fast_task_period_ms, FASTTASK_PERIOD_MIN_MS, FASTTASK_PERIOD_MAX_MS); - counter++; - static_counter++; + counter = counter + 1; + static_counter = static_counter + 1; if (counter > params->counter_max) { counter = 0; static_counter = 0; @@ -275,7 +322,9 @@ static void fastTask(void *parameter) { static void slowTask(void *parameter) { (void)parameter; + XCP_COMMENT(counter, "Local measurement variable in `slowTask`"); volatile uint16_t counter = 0; + float phase = 0.0f; uint32_t slow_task_period_ms; uint32_t fast_task_period_ms; @@ -302,7 +351,7 @@ static void slowTask(void *parameter) { clamp_parameter(slow_task_period_ms, params->slow_task_period_ms, SLOWTASK_PERIOD_MIN_MS, SLOWTASK_PERIOD_MAX_MS); fast_task_period_ms = params->fast_task_period_ms; - counter++; + counter = counter + 1; if (counter > params->counter_max) { counter = 0; } @@ -334,6 +383,9 @@ static void slowTask(void *parameter) { DaqCreateAndTriggerEvent(slowTask); + // Call the demo function foo, keeps it and its local static variable in the linked image + foo(); + // printf("slowTask: counter = %u, period = %u ms, channel1 = %f\n", counter, slow_task_period_ms, channel1); #ifdef OPTION_DISPLAY displayUpdate(slow_task_period_ms, counter, fast_task_period_ms, global_counter); diff --git a/examples/no_a2l_demo/CANape/no_a2l_demo.a2l b/examples/no_a2l_demo/CANape/no_a2l_demo.a2l index c6439e5b..75fc31ac 100644 --- a/examples/no_a2l_demo/CANape/no_a2l_demo.a2l +++ b/examples/no_a2l_demo/CANape/no_a2l_demo.a2l @@ -1,8 +1,8 @@  -/* Created by xcp_client with ELF/DWARF information only, offline mode - 2026-08-31 16:47:15 */ +/* Created by xcp_client with ELF/DWARF information only, offline mode - 2026-09-10 04:34:08 */ ASAP2_VERSION 1 71 /begin PROJECT project_name "" -/begin HEADER "" VERSION "1.0" PROJECT_NO XCPLITE__CASDD /end HEADER +/begin HEADER "" VERSION "1.0" PROJECT_NO XCPLITE__ACSDD /end HEADER /begin MODULE project_name "" @@ -82,8 +82,8 @@ ASAP2_VERSION 1 71 /begin TYPEDEF_CHARACTERISTIC C_F32 "" VALUE F32 0 NO_COMPU_METHOD -1e12 1e12 /end TYPEDEF_CHARACTERISTIC /begin MOD_PAR "" -EPK "V2.1.10" ADDR_EPK 0x000302C0 -/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x80000000 7 -1 -1 -1 -1 -1 +EPK "V2.1.10" ADDR_EPK 0x00030310 +/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x30310 7 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 0 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -92,7 +92,7 @@ EPK "V2.1.10" ADDR_EPK 0x000302C0 /end SEGMENT /end IF_DATA /end MEMORY_SEGMENT -/begin MEMORY_SEGMENT counter_control "" DATA FLASH INTERN 0x80010000 4 -1 -1 -1 -1 -1 +/begin MEMORY_SEGMENT params "" DATA FLASH INTERN 0xBE98 72 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 1 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -101,7 +101,7 @@ EPK "V2.1.10" ADDR_EPK 0x000302C0 /end SEGMENT /end IF_DATA /end MEMORY_SEGMENT -/begin MEMORY_SEGMENT params "" DATA FLASH INTERN 0x80020000 72 -1 -1 -1 -1 -1 +/begin MEMORY_SEGMENT counter_control "" DATA FLASH INTERN 0xBE90 4 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 2 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -114,7 +114,7 @@ EPK "V2.1.10" ADDR_EPK 0x000302C0 /begin IF_DATA XCP /begin PROTOCOL_LAYER - 0x0104 1000 2000 0 0 0 0 0 248 248 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE + 0x0104 1000 2000 0 0 0 0 0 248 1024 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE OPTIONAL_CMD GET_COMM_MODE_INFO OPTIONAL_CMD GET_ID OPTIONAL_CMD SET_REQUEST @@ -152,14 +152,14 @@ EPK "V2.1.10" ADDR_EPK 0x000302C0 0x1 SIZE_DWORD UNIT_1US TIMESTAMP_FIXED /end TIMESTAMP_SUPPORTED - /* compilation unit = 0, function = main, CFA = 96 */ - /begin EVENT "mainloop" "mainloop" 3 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT - /* compilation unit = 0, function = bar, CFA = 16 */ - /begin EVENT "bar" "bar" 2 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT - /* compilation unit = 0, function = foo, CFA = 80 */ - /begin EVENT "foo" "foo" 1 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT - /* compilation unit = 0, function = task, CFA = 80 */ + /* compilation unit = 0, function = task, CFA = 0 */ /begin EVENT "task" "task" 0 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT + /* compilation unit = 0, function = foo, CFA = 0 */ + /begin EVENT "foo" "foo" 1 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT + /* compilation unit = 0, function = bar, CFA = 0 */ + /begin EVENT "bar" "bar" 2 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT + /* compilation unit = 0, function = main, CFA = 0 */ + /begin EVENT "mainloop" "mainloop" 3 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT /end DAQ @@ -218,72 +218,68 @@ EPK "V2.1.10" ADDR_EPK 0x000302C0 /* Measurements */ -/* Measurements for event 'mainloop' */ -/begin MEASUREMENT main.counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0xC1004E ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 3 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT main.static_counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30310 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 3 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT calseg_id_counter_control "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3020C ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 3 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT main.xcp_epk_keep "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0xC1004D ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 3 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT - -/* Measurements for event 'bar' */ +/* Measurements for event 'task' */ +/begin MEASUREMENT task.counter "Local measurement variable in thread function `task`" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x1001C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT task.static_counter "Static local measurement variable in thread function `task`" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30324 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT /* Measurements for event 'foo' */ -/begin MEASUREMENT foo.counter "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x41004C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.static_counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3030E ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x410048 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x410040 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x41003F ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x41003C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x410038 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x410030 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x41002F ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x41002C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0x410028 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_int64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x410020 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin INSTANCE foo.test_struct "" test_struct 0x410010 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT foo.static_counter "Local static measurement variable in function `foo`" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30328 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x40FFFC ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x40FFF8 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0x40FFF4 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x40FFE8 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.counter "Local captured measurement variable in function `foo`" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x400000 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x400004 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x400008 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x400010 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x400012 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x400014 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x400018 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE foo.test_struct "" test_struct 0x400020 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT foo.test_array "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x40002C ECU_ADDRESS_EXTENSION 3 MATRIX_DIM 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/* Measurements for event 'task' */ -/begin MEASUREMENT global_running "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30208 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT calseg_id_params "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3020A ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_counter "Global measurement variable" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3030C ECU_ADDRESS_EXTENSION 1 READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30256 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30254 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x30250 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x30248 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x30246 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x30244 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0x30240 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_int64 "" A_INT64 IDENTITY 0 0 -9223372036854776000 9223372036854776000 ECU_ADDRESS 0x30238 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x30230 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x30228 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_bool "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30223 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_array "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30220 ECU_ADDRESS_EXTENSION 1 MATRIX_DIM 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin INSTANCE global_test_struct "" test_struct 0x30210 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE -/begin MEASUREMENT task.counter "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x1004C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT task.static_counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3030A ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT gModuleAddrValid "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30320 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT XCPLITE__CASDD "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0xF720 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT calseg_id_epk "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3025A ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT XcpServerReceiveThread.ctr "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x32970 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT last_time "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x32978 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/* Measurements for event 'bar' */ + +/* Measurements for event 'mainloop' */ +/begin MEASUREMENT counter "Global measurement variable" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3031C READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_counter "Global measurement variable" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30320 READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x3021A /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3021C /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x30220 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x30228 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x30230 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x30232 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0x30234 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_int64 "" A_INT64 IDENTITY 0 0 -9223372036854776000 9223372036854776000 ECU_ADDRESS 0x30238 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x30240 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x30248 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_bool "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30250 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_array "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30251 MATRIX_DIM 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE global_test_struct "" test_struct 0x30254 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT main.static_counter "Static local measurement variable in function `main`" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3032C /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT calseg_id_counter_control "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30264 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT main.xcp_epk_keep "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0xC1001C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT calseg_id_params "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30218 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_running "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30260 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 3 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT /* Measurements without fixed event */ -/begin GROUP Measurements "" ROOT /begin SUB_GROUP mainloop bar foo task /end SUB_GROUP /end GROUP -/begin GROUP mainloop "" /begin REF_MEASUREMENT main.counter main.static_counter calseg_id_counter_control main.xcp_epk_keep /end REF_MEASUREMENT /end GROUP +/begin GROUP Measurements "" ROOT /begin SUB_GROUP task foo bar mainloop /end SUB_GROUP /end GROUP +/begin GROUP task "" /begin REF_MEASUREMENT task.counter task.static_counter /end REF_MEASUREMENT /end GROUP +/begin GROUP foo "" /begin REF_MEASUREMENT foo.static_counter foo.test_int8 foo.test_int16 foo.test_int32 foo.test_int64 foo.counter foo.test_float foo.test_double foo.test_uint8 foo.test_uint16 foo.test_uint32 foo.test_uint64 foo.test_struct foo.test_array /end REF_MEASUREMENT /end GROUP /begin GROUP bar "" /begin REF_MEASUREMENT /end REF_MEASUREMENT /end GROUP -/begin GROUP foo "" /begin REF_MEASUREMENT foo.counter foo.static_counter foo.test_float foo.test_double foo.test_uint8 foo.test_uint16 foo.test_uint32 foo.test_uint64 foo.test_int8 foo.test_int16 foo.test_int32 foo.test_int64 foo.test_struct /end REF_MEASUREMENT /end GROUP -/begin GROUP task "" /begin REF_MEASUREMENT global_running calseg_id_params global_counter global_test_uint8 global_test_uint16 global_test_uint32 global_test_uint64 global_test_int8 global_test_int16 global_test_int32 global_test_int64 global_test_float global_test_double global_test_bool global_test_array global_test_struct task.counter task.static_counter gModuleAddrValid XCPLITE__CASDD calseg_id_epk XcpServerReceiveThread.ctr last_time /end REF_MEASUREMENT /end GROUP +/begin GROUP mainloop "" /begin REF_MEASUREMENT counter global_counter global_test_uint8 global_test_uint16 global_test_uint32 global_test_uint64 global_test_int8 global_test_int16 global_test_int32 global_test_int64 global_test_float global_test_double global_test_bool global_test_array global_test_struct main.static_counter calseg_id_counter_control main.xcp_epk_keep calseg_id_params global_running /end REF_MEASUREMENT /end GROUP /* Axis */ /* Characteristics */ -/begin INSTANCE counter_control "" counter_control 0x80010000 /end INSTANCE -/begin INSTANCE params "" params 0x80020000 /end INSTANCE +/begin INSTANCE counter_control "" counter_control 0xBE90 /end INSTANCE +/begin INSTANCE params "" params 0xBE98 /end INSTANCE /* Characteristic and Axis Groups */ -/begin GROUP Characteristics "" ROOT /begin SUB_GROUP counter_control params /end SUB_GROUP /end GROUP -/begin GROUP counter_control "" /begin REF_CHARACTERISTIC counter_control /end REF_CHARACTERISTIC /end GROUP +/begin GROUP Characteristics "" ROOT /begin SUB_GROUP params counter_control /end SUB_GROUP /end GROUP /begin GROUP params "" /begin REF_CHARACTERISTIC params /end REF_CHARACTERISTIC /end GROUP +/begin GROUP counter_control "" /begin REF_CHARACTERISTIC counter_control /end REF_CHARACTERISTIC /end GROUP /end MODULE /end PROJECT diff --git a/examples/no_a2l_demo/CANape/no_a2l_demo.elf b/examples/no_a2l_demo/CANape/no_a2l_demo.elf index 22e4fbab..d85e763b 100755 Binary files a/examples/no_a2l_demo/CANape/no_a2l_demo.elf and b/examples/no_a2l_demo/CANape/no_a2l_demo.elf differ diff --git a/examples/no_a2l_demo/CANape/no_a2l_demo.log b/examples/no_a2l_demo/CANape/no_a2l_demo.log new file mode 100644 index 00000000..702cf967 --- /dev/null +++ b/examples/no_a2l_demo/CANape/no_a2l_demo.log @@ -0,0 +1,1019 @@ + + +==================================================================================================== +Parsed ELF object file: /Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo/CANape/no_a2l_demo.elf +File format: Elf +Architecture: Aarch64 +Endianness: Little + + +Sections: + Name: .interp Addr: 0x00000238 Size: 27 bytes Kind: ReadOnlyData + Name: .note.gnu.build-id Addr: 0x00000254 Size: 36 bytes Kind: Note + Name: .note.ABI-tag Addr: 0x00000278 Size: 32 bytes Kind: Note + Name: .hash Addr: 0x00000298 Size: 560 bytes Kind: Metadata + Name: .gnu.hash Addr: 0x000004c8 Size: 28 bytes Kind: Elf(1879048182) + Name: .dynsym Addr: 0x000004e8 Size: 1704 bytes Kind: Metadata + Name: .dynstr Addr: 0x00000b90 Size: 804 bytes Kind: Metadata + Name: .gnu.version Addr: 0x00000eb4 Size: 142 bytes Kind: Elf(1879048191) + Name: .gnu.version_r Addr: 0x00000f48 Size: 80 bytes Kind: Elf(1879048190) + Name: .rela.dyn Addr: 0x00000f98 Size: 744 bytes Kind: Metadata + Name: .rela.plt Addr: 0x00001280 Size: 1560 bytes Kind: Metadata + Name: .init Addr: 0x00001898 Size: 24 bytes Kind: Text + Name: .plt Addr: 0x000018b0 Size: 1072 bytes Kind: Text + Name: .text Addr: 0x00001d00 Size: 41328 bytes Kind: Text + Name: .fini Addr: 0x0000be70 Size: 20 bytes Kind: Text + Name: .rodata Addr: 0x0000be88 Size: 13921 bytes Kind: ReadOnlyData + Name: xcp_meta Addr: 0x0000f4f0 Size: 349 bytes Kind: ReadOnlyData + Name: .eh_frame_hdr Addr: 0x0000f650 Size: 1668 bytes Kind: ReadOnlyData + Name: .eh_frame Addr: 0x0000fcd8 Size: 5940 bytes Kind: ReadOnlyData + Name: .init_array Addr: 0x0002fd48 Size: 16 bytes Kind: Elf(14) + Name: .fini_array Addr: 0x0002fd58 Size: 8 bytes Kind: Elf(15) + Name: .dynamic Addr: 0x0002fd60 Size: 528 bytes Kind: Metadata + Name: .got Addr: 0x0002ff70 Size: 120 bytes Kind: Data + Name: .got.plt Addr: 0x0002ffe8 Size: 544 bytes Kind: Data + Name: .data Addr: 0x00030208 Size: 102 bytes Kind: Data + Name: xcp_cals Addr: 0x00030270 Size: 96 bytes Kind: Data + Name: xcp_evts Addr: 0x000302d0 Size: 64 bytes Kind: Data + Name: xcp_epk Addr: 0x00030310 Size: 8 bytes Kind: Data + Name: .bss Addr: 0x00030318 Size: 9976 bytes Kind: UninitializedData + Name: .comment Addr: 0x00000000 Size: 67 bytes Kind: OtherString + Name: .debug_info Addr: 0x00000000 Size: 31249 bytes Kind: Other + Name: .debug_abbrev Addr: 0x00000000 Size: 6858 bytes Kind: Other + Name: .debug_line Addr: 0x00000000 Size: 24156 bytes Kind: Other + Name: .debug_str Addr: 0x00000000 Size: 13181 bytes Kind: OtherString + Name: .debug_addr Addr: 0x00000000 Size: 5744 bytes Kind: Other + Name: .debug_line_str Addr: 0x00000000 Size: 1149 bytes Kind: OtherString + Name: .debug_loclists Addr: 0x00000000 Size: 16316 bytes Kind: Other + Name: .debug_rnglists Addr: 0x00000000 Size: 2080 bytes Kind: Other + Name: .debug_str_offsets Addr: 0x00000000 Size: 6872 bytes Kind: Other + Name: .symtab Addr: 0x00000000 Size: 14712 bytes Kind: Metadata + Name: .strtab Addr: 0x00000000 Size: 7169 bytes Kind: Metadata + Name: .shstrtab Addr: 0x00000000 Size: 422 bytes Kind: Metadata + +=============================================================== + +Symbol table: + `"Scrt1.o"`: addr=0, Symbol { name: "Scrt1.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$d"`: addr=278, Symbol { name: "$d", address: 632, size: 0, kind: Unknown, section: Section(SectionIndex(3)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"__abi_tag"`: addr=278, Symbol { name: "__abi_tag", address: 632, size: 32, kind: Data, section: Section(SectionIndex(3)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$x"`: addr=1d40, Symbol { name: "$x", address: 7488, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=fcec, Symbol { name: "$d", address: 64748, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=be88, Symbol { name: "$d", address: 48776, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"crti.o"`: addr=0, Symbol { name: "crti.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=1d74, Symbol { name: "$x", address: 7540, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"call_weak_fn"`: addr=1d74, Symbol { name: "call_weak_fn", address: 7540, size: 20, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"$x"`: addr=1898, Symbol { name: "$x", address: 6296, size: 0, kind: Unknown, section: Section(SectionIndex(12)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$x"`: addr=be70, Symbol { name: "$x", address: 48752, size: 0, kind: Unknown, section: Section(SectionIndex(15)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"crtn.o"`: addr=0, Symbol { name: "crtn.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=18a8, Symbol { name: "$x", address: 6312, size: 0, kind: Unknown, section: Section(SectionIndex(12)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$x"`: addr=be7c, Symbol { name: "$x", address: 48764, size: 0, kind: Unknown, section: Section(SectionIndex(15)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"lse-init.o"`: addr=0, Symbol { name: "lse-init.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=1d00, Symbol { name: "$x", address: 7424, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"init_have_lse_atomics"`: addr=1d00, Symbol { name: "init_have_lse_atomics", address: 7424, size: 36, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"$d"`: addr=2fd50, Symbol { name: "$d", address: 195920, size: 0, kind: Unknown, section: Section(SectionIndex(20)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=32a08, Symbol { name: "$d", address: 207368, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=113e8, Symbol { name: "$d", address: 70632, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"crtstuff.c"`: addr=0, Symbol { name: "crtstuff.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=1d90, Symbol { name: "$x", address: 7568, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"deregister_tm_clones"`: addr=1d90, Symbol { name: "deregister_tm_clones", address: 7568, size: 0, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"register_tm_clones"`: addr=1dc0, Symbol { name: "register_tm_clones", address: 7616, size: 0, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"$d"`: addr=30210, Symbol { name: "$d", address: 197136, size: 0, kind: Unknown, section: Section(SectionIndex(25)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"__do_global_dtors_aux"`: addr=1e00, Symbol { name: "__do_global_dtors_aux", address: 7680, size: 0, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"completed.0"`: addr=30318, Symbol { name: "completed.0", address: 197400, size: 1, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d"`: addr=2fd58, Symbol { name: "$d", address: 195928, size: 0, kind: Unknown, section: Section(SectionIndex(21)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"__do_global_dtors_aux_fini_array_entry"`: addr=2fd58, Symbol { name: "__do_global_dtors_aux_fini_array_entry", address: 195928, size: 0, kind: Data, section: Section(SectionIndex(21)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"frame_dummy"`: addr=1e50, Symbol { name: "frame_dummy", address: 7760, size: 0, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"$d"`: addr=2fd48, Symbol { name: "$d", address: 195912, size: 0, kind: Unknown, section: Section(SectionIndex(20)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"__frame_dummy_init_array_entry"`: addr=2fd48, Symbol { name: "__frame_dummy_init_array_entry", address: 195912, size: 0, kind: Data, section: Section(SectionIndex(20)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d"`: addr=fd00, Symbol { name: "$d", address: 64768, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=30318, Symbol { name: "$d", address: 197400, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"main.c"`: addr=0, Symbol { name: "main.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=1e54, Symbol { name: "$x.0", address: 7764, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"global_running"`: addr=30260, Symbol { name: "global_running", address: 197216, size: 1, kind: Data, section: Section(SectionIndex(25)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"task.evt__task"`: addr=302d0, Symbol { name: "task.evt__task", address: 197328, size: 16, kind: Data, section: Section(SectionIndex(27)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"task.static_counter"`: addr=30324, Symbol { name: "task.static_counter", address: 197412, size: 2, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"calseg_id_params"`: addr=30218, Symbol { name: "calseg_id_params", address: 197144, size: 2, kind: Data, section: Section(SectionIndex(25)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"foo.static_counter"`: addr=30328, Symbol { name: "foo.static_counter", address: 197416, size: 2, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"foo.evt__foo"`: addr=302e0, Symbol { name: "foo.evt__foo", address: 197344, size: 16, kind: Data, section: Section(SectionIndex(27)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"bar.evt__bar"`: addr=302f0, Symbol { name: "bar.evt__bar", address: 197360, size: 16, kind: Data, section: Section(SectionIndex(27)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"sig_handler"`: addr=239c, Symbol { name: "sig_handler", address: 9116, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"main.gXcpEpkString"`: addr=30310, Symbol { name: "main.gXcpEpkString", address: 197392, size: 8, kind: Data, section: Section(SectionIndex(28)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"main.calseg_id_counter_control"`: addr=30264, Symbol { name: "main.calseg_id_counter_control", address: 197220, size: 2, kind: Data, section: Section(SectionIndex(25)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"main.evt__mainloop"`: addr=30300, Symbol { name: "main.evt__mainloop", address: 197376, size: 16, kind: Data, section: Section(SectionIndex(27)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"main.static_counter"`: addr=3032c, Symbol { name: "main.static_counter", address: 197420, size: 2, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.1"`: addr=be90, Symbol { name: "$d.1", address: 48784, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.2"`: addr=beef, Symbol { name: "$d.2", address: 48879, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=30218, Symbol { name: "$d.3", address: 197144, size: 0, kind: Unknown, section: Section(SectionIndex(25)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"calseg__params"`: addr=30270, Symbol { name: "calseg__params", address: 197232, size: 32, kind: Data, section: Section(SectionIndex(26)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.4"`: addr=30270, Symbol { name: "$d.4", address: 197232, size: 0, kind: Unknown, section: Section(SectionIndex(26)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"xcp_meta__min__params__delay_us"`: addr=f4f0, Symbol { name: "xcp_meta__min__params__delay_us", address: 62704, size: 8, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.5"`: addr=f4f0, Symbol { name: "$d.5", address: 62704, size: 0, kind: Unknown, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"xcp_meta__max__params__delay_us"`: addr=f4f8, Symbol { name: "xcp_meta__max__params__delay_us", address: 62712, size: 8, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"xcp_meta__unit__params__delay_us"`: addr=f500, Symbol { name: "xcp_meta__unit__params__delay_us", address: 62720, size: 3, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"xcp_meta__comment__counter"`: addr=f503, Symbol { name: "xcp_meta__comment__counter", address: 62723, size: 28, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"xcp_meta__read_write__counter"`: addr=f51f, Symbol { name: "xcp_meta__read_write__counter", address: 62751, size: 1, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.6"`: addr=3031c, Symbol { name: "$d.6", address: 197404, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"xcp_meta__comment__global_counter"`: addr=f520, Symbol { name: "xcp_meta__comment__global_counter", address: 62752, size: 28, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"xcp_meta__read_write__global_counter"`: addr=f53c, Symbol { name: "xcp_meta__read_write__global_counter", address: 62780, size: 1, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"task.xcp_meta__comment__static_counter"`: addr=f53d, Symbol { name: "task.xcp_meta__comment__static_counter", address: 62781, size: 60, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"task.xcp_meta__comment__counter"`: addr=f579, Symbol { name: "task.xcp_meta__comment__counter", address: 62841, size: 53, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.7"`: addr=302d0, Symbol { name: "$d.7", address: 197328, size: 0, kind: Unknown, section: Section(SectionIndex(27)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"foo.xcp_meta__comment__static_counter"`: addr=f5ae, Symbol { name: "foo.xcp_meta__comment__static_counter", address: 62894, size: 52, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"foo.xcp_meta__comment__foo__counter"`: addr=f5e2, Symbol { name: "foo.xcp_meta__comment__foo__counter", address: 62946, size: 54, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.8"`: addr=30310, Symbol { name: "$d.8", address: 197392, size: 0, kind: Unknown, section: Section(SectionIndex(28)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"main.xcp_meta__comment__static_counter"`: addr=f618, Symbol { name: "main.xcp_meta__comment__static_counter", address: 63000, size: 53, kind: Data, section: Section(SectionIndex(17)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"main.calseg__counter_control"`: addr=30290, Symbol { name: "main.calseg__counter_control", address: 197264, size: 32, kind: Data, section: Section(SectionIndex(26)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.9"`: addr=0, Symbol { name: "$d.9", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=0, Symbol { name: "$d.10", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=0, Symbol { name: "$d.11", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=0, Symbol { name: "$d.12", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.13"`: addr=0, Symbol { name: "$d.13", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.14"`: addr=0, Symbol { name: "$d.14", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.15"`: addr=0, Symbol { name: "$d.15", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.16"`: addr=26, Symbol { name: "$d.16", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.17"`: addr=fd60, Symbol { name: "$d.17", address: 64864, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.18"`: addr=0, Symbol { name: "$d.18", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.19"`: addr=0, Symbol { name: "$d.19", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"xcpappl.c"`: addr=0, Symbol { name: "xcpappl.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=23a8, Symbol { name: "$x.0", address: 9128, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"__callback_idle"`: addr=30330, Symbol { name: "__callback_idle", address: 197424, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_connect"`: addr=30338, Symbol { name: "__callback_connect", address: 197432, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_prepare_daq"`: addr=30340, Symbol { name: "__callback_prepare_daq", address: 197440, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_start_daq"`: addr=30348, Symbol { name: "__callback_start_daq", address: 197448, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_stop_daq"`: addr=30350, Symbol { name: "__callback_stop_daq", address: 197456, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_get_cal_page"`: addr=30358, Symbol { name: "__callback_get_cal_page", address: 197464, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_set_cal_page"`: addr=30360, Symbol { name: "__callback_set_cal_page", address: 197472, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_init_cal"`: addr=30368, Symbol { name: "__callback_init_cal", address: 197480, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_check"`: addr=30370, Symbol { name: "__callback_check", address: 197488, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_get_clock"`: addr=30378, Symbol { name: "__callback_get_clock", address: 197496, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_get_clock_state"`: addr=30380, Symbol { name: "__callback_get_clock_state", address: 197504, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__callback_get_clock_info_grandmaster"`: addr=30388, Symbol { name: "__callback_get_clock_info_grandmaster", address: 197512, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"gModuleAddrValid"`: addr=30399, Symbol { name: "gModuleAddrValid", address: 197529, size: 1, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"dump_phdr"`: addr=28d0, Symbol { name: "dump_phdr", address: 10448, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"gModuleAddr"`: addr=303a0, Symbol { name: "gModuleAddr", address: 197536, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"gXcpA2lName"`: addr=303a8, Symbol { name: "gXcpA2lName", address: 197544, size: 256, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"gXcpElfName"`: addr=304a8, Symbol { name: "gXcpElfName", address: 197800, size: 256, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"gXcpFile"`: addr=305a8, Symbol { name: "gXcpFile", address: 198056, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"gXcpFileLength"`: addr=305b0, Symbol { name: "gXcpFileLength", address: 198064, size: 4, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.1"`: addr=c0c3, Symbol { name: "$d.1", address: 49347, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.2"`: addr=30330, Symbol { name: "$d.2", address: 197424, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=c0c9, Symbol { name: "$d.3", address: 49353, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.4"`: addr=bb, Symbol { name: "$d.4", address: 187, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.5"`: addr=2ad, Symbol { name: "$d.5", address: 685, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.6"`: addr=a16, Symbol { name: "$d.6", address: 2582, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.7"`: addr=43, Symbol { name: "$d.7", address: 67, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.8"`: addr=2b4, Symbol { name: "$d.8", address: 692, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.9"`: addr=0, Symbol { name: "$d.9", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=280, Symbol { name: "$d.10", address: 640, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=26, Symbol { name: "$d.11", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=fd60, Symbol { name: "$d.12", address: 64864, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.13"`: addr=32b, Symbol { name: "$d.13", address: 811, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.14"`: addr=0, Symbol { name: "$d.14", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"xcplite.c"`: addr=0, Symbol { name: "xcplite.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=2fcc, Symbol { name: "$x.0", address: 12236, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.1"`: addr=c424, Symbol { name: "$d.1", address: 50212, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"XcpAsyncCommand"`: addr=4508, Symbol { name: "XcpAsyncCommand", address: 17672, size: 5896, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpTriggerDaqEvent_"`: addr=3aac, Symbol { name: "XcpTriggerDaqEvent_", address: 15020, size: 684, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpStopDaq"`: addr=4480, Symbol { name: "XcpStopDaq", address: 17536, size: 104, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpSendResponse"`: addr=6ee4, Symbol { name: "XcpSendResponse", address: 28388, size: 864, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpAddOdtEntry"`: addr=6ad0, Symbol { name: "XcpAddOdtEntry", address: 27344, size: 544, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpAllocOdtEntry"`: addr=6814, Symbol { name: "XcpAllocOdtEntry", address: 26644, size: 272, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpAllocOdt"`: addr=6720, Symbol { name: "XcpAllocOdt", address: 26400, size: 244, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpAllocDaq"`: addr=6630, Symbol { name: "XcpAllocDaq", address: 26160, size: 240, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpSetDaqPtr"`: addr=6a2c, Symbol { name: "XcpSetDaqPtr", address: 27180, size: 164, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"calcChecksum"`: addr=64e0, Symbol { name: "calcChecksum", address: 25824, size: 336, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpSetDaqListMode"`: addr=6924, Symbol { name: "XcpSetDaqListMode", address: 26916, size: 264, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpStopDaqList"`: addr=6cf0, Symbol { name: "XcpStopDaqList", address: 27888, size: 200, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpStopSelectedDaqLists"`: addr=6db8, Symbol { name: "XcpStopSelectedDaqLists", address: 28088, size: 300, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpStartSelectedDaqLists"`: addr=7244, Symbol { name: "XcpStartSelectedDaqLists", address: 29252, size: 604, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpStartDaq"`: addr=74a0, Symbol { name: "XcpStartDaq", address: 29856, size: 172, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpInit.calseg_id_epk"`: addr=3026c, Symbol { name: "XcpInit.calseg_id_epk", address: 197228, size: 2, kind: Data, section: Section(SectionIndex(25)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"gXcpCRC16CCITTtab"`: addr=c572, Symbol { name: "gXcpCRC16CCITTtab", address: 50546, size: 512, kind: Data, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.2"`: addr=305b8, Symbol { name: "$d.2", address: 198072, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=30268, Symbol { name: "$d.3", address: 197224, size: 0, kind: Unknown, section: Section(SectionIndex(25)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.4"`: addr=c84c, Symbol { name: "$d.4", address: 51276, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"XcpInit.calseg__epk"`: addr=302b0, Symbol { name: "XcpInit.calseg__epk", address: 197296, size: 32, kind: Data, section: Section(SectionIndex(26)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.5"`: addr=302b0, Symbol { name: "$d.5", address: 197296, size: 0, kind: Unknown, section: Section(SectionIndex(26)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.6"`: addr=5d3, Symbol { name: "$d.6", address: 1491, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.7"`: addr=61e, Symbol { name: "$d.7", address: 1566, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.8"`: addr=159e, Symbol { name: "$d.8", address: 5534, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.9"`: addr=63, Symbol { name: "$d.9", address: 99, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=600, Symbol { name: "$d.10", address: 1536, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=0, Symbol { name: "$d.11", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=5b0, Symbol { name: "$d.12", address: 1456, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.13"`: addr=26, Symbol { name: "$d.13", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.14"`: addr=fd48, Symbol { name: "$d.14", address: 64840, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.15"`: addr=a8d, Symbol { name: "$d.15", address: 2701, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.16"`: addr=0, Symbol { name: "$d.16", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"xcpethserver.c"`: addr=0, Symbol { name: "xcpethserver.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=754c, Symbol { name: "$x.0", address: 30028, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"gXcpServer"`: addr=32930, Symbol { name: "gXcpServer", address: 207152, size: 48, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"XcpServerReceiveThread"`: addr=7708, Symbol { name: "XcpServerReceiveThread", address: 30472, size: 276, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpServerTransmitThread"`: addr=781c, Symbol { name: "XcpServerTransmitThread", address: 30748, size: 160, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"XcpServerReceiveThread.ctr"`: addr=32960, Symbol { name: "XcpServerReceiveThread.ctr", address: 207200, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"XcpServerReceiveThread.last_time"`: addr=32968, Symbol { name: "XcpServerReceiveThread.last_time", address: 207208, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.1"`: addr=32930, Symbol { name: "$d.1", address: 207152, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.2"`: addr=dcb8, Symbol { name: "$d.2", address: 56504, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=1bd8, Symbol { name: "$d.3", address: 7128, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.4"`: addr=b6d, Symbol { name: "$d.4", address: 2925, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.5"`: addr=3bd3, Symbol { name: "$d.5", address: 15315, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.6"`: addr=35b, Symbol { name: "$d.6", address: 859, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.7"`: addr=c0c, Symbol { name: "$d.7", address: 3084, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.8"`: addr=0, Symbol { name: "$d.8", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.9"`: addr=d70, Symbol { name: "$d.9", address: 3440, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=26, Symbol { name: "$d.10", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=fd38, Symbol { name: "$d.11", address: 64824, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=3082, Symbol { name: "$d.12", address: 12418, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.13"`: addr=0, Symbol { name: "$d.13", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"xcpethtl.c"`: addr=0, Symbol { name: "xcpethtl.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=79a8, Symbol { name: "$x.0", address: 31144, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"gXcpTl"`: addr=32970, Symbol { name: "gXcpTl", address: 207216, size: 104, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"XcpEthTlSendV"`: addr=7a3c, Symbol { name: "XcpEthTlSendV", address: 31292, size: 216, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"handleXcpCommand"`: addr=7f70, Symbol { name: "handleXcpCommand", address: 32624, size: 708, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"$d.1"`: addr=32970, Symbol { name: "$d.1", address: 207216, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.2"`: addr=de58, Symbol { name: "$d.2", address: 56920, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=1cc0, Symbol { name: "$d.3", address: 7360, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.4"`: addr=d6f, Symbol { name: "$d.4", address: 3439, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.5"`: addr=3fd9, Symbol { name: "$d.5", address: 16345, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.6"`: addr=385, Symbol { name: "$d.6", address: 901, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.7"`: addr=d44, Symbol { name: "$d.7", address: 3396, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.8"`: addr=0, Symbol { name: "$d.8", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.9"`: addr=e88, Symbol { name: "$d.9", address: 3720, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=26, Symbol { name: "$d.10", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=fd20, Symbol { name: "$d.11", address: 64800, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=3418, Symbol { name: "$d.12", address: 13336, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.13"`: addr=0, Symbol { name: "$d.13", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"queue64v.c"`: addr=0, Symbol { name: "queue64v.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=86e8, Symbol { name: "$x.0", address: 34536, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.1"`: addr=e442, Symbol { name: "$d.1", address: 58434, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.2"`: addr=204f, Symbol { name: "$d.2", address: 8271, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=ff4, Symbol { name: "$d.3", address: 4084, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.4"`: addr=493f, Symbol { name: "$d.4", address: 18751, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.5"`: addr=409, Symbol { name: "$d.5", address: 1033, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.6"`: addr=f70, Symbol { name: "$d.6", address: 3952, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.7"`: addr=0, Symbol { name: "$d.7", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.8"`: addr=1090, Symbol { name: "$d.8", address: 4240, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.9"`: addr=26, Symbol { name: "$d.9", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=fd08, Symbol { name: "$d.10", address: 64776, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=3b68, Symbol { name: "$d.11", address: 15208, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=0, Symbol { name: "$d.12", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"cal.c"`: addr=0, Symbol { name: "cal.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=8da8, Symbol { name: "$x.0", address: 36264, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"XcpCreateCalSeg_"`: addr=8ff8, Symbol { name: "XcpCreateCalSeg_", address: 36856, size: 1040, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"$d.1"`: addr=e5e2, Symbol { name: "$d.1", address: 58850, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.2"`: addr=258b, Symbol { name: "$d.2", address: 9611, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=1206, Symbol { name: "$d.3", address: 4614, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.4"`: addr=4f07, Symbol { name: "$d.4", address: 20231, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.5"`: addr=474, Symbol { name: "$d.5", address: 1140, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.6"`: addr=10e4, Symbol { name: "$d.6", address: 4324, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.7"`: addr=0, Symbol { name: "$d.7", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.8"`: addr=1140, Symbol { name: "$d.8", address: 4416, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.9"`: addr=26, Symbol { name: "$d.9", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=fcf8, Symbol { name: "$d.10", address: 64760, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=4009, Symbol { name: "$d.11", address: 16393, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=0, Symbol { name: "$d.12", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"platform.c"`: addr=0, Symbol { name: "platform.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=a594, Symbol { name: "$x.0", address: 42388, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"__gClockRealtime"`: addr=329f8, Symbol { name: "__gClockRealtime", address: 207352, size: 16, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__gClockMonotonic"`: addr=329e8, Symbol { name: "__gClockMonotonic", address: 207336, size: 16, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__gClock"`: addr=329d8, Symbol { name: "__gClock", address: 207320, size: 16, kind: Data, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"$d.1"`: addr=eb8b, Symbol { name: "$d.1", address: 60299, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.2"`: addr=329d8, Symbol { name: "$d.2", address: 207320, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=35af, Symbol { name: "$d.3", address: 13743, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.4"`: addr=15dd, Symbol { name: "$d.4", address: 5597, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.5"`: addr=620e, Symbol { name: "$d.5", address: 25102, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.6"`: addr=679, Symbol { name: "$d.6", address: 1657, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.7"`: addr=14f4, Symbol { name: "$d.7", address: 5364, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.8"`: addr=0, Symbol { name: "$d.8", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.9"`: addr=1368, Symbol { name: "$d.9", address: 4968, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=26, Symbol { name: "$d.10", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=fce0, Symbol { name: "$d.11", address: 64736, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=4ff3, Symbol { name: "$d.12", address: 20467, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.13"`: addr=0, Symbol { name: "$d.13", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"sockets.c"`: addr=0, Symbol { name: "sockets.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x.0"`: addr=aba8, Symbol { name: "$x.0", address: 43944, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.1"`: addr=ec23, Symbol { name: "$d.1", address: 60451, size: 0, kind: Unknown, section: Section(SectionIndex(16)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.2"`: addr=3732, Symbol { name: "$d.2", address: 14130, size: 0, kind: Unknown, section: Section(SectionIndex(37)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.3"`: addr=183b, Symbol { name: "$d.3", address: 6203, size: 0, kind: Unknown, section: Section(SectionIndex(32)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.4"`: addr=690e, Symbol { name: "$d.4", address: 26894, size: 0, kind: Unknown, section: Section(SectionIndex(31)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.5"`: addr=694, Symbol { name: "$d.5", address: 1684, size: 0, kind: Unknown, section: Section(SectionIndex(38)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.6"`: addr=16b4, Symbol { name: "$d.6", address: 5812, size: 0, kind: Unknown, section: Section(SectionIndex(39)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.7"`: addr=0, Symbol { name: "$d.7", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(34)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.8"`: addr=14e8, Symbol { name: "$d.8", address: 5352, size: 0, kind: Unknown, section: Section(SectionIndex(35)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.9"`: addr=26, Symbol { name: "$d.9", address: 38, size: 0, kind: Unknown, section: Section(SectionIndex(30)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.10"`: addr=fcd0, Symbol { name: "$d.10", address: 64720, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.11"`: addr=53b0, Symbol { name: "$d.11", address: 21424, size: 0, kind: Unknown, section: Section(SectionIndex(33)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d.12"`: addr=0, Symbol { name: "$d.12", address: 0, size: 0, kind: Unknown, section: Section(SectionIndex(36)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"cas_4_1.o"`: addr=0, Symbol { name: "cas_4_1.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=bcb0, Symbol { name: "$x", address: 48304, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=11328, Symbol { name: "$d", address: 70440, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"cas_8_1.o"`: addr=0, Symbol { name: "cas_8_1.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=bcf0, Symbol { name: "$x", address: 48368, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=11340, Symbol { name: "$d", address: 70464, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"cas_1_3.o"`: addr=0, Symbol { name: "cas_1_3.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=bd30, Symbol { name: "$x", address: 48432, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=11358, Symbol { name: "$d", address: 70488, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"cas_1_4.o"`: addr=0, Symbol { name: "cas_1_4.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=bd70, Symbol { name: "$x", address: 48496, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=11370, Symbol { name: "$d", address: 70512, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"ldadd_1_1.o"`: addr=0, Symbol { name: "ldadd_1_1.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=bdb0, Symbol { name: "$x", address: 48560, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=11388, Symbol { name: "$d", address: 70536, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"swp_8_1.o"`: addr=0, Symbol { name: "swp_8_1.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=bde0, Symbol { name: "$x", address: 48608, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=113a0, Symbol { name: "$d", address: 70560, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"ldadd_8_1.o"`: addr=0, Symbol { name: "ldadd_8_1.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=be10, Symbol { name: "$x", address: 48656, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=113b8, Symbol { name: "$d", address: 70584, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"ldadd_8_3.o"`: addr=0, Symbol { name: "ldadd_8_3.o", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$x"`: addr=be40, Symbol { name: "$x", address: 48704, size: 0, kind: Unknown, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"$d"`: addr=113d0, Symbol { name: "$d", address: 70608, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"crtstuff.c"`: addr=0, Symbol { name: "crtstuff.c", address: 0, size: 0, kind: File, section: None, scope: Compilation, weak: false, flags: Elf { st_info: 4, st_other: 0 } } + `"$d"`: addr=11408, Symbol { name: "$d", address: 70664, size: 0, kind: Unknown, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"__FRAME_END__"`: addr=11408, Symbol { name: "__FRAME_END__", address: 70664, size: 0, kind: Data, section: Section(SectionIndex(19)), scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__aarch64_cas8_relax"`: addr=bcf0, Symbol { name: "__aarch64_cas8_relax", address: 48368, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"__aarch64_cas4_relax"`: addr=bcb0, Symbol { name: "__aarch64_cas4_relax", address: 48304, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"__aarch64_cas1_acq_rel"`: addr=bd70, Symbol { name: "__aarch64_cas1_acq_rel", address: 48496, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"__aarch64_swp8_relax"`: addr=bde0, Symbol { name: "__aarch64_swp8_relax", address: 48608, size: 44, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"__aarch64_ldadd1_relax"`: addr=bdb0, Symbol { name: "__aarch64_ldadd1_relax", address: 48560, size: 48, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"__aarch64_ldadd8_rel"`: addr=be40, Symbol { name: "__aarch64_ldadd8_rel", address: 48704, size: 48, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"__aarch64_cas1_rel"`: addr=bd30, Symbol { name: "__aarch64_cas1_rel", address: 48432, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"_DYNAMIC"`: addr=2fd60, Symbol { name: "_DYNAMIC", address: 195936, size: 0, kind: Data, section: Absolute, scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__GNU_EH_FRAME_HDR"`: addr=f650, Symbol { name: "__GNU_EH_FRAME_HDR", address: 63056, size: 0, kind: Unknown, section: Section(SectionIndex(18)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"_GLOBAL_OFFSET_TABLE_"`: addr=2ff70, Symbol { name: "_GLOBAL_OFFSET_TABLE_", address: 196464, size: 0, kind: Data, section: Absolute, scope: Compilation, weak: false, flags: Elf { st_info: 1, st_other: 0 } } + `"__aarch64_ldadd8_relax"`: addr=be10, Symbol { name: "__aarch64_ldadd8_relax", address: 48656, size: 48, kind: Text, section: Section(SectionIndex(14)), scope: Compilation, weak: false, flags: Elf { st_info: 2, st_other: 0 } } + `"$x"`: addr=18b0, Symbol { name: "$x", address: 6320, size: 0, kind: Unknown, section: Section(SectionIndex(13)), scope: Compilation, weak: false, flags: Elf { st_info: 0, st_other: 0 } } + `"memcpy@GLIBC_2.17"`: addr=0, Symbol { name: "memcpy@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queueAcquire"`: addr=89c4, Symbol { name: "queueAcquire", address: 35268, size: 292, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queueDeinit"`: addr=896c, Symbol { name: "queueDeinit", address: 35180, size: 88, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_uint16"`: addr=3021c, Symbol { name: "global_test_uint16", address: 197148, size: 2, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"XcpTlSendCrm"`: addr=79a8, Symbol { name: "XcpTlSendCrm", address: 31144, size: 148, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEthServerDebugInfo"`: addr=79a4, Symbol { name: "XcpEthServerDebugInfo", address: 31140, size: 4, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_int64"`: addr=30238, Symbol { name: "global_test_int64", address: 197176, size: 8, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"__stop_xcp_evts"`: addr=30310, Symbol { name: "__stop_xcp_evts", address: 197392, size: 0, kind: Unknown, section: Section(SectionIndex(27)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 3 } } + `"XcpEthTlGetInfo"`: addr=8418, Symbol { name: "XcpEthTlGetInfo", address: 33816, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"getifaddrs@GLIBC_2.17"`: addr=0, Symbol { name: "getifaddrs@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetSessionStatus"`: addr=3060, Symbol { name: "XcpGetSessionStatus", address: 12384, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetCalSegCount"`: addr=9468, Symbol { name: "XcpGetCalSegCount", address: 37992, size: 36, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"sendto@GLIBC_2.17"`: addr=0, Symbol { name: "sendto@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetSegPageInfo"`: addr=a000, Symbol { name: "XcpGetSegPageInfo", address: 40960, size: 252, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpGetBaseAddr"`: addr=2824, Symbol { name: "ApplXcpGetBaseAddr", address: 10276, size: 164, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"strlen@GLIBC_2.17"`: addr=0, Symbol { name: "strlen@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetEventCycleTime"`: addr=35ac, Symbol { name: "XcpGetEventCycleTime", address: 13740, size: 84, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketRecv"`: addr=b3c4, Symbol { name: "socketRecv", address: 46020, size: 644, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetCalSegBaseAddress"`: addr=96e8, Symbol { name: "XcpGetCalSegBaseAddress", address: 38632, size: 100, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__libc_start_main@GLIBC_2.34"`: addr=0, Symbol { name: "__libc_start_main@GLIBC_2.34", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queueRelease"`: addr=8d58, Symbol { name: "queueRelease", address: 36184, size: 80, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queuePush"`: addr=8ae8, Symbol { name: "queuePush", address: 35560, size: 68, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpSetA2lName"`: addr=29f4, Symbol { name: "XcpSetA2lName", address: 10740, size: 100, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"listen@GLIBC_2.17"`: addr=0, Symbol { name: "listen@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"_ITM_deregisterTMCloneTable"`: addr=0, Symbol { name: "_ITM_deregisterTMCloneTable", address: 0, size: 0, kind: Unknown, section: Undefined, scope: Unknown, weak: true, flags: Elf { st_info: 32, st_other: 0 } } + `"data_start"`: addr=30208, Symbol { name: "data_start", address: 197128, size: 0, kind: Unknown, section: Section(SectionIndex(25)), scope: Dynamic, weak: true, flags: Elf { st_info: 32, st_other: 0 } } + `"socketClose"`: addr=af04, Symbol { name: "socketClose", address: 44804, size: 56, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockGetRealtimeNs"`: addr=a954, Symbol { name: "clockGetRealtimeNs", address: 43348, size: 60, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockInit"`: addr=a78c, Symbol { name: "clockInit", address: 42892, size: 456, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"strnlen@GLIBC_2.17"`: addr=0, Symbol { name: "strnlen@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__bss_start__"`: addr=30318, Symbol { name: "__bss_start__", address: 197400, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 0 } } + `"socketSendToV"`: addr=b890, Symbol { name: "socketSendToV", address: 47248, size: 512, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketStartup"`: addr=abbc, Symbol { name: "socketStartup", address: 43964, size: 8, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetEvent"`: addr=3550, Symbol { name: "XcpGetEvent", address: 13648, size: 40, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterFreezeDaqCallback"`: addr=23e4, Symbol { name: "ApplXcpRegisterFreezeDaqCallback", address: 9188, size: 4, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketSetTimeout"`: addr=afd4, Symbol { name: "socketSetTimeout", address: 45012, size: 184, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpGetId"`: addr=2c00, Symbol { name: "ApplXcpGetId", address: 11264, size: 972, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"bind@GLIBC_2.17"`: addr=0, Symbol { name: "bind@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_uint64"`: addr=30228, Symbol { name: "global_test_uint64", address: 197160, size: 8, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"XcpCalSegReadMemory"`: addr=9a14, Symbol { name: "XcpCalSegReadMemory", address: 39444, size: 276, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ftell@GLIBC_2.17"`: addr=0, Symbol { name: "ftell@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__cxa_finalize@GLIBC_2.17"`: addr=0, Symbol { name: "__cxa_finalize@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: true, flags: Elf { st_info: 34, st_other: 0 } } + `"socketShutdown"`: addr=aee0, Symbol { name: "socketShutdown", address: 44768, size: 36, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetA2lName"`: addr=2a58, Symbol { name: "XcpGetA2lName", address: 10840, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpUserCommand"`: addr=2904, Symbol { name: "ApplXcpUserCommand", address: 10500, size: 24, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clock_gettime@GLIBC_2.17"`: addr=0, Symbol { name: "clock_gettime@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_int32"`: addr=30234, Symbol { name: "global_test_int32", address: 197172, size: 4, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"XcpSendEvent"`: addr=5ca4, Symbol { name: "XcpSendEvent", address: 23716, size: 224, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"stderr@GLIBC_2.17"`: addr=0, Symbol { name: "stderr@GLIBC_2.17", address: 0, size: 0, kind: Data, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"_bss_end__"`: addr=32a10, Symbol { name: "_bss_end__", address: 207376, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 0 } } + `"global_test_bool"`: addr=30250, Symbol { name: "global_test_bool", address: 197200, size: 1, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"ApplXcpReadFile"`: addr=2ac0, Symbol { name: "ApplXcpReadFile", address: 10944, size: 320, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpGetCalPage"`: addr=2948, Symbol { name: "ApplXcpGetCalPage", address: 10568, size: 44, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEthTlShutdown"`: addr=83cc, Symbol { name: "XcpEthTlShutdown", address: 33740, size: 76, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"_edata"`: addr=30318, Symbol { name: "_edata", address: 197400, size: 0, kind: Unknown, section: Section(SectionIndex(28)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 0 } } + `"socketListen"`: addr=b08c, Symbol { name: "socketListen", address: 45196, size: 128, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"bar"`: addr=206c, Symbol { name: "bar", address: 8300, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockGetMonotonicUsLast"`: addr=aaf8, Symbol { name: "clockGetMonotonicUsLast", address: 43768, size: 56, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetLocalEpk"`: addr=3184, Symbol { name: "XcpGetLocalEpk", address: 12676, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEventExtAt_"`: addr=39bc, Symbol { name: "XcpEventExtAt_", address: 14780, size: 240, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"fexists"`: addr=ab84, Symbol { name: "fexists", address: 43908, size: 36, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketSendV"`: addr=ba90, Symbol { name: "socketSendV", address: 47760, size: 532, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"snprintf@GLIBC_2.17"`: addr=0, Symbol { name: "snprintf@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_uint32"`: addr=30220, Symbol { name: "global_test_uint32", address: 197152, size: 4, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"clockGetString"`: addr=a688, Symbol { name: "clockGetString", address: 42632, size: 260, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketGetErrorString"`: addr=aba8, Symbol { name: "socketGetErrorString", address: 43944, size: 20, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__start_xcp_cals"`: addr=30270, Symbol { name: "__start_xcp_cals", address: 197232, size: 0, kind: Unknown, section: Section(SectionIndex(26)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 3 } } + `"socketRecvFrom"`: addr=b278, Symbol { name: "socketRecvFrom", address: 45688, size: 332, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_uint8"`: addr=3021a, Symbol { name: "global_test_uint8", address: 197146, size: 1, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"ApplXcpRegisterGetCalPageCallback"`: addr=23e8, Symbol { name: "ApplXcpRegisterGetCalPageCallback", address: 9192, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterIdleCallback"`: addr=23a8, Symbol { name: "ApplXcpRegisterIdleCallback", address: 9128, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"_fini"`: addr=be70, Symbol { name: "_fini", address: 48752, size: 0, kind: Text, section: Section(SectionIndex(15)), scope: Linkage, weak: false, flags: Elf { st_info: 18, st_other: 2 } } + `"XcpLockCalSeg"`: addr=983c, Symbol { name: "XcpLockCalSeg", address: 38972, size: 288, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__bss_end__"`: addr=32a10, Symbol { name: "__bss_end__", address: 207376, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 0 } } + `"XcpSendTerminateSessionEvent"`: addr=5d84, Symbol { name: "XcpSendTerminateSessionEvent", address: 23940, size: 148, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"signal@GLIBC_2.17"`: addr=0, Symbol { name: "signal@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"fclose@GLIBC_2.17"`: addr=0, Symbol { name: "fclose@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_int8"`: addr=30230, Symbol { name: "global_test_int8", address: 197168, size: 1, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"fopen@GLIBC_2.17"`: addr=0, Symbol { name: "fopen@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterFlushCallback"`: addr=2424, Symbol { name: "ApplXcpRegisterFlushCallback", address: 9252, size: 4, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"malloc@GLIBC_2.17"`: addr=0, Symbol { name: "malloc@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEthServerInit"`: addr=75a4, Symbol { name: "XcpEthServerInit", address: 30116, size: 356, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketAccept"`: addr=b10c, Symbol { name: "socketAccept", address: 45324, size: 68, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterFreezeCalCallback"`: addr=2400, Symbol { name: "ApplXcpRegisterFreezeCalCallback", address: 9216, size: 4, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEthTlHandleCommands"`: addr=7b14, Symbol { name: "XcpEthTlHandleCommands", address: 31508, size: 1116, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpBackgroundTasks"`: addr=5c10, Symbol { name: "XcpBackgroundTasks", address: 23568, size: 148, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"setsockopt@GLIBC_2.17"`: addr=0, Symbol { name: "setsockopt@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpCheckMemory"`: addr=291c, Symbol { name: "ApplXcpCheckMemory", address: 10524, size: 44, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpSetElfName"`: addr=2a64, Symbol { name: "XcpSetElfName", address: 10852, size: 80, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterInitCalCallback"`: addr=2404, Symbol { name: "ApplXcpRegisterInitCalCallback", address: 9220, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpIsDaqEventRunning"`: addr=30b4, Symbol { name: "XcpIsDaqEventRunning", address: 12468, size: 120, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_mutexattr_settype@GLIBC_2.34"`: addr=0, Symbol { name: "pthread_mutexattr_settype@GLIBC_2.34", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpConnect"`: addr=2428, Symbol { name: "ApplXcpConnect", address: 9256, size: 96, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketJoin"`: addr=b150, Symbol { name: "socketJoin", address: 45392, size: 296, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketEnableTimestamps"`: addr=ae98, Symbol { name: "socketEnableTimestamps", address: 44696, size: 72, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetCalSegIndex"`: addr=958c, Symbol { name: "XcpGetCalSegIndex", address: 38284, size: 108, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterStopDaqCallback"`: addr=23d8, Symbol { name: "ApplXcpRegisterStopDaqCallback", address: 9176, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCalSegSetCalPage"`: addr=a230, Symbol { name: "XcpCalSegSetCalPage", address: 41520, size: 408, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queuePeek"`: addr=8b64, Symbol { name: "queuePeek", address: 35684, size: 500, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpStopDaq"`: addr=2584, Symbol { name: "ApplXcpStopDaq", address: 9604, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"counter_control"`: addr=be90, Symbol { name: "counter_control", address: 48784, size: 4, kind: Data, section: Section(SectionIndex(16)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"XcpCheckDaqLists"`: addr=3718, Symbol { name: "XcpCheckDaqLists", address: 14104, size: 676, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"recvfrom@GLIBC_2.17"`: addr=0, Symbol { name: "recvfrom@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpWriteMta"`: addr=3204, Symbol { name: "XcpWriteMta", address: 12804, size: 236, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"aligned_alloc@GLIBC_2.17"`: addr=0, Symbol { name: "aligned_alloc@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpDeinit"`: addr=64b4, Symbol { name: "XcpDeinit", address: 25780, size: 44, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEvent"`: addr=3f88, Symbol { name: "XcpEvent", address: 16264, size: 108, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpGetModuleAddr"`: addr=26fc, Symbol { name: "ApplXcpGetModuleAddr", address: 9980, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockGetRealtimeNsLast"`: addr=ab30, Symbol { name: "clockGetRealtimeNsLast", address: 43824, size: 28, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"memset@GLIBC_2.17"`: addr=0, Symbol { name: "memset@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"gmtime_r@GLIBC_2.17"`: addr=0, Symbol { name: "gmtime_r@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpRegisterSectionCalSegs"`: addr=8da8, Symbol { name: "XcpRegisterSectionCalSegs", address: 36264, size: 440, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"sleep@GLIBC_2.17"`: addr=0, Symbol { name: "sleep@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpIsDaqRunning"`: addr=308c, Symbol { name: "XcpIsDaqRunning", address: 12428, size: 40, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetEventCount"`: addr=3518, Symbol { name: "XcpGetEventCount", address: 13592, size: 56, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"accept@GLIBC_2.17"`: addr=0, Symbol { name: "accept@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockGetMonotonicNsLast"`: addr=aadc, Symbol { name: "clockGetMonotonicNsLast", address: 43740, size: 28, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpGetAddrExt"`: addr=28c8, Symbol { name: "ApplXcpGetAddrExt", address: 10440, size: 8, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"sendmsg@GLIBC_2.17"`: addr=0, Symbol { name: "sendmsg@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterGetClockInfoGrandmasterCallback"`: addr=2654, Symbol { name: "ApplXcpRegisterGetClockInfoGrandmasterCallback", address: 9812, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"gXcpLocalData"`: addr=32818, Symbol { name: "gXcpLocalData", address: 206872, size: 280, kind: Data, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"bcmp@GLIBC_2.17"`: addr=0, Symbol { name: "bcmp@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetCalSeg"`: addr=94a8, Symbol { name: "XcpGetCalSeg", address: 38056, size: 80, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterPrepareDaqCallback"`: addr=23c0, Symbol { name: "ApplXcpRegisterPrepareDaqCallback", address: 9152, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetMemSegCount"`: addr=948c, Symbol { name: "XcpGetMemSegCount", address: 38028, size: 28, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpCopyCalPage"`: addr=29a0, Symbol { name: "ApplXcpCopyCalPage", address: 10656, size: 84, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpReadMta"`: addr=32f0, Symbol { name: "XcpReadMta", address: 13040, size: 188, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__data_start"`: addr=30208, Symbol { name: "__data_start", address: 197128, size: 0, kind: Unknown, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 0 } } + `"ApplXcpGetClockInfoGrandmaster"`: addr=2660, Symbol { name: "ApplXcpGetClockInfoGrandmaster", address: 9824, size: 44, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCalSegBeginAtomicTransaction"`: addr=a4d0, Symbol { name: "XcpCalSegBeginAtomicTransaction", address: 42192, size: 128, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterReadCallback"`: addr=241c, Symbol { name: "ApplXcpRegisterReadCallback", address: 9244, size: 4, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"shutdown@GLIBC_2.17"`: addr=0, Symbol { name: "shutdown@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"strerror@GLIBC_2.17"`: addr=0, Symbol { name: "strerror@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_mutex_init@GLIBC_2.17"`: addr=0, Symbol { name: "pthread_mutex_init@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"close@GLIBC_2.17"`: addr=0, Symbol { name: "close@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterSetCalPageCallback"`: addr=23f4, Symbol { name: "ApplXcpRegisterSetCalPageCallback", address: 9204, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpFindCalSegByAddr"`: addr=94f8, Symbol { name: "XcpFindCalSegByAddr", address: 38136, size: 148, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"strrchr@GLIBC_2.17"`: addr=0, Symbol { name: "strrchr@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__atomic_load_4@LIBATOMIC_1.0"`: addr=0, Symbol { name: "__atomic_load_4@LIBATOMIC_1.0", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"recv@GLIBC_2.17"`: addr=0, Symbol { name: "recv@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__gmon_start__"`: addr=0, Symbol { name: "__gmon_start__", address: 0, size: 0, kind: Unknown, section: Undefined, scope: Unknown, weak: true, flags: Elf { st_info: 32, st_other: 0 } } + `"XcpGetEventName"`: addr=3578, Symbol { name: "XcpGetEventName", address: 13688, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__dso_handle"`: addr=30210, Symbol { name: "__dso_handle", address: 197136, size: 0, kind: Data, section: Section(SectionIndex(25)), scope: Linkage, weak: false, flags: Elf { st_info: 17, st_other: 2 } } + `"__getauxval@GLIBC_2.17"`: addr=0, Symbol { name: "__getauxval@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"fseek@GLIBC_2.17"`: addr=0, Symbol { name: "fseek@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"abort@GLIBC_2.17"`: addr=0, Symbol { name: "abort@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpIsActivated"`: addr=3044, Symbol { name: "XcpIsActivated", address: 12356, size: 16, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCreateCalBlk"`: addr=97fc, Symbol { name: "XcpCreateCalBlk", address: 38908, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpFindEvent"`: addr=3654, Symbol { name: "XcpFindEvent", address: 13908, size: 196, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"_IO_stdin_used"`: addr=be88, Symbol { name: "_IO_stdin_used", address: 48776, size: 4, kind: Data, section: Section(SectionIndex(16)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"XcpCalSegCopyCalPage"`: addr=a3c8, Symbol { name: "XcpCalSegCopyCalPage", address: 41928, size: 264, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"gXcpData"`: addr=305b8, Symbol { name: "gXcpData", address: 198072, size: 8800, kind: Data, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"ApplXcpGetClock64"`: addr=25f0, Symbol { name: "ApplXcpGetClock64", address: 9712, size: 44, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"access@GLIBC_2.17"`: addr=0, Symbol { name: "access@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_double"`: addr=30248, Symbol { name: "global_test_double", address: 197192, size: 8, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"global_counter"`: addr=30320, Symbol { name: "global_counter", address: 197408, size: 2, kind: Data, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"puts@GLIBC_2.17"`: addr=0, Symbol { name: "puts@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpStart"`: addr=6268, Symbol { name: "XcpStart", address: 25192, size: 588, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpIsConnected"`: addr=307c, Symbol { name: "XcpIsConnected", address: 12412, size: 16, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"freeifaddrs@GLIBC_2.17"`: addr=0, Symbol { name: "freeifaddrs@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCommand"`: addr=44e8, Symbol { name: "XcpCommand", address: 17640, size: 32, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"strcmp@GLIBC_2.17"`: addr=0, Symbol { name: "strcmp@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"params"`: addr=be98, Symbol { name: "params", address: 48792, size: 72, kind: Data, section: Section(SectionIndex(16)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"pthread_detach@GLIBC_2.34"`: addr=0, Symbol { name: "pthread_detach@GLIBC_2.34", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"fread@GLIBC_2.17"`: addr=0, Symbol { name: "fread@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"gXcpLogLevel"`: addr=30268, Symbol { name: "gXcpLogLevel", address: 197224, size: 1, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"__stop_xcp_cals"`: addr=302d0, Symbol { name: "__stop_xcp_cals", address: 197328, size: 0, kind: Unknown, section: Section(SectionIndex(26)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 3 } } + `"clockGetMonotonicUs"`: addr=aa24, Symbol { name: "clockGetMonotonicUs", address: 43556, size: 92, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__aarch64_have_lse_atomics"`: addr=32a08, Symbol { name: "__aarch64_have_lse_atomics", address: 207368, size: 1, kind: Data, section: Section(SectionIndex(29)), scope: Linkage, weak: false, flags: Elf { st_info: 17, st_other: 2 } } + `"foo"`: addr=1f84, Symbol { name: "foo", address: 8068, size: 232, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clock_getres@GLIBC_2.17"`: addr=0, Symbol { name: "clock_getres@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCalSegPublishAll"`: addr=9b28, Symbol { name: "XcpCalSegPublishAll", address: 39720, size: 388, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterStartDaqCallback"`: addr=23cc, Symbol { name: "ApplXcpRegisterStartDaqCallback", address: 9164, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketBind"`: addr=ad4c, Symbol { name: "socketBind", address: 44364, size: 332, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"dl_iterate_phdr@GLIBC_2.17"`: addr=0, Symbol { name: "dl_iterate_phdr@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetProjectName"`: addr=3144, Symbol { name: "XcpGetProjectName", address: 12612, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCalSegGetCalPage"`: addr=a0fc, Symbol { name: "XcpCalSegGetCalPage", address: 41212, size: 308, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpDeinitCalSegList"`: addr=9448, Symbol { name: "XcpDeinitCalSegList", address: 37960, size: 32, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"_end"`: addr=32a10, Symbol { name: "_end", address: 207376, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 0 } } + `"XcpEventExtAt_Var"`: addr=4208, Symbol { name: "XcpEventExtAt_Var", address: 16904, size: 436, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"free@GLIBC_2.17"`: addr=0, Symbol { name: "free@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"_start"`: addr=1d40, Symbol { name: "_start", address: 7488, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpStartDaq"`: addr=2544, Symbol { name: "ApplXcpStartDaq", address: 9540, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"counter"`: addr=3031c, Symbol { name: "counter", address: 197404, size: 2, kind: Data, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"clockGetRealtimeUsLast"`: addr=ab4c, Symbol { name: "clockGetRealtimeUsLast", address: 43852, size: 56, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpTlWaitForTransmitQueueEmpty"`: addr=867c, Symbol { name: "XcpTlWaitForTransmitQueueEmpty", address: 34428, size: 88, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpIsStarted"`: addr=306c, Symbol { name: "XcpIsStarted", address: 12396, size: 16, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"gXcpBaseAddr"`: addr=30390, Symbol { name: "gXcpBaseAddr", address: 197520, size: 8, kind: Data, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"XcpGetElfName"`: addr=2ab4, Symbol { name: "XcpGetElfName", address: 10932, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"sleepUs"`: addr=a594, Symbol { name: "sleepUs", address: 42388, size: 60, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEventExt_"`: addr=3d58, Symbol { name: "XcpEventExt_", address: 15704, size: 244, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketSendTo"`: addr=b648, Symbol { name: "socketSendTo", address: 46664, size: 368, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__end__"`: addr=32a10, Symbol { name: "__end__", address: 207376, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 0 } } + `"ApplXcpSetCalPage"`: addr=2974, Symbol { name: "ApplXcpSetCalPage", address: 10612, size: 44, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"nanosleep@GLIBC_2.17"`: addr=0, Symbol { name: "nanosleep@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queueInit"`: addr=8884, Symbol { name: "queueInit", address: 34948, size: 232, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpTlGetCtr"`: addr=86d4, Symbol { name: "XcpTlGetCtr", address: 34516, size: 20, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"send@GLIBC_2.17"`: addr=0, Symbol { name: "send@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"sleepMs"`: addr=a5d0, Symbol { name: "sleepMs", address: 42448, size: 88, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketGetMAC"`: addr=af3c, Symbol { name: "socketGetMAC", address: 44860, size: 152, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__bss_start"`: addr=30318, Symbol { name: "__bss_start", address: 197400, size: 0, kind: Unknown, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 0 } } + `"ApplXcpDisconnect"`: addr=2488, Symbol { name: "ApplXcpDisconnect", address: 9352, size: 48, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpInit"`: addr=5f18, Symbol { name: "XcpInit", address: 24344, size: 848, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterGetClockCallback"`: addr=25e4, Symbol { name: "ApplXcpRegisterGetClockCallback", address: 9700, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"main"`: addr=20ac, Symbol { name: "main", address: 8364, size: 752, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetDaqStartTime"`: addr=312c, Symbol { name: "XcpGetDaqStartTime", address: 12588, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpSetLogLevel"`: addr=2fcc, Symbol { name: "XcpSetLogLevel", address: 12236, size: 120, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockGetRealtimeUs"`: addr=aa80, Symbol { name: "clockGetRealtimeUs", address: 43648, size: 92, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"fwrite@GLIBC_2.17"`: addr=0, Symbol { name: "fwrite@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_create@GLIBC_2.34"`: addr=0, Symbol { name: "pthread_create@GLIBC_2.34", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"gXcpBaseAddrValid"`: addr=30398, Symbol { name: "gXcpBaseAddrValid", address: 197528, size: 1, kind: Data, section: Section(SectionIndex(29)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"XcpGetCalSegName"`: addr=9648, Symbol { name: "XcpGetCalSegName", address: 38472, size: 80, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socket@GLIBC_2.17"`: addr=0, Symbol { name: "socket@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpGetAddr"`: addr=2730, Symbol { name: "ApplXcpGetAddr", address: 10032, size: 244, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterWriteCallback"`: addr=2420, Symbol { name: "ApplXcpRegisterWriteCallback", address: 9248, size: 4, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpUnlockCalSeg"`: addr=995c, Symbol { name: "XcpUnlockCalSeg", address: 39260, size: 184, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_mutex_destroy@GLIBC_2.17"`: addr=0, Symbol { name: "pthread_mutex_destroy@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpBackgroundTasks"`: addr=25c4, Symbol { name: "ApplXcpBackgroundTasks", address: 9668, size: 32, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetCalSegNumber"`: addr=95f8, Symbol { name: "XcpGetCalSegNumber", address: 38392, size: 80, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEthServerShutdown"`: addr=78bc, Symbol { name: "XcpEthServerShutdown", address: 30908, size: 232, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_struct"`: addr=30254, Symbol { name: "global_test_struct", address: 197204, size: 12, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"__start_xcp_evts"`: addr=302d0, Symbol { name: "__start_xcp_evts", address: 197328, size: 0, kind: Unknown, section: Section(SectionIndex(27)), scope: Dynamic, weak: false, flags: Elf { st_info: 16, st_other: 3 } } + `"socketSend"`: addr=b7b8, Symbol { name: "socketSend", address: 47032, size: 216, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCreateCalSeg"`: addr=97bc, Symbol { name: "XcpCreateCalSeg", address: 38844, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"task"`: addr=1e54, Symbol { name: "task", address: 7764, size: 304, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCalSegWriteMemory"`: addr=9cac, Symbol { name: "XcpCalSegWriteMemory", address: 40108, size: 400, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterConnectCallback"`: addr=23b4, Symbol { name: "ApplXcpRegisterConnectCallback", address: 9140, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockGetMonotonicNs"`: addr=a990, Symbol { name: "clockGetMonotonicNs", address: 43408, size: 60, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpGetClockState"`: addr=2628, Symbol { name: "ApplXcpGetClockState", address: 9768, size: 44, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpPrint"`: addr=5e18, Symbol { name: "XcpPrint", address: 24088, size: 256, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queueInitFromMemory"`: addr=86e8, Symbol { name: "queueInitFromMemory", address: 34536, size: 348, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpSetMta"`: addr=33ac, Symbol { name: "XcpSetMta", address: 13228, size: 364, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_float"`: addr=30240, Symbol { name: "global_test_float", address: 197184, size: 4, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"if_nametoindex@GLIBC_2.17"`: addr=0, Symbol { name: "if_nametoindex@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterCheckCallback"`: addr=2410, Symbol { name: "ApplXcpRegisterCheckCallback", address: 9232, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetEcuEpk"`: addr=31c4, Symbol { name: "XcpGetEcuEpk", address: 12740, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__atomic_store_4@LIBATOMIC_1.0"`: addr=0, Symbol { name: "__atomic_store_4@LIBATOMIC_1.0", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetSegInfo"`: addr=9e3c, Symbol { name: "XcpGetSegInfo", address: 40508, size: 452, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEventExtAt"`: addr=3e80, Symbol { name: "XcpEventExtAt", address: 16000, size: 264, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEthTlInit"`: addr=8234, Symbol { name: "XcpEthTlInit", address: 33332, size: 408, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queueClear"`: addr=8844, Symbol { name: "queueClear", address: 34884, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__TMC_END__"`: addr=30270, Symbol { name: "__TMC_END__", address: 197232, size: 0, kind: Data, section: Section(SectionIndex(26)), scope: Linkage, weak: false, flags: Elf { st_info: 17, st_other: 2 } } + `"XcpEventAt"`: addr=3ff4, Symbol { name: "XcpEventAt", address: 16372, size: 88, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCalSegEndAtomicTransaction"`: addr=a550, Symbol { name: "XcpCalSegEndAtomicTransaction", address: 42320, size: 68, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"strncpy@GLIBC_2.17"`: addr=0, Symbol { name: "strncpy@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"global_test_array"`: addr=30251, Symbol { name: "global_test_array", address: 197201, size: 3, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"_ITM_registerTMCloneTable"`: addr=0, Symbol { name: "_ITM_registerTMCloneTable", address: 0, size: 0, kind: Unknown, section: Undefined, scope: Unknown, weak: true, flags: Elf { st_info: 32, st_other: 0 } } + `"global_test_int16"`: addr=30232, Symbol { name: "global_test_int16", address: 197170, size: 2, kind: Data, section: Section(SectionIndex(25)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"XcpInitCalSegList"`: addr=9408, Symbol { name: "XcpInitCalSegList", address: 37896, size: 64, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_self@GLIBC_2.17"`: addr=0, Symbol { name: "pthread_self@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"mutexInit"`: addr=a628, Symbol { name: "mutexInit", address: 42536, size: 76, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_mutexattr_init@GLIBC_2.34"`: addr=0, Symbol { name: "pthread_mutexattr_init@GLIBC_2.34", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"printf@GLIBC_2.17"`: addr=0, Symbol { name: "printf@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockGet"`: addr=a9cc, Symbol { name: "clockGet", address: 43468, size: 60, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"__errno_location@GLIBC_2.17"`: addr=0, Symbol { name: "__errno_location@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpRegisterGetClockStateCallback"`: addr=261c, Symbol { name: "ApplXcpRegisterGetClockStateCallback", address: 9756, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpDisconnect"`: addr=43bc, Symbol { name: "XcpDisconnect", address: 17340, size: 196, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_join@GLIBC_2.34"`: addr=0, Symbol { name: "pthread_join@GLIBC_2.34", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XCPLITE__ACSDD"`: addr=c570, Symbol { name: "XCPLITE__ACSDD", address: 50544, size: 2, kind: Data, section: Section(SectionIndex(16)), scope: Dynamic, weak: false, flags: Elf { st_info: 17, st_other: 0 } } + `"mutexDestroy"`: addr=a674, Symbol { name: "mutexDestroy", address: 42612, size: 20, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"putchar@GLIBC_2.17"`: addr=0, Symbol { name: "putchar@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEthServerStatus"`: addr=754c, Symbol { name: "XcpEthServerStatus", address: 30028, size: 88, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpFindCalSeg"`: addr=8f60, Symbol { name: "XcpFindCalSeg", address: 36704, size: 152, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpTlHandleTransmitQueue"`: addr=844c, Symbol { name: "XcpTlHandleTransmitQueue", address: 33868, size: 560, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_cancel@GLIBC_2.34"`: addr=0, Symbol { name: "pthread_cancel@GLIBC_2.34", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"pthread_mutex_lock@GLIBC_2.17"`: addr=0, Symbol { name: "pthread_mutex_lock@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpPrepareDaq"`: addr=24b8, Symbol { name: "ApplXcpPrepareDaq", address: 9400, size: 140, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetInitMode"`: addr=3054, Symbol { name: "XcpGetInitMode", address: 12372, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"ApplXcpSetBaseAddr"`: addr=268c, Symbol { name: "ApplXcpSetBaseAddr", address: 9868, size: 112, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"_init"`: addr=1898, Symbol { name: "_init", address: 6296, size: 0, kind: Text, section: Section(SectionIndex(12)), scope: Linkage, weak: false, flags: Elf { st_info: 18, st_other: 2 } } + `"pthread_mutex_unlock@GLIBC_2.17"`: addr=0, Symbol { name: "pthread_mutex_unlock@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetDaqOverflowCount"`: addr=3138, Symbol { name: "XcpGetDaqOverflowCount", address: 12600, size: 12, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"clockGetLast"`: addr=aa08, Symbol { name: "clockGetLast", address: 43528, size: 28, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetCalSegSize"`: addr=9698, Symbol { name: "XcpGetCalSegSize", address: 38552, size: 80, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"fprintf@GLIBC_2.17"`: addr=0, Symbol { name: "fprintf@GLIBC_2.17", address: 0, size: 0, kind: Text, section: Undefined, scope: Unknown, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpGetEventPriority"`: addr=3600, Symbol { name: "XcpGetEventPriority", address: 13824, size: 84, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpCreateCalSegPreloaded"`: addr=974c, Symbol { name: "XcpCreateCalSegPreloaded", address: 38732, size: 112, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEventExt"`: addr=3e4c, Symbol { name: "XcpEventExt", address: 15948, size: 52, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketCleanup"`: addr=abc4, Symbol { name: "socketCleanup", address: 43972, size: 4, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"socketOpen"`: addr=abc8, Symbol { name: "socketOpen", address: 43976, size: 388, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"queueLevel"`: addr=8b2c, Symbol { name: "queueLevel", address: 35628, size: 56, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + `"XcpEventExt_Var"`: addr=404c, Symbol { name: "XcpEventExt_Var", address: 16460, size: 444, kind: Text, section: Section(SectionIndex(14)), scope: Dynamic, weak: false, flags: Elf { st_info: 18, st_other: 0 } } + + +==================================================================================================== +DebugData information summary: + Compilation units: 9 units + Compiler: Debian clang version 14.0.6 + Sections: 29 + Endianness: Little Endian + Variables 582 with 248 unique names + Demangled names: 0 entries + Type names: 116 named types + Types: 461 total types + EPK string: `V2.1.10` at address 0x00030310 + XCP metadata section (xcp_meta) found at address 0x0000F4F0, 349 bytes + +==================================================================================================== +Compilation units in debug_data.unit_names: + Unit 0: main_c + Unit 1: xcpappl_c + Unit 2: xcplite_c + Unit 3: xcpethserver_c + Unit 4: xcpethtl_c + Unit 5: queue64v_c + Unit 6: cal_c + Unit 7: platform_c + Unit 8: sockets_c + +==================================================================================================== +DWARF sections by address: + '.interp': 0x00000238, 568 bytes (595) + '.note.gnu.build-id': 0x00000254, 28 bytes (632) + '.note.ABI-tag': 0x00000278, 36 bytes (664) + '.hash': 0x00000298, 32 bytes (1224) + '.gnu.hash': 0x000004c8, 560 bytes (1252) + '.dynsym': 0x000004e8, 32 bytes (2960) + '.dynstr': 0x00000b90, 1704 bytes (3764) + '.gnu.version': 0x00000eb4, 804 bytes (3906) + '.gnu.version_r': 0x00000f48, 148 bytes (3992) + '.rela.dyn': 0x00000f98, 80 bytes (4736) + '.rela.plt': 0x00001280, 744 bytes (6296) + '.init': 0x00001898, 1560 bytes (6320) + '.plt': 0x000018b0, 24 bytes (7392) + '.text': 0x00001d00, 1104 bytes (48752) + '.fini': 0x0000be70, 41328 bytes (48772) + '.rodata': 0x0000be88, 24 bytes (62697) + 'xcp_meta': 0x0000f4f0, 13928 bytes (63053) + '.eh_frame_hdr': 0x0000f650, 352 bytes (64724) + '.eh_frame': 0x0000fcd8, 1672 bytes (70668) + '.init_array': 0x0002fd48, 131184 bytes (195928) + '.fini_array': 0x0002fd58, 16 bytes (195936) + '.dynamic': 0x0002fd60, 8 bytes (196464) + '.got': 0x0002ff70, 528 bytes (196584) + '.got.plt': 0x0002ffe8, 120 bytes (197128) + '.data': 0x00030208, 544 bytes (197230) + 'xcp_cals': 0x00030270, 104 bytes (197328) + 'xcp_evts': 0x000302d0, 96 bytes (197392) + 'xcp_epk': 0x00030310, 64 bytes (197400) + '.bss': 0x00030318, 8 bytes (207376) + +==================================================================================================== +A2L Creator variables: +calseg__params': main_c:'' : addr=0:0x00030270 +xcp_meta__min__params__delay_us': main_c:'' : addr=0:0x0000F4F0 +xcp_meta__max__params__delay_us': main_c:'' : addr=0:0x0000F4F8 +xcp_meta__unit__params__delay_us': main_c:'' : addr=0:0x0000F500 +2 instances of 'xcp_meta__comment__counter' found, skipped +xcp_meta__read_write__counter': main_c:'' : addr=0:0x0000F51F +xcp_meta__comment__global_counter': main_c:'' : addr=0:0x0000F520 +xcp_meta__read_write__global_counter': main_c:'' : addr=0:0x0000F53C +3 instances of 'xcp_meta__comment__static_counter' found, skipped +evt__task': main_c:'task' : addr=0:0x000302D0 +trg__AASD__task': main_c:'task' : addr=0:0x00000000 +xcp_meta__comment__foo__counter': main_c:'foo' : addr=0:0x0000F5E2 +evt__foo': main_c:'foo' : addr=0:0x000302E0 +trg__AASR__foo': main_c:'foo' : addr=0:0x00000000 +evt__bar': main_c:'bar' : addr=0:0x000302F0 +trg__AAS__bar': main_c:'bar' : addr=0:0x00000000 +calseg__counter_control': main_c:'main' : addr=0:0x00030290 +evt__mainloop': main_c:'main' : addr=0:0x00030300 +trg__AAS__mainloop': main_c:'main' : addr=0:0x00000000 +calseg__epk': xcplite_c:'XcpInit' : addr=0:0x000302B0 + + +==================================================================================================== +Variables: + (Skipping system variables '__' and global XCP variables 'gXcp..' and 'gA2l..') +counter_control 1: +params 1: +calseg__params 1: +xcp_meta__min__params__delay_us 1: +xcp_meta__max__params__delay_us 1: +xcp_meta__unit__params__delay_us 1: +xcp_meta__comment__counter 2: +xcp_meta__read_write__counter 1: +counter 3: +xcp_meta__comment__global_counter 1: +xcp_meta__read_write__global_counter 1: +global_counter 1: +global_test_uint8 1: +global_test_uint16 1: +global_test_uint32 1: +global_test_uint64 1: +global_test_int8 1: +global_test_int16 1: +global_test_int32 1: +global_test_int64 1: +global_test_float 1: +global_test_double 1: +global_test_bool 1: +global_test_array 1: +global_test_struct 1: +xcp_meta__comment__static_counter 3: +static_counter 3: +evt__task 1: +evt_id_task 1: +trg__AASD__task 1: +heap_struct 1: +delay 2: +xcp_meta__comment__foo__counter 1: +evt__foo 1: +trg__AASR__foo 1: +test_int8 1: +test_int16 1: +test_int32 1: +test_int64 1: +test_float 1: +test_double 1: +test_uint8 1: +test_uint16 1: +test_uint32 1: +test_uint64 1: +test_struct 1: +test_array 1: +cap__foo 1: +evt__bar 1: +trg__AAS__bar 1: +calseg_id_counter_control 1: +calseg__counter_control 1: +evt__mainloop 1: +evt_id_mainloop 1: +trg__AAS__mainloop 1: +xcp_epk_keep 1: +p_params 1: +p_counter_control 1: +calseg_id_params 1: +global_running 1: +gModuleAddrValid 1: +gModuleAddr 1: +addr 2: +base 1: +b 2: +diff 1: +dot 1: +len 14: +project_name 1: +epk 1: +XCPLITE__ACSDD 1: +calseg_id_epk 1: +calseg__epk 1: +p 2: +event 6: +daq 17: +res 8: +calseg_index 10: +c 10: +begin 13: +end 13: +event_desc 10: +e 13: +i 35: +size 8: +ext 1: +old_value 10: +err 10: +n 29: +odt 4: +count 4: +eventName 1: +timeUnit 1: +timeCycle 1: +eventNumber 1: +clock 2: +mode 4: +prio 1: +prescaler 1: +idx 1: +src_segment 1: +src_page 1: +dst_segment 1: +dst_page 1: +segment 4: +page 2: +segInfo 1: +mapIndex 1: +subcmd 1: +hs 2: +d0 2: +queue_buffer 8: +dst 2: +addr_ext_ptr 2: +size_ptr 2: +addr_ptr 2: +el 2: +src 3: +bases 6: +args 2: +ts 4: +t 5: +base_offset 1: +e0 1: +daq_odt 2: +xcpFirstOdt 1: +s 5: +r 2: +odt0 1: +value 1: +sum 1: +event_id0 1: +daq0_next 1: +daq0 1: +d 3: +now 2: +crm 4: +l 3: +uuid 1: +max_size 1: +ctr 2: +last_time 1: +msg 3: +buf 1: +msgBuf 1: +srcAddr 1: +srcPort 1: +connected 1: +bind_addr 1: +queue_buffers 1: +length 1: +index 3: +total_lost 1: +retries 1: +lost 3: +flush 1: +max_level 2: +level 2: +queue 13: +aligned_size 3: +entry_len 1: +entry 3: +tail 2: +head 3: +ret 8: +flush_offset 2: +peek_tail 1: +entry_size 1: +entry_state 1: +header 1: +calseg 6: +aligned_page_size 2: +old_used 2: +new_used 2: +name_len 2: +seg_index 1: +seg 1: +old_lock_count 2: +ecu_page_next 1: +ecu_page 1: +offset 2: +free_page 3: +xcp_page_new 3: +xcp_page_old 3: +timeout 4: +res1 1: +dst_seg_index 1: +srcPtr 1: +timerem 2: +ma 1: +tm 3: +fns 3: +gtr 1: +sock 10: +useTCP 1: +reuseaddr 1: +yes 1: +pmtu 1: +a 1: +ifaddrs 1: +ifa 1: +tv 1: +sa 3: +sa_size 1: +group 1: +srclen 1: +received 1: +timeout_counter 1: +iov 2: +total 2: +remaining 1: +Found 3 segment definition marker variables: +0: 'epk' - number=Some(0), addr=00000000' + found in xcplite_c:'XcpInit' +1: 'params' - number=Some(1), addr=00030270' + found in main_c:'' +2: 'counter_control' - number=Some(2), addr=00030290' + found in main_c:'main' +Calibration segment 'params' type information found, type=params, size = 72 + type = Struct params(9 members) +Calibration segment 'counter_control' type information found, type=counter_control, size = 4 + type = Struct counter_control(2 members) + Event 'task' trigger in function 'task', frame base FramePointer + Event 'foo' trigger in function 'foo', frame base FramePointer + Event 'bar' trigger in function 'bar', frame base FramePointer + Event 'mainloop' trigger in function 'main', frame base FramePointer + Add characteristic instance for counter_control: addr = 0:0x0000be90 type = Struct counter_control(2 members) + Registered variable 'counter_control' type_name = 'counter_control', size = 4, event_id = Some(3) + Add characteristic instance for params: addr = 0:0x0000be98 type = Struct params(9 members) + Registered variable 'params' type_name = 'params', size = 72, event_id = Some(3) + Add measurement instance for counter: addr = 0:0x0003031c type = Uint16 + Registered variable 'counter' type_name = 'uint16_t', size = 2, event_id = Some(3) + Add measurement instance for task.counter: addr = 2:0x0001001c type = Uint32 + Registered variable 'task.counter' type_name = 'uint32_t', size = 4, event_id = Some(0) + Add measurement instance for global_counter: addr = 0:0x00030320 type = Uint16 + Registered variable 'global_counter' type_name = 'uint16_t', size = 2, event_id = Some(3) + Add measurement instance for global_test_uint8: addr = 0:0x0003021a type = Uint8 + Registered variable 'global_test_uint8' type_name = 'uint8_t', size = 1, event_id = Some(3) + Add measurement instance for global_test_uint16: addr = 0:0x0003021c type = Uint16 + Registered variable 'global_test_uint16' type_name = 'uint16_t', size = 2, event_id = Some(3) + Add measurement instance for global_test_uint32: addr = 0:0x00030220 type = Uint32 + Registered variable 'global_test_uint32' type_name = 'uint32_t', size = 4, event_id = Some(3) + Add measurement instance for global_test_uint64: addr = 0:0x00030228 type = Uint64 + Registered variable 'global_test_uint64' type_name = 'uint64_t', size = 8, event_id = Some(3) + Add measurement instance for global_test_int8: addr = 0:0x00030230 type = Sint8 + Registered variable 'global_test_int8' type_name = 'int8_t', size = 1, event_id = Some(3) + Add measurement instance for global_test_int16: addr = 0:0x00030232 type = Sint16 + Registered variable 'global_test_int16' type_name = 'int16_t', size = 2, event_id = Some(3) + Add measurement instance for global_test_int32: addr = 0:0x00030234 type = Sint32 + Registered variable 'global_test_int32' type_name = 'int32_t', size = 4, event_id = Some(3) + Add measurement instance for global_test_int64: addr = 0:0x00030238 type = Sint64 + Registered variable 'global_test_int64' type_name = 'int64_t', size = 8, event_id = Some(3) + Add measurement instance for global_test_float: addr = 0:0x00030240 type = Float + Registered variable 'global_test_float' type_name = 'float', size = 4, event_id = Some(3) + Add measurement instance for global_test_double: addr = 0:0x00030248 type = Double + Registered variable 'global_test_double' type_name = 'double', size = 8, event_id = Some(3) + Add measurement instance for global_test_bool: addr = 0:0x00030250 type = Uint8 + Registered variable 'global_test_bool' type_name = '_Bool', size = 1, event_id = Some(3) + Add measurement instance for global_test_array: addr = 0:0x00030251 type = Array([3] x Uint8) + Registered variable 'global_test_array' type_name = 'uint8_t', size = 3, event_id = Some(3) + Add measurement instance for global_test_struct: addr = 0:0x00030254 type = Struct test_struct(4 members) + Registered variable 'global_test_struct' type_name = 'test_struct', size = 12, event_id = Some(3) + Add measurement instance for task.static_counter: addr = 0:0x00030324 type = Uint16 + Registered variable 'task.static_counter' type_name = 'uint16_t', size = 2, event_id = Some(0) + Add measurement instance for foo.static_counter: addr = 0:0x00030328 type = Uint16 + Registered variable 'foo.static_counter' type_name = 'uint16_t', size = 2, event_id = Some(1) + Add measurement instance for main.static_counter: addr = 0:0x0003032c type = Uint16 + Registered variable 'main.static_counter' type_name = 'uint16_t', size = 2, event_id = Some(3) + Add measurement instance for foo.test_int8: addr = 2:0x0040fffc type = Sint8 + Registered variable 'foo.test_int8' type_name = 'int8_t', size = 1, event_id = Some(1) + Add measurement instance for foo.test_int16: addr = 2:0x0040fff8 type = Sint16 + Registered variable 'foo.test_int16' type_name = 'int16_t', size = 2, event_id = Some(1) + Add measurement instance for foo.test_int32: addr = 2:0x0040fff4 type = Sint32 + Registered variable 'foo.test_int32' type_name = 'int32_t', size = 4, event_id = Some(1) + Add measurement instance for foo.test_int64: addr = 2:0x0040ffe8 type = Uint64 + Registered variable 'foo.test_int64' type_name = 'uint64_t', size = 8, event_id = Some(1) + Add measurement instance for calseg_id_counter_control: addr = 0:0x00030264 type = Uint16 + Registered variable 'calseg_id_counter_control' type_name = 'tXcpCalSegIndex', size = 2, event_id = Some(3) + Add measurement instance for main.xcp_epk_keep: addr = 2:0x00c1001c type = Uint8 + Registered variable 'main.xcp_epk_keep' type_name = 'char', size = 1, event_id = Some(3) + Add measurement instance for calseg_id_params: addr = 0:0x00030218 type = Uint16 + Registered variable 'calseg_id_params' type_name = 'tXcpCalSegIndex', size = 2, event_id = Some(3) + Add measurement instance for global_running: addr = 0:0x00030260 type = Uint8 + Registered variable 'global_running' type_name = '_Bool', size = 1, event_id = Some(3) + Add measurement instance for captured foo.counter: addr = 3:0x00400000 + Add measurement instance for captured foo.test_float: addr = 3:0x00400004 + Add measurement instance for captured foo.test_double: addr = 3:0x00400008 + Add measurement instance for captured foo.test_uint8: addr = 3:0x00400010 + Add measurement instance for captured foo.test_uint16: addr = 3:0x00400012 + Add measurement instance for captured foo.test_uint32: addr = 3:0x00400014 + Add measurement instance for captured foo.test_uint64: addr = 3:0x00400018 + Add measurement instance for captured foo.test_struct: addr = 3:0x00400020 + Add measurement instance for captured foo.test_array: addr = 3:0x0040002c + Metadata xcp_meta__min__params__delay_us applied to typedef field 'params.delay_us' + Metadata xcp_meta__max__params__delay_us applied to typedef field 'params.delay_us' + Metadata xcp_meta__unit__params__delay_us applied to typedef field 'params.delay_us' + Metadata comment xcp_meta__comment__counter applied to instance 'counter' + Metadata comment xcp_meta__comment__counter applied to instance 'task.counter' + Metadata read_write xcp_meta__read_write__counter applied to instance 'counter' + Metadata comment xcp_meta__comment__global_counter applied to instance 'global_counter' + Metadata read_write xcp_meta__read_write__global_counter applied to instance 'global_counter' + Metadata comment xcp_meta__comment__static_counter applied to instance 'task.static_counter' + Metadata comment xcp_meta__comment__static_counter applied to instance 'foo.static_counter' + Metadata comment xcp_meta__comment__static_counter applied to instance 'main.static_counter' + Metadata comment xcp_meta__comment__foo__counter applied to instance 'foo.counter' diff --git a/examples/no_a2l_demo/README.md b/examples/no_a2l_demo/README.md index bc602146..6c34a9f2 100644 --- a/examples/no_a2l_demo/README.md +++ b/examples/no_a2l_demo/README.md @@ -11,54 +11,15 @@ The A2L database is instead generated offline by a tool. The `xcpclient` test tool, which is part of this repository under `tools/xcpclient/`, includes an XCPlite-specific ELF->A2L generator that reads the ELF file and DWARF debug information to create a complete, plug&play A2L database for the application. -## The XCPlite Build Time A2L Generation Concept - -The fundamental idea is to provide additional compile-time and link-time information in the ELF file, which is used by a specialized A2L database creator (ELF -> A2L converter) designed exclusively for XCPlite. The XCPlite A2L creator knows implementation details of the XCPlite code instrumentation library to automate the A2L generation process as much as possible: -- It automatically detects all events and calibration memory segments created by the XCPlite code instrumentation API macros. -- It detects the code locations of the event trigger points and automatically associates local and member variables with complex types existing in each events scope. -- It can add metadata for calibration parameters and measurement variables, such as physical units, min/max limits, and scaling information. - -In addition to that, the information generated at link time is used by the XCPlite runtime to register events and calibration segments with deterministic order and indexing. Events and calibration segments may be declared anywhere in the code, the A2L file will remain stable, independent of code execution order. - -The test XCP client in the xcpclient tool can work with the ELF file directly, no need for a separate A2L file. The A2L file is needed for tools like CANape, which support the XCP protocol in the standard way. - - - - -### Using of the xcpclient A2L Creator - -An XCPlite specific A2L creator/writer with ELF/DWARF reader is built into the xcpclient tool. - -Option 1: A2L template generation: - -- Creates a complete A2L template with IF_DATA, epk version, memory segments and events from ELF by detecting static segment and event marker variables created by the XCPlite code instrumentation - - -Option 2: Full A2L content generation: - -- Calibration parameters - Option 1: #undef OPTION_CAL_SEGMENTS_ABS - XCP is configured for segment relative addressing mode - Address calibration parameters by their segment number and offset. - This is the preferred option for 64-bit microprocessors, where the calibration segments may be located anywhere in the 64-bit address space. - Option 2: #define OPTION_CAL_SEGMENTS_ABS - XCP configured for absolute calibration segment addressing - This is the preferred option for microcontrollers - The reference pages of all calibration parameters must be in addressable (4 GB - 32bit) global memory (.bss or .rodata segment must be in this range) - Detect calibration parameters by the address of their default/reference page by naming convention and segment marker variable - -- Measurement variables - Global or static measurement variables are restricted to be in a addressable (4 GB - 32bit) global memory range. A2L addresses are relative in this range - Local variables on stack are measured by knowing their CFA offset in the current stack frame, and variables on heap are addressed relative to explicitly given anchor addresses - The creator takes all global, static and local variables into account in specified compilation units - It tries to detect an appropriate fixed event for each variable by detecting an event trigger in the same function, if not it uses the unsafe default event named `async` as default event - -- Add all types required for the variables found as TYPEDEF_STRUCTURE - -Content generation in Option 2 can alternatively be done manually, with any other A2L tool from Vector or open source tools. - +## Offline A2L generation +The concept, the workflow, the rules for the application code, the naming of types and variables, the supported types and the +diagnostics of the ELF/DWARF to A2L generator are described in [docs/OFFLINE_A2L.md](../../docs/OFFLINE_A2L.md). In short: +- Use the instrumentation macros, not the raw C API, only the macros emit the ELF markers. +- Build with debug information (`-g`, `Debug` or `RelWithDebInfo`). +- Mark local measurement variables `volatile`, so that they stay on the stack frame in optimized builds. +- Declare calibration segments with `CalSegDecl` or `CalSegDeclRef`, the default page needs static storage duration. ## Library Configuration Override @@ -105,41 +66,6 @@ The same pattern can be used to create any other application-specific configurat --- -## How xcpclient finds your events and variables - -`xcpclient --create-a2l` discovers events, calibration segments, and local variables -automatically — **without any runtime A2L calls in your code** — by reading named ELF -sections and DWARF debug info written by the XCPlite macros. - -To make this work correctly, follow these rules: - -1. **Always use the macros, never the raw C API** (`XcpCreateEvent`, `XcpCreateCalSeg`, etc.). - Only the macros emit the ELF section data and DWARF anchors that xcpclient needs. - -2. **Mark local measurement variables `volatile`** in optimized builds. - Without `volatile` the compiler may eliminate stack variables or give them unreliable - DWARF location expressions: - ```c - void myTask(void) { - volatile uint32_t counter = 0; // XCP: keep on stack for offline A2L - DaqCreateAndTriggerEvent(myTask); - } - ``` - -3. **Build with debug information** (`-g` / `CMAKE_BUILD_TYPE=Debug` or `RelWithDebInfo`). - xcpclient reads DWARF; stripped builds have no type or location data. - -4. **Prefer file-scope `CalSegDecl` / `CalSegDeclRef`** so the descriptor is clearly - visible and allocated for program lifetime. A local-scope `CalSegDeclRef` is also - valid when used intentionally to keep visibility local, as long as the default - object has static storage duration. - -For the full ELF/DWARF mechanics — section layouts, the `trg__` anchor naming convention, -and address encoding — see -[docs/TECHNICAL.md — Offline A2L Generation](../../docs/TECHNICAL.md#offline-a2l-generation--elfdwarf-internals). - ---- - ## API subset for no-A2L workflows The following API subset remains fully available and is unchanged: @@ -219,13 +145,15 @@ cmake -B build-no_a2l -S . -DXCPLITE_CONFIGURATION=no_a2l -DCMAKE_BUILD_TYPE=Deb cmake --build build-no_a2l # Generate an A2L file for the no_a2l_demo application offline from its ELF file +# The ELF file must come from a Linux build: executables built on macOS (Mach-O) contain no DWARF debug information +# and are rejected by xcpclient, see create_a2l.sh for a remote build on a Linux target # Example: # Add all variables -xcpclient --offline --elf build-no_a2l/no_a2l_demo --a2l no_a2l_demo.a2l --create-a2l --verbose 1 +xcpclient --offline --elf build-no_a2l/no_a2l_demo --a2l no_a2l_demo.a2l --create-a2l --default-event=mainloop --verbose 1 # Add the given IP address:port and protocol to the generated A2L file -xcpclient --udp --dest-addr 192.168.0.206 --offline --elf build-no_a2l/no_a2l_demo --a2l no_a2l_demo.a2l --create-a2l +xcpclient --udp --dest-addr 192.168.0.206 --offline --elf build-no_a2l/no_a2l_demo --a2l no_a2l_demo.a2l --create-a2l --default-event=mainloop # Filter on specific variables and compilation units -xcpclient --offline --udp --dest-addr 192.168.0.206 --elf build-no_a2l/no_a2l_demo --a2l no_a2l_demo.a2l --create-a2l --elf-unit-filter main --elf-var-filter "^(counter|params)" +xcpclient --offline --udp --dest-addr 192.168.0.206 --elf build-no_a2l/no_a2l_demo --a2l no_a2l_demo.a2l --create-a2l --default-event=mainloop --elf-unit-filter main --elf-var-filter "^(counter|params)" # Connect to the XCP on UDP server on 192.168.0.206:5555, upload ELF file from target (requires OPTION_ENABLE_ELF_UPLOAD) and create the A2L file @@ -235,48 +163,3 @@ xcpclient --udp --dest-addr 192.168.0.206 --elf no_a2l_demo.elf --upload-elf - xcpclient --udp --dest-addr=192.168.0.206:5555 --elf no_a2l_demo.elf --elf-var-filter "global_counter" --mea ".*" --time 5 --csv no_a2l_demo.csv ``` - - -## Other A2L generation options - -### Using Vector CANape integrated A2L editor and ELF file support - -Drop the template generated by xcpclient into CANape and create a new XCP on Ethernet device. -Enable access to the ELF file in the device configuration. -Use the A2L editor to add individual measurement parameters. -For calibration segments or blocks, add the complete default value structure as an INSTANCE of TYPEDEF_STRUCTURE or add the variables as CHARACTERISTIC. - - -### Using Vector A2L-Toolset A2L-Creator to add measurement and calibration metadata - -The example code contains some A2L creator metadata annotation to add metadata such as calibration variable limits and physical units. -The A2L Creator is a commercial Vector product. - -### Using Open Source a2ltool - -Example: -Add the calibration segment `params` and the measurement variable `counter` to the A2L template: - -```bash -a2ltool --update --measurement-regex "counter" --characteristic-regex "params" --elffile no_a2l_demo.elf --enable-structures --output no_a2l_demo.a2l -``` - - - - - -### TODO List and open issues - -- Improve how to deal with enum size -- Heap measurement variables - The A2L creator can not handle heap variables yet - Needs to detect trg__AAS or trg__AASD type and analyze the argument type of DaqTriggerEvent(), pointer to type -- Thread local variables - The A2L creator can not handle thread local variables yet - The DAQ capture method does not work for TLS, need a ApplXcpGetTlsBaseAddress() function, maybe introduce AAST type - Detect the base address of the TLS block, like it is done in ApplXcpGetBaseAddr()/xcp_get_base_addr() for the global variables - The DaqCapture macros as an alternative, does not work yet -- Function parameters - Define a macro to declare function parameters as XCP_MEA, which spills them to stack - A2L Creator ELF reader parser must detect the function parameters with the CFA offset in the stack frame -- Make sure the event trigger location and the variable location have the same CFA (not seen any violations yet) diff --git a/examples/no_a2l_demo/create_a2l.sh b/examples/no_a2l_demo/create_a2l.sh index 5b44081f..2e19d2f4 100755 --- a/examples/no_a2l_demo/create_a2l.sh +++ b/examples/no_a2l_demo/create_a2l.sh @@ -8,6 +8,7 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # The script syncs the example project to the target, builds it there, runs it with XCP on Ethernet, # downloads the ELF file to the local machine and creates an A2L file. # Prerequisites: +# - The target machine must be Linux # - The target must be reachable via SSH and have rsync installed # - The local machine must have rsync and scp installed # - The local machine must have xcpclient installed @@ -29,8 +30,7 @@ ELFFILE="$REPO_ROOT/examples/no_a2l_demo/CANape/no_a2l_demo.elf" # Build type for target executable: Release, RelWithDebInfo or Debug # RelWithDebInfo is default to demonstrate operation with with -O1 and NDEBUG -# Optimization level >= -O1 keeps variables in registers whenever possible, so local variables cannot be measured -# The most efficient solution to keep local variables measurable is to use the DaqCapture macro, another option is mto ark the variable as volatile (with the provided macro XCP_MEA +# Optimization level >= -O1 keeps variables in registers whenever possible, so these local variables cannot be measured # Debug mode is the least efficient but keeps all variables and stack frames intact BUILD_TYPE="RelWithDebInfo" # -O0 @@ -39,8 +39,9 @@ BUILD_TYPE="RelWithDebInfo" #BUILD_TYPE="Release" # Run a simple test calibration and measurement -TEST=true -#TEST=false +TEST=false +# CSV measurement file path on local machine +CSVFILE="$REPO_ROOT/examples/no_a2l_demo/CANape/no_a2l_demo.csv" # Target connection details @@ -94,8 +95,12 @@ fi # Build on target -echo "Build executable on Target ..." -ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && ./build.sh $BUILD_TYPE no_a2l examples" 1> /dev/null +# Always a clean build: if the target has no NTP its clock may skew +# Optionally force gnu or clang, default to clang which is the more demanding one +echo "Clean build executable on Target ..." +#ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && ./build.sh $BUILD_TYPE no_a2l examples clean" 1> /dev/null +#ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && CC=gcc CXX=g++ ./build.sh $BUILD_TYPE no_a2l examples clean" 1> /dev/null +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && CC=clang CXX=clang++ ./build.sh $BUILD_TYPE no_a2l examples clean" 1> /dev/null if [ $? -ne 0 ]; then echo "❌ FAILED: Build on target" exit 1 @@ -122,10 +127,14 @@ echo "========================================================================== echo "Creating A2L file from XCPlite ELF file ..." echo "========================================================================================================" echo "" -echo "Command: $XCPCLIENT --log-level=3 --verbose=2 --dest-addr=$TARGET_HOST --udp --offline --elf \"$ELFFILE\" --create-a2l --a2l \"$A2LFILE\"" -$XCPCLIENT --log-level=3 --verbose=2 --dest-addr=$TARGET_HOST --udp --offline --elf "$ELFFILE" --create-a2l --a2l "$A2LFILE" >> "$LOGFILE" -if [ $? -ne 0 ]; then - echo "❌ FAILED: xcpclient returned error" +# Remove the A2L file of a previous run, so a failed generation can not leave a stale A2L file behind +rm -f "$A2LFILE" +XCPCLIENT_ARGS=(--log-level=1 --verbose=3 --dest-addr="$TARGET_HOST" --udp --offline --elf "$ELFFILE" --elf-unit-filter main --create-a2l --a2l "$A2LFILE" --default-event=mainloop) +echo "Command: $XCPCLIENT ${XCPCLIENT_ARGS[*]}" +"$XCPCLIENT" "${XCPCLIENT_ARGS[@]}" >> "$LOGFILE" +if [ $? -ne 0 ] || [ ! -f "$A2LFILE" ]; then + echo "❌ FAILED: xcpclient could not create the A2L file $A2LFILE, see $LOGFILE" + grep "\[ERROR\]" "$LOGFILE" exit 1 fi @@ -149,12 +158,20 @@ sleep 1 echo "========================================================================================================" echo "Test connect" echo "========================================================================================================" +read -p "Press any key to continue..." -n1 -s $XCPCLIENT --log-level=3 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --list-mea . --list-cal . +sleep 1 echo "========================================================================================================" echo "Test measurement" echo "========================================================================================================" -$XCPCLIENT --log-level=2 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --mea counter --time 1 --verbose 2 +read -p "Press any key to continue..." -n1 -s +# Log measurement to stdout +$XCPCLIENT --log-level=3 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --mea . --time 3 --verbose=2 +# Log measurement to CSV file +#$XCPCLIENT --log-level=3 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --mea . --time 3 --csv "$CSVFILE" +read -p "Press any key to continue..." -n1 -s +sleep 1 ssh "$TARGET_USER@$TARGET_HOST" "pkill -f no_a2l_demo" diff --git a/examples/no_a2l_demo/src/main.c b/examples/no_a2l_demo/src/main.c index f4276e36..c101264f 100644 --- a/examples/no_a2l_demo/src/main.c +++ b/examples/no_a2l_demo/src/main.c @@ -30,7 +30,7 @@ static void sig_handler(int sig) { global_running = false; } #define OPTION_SERVER_PORT 5555 // Port #define OPTION_SERVER_ADDR {0, 0, 0, 0} // Bind addr, 0.0.0.0 = ANY #define OPTION_QUEUE_SIZE (1024 * 8) // Size of the measurement queue in bytes, must be a multiple of 8 -#define OPTION_LOG_LEVEL 4 // Log level, 0 = no log, 1 = error, 2 = warning, 3 = info, 4 = debug +#define OPTION_LOG_LEVEL 3 // Log level, 0 = no log, 1 = error, 2 = warning, 3 = info, 4 = debug //----------------------------------------------------------------------------------------------------- // Demo calibration parameters @@ -106,13 +106,16 @@ XCP_UNIT(params__delay_us, "us"); // Global measurement variable // Modified in function foo // Measuring it in main or task, is possible, but asynchronous and may give inconsistent results +XCP_COMMENT(counter, "Global measurement variable"); // Example for meta data annotation as code +XCP_READ_WRITE(counter); // Example for meta data annotation as code +uint16_t counter = 0; XCP_COMMENT(global_counter, "Global measurement variable"); // Example for meta data annotation as code XCP_READ_WRITE(global_counter); // Example for meta data annotation as code uint16_t global_counter = 0; // A2L Creator code parser annotation /* -@@ SYMBOL = global_counter +@@ SYMBOL = counter @@ DESCRIPTION = "Global measurement variable" @@ END */ @@ -145,11 +148,11 @@ THREAD_FUNC_RETURN task(void *p) { printf("Start thread %u ...\n", get_thread_id()); // Static local scope measurement variable - XCP_COMMENT(static_counter, "Static local measurement variable in function task"); // Example for meta data annotation as code + XCP_COMMENT(static_counter, "Static local measurement variable in thread function `task`"); // Example for meta data annotation as code volatile static uint16_t static_counter = 0; // Local measurement variable - XCP_COMMENT(counter, "Local measurement variable in function task"); // Example for meta data annotation as code + XCP_COMMENT(counter, "Local measurement variable in thread function `task`"); // Example for meta data annotation as code volatile uint32_t counter = 0; // Heap measurement variable @@ -172,7 +175,7 @@ THREAD_FUNC_RETURN task(void *p) { DaqTriggerEventExt(task, heap_struct); // Sleep for a tunable amount of time (not inside the lock for the calibration parameter block, to not block the XCP server or other threads unnecessarily long) - uint32_t delay = ((const struct params *)CalSegLock(params))->delay_us; + uint32_t delay = CalSegLock(params)->delay_us; CalSegUnlock(params); sleepUs(delay); } @@ -184,32 +187,40 @@ THREAD_FUNC_RETURN task(void *p) { //----------------------------------------------------------------------------------------------------- // Demo functions -void foo(void) { +// Avoid inlining to be able to measure local variables +// xcpclient ELF->A2L does not support inlined function and silently drop them +XCP_NOINLINE void foo(void) { // Static local scope measurement variable + XCP_COMMENT(static_counter, "Local static measurement variable in function `foo`"); volatile static uint16_t static_counter = 0; - // Local variable - volatile uint32_t counter = 0; + // Local measurement variable + XCP_COMMENT(foo__counter, "Local captured measurement variable in function `foo`"); + uint32_t counter = 0; // More local measurement variables - volatile float test_float = 0.1f; - volatile double test_double = 0.2; - volatile uint8_t test_uint8 = 1; - volatile uint16_t test_uint16 = 2; - volatile uint32_t test_uint32 = 3; - volatile uint64_t test_uint64 = 4; - volatile int8_t test_int8 = -1; - volatile int16_t test_int16 = -2; - volatile int32_t test_int32 = -3; - volatile uint64_t test_int64 = 1; - volatile struct test_struct test_struct = {1, -2, 0.3f, {1, 2, 3}}; - // uint8_t test_array[3] = {1, 2, 3}; - + // Measured via capture, variables stay in their registers + float test_float = 0.1f; + double test_double = 0.2; + uint8_t test_uint8 = 1; + uint16_t test_uint16 = 2; + uint32_t test_uint32 = 3; + uint64_t test_uint64 = 4; + struct test_struct test_struct = {1, -2, 0.3f, {1, 2, 3}}; + uint8_t test_array[3] = {1, 2, 3}; + + // Measure via stack, register variables spilled to stack + XCP_MEAS int8_t test_int8 = -1; + XCP_MEAS int16_t test_int16 = -2; + XCP_MEAS int32_t test_int32 = -3; + XCP_MEAS uint64_t test_int64 = 1; + + global_counter++; + static_counter++; counter = global_counter; - static_counter = global_counter; - DaqCreateAndTriggerEvent(foo); + DaqCreateAndTriggerEventCapture(foo, counter, test_float, test_double, test_uint8, test_uint16, test_uint32, test_uint64, test_struct, test_array); } // Never called @@ -236,7 +247,7 @@ int main(int argc, char *argv[]) { printf("(no optimization)\n"); #endif printf("Address of 'params': %p (%u:%08X)(%zu bytes)\n", ¶ms, ApplXcpGetAddrExt((uint8_t *)¶ms), ApplXcpGetAddr((uint8_t *)¶ms), sizeof(params)); - printf("Address of 'global_counter': %p (%u:%08X)\n", &global_counter, ApplXcpGetAddrExt((uint8_t *)&global_counter), ApplXcpGetAddr((uint8_t *)&global_counter)); + printf("Address of 'counter': %p (%u:%08X)\n", &counter, ApplXcpGetAddrExt((uint8_t *)&counter), ApplXcpGetAddr((uint8_t *)&counter)); printf("\n"); signal(SIGINT, sig_handler); @@ -264,9 +275,7 @@ int main(int argc, char *argv[]) { create_thread(&__t1, NULL, task, NULL); // Demo measurement variables - XCP_COMMENT(main__counter, "Local measurement variable in main"); - volatile uint16_t counter = 0; - XCP_COMMENT(main__static_counter, "Static local measurement variable in main"); + XCP_COMMENT(static_counter, "Static local measurement variable in function `main`"); volatile static uint16_t static_counter = 0; // Calibration parameter counter_max @@ -284,16 +293,15 @@ int main(int argc, char *argv[]) { // Returns a pointer to the active page (working or reference) of the calibration segment or block const struct counter_control *p_counter_control = CalSegLock(counter_control); - global_counter += p_counter_control->counter_inc; - if (global_counter > p_counter_control->counter_max) { // Limit the global counter with the counter_max calibration value - global_counter = 0; + counter += p_counter_control->counter_inc; + if (counter > p_counter_control->counter_max) { // Limit the global counter with the counter_max calibration value + counter = 0; } // Unlock the calibration block CalSegUnlock(counter_control); - counter = global_counter; - static_counter = global_counter; + static_counter++; // Demonstrate calibration thread safety and consistency const struct params *p_params = CalSegLock(params); @@ -308,13 +316,12 @@ int main(int argc, char *argv[]) { // Function calls foo(); // Call a function to demonstrate the DaqCreateAndTriggerEvent macro in foo - // bar(); // Uncomment to demonstrate that the event in bar is created, but the code is never executed, so the event exists, but is never triggered // Trigger the measurement event "mainloop" DaqTriggerEvent(mainloop); // Sleep for a tunable amount of time (not inside the lock for the calibration parameter block, to not block the XCP server or other threads unnecessarily long) - uint32_t delay = ((const struct params *)CalSegLock(params))->delay_us; + uint32_t delay = CalSegLock(params)->delay_us; CalSegUnlock(params); sleepUs(delay); diff --git a/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l b/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l index b78c5444..b356d8a6 100644 --- a/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l +++ b/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l @@ -1,8 +1,8 @@  -/* Created by xcp_client with ELF/DWARF information only, offline mode - 2026-07-28 19:50:53 */ +/* Created by xcp_client with ELF/DWARF information only, offline mode - 2026-09-10 04:34:14 */ ASAP2_VERSION 1 71 /begin PROJECT project_name "" -/begin HEADER "" VERSION "1.0" PROJECT_NO XCPLITE__CASDD /end HEADER +/begin HEADER "" VERSION "1.0" PROJECT_NO XCPLITE__ACSDD /end HEADER /begin MODULE project_name "" @@ -82,8 +82,8 @@ ASAP2_VERSION 1 71 /begin TYPEDEF_CHARACTERISTIC C_F32 "" VALUE F32 0 NO_COMPU_METHOD -1e12 1e12 /end TYPEDEF_CHARACTERISTIC /begin MOD_PAR "" -EPK "109" ADDR_EPK 0x00030310 -/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x80000000 3 -1 -1 -1 -1 -1 +EPK "V2.1.10" ADDR_EPK 0x00030390 +/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x30390 7 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 0 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -92,7 +92,7 @@ EPK "109" ADDR_EPK 0x00030310 /end SEGMENT /end IF_DATA /end MEMORY_SEGMENT -/begin MEMORY_SEGMENT task_counter_ctl_params "" DATA FLASH INTERN 0x80010000 12 -1 -1 -1 -1 -1 +/begin MEMORY_SEGMENT params "" DATA FLASH INTERN 0xC808 64 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 1 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -101,7 +101,7 @@ EPK "109" ADDR_EPK 0x00030310 /end SEGMENT /end IF_DATA /end MEMORY_SEGMENT -/begin MEMORY_SEGMENT counter_ctl_params "" DATA FLASH INTERN 0x80020000 8 -1 -1 -1 -1 -1 +/begin MEMORY_SEGMENT counter_ctl_params "" DATA FLASH INTERN 0xC848 8 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 2 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -110,7 +110,7 @@ EPK "109" ADDR_EPK 0x00030310 /end SEGMENT /end IF_DATA /end MEMORY_SEGMENT -/begin MEMORY_SEGMENT params "" DATA FLASH INTERN 0x80030000 64 -1 -1 -1 -1 -1 +/begin MEMORY_SEGMENT task_counter_ctl_params "" DATA FLASH INTERN 0xC854 12 -1 -1 -1 -1 -1 /begin IF_DATA XCP /begin SEGMENT 3 /*number*/ 2 /*pages*/ 0 /* addr_ext*/ 0 0 /begin CHECKSUM XCP_ADD_44 MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM @@ -123,7 +123,7 @@ EPK "109" ADDR_EPK 0x00030310 /begin IF_DATA XCP /begin PROTOCOL_LAYER - 0x0104 1000 2000 0 0 0 0 0 248 248 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE + 0x0104 1000 2000 0 0 0 0 0 248 1024 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE OPTIONAL_CMD GET_COMM_MODE_INFO OPTIONAL_CMD GET_ID OPTIONAL_CMD SET_REQUEST @@ -161,24 +161,77 @@ EPK "109" ADDR_EPK 0x00030310 0x1 SIZE_DWORD UNIT_1US TIMESTAMP_FIXED /end TIMESTAMP_SUPPORTED - /* compilation unit = 0, function = main, CFA = 96 */ + /* compilation unit = 0, function = foo, CFA = 0 */ + /begin EVENT "foo" "foo" 0 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT + /* compilation unit = 0, function = task, CFA = 0 */ + /begin EVENT "task" "task" 1 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT + /* compilation unit = 0, function = main, CFA = 0 */ /begin EVENT "mainloop" "mainloop" 2 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT - /* compilation unit = 0, function = task, CFA = 96 */ - /begin EVENT "task" "task" 0 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT - /* compilation unit = 0, function = foo, CFA = 128 */ - /begin EVENT "foo" "foo" 1 DAQ 0xFF 0 0 0 CONSISTENCY DAQ /end EVENT /end DAQ - /begin XCP_ON_UDP_IP 0x0104 5555 ADDRESS "10.211.55.4" /end XCP_ON_UDP_IP + /begin XCP_ON_UDP_IP 0x0104 5555 ADDRESS "192.168.0.206" /end XCP_ON_UDP_IP /end IF_DATA /* TypeDefs */ +/begin TYPEDEF_MEASUREMENT _M_i "" UBYTE IDENTITY 0 0 0 255 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE __atomic_base_bool_ "" 1 + /begin STRUCTURE_COMPONENT _M_i _M_i 0 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE /begin TYPEDEF_STRUCTURE atomic_bool_ "" 1 + /begin STRUCTURE_COMPONENT _M_base __atomic_base_bool_ 0 /end STRUCTURE_COMPONENT /end TYPEDEF_STRUCTURE -/begin TYPEDEF_STRUCTURE __atomic_base_bool_ "" 1 +/begin TYPEDEF_MEASUREMENT a "" UWORD IDENTITY 0 0 0 65535 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT b "" SWORD IDENTITY 0 0 -32768 32767 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT f "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT d "" UBYTE IDENTITY 0 0 0 255 MATRIX_DIM 3 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE test_struct "" 12 + /begin STRUCTURE_COMPONENT a a 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT b b 2 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT f f 4 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT d d 8 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE +/begin TYPEDEF_MEASUREMENT test_class.a "" SWORD IDENTITY 0 0 -32768 32767 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT test_class.b "" UWORD IDENTITY 0 0 0 65535 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT test_class.f "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT test_class.d "" UWORD IDENTITY 0 0 0 65535 MATRIX_DIM 3 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE test_class "" 24 + /begin STRUCTURE_COMPONENT a test_class.a 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT b test_class.b 2 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT f test_class.f 8 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT d test_class.d 16 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE +/begin TYPEDEF_MEASUREMENT speed "" SLONG IDENTITY 0 0 -2147483648 2147483647 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE motor_control.Input "" 4 + /begin STRUCTURE_COMPONENT speed speed 0 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE +/begin TYPEDEF_MEASUREMENT flow "" SLONG IDENTITY 0 0 -2147483648 2147483647 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT pressure "" SLONG IDENTITY 0 0 -2147483648 2147483647 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE valve_control.Input "" 8 + /begin STRUCTURE_COMPONENT flow flow 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT pressure pressure 4 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE +/begin TYPEDEF_MEASUREMENT calseg_ "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT value_ "" UWORD IDENTITY 0 0 0 65535 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE CounterControl_unsigned_short__CounterCtlParamsTemplate_unsigned_short___ "" 16 + /begin STRUCTURE_COMPONENT calseg_ calseg_ 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT value_ value_ 8 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE +/begin TYPEDEF_CHARACTERISTIC max "" VALUE U32 0 IDENTITY 0 4294967295 /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC inc "" VALUE U32 0 IDENTITY 0 4294967295 /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC state "" VALUE U32 0 IDENTITY 0 4294967295 /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_STRUCTURE TaskCounterCtlParams "" 12 + /begin STRUCTURE_COMPONENT max max 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT inc inc 4 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT state state 8 /end STRUCTURE_COMPONENT +/end TYPEDEF_STRUCTURE +/begin TYPEDEF_MEASUREMENT indexp_ "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_MEASUREMENT default_params_ "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_STRUCTURE CalSegRef_const_CounterCtlParamsTemplate_unsigned_int___ "" 16 + /begin STRUCTURE_COMPONENT indexp_ indexp_ 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT default_params_ default_params_ 8 /end STRUCTURE_COMPONENT /end TYPEDEF_STRUCTURE /begin TYPEDEF_CHARACTERISTIC delay_us "" VALUE U32 0 IDENTITY 1 10000 PHYS_UNIT "us" /end TYPEDEF_CHARACTERISTIC /begin TYPEDEF_CHARACTERISTIC test_par_double "" VALUE F64 0 NO_COMPU_METHOD -100000000000000000000000000000000 100000000000000000000000000000000 /end TYPEDEF_CHARACTERISTIC @@ -207,102 +260,91 @@ EPK "109" ADDR_EPK 0x00030310 /begin STRUCTURE_COMPONENT test_par_uint8_array test_par_uint8_array 38 /end STRUCTURE_COMPONENT /begin STRUCTURE_COMPONENT test_par_struct test_par_struct 48 /end STRUCTURE_COMPONENT /end TYPEDEF_STRUCTURE -/begin TYPEDEF_MEASUREMENT a "" UWORD IDENTITY 0 0 0 65535 /end TYPEDEF_MEASUREMENT -/begin TYPEDEF_MEASUREMENT b "" SWORD IDENTITY 0 0 -32768 32767 /end TYPEDEF_MEASUREMENT -/begin TYPEDEF_MEASUREMENT f "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 /end TYPEDEF_MEASUREMENT -/begin TYPEDEF_MEASUREMENT d "" UBYTE IDENTITY 0 0 0 255 MATRIX_DIM 3 /end TYPEDEF_MEASUREMENT -/begin TYPEDEF_STRUCTURE test_struct "" 12 - /begin STRUCTURE_COMPONENT a a 0 /end STRUCTURE_COMPONENT - /begin STRUCTURE_COMPONENT b b 2 /end STRUCTURE_COMPONENT - /begin STRUCTURE_COMPONENT f f 4 /end STRUCTURE_COMPONENT - /begin STRUCTURE_COMPONENT d d 8 /end STRUCTURE_COMPONENT -/end TYPEDEF_STRUCTURE -/begin TYPEDEF_CHARACTERISTIC max "" VALUE U16 0 IDENTITY 0 65535 /end TYPEDEF_CHARACTERISTIC -/begin TYPEDEF_CHARACTERISTIC inc "" VALUE U16 0 IDENTITY 0 65535 /end TYPEDEF_CHARACTERISTIC -/begin TYPEDEF_CHARACTERISTIC state "" VALUE U32 0 IDENTITY 0 4294967295 /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC CounterCtlParams.max "" VALUE U16 0 IDENTITY 0 65535 /end TYPEDEF_CHARACTERISTIC +/begin TYPEDEF_CHARACTERISTIC CounterCtlParams.inc "" VALUE U16 0 IDENTITY 0 65535 /end TYPEDEF_CHARACTERISTIC /begin TYPEDEF_STRUCTURE CounterCtlParams "" 8 - /begin STRUCTURE_COMPONENT max max 0 /end STRUCTURE_COMPONENT - /begin STRUCTURE_COMPONENT inc inc 2 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT max CounterCtlParams.max 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT inc CounterCtlParams.inc 2 /end STRUCTURE_COMPONENT /begin STRUCTURE_COMPONENT state state 4 /end STRUCTURE_COMPONENT /end TYPEDEF_STRUCTURE -/begin TYPEDEF_STRUCTURE TaskCounterCtlParams "" 12 - /begin STRUCTURE_COMPONENT max max 0 /end STRUCTURE_COMPONENT - /begin STRUCTURE_COMPONENT inc inc 4 /end STRUCTURE_COMPONENT - /begin STRUCTURE_COMPONENT state state 8 /end STRUCTURE_COMPONENT +/begin TYPEDEF_STRUCTURE CalSegRef_const_CounterCtlParamsTemplate_unsigned_short___ "" 16 + /begin STRUCTURE_COMPONENT indexp_ indexp_ 0 /end STRUCTURE_COMPONENT + /begin STRUCTURE_COMPONENT default_params_ default_params_ 8 /end STRUCTURE_COMPONENT /end TYPEDEF_STRUCTURE /* Measurements */ -/* Measurements for event 'mainloop' */ -/begin MEASUREMENT main.counter "Local counter in main" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x810056 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 2 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT main.static_counter "Static local counter in main" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30364 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 2 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT main.xcp_epk_keep "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x810055 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 2 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/* Measurements for event 'foo' */ +/begin MEASUREMENT foo.static_counter "Static local measurement variable in function foo" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x303C4 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x1001C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x10018 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0xFFFC ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_int64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0xFFF0 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.counter "Local captured measurement variable in function foo" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x0 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x4 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x8 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x10 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x12 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x14 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT foo.test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x18 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE foo.test_struct "" test_struct 0x20 ECU_ADDRESS_EXTENSION 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT foo.test_array "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x2C ECU_ADDRESS_EXTENSION 3 MATRIX_DIM 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT /* Measurements for event 'task' */ -/begin INSTANCE gRun "" atomic_bool_ 0x30230 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE -/begin MEASUREMENT OPTION_PROJECT_NAME "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0xC030 ECU_ADDRESS_EXTENSION 1 MATRIX_DIM 16 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT OPTION_SERVER_ADDR "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0xC040 ECU_ADDRESS_EXTENSION 1 MATRIX_DIM 4 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT calseg_id_params "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30234 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_counter "Global measurement variable" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30362 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x3027E ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3027C ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x30278 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x30270 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x3026E ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x3026C ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0x30268 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_int64 "" A_INT64 IDENTITY 0 0 -9223372036854776000 9223372036854776000 ECU_ADDRESS 0x30260 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x30258 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x30250 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_bool "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x3024B ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT global_test_array "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30248 ECU_ADDRESS_EXTENSION 1 MATRIX_DIM 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin INSTANCE global_test_struct "" test_struct 0x30238 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE -/begin MEASUREMENT calseg_id_counter_ctl_params "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30236 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT task.counter "Local measurement variable in function task" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x1005C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT task.static_counter "Static local measurement variable in function task" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x30350 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT calseg_id_task_counter_ctl_params "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30232 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT gModuleAddrValid "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30378 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT XCPLITE__CASDD "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0xDE48 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT calseg_id_epk "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3028A ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT XcpServerReceiveThread.ctr "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x329C8 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT last_time "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x329D0 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT task.static_counter "Static local measurement variable in function task" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x303C8 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT calseg_id_task_counter_ctl_params "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x302CA /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE counter_ctl_task_calseg_handle "" CalSegRef_const_CounterCtlParamsTemplate_unsigned_int___ 0x2FD20 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 1 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE -/* Measurements for event 'foo' */ -/begin MEASUREMENT foo.counter "Local measurement variable in function foo" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x41006E ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.static_counter "Static local measurement variable in function foo" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30360 ECU_ADDRESS_EXTENSION 1 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x410068 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x410060 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x41005F ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x41005C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x410058 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x410050 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x41004F ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x41004C ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0x410048 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin MEASUREMENT foo.test_int64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x410040 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT -/begin INSTANCE foo.test_struct "" test_struct 0x410030 ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end INSTANCE -/begin MEASUREMENT test_array "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x30248 ECU_ADDRESS_EXTENSION 1 MATRIX_DIM 3 /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 1 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/* Measurements for event 'mainloop' */ +/begin INSTANCE gRun "" atomic_bool_ 0x30268 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT global_counter "Global measurement variable" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x303A2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_uint8 "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x3026E /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_uint16 "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x30270 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_uint32 "" ULONG IDENTITY 0 0 0 4294967295 ECU_ADDRESS 0x30274 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_uint64 "" A_UINT64 IDENTITY 0 0 0 18446744073709552000 ECU_ADDRESS 0x30278 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_int8 "" SBYTE IDENTITY 0 0 -128 127 ECU_ADDRESS 0x30280 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_int16 "" SWORD IDENTITY 0 0 -32768 32767 ECU_ADDRESS 0x30282 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_int32 "" SLONG IDENTITY 0 0 -2147483648 2147483647 ECU_ADDRESS 0x30284 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_int64 "" A_INT64 IDENTITY 0 0 -9223372036854776000 9223372036854776000 ECU_ADDRESS 0x30288 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_float "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x30290 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_double "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -100000000000000000000000000000000 100000000000000000000000000000000 ECU_ADDRESS 0x30298 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_bool "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x302A0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_test_array "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x302A1 MATRIX_DIM 3 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE global_test_struct "" test_struct 0x302A4 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin INSTANCE global_test_class "" test_class 0x302B0 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin INSTANCE motor_control.input "Motor control input" motor_control.Input 0x303A4 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin INSTANCE valve_control.input "Valve control input" valve_control.Input 0x303A8 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin INSTANCE counter_ctl "" CounterControl_unsigned_short__CounterCtlParamsTemplate_unsigned_short___ 0x303B0 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT bar.static_counter "Static local measurement variable in function bar, writable" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x303C0 READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT main.static_counter "Static local counter in main" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x303CC /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT main.xcp_epk_keep "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0x80FFFC ECU_ADDRESS_EXTENSION 2 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT calseg_id_params "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x3026C /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT calseg_id_counter_ctl_params "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x302C8 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin INSTANCE counter_ctl_calseg_handle "" CalSegRef_const_CounterCtlParamsTemplate_unsigned_short___ 0x2FD10 ECU_ADDRESS_EXTENSION 0 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end INSTANCE +/begin MEASUREMENT OPTION_PROJECT_NAME "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0xC860 MATRIX_DIM 16 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT OPTION_SERVER_ADDR "" UBYTE IDENTITY 0 0 0 255 ECU_ADDRESS 0xC870 MATRIX_DIM 4 /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT global_static_counter "" UWORD IDENTITY 0 0 0 65535 ECU_ADDRESS 0x303CE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 2 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT /* Measurements without fixed event */ -/begin GROUP Measurements "" ROOT /begin SUB_GROUP mainloop task foo /end SUB_GROUP /end GROUP -/begin GROUP mainloop "" /begin REF_MEASUREMENT main.counter main.static_counter main.xcp_epk_keep /end REF_MEASUREMENT /end GROUP -/begin GROUP task "" /begin REF_MEASUREMENT gRun OPTION_PROJECT_NAME OPTION_SERVER_ADDR calseg_id_params global_counter global_test_uint8 global_test_uint16 global_test_uint32 global_test_uint64 global_test_int8 global_test_int16 global_test_int32 global_test_int64 global_test_float global_test_double global_test_bool global_test_array global_test_struct calseg_id_counter_ctl_params task.counter task.static_counter calseg_id_task_counter_ctl_params gModuleAddrValid XCPLITE__CASDD calseg_id_epk XcpServerReceiveThread.ctr last_time /end REF_MEASUREMENT /end GROUP -/begin GROUP foo "" /begin REF_MEASUREMENT foo.counter foo.static_counter foo.test_float foo.test_double foo.test_uint8 foo.test_uint16 foo.test_uint32 foo.test_uint64 foo.test_int8 foo.test_int16 foo.test_int32 foo.test_int64 foo.test_struct test_array /end REF_MEASUREMENT /end GROUP +/begin GROUP Measurements "" ROOT /begin SUB_GROUP foo task mainloop /end SUB_GROUP /end GROUP +/begin GROUP foo "" /begin REF_MEASUREMENT foo.static_counter foo.test_int8 foo.test_int16 foo.test_int32 foo.test_int64 foo.counter foo.test_float foo.test_double foo.test_uint8 foo.test_uint16 foo.test_uint32 foo.test_uint64 foo.test_struct foo.test_array /end REF_MEASUREMENT /end GROUP +/begin GROUP task "" /begin REF_MEASUREMENT task.static_counter calseg_id_task_counter_ctl_params counter_ctl_task_calseg_handle /end REF_MEASUREMENT /end GROUP +/begin GROUP mainloop "" /begin REF_MEASUREMENT gRun global_counter global_test_uint8 global_test_uint16 global_test_uint32 global_test_uint64 global_test_int8 global_test_int16 global_test_int32 global_test_int64 global_test_float global_test_double global_test_bool global_test_array global_test_struct global_test_class motor_control.input valve_control.input counter_ctl bar.static_counter main.static_counter main.xcp_epk_keep calseg_id_params calseg_id_counter_ctl_params counter_ctl_calseg_handle OPTION_PROJECT_NAME OPTION_SERVER_ADDR global_static_counter /end REF_MEASUREMENT /end GROUP /* Axis */ /* Characteristics */ -/begin INSTANCE params "" params 0x80030000 /end INSTANCE -/begin INSTANCE counter_ctl_params "" CounterCtlParams 0x80020000 /end INSTANCE -/begin INSTANCE task_counter_ctl_params "" TaskCounterCtlParams 0x80010000 /end INSTANCE +/begin INSTANCE task_counter_ctl_params "" TaskCounterCtlParams 0xC854 /end INSTANCE +/begin INSTANCE params "" params 0xC808 /end INSTANCE +/begin INSTANCE counter_ctl_params "" CounterCtlParams 0xC848 /end INSTANCE /* Characteristic and Axis Groups */ -/begin GROUP Characteristics "" ROOT /begin SUB_GROUP task_counter_ctl_params counter_ctl_params params /end SUB_GROUP /end GROUP -/begin GROUP task_counter_ctl_params "" /begin REF_CHARACTERISTIC task_counter_ctl_params /end REF_CHARACTERISTIC /end GROUP -/begin GROUP counter_ctl_params "" /begin REF_CHARACTERISTIC counter_ctl_params /end REF_CHARACTERISTIC /end GROUP +/begin GROUP Characteristics "" ROOT /begin SUB_GROUP params counter_ctl_params task_counter_ctl_params /end SUB_GROUP /end GROUP /begin GROUP params "" /begin REF_CHARACTERISTIC params /end REF_CHARACTERISTIC /end GROUP +/begin GROUP counter_ctl_params "" /begin REF_CHARACTERISTIC counter_ctl_params /end REF_CHARACTERISTIC /end GROUP +/begin GROUP task_counter_ctl_params "" /begin REF_CHARACTERISTIC task_counter_ctl_params /end REF_CHARACTERISTIC /end GROUP /end MODULE /end PROJECT diff --git a/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.elf b/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.elf new file mode 100755 index 00000000..bb8ffe7a Binary files /dev/null and b/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.elf differ diff --git a/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.log b/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.log new file mode 100644 index 00000000..dbd7d77a --- /dev/null +++ b/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.log @@ -0,0 +1,119 @@ + +[INFO ] xcp_client - version 4.0.0 +[INFO ] Default event mainloop for variables without a fixed event +[INFO ] Using A2L file name from command line argument: /Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l +[INFO ] A2L path: /Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l +[INFO ] Generate A2L file /Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l with ELF/DWARF information only, offline mode +[INFO ] Reading ELF file: /Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.elf +[INFO ] Loading debug information from ELF file: /Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.elf +[INFO ] Compiler: Debian clang version 14.0.6 +[INFO ] =============================================================== +[INFO ] Target signature found: ACSDD +[INFO ] Using absolute addressing for calibration segments +[INFO ] =============================================================== +[INFO ] EPK segment memory section found at address = 0x00030390 +[INFO ] EPK string: 'V2.1.10' +[INFO ] =============================================================== +[INFO ] Registering event information: +[INFO ] =============================================================== +[INFO ] Found XCP event descriptor memory section at address = 0x00030360, size = 48 bytes +[INFO ] Event definition for event 'foo' found in main_cpp:foo, addr = 0x30360 +[INFO ] New event 'foo' found: event id = 0 +[INFO ] Event definition for event 'task' found in main_cpp:task, addr = 0x30370 +[INFO ] New event 'task' found: event id = 1 +[INFO ] Event definition for event 'mainloop' found in main_cpp:main, addr = 0x30380 +[INFO ] New event 'mainloop' found: event id = 2 +[INFO ] =============================================================== +[INFO ] Registering segment information (absolute addressing mode): +[INFO ] =============================================================== +[INFO ] Calibration segment 'epk' not yet defined in registry +[INFO ] Created segment 0: 'epk': addr = 0x00030390, size = 7, mem_addr = 0x00030390 +Calibration segment 'params' type information found, type=params, size = 64 +[INFO ] Calibration segment 'params' default page variable found in debug data: Address = 0xc808, Size = 0x40 +[INFO ] Calibration segment 'params' not yet defined in registry +[INFO ] Created segment 1: 'params': addr = 0x0000C808, size = 64, mem_addr = 0x0000C808 +Calibration segment 'counter_ctl_params' type information found, type=CounterCtlParams, size = 8 +[INFO ] Calibration segment 'counter_ctl_params' default page variable found in debug data: Address = 0xc848, Size = 0x8 +[INFO ] Calibration segment 'counter_ctl_params' not yet defined in registry +[INFO ] Created segment 2: 'counter_ctl_params': addr = 0x0000C848, size = 8, mem_addr = 0x0000C848 +Calibration segment 'task_counter_ctl_params' type information found, type=TaskCounterCtlParams, size = 12 +[INFO ] Calibration segment 'task_counter_ctl_params' default page variable found in debug data: Address = 0xc854, Size = 0xc +[INFO ] Calibration segment 'task_counter_ctl_params' not yet defined in registry +[INFO ] Created segment 3: 'task_counter_ctl_params': addr = 0x0000C854, size = 12, mem_addr = 0x0000C854 +[INFO ] =============================================================== +[INFO ] Registering event locations: +[INFO ] =============================================================== +[INFO ] Event foo trigger found in main_cpp:foo, address resolver mode AASR +[INFO ] Event task trigger found in main_cpp:task, address resolver mode AAS +[INFO ] Event mainloop trigger found in main_cpp:main, address resolver mode AAS +[INFO ] =============================================================== +[INFO ] Registering variables: +[INFO ] =============================================================== +[INFO ] Default event 'mainloop' (id 2) for global and static variables without an event trigger +[INFO ] Compilation unit filter: 'main' +[INFO ] Global variable 'gRun', event id = Some(2) +[INFO ] Global variable 'global_counter', event id = Some(2) +[INFO ] Global variable 'global_test_uint8', event id = Some(2) +[INFO ] Global variable 'global_test_uint16', event id = Some(2) +[INFO ] Global variable 'global_test_uint32', event id = Some(2) +[INFO ] Global variable 'global_test_uint64', event id = Some(2) +[INFO ] Global variable 'global_test_int8', event id = Some(2) +[INFO ] Global variable 'global_test_int16', event id = Some(2) +[INFO ] Global variable 'global_test_int32', event id = Some(2) +[INFO ] Global variable 'global_test_int64', event id = Some(2) +[INFO ] Global variable 'global_test_float', event id = Some(2) +[INFO ] Global variable 'global_test_double', event id = Some(2) +[INFO ] Global variable 'global_test_bool', event id = Some(2) +[INFO ] Global variable 'global_test_array', event id = Some(2) +[INFO ] Global variable 'global_test_struct', event id = Some(2) +[INFO ] Global variable 'global_test_class', event id = Some(2) +[INFO ] Global variable 'input', event id = Some(2) +[INFO ] Struct/class type 'Input' in main_cpp registered as typedef 'motor_control.Input', the type name is used in different scopes +[INFO ] Global variable 'input', event id = Some(2) +[INFO ] Struct/class type 'Input' in main_cpp registered as typedef 'valve_control.Input', the type name is used in different scopes +[INFO ] Global variable 'counter_ctl', event id = Some(2) +[INFO ] Static variable 'static_counter' local to function 'Some("bar")', no event found in this function, event id = Some(2) +[INFO ] Static variable 'static_counter' local to function 'Some("foo")', event id = 0 +[INFO ] Static variable 'static_counter' local to function 'Some("task")', event id = 1 +[INFO ] Static variable 'static_counter' local to function 'Some("main")', event id = 2 +[INFO ] Local variable 'test_int8' in function 'Some("foo")', event id = Some(0), offset = 28 +[INFO ] Local variable 'test_int16' in function 'Some("foo")', event id = Some(0), offset = 24 +[INFO ] Local variable 'test_int32' in function 'Some("foo")', event id = Some(0), offset = -4 +[INFO ] Local variable 'test_int64' in function 'Some("foo")', event id = Some(0), offset = -16 +[INFO ] Static variable 'task_counter_ctl_params' local to function 'Some("task")', event id = 1 +[INFO ] Static variable 'calseg_id_task_counter_ctl_params' local to function 'Some("task")', event id = 1 +[INFO ] Static variable 'counter_ctl_task_calseg_handle' local to function 'Some("task")', event id = 1 +[INFO ] Local variable 'xcp_epk_keep' in function 'Some("main")', event id = Some(2), offset = -4 +[INFO ] Global variable 'params', event id = Some(2) +[INFO ] Global variable 'calseg_id_params', event id = Some(2) +[INFO ] Global variable 'counter_ctl_params', event id = Some(2) +[INFO ] Global variable 'calseg_id_counter_ctl_params', event id = Some(2) +[INFO ] Global variable 'counter_ctl_calseg_handle', event id = Some(2) +[INFO ] Global variable 'OPTION_PROJECT_NAME', event id = Some(2) +[INFO ] Global variable 'OPTION_SERVER_ADDR', event id = Some(2) +[INFO ] Global variable 'global_static_counter', event id = Some(2) +[INFO ] =============================================================== +[INFO ] Registering captured variables: +[INFO ] =============================================================== +[INFO ] Capture of event 'foo' in function 'foo' with 9 variables +[INFO ] Captured variable 'counter' in function 'foo', event id = 0, offset = 0 +[INFO ] Captured variable 'test_float' in function 'foo', event id = 0, offset = 4 +[INFO ] Captured variable 'test_double' in function 'foo', event id = 0, offset = 8 +[INFO ] Captured variable 'test_uint8' in function 'foo', event id = 0, offset = 16 +[INFO ] Captured variable 'test_uint16' in function 'foo', event id = 0, offset = 18 +[INFO ] Captured variable 'test_uint32' in function 'foo', event id = 0, offset = 20 +[INFO ] Captured variable 'test_uint64' in function 'foo', event id = 0, offset = 24 +[INFO ] Captured variable 'test_struct' in function 'foo', event id = 0, offset = 32 +[INFO ] Captured variable 'test_array' in function 'foo', event id = 0, offset = 44 +[INFO ] =============================================================== +[INFO ] Registering metadata from xcp_meta section: +[INFO ] =============================================================== +[WARN ] Metadata 'xcp_meta__comment__task__counter': no matching registry entry for 'task.task.counter' or 'task.counter' +[WARN ] Metadata 'xcp_meta__comment__main__counter': no matching registry entry for 'main.main.counter' or 'main.counter' +[INFO ] =============================================================== +[INFO ] Write A2L file "/Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l" +[INFO ] A2L writer: transport layer: UDP 192.168.0.206:5555 +[INFO ] Check A2L file "/Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l" +[INFO ] A2L file check ok +[INFO ] Created A2L with file: /Users/rainer/git/XCPlite-RainerZ/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.a2l ELF/DWARF information only, offline mode +[INFO ] Default event 'mainloop' resolved to event id 2 diff --git a/examples/no_a2l_demo_cpp/README.md b/examples/no_a2l_demo_cpp/README.md index 94003ccb..e9893ad4 100644 --- a/examples/no_a2l_demo_cpp/README.md +++ b/examples/no_a2l_demo_cpp/README.md @@ -4,6 +4,7 @@ Demonstrates XCPlite usage in C++ without runtime on-target A2L database generat Requires xcpclient tool or manual A2L generation from ELF file. Please find more general information in [no_a2l_demo C version](../no_a2l_demo/README.md). +The offline A2L generation is described in [docs/OFFLINE_A2L.md](../../docs/OFFLINE_A2L.md). ## C++ Calibration Parameter Pattern Used @@ -16,6 +17,15 @@ This example demonstrates an idiomatic C++ pattern for calibration-aware compone The static-lifetime requirement is important: calibration segment default objects must remain valid for the full program lifetime. The static-lifetime default instance initializes the calibration segment, and its address is used as the A2L instance address. For const defaults, this instance typically resides in the rodata section. +## Types and Variables with the Same Name in Different Namespaces + +The namespaces `motor_control` and `valve_control` both define a struct `Input` and a global variable `input`. +The DWARF debug information only contains the unqualified names and A2L has one flat name space for typedefs, so the +ELF->A2L generator qualifies both with their namespace: the typedefs become `motor_control.Input` and `valve_control.Input`, +the instances `motor_control.input` and `valve_control.input`. Types and variables with a unique name keep their plain name. +The `XCP_COMMENT` annotations of both variables are placed inside the namespaces and use the plain name `input`, +the generator qualifies the annotation with the namespace it is placed in. + ## Build and Run ```bash diff --git a/examples/no_a2l_demo_cpp/create_a2l.sh b/examples/no_a2l_demo_cpp/create_a2l.sh index 2e3b9158..d1dc804e 100755 --- a/examples/no_a2l_demo_cpp/create_a2l.sh +++ b/examples/no_a2l_demo_cpp/create_a2l.sh @@ -8,10 +8,13 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # The script syncs the example project to the target, builds it there, runs it with XCP on Ethernet, # downloads the ELF file to the local machine and creates an A2L file. # Prerequisites: +# - The target machine must be Linux # - The target must be reachable via SSH and have rsync installed # - The local machine must have rsync and scp installed # - The local machine must have xcpclient installed +# A local build is possible on Linux only: executables built on macOS (Mach-O) contain no DWARF debug information, +# the xcpclient A2L generator can not create an A2L file from them #====================================================================================================================== # Parameters @@ -30,7 +33,6 @@ ELFFILE="$REPO_ROOT/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.elf" # Build type for target executable: Release, RelWithDebInfo or Debug # RelWithDebInfo is default to demonstrate operation with with -O1 and NDEBUG # Optimization level >= -O1 keeps variables in registers whenever possible, so local variables cannot be measured -# The most efficient solution to keep local variables measurable is to use the DaqCapture macro, another option is mto ark the variable as volatile (with the provided macro XCP_MEA # Debug mode is the least efficient but keeps all variables and stack frames intact BUILD_TYPE="RelWithDebInfo" # -O0 @@ -39,15 +41,17 @@ BUILD_TYPE="RelWithDebInfo" #BUILD_TYPE="Release" # Run a simple test calibration and measurement -TEST=true -#TEST=false +#TEST=true +TEST=false +# CSV measurement file path on local machine +CSVFILE="$REPO_ROOT/examples/no_a2l_demo_cpp/CANape/no_a2l_demo_cpp.csv" # Target connection details -TARGET_USER="parallels" -TARGET_HOST="10.211.55.4" -#TARGET_USER="rainer" -#TARGET_HOST="192.168.0.206" +#TARGET_USER="parallels" +#TARGET_HOST="10.211.55.4" +TARGET_USER="rainer" +TARGET_HOST="192.168.0.206" TARGET_PATH="~/XCPlite-Test" TARGET_BUILD_DIR="build-no_a2l" TARGET_BINARY="no_a2l_demo_cpp" @@ -94,8 +98,12 @@ fi # Build on target -echo "Build executable on Target ..." -ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && ./build.sh $BUILD_TYPE no_a2l examples" 1> /dev/null +# Always a clean build: if the target has no NTP its clock may skew +# Optionally force gnu or clang, default to clang which is the more demanding one +echo "Clean build executable on Target ..." +#ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && ./build.sh $BUILD_TYPE no_a2l examples clean" 1> /dev/null +#ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && CC=gcc CXX=g++ ./build.sh $BUILD_TYPE no_a2l examples clean" 1> /dev/null +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && CC=clang CXX=clang++ ./build.sh $BUILD_TYPE no_a2l examples clean" 1> /dev/null if [ $? -ne 0 ]; then echo "❌ FAILED: Build on target" exit 1 @@ -124,10 +132,14 @@ echo "========================================================================== echo "" # --log-level is program flow verbosity # --verbose is information detail level -echo "Command: $XCPCLIENT --log-level=3 --verbose=5 --dest-addr=$TARGET_HOST --udp --offline --elf \"$ELFFILE\" --create-a2l --a2l \"$A2LFILE\"" -$XCPCLIENT --log-level=3 --verbose=5 --dest-addr=$TARGET_HOST --udp --offline --elf "$ELFFILE" --create-a2l --a2l "$A2LFILE" >> "$LOGFILE" -if [ $? -ne 0 ]; then - echo "❌ FAILED: xcpclient returned error" +# Remove the A2L file of a previous run, so a failed generation can not leave a stale A2L file behind +rm -f "$A2LFILE" +XCPCLIENT_ARGS=(--log-level=3 --verbose=0 --dest-addr="$TARGET_HOST" --udp --offline --elf "$ELFFILE" --elf-unit-filter main --create-a2l --a2l "$A2LFILE" --default-event=mainloop) +echo "Command: $XCPCLIENT ${XCPCLIENT_ARGS[*]}" +"$XCPCLIENT" "${XCPCLIENT_ARGS[@]}" >> "$LOGFILE" +if [ $? -ne 0 ] || [ ! -f "$A2LFILE" ]; then + echo "❌ FAILED: xcpclient could not create the A2L file $A2LFILE, see $LOGFILE" + grep "\[ERROR\]" "$LOGFILE" exit 1 fi @@ -153,13 +165,14 @@ echo "Test connect" echo "========================================================================================================" read -p "Press any key to continue..." -n1 -s $XCPCLIENT --log-level=3 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --list-mea . --list-cal . +sleep 1 echo "========================================================================================================" echo "Test measurement" echo "========================================================================================================" read -p "Press any key to continue..." -n1 -s -$XCPCLIENT --log-level=2 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --mea counter --time 1 --verbose 2 - +$XCPCLIENT --log-level=3 --dest-addr=$TARGET_HOST:5555 --udp --a2l "$A2LFILE" --mea counter --time 3 --csv "$CSVFILE" +sleep 1 ssh "$TARGET_USER@$TARGET_HOST" "pkill -f no_a2l_demo_cpp" diff --git a/examples/no_a2l_demo_cpp/src/main.cpp b/examples/no_a2l_demo_cpp/src/main.cpp index 05c1de36..336e5705 100644 --- a/examples/no_a2l_demo_cpp/src/main.cpp +++ b/examples/no_a2l_demo_cpp/src/main.cpp @@ -105,10 +105,11 @@ XCP_UNIT(params__delay_us, "us"); */ //----------------------------------------------------------------------------------------------------- -// Demo global measurement values +// Demo global measurement variables // Global measurement variable uint16_t global_counter = 0; +static uint16_t global_static_counter = 0; // Meta data annotation as code // Modified in function foo, measuring it in main or task, is possible, but asynchronous and may give inconsistent results @@ -133,7 +134,9 @@ int64_t global_test_int64 = -64; float global_test_float = 0.4f; double global_test_double = 0.8; bool global_test_bool = true; + uint8_t global_test_array[3] = {1, 2, 3}; + struct test_struct { uint16_t a; int16_t b; @@ -142,6 +145,43 @@ struct test_struct { }; struct test_struct global_test_struct = {1, -2, 0.3f, {1, 2, 3}}; +class test_class { + public: + int16_t a; + uint16_t b; + double f; + uint16_t d[3]; +}; +class test_class global_test_class = {1, 2, 0.3, {1, 2, 3}}; + +//----------------------------------------------------------------------------------------------------- +// Demo namespaces with types and variables of the same name + +// Types with the same name in different namespaces (or nested in different classes) are common in larger code bases. +// A2L has one flat name space for typedefs. +// The ELF->A2L generator therefore qualifies the typedef names of such types with their namespace or enclosing class +// (motor_control.Input, valve_control.Input), types with a unique name keep their plain name. +// Global variables with the same name in different namespaces are qualified with their namespace as well (motor_control.input, valve_control.input). +// Metadata annotations (XCP_COMMENT, XCP_UNIT, ...) placed in the same namespace as the variable do not need the namespace prefix, +// the ELF->A2L generator qualifies the name with the namespace of the annotation itself. + +namespace motor_control { +struct Input { + int32_t speed; +}; +Input input = {0}; +XCP_COMMENT(input, "Motor control input"); +} // namespace motor_control + +namespace valve_control { +struct Input { + int32_t flow; + int32_t pressure; +}; +Input input = {0, 0}; +XCP_COMMENT(input, "Valve control input"); +} // namespace valve_control + //----------------------------------------------------------------------------------------------------- // Demo class @@ -171,11 +211,13 @@ template class CounterControl { if (value > cal->max || cal->state == RESET) { value = 0; } + value_ = value; } private: // The calibration segment handle is stored as a member variable, and is used to access the calibration parameters in a thread-safe and consistent manner const CalSegHandle &calseg_; + mutable CounterType value_; }; //----------------------------------------------------------------------------------------------------- @@ -202,33 +244,53 @@ CounterControl counter_ctl(counter_ctl_calseg_handle //----------------------------------------------------------------------------------------------------- // Demo functions -void foo() { +void bar() { + + XCP_COMMENT(bar__static_counter, "Static local measurement variable in function bar, writable"); + XCP_READ_WRITE(bar__static_counter); + volatile static uint16_t static_counter = 0; + + volatile uint16_t counter; + + static_counter++; + counter = static_counter; +} + +// Avoid inlining to be able to measure local variables +// xcpclient ELF->A2L does not support inlined function and silently drop them +XCP_NOINLINE void foo() { // Static local scope measurement variable - XCP_COMMENT(foo__static_counter, "Static local measurement variable in function foo"); // Example for meta data annotation as code + XCP_COMMENT(foo__static_counter, "Static local measurement variable in function foo"); static uint16_t static_counter = 0; // Operate the local static counters using the global counter ctl instance counter_ctl.step(static_counter); - // Local variables - // volatile to prevent compiler optimization - XCP_COMMENT(foo__counter, "Local measurement variable in function foo"); // Example for meta data annotation as code - volatile uint16_t counter = static_counter; - volatile float test_float = 0.001f * counter; - volatile double test_double = 0.001 * counter; - volatile uint8_t test_uint8 = 1; - volatile uint16_t test_uint16 = 2; - volatile uint32_t test_uint32 = 3; - volatile uint64_t test_uint64 = 4; - volatile int8_t test_int8 = -1; - volatile int16_t test_int16 = -2; - volatile int32_t test_int32 = -3; - volatile uint64_t test_int64 = 1; - volatile struct test_struct test_struct = {1, -2, 0.001f * counter, {1, 2, 3}}; + // Local measurement variable + XCP_COMMENT(foo__counter, "Local captured measurement variable in function foo"); + uint16_t counter = static_counter; + + // More local measurement variables + // Measured via capture, variables stay in their registers + float test_float = 0.001f * counter; + double test_double = 0.001 * counter; + uint8_t test_uint8 = 1; + uint16_t test_uint16 = 2; + uint32_t test_uint32 = 3; + uint64_t test_uint64 = 4; + struct test_struct test_struct = {1, -2, 0.001f * counter, {1, 2, 3}}; uint8_t test_array[3] = {1, 2, 3}; - DaqCreateAndTriggerEvent(foo); + // Measure via stack, register variables spilled to stack + XCP_MEAS int8_t test_int8 = -1; + XCP_MEAS int16_t test_int16 = -2; + XCP_MEAS int32_t test_int32 = -3; + XCP_MEAS uint64_t test_int64 = 1; + + bar(); + + DaqCreateAndTriggerEventCapture(foo, counter, test_float, test_double, test_uint8, test_uint16, test_uint32, test_uint64, test_struct, test_array); } //----------------------------------------------------------------------------------------------------- @@ -322,9 +384,15 @@ int main(int argc, char *argv[]) { while (gRun) { counter_ctl.step(global_counter); + counter_ctl.step(global_static_counter); counter_ctl.step(static_counter); counter_ctl.step(counter); + // Update the measurement variables of the demo namespaces + motor_control::input.speed = counter; + valve_control::input.flow = counter / 2; + valve_control::input.pressure = -counter; + // Demonstrate calibration thread safety and consistency (typical concern on 32 bit microctls) { // Lock the global calibration parameter block gCalSeg for safe access diff --git a/examples/udp_raw_demo/CANape/XCP_104.aml b/examples/udp_raw_demo/CANape/XCP_104.aml new file mode 100644 index 00000000..1aa8cf84 --- /dev/null +++ b/examples/udp_raw_demo/CANape/XCP_104.aml @@ -0,0 +1,1329 @@ +/begin A2ML + +/***********************************************************/ +/* */ +/* ASAP2 meta language for XCP protocol layer V1.4 */ +/* */ +/* File version description */ +/* --------------------------------------------------- */ +/* 1.4.0 initial version */ +/* 1.4.1 taggedstruct member */ +/* OPTIMISATION_TYPE_ODT_STRICT was added */ +/* */ +/* Datatypes: */ +/* */ +/* A2ML description */ +/* --------------------------------------------------- */ +/* uchar unsigned 8 Bit */ +/* char signed 8 Bit */ +/* uint unsigned integer 16 Bit */ +/* int signed integer 16 Bit */ +/* ulong unsigned integer 32 Bit */ +/* long signed integer 32 Bit */ +/* int64 signed integer 64 Bit */ +/* uint64 unsigned integer 64 Bit */ +/* float float point 32 Bit IEEE 754 */ +/* double float point 64 Bit IEEE 754 */ +/* */ +/***********************************************************/ + +/*************** start of PROTOCOL_LAYER *******************/ + +struct Protocol_Layer { /* At MODULE */ + + uint; /* XCP protocol layer version */ + /* "1.4" = 0x0104 */ + + uint; /* T1 [ms] */ + uint; /* T2 [ms] */ + uint; /* T3 [ms] */ + uint; /* T4 [ms] */ + uint; /* T5 [ms] */ + uint; /* T6 [ms] */ + uint; /* T7 [ms] */ + + uchar; /* MAX_CTO */ + uint; /* MAX_DTO default for DAQ and STIM */ + + enum { /* BYTE_ORDER */ + "BYTE_ORDER_MSB_LAST" = 0, + "BYTE_ORDER_MSB_FIRST" = 1 + }; + + enum { /* ADDRESS_GRANULARITY */ + "ADDRESS_GRANULARITY_BYTE" = 1, + "ADDRESS_GRANULARITY_WORD" = 2, + "ADDRESS_GRANULARITY_DWORD" = 4 + }; + + taggedstruct { /* optional */ + + ("OPTIONAL_CMD" enum { /* XCP-Code of optional level 0 commands */ + /* supported by the slave */ + "GET_COMM_MODE_INFO" = 0xFB, + "GET_ID" = 0xFA, + "SET_REQUEST" = 0xF9, + "GET_SEED" = 0xF8, + "UNLOCK" = 0xF7, + "SET_MTA" = 0xF6, + "UPLOAD" = 0xF5, + "SHORT_UPLOAD" = 0xF4, + "BUILD_CHECKSUM" = 0xF3, + "TRANSPORT_LAYER_CMD" = 0xF2, + "USER_CMD" = 0xF1, + "DOWNLOAD" = 0xF0, + "DOWNLOAD_NEXT" = 0xEF, + "DOWNLOAD_MAX" = 0xEE, + "SHORT_DOWNLOAD" = 0xED, + "MODIFY_BITS" = 0xEC, + "SET_CAL_PAGE" = 0xEB, + "GET_CAL_PAGE" = 0xEA, + "GET_PAG_PROCESSOR_INFO" = 0xE9, + "GET_SEGMENT_INFO" = 0xE8, + "GET_PAGE_INFO" = 0xE7, + "SET_SEGMENT_MODE" = 0xE6, + "GET_SEGMENT_MODE" = 0xE5, + "COPY_CAL_PAGE" = 0xE4, + "CLEAR_DAQ_LIST" = 0xE3, + "SET_DAQ_PTR" = 0xE2, + "WRITE_DAQ" = 0xE1, + "SET_DAQ_LIST_MODE" = 0xE0, + "GET_DAQ_LIST_MODE" = 0xDF, + "START_STOP_DAQ_LIST" = 0xDE, + "START_STOP_SYNCH" = 0xDD, + "GET_DAQ_CLOCK" = 0xDC, + "READ_DAQ" = 0xDB, + "GET_DAQ_PROCESSOR_INFO" = 0xDA, + "GET_DAQ_RESOLUTION_INFO" = 0xD9, + "GET_DAQ_LIST_INFO" = 0xD8, + "GET_DAQ_EVENT_INFO" = 0xD7, + "FREE_DAQ" = 0xD6, + "ALLOC_DAQ" = 0xD5, + "ALLOC_ODT" = 0xD4, + "ALLOC_ODT_ENTRY" = 0xD3, + "PROGRAM_START" = 0xD2, + "PROGRAM_CLEAR" = 0xD1, + "PROGRAM" = 0xD0, + "PROGRAM_RESET" = 0xCF, + "GET_PGM_PROCESSOR_INFO" = 0xCE, + "GET_SECTOR_INFO" = 0xCD, + "PROGRAM_PREPARE" = 0xCC, + "PROGRAM_FORMAT" = 0xCB, + "PROGRAM_NEXT" = 0xCA, + "PROGRAM_MAX" = 0xC9, + "PROGRAM_VERIFY" = 0xC8, + "WRITE_DAQ_MULTIPLE" = 0xC7, + "TIME_CORRELATION_PROPERTIES" = 0xC6, + "DTO_CTR_PROPERTIES" = 0xC5 + /* do not use 0xC0 command code here, it is reserved as command extension code */ + })*; + + ("OPTIONAL_LEVEL1_CMD" enum { /* XCP-Code of optional level 1 commands, starting with level 0 0xC0 as first byte */ + "GET_VERSION" = 0x00, + "SET_DAQ_PACKED_MODE" = 0x01, + "GET_DAQ_PACKED_MODE" = 0x02, + "SW_DBG_COMMAND_SPACE" = 0xFC, + "POD_COMMAND_SPACE" = 0xFD + /* 0xFE shall not be used */ + /* 0xFF reserved for optional level 2 command space extension */ + })*; + + "COMMUNICATION_MODE_SUPPORTED" taggedunion { /* optional modes supported */ + "BLOCK" taggedstruct { + "SLAVE"; /* Slave Block Mode supported */ + "MASTER" struct { /* Master Block Mode supported */ + uchar; /* MAX_BS */ + uchar; /* MIN_ST */ + }; + }; + "INTERLEAVED" uchar; /* QUEUE_SIZE */ + }; + + "SEED_AND_KEY_EXTERNAL_FUNCTION" char[256]; /* Name of the Seed&Key function */ + /* including file extension */ + /* without path */ + "MAX_DTO_STIM" uint; /* overrules MAX_DTO see above for STIM use case */ + + block "ECU_STATES" taggedstruct{ + + (block "STATE" struct{ + uchar; /* STATE_NUMBER */ + char[100]; /* STATE_NAME */ + taggedstruct { + "ECU_SWITCHED_TO_DEFAULT_PAGE"; + }; + enum { /* CAL/PAG RESOURCE */ + "NOT_ACTIVE" = 0, + "ACTIVE" = 1, + "GETTER_ONLY" = 2 /* Setter methods not allowed */ + }; + enum { /* DAQ RESOURCE */ + "NOT_ACTIVE" = 0, + "ACTIVE" = 1 + }; + enum { /* STIM RESOURCE */ + "NOT_ACTIVE" = 0, + "ACTIVE" = 1 + }; + enum { /* PGM RESOURCE */ + "NOT_ACTIVE" = 0, + "ACTIVE" = 1 + }; + + taggedstruct { + + (block "MEMORY_ACCESS" struct{ /* CAL/PAG AVAILABLE */ + uchar; /* SEGMENT_NUMBER */ + uchar; /* PAGE_NUMBER */ + enum { + "READ_ACCESS_NOT_ALLOWED" = 0, + "READ_ACCESS_ALLOWED" = 1 + }; + enum { + "WRITE_ACCESS_NOT_ALLOWED" = 0, + "WRITE_ACCESS_ALLOWED" = 1 + }; + })*; + + }; + + })*; + + }; + + }; + +}; + +/***************** end of PROTOCOL_LAYER *******************/ + + + +/********************* start of DAQ ************************/ + +struct Daq { /* DAQ supported, at MODULE */ + enum { /* DAQ_CONFIG_TYPE */ + "STATIC" = 0, + "DYNAMIC" = 1 + }; + + uint; /* MAX_DAQ */ + uint; /* MAX_EVENT_CHANNEL */ + uchar; /* MIN_DAQ */ + + enum { /* OPTIMISATION_TYPE */ + "OPTIMISATION_TYPE_DEFAULT" = 0, + "OPTIMISATION_TYPE_ODT_TYPE_16" = 1, + "OPTIMISATION_TYPE_ODT_TYPE_32" = 2, + "OPTIMISATION_TYPE_ODT_TYPE_64" = 3, + "OPTIMISATION_TYPE_ODT_TYPE_ALIGNMENT" = 4, + "OPTIMISATION_TYPE_MAX_ENTRY_SIZE" = 5 + }; + + enum { /* ADDRESS_EXTENSION */ + "ADDRESS_EXTENSION_FREE" = 0, + "ADDRESS_EXTENSION_ODT" = 1, + "ADDRESS_EXTENSION_DAQ" = 3 + }; + + enum { /* IDENTIFICATION_FIELD */ + "IDENTIFICATION_FIELD_TYPE_ABSOLUTE" = 0, + "IDENTIFICATION_FIELD_TYPE_RELATIVE_BYTE" = 1, + "IDENTIFICATION_FIELD_TYPE_RELATIVE_WORD" = 2, + "IDENTIFICATION_FIELD_TYPE_RELATIVE_WORD_ALIGNED" = 3 + }; + + enum { /* GRANULARITY_ODT_ENTRY_SIZE_DAQ */ + "GRANULARITY_ODT_ENTRY_SIZE_DAQ_BYTE" = 1, + "GRANULARITY_ODT_ENTRY_SIZE_DAQ_WORD" = 2, + "GRANULARITY_ODT_ENTRY_SIZE_DAQ_DWORD" = 4, + "GRANULARITY_ODT_ENTRY_SIZE_DAQ_DLONG" = 8 + }; + + uchar; /* MAX_ODT_ENTRY_SIZE_DAQ */ + + enum { /* OVERLOAD_INDICATION */ + "NO_OVERLOAD_INDICATION" = 0, + "OVERLOAD_INDICATION_PID" = 1, + "OVERLOAD_INDICATION_EVENT" = 2 + }; + + taggedstruct { /* optional */ + "DAQ_ALTERNATING_SUPPORTED" uint; /* Display_Event_Channel_Number */ + "PRESCALER_SUPPORTED"; + "RESUME_SUPPORTED"; + "STORE_DAQ_SUPPORTED"; + "DTO_CTR_FIELD_SUPPORTED"; + "OPTIMISATION_TYPE_ODT_STRICT"; /* strict mode shall only be used in combination with */ + /* OPTIMISATION_TYPE_ODT_TYPE_16 */ + /* OPTIMISATION_TYPE_ODT_TYPE_32 */ + /* OPTIMISATION_TYPE_ODT_TYPE_64 */ + + block "STIM" struct { /* STIM supported */ + + enum { /* GRANULARITY_ODT_ENTRY_SIZE_STIM */ + "GRANULARITY_ODT_ENTRY_SIZE_STIM_BYTE" = 1, + "GRANULARITY_ODT_ENTRY_SIZE_STIM_WORD" = 2, + "GRANULARITY_ODT_ENTRY_SIZE_STIM_DWORD" = 4, + "GRANULARITY_ODT_ENTRY_SIZE_STIM_DLONG" = 8 + }; + + uchar; /* MAX_ODT_ENTRY_SIZE_STIM */ + + taggedstruct { /* bitwise stimulation */ + "BIT_STIM_SUPPORTED"; + "MIN_ST_STIM" uchar; /* separation time between DTOs */ + /* time in units of 100 microseconds */ + }; + }; + + block "TIMESTAMP_SUPPORTED" struct { + uint; /* TIMESTAMP_TICKS */ + enum { /* TIMESTAMP_SIZE */ + "NO_TIME_STAMP" = 0, + "SIZE_BYTE" = 1, + "SIZE_WORD" = 2, + "SIZE_DWORD" = 4 + }; + enum { /* RESOLUTION OF TIMESTAMP */ + "UNIT_1NS" = 0, + "UNIT_10NS" = 1, + "UNIT_100NS" = 2, + "UNIT_1US" = 3, + "UNIT_10US" = 4, + "UNIT_100US" = 5, + "UNIT_1MS" = 6, + "UNIT_10MS" = 7, + "UNIT_100MS" = 8, + "UNIT_1S" = 9, + "UNIT_1PS" = 10, + "UNIT_10PS" = 11, + "UNIT_100PS" = 12 + }; + taggedstruct { + "TIMESTAMP_FIXED"; + }; + }; + + "PID_OFF_SUPPORTED"; + + /* Configuration Limits */ + "MAX_DAQ_TOTAL" uint; + "MAX_ODT_TOTAL" uint; + "MAX_ODT_DAQ_TOTAL" uint; + "MAX_ODT_STIM_TOTAL" uint; + "MAX_ODT_ENTRIES_TOTAL" uint; + "MAX_ODT_ENTRIES_DAQ_TOTAL" uint; + "MAX_ODT_ENTRIES_STIM_TOTAL" uint; + + "CPU_LOAD_MAX_TOTAL" float; + "CORE_LOAD_MAX_TOTAL" float; /* max load of all cores */ + + (block "CORE_LOAD_MAX" struct { + uint; /* CORE_NR: core reference number */ + float; /* CORE_LOAD_MAX: max load of core(CORE_NR) */ + })*; + + block "DAQ_MEMORY_CONSUMPTION" struct { + ulong; /* DAQ_MEMORY_LIMIT: in Elements[AG] */ + uint; /* DAQ_SIZE: number of elements[AG] per DAQ list */ + uint; /* ODT_SIZE: number of elements[AG] per ODT */ + uint; /* ODT_ENTRY_SIZE: number of elements[AG] per ODT_entry */ + uint; /* ODT_DAQ_BUFFER_ELEMENT_SIZE: number of */ + /* payload elements[AG]*factor = sizeof(send buffer)[AG] */ + uint; /* ODT_STIM_BUFFER_ELEMENT_SIZE: number of */ + /* payload elements[AG]*factor = sizeof(receive buffer)[AG] */ + taggedstruct { + block "BUFFER_RESERVE" struct { /* default for all EVENTs */ + uchar; /* ODT_DAQ_BUFFER_ELEMENT_RESERVE in % of */ + /* ODT_DAQ_BUFFER_ELEMENT_SIZE */ + uchar; /* ODT_STIM_BUFFER_ELEMENT_RESERVE in % of */ + /* ODT_STIM_BUFFER_ELEMENT_SIZE */ + }; + }; + }; + +/******************* start of DAQ_LIST *********************/ + + (block "DAQ_LIST" struct { /* DAQ_LIST */ + /* multiple possible */ + uint; /* DAQ_LIST_NUMBER */ + taggedstruct { /* optional */ + "DAQ_LIST_TYPE" enum { + "DAQ" = 1, /* DIRECTION = DAQ only */ + "STIM" = 2, /* DIRECTION = STIM only */ + "DAQ_STIM" = 3 /* both directions possible */ + /* but not simultaneously */ + }; + + "MAX_ODT" uchar; /* MAX_ODT */ + "MAX_ODT_ENTRIES" uchar; /* MAX_ODT_ENTRIES */ + + "FIRST_PID" uchar; /* FIRST_PID for this DAQ_LIST */ + "EVENT_FIXED" uint; /* this DAQ_LIST always */ + /* in this event */ + "DAQ_PACKED_MODE_SUPPORTED"; /* supports DAQ packed mode */ + + block "PREDEFINED" taggedstruct { /* predefined */ + /* not configurable DAQ_LIST */ + (block "ODT" struct { + uchar; /* ODT number */ + taggedstruct { + ("ODT_ENTRY" struct { + uchar; /* ODT_ENTRY number */ + ulong; /* address of element */ + uchar; /* address extension of element */ + uchar; /* size of element [AG] */ + uchar; /* BIT_OFFSET */ + })*; + }; /* end of ODT_ENTRY */ + })*; /* end of ODT */ + }; /* end of PREDEFINED */ + }; + })*; + +/******************* end of DAQ_LIST ***********************/ + +/******************* start of EVENT ************************/ + + (block "EVENT" struct { /* EVENT */ + /* multiple possible */ + char[101]; /* EVENT_CHANNEL_NAME */ + char[9]; /* EVENT_CHANNEL_SHORT_NAME */ + uint; /* EVENT_CHANNEL_NUMBER */ + + enum { + "DAQ" = 1, /* only DAQ_LISTs */ + /* with DIRECTION = DAQ */ + "STIM" = 2, /* only DAQ_LISTs */ + /* with DIRECTION = STIM */ + "DAQ_STIM" = 3 /* both kind of DAQ_LISTs */ + }; + + uchar; /* MAX_DAQ_LIST */ + uchar; /* EVENT_CHANNEL_TIME_CYCLE */ + uchar; /* EVENT_CHANNEL_TIME_UNIT */ + uchar; /* EVENT_CHANNEL_PRIORITY */ + taggedstruct { /* optional */ + + "COMPLEMENTARY_BYPASS_EVENT_CHANNEL_NUMBER" uint; /* for compatibility reasons */ + /* not to be considered, if 1.3 Bypassing features are implemented */ + "CONSISTENCY" enum { + "DAQ" = 0, + "EVENT" = 1, + "ODT" = 2, + "NONE" = 3 + }; + + "EVENT_COUNTER_PRESENT"; + "RELATED_EVENT_CHANNEL_NUMBER" uint; + "RELATED_EVENT_CHANNEL_NUMBER_FIXED"; /* RELATED_EVENT_CHANNEL_NUMBER can not be modified. */ + "DTO_CTR_DAQ_MODE" enum { /* When inserting the DTO CTR field: */ + "INSERT_COUNTER" = 0, /* - use CTR of the related event channel */ + "INSERT_STIM_COUNTER_COPY" = 1 /* - use STIM CTR CPY of the related event channel */ + }; + "DTO_CTR_DAQ_MODE_FIXED"; /* DTO_CTR_DAQ_MODE properties can not be modified. */ + "DTO_CTR_STIM_MODE" enum { /* When receiving DTOs with CTR field: */ + "DO_NOT_CHECK_COUNTER" = 0, /* - do not check CTR */ + "CHECK_COUNTER" = 1 /* - check CTR */ + }; + "DTO_CTR_STIM_MODE_FIXED"; /* DTO_CTR_STIM_MODE properties can not be modified */ + "STIM_DTO_CTR_COPY_PRESENT"; /* DTO CTR can be saved for later reference */ + + block "DAQ_PACKED_MODE" struct { /* DAQ packed mode, applies for all associated DAQ lists */ + enum { /* El. A,B,C,D, 3 samples */ + "ELEMENT_GROUPED" = 1, /* A0A1A2B0B1B2C0C1C2D0D1D2 */ + "EVENT_GROUPED" = 2 /* A0B0C0D0A1B1C1D1A2B2C2D2 */ + }; + + enum { /* timestamp mode */ + "STS_LAST" = 0, /* single timestamp of last sample */ + "STS_FIRST" = 1 /* single timestamp of first sample */ + }; + + enum { /* usage */ + "OPTIONAL" = 0, /* optional, EVENT allows also non-packed mode */ + "MANDATORY" = 1 /* mandatory, only packed mode allowed */ + }; + + uint; /* DAQ packed mode sample count */ + taggedstruct { + ("ALT_SAMPLE_COUNT" uint)*; /* other valid sample count values (optional) */ + }; + }; + + block "MIN_CYCLE_TIME" struct { /* Configuration with 0-0 not allowed */ + uchar; /* EVENT_CHANNEL_TIME_CYCLE */ + uchar; /* EVENT_CHANNEL_TIME_UNIT */ + }; + block "BUFFER_RESERVE_EVENT" struct { + /* overrules default BUFFER_RESERVE for this EVENT */ + uchar; /* ODT_DAQ_BUFFER_ELEMENT_RESERVE in % of ODT_DAQ_BUFFER_ELEMENT_SIZE */ + uchar; /* ODT_STIM_BUFFER_ELEMENT_RESERVE in % of ODT_STIM_BUFFER_ELEMENT_SIZE */ + }; + + "CPU_LOAD_MAX" float; + + block "CPU_LOAD_CONSUMPTION_DAQ" struct { + float; /* DAQ_FACTOR */ + float; /* ODT_FACTOR */ + float; /* ODT_ENTRY_FACTOR */ + taggedstruct { + (block "ODT_ENTRY_SIZE_FACTOR_TABLE" struct{ + uint; /* SIZE */ + float; /* SIZE_FACTOR */ + })*; + block "CORE_LOAD_EP" struct { + uint; /* CORE_NR: core reference number */ + float; /* CORE_LOAD_EP_MAX: max load of this event part */ + }; + }; + }; + + block "CPU_LOAD_CONSUMPTION_STIM" struct { + float; /* DAQ_FACTOR */ + float; /* ODT_FACTOR */ + float; /* ODT_ENTRY_FACTOR */ + taggedstruct { + (block "ODT_ENTRY_SIZE_FACTOR_TABLE" struct{ + uint; /* SIZE */ + float; /* SIZE_FACTOR */ + })*; + block "CORE_LOAD_EP" struct { + uint; /* CORE_NR: core reference number */ + float; /* CORE_LOAD_EP_MAX: max load of this event part */ + }; + }; + }; + + block "CPU_LOAD_CONSUMPTION_QUEUE" struct { + /* default for DAQ and STIM QUEUE */ + float; /* ODT_FACTOR */ + float; /* ODT_ELEMENT_LOAD: length in elements[AG] */ + taggedstruct { + block "CORE_LOAD_EP" struct { + uint; /* CORE_NR: core reference number */ + float; /* CORE_LOAD_EP_MAX: max load of this event part */ + }; + }; + }; + + block "CPU_LOAD_CONSUMPTION_QUEUE_STIM" struct { + /* overrules CPU_LOAD_CONSUMPTION_QUEUE for STIM QUEUE */ + float; /* ODT_FACTOR */ + float; /* ODT_ELEMENT_LOAD: length in elements[AG] */ + taggedstruct { + block "CORE_LOAD_EP" struct { + uint; /* CORE_NR: core reference number */ + float; /* CORE_LOAD_EP_MAX: max load of this event part */ + }; + }; + }; + }; + })*; + +/********************* end of EVENT ************************/ + + }; /* end of optional at DAQ */ + +}; + +/********************* end of DAQ **************************/ + + +/***************** start of DAQ_EVENT **********************/ + +taggedunion Daq_Event { /* at MEASUREMENT */ + "FIXED_EVENT_LIST" taggedstruct { + ("EVENT" uint)*; + }; + "VARIABLE" taggedstruct { + block "AVAILABLE_EVENT_LIST" taggedstruct { + ("EVENT" uint)*; + }; + block "DEFAULT_EVENT_LIST" taggedstruct { + ("EVENT" uint)*; + }; + block "CONSISTENCY_EVENT_LIST" taggedstruct { + ("EVENT" uint)*; + }; + }; +}; + +/******************** end of DAQ_EVENT *********************/ + + +/********************** start of PAG ***********************/ + +struct Pag { /* PAG supported, at MODULE */ + uchar; /* MAX_SEGMENTS */ + taggedstruct { /* optional */ + "FREEZE_SUPPORTED"; + }; + +}; + +/*********************** end of PAG ************************/ + + +/********************** start of PGM ***********************/ + +struct Pgm { /* PGM supported, at MODULE */ + + enum { + "PGM_MODE_ABSOLUTE" = 1, + "PGM_MODE_FUNCTIONAL" = 2, + "PGM_MODE_ABSOLUTE_AND_FUNCTIONAL" = 3 + }; + uchar; /* MAX_SECTORS */ + uchar; /* MAX_CTO_PGM */ + + taggedstruct { /* optional */ + (block "SECTOR" struct { /* SECTOR */ + /* multiple possible */ + char[101]; /* SECTOR_NAME */ + uchar; /* SECTOR_NUMBER */ + ulong; /* Address */ + ulong; /* Length */ + uchar; /* CLEAR_SEQUENCE_NUMBER */ + uchar; /* PROGRAM_SEQUENCE_NUMBER */ + uchar; /* PROGRAM_METHOD */ + })*; /* end of SECTOR */ + + "COMMUNICATION_MODE_SUPPORTED" taggedunion { /* optional modes supported */ + "BLOCK" taggedstruct { + "SLAVE"; /* Slave Block Mode supported */ + "MASTER" struct { /* Master Block Mode supported */ + uchar; /* MAX_BS_PGM */ + uchar; /* MIN_ST_PGM */ + }; + }; + "INTERLEAVED" uchar; /* QUEUE_SIZE_PGM */ + }; + }; +}; + +/*********************** end of PGM ************************/ + + +/******************** start of SEGMENT *********************/ + +struct Segment { /* at MEMORY_SEGMENT */ + uchar; /* SEGMENT_NUMBER */ + uchar; /* number of pages */ + uchar; /* ADDRESS_EXTENSION */ + uchar; /* COMPRESSION_METHOD */ + uchar; /* ENCRYPTION_METHOD */ + + taggedstruct { /* optional */ + block "CHECKSUM" struct { + enum { /* checksum type */ + "XCP_ADD_11" = 1, + "XCP_ADD_12" = 2, + "XCP_ADD_14" = 3, + "XCP_ADD_22" = 4, + "XCP_ADD_24" = 5, + "XCP_ADD_44" = 6, + "XCP_CRC_16" = 7, + "XCP_CRC_16_CITT" = 8, + "XCP_CRC_32" = 9, + "XCP_USER_DEFINED" = 255 + }; + + taggedstruct { + "MAX_BLOCK_SIZE" ulong; /* maximum block size */ + /* for checksum calculation */ + "EXTERNAL_FUNCTION" char[256]; /* Name of the Checksum function */ + /* including file extension */ + /* without path */ + "MTA_BLOCK_SIZE_ALIGN" uint; /* required alignment of MTA and block size */ + }; + }; + + "DEFAULT_PAGE_NUMBER" uchar; /* Number of the default page */ + + (block "PAGE" struct { /* PAGES for this SEGMENT */ + /* multiple possible */ + uchar; /* PAGE_NUMBER */ + + enum { /* ECU_ACCESS_TYPE */ + "ECU_ACCESS_NOT_ALLOWED" = 0, + "ECU_ACCESS_WITHOUT_XCP_ONLY" = 1, + "ECU_ACCESS_WITH_XCP_ONLY" = 2, + "ECU_ACCESS_DONT_CARE" = 3 + }; + + enum { /* XCP_READ_ACCESS_TYPE */ + "XCP_READ_ACCESS_NOT_ALLOWED" = 0, + "XCP_READ_ACCESS_WITHOUT_ECU_ONLY" = 1, + "XCP_READ_ACCESS_WITH_ECU_ONLY" = 2, + "XCP_READ_ACCESS_DONT_CARE" = 3 + }; + + enum { /* XCP_WRITE_ACCESS_TYPE */ + "XCP_WRITE_ACCESS_NOT_ALLOWED" = 0, + "XCP_WRITE_ACCESS_WITHOUT_ECU_ONLY" = 1, + "XCP_WRITE_ACCESS_WITH_ECU_ONLY" = 2, + "XCP_WRITE_ACCESS_DONT_CARE" = 3 + }; + taggedstruct { + "INIT_SEGMENT" uchar; /* references segment that initialises this page */ + }; + + })*; /* end of PAGE */ + + (block "ADDRESS_MAPPING" struct { /* multiple possible */ + ulong; /* source address */ + ulong; /* destination address */ + ulong; /* length */ + })*; + + "PGM_VERIFY" ulong; /* verification value for PGM */ + }; /* end of optional */ + +}; + +/********************** end of SEGMENT *********************/ + + +/***************** start of TIME_CORRELATION ***************/ +taggedstruct Time_Correlation { + +/***********************************************************/ +/* XCP_SLAVE_CLOCK and ECU_CLOCK need not */ +/* necessarily be the same clock, i.e. in case of */ +/* an external XCP Slave, these clocks might differ */ +/***********************************************************/ + + "DAQ_TIMESTAMPS_RELATE_TO" enum { + "XCP_SLAVE_CLOCK" = 0, + "ECU_CLOCK" = 1 + }; + + (block "CLOCK" struct { + char; /* globally unique clock identifier (UUID/EUI), 1st octet (most significant byte) */ + char; /* globally unique clock identifier (UUID/EUI), 2nd octet */ + char; /* globally unique clock identifier (UUID/EUI), 3rd octet */ + char; /* globally unique clock identifier (UUID/EUI), 4th octet */ + char; /* globally unique clock identifier (UUID/EUI), 5th octet */ + char; /* globally unique clock identifier (UUID/EUI), 6th octet */ + char; /* globally unique clock identifier (UUID/EUI), 7th octet */ + char; /* globally unique clock identifier (UUID/EUI), 8th octet (least significant byte) */ + + enum { /* clock enumerator */ + "XCP_SLAVE_CLOCK" = 0, + "ECU_CLOCK" = 1, + "XCP_SLAVE_GRANDMASTER_CLOCK" = 2, /* related to XCP_SLAVE_CLOCK */ + "ECU_GRANDMASTER_CLOCK" = 3 /* related to ECU_CLOCK in case of an external slave */ + }; + enum { /* readability */ + "RANDOMLY_READABLE" = 0, + "LIMITED_READABLE" = 1, + "NOT_READABLE" = 2 + }; + enum { /* synchronization features */ + "SYN_UNSUPPORTED" = 0, /* clock neither supports synchronization */ + /* nor syntonization */ + "SYNCHRONIZATION_ONLY" = 1, /* clock only supports synchronization to */ + /* external grandmaster clock */ + "SYNTONIZATION_ONLY" = 2, /* clock only supports syntonization to */ + /* external grandmaster clock */ + "SYN_ALL" = 3 /* clock supports synchronization as well */ + /* as syntonization to external grandmaster clock */ + }; + uchar; /* clock quality, stratum level */ + + taggedstruct { + block "TIMESTAMP_CHARACTERIZATION" struct { + uint; /* TIMESTAMP_TICKS */ + enum { /* RESOLUTION OF TIMESTAMP */ + "UNIT_1NS" = 0, + "UNIT_10NS" = 1, + "UNIT_100NS" = 2, + "UNIT_1US" = 3, + "UNIT_10US" = 4, + "UNIT_100US" = 5, + "UNIT_1MS" = 6, + "UNIT_10MS" = 7, + "UNIT_100MS" = 8, + "UNIT_1S" = 9, + "UNIT_1PS" = 10, + "UNIT_10PS" = 11, + "UNIT_100PS" = 12 + }; + enum { /* NATIVE TIMESTAMP SIZE */ + "SIZE_FOUR_BYTE" = 4, + "SIZE_EIGHT_BYTE" = 8 + }; + }; + }; + + uint64; /* MAX_TIMESTAMP_VALUE_BEFORE_WRAP_AROUND */ + enum { /* epoch */ + "ATOMIC_TIME" = 0, /* TAI */ + "UNIVERSAL_COORDINATED_TIME" = 1, /* UTC */ + "ARBITRARY" = 2 /* unknown */ + }; + })*; +}; +/***************** end of TIME_CORRELATION ****************/ +/***************** start of Common Parameters **************/ + +taggedstruct Common_Parameters { + + block "PROTOCOL_LAYER" struct Protocol_Layer; + block "TIME_CORRELATION" taggedstruct Time_Correlation; + + block "SEGMENT" struct Segment; + + block "DAQ" struct Daq; + block "PAG" struct Pag; + block "PGM" struct Pgm; + + block "DAQ_EVENT" taggedunion Daq_Event; + +}; + +/****************** end of Common Parameters ***************/ + +/************************ start of CAN *********************/ + +struct CAN_Parameters { /* At MODULE */ + uint; /* XCP on CAN version */ + /* "1.4" = 0x0104 */ + taggedstruct { /* optional */ + "CAN_ID_BROADCAST" ulong; /* Auto detection CAN-ID */ + /* master -> slaves */ + /* Bit31= 1: extended identifier */ + /* Bit30= 1: CAN-FD identifier */ + "CAN_ID_MASTER" ulong; /* CMD/STIM CAN-ID */ + /* master -> slave */ + /* Bit31= 1: extended identifier */ + /* Bit30= 1: CAN-FD identifier */ + "CAN_ID_MASTER_INCREMENTAL"; /* master uses range of CAN-IDs */ + /* start of range = CAN_ID_MASTER */ + /* end of range = CAN_ID_MASTER+MAX_BS(_PGM)-1 */ + "CAN_ID_SLAVE" ulong; /* RES/ERR/EV/SERV/DAQ CAN-ID */ + /* slave -> master */ + /* Bit31= 1: extended identifier */ + /* Bit30= 1: CAN-FD identifier */ + "CAN_ID_GET_DAQ_CLOCK_MULTICAST" ulong; /* Only to be used for GET_DAQ_CLOCK_MULTICAST */ + /* master -> slaves */ + /* Bit31= 1: extended identifier */ + /* Bit30= 1: CAN-FD identifier */ + "BAUDRATE" ulong; /* BAUDRATE [Hz] */ + "SAMPLE_POINT" uchar; /* sample point */ + /* [% complete bit time] */ + "SAMPLE_RATE" enum { + "SINGLE" = 1, /* 1 sample per bit */ + "TRIPLE" = 3 /* 3 samples per bit */ + }; + "BTL_CYCLES" uchar; /* BTL_CYCLES */ + /* [slots per bit time] */ + "SJW" uchar; /* length synchr. segment */ + /* [BTL_CYCLES] */ + "SYNC_EDGE" enum { + "SINGLE" = 1, /* on falling edge only */ + "DUAL" = 2 /* on falling and rising edge */ + }; + "MAX_DLC_REQUIRED"; /* master to slave frames */ + /* always to have DLC = MAX_DLC = 8 */ + + (block "DAQ_LIST_CAN_ID" struct { /* At IF_DATA DAQ */ + uint; /* reference to DAQ_LIST_NUMBER */ + taggedstruct { /* exclusive tags */ + /* either VARIABLE or FIXED */ + "VARIABLE"; + "FIXED" ulong; /* this DAQ_LIST always */ + /* on this CAN_ID */ + }; + + })*; + (block "EVENT_CAN_ID_LIST" struct { /* At IF_DATA DAQ */ + uint; /* reference to EVENT_NUMBER */ + taggedstruct { /* exclusive tags */ + ("FIXED" ulong)*; /* this Event always on this ID */ + }; + })*; + + "MAX_BUS_LOAD" ulong; /* maximum available bus */ + /* load in percent */ + + "MEASUREMENT_SPLIT_ALLOWED"; /* Supports splitting of measurements to increase payload for MAX_DTO <= 8 */ + + block "CAN_FD" struct { /* The CAN_FD block definition indicates the use of CAN-FD frames */ + taggedstruct { + + "MAX_DLC" uint; /* 8, 12, 16, 20, 24, 32, 48 or 64 */ + "CAN_FD_DATA_TRANSFER_BAUDRATE" ulong; /* BAUDRATE [Hz] */ + + "SAMPLE_POINT" uchar; /* sample point receiver */ + /* [% complete bit time] */ + + "BTL_CYCLES" uchar; /* BTL_CYCLES */ + /* [slots per bit time] */ + "SJW" uchar; /* length synchr. segment */ + /* [BTL_CYCLES] */ + "SYNC_EDGE" enum { + "SINGLE" = 1, /* on falling edge only */ + "DUAL" = 2 /* on falling and rising edge */ + }; + + "MAX_DLC_REQUIRED"; /* master to slave frames */ + /* always to have DLC = MAX_DLC_for CAN-FD */ + + "SECONDARY_SAMPLE_POINT" uchar; /* sender sample point */ + /* [% complete bit time] */ + "TRANSCEIVER_DELAY_COMPENSATION" enum { + "OFF" = 0, + "ON" = 1 + }; + }; + }; + }; + + taggedstruct { + ("OPTIONAL_TL_SUBCMD" enum { /* XCP-Code of optional transport layer */ + /* specific subcommand supported by the slave */ + "GET_SLAVE_ID" = 0xFF, + "GET_DAQ_ID" = 0xFE, + "SET_DAQ_ID" = 0xFD, + "GET_DAQ_CLOCK_MULTICAST" = 0xFA + })*; + }; +}; + +/************************* end of CAN **********************/ + +/********************** start of SxI ***********************/ + +struct SxI_Parameters { /* At MODULE */ + uint; /* XCP on SxI version */ + /* "1.4" = 0x0104 */ + ulong; /* BAUDRATE [Hz] */ + taggedstruct { /* exclusive tags */ + "ASYNCH_FULL_DUPLEX_MODE" struct { + enum { + "PARITY_NONE" = 0, + "PARITY_ODD" = 1, + "PARITY_EVEN" = 2 + }; + enum { + "ONE_STOP_BIT" = 1, + "TWO_STOP_BITS" = 2 + }; + taggedstruct { + block "FRAMING" struct { + uchar; /* SYNC */ + uchar; /* ESC */ + }; + }; + }; + "SYNCH_FULL_DUPLEX_MODE_BYTE"; + "SYNCH_FULL_DUPLEX_MODE_WORD"; + "SYNCH_FULL_DUPLEX_MODE_DWORD"; + "SYNCH_MASTER_SLAVE_MODE_BYTE"; + "SYNCH_MASTER_SLAVE_MODE_WORD"; + "SYNCH_MASTER_SLAVE_MODE_DWORD"; + }; + enum { + "HEADER_LEN_BYTE" = 0, + "HEADER_LEN_CTR_BYTE" = 1, + "HEADER_LEN_FILL_BYTE" = 2, + "HEADER_LEN_WORD" = 3, + "HEADER_LEN_CTR_WORD" = 4, + "HEADER_LEN_FILL_WORD" = 5 + }; + enum { + "NO_CHECKSUM" = 0, + "CHECKSUM_BYTE" = 1, + "CHECKSUM_WORD" = 2 + }; + +}; + +/*************************** end of SxI ********************/ + + +/************************ start of TCP_IP ******************/ + +struct TCP_IP_Parameters { + + uint; /* XCP on TCP_IP version */ + /* "1.4" = 0x0104 */ + uint; /* PORT */ + + taggedunion { + "HOST_NAME" char[256]; + "ADDRESS" char[15]; + "IPV6" char[39]; + }; + taggedstruct{ + "MAX_BUS_LOAD" ulong; /* maximum available bus */ + /* load in percent */ + "MAX_BIT_RATE" ulong; /* Network speed which is */ + /* the base for MAX_BUS_LOAD in Mbit */ + }; + + taggedstruct{ + "PACKET_ALIGNMENT" enum { + "PACKET_ALIGNMENT_8" = 0, /* This is the default if the keyword is missing */ + "PACKET_ALIGNMENT_16" = 1, + "PACKET_ALIGNMENT_32" = 2 + }; + }; + + taggedstruct{ + ("OPTIONAL_TL_SUBCMD" enum { /* XCP-Code of optional transport layer */ + /* specific subcommand supported by the slave */ + "GET_SLAVE_ID" = 0xFF, + "GET_SLAVE_ID_EXTENDED" = 0xFD, + "SET_SLAVE_IP_ADDRESS" = 0xFC, + "GET_DAQ_CLOCK_MULTICAST" = 0xFA + })*; + }; +}; + +/************************* end of TCP_IP *******************/ + +/************************ start of UDP_IP ******************/ + +struct UDP_IP_Parameters { + + uint; /* XCP on UDP_IP version */ + /* "1.4" = 0x0104 */ + uint; /* PORT */ + + taggedunion { + "HOST_NAME" char[256]; + "ADDRESS" char[15]; + "IPV6" char[39]; + }; + taggedstruct{ + "MAX_BUS_LOAD" ulong; /* maximum available bus */ + /* load in percent */ + "MAX_BIT_RATE" ulong; /* Network speed which is */ + /* the base for MAX_BUS_LOAD in Mbit */ + }; + + taggedstruct{ + "PACKET_ALIGNMENT" enum { + "PACKET_ALIGNMENT_8" = 0, /* This is the default if the keyword is missing */ + "PACKET_ALIGNMENT_16" = 1, + "PACKET_ALIGNMENT_32" = 2 + }; + }; + + taggedstruct{ + ("OPTIONAL_TL_SUBCMD" enum { /* XCP-Code of optional transport layer */ + /* specific subcommand supported by the slave */ + "GET_SLAVE_ID" = 0xFF, + "GET_SLAVE_ID_EXTENDED" = 0xFD, + "SET_SLAVE_IP_ADDRESS" = 0xFC, + "GET_DAQ_CLOCK_MULTICAST" = 0xFA + })*; + }; +}; + +/*************************** end of UDP_IP *****************/ + +/************************ start of USB *********************/ + +struct ep_parameters { + uchar; /* ENDPOINT_NUMBER, not endpoint address */ + enum { + "BULK_TRANSFER" = 2, /* Numbers according to USB spec. */ + "INTERRUPT_TRANSFER" = 3 + }; + uint; /* wMaxPacketSize: Maximum packet */ + /* size of endpoint in bytes */ + uchar; /* bInterval: polling of endpoint */ + enum { /* Packing of XCP Messages */ + "MESSAGE_PACKING_SINGLE" = 0, /* Single per USB data packet */ + "MESSAGE_PACKING_MULTIPLE" = 1, /* Multiple per USB data packet */ + "MESSAGE_PACKING_STREAMING" = 2 /* No restriction by packet sizes */ + }; + enum { /* Alignment mandatory for all */ + "ALIGNMENT_8_BIT" = 0, /* packing types */ + "ALIGNMENT_16_BIT"= 1, + "ALIGNMENT_32_BIT"= 2, + "ALIGNMENT_64_BIT"= 3 + }; + taggedstruct { /* Optional */ + "RECOMMENDED_HOST_BUFSIZE" uint; /* Recommended size for the host */ + /* buffer size. The size is defined */ + /* as multiple of wMaxPacketSize. */ + }; +}; /* end of ep_parameters */ + +struct USB_Parameters { + uint; /* XCP on USB version */ + /* 1.4 = 0x0104 */ + uint; /* Vendor ID */ + uint; /* Product ID */ + uchar; /* Number of interface */ + enum { + "HEADER_LEN_BYTE" = 0, + "HEADER_LEN_CTR_BYTE" = 1, + "HEADER_LEN_FILL_BYTE" = 2, + "HEADER_LEN_WORD" = 3, + "HEADER_LEN_CTR_WORD" = 4, + "HEADER_LEN_FILL_WORD" = 5 + }; + taggedunion { /* OUT-EP for CMD and */ + /* STIM (if not specified otherwise) */ + block "OUT_EP_CMD_STIM" struct ep_parameters; + }; + taggedunion { /* IN-EP for RES/ERR, */ + /* DAQ (if not specified otherwise) and */ + /* EV/SERV (if not specified otherwise) */ + block "IN_EP_RESERR_DAQ_EVSERV" struct ep_parameters; + }; + /* Begin of optional */ + taggedstruct { /* Optional */ + "ALTERNATE_SETTING_NO" uchar; /* Number of alternate setting */ + /* String Descriptor of XCP */ + /* interface */ + "INTERFACE_STRING_DESCRIPTOR" char [101]; + /* multiple OUT-EP's for STIM */ + (block "OUT_EP_ONLY_STIM" struct ep_parameters)*; + /* multiple IN-EP's for DAQ */ + (block "IN_EP_ONLY_DAQ" struct ep_parameters)*; + /* only one IN-EP for EV/SERV */ + block "IN_EP_ONLY_EVSERV" struct ep_parameters; + (block "DAQ_LIST_USB_ENDPOINT" struct { + uint; /* reference to DAQ_LIST_NUMBER */ + taggedstruct { /* only mentioned if not VARIABLE */ + "FIXED_IN" uchar; /* this DAQ list always */ + /* ENDPOINT_NUMBER, not endpoint address */ + "FIXED_OUT" uchar; /* this STIM list always */ + /* ENDPOINT_NUMBER, not endpoint address */ + }; + })*; /* end of DAQ_LIST_USB_ENDPOINT */ + }; + + taggedstruct { + ("OPTIONAL_TL_SUBCMD" enum { /* XCP-Code of optional transport layer */ + /* specific subcommand supported by the slave */ + "GET_DAQ_EP" = 0xFF, + "SET_DAQ_EP" = 0xFE + })*; + + }; /* end of optional */ +}; + +/************************* end of USB **********************/ + +/************************ start of FLX *********************/ + +enum packet_assignment_type { + "NOT_ALLOWED", + "FIXED", + "VARIABLE_INITIALISED", + "VARIABLE" +}; /* end of packet_assignment_type */ + +struct buffer { + + uchar; /* FLX_BUF */ + + taggedstruct { + + "MAX_FLX_LEN_BUF" taggedunion { + "FIXED" uchar; /* constant value */ + "VARIABLE" uchar; /* initial value */ + }; /* end of MAX_FLX_LEN_BUF */ + + block "LPDU_ID" taggedstruct { + + "FLX_SLOT_ID" taggedunion { + "FIXED" uint; + "VARIABLE" taggedstruct{ + "INITIAL_VALUE" uint; + }; + }; /* end of FLX_SLOT_ID */ + + "OFFSET" taggedunion { + "FIXED" uchar; + "VARIABLE" taggedstruct{ + "INITIAL_VALUE" uchar; + }; + }; /* end of OFFSET */ + + "CYCLE_REPETITION" taggedunion { + "FIXED" uchar; + "VARIABLE" taggedstruct{ + "INITIAL_VALUE" uchar; + }; + }; /* end of CYCLE_REPETITION */ + + "CHANNEL" taggedunion { + "FIXED" enum { + "A" = 0, + "B" = 1 + }; + "VARIABLE" taggedstruct{ + "INITIAL_VALUE" enum { + "A" = 0, + "B" = 1 + }; + }; + }; /* end of CHANNEL */ + + }; /* end of LPDU_ID */ + + block "XCP_PACKET" taggedstruct { + + "CMD" enum packet_assignment_type; + "RES_ERR" enum packet_assignment_type; + "EV_SERV" enum packet_assignment_type; + "DAQ" enum packet_assignment_type; + "STIM" enum packet_assignment_type; + "MULTICAST" enum packet_assignment_type; + + }; /* end of XCP_PACKET */ + }; + +}; /* end of buffer */ + +struct FLX_Parameters { + + uint; /* XCP on FlexRay version */ + /* "1.4" = 0x0104 */ + + uint; /* T1_FLX [ms] */ + + char[256]; /* FIBEX-file including CHI information */ + /* including extension */ + /* without path */ + + char[256]; /* Cluster-ID */ + + uchar; /* NAX */ + + enum { + "HEADER_NAX" = 0, + "HEADER_NAX_FILL" = 1, + "HEADER_NAX_CTR" = 2, + "HEADER_NAX_FILL3" = 3, + "HEADER_NAX_CTR_FILL2" = 4, + "HEADER_NAX_LEN" = 5, + "HEADER_NAX_CTR_LEN" = 6, + "HEADER_NAX_FILL2_LEN" = 7, + "HEADER_NAX_CTR_FILL_LEN" = 8 + }; + + + enum { + "PACKET_ALIGNMENT_8" = 0, + "PACKET_ALIGNMENT_16" = 1, + "PACKET_ALIGNMENT_32" = 2 + }; + + taggedunion { + block "INITIAL_CMD_BUFFER" struct buffer; + }; + + taggedunion { + block "INITIAL_RES_ERR_BUFFER" struct buffer; + }; + + taggedstruct { + (block "POOL_BUFFER" struct buffer)*; + }; + + taggedstruct { + ("OPTIONAL_TL_SUBCMD" enum { /* XCP-Code of optional transport layer */ + /* specific subcommand supported by the slave */ + "FLX_ASSIGN" = 0xFF, + "FLX_ACTIVATE" = 0xFE, + "FLX_DEACTIVATE" = 0xFD, + "GET_DAQ_FLX_BUF" = 0xFC, + "SET_DAQ_FLX_BUF" = 0xFB, + "GET_DAQ_CLOCK_MULTICAST" = 0xFA + })*; + }; +}; + +block "IF_DATA" taggedunion if_data { +"XCP" struct { + taggedstruct Common_Parameters; /* default parameters */ + taggedstruct { + block "XCP_ON_CAN" struct { + struct CAN_Parameters; /* specific for CAN */ + taggedstruct Common_Parameters; /* overruling of default */ + }; + block "XCP_ON_SxI" struct { + struct SxI_Parameters; /* specific for SxI */ + taggedstruct Common_Parameters; /* overruling of default */ + }; + block "XCP_ON_TCP_IP" struct { + struct TCP_IP_Parameters; /* specific for TCP_IP */ + taggedstruct Common_Parameters; /* overruling of default */ + }; + block "XCP_ON_UDP_IP" struct { + struct UDP_IP_Parameters; /* specific for UDP */ + taggedstruct Common_Parameters; /* overruling of default */ + }; + block "XCP_ON_USB" struct { + struct USB_Parameters; /* specific for USB */ + taggedstruct Common_Parameters; /* overruling of default */ + }; + block "XCP_ON_FLX" struct { + struct FLX_Parameters; /* specific for FlexRay */ + taggedstruct Common_Parameters; /* overruling of default */ + }; + }; /* transport layer parameters*/ +}; + +"XCPplus" struct { + uint; /* XCP plus AML structure version */ + taggedstruct Common_Parameters; /* default parameters */ + taggedstruct { + (block "XCP_ON_CAN" struct { + struct CAN_Parameters; /* specific for CAN */ + taggedstruct Common_Parameters; /* overruling of default */ + taggedstruct { + "TRANSPORT_LAYER_INSTANCE" char[101]; /* name of the transport layer instance */ + }; + })*; + (block "XCP_ON_SxI" struct { + struct SxI_Parameters; /* specific for SxI */ + taggedstruct Common_Parameters; /* overruling of default */ + taggedstruct { + "TRANSPORT_LAYER_INSTANCE" char[101]; /* name of the transport layer instance */ + }; + })*; + (block "XCP_ON_TCP_IP" struct { + struct TCP_IP_Parameters; /* specific for TCP_IP */ + taggedstruct Common_Parameters; /* overruling of default */ + taggedstruct { + "TRANSPORT_LAYER_INSTANCE" char[101]; /* name of the transport layer instance */ + }; + })*; + (block "XCP_ON_UDP_IP" struct { + struct UDP_IP_Parameters; /* specific for UDP */ + taggedstruct Common_Parameters; /* overruling of default */ + taggedstruct { + "TRANSPORT_LAYER_INSTANCE" char[101]; /* name of the transport layer instance */ + }; + })*; + (block "XCP_ON_USB" struct { + struct USB_Parameters; /* specific for USB */ + taggedstruct Common_Parameters; /* overruling of default */ + taggedstruct { + "TRANSPORT_LAYER_INSTANCE" char[101]; /* name of the transport layer instance */ + }; + })*; + (block "XCP_ON_FLX" struct { + struct FLX_Parameters; /* specific for FlexRay */ + taggedstruct Common_Parameters; /* overruling of default */ + taggedstruct { + "TRANSPORT_LAYER_INSTANCE" char[101]; /* name of the transport layer instance */ + }; + })*; + }; /* transport layer specific parameters */ +}; +}; + +/end A2ML diff --git a/examples/udp_raw_demo/CANape/udp_raw_demo.a2l b/examples/udp_raw_demo/CANape/udp_raw_demo.a2l new file mode 100644 index 00000000..fc7fca2d --- /dev/null +++ b/examples/udp_raw_demo/CANape/udp_raw_demo.a2l @@ -0,0 +1,162 @@ +ASAP2_VERSION 1 71 +/begin PROJECT udp_raw_demo "" + +/begin HEADER "" VERSION "1.0" PROJECT_NO XCPLITE__CASDD /end HEADER + +/begin MODULE udp_raw_demo "" + +/include "XCP_104.aml" + +/begin MOD_COMMON "" +BYTE_ORDER MSB_LAST +ALIGNMENT_BYTE 1 +ALIGNMENT_WORD 1 +ALIGNMENT_LONG 1 +ALIGNMENT_FLOAT16_IEEE 1 +ALIGNMENT_FLOAT32_IEEE 1 +ALIGNMENT_FLOAT64_IEEE 1 +ALIGNMENT_INT64 1 +/end MOD_COMMON + + +/begin COMPU_METHOD conv.bool "" TAB_VERB "%.0" "" COMPU_TAB_REF conv.bool.table /end COMPU_METHOD +/begin COMPU_VTAB conv.bool.table "" TAB_VERB 2 0 "false" 1 "true" /end COMPU_VTAB + +/begin RECORD_LAYOUT U8 FNC_VALUES 1 UBYTE ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_U8 AXIS_PTS_X 1 UBYTE INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_U8 "" UBYTE NO_COMPU_METHOD 0 0 0 255 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_U8 "" VALUE U8 0 NO_COMPU_METHOD 0 255 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT U16 FNC_VALUES 1 UWORD ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_U16 AXIS_PTS_X 1 UWORD INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_U16 "" UWORD NO_COMPU_METHOD 0 0 0 65535 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_U16 "" VALUE U16 0 NO_COMPU_METHOD 0 65535 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT U32 FNC_VALUES 1 ULONG ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_U32 AXIS_PTS_X 1 ULONG INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_U32 "" ULONG NO_COMPU_METHOD 0 0 0 4294967295 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_U32 "" VALUE U32 0 NO_COMPU_METHOD 0 4.29497e+09 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT U64 FNC_VALUES 1 A_UINT64 ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_U64 AXIS_PTS_X 1 A_UINT64 INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_U64 "" A_UINT64 NO_COMPU_METHOD 0 0 0 1000000000000 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_U64 "" VALUE U64 0 NO_COMPU_METHOD 0 1e+12 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT I8 FNC_VALUES 1 SBYTE ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_I8 AXIS_PTS_X 1 SBYTE INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_I8 "" SBYTE NO_COMPU_METHOD 0 0 -128 127 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_I8 "" VALUE I8 0 NO_COMPU_METHOD -128 127 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT I16 FNC_VALUES 1 SWORD ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_I16 AXIS_PTS_X 1 SWORD INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_I16 "" SWORD NO_COMPU_METHOD 0 0 -32768 32767 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_I16 "" VALUE I16 0 NO_COMPU_METHOD -32768 32767 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT I32 FNC_VALUES 1 SLONG ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_I32 AXIS_PTS_X 1 SLONG INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_I32 "" SLONG NO_COMPU_METHOD 0 0 -2147483648 2147483647 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_I32 "" VALUE I32 0 NO_COMPU_METHOD -2.14748e+09 2.14748e+09 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT I64 FNC_VALUES 1 A_INT64 ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_I64 AXIS_PTS_X 1 A_INT64 INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_I64 "" A_INT64 NO_COMPU_METHOD 0 0 -1000000000000 1000000000000 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_I64 "" VALUE I64 0 NO_COMPU_METHOD -1e+12 1e+12 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT F32 FNC_VALUES 1 FLOAT32_IEEE ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_F32 AXIS_PTS_X 1 FLOAT32_IEEE INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_F32 "" FLOAT32_IEEE NO_COMPU_METHOD 0 0 -1e+12 1e+12 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_F32 "" VALUE F32 0 NO_COMPU_METHOD -1e+12 1e+12 /end TYPEDEF_CHARACTERISTIC +/begin RECORD_LAYOUT F64 FNC_VALUES 1 FLOAT64_IEEE ROW_DIR DIRECT /end RECORD_LAYOUT +/begin RECORD_LAYOUT A_F64 AXIS_PTS_X 1 FLOAT64_IEEE INDEX_INCR DIRECT /end RECORD_LAYOUT +/begin TYPEDEF_MEASUREMENT M_F64 "" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -1e+12 1e+12 /end TYPEDEF_MEASUREMENT +/begin TYPEDEF_CHARACTERISTIC C_F64 "" VALUE F64 0 NO_COMPU_METHOD -1e+12 1e+12 /end TYPEDEF_CHARACTERISTIC + + +/*-----------------------------------------------------------------------------------------*/ + +/begin CHARACTERISTIC params.counter_max "Maximum counter value" VALUE 0x80010004 U16 0 NO_COMPU_METHOD 0 65535 /end CHARACTERISTIC +/begin CHARACTERISTIC params.delay_us "Mainloop delay time in us" VALUE 0x80010000 U32 0 NO_COMPU_METHOD 0 500000 PHYS_UNIT "us" /end CHARACTERISTIC +/begin CHARACTERISTIC params.amplitude "Amplitude of the demo signal" VALUE 0x80010008 F32 0 NO_COMPU_METHOD 0 1000 /end CHARACTERISTIC +/begin MEASUREMENT global_counter "Global free running counter" ULONG NO_COMPU_METHOD 0 0 0 4.29497e+09 ECU_ADDRESS 0x30258 ECU_ADDRESS_EXTENSION 1 READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0x0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT demo_signal "Demo signal" FLOAT64_IEEE NO_COMPU_METHOD 0 0 -1000 1000 ECU_ADDRESS 0x30260 ECU_ADDRESS_EXTENSION 1 READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT VARIABLE /begin DEFAULT_EVENT_LIST EVENT 0x0 /end DEFAULT_EVENT_LIST /end DAQ_EVENT /end IF_DATA /end MEASUREMENT +/begin MEASUREMENT counter "Mainloop counter" UWORD NO_COMPU_METHOD 0 0 0 65535 ECU_ADDRESS 0x10096 ECU_ADDRESS_EXTENSION 2 READ_WRITE /begin IF_DATA XCP /begin DAQ_EVENT FIXED_EVENT_LIST EVENT 0x0 /end DAQ_EVENT /end IF_DATA /end MEASUREMENT + +/* Typedefs */ + +/* Groups */ +/begin GROUP params "Calibration Segment" ROOT /begin REF_CHARACTERISTIC params.counter_max params.delay_us params.amplitude /end REF_CHARACTERISTIC /end GROUP +/begin GROUP mainloop "Measurement event group" /begin REF_MEASUREMENT global_counter demo_signal counter /end REF_MEASUREMENT /end GROUP + +/* Conversions */ + +/*-----------------------------------------------------------------------------------------*/ + + +/begin GROUP Events "Events" ROOT /begin SUB_GROUP mainloop /end SUB_GROUP /end GROUP + +/begin MOD_PAR "" +EPK "V2.2.0" ADDR_EPK 0x80000000 +/begin MEMORY_SEGMENT epk "" DATA FLASH INTERN 0x80000000 32 -1 -1 -1 -1 -1 +/begin IF_DATA XCP + /begin SEGMENT 0 /* number */ 2 /* pages */ 0 /*addr_ext*/ 0 0 + /begin CHECKSUM XCP_CRC_16_CITT MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM + /begin PAGE 0 ECU_ACCESS_DONT_CARE XCP_READ_ACCESS_DONT_CARE XCP_WRITE_ACCESS_DONT_CARE /end PAGE + /begin PAGE 1 ECU_ACCESS_DONT_CARE XCP_READ_ACCESS_DONT_CARE XCP_WRITE_ACCESS_NOT_ALLOWED /end PAGE + /end SEGMENT +/end IF_DATA +/end MEMORY_SEGMENT +/begin MEMORY_SEGMENT params "" DATA FLASH INTERN 0x80010000 12 -1 -1 -1 -1 -1 +/begin IF_DATA XCP + /begin SEGMENT 1 /* number */ 2 /* pages */ 0 /*addr_ext*/ 0 0 + /begin CHECKSUM XCP_CRC_16_CITT MAX_BLOCK_SIZE 0xFFFF EXTERNAL_FUNCTION "" /end CHECKSUM + /begin PAGE 0 ECU_ACCESS_DONT_CARE XCP_READ_ACCESS_DONT_CARE XCP_WRITE_ACCESS_DONT_CARE /end PAGE + /begin PAGE 1 ECU_ACCESS_DONT_CARE XCP_READ_ACCESS_DONT_CARE XCP_WRITE_ACCESS_NOT_ALLOWED /end PAGE + /end SEGMENT +/end IF_DATA +/end MEMORY_SEGMENT +/end MOD_PAR + + +/begin IF_DATA XCP +/begin PROTOCOL_LAYER + 0x104 1000 2000 0 0 0 0 0 248 1024 BYTE_ORDER_MSB_LAST ADDRESS_GRANULARITY_BYTE +OPTIONAL_CMD GET_COMM_MODE_INFO +OPTIONAL_CMD GET_ID +OPTIONAL_CMD SET_REQUEST +OPTIONAL_CMD SET_MTA +OPTIONAL_CMD UPLOAD +OPTIONAL_CMD SHORT_UPLOAD +OPTIONAL_CMD DOWNLOAD +OPTIONAL_CMD SHORT_DOWNLOAD +OPTIONAL_CMD GET_CAL_PAGE +OPTIONAL_CMD SET_CAL_PAGE +OPTIONAL_CMD COPY_CAL_PAGE +OPTIONAL_CMD GET_PAG_PROCESSOR_INFO +OPTIONAL_CMD GET_SEGMENT_INFO +OPTIONAL_CMD GET_PAGE_INFO +OPTIONAL_CMD GET_SEGMENT_MODE +OPTIONAL_CMD SET_SEGMENT_MODE +OPTIONAL_CMD BUILD_CHECKSUM +OPTIONAL_CMD USER_CMD +OPTIONAL_CMD GET_DAQ_RESOLUTION_INFO +OPTIONAL_CMD GET_DAQ_PROCESSOR_INFO +OPTIONAL_CMD GET_DAQ_EVENT_INFO +OPTIONAL_CMD FREE_DAQ +OPTIONAL_CMD ALLOC_DAQ +OPTIONAL_CMD ALLOC_ODT +OPTIONAL_CMD ALLOC_ODT_ENTRY +OPTIONAL_CMD SET_DAQ_PTR +OPTIONAL_CMD WRITE_DAQ +OPTIONAL_CMD GET_DAQ_LIST_MODE +OPTIONAL_CMD SET_DAQ_LIST_MODE +OPTIONAL_CMD START_STOP_SYNCH +OPTIONAL_CMD START_STOP_DAQ_LIST +OPTIONAL_CMD GET_DAQ_CLOCK +OPTIONAL_CMD WRITE_DAQ_MULTIPLE +OPTIONAL_CMD TIME_CORRELATION_PROPERTIES +OPTIONAL_LEVEL1_CMD GET_VERSION +/end PROTOCOL_LAYER +/begin DAQ +DYNAMIC 0 1 0 OPTIMISATION_TYPE_DEFAULT ADDRESS_EXTENSION_FREE IDENTIFICATION_FIELD_TYPE_RELATIVE_BYTE GRANULARITY_ODT_ENTRY_SIZE_DAQ_BYTE 0xF8 OVERLOAD_INDICATION_PID +/begin TIMESTAMP_SUPPORTED 0x1 SIZE_DWORD UNIT_1NS TIMESTAMP_FIXED /end TIMESTAMP_SUPPORTED +/begin EVENT "mainloop" "mainloop" 0x0 DAQ 0xFF 0 0 0 CONSISTENCY EVENT /end EVENT +/end DAQ +/begin XCP_ON_UDP_IP + 0x104 5555 ADDRESS "192.168.0.220" +/end XCP_ON_UDP_IP +/end IF_DATA + +/end MODULE +/end PROJECT diff --git a/examples/udp_raw_demo/README.md b/examples/udp_raw_demo/README.md new file mode 100644 index 00000000..8491cf90 --- /dev/null +++ b/examples/udp_raw_demo/README.md @@ -0,0 +1,227 @@ +# udp_raw_demo — Raw Ethernet Transport (no TCP/IP stack) + +XCP on UDP/IPv4 with the transport implemented **inside xcplib**, on top of a thin raw +Ethernet HAL. For targets that have no TCP/IP stack at all — a bare-metal EMAC driver, or an +RTOS Ethernet abstraction without lwIP. This demo is the Linux development and test vehicle +for that transport, and the template for embedded ports. + +Design and internals: [docs/SOCKET_RAW.md](../../docs/SOCKET_RAW.md). + +> **Linux only.** The HAL backend uses `AF_PACKET` and needs `CAP_NET_RAW`. +> A build of the `raw` configuration on macOS or Windows stops with a clear `#error`. + +--- + +## What it demonstrates + +| Feature | How it is demonstrated | +|---|---| +| Raw Ethernet transport | `OPTION_ENABLE_UDP_RAW` — Ethernet, IPv4 and UDP headers are built by xcplib, no OS socket API involved | +| Interface selection | `socketRawSetInterface(ifname)` before `XcpEthServerInit`, from the `--if` command line option | +| Explicit local address | `XcpEthServerInit(addr, ...)` with a **concrete** IPv4 address — there is no IP stack and no DHCP, so `0.0.0.0` (ANY) is rejected | +| ARP and ICMP | answered by xcplib itself: ARP Requests for our IP get a Reply, and `ping` is answered | +| Calibration segment | `XcpCreateCalSeg` + `A2lSetSegmentAddrMode` / `A2lCreateParameter` for `counter_max`, `delay_us`, `amplitude` | +| Measurement | event `mainloop` with `global_counter` and `demo_signal` (absolute addressing) and `counter` (stack frame relative) | + +The demo is intentionally close to [hello_xcp](../hello_xcp/README.md); the difference is the +transport and the mandatory address configuration. Everything above the transport layer — A2L +generation, calibration segments, DAQ — is unchanged. + +### Files + +| File | Purpose | +|---|---| +| `src/main.c` | Demo application — command line, server setup, calibration segment, event | +| `test.sh` | Sync, build, run and test against a remote Linux target over SSH | +| `CANape/` | CANape project and the A2L file uploaded by `test.sh` | + +--- + +## Building + +```bash +./build.sh raw examples +``` + +Or with CMake directly: + +```bash +cmake -B build-raw -S . -DXCPLITE_CONFIGURATION=raw -DXCPLITE_BUILD_EXAMPLES=ON -DCMAKE_BUILD_TYPE=Debug +cmake --build build-raw --target udp_raw_demo +``` + +The `raw` configuration is selected by `XCPLITE_CONFIGURATION=raw` +([src/xcplib_raw_cfg.h](../../src/xcplib_raw_cfg.h)) and builds into `build-raw/`. + +--- + +## Running + +``` +Usage: udp_raw_demo [--if ] [--ip ] [--port ] + + --if Ethernet interface for the raw transport (default: eth0) + --ip local IPv4 address of this target (default: 192.168.0.220) + --port XCP UDP port (default: 5555) +``` + +The process needs `CAP_NET_RAW`, either by granting the capability once: + +```bash +sudo setcap cap_net_raw+ep ./build-raw/udp_raw_demo +./build-raw/udp_raw_demo --if eth0 --ip 192.168.1.240 +``` + +or by running it as root. + +**The address must not be one the kernel owns.** xcplib answers ARP and ICMP for it itself; if the +kernel also had that address it would answer first and send ICMP port unreachable for the XCP port. +Pick a spare address outside the DHCP pool, or use the isolated setup below. + +--- + +## Isolated test setup (recommended) + +[test/test_socket_raw.sh](../../test/test_socket_raw.sh) creates a `veth` pair with the target in +its own network namespace and **no kernel IP on the target side**, so xcplib alone owns +`192.168.90.2`. It then runs ARP, `ping` and XCP CONNECT checks: + +```bash +./build.sh raw examples +sudo ./test/test_socket_raw.sh # run the checks and clean up +sudo ./test/test_socket_raw.sh --keep # leave it running for manual tests +``` + +With `--keep`, connect from the same machine: + +```bash +ping 192.168.90.2 # proves the HAL, MAC filter, ARP and the IPv4 header +tcpdump -i veth0 -nn -e -vv # watch the frames +xcpclient --dest-addr 192.168.90.2 --port 5555 --udp +``` + +`ping` is the highest value first check: if it answers, the Ethernet HAL, the MAC filter, the ARP +responder, the IPv4 header build and the header checksum are all working — before any XCP tooling +is involved. + +--- + +## Remote target: build and test on a Raspberry Pi + +[test.sh](test.sh) drives the whole loop against a remote Linux target over SSH: sync the sources, +build there, grant `CAP_NET_RAW`, start the demo, upload the A2L and run a test measurement. +Edit the parameters at the top of the script: + +| Variable | Meaning | +|---|---| +| `TARGET_USER` / `TARGET_HOST` | SSH login of the build machine | +| `TARGET_PATH` | where the sources are synced to on the target | +| `TARGET_IP` / `TARGET_PORT` | address the **demo** serves, must match its `--ip` / default | +| `BUILD_TYPE` | `Debug`, `RelWithDebInfo` (default) or `Release` | + +```bash +./examples/udp_raw_demo/test.sh +``` + +Prerequisites: SSH access with keys, `rsync` on both sides, and `xcpclient` on the local machine +(`./build.sh rust_tools`, or `cargo install --path tools/xcpclient`). + +--- + +## Using xcpclient + +**xcpclient does not upload the A2L automatically.** Without any A2L option it queries the A2L file +name from the target (`GET_ID` `ASAM_NAME`) and then expects a file of that name **in the current +directory**: + +```bash +xcpclient --dest-addr 192.168.0.220:5555 --udp +# [INFO ] Using A2L file name from XCP server GET_ID ASAM_NAME: udp_raw_demo_V2.2.0 +# [INFO ] A2L path: udp_raw_demo_V2.2.0.a2l +# [ERROR] Could not load A2L file ... No such file or directory +``` + +To fetch it from the target, ask for the upload explicitly and name the local file: + +```bash +xcpclient --dest-addr 192.168.0.220:5555 --udp --upload-a2l --a2l ./udp_raw_demo.a2l +``` + +> The uploaded A2L starts with `/include "XCP_104.aml"`, so that file has to sit next to it or the +> parse fails. Copy it from the repository root: `cp XCP_104.aml .` + +List what the target offers, and run a short measurement: + +```bash +# list all measurement and calibration variables (the argument is a regex, "." matches everything) +xcpclient --dest-addr 192.168.0.220:5555 --udp --a2l ./udp_raw_demo.a2l --list-mea . --list-cal . + +# measure for 2 seconds (the argument is a regex, "counter" matches counter and global_counter) +xcpclient --dest-addr 192.168.0.220:5555 --udp --a2l ./udp_raw_demo.a2l --mea counter --time 2 +``` + +Typical output of the list command for this demo: + +``` +Calibration variables: + params.counter_max 0:80010004 = 1024 + params.delay_us 0:80010000 = 1000 + params.amplitude 0:80010008 = 100 + +Measurement variables: + global_counter 1:0x00030258 event 0 4 byte unsigned + demo_signal 1:0x00030260 event 0 8 byte float + counter 2:0x00010096 event 0 2 byte unsigned +``` + +Note the address extensions: `1` is absolute addressing, `2` is stack frame relative — `counter` is +a local variable of the mainloop. + +--- + +## Configuration + +Options in [src/xcplib_raw_cfg.h](../../src/xcplib_raw_cfg.h): + +| Option | Default | Purpose | +|---|---|---| +| `OPTION_UDP_RAW_IFNAME` | `"eth0"` | default interface, overridden by `--if` | +| `OPTION_UDP_RAW_ENABLE_ICMP_ECHO` | on | answer `ping` | +| `OPTION_UDP_RAW_UDP_CHECKSUM_ZERO` | on | transmit UDP checksum 0, legal for IPv4 (RFC 768) | +| `OPTION_UDP_RAW_VERIFY_RX_CHECKSUM` | on | verify received IPv4 header checksums | +| `OPTION_UDP_RAW_GRATUITOUS_ARP` | off | announce our IP/MAC on bind | +| `OPTION_UDP_RAW_ZERO_COPY` | on | write the Ethernet/IPv4/UDP header into queue headroom instead of copying the payload | + +Two notes: + +- With the UDP checksum zeroed, `tcpdump` and Wireshark cannot validate the UDP framing. Switch to + `OPTION_UDP_RAW_UDP_CHECKSUM_COMPUTE` while bringing up a new target if you want that check. +- `OPTION_UDP_RAW_ZERO_COPY` mainly pays off on embedded targets. The copy it removes is ~12 MB/s of + memory bandwidth at a saturated 100 Mbit/s — negligible on a PC or a Raspberry Pi, a meaningful + fraction of a core on a microcontroller. Turn it off to fall back to the copying transmit path, + which is useful when bringing up a new HAL backend. + +--- + +## Porting to a target without an IP stack + +Implement the functions of [src/socket_raw_hal.h](../../src/socket_raw_hal.h) for your EMAC — +open/close, get MAC, send and receive one complete Ethernet frame, plus an optional wakeup. +[src/socket_raw_hal_linux.c](../../src/socket_raw_hal_linux.c) is the reference backend; everything +above it (UDP/IPv4, ARP, ICMP, the receive filter) is shared and needs no porting. + +A backend can also live outside this repository: define `OPTION_UDP_RAW_HAL_EXTERNAL` and xcplib +selects none, leaving the `eth_hal_*` symbols for your application to link. See +[docs/SOCKET_RAW.md](../../docs/SOCKET_RAW.md). + +--- + +## Tests + +```bash +./build.sh raw tests +./build-raw/socket_raw_test +``` + +Unit tests for the parts that need no network: checksums against the RFC 1071 reference vector, +wire struct packing, the frame build, the receive filter, and the ARP and ICMP responders. diff --git a/examples/udp_raw_demo/src/main.c b/examples/udp_raw_demo/src/main.c new file mode 100644 index 00000000..5b15865d --- /dev/null +++ b/examples/udp_raw_demo/src/main.c @@ -0,0 +1,208 @@ +// udp_raw_demo - XCPlite/libxcplite demo for the raw Ethernet transport (OPTION_ENABLE_UDP_RAW) +// +// XCP on UDP/IPv4 implemented inside xcplib on top of a raw Ethernet HAL, for targets +// which have no TCP/IP stack. This demo is the Linux development and test vehicle. +// +// Unlike the other examples this one needs an explicit local IPv4 address: there is no +// IP stack and no DHCP, so binding to 0.0.0.0 (ANY) has no meaning. The interface and the +// address are therefore taken from the command line. +// +// Build and run (see docs/SOCKET_RAW.md for the full network setup): +// ./build.sh raw examples +// sudo setcap cap_net_raw+ep ./build-raw/udp_raw_demo +// ./build-raw/udp_raw_demo --if eth0 --ip 192.168.1.240 + +#include // for assert +#include // for signal handling +#include // for bool +#include // for uintxx_t +#include // for printf +#include // for strtoul +#include // for strcmp + +// Include XCPlite/libxcplite C headers +#include // for A2l generation +#include // for application programming interface + +#include "sockets.h" // for socketRawSetInterface + +//----------------------------------------------------------------------------------------------------- +// XCP params + +#define OPTION_PROJECT_NAME "udp_raw_demo" +#define OPTION_PROJECT_VERSION "V2.2.0" +#define OPTION_SERVER_PORT 5555 +#define OPTION_QUEUE_SIZE (1024 * 32) +#define OPTION_LOG_LEVEL 4 + +#define OPTION_XCP_MODE (XCP_MODE_PERSISTENCE | XCP_MODE_LOCAL) +#define OPTION_A2L_MODE (A2L_MODE_WRITE_ONCE | A2L_MODE_FINALIZE_ON_CONNECT | A2L_MODE_AUTO_GROUPS) + +// Defaults, overridden by --if and --ip +#define DEFAULT_IFNAME "eth0" +#define DEFAULT_IP {192, 168, 0, 220} + +//----------------------------------------------------------------------------------------------------- +// Demo calibration parameters + +typedef struct params { + uint32_t delay_us; // Mainloop delay time in us + uint16_t counter_max; // Maximum value for the counter + float amplitude; // Amplitude of the demo signal +} params_t; + +const params_t params = {.delay_us = 1000, .counter_max = 1024, .amplitude = 100.0f}; + +tXcpCalSegIndex params_calseg = XCP_UNDEFINED_CALSEG; + +//----------------------------------------------------------------------------------------------------- +// Demo global measurement values + +uint32_t global_counter = 0; +double demo_signal = 0.0; + +//----------------------------------------------------------------------------------------------------- +// Command line + +static void usage(const char *argv0) { + printf("\nUsage: %s [--if ] [--ip ] [--port ]\n" + "\n" + " --if Ethernet interface for the raw transport (default: %s)\n" + " --ip local IPv4 address of this target (default: 192.168.90.2)\n" + " There is no IP stack and no DHCP, so a concrete address is required.\n" + " It must NOT be an address owned by the kernel of this machine.\n" + " --port XCP UDP port (default: %u)\n" + "\n" + "Needs CAP_NET_RAW: sudo setcap cap_net_raw+ep %s\n" + "See docs/SOCKET_RAW.md for the test network setup.\n\n", + argv0, DEFAULT_IFNAME, (unsigned)OPTION_SERVER_PORT, argv0); +} + +// Parse "a.b.c.d" into 4 bytes, returns false on a malformed address +static bool parseIp(const char *s, uint8_t *addr) { + unsigned v[4]; + if (sscanf(s, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]) != 4) + return false; + for (int i = 0; i < 4; i++) { + if (v[i] > 255) + return false; + addr[i] = (uint8_t)v[i]; + } + return true; +} + +//----------------------------------------------------------------------------------------------------- +// Demo main + +static volatile bool running = true; +static void sig_handler(int sig) { + (void)sig; + running = false; +} + +int main(int argc, char *argv[]) { + + const char *ifname = DEFAULT_IFNAME; + uint8_t addr[4] = DEFAULT_IP; + uint16_t port = OPTION_SERVER_PORT; + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--if") && i + 1 < argc) { + ifname = argv[++i]; + } else if (!strcmp(argv[i], "--ip") && i + 1 < argc) { + if (!parseIp(argv[++i], addr)) { + printf("Invalid IPv4 address '%s'\n", argv[i]); + return 1; + } + } else if (!strcmp(argv[i], "--port") && i + 1 < argc) { + port = (uint16_t)strtoul(argv[++i], NULL, 10); + } else { + usage(argv[0]); + return (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) ? 0 : 1; + } + } + + printf("\nXCP on raw Ethernet udp_raw_demo %uBit %s\n", (uint32_t)(sizeof(void *) * 8), OPTION_PROJECT_VERSION); + printf(" Interface : %s\n", ifname); + printf(" Address : %u.%u.%u.%u:%u\n\n", addr[0], addr[1], addr[2], addr[3], port); + + signal(SIGINT, sig_handler); + signal(SIGTERM, sig_handler); + + XcpSetLogLevel(OPTION_LOG_LEVEL); + + if (!XcpInit(OPTION_PROJECT_NAME, OPTION_PROJECT_VERSION, OPTION_XCP_MODE)) { + printf("Failed to initialize XCP\n"); + return 1; + } + XcpSetElfName(argv[0]); + + // XCP: Select the Ethernet interface for the raw transport, before starting the server + socketRawSetInterface(ifname); + + // XCP: Initialize the XCP Server. + // The address is mandatory here: the raw transport rejects 0.0.0.0 (ANY), there is no + // IP stack which could resolve it. useTCP is false, the raw transport is UDP only. + if (!XcpEthServerInit(addr, port, false, OPTION_QUEUE_SIZE)) { + printf("Failed to start the XCP server.\n" + " Check that the interface exists, that this binary has CAP_NET_RAW\n" + " (sudo setcap cap_net_raw+ep %s) and that the address is not owned by the kernel.\n", + argv[0]); + return 1; + } + + if (!A2lInit(addr, port, false, OPTION_A2L_MODE)) { + return 1; + } + + params_calseg = XcpCreateCalSeg("params", ¶ms, sizeof(params)); + assert(params_calseg != XCP_UNDEFINED_CALSEG); + + A2lSetSegmentAddrMode(params_calseg, params); + A2lCreateParameter(params.counter_max, "Maximum counter value", "", 0, 65535); + A2lCreateParameter(params.delay_us, "Mainloop delay time in us", "us", 0, 500000); + A2lCreateParameter(params.amplitude, "Amplitude of the demo signal", "", 0.0, 1000.0); + + uint16_t counter = 0; + + // XCP: Create a measurement event and register the measurement variables + DaqCreateEvent(mainloop); + A2lOnce() { + A2lSetAbsoluteAddrMode(mainloop); + A2lCreateMeasurement(global_counter, "Global free running counter"); + A2lCreatePhysMeasurement(demo_signal, "Demo signal", "", -1000.0, 1000.0); + A2lSetStackAddrMode(mainloop); + A2lCreateMeasurement(counter, "Mainloop counter"); + } + + printf("XCP server running. Connect with CANape or xcpclient to %u.%u.%u.%u:%u\n", addr[0], addr[1], addr[2], addr[3], port); + printf("Try 'ping %u.%u.%u.%u' first - it proves the Ethernet HAL, ARP and the IPv4 header build.\n\n", addr[0], addr[1], addr[2], addr[3]); + + // Mainloop + uint32_t delay_us = 1000; + while (running) { + + const params_t *p = (params_t *)XcpLockCalSeg(params_calseg); + delay_us = p->delay_us; + + counter++; + if (counter > p->counter_max) { + counter = 0; + } + global_counter++; + demo_signal = (double)p->amplitude * (double)counter / 1000.0; + + XcpUnlockCalSeg(params_calseg); + + // XCP: Trigger the measurement event + DaqTriggerEvent(mainloop); + + sleepUs(delay_us); + } + + printf("\nShutting down...\n"); + XcpDisconnect(); // Force disconnect the XCP client + A2lFinalize(); // Finalize A2L generation, if not done yet + XcpEthServerShutdown(); // Stop the XCP server + return 0; +} diff --git a/examples/udp_raw_demo/test.sh b/examples/udp_raw_demo/test.sh new file mode 100755 index 00000000..00b520c4 --- /dev/null +++ b/examples/udp_raw_demo/test.sh @@ -0,0 +1,213 @@ +#!/bin/bash + +# A2L file creator for the udp_raw_demo example project + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# The script syncs the example project to the target, builds it there, runs it with XCP on Ethernet, +# downloads the ELF file and the A2L file to the local machine. +# Prerequisites: +# - The target must be reachable via SSH and have rsync installed +# - The local machine must have rsync and scp installed +# - The local machine must have xcpclient installed + + +#====================================================================================================================== +# Parameters +#====================================================================================================================== + +LOGFILE="$REPO_ROOT/examples/udp_raw_demo/CANape/udp_raw_demo.log" +#LOGFILE='/dev/stdout' +#LOGFILE="/dev/null" + +# A2L file path on local machine +A2LFILE="$REPO_ROOT/examples/udp_raw_demo/CANape/udp_raw_demo.a2l" + +# ELF file path on local machine +ELFFILE="$REPO_ROOT/examples/udp_raw_demo/CANape/udp_raw_demo.elf" + +# Build type for target executable: Release, RelWithDebInfo or Debug +# RelWithDebInfo is default to demonstrate operation with with -O1 and NDEBUG +# Optimization level >= -O1 keeps variables in registers whenever possible, so these local variables cannot be measured +# Debug mode is the least efficient but keeps all variables and stack frames intact +BUILD_TYPE="RelWithDebInfo" +# -O0 +#BUILD_TYPE="Debug" +# -O2 no debug symbols +#BUILD_TYPE="Release" + +# Run a simple test calibration and measurement +TEST=true +#TEST=false + + +# Target connection details +#TARGET_USER="parallels" +#TARGET_HOST="10.211.55.4" +TARGET_USER="rainer" +TARGET_HOST="192.168.0.206" +TARGET_PATH="~/XCPlite-Test" +TARGET_BUILD_DIR="build-raw" +TARGET_BINARY="udp_raw_demo" +TARGET_IP="192.168.0.220" +TARGET_PORT="5555" + +# Path to xcpclient tool executable (assuming cargo installed it to ~/.cargo/bin) +XCPCLIENT="xcpclient" + + +#====================================================================================================================== +# Sync Target, Build Application on Target, Download ELF, Start ECU, ... +#====================================================================================================================== + + +echo "========================================================================================================" +echo "A2L file creator for the udp_raw_demo example project" +echo "========================================================================================================" + +mkdir -p "$(dirname "$LOGFILE")" +echo "Logging to $LOGFILE enabled" +echo "" > "$LOGFILE" + + +#====================================================================================================================== +# Sync target +#====================================================================================================================== + +# Sync target +echo "Sync target ..." +rsync -avz --delete \ + --include='/build.sh' \ + --include='/CMakeLists.txt' \ + --include='/cmake/***' \ + --include='/inc/***' \ + --include='/src/***' \ + --include='/examples/' \ + --include='/examples/udp_raw_demo/***' \ + --include='/examples/udp_raw_demo_cpp/***' \ + --exclude='*' \ + "$REPO_ROOT/" "$TARGET_USER@$TARGET_HOST:$TARGET_PATH/" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Rsync with target" + exit 1 +fi + + +#====================================================================================================================== +# Build on target +#====================================================================================================================== + +echo "Build executable on Target ..." +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && ./build.sh $BUILD_TYPE raw examples" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: Build on target" + exit 1 +fi + + + + +#====================================================================================================================== +# Upload ELF and create A2L file +# Create the A2L from ELF file with xcpclient tool +#====================================================================================================================== + +# Download the target executable for the local A2L generation process +#echo "Downloading ELF file from target $TARGET_PATH/$TARGET_BUILD_DIR/$TARGET_BINARY to $ELFFILE ..." +#scp "$TARGET_USER@$TARGET_HOST:$TARGET_PATH/$TARGET_BUILD_DIR/$TARGET_BINARY" "$ELFFILE" 1> /dev/null +#if [ $? -ne 0 ]; then +# echo "❌ FAILED: Download $TARGET_PATH/$TARGET_BUILD_DIR/$TARGET_BINARY" +# exit 1 +#fi + +#echo "" +#echo "========================================================================================================" +#echo "Creating A2L file from XCPlite ELF file ..." +#echo "========================================================================================================" +#echo "" +#echo "Command: $XCPCLIENT --log-level=3 --verbose=0 --dest-addr=$TARGET_HOST --udp --offline --elf \"$ELFFILE\" --create-a2l --a2l \"$A2LFILE\"" +#$XCPCLIENT --log-level=3 --verbose=0 --dest-addr=$TARGET_HOST --udp --offline --elf "$ELFFILE" --create-a2l --a2l "$A2LFILE" >> "$LOGFILE" +#if [ $? -ne 0 ]; then +# echo "❌ FAILED: xcpclient returned error" +# exit 1 +#fi + +#echo "" +#echo "✅ SUCCESS:" +#echo "Created a new A2L file $A2LFILE" +#echo "" + + +#====================================================================================================================== +# Enable raw socket access for the target executable (requires root privileges) +# sudo setcap cap_net_raw+ep ./build-raw/udp_raw_demo +#====================================================================================================================== + +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && sudo setcap cap_net_raw+ep ./$TARGET_BUILD_DIR/$TARGET_BINARY" 1> /dev/null +if [ $? -ne 0 ]; then + echo "❌ FAILED: setcap cap_net_raw+ep $TARGET_PATH/$TARGET_BUILD_DIR/$TARGET_BINARY" + exit 1 +fi + + + +#====================================================================================================================== +# Test +#====================================================================================================================== + +if [ "$TEST" = true ]; then + +# Start the target executable in the background +ssh "$TARGET_USER@$TARGET_HOST" "cd $TARGET_PATH && ./$TARGET_BUILD_DIR/$TARGET_BINARY" & +SSH_PID=$! + +# Wait until the target is actually serving, instead of a fixed sleep. It has to open the raw +# socket, read the interface MAC and bind, which takes longer than a normal socket bind, and a +# failure here (address in use, missing capability, wrong interface) would otherwise only show up +# as an xcpclient timeout further down. +# Note: pgrep/pkill -x matches the process NAME. Do NOT use -f here: it matches the full command +# line, and the ssh command line on the target contains "$TARGET_BINARY" itself, so -f would match +# the ssh session as well and terminate it. +echo "Waiting for $TARGET_BINARY to come up on $TARGET_IP ..." +ssh "$TARGET_USER@$TARGET_HOST" "for i in \$(seq 1 20); do pgrep -x $TARGET_BINARY > /dev/null && exit 0; sleep 0.5; done; exit 1" +if [ $? -ne 0 ]; then + echo "❌ FAILED: $TARGET_BINARY is not running on the target" + echo " Check the interface name, that $TARGET_IP is free and not owned by the target kernel," + echo " and that setcap cap_net_raw+ep was applied." + exit 1 +fi + +echo "========================================================================================================" +echo "Connect and upload A2L" +echo "List measurements and calibrations" +TEST_FAILED=0 +$XCPCLIENT --log-level=3 --dest-addr=$TARGET_IP:$TARGET_PORT --udp --upload-a2l --a2l "$A2LFILE" --list-mea . --list-cal . +if [ $? -ne 0 ]; then + echo "❌ FAILED: xcpclient connect, A2L upload or variable listing" + TEST_FAILED=1 +fi + +if [ $TEST_FAILED -eq 0 ]; then +echo "========================================================================================================" +echo "Test measurement" +echo "========================================================================================================" +$XCPCLIENT --log-level=2 --dest-addr=$TARGET_IP:$TARGET_PORT --udp --a2l "$A2LFILE" --mea counter --time 2 --verbose 2 +if [ $? -ne 0 ]; then + echo "❌ FAILED: xcpclient measurement" + TEST_FAILED=1 +fi +fi + +# Stop the target executable +# -x matches the process name exactly. -f would also match this very ssh command line, because it +# contains the binary name, and would terminate the ssh session instead of (or as well as) the demo. +ssh "$TARGET_USER@$TARGET_HOST" "pkill -x $TARGET_BINARY" +wait "$SSH_PID" 2>/dev/null + +if [ $TEST_FAILED -ne 0 ]; then + exit 1 +fi +echo "✅ SUCCESS: udp_raw_demo test passed" + +fi \ No newline at end of file diff --git a/inc/xcplib.h b/inc/xcplib.h index cd7cc611..54b394b5 100644 --- a/inc/xcplib.h +++ b/inc/xcplib.h @@ -314,21 +314,16 @@ static_assert(sizeof(((tXcpEventDescriptor *)0)->res) > 0, "tXcpEventDescriptor // Linker-synthesized section boundary symbols, resolved at link time #if defined(__ELF__) -// Declared weak: if no object file contributes to the xcp_evts section the symbols resolve -// to NULL rather than causing an undefined-reference linker error. Keeps Linux (production) -// builds with zero section-registered events linkable and graceful. extern const tXcpEventDescriptor __start_xcp_evts[] __attribute__((weak)); extern const tXcpEventDescriptor __stop_xcp_evts[] __attribute__((weak)); #elif defined(__APPLE__) -// Mach-O (ld64) boundary symbols. Not weak: if no descriptor is ever placed in the section -// the link fails with an undefined-symbol error. That is acceptable here - macOS is a -// development-only target and a build with zero events is a non-functional configuration. extern const tXcpEventDescriptor __start_xcp_evts[] __asm("section$start$__DATA$xcp_evts"); extern const tXcpEventDescriptor __stop_xcp_evts[] __asm("section$end$__DATA$xcp_evts"); +#elif defined(_MSC_VER) +#define __start_xcp_evts ((const tXcpEventDescriptor *)NULL) +#define __stop_xcp_evts ((const tXcpEventDescriptor *)NULL) #else -#ifndef _WIN32 -#error "Unsupported platform for event segment registration" -#endif +#error "Unsupported platform for section based event pre-registration" #endif #endif // __XCPLITE_H__ @@ -344,6 +339,17 @@ extern const tXcpEventDescriptor __stop_xcp_evts[] __asm("section$end$__DATA$xcp #define XCP_EVENT_SECTION_ATTR /* section-based registration not supported on this platform */ #endif +// Attribute for functions which trigger an event and measure their local variables: such a function must not be inlined. +// An inlined function has a copy with its own stack frame at each call site, there is no stack frame relative address which is +// valid for all copies. The offline A2L generator (xcpclient) does not register the local variables of an inlined function, see docs/OFFLINE_A2L.md +#if defined(__GNUC__) || defined(__clang__) +#define XCP_NOINLINE __attribute__((noinline)) +#elif defined(_MSC_VER) +#define XCP_NOINLINE __declspec(noinline) +#else +#define XCP_NOINLINE +#endif + // Link-time event id derived from the descriptor's position in the xcp_evts section // Only with clang on Linux, this is a link-time constant, usable as a static initializer #if defined(__ELF__) || defined(__APPLE__) @@ -468,15 +474,20 @@ void XcpEventEnable(tXcpEventId event, bool enable); // This defines the maximum stack frame size which can be accessed #define XCP_FRAME_ADDR_OFFSET 0x10000 -// Xtensa GCC: DWARF locations are relative to CFA, while __builtin_frame_address(0) returns the frame pointer after the entry instruction. -#if (defined(__GNUC__) || defined(__clang__)) && defined(__XTENSA__) +// The frame address must be the frame base which the compiler uses in the DWARF locations of the local variables (DW_AT_frame_base), +// the offline A2L generator (xcpclient) takes the variable offsets from there without any further correction: +// - clang describes the local variables relative to the frame pointer register, __builtin_frame_address(0) is the frame pointer and +// forces the function to keep one +// - GCC describes the local variables relative to the canonical frame address (CFA, DW_OP_call_frame_cfa), __builtin_dwarf_cfa() is +// the CFA on every architecture and does not force a frame pointer +// The on-target A2L generation uses the same macro for the registration and for the trigger, any consistent value works there +#if defined(__clang__) -#define xcp_get_frame_addr() (const uint8_t *)((uint8_t *)__builtin_dwarf_cfa() - XCP_FRAME_ADDR_OFFSET) +#define xcp_get_frame_addr() (const uint8_t *)((uint8_t *)__builtin_frame_address(0) - XCP_FRAME_ADDR_OFFSET) -// Linux, MACOS gnu and clang compiler -#elif defined(__GNUC__) || defined(__clang__) +#elif defined(__GNUC__) -#define xcp_get_frame_addr() (const uint8_t *)((uint8_t *)__builtin_frame_address(0) - XCP_FRAME_ADDR_OFFSET) +#define xcp_get_frame_addr() (const uint8_t *)((uint8_t *)__builtin_dwarf_cfa() - XCP_FRAME_ADDR_OFFSET) // MSVC compiler #elif defined(_MSC_VER) @@ -523,6 +534,7 @@ extern const uint8_t *gXcpBaseAddr; // If needed, uses local scope static or thread local storage to create a once pattern for the event lookup to save runtime overhead // All macros can be used to measure variables registered in absolute addressing mode as well // Note that XCP_EVENT_SECTION_SET_ID expands to nothing on platforms where the event id is a link-time constant +// A function which triggers an event and measures its local variables (stack relative addressing) must not be inlined, mark it XCP_NOINLINE // @@@@ TODO: Not all permutations of name, string, index with At implemented @@ -605,6 +617,102 @@ extern const uint8_t *gXcpBaseAddr; XcpEventExt_Var(trg__AAS__##event_name, 1, xcp_get_frame_addr()); \ } +// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +// Capture of local variables + +// A local variable which the compiler keeps in a register has no memory location and can not be measured. Instead of forcing it +// into memory for its whole lifetime with 'volatile', the capture macros copy the given variables into a struct on the stack when +// the event is triggered, and pass the address of that struct as the base address of address extension 3. +// The offline A2L generator (xcpclient) unfolds the struct and registers its members with the names of the original variables, +// see docs/OFFLINE_A2L.md. The originals stay in registers, the copy of a scalar is a single store instruction. +// The capture struct is alive while the event is triggered, which is when the XCP server reads it, synchronously for DAQ and +// asynchronously for polling (the pending command is executed in the trigger). +// Restrictions: up to XCP_CAPTURE_MAX_COUNT variables, each given as a plain identifier of a local variable, a parameter or a +// global variable. Bitfield members can not be captured, and const qualified variables only in C, in C++ a const member would +// leave the capture struct without a default constructor. Not available with MSVC. +// In C++ the captured objects must be trivially copyable, they are copied byte wise. +// A function with a capture may be inlined, unlike a function which measures its local variables on the stack. + +#define XCP_CAPTURE_MAX_COUNT 16 + +#define XCP_CAP_CAT_(a, b) a##b +#define XCP_CAP_CAT(a, b) XCP_CAP_CAT_(a, b) + +// Number of arguments (1 to XCP_CAPTURE_MAX_COUNT) +#define XCP_CAP_NARG(...) XCP_CAP_NARG_(__VA_ARGS__, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) +#define XCP_CAP_NARG_(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, N, ...) N + +// Apply m(c, x) to each argument x, with the context c +#define XCP_CAP_FE_1(m, c, x) m(c, x) +#define XCP_CAP_FE_2(m, c, x, ...) m(c, x) XCP_CAP_FE_1(m, c, __VA_ARGS__) +#define XCP_CAP_FE_3(m, c, x, ...) m(c, x) XCP_CAP_FE_2(m, c, __VA_ARGS__) +#define XCP_CAP_FE_4(m, c, x, ...) m(c, x) XCP_CAP_FE_3(m, c, __VA_ARGS__) +#define XCP_CAP_FE_5(m, c, x, ...) m(c, x) XCP_CAP_FE_4(m, c, __VA_ARGS__) +#define XCP_CAP_FE_6(m, c, x, ...) m(c, x) XCP_CAP_FE_5(m, c, __VA_ARGS__) +#define XCP_CAP_FE_7(m, c, x, ...) m(c, x) XCP_CAP_FE_6(m, c, __VA_ARGS__) +#define XCP_CAP_FE_8(m, c, x, ...) m(c, x) XCP_CAP_FE_7(m, c, __VA_ARGS__) +#define XCP_CAP_FE_9(m, c, x, ...) m(c, x) XCP_CAP_FE_8(m, c, __VA_ARGS__) +#define XCP_CAP_FE_10(m, c, x, ...) m(c, x) XCP_CAP_FE_9(m, c, __VA_ARGS__) +#define XCP_CAP_FE_11(m, c, x, ...) m(c, x) XCP_CAP_FE_10(m, c, __VA_ARGS__) +#define XCP_CAP_FE_12(m, c, x, ...) m(c, x) XCP_CAP_FE_11(m, c, __VA_ARGS__) +#define XCP_CAP_FE_13(m, c, x, ...) m(c, x) XCP_CAP_FE_12(m, c, __VA_ARGS__) +#define XCP_CAP_FE_14(m, c, x, ...) m(c, x) XCP_CAP_FE_13(m, c, __VA_ARGS__) +#define XCP_CAP_FE_15(m, c, x, ...) m(c, x) XCP_CAP_FE_14(m, c, __VA_ARGS__) +#define XCP_CAP_FE_16(m, c, x, ...) m(c, x) XCP_CAP_FE_15(m, c, __VA_ARGS__) +#define XCP_CAP_FOR_EACH(m, c, ...) XCP_CAP_CAT(XCP_CAP_FE_, XCP_CAP_NARG(__VA_ARGS__))(m, c, __VA_ARGS__) + +// One struct member per captured variable, with the type of the variable and its name with a trailing underscore. The trailing +// underscore is needed in C++: a name must not change its meaning within a class scope, and a member named like the variable in +// its own type expression does exactly that, GCC rejects it. The offline A2L generator removes the trailing underscore again and +// registers the member with the name of the variable, see docs/OFFLINE_A2L.md +#define XCP_CAP_MEMBER(c, x) __typeof__(x) x##_; + +// Copy one variable into the capture struct. The casts avoid a discarded qualifier warning for a volatile variable, the builtin +// is expanded inline for the constant size, so the address of the variable does not force it into memory +#define XCP_CAP_COPY(c, x) __builtin_memcpy((void *)&(c).x##_, (const void *)&(x), sizeof(x)); + +// Declare the capture struct cap__ and fill it +#define XCP_CAPTURE(event_name, ...) \ + struct { \ + XCP_CAP_FOR_EACH(XCP_CAP_MEMBER, 0, __VA_ARGS__) \ + } cap__##event_name; \ + XCP_CAP_FOR_EACH(XCP_CAP_COPY, cap__##event_name, __VA_ARGS__) + +/// Trigger the XCP event 'event_name' and capture the given local variables for measurement, AASR +/// @param event_name Name given as identifier, the event must exist (DaqCreateEvent) +/// @param ... The local variables to capture, plain identifiers, up to XCP_CAPTURE_MAX_COUNT +#define DaqTriggerEventCapture(event_name, ...) \ + { \ + XCP_CAPTURE(event_name, __VA_ARGS__) \ + static tXcpEventId trg__AASR__##event_name = XCP_EVENT_SECTION_GET_LINKTIME_ID(evt__##event_name); \ + XCP_EVENT_SECTION_SET_ID(evt__##event_name, trg__AASR__##event_name); \ + XcpEventExt_Var(trg__AASR__##event_name, 2, xcp_get_frame_addr(), (const uint8_t *)&cap__##event_name); \ + } + +/// Trigger the XCP event 'event_name' with a given timestamp and capture the given local variables for measurement, AASR +/// @param event_name Name given as identifier, the event must exist (DaqCreateEvent) +/// @param clock Timestamp of the event +/// @param ... The local variables to capture, plain identifiers, up to XCP_CAPTURE_MAX_COUNT +#define DaqTriggerEventCaptureAt(event_name, clock, ...) \ + { \ + XCP_CAPTURE(event_name, __VA_ARGS__) \ + static tXcpEventId trg__AASR__##event_name = XCP_EVENT_SECTION_GET_LINKTIME_ID(evt__##event_name); \ + XCP_EVENT_SECTION_SET_ID(evt__##event_name, trg__AASR__##event_name); \ + XcpEventExtAt_Var(trg__AASR__##event_name, clock, 2, xcp_get_frame_addr(), (const uint8_t *)&cap__##event_name); \ + } + +/// Create and trigger the XCP event 'event_name' and capture the given local variables for measurement, AASR +/// @param event_name Name given as identifier +/// @param ... The local variables to capture, plain identifiers, up to XCP_CAPTURE_MAX_COUNT +#define DaqCreateAndTriggerEventCapture(event_name, ...) \ + { \ + XCP_CAPTURE(event_name, __VA_ARGS__) \ + static const tXcpEventDescriptor evt__##event_name XCP_EVENT_SECTION_ATTR = {.name = #event_name, .cycle_time_ns = 0, .priority = 0}; \ + static tXcpEventId trg__AASR__##event_name = XCP_EVENT_SECTION_GET_LINKTIME_ID(evt__##event_name); \ + XCP_EVENT_SECTION_SET_ID(evt__##event_name, trg__AASR__##event_name); \ + XcpEventExt_Var(trg__AASR__##event_name, 2, xcp_get_frame_addr(), (const uint8_t *)&cap__##event_name); \ + } + // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- // Enable/disable events @@ -667,15 +775,15 @@ extern const uint8_t *gXcpBaseAddr; // Note on local variable and function parameter visibility: // When runtime A2L generation is not used, the compiler may optimize local variables and function parameters to be stored in CPU registers only, without a memory location on the // stack In this case, XCPlite can not measure these variables since there is no memory location to read from, reading the register value is not supported yet To prevent this -// optimization, the variable must be marked as 'volatile' to force the compiler to always read and write it from/to memory The XCP_MEA and XCP_MEAS macros mark a (local) variable -// as volatile for this purpose An alternative is to use the DaqCapture macro to capture the variable in a hidden static variable for measurement +// optimization, the variable must be marked as 'volatile' to force the compiler to always read and write it from/to memory The XCP_MEAS macros mark a (local) variable +// as volatile for this purpose An alternative is to trigger the event with DaqTriggerEventCapture, which copies the variables into a capture struct on +// the stack and leaves the originals in their registers // The A2L updater/creator in xcpclient can handle only simple location expressions such as absolute addresses, stack relative addresses (CFA) and calibration segment relative -// addresses For complex cases, use the DaqCapture macro to capture the variable in a hidden static variable +// addresses For complex cases, use DaqTriggerEventCapture to capture the variables in a capture struct /// Attribute to mark a local variable as measurable /// Example usage: XCP_MEAS int32_t my_var = 0; -#define XCP_MEA volatile #define XCP_MEAS volatile // Macro to force a function parameter to be stored on the stack @@ -684,16 +792,6 @@ extern const uint8_t *gXcpBaseAddr; // Compiler memory barrier to prevent reordering of memory accesses across this point #define XCP_MEMORY_BARRIER() asm volatile("" ::: "memory") -/// Capture a local variable for measurement with a specific event -/// The variable must be in scope when the event is triggered with DaqTriggerEvent -/// The build time A2L file generator will find the hidden static variable 'daq__##event##__##var' and create the measurement with approriate addressing mode and -/// event association -#define DaqCapture(event, var) \ - do { \ - static __typeof__(var) daq__##event##__##var; \ - daq__##event##__##var = var; \ - } while (0) - // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- // Misc diff --git a/inc/xcplib.hpp b/inc/xcplib.hpp index 284b6fcb..fa22a76a 100644 --- a/inc/xcplib.hpp +++ b/inc/xcplib.hpp @@ -13,7 +13,7 @@ | ----------------------------------------------------------------------------*/ -#include // for std::once_flag, std::call_once +#include // for std::once_flag, std::call_once #include "xcplib_cfg.h" // for OPTION_xxx, must include the correct configuration override file XCPLIB_CFG_OVERRIDE #ifndef XCPLITE_CONFIGURATION diff --git a/src/a2l_writer.c b/src/a2l_writer.c index c927bc0b..cf647856 100644 --- a/src/a2l_writer.c +++ b/src/a2l_writer.c @@ -26,6 +26,8 @@ #include // for #include "dbg_print.h" // for DBG_PRINT +#include "platform.h" // for SNPRINTF, SPRINTF +#include "sockets.h" // for socketGetLocalAddr #include "xcp_cfg.h" // for XCP_xxx #include "xcplite.h" // for tXcpCalSeg, tXcpDaqLists, XcpXxx, ApplXcpXxx, ... #include "xcptl_cfg.h" // for XCPTL_xxx diff --git a/src/platform.c b/src/platform.c index 4a4b0fb8..b8973a2d 100644 --- a/src/platform.c +++ b/src/platform.c @@ -8,7 +8,6 @@ | Sleep | Threads | Mutex -| Sockets | Clock | Virtual memory | Keyboard @@ -16,7 +15,7 @@ | Code released into public domain, no attribution required ----------------------------------------------------------------------------*/ -#include "platform.h" +#include "platform.h" // for platform defines (WIN_, LINUX_, MACOS_) and specific implementation of sockets, clock, thread, mutex, spinlock #include // for malloc, free #if !defined(_WIN) @@ -436,1571 +435,6 @@ void mutexDestroy(MUTEX *m) { pthread_mutex_destroy(m); } #endif -/**************************************************************************/ -// Sockets -/**************************************************************************/ - -#if defined(OPTION_ENABLE_TCP) || defined(OPTION_ENABLE_UDP) - -const char *socketGetErrorString(int32_t err) { -#if !defined(_WIN) - return strerror(err); -#else - switch (err) { - case SOCKET_ERROR_ABORT: - return "connection aborted"; - case SOCKET_ERROR_RESET: - return "connection reset"; - case SOCKET_ERROR_INTR: - return "interrupted"; - case SOCKET_ERROR_TIMEDOUT: - return "timed out"; - case SOCKET_ERROR_WBLOCK: - return "would block"; - case SOCKET_ERROR_PIPE: - return "broken pipe"; - case SOCKET_ERROR_BADF: - return "bad file descriptor"; - case SOCKET_ERROR_NOTCONN: - return "not connected"; - default: - return "unknown socket error"; - } -#endif -} - -//-------------------------------------------------------------------------- -// FreeRTOS platforms - -#if defined(_FREE_RTOS) && !defined(FREE_RTOS_POSIX_SIM) // FreeRTOS sockets - -#ifdef OPTION_ENABLE_TCP -#error "FreeRTOS TCP socket functions not implemented yet" -#endif - -#if defined(OPTION_FREERTOS_LWIP) -#include "lwip/errno.h" // lwIP errno values mapped to POSIX codes -#include "lwip/sockets.h" // lwip_socket, lwip_bind, lwip_sendto, lwip_recvfrom, lwip_close, lwip_shutdown, lwip_setsockopt -#endif - -// socketStartup: lwIP networking is initialised by the application (e.g. tcpip_init) — no-op here -bool socketStartup(void) { -#if defined(OPTION_FREERTOS_LWIP) - return true; -#else - DBG_PRINT_ERROR("FREE_RTOS:socketStartup not implemented\n"); - return true; -#endif -} - -// socketCleanup: no teardown required for lwIP -void socketCleanup(void) { -#if !defined(OPTION_FREERTOS_LWIP) - DBG_PRINT_ERROR("FREE_RTOS:socketCleanup not implemented\n"); -#endif -} - -// Create a UDP socket (TCP not supported: OPTION_ENABLE_TCP must not be defined) -bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags) { -#if defined(OPTION_FREERTOS_LWIP) - assert(socketp != NULL); - assert(!(flags & SOCKET_MODE_TCP)); // TCP not supported on FreeRTOS/lwIP - - int sock = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sock < 0) { - DBG_PRINTF_ERROR("socketOpen: lwip_socket failed (errno=%d,%s)\n", errno, socketGetErrorString(errno)); - return false; - } - if (flags & SOCKET_MODE_REUSEADDR) { - int yes = 1; - if (lwip_setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) { - DBG_PRINTF_WARNING("socketOpen: SO_REUSEADDR failed (errno=%d,%s)\n", errno, socketGetErrorString(errno)); - } - } - *socketp = sock; - DBG_PRINTF5("socketOpen: lwIP UDP socket %d opened\n", sock); - return true; -#else - DBG_PRINT_ERROR("FREE_RTOS:socketOpen not implemented\n"); - return false; -#endif -} - -// Bind socket to a local address and port -// addr: network-byte-order IPv4 address; NULL or 0.x.x.x binds to INADDR_ANY -bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port) { -#if defined(OPTION_FREERTOS_LWIP) - assert(socket != INVALID_SOCKET_HANDLE); - struct sockaddr_in a; - memset(&a, 0, sizeof(a)); - a.sin_family = AF_INET; - a.sin_port = htons(port); - if (addr != NULL && addr[0] != 0) { - memcpy(&a.sin_addr.s_addr, addr, sizeof(a.sin_addr.s_addr)); - } else { - a.sin_addr.s_addr = htonl(INADDR_ANY); - } - if (lwip_bind(socket, (struct sockaddr *)&a, sizeof(a)) < 0) { - DBG_PRINTF_ERROR("socketBind: lwip_bind failed (errno=%d,%s) on port %u\n", errno, socketGetErrorString(errno), port); - return false; - } - DBG_PRINTF5("socketBind: bound to port %u\n", port); - return true; -#else - DBG_PRINT_ERROR("FREE_RTOS:socketBind not implemented\n"); - return false; -#endif -} - -// Shutdown socket — unblocks a thread blocked in socketRecvFrom -bool socketShutdown(SOCKET_HANDLE socket) { -#if defined(OPTION_FREERTOS_LWIP) - if (socket != INVALID_SOCKET_HANDLE) { - lwip_shutdown(socket, SHUT_RDWR); - } - return true; -#else - DBG_PRINT_ERROR("FREE_RTOS:socketShutdown not implemented\n"); - return true; -#endif -} - -// Close socket and free the handle -bool socketClose(SOCKET_HANDLE *socketp) { -#if defined(OPTION_FREERTOS_LWIP) - assert(socketp != NULL); - if (*socketp != INVALID_SOCKET_HANDLE) { - lwip_close(*socketp); - *socketp = INVALID_SOCKET_HANDLE; - } - return true; -#else - DBG_PRINT_ERROR("FREE_RTOS:socketClose not implemented\n"); - return true; -#endif -} - -// Receive a UDP datagram (blocking) -// Returns: > 0 bytes received, 0 on timeout/EAGAIN, -1 on error or socket closed -int16_t socketRecvFrom(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, uint8_t *srcAddr, uint16_t *srcPort, uint64_t *time) { -#if defined(OPTION_FREERTOS_LWIP) - assert(socket != INVALID_SOCKET_HANDLE); - struct sockaddr_in src; - socklen_t srclen = sizeof(src); - memset(&src, 0, sizeof(src)); - int16_t n = (int16_t)lwip_recvfrom(socket, buffer, bufferSize, 0, (struct sockaddr *)&src, &srclen); - if (n == 0) { - return 0; // Zero-length datagram or graceful close - } - if (n < 0) { - int32_t err = errno; - if (socketTimeout(err)) { - return 0; // Timeout — caller loops and does background work - } - DBG_PRINTF_ERROR("socketRecvFrom: lwip_recvfrom failed (errno=%d,%s)\n", err, socketGetErrorString(err)); - return -1; - } - if (srcAddr != NULL) { - memcpy(srcAddr, &src.sin_addr.s_addr, 4); - } - if (srcPort != NULL) { - *srcPort = ntohs(src.sin_port); - } - if (time != NULL) { - *time = clockGet(); // No hardware timestamps on lwIP; use XCP clock - } - return n; -#else - DBG_PRINT_ERROR("FREE_RTOS:socketRecvFrom not implemented\n"); - return -1; -#endif -} - -// Send a UDP datagram to addr:port -// Returns: bytes sent, 0 on closed socket, -1 on error -int16_t socketSendTo(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize, const uint8_t *addr, uint16_t port, uint64_t *time) { -#if defined(OPTION_FREERTOS_LWIP) - assert(socket != INVALID_SOCKET_HANDLE); - assert(addr != NULL); - struct sockaddr_in dst; - memset(&dst, 0, sizeof(dst)); - dst.sin_family = AF_INET; - dst.sin_port = htons(port); - memcpy(&dst.sin_addr.s_addr, addr, sizeof(dst.sin_addr.s_addr)); - if (time != NULL) { - *time = clockGet(); // No hardware timestamps on lwIP; use XCP clock at send time - } - int16_t n = (int16_t)lwip_sendto(socket, buffer, bufferSize, 0, (struct sockaddr *)&dst, sizeof(dst)); - if (n < 0) { - int32_t err = errno; - if (socketIsClosed(err)) { - return 0; // Socket closed - } - DBG_PRINTF_ERROR("socketSendTo: lwip_sendto failed (errno=%d,%s)\n", err, socketGetErrorString(err)); - return -1; - } - return n; -#else - DBG_PRINT_ERROR("FREE_RTOS:socketSendTo not implemented\n"); - return -1; -#endif -} - -// Set receive timeout on a blocking socket -// timeoutMs == 0 restores infinite blocking -bool socketSetTimeout(SOCKET_HANDLE socket, uint32_t timeoutMs) { -#if defined(OPTION_FREERTOS_LWIP) - assert(socket != INVALID_SOCKET_HANDLE); - struct timeval tv; - tv.tv_sec = (long)(timeoutMs / 1000U); - tv.tv_usec = (long)(timeoutMs % 1000U) * 1000L; - if (lwip_setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { - DBG_PRINTF_WARNING("socketSetTimeout: lwip_setsockopt SO_RCVTIMEO failed (errno=%d,%s)\n", errno, socketGetErrorString(errno)); - return false; - } - DBG_PRINTF5("socketSetTimeout: set to %u ms\n", timeoutMs); - return true; -#else - DBG_PRINT_ERROR("FREE_RTOS:socketSetTimeout not implemented\n"); - return true; -#endif -} - -#else - -//-------------------------------------------------------------------------- -// Non-Windows platforms -#if !defined(_WIN) - -#include // for getifaddrs, struct ifaddrs - -#include // for htons, htonl -#include // for sockaddr_in -#include // for socket functions - -#if defined(_LINUX) // Linux platform - -#include // for if_nametoindex, struct ifreq, IFNAMSIZ -#include // for struct sockaddr_ll (AF_PACKET, used by socketGetMAC) - -#if defined(OPTION_SOCKET_HW_TIMESTAMPS) // Linux platform hardware time stamping support -#include -#include -#include // for SIOCSHWTSTAMP -#include -#endif // defined(OPTION_SOCKET_HW_TIMESTAMPS) - -#endif // Linux - -#if defined(_MACOS) || defined(_QNX) // MacOS or QNX platforms -#include -#endif // MacOS or QNX platforms - -bool socketStartup(void) { return true; } - -void socketCleanup(void) {} - -// Create a socket, TCP or UDP -// flag SOCKET_MODE_HW_TIMESTAMPING: Enable hardware timestamping (Linux only, requires root) -// flag SOCKET_MODE_SW_TIMESTAMPING: Enable software timestamping (Linux only) -bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags) { - - assert(socketp != NULL); - SOCKET sock = INVALID_SOCKET; - - bool useTCP = flags & SOCKET_MODE_TCP; - bool reuseaddr = flags & SOCKET_MODE_REUSEADDR; - - // Create a socket - sock = socket(AF_INET, useTCP ? SOCK_STREAM : SOCK_DGRAM, 0); - if (sock < 0) { - DBG_PRINT_ERROR("cannot open socket!\n"); - return 0; - } - - if (reuseaddr) { - int yes = 1; - if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) { - DBG_PRINTF_WARNING("Failed to enable SO_REUSEADDR on socket (errno=%d,%s)\n", errno, socketGetErrorString(errno)); - } else { - DBG_PRINT5("SO_REUSEADDR enabled on socket\n"); - } - } - -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - if (flags & SOCKET_MODE_GET_IF_INFO) { - int yes = 1; - if (setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &yes, sizeof(yes)) < 0) { - DBG_PRINTF_WARNING("Failed to enable IP_PKTINFO on socket (errno=%d,%s)\n", errno, socketGetErrorString(errno)); - } else { - DBG_PRINT5("IP_PKTINFO enabled\n"); - } - } -#endif - -// Enable timestamps if requested -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - - bool hw_timestamps = flags & SOCKET_MODE_HW_TIMESTAMPING; - bool sw_timestamps = flags & SOCKET_MODE_SW_TIMESTAMPING; - if (hw_timestamps) { - // Enable SO_TIMESTAMPING for full hardware and software timestamping support - // This is required for PTP SYNC message timestamping - // SO_TIMESTAMPING supersedes SO_TIMESTAMPNS and provides: - // - Hardware RX/TX timestamps (if NIC/driver supports it) - // - Software RX/TX timestamps (always available as fallback) - // - Raw hardware clock access - // - // The timestamp array returned in control messages: - // [0] = Software timestamp - // [1] = Deprecated (legacy) - // [2] = Hardware timestamp (from NIC PHY) - uint32_t flags = SOF_TIMESTAMPING_TX_SOFTWARE | // Software TX timestamp (always available) - SOF_TIMESTAMPING_RX_SOFTWARE | // Software RX timestamp (always available) - SOF_TIMESTAMPING_SOFTWARE | // Enable software timestamp generation - SOF_TIMESTAMPING_TX_HARDWARE | // Hardware TX timestamp (if available) - SOF_TIMESTAMPING_RX_HARDWARE | // Hardware RX timestamp (if available) - SOF_TIMESTAMPING_RAW_HARDWARE | // Use raw hardware clock (required for HW timestamps) - SOF_TIMESTAMPING_OPT_TSONLY | // Return only timestamp, not packet data - // SOF_TIMESTAMPING_OPT_TX_SWHW | // Generate both SW and HW TX timestamps - 0; - if (setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags)) < 0) { - DBG_PRINTF_ERROR("Failed to enable socket hardware timestamps (SO_TIMESTAMPING, errno=%d,%s)\n", errno, socketGetErrorString(errno)); - } else { - DBG_PRINTF5("Hardware timestamping enabled on socket (SO_TIMESTAMPING flags=0x%X)\n", flags); - } - } - - if (sw_timestamps) { - - // Enable software timestamps, if required - int yes = 1; - if (setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPNS, &yes, sizeof(yes)) < 0) { - DBG_PRINTF_ERROR("Failed to enable socket software timestamps (SO_TIMESTAMPNS, errno=%d,%s)\n", errno, socketGetErrorString(errno)); - } else { - DBG_PRINT5("Software timestamps enabled on socket (SO_TIMESTAMPNS)\n"); - } - } -#endif - -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - SOCKET_HANDLE socket = (struct socket *)malloc(sizeof(struct socket)); - memset(socket, 0, sizeof(struct socket)); - socket->sock = sock; - *socketp = socket; -#else - *socketp = sock; -#endif - return true; -} - -bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port) { - - assert(socket != INVALID_SOCKET_HANDLE); - assert(addr != NULL); - - SOCKET sock = SOCKET_FD(socket); - - // Bind the socket to any address and the specified port - SOCKADDR_IN a; - a.sin_family = AF_INET; - if (addr != NULL && addr[0] != 0) { - a.sin_addr.s_addr = *(uint32_t *)addr; // Bind to the specific addr given - } else { - a.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to any addr - } - a.sin_port = htons(port); - if (bind(sock, (SOCKADDR *)&a, sizeof(a)) < 0) { - DBG_PRINTF_ERROR("socketBind failed (errno=%d,%s) - cannot bind on %u.%u.%u.%u port %u!\n", socketGetLastError(), socketGetErrorString(socketGetLastError()), - addr ? addr[0] : 0, addr ? addr[1] : 0, addr ? addr[2] : 0, addr ? addr[3] : 0, port); - if (port < 1024) { - DBG_PRINT_ERROR("Binding to ports <1024 may require root privileges on Linux!\n"); - } - return 0; - } - return true; -} - -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - -// Bind socket to a specific network interface by name (Linux only) -// This is useful for multicast reception on a specific interface while binding to INADDR_ANY -// Requires root privileges on Linux -bool socketBindToDevice(SOCKET_HANDLE socket, const char *ifname) { - - assert(socket != INVALID_SOCKET_HANDLE); - - int sock = SOCKET_FD(socket); - if (ifname != NULL && ifname[0] != '\0') { - if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname)) < 0) { - DBG_PRINTF_ERROR("socketBindToDevice failed (errno=%d,%s) - cannot bind to device %s !\n", socketGetLastError(), socketGetErrorString(socketGetLastError()), ifname); - return false; - } - DBG_PRINTF3("Socket bound to device %s\n", ifname); - - // Store interface name - strncpy(socket->ifname, ifname, sizeof(socket->ifname) - 1); - socket->ifname[sizeof(socket->ifname) - 1] = '\0'; - - // Store interface index - unsigned int ifindex = if_nametoindex(ifname); - socket->ifindex = ifindex; - } - return true; -} - -// Enable hardware timestamping and/or software on a network interface -// This configures the NIC driver to generate timestamps for PTP packets -// Must be called after socket is created and bound -// ifname: Network interface name (e.g., "eth0"). If NULL, uses first non-loopback interface. -// Returns true on success, false on failure (falls back to software timestamps) -bool socketEnableTimestamps(SOCKET_HANDLE socket, bool ptpOnly) { - - assert(socket != NULL); - int sock = socket->sock; - - struct ifreq ifr; - struct hwtstamp_config hwconfig; - - // Use socket's ifname - const char *ifname = socket->ifname[0] != '\0' ? socket->ifname : NULL; - - memset(&ifr, 0, sizeof(ifr)); - memset(&hwconfig, 0, sizeof(hwconfig)); - - // If no interface specified, try to find the first non-loopback interface - if (ifname == NULL) { - DBG_PRINT_WARNING("socketEnableTimestamps: No ifname specified, searching for first non-loopback interface\n"); - struct ifaddrs *ifaddrs, *ifa; - if (getifaddrs(&ifaddrs) == 0) { - for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { - if (ifa->ifa_addr != NULL && ifa->ifa_addr->sa_family == AF_INET) { - struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; - if (sa->sin_addr.s_addr != htonl(INADDR_LOOPBACK)) { - strncpy(ifr.ifr_name, ifa->ifa_name, IFNAMSIZ - 1); - break; - } - } - } - freeifaddrs(ifaddrs); - } - if (ifr.ifr_name[0] == '\0') { - DBG_PRINT_ERROR("socketEnableTimestamps: No suitable interface found\n"); - return false; - } - } else { - strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); - } - - DBG_PRINTF5("socketEnableTimestamps: Enabling timestamps on interface %s\n", ifr.ifr_name); - - // Configure hardware timestamping: - // tx_type: HWTSTAMP_TX_ON enables TX timestamps for all packets - // rx_filter: HWTSTAMP_FILTER_ALL or HWTSTAMP_FILTER_PTP_V2_EVENT for PTP packets - hwconfig.flags = 0; - hwconfig.tx_type = HWTSTAMP_TX_ON; // Enable TX hardware timestamps - hwconfig.rx_filter = ptpOnly ? HWTSTAMP_FILTER_PTP_V2_EVENT : HWTSTAMP_FILTER_ALL; // Timestamp all incoming packets (or use HWTSTAMP_FILTER_PTP_V2_EVENT for PTP only) - - ifr.ifr_data = (char *)&hwconfig; - - if (ioctl(sock, SIOCSHWTSTAMP, &ifr) < 0) { - - // SIOCSHWTSTAMP requires CAP_NET_ADMIN or root privileges - // Some NICs may not support it, or the filter mode may not be supported - DBG_PRINTF_WARNING("socketEnableTimestamps: ioctl SIOCSHWTSTAMP failed for %s (errno=%d: %s)\n", ifr.ifr_name, errno, strerror(errno)); - DBG_PRINT_WARNING("Hardware timestamping may require root privileges or may not be supported by this NIC\n"); - - // Try with a less restrictive filter - hwconfig.rx_filter = HWTSTAMP_FILTER_NONE; // No RX filter, just enable TX - hwconfig.tx_type = HWTSTAMP_TX_ON; - if (ioctl(sock, SIOCSHWTSTAMP, &ifr) < 0) { - DBG_PRINTF_WARNING("socketEnableTimestamps: Fallback also failed (errno=%d: %s)\n", errno, strerror(errno)); - return false; - } - DBG_PRINTF_WARNING("socketEnableTimestamps: Enabled TX-only hardware timestamps on %s\n", ifr.ifr_name); - return true; - } - - DBG_PRINTF5("Hardware timestamping enabled on %s (tx_type=%d, rx_filter=%d)\n", ifr.ifr_name, hwconfig.tx_type, hwconfig.rx_filter); - return true; -} - -#else - -// Hardware timestamping not supported on this platform -// Stub for non-Linux platforms -bool socketEnableTimestamps(SOCKET_HANDLE socket, bool ptpOnly) { - (void)socket; - (void)ptpOnly; - DBG_PRINT_ERROR("socketEnableTimestamps: Socket hardware timestamping not supported on this platform!\n"); - return false; -} - -#endif // Linux with OPTION_SOCKET_HW_TIMESTAMPS - -// Shutdown socket -// Block rx and tx direction -bool socketShutdown(SOCKET_HANDLE socket) { - if (socket != INVALID_SOCKET_HANDLE) { - shutdown(SOCKET_FD(socket), SHUT_RDWR); - } - return true; -} - -// Close socket -// Make addr reusable -bool socketClose(SOCKET_HANDLE *socketp) { - assert(socketp != NULL); -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - if (*socketp != NULL) { - close((*socketp)->sock); - free(*socketp); - *socketp = NULL; - } -#else - if (*socketp != INVALID_SOCKET_HANDLE) { - close(*socketp); - *socketp = INVALID_SOCKET_HANDLE; - } -#endif - return true; -} - -// Get MAC address of a network interface by name -bool socketGetMAC(char *ifname, uint8_t *mac) { - - assert(ifname != NULL); - struct ifaddrs *ifaddrs, *ifa; - if (getifaddrs(&ifaddrs) == 0) { - for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { - if (!strcmp(ifa->ifa_name, ifname)) { -#if defined(_MACOS) || defined(_QNX) - if (ifa->ifa_addr->sa_family == AF_LINK) { - memcpy(mac, (uint8_t *)LLADDR((struct sockaddr_dl *)ifa->ifa_addr), 6); - DBG_PRINTF5(" %s: MAC = %02X-%02X-%02X-%02X-%02X-%02X\n", ifa->ifa_name, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - } -#else - if (ifa->ifa_addr->sa_family == AF_PACKET) { - struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; - memcpy(mac, s->sll_addr, 6); - DBG_PRINTF5(" %s: MAC = %02X-%02X-%02X-%02X-%02X-%02X\n", ifa->ifa_name, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - break; - } -#endif - } - } - freeifaddrs(ifaddrs); - return (ifa != NULL); - } - return false; -} - -#ifdef OPTION_ENABLE_GET_LOCAL_ADDR - -// Get local IP address and MAC address of the first non-loopback interface -bool socketGetLocalAddr(uint8_t *mac, uint8_t *addr) { - static uint32_t __addr1 = 0; - static uint8_t __mac1[6] = {0, 0, 0, 0, 0, 0}; -#ifdef DBG_LEVEL - char strbuf[64]; // @@@@ STACK buffer for IP addr string -#endif - if (__addr1 == 0) { - struct ifaddrs *ifaddrs, *ifa; - struct ifaddrs *ifa1 = NULL; - if (-1 != getifaddrs(&ifaddrs)) { - for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { - if ((NULL != ifa->ifa_addr) && (AF_INET == ifa->ifa_addr->sa_family)) { // IPV4 - struct sockaddr_in *sa = (struct sockaddr_in *)(ifa->ifa_addr); - if (0x100007f != sa->sin_addr.s_addr) { /* not 127.0.0.1 */ - if (__addr1 == 0) { - __addr1 = sa->sin_addr.s_addr; - ifa1 = ifa; - break; - } - } - } - } - if (__addr1 != 0 && ifa1 != NULL) { - socketGetMAC(ifa1->ifa_name, __mac1); -#ifdef DBG_LEVEL - if (DBG_LEVEL >= 5) { - inet_ntop(AF_INET, &__addr1, strbuf, sizeof(strbuf)); - printf(" Use IPV4 adapter %s with IP=%s, MAC=%02X-%02X-%02X-%02X-%02X-%02X for A2L info and clock " - "UUID\n", - ifa1->ifa_name, strbuf, __mac1[0], __mac1[1], __mac1[2], __mac1[3], __mac1[4], __mac1[5]); - } -#endif - } - freeifaddrs(ifaddrs); - } - } - if (__addr1 != 0) { - if (mac) - memcpy(mac, __mac1, 6); - if (addr) - memcpy(addr, &__addr1, 4); - return true; - } else { - return false; - } -} - -#endif // OPTION_ENABLE_GET_LOCAL_ADDR - -//-------------------------------------------------------------------------- -#else // Windows platform - -// Winsock -#pragma comment(lib, "ws2_32.lib") - -int32_t socketGetLastError(void) { return WSAGetLastError(); } - -bool socketStartup(void) { - - int err; - WORD wsaVersionRequested; - WSADATA wsaData; - - // Init Winsock2 - wsaVersionRequested = MAKEWORD(2, 2); - err = WSAStartup(wsaVersionRequested, &wsaData); - if (err != 0) { - DBG_PRINTF_ERROR("WSAStartup failed with error %d!\n", err); - return false; - } - if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { // Confirm that the WinSock DLL supports 2.2 - DBG_PRINT_ERROR("Could not find a usable version of Winsock.dll!\n"); - WSACleanup(); - return false; - } - - return true; -} - -void socketCleanup(void) { WSACleanup(); } - -// Create a socket, TCP or UDP -bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags) { - - assert(socketp != NULL); - SOCKET sock = -1; - - bool useTCP = flags & SOCKET_MODE_TCP; - bool reuseaddr = flags & SOCKET_MODE_REUSEADDR; - - // Create a socket - if (!useTCP) { - sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - -// Avoid send to UDP nowhere problem (ignore ICMP host unreachable - server has no open socket on client port) -// (stack-overflow 34242622) -#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12) - bool bNewBehavior = false; - DWORD dwBytesReturned = 0; - if (sock != INVALID_SOCKET) { - WSAIoctl(sock, SIO_UDP_CONNRESET, &bNewBehavior, sizeof bNewBehavior, NULL, 0, &dwBytesReturned, NULL, NULL); - } - } else { - sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - } - if (sock == INVALID_SOCKET) { - DBG_PRINTF_ERROR("socketOpen failed (errno=%d,%s) - could not create socket!\n", socketGetLastError(), socketGetErrorString(socketGetLastError())); - return false; - } - - // Make addr reusable - if (reuseaddr) { - uint32_t one = 1; - if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&one, sizeof(one)) < 0) { - DBG_PRINTF_WARNING("socketOpen failed (errno=%d,%s) - could not enable SO_REUSEADDR on socket\n", socketGetLastError(), socketGetErrorString(socketGetLastError())); - } - } - - *socketp = sock; - return true; -} - -bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port) { - - assert(socket != INVALID_SOCKET_HANDLE); - SOCKET sock = socket; - - // Bind the socket to any address and the specified port - SOCKADDR_IN a; - a.sin_family = AF_INET; - if (addr != NULL && *(uint32_t *)addr != 0) { - a.sin_addr.s_addr = *(uint32_t *)addr; // Bind to the specific addr given - } else { // NULL or 0.x.x.x - a.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to any addr - } - a.sin_port = htons(port); - if (bind(sock, (SOCKADDR *)&a, sizeof(a)) < 0) { - if (socketGetLastError() == WSAEADDRINUSE) { - DBG_PRINT_ERROR("Port is already in use!\n"); - } else { - DBG_PRINTF_ERROR("socketBind failed (errno=%d,%s) - cannot bind on %u.%u.%u.%u port %u!\n", socketGetLastError(), socketGetErrorString(socketGetLastError()), - addr ? addr[0] : 0, addr ? addr[1] : 0, addr ? addr[2] : 0, addr ? addr[3] : 0, port); - } - return false; - } - return true; -} - -// Shutdown socket -// Block rx and tx direction -bool socketShutdown(SOCKET_HANDLE socket) { - - assert(socket != INVALID_SOCKET_HANDLE); - SOCKET sock = socket; - - if (sock != INVALID_SOCKET) { - shutdown(sock, SD_BOTH); - } - return true; -} - -// Close socket -// Make addr reusable -bool socketClose(SOCKET_HANDLE *socketp) { - - assert(socketp != NULL); - if (*socketp != INVALID_SOCKET_HANDLE) { - closesocket(*socketp); - *socketp = INVALID_SOCKET_HANDLE; - } - return true; -} - -#ifdef OPTION_ENABLE_GET_LOCAL_ADDR - -#include -#pragma comment(lib, "IPHLPAPI.lib") -#define _WINSOCK_DEPRECATED_NO_WARNINGS - -bool socketGetLocalAddr(uint8_t *mac, uint8_t *addr) { - - static uint8_t __addr1[4] = {0, 0, 0, 0}; - static uint8_t __mac1[6] = {0, 0, 0, 0, 0, 0}; - uint32_t a; - PIP_ADAPTER_INFO pAdapterInfo; - PIP_ADAPTER_INFO pAdapter = NULL; - DWORD dwRetVal = 0; - - if (__addr1[0] == 0) { - - ULONG ulOutBufLen = sizeof(IP_ADAPTER_INFO); - pAdapterInfo = (IP_ADAPTER_INFO *)malloc(sizeof(IP_ADAPTER_INFO)); - if (pAdapterInfo == NULL) - return 0; - - if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) { - free(pAdapterInfo); - pAdapterInfo = (IP_ADAPTER_INFO *)malloc(ulOutBufLen); - if (pAdapterInfo == NULL) - return 0; - } - if ((dwRetVal = GetAdaptersInfo(pAdapterInfo, &ulOutBufLen)) == NO_ERROR) { - pAdapter = pAdapterInfo; - while (pAdapter) { - if (pAdapter->Type == MIB_IF_TYPE_ETHERNET) { - inet_pton(AF_INET, pAdapter->IpAddressList.IpAddress.String, &a); - if (a != 0) { -#ifdef DBG_LEVEL - DBG_PRINTF5(" Ethernet adapter %" PRIu32 ":", (uint32_t)pAdapter->Index); - // DBG_PRINTF5(" %s", pAdapter->AdapterName); - DBG_PRINTF5(" %s", pAdapter->Description); - DBG_PRINTF5(" %02X-%02X-%02X-%02X-%02X-%02X", pAdapter->Address[0], pAdapter->Address[1], pAdapter->Address[2], pAdapter->Address[3], pAdapter->Address[4], - pAdapter->Address[5]); - DBG_PRINTF5(" %s", pAdapter->IpAddressList.IpAddress.String); - // DBG_PRINTF5(" %s", pAdapter->IpAddressList.IpMask.String); - // DBG_PRINTF5(" Gateway: %s", pAdapter->GatewayList.IpAddress.String); - // if (pAdapter->DhcpEnabled) DBG_PRINTF5(" DHCP"); - DBG_PRINT5("\n"); -#endif - if (__addr1[0] == 0) { - memcpy(__addr1, (uint8_t *)&a, 4); - memcpy(__mac1, pAdapter->Address, 6); - } - } - } - pAdapter = pAdapter->Next; - } - } - if (pAdapterInfo) - free(pAdapterInfo); - } - - if (__addr1[0] != 0) { - if (mac) - memcpy(mac, __mac1, 6); - if (addr) - memcpy(addr, __addr1, 4); - return true; - } - return false; -} - -#endif // OPTION_ENABLE_GET_LOCAL_ADDR - -#endif // _WIN - -// Set receive timeout on a socket -// timeoutMs: timeout in milliseconds, 0 = infinite blocking (restore default) -bool socketSetTimeout(SOCKET_HANDLE socket, uint32_t timeoutMs) { - assert(socket != INVALID_SOCKET_HANDLE); -#if defined(_WIN) - DWORD tv = (DWORD)timeoutMs; - if (setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv)) < 0) { - DBG_PRINTF_WARNING("socketSetTimeout: setsockopt SO_RCVTIMEO failed (errno=%d,%s)\n", socketGetLastError(), socketGetErrorString(socketGetLastError())); - return false; - } -#else - struct timeval tv; - tv.tv_sec = timeoutMs / 1000; - tv.tv_usec = (int32_t)(timeoutMs % 1000) * 1000; - if (setsockopt(SOCKET_FD(socket), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { - DBG_PRINTF_WARNING("socketSetTimeout: setsockopt SO_RCVTIMEO failed (errno=%d,%s)\n", errno, socketGetErrorString(errno)); - return false; - } -#endif - DBG_PRINTF5("socketSetTimeout: set to %u ms\n", timeoutMs); - return true; -} - -#if defined(OPTION_ENABLE_TCP) - -// Listen on a TCP socket -bool socketListen(SOCKET_HANDLE socket) { - assert(socket != INVALID_SOCKET_HANDLE); - if (listen(SOCKET_FD(socket), 5)) { - DBG_PRINTF_ERROR("socketListen failed (errno=%d,%s)!\n", socketGetLastError(), socketGetErrorString(socketGetLastError())); - return 0; - } - return 1; -} - -// Accept a connection on a listening TCP socket -// Returns the remote address if addr != NULL -SOCKET_HANDLE socketAccept(SOCKET_HANDLE listenSocket, uint8_t *addr) { - assert(listenSocket != INVALID_SOCKET_HANDLE); - struct sockaddr_in sa; - socklen_t sa_size = sizeof(sa); - SOCKET sock = accept(SOCKET_FD(listenSocket), (struct sockaddr *)&sa, &sa_size); - if (addr) - *(uint32_t *)addr = sa.sin_addr.s_addr; -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - SOCKET_HANDLE socket = (struct socket *)malloc(sizeof(struct socket)); - memset(socket, 0, sizeof(struct socket)); - socket->sock = sock; - socket->ifindex = listenSocket->ifindex; - memcpy(socket->ifname, listenSocket->ifname, sizeof(socket->ifname)); - return socket; -#else - return sock; -#endif -} - -#endif // OPTION_ENABLE_TCP - -#if !defined(_FREE_RTOS) || defined(FREE_RTOS_POSIX_SIM) - -// Join a multicast group on a UDP socket -// maddr: Multicast group address (network byte order) -bool socketJoin(SOCKET_HANDLE socket, const uint8_t *maddr, const uint8_t *ifaddr, const char *ifname) { - - assert(socket != INVALID_SOCKET_HANDLE); - SOCKET sock = SOCKET_FD(socket); - -#if defined(_LINUX) - // On Linux, use ip_mreqn which allows specifying interface by name or index - struct ip_mreqn group; - memset(&group, 0, sizeof(group)); - group.imr_multiaddr.s_addr = *(uint32_t *)maddr; - - // Priority: interface name > interface address > INADDR_ANY - if (ifname != NULL && ifname[0] != '\0') { - // Use interface name (most reliable for multicast on Linux) - group.imr_ifindex = if_nametoindex(ifname); - if (group.imr_ifindex == 0) { - DBG_PRINTF_ERROR("socketJoin: Interface %s not found!\n", ifname); - return 0; - } -#if defined(OPTION_SOCKET_HW_TIMESTAMPS) - socket->ifindex = group.imr_ifindex; - strncpy(socket->ifname, ifname, sizeof(socket->ifname) - 1); - socket->ifname[sizeof(socket->ifname) - 1] = '\0'; -#endif - DBG_PRINTF5("Joining multicast group on interface %s (index %d)\n", ifname, group.imr_ifindex); - -#if defined(OPTION_SOCKET_HW_TIMESTAMPS) - // Get MAC address for the interface and save it in the socket structure - if (!socketGetMAC(socket->ifname, socket->ifmac)) { - DBG_PRINTF_WARNING("socketJoin: Failed to get MAC address for interface %s!\n", ifname); - } -#endif - - } else if (ifaddr != NULL && !(ifaddr[0] == 0 && ifaddr[1] == 0 && ifaddr[2] == 0 && ifaddr[3] == 0)) { - // Use interface address - group.imr_address.s_addr = *(uint32_t *)ifaddr; -#if defined(OPTION_SOCKET_HW_TIMESTAMPS) - socket->ifaddr = *(uint32_t *)ifaddr; -#endif - - DBG_PRINTF5("Joining multicast group on interface address %u.%u.%u.%u\n", ifaddr[0], ifaddr[1], ifaddr[2], ifaddr[3]); - - } else { - // Use INADDR_ANY (kernel picks interface based on routing) - group.imr_address.s_addr = htonl(INADDR_ANY); - - DBG_PRINT5("Joining multicast group on INADDR_ANY\n"); - } - - if (0 > setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char *)&group, sizeof(group))) { - DBG_PRINTF_ERROR("socketJoin failed (errno=%d,%s) - can't set multicast socket option IP_ADD_MEMBERSHIP!\n", socketGetLastError(), - socketGetErrorString(socketGetLastError())); - return 0; - } -#else - // Non-Linux platforms: use standard struct ip_mreq (address-based only) - struct ip_mreq group; - group.imr_multiaddr.s_addr = *(uint32_t *)maddr; - // Use the specified interface address, or INADDR_ANY if NULL or 0.0.0.0 - if (ifaddr == NULL || (ifaddr[0] == 0 && ifaddr[1] == 0 && ifaddr[2] == 0 && ifaddr[3] == 0)) { - group.imr_interface.s_addr = htonl(INADDR_ANY); - } else { - group.imr_interface.s_addr = *(uint32_t *)ifaddr; - } - if (0 > setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char *)&group, sizeof(group))) { - DBG_PRINTF_ERROR("socketJoin failed (errno=%d,%s) - can't set multicast socket option IP_ADD_MEMBERSHIP!\n", socketGetLastError(), - socketGetErrorString(socketGetLastError())); - return 0; - } - (void)ifname; // Unused on non-Linux platforms -#endif - return 1; -} - -// Receive from UDP socket -// Blocking mode only, with optional timeout set with socketSetTimeout() -// Returns optional receive timestamps if (time != NULL) -// Support hardware timestamps if enabled on the socket and with OPTION_SOCKET_HW_TIMESTAMPS defined, otherwise system time is used -// Return values: -// n > 0 : number of bytes received -// n == 0 : timeout (set with socketTimeout) expired or would-block — no data yet, caller should loop and do background work -// n < 0 : socket closed (graceful or reset) or unrecoverable error — caller should exit the receive loop -int16_t socketRecvFrom(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, uint8_t *addr, uint16_t *port, uint64_t *time) { - - assert(socket != INVALID_SOCKET_HANDLE); - SOCKET sock = SOCKET_FD(socket); - assert(sock != INVALID_SOCKET); - - SOCKADDR_IN src; - src.sin_port = 0; - src.sin_addr.s_addr = 0; - - int16_t n = 0; - -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - // Always use recvmsg() on Linux with HW_TIMESTAMPS: needed for IP_PKTINFO and optional timestamps. - // Removing the if(time!=NULL) gate here is critical — without it the else clause - // would dangle onto the port-extraction statement after #endif, causing no receive when time==NULL. - { - struct iovec iov; - struct msghdr msg; - char control[CMSG_SPACE(sizeof(struct timespec) * 3) + CMSG_SPACE(sizeof(struct in_pktinfo))]; - iov.iov_base = buffer; - iov.iov_len = bufferSize; - memset(&msg, 0, sizeof(msg)); - msg.msg_name = &src; - msg.msg_namelen = sizeof(src); - msg.msg_flags = 0; - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - msg.msg_control = control; - msg.msg_controllen = sizeof(control); - n = (int16_t)recvmsg(sock, &msg, 0); - - // n = 0, zero-length UDP datagram, not a socket close, caller loops - if (n == 0) { - return 0; // Timeout — caller loops and does background work - } - - // n < 0, error or timeout - else if (n < 0) { - int32_t err = socketGetLastError(); - if (socketTimeout(err)) { - return 0; // Timeout — caller loops and does background work - } - DBG_PRINTF_ERROR("socketRecvFrom: recvmsg failed (errno=%d,%s, result=%d)!\n", err, socketGetErrorString(err), n); - return -1; - } - - // Extract timestamp and interface info from control messages if available - if (time != NULL) - *time = 0; - struct timespec *hw = NULL; - struct timespec *sw = NULL; - struct cmsghdr *cmsg; - uint16_t n = 0; - for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { - n++; - int level = cmsg->cmsg_level; - int type = cmsg->cmsg_type; - - DBG_PRINTF6("socketRecvFrom: cmsg level=%d type=%d (%s)\n", level, type, // - (level == SOL_SOCKET && type == SO_TIMESTAMPING) ? "SO_TIMESTAMPING" - : (level == SOL_SOCKET && type == SO_TIMESTAMPNS) ? "SO_TIMESTAMPNS" - : (level == IPPROTO_IP && type == IP_PKTINFO) ? "IP_PKTINFO" - : "UNKNOWN"); - - if (SOL_SOCKET == level && SO_TIMESTAMPING == type) { - if (cmsg->cmsg_len < sizeof(struct timespec) * 3) { - DBG_PRINT_WARNING("short SO_TIMESTAMPING message"); - break; - } - assert(hw == NULL); - hw = (struct timespec *)CMSG_DATA(cmsg); - } else if (SOL_SOCKET == level && SO_TIMESTAMPNS == type) { - if (cmsg->cmsg_len < sizeof(struct timespec)) { - DBG_PRINT_WARNING("short SO_TIMESTAMPNS message"); - break; - } - sw = (struct timespec *)CMSG_DATA(cmsg); - } else if (IPPROTO_IP == level && IP_PKTINFO == type) { - struct in_pktinfo *pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsg); - // Always print IP_PKTINFO for debugging (use printf, not DBG_PRINTF) - DBG_PRINTF6("socketRecvFrom: IP_PKTINFO - ipi_ifindex=%d, ipi_addr=%08x, ipi_spec_dst=%08x, socket->ifindex=%d\n", pktinfo->ipi_ifindex, - ntohl(pktinfo->ipi_addr.s_addr), ntohl(pktinfo->ipi_spec_dst.s_addr), socket->ifindex); - assert(socket->ifindex == 0 || socket->ifindex == pktinfo->ipi_ifindex); - // Note: Just to be sure, we always get timestamps from expected if. Currently no mechanism to return this info to caller - } - } - if (n == 0) { - DBG_PRINT6("socketRecvFrom: No control messages received\n"); - } - - // Process timestamps if requested - if (time != NULL) { - uint64_t t = 0; - if (hw != NULL) { - struct timespec *ts; - ts = &hw[2]; - t = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; - if (t != 0) { - DBG_PRINT6("socketRecvFrom: timestamp taken from control messages SO_TIMESTAMPING [2]\n"); - } else { - ts = &hw[0]; - t = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; - if (t != 0) { - DBG_PRINT6("socketRecvFrom: timestamp taken from control messages SO_TIMESTAMPING [0]\n"); - } - } - - // { - // uint64_t t_hw = hw[2].tv_sec * 1000000000ULL + hw[2].tv_nsec; - // uint64_t t_sw = hw[0].tv_sec * 1000000000ULL + hw[0].tv_nsec; - // printf("socketRecvFrom: HW timestamp = %" PRIu64 " ns, SW timestamp = %" PRIu64 " ns, diff = %" PRIi64 " ns\n", t_hw, t_sw, (int64_t)(t_hw - t_sw)); - // } - } - if (t == 0 && sw != NULL) { - struct timespec *ts = sw; - t = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; - DBG_PRINT5("socketRecvFrom: timestamp taken from control messages SO_TIMESTAMPNS\n"); - } - if (t == 0) { - DBG_PRINT_WARNING("socketRecvFrom: No timestamp found in control messages\n"); - } - *time = t; - } - } -#else - { - socklen_t srclen = sizeof(src); - n = (int16_t)recvfrom(sock, (char *)buffer, bufferSize, 0, (SOCKADDR *)&src, &srclen); - - // n = 0, zero-length UDP datagram, not a socket close, caller loops - if (n == 0) { - return 0; // Timeout — caller loops and does background work - } else if (n < 0) { - int32_t err = socketGetLastError(); - // DBG_PRINTF6("socketRecvFrom: recvfrom returned n<0 (errno=%d,%s)\n", err, socketGetErrorString(err)); - - if (socketTimeout(err)) { - // DBG_PRINTF6("socketRecvFrom: recvfrom returned n<0, (errno=%d,%s), socket timeout, return 0\n", err, socketGetErrorString(err)); - return 0; // Timeout - } - - DBG_PRINTF_ERROR("socketRecvFrom: failed n=%d (errno=%u,%s) , return -1\n", n, err, socketGetErrorString(err)); - return -1; - } - - if (time != NULL) { - assert(false && "Hardware timestamp are not enabled, would return system time"); - *time = clockGet(); - } - } -#endif - - if (port) - *port = htons(src.sin_port); - if (addr) - memcpy(addr, &src.sin_addr.s_addr, 4); - -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - DBG_PRINTF6("socketRecvFrom: sock=%d, ifindex=%d returned n=%u, time=%" PRIu64 "\n", sock, socket->ifindex, n, time ? *time : 0); -#else - DBG_PRINTF6("socketRecvFrom: sock=%d returned n=%u, time=%" PRIu64 "\n", sock, n, time ? *time : 0); -#endif - - return n; -} - -// Receive from TCP socket -// Blocking mode only, with optional timeout set with socketSetTimeout() -// For UDP use socketRecvFrom() instead, which also returns the source address and supports timestamps -// Return values: -// n > 0 : number of bytes received -// n == 0 : timeout (set with socketTimeout) expired or would-block — no data yet, caller should loop and do background work -// n < 0 : socket closed (graceful or reset) or unrecoverable error — caller should exit the receive loop -#if defined(OPTION_ENABLE_TCP) -int16_t socketRecv(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t buffer_size, bool waitAll) { - - assert(socket != INVALID_SOCKET_HANDLE); - // assert(socket->flags & SOCKET_MODE_TCP); // Use socketRecvFrom() for UDP sockets - assert(buffer_size > 0); - SOCKET sock = SOCKET_FD(socket); - assert(sock != INVALID_SOCKET); - - if (!waitAll) { - int16_t n = (int16_t)recv(sock, (char *)buffer, buffer_size, 0); - - // n = 0, socket close - if (n == 0) { - DBG_PRINT6("socketRecv: recv returned n=0, socket closed, return -1\n"); - return -1; // Socket closed - } - - // n < 0, error or timeout - else if (n < 0) { - int32_t err = socketGetLastError(); - if (socketTimeout(err)) { - DBG_PRINTF_ERROR("socketRecv: recv returned n<0, socket timeout (errno=%d,%s), return 0\n", err, socketGetErrorString(err)); - return 0; // Timeout, no data yet - } - DBG_PRINTF_ERROR("socketRecv: recv returned n<0, socket error (errno=%d,%s), return -1\n", err, socketGetErrorString(err)); - return -1; // Error - } - return n; - } - - // waitAll: loop until exactly `size` bytes have been received. - // MSG_WAITALL alone is not sufficient when SO_RCVTIMEO is set - // Linux may return a partial size if the timeout fires mid-read. - // We therefore implement a loop on top and return the timeout to the caller only when there is no data yet - uint16_t received = 0; - uint32_t timeout_counter = 0; - for (;;) { - int16_t n = (int16_t)recv(sock, (char *)buffer + received, (uint16_t)(buffer_size - received), MSG_WAITALL); - - // n = 0, socket close - if (n == 0) { - DBG_PRINT6("socketRecv: recv waitall returned n=0, socket closed, return -1\n"); - return -1; // Socket closed - } - - // n < 0, error or timeout - else if (n < 0) { - int32_t err = socketGetLastError(); - if (socketTimeout(err)) { - DBG_PRINTF6("socketRecv: recv waitall returned n<0, socket timeout (errno=%d,%s), return 0\n", err, socketGetErrorString(err)); - if (received == 0) { - return 0; // Timeout only before any data ok - } - DBG_PRINT_ERROR("socketRecv: recv waitall returned n<0, timeout mid-frame, return -1\n"); - return -1; // Partial frame received — TCP stream is desynchronised - } - DBG_PRINTF_ERROR("socketRecv: recv waitall returned n<0, socket error (errno=%d,%s), return -1\n", err, socketGetErrorString(err)); - return -1; // Error - } - - received = (uint16_t)(received + (uint16_t)n); - if (received >= buffer_size) { - break; // done - } - - if (++timeout_counter >= 4) { - DBG_PRINT_ERROR("socketRecv: recv waitall timeout mid-frame, giving up after 4 attempts\n"); - break; // loop protection: should never happen - } - - DBG_PRINTF_WARNING("socketRecv waitall: received %u bytes, waiting for %u more\n", received, buffer_size - received); - } - - assert(received == buffer_size); - return (int16_t)received; -} -#endif // OPTION_ENABLE_TCP - -// Send datagram on UDP socket -// Returns number of bytes sent or -1 on error -// Requests and may returns optional send time if (time != NULL) -// Support hardware timestamps if enabled on the socket and with OPTION_SOCKET_HW_TIMESTAMPS defined, otherwise system time is used -// If *time = 0 on return, no timestamp is available yet, but can be obtained with socketGetSendTime() -// On non-Linux platforms, *time is set to system time at send -// Returns total number of bytes sent, 0 on socket closed or -1 on error -int16_t socketSendTo(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t size, const uint8_t *addr, uint16_t port, uint64_t *time) { - - assert(socket != INVALID_SOCKET_HANDLE); - SOCKET sock = SOCKET_FD(socket); - assert(sock != INVALID_SOCKET); - -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - DBG_PRINTF6("socketSendTo: sock=%d, ifindex=%d\n", sock, socket->ifindex); -#else - DBG_PRINTF6("socketSendTo: sock=%d\n", sock); -#endif - - SOCKADDR_IN sa; - sa.sin_family = AF_INET; -#if defined(_WIN) // Windows - memcpy(&sa.sin_addr.S_un.S_addr, addr, 4); -#else - memcpy(&sa.sin_addr.s_addr, addr, 4); -#endif - sa.sin_port = htons(port); - -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - if (time != NULL) { - // On Linux, we need to use sendmsg() with SO_TIMESTAMPING control message - // to request TX timestamp generation for this specific packet - struct iovec iov; - struct msghdr msg; - char control[CMSG_SPACE(sizeof(uint32_t))]; - struct cmsghdr *cmsg; - - iov.iov_base = (void *)buffer; - iov.iov_len = size; - - memset(&msg, 0, sizeof(msg)); - msg.msg_name = &sa; - msg.msg_namelen = sizeof(sa); - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - msg.msg_control = control; - msg.msg_controllen = sizeof(control); - - // Add control message to request timestamp generation - cmsg = CMSG_FIRSTHDR(&msg); - cmsg->cmsg_level = SOL_SOCKET; - cmsg->cmsg_type = SO_TIMESTAMPING; - cmsg->cmsg_len = CMSG_LEN(sizeof(uint32_t)); - - // Request both hardware and software timestamps - // Hardware timestamp will be used if available, otherwise fall back to software - uint32_t ts_flags = SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_TX_HARDWARE; - memcpy(CMSG_DATA(cmsg), &ts_flags, sizeof(ts_flags)); - *time = 0; // Clear time, to indicate that it may be obtained later with socketGetSendTime() - ssize_t n = sendmsg(sock, &msg, 0); - if (n < 0) { - int32_t err = socketGetLastError(); - if (socketWouldBlock(err)) { - DBG_PRINT_ERROR("socketSendTo: unexpected WBLOCK\n"); - return -1; // Should never happen on a blocking socket - } - if (socketIsClosed(err)) { - DBG_PRINTF6("socketSendTo: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); - return 0; // Transmit socket closed - } - DBG_PRINTF_ERROR("socketSendTo: sendmsg failed with errno=%d,%s!\n", err, socketGetErrorString(err)); - return -1; - } - return (int16_t)n; - } -#else - - if (time != NULL) - *time = clockGet(); // Return system time as send time on non-Linux platforms - -#endif - ssize_t n = sendto(sock, (const char *)buffer, size, 0, (SOCKADDR *)&sa, (uint16_t)sizeof(sa)); - if (n < 0) { - int32_t err = socketGetLastError(); - if (socketWouldBlock(err)) { - DBG_PRINT_ERROR("socketSendTo: unexpected WBLOCK\n"); - return -1; // Should never happen on a blocking socket - } - if (socketIsClosed(err)) { - DBG_PRINTF6("socketSendTo: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); - return 0; // Transmit socket closed - } - DBG_PRINTF_ERROR("socketSendTo: sendto failed with errno=%d,%s!\n", err, socketGetErrorString(err)); - return -1; - } - return (int16_t)n; -} - -// Send buffer on a TCP socket -// Thread safe -// Returns total number of bytes sent, 0 on socket closed or -1 on error -#if defined(OPTION_ENABLE_TCP) -int16_t socketSend(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t size) { - - assert(socket != INVALID_SOCKET_HANDLE); - SOCKET sock = SOCKET_FD(socket); - assert(sock != INVALID_SOCKET); - - ssize_t n = send(sock, (const char *)buffer, size, 0); - if (n < 0) { - int32_t err = socketGetLastError(); - if (socketWouldBlock(err)) { - DBG_PRINT_ERROR("socketSend: unexpected WBLOCK\n"); - return -1; // Should never happen on a blocking socket - } - if (socketIsClosed(err)) { - DBG_PRINTF6("socketSend: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); - return 0; // Transmit socket closed - } - DBG_PRINTF_ERROR("socketSend: send failed with errno=%d,%s!\n", err, socketGetErrorString(err)); - return -1; - } - return (int16_t)n; -} -#endif // OPTION_ENABLE_TCP - -#endif // !defined(_FREE_RTOS) || defined(FREE_RTOS_POSIX_SIM) - -// Vectored IO send and receive functions using sendmsg/recvmsg with iovec for efficient scatter-gather I/O -#if !defined(_WIN) && !defined(_FREE_RTOS) - -// Send multiple datagrams on a UDP socket -// Returns number of bytes sent or -1 on error -// Send multiple buffers as a UDP datagram to a specific address/port -// Using iovec for efficient scatter-gather I/O (POSIX: Linux, macOS, QNX) -// Thread safe -// buffers: array of pointers to data buffers -// sizes: array of buffer sizes, one per buffer -// count: number of buffers -// Returns total number of bytes sent, 0 on socket closed or -1 on error -int16_t socketSendToV(SOCKET_HANDLE socket, tQueueBuffer buffers[], uint16_t count, const uint8_t *addr, uint16_t port) { - - assert(socket != INVALID_SOCKET_HANDLE); - SOCKET sock = SOCKET_FD(socket); - assert(sock != INVALID_SOCKET); - - SOCKADDR_IN sa; - sa.sin_family = AF_INET; - memcpy(&sa.sin_addr.s_addr, addr, 4); - sa.sin_port = htons(port); - - // Build iovec array on the stack - VLAs are acceptable here as count is usually small - struct iovec iov[count]; - uint32_t total = 0; - for (uint16_t i = 0; i < count; i++) { - iov[i].iov_base = (void *)buffers[i].buffer; - iov[i].iov_len = buffers[i].size; - total += buffers[i].size; - } - - struct msghdr msg; - memset(&msg, 0, sizeof(msg)); - msg.msg_name = &sa; - msg.msg_namelen = sizeof(sa); - msg.msg_iov = iov; - msg.msg_iovlen = count; - - ssize_t n = sendmsg(sock, &msg, 0); - if (n < 0) { - int32_t err = socketGetLastError(); - if (socketWouldBlock(err)) { - DBG_PRINT_ERROR("socketSendToV: unexpected WBLOCK\n"); - return -1; // Should never happen on a blocking socket - } - if (socketIsClosed(err)) { - DBG_PRINTF6("socketSendToV: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); - return 0; // Transmit socket closed - } - DBG_PRINTF_ERROR("socketSendToV: sendmsg failed with errno=%d,%s!\n", err, socketGetErrorString(err)); - return -1; - } - if (total != n) { - DBG_PRINTF_WARNING("socketSendToV: partial send, sent %" PRIu32 " of %" PRIu32 " bytes\n", (uint32_t)n, total); - return -1; // Treat partial sends as an error on UDP sockets, as the caller cannot recover - } - return (int16_t)n; -} - -// Send multiple buffers on a TCP socket -// Using iovec for efficient scatter-gather I/O (POSIX: Linux, macOS, QNX) -// Thread safe -// buffers: array of pointers to data buffers -// sizes: array of buffer sizes, one per buffer -// count: number of buffers -// Returns total number of bytes sent, 0 on socket closed or -1 on error -int16_t socketSendV(SOCKET_HANDLE socket, tQueueBuffer buffers[], uint16_t count) { - - assert(socket != INVALID_SOCKET_HANDLE); - SOCKET sock = SOCKET_FD(socket); - assert(sock != INVALID_SOCKET); - - // Build iovec array on the stack - VLAs are acceptable here as count is usually small - struct iovec iov[count]; - for (uint16_t i = 0; i < count; i++) { - iov[i].iov_base = (void *)buffers[i].buffer; - iov[i].iov_len = buffers[i].size; - } - - struct msghdr msg; - memset(&msg, 0, sizeof(msg)); - msg.msg_iov = iov; - msg.msg_iovlen = count; - - // TCP streams may deliver partial sends: loop until all data is accepted by the kernel - // Advance iovec entries as bytes are consumed to avoid re-sending already sent data - // Note: all sockets in this codebase are blocking (see socketOpen), so WBLOCK must not - // occur. If it does mid-loop, the iovec state is partially consumed and the caller cannot - // recover, so it is treated as an unrecoverable error rather than returning a partial count. - int32_t total = 0; - for (;;) { - ssize_t n = sendmsg(sock, &msg, 0); - if (n < 0) { - int32_t err = socketGetLastError(); - if (socketWouldBlock(err)) { - DBG_PRINT_ERROR("socketSendV: unexpected WBLOCK\n"); - return -1; // Should never happen on a blocking socket - } - if (socketIsClosed(err)) { - DBG_PRINTF6("socketSendV: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); - return 0; // Transmit socket closed - } - DBG_PRINTF_ERROR("socketSendV: sendmsg failed with errno=%d,%s!\n", err, socketGetErrorString(err)); - return -1; - } - total += (int32_t)n; - - // Advance the iovec past the bytes already sent - size_t remaining = (size_t)n; - while (msg.msg_iovlen > 0 && remaining >= msg.msg_iov[0].iov_len) { - remaining -= msg.msg_iov[0].iov_len; - msg.msg_iov++; - msg.msg_iovlen--; - } - if (msg.msg_iovlen == 0) - break; // All data sent - // Adjust the first remaining iovec for the partial send - msg.msg_iov[0].iov_base = (uint8_t *)msg.msg_iov[0].iov_base + remaining; - msg.msg_iov[0].iov_len -= remaining; - } - - return (int16_t)total; -} - -#endif // !defined(_WIN) && !defined(_FREE_RTOS) - -// Get send time of last sent packet -// Retrieves TX hardware timestamp and kernel software timestamp from socket error queue -// Returns false if no timestamp available or on error -// On non-Linux platforms, this function always returns false -// On Linux, requires OPTION_SOCKET_HW_TIMESTAMPS defined and hardware timestamping enabled on the socket -// hw_time and sw_time are optional, set to NULL if not needed -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) -bool socketGetSendTime(SOCKET_HANDLE socket, uint64_t *hw_time, uint64_t *sw_time) { - - assert(socket != NULL); - SOCKET sock = socket->sock; - assert(sock != INVALID_SOCKET); - - if (hw_time) - *hw_time = 0; - if (sw_time) - *sw_time = 0; - - char control[512]; - char data[1]; - struct iovec iov; - struct msghdr msg; - struct cmsghdr *cmsg; - struct timespec *ts = NULL; - - iov.iov_base = data; - iov.iov_len = sizeof(data); - - memset(&msg, 0, sizeof(msg)); - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - msg.msg_control = control; - msg.msg_controllen = sizeof(control); - - DBG_PRINT5("socketGetSendTime: Reading from error queue...\n"); - - // Read from error queue with retries (timeout 10ms) - ssize_t ret = -1; - for (uint32_t attempt = 0; attempt < 10; attempt++) { - ret = recvmsg(sock, &msg, MSG_ERRQUEUE | MSG_DONTWAIT); - if (ret >= 0) { - DBG_PRINTF5("socketGetSendTime: Got message from error queue after %u attempts, ret=%ld\n", attempt, ret); - break; - } - if (errno != EAGAIN && errno != EWOULDBLOCK) { - DBG_PRINTF_ERROR("socketGetSendTime: recvmsg error queue failed with errno=%d (%s)\n", errno, strerror(errno)); - return false; - } - // Wait a bit and retry - sleepUs(1000); // 1ms - } - if (ret < 0) { - DBG_PRINT_WARNING("socketGetSendTime: Timeout, no TX timestamp available after retries\n"); - return false; - } - - // Look for timestamps in control messages - for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { - DBG_PRINTF5("socketGetSendTime: Found cmsg level=%d type=%d (SOL_SOCKET=%d SO_TIMESTAMPING=%d)\n", cmsg->cmsg_level, cmsg->cmsg_type, SOL_SOCKET, SO_TIMESTAMPING); - if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMPING) { - // SO_TIMESTAMPING returns 3 timespec structures: software, deprecated, hardware - struct timespec *ts_array = (struct timespec *)CMSG_DATA(cmsg); - - DBG_PRINTF5("socketGetSendTime: ts[0]=%ld.%09ld ts[1]=%ld.%09ld ts[2]=%ld.%09ld\n", ts_array[0].tv_sec, ts_array[0].tv_nsec, ts_array[1].tv_sec, ts_array[1].tv_nsec, - ts_array[2].tv_sec, ts_array[2].tv_nsec); - - // hardware timestamp (index 2) - ts = &ts_array[2]; - if (ts->tv_sec != 0 || ts->tv_nsec != 0) { - if (hw_time) - *hw_time = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; - DBG_PRINTF5("socketGetSendTime: Using HW TX timestamp: %ld.%09ld\n", ts->tv_sec, ts->tv_nsec); - } - - // software timestamp (index 0) - ts = &ts_array[0]; - if (ts->tv_sec != 0 || ts->tv_nsec != 0) { - if (sw_time) - *sw_time = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; - DBG_PRINTF5("socketGetSendTime: Using SW TX timestamp: %ld.%09ld\n", ts->tv_sec, ts->tv_nsec); - } - - if ((hw_time == NULL || *hw_time != 0) && (sw_time == NULL || *sw_time != 0)) { - break; // Got what we needed - } - } - } - - if ((hw_time == NULL || *hw_time != 0) && (sw_time == NULL || *sw_time != 0)) { - DBG_PRINTF5("socketGetSendTime: hw=%" PRIu64 ", sw=%" PRIu64 ", sys= %" PRIu64 "\n", hw_time ? *hw_time : 0, sw_time ? *sw_time : 0, clockGet()); - return true; // Got all requested timestamps - } - if (hw_time != NULL && *hw_time == 0) - DBG_PRINT_WARNING("socketGetSendTime: No hardware TX timestamp found\n"); - if (sw_time != NULL && *sw_time == 0) - DBG_PRINT_WARNING("socketGetSendTime: No software TX timestamp found\n"); - - return false; -} -#endif // defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) - -#endif // !_WIN - -#endif - /**************************************************************************/ // Clock /**************************************************************************/ @@ -2345,7 +779,7 @@ bool clockInit(void) { char ts[64]; // @@@@ STACK buffer for clock value t = clockGet(); clockGetString(ts, sizeof(ts), t); - printf(" Now = %I64u (%I64u per us) %s\n", t, (CLOCK_TICKS_PER_S / 1000000), ts); + printf(" Now = %" PRIu64 " (%" PRIu64 " per us) %s\n", t, (uint64_t)(CLOCK_TICKS_PER_S / 1000000), ts); } #endif diff --git a/src/platform.h b/src/platform.h index 79c3a0d6..2f32fe4a 100644 --- a/src/platform.h +++ b/src/platform.h @@ -11,7 +11,6 @@ | Sleep | Threads | Mutex -| Sockets | Clock | Virtual memory | Keyboard @@ -78,7 +77,7 @@ /* OPTION_ATOMIC_EMULATION OPTION_ENABLE_KEYBOARD -OPTION_ENABLE_TCP and/or OPTION_ENABLE_UDP +OPTION_ENABLE_TCP and/or OPTION_ENABLE_UDP or OPTION_ENABLE_UDP_RAW OPTION_SOCKET_HW_TIMESTAMPS (for Linux PTP tooling only) OPTION_ENABLE_GET_LOCAL_ADDR OPTION_CLOCK_TICKS_1NS or OPTION_CLOCK_TICKS_1US @@ -138,7 +137,7 @@ using std::atomic_uint_least64_t; using std::atomic_uint_least8_t; #endif -// When testing FreeRTOS code paths on macOS/Linux, we use OS-specific sockets and clock code in platform.c +// When testing FreeRTOS code paths on macOS/Linux, we use OS-specific sockets and clock code in platform.c and sockets.c #if defined(FREE_RTOS_POSIX_SIM) #if defined(__APPLE__) @@ -351,10 +350,25 @@ void mutexDestroy(MUTEX *m); //------------------------------------------------------------------------------- // Threads +// create_thread() result convention +// +// POSIX expression, 0 on success (pthread_create) +// Windows expression, 0 on success (adapted below, CreateThread itself returns a HANDLE) +// FreeRTOS STATEMENTS, both variants assert on failure - they have no value and cannot be tested +// +// Windows follows the POSIX convention so that a check reads the same way on both rather than +// meaning the opposite thing. A *portable* check is still not possible, because the FreeRTOS +// variants are statements: `if (create_thread(...))` does not compile there. That is deliberate - +// it fails at build time instead of silently. No caller in this repository tests the result. +// +// If you need to know that a thread is actually running, have the thread set a flag as its first +// action and wait for it. That is portable and proves more than a creation result: see +// cmpRestStart() in examples/cmp_demo/src/cmp_rest.c. + #if defined(_WIN) // Windows typedef HANDLE THREAD_HANDLE; -#define create_thread(thread_handle_ptr, attr, thread, args) *thread_handle_ptr = CreateThread(0, 0, thread, args, 0, NULL) +#define create_thread(thread_handle_ptr, attr, thread, args) (((*(thread_handle_ptr) = CreateThread(0, 0, thread, args, 0, NULL)) == NULL) ? -1 : 0) #define join_thread(h) WaitForSingleObject(h, INFINITE); #define cancel_thread(h) \ do { \ @@ -452,244 +466,6 @@ typedef pthread_t THREAD_HANDLE; #error "Thread-local storage not supported" #endif -//------------------------------------------------------------------------------- -// Platform independent socket functions - -#if defined(OPTION_ENABLE_TCP) || defined(OPTION_ENABLE_UDP) - -// Note: -// SOCKET_HANDLE is an opaque type that may wrap the OS socket handle and additional info (e.g. for Linux hardware timestamping) -// INVALID_SOCKET_HANDLE is the invalid value for SOCKET_HANDLE -// SOCKET_FD(s) extracts the raw OS socket fd from a SOCKET_HANDLE (which may be a struct socket pointer on Linux with HW timestamps) - -#if !defined(_WIN) // Non-Windows platform sockets - -#if !defined(_WIN) && !defined(_FREE_RTOS) -#include "queue.h" // for tQueueBuffer -#endif - -#define SOCKET int -#define INVALID_SOCKET (-1) - -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) -// For Linux hardware timestamping support, SOCKET_HANDLE is a pointer to struct socket which contains the socket fd and interface info for timestamp retrieval -struct socket { - SOCKET sock; - uint32_t addr; // Bind address (network byte order) maybe INADDR_ANY - uint16_t port; // Port - unsigned int ifindex; // Interface index - char ifname[16]; // Interface name - uint32_t ifaddr; // Interface address - uint8_t ifmac[6]; // Interface MAC address -}; -typedef struct socket *SOCKET_HANDLE; -#define INVALID_SOCKET_HANDLE NULL -#define SOCKET_FD(s) ((s)->sock) // Extract the OS socket fd from a SOCKET_HANDLE -#else -// Linux (without HW timestamps), FreeRTOS, macOS, QNX: SOCKET_HANDLE is the raw OS fd -typedef SOCKET SOCKET_HANDLE; -#define INVALID_SOCKET_HANDLE INVALID_SOCKET -#define SOCKET_FD(s) (s) // Extract the OS socket fd from a SOCKET_HANDLE -#endif - -#define SOCKADDR_IN struct sockaddr_in -#define SOCKADDR struct sockaddr - -#undef htonll -#define htonll(val) ((((uint64_t)htonl((uint32_t)(val))) << 32) + htonl((uint32_t)((val) >> 32))) - -#include // for errno and error codes from socketGetLastError - -#define SOCKET_ERROR_ABORT ECONNABORTED // 53 -#define SOCKET_ERROR_RESET ECONNRESET // 54 -#define SOCKET_ERROR_INTR EINTR // 4 -#define SOCKET_ERROR_TIMEDOUT ETIMEDOUT // 60 -#define SOCKET_ERROR_WBLOCK EAGAIN // 35 EWOULDBLOCK is the same as EAGAIN on Linux, but may be different on other platforms -#define SOCKET_ERROR_PIPE EPIPE // 32 -#define SOCKET_ERROR_BADF EBADF // 9 -#define SOCKET_ERROR_NOTCONN ENOTCONN // 107 (57 macOS) Socket is not connected - -#define socketGetLastError(void) errno -#define socketIsClosed(err) ((err) == ENOTCONN || (err) == ECONNABORTED || (err) == EBADF || (err) == ECONNRESET) -#define socketWouldBlock(err) ((err) == EAGAIN || (err) == EWOULDBLOCK) -#define socketTimeout(err) ((err) == ETIMEDOUT || (err) == EAGAIN || (err) == EWOULDBLOCK || (err) == EINTR) - -#else // Windows sockets - -#include -#include - -typedef SOCKET SOCKET_HANDLE; -#define INVALID_SOCKET_HANDLE INVALID_SOCKET -#define SOCKET_FD(s) (s) - -#define SOCKADDR_IN struct sockaddr_in -#define SOCKADDR struct sockaddr - -#include // for errno and error codes from socketGetLastError -int32_t socketGetLastError(void); -#define SOCKET_ERROR_ABORT WSAECONNABORTED // 10053 -#define SOCKET_ERROR_RESET WSAECONNRESET // 10054 -#define SOCKET_ERROR_INTR WSAEINTR // 10004 -#define SOCKET_ERROR_TIMEDOUT WSAETIMEDOUT // 10060 -#define SOCKET_ERROR_WBLOCK WSAEWOULDBLOCK // 10035 -#define SOCKET_ERROR_PIPE WSAESHUTDOWN // 10058 -#define SOCKET_ERROR_BADF WSAEBADF // 10009 -#define SOCKET_ERROR_NOTCONN WSAENOTCONN // 10057 -#define socketIsClosed(err) ((err) == WSAECONNABORTED || (err) == WSAEBADF || (err) == WSAECONNRESET || (err) == WSAEINTR) -#define socketWouldBlock(err) ((err) == WSAEWOULDBLOCK) -#define socketTimeout(err) ((err) == WSAETIMEDOUT) - -#define ssize_t int - -#endif - -// Socket mode flags -#define SOCKET_MODE_TCP (1 << 0) // TCP socket -#define SOCKET_MODE_REUSEADDR (1 << 2) // Allow reuse of local address -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) -#define SOCKET_MODE_GET_IF_INFO (1 << 6) // Enable IP_PKTINFO to identify the receiving interface (Linux only) -#define SOCKET_MODE_HW_TIMESTAMPING (1 << 4) // Enable hardware timestamping (Linux only, requires root) -#define SOCKET_MODE_SW_TIMESTAMPING (1 << 5) // Enable kernel software timestamping (Linux only, requires root) -#endif - -// Socket functions - -// Initialize the socket subsystem (Windows: WSAStartup; no-op on POSIX) -// Must be called once before any other socket function -// Returns true on success -bool socketStartup(void); - -// Clean up the socket subsystem (Windows: WSACleanup; no-op on POSIX) -void socketCleanup(void); - -// Return a static human-readable string for a SOCKET_ERROR_* error code -// Returns "unknown socket error" for unrecognized codes -const char *socketGetErrorString(int32_t err); - -// Create a TCP or UDP socket with the given SOCKET_MODE_xxx flags -// Sockets are always created in blocking mode, a timeout may be set with socketSetTimeout() -// SOCKET_MODE_TCP: TCP stream socket (default: UDP datagram) -// SOCKET_MODE_REUSEADDR: set SO_REUSEADDR to allow rapid port reuse after restart -// SOCKET_MODE_HW_TIMESTAMPING / SOCKET_MODE_SW_TIMESTAMPING: enable timestamps (Linux with hardware timestamps only) -// SOCKET_MODE_GET_IF_INFO: enable IP_PKTINFO to identify the receiving interface (Linux with hardware timestamps only) -// Returns true on success -bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags); - -// Bind socket to a local address and port -// addr: network-byte-order IPv4 address; NULL or 0.0.0.0 binds to INADDR_ANY -// Returns true on success -bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port); - -// Bind socket to a specific network interface by name (Linux only, requires root) -// Useful for multicast reception on a specific interface when bound to INADDR_ANY -// ifname: interface name, e.g. "eth0"; NULL or empty string is a no-op -// Returns true on success (returns true with a warning on non-Linux platforms) -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) -bool socketBindToDevice(SOCKET_HANDLE socket, const char *ifname); -#endif - -// Configure the NIC driver to generate hardware timestamps (Linux only, requires root) -// Must be called after socketBind; uses the interface name stored by socketBind/socketBindToDevice -// ptpOnly: true = timestamp PTP event packets only; false = timestamp all packets -// Falls back gracefully if the NIC does not support hardware timestamps -// Returns true on success -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) -bool socketEnableTimestamps(SOCKET_HANDLE socket, bool ptpOnly); -#endif - -// Join an IPv4 multicast group on a UDP socket -// maddr: multicast group address (network byte order) -// Interface selection priority: ifname > ifaddr > INADDR_ANY (kernel routing) -// Returns true on success -bool socketJoin(SOCKET_HANDLE socket, const uint8_t *maddr, const uint8_t *ifaddr, const char *ifname); - -// Start listening for incoming TCP connections -// Returns true on success -bool socketListen(SOCKET_HANDLE socket); - -// Accept an incoming TCP connection (blocking) -// addr: filled with the remote IPv4 address (network byte order) if non-NULL -// Returns a new connected SOCKET_HANDLE; the caller is responsible for closing it -SOCKET_HANDLE socketAccept(SOCKET_HANDLE socket, uint8_t *addr); - -// Receive from a TCP socket (blocking) -// waitAll: true = MSG_WAITALL, block until bufferSize bytes arrive -// Return values: > 0 bytes received -// == 0 timeout (set with socketSetRecvTimeout) — no data yet, do background work and loop -// < 0 socket closed (graceful or reset) or error — check with socketIsClosed(socketGetLastError()) and exit the receive loop -int16_t socketRecv(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, bool waitAll); - -// Receive a UDP datagram (blocking) -// srcAddr / srcPort: filled with sender's address/port if non-NULL -// time: optional receive timestamp (NULL to skip); hardware or software depending on socket flags -// Return values: > 0 bytes received -// == 0 timeout (set with socketSetRecvTimeout) — no data yet, do background work and loop -// < 0 socket closed or error — check with socketIsClosed(socketGetLastError()) and exit the receive loop -int16_t socketRecvFrom(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, uint8_t *srcAddr, uint16_t *srcPort, uint64_t *time); - -// Send a UDP datagram to addr:port -// time: optional send timestamp (NULL to skip) -// on Linux with HW timestamps: *time is set to 0; call socketGetSendTime() afterwards to retrieve it -// on other platforms: *time is set to the system clock at send time -// Returns: bytes sent, 0 on closed socket, -1 on error -int16_t socketSendTo(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize, const uint8_t *addr, uint16_t port, uint64_t *time); - -// Send data on a TCP socket (blocking; loops internally on partial sends) -// Returns: bytes sent, 0 on closed socket, -1 on error -int16_t socketSend(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize); - -#if !defined(_WIN) && !defined(_FREE_RTOS) -// Send multiple buffers as a single UDP datagram (scatter-gather I/O via sendmsg, POSIX only) -// Returns: total bytes sent, 0 on closed socket, -1 on error (partial UDP sends treated as error) -int16_t socketSendToV(SOCKET_HANDLE socket, tQueueBuffer buffers[], uint16_t count, const uint8_t *addr, uint16_t port); - -// Send multiple buffers on a TCP socket (scatter-gather I/O via sendmsg, POSIX only) -// Loops internally until all data is accepted by the kernel -// Returns: total bytes sent, 0 on closed socket, -1 on error -int16_t socketSendV(SOCKET_HANDLE socket, tQueueBuffer buffers[], uint16_t count); -#endif - -// Retrieve TX hardware and/or software timestamp after socketSendTo (Linux only) -// Must be called shortly after socketSendTo returned *time==0 -// Requires OPTION_SOCKET_HW_TIMESTAMPS and socketEnableTimestamps() to have been called -// txHwTime / txSwTime: set to 0 if the respective timestamp is not available; NULL to skip -// Returns true if at least one requested timestamp was successfully retrieved -#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) -bool socketGetSendTime(SOCKET_HANDLE socket, uint64_t *txHwTime, uint64_t *txSwTime); -#endif - -// Set receive timeout on a blocking socket -// timeoutMs: timeout in milliseconds; 0 = restore infinite blocking -// With a timeout set, socketRecv/socketRecvFrom return 0 on expiry instead of blocking indefinitely, -// allowing the receive thread to perform background work before looping back -// Works for both TCP and UDP; use socketShutdown() to signal a receive thread to exit -// Returns true on success -bool socketSetTimeout(SOCKET_HANDLE socket, uint32_t timeoutMs); - -// Shut down both directions of the socket (SHUT_RDWR / SD_BOTH) -// Unblocks a thread currently blocked in socketRecv or socketRecvFrom, causing it to return -1 -bool socketShutdown(SOCKET_HANDLE socket); - -// Close the OS socket, free the SOCKET_HANDLE, and set *socketp to NULL -// Returns true on success -bool socketClose(SOCKET_HANDLE *socketp); - -// Get the MAC address of a network interface by name (e.g. "eth0") -// mac: output buffer, must point to at least 6 bytes -// Returns true on success -bool socketGetMAC(char *ifname, uint8_t *mac); - -#ifdef OPTION_ENABLE_GET_LOCAL_ADDR -// Get the IPv4 address and MAC of the first non-loopback Ethernet interface -// mac / addr: output buffers (6 / 4 bytes respectively); either may be NULL -// Result is cached after the first successful call -// Returns true on success -bool socketGetLocalAddr(uint8_t *mac, uint8_t *addr); -#endif - -#endif - //------------------------------------------------------------------------------- // High resolution clock diff --git a/src/queue.h b/src/queue.h index ff83d446..53f2ca94 100644 --- a/src/queue.h +++ b/src/queue.h @@ -27,7 +27,7 @@ #include // Queue parameter configuration: -// Configuration for XCP on Ethernet transport layer with 4 byte transport layer header (ctr+len) +// Configuration for XCP on Ethernet transport layer with a 4 byte transport layer header (ctr+len) // Using XCP parameters from xcptl_cfg.h: XCPTL_MAX_DTO_SIZE, XCPTL_MAX_SEGMENT_SIZE, QUEUE_PAYLOAD_SIZE_ALIGNMENT: // Queue entries may include space for a consumer header with user defined size // This allows the consumer to add a header to the queue entry without copying and merging data. @@ -35,7 +35,21 @@ // Other use cases can use this space for other purposes, e.g. to store a timestamp or a protocol header, or it can be set to 0 if not needed. #include "xcptl_cfg.h" // for XCPTL_TRANSPORT_LAYER_HEADER_SIZE, XCPTL_MAX_DTO_SIZE, XCPTL_MAX_SEGMENT_SIZE, QUEUE_PAYLOAD_SIZE_ALIGNMENT #define QUEUE_ENTRY_USER_HEADER_SIZE (XCPTL_TRANSPORT_LAYER_HEADER_SIZE) // (for XCP transport layer header with XCPTL_TRANSPORT_LAYER_HEADER_SIZE) -#define QUEUE_ENTRY_USER_PAYLOAD_SIZE (XCPTL_MAX_DTO_SIZE) // In the variable size queue, used for plausibility checking the requested payload size in queueAcquire + +// Space reserved in front of a complete SEGMENT, for a consumer which needs to prepend a header to the whole segment without copying it. +// This is for the 32 bit queue implementation (queue32.c, queue32m.c) which accumulates multiple messages into a segment. +// The reserved space is not relevant in the 64 bit queue implementations (queue64v.c, queue64f.c) which do not accumulate messages into segments, this is done by the vectored IO +// transmit path.. Note the difference to QUEUE_ENTRY_USER_HEADER_SIZE above, they are easy to confuse: +// QUEUE_ENTRY_USER_HEADER_SIZE is per MESSAGE - every message in a segment carries one +// QUEUE_SEGMENT_HEADER_SIZE is per SEGMENT - reserved once, in front of the whole segment +// For XCP on raw Ethernet a segment is one Ethernet frame, so its link header is needed exactly +// once, in front. Reserving it per message instead would put the space inside the datagram payload +// and multiply it by the number of accumulated messages. +// Only meaningful for queue variants which accumulate messages into segments (queue32.c, queue32m.c). +// 0 disables the reservation, and the queue entry layout is then unchanged. +#define QUEUE_SEGMENT_HEADER_SIZE (XCPTL_TX_HEADROOM) + +#define QUEUE_ENTRY_USER_PAYLOAD_SIZE (XCPTL_MAX_DTO_SIZE) // In the variable size queue, used for plausibility checking the requested payload size in queueAcquire #define QUEUE_ENTRY_USER_SIZE (XCPTL_MAX_DTO_SIZE + XCPTL_TRANSPORT_LAYER_HEADER_SIZE) #define QUEUE_MAX_ENTRY_SIZE (XCPTL_MAX_DTO_SIZE + XCPTL_TRANSPORT_LAYER_HEADER_SIZE) #define QUEUE_PAYLOAD_SIZE_ALIGNMENT (XCPTL_PACKET_ALIGNMENT) @@ -50,9 +64,19 @@ */ // Check preconditions +// The queue implementations round sizes up with a bit mask, so the alignment must be a power of two. +// Each variant additionally requires a minimum, checked in the variant itself, because the reason +// differs: the 64 bit queues need a multiple of 4 for their atomic entry header, the segment +// accumulating queues need a multiple of 2 for the 16 bit message header fields. +#if QUEUE_PAYLOAD_SIZE_ALIGNMENT < 1 || (QUEUE_PAYLOAD_SIZE_ALIGNMENT & (QUEUE_PAYLOAD_SIZE_ALIGNMENT - 1)) != 0 +#error "QUEUE_PAYLOAD_SIZE_ALIGNMENT must be a power of two (see XCPTL_PACKET_ALIGNMENT)" +#endif #if (QUEUE_MAX_ENTRY_SIZE % QUEUE_PAYLOAD_SIZE_ALIGNMENT) != 0 #error "QUEUE_MAX_ENTRY_SIZE should be aligned to QUEUE_PAYLOAD_SIZE_ALIGNMENT" #endif +#if (QUEUE_SEGMENT_HEADER_SIZE % QUEUE_PAYLOAD_SIZE_ALIGNMENT) != 0 +#error "QUEUE_SEGMENT_HEADER_SIZE must be aligned to QUEUE_PAYLOAD_SIZE_ALIGNMENT, otherwise it would misalign the segment payload" +#endif #if (QUEUE_MAX_ENTRY_SIZE > 0xFFFF) #error "QUEUE_MAX_ENTRY_SIZE must not exceed 0xFFFF" #endif @@ -114,7 +138,8 @@ tQueueBuffer queueAcquire(tQueueHandle queue_handle, uint16_t payload_size); /// @return Queue buffer. void queuePush(tQueueHandle queue_handle, const tQueueBuffer *queue_buffer, bool priority); -/// Get a queue entry without removing it from the queue. +/// Peek at a queue entry without removing it from the queue. +/// Only supported by the 64 bit queue implementations (queue64v.c, queue64f.c). /// Single consumer thread only, not thread safe. /// @param queue_handle Queue handle. /// @param index Peak ahead index. Can not peak ahead uncommitted entries!!! @@ -124,9 +149,12 @@ void queuePush(tQueueHandle queue_handle, const tQueueBuffer *queue_buffer, bool /// NOTE: The returned buffer must be released using `queueRelease` and in the same order as they were obtained (sequential index order). /// NOTE: The payload already includes header space for the XCP transport layer header (ctr+len) in the buffer, but the transport layer counter is not set yet! /// NOTE: The function may be called multiple times with the same index, but the entries obtained must be released in sequential index order. +#if defined(OPTION_QUEUE_64_FIX_SIZE) || defined(OPTION_QUEUE_64_VAR_SIZE) tQueueBuffer queuePeek(tQueueHandle queue_handle, uint32_t index, uint32_t *packets_lost, bool *flush_requested); +#endif -/// Get the next entry or multiple accumulated entries from the queue. +/// Get the next entry or multiple accumulated (segment) entries from the queue. +/// Accumulate multiple messages into a segment is only supported by the 32 bit queue implementations (queue32.c, queue32m.c). /// Single consumer thread only, not thread safe. /// @param queue_handle Queue handle. /// @param accumulate Accumulate multiple message entries into one segment (sequential memory), up to the maximum segment size (queue32 only). @@ -135,7 +163,7 @@ tQueueBuffer queuePeek(tQueueHandle queue_handle, uint32_t index, uint32_t *pack /// @return Queue buffer tQueueBuffer::size is 0 if no buffer can be popped from the queue. /// NOTE: The returned buffer must be released using `queueRelease` before any other call to queuePop. /// NOTE: As there may be multiple accumulated entries, queuePop initializes the XCP transport layer counter in the message by calling XcpTlGetCtr() -#ifndef OPTION_QUEUE_64_FIX_SIZE +#if defined(OPTION_QUEUE_32) tQueueBuffer queuePop(tQueueHandle queue_handle, bool accumulate, bool priority, uint32_t *packets_lost); #endif diff --git a/src/queue32.c b/src/queue32.c index d1c47c1d..dee0236e 100644 --- a/src/queue32.c +++ b/src/queue32.c @@ -25,6 +25,7 @@ #include // for assert #include // for PRIu64 #include // for bool +#include // for offsetof #include // for uint32_t, uint64_t, uint8_t, int64_t #include // for free, malloc #include // for memcpy, strcmp @@ -60,14 +61,27 @@ typedef struct { } tXcpMessage; static_assert(sizeof(tXcpMessage) == XCPTL_TRANSPORT_LAYER_HEADER_SIZE, "tXcpMessage size must be equal to XCPTL_TRANSPORT_LAYER_HEADER_SIZE"); +// The accumulated messages carry a tXcpMessage header of two uint16_t fields, which is accessed +// while walking a segment, so the message alignment must be a multiple of 2. +#if (XCPTL_PACKET_ALIGNMENT % 2) != 0 +#error "XCPTL_PACKET_ALIGNMENT must be a multiple of 2 in this queue variant: the 16 bit message header fields require it" +#endif typedef struct { - uint32_t magic; // Magic number to identify the segment buffer - uint16_t uncommitted; // Number of uncommitted messages in this segment - uint16_t size; // Number of overall bytes in this segment + uint32_t magic; // Magic number to identify the segment buffer + uint16_t uncommitted; // Number of uncommitted messages in this segment + uint16_t size; // Number of overall bytes in this segment +#if QUEUE_SEGMENT_HEADER_SIZE > 0 + // Space for the consumer to prepend a header to the whole segment without copying it, + // used by the raw Ethernet transport for the Ethernet/IPv4/UDP header. See queue.h. + uint8_t segment_header[QUEUE_SEGMENT_HEADER_SIZE]; +#endif uint8_t msg_buffer[XCPTL_MAX_SEGMENT_SIZE]; // Segment/UDP MTU - concatenated transport layer messages tXcpMessage } tXcpSegmentBuffer; +// The segment payload must stay aligned, the accumulated tXcpMessage headers are read as words +static_assert((offsetof(tXcpSegmentBuffer, msg_buffer) % XCPTL_PACKET_ALIGNMENT) == 0, "segment payload must stay aligned to XCPTL_PACKET_ALIGNMENT"); + typedef struct Queue { uint32_t queue_buffer_size; // Size of queue memory allocated in bytes @@ -210,11 +224,18 @@ tQueueBuffer queueAcquire(tQueueHandle queue_handle, uint16_t packet_size) { return ret; } -#if XCPTL_PACKET_ALIGNMENT == 4 - packet_size = (uint16_t)((packet_size + 3) & 0xFFFC); // Add fill %4 -#else - assert(false); -#endif + // Round up to XCPTL_PACKET_ALIGNMENT, queue.h checks it to be a power of two. + // + // @@@@ TODO: The fill is included in the message dlc below, so LEN on the wire is larger than + // the actual XCP packet and the XCP client sees trailing filler bytes. The padding only exists + // so that the NEXT message in the segment starts aligned, so it is unnecessary for the last + // (or only) message in a datagram - a single message datagram is padded for no reason, which + // has surprised users. Trimming it would need the true unpadded length kept per message, it is + // not stored anywhere today. Note command responses are NOT padded (XcpTlSendCrm sets dlc + // directly), so the behaviour is asymmetric between DAQ and CRM. + // To be checked against ASAM XCP Part 3 (XCP on Ethernet): is a LEN larger than the packet + // content legal, and is any alignment required at all? See docs/TECHNICAL.md, Known Issues. + packet_size = (uint16_t)((packet_size + (XCPTL_PACKET_ALIGNMENT - 1)) & ~(XCPTL_PACKET_ALIGNMENT - 1)); msg_size = (uint16_t)(packet_size + XCPTL_TRANSPORT_LAYER_HEADER_SIZE); diff --git a/src/queue32m.c b/src/queue32m.c index 43fedfd9..ed13702f 100644 --- a/src/queue32m.c +++ b/src/queue32m.c @@ -24,6 +24,7 @@ #include // for assert #include // for PRIu64 #include // for bool +#include // for offsetof #include // for uint32_t, uint64_t, uint8_t, int64_t #include "xcptl.h" // for XcpTlGetCtr @@ -56,14 +57,27 @@ typedef struct { } tXcpMessage; static_assert(sizeof(tXcpMessage) == XCPTL_TRANSPORT_LAYER_HEADER_SIZE, "tXcpMessage size must be equal to XCPTL_TRANSPORT_LAYER_HEADER_SIZE"); +// The accumulated messages carry a tXcpMessage header of two uint16_t fields, which is accessed +// while walking a segment, so the message alignment must be a multiple of 2. +#if (XCPTL_PACKET_ALIGNMENT % 2) != 0 +#error "XCPTL_PACKET_ALIGNMENT must be a multiple of 2 in this queue variant: the 16 bit message header fields require it" +#endif typedef struct { - uint32_t magic; // Magic number to identify the segment buffer - uint16_t uncommitted; // Number of uncommitted messages in this segment - uint16_t size; // Number of overall bytes in this segment + uint32_t magic; // Magic number to identify the segment buffer + uint16_t uncommitted; // Number of uncommitted messages in this segment + uint16_t size; // Number of overall bytes in this segment +#if QUEUE_SEGMENT_HEADER_SIZE > 0 + // Space for the consumer to prepend a header to the whole segment without copying it, + // used by the raw Ethernet transport for the Ethernet/IPv4/UDP header. See queue.h. + uint8_t segment_header[QUEUE_SEGMENT_HEADER_SIZE]; +#endif uint8_t msg_buffer[XCPTL_MAX_SEGMENT_SIZE]; // Segment/UDP MTU - concatenated transport layer messages tXcpMessage } tXcpSegmentBuffer; +// The segment payload must stay aligned, the accumulated tXcpMessage headers are read as words +static_assert((offsetof(tXcpSegmentBuffer, msg_buffer) % XCPTL_PACKET_ALIGNMENT) == 0, "segment payload must stay aligned to XCPTL_PACKET_ALIGNMENT"); + typedef struct Queue { uint32_t queue_buffer_size; // Size of queue memory allocated in bytes @@ -83,8 +97,6 @@ typedef struct Queue { } tQueue; - - /* STM32H7 memory placement — DTCM vs AXI SRAM vs non-cacheable The STM32H7 has distinct memory regions with very different characteristics: @@ -137,7 +149,6 @@ static tXcpSegmentBuffer s_queue_buf[N] __attribute__((section(".noncacheable")) */ - //------------------------------------------------------------------------------------------------------------------------------------------------------- // Locking @@ -277,11 +288,18 @@ tQueueBuffer queueAcquire(tQueueHandle _queue_handle, uint16_t packet_size) { return ret; } -#if XCPTL_PACKET_ALIGNMENT == 4 - packet_size = (uint16_t)((packet_size + 3) & 0xFFFC); // Add fill %4 -#else - assert(false); -#endif + // Round up to XCPTL_PACKET_ALIGNMENT, queue.h checks it to be a power of two. + // + // @@@@ TODO: The fill is included in the message dlc below, so LEN on the wire is larger than + // the actual XCP packet and the XCP client sees trailing filler bytes. The padding only exists + // so that the NEXT message in the segment starts aligned, so it is unnecessary for the last + // (or only) message in a datagram - a single message datagram is padded for no reason, which + // has surprised users. Trimming it would need the true unpadded length kept per message, it is + // not stored anywhere today. Note command responses are NOT padded (XcpTlSendCrm sets dlc + // directly), so the behaviour is asymmetric between DAQ and CRM. + // To be checked against ASAM XCP Part 3 (XCP on Ethernet): is a LEN larger than the packet + // content legal, and is any alignment required at all? See docs/TECHNICAL.md, Known Issues. + packet_size = (uint16_t)((packet_size + (XCPTL_PACKET_ALIGNMENT - 1)) & ~(XCPTL_PACKET_ALIGNMENT - 1)); msg_size = (uint16_t)(packet_size + XCPTL_TRANSPORT_LAYER_HEADER_SIZE); diff --git a/src/queue64f.c b/src/queue64f.c index 53e20a54..e98f47ab 100644 --- a/src/queue64f.c +++ b/src/queue64f.c @@ -123,6 +123,18 @@ static_assert(sizeof(void *) == 8, "This implementation requires a 64 Bit platfo #error "(QUEUE_ENTRY_USER_PAYLOAD_SIZE+8) should be modulo CACHE_LINE_SIZE for optimal performance" #endif +// Every queue entry starts with an atomic_uint_least32_t entry_header and entries are laid out back +// to back, so the alignment of the entry length decides the alignment of that atomic. It must +// therefore be a multiple of 4. This is what made QUEUE_PAYLOAD_SIZE_ALIGNMENT == 2 unusable here. +#if (QUEUE_PAYLOAD_SIZE_ALIGNMENT % 4) != 0 +#error "QUEUE_PAYLOAD_SIZE_ALIGNMENT must be a multiple of 4 in this queue variant: the atomic entry header requires it" +#endif + +// This queue does not support message accumulation into segments (queue_pop() is not supported), so the segment header size must be 0 +#if QUEUE_SEGMENT_HEADER_SIZE > 0 +#error "QUEUE_SEGMENT_HEADER_SIZE not supported in this queue variant" +#endif + //------------------------------------------------------------------------------------------------------------------------------------------------------- // Test @@ -402,16 +414,10 @@ tQueueBuffer queueAcquire(tQueueHandle queue_handle, uint16_t packet_len) { // Align the entry length uint16_t entry_len = packet_len + QUEUE_ENTRY_USER_HEADER_SIZE; -#if QUEUE_PAYLOAD_SIZE_ALIGNMENT == 2 - entry_len = (uint16_t)((entry_len + 1) & 0xFFFE); // Add fill %2 -#error "QUEUE_PAYLOAD_SIZE_ALIGNMENT == 2 is not supported, use 4" -#endif -#if QUEUE_PAYLOAD_SIZE_ALIGNMENT == 4 - entry_len = (uint16_t)((entry_len + 3) & 0xFFFC); // Add fill %4 -#endif -#if QUEUE_PAYLOAD_SIZE_ALIGNMENT == 8 - entry_len = (uint16_t)((entry_len + 7) & 0xFFF8); // Add fill %8 -#endif + // Round up to QUEUE_PAYLOAD_SIZE_ALIGNMENT, queue.h checks it to be a power of two. + // This used to be #if branches for the values 2, 4 and 8, where an unknown value silently + // skipped the alignment altogether and the branch for 8 was unreachable. + entry_len = (uint16_t)((entry_len + (QUEUE_PAYLOAD_SIZE_ALIGNMENT - 1)) & ~(QUEUE_PAYLOAD_SIZE_ALIGNMENT - 1)); assert(entry_len <= QUEUE_MAX_ENTRY_SIZE); #ifdef TEST_ACQUIRE_LOCK_TIMING diff --git a/src/queue64v.c b/src/queue64v.c index 2368cb69..bab707d5 100644 --- a/src/queue64v.c +++ b/src/queue64v.c @@ -90,6 +90,18 @@ static_assert(sizeof(void *) == 8, "This implementation requires a 64 Bit platfo // Test atomic_uint_least32_t availability static_assert(sizeof(atomic_uint_least32_t) == 4, "atomic_uint_least32_t must be 4 bytes"); +// Every queue entry starts with an atomic_uint_least32_t entry_header and entries are laid out back +// to back, so the alignment of the entry length decides the alignment of that atomic. It must +// therefore be a multiple of 4. This is what made QUEUE_PAYLOAD_SIZE_ALIGNMENT == 2 unusable here. +#if (QUEUE_PAYLOAD_SIZE_ALIGNMENT % 4) != 0 +#error "QUEUE_PAYLOAD_SIZE_ALIGNMENT must be a multiple of 4 in this queue variant: the atomic entry header requires it" +#endif + +// This queue does not support message accumulation into segments (queue_pop() is not supported), so the segment header size must be 0 +#if QUEUE_SEGMENT_HEADER_SIZE > 0 +#error "QUEUE_SEGMENT_HEADER_SIZE not supported in this queue variant" +#endif + //------------------------------------------------------------------------------------------------------------------------------------------------------- // Test @@ -369,16 +381,10 @@ tQueueBuffer queueAcquire(tQueueHandle queue_handle, uint16_t packet_len) { // Align the entry length uint16_t entry_len = packet_len + QUEUE_ENTRY_USER_HEADER_SIZE; -#if QUEUE_PAYLOAD_SIZE_ALIGNMENT == 2 - entry_len = (uint16_t)((entry_len + 1) & 0xFFFE); // Add fill %2 -#error "QUEUE_PAYLOAD_SIZE_ALIGNMENT == 2 is not supported, use 4" -#endif -#if QUEUE_PAYLOAD_SIZE_ALIGNMENT == 4 - entry_len = (uint16_t)((entry_len + 3) & 0xFFFC); // Add fill %4 -#endif -#if QUEUE_PAYLOAD_SIZE_ALIGNMENT == 8 - entry_len = (uint16_t)((entry_len + 7) & 0xFFF8); // Add fill %8 -#endif + // Round up to QUEUE_PAYLOAD_SIZE_ALIGNMENT, queue.h checks it to be a power of two. + // This used to be #if branches for the values 2, 4 and 8, where an unknown value silently + // skipped the alignment altogether and the branch for 8 was unreachable. + entry_len = (uint16_t)((entry_len + (QUEUE_PAYLOAD_SIZE_ALIGNMENT - 1)) & ~(QUEUE_PAYLOAD_SIZE_ALIGNMENT - 1)); assert(entry_len <= QUEUE_MAX_ENTRY_SIZE); #ifdef TEST_ACQUIRE_LOCK_TIMING diff --git a/src/shm.c b/src/shm.c index 17bf1403..b0d84cb3 100644 --- a/src/shm.c +++ b/src/shm.c @@ -17,6 +17,7 @@ #ifdef OPTION_SHM_MODE #include // for assert +#include // for errno, ESRCH #include // for kill #include // for va_list, va_start, va_arg, va_end #include // for uint8_t, uint16_t, ... diff --git a/src/socket_raw.c b/src/socket_raw.c new file mode 100644 index 00000000..27dfc470 --- /dev/null +++ b/src/socket_raw.c @@ -0,0 +1,859 @@ +/*---------------------------------------------------------------------------- +| File: +| socket_raw.c +| +| Description: +| Raw-Ethernet XCP/UDP transport (OPTION_ENABLE_UDP_RAW) +| Hand-crafted UDP/IPv4 layer over a raw Ethernet HAL (socket_raw_hal.h), +| for targets without a TCP/IP stack. See docs/SOCKET_RAW.md +| +| Implements the socket API subset used by the XCP Ethernet transport layer: +| socketStartup, socketCleanup, socketGetErrorString, socketGetLastError, +| socketOpen, socketBind, socketRecvFrom, socketSendTo, +| socketSetTimeout, socketShutdown, socketClose +| +| ARP is answer-only: XCP is always master initiated, so the peer MAC is learned +| from the received frame and ARP Requests are never sent. Answering ARP Requests +| for our IP is required, the XCP client stack resolves us before the first CONNECT. +| +| Because the peer MAC is learned from the frame rather than resolved, no netmask +| and no default gateway are needed: with a client behind a router, the router MAC +| arrives as the frame source and the responses go back to it. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include "sockets.h" + +#include // for memcpy, memcmp, memset + +#include "assert.h" +#include "dbg_print.h" +#include "socket_raw_hal.h" // for the Ethernet HAL and the backend selection +#include "xcptl_cfg.h" // for XCPTL_MAX_SEGMENT_SIZE, XCPTL_MAX_CTO_SIZE + +#ifdef OPTION_ENABLE_UDP_RAW + +// All targets in scope are little endian (x86-64, ARM Cortex-M, Xtensa, Windows/XLAPI). +// The wire format stays big endian, but with the host endianness known that is a fixed +// byte swap rather than a portability question. MSVC defines no __BYTE_ORDER__ and +// targets little endian architectures only, so the guard simply does not fire there. +#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__) +#error "OPTION_ENABLE_UDP_RAW assumes a little endian host" +#endif + +//------------------------------------------------------------------------------------------------------- +// Wire format +// Host is little endian, the wire is big endian, so conversion is a plain byte swap. +// Uppercase to avoid any collision with a htons/ntohs macro from a platform header. + +// Note: do NOT name this HTONS/NTOHS - BSD derived platforms (macOS) define those +// uppercase names in as in-place ASSIGNMENT macros, which is a silent +// semantic collision. BE16 is a plain value returning byte swap, used in both directions. +// It is a function, not a macro, so that arguments with side effects (BE16(ident++)) +// are evaluated exactly once. +static inline uint16_t BE16(uint16_t v) { return (uint16_t)(((v & 0x00FFu) << 8) | ((v & 0xFF00u) >> 8)); } + +#pragma pack(push, 1) + +typedef struct { + uint8_t dst[6]; + uint8_t src[6]; + uint16_t ethertype; +} tEthHdr; + +typedef struct { + uint8_t ver_ihl; // 0x45 = IPv4, header length 5 words + uint8_t tos; + uint16_t total_length; // IPv4 header + UDP header + payload + uint16_t ident; + uint16_t flags_frag; // DF set, no fragmentation + uint8_t ttl; + uint8_t protocol; + uint16_t checksum; + uint8_t src[4]; // network order, never byte swapped + uint8_t dst[4]; +} tIp4Hdr; + +typedef struct { + uint16_t src_port; + uint16_t dst_port; + uint16_t length; // UDP header + payload + uint16_t checksum; +} tUdpHdr; + +typedef struct { + uint16_t htype; + uint16_t ptype; + uint8_t hlen; + uint8_t plen; + uint16_t oper; + uint8_t sha[6]; // sender hardware address + uint8_t spa[4]; // sender protocol address + uint8_t tha[6]; // target hardware address + uint8_t tpa[4]; // target protocol address +} tArpHdr; + +typedef struct { + uint8_t type; + uint8_t code; + uint16_t checksum; +} tIcmpHdr; + +#pragma pack(pop) + +#define ETH_HDR_LEN 14 +#define IP4_HDR_LEN 20 +#define UDP_HDR_LEN 8 +#define ARP_LEN 28 +#define RAW_HDR_LEN (ETH_HDR_LEN + IP4_HDR_LEN + UDP_HDR_LEN) // 42 + +// One XCP segment must fit into one Ethernet frame, enforced in xcptl_cfg.h +#define RAW_MAX_FRAME (RAW_HDR_LEN + XCPTL_MAX_SEGMENT_SIZE) + +#if XCPTL_TX_HEADROOM > 0 && (XCPTL_TX_HEADROOM < RAW_HDR_LEN) +#error "XCPTL_TX_HEADROOM must be at least 42 bytes to hold the Ethernet, IPv4 and UDP header" +#endif + +#define ETHERTYPE_IPV4 0x0800 +#define ETHERTYPE_ARP 0x0806 +#define ETHERTYPE_VLAN 0x8100 + +#define IP_PROTO_ICMP 1 +#define IP_PROTO_UDP 17 + +#define ARP_OPER_REQUEST 1 +#define ARP_OPER_REPLY 2 + +#define ICMP_TYPE_ECHO_REQUEST 8 +#define ICMP_TYPE_ECHO_REPLY 0 + +static const uint8_t sBroadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +//------------------------------------------------------------------------------------------------------- +// Socket context + +struct socket_raw { + tEthHalCtx *hal; + uint8_t local_mac[6]; + uint8_t local_ip[4]; // network order, set by socketBind + uint16_t local_port; // host order + uint8_t peer_mac[6]; // learned from the last accepted UDP datagram + bool peer_mac_valid; + uint16_t ip_ident; + uint32_t recv_timeout_ms; // 0 = infinite + volatile bool shutdown_requested; + bool is_open; + bool is_bound; + MUTEX tx_mutex; // serializes header build and eth_hal_send, see docs/SOCKET_RAW.md +}; + +// The raw transport is one per target, no heap needed on bare metal targets +static struct socket_raw sSocketRaw; + +// Backend specific interface selector, see socketRawSetInterface() +static const char *sInterfaceConfig = NULL; + +// Receive buffer with the NET_IP_ALIGN lead pad: if the compiler places sRxBuf on a 4 byte +// boundary, which it does in practice for a static array, the IPv4 header lands 4 byte aligned too. +// This is deliberately not enforced with an alignment attribute: it is only a codegen hint, and the +// packed wire structs make every access alignment safe regardless. +static uint8_t sRxBuf[2 + RAW_MAX_FRAME + 4]; +#define RX_FRAME (&sRxBuf[2]) + +// Buffer for ARP and ICMP replies, only used from the receive thread, under tx_mutex when sending +static uint8_t sCtrlBuf[RAW_MAX_FRAME]; + +// Last error +static int32_t sLastError = SOCKET_ERROR_NONE; + +//------------------------------------------------------------------------------------------------------- +// Errors + +int32_t socketGetLastError(void) { return sLastError; } + +const char *socketGetErrorString(int32_t err) { + switch (err) { + case SOCKET_ERROR_NONE: + return "no error"; + case SOCKET_ERROR_TIMEDOUT: + return "timed out"; + case SOCKET_ERROR_BADF: + return "socket closed"; + case SOCKET_ERROR_NOTCONN: + return "not connected"; + case SOCKET_ERROR_HAL: + return "Ethernet HAL error"; + case SOCKET_ERROR_TOOBIG: + return "frame too large"; + case SOCKET_ERROR_NOPEER: + return "peer MAC unknown"; + case SOCKET_ERROR_MSGSIZE: + return "frame too large for the link MTU"; + default: + return "unknown socket error"; + } +} + +//------------------------------------------------------------------------------------------------------- +// Checksums (RFC 1071) +// The byte stream is summed as big endian 16 bit words, the numeric result is stored +// into the header field with BE16() so the high byte goes first on the wire. + +static uint16_t checksum16(const uint8_t *p, uint16_t len, uint32_t sum) { + while (len > 1) { + sum += ((uint32_t)p[0] << 8) | (uint32_t)p[1]; + p += 2; + len = (uint16_t)(len - 2); + } + if (len > 0) { + sum += (uint32_t)p[0] << 8; // odd trailing byte, padded with zero + } + while ((sum >> 16) != 0) { + sum = (sum & 0xFFFFu) + (sum >> 16); + } + return (uint16_t)(~sum & 0xFFFFu); +} + +static uint16_t ipHeaderChecksum(tIp4Hdr *ip) { + ip->checksum = 0; + return checksum16((const uint8_t *)ip, IP4_HDR_LEN, 0); +} + +#ifdef OPTION_UDP_RAW_UDP_CHECKSUM_COMPUTE +// UDP checksum over the IPv4 pseudo header and the UDP header + payload (RFC 768) +static uint16_t udpChecksum(const tIp4Hdr *ip, const uint8_t *udp, uint16_t udp_len) { + uint32_t sum = 0; + // Pseudo header: src ip, dst ip, zero + protocol, UDP length + sum += ((uint32_t)ip->src[0] << 8) | ip->src[1]; + sum += ((uint32_t)ip->src[2] << 8) | ip->src[3]; + sum += ((uint32_t)ip->dst[0] << 8) | ip->dst[1]; + sum += ((uint32_t)ip->dst[2] << 8) | ip->dst[3]; + sum += (uint32_t)IP_PROTO_UDP; + sum += (uint32_t)udp_len; + uint16_t c = checksum16(udp, udp_len, sum); + // An all zero checksum must be transmitted as all ones, 0 means "no checksum" + return (c == 0) ? 0xFFFFu : c; +} +#endif + +//------------------------------------------------------------------------------------------------------- +// Address helpers + +static bool isValidLocalIp(const uint8_t *addr) { + if (addr == NULL) + return false; + if (addr[0] == 0) + return false; // 0.0.0.0 (ANY) has no meaning without an IP stack + if (addr[0] == 255 && addr[1] == 255 && addr[2] == 255 && addr[3] == 255) + return false; // broadcast + if (addr[0] >= 224 && addr[0] <= 239) + return false; // multicast + if (addr[0] == 127) + return false; // loopback, there is no loopback on a raw Ethernet link + return true; +} + +static bool isOurMac(const uint8_t *mac) { return memcmp(mac, sSocketRaw.local_mac, 6) == 0; } +static bool isBroadcastMac(const uint8_t *mac) { return memcmp(mac, sBroadcastMac, 6) == 0; } + +//------------------------------------------------------------------------------------------------------- +// Transmit + +// Send a fully built frame, serialized against the other transmit paths +static int16_t sendFrame(const uint8_t *frame, uint16_t len) { + mutexLock(&sSocketRaw.tx_mutex); + int16_t r = eth_hal_send(sSocketRaw.hal, frame, len); + mutexUnlock(&sSocketRaw.tx_mutex); + if (r < 0) { + sLastError = (r == ETH_HAL_ERROR_SIZE) ? SOCKET_ERROR_MSGSIZE : SOCKET_ERROR_HAL; + } + return r; +} + +//------------------------------------------------------------------------------------------------------- +// ARP - answer only +// +// We never send ARP Requests: the peer MAC is learned from the received XCP datagram. +// Answering Requests for our IP is mandatory, the XCP client stack resolves us first. +// The sender MAC of an ARP frame is deliberately NOT learned as the peer: an unrelated +// host asking for our IP must not be able to redirect the DAQ stream. + +static void handleArp(const uint8_t *frame, uint16_t len) { + + if (len < ETH_HDR_LEN + ARP_LEN) + return; + + const tArpHdr *arp = (const tArpHdr *)(frame + ETH_HDR_LEN); + if (arp->htype != BE16(1)) + return; // not Ethernet + if (arp->ptype != BE16(ETHERTYPE_IPV4)) + return; // not IPv4 + if (arp->hlen != 6 || arp->plen != 4) + return; + if (arp->oper != BE16(ARP_OPER_REQUEST)) + return; // we never send Requests, so Replies are of no interest + if (memcmp(arp->tpa, sSocketRaw.local_ip, 4) != 0) + return; // not asking for our IP + + // Build the Reply + memset(sCtrlBuf, 0, ETH_HDR_LEN + ARP_LEN); + tEthHdr *eth = (tEthHdr *)sCtrlBuf; + memcpy(eth->dst, arp->sha, 6); + memcpy(eth->src, sSocketRaw.local_mac, 6); + eth->ethertype = BE16(ETHERTYPE_ARP); + + tArpHdr *rep = (tArpHdr *)(sCtrlBuf + ETH_HDR_LEN); + rep->htype = BE16(1); + rep->ptype = BE16(ETHERTYPE_IPV4); + rep->hlen = 6; + rep->plen = 4; + rep->oper = BE16(ARP_OPER_REPLY); + memcpy(rep->sha, sSocketRaw.local_mac, 6); + memcpy(rep->spa, sSocketRaw.local_ip, 4); + memcpy(rep->tha, arp->sha, 6); + memcpy(rep->tpa, arp->spa, 4); + + DBG_PRINTF5("socket_raw: ARP request for %u.%u.%u.%u, sending reply\n", sSocketRaw.local_ip[0], sSocketRaw.local_ip[1], sSocketRaw.local_ip[2], sSocketRaw.local_ip[3]); + sendFrame(sCtrlBuf, ETH_HDR_LEN + ARP_LEN); +} + +#ifdef OPTION_UDP_RAW_GRATUITOUS_ARP +// Gratuitous ARP announcement: primes switch MAC tables and the ARP cache of the client. +// Not required for correctness, ARP Requests for our IP are always answered. +static void sendGratuitousArp(void) { + + memset(sCtrlBuf, 0, ETH_HDR_LEN + ARP_LEN); + tEthHdr *eth = (tEthHdr *)sCtrlBuf; + memcpy(eth->dst, sBroadcastMac, 6); + memcpy(eth->src, sSocketRaw.local_mac, 6); + eth->ethertype = BE16(ETHERTYPE_ARP); + + tArpHdr *arp = (tArpHdr *)(sCtrlBuf + ETH_HDR_LEN); + arp->htype = BE16(1); + arp->ptype = BE16(ETHERTYPE_IPV4); + arp->hlen = 6; + arp->plen = 4; + arp->oper = BE16(ARP_OPER_REQUEST); + memcpy(arp->sha, sSocketRaw.local_mac, 6); + memcpy(arp->spa, sSocketRaw.local_ip, 4); + memset(arp->tha, 0, 6); + memcpy(arp->tpa, sSocketRaw.local_ip, 4); // announcement: target == sender + + DBG_PRINT3(" Sending gratuitous ARP announcement\n"); + sendFrame(sCtrlBuf, ETH_HDR_LEN + ARP_LEN); +} +#endif + +//------------------------------------------------------------------------------------------------------- +// ICMP Echo - answer only +// +// A successful ping proves the Ethernet HAL, the MAC filter, the ARP reply, the IPv4 +// header build and the header checksum all work, before any XCP tooling is involved. + +#ifdef OPTION_UDP_RAW_ENABLE_ICMP_ECHO +static void handleIcmp(const uint8_t *frame, uint16_t len, const tIp4Hdr *ip, uint16_t ip_hdr_len) { + + uint16_t total_length = BE16(ip->total_length); + if (total_length < ip_hdr_len + (uint16_t)sizeof(tIcmpHdr)) + return; + uint16_t icmp_len = (uint16_t)(total_length - ip_hdr_len); + if ((uint32_t)ETH_HDR_LEN + total_length > len) + return; // truncated + if ((uint32_t)ETH_HDR_LEN + IP4_HDR_LEN + icmp_len > sizeof(sCtrlBuf)) + return; // would not fit into the reply buffer + + const tIcmpHdr *icmp = (const tIcmpHdr *)(frame + ETH_HDR_LEN + ip_hdr_len); + if (icmp->type != ICMP_TYPE_ECHO_REQUEST || icmp->code != 0) + return; + + const tEthHdr *req_eth = (const tEthHdr *)frame; + + // Ethernet header: back to the sender + tEthHdr *eth = (tEthHdr *)sCtrlBuf; + memcpy(eth->dst, req_eth->src, 6); + memcpy(eth->src, sSocketRaw.local_mac, 6); + eth->ethertype = BE16(ETHERTYPE_IPV4); + + // IPv4 header: fresh, without any options of the request + tIp4Hdr *rip = (tIp4Hdr *)(sCtrlBuf + ETH_HDR_LEN); + memset(rip, 0, IP4_HDR_LEN); + rip->ver_ihl = 0x45; + rip->total_length = BE16((uint16_t)(IP4_HDR_LEN + icmp_len)); + rip->ident = BE16(sSocketRaw.ip_ident++); + rip->flags_frag = BE16(0x4000); // DF + rip->ttl = 64; + rip->protocol = IP_PROTO_ICMP; + memcpy(rip->src, sSocketRaw.local_ip, 4); + memcpy(rip->dst, ip->src, 4); + rip->checksum = BE16(ipHeaderChecksum(rip)); + + // ICMP: copy the request, turn it into a reply and recompute the checksum. + // Ping is a manual bring-up aid, not a hot path, so a full recompute is preferred + // over an incremental update - it is simpler and has no carry handling to get wrong. + uint8_t *ricmp = sCtrlBuf + ETH_HDR_LEN + IP4_HDR_LEN; + memcpy(ricmp, frame + ETH_HDR_LEN + ip_hdr_len, icmp_len); + ((tIcmpHdr *)ricmp)->type = ICMP_TYPE_ECHO_REPLY; + ((tIcmpHdr *)ricmp)->checksum = 0; + ((tIcmpHdr *)ricmp)->checksum = BE16(checksum16(ricmp, icmp_len, 0)); + + DBG_PRINTF5("socket_raw: ICMP echo request from %u.%u.%u.%u, sending reply\n", ip->src[0], ip->src[1], ip->src[2], ip->src[3]); + sendFrame(sCtrlBuf, (uint16_t)(ETH_HDR_LEN + IP4_HDR_LEN + icmp_len)); +} +#endif + +//------------------------------------------------------------------------------------------------------- +// Receive path +// +// handleFrame classifies one received frame. The filter order is cheapest and most +// discriminating first, so that on a busy link almost every foreign frame dies early. +// Returns: > 0 payload bytes copied to buffer, this is an XCP datagram for us +// == 0 not for us, or consumed (ARP/ICMP answered) - the caller keeps looping + +static int16_t handleFrame(const uint8_t *frame, uint16_t len, uint8_t *buffer, uint16_t bufferSize, uint8_t *srcAddr, uint16_t *srcPort) { + + if (len < ETH_HDR_LEN) + return 0; + + const tEthHdr *eth = (const tEthHdr *)frame; + uint16_t ethertype = BE16(eth->ethertype); + + if (ethertype == ETHERTYPE_ARP) { + handleArp(frame, len); + return 0; + } + if (ethertype != ETHERTYPE_IPV4) { + if (ethertype == ETHERTYPE_VLAN) { + // VLAN tagged frames are out of scope for this transport. Report it once per + // frame at a high debug level: a trunk port is then diagnosable instead of + // silently dead. See docs/SOCKET_RAW.md. + DBG_PRINT5("socket_raw: VLAN tagged frame dropped (802.1Q is not supported)\n"); + } + return 0; + } + + // Our unicast MAC or broadcast only. + // Normally redundant: the socket is not put into promiscuous mode, so the NIC hardware filter + // already drops the unicast traffic of other hosts. It becomes load bearing as soon as anything + // else enables promiscuous mode on the interface - running tcpdump on it while debugging is + // enough - because then foreign unicast frames do arrive and must not enter the XCP path. + if (!isOurMac(eth->dst) && !isBroadcastMac(eth->dst)) + return 0; + + if (len < ETH_HDR_LEN + IP4_HDR_LEN) + return 0; + const tIp4Hdr *ip = (const tIp4Hdr *)(frame + ETH_HDR_LEN); + + if ((ip->ver_ihl >> 4) != 4) + return 0; + uint16_t ip_hdr_len = (uint16_t)((ip->ver_ihl & 0x0F) * 4); + if (ip_hdr_len < IP4_HDR_LEN) + return 0; + uint16_t total_length = BE16(ip->total_length); + if (total_length < ip_hdr_len) + return 0; + if ((uint32_t)ETH_HDR_LEN + total_length > len) + return 0; // truncated frame + + // No reassembly: a fragmented XCP datagram is a configuration error worth reporting + if ((BE16(ip->flags_frag) & 0x3FFF) != 0) { + DBG_PRINT_WARNING("socket_raw: fragmented IPv4 datagram dropped, the raw transport does not reassemble\n"); + return 0; + } + + // Addressed to us + if (memcmp(ip->dst, sSocketRaw.local_ip, 4) != 0) { + static const uint8_t bcast_ip[4] = {255, 255, 255, 255}; + if (memcmp(ip->dst, bcast_ip, 4) != 0) + return 0; + } + +#ifdef OPTION_UDP_RAW_VERIFY_RX_CHECKSUM + // Summing a correct header including its checksum field yields 0. + // On a switched link the Ethernet FCS already covers the wire, so this mostly + // catches our own parser bugs - which is exactly the point during bring-up. + if (checksum16((const uint8_t *)ip, ip_hdr_len, 0) != 0) { + DBG_PRINT_WARNING("socket_raw: IPv4 header checksum error, frame dropped\n"); + return 0; + } +#endif + +#ifdef OPTION_UDP_RAW_ENABLE_ICMP_ECHO + if (ip->protocol == IP_PROTO_ICMP) { + handleIcmp(frame, len, ip, ip_hdr_len); + return 0; + } +#endif + + if (ip->protocol != IP_PROTO_UDP) + return 0; + + if (total_length < ip_hdr_len + UDP_HDR_LEN) + return 0; + const tUdpHdr *udp = (const tUdpHdr *)(frame + ETH_HDR_LEN + ip_hdr_len); + + // Our port - on a busy link almost every remaining frame dies here + if (BE16(udp->dst_port) != sSocketRaw.local_port) + return 0; + + uint16_t udp_len = BE16(udp->length); + if (udp_len < UDP_HDR_LEN) + return 0; + if (udp_len > (uint16_t)(total_length - ip_hdr_len)) + return 0; // inconsistent with the IPv4 total length + uint16_t payload_len = (uint16_t)(udp_len - UDP_HDR_LEN); + + // Never truncate: a truncated XCP message fails the dlc check in xcpethtl.c and + // surfaces as a confusing "Corrupt message received!" + if (payload_len > bufferSize) { + DBG_PRINTF_WARNING("socket_raw: UDP payload of %u bytes exceeds the receive buffer of %u bytes, frame dropped\n", payload_len, bufferSize); + return 0; + } + + memcpy(buffer, (const uint8_t *)udp + UDP_HDR_LEN, payload_len); + if (srcAddr != NULL) + memcpy(srcAddr, ip->src, 4); + if (srcPort != NULL) + *srcPort = BE16(udp->src_port); + + // Learn the peer - only from an accepted datagram, never from ARP or a broadcast + if (!isBroadcastMac(eth->dst)) { + memcpy(sSocketRaw.peer_mac, eth->src, 6); + sSocketRaw.peer_mac_valid = true; + } + + return (int16_t)payload_len; +} + +//------------------------------------------------------------------------------------------------------- +// Socket API + +bool socketStartup(void) { + memset(&sSocketRaw, 0, sizeof(sSocketRaw)); + sLastError = SOCKET_ERROR_NONE; + return true; +} + +void socketCleanup(void) {} + +// Select the Ethernet interface used by the raw transport. +// The string is backend specific and opaque here (Linux: interface name such as "eth0"). +// Must be called before XcpEthServerInit(), defaults to OPTION_UDP_RAW_IFNAME. +void socketRawSetInterface(const char *config) { sInterfaceConfig = config; } + +// Get the local MAC address, as reported by the Ethernet HAL in socketOpen() +bool socketRawGetLocalMac(SOCKET_HANDLE socket, uint8_t *mac) { + if (socket == INVALID_SOCKET_HANDLE || mac == NULL || !socket->is_open) + return false; + memcpy(mac, socket->local_mac, 6); + return true; +} + +bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags) { + + assert(socketp != NULL); + *socketp = INVALID_SOCKET_HANDLE; + + if ((flags & SOCKET_MODE_TCP) != 0) { + DBG_PRINT_ERROR("socketOpen: the raw Ethernet transport does not support TCP\n"); + return false; + } + if (sSocketRaw.is_open) { + DBG_PRINT_ERROR("socketOpen: the raw Ethernet transport supports one socket only\n"); + return false; + } + + memset(&sSocketRaw, 0, sizeof(sSocketRaw)); + sSocketRaw.recv_timeout_ms = 0; // infinite until socketSetTimeout() + + if (!eth_hal_open(sInterfaceConfig, &sSocketRaw.hal)) { + sLastError = SOCKET_ERROR_HAL; + return false; + } + if (!eth_hal_get_mac(sSocketRaw.hal, sSocketRaw.local_mac)) { + DBG_PRINT_ERROR("socketOpen: could not read the MAC address from the Ethernet HAL\n"); + eth_hal_close(sSocketRaw.hal); + sSocketRaw.hal = NULL; + sLastError = SOCKET_ERROR_HAL; + return false; + } + // A multicast or all zero MAC means the HAL did not report a usable address + if ((sSocketRaw.local_mac[0] & 0x01) != 0) { + DBG_PRINT_ERROR("socketOpen: the Ethernet HAL reported a multicast MAC address\n"); + eth_hal_close(sSocketRaw.hal); + sSocketRaw.hal = NULL; + sLastError = SOCKET_ERROR_HAL; + return false; + } + static const uint8_t zero_mac[6] = {0, 0, 0, 0, 0, 0}; + if (memcmp(sSocketRaw.local_mac, zero_mac, 6) == 0) { + DBG_PRINT_ERROR("socketOpen: the Ethernet HAL reported an all zero MAC address\n"); + eth_hal_close(sSocketRaw.hal); + sSocketRaw.hal = NULL; + sLastError = SOCKET_ERROR_HAL; + return false; + } + + mutexInit(&sSocketRaw.tx_mutex, false, 1000); + sSocketRaw.is_open = true; + *socketp = &sSocketRaw; + return true; +} + +// Bind to the local IPv4 address and UDP port. +// There is no DHCP and no IP stack, so the address must be a concrete unicast address: +// the application passes it to XcpEthServerInit(), from where it reaches this function. +bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port) { + + assert(socket != INVALID_SOCKET_HANDLE); + + if (!isValidLocalIp(addr)) { + DBG_PRINTF_ERROR("socketBind: the raw Ethernet transport needs a concrete local IPv4 address, got %u.%u.%u.%u.\n" + " There is no IP stack and no DHCP: pass the address of this target to XcpEthServerInit().\n", + addr != NULL ? addr[0] : 0, addr != NULL ? addr[1] : 0, addr != NULL ? addr[2] : 0, addr != NULL ? addr[3] : 0); + return false; + } + + memcpy(socket->local_ip, addr, 4); + socket->local_port = port; + socket->is_bound = true; + + DBG_PRINTF3(" Raw Ethernet transport bound to %u.%u.%u.%u:%u, MAC=%02X:%02X:%02X:%02X:%02X:%02X\n", addr[0], addr[1], addr[2], addr[3], port, socket->local_mac[0], + socket->local_mac[1], socket->local_mac[2], socket->local_mac[3], socket->local_mac[4], socket->local_mac[5]); + +#ifdef OPTION_UDP_RAW_GRATUITOUS_ARP + sendGratuitousArp(); +#endif + + return true; +} + +bool socketSetTimeout(SOCKET_HANDLE socket, uint32_t timeoutMs) { + assert(socket != INVALID_SOCKET_HANDLE); + socket->recv_timeout_ms = timeoutMs; // 0 = infinite + return true; +} + +bool socketShutdown(SOCKET_HANDLE socket) { + if (socket == INVALID_SOCKET_HANDLE) + return true; + socket->shutdown_requested = true; + eth_hal_wakeup(socket->hal); // unblock a receive in progress + return true; +} + +bool socketClose(SOCKET_HANDLE *socketp) { + assert(socketp != NULL); + SOCKET_HANDLE socket = *socketp; + *socketp = INVALID_SOCKET_HANDLE; + if (socket == INVALID_SOCKET_HANDLE || !socket->is_open) + return true; + socket->is_open = false; + if (socket->hal != NULL) { + eth_hal_close(socket->hal); + socket->hal = NULL; + } + mutexDestroy(&socket->tx_mutex); + return true; +} + +// Receive one XCP datagram, blocking with the timeout set by socketSetTimeout(). +// +// A raw socket sees every frame on the wire, not just ours. Returning 0 for each +// filtered frame would make the caller run its background tasks once per foreign +// frame, so this loops internally instead. The deadline is absolute and computed +// once on entry: filtered traffic consumes the timeout budget but never extends it +// and never causes an early return, so the blocking time per call stays bounded by +// what the caller asked for. +// +// Return values: > 0 bytes received +// == 0 timeout, no data - the caller does background work and loops +// < 0 socket closed or error - the caller exits its receive loop +int16_t socketRecvFrom(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, uint8_t *srcAddr, uint16_t *srcPort, uint64_t *time) { + + assert(socket != INVALID_SOCKET_HANDLE); + assert(buffer != NULL); + + // Cap on a single HAL wait, so shutdown is noticed even with an infinite timeout + const uint32_t max_slice_ms = 100; + + bool infinite = (socket->recv_timeout_ms == 0); + uint64_t deadline_ns = infinite ? 0 : clockGetMonotonicNs() + (uint64_t)socket->recv_timeout_ms * 1000000ULL; + + for (;;) { + + if (socket->shutdown_requested || !socket->is_open) { + sLastError = SOCKET_ERROR_BADF; + return -1; + } + + uint32_t slice_ms = max_slice_ms; + if (!infinite) { + uint64_t now_ns = clockGetMonotonicNs(); + if (now_ns >= deadline_ns) { + sLastError = SOCKET_ERROR_TIMEDOUT; + return 0; // timeout + } + uint64_t remaining_ms = (deadline_ns - now_ns) / 1000000ULL; + if (remaining_ms < slice_ms) + slice_ms = (uint32_t)remaining_ms; + if (slice_ms == 0) + slice_ms = 1; // do not busy poll on the last fraction of a millisecond + } + + int16_t n = eth_hal_recv(socket->hal, RX_FRAME, RAW_MAX_FRAME, slice_ms); + if (n < 0) { + sLastError = SOCKET_ERROR_HAL; + return -1; + } + if (n == 0) + continue; // slice expired or frame suppressed by the HAL, re-check the deadline + + int16_t r = handleFrame(RX_FRAME, (uint16_t)n, buffer, bufferSize, srcAddr, srcPort); + if (r > 0) { + if (time != NULL) + *time = clockGet(); + return r; + } + // Not for us, or consumed by the ARP/ICMP handlers - keep waiting for our datagram + } +} + +// Build the Ethernet/IPv4/UDP header for a payload of payload_len bytes into hdr[0..41]. +// Precondition: tx_mutex held - ip_ident is incremented and peer_mac is read here. +// If the UDP checksum is computed, the payload must already be contiguous behind the header. +static void buildFrameHeader(struct socket_raw *socket, uint8_t *hdr, uint16_t payload_len, const uint8_t *addr, uint16_t port) { + + tEthHdr *eth = (tEthHdr *)hdr; + memcpy(eth->dst, socket->peer_mac, 6); + memcpy(eth->src, socket->local_mac, 6); + eth->ethertype = BE16(ETHERTYPE_IPV4); + + tIp4Hdr *ip = (tIp4Hdr *)(hdr + ETH_HDR_LEN); + memset(ip, 0, IP4_HDR_LEN); + ip->ver_ihl = 0x45; + ip->total_length = BE16((uint16_t)(IP4_HDR_LEN + UDP_HDR_LEN + payload_len)); + ip->ident = BE16(socket->ip_ident); + socket->ip_ident++; + ip->flags_frag = BE16(0x4000); // DF, never fragment + ip->ttl = 64; + ip->protocol = IP_PROTO_UDP; + memcpy(ip->src, socket->local_ip, 4); + memcpy(ip->dst, addr, 4); + ip->checksum = BE16(ipHeaderChecksum(ip)); + + tUdpHdr *udp = (tUdpHdr *)(hdr + ETH_HDR_LEN + IP4_HDR_LEN); + udp->src_port = BE16(socket->local_port); + udp->dst_port = BE16(port); + udp->length = BE16((uint16_t)(UDP_HDR_LEN + payload_len)); + udp->checksum = 0; +#ifdef OPTION_UDP_RAW_UDP_CHECKSUM_COMPUTE + udp->checksum = BE16(udpChecksum(ip, (const uint8_t *)udp, (uint16_t)(UDP_HDR_LEN + payload_len))); +#endif + // OPTION_UDP_RAW_UDP_CHECKSUM_ZERO: 0 is legal for IPv4 and means "no checksum" (RFC 768) + // OPTION_UDP_RAW_UDP_CHECKSUM_HW: 0 as well, the EMAC inserts the checksum +} + +// Common checks for both send paths, returns false and sets sLastError when the send must not happen +static bool checkSend(SOCKET_HANDLE socket, uint16_t bufferSize, int16_t *result) { + + if (!socket->is_open) { + sLastError = SOCKET_ERROR_BADF; + *result = 0; // closed socket + return false; + } + if (bufferSize == 0 || bufferSize > XCPTL_MAX_SEGMENT_SIZE) { + DBG_PRINTF_ERROR("socketSendTo: payload of %u bytes exceeds the maximum segment size of %u\n", bufferSize, (uint16_t)XCPTL_MAX_SEGMENT_SIZE); + sLastError = SOCKET_ERROR_TOOBIG; + *result = -1; + return false; + } + // The peer MAC is learned from the received datagram, and the transport layer only + // sends after it has received something, so this cannot normally happen + if (!socket->peer_mac_valid) { + DBG_PRINT_ERROR("socketSendTo: peer MAC unknown, nothing has been received yet\n"); + sLastError = SOCKET_ERROR_NOPEER; + *result = -1; + return false; + } + return true; +} + +// Map an eth_hal_send result to the socket API return value +static int16_t mapSendResult(int16_t r, uint16_t bufferSize) { + if (r < 0) { + // The HAL reports a frame too large for the link separately: like the socket transport + // with IP_MTU_DISCOVER, this surfaces as a distinct error rather than silent truncation. + // There is no IPv4 fragmentation here, so this is always a configuration problem. + if (r == ETH_HAL_ERROR_SIZE) { + sLastError = SOCKET_ERROR_MSGSIZE; + DBG_PRINTF_ERROR("socketSendTo: segment of %u bytes does not fit into one Ethernet frame on this link.\n" + " Reduce OPTION_MTU (currently %u, giving XCPTL_MAX_SEGMENT_SIZE=%u), see the interface MTU reported above.\n", + bufferSize, (unsigned)OPTION_MTU, (unsigned)XCPTL_MAX_SEGMENT_SIZE); + } else { + sLastError = SOCKET_ERROR_HAL; + } + return -1; + } + return (int16_t)bufferSize; // the payload size, as the transport layer expects +} + +// Send one UDP datagram to addr:port. +// The payload is copied into a frame buffer behind the header. Used for command responses, which +// are built on the stack and therefore have no headroom in front of them. +// Returns: bytes sent (the PAYLOAD size, not the frame size - XcpEthTlSend compares the result +// against the payload size), 0 on closed socket, -1 on error +int16_t socketSendTo(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize, const uint8_t *addr, uint16_t port, uint64_t *time) { + + assert(socket != INVALID_SOCKET_HANDLE); + assert(buffer != NULL); + assert(addr != NULL); + + int16_t early; + if (!checkSend(socket, bufferSize, &early)) + return early; + + static uint8_t tx_frame[RAW_MAX_FRAME]; + + mutexLock(&socket->tx_mutex); + memcpy(tx_frame + RAW_HDR_LEN, buffer, bufferSize); + buildFrameHeader(socket, tx_frame, bufferSize, addr, port); + if (time != NULL) + *time = clockGet(); + int16_t r = eth_hal_send(socket->hal, tx_frame, (uint16_t)(RAW_HDR_LEN + bufferSize)); + mutexUnlock(&socket->tx_mutex); + + return mapSendResult(r, bufferSize); +} + +#if XCPTL_TX_HEADROOM > 0 +// Send one UDP datagram from a buffer which has XCPTL_TX_HEADROOM writable bytes in front +// of it, so the header is written in place and the payload is not copied at all. +// Used for the DAQ transmit path, where the buffer is a transmit queue segment. +// Returns: as socketSendTo +int16_t socketSendToReserved(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize, const uint8_t *addr, uint16_t port, uint64_t *time) { + + assert(socket != INVALID_SOCKET_HANDLE); + assert(buffer != NULL); + assert(addr != NULL); + + int16_t early; + if (!checkSend(socket, bufferSize, &early)) + return early; + + // The caller guarantees the reservation, see the contract in sockets.h. The header is written + // right justified into it, which is why the reservation may be larger than RAW_HDR_LEN. + uint8_t *frame = (uint8_t *)(uintptr_t)buffer - RAW_HDR_LEN; + + mutexLock(&socket->tx_mutex); + buildFrameHeader(socket, frame, bufferSize, addr, port); + if (time != NULL) + *time = clockGet(); + int16_t r = eth_hal_send(socket->hal, frame, (uint16_t)(RAW_HDR_LEN + bufferSize)); + mutexUnlock(&socket->tx_mutex); + + return mapSendResult(r, bufferSize); +} +#endif // XCPTL_TX_HEADROOM > 0 + +#endif // OPTION_ENABLE_UDP_RAW diff --git a/src/socket_raw_hal.h b/src/socket_raw_hal.h new file mode 100644 index 00000000..d031f1d5 --- /dev/null +++ b/src/socket_raw_hal.h @@ -0,0 +1,107 @@ +#pragma once + +/*---------------------------------------------------------------------------- +| File: +| socket_raw_hal.h +| +| Description: +| Raw Ethernet HAL for the OPTION_ENABLE_UDP_RAW transport (src/socket_raw.c) +| +| socket_raw.c implements UDP/IPv4, ARP and ICMP on top of this interface. +| A port has to provide send/receive of complete Ethernet frames and the local +| MAC address - nothing else. Backends: +| socket_raw_hal_linux.c Linux AF_PACKET (development and test) +| socket_raw_hal_xlapi.c Vector XLAPI on Windows (future) +| socket_raw_hal_cmp.c ASAM CMP capture modules (future) +| +| Frame contract: +| Frames are complete Ethernet frames WITHOUT FCS: dst MAC, src MAC, EtherType, +| payload. Frames as short as 50 bytes are passed to eth_hal_send(); if the MAC +| of the port does not pad to the 60 byte Ethernet minimum, the port must do it. +| The largest frame socket_raw.c will ever pass, and the max_len it offers to +| eth_hal_recv(), is 42 + XCPTL_MAX_SEGMENT_SIZE, which is OPTION_MTU + 10 +| (1434 bytes with the OPTION_MTU of 1420 the raw configuration uses). Jumbo frames +| are therefore supported by configuring OPTION_MTU accordingly. 802.1Q VLAN tags are not. +| Whether the link can actually carry that frame is a RUNTIME property that only +| the backend knows: if it cannot, eth_hal_send() returns ETH_HAL_ERROR_SIZE. +| There is deliberately no compile time frame size limit, see docs/SOCKET_RAW.md. +| +| Threading contract: +| eth_hal_send() always called with the transmit mutex of socket_raw.c held, +| therefore it does NOT need to be reentrant +| eth_hal_recv() called from the XCP receive thread only +| eth_hal_wakeup() may be called from any thread +| +| Backend specific configuration (interface name, XLAPI channel, CMP device and +| stream id, ...) is passed as an opaque string to eth_hal_open() and parsed by +| the backend. socket_raw.c never interprets it. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include // for bool +#include // for uint8_t, uint16_t, int16_t + +#include "platform.h" // for the platform defines (_LINUX, _WIN, ...) and OPTION_xxx + +#ifdef OPTION_ENABLE_UDP_RAW + +// Select the HAL backend +// +// OPTION_UDP_RAW_HAL_EXTERNAL lets an application provide its own backend out of tree: xcplib then +// selects none, and the eth_hal_* symbols stay undefined in libxcplite until the application links +// its own implementation against it. Use this for backends which do not belong in the library, for +// example ASAM CMP for testing XCP tools through capture modules, or a vendor specific interface. +// The application implements the functions declared below, compiles its own source file, and +// defines OPTION_UDP_RAW_HAL_EXTERNAL in its configuration override header. +#if defined(OPTION_UDP_RAW_HAL_EXTERNAL) +// The application provides the backend, nothing is selected here +#elif defined(_LINUX) +// socket_raw_hal_linux.c +#else +#error "OPTION_ENABLE_UDP_RAW has a HAL backend for Linux (AF_PACKET) only, or define OPTION_UDP_RAW_HAL_EXTERNAL to supply your own - see docs/SOCKET_RAW.md" +#endif + +// Error returns of eth_hal_send() / eth_hal_recv() +// ETH_HAL_ERROR_SIZE is reported separately because it is a configuration problem, not a +// transient error: the frame is larger than the link can carry and there is no fragmentation. +#define ETH_HAL_ERROR (-1) +#define ETH_HAL_ERROR_SIZE (-2) + +// Opaque per interface context of the HAL backend +typedef struct eth_hal_ctx tEthHalCtx; + +// Open the Ethernet interface +// config: backend specific selector, may be NULL when the backend needs none +// Linux: interface name, e.g. "eth0" +// Returns true on success, *ctx is then valid until eth_hal_close() +bool eth_hal_open(const char *config, tEthHalCtx **ctx); + +// Close the Ethernet interface and release all resources +void eth_hal_close(tEthHalCtx *ctx); + +// Get the MAC address of the interface +// mac: output buffer, must point to at least 6 bytes +// Returns true on success +bool eth_hal_get_mac(tEthHalCtx *ctx, uint8_t *mac); + +// Send one complete Ethernet frame (without FCS) +// Returns: len on success +// ETH_HAL_ERROR_SIZE if the frame exceeds what this interface can carry +// ETH_HAL_ERROR on any other error +int16_t eth_hal_send(tEthHalCtx *ctx, const uint8_t *frame, uint16_t len); + +// Receive one complete Ethernet frame (without FCS), blocking with timeout +// Frames sent by this application itself must not be returned +// timeout_ms: 0 = poll and return immediately +// Returns: > 0 bytes received +// == 0 timeout expired, no frame available +// < 0 fatal error +int16_t eth_hal_recv(tEthHalCtx *ctx, uint8_t *frame, uint16_t max_len, uint32_t timeout_ms); + +// Abort a blocked eth_hal_recv() +// Optional: a backend which can not do this may implement it as a no-op, socket_raw.c +// caps each eth_hal_recv() timeout slice so shutdown still works, just less promptly +void eth_hal_wakeup(tEthHalCtx *ctx); + +#endif // OPTION_ENABLE_UDP_RAW diff --git a/src/socket_raw_hal_linux.c b/src/socket_raw_hal_linux.c new file mode 100644 index 00000000..29ab825e --- /dev/null +++ b/src/socket_raw_hal_linux.c @@ -0,0 +1,265 @@ +/*---------------------------------------------------------------------------- +| File: +| socket_raw_hal_linux.c +| +| Description: +| Raw Ethernet HAL backend for Linux, using AF_PACKET (see socket_raw_hal.h) +| +| Requires CAP_NET_RAW: +| sudo setcap cap_net_raw+ep (or run as root) +| +| SOCK_RAW (not SOCK_DGRAM) because socket_raw.c builds its own Ethernet header, +| ETH_P_ALL (not ETH_P_IP) because ARP frames must be seen as well. +| +| A blocked eth_hal_recv() is unblocked with an eventfd rather than SO_RCVTIMEO, +| so shutdown is immediate instead of up to one timeout slice late, and so an +| infinite timeout stays interruptible. +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include "platform.h" // for the platform defines (_LINUX) and OPTION_xxx via xcplib_cfg.h + +// Not compiled when the application supplies its own backend, see OPTION_UDP_RAW_HAL_EXTERNAL +#if defined(OPTION_ENABLE_UDP_RAW) && defined(_LINUX) && !defined(OPTION_UDP_RAW_HAL_EXTERNAL) + +#include // for htons +#include // for errno +#include // for ETH_P_ALL +#include // for sockaddr_ll, PACKET_OUTGOING, PACKET_IGNORE_OUTGOING +#include // for ifreq, IFNAMSIZ +#include // for poll +#include // for malloc, free +#include // for memset, memcpy, strncpy, strerror +#include // for eventfd +#include // for ioctl, SIOCGIFINDEX, SIOCGIFHWADDR +#include // for socket, bind, setsockopt, recvfrom +#include // for ssize_t +#include // for close, read, write + +#include "assert.h" +#include "dbg_print.h" +#include "socket_raw_hal.h" + +#define ETH_HDR_LEN 14 // Ethernet header, not covered by the interface MTU + +struct eth_hal_ctx { + int fd; // AF_PACKET socket + int wakeup_fd; // eventfd used by eth_hal_wakeup() + int ifindex; // interface index + unsigned int mtu; // interface MTU, i.e. the largest IP packet, excluding the Ethernet header + uint8_t mac[6]; // interface MAC address + char ifname[IFNAMSIZ]; // interface name +}; + +bool eth_hal_open(const char *config, tEthHalCtx **ctxp) { + + assert(ctxp != NULL); + *ctxp = NULL; + + const char *ifname = (config != NULL && config[0] != 0) ? config : OPTION_UDP_RAW_IFNAME; + + tEthHalCtx *ctx = (tEthHalCtx *)malloc(sizeof(tEthHalCtx)); + if (ctx == NULL) { + DBG_PRINT_ERROR("eth_hal_open: out of memory\n"); + return false; + } + memset(ctx, 0, sizeof(tEthHalCtx)); + ctx->fd = -1; + ctx->wakeup_fd = -1; + strncpy(ctx->ifname, ifname, sizeof(ctx->ifname) - 1); + + // Raw packet socket, all EtherTypes (IPv4 and ARP are both needed) + ctx->fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (ctx->fd < 0) { + if (errno == EPERM || errno == EACCES) { + DBG_PRINTF_ERROR("eth_hal_open: AF_PACKET socket denied (errno=%d, %s).\n" + " The raw Ethernet transport needs CAP_NET_RAW:\n" + " sudo setcap cap_net_raw+ep \n", + errno, strerror(errno)); + } else { + DBG_PRINTF_ERROR("eth_hal_open: AF_PACKET socket failed (errno=%d, %s)\n", errno, strerror(errno)); + } + goto error; + } + + // Interface index + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ctx->ifname, IFNAMSIZ - 1); + if (ioctl(ctx->fd, SIOCGIFINDEX, &ifr) < 0) { + DBG_PRINTF_ERROR("eth_hal_open: interface '%s' not found (errno=%d, %s)\n", ctx->ifname, errno, strerror(errno)); + goto error; + } + ctx->ifindex = ifr.ifr_ifindex; + + // Interface MAC address + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ctx->ifname, IFNAMSIZ - 1); + if (ioctl(ctx->fd, SIOCGIFHWADDR, &ifr) < 0) { + DBG_PRINTF_ERROR("eth_hal_open: SIOCGIFHWADDR for '%s' failed (errno=%d, %s)\n", ctx->ifname, errno, strerror(errno)); + goto error; + } + memcpy(ctx->mac, ifr.ifr_hwaddr.sa_data, 6); + + // Interface MTU, used to explain an EMSGSIZE on send. The MTU is the largest IP packet, + // the 14 byte Ethernet header comes on top of it. + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ctx->ifname, IFNAMSIZ - 1); + if (ioctl(ctx->fd, SIOCGIFMTU, &ifr) < 0) { + DBG_PRINTF_WARNING("eth_hal_open: SIOCGIFMTU for '%s' failed (errno=%d, %s)\n", ctx->ifname, errno, strerror(errno)); + ctx->mtu = 1500; // assume standard Ethernet, only used for diagnostics + } else { + ctx->mtu = (unsigned int)ifr.ifr_mtu; + } + + // Bind to this interface only + struct sockaddr_ll sll; + memset(&sll, 0, sizeof(sll)); + sll.sll_family = AF_PACKET; + sll.sll_protocol = htons(ETH_P_ALL); + sll.sll_ifindex = ctx->ifindex; + if (bind(ctx->fd, (struct sockaddr *)&sll, sizeof(sll)) < 0) { + DBG_PRINTF_ERROR("eth_hal_open: bind to '%s' failed (errno=%d, %s)\n", ctx->ifname, errno, strerror(errno)); + goto error; + } + + // Do not loop our own transmitted frames back into the receive path. + // PACKET_IGNORE_OUTGOING needs Linux >= 4.20, the PACKET_OUTGOING check in + // eth_hal_recv() is the portable fallback and stays in place regardless. +#ifdef PACKET_IGNORE_OUTGOING + int one = 1; + if (setsockopt(ctx->fd, SOL_PACKET, PACKET_IGNORE_OUTGOING, &one, sizeof(one)) < 0) { + DBG_PRINTF_WARNING("eth_hal_open: PACKET_IGNORE_OUTGOING not available (errno=%d, %s), using the sll_pkttype filter only\n", errno, strerror(errno)); + } +#endif + + // Used to unblock a receive in progress + ctx->wakeup_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (ctx->wakeup_fd < 0) { + DBG_PRINTF_ERROR("eth_hal_open: eventfd failed (errno=%d, %s)\n", errno, strerror(errno)); + goto error; + } + + DBG_PRINTF3(" Raw Ethernet HAL on %s (index %d), MAC=%02X:%02X:%02X:%02X:%02X:%02X, MTU=%u (max frame %u bytes)\n", ctx->ifname, ctx->ifindex, ctx->mac[0], ctx->mac[1], + ctx->mac[2], ctx->mac[3], ctx->mac[4], ctx->mac[5], ctx->mtu, ctx->mtu + ETH_HDR_LEN); + + *ctxp = ctx; + return true; + +error: + if (ctx->fd >= 0) + close(ctx->fd); + if (ctx->wakeup_fd >= 0) + close(ctx->wakeup_fd); + free(ctx); + return false; +} + +void eth_hal_close(tEthHalCtx *ctx) { + if (ctx == NULL) + return; + if (ctx->fd >= 0) + close(ctx->fd); + if (ctx->wakeup_fd >= 0) + close(ctx->wakeup_fd); + free(ctx); +} + +bool eth_hal_get_mac(tEthHalCtx *ctx, uint8_t *mac) { + assert(ctx != NULL); + assert(mac != NULL); + memcpy(mac, ctx->mac, 6); + return true; +} + +int16_t eth_hal_send(tEthHalCtx *ctx, const uint8_t *frame, uint16_t len) { + + assert(ctx != NULL); + assert(frame != NULL); + + for (;;) { + ssize_t n = write(ctx->fd, frame, len); + if (n < 0) { + if (errno == EINTR) + continue; // interrupted before sending, retry + // The frame is larger than MTU + 14 for this interface. Report it separately and + // name the interface MTU: only the HAL knows that, and it is what has to be fixed. + if (errno == EMSGSIZE) { + DBG_PRINTF_ERROR("eth_hal_send: frame of %u bytes is too large for interface %s (MTU %u, so at most %u bytes per frame)\n", len, ctx->ifname, ctx->mtu, + ctx->mtu + ETH_HDR_LEN); + return ETH_HAL_ERROR_SIZE; + } + DBG_PRINTF_ERROR("eth_hal_send: write failed (errno=%d, %s)\n", errno, strerror(errno)); + return ETH_HAL_ERROR; + } + if (n != (ssize_t)len) { + DBG_PRINTF_ERROR("eth_hal_send: partial write %zd of %u bytes\n", n, len); + return ETH_HAL_ERROR; + } + return (int16_t)len; + } +} + +int16_t eth_hal_recv(tEthHalCtx *ctx, uint8_t *frame, uint16_t max_len, uint32_t timeout_ms) { + + assert(ctx != NULL); + assert(frame != NULL); + + struct pollfd pfd[2]; + pfd[0].fd = ctx->fd; + pfd[0].events = POLLIN; + pfd[0].revents = 0; + pfd[1].fd = ctx->wakeup_fd; + pfd[1].events = POLLIN; + pfd[1].revents = 0; + + int r = poll(pfd, 2, (int)timeout_ms); + if (r < 0) { + if (errno == EINTR) + return 0; // treat as timeout, the caller re-evaluates its deadline + DBG_PRINTF_ERROR("eth_hal_recv: poll failed (errno=%d, %s)\n", errno, strerror(errno)); + return ETH_HAL_ERROR; + } + if (r == 0) + return 0; // timeout + + // Wakeup requested by eth_hal_wakeup(): drain and report "no frame". + // socket_raw.c re-checks its shutdown flag at the top of its receive loop. + if (pfd[1].revents & POLLIN) { + uint64_t v; + ssize_t rd = read(ctx->wakeup_fd, &v, sizeof(v)); + (void)rd; + return 0; + } + + if (!(pfd[0].revents & POLLIN)) + return 0; + + struct sockaddr_ll from; + socklen_t fromlen = sizeof(from); + memset(&from, 0, sizeof(from)); + ssize_t n = recvfrom(ctx->fd, frame, max_len, 0, (struct sockaddr *)&from, &fromlen); + if (n < 0) { + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) + return 0; + DBG_PRINTF_ERROR("eth_hal_recv: recvfrom failed (errno=%d, %s)\n", errno, strerror(errno)); + return ETH_HAL_ERROR; + } + + // Portable fallback for PACKET_IGNORE_OUTGOING: drop our own transmitted frames + if (from.sll_pkttype == PACKET_OUTGOING) + return 0; + + return (int16_t)n; +} + +void eth_hal_wakeup(tEthHalCtx *ctx) { + if (ctx == NULL || ctx->wakeup_fd < 0) + return; + uint64_t v = 1; + ssize_t n = write(ctx->wakeup_fd, &v, sizeof(v)); + (void)n; +} + +#endif // OPTION_ENABLE_UDP_RAW && _LINUX && !OPTION_UDP_RAW_HAL_EXTERNAL diff --git a/src/sockets.c b/src/sockets.c new file mode 100644 index 00000000..ce51dd20 --- /dev/null +++ b/src/sockets.c @@ -0,0 +1,1654 @@ +/*---------------------------------------------------------------------------- +| File: +| sockets.c +| +| Description: +| Platform socket abstraction layer (Linux/Windows/macOS/QNX/FreeRTOS) +| +| Code released into public domain, no attribution required + ----------------------------------------------------------------------------*/ + +#include "sockets.h" + +#include // for malloc, free +#include // for memset, memcpy, strerror, strncpy +#if !defined(_WIN) +#include // for close +#endif + +#include "assert.h" +#include "dbg_print.h" +#include "xcptl_cfg.h" // for OPTION_MTU and XCPTL_MAX_SEGMENT_SIZE in the EMSGSIZE diagnostic + +#if (defined(OPTION_ENABLE_TCP) || defined(OPTION_ENABLE_UDP)) && !defined(OPTION_ENABLE_UDP_RAW) + +const char *socketGetErrorString(int32_t err) { +#if !defined(_WIN) + return strerror(err); +#else + switch (err) { + case SOCKET_ERROR_ABORT: + return "connection aborted"; + case SOCKET_ERROR_RESET: + return "connection reset"; + case SOCKET_ERROR_INTR: + return "interrupted"; + case SOCKET_ERROR_TIMEDOUT: + return "timed out"; + case SOCKET_ERROR_WBLOCK: + return "would block"; + case SOCKET_ERROR_PIPE: + return "broken pipe"; + case SOCKET_ERROR_BADF: + return "bad file descriptor"; + case SOCKET_ERROR_NOTCONN: + return "not connected"; + default: + return "unknown socket error"; + } +#endif +} + +//-------------------------------------------------------------------------- +// FreeRTOS platforms + +#if defined(_FREE_RTOS) && !defined(FREE_RTOS_POSIX_SIM) // FreeRTOS sockets + +#ifdef OPTION_ENABLE_TCP +#error "FreeRTOS TCP socket functions not implemented yet" +#endif + +#if defined(OPTION_FREERTOS_LWIP) +#include "lwip/errno.h" // lwIP errno values mapped to POSIX codes +#include "lwip/netif.h" // netif_default, struct netif::mtu, for the segment size check in socketSendTo +#include "lwip/sockets.h" // lwip_socket, lwip_bind, lwip_sendto, lwip_recvfrom, lwip_close, lwip_shutdown, lwip_setsockopt +#endif + +// socketStartup: lwIP networking is initialised by the application (e.g. tcpip_init) — no-op here +bool socketStartup(void) { +#if defined(OPTION_FREERTOS_LWIP) + return true; +#else + DBG_PRINT_ERROR("FREE_RTOS:socketStartup not implemented\n"); + return true; +#endif +} + +// socketCleanup: no teardown required for lwIP +void socketCleanup(void) { +#if !defined(OPTION_FREERTOS_LWIP) + DBG_PRINT_ERROR("FREE_RTOS:socketCleanup not implemented\n"); +#endif +} + +// Create a UDP socket (TCP not supported: OPTION_ENABLE_TCP must not be defined) +bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags) { +#if defined(OPTION_FREERTOS_LWIP) + assert(socketp != NULL); + assert(!(flags & SOCKET_MODE_TCP)); // TCP not supported on FreeRTOS/lwIP + + int sock = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + DBG_PRINTF_ERROR("socketOpen: lwip_socket failed (errno=%d,%s)\n", errno, socketGetErrorString(errno)); + return false; + } + if (flags & SOCKET_MODE_REUSEADDR) { + int yes = 1; + if (lwip_setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) { + DBG_PRINTF_WARNING("socketOpen: SO_REUSEADDR failed (errno=%d,%s)\n", errno, socketGetErrorString(errno)); + } + } + *socketp = sock; + DBG_PRINTF5("socketOpen: lwIP UDP socket %d opened\n", sock); + return true; +#else + DBG_PRINT_ERROR("FREE_RTOS:socketOpen not implemented\n"); + return false; +#endif +} + +// Bind socket to a local address and port +// addr: network-byte-order IPv4 address; NULL or 0.x.x.x binds to INADDR_ANY +bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port) { +#if defined(OPTION_FREERTOS_LWIP) + assert(socket != INVALID_SOCKET_HANDLE); + struct sockaddr_in a; + memset(&a, 0, sizeof(a)); + a.sin_family = AF_INET; + a.sin_port = htons(port); + if (addr != NULL && addr[0] != 0) { + a.sin_addr.s_addr = *(uint32_t *)addr; + } else { + a.sin_addr.s_addr = htonl(INADDR_ANY); + } + if (lwip_bind(socket, (struct sockaddr *)&a, sizeof(a)) < 0) { + DBG_PRINTF_ERROR("socketBind: lwip_bind failed (errno=%d,%s) on port %u\n", errno, socketGetErrorString(errno), port); + return false; + } + DBG_PRINTF5("socketBind: bound to port %u\n", port); + return true; +#else + DBG_PRINT_ERROR("FREE_RTOS:socketBind not implemented\n"); + return false; +#endif +} + +// Shutdown socket — unblocks a thread blocked in socketRecvFrom +bool socketShutdown(SOCKET_HANDLE socket) { +#if defined(OPTION_FREERTOS_LWIP) + if (socket != INVALID_SOCKET_HANDLE) { + lwip_shutdown(socket, SHUT_RDWR); + } + return true; +#else + DBG_PRINT_ERROR("FREE_RTOS:socketShutdown not implemented\n"); + return true; +#endif +} + +// Close socket and free the handle +bool socketClose(SOCKET_HANDLE *socketp) { +#if defined(OPTION_FREERTOS_LWIP) + assert(socketp != NULL); + if (*socketp != INVALID_SOCKET_HANDLE) { + lwip_close(*socketp); + *socketp = INVALID_SOCKET_HANDLE; + } + return true; +#else + DBG_PRINT_ERROR("FREE_RTOS:socketClose not implemented\n"); + return true; +#endif +} + +// Receive a UDP datagram (blocking) +// Returns: > 0 bytes received, 0 on timeout/EAGAIN, -1 on error or socket closed +int16_t socketRecvFrom(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, uint8_t *srcAddr, uint16_t *srcPort, uint64_t *time) { +#if defined(OPTION_FREERTOS_LWIP) + assert(socket != INVALID_SOCKET_HANDLE); + struct sockaddr_in src; + socklen_t srclen = sizeof(src); + memset(&src, 0, sizeof(src)); + int16_t n = (int16_t)lwip_recvfrom(socket, buffer, bufferSize, 0, (struct sockaddr *)&src, &srclen); + if (n == 0) { + return 0; // Zero-length datagram or graceful close + } + if (n < 0) { + int32_t err = errno; + if (socketTimeout(err)) { + return 0; // Timeout — caller loops and does background work + } + DBG_PRINTF_ERROR("socketRecvFrom: lwip_recvfrom failed (errno=%d,%s)\n", err, socketGetErrorString(err)); + return -1; + } + if (srcAddr != NULL) { + memcpy(srcAddr, &src.sin_addr.s_addr, 4); + } + if (srcPort != NULL) { + *srcPort = ntohs(src.sin_port); + } + if (time != NULL) { + *time = clockGet(); // No hardware timestamps on lwIP; use XCP clock + } + return n; +#else + DBG_PRINT_ERROR("FREE_RTOS:socketRecvFrom not implemented\n"); + return -1; +#endif +} + +// Send a UDP datagram to addr:port +// Returns: bytes sent, 0 on closed socket, -1 on error +int16_t socketSendTo(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize, const uint8_t *addr, uint16_t port, uint64_t *time) { +#if defined(OPTION_FREERTOS_LWIP) + assert(socket != INVALID_SOCKET_HANDLE); + assert(addr != NULL); + struct sockaddr_in dst; + memset(&dst, 0, sizeof(dst)); + dst.sin_family = AF_INET; + dst.sin_port = htons(port); + dst.sin_addr.s_addr = *(uint32_t *)addr; + if (time != NULL) { + *time = clockGet(); // No hardware timestamps on lwIP; use XCP clock at send time + } + + // lwIP sets no DF option - it has no IP_DONTFRAG - so unlike Linux, macOS/BSD, QNX and Windows + // it does not refuse an oversized datagram: it fragments or drops it according to its own + // IP_FRAG build setting, silently either way. That makes lwIP the one transport where an + // OPTION_MTU larger than the link MTU degrades measurement without any diagnostic, so check it + // here. netif->mtu is the IP MTU, so the 20 byte IPv4 and 8 byte UDP headers are added. + // + // Reported once, not per datagram: this is the DAQ transmit path. Best effort - the default + // netif is not necessarily the one routing to dst on a multi homed target, so a false report + // is possible there, and it costs one log line and nothing else. + if (netif_default != NULL && (uint32_t)bufferSize + 20u + 8u > (uint32_t)netif_default->mtu) { + static bool mtu_reported = false; + if (!mtu_reported) { + mtu_reported = true; + DBG_PRINTF_WARNING("socketSendTo: segment of %u bytes does not fit the link MTU of %u and lwIP will\n" + " fragment or drop it. Reduce OPTION_MTU (currently %u, giving XCPTL_MAX_SEGMENT_SIZE=%u).\n", + (unsigned)bufferSize, (unsigned)netif_default->mtu, (unsigned)OPTION_MTU, (unsigned)XCPTL_MAX_SEGMENT_SIZE); + } + } + + int16_t n = (int16_t)lwip_sendto(socket, buffer, bufferSize, 0, (struct sockaddr *)&dst, sizeof(dst)); + if (n < 0) { + int32_t err = errno; + if (socketIsClosed(err)) { + return 0; // Socket closed + } + DBG_PRINTF_ERROR("socketSendTo: lwip_sendto failed (errno=%d,%s)\n", err, socketGetErrorString(err)); + return -1; + } + return n; +#else + DBG_PRINT_ERROR("FREE_RTOS:socketSendTo not implemented\n"); + return -1; +#endif +} + +// Set receive timeout on a blocking socket +// timeoutMs == 0 restores infinite blocking +bool socketSetTimeout(SOCKET_HANDLE socket, uint32_t timeoutMs) { +#if defined(OPTION_FREERTOS_LWIP) + assert(socket != INVALID_SOCKET_HANDLE); + struct timeval tv; + tv.tv_sec = (long)(timeoutMs / 1000U); + tv.tv_usec = (long)(timeoutMs % 1000U) * 1000L; + if (lwip_setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { + DBG_PRINTF_WARNING("socketSetTimeout: lwip_setsockopt SO_RCVTIMEO failed (errno=%d,%s)\n", errno, socketGetErrorString(errno)); + return false; + } + DBG_PRINTF5("socketSetTimeout: set to %u ms\n", timeoutMs); + return true; +#else + DBG_PRINT_ERROR("FREE_RTOS:socketSetTimeout not implemented\n"); + return true; +#endif +} + +#else + +//-------------------------------------------------------------------------- +// Non-Windows platforms +#if !defined(_WIN) + +#include // for getifaddrs, struct ifaddrs + +#include // for htons, htonl +#include // for sockaddr_in +#include // for socket functions + +#if defined(_LINUX) // Linux platform + +#include // for if_nametoindex, struct ifreq, IFNAMSIZ +#include // for struct sockaddr_ll (AF_PACKET, used by socketGetMAC) + +#if defined(OPTION_SOCKET_HW_TIMESTAMPS) // Linux platform hardware time stamping support +#include +#include +#include // for SIOCSHWTSTAMP +#include +#endif // defined(OPTION_SOCKET_HW_TIMESTAMPS) + +#endif // Linux + +#if defined(_MACOS) || defined(_QNX) // MacOS or QNX platforms +#include +#endif // MacOS or QNX platforms + +bool socketStartup(void) { return true; } + +void socketCleanup(void) {} + +// Create a socket, TCP or UDP +// flag SOCKET_MODE_HW_TIMESTAMPING: Enable hardware timestamping (Linux only, requires root) +// flag SOCKET_MODE_SW_TIMESTAMPING: Enable software timestamping (Linux only) +bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags) { + + assert(socketp != NULL); + SOCKET sock = INVALID_SOCKET; + + bool useTCP = flags & SOCKET_MODE_TCP; + bool reuseaddr = flags & SOCKET_MODE_REUSEADDR; + + // Create a socket + sock = socket(AF_INET, useTCP ? SOCK_STREAM : SOCK_DGRAM, 0); + if (sock < 0) { + DBG_PRINT_ERROR("cannot open socket!\n"); + return 0; + } + + if (reuseaddr) { + int yes = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) { + DBG_PRINTF_WARNING("Failed to enable SO_REUSEADDR on socket (errno=%d,%s)\n", errno, socketGetErrorString(errno)); + } else { + DBG_PRINT5("SO_REUSEADDR enabled on socket\n"); + } + } + + // Never fragment outgoing datagrams. + // IPv4 fragmentation is actively harmful for DAQ: losing one fragment loses the whole + // datagram, reassembly adds jitter and the reassembly buffers can overflow at DAQ rates. + // Without this, an OPTION_MTU larger than the path MTU degrades measurement quality + // silently and indefinitely. With it, socketSendTo fails with EMSGSIZE on the first + // oversized segment, which is the diagnostic the user actually needs. + // This also makes the socket transport behave like the raw Ethernet transport, which + // cannot fragment at all (see docs/SOCKET_RAW.md). + if (!useTCP) { // TCP does its own path MTU handling +#if defined(_LINUX) + int pmtu = IP_PMTUDISC_DO; // always set DF, honour the discovered path MTU + if (setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, &pmtu, sizeof(pmtu)) < 0) { + DBG_PRINTF_WARNING("Failed to enable IP_MTU_DISCOVER on socket (errno=%d,%s), datagrams may be fragmented\n", errno, socketGetErrorString(errno)); + } else { + DBG_PRINT5("IP_MTU_DISCOVER=IP_PMTUDISC_DO enabled, datagrams will not be fragmented\n"); + } +#elif defined(IP_DONTFRAG) // macOS, QNX and other BSD derived platforms + int yes = 1; + if (setsockopt(sock, IPPROTO_IP, IP_DONTFRAG, &yes, sizeof(yes)) < 0) { + DBG_PRINTF_WARNING("Failed to enable IP_DONTFRAG on socket (errno=%d,%s), datagrams may be fragmented\n", errno, socketGetErrorString(errno)); + } else { + DBG_PRINT5("IP_DONTFRAG enabled, datagrams will not be fragmented\n"); + } +#else + DBG_PRINT5("Don't fragment not supported on this platform, datagrams may be fragmented\n"); +#endif + } + +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + if (flags & SOCKET_MODE_GET_IF_INFO) { + int yes = 1; + if (setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &yes, sizeof(yes)) < 0) { + DBG_PRINTF_WARNING("Failed to enable IP_PKTINFO on socket (errno=%d,%s)\n", errno, socketGetErrorString(errno)); + } else { + DBG_PRINT5("IP_PKTINFO enabled\n"); + } + } +#endif + +// Enable timestamps if requested +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + + bool hw_timestamps = flags & SOCKET_MODE_HW_TIMESTAMPING; + bool sw_timestamps = flags & SOCKET_MODE_SW_TIMESTAMPING; + if (hw_timestamps) { + // Enable SO_TIMESTAMPING for full hardware and software timestamping support + // This is required for PTP SYNC message timestamping + // SO_TIMESTAMPING supersedes SO_TIMESTAMPNS and provides: + // - Hardware RX/TX timestamps (if NIC/driver supports it) + // - Software RX/TX timestamps (always available as fallback) + // - Raw hardware clock access + // + // The timestamp array returned in control messages: + // [0] = Software timestamp + // [1] = Deprecated (legacy) + // [2] = Hardware timestamp (from NIC PHY) + uint32_t flags = SOF_TIMESTAMPING_TX_SOFTWARE | // Software TX timestamp (always available) + SOF_TIMESTAMPING_RX_SOFTWARE | // Software RX timestamp (always available) + SOF_TIMESTAMPING_SOFTWARE | // Enable software timestamp generation + SOF_TIMESTAMPING_TX_HARDWARE | // Hardware TX timestamp (if available) + SOF_TIMESTAMPING_RX_HARDWARE | // Hardware RX timestamp (if available) + SOF_TIMESTAMPING_RAW_HARDWARE | // Use raw hardware clock (required for HW timestamps) + SOF_TIMESTAMPING_OPT_TSONLY | // Return only timestamp, not packet data + // SOF_TIMESTAMPING_OPT_TX_SWHW | // Generate both SW and HW TX timestamps + 0; + if (setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags)) < 0) { + DBG_PRINTF_ERROR("Failed to enable socket hardware timestamps (SO_TIMESTAMPING, errno=%d,%s)\n", errno, socketGetErrorString(errno)); + } else { + DBG_PRINTF5("Hardware timestamping enabled on socket (SO_TIMESTAMPING flags=0x%X)\n", flags); + } + } + + if (sw_timestamps) { + + // Enable software timestamps, if required + int yes = 1; + if (setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPNS, &yes, sizeof(yes)) < 0) { + DBG_PRINTF_ERROR("Failed to enable socket software timestamps (SO_TIMESTAMPNS, errno=%d,%s)\n", errno, socketGetErrorString(errno)); + } else { + DBG_PRINT5("Software timestamps enabled on socket (SO_TIMESTAMPNS)\n"); + } + } +#endif + +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + SOCKET_HANDLE socket = (struct socket *)malloc(sizeof(struct socket)); + memset(socket, 0, sizeof(struct socket)); + socket->sock = sock; + *socketp = socket; +#else + *socketp = sock; +#endif + return true; +} + +bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port) { + + assert(socket != INVALID_SOCKET_HANDLE); + assert(addr != NULL); + + SOCKET sock = SOCKET_FD(socket); + + // Bind the socket to any address and the specified port + SOCKADDR_IN a; + a.sin_family = AF_INET; + if (addr != NULL && addr[0] != 0) { + a.sin_addr.s_addr = *(uint32_t *)addr; // Bind to the specific addr given + } else { + a.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to any addr + } + a.sin_port = htons(port); + if (bind(sock, (SOCKADDR *)&a, sizeof(a)) < 0) { + DBG_PRINTF_ERROR("socketBind failed (errno=%d,%s) - cannot bind on %u.%u.%u.%u port %u!\n", socketGetLastError(), socketGetErrorString(socketGetLastError()), + addr ? addr[0] : 0, addr ? addr[1] : 0, addr ? addr[2] : 0, addr ? addr[3] : 0, port); + if (port < 1024) { + DBG_PRINT_ERROR("Binding to ports <1024 may require root privileges on Linux!\n"); + } + return 0; + } + return true; +} + +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + +// Bind socket to a specific network interface by name (Linux only) +// This is useful for multicast reception on a specific interface while binding to INADDR_ANY +// Requires root privileges on Linux +bool socketBindToDevice(SOCKET_HANDLE socket, const char *ifname) { + + assert(socket != INVALID_SOCKET_HANDLE); + + int sock = SOCKET_FD(socket); + if (ifname != NULL && ifname[0] != '\0') { + if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname)) < 0) { + DBG_PRINTF_ERROR("socketBindToDevice failed (errno=%d,%s) - cannot bind to device %s !\n", socketGetLastError(), socketGetErrorString(socketGetLastError()), ifname); + return false; + } + DBG_PRINTF3("Socket bound to device %s\n", ifname); + + // Store interface name + strncpy(socket->ifname, ifname, sizeof(socket->ifname) - 1); + socket->ifname[sizeof(socket->ifname) - 1] = '\0'; + + // Store interface index + unsigned int ifindex = if_nametoindex(ifname); + socket->ifindex = ifindex; + } + return true; +} + +// Enable hardware timestamping and/or software on a network interface +// This configures the NIC driver to generate timestamps for PTP packets +// Must be called after socket is created and bound +// ifname: Network interface name (e.g., "eth0"). If NULL, uses first non-loopback interface. +// Returns true on success, false on failure (falls back to software timestamps) +bool socketEnableTimestamps(SOCKET_HANDLE socket, bool ptpOnly) { + + assert(socket != NULL); + int sock = socket->sock; + + struct ifreq ifr; + struct hwtstamp_config hwconfig; + + // Use socket's ifname + const char *ifname = socket->ifname[0] != '\0' ? socket->ifname : NULL; + + memset(&ifr, 0, sizeof(ifr)); + memset(&hwconfig, 0, sizeof(hwconfig)); + + // If no interface specified, try to find the first non-loopback interface + if (ifname == NULL) { + DBG_PRINT_WARNING("socketEnableTimestamps: No ifname specified, searching for first non-loopback interface\n"); + struct ifaddrs *ifaddrs, *ifa; + if (getifaddrs(&ifaddrs) == 0) { + for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr != NULL && ifa->ifa_addr->sa_family == AF_INET) { + struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; + if (sa->sin_addr.s_addr != htonl(INADDR_LOOPBACK)) { + strncpy(ifr.ifr_name, ifa->ifa_name, IFNAMSIZ - 1); + break; + } + } + } + freeifaddrs(ifaddrs); + } + if (ifr.ifr_name[0] == '\0') { + DBG_PRINT_ERROR("socketEnableTimestamps: No suitable interface found\n"); + return false; + } + } else { + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); + } + + DBG_PRINTF5("socketEnableTimestamps: Enabling timestamps on interface %s\n", ifr.ifr_name); + + // Configure hardware timestamping: + // tx_type: HWTSTAMP_TX_ON enables TX timestamps for all packets + // rx_filter: HWTSTAMP_FILTER_ALL or HWTSTAMP_FILTER_PTP_V2_EVENT for PTP packets + hwconfig.flags = 0; + hwconfig.tx_type = HWTSTAMP_TX_ON; // Enable TX hardware timestamps + hwconfig.rx_filter = ptpOnly ? HWTSTAMP_FILTER_PTP_V2_EVENT : HWTSTAMP_FILTER_ALL; // Timestamp all incoming packets (or use HWTSTAMP_FILTER_PTP_V2_EVENT for PTP only) + + ifr.ifr_data = (char *)&hwconfig; + + if (ioctl(sock, SIOCSHWTSTAMP, &ifr) < 0) { + + // SIOCSHWTSTAMP requires CAP_NET_ADMIN or root privileges + // Some NICs may not support it, or the filter mode may not be supported + DBG_PRINTF_WARNING("socketEnableTimestamps: ioctl SIOCSHWTSTAMP failed for %s (errno=%d: %s)\n", ifr.ifr_name, errno, strerror(errno)); + DBG_PRINT_WARNING("Hardware timestamping may require root privileges or may not be supported by this NIC\n"); + + // Try with a less restrictive filter + hwconfig.rx_filter = HWTSTAMP_FILTER_NONE; // No RX filter, just enable TX + hwconfig.tx_type = HWTSTAMP_TX_ON; + if (ioctl(sock, SIOCSHWTSTAMP, &ifr) < 0) { + DBG_PRINTF_WARNING("socketEnableTimestamps: Fallback also failed (errno=%d: %s)\n", errno, strerror(errno)); + return false; + } + DBG_PRINTF_WARNING("socketEnableTimestamps: Enabled TX-only hardware timestamps on %s\n", ifr.ifr_name); + return true; + } + + DBG_PRINTF5("Hardware timestamping enabled on %s (tx_type=%d, rx_filter=%d)\n", ifr.ifr_name, hwconfig.tx_type, hwconfig.rx_filter); + return true; +} + +#else + +// Hardware timestamping not supported on this platform +// Stub for non-Linux platforms +bool socketEnableTimestamps(SOCKET_HANDLE socket, bool ptpOnly) { + (void)socket; + (void)ptpOnly; + DBG_PRINT_ERROR("socketEnableTimestamps: Socket hardware timestamping not supported on this platform!\n"); + return false; +} + +#endif // Linux with OPTION_SOCKET_HW_TIMESTAMPS + +// Shutdown socket +// Block rx and tx direction +bool socketShutdown(SOCKET_HANDLE socket) { + if (socket != INVALID_SOCKET_HANDLE) { + shutdown(SOCKET_FD(socket), SHUT_RDWR); + } + return true; +} + +// Close socket +// Make addr reusable +bool socketClose(SOCKET_HANDLE *socketp) { + assert(socketp != NULL); +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + if (*socketp != NULL) { + close((*socketp)->sock); + free(*socketp); + *socketp = NULL; + } +#else + if (*socketp != INVALID_SOCKET_HANDLE) { + close(*socketp); + *socketp = INVALID_SOCKET_HANDLE; + } +#endif + return true; +} + +// Get MAC address of a network interface by name +bool socketGetMAC(char *ifname, uint8_t *mac) { + + assert(ifname != NULL); + struct ifaddrs *ifaddrs, *ifa; + if (getifaddrs(&ifaddrs) == 0) { + for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { + if (!strcmp(ifa->ifa_name, ifname)) { +#if defined(_MACOS) || defined(_QNX) + if (ifa->ifa_addr->sa_family == AF_LINK) { + memcpy(mac, (uint8_t *)LLADDR((struct sockaddr_dl *)ifa->ifa_addr), 6); + DBG_PRINTF5(" %s: MAC = %02X-%02X-%02X-%02X-%02X-%02X\n", ifa->ifa_name, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } +#else + if (ifa->ifa_addr->sa_family == AF_PACKET) { + struct sockaddr_ll *s = (struct sockaddr_ll *)ifa->ifa_addr; + memcpy(mac, s->sll_addr, 6); + DBG_PRINTF5(" %s: MAC = %02X-%02X-%02X-%02X-%02X-%02X\n", ifa->ifa_name, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + break; + } +#endif + } + } + freeifaddrs(ifaddrs); + return (ifa != NULL); + } + return false; +} + +#ifdef OPTION_ENABLE_GET_LOCAL_ADDR + +// Get local IP address and MAC address of the first non-loopback interface +bool socketGetLocalAddr(uint8_t *mac, uint8_t *addr) { + static uint32_t __addr1 = 0; + static uint8_t __mac1[6] = {0, 0, 0, 0, 0, 0}; +#ifdef DBG_LEVEL + char strbuf[64]; // @@@@ STACK buffer for IP addr string +#endif + if (__addr1 == 0) { + struct ifaddrs *ifaddrs, *ifa; + struct ifaddrs *ifa1 = NULL; + if (-1 != getifaddrs(&ifaddrs)) { + for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { + if ((NULL != ifa->ifa_addr) && (AF_INET == ifa->ifa_addr->sa_family)) { // IPV4 + struct sockaddr_in *sa = (struct sockaddr_in *)(ifa->ifa_addr); + if (0x100007f != sa->sin_addr.s_addr) { /* not 127.0.0.1 */ + if (__addr1 == 0) { + __addr1 = sa->sin_addr.s_addr; + ifa1 = ifa; + break; + } + } + } + } + if (__addr1 != 0 && ifa1 != NULL) { + socketGetMAC(ifa1->ifa_name, __mac1); +#ifdef DBG_LEVEL + if (DBG_LEVEL >= 5) { + inet_ntop(AF_INET, &__addr1, strbuf, sizeof(strbuf)); + printf(" Use IPV4 adapter %s with IP=%s, MAC=%02X-%02X-%02X-%02X-%02X-%02X for A2L info and clock " + "UUID\n", + ifa1->ifa_name, strbuf, __mac1[0], __mac1[1], __mac1[2], __mac1[3], __mac1[4], __mac1[5]); + } +#endif + } + freeifaddrs(ifaddrs); + } + } + if (__addr1 != 0) { + if (mac) + memcpy(mac, __mac1, 6); + if (addr) + memcpy(addr, &__addr1, 4); + return true; + } else { + return false; + } +} + +#endif // OPTION_ENABLE_GET_LOCAL_ADDR + +//-------------------------------------------------------------------------- +#else // Windows platform + +// Winsock +#pragma comment(lib, "ws2_32.lib") + +int32_t socketGetLastError(void) { return WSAGetLastError(); } + +bool socketStartup(void) { + + int err; + WORD wsaVersionRequested; + WSADATA wsaData; + + // Init Winsock2 + wsaVersionRequested = MAKEWORD(2, 2); + err = WSAStartup(wsaVersionRequested, &wsaData); + if (err != 0) { + DBG_PRINTF_ERROR("WSAStartup failed with error %d!\n", err); + return false; + } + if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) { // Confirm that the WinSock DLL supports 2.2 + DBG_PRINT_ERROR("Could not find a usable version of Winsock.dll!\n"); + WSACleanup(); + return false; + } + + return true; +} + +void socketCleanup(void) { WSACleanup(); } + +// Create a socket, TCP or UDP +bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags) { + + assert(socketp != NULL); + SOCKET sock = -1; + + bool useTCP = flags & SOCKET_MODE_TCP; + bool reuseaddr = flags & SOCKET_MODE_REUSEADDR; + + // Create a socket + if (!useTCP) { + sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + +// Avoid send to UDP nowhere problem (ignore ICMP host unreachable - server has no open socket on client port) +// (stack-overflow 34242622) +#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12) + bool bNewBehavior = false; + DWORD dwBytesReturned = 0; + if (sock != INVALID_SOCKET) { + WSAIoctl(sock, SIO_UDP_CONNRESET, &bNewBehavior, sizeof bNewBehavior, NULL, 0, &dwBytesReturned, NULL, NULL); + } + } else { + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + } + if (sock == INVALID_SOCKET) { + DBG_PRINTF_ERROR("socketOpen failed (errno=%d,%s) - could not create socket!\n", socketGetLastError(), socketGetErrorString(socketGetLastError())); + return false; + } + + // Make addr reusable + if (reuseaddr) { + uint32_t one = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&one, sizeof(one)) < 0) { + DBG_PRINTF_WARNING("socketOpen failed (errno=%d,%s) - could not enable SO_REUSEADDR on socket\n", socketGetLastError(), socketGetErrorString(socketGetLastError())); + } + } + + // Never fragment outgoing datagrams - see the comment in the POSIX socketOpen above + if (!useTCP) { + DWORD one = 1; + if (setsockopt(sock, IPPROTO_IP, IP_DONTFRAGMENT, (const char *)&one, sizeof(one)) < 0) { + DBG_PRINTF_WARNING("socketOpen failed (errno=%d,%s) - could not enable IP_DONTFRAGMENT, datagrams may be fragmented\n", socketGetLastError(), + socketGetErrorString(socketGetLastError())); + } + } + + *socketp = sock; + return true; +} + +bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port) { + + assert(socket != INVALID_SOCKET_HANDLE); + SOCKET sock = socket; + + // Bind the socket to any address and the specified port + SOCKADDR_IN a; + a.sin_family = AF_INET; + if (addr != NULL && *(uint32_t *)addr != 0) { + a.sin_addr.s_addr = *(uint32_t *)addr; // Bind to the specific addr given + } else { // NULL or 0.x.x.x + a.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to any addr + } + a.sin_port = htons(port); + if (bind(sock, (SOCKADDR *)&a, sizeof(a)) < 0) { + if (socketGetLastError() == WSAEADDRINUSE) { + DBG_PRINT_ERROR("Port is already in use!\n"); + } else { + DBG_PRINTF_ERROR("socketBind failed (errno=%d,%s) - cannot bind on %u.%u.%u.%u port %u!\n", socketGetLastError(), socketGetErrorString(socketGetLastError()), + addr ? addr[0] : 0, addr ? addr[1] : 0, addr ? addr[2] : 0, addr ? addr[3] : 0, port); + } + return false; + } + return true; +} + +// Shutdown socket +// Block rx and tx direction +bool socketShutdown(SOCKET_HANDLE socket) { + + assert(socket != INVALID_SOCKET_HANDLE); + SOCKET sock = socket; + + if (sock != INVALID_SOCKET) { + shutdown(sock, SD_BOTH); + } + return true; +} + +// Close socket +// Make addr reusable +bool socketClose(SOCKET_HANDLE *socketp) { + + assert(socketp != NULL); + if (*socketp != INVALID_SOCKET_HANDLE) { + closesocket(*socketp); + *socketp = INVALID_SOCKET_HANDLE; + } + return true; +} + +#ifdef OPTION_ENABLE_GET_LOCAL_ADDR + +#include +#pragma comment(lib, "IPHLPAPI.lib") +#define _WINSOCK_DEPRECATED_NO_WARNINGS + +bool socketGetLocalAddr(uint8_t *mac, uint8_t *addr) { + + static uint8_t __addr1[4] = {0, 0, 0, 0}; + static uint8_t __mac1[6] = {0, 0, 0, 0, 0, 0}; + uint32_t a; + PIP_ADAPTER_INFO pAdapterInfo; + PIP_ADAPTER_INFO pAdapter = NULL; + DWORD dwRetVal = 0; + + if (__addr1[0] == 0) { + + ULONG ulOutBufLen = sizeof(IP_ADAPTER_INFO); + pAdapterInfo = (IP_ADAPTER_INFO *)malloc(sizeof(IP_ADAPTER_INFO)); + if (pAdapterInfo == NULL) + return 0; + + if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) { + free(pAdapterInfo); + pAdapterInfo = (IP_ADAPTER_INFO *)malloc(ulOutBufLen); + if (pAdapterInfo == NULL) + return 0; + } + if ((dwRetVal = GetAdaptersInfo(pAdapterInfo, &ulOutBufLen)) == NO_ERROR) { + pAdapter = pAdapterInfo; + while (pAdapter) { + if (pAdapter->Type == MIB_IF_TYPE_ETHERNET) { + inet_pton(AF_INET, pAdapter->IpAddressList.IpAddress.String, &a); + if (a != 0) { +#ifdef DBG_LEVEL + DBG_PRINTF5(" Ethernet adapter %" PRIu32 ":", (uint32_t)pAdapter->Index); + // DBG_PRINTF5(" %s", pAdapter->AdapterName); + DBG_PRINTF5(" %s", pAdapter->Description); + DBG_PRINTF5(" %02X-%02X-%02X-%02X-%02X-%02X", pAdapter->Address[0], pAdapter->Address[1], pAdapter->Address[2], pAdapter->Address[3], pAdapter->Address[4], + pAdapter->Address[5]); + DBG_PRINTF5(" %s", pAdapter->IpAddressList.IpAddress.String); + // DBG_PRINTF5(" %s", pAdapter->IpAddressList.IpMask.String); + // DBG_PRINTF5(" Gateway: %s", pAdapter->GatewayList.IpAddress.String); + // if (pAdapter->DhcpEnabled) DBG_PRINTF5(" DHCP"); + DBG_PRINT5("\n"); +#endif + if (__addr1[0] == 0) { + memcpy(__addr1, (uint8_t *)&a, 4); + memcpy(__mac1, pAdapter->Address, 6); + } + } + } + pAdapter = pAdapter->Next; + } + } + if (pAdapterInfo) + free(pAdapterInfo); + } + + if (__addr1[0] != 0) { + if (mac) + memcpy(mac, __mac1, 6); + if (addr) + memcpy(addr, __addr1, 4); + return true; + } + return false; +} + +#endif // OPTION_ENABLE_GET_LOCAL_ADDR + +#endif // _WIN + +// Set receive timeout on a socket +// timeoutMs: timeout in milliseconds, 0 = infinite blocking (restore default) +bool socketSetTimeout(SOCKET_HANDLE socket, uint32_t timeoutMs) { + assert(socket != INVALID_SOCKET_HANDLE); +#if defined(_WIN) + DWORD tv = (DWORD)timeoutMs; + if (setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof(tv)) < 0) { + DBG_PRINTF_WARNING("socketSetTimeout: setsockopt SO_RCVTIMEO failed (errno=%d,%s)\n", socketGetLastError(), socketGetErrorString(socketGetLastError())); + return false; + } +#else + struct timeval tv; + tv.tv_sec = timeoutMs / 1000; + tv.tv_usec = (int32_t)(timeoutMs % 1000) * 1000; + if (setsockopt(SOCKET_FD(socket), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { + DBG_PRINTF_WARNING("socketSetTimeout: setsockopt SO_RCVTIMEO failed (errno=%d,%s)\n", errno, socketGetErrorString(errno)); + return false; + } +#endif + DBG_PRINTF5("socketSetTimeout: set to %u ms\n", timeoutMs); + return true; +} + +#if defined(OPTION_ENABLE_TCP) + +// Listen on a TCP socket +bool socketListen(SOCKET_HANDLE socket) { + assert(socket != INVALID_SOCKET_HANDLE); + if (listen(SOCKET_FD(socket), 5)) { + DBG_PRINTF_ERROR("socketListen failed (errno=%d,%s)!\n", socketGetLastError(), socketGetErrorString(socketGetLastError())); + return 0; + } + return 1; +} + +// Accept a connection on a listening TCP socket +// Returns the remote address if addr != NULL +SOCKET_HANDLE socketAccept(SOCKET_HANDLE listenSocket, uint8_t *addr) { + assert(listenSocket != INVALID_SOCKET_HANDLE); + struct sockaddr_in sa; + socklen_t sa_size = sizeof(sa); + SOCKET sock = accept(SOCKET_FD(listenSocket), (struct sockaddr *)&sa, &sa_size); + if (addr) + *(uint32_t *)addr = sa.sin_addr.s_addr; +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + SOCKET_HANDLE socket = (struct socket *)malloc(sizeof(struct socket)); + memset(socket, 0, sizeof(struct socket)); + socket->sock = sock; + socket->ifindex = listenSocket->ifindex; + memcpy(socket->ifname, listenSocket->ifname, sizeof(socket->ifname)); + return socket; +#else + return sock; +#endif +} + +#endif // OPTION_ENABLE_TCP + +#if !defined(_FREE_RTOS) || defined(FREE_RTOS_POSIX_SIM) + +// Join a multicast group on a UDP socket +// maddr: Multicast group address (network byte order) +bool socketJoin(SOCKET_HANDLE socket, const uint8_t *maddr, const uint8_t *ifaddr, const char *ifname) { + + assert(socket != INVALID_SOCKET_HANDLE); + SOCKET sock = SOCKET_FD(socket); + +#if defined(_LINUX) + // On Linux, use ip_mreqn which allows specifying interface by name or index + struct ip_mreqn group; + memset(&group, 0, sizeof(group)); + group.imr_multiaddr.s_addr = *(uint32_t *)maddr; + + // Priority: interface name > interface address > INADDR_ANY + if (ifname != NULL && ifname[0] != '\0') { + // Use interface name (most reliable for multicast on Linux) + group.imr_ifindex = if_nametoindex(ifname); + if (group.imr_ifindex == 0) { + DBG_PRINTF_ERROR("socketJoin: Interface %s not found!\n", ifname); + return 0; + } +#if defined(OPTION_SOCKET_HW_TIMESTAMPS) + socket->ifindex = group.imr_ifindex; + strncpy(socket->ifname, ifname, sizeof(socket->ifname) - 1); + socket->ifname[sizeof(socket->ifname) - 1] = '\0'; +#endif + DBG_PRINTF5("Joining multicast group on interface %s (index %d)\n", ifname, group.imr_ifindex); + +#if defined(OPTION_SOCKET_HW_TIMESTAMPS) + // Get MAC address for the interface and save it in the socket structure + if (!socketGetMAC(socket->ifname, socket->ifmac)) { + DBG_PRINTF_WARNING("socketJoin: Failed to get MAC address for interface %s!\n", ifname); + } +#endif + + } else if (ifaddr != NULL && !(ifaddr[0] == 0 && ifaddr[1] == 0 && ifaddr[2] == 0 && ifaddr[3] == 0)) { + // Use interface address + group.imr_address.s_addr = *(uint32_t *)ifaddr; +#if defined(OPTION_SOCKET_HW_TIMESTAMPS) + socket->ifaddr = *(uint32_t *)ifaddr; +#endif + + DBG_PRINTF5("Joining multicast group on interface address %u.%u.%u.%u\n", ifaddr[0], ifaddr[1], ifaddr[2], ifaddr[3]); + + } else { + // Use INADDR_ANY (kernel picks interface based on routing) + group.imr_address.s_addr = htonl(INADDR_ANY); + + DBG_PRINT5("Joining multicast group on INADDR_ANY\n"); + } + + if (0 > setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char *)&group, sizeof(group))) { + DBG_PRINTF_ERROR("socketJoin failed (errno=%d,%s) - can't set multicast socket option IP_ADD_MEMBERSHIP!\n", socketGetLastError(), + socketGetErrorString(socketGetLastError())); + return 0; + } +#else + // Non-Linux platforms: use standard struct ip_mreq (address-based only) + struct ip_mreq group; + group.imr_multiaddr.s_addr = *(uint32_t *)maddr; + // Use the specified interface address, or INADDR_ANY if NULL or 0.0.0.0 + if (ifaddr == NULL || (ifaddr[0] == 0 && ifaddr[1] == 0 && ifaddr[2] == 0 && ifaddr[3] == 0)) { + group.imr_interface.s_addr = htonl(INADDR_ANY); + } else { + group.imr_interface.s_addr = *(uint32_t *)ifaddr; + } + if (0 > setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char *)&group, sizeof(group))) { + DBG_PRINTF_ERROR("socketJoin failed (errno=%d,%s) - can't set multicast socket option IP_ADD_MEMBERSHIP!\n", socketGetLastError(), + socketGetErrorString(socketGetLastError())); + return 0; + } + (void)ifname; // Unused on non-Linux platforms +#endif + return 1; +} + +// Receive from UDP socket +// Blocking mode only, with optional timeout set with socketSetTimeout() +// Returns optional receive timestamps if (time != NULL) +// Support hardware timestamps if enabled on the socket and with OPTION_SOCKET_HW_TIMESTAMPS defined, otherwise system time is used +// Return values: +// n > 0 : number of bytes received +// n == 0 : timeout (set with socketTimeout) expired or would-block — no data yet, caller should loop and do background work +// n < 0 : socket closed (graceful or reset) or unrecoverable error — caller should exit the receive loop +int16_t socketRecvFrom(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, uint8_t *addr, uint16_t *port, uint64_t *time) { + + assert(socket != INVALID_SOCKET_HANDLE); + SOCKET sock = SOCKET_FD(socket); + assert(sock != INVALID_SOCKET); + + SOCKADDR_IN src; + src.sin_port = 0; + src.sin_addr.s_addr = 0; + + int16_t n = 0; + +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + // Always use recvmsg() on Linux with HW_TIMESTAMPS: needed for IP_PKTINFO and optional timestamps. + // Removing the if(time!=NULL) gate here is critical — without it the else clause + // would dangle onto the port-extraction statement after #endif, causing no receive when time==NULL. + { + struct iovec iov; + struct msghdr msg; + char control[CMSG_SPACE(sizeof(struct timespec) * 3) + CMSG_SPACE(sizeof(struct in_pktinfo))]; + iov.iov_base = buffer; + iov.iov_len = bufferSize; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &src; + msg.msg_namelen = sizeof(src); + msg.msg_flags = 0; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + n = (int16_t)recvmsg(sock, &msg, 0); + + // n = 0, zero-length UDP datagram, not a socket close, caller loops + if (n == 0) { + return 0; // Timeout — caller loops and does background work + } + + // n < 0, error or timeout + else if (n < 0) { + int32_t err = socketGetLastError(); + if (socketTimeout(err)) { + return 0; // Timeout — caller loops and does background work + } + DBG_PRINTF_ERROR("socketRecvFrom: recvmsg failed (errno=%d,%s, result=%d)!\n", err, socketGetErrorString(err), n); + return -1; + } + + // Extract timestamp and interface info from control messages if available + if (time != NULL) + *time = 0; + struct timespec *hw = NULL; + struct timespec *sw = NULL; + struct cmsghdr *cmsg; + uint16_t n = 0; + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + n++; + int level = cmsg->cmsg_level; + int type = cmsg->cmsg_type; + + DBG_PRINTF6("socketRecvFrom: cmsg level=%d type=%d (%s)\n", level, type, // + (level == SOL_SOCKET && type == SO_TIMESTAMPING) ? "SO_TIMESTAMPING" + : (level == SOL_SOCKET && type == SO_TIMESTAMPNS) ? "SO_TIMESTAMPNS" + : (level == IPPROTO_IP && type == IP_PKTINFO) ? "IP_PKTINFO" + : "UNKNOWN"); + + if (SOL_SOCKET == level && SO_TIMESTAMPING == type) { + if (cmsg->cmsg_len < sizeof(struct timespec) * 3) { + DBG_PRINT_WARNING("short SO_TIMESTAMPING message"); + break; + } + assert(hw == NULL); + hw = (struct timespec *)CMSG_DATA(cmsg); + } else if (SOL_SOCKET == level && SO_TIMESTAMPNS == type) { + if (cmsg->cmsg_len < sizeof(struct timespec)) { + DBG_PRINT_WARNING("short SO_TIMESTAMPNS message"); + break; + } + sw = (struct timespec *)CMSG_DATA(cmsg); + } else if (IPPROTO_IP == level && IP_PKTINFO == type) { + struct in_pktinfo *pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsg); + // Always print IP_PKTINFO for debugging (use printf, not DBG_PRINTF) + DBG_PRINTF6("socketRecvFrom: IP_PKTINFO - ipi_ifindex=%d, ipi_addr=%08x, ipi_spec_dst=%08x, socket->ifindex=%d\n", pktinfo->ipi_ifindex, + ntohl(pktinfo->ipi_addr.s_addr), ntohl(pktinfo->ipi_spec_dst.s_addr), socket->ifindex); + assert(socket->ifindex == 0 || socket->ifindex == pktinfo->ipi_ifindex); + // Note: Just to be sure, we always get timestamps from expected if. Currently no mechanism to return this info to caller + } + } + if (n == 0) { + DBG_PRINT6("socketRecvFrom: No control messages received\n"); + } + + // Process timestamps if requested + if (time != NULL) { + uint64_t t = 0; + if (hw != NULL) { + struct timespec *ts; + ts = &hw[2]; + t = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; + if (t != 0) { + DBG_PRINT6("socketRecvFrom: timestamp taken from control messages SO_TIMESTAMPING [2]\n"); + } else { + ts = &hw[0]; + t = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; + if (t != 0) { + DBG_PRINT6("socketRecvFrom: timestamp taken from control messages SO_TIMESTAMPING [0]\n"); + } + } + + // { + // uint64_t t_hw = hw[2].tv_sec * 1000000000ULL + hw[2].tv_nsec; + // uint64_t t_sw = hw[0].tv_sec * 1000000000ULL + hw[0].tv_nsec; + // printf("socketRecvFrom: HW timestamp = %" PRIu64 " ns, SW timestamp = %" PRIu64 " ns, diff = %" PRIi64 " ns\n", t_hw, t_sw, (int64_t)(t_hw - t_sw)); + // } + } + if (t == 0 && sw != NULL) { + struct timespec *ts = sw; + t = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; + DBG_PRINT5("socketRecvFrom: timestamp taken from control messages SO_TIMESTAMPNS\n"); + } + if (t == 0) { + DBG_PRINT_WARNING("socketRecvFrom: No timestamp found in control messages\n"); + } + *time = t; + } + } +#else + { + socklen_t srclen = sizeof(src); + n = (int16_t)recvfrom(sock, (char *)buffer, bufferSize, 0, (SOCKADDR *)&src, &srclen); + + // n = 0, zero-length UDP datagram, not a socket close, caller loops + if (n == 0) { + return 0; // Timeout — caller loops and does background work + } else if (n < 0) { + int32_t err = socketGetLastError(); + // DBG_PRINTF6("socketRecvFrom: recvfrom returned n<0 (errno=%d,%s)\n", err, socketGetErrorString(err)); + + if (socketTimeout(err)) { + // DBG_PRINTF6("socketRecvFrom: recvfrom returned n<0, (errno=%d,%s), socket timeout, return 0\n", err, socketGetErrorString(err)); + return 0; // Timeout + } + + DBG_PRINTF_ERROR("socketRecvFrom: failed n=%d (errno=%u,%s) , return -1\n", n, err, socketGetErrorString(err)); + return -1; + } + + if (time != NULL) { + assert(false && "Hardware timestamp are not enabled, would return system time"); + *time = clockGet(); + } + } +#endif + + if (port) + *port = htons(src.sin_port); + if (addr) + memcpy(addr, &src.sin_addr.s_addr, 4); + +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + DBG_PRINTF6("socketRecvFrom: sock=%d, ifindex=%d returned n=%u, time=%" PRIu64 "\n", sock, socket->ifindex, n, time ? *time : 0); +#else + DBG_PRINTF6("socketRecvFrom: sock=%d returned n=%u, time=%" PRIu64 "\n", sock, n, time ? *time : 0); +#endif + + return n; +} + +// Receive from TCP socket +// Blocking mode only, with optional timeout set with socketSetTimeout() +// For UDP use socketRecvFrom() instead, which also returns the source address and supports timestamps +// Return values: +// n > 0 : number of bytes received +// n == 0 : timeout (set with socketTimeout) expired or would-block — no data yet, caller should loop and do background work +// n < 0 : socket closed (graceful or reset) or unrecoverable error — caller should exit the receive loop +#if defined(OPTION_ENABLE_TCP) +int16_t socketRecv(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t buffer_size, bool waitAll) { + + assert(socket != INVALID_SOCKET_HANDLE); + // assert(socket->flags & SOCKET_MODE_TCP); // Use socketRecvFrom() for UDP sockets + assert(buffer_size > 0); + SOCKET sock = SOCKET_FD(socket); + assert(sock != INVALID_SOCKET); + + if (!waitAll) { + int16_t n = (int16_t)recv(sock, (char *)buffer, buffer_size, 0); + + // n = 0, socket close + if (n == 0) { + DBG_PRINT6("socketRecv: recv returned n=0, socket closed, return -1\n"); + return -1; // Socket closed + } + + // n < 0, error or timeout + else if (n < 0) { + int32_t err = socketGetLastError(); + if (socketTimeout(err)) { + DBG_PRINTF_ERROR("socketRecv: recv returned n<0, socket timeout (errno=%d,%s), return 0\n", err, socketGetErrorString(err)); + return 0; // Timeout, no data yet + } + DBG_PRINTF_ERROR("socketRecv: recv returned n<0, socket error (errno=%d,%s), return -1\n", err, socketGetErrorString(err)); + return -1; // Error + } + return n; + } + + // waitAll: loop until exactly `size` bytes have been received. + // MSG_WAITALL alone is not sufficient when SO_RCVTIMEO is set + // Linux may return a partial size if the timeout fires mid-read. + // We therefore implement a loop on top and return the timeout to the caller only when there is no data yet + uint16_t received = 0; + uint32_t timeout_counter = 0; + for (;;) { + int16_t n = (int16_t)recv(sock, (char *)buffer + received, (uint16_t)(buffer_size - received), MSG_WAITALL); + + // n = 0, socket close + if (n == 0) { + DBG_PRINT6("socketRecv: recv waitall returned n=0, socket closed, return -1\n"); + return -1; // Socket closed + } + + // n < 0, error or timeout + else if (n < 0) { + int32_t err = socketGetLastError(); + if (socketTimeout(err)) { + DBG_PRINTF6("socketRecv: recv waitall returned n<0, socket timeout (errno=%d,%s), return 0\n", err, socketGetErrorString(err)); + if (received == 0) { + return 0; // Timeout only before any data ok + } + DBG_PRINT_ERROR("socketRecv: recv waitall returned n<0, timeout mid-frame, return -1\n"); + return -1; // Partial frame received — TCP stream is desynchronised + } + DBG_PRINTF_ERROR("socketRecv: recv waitall returned n<0, socket error (errno=%d,%s), return -1\n", err, socketGetErrorString(err)); + return -1; // Error + } + + received = (uint16_t)(received + (uint16_t)n); + if (received >= buffer_size) { + break; // done + } + + if (++timeout_counter >= 4) { + DBG_PRINT_ERROR("socketRecv: recv waitall timeout mid-frame, giving up after 4 attempts\n"); + break; // loop protection: should never happen + } + + DBG_PRINTF_WARNING("socketRecv waitall: received %u bytes, waiting for %u more\n", received, buffer_size - received); + } + + assert(received == buffer_size); + return (int16_t)received; +} +#endif // OPTION_ENABLE_TCP + +// Send datagram on UDP socket +// Returns number of bytes sent or -1 on error +// Requests and may returns optional send time if (time != NULL) +// Support hardware timestamps if enabled on the socket and with OPTION_SOCKET_HW_TIMESTAMPS defined, otherwise system time is used +// If *time = 0 on return, no timestamp is available yet, but can be obtained with socketGetSendTime() +// On non-Linux platforms, *time is set to system time at send +// Returns total number of bytes sent, 0 on socket closed or -1 on error +int16_t socketSendTo(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t size, const uint8_t *addr, uint16_t port, uint64_t *time) { + + assert(socket != INVALID_SOCKET_HANDLE); + SOCKET sock = SOCKET_FD(socket); + assert(sock != INVALID_SOCKET); + +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + DBG_PRINTF6("socketSendTo: sock=%d, ifindex=%d\n", sock, socket->ifindex); +#else + DBG_PRINTF6("socketSendTo: sock=%d\n", sock); +#endif + + SOCKADDR_IN sa; + sa.sin_family = AF_INET; +#if defined(_WIN) // Windows + memcpy(&sa.sin_addr.S_un.S_addr, addr, 4); +#else + memcpy(&sa.sin_addr.s_addr, addr, 4); +#endif + sa.sin_port = htons(port); + +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + if (time != NULL) { + // On Linux, we need to use sendmsg() with SO_TIMESTAMPING control message + // to request TX timestamp generation for this specific packet + struct iovec iov; + struct msghdr msg; + char control[CMSG_SPACE(sizeof(uint32_t))]; + struct cmsghdr *cmsg; + + iov.iov_base = (void *)buffer; + iov.iov_len = size; + + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + // Add control message to request timestamp generation + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SO_TIMESTAMPING; + cmsg->cmsg_len = CMSG_LEN(sizeof(uint32_t)); + + // Request both hardware and software timestamps + // Hardware timestamp will be used if available, otherwise fall back to software + uint32_t ts_flags = SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_TX_HARDWARE; + memcpy(CMSG_DATA(cmsg), &ts_flags, sizeof(ts_flags)); + *time = 0; // Clear time, to indicate that it may be obtained later with socketGetSendTime() + ssize_t n = sendmsg(sock, &msg, 0); + if (n < 0) { + int32_t err = socketGetLastError(); + if (socketWouldBlock(err)) { + DBG_PRINT_ERROR("socketSendTo: unexpected WBLOCK\n"); + return -1; // Should never happen on a blocking socket + } + if (socketIsClosed(err)) { + DBG_PRINTF6("socketSendTo: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); + return 0; // Transmit socket closed + } + DBG_PRINTF_ERROR("socketSendTo: sendmsg failed with errno=%d,%s!\n", err, socketGetErrorString(err)); + return -1; + } + return (int16_t)n; + } +#else + + if (time != NULL) + *time = clockGet(); // Return system time as send time on non-Linux platforms + +#endif + ssize_t n = sendto(sock, (const char *)buffer, size, 0, (SOCKADDR *)&sa, (uint16_t)sizeof(sa)); + if (n < 0) { + int32_t err = socketGetLastError(); + if (socketWouldBlock(err)) { + DBG_PRINT_ERROR("socketSendTo: unexpected WBLOCK\n"); + return -1; // Should never happen on a blocking socket + } + if (socketIsClosed(err)) { + DBG_PRINTF6("socketSendTo: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); + return 0; // Transmit socket closed + } + // EMSGSIZE means the datagram exceeds the path MTU and the DF bit set in socketOpen + // forbids fragmenting it. That is a configuration problem, not a transient error. + if (err == SOCKET_ERROR_MSGSIZE) { + DBG_PRINTF_ERROR("socketSendTo: segment of %u bytes exceeds the path MTU and must not be fragmented.\n" + " Reduce OPTION_MTU (currently %u, giving XCPTL_MAX_SEGMENT_SIZE=%u) to fit the link.\n", + (unsigned)size, (unsigned)OPTION_MTU, (unsigned)XCPTL_MAX_SEGMENT_SIZE); + return -1; + } + DBG_PRINTF_ERROR("socketSendTo: sendto failed with errno=%d,%s!\n", err, socketGetErrorString(err)); + return -1; + } + return (int16_t)n; +} + +// Send buffer on a TCP socket +// Thread safe +// Returns total number of bytes sent, 0 on socket closed or -1 on error +#if defined(OPTION_ENABLE_TCP) +int16_t socketSend(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t size) { + + assert(socket != INVALID_SOCKET_HANDLE); + SOCKET sock = SOCKET_FD(socket); + assert(sock != INVALID_SOCKET); + + ssize_t n = send(sock, (const char *)buffer, size, 0); + if (n < 0) { + int32_t err = socketGetLastError(); + if (socketWouldBlock(err)) { + DBG_PRINT_ERROR("socketSend: unexpected WBLOCK\n"); + return -1; // Should never happen on a blocking socket + } + if (socketIsClosed(err)) { + DBG_PRINTF6("socketSend: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); + return 0; // Transmit socket closed + } + DBG_PRINTF_ERROR("socketSend: send failed with errno=%d,%s!\n", err, socketGetErrorString(err)); + return -1; + } + return (int16_t)n; +} +#endif // OPTION_ENABLE_TCP + +#endif // !defined(_FREE_RTOS) || defined(FREE_RTOS_POSIX_SIM) + +// Vectored IO send and receive functions using sendmsg/recvmsg with iovec for efficient scatter-gather I/O +#if !defined(_WIN) && !defined(_FREE_RTOS) + +// Send multiple datagrams on a UDP socket +// Returns number of bytes sent or -1 on error +// Send multiple buffers as a UDP datagram to a specific address/port +// Using iovec for efficient scatter-gather I/O (POSIX: Linux, macOS, QNX) +// Thread safe +// buffers: array of pointers to data buffers +// sizes: array of buffer sizes, one per buffer +// count: number of buffers +// Returns total number of bytes sent, 0 on socket closed or -1 on error +int16_t socketSendToV(SOCKET_HANDLE socket, tQueueBuffer buffers[], uint16_t count, const uint8_t *addr, uint16_t port) { + + assert(socket != INVALID_SOCKET_HANDLE); + SOCKET sock = SOCKET_FD(socket); + assert(sock != INVALID_SOCKET); + + SOCKADDR_IN sa; + sa.sin_family = AF_INET; + memcpy(&sa.sin_addr.s_addr, addr, 4); + sa.sin_port = htons(port); + + // Build iovec array on the stack - VLAs are acceptable here as count is usually small + struct iovec iov[count]; + uint32_t total = 0; + for (uint16_t i = 0; i < count; i++) { + iov[i].iov_base = (void *)buffers[i].buffer; + iov[i].iov_len = buffers[i].size; + total += buffers[i].size; + } + + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + msg.msg_iov = iov; + msg.msg_iovlen = count; + + ssize_t n = sendmsg(sock, &msg, 0); + if (n < 0) { + int32_t err = socketGetLastError(); + if (socketWouldBlock(err)) { + DBG_PRINT_ERROR("socketSendToV: unexpected WBLOCK\n"); + return -1; // Should never happen on a blocking socket + } + if (socketIsClosed(err)) { + DBG_PRINTF6("socketSendToV: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); + return 0; // Transmit socket closed + } + if (err == SOCKET_ERROR_MSGSIZE) { + DBG_PRINTF_ERROR("socketSendToV: segment of %" PRIu32 " bytes exceeds the path MTU and must not be fragmented.\n" + " Reduce OPTION_MTU (currently %u, giving XCPTL_MAX_SEGMENT_SIZE=%u) to fit the link.\n", + total, (unsigned)OPTION_MTU, (unsigned)XCPTL_MAX_SEGMENT_SIZE); + return -1; + } + DBG_PRINTF_ERROR("socketSendToV: sendmsg failed with errno=%d,%s!\n", err, socketGetErrorString(err)); + return -1; + } + if (total != n) { + DBG_PRINTF_WARNING("socketSendToV: partial send, sent %" PRIu32 " of %" PRIu32 " bytes\n", (uint32_t)n, total); + return -1; // Treat partial sends as an error on UDP sockets, as the caller cannot recover + } + return (int16_t)n; +} + +// Send multiple buffers on a TCP socket +// Using iovec for efficient scatter-gather I/O (POSIX: Linux, macOS, QNX) +// Thread safe +// buffers: array of pointers to data buffers +// sizes: array of buffer sizes, one per buffer +// count: number of buffers +// Returns total number of bytes sent, 0 on socket closed or -1 on error +int16_t socketSendV(SOCKET_HANDLE socket, tQueueBuffer buffers[], uint16_t count) { + + assert(socket != INVALID_SOCKET_HANDLE); + SOCKET sock = SOCKET_FD(socket); + assert(sock != INVALID_SOCKET); + + // Build iovec array on the stack - VLAs are acceptable here as count is usually small + struct iovec iov[count]; + for (uint16_t i = 0; i < count; i++) { + iov[i].iov_base = (void *)buffers[i].buffer; + iov[i].iov_len = buffers[i].size; + } + + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = iov; + msg.msg_iovlen = count; + + // TCP streams may deliver partial sends: loop until all data is accepted by the kernel + // Advance iovec entries as bytes are consumed to avoid re-sending already sent data + // Note: all sockets in this codebase are blocking (see socketOpen), so WBLOCK must not + // occur. If it does mid-loop, the iovec state is partially consumed and the caller cannot + // recover, so it is treated as an unrecoverable error rather than returning a partial count. + int32_t total = 0; + for (;;) { + ssize_t n = sendmsg(sock, &msg, 0); + if (n < 0) { + int32_t err = socketGetLastError(); + if (socketWouldBlock(err)) { + DBG_PRINT_ERROR("socketSendV: unexpected WBLOCK\n"); + return -1; // Should never happen on a blocking socket + } + if (socketIsClosed(err)) { + DBG_PRINTF6("socketSendV: socket closed (errno=%d,%s)\n", err, socketGetErrorString(err)); + return 0; // Transmit socket closed + } + DBG_PRINTF_ERROR("socketSendV: sendmsg failed with errno=%d,%s!\n", err, socketGetErrorString(err)); + return -1; + } + total += (int32_t)n; + + // Advance the iovec past the bytes already sent + size_t remaining = (size_t)n; + while (msg.msg_iovlen > 0 && remaining >= msg.msg_iov[0].iov_len) { + remaining -= msg.msg_iov[0].iov_len; + msg.msg_iov++; + msg.msg_iovlen--; + } + if (msg.msg_iovlen == 0) + break; // All data sent + // Adjust the first remaining iovec for the partial send + msg.msg_iov[0].iov_base = (uint8_t *)msg.msg_iov[0].iov_base + remaining; + msg.msg_iov[0].iov_len -= remaining; + } + + return (int16_t)total; +} + +#endif // !defined(_WIN) && !defined(_FREE_RTOS) + +// Get send time of last sent packet +// Retrieves TX hardware timestamp and kernel software timestamp from socket error queue +// Returns false if no timestamp available or on error +// On non-Linux platforms, this function always returns false +// On Linux, requires OPTION_SOCKET_HW_TIMESTAMPS defined and hardware timestamping enabled on the socket +// hw_time and sw_time are optional, set to NULL if not needed +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) +bool socketGetSendTime(SOCKET_HANDLE socket, uint64_t *hw_time, uint64_t *sw_time) { + + assert(socket != NULL); + SOCKET sock = socket->sock; + assert(sock != INVALID_SOCKET); + + if (hw_time) + *hw_time = 0; + if (sw_time) + *sw_time = 0; + + char control[512]; + char data[1]; + struct iovec iov; + struct msghdr msg; + struct cmsghdr *cmsg; + struct timespec *ts = NULL; + + iov.iov_base = data; + iov.iov_len = sizeof(data); + + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + DBG_PRINT5("socketGetSendTime: Reading from error queue...\n"); + + // Read from error queue with retries (timeout 10ms) + ssize_t ret = -1; + for (uint32_t attempt = 0; attempt < 10; attempt++) { + ret = recvmsg(sock, &msg, MSG_ERRQUEUE | MSG_DONTWAIT); + if (ret >= 0) { + DBG_PRINTF5("socketGetSendTime: Got message from error queue after %u attempts, ret=%ld\n", attempt, ret); + break; + } + if (errno != EAGAIN && errno != EWOULDBLOCK) { + DBG_PRINTF_ERROR("socketGetSendTime: recvmsg error queue failed with errno=%d (%s)\n", errno, strerror(errno)); + return false; + } + // Wait a bit and retry + sleepUs(1000); // 1ms + } + if (ret < 0) { + DBG_PRINT_WARNING("socketGetSendTime: Timeout, no TX timestamp available after retries\n"); + return false; + } + + // Look for timestamps in control messages + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + DBG_PRINTF5("socketGetSendTime: Found cmsg level=%d type=%d (SOL_SOCKET=%d SO_TIMESTAMPING=%d)\n", cmsg->cmsg_level, cmsg->cmsg_type, SOL_SOCKET, SO_TIMESTAMPING); + if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMPING) { + // SO_TIMESTAMPING returns 3 timespec structures: software, deprecated, hardware + struct timespec *ts_array = (struct timespec *)CMSG_DATA(cmsg); + + DBG_PRINTF5("socketGetSendTime: ts[0]=%ld.%09ld ts[1]=%ld.%09ld ts[2]=%ld.%09ld\n", ts_array[0].tv_sec, ts_array[0].tv_nsec, ts_array[1].tv_sec, ts_array[1].tv_nsec, + ts_array[2].tv_sec, ts_array[2].tv_nsec); + + // hardware timestamp (index 2) + ts = &ts_array[2]; + if (ts->tv_sec != 0 || ts->tv_nsec != 0) { + if (hw_time) + *hw_time = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; + DBG_PRINTF5("socketGetSendTime: Using HW TX timestamp: %ld.%09ld\n", ts->tv_sec, ts->tv_nsec); + } + + // software timestamp (index 0) + ts = &ts_array[0]; + if (ts->tv_sec != 0 || ts->tv_nsec != 0) { + if (sw_time) + *sw_time = (uint64_t)ts->tv_sec * 1000000000ULL + (uint64_t)ts->tv_nsec; + DBG_PRINTF5("socketGetSendTime: Using SW TX timestamp: %ld.%09ld\n", ts->tv_sec, ts->tv_nsec); + } + + if ((hw_time == NULL || *hw_time != 0) && (sw_time == NULL || *sw_time != 0)) { + break; // Got what we needed + } + } + } + + if ((hw_time == NULL || *hw_time != 0) && (sw_time == NULL || *sw_time != 0)) { + DBG_PRINTF5("socketGetSendTime: hw=%" PRIu64 ", sw=%" PRIu64 ", sys= %" PRIu64 "\n", hw_time ? *hw_time : 0, sw_time ? *sw_time : 0, clockGet()); + return true; // Got all requested timestamps + } + if (hw_time != NULL && *hw_time == 0) + DBG_PRINT_WARNING("socketGetSendTime: No hardware TX timestamp found\n"); + if (sw_time != NULL && *sw_time == 0) + DBG_PRINT_WARNING("socketGetSendTime: No software TX timestamp found\n"); + + return false; +} +#endif // defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) + +#endif // !_WIN (closes the #else of #if _FREE_RTOS && !FREE_RTOS_POSIX_SIM) + +#endif // OPTION_ENABLE_TCP || OPTION_ENABLE_UDP && !OPTION_ENABLE_UDP_RAW diff --git a/src/sockets.h b/src/sockets.h new file mode 100644 index 00000000..36f59638 --- /dev/null +++ b/src/sockets.h @@ -0,0 +1,377 @@ +#pragma once +#define __SOCKETS_H__ + +/*---------------------------------------------------------------------------- +| File: +| sockets.h +| +| Description: +| Platform socket abstraction layer (Linux/Windows/macOS/QNX/FreeRTOS) +| +| Requires OPTION_ENABLE_TCP and/or OPTION_ENABLE_UDP, or OPTION_ENABLE_UDP_RAW +| (mutually exclusive) — the entire API is compiled away without one of them. +| +| Build variants and supported functions: +| +| _FREE_RTOS && !FREE_RTOS_POSIX_SIM (bare-metal FreeRTOS): +| OPTION_FREERTOS_LWIP defined: +| socketStartup, socketCleanup, socketOpen (UDP only, TCP not supported), +| socketBind, socketShutdown, socketClose, +| socketRecvFrom, socketSendTo, socketSetTimeout +| OPTION_FREERTOS_LWIP not defined: +| All functions are error stubs — the caller must provide the implementation. +| +| POSIX: _LINUX / _MACOS / _QNX (and FREE_RTOS_POSIX_SIM): +| Base: +| socketStartup (no-op), socketCleanup (no-op), +| socketOpen, socketBind, socketShutdown, socketClose, +| socketGetMAC, socketSetTimeout, +| socketJoin, socketRecvFrom, socketSendTo +| + OPTION_ENABLE_TCP: +| socketListen, socketAccept, socketRecv, socketSend +| + !_FREE_RTOS (scatter-gather via sendmsg): +| socketSendToV, socketSendV +| + OPTION_ENABLE_GET_LOCAL_ADDR: +| socketGetLocalAddr +| + _LINUX && OPTION_SOCKET_HW_TIMESTAMPS: +| SOCKET_HANDLE becomes struct socket* (fd + interface metadata) +| socketBindToDevice, socketEnableTimestamps, socketGetSendTime +| socketRecvFrom uses recvmsg with SO_TIMESTAMPING / SO_TIMESTAMPNS +| socketSendTo uses sendmsg with per-packet timestamp request +| +| _WIN (Windows / Winsock2): +| Base: +| socketStartup (WSAStartup), socketCleanup (WSACleanup), +| socketOpen, socketBind, socketShutdown, socketClose, +| socketSetTimeout, socketJoin, socketRecvFrom, socketSendTo +| + OPTION_ENABLE_TCP: +| socketListen, socketAccept, socketRecv, socketSend +| + OPTION_ENABLE_GET_LOCAL_ADDR: +| socketGetLocalAddr +| No scatter-gather I/O (sendmsg not available on Windows). +| No hardware timestamping. +| +| OPTION_ENABLE_UDP_RAW (raw Ethernet, mutually exclusive with the above): +| A hand-crafted UDP/IPv4 layer over a raw Ethernet HAL, for targets without +| any TCP/IP stack. Implemented in socket_raw.c, HAL in socket_raw_hal*.c. +| Requires OPTION_QUEUE_32 - the 64 bit queues use the vectored send path, +| which this variant does not provide. Enforced in xcptl_cfg.h. +| Supported subset - see docs/SOCKET_RAW.md for the full design: +| socketStartup, socketCleanup, socketGetErrorString, +| socketOpen, socketBind, socketShutdown, socketClose, +| socketRecvFrom, socketSendTo, socketSetTimeout +| Not provided: TCP (socketListen/Accept/Recv/Send), multicast (socketJoin), +| vectored I/O (socketSendToV/socketSendV), hardware timestamps, +| socketGetMAC, socketGetLocalAddr. +| +| Copyright (c) Vector Informatik GmbH. All rights reserved. +| See LICENSE file in the project root for details. +| + ----------------------------------------------------------------------------*/ + +#include "platform.h" // for platform defines (WIN_, LINUX_, MACOS_) and specific implementation of sockets, clock, thread, mutex, spinlock + +#ifdef __cplusplus +extern "C" { +#endif + +// Platform independent socket functions + +#if defined(OPTION_ENABLE_TCP) || defined(OPTION_ENABLE_UDP) || defined(OPTION_ENABLE_UDP_RAW) + +// Note: +// SOCKET_HANDLE is an opaque type that may wrap the OS socket handle and additional info (e.g. for Linux hardware timestamping) +// INVALID_SOCKET_HANDLE is the invalid value for SOCKET_HANDLE +// SOCKET_FD(s) extracts the raw OS socket fd from a SOCKET_HANDLE (which may be a struct socket pointer on Linux with HW timestamps) + +#if defined(OPTION_ENABLE_UDP_RAW) // Raw Ethernet transport (no OS socket API) + +// SOCKET_HANDLE is an opaque context defined in socket_raw.c +// There is no OS socket fd, therefore SOCKET_FD() is deliberately not defined +#include "xcptl_cfg.h" // for XCPTL_TX_HEADROOM + +struct socket_raw; +typedef struct socket_raw *SOCKET_HANDLE; +#define INVALID_SOCKET_HANDLE NULL + +// Self contained error codes - no on bare metal targets +#define SOCKET_ERROR_NONE 0 +#define SOCKET_ERROR_TIMEDOUT 1 +#define SOCKET_ERROR_BADF 2 +#define SOCKET_ERROR_NOTCONN 3 +#define SOCKET_ERROR_HAL 4 +#define SOCKET_ERROR_TOOBIG 5 +#define SOCKET_ERROR_NOPEER 6 +#define SOCKET_ERROR_MSGSIZE 7 // frame exceeds what the link can carry, no fragmentation + +// Last error of the calling context, set by the raw socket functions +int32_t socketGetLastError(void); + +#define socketIsClosed(err) ((err) == SOCKET_ERROR_BADF || (err) == SOCKET_ERROR_NOTCONN) +#define socketWouldBlock(err) ((err) == SOCKET_ERROR_TIMEDOUT) +#define socketTimeout(err) ((err) == SOCKET_ERROR_TIMEDOUT) + +#elif !defined(_WIN) // Non-Windows platform sockets + +#if !defined(_WIN) && !defined(_FREE_RTOS) +#include "queue.h" // for tQueueBuffer +#endif + +#define SOCKET int +#define INVALID_SOCKET (-1) + +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) +// For Linux hardware timestamping support, SOCKET_HANDLE is a pointer to struct socket which contains the socket fd and interface info for timestamp retrieval +struct socket { + SOCKET sock; + uint32_t addr; // Bind address (network byte order) maybe INADDR_ANY + uint16_t port; // Port + unsigned int ifindex; // Interface index + char ifname[16]; // Interface name + uint32_t ifaddr; // Interface address + uint8_t ifmac[6]; // Interface MAC address +}; +typedef struct socket *SOCKET_HANDLE; +#define INVALID_SOCKET_HANDLE NULL +#define SOCKET_FD(s) ((s)->sock) // Extract the OS socket fd from a SOCKET_HANDLE +#else +// Linux (without HW timestamps), FreeRTOS, macOS, QNX: SOCKET_HANDLE is the raw OS fd +typedef SOCKET SOCKET_HANDLE; +#define INVALID_SOCKET_HANDLE INVALID_SOCKET +#define SOCKET_FD(s) (s) // Extract the OS socket fd from a SOCKET_HANDLE +#endif + +#define SOCKADDR_IN struct sockaddr_in +#define SOCKADDR struct sockaddr + +#undef htonll +#define htonll(val) ((((uint64_t)htonl((uint32_t)(val))) << 32) + htonl((uint32_t)((val) >> 32))) + +#include // for errno and error codes from socketGetLastError + +#define SOCKET_ERROR_ABORT ECONNABORTED // 53 +#define SOCKET_ERROR_RESET ECONNRESET // 54 +#define SOCKET_ERROR_INTR EINTR // 4 +#define SOCKET_ERROR_TIMEDOUT ETIMEDOUT // 60 +#define SOCKET_ERROR_WBLOCK EAGAIN // 35 EWOULDBLOCK is the same as EAGAIN on Linux, but may be different on other platforms +#define SOCKET_ERROR_PIPE EPIPE // 32 +#define SOCKET_ERROR_BADF EBADF // 9 +#define SOCKET_ERROR_NOTCONN ENOTCONN // 107 (57 macOS) Socket is not connected +#define SOCKET_ERROR_MSGSIZE EMSGSIZE // 90 (40 macOS) Datagram too large for the path MTU (DF is set, see socketOpen) + +#define socketGetLastError(void) errno +#define socketIsClosed(err) ((err) == ENOTCONN || (err) == ECONNABORTED || (err) == EBADF || (err) == ECONNRESET) +#define socketWouldBlock(err) ((err) == EAGAIN || (err) == EWOULDBLOCK) +#define socketTimeout(err) ((err) == ETIMEDOUT || (err) == EAGAIN || (err) == EWOULDBLOCK || (err) == EINTR) + +#else // Windows sockets + +#include +#include + +typedef SOCKET SOCKET_HANDLE; +#define INVALID_SOCKET_HANDLE INVALID_SOCKET +#define SOCKET_FD(s) (s) + +#define SOCKADDR_IN struct sockaddr_in +#define SOCKADDR struct sockaddr + +#include // for errno and error codes from socketGetLastError +int32_t socketGetLastError(void); +#define SOCKET_ERROR_ABORT WSAECONNABORTED // 10053 +#define SOCKET_ERROR_RESET WSAECONNRESET // 10054 +#define SOCKET_ERROR_INTR WSAEINTR // 10004 +#define SOCKET_ERROR_TIMEDOUT WSAETIMEDOUT // 10060 +#define SOCKET_ERROR_WBLOCK WSAEWOULDBLOCK // 10035 +#define SOCKET_ERROR_PIPE WSAESHUTDOWN // 10058 +#define SOCKET_ERROR_BADF WSAEBADF // 10009 +#define SOCKET_ERROR_NOTCONN WSAENOTCONN // 10057 +#define SOCKET_ERROR_MSGSIZE WSAEMSGSIZE // 10040 Datagram too large for the path MTU (DF is set, see socketOpen) +#define socketIsClosed(err) ((err) == WSAECONNABORTED || (err) == WSAEBADF || (err) == WSAECONNRESET || (err) == WSAEINTR) +#define socketWouldBlock(err) ((err) == WSAEWOULDBLOCK) +#define socketTimeout(err) ((err) == WSAETIMEDOUT) + +#define ssize_t int + +#endif + +// Socket mode flags +#define SOCKET_MODE_TCP (1 << 0) // TCP socket +#define SOCKET_MODE_REUSEADDR (1 << 2) // Allow reuse of local address +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) +#define SOCKET_MODE_GET_IF_INFO (1 << 6) // Enable IP_PKTINFO to identify the receiving interface (Linux only) +#define SOCKET_MODE_HW_TIMESTAMPING (1 << 4) // Enable hardware timestamping (Linux only, requires root) +#define SOCKET_MODE_SW_TIMESTAMPING (1 << 5) // Enable kernel software timestamping (Linux only, requires root) +#endif + +// Socket functions + +// Initialize the socket subsystem (Windows: WSAStartup; no-op on POSIX) +// Must be called once before any other socket function +// Returns true on success +bool socketStartup(void); + +// Clean up the socket subsystem (Windows: WSACleanup; no-op on POSIX) +void socketCleanup(void); + +// Return a static human-readable string for a SOCKET_ERROR_* error code +// Returns "unknown socket error" for unrecognized codes +const char *socketGetErrorString(int32_t err); + +// Create a TCP or UDP socket with the given SOCKET_MODE_xxx flags +// Sockets are always created in blocking mode, a timeout may be set with socketSetTimeout() +// SOCKET_MODE_TCP: TCP stream socket (default: UDP datagram) +// SOCKET_MODE_REUSEADDR: set SO_REUSEADDR to allow rapid port reuse after restart +// SOCKET_MODE_HW_TIMESTAMPING / SOCKET_MODE_SW_TIMESTAMPING: enable timestamps (Linux with hardware timestamps only) +// SOCKET_MODE_GET_IF_INFO: enable IP_PKTINFO to identify the receiving interface (Linux with hardware timestamps only) +// Returns true on success +bool socketOpen(SOCKET_HANDLE *socketp, uint16_t flags); + +// Bind socket to a local address and port +// addr: network-byte-order IPv4 address; NULL or 0.0.0.0 binds to INADDR_ANY +// Returns true on success +bool socketBind(SOCKET_HANDLE socket, const uint8_t *addr, uint16_t port); + +// Bind socket to a specific network interface by name (Linux only, requires root) +// Useful for multicast reception on a specific interface when bound to INADDR_ANY +// ifname: interface name, e.g. "eth0"; NULL or empty string is a no-op +// Returns true on success (returns true with a warning on non-Linux platforms) +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) +bool socketBindToDevice(SOCKET_HANDLE socket, const char *ifname); +#endif + +// Configure the NIC driver to generate hardware timestamps (Linux only, requires root) +// Must be called after socketBind; uses the interface name stored by socketBind/socketBindToDevice +// ptpOnly: true = timestamp PTP event packets only; false = timestamp all packets +// Falls back gracefully if the NIC does not support hardware timestamps +// Returns true on success +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) +bool socketEnableTimestamps(SOCKET_HANDLE socket, bool ptpOnly); +#endif + +#if !defined(OPTION_ENABLE_UDP_RAW) // not provided by the raw Ethernet transport + +// Join an IPv4 multicast group on a UDP socket +// maddr: multicast group address (network byte order) +// Interface selection priority: ifname > ifaddr > INADDR_ANY (kernel routing) +// Returns true on success +bool socketJoin(SOCKET_HANDLE socket, const uint8_t *maddr, const uint8_t *ifaddr, const char *ifname); + +// Start listening for incoming TCP connections +// Returns true on success +bool socketListen(SOCKET_HANDLE socket); + +// Accept an incoming TCP connection (blocking) +// addr: filled with the remote IPv4 address (network byte order) if non-NULL +// Returns a new connected SOCKET_HANDLE; the caller is responsible for closing it +SOCKET_HANDLE socketAccept(SOCKET_HANDLE socket, uint8_t *addr); + +// Receive from a TCP socket (blocking) +// waitAll: true = MSG_WAITALL, block until bufferSize bytes arrive +// Return values: > 0 bytes received +// == 0 timeout (set with socketSetRecvTimeout) — no data yet, do background work and loop +// < 0 socket closed (graceful or reset) or error — check with socketIsClosed(socketGetLastError()) and exit the receive loop +int16_t socketRecv(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, bool waitAll); + +#endif // !OPTION_ENABLE_UDP_RAW + +// Receive a UDP datagram (blocking) +// srcAddr / srcPort: filled with sender's address/port if non-NULL +// time: optional receive timestamp (NULL to skip); hardware or software depending on socket flags +// Return values: > 0 bytes received +// == 0 timeout (set with socketSetRecvTimeout) — no data yet, do background work and loop +// < 0 socket closed or error — check with socketIsClosed(socketGetLastError()) and exit the receive loop +int16_t socketRecvFrom(SOCKET_HANDLE socket, uint8_t *buffer, uint16_t bufferSize, uint8_t *srcAddr, uint16_t *srcPort, uint64_t *time); + +// Send a UDP datagram to addr:port +// time: optional send timestamp (NULL to skip) +// on Linux with HW timestamps: *time is set to 0; call socketGetSendTime() afterwards to retrieve it +// on other platforms: *time is set to the system clock at send time +// Returns: bytes sent, 0 on closed socket, -1 on error +int16_t socketSendTo(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize, const uint8_t *addr, uint16_t port, uint64_t *time); + +#if !defined(OPTION_ENABLE_UDP_RAW) // not provided by the raw Ethernet transport +// Send data on a TCP socket (blocking; loops internally on partial sends) +// Returns: bytes sent, 0 on closed socket, -1 on error +int16_t socketSend(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize); +#endif + +#if !defined(_WIN) && !defined(_FREE_RTOS) && !defined(OPTION_ENABLE_UDP_RAW) +// Send multiple buffers as a single UDP datagram (scatter-gather I/O via sendmsg, POSIX only) +// Returns: total bytes sent, 0 on closed socket, -1 on error (partial UDP sends treated as error) +int16_t socketSendToV(SOCKET_HANDLE socket, tQueueBuffer buffers[], uint16_t count, const uint8_t *addr, uint16_t port); + +// Send multiple buffers on a TCP socket (scatter-gather I/O via sendmsg, POSIX only) +// Loops internally until all data is accepted by the kernel +// Returns: total bytes sent, 0 on closed socket, -1 on error +int16_t socketSendV(SOCKET_HANDLE socket, tQueueBuffer buffers[], uint16_t count); +#endif + +// Retrieve TX hardware and/or software timestamp after socketSendTo (Linux only) +// Must be called shortly after socketSendTo returned *time==0 +// Requires OPTION_SOCKET_HW_TIMESTAMPS and socketEnableTimestamps() to have been called +// txHwTime / txSwTime: set to 0 if the respective timestamp is not available; NULL to skip +// Returns true if at least one requested timestamp was successfully retrieved +#if defined(_LINUX) && defined(OPTION_SOCKET_HW_TIMESTAMPS) +bool socketGetSendTime(SOCKET_HANDLE socket, uint64_t *txHwTime, uint64_t *txSwTime); +#endif + +#if defined(OPTION_ENABLE_UDP_RAW) +// Select the Ethernet interface used by the raw Ethernet transport +// config: backend specific and opaque, e.g. the interface name "eth0" for the Linux +// AF_PACKET backend. NULL restores the OPTION_UDP_RAW_IFNAME default. +// Must be called before XcpEthServerInit(); the string is not copied and must stay valid. +void socketRawSetInterface(const char *config); + +// Get the local MAC address of the raw Ethernet transport, as reported by its HAL +// mac: output buffer, must point to at least 6 bytes +// Returns true on success +bool socketRawGetLocalMac(SOCKET_HANDLE socket, uint8_t *mac); + +#if XCPTL_TX_HEADROOM > 0 +// Send a UDP datagram from a buffer which has writable headroom in front of it (zero copy). +// PRECONDITION: buffer must have XCPTL_TX_HEADROOM writable bytes immediately before it. +// The Ethernet/IPv4/UDP header is written there in place and the payload is not copied. +// Used for the DAQ transmit path, where the buffer is a transmit queue segment (see queue.h). +// Command responses are built on the stack, have no headroom, and use socketSendTo instead. +// Returns: bytes sent (the payload size), 0 on closed socket, -1 on error +int16_t socketSendToReserved(SOCKET_HANDLE socket, const uint8_t *buffer, uint16_t bufferSize, const uint8_t *addr, uint16_t port, uint64_t *time); +#endif +#endif + +// Set receive timeout on a blocking socket +// timeoutMs: timeout in milliseconds; 0 = restore infinite blocking +// With a timeout set, socketRecv/socketRecvFrom return 0 on expiry instead of blocking indefinitely, +// allowing the receive thread to perform background work before looping back +// Works for both TCP and UDP; use socketShutdown() to signal a receive thread to exit +// Returns true on success +bool socketSetTimeout(SOCKET_HANDLE socket, uint32_t timeoutMs); + +// Shut down both directions of the socket (SHUT_RDWR / SD_BOTH) +// Unblocks a thread currently blocked in socketRecv or socketRecvFrom, causing it to return -1 +bool socketShutdown(SOCKET_HANDLE socket); + +// Close the OS socket, free the SOCKET_HANDLE, and set *socketp to NULL +// Returns true on success +bool socketClose(SOCKET_HANDLE *socketp); + +#if !defined(OPTION_ENABLE_UDP_RAW) // the raw transport reads the MAC from its Ethernet HAL +// Get the MAC address of a network interface by name (e.g. "eth0") +// mac: output buffer, must point to at least 6 bytes +// Returns true on success +bool socketGetMAC(char *ifname, uint8_t *mac); +#endif + +#ifdef OPTION_ENABLE_GET_LOCAL_ADDR +// Get the IPv4 address and MAC of the first non-loopback Ethernet interface +// mac / addr: output buffers (6 / 4 bytes respectively); either may be NULL +// Result is cached after the first successful call +// Returns true on success +bool socketGetLocalAddr(uint8_t *mac, uint8_t *addr); +#endif + +#endif + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/src/util.c b/src/util.c index f9e24990..e1a4e756 100644 --- a/src/util.c +++ b/src/util.c @@ -24,7 +24,7 @@ #include // for sprintf, memset #include "dbg_print.h" // for DBG_PRINTF -#include "platform.h" +#include "platform.h" // for platform defines (WIN_, LINUX_, MACOS_) and specific implementation of sockets, clock, thread, mutex, spinlock /**************************************************************************/ // Simple pseudo random generator diff --git a/src/xcp_cfg.h b/src/xcp_cfg.h index 9f5e0745..e3136d30 100644 --- a/src/xcp_cfg.h +++ b/src/xcp_cfg.h @@ -252,7 +252,7 @@ XCPlite multi application absolute addressing: XCP_ADDRESS_MODE_XCPLITE__CXSDD ( #else #define XCP_ADDR_EPK 0xFFFFFF00 // Absolute EPK address -#define XcpAddrEncodeSegIndex(seg_index, offset) (0x80000000 + (((uint32_t)(seg_index)) << 16) + (offset)) +#define XcpAddrEncodeSegIndex(seg_index, offset) ((uint32_t)(0x80000000 + (((uint32_t)(seg_index)) << 16) + (offset))) #endif diff --git a/src/xcpappl.c b/src/xcpappl.c index b228c5d5..7cbd9c09 100644 --- a/src/xcpappl.c +++ b/src/xcpappl.c @@ -16,7 +16,8 @@ #include // for strncpy #include "dbg_print.h" // for DBG_PRINTF3, DBG_PRINT4, DBG_PRINTF4, DBG... -#include "platform.h" // for platform defines (WIN_, LINUX_, MACOS_) and specific implementation of sockets, clock, thread, mutex +#include "platform.h" // for clockGet, SNPRINTF, STRNLEN, platform defines +#include "sockets.h" // for socketGetLocalAddr #include "xcp.h" // for CRC_XXX #include "xcp_cfg.h" // for XCP_ENABLE_xxx #include "xcplib_cfg.h" // for OPTION_xxx @@ -558,6 +559,13 @@ static uint32_t openFile(const char *filename) { fseek(gXcpFile, 0, SEEK_END); gXcpFileLength = (uint32_t)ftell(gXcpFile); fseek(gXcpFile, 0, SEEK_SET); + // @@@@ TODO: This assert aborts the whole XCP server process when the file is empty, which a + // remote XCP client can trigger with GET_ID A2L upload (a previous crashed run leaves a 0 byte + // A2L behind, and it is then reproduced on every subsequent start). An empty or unreadable file + // is a normal runtime condition, not a programming error: return 0 here and let the caller + // report it. In a release build (NDEBUG) the assert disappears and exactly that already happens + // - the client then reports "A2L file not available, GET_ID 4 returned size 0" - so this is a + // debug-only abort and a silent Debug/Release behaviour difference. assert(gXcpFileLength > 0); DBG_PRINTF4("File %s ready for upload, size=%u\n", filename, gXcpFileLength); return gXcpFileLength; @@ -569,6 +577,13 @@ bool ApplXcpReadFile(uint8_t size, uint32_t addr, uint8_t *data) { DBG_PRINT_ERROR("File not open for reading!\n"); return false; } + // @@@@ TODO: Two robustness issues here, neither with a confirmed failure today - A2L upload + // itself is verified working (use: xcpclient --upload-a2l --a2l ): + // 1) the diagnostic lumps "offset out of range" and "short read" into one misleading text. + // A short read is reported as "exceeds file length", which sends debugging the wrong way. + // 2) the read is purely sequential and ignores addr: it relies on the FILE* position and + // closes the file at EOF, so any retry, re-set MTA or non sequential block transfer would + // fail. Seeking to addr before reading would make this robust regardless of client behaviour. if (addr + size > gXcpFileLength || size != fread(data, 1, (uint32_t)size, gXcpFile)) { closeFile(); DBG_PRINTF_ERROR("ApplXcpReadFile addr=%u size=%u exceeds file length=%u\n", addr, size, gXcpFileLength); diff --git a/src/xcpethserver.c b/src/xcpethserver.c index e5378806..7394d6a7 100644 --- a/src/xcpethserver.c +++ b/src/xcpethserver.c @@ -19,8 +19,9 @@ #include // for uintxx_t #include "dbg_print.h" // for DBG_LEVEL, DBG_PRINT3, DBG_PRINTF4, DBG... -#include "platform.h" // for platform defines (WIN_, LINUX_, MACOS_) and specific implementation of sockets, clock, thread, mutex +#include "platform.h" // for THREAD_HANDLE, create_thread, cancel_thread, sleepUs, clockGetMonotonicNs, ... #include "queue.h" // for tQueueHandle, queueInitFromMemory, ... +#include "sockets.h" // for socketStartup, socketCleanup #include "xcp.h" // for CRC_XXX #include "xcplib_cfg.h" // for OPTION_xxx, TEST_xxx #include "xcplite.h" // for tXcpDaqLists, XcpXxx, ApplXcpXxx, ... @@ -47,8 +48,8 @@ #endif #endif -#if !defined(OPTION_ENABLE_TCP) && !defined(OPTION_ENABLE_UDP) -#error "Please define OPTION_ENABLE_TCP or OPTION_ENABLE_UDP" +#if !defined(OPTION_ENABLE_TCP) && !defined(OPTION_ENABLE_UDP) && !defined(OPTION_ENABLE_UDP_RAW) +#error "Please define OPTION_ENABLE_TCP or OPTION_ENABLE_UDP or OPTION_ENABLE_UDP_RAW" #endif static THREAD_FUNC_RETURN XcpServerReceiveThread(void *par); @@ -483,6 +484,9 @@ THREAD_FUNC_RETURN XcpServerReceiveThread(void *par) { uint64_t now = clockGetMonotonicNs(); // Drive the current last time with XCPTL_RECV_TIMEOUT_MS cycle in this loop // Blocking, with timeout to allow handling background tasks in this thread as well + // @@@@ TODO: This terminates the receive thread on ANY false return, including a merely + // corrupt datagram (see the dlc check in xcpethtl.c). Distinguish "fatal socket error" + // from "bad packet, keep serving" so a malformed frame cannot kill the server. if (!XcpEthTlHandleCommands()) { DBG_PRINT_ERROR("XcpEthTlHandleCommands failed!\n"); break; // error -> terminate thread diff --git a/src/xcpethtl.c b/src/xcpethtl.c index 550e8201..0e0a7b50 100644 --- a/src/xcpethtl.c +++ b/src/xcpethtl.c @@ -18,9 +18,10 @@ #include // for uintxx_t #include // for memcpy, strcmp -#include "dbg_print.h" // for DBG_LEVEL, DBG_PRINT, ... -#include "platform.h" // for platform defines (WIN_, LINUX_, MACOS_) and specific implementation of sockets, clock, thread, mutex -#include "queue.h" +#include "dbg_print.h" // for DBG_LEVEL, DBG_PRINT, ... +#include "platform.h" // for MUTEX, THREAD_HANDLE, create_thread, sleepMs, clockGetMonotonicNs, ... +#include "queue.h" // for tQueueHandle, queueInitFromMemory, queuePush, queuePop, ... +#include "sockets.h" // for SOCKET_HANDLE, socketXxx #include "xcp.h" // for CRC_XXX #include "xcp_cfg.h" // for XCP_xxx #include "xcplib_cfg.h" // for OPTION_xxx @@ -126,13 +127,18 @@ static int handleXcpMulticastCommand(int n, tXcpCtoMessage *p, uint8_t *dstAddr, // Transmit a UDP datagram or TCP segment (contains multiple XCP DTO messages or a single CRM message (len+ctr+packet+fill)) // Must be thread safe, because it is called from CMD and from DAQ thread +// has_headroom: true if XCPTL_TX_HEADROOM writable bytes precede data, which lets the raw Ethernet +// transport write its header in place instead of copying the payload (zero copy). +// Only the transmit queue segments have that headroom, CRM messages are built on the +// stack and do not. Ignored unless XCPTL_TX_HEADROOM > 0. // Returns false on error -static bool XcpEthTlSend(const uint8_t *data, uint16_t size, const uint8_t *addr, uint16_t port) { +static bool XcpEthTlSend(const uint8_t *data, uint16_t size, const uint8_t *addr, uint16_t port, bool has_headroom) { int r; assert(size > 0 && size <= XCPTL_MAX_SEGMENT_SIZE); assert(data != NULL); + (void)has_headroom; // unused unless the zero copy transmit path is enabled DBG_PRINTF5("XcpEthTlSend: msg_len = %u\n", size); #ifdef TEST_ENABLE_DBG_METRICS @@ -153,7 +159,14 @@ static bool XcpEthTlSend(const uint8_t *data, uint16_t size, const uint8_t *addr DBG_PRINT_ERROR("XcpEthTlSend: invalid master address!\n"); return false; } - r = socketSendTo(gXcpTl.socket, data, size, gXcpTl.master_addr, gXcpTl.master_port, NULL); +#if XCPTL_TX_HEADROOM > 0 + if (has_headroom) { // zero copy: the transport writes its header into the headroom + r = socketSendToReserved(gXcpTl.socket, data, size, gXcpTl.master_addr, gXcpTl.master_port, NULL); + } else +#endif + { + r = socketSendTo(gXcpTl.socket, data, size, gXcpTl.master_addr, gXcpTl.master_port, NULL); + } } } #endif // UDP @@ -235,6 +248,8 @@ void XcpTlSendCrm(const uint8_t *data, uint8_t size) { mutexLock(&gXcpTl.ctr_mutex); // Build XCP CTO message (ctr+dlc+packet) + // Note: dlc is the exact packet size here, command responses are NOT padded to + // XCPTL_PACKET_ALIGNMENT, unlike DAQ messages built in queueAcquire - see the TODO there tXcpCtoMessage msg; // @@@@ STACK buffer for tXcpCtoMessage msg.dlc = size; msg.ctr = gXcpTl.ctr++; // Get next response packet counter @@ -247,7 +262,7 @@ void XcpTlSendCrm(const uint8_t *data, uint8_t size) { tQueueBuffer buf = {.buffer = (uint8_t *)&msg, .size = (uint16_t)(size + XCPTL_TRANSPORT_LAYER_HEADER_SIZE)}; XcpEthTlSendV(&buf, 1); #else - XcpEthTlSend((const uint8_t *)&msg, (uint16_t)(size + XCPTL_TRANSPORT_LAYER_HEADER_SIZE), NULL, 0); + XcpEthTlSend((const uint8_t *)&msg, (uint16_t)(size + XCPTL_TRANSPORT_LAYER_HEADER_SIZE), NULL, 0, false); // stack buffer, no headroom #endif mutexUnlock(&gXcpTl.ctr_mutex); @@ -267,7 +282,7 @@ void XcpEthTlSendMulticastCrm(const uint8_t *packet, uint16_t packet_size, const memcpy(msg.packet, packet, packet_size); // No error handling, loosing a CRM message will lead to a timeout in the XCP client - XcpEthTlSend((uint8_t *)&msg, (uint16_t)(packet_size + XCPTL_TRANSPORT_LAYER_HEADER_SIZE), addr, port); + XcpEthTlSend((uint8_t *)&msg, (uint16_t)(packet_size + XCPTL_TRANSPORT_LAYER_HEADER_SIZE), addr, port, false); // stack buffer, no headroom } #endif @@ -490,6 +505,11 @@ bool XcpEthTlHandleCommands(void) { #ifdef TEST_ENABLE_DBG_METRICS gXcpRxPacketCount++; #endif + // @@@@ TODO: A single malformed datagram terminates the XCP server receive thread. + // Returning false here makes XcpServerReceiveThread break out of its loop (xcpethserver.c), + // so any host on the network can permanently kill the XCP server with one packet. + // Reproduced on UDP and on the raw Ethernet transport. A corrupt datagram should be + // counted and dropped, and only a real socket error should terminate the thread. if (msgBuf.dlc != n - XCPTL_TRANSPORT_LAYER_HEADER_SIZE) { DBG_PRINT_ERROR("XcpEthTlHandleCommands: Corrupt message received!\n"); return false; // Error @@ -635,6 +655,15 @@ bool XcpEthTlInit(const uint8_t *addr, uint16_t port, bool useTCP, tQueueHandle DBG_PRINTF3(" Listening for XCP commands on UDP %u.%u.%u.%u port %u\n", bind_addr[0], bind_addr[1], bind_addr[2], bind_addr[3], port); } +#ifdef OPTION_ENABLE_UDP_RAW + // The raw Ethernet transport knows both values for certain: the application supplied + // the IP address (0.0.0.0 is rejected by socketBind) and the Ethernet HAL supplied the + // MAC. Fill them unconditionally so XcpEthTlGetInfo and the A2L IF_DATA report the real + // address instead of the 127.0.0.1 fallback, without needing OPTION_ENABLE_GET_LOCAL_ADDR. + memcpy(gXcpTl.server_addr, bind_addr, 4); + socketRawGetLocalMac(gXcpTl.socket, gXcpTl.server_mac); +#endif + #ifdef OPTION_ENABLE_GET_LOCAL_ADDR { uint8_t addr1[4] = {0, 0, 0, 0}; @@ -711,7 +740,7 @@ void XcpEthTlGetInfo(bool *isTcp, uint8_t *mac, uint8_t *addr, uint16_t *port) { if (isTcp != NULL) *isTcp = gXcpTl.server_use_tcp; -#ifdef OPTION_ENABLE_GET_LOCAL_ADDR +#if defined(OPTION_ENABLE_GET_LOCAL_ADDR) || defined(OPTION_ENABLE_UDP_RAW) if (addr != NULL) memcpy(addr, gXcpTl.server_addr, 4); if (mac != NULL) @@ -924,7 +953,8 @@ int32_t XcpTlHandleTransmitQueue(void) { break; // queue is empty, break inner loop and sleep a bit } else { // Send this frame (blocking) - bool r = XcpEthTlSend(b, l, NULL, 0); + // A transmit queue segment has QUEUE_SEGMENT_HEADER_SIZE headroom in front of it + bool r = XcpEthTlSend(b, l, NULL, 0, true); mutexUnlock(&gXcpTl.ctr_mutex); // Free this buffer diff --git a/src/xcplib_cfg.h b/src/xcplib_cfg.h index d330255d..931c3bd4 100644 --- a/src/xcplib_cfg.h +++ b/src/xcplib_cfg.h @@ -15,10 +15,12 @@ The values for XCP_xxx and XCPTL_xxx define constants (in xcp_cfg.h and xcptl_cfg.h) may depend on options */ -// XCPlite version, currently V2.1.x +// XCPlite version, currently V2.2.x +// Keep in sync with project(xcplite VERSION ...) in CMakeLists.txt, which is what +// find_package(xcplite) reports to a consuming project. #define OPTION_VERSION_MAJOR 2 -#define OPTION_VERSION_MINOR 1 -#define OPTION_VERSION_PATCH 10 +#define OPTION_VERSION_MINOR 2 +#define OPTION_VERSION_PATCH 1 // CANape version compatibility // Disable workarounds for CANape versions < 24SP2 @@ -73,7 +75,7 @@ #define OPTION_ENABLE_TCP #define OPTION_ENABLE_UDP -#define OPTION_MTU 8000 // IP MTU; jumbo frames supported +#define OPTION_MTU 1500 // IP MTU; jumbo frames support depend on path MTU in your network #define OPTION_SERVER_FORCEFULL_TERMINATION // Don't wait for the rx and tx thread to finish, just terminate them //------------------------------------------------------------------------------- @@ -152,7 +154,7 @@ // Transport layer queue, with variable queue entry size, 32 bit not lockless with mutex or critical_section synchronization // Mandatory for Windows and 32 bit platforms // #define OPTION_QUEUE_32 // (queue32.c for Windows or queue32m.c optimized for FreeRTOS) -#if defined(OPTION_ATOMIC_EMULATION) || defined(PLATFORM_32_BIT) +#if defined(OPTION_ATOMIC_EMULATION) || defined(PLATFORM_32BIT) #undef OPTION_QUEUE_64_VAR_SIZE #undef OPTION_QUEUE_64_FIX_SIZE #define OPTION_QUEUE_32 diff --git a/src/xcplib_no_a2l_cfg.h b/src/xcplib_no_a2l_cfg.h index 55ad0da4..497b7a87 100644 --- a/src/xcplib_no_a2l_cfg.h +++ b/src/xcplib_no_a2l_cfg.h @@ -38,7 +38,7 @@ // Default: Relative addressing mode (address extension 0 is segment relative addressing) // Option: Absolute addressing mode (address extension 0 is absolute addressing) -// #define OPTION_CAL_SEGMENTS_ABS +#define OPTION_CAL_SEGMENTS_ABS //------------------------------------------------------------------------------- // Events diff --git a/src/xcplib_raw_cfg.h b/src/xcplib_raw_cfg.h new file mode 100644 index 00000000..3d30fd62 --- /dev/null +++ b/src/xcplib_raw_cfg.h @@ -0,0 +1,104 @@ +#pragma once + +/*---------------------------------------------------------------------------- +| File: +| xcplib_raw_cfg.h +| +| Description: +| XCPlite configuration OVERRIDES for the raw Ethernet transport +| Applied AFTER the defaults in xcplib_cfg.h via: +| cmake: target_compile_definitions(xcplite PRIVATE "XCPLIB_CFG_OVERRIDE=\"xcplib_raw_cfg.h\"") +| +| XCP on UDP/IPv4 implemented inside xcplib (src/socket_raw.c) on top of a thin +| raw Ethernet HAL (src/socket_raw_hal.h), for targets which have no TCP/IP stack. +| This configuration is the Linux development and test vehicle for that transport, +| and the template for embedded ports (FreeRTOS/bare metal) or Vector XLAPI, ASAM CMP. +| +| Key differences in overrides from the defaults in xcplib_cfg.h: +| - OPTION_ENABLE_UDP_RAW instead of OPTION_ENABLE_UDP / OPTION_ENABLE_TCP +| - OPTION_QUEUE_32 is MANDATORY (the 64 bit queues use the vectored send path +| socketSendToV, which the raw transport does not implement) +| - Standard Ethernet MTU less encapsulation headroom, no jumbo frames +| (the raw transport does not fragment IPv4), see OPTION_MTU below +| Addressing scheme: +| Default (segment relative addressing on address extension 0) +| Platform requirements: +| Linux with CAP_NET_RAW (AF_PACKET). See docs/SOCKET_RAW.md for the test setup. +| Examples: +| udp_raw_demo +| Tests: +| test/test_socket_raw.sh + ----------------------------------------------------------------------------*/ + +//------------------------------------------------------------------------------- +// XCP server transport + +#undef OPTION_ENABLE_TCP +#undef OPTION_ENABLE_UDP +#define OPTION_ENABLE_UDP_RAW + +// This configuration uses the built in Linux AF_PACKET backend (src/socket_raw_hal_linux.c). +// A project which supplies its own Ethernet HAL out of tree - for example an ASAM CMP backend - +// defines OPTION_UDP_RAW_HAL_EXTERNAL in ITS OWN configuration override header instead, and links +// its implementation against libxcplite. See docs/SOCKET_RAW.md. +// #define OPTION_UDP_RAW_HAL_EXTERNAL + +// (1420 - 28) & ~7 = 1392 bytes max UDP payload, so the largest frame on the wire is +// 42 + 1392 = 1434 bytes and its IP packet is 1420 bytes. The raw transport does not fragment IPv4, so one segment must fit +// into one frame and an oversized frame can only be refused, never split. +// +// This is deliberately below the 1500 of a standard Ethernet link. The headroom is for a HAL +// backend which ENCAPSULATES the frame before putting it on the wire: an out of tree backend +// (OPTION_UDP_RAW_HAL_EXTERNAL) may add a header of its own, and at the full link MTU a +// segment of 1472 bytes already fills a 1500 byte path, leaving it nothing. Raise this to +// 1500 to use the full standard Ethernet MTU when the backend adds nothing. +#undef OPTION_MTU +#define OPTION_MTU 1420 + +//------------------------------------------------------------------------------- +// Transmit queue + +// MANDATORY for OPTION_ENABLE_UDP_RAW, enforced by a #error in xcptl_cfg.h. +// The default on 64 bit hosts would be OPTION_QUEUE_64_VAR_SIZE, whose transmit path +// uses socketSendToV (scatter-gather), which the raw transport does not provide. +#undef OPTION_QUEUE_64_VAR_SIZE +#undef OPTION_QUEUE_64_FIX_SIZE +#define OPTION_QUEUE_32 + +// Zero copy transmit: reserve XCPTL_TX_HEADROOM bytes in front of every transmit queue +// segment so the Ethernet/IPv4/UDP header can be written in place (see xcptl_cfg.h). +#define OPTION_UDP_RAW_ZERO_COPY + +//------------------------------------------------------------------------------- +// Raw Ethernet transport parameters +// Note: apart from OPTION_UDP_RAW_IFNAME these configure the shared UDP/IPv4/ARP/ICMP layer in +// socket_raw.c, not the Ethernet backend, so they apply whichever backend is selected. + +// Default network interface, used when the application does not select one. +// The udp_raw_demo overrides this with its --if command line option. +#define OPTION_UDP_RAW_IFNAME "eth0" + +// Answer ICMP Echo Requests (ping). +// Very useful during bring-up: a successful ping proves the Ethernet HAL, the MAC +// filter, the ARP reply, the IPv4 header build and the header checksum all work, +// before any XCP tooling is involved. +#define OPTION_UDP_RAW_ENABLE_ICMP_ECHO + +// UDP checksum on transmit - exactly one of the following: +// _ZERO write 0x0000. Legal for IPv4 (RFC 768) and costs nothing. Default. +// Note tcpdump/Wireshark then cannot validate the UDP framing - switch to +// _COMPUTE temporarily during bring-up if that check is wanted. +// _COMPUTE RFC 768 software checksum over pseudo header and payload +// _HW leave 0, the EMAC inserts it (STM32 ETH, ESP32 EMAC support this) +#define OPTION_UDP_RAW_UDP_CHECKSUM_ZERO +// #define OPTION_UDP_RAW_UDP_CHECKSUM_COMPUTE +// #define OPTION_UDP_RAW_UDP_CHECKSUM_HW + +// Verify IPv4/UDP checksums of received frames. +// On a switched link the Ethernet FCS already covers the wire, so this mostly catches +// our own parser bugs - which is exactly what is wanted during bring-up. +#define OPTION_UDP_RAW_VERIFY_RX_CHECKSUM + +// Send a gratuitous ARP announcement on bind, to prime switch MAC tables and the +// ARP cache of the XCP client. Not required: ARP Requests for our IP are always answered. +// #define OPTION_UDP_RAW_GRATUITOUS_ARP diff --git a/src/xcplib_rtos_cfg.h b/src/xcplib_rtos_cfg.h index 5ba320d5..af1e75ba 100644 --- a/src/xcplib_rtos_cfg.h +++ b/src/xcplib_rtos_cfg.h @@ -14,13 +14,13 @@ | - No jumbo frames, standard Ethernet MTU of 1500 bytes (1472 bytes UDP payload) | - No TCP support (not implemented yet for FreeRTOS) | - Reduced memory footprint -| - 32-bit DAQ queue -| - Clock resolution 1 µs +| - 32 bit DAQ queue +| - Clock resolution 1us | - No file system | - No on-target A2L generation | - No persistence, no A2L/ELF upload (no filesystem) | - No forceful thread termination (use vTaskDelete instead) -| - Reduced queue size, maximum event count, and calibration segment count to fit in embedded SRAM +| - Reduced queue size, and max event number and calibration segment counts to fit in embedded SRAM | | Optional: | OPTION_ENABLE_TCP not implemented yet for FreeRTOS @@ -28,7 +28,7 @@ | Addressing scheme: | Absolute memory addressing with A2L segments as absolute memory regions with static lifetime default page, no segment relative addressing | Platform requirements: -| No filesystem required, 32-bit platform, currently only FreeRTOS; ThreadX support is planned +| No filesystem required, 32 bit platform, currently only FreeRTOS, ThreadX planned to be supported in the future | Examples: | freertos_demo - FreeRTOS POSIX simulator (Linux only), for testing FreeRTOS xcplite support on the host | cmake: XCPLITE_CONFIGURATION=rtos, XCPLITE_BUILD_EXAMPLES=ON @@ -41,7 +41,7 @@ ----------------------------------------------------------------------------*/ -// FreeRTOS RX and TX task stack depth (in bytes) and priority +// FreeRTOS rx and tx task stack depth (in bytes) and priority // On the POSIX simulator the size must be considerably larger than usual // Tune these values to the actual needs of the XCP server tasks on your target #if defined(FREE_RTOS_POSIX_SIM) @@ -62,7 +62,7 @@ //------------------------------------------------------------------------------- // Clock -// FreeRTOS clock is assumed to have 1 µs ticks by default +// FreeRTOS clock is assumed to have 1us ticks by default // Adjust below if your clock has a different resolution, but be aware of the consequences regarding rounding errors and representation problems #undef OPTION_CLOCK_TICKS_1NS #define OPTION_CLOCK_TICKS_1US // Default for FreeRTOS @@ -79,17 +79,24 @@ //------------------------------------------------------------------------------- // XCP server #undef OPTION_ENABLE_TCP // TCP support stubs not implemented yet for FreeRTOS +// OPTION_ENABLE_UDP stays enabled: FreeRTOS targets use the lwIP socket API (OPTION_FREERTOS_LWIP above), +// the POSIX simulator uses host sockets. For targets without any IP stack, use XCPLITE_CONFIGURATION=raw +// (OPTION_ENABLE_UDP_RAW, hand-crafted UDP/IP over a raw Ethernet HAL) - see docs/SOCKET_RAW.md + #undef OPTION_MTU -#define OPTION_MTU 1500 // Standard Ethernet MTU +#define OPTION_MTU 1500 // Standard Ethernet MTU: (1500 - 28) & ~7 = 1472 bytes max UDP payload #undef OPTION_SERVER_FORCEFULL_TERMINATION // FreeRTOS uses vTaskDelete(NULL) to end tasks — no forceful termination //------------------------------------------------------------------------------- // Calibration -// Calibration segment management +// Calibration segment management and RCU is enabled in the default configuration +// We use that for FreeRTOS, it does not support a fully section registered approach yet +// Calibration segments are detected in XcpInit by their static descriptors and allocated from the calibration memory bump allocator // #undef OPTION_CAL_SEGMENTS -// Maximum calibration segment count and total memory size (each segment needs three copies of its data) +// Calibration segments max count and total memory size for the calibration memory bump allocator +// (each segment needs 3 copies of its data) #undef OPTION_CAL_SEGMENT_COUNT #define OPTION_CAL_SEGMENT_COUNT 8 #undef OPTION_CAL_MEM_SIZE @@ -99,7 +106,7 @@ #undef OPTION_ENABLE_PERSISTENCE // Absolute addressing (compatible with most A2L tools and xcpclient) -// Address extension 0 is absolute addressing (linker map / ELF address == XCP address) +// Address extension 0 is absolute addressing (linker map / elf address == 32 bit XCP address) // Calibration segments have absolute addresses, segment relative addressing is still available on address extension 1 #define OPTION_CAL_SEGMENTS_ABS @@ -121,23 +128,13 @@ #undef OPTION_QUEUE_64_VAR_SIZE #undef OPTION_QUEUE_64_FIX_SIZE #define OPTION_QUEUE_32 -// Number of statically allocated XCP transmit queue segments (minimum 2) -#ifndef OPTION_QUEUE_32_SEGMENT_COUNT -#define OPTION_QUEUE_32_SEGMENT_COUNT 16U -#endif -#if OPTION_QUEUE_32_SEGMENT_COUNT < 2U -#error "OPTION_QUEUE_32_SEGMENT_COUNT must be at least 2" -#endif -// The XcpEthServerInit queue size parameter is ignored for this fixed-size queue variant -#define OPTION_QUEUE_32_SIZE (OPTION_QUEUE_32_SEGMENT_COUNT * sizeof(tXcpSegmentBuffer)) -// Optional application-specific placement for the static queue state and buffer: -// #define OPTION_QUEUE_32_ATTRIBUTE __attribute__((section(".dtcm"))) -// #define OPTION_QUEUE_32_BUFFER_ATTRIBUTE __attribute__((section(".noncacheable"))) -// Use a critical section instead of a mutex; locked sequences are only a few instructions +// Fixed 4 KB for the queue buffer, parameter of XcpEthServerInit ignored, must be a multiple of sizeof(tXcpSegmentBuffer) +#define OPTION_QUEUE_32_SIZE (16 * sizeof(tXcpSegmentBuffer)) +// Use a crtical section instead of a mutex, locked sequences are only a few instructions #define OPTION_QUEUE32_CRITICAL_SECTION #undef OPTION_QUEUE32_MUTEX -// Create an asynchronous, cyclic DAQ event with event ID 0 for asynchronous data acquisition +// Create an asynchronous, cyclic DAQ event with event id 0 for asynchronous data acquisition // Global variables default to this event // Does not work with section registered events #undef OPTION_DAQ_ASYNC_EVENT diff --git a/src/xcplite.c b/src/xcplite.c index 183f6be7..5e63fc93 100644 --- a/src/xcplite.c +++ b/src/xcplite.c @@ -69,17 +69,17 @@ #include // for getpid() #endif -#include "dbg_print.h" // for DBG_LEVEL, DBG_PRINT3, DBG_PRINTF4, DBG... +#include "dbg_print.h" // for DBG_LEVEL, DBG_PRINT3, DBG_PRINTF4, DBG... #ifdef OPTION_ENABLE_PERSISTENCE #include "persistence.h" // for XcpBinFreezeCalSeg #endif -#include "platform.h" // for atomics -#include "queue.h" // for QueueXxx transport queue layer interface +#include "platform.h" // for atomics +#include "queue.h" // for QueueXxx transport queue layer interface #ifdef OPTION_SHM_MODE #include "shm.h" // for shared memory management, declares nothing outside SHM mode #endif -#include "xcp.h" // XCP protocol definitions -#include "xcptl.h" // for transport layer abstraction XcpTlWaitForTransmitQueueEmpty and XcpTlSendCrm +#include "xcp.h" // XCP protocol definitions +#include "xcptl.h" // for transport layer abstraction XcpTlWaitForTransmitQueueEmpty and XcpTlSendCrm #ifdef OPTION_CAL_SEGMENTS #include "cal.h" // for XcpCalSegXxx @@ -1015,7 +1015,7 @@ uint16_t XcpGetEventCount(void) { const tXcpEventDescriptor *begin = __start_xcp_evts; const tXcpEventDescriptor *end = __stop_xcp_evts; if (begin != NULL && end != NULL && begin < end) { - return (end - begin); + return ((uint16_t)(end - begin)); } else { return 0; } @@ -2621,6 +2621,7 @@ static uint8_t XcpAsyncCommand(bool async, const uint32_t *cmdBuf, uint8_t cmdLe check_error(XcpAddOdtEntry(CRO_WRITE_DAQ_ADDR, CRO_WRITE_DAQ_EXT, CRO_WRITE_DAQ_SIZE)); } break; +#if XCP_PROTOCOL_LAYER_VERSION >= 0x0101 case CC_WRITE_DAQ_MULTIPLE: { check_len(CRO_WRITE_DAQ_MULTIPLE_LEN(1)); uint8_t n = CRO_WRITE_DAQ_MULTIPLE_NODAQ; @@ -2629,7 +2630,7 @@ static uint8_t XcpAsyncCommand(bool async, const uint32_t *cmdBuf, uint8_t cmdLe check_error(XcpAddOdtEntry(CRO_WRITE_DAQ_MULTIPLE_ADDR(i), CRO_WRITE_DAQ_MULTIPLE_EXT(i), CRO_WRITE_DAQ_MULTIPLE_SIZE(i))); } } break; - +#endif case CC_START_STOP_DAQ_LIST: // start, stop, select individual daq list { check_len(CRO_START_STOP_DAQ_LIST_LEN); @@ -2851,14 +2852,13 @@ static uint8_t XcpAsyncCommand(bool async, const uint32_t *cmdBuf, uint8_t cmdLe CRM_GET_DAQ_CLOCK_SYNCH_STATE = ApplXcpGetClockState(); #endif if (CRM_LEN > XCPTL_MAX_CTO_SIZE) - error(CRC_CMD_UNKNOWN); // Extended mode needs enough CTO size - } else -#endif // >= 0x0103 - { // Legacy format + error(CRC_CMD_UNKNOWN); // Extended mode needs enough CTO size + } else { // Legacy format CRM_GET_DAQ_CLOCK_PAYLOAD_FMT = DAQ_CLOCK_PAYLOAD_FMT_SLV_32; // FMT_XCP_SLV = size of timestamp is DWORD CRM_LEN = CRM_GET_DAQ_CLOCK_LEN; CRM_GET_DAQ_CLOCK_TIME = (uint32_t)ApplXcpGetClock64(); } +#endif // >= 0x0103 } break; #if XCP_PROTOCOL_LAYER_VERSION >= 0x0104 @@ -3373,7 +3373,6 @@ void XcpStart(tQueueHandle queue_handle, bool resumeMode) { local_mut.clock_info.server.nativeTimestampSize = 4; // NATIVE_TIMESTAMP_SIZE_LONG; local_mut.clock_info.server.valueBeforeWrapAround = 0xFFFFFFFFULL; #endif -#endif // XCP_PROTOCOL_LAYER_VERSION >= 0x0103 #ifdef XCP_ENABLE_PTP // Default UUID of the XCP server clock @@ -3406,6 +3405,7 @@ void XcpStart(tQueueHandle queue_handle, bool resumeMode) { local.clock_info.server.UUID[6], local.clock_info.server.UUID[7]); #endif // PTP +#endif // XCP_PROTOCOL_LAYER_VERSION >= 0x0103 #endif // XCP_ENABLE_PROTOCOL_LAYER_ETH DBG_PRINT3("Start XCP protocol layer\n"); @@ -3614,12 +3614,14 @@ static void XcpPrintCmd(const tXcpCto *cmdBuf) { printf(" SHORT_UPLOAD addr=%08Xh, addrext=%02Xh, size=%u\n", CRO_SHORT_UPLOAD_ADDR, CRO_SHORT_UPLOAD_EXT, CRO_SHORT_UPLOAD_SIZE); } break; +#if XCP_PROTOCOL_LAYER_VERSION >= 0x0101 case CC_WRITE_DAQ_MULTIPLE: { printf(" WRITE_DAQ_MULTIPLE count=%u\n", CRO_WRITE_DAQ_MULTIPLE_NODAQ); for (int i = 0; i < CRO_WRITE_DAQ_MULTIPLE_NODAQ; i++) { printf(" %u: size=%u,addr=%08Xh,%02Xh\n", i, CRO_WRITE_DAQ_MULTIPLE_SIZE(i), CRO_WRITE_DAQ_MULTIPLE_ADDR(i), CRO_WRITE_DAQ_MULTIPLE_EXT(i)); } } break; +#endif #if XCP_PROTOCOL_LAYER_VERSION >= 0x0103 case CC_TIME_CORRELATION_PROPERTIES: diff --git a/src/xcplite.h b/src/xcplite.h index f0be90c3..9beca245 100644 --- a/src/xcplite.h +++ b/src/xcplite.h @@ -149,10 +149,18 @@ extern const tXcpEventDescriptor __stop_xcp_evts[] __attribute__((weak)); // development-only target and a build with zero events is a non-functional configuration. extern const tXcpEventDescriptor __start_xcp_evts[] __asm("section$start$__DATA$xcp_evts"); extern const tXcpEventDescriptor __stop_xcp_evts[] __asm("section$end$__DATA$xcp_evts"); +#elif defined(_MSC_VER) +// MSVC/COFF has no reliable linker-synthesized section boundary symbols (unlike ELF/Mach-O: in practice +// link.exe does not pack '$'-subsection contributions from different object files contiguously/predictably). +// Event pre-registration via section scanning is therefore not used on MSVC; DaqCreateEvent() and the +// DaqTriggerEvent family instead resolve/create events directly via XcpCreateEvent() at each call site +// (idempotent by name), see the XCP_EVENT_SECTION_SET_ID '_MSC_VER' branch below. Setting these to NULL +// makes XcpRegisterSectionEvents() at XcpInit() gracefully find nothing, exactly like an ELF/Mach-O build +// with zero section-registered events. +#define __start_xcp_evts ((const tXcpEventDescriptor *)NULL) +#define __stop_xcp_evts ((const tXcpEventDescriptor *)NULL) #else -#ifndef _WIN32 -#error "Unsupported platform for event segment registration" -#endif +#error "Unsupported platform for section based event pre-registration" #endif #endif // __XCPLIB_H__ diff --git a/src/xcptl_cfg.h b/src/xcptl_cfg.h index 79f50b51..56bd8e4f 100644 --- a/src/xcptl_cfg.h +++ b/src/xcptl_cfg.h @@ -13,13 +13,24 @@ #include "xcplib_cfg.h" // for OPTION_xxx -#if defined(OPTION_ENABLE_UDP) +#if defined(OPTION_ENABLE_UDP) || defined(OPTION_ENABLE_UDP_RAW) #define XCPTL_ENABLE_UDP #endif #if defined(OPTION_ENABLE_TCP) #define XCPTL_ENABLE_TCP #endif +// Raw Ethernet transport (OPTION_ENABLE_UDP_RAW) restrictions - see docs/SOCKET_RAW.md +#if defined(OPTION_ENABLE_UDP_RAW) && (defined(OPTION_ENABLE_UDP) || defined(OPTION_ENABLE_TCP)) +#error "OPTION_ENABLE_UDP_RAW is mutually exclusive with OPTION_ENABLE_UDP / OPTION_ENABLE_TCP" +#endif +#if defined(OPTION_ENABLE_UDP_RAW) && !defined(OPTION_QUEUE_32) +#error "OPTION_ENABLE_UDP_RAW requires OPTION_QUEUE_32: the 64 bit queues use the vectored send path (socketSendToV), which the raw transport does not implement" +#endif +#if defined(OPTION_ENABLE_UDP_RAW) && defined(OPTION_SHM_MODE) +#error "OPTION_ENABLE_UDP_RAW is not supported in SHM mode (queueInitFromMemory is implemented for the 64 bit queues only)" +#endif + // Transport layer version #define XCP_TRANSPORT_LAYER_VERSION 0x0104 @@ -47,11 +58,56 @@ #define XCPTL_MAX_SEGMENT_SIZE (1500 - 20 - 8) #endif +// Note on OPTION_MTU: +// OPTION_MTU is the link MTU, the Ethernet header is NOT part of it. +// XCPTL_MAX_SEGMENT_SIZE = (OPTION_MTU - 28) & ~7 reserves 28 bytes for the IPv4 and UDP headers +// and then aligns down as the transport layer requires, so the resulting IP packet is at most +// OPTION_MTU bytes, and exactly OPTION_MTU when OPTION_MTU - 28 is already a multiple of 8: +// 1500 -> segment 1472 -> IP packet 1500. +// The invariant is therefore: OPTION_MTU <= link MTU. +// Before V2.1.11 OPTION_MTU was the link MTU rounded UP to a multiple of 8 (1504 for a 1500 byte +// link) and the invariant was OPTION_MTU <= link MTU + 4. +// An OPTION_MTU too large for the link is NOT caught at compile time - the link MTU is a runtime +// property that only the target knows. It is reported at runtime instead: +// - socket transport: socketOpen sets DF on Linux, macOS/BSD, QNX and Windows, so sendto fails +// with EMSGSIZE and socketSendTo names the segment size and the OPTION_MTU +// - raw transport: eth_hal_send reports ETH_HAL_ERROR_SIZE +// Neither of those two fragments IPv4. +// +// lwIP is the exception: its socketOpen (the separate FreeRTOS implementation in sockets.c) sets +// no DF option, because lwIP has no IP_DONTFRAG, so an oversized datagram is not refused - lwIP +// fragments or drops it according to its own IP_FRAG build setting. socketSendTo therefore compares +// the segment against netif_default->mtu itself and warns once, but it still hands the datagram to +// lwIP: the check is a diagnostic, not a guard. On lwIP, OPTION_MTU has to be right. + // Receive timeout in milliseconds (rate of periodic checks for shutdown and background tasks in the receive thread) #define XCPTL_RECV_TIMEOUT_MS 100 -// Alignment for packet concatenation -#define XCPTL_PACKET_ALIGNMENT 4 // Packet alignment for multiple XCP transport layer packets in a XCP transport layer message +// Size granularity of the protocol layer packet inside a transport layer message +// A message is: WORD len + WORD ctr + protocol layer packet + fill. +// The packet size is rounded up to this alignment (that is the "fill"), so that the messages +// concatenated into a segment all start aligned - the message header (len+ctr) is word accessed. +// Also used as QUEUE_PAYLOAD_SIZE_ALIGNMENT by all queue variants, see queue.h. +// Only 4 is supported and queue.h enforces that with an #error - do not change this value. +#define XCPTL_PACKET_ALIGNMENT 4 + +// Transmit headroom: space reserved in front of a complete transmit segment, so the transport can +// prepend its link headers in place instead of copying the payload into a separate frame buffer. +// Used by the raw Ethernet transport for the 42 byte Ethernet + IPv4 + UDP header. +// 48 instead of 42 keeps the segment payload 8 byte aligned; the header is written right justified +// at (segment - 42), which also lands the IPv4 header on a 4 byte boundary. +#if defined(OPTION_ENABLE_UDP_RAW) && defined(OPTION_UDP_RAW_ZERO_COPY) +#define XCPTL_TX_HEADROOM 48 +#else +#define XCPTL_TX_HEADROOM 0 +#endif + +// Only the segment accumulating queues (queue32.c, queue32m.c) reserve segment headroom. +// This cannot be violated today because OPTION_ENABLE_UDP_RAW already requires OPTION_QUEUE_32, +// but check it explicitly so a future transport cannot silently lose the reservation. +#if (XCPTL_TX_HEADROOM > 0) && !defined(OPTION_QUEUE_32) +#error "XCPTL_TX_HEADROOM requires OPTION_QUEUE_32: only the segment accumulating queues reserve segment headroom" +#endif // Transport layer message header size // This is fixed, no other options supported yet diff --git a/test/daq_test/README.md b/test/daq_test/README.md index 3923446f..2860c611 100644 --- a/test/daq_test/README.md +++ b/test/daq_test/README.md @@ -1,12 +1,22 @@ # Daq test +Use CANape or + +```bash +xcpclient --udp --test +xcpclient --udp --upload-a2l --mea . +``` + +Test results (producer acquire lock time histogram) shown on termination (ctrl-c): + + ``` // Test parameters #define THREAD_COUNT 8 // Number of threads to create -#define THREAD_DELAY_US 1000 // Default delay in microseconds for the thread loops, calibration parameter +#define THREAD_DELAY_US 500 // Default delay in microseconds for the thread loops, calibration parameter #define THREAD_DELAY_OFFSET_US 50 // Default offset added to the delay (* task index) for each thread instance, to create different sampling rates #define THREAD_TIME_SHIFT_NS \ (1000000000 / THREAD_COUNT) // Default time shift in nanoseconds (* task index) for each thread instance, to disturb the sequential time ordering of events @@ -95,4 +105,38 @@ Lock time histogram (418221 events): >320000ns 0 0.00% + +Apple silicon: 13816 event/s, 0.263 Mbyte/s +Producer acquire lock time statistics: + count=337241 max=2565102ns avg=77ns (cal=23ns) + +Lock time histogram (337241 events): + Range Count % Bar + -------------------- ---------- ------- ------------------------------ + 0-10ns 30248 8.97% ##### + 10-20ns 49729 14.75% ######### + 20-40ns 0 0.00% + 40-80ns 160010 47.45% ############################## + 80-120ns 79789 23.66% ############## + 120-160ns 10323 3.06% # + 160-200ns 1906 0.57% + 200-300ns 1634 0.48% + 300-400ns 1575 0.47% + 400-500ns 503 0.15% + 500-600ns 262 0.08% + 600-700ns 318 0.09% + 700-800ns 132 0.04% + 800-900ns 105 0.03% + 900-1000ns 99 0.03% + 1000-1250ns 402 0.12% + 1250-1500ns 29 0.01% + 1500-1750ns 21 0.01% + 1750-2000ns 31 0.01% + 2000-3000ns 53 0.02% + 3000-4000ns 21 0.01% + 4000-5000ns 10 0.00% + 5000-7500ns 9 0.00% + >7500ns 32 0.01% + + ``` \ No newline at end of file diff --git a/test/daq_test/src/main.c b/test/daq_test/src/main.c index 1982fba5..694fc7f3 100644 --- a/test/daq_test/src/main.c +++ b/test/daq_test/src/main.c @@ -27,9 +27,9 @@ //----------------------------------------------------------------------------------------------------- // Test configuration -#define THREAD_COUNT 8 // Number of threads to create -#define THREAD_DELAY_US 1000 // Default delay in microseconds for the thread loops, calibration parameter -#define THREAD_DELAY_OFFSET_US 100 // Default offset added to the delay (* task index) for each thread instance, to create different sampling rates +#define THREAD_COUNT 8 // Number of threads to create +#define THREAD_DELAY_US 500 // Default delay in microseconds for the thread loops, calibration parameter +#define THREAD_DELAY_OFFSET_US 50 // Default offset added to the delay (* task index) for each thread instance, to create different sampling rates #define THREAD_TIME_SHIFT_US \ (500000 / THREAD_COUNT) // Default time shift in microseconds (* task index) for each thread instance, to disturb the sequential time ordering of events diff --git a/test/queue_test/src/main.c b/test/queue_test/src/main.c index e67682ba..dd656414 100644 --- a/test/queue_test/src/main.c +++ b/test/queue_test/src/main.c @@ -22,10 +22,7 @@ // Public XCPlite API #include "xcplib_cfg.h" // for OPTION_xxx -// Disable socket support with vectored IO to avoid platform.h includes queue.h -#undef OPTION_ENABLE_TCP -#undef OPTION_ENABLE_UDP -#include "platform.h" +#include "platform.h" // for THREAD_HANDLE, MUTEX, THREAD_FUNC_RETURN, create_thread, cancel_thread, sleepUs, clockGetMonotonicNs, ... // Option XCP server for online performance monitoring and logging of the queue test #ifdef USE_XCP diff --git a/test/socket_raw_test/src/main.c b/test/socket_raw_test/src/main.c new file mode 100644 index 00000000..6cb33247 --- /dev/null +++ b/test/socket_raw_test/src/main.c @@ -0,0 +1,462 @@ +// socket_raw_test - unit tests for the raw Ethernet transport (OPTION_ENABLE_UDP_RAW) +// +// Covers the parts of src/socket_raw.c that are pure logic and can be tested without a +// network: IPv4/ICMP checksums, wire struct packing, the Ethernet/IPv4/UDP frame build, +// the receive filter, and the ARP and ICMP Echo responders. +// +// socket_raw.c is included directly so the test can reach its static helpers and drive +// the socket context without a HAL. src/stubs.c provides a fake Ethernet HAL which +// captures the transmitted frame instead of sending it. + +#include // for offsetof +#include +#include + +#include "socket_raw.c" + +static int fails = 0; + +#define CHECK(what, cond) \ + do { \ + printf("%-52s %s\n", (what), (cond) ? "OK" : "FAIL"); \ + if (!(cond)) \ + fails++; \ + } while (0) + +static void expect16(const char *what, uint16_t got, uint16_t want) { + printf("%-52s got=0x%04X want=0x%04X %s\n", what, got, want, got == want ? "OK" : "FAIL"); + if (got != want) + fails++; +} + +// Buffers are sized from the configuration, not hardcoded: OPTION_MTU may select jumbo frames +#define TEST_BUF_SIZE (RAW_MAX_FRAME + 64) + +// Fake HAL capture, filled by eth_hal_send() in src/stubs.c +uint8_t gTxFrame[TEST_BUF_SIZE]; +uint16_t gTxLen; +int gTxCount; + +//----------------------------------------------------------------------------------------------------- +// Test fixture + +static const uint8_t LOCAL_MAC[6] = {0x02, 0x11, 0x22, 0x33, 0x44, 0x55}; +static const uint8_t PEER_MAC[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; +static const uint8_t LOCAL_IP[4] = {192, 168, 90, 2}; +static const uint8_t PEER_IP[4] = {192, 168, 90, 1}; +static const uint8_t BCAST_MAC[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +#define LOCAL_PORT 5555 +#define PEER_PORT 50000 + +static void setupSocket(void) { + memset(&sSocketRaw, 0, sizeof(sSocketRaw)); + memcpy(sSocketRaw.local_mac, LOCAL_MAC, 6); + memcpy(sSocketRaw.local_ip, LOCAL_IP, 4); + sSocketRaw.local_port = LOCAL_PORT; + sSocketRaw.is_open = true; + sSocketRaw.is_bound = true; + gTxCount = 0; + gTxLen = 0; +} + +// Build an incoming UDP frame +static uint16_t buildRxUdp(uint8_t *f, const uint8_t *dst_mac, const uint8_t *dst_ip, uint16_t dst_port, const uint8_t *payload, uint16_t plen, uint16_t frag) { + tEthHdr *eth = (tEthHdr *)f; + memcpy(eth->dst, dst_mac, 6); + memcpy(eth->src, PEER_MAC, 6); + eth->ethertype = BE16(ETHERTYPE_IPV4); + tIp4Hdr *ip = (tIp4Hdr *)(f + ETH_HDR_LEN); + memset(ip, 0, IP4_HDR_LEN); + ip->ver_ihl = 0x45; + ip->total_length = BE16((uint16_t)(IP4_HDR_LEN + UDP_HDR_LEN + plen)); + ip->flags_frag = BE16(frag); + ip->ttl = 64; + ip->protocol = IP_PROTO_UDP; + memcpy(ip->src, PEER_IP, 4); + memcpy(ip->dst, dst_ip, 4); + ip->checksum = BE16(ipHeaderChecksum(ip)); + tUdpHdr *udp = (tUdpHdr *)(f + ETH_HDR_LEN + IP4_HDR_LEN); + udp->src_port = BE16(PEER_PORT); + udp->dst_port = BE16(dst_port); + udp->length = BE16((uint16_t)(UDP_HDR_LEN + plen)); + udp->checksum = 0; + memcpy(f + RAW_HDR_LEN, payload, plen); + return (uint16_t)(RAW_HDR_LEN + plen); +} + +static uint16_t buildArpRequest(uint8_t *f, const uint8_t *target_ip, uint16_t oper) { + tEthHdr *e = (tEthHdr *)f; + memcpy(e->dst, BCAST_MAC, 6); + memcpy(e->src, PEER_MAC, 6); + e->ethertype = BE16(ETHERTYPE_ARP); + tArpHdr *a = (tArpHdr *)(f + ETH_HDR_LEN); + a->htype = BE16(1); + a->ptype = BE16(ETHERTYPE_IPV4); + a->hlen = 6; + a->plen = 4; + a->oper = BE16(oper); + memcpy(a->sha, PEER_MAC, 6); + memcpy(a->spa, PEER_IP, 4); + memset(a->tha, 0, 6); + memcpy(a->tpa, target_ip, 4); + return ETH_HDR_LEN + ARP_LEN; +} + +static uint16_t buildIcmpEcho(uint8_t *f, uint16_t data_len) { + tEthHdr *e = (tEthHdr *)f; + memcpy(e->dst, LOCAL_MAC, 6); + memcpy(e->src, PEER_MAC, 6); + e->ethertype = BE16(ETHERTYPE_IPV4); + uint16_t icmp_len = (uint16_t)(sizeof(tIcmpHdr) + 4 + data_len); // header + id/seq + data + tIp4Hdr *ip = (tIp4Hdr *)(f + ETH_HDR_LEN); + memset(ip, 0, IP4_HDR_LEN); + ip->ver_ihl = 0x45; + ip->total_length = BE16((uint16_t)(IP4_HDR_LEN + icmp_len)); + ip->flags_frag = BE16(0x4000); + ip->ttl = 64; + ip->protocol = IP_PROTO_ICMP; + memcpy(ip->src, PEER_IP, 4); + memcpy(ip->dst, LOCAL_IP, 4); + ip->checksum = BE16(ipHeaderChecksum(ip)); + uint8_t *icmp = f + ETH_HDR_LEN + IP4_HDR_LEN; + memset(icmp, 0, icmp_len); + icmp[0] = ICMP_TYPE_ECHO_REQUEST; + icmp[4] = 0x12; + icmp[5] = 0x34; + icmp[7] = 0x01; // id, seq + for (uint16_t i = 0; i < data_len; i++) + icmp[8 + i] = (uint8_t)(i & 0xFF); + ((tIcmpHdr *)icmp)->checksum = BE16(checksum16(icmp, icmp_len, 0)); + return (uint16_t)(ETH_HDR_LEN + IP4_HDR_LEN + icmp_len); +} + +//----------------------------------------------------------------------------------------------------- +// Checksums and wire layout + +static void test_checksums(void) { + + // Reference IPv4 header (RFC 1071 worked example), checksum field zeroed: + // 4500 0073 0000 4000 4011 0000 c0a8 0001 c0a8 00c7 -> 0xb861 + uint8_t hdr[20] = {0x45, 0x00, 0x00, 0x73, 0x00, 0x00, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x01, 0xc0, 0xa8, 0x00, 0xc7}; + expect16("IPv4 header checksum (reference vector)", checksum16(hdr, 20, 0), 0xb861); + + uint16_t c = checksum16(hdr, 20, 0); + hdr[10] = (uint8_t)(c >> 8); + hdr[11] = (uint8_t)(c & 0xFF); + expect16("verify: sum over header incl. checksum", checksum16(hdr, 20, 0), 0x0000); + + // Same header via the struct path used by socketSendTo: checks BE16 and packing + tIp4Hdr ip; + memset(&ip, 0, sizeof(ip)); + ip.ver_ihl = 0x45; + ip.total_length = BE16(0x0073); + ip.flags_frag = BE16(0x4000); + ip.ttl = 0x40; + ip.protocol = 17; + ip.src[0] = 192; + ip.src[1] = 168; + ip.src[2] = 0; + ip.src[3] = 1; + ip.dst[0] = 192; + ip.dst[1] = 168; + ip.dst[2] = 0; + ip.dst[3] = 199; + expect16("struct path: same header via tIp4Hdr", ipHeaderChecksum(&ip), 0xb861); + ip.checksum = BE16(ipHeaderChecksum(&ip)); + expect16("struct path: verify sums to 0", checksum16((const uint8_t *)&ip, 20, 0), 0x0000); + + uint8_t odd[3] = {0x12, 0x34, 0x56}; + expect16("odd length: trailing byte zero padded", checksum16(odd, 3, 0), (uint16_t)~(0x1234 + 0x5600)); + + CHECK("wire struct packing (14/20/8/28/4)", sizeof(tEthHdr) == 14 && sizeof(tIp4Hdr) == 20 && sizeof(tUdpHdr) == 8 && sizeof(tArpHdr) == 28 && sizeof(tIcmpHdr) == 4); + CHECK("RAW_HDR_LEN is 42", RAW_HDR_LEN == 42); + // MTU independent: the frame is the 42 byte header plus one full segment. + // With the default OPTION_MTU of 1504 that is 1514 bytes, but jumbo configurations are valid. + CHECK("max frame == 42 + max segment", RAW_MAX_FRAME == RAW_HDR_LEN + XCPTL_MAX_SEGMENT_SIZE); + CHECK("max frame == OPTION_MTU + 10", RAW_MAX_FRAME == OPTION_MTU + 10); +} + +//----------------------------------------------------------------------------------------------------- +// Frame build and receive filter + +static void test_frames(void) { + + static uint8_t rx[TEST_BUF_SIZE], out[TEST_BUF_SIZE]; + uint8_t srcAddr[4]; + uint16_t srcPort; + const uint8_t payload[] = {0x02, 0x00, 0x00, 0x00, 0xFF, 0x00}; // XCP CONNECT message + uint16_t n; + int16_t r; + + setupSocket(); + n = buildRxUdp(rx, LOCAL_MAC, LOCAL_IP, LOCAL_PORT, payload, sizeof(payload), 0x4000); + r = handleFrame(rx, n, out, sizeof(out), srcAddr, &srcPort); + CHECK("RX: accepted, payload length", r == (int16_t)sizeof(payload)); + CHECK("RX: payload content", memcmp(out, payload, sizeof(payload)) == 0); + CHECK("RX: source address extracted", memcmp(srcAddr, PEER_IP, 4) == 0); + CHECK("RX: source port extracted", srcPort == PEER_PORT); + CHECK("RX: peer MAC learned", sSocketRaw.peer_mac_valid && !memcmp(sSocketRaw.peer_mac, PEER_MAC, 6)); + + setupSocket(); + n = buildRxUdp(rx, LOCAL_MAC, LOCAL_IP, 9999, payload, sizeof(payload), 0x4000); + CHECK("RX: wrong UDP port dropped", handleFrame(rx, n, out, sizeof(out), srcAddr, &srcPort) == 0); + CHECK("RX: wrong port does not learn the peer", !sSocketRaw.peer_mac_valid); + + setupSocket(); + const uint8_t other_mac[6] = {0x02, 0x99, 0x99, 0x99, 0x99, 0x99}; + n = buildRxUdp(rx, other_mac, LOCAL_IP, LOCAL_PORT, payload, sizeof(payload), 0x4000); + CHECK("RX: foreign destination MAC dropped", handleFrame(rx, n, out, sizeof(out), srcAddr, &srcPort) == 0); + + setupSocket(); + const uint8_t other_ip[4] = {192, 168, 90, 77}; + n = buildRxUdp(rx, LOCAL_MAC, other_ip, LOCAL_PORT, payload, sizeof(payload), 0x4000); + CHECK("RX: foreign destination IP dropped", handleFrame(rx, n, out, sizeof(out), srcAddr, &srcPort) == 0); + + setupSocket(); // MF set = fragment + n = buildRxUdp(rx, LOCAL_MAC, LOCAL_IP, LOCAL_PORT, payload, sizeof(payload), 0x2000); + CHECK("RX: IPv4 fragment dropped", handleFrame(rx, n, out, sizeof(out), srcAddr, &srcPort) == 0); + + setupSocket(); // must be dropped, never truncated + static uint8_t big[600]; + memset(big, 0xA5, sizeof(big)); + n = buildRxUdp(rx, LOCAL_MAC, LOCAL_IP, LOCAL_PORT, big, sizeof(big), 0x4000); + CHECK("RX: oversized payload dropped, not truncated", handleFrame(rx, n, out, 64, srcAddr, &srcPort) == 0); + + setupSocket(); + n = buildRxUdp(rx, LOCAL_MAC, LOCAL_IP, LOCAL_PORT, payload, sizeof(payload), 0x4000); + rx[ETH_HDR_LEN + 10] ^= 0xFF; // corrupt the IPv4 header checksum + CHECK("RX: bad IPv4 header checksum dropped", handleFrame(rx, n, out, sizeof(out), srcAddr, &srcPort) == 0); + + setupSocket(); + n = buildRxUdp(rx, LOCAL_MAC, LOCAL_IP, LOCAL_PORT, payload, sizeof(payload), 0x4000); + ((tEthHdr *)rx)->ethertype = BE16(ETHERTYPE_VLAN); + CHECK("RX: VLAN tagged frame dropped", handleFrame(rx, n, out, sizeof(out), srcAddr, &srcPort) == 0); + + // Transmit + setupSocket(); + memcpy(sSocketRaw.peer_mac, PEER_MAC, 6); + sSocketRaw.peer_mac_valid = true; + int16_t sent = socketSendTo(&sSocketRaw, payload, sizeof(payload), PEER_IP, PEER_PORT, NULL); + CHECK("TX: returns the PAYLOAD size, not the frame size", sent == (int16_t)sizeof(payload)); + CHECK("TX: one frame handed to the HAL", gTxCount == 1); + CHECK("TX: frame length is 42 + payload", gTxLen == RAW_HDR_LEN + sizeof(payload)); + tEthHdr *te = (tEthHdr *)gTxFrame; + CHECK("TX: destination MAC is the learned peer", memcmp(te->dst, PEER_MAC, 6) == 0); + CHECK("TX: source MAC is ours", memcmp(te->src, LOCAL_MAC, 6) == 0); + CHECK("TX: ethertype IPv4", BE16(te->ethertype) == ETHERTYPE_IPV4); + tIp4Hdr *ti = (tIp4Hdr *)(gTxFrame + ETH_HDR_LEN); + CHECK("TX: IPv4 header checksum valid", checksum16((uint8_t *)ti, IP4_HDR_LEN, 0) == 0); + CHECK("TX: DF set, no fragmentation", (BE16(ti->flags_frag) & 0x3FFF) == 0 && (BE16(ti->flags_frag) & 0x4000) != 0); + CHECK("TX: IPv4 total length field", BE16(ti->total_length) == IP4_HDR_LEN + UDP_HDR_LEN + sizeof(payload)); + CHECK("TX: protocol UDP, ttl 64", ti->protocol == IP_PROTO_UDP && ti->ttl == 64); + tUdpHdr *tu = (tUdpHdr *)(gTxFrame + ETH_HDR_LEN + IP4_HDR_LEN); + CHECK("TX: UDP ports", BE16(tu->src_port) == LOCAL_PORT && BE16(tu->dst_port) == PEER_PORT); + CHECK("TX: UDP length field", BE16(tu->length) == UDP_HDR_LEN + sizeof(payload)); + CHECK("TX: payload copied intact", memcmp(gTxFrame + RAW_HDR_LEN, payload, sizeof(payload)) == 0); + + // Round trip: our own frame, addresses swapped, must parse back to the payload + static uint8_t rt[TEST_BUF_SIZE]; + uint16_t rt_len = gTxLen; + memcpy(rt, gTxFrame, rt_len); + setupSocket(); + tEthHdr *re = (tEthHdr *)rt; + memcpy(re->dst, LOCAL_MAC, 6); + memcpy(re->src, PEER_MAC, 6); + tIp4Hdr *ri = (tIp4Hdr *)(rt + ETH_HDR_LEN); + memcpy(ri->src, PEER_IP, 4); + memcpy(ri->dst, LOCAL_IP, 4); + ri->checksum = BE16(ipHeaderChecksum(ri)); + tUdpHdr *ru = (tUdpHdr *)(rt + ETH_HDR_LEN + IP4_HDR_LEN); + ru->src_port = BE16(PEER_PORT); + ru->dst_port = BE16(LOCAL_PORT); + r = handleFrame(rt, rt_len, out, sizeof(out), srcAddr, &srcPort); + CHECK("Round trip: TX frame parses back to the payload", r == (int16_t)sizeof(payload) && !memcmp(out, payload, sizeof(payload))); + + // A maximum size segment must still fit into one Ethernet frame + setupSocket(); + memcpy(sSocketRaw.peer_mac, PEER_MAC, 6); + sSocketRaw.peer_mac_valid = true; + static uint8_t maxp[XCPTL_MAX_SEGMENT_SIZE]; + memset(maxp, 0x5A, sizeof(maxp)); + sent = socketSendTo(&sSocketRaw, maxp, sizeof(maxp), PEER_IP, PEER_PORT, NULL); + CHECK("TX: maximum segment accepted", sent == (int16_t)sizeof(maxp)); + CHECK("TX: maximum frame is 42 + max segment", gTxLen == RAW_HDR_LEN + XCPTL_MAX_SEGMENT_SIZE); + + // No peer learned yet -> must not send + setupSocket(); + CHECK("TX: refuses to send before a peer is known", socketSendTo(&sSocketRaw, payload, sizeof(payload), PEER_IP, PEER_PORT, NULL) == -1 && gTxCount == 0); +} + +//----------------------------------------------------------------------------------------------------- +// ARP and ICMP responders + +static void test_arp_icmp(void) { + + static uint8_t f[TEST_BUF_SIZE], out[TEST_BUF_SIZE]; + uint8_t srcAddr[4]; + uint16_t srcPort; + uint16_t n; + + setupSocket(); + n = buildArpRequest(f, LOCAL_IP, ARP_OPER_REQUEST); + handleFrame(f, n, out, sizeof(out), srcAddr, &srcPort); + CHECK("ARP: reply sent for a request for our IP", gTxCount == 1); + CHECK("ARP: reply length 42", gTxLen == ETH_HDR_LEN + ARP_LEN); + tEthHdr *re = (tEthHdr *)gTxFrame; + tArpHdr *ra = (tArpHdr *)(gTxFrame + ETH_HDR_LEN); + CHECK("ARP: unicast back to the requester", memcmp(re->dst, PEER_MAC, 6) == 0); + CHECK("ARP: source MAC is ours", memcmp(re->src, LOCAL_MAC, 6) == 0); + CHECK("ARP: operation is Reply(2)", BE16(ra->oper) == ARP_OPER_REPLY); + CHECK("ARP: sender hw/proto are ours", !memcmp(ra->sha, LOCAL_MAC, 6) && !memcmp(ra->spa, LOCAL_IP, 4)); + CHECK("ARP: target hw/proto are the requester", !memcmp(ra->tha, PEER_MAC, 6) && !memcmp(ra->tpa, PEER_IP, 4)); + CHECK("ARP: does NOT learn the peer (anti hijack)", !sSocketRaw.peer_mac_valid); + + setupSocket(); + const uint8_t foreign_ip[4] = {192, 168, 90, 99}; + n = buildArpRequest(f, foreign_ip, ARP_OPER_REQUEST); + handleFrame(f, n, out, sizeof(out), srcAddr, &srcPort); + CHECK("ARP: request for a foreign IP ignored", gTxCount == 0); + + setupSocket(); + n = buildArpRequest(f, LOCAL_IP, ARP_OPER_REPLY); + handleFrame(f, n, out, sizeof(out), srcAddr, &srcPort); + CHECK("ARP: unsolicited Reply ignored", gTxCount == 0); + + setupSocket(); + n = buildIcmpEcho(f, 56); // classic ping payload + handleFrame(f, n, out, sizeof(out), srcAddr, &srcPort); + CHECK("ICMP: echo reply sent", gTxCount == 1); + tEthHdr *ie = (tEthHdr *)gTxFrame; + tIp4Hdr *ii = (tIp4Hdr *)(gTxFrame + ETH_HDR_LEN); + uint8_t *ic = gTxFrame + ETH_HDR_LEN + IP4_HDR_LEN; + uint16_t icmp_len = (uint16_t)(BE16(ii->total_length) - IP4_HDR_LEN); + CHECK("ICMP: back to the requester MAC", memcmp(ie->dst, PEER_MAC, 6) == 0); + CHECK("ICMP: IPv4 addresses swapped", !memcmp(ii->src, LOCAL_IP, 4) && !memcmp(ii->dst, PEER_IP, 4)); + CHECK("ICMP: IPv4 header checksum valid", checksum16((uint8_t *)ii, IP4_HDR_LEN, 0) == 0); + CHECK("ICMP: type is Echo Reply(0)", ic[0] == ICMP_TYPE_ECHO_REPLY); + CHECK("ICMP: checksum valid", checksum16(ic, icmp_len, 0) == 0); + CHECK("ICMP: id and sequence preserved", ic[4] == 0x12 && ic[5] == 0x34 && ic[7] == 0x01); + CHECK("ICMP: payload echoed back", ic[8] == 0 && ic[9] == 1 && ic[8 + 55] == 55); + CHECK("ICMP: reply length matches the request", gTxLen == n); + + setupSocket(); + n = buildIcmpEcho(f, 1400); // ping -s 1400 + handleFrame(f, n, out, sizeof(out), srcAddr, &srcPort); + CHECK("ICMP: large echo (1400 bytes) answered", gTxCount == 1 && gTxLen == n); + ii = (tIp4Hdr *)(gTxFrame + ETH_HDR_LEN); + ic = gTxFrame + ETH_HDR_LEN + IP4_HDR_LEN; + CHECK("ICMP: large echo checksum valid", checksum16(ic, (uint16_t)(BE16(ii->total_length) - IP4_HDR_LEN), 0) == 0); + + setupSocket(); + // handleIcmp drops the request when it would not fit the reply buffer, i.e. when the echo + // data exceeds one full segment. Derive it so this holds for a jumbo OPTION_MTU as well. + n = buildIcmpEcho(f, (uint16_t)(XCPTL_MAX_SEGMENT_SIZE + 1)); + handleFrame(f, n, out, sizeof(out), srcAddr, &srcPort); + CHECK("ICMP: oversized echo dropped, no reply", gTxCount == 0); +} + +//----------------------------------------------------------------------------------------------------- +// Zero copy transmit (OPTION_UDP_RAW_ZERO_COPY) + +#if XCPTL_TX_HEADROOM > 0 + +// Mimics a transmit queue segment: XCPTL_TX_HEADROOM writable bytes in front of the payload, laid +// out so the payload start has the same alignment as tXcpSegmentBuffer::msg_buffer. +// At file scope because a function local typedef is not portable inside offsetof (GCC rejects it). +typedef struct { + uint32_t magic; + uint16_t uncommitted; + uint16_t size; + uint8_t headroom[XCPTL_TX_HEADROOM]; + uint8_t msg_buffer[XCPTL_MAX_SEGMENT_SIZE]; +} tTestSegment; + +static void test_zero_copy(void) { + + const uint8_t payload[] = {0x02, 0x00, 0x00, 0x00, 0xFF, 0x00}; + + static tTestSegment seg; + + CHECK("ZC: test fixture matches the queue layout", (offsetof(tTestSegment, msg_buffer) % XCPTL_PACKET_ALIGNMENT) == 0); + + setupSocket(); + memcpy(sSocketRaw.peer_mac, PEER_MAC, 6); + sSocketRaw.peer_mac_valid = true; + memset(&seg, 0, sizeof(seg)); + memcpy(seg.msg_buffer, payload, sizeof(payload)); + + // Poison the headroom so we can see exactly which bytes the transport writes + memset(seg.headroom, 0xCD, sizeof(seg.headroom)); + + int16_t sent = socketSendToReserved(&sSocketRaw, seg.msg_buffer, sizeof(payload), PEER_IP, PEER_PORT, NULL); + CHECK("ZC: returns the payload size", sent == (int16_t)sizeof(payload)); + CHECK("ZC: one frame handed to the HAL", gTxCount == 1); + CHECK("ZC: frame length is 42 + payload", gTxLen == RAW_HDR_LEN + sizeof(payload)); + + // The header must be written right justified, i.e. ending exactly where the payload starts + const uint8_t *frame = seg.msg_buffer - RAW_HDR_LEN; + CHECK("ZC: header written at payload - 42", frame == &seg.headroom[XCPTL_TX_HEADROOM - RAW_HDR_LEN]); + CHECK("ZC: the 6 bytes before the header are untouched", seg.headroom[0] == 0xCD && seg.headroom[XCPTL_TX_HEADROOM - RAW_HDR_LEN - 1] == 0xCD); + + // The frame the HAL received must be the segment itself, header + payload contiguous + CHECK("ZC: HAL frame equals segment header + payload", memcmp(gTxFrame, frame, gTxLen) == 0); + CHECK("ZC: payload NOT copied, still in place", memcmp(seg.msg_buffer, payload, sizeof(payload)) == 0); + + const tEthHdr *eth = (const tEthHdr *)frame; + const tIp4Hdr *ip = (const tIp4Hdr *)(frame + ETH_HDR_LEN); + const tUdpHdr *udp = (const tUdpHdr *)(frame + ETH_HDR_LEN + IP4_HDR_LEN); + CHECK("ZC: destination MAC is the peer", memcmp(eth->dst, PEER_MAC, 6) == 0); + CHECK("ZC: ethertype IPv4", BE16(eth->ethertype) == ETHERTYPE_IPV4); + CHECK("ZC: IPv4 header checksum valid", checksum16((const uint8_t *)ip, IP4_HDR_LEN, 0) == 0); + CHECK("ZC: IPv4 header is 4 byte aligned", (((uintptr_t)ip) % 4) == 0); + CHECK("ZC: total length field", BE16(ip->total_length) == IP4_HDR_LEN + UDP_HDR_LEN + sizeof(payload)); + CHECK("ZC: UDP ports and length", BE16(udp->src_port) == LOCAL_PORT && BE16(udp->dst_port) == PEER_PORT && BE16(udp->length) == UDP_HDR_LEN + sizeof(payload)); + + // A full size segment must still produce exactly one frame of the expected length + setupSocket(); + memcpy(sSocketRaw.peer_mac, PEER_MAC, 6); + sSocketRaw.peer_mac_valid = true; + memset(seg.msg_buffer, 0x5A, XCPTL_MAX_SEGMENT_SIZE); + sent = socketSendToReserved(&sSocketRaw, seg.msg_buffer, XCPTL_MAX_SEGMENT_SIZE, PEER_IP, PEER_PORT, NULL); + CHECK("ZC: full segment accepted", sent == (int16_t)XCPTL_MAX_SEGMENT_SIZE); + CHECK("ZC: full frame is 42 + max segment", gTxLen == RAW_HDR_LEN + XCPTL_MAX_SEGMENT_SIZE); + + // Same datagram built by both paths must be byte identical apart from the IPv4 identification + setupSocket(); + memcpy(sSocketRaw.peer_mac, PEER_MAC, 6); + sSocketRaw.peer_mac_valid = true; + socketSendTo(&sSocketRaw, payload, sizeof(payload), PEER_IP, PEER_PORT, NULL); + static uint8_t copy_frame[TEST_BUF_SIZE]; + uint16_t copy_len = gTxLen; + memcpy(copy_frame, gTxFrame, copy_len); + sSocketRaw.ip_ident = 0; // both paths start from the same identification + gTxCount = 0; + memcpy(seg.msg_buffer, payload, sizeof(payload)); + socketSendToReserved(&sSocketRaw, seg.msg_buffer, sizeof(payload), PEER_IP, PEER_PORT, NULL); + CHECK("ZC: copy and zero copy produce the same length", copy_len == gTxLen); + // zero the identification in both before comparing + ((tIp4Hdr *)(copy_frame + ETH_HDR_LEN))->ident = 0; + ((tIp4Hdr *)(gTxFrame + ETH_HDR_LEN))->ident = 0; + CHECK("ZC: copy and zero copy produce identical frames", memcmp(copy_frame, gTxFrame, copy_len) == 0); +} +#endif // XCPTL_TX_HEADROOM > 0 + +//----------------------------------------------------------------------------------------------------- + +int main(void) { + printf("\nsocket_raw_test - raw Ethernet transport unit tests\n"); + printf("\n--- checksums and wire layout ---\n"); + test_checksums(); + printf("\n--- frame build and receive filter ---\n"); + test_frames(); + printf("\n--- ARP and ICMP responders ---\n"); + test_arp_icmp(); +#if XCPTL_TX_HEADROOM > 0 + printf("\n--- zero copy transmit ---\n"); + test_zero_copy(); +#else + printf("\n--- zero copy transmit: skipped (OPTION_UDP_RAW_ZERO_COPY off) ---\n"); +#endif + printf("\n%s (%d failures)\n\n", fails ? "FAILED" : "ALL PASSED", fails); + return fails != 0; +} diff --git a/test/socket_raw_test/src/stubs.c b/test/socket_raw_test/src/stubs.c new file mode 100644 index 00000000..c6384203 --- /dev/null +++ b/test/socket_raw_test/src/stubs.c @@ -0,0 +1,63 @@ +// socket_raw_test - fake Ethernet HAL and platform stubs +// +// eth_hal_send() captures the frame instead of transmitting it, so the test can inspect +// exactly what socket_raw.c put on the wire. Everything else is a minimal stub: the test +// drives socket_raw.c directly and never opens a real interface. + +#include +#include +#include + +#include "platform.h" +#include "socket_raw_hal.h" +#include "xcptl_cfg.h" // for XCPTL_MAX_SEGMENT_SIZE, the capture buffer is sized from the configuration + +#define TEST_BUF_SIZE (42 + XCPTL_MAX_SEGMENT_SIZE + 64) + +// Captured transmit frame, see test/socket_raw_test/src/main.c +extern uint8_t gTxFrame[TEST_BUF_SIZE]; +extern uint16_t gTxLen; +extern int gTxCount; + +uint8_t gXcpLogLevel = 0; + +uint64_t clockGet(void) { return 0; } +uint64_t clockGetMonotonicNs(void) { return 0; } + +void mutexInit(MUTEX *m, bool recursive, uint32_t spinCount) { + (void)m; + (void)recursive; + (void)spinCount; +} +void mutexDestroy(MUTEX *m) { (void)m; } + +bool eth_hal_open(const char *config, tEthHalCtx **ctx) { + (void)config; + (void)ctx; + return false; +} +void eth_hal_close(tEthHalCtx *ctx) { (void)ctx; } +bool eth_hal_get_mac(tEthHalCtx *ctx, uint8_t *mac) { + (void)ctx; + (void)mac; + return false; +} + +int16_t eth_hal_send(tEthHalCtx *ctx, const uint8_t *frame, uint16_t len) { + (void)ctx; + if (len <= sizeof(gTxFrame)) + memcpy(gTxFrame, frame, len); + gTxLen = len; + gTxCount++; + return (int16_t)len; +} + +int16_t eth_hal_recv(tEthHalCtx *ctx, uint8_t *frame, uint16_t max_len, uint32_t timeout_ms) { + (void)ctx; + (void)frame; + (void)max_len; + (void)timeout_ms; + return 0; +} + +void eth_hal_wakeup(tEthHalCtx *ctx) { (void)ctx; } diff --git a/test/test.sh b/test/test.sh index bd10e710..bee9e1ef 100755 --- a/test/test.sh +++ b/test/test.sh @@ -161,13 +161,14 @@ fi # Function to get protocol for an example # TCP examples: hello_xcp, hello_xcp_cpp, struct_demo # UDP examples: c_demo, cpp_demo, multi_thread_demo +# Keep in sync with OPTION_USE_TCP in the main source file of each example get_example_protocol() { local example_name="$1" case "$example_name" in - hello_xcp|hello_xcp_cpp|point_cloud_demo|struct_demo) + point_cloud_demo|struct_demo) echo "tcp" ;; - c_demo|cpp_demo|multi_thread_demo) + hello_xcp|hello_xcp_cpp|c_demo|cpp_demo|multi_thread_demo) echo "udp" ;; *) @@ -186,6 +187,7 @@ SKIPPED=0 # Counter for xcpclient tests XCP_CRASHED=0 +XCP_FAILED=0 # Counter for hex file comparisons HEX_COMPARED=0 @@ -292,7 +294,8 @@ run_example() { log_plain "${RED} This indicates a bug in xcpclient - check the log file for details${NC}" XCP_CRASHED=$((XCP_CRASHED + 1)) else - log_plain "${YELLOW} ⚠ xcpclient exited with code $xcp_exit (connection may have failed)${NC}" + log_plain "${RED} ✗ xcpclient test failed (exit code $xcp_exit) - check the log file for details${NC}" + XCP_FAILED=$((XCP_FAILED + 1)) fi # Give the server a moment to finish writing the A2L file @@ -320,9 +323,8 @@ run_example() { done if [ "$process_exited" = true ]; then - # Process exited before timeout - check exit code - wait "$pid" 2>/dev/null || true - EXIT_CODE=$? + # Process exited before timeout - check exit code (the || form keeps set -e from aborting the script) + wait "$pid" 2>/dev/null && EXIT_CODE=0 || EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then log_plain "${GREEN} Passed: ${example} completed successfully${NC}" PASSED=$((PASSED + 1)) @@ -528,6 +530,10 @@ if [ $XCP_CRASHED -gt 0 ]; then log_plain "${RED} This indicates bugs in xcpclient that need to be fixed!${NC}" log_plain "" fi +if [ $XCP_FAILED -gt 0 ]; then + log_plain "${RED}✗ xcpclient test failed $XCP_FAILED time(s)${NC}" + log_plain "" +fi if [ $HEX_COMPARED -gt 0 ]; then log_plain "${BLUE}HEX File Comparisons:${NC}" log_plain " Compared: $HEX_COMPARED" @@ -579,6 +585,9 @@ log_plain "Full log saved to: $LOG_FILE" if [ $FAILED -gt 0 ]; then log_plain "${RED}Some examples failed!${NC}" exit 1 +elif [ $XCP_FAILED -gt 0 ] || [ $XCP_CRASHED -gt 0 ]; then + log_plain "${RED}Some xcpclient tests failed!${NC}" + exit 1 elif [ $HEX_MISMATCHED -gt 0 ]; then log_plain "${RED}Some HEX files don't match their fixtures!${NC}" exit 1 diff --git a/test/test_socket_raw.sh b/test/test_socket_raw.sh new file mode 100755 index 00000000..f5639aae --- /dev/null +++ b/test/test_socket_raw.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# test_socket_raw.sh - network test setup for the raw Ethernet transport (Linux only) +# +# Phase A of the bring-up described in docs/SOCKET_RAW.md: creates an isolated veth pair +# with the target in its own network namespace, so the kernel IP stack does not compete +# with socket_raw.c - it would otherwise answer the ARP itself and send ICMP port +# unreachable for the XCP UDP port. +# +# Needs root (network namespaces and AF_PACKET). Not part of test/test.sh for that reason. +# +# Usage: +# sudo ./test/test_socket_raw.sh [--keep] +# --keep leave the namespace and the demo running for manual tests +# +# Once running, from this host: +# ping 192.168.90.2 +# arping -I veth0 192.168.90.2 +# tcpdump -i veth0 -nn -e -vv +# xcpclient --addr 192.168.90.2 --port 5555 + +set -u + +NS=xcpraw +HOST_IF=veth0 +TARGET_IF=veth1 +HOST_IP=192.168.90.1 +TARGET_IP=192.168.90.2 +PORT=5555 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEMO="$SCRIPT_DIR/../build-raw/udp_raw_demo" +KEEP=false +[[ "${1:-}" == "--keep" ]] && KEEP=true + +DEMO_PID="" + +cleanup() { + echo "" + echo "Cleaning up..." + [[ -n "$DEMO_PID" ]] && kill "$DEMO_PID" 2>/dev/null + ip netns del "$NS" 2>/dev/null + ip link del "$HOST_IF" 2>/dev/null + echo "Done." +} + +if [[ $EUID -ne 0 ]]; then + echo "ERROR: needs root (network namespaces and AF_PACKET)." + echo " sudo $0" + exit 1 +fi + +if [[ ! -x "$DEMO" ]]; then + echo "ERROR: $DEMO not found or not executable." + echo " Build it first: ./build.sh raw examples" + exit 1 +fi + +trap cleanup EXIT INT TERM + +# Remove leftovers of a previous run +ip netns del "$NS" 2>/dev/null +ip link del "$HOST_IF" 2>/dev/null + +echo "Setting up namespace '$NS' with $HOST_IF <-> $TARGET_IF ..." +ip netns add "$NS" || exit 1 +ip link add "$HOST_IF" type veth peer name "$TARGET_IF" || exit 1 +ip link set "$TARGET_IF" netns "$NS" || exit 1 +ip addr add "$HOST_IP/24" dev "$HOST_IF" || exit 1 +ip link set "$HOST_IF" up || exit 1 +# Deliberately NO IP address on the target side: socket_raw.c owns $TARGET_IP, not the kernel +ip netns exec "$NS" ip link set "$TARGET_IF" up || exit 1 +ip netns exec "$NS" ip link set lo up + +echo " host : $HOST_IF $HOST_IP" +echo " target : $TARGET_IF (in netns $NS, no kernel IP) -> xcplib owns $TARGET_IP:$PORT" +echo "" + +echo "Starting udp_raw_demo in the namespace ..." +ip netns exec "$NS" "$DEMO" --if "$TARGET_IF" --ip "$TARGET_IP" --port "$PORT" & +DEMO_PID=$! +sleep 2 + +if ! kill -0 "$DEMO_PID" 2>/dev/null; then + echo "ERROR: udp_raw_demo exited immediately - see its output above." + exit 1 +fi + +FAILED=0 + +echo "" +echo "=== 1. ARP: does the target answer a request for $TARGET_IP? ===" +if command -v arping >/dev/null 2>&1; then + if arping -I "$HOST_IF" -c 3 -w 3 "$TARGET_IP" >/dev/null 2>&1; then + echo " OK - ARP reply received" + else + echo " FAILED - no ARP reply" + FAILED=1 + fi +else + echo " SKIPPED - arping not installed" +fi + +echo "" +echo "=== 2. ICMP: does the target answer a ping? ===" +echo " (this also proves the Ethernet HAL, MAC filter, IPv4 header and its checksum)" +if ping -c 3 -W 2 "$TARGET_IP" >/dev/null 2>&1; then + echo " OK - ping replies received" +else + echo " FAILED - no ping reply" + FAILED=1 +fi + +echo "" +echo "=== 3. XCP: CONNECT over the raw transport ===" +python3 - "$TARGET_IP" "$PORT" <<'PYEOF' +import socket, struct, sys +ip, port = sys.argv[1], int(sys.argv[2]) +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(3) +pkt = b'\xFF\x00' # CONNECT, mode 0 +s.sendto(struct.pack(' // for htons, htonl #include // for assert diff --git a/tools/ptptool/src/ptp/ptp.h b/tools/ptptool/src/ptp/ptp.h index 455d09fd..7676beb9 100644 --- a/tools/ptptool/src/ptp/ptp.h +++ b/tools/ptptool/src/ptp/ptp.h @@ -4,8 +4,9 @@ #include // for bool #include // for uintxx_t -#include "platform.h" // from libxcplite for SOCKET_HANDLE, THREAD_HANDLE, ... +#include "platform.h" // from libxcplite for THREAD_HANDLE, MUTEX #include "ptpHdr.h" // for struct ptphdr +#include "sockets.h" // from libxcplite for SOCKET_HANDLE #include "util.h" // for average filter //------------------------------------------------------------------------------------------------------- diff --git a/tools/ptptool/src/ptp/ptp_client.c b/tools/ptptool/src/ptp/ptp_client.c index a584e567..7cb183a5 100644 --- a/tools/ptptool/src/ptp/ptp_client.c +++ b/tools/ptptool/src/ptp/ptp_client.c @@ -23,7 +23,8 @@ #include // for malloc, free #include // for sprintf -#include "platform.h" // from libxcplite for SOCKET_HANDLE, ... +#include "platform.h" // from libxcplite for MUTEX, mutexInit, mutexLock, mutexUnlock, clockGetMonotonicNs, sleepUs +#include "sockets.h" // from libxcplite for SOCKET_HANDLE, htonll #include "ptp.h" diff --git a/tools/ptptool/src/ptp/ptp_client.h b/tools/ptptool/src/ptp/ptp_client.h index 431f24c6..20cf2df4 100644 --- a/tools/ptptool/src/ptp/ptp_client.h +++ b/tools/ptptool/src/ptp/ptp_client.h @@ -4,7 +4,8 @@ #include // for bool #include // for uintxx_t -#include "platform.h" // from libxcplite for SOCKET_HANDLE, MUTEX, ... +#include "platform.h" // from libxcplite for MUTEX +#include "sockets.h" // from libxcplite for SOCKET_HANDLE #include "util.h" // from libxcplite for average and linear regression filters #include "ptp.h" // for tPtp, OPTION_ENABLE_XCP diff --git a/tools/ptptool/src/ptp/ptp_master.h b/tools/ptptool/src/ptp/ptp_master.h index abd8780f..02f78c89 100644 --- a/tools/ptptool/src/ptp/ptp_master.h +++ b/tools/ptptool/src/ptp/ptp_master.h @@ -4,7 +4,8 @@ #include // for bool #include // for uintxx_t -#include "platform.h" // from libxcplite for SOCKET_HANDLE, ... +#include "platform.h" // from libxcplite for MUTEX +#include "sockets.h" // from libxcplite for SOCKET_HANDLE #include "util.h" // from libxcplite for average filter #include "ptp.h" // for tPtp, OPTION_ENABLE_XCP diff --git a/tools/ptptool/src/ptp/ptp_observer.h b/tools/ptptool/src/ptp/ptp_observer.h index 6341d10e..e353f37e 100644 --- a/tools/ptptool/src/ptp/ptp_observer.h +++ b/tools/ptptool/src/ptp/ptp_observer.h @@ -4,7 +4,8 @@ #include // for bool #include // for uintxx_t -#include "platform.h" // from libxcplite for SOCKET_HANDLE, MUTEX, ... +#include "platform.h" // from libxcplite for MUTEX +#include "sockets.h" // from libxcplite for SOCKET_HANDLE #include "util.h" // from libxcplite for average and linear regression filters #include "ptp.h" // for tPtp, OPTION_ENABLE_XCP diff --git a/tools/xcpclient/Cargo.lock b/tools/xcpclient/Cargo.lock index d507f5ae..3e4ce629 100644 --- a/tools/xcpclient/Cargo.lock +++ b/tools/xcpclient/Cargo.lock @@ -4,21 +4,21 @@ version = 4 [[package]] name = "a2lfile" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6590875cc309e5dda5c44e015971a595e7df6dabdf940036f9210cd5598c8513" +checksum = "66b919d5d2959281846a4f0ff0eefb1fb98fbbf1a630f050a534431a5d503602" dependencies = [ "a2lmacros", "fnv", "num-traits", - "thiserror 2.0.18", + "thiserror 2.0.20", ] [[package]] name = "a2lmacros" -version = "3.1.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48000235ecfe68ae5d7413286720b0334d29956baacd9f5d47ffa57dbbee09de" +checksum = "cc77d245dcf8a4f9778211028972f706425ea4856cd43d5a3b1373d771eb5c1a" dependencies = [ "proc-macro2", "quote", @@ -26,18 +26,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "atomic" @@ -109,21 +109,27 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -139,15 +145,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.60" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", "shlex", @@ -174,9 +180,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -184,9 +190,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -196,14 +202,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] @@ -233,11 +239,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "env_filter" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -245,9 +282,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -287,9 +324,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "fnv" @@ -297,6 +334,30 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "gimli" version = "0.34.0" @@ -347,9 +408,9 @@ checksum = "365a784774bb381e8c19edb91190a90d7f2625e057b55de2bc0f6b57bc779ff2" [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown", @@ -369,10 +430,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ + "defmt", + "jiff-core", "jiff-static", "log", "portable-atomic", @@ -380,24 +443,35 @@ dependencies = [ "serde_core", ] +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ + "jiff-core", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -409,9 +483,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "lock_api" @@ -424,30 +498,30 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] [[package]] name = "mio" -version = "1.2.0" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "wasi", @@ -515,33 +589,33 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -552,14 +626,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -569,9 +643,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -580,15 +654,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "scopeguard" @@ -598,9 +672,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -608,29 +682,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -650,9 +724,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -664,17 +738,23 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" -version = "1.15.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys", @@ -688,9 +768,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -708,11 +799,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.20", ] [[package]] @@ -723,25 +814,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -756,13 +847,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.5", ] [[package]] @@ -841,9 +932,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -854,9 +945,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -864,22 +955,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] @@ -905,7 +996,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -916,7 +1007,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -964,17 +1055,17 @@ dependencies = [ [[package]] name = "xcp_register_type_derive" version = "3.0.9" -source = "git+https://github.com/RainerZ/xcp-lite.git?branch=V3.0.9#e9da7e9023a2b318bd5496705713254a3610de13" +source = "git+https://github.com/vectorgrp/xcp-lite#6c65adab349bbceab4194ebf41058ee99eca8318" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "xcp_registry" version = "3.0.9" -source = "git+https://github.com/RainerZ/xcp-lite.git?branch=V3.0.9#e9da7e9023a2b318bd5496705713254a3610de13" +source = "git+https://github.com/vectorgrp/xcp-lite#6c65adab349bbceab4194ebf41058ee99eca8318" dependencies = [ "a2lfile", "log", @@ -988,7 +1079,7 @@ dependencies = [ [[package]] name = "xcpclient" -version = "3.0.9" +version = "4.0.0" dependencies = [ "anyhow", "byteorder", @@ -1008,13 +1099,13 @@ dependencies = [ "parking_lot", "regex", "serde", - "thiserror 2.0.18", + "thiserror 2.0.20", "tokio", "xcp_registry", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tools/xcpclient/Cargo.toml b/tools/xcpclient/Cargo.toml index dfb5b319..8b13a867 100644 --- a/tools/xcpclient/Cargo.toml +++ b/tools/xcpclient/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xcpclient" -version = "3.0.9" +version = "4.0.0" edition = "2024" resolver = "2" authors = ["RainerZ"] @@ -39,9 +39,9 @@ cpp_demangle = "0.5" # Registry # From VectorGrp/xcp-lite -#xcp_registry = { git = "https://github.com/vectorgrp/xcp-lite", features = ["a2l_reader"] } -# From fork RainerZ/xcp-lite -xcp_registry = { git = "https://github.com/RainerZ/xcp-lite.git", branch = "V3.0.9", features = ["a2l_reader"] } +xcp_registry = { git = "https://github.com/vectorgrp/xcp-lite", features = ["a2l_reader"] } +# From fork RainerZ/xcp-lite V3.0.9 +# xcp_registry = { git = "https://github.com/RainerZ/xcp-lite.git", branch = "V3.0.9", features = ["a2l_reader"] } # From local directory current branch # xcp_registry = { path = "../../../xcp-lite-RainerZ/xcp_registry", features = ["a2l_reader"] } diff --git a/tools/xcpclient/README.md b/tools/xcpclient/README.md index fc99b03b..54dd145b 100644 --- a/tools/xcpclient/README.md +++ b/tools/xcpclient/README.md @@ -1,74 +1,11 @@ # xcpclient -XCP test client implementation in Rust +XCP test client and A2L generator implementation in Rust Used for integration testing and for uploading or generating A2L files. Partial XCP implementation with hard-coded protocol settings for XCPlite. -## How offline A2L generation works - -xcpclient generates A2L files from ELF/DWARF debug information written into the firmware by -the XCPlite instrumentation macros — no runtime A2L code is needed in the application. - -**Three sources of information** are combined: - -1. **`xcp_evts` ELF section** — the XCPlite macros (`DaqCreateEvent`, `DaqCreateAndTriggerEvent`) - emit a `tXcpEventDescriptor` constant per event into this named section. xcpclient iterates - it to discover every event defined in the firmware, including name, cycle time, and priority. - -2. **`xcp_cals` ELF section** — `CalSegDecl` emits a `tXcpCalSegDescriptor` constant per - calibration segment into this section, containing the segment name, address of the default - page, and its size. xcpclient uses this to discover all calibration segments. - -3. **DWARF debug info** — every trigger macro also emits a named static variable (e.g. - `trg__AAS__eventname`) whose DWARF lexical scope covers the same local variables as the - trigger point. xcpclient walks the DWARF to find these anchor variables, reads the - addressing mode from their name, and associates all in-scope local variables with the - corresponding event as measurements. - -**Preconditions in the application code:** -- Use the XCPlite macros (`DaqCreateEvent`, `DaqTriggerEvent`, `CalSegDecl`, …), - never the raw C API — only macros emit the ELF markers. -- Build with debug info (`-g` / `Debug` or `RelWithDebInfo`). -- Mark local measurement variables `volatile` so the compiler keeps them on the stack frame - and DWARF location expressions remain valid in optimized builds. - -For the full technical specification — ELF section layouts, the `trg__` anchor naming -convention, and the `AddrExt` encoding — see -[docs/TECHNICAL.md — Offline A2L Generation](../../docs/TECHNICAL.md#offline-a2l-generation--elfdwarf-internals). - -### Supported types and known limitations - -The DWARF type information is mapped to A2L objects as follows: - -| C/C++ type | A2L representation | -|---|---| -| `bool`, integer and floating point types | `MEASUREMENT` or `CHARACTERISTIC` of the matching A2L data type | -| `enum` | integer of the enum's size; for variables the enumerators become a verbal conversion table, enum struct members are plain integers | -| one- and two-dimensional arrays | `MEASUREMENT` / `CHARACTERISTIC` with `MATRIX_DIM` (`VAL_BLK`, `CURVE`, `MAP`); arrays of structs become arrays of typedef instances | -| `struct`, `class`, template instantiations | `TYPEDEF_STRUCTURE` + `INSTANCE`; nested structs and classes become nested typedefs; private members are included; base class members are flattened into the derived type for all combinations of `struct`/`class` bases; `static`/`constexpr` members are skipped | -| pointers as struct or class members | the address value as unsigned integer of the target's pointer size, the pointee is not followed | - -Type names which are not valid A2L identifiers (template instantiations such as `TplStruct`) are sanitized to `TplStruct_float_`. -The `TYPEDEF_MEASUREMENT`/`TYPEDEF_CHARACTERISTIC` of a struct field is named after the field; if another structure has a field with -the same name but a different type or metadata, the name is qualified with the structure name (`TplStruct_float_.value`). - -Not supported, skipped and reported as warnings (log level 2 and above): - -- Variables of pointer type (measure the pointed-to variable instead). -- Unions, bitfields and function pointers. A struct member of such a type is written as a one byte `UBYTE` placeholder - so that the remaining members of the structure keep their offsets. -- Arrays with more than two dimensions (written as a one byte placeholder). -- C++ pointer-to-member types (`DW_TAG_ptr_to_member_type`): a struct or class containing one cannot be read at all, - so it and every class deriving from it end up without members. This is a limitation of the a2ltool DWARF reader - this code is based on. -- C++ library containers (`std::vector`, `std::string`, smart pointers, ...) are read as the structs they are; - the heap data behind them is not reachable. - - - - -XCP client v2.1.x for testing XCP servers and managing A2L and HEX files. +XCP client v3.x for testing XCP servers and managing A2L and HEX files. This tool can: - Connect to XCP on Ethernet servers via TCP or UDP and show information about the XCP protocol and the target ECU @@ -81,25 +18,64 @@ This tool can: - List available measurement variables and parameters with regex patterns - Test data acquisition (DAQ) - Execute test sequences +- Check the EPK of the A2L file against the EPK reported by the target, abort on mismatch unless `--yes` is given +- Load options from a TOML configuration file (`--config`, see `xcpclient.toml`) + +## Offline A2L generation + +xcpclient contains the XCPlite specific ELF/DWARF to A2L generator: `xcpclient --create-a2l --elf ` reads the markers the +XCPlite instrumentation macros leave in the ELF file (events, calibration segments, trigger points, metadata) and the DWARF debug +information, and writes a complete A2L file without any runtime A2L code in the application. The workflow, the rules for the +application code, the naming of types and variables, the supported types and the diagnostics are described in +[docs/OFFLINE_A2L.md](../../docs/OFFLINE_A2L.md), the markers in [docs/TECHNICAL.md](../../docs/TECHNICAL.md#instrumentation-markers-for-offline-a2l-tools). + +The generator reads ELF files with DWARF debug information (Linux, QNX, embedded targets). macOS is not supported: executables built +on macOS (Mach-O) contain no DWARF debug information, the macOS linker leaves it in the object files and in the `.dSYM` bundle. +xcpclient rejects Mach-O files with an error message, generate the A2L file from a Linux build of the application instead. + +xcpclient exits with status 1 on any error (connection failed, ELF or A2L file not found or not usable, test failed), so scripts can +detect failures by the exit status. +## Usage +```text Usage: xcpclient [OPTIONS] Options: + --config + Load arguments from a TOML config file. Command-line arguments take precedence + + [default: ""] + --log-level - Log level (Off=0, Error=1, Warn=2, Info=3, Debug=4, Trace=5) [default: 3] + Program flow log level (Off=0, Error=1, Warn=2, Info=3, Debug=4, Trace=5) + + [default: 3] --verbose - Verbose output Enables additional output when reading ELF files and creating A2L files - + Content information detail verbosity level + + [default: 0] + --dest-addr - XCP server address (IP address or IP:port). If port is omitted, uses --port parameter [default: 127.0.0.1] + XCP server address (IP address or IP:port). If port is omitted, uses --port parameter + + [default: 127.0.0.1] --port - XCP server port number (used when --dest-addr doesn't include port) [default: 5555] + XCP server port number (used when --dest-addr doesn't include port) + + [default: 5555] --bind-addr - Bind address (IP address or IP:port). If port is omitted, system assigns an available port [default: 0.0.0.0] + Bind address (IP address or IP:port). If port is omitted, system assigns an available port + + [default: 0.0.0.0] + + --baud-rate + Baud rate for XCP communication (only applicable for certain protocols) + + [default: 115200] --tcp Use TCP for XCP communication.. @@ -107,8 +83,13 @@ Options: --udp Use UDP for XCP communication + --sxi + Use SxI for XCP communication + --connect-mode XCP connect mode + + [default: 0] --offline Force offline mode (no network communication), communication parameters are used to create A2L file @@ -116,6 +97,8 @@ Options: --a2l Specify and overide the name of the A2L file name. If not specified, The A2L file name is read from the XCP server + [default: ""] + --upload-a2l Upload A2L file from XCP server. Requires that the XCP server supports GET_ID A2L upload @@ -132,25 +115,32 @@ Options: Upload ELF file from XCP server. Requires that the XCP server supports proprietary GET_ID ELF upload command --elf - Specify the name of an ELF file, create an A2L file from ELF debug information. If connected to a XCP server, events and memory segments will be extracted from the XCP server + Specify the name of an ELF file, create an A2L file from ELF debug information. If connected to a XCP server, events and memory segments will be extracted from the XCP server. ELF files with DWARF debug information only (Linux, QNX, embedded targets), macOS Mach-O executables are not supported + + [default: ""] --elf-unit-limit Parse only compilations units <= n + + [default: 18446744073709551615] --elf-var-filter - Regex pattern to filter variable names when registering from an ELF file. - Only variables whose names match the pattern are included in the A2L output. - If not specified (or empty), all variables are registered. - Example: --elf-var-filter "counter.*" + Regex pattern to filter variable names when registering from an ELF file. Only variables whose names match the pattern are included in the A2L output. If not specified (or empty), all variables are registered + + [default: ""] + + --elf-skip-no-metadata + Skip variables without any metadata (XCP_UNIT / XCP_LIMITS / XCP_COMMENT) when creating an A2L file from an ELF file. Only variables that have at least one metadata annotation are included in the A2L output --elf-unit-filter - Regex pattern to filter variables by their compilation unit (source file) name. - Only variables defined in compilation units whose name matches are included in the A2L output. - If not specified (or empty), variables from all compilation units are registered. - Example: --elf-cu-filter "my_module.*" + Regex pattern to filter variables by their compilation unit (source file) name. Only variables defined in compilation units whose name matches are included in the A2L output. If not specified (or empty), variables from all compilation units are registered + + [default: ""] --bin Specify the pathname of a binary file (Intel-HEX) for calibration parameter segment data + + [default: ""] --upload-bin Upload all calibration segments working page data from target and store into a binary file. Requires that the XCP server supports GET_ID A2L upload @@ -160,30 +150,45 @@ Options: --list-mea Lists all specified measurement variables (regex) found in the A2L file + + [default: ""] --mea ... Specify variable names for DAQ measurement (list), may be list of names separated by space or single regular expressions (e.g. ".*") + --default-event + Event for variables without a fixed event (global variables and static variables in functions without an event trigger), given by event id or event name. Used for their DAQ measurement and assigned to them as default event when an A2L file is created from an ELF file. An event name is looked up in the event list (from the XCP server, the ELF file or the A2L file), xcpclient aborts when it is not found. If not specified, such variables get no event and can not be measured with xcpclient + --time