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 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.