Skip to content

Commit afe7f03

Browse files
authored
docs: add Future — LIN Bus Simulator roadmap section (#14)
* docs: add Future — LIN Bus Simulator roadmap section 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> * 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 <binary> argument. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --------- Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com>
1 parent bc61be7 commit afe7f03

2 files changed

Lines changed: 141 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ jobs:
151151
run: go install github.com/SoundMatt/RELAY/cmd/relay@latest
152152

153153
- name: RELAY conformance gate
154-
run: relay conform ./build/cli/cpp-lin-cli --strict
154+
run: relay conform --strict ./build/cli/cpp-lin-cli
155155

156156
- name: RELAY interop gate (§20 Continuous Conformance)
157157
run: relay interop --protocol LIN ./build/cli/cpp-lin-cli

ROADMAP.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,143 @@
8383
- [ ] DO-178C DAL-C qualification evidence package
8484
- [ ] MISRA C++ 2023 compliance report
8585
- [ ] 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

Comments
 (0)