From 582ca0141a07a1e401d98316ef5f5c186173a324 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:08:35 -0700 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20add=20Future=20=E2=80=94=20LIN=20Bu?= =?UTF-8?q?s=20Simulator=20roadmap=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LIN has no OS-native virtual bus (unlike CAN's vcan) and no third-party LIN stack in this ecosystem (unlike DDS's CycloneDDS), so a well-designed simulator matters more here than for either. Scopes a phased plan grounded in the current virt::Bus/master::Node/slave::Node/ldf::parser code: a minimal in-process multi-slave simulator with real slave identities and registration-conflict detection (Phase 1), fault injection for wrong checksum/PID/no-response testing (Phase 2), multi-slave schedule conflict modeling (Phase 3), and a stretch-goal cross-process transport that would make this the mechanism for real go-LIN/cpp-LIN/rust-LIN interop testing, mirroring rust-DDS's two-process self-interop pattern (Phase 4). Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- ROADMAP.md | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index a510f07..d3c1507 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -83,3 +83,143 @@ - [ ] DO-178C DAL-C qualification evidence package - [ ] MISRA C++ 2023 compliance report - [ ] ISO/SAE 21434 TARA update (v1.0 threat model) + +## Future — LIN Bus Simulator + +LIN is in a different position than this ecosystem's other buses. CAN has +Linux's real `vcan` kernel interface plus `can-utils` as an independent +oracle; DDS has CycloneDDS as a genuine third-party peer (see cpp-DDS's +`interop/` CycloneDDS-peer harness and rust-DDS's `cyclone-interop` feature). +LIN has neither: no OS-native virtual LIN device, and no widely-used +third-party LIN stack anywhere in this ecosystem to test against. A +deliberately-designed simulator is therefore worth more here than it would be +for CAN or DDS — it is not just a convenience, it is close to the only +practical way to develop and test master/slave LIN logic without hardware, +and (if a transport phase is ever added) the only plausible route to real +interop testing between go-LIN, cpp-LIN, and rust-LIN. + +**What already exists, precisely.** `lin::virt::Bus` (`include/lin/virtual/bus.hpp`, +aliased as `lin::mock::Bus` per spec §13.7.1) is an in-process, +thread-safe `IMasterBus` — the only one in the codebase. `master::Node::run()` +(`include/lin/master/node.hpp`) already drives a schedule table +(`std::vector`) in a loop, calling `IMasterBus::send_header()` +per slot and invoking `on_frame`/`on_error` callbacks, and that schedule can +already be sourced straight from a parsed `.ldf` file via +`lin::ldf::DB::schedule(name)`. `slave::Node::set_response()` +(`include/lin/slave/node.hpp`) is a thin wrapper over `IBus::publish()`. None +of this amounts to a simulator, for three concrete reasons visible directly +in `src/virtual/bus.cpp`: + +- **Responses are one flat map, not independent slave identities.** + `virt::Bus::responses_` is a single `std::map` + keyed by frame ID. Any number of `slave::Node` instances can share one + `Bus`, but they all write into the same map — two "slaves" registering the + same ID silently clobber each other (last `publish()` wins) with no + conflict reported. There is no notion of a slave as a distinct simulated + ECU with its own behavior. +- **`send_header()` cannot produce a bad frame.** `Bus::send_header()` always + calls `protect_id()` and `calc_checksum()` itself from the stored data — + correctness is baked into the transport. There is no code path to make a + simulated slave answer with a wrong checksum, a wrong PID, or silence on a + frame it's normally registered for, which means today's error-handling + tests can only construct bad `Frame` structs by hand; they never exercise + `master::Node::on_error` or `lin::safety::Receiver::unwrap`'s `E2EError` + path against a wire-realistic fault. +- **It is strictly in-process.** `Bus::create()` returns a + `std::shared_ptr` that only means anything inside one process's + address space. There is no transport that lets two separate OS processes — + two `cpp-lin-cli` instances, or a `cpp-lin` master talking to a `go-lin` or + `rust-lin` slave — exchange frames at all. + +### Phase 1 — Minimal useful simulator (in-process, deterministic) + +The smallest cut that is genuinely useful on its own, with no transport work: + +- New `lin::sim` namespace (`include/lin/sim/*.hpp`, `src/sim/*.cpp`), + following this repo's existing convention of one namespace per concern + (`lin::virt`, `lin::master`, `lin::slave`, `lin::ldf`). +- `sim::ISlaveBehavior` — a pure virtual base class, one required method: + `virtual std::optional on_header(uint8_t id, uint8_t pid) = 0;`, + giving each simulated ECU a real identity instead of a shared map entry. + `sim::StaticSlaveBehavior` ships as the default implementation and + internally delegates to `lin::slave::Node::set_response()`, so existing + callers of `slave::Node` are not obsoleted — they become the trivial case + of a behavior-driven slave. +- `sim::Simulator` — an RAII owner (constructor wires a `shared_ptr`, + a `master::Node`, and N registered behaviors together; destructor calls + `bus->close()`) with `std::error_code add_slave(uint8_t id, + std::unique_ptr)`. Unlike today's `virt::Bus::responses_`, + `add_slave` fails with a distinct error instead of silently overwriting an + existing registration for the same ID — the first concrete conflict check + this simulator adds over what exists today. +- A `MasterSchedule` helper adding an explicit `on_slot_complete` hook around + `master::Node::run()`, so a test can step one schedule cycle and assert on + it, rather than only being able to run the loop until an + `std::atomic` flag flips (`run()`'s only stop mechanism today). +- Tests under `tests/test_sim.cpp`; links against the existing `cpp-lin` + CMake target, no new build option required. + +### Phase 2 — Fault injection + +- `sim::FaultInjector`, a decorator implementing `ISlaveBehavior` and wrapping + any other behavior (composition, not a change to the `ISlaveBehavior` + interface), adding: no response (return `std::nullopt`), wrong checksum, + wrong/garbled PID, and configurable response latency/jitter. +- Delivering a deliberately-wrong checksum or PID needs a raw-injection path + on the transport, since `virt::Bus::send_header()` currently recomputes + both from the registered data unconditionally (see above) — this phase's + transport-side prerequisite is a `virt::Bus` (or `sim`-owned) entry point + that accepts a fully-formed, un-recomputed `Frame` for the wire. +- Fault schedules (e.g. "corrupt every 5th response to 0x21", "0x30 never + responds") so error-path tests are reproducible rather than probabilistic. +- Payoff: this is what finally lets `master::Node::on_error` and + `lin::safety::Receiver::unwrap`'s `E2EError` throw path be exercised + against realistic simulated wire faults, not just hand-built bad `Frame` + values in unit tests. + +### Phase 3 — Multi-slave scheduling conflicts + +- Model real bus-contention scenarios beyond simple registration collisions: + overlapping schedule tables assigning the same slot to two slaves, slot + overruns (a slave's simulated response arrives after the master's + configured `ScheduleEntry::delay_ms` — invisible today, since + `master::Node::run()` just sleeps a fixed `delay_ms` after each + `send_header()` with no deadline enforcement), and diagnostic ID + (`kLINDiagRequestID` 0x3C / `kLINDiagResponseID` 0x3D) arbitration when more + than one simulated ECU is configured to answer a diagnostic request. +- Surface these as `on_error` conditions or a `sim::ConflictReport`, so a + schedule table parsed straight out of an `.ldf` file + (`lin::ldf::DB::schedule()`) can be validated against a simulated slave + population before it ever reaches real hardware. + +### Phase 4 (stretch) — Cross-process transport for interop testing + +- A second `IMasterBus`/`IBus` implementation, `lin::ipc::Bus` + (`include/lin/ipc/bus.hpp` + `src/ipc/bus.cpp`), backed by a Unix domain + socket (or POSIX shared memory + semaphore for lower-latency same-host + use), framing `Frame` records — reusing `cli/json.hpp`'s existing JSON + helpers for the wire encoding would keep it consistent with the CLI's own + `convert` command rather than inventing a second format. Gated behind a new + CMake option following this repo's existing `CPPLIN_BUILD_CLI` / + `RELAY_BUILD_CLI` naming convention, e.g. `CPPLIN_SIM_IPC` (default `OFF`). +- With that in place, two separate processes — a `sim-master` and a + `sim-slave` binary, or subcommands added to `cpp-lin-cli` — opening the same + socket path would give cpp-LIN its own two-process self-interop test, + mirroring rust-DDS's `rtps-interop-peer` pattern (one proven binary run + twice as independent OS processes) and the shape of go-DDS/cpp-DDS's + CycloneDDS-peer harness (opt-in `interop/` directory, `docker-compose.yml`, + `INTEROP_*` env vars, a probe-gated CI job that skips rather than fails + when no peer is reachable). The difference from DDS's version: there is no + third-party LIN stack to play the CycloneDDS role, so the peer is a second + instance of cpp-LIN itself — and, longer term, `go-lin`/`rust-lin` speaking + the same wire framing over the same socket, which is the actual path to + real cross-language LIN interop testing. +- Explicitly not a claim that `lin::ipc::Bus` is a real LIN transport — real + LIN is a single-wire UART-level serial bus (see v0.7.0's planned + `termios`/SocketLIN/PEAK hardware work for that). This is a test/interop + *simulation* transport crossing a process boundary, the same role + `virt::Bus` already plays inside one process. +- This phase is explicitly a stretch goal, not a prerequisite: Phases 1–3 + deliver full standalone dev/test value with zero process-boundary or IPC + work. Cross-process transport only matters once real interop testing + between go-LIN/cpp-LIN/rust-LIN is prioritized. From f188889a18ce73fbd29c37626489f1f79564f945 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:18:28 -0700 Subject: [PATCH 2/2] fix: correct relay conform flag order in CI RELAY v1.13 fixed relay conform to reject --strict placed after the binary path instead of silently ignoring it (the exact bug that made this repo's misordered invocation appear to work before). Flags must precede the positional argument. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff248aa..a5f21da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,7 +151,7 @@ jobs: run: go install github.com/SoundMatt/RELAY/cmd/relay@latest - name: RELAY conformance gate - run: relay conform ./build/cli/cpp-lin-cli --strict + run: relay conform --strict ./build/cli/cpp-lin-cli - name: RELAY interop gate (§20 Continuous Conformance) run: relay interop --protocol LIN ./build/cli/cpp-lin-cli