|
83 | 83 | - [ ] DO-178C DAL-C qualification evidence package |
84 | 84 | - [ ] MISRA C++ 2023 compliance report |
85 | 85 | - [ ] ISO/SAE 21434 TARA update (v1.0 threat model) |
| 86 | + |
| 87 | +## Future — LIN Bus Simulator |
| 88 | + |
| 89 | +LIN is in a different position than this ecosystem's other buses. CAN has |
| 90 | +Linux's real `vcan` kernel interface plus `can-utils` as an independent |
| 91 | +oracle; DDS has CycloneDDS as a genuine third-party peer (see cpp-DDS's |
| 92 | +`interop/` CycloneDDS-peer harness and rust-DDS's `cyclone-interop` feature). |
| 93 | +LIN has neither: no OS-native virtual LIN device, and no widely-used |
| 94 | +third-party LIN stack anywhere in this ecosystem to test against. A |
| 95 | +deliberately-designed simulator is therefore worth more here than it would be |
| 96 | +for CAN or DDS — it is not just a convenience, it is close to the only |
| 97 | +practical way to develop and test master/slave LIN logic without hardware, |
| 98 | +and (if a transport phase is ever added) the only plausible route to real |
| 99 | +interop testing between go-LIN, cpp-LIN, and rust-LIN. |
| 100 | + |
| 101 | +**What already exists, precisely.** `lin::virt::Bus` (`include/lin/virtual/bus.hpp`, |
| 102 | +aliased as `lin::mock::Bus` per spec §13.7.1) is an in-process, |
| 103 | +thread-safe `IMasterBus` — the only one in the codebase. `master::Node::run()` |
| 104 | +(`include/lin/master/node.hpp`) already drives a schedule table |
| 105 | +(`std::vector<ScheduleEntry>`) in a loop, calling `IMasterBus::send_header()` |
| 106 | +per slot and invoking `on_frame`/`on_error` callbacks, and that schedule can |
| 107 | +already be sourced straight from a parsed `.ldf` file via |
| 108 | +`lin::ldf::DB::schedule(name)`. `slave::Node::set_response()` |
| 109 | +(`include/lin/slave/node.hpp`) is a thin wrapper over `IBus::publish()`. None |
| 110 | +of this amounts to a simulator, for three concrete reasons visible directly |
| 111 | +in `src/virtual/bus.cpp`: |
| 112 | + |
| 113 | +- **Responses are one flat map, not independent slave identities.** |
| 114 | + `virt::Bus::responses_` is a single `std::map<uint8_t, ResponseEntry>` |
| 115 | + keyed by frame ID. Any number of `slave::Node` instances can share one |
| 116 | + `Bus`, but they all write into the same map — two "slaves" registering the |
| 117 | + same ID silently clobber each other (last `publish()` wins) with no |
| 118 | + conflict reported. There is no notion of a slave as a distinct simulated |
| 119 | + ECU with its own behavior. |
| 120 | +- **`send_header()` cannot produce a bad frame.** `Bus::send_header()` always |
| 121 | + calls `protect_id()` and `calc_checksum()` itself from the stored data — |
| 122 | + correctness is baked into the transport. There is no code path to make a |
| 123 | + simulated slave answer with a wrong checksum, a wrong PID, or silence on a |
| 124 | + frame it's normally registered for, which means today's error-handling |
| 125 | + tests can only construct bad `Frame` structs by hand; they never exercise |
| 126 | + `master::Node::on_error` or `lin::safety::Receiver::unwrap`'s `E2EError` |
| 127 | + path against a wire-realistic fault. |
| 128 | +- **It is strictly in-process.** `Bus::create()` returns a |
| 129 | + `std::shared_ptr<Bus>` that only means anything inside one process's |
| 130 | + address space. There is no transport that lets two separate OS processes — |
| 131 | + two `cpp-lin-cli` instances, or a `cpp-lin` master talking to a `go-lin` or |
| 132 | + `rust-lin` slave — exchange frames at all. |
| 133 | + |
| 134 | +### Phase 1 — Minimal useful simulator (in-process, deterministic) |
| 135 | + |
| 136 | +The smallest cut that is genuinely useful on its own, with no transport work: |
| 137 | + |
| 138 | +- New `lin::sim` namespace (`include/lin/sim/*.hpp`, `src/sim/*.cpp`), |
| 139 | + following this repo's existing convention of one namespace per concern |
| 140 | + (`lin::virt`, `lin::master`, `lin::slave`, `lin::ldf`). |
| 141 | +- `sim::ISlaveBehavior` — a pure virtual base class, one required method: |
| 142 | + `virtual std::optional<Frame> on_header(uint8_t id, uint8_t pid) = 0;`, |
| 143 | + giving each simulated ECU a real identity instead of a shared map entry. |
| 144 | + `sim::StaticSlaveBehavior` ships as the default implementation and |
| 145 | + internally delegates to `lin::slave::Node::set_response()`, so existing |
| 146 | + callers of `slave::Node` are not obsoleted — they become the trivial case |
| 147 | + of a behavior-driven slave. |
| 148 | +- `sim::Simulator` — an RAII owner (constructor wires a `shared_ptr<virt::Bus>`, |
| 149 | + a `master::Node`, and N registered behaviors together; destructor calls |
| 150 | + `bus->close()`) with `std::error_code add_slave(uint8_t id, |
| 151 | + std::unique_ptr<ISlaveBehavior>)`. Unlike today's `virt::Bus::responses_`, |
| 152 | + `add_slave` fails with a distinct error instead of silently overwriting an |
| 153 | + existing registration for the same ID — the first concrete conflict check |
| 154 | + this simulator adds over what exists today. |
| 155 | +- A `MasterSchedule` helper adding an explicit `on_slot_complete` hook around |
| 156 | + `master::Node::run()`, so a test can step one schedule cycle and assert on |
| 157 | + it, rather than only being able to run the loop until an |
| 158 | + `std::atomic<bool>` flag flips (`run()`'s only stop mechanism today). |
| 159 | +- Tests under `tests/test_sim.cpp`; links against the existing `cpp-lin` |
| 160 | + CMake target, no new build option required. |
| 161 | + |
| 162 | +### Phase 2 — Fault injection |
| 163 | + |
| 164 | +- `sim::FaultInjector`, a decorator implementing `ISlaveBehavior` and wrapping |
| 165 | + any other behavior (composition, not a change to the `ISlaveBehavior` |
| 166 | + interface), adding: no response (return `std::nullopt`), wrong checksum, |
| 167 | + wrong/garbled PID, and configurable response latency/jitter. |
| 168 | +- Delivering a deliberately-wrong checksum or PID needs a raw-injection path |
| 169 | + on the transport, since `virt::Bus::send_header()` currently recomputes |
| 170 | + both from the registered data unconditionally (see above) — this phase's |
| 171 | + transport-side prerequisite is a `virt::Bus` (or `sim`-owned) entry point |
| 172 | + that accepts a fully-formed, un-recomputed `Frame` for the wire. |
| 173 | +- Fault schedules (e.g. "corrupt every 5th response to 0x21", "0x30 never |
| 174 | + responds") so error-path tests are reproducible rather than probabilistic. |
| 175 | +- Payoff: this is what finally lets `master::Node::on_error` and |
| 176 | + `lin::safety::Receiver::unwrap`'s `E2EError` throw path be exercised |
| 177 | + against realistic simulated wire faults, not just hand-built bad `Frame` |
| 178 | + values in unit tests. |
| 179 | + |
| 180 | +### Phase 3 — Multi-slave scheduling conflicts |
| 181 | + |
| 182 | +- Model real bus-contention scenarios beyond simple registration collisions: |
| 183 | + overlapping schedule tables assigning the same slot to two slaves, slot |
| 184 | + overruns (a slave's simulated response arrives after the master's |
| 185 | + configured `ScheduleEntry::delay_ms` — invisible today, since |
| 186 | + `master::Node::run()` just sleeps a fixed `delay_ms` after each |
| 187 | + `send_header()` with no deadline enforcement), and diagnostic ID |
| 188 | + (`kLINDiagRequestID` 0x3C / `kLINDiagResponseID` 0x3D) arbitration when more |
| 189 | + than one simulated ECU is configured to answer a diagnostic request. |
| 190 | +- Surface these as `on_error` conditions or a `sim::ConflictReport`, so a |
| 191 | + schedule table parsed straight out of an `.ldf` file |
| 192 | + (`lin::ldf::DB::schedule()`) can be validated against a simulated slave |
| 193 | + population before it ever reaches real hardware. |
| 194 | + |
| 195 | +### Phase 4 (stretch) — Cross-process transport for interop testing |
| 196 | + |
| 197 | +- A second `IMasterBus`/`IBus` implementation, `lin::ipc::Bus` |
| 198 | + (`include/lin/ipc/bus.hpp` + `src/ipc/bus.cpp`), backed by a Unix domain |
| 199 | + socket (or POSIX shared memory + semaphore for lower-latency same-host |
| 200 | + use), framing `Frame` records — reusing `cli/json.hpp`'s existing JSON |
| 201 | + helpers for the wire encoding would keep it consistent with the CLI's own |
| 202 | + `convert` command rather than inventing a second format. Gated behind a new |
| 203 | + CMake option following this repo's existing `CPPLIN_BUILD_CLI` / |
| 204 | + `RELAY_BUILD_CLI` naming convention, e.g. `CPPLIN_SIM_IPC` (default `OFF`). |
| 205 | +- With that in place, two separate processes — a `sim-master` and a |
| 206 | + `sim-slave` binary, or subcommands added to `cpp-lin-cli` — opening the same |
| 207 | + socket path would give cpp-LIN its own two-process self-interop test, |
| 208 | + mirroring rust-DDS's `rtps-interop-peer` pattern (one proven binary run |
| 209 | + twice as independent OS processes) and the shape of go-DDS/cpp-DDS's |
| 210 | + CycloneDDS-peer harness (opt-in `interop/` directory, `docker-compose.yml`, |
| 211 | + `INTEROP_*` env vars, a probe-gated CI job that skips rather than fails |
| 212 | + when no peer is reachable). The difference from DDS's version: there is no |
| 213 | + third-party LIN stack to play the CycloneDDS role, so the peer is a second |
| 214 | + instance of cpp-LIN itself — and, longer term, `go-lin`/`rust-lin` speaking |
| 215 | + the same wire framing over the same socket, which is the actual path to |
| 216 | + real cross-language LIN interop testing. |
| 217 | +- Explicitly not a claim that `lin::ipc::Bus` is a real LIN transport — real |
| 218 | + LIN is a single-wire UART-level serial bus (see v0.7.0's planned |
| 219 | + `termios`/SocketLIN/PEAK hardware work for that). This is a test/interop |
| 220 | + *simulation* transport crossing a process boundary, the same role |
| 221 | + `virt::Bus` already plays inside one process. |
| 222 | +- This phase is explicitly a stretch goal, not a prerequisite: Phases 1–3 |
| 223 | + deliver full standalone dev/test value with zero process-boundary or IPC |
| 224 | + work. Cross-process transport only matters once real interop testing |
| 225 | + between go-LIN/cpp-LIN/rust-LIN is prioritized. |
0 commit comments