From 5feaed1c02dc092c515d4329627e619b04145268 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:08:47 -0700 Subject: [PATCH] =?UTF-8?q?docs:=20roadmap=20=E2=80=94=20future=20LIN=20bu?= =?UTF-8?q?s=20simulator=20planning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture a concrete, phased plan for a LIN bus simulator: independent per-slave goroutines with pluggable/faultable response behavior (wrong checksum, wrong PID, silent slave), multi-slave conflict detection, and a stretch-goal cross-process transport (Unix domain socket) that would double as a mechanism for go-LIN/cpp-LIN/rust-LIN interop testing — mirroring what vcan+can-utils gives go-CAN and CycloneDDS gives the DDS family, neither of which LIN has an equivalent for today. Grounded in the current codebase: virtual.Bus is a single shared map with no fault-injection path, slave.Node has no independent behavior, and master.Node already has a real schedule-table engine that a simulator transport can reuse unmodified. Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- ROADMAP.md | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 3a82dde..23c67f6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,3 +129,172 @@ statistics (#10), godoc examples (#11), go-FuSa/coverage (#9). ### 9 — Observability - `stats.Collector` — frames/sec, per-frame-ID counters, estimated bus load percentage + +--- + +## Future — LIN Bus Simulator + +Not scheduled against a milestone or issue yet. Captured here as a scoped plan +because LIN has a testing gap none of the other x-Net members have: go-CAN can +validate against Linux's real `vcan` kernel interface, and go-DDS/cpp-DDS/ +rust-DDS can validate against CycloneDDS as a third-party peer (or, per +rust-DDS's current interop work, a second process of themselves). LIN has +neither a kernel-native virtual bus nor a widely-used third-party stack in +this ecosystem to test against. A real simulator is the only way to close +that gap. + +### What already exists (read from the current codebase, not assumed) + +- `virtual.Bus` (`virtual/bus.go`) is **not** a slave-node simulator. It is a + single shared `map[uint8]responseEntry` guarded by one `sync.RWMutex`. + `Publish`/`PublishClassic` write an entry; `SendHeader` reads the entry back, + computes `lin.ProtectID` and `lin.CalcChecksum` *correctly* every time, and + broadcasts the resulting `lin.Frame`. There is no path by which a registered + response can come back with a wrong checksum, a wrong PID, or no response at + all (short of not registering one, which only exercises `ErrNoResponse`, + not "slave went silent mid-session"). +- `slave.Node` (`slave/slave.go`) is a thin wrapper: `SetResponse` just calls + `bus.Publish` and tracks `RegisteredIDs()`. It has no goroutine, no + internal state machine, and no independent timing — it is a registration + call, not a simulated ECU. Two slaves registering the same ID silently + overwrite each other in the map; nothing detects or reports the conflict. +- `master.Node` (`master/master.go`), by contrast, is already a real + schedule-table engine: `Run(ctx)` walks `[]lin.ScheduleEntry{ID, DelayMs}` + in a loop, calls `bus.SendHeader` per slot, sleeps `DelayMs`, and drives + `OnFrame`/`OnError`. It already supports LIN 2.x §2.3.2.4 sporadic frame + slots via `SetSporadicGroup`/`SetPending`. This does not need to be + reinvented for a simulator — it needs a transport underneath it that behaves + like a bus with independently-acting slaves instead of a static lookup + table. +- `ldf.Database` already parses node declarations, frame-to-publisher + assignments, and schedule tables (`db.Schedule(name)`) from real `.ldf` + files. A simulator can and should be drivable directly from an LDF, not + just from hand-built `ScheduleEntry` slices. +- Everything above lives in one Go process. There is no serialization format + for `lin.Frame` beyond the JSON tags already on the struct, and no IPC of + any kind — `virtual.Bus` cannot be shared across two `go-LIN` binaries, let + alone between a go-LIN process and a cpp-LIN or rust-LIN process. + +### What a real simulator needs that doesn't exist today + +1. **Independent slave behavior**, not a shared map. Each simulated slave + needs its own goroutine (or at least its own decision point) so that + "does this slave answer, and with what" is a per-slave, per-cycle + decision — not a value looked up out of one struct shared with every + other slave on the bus. +2. **Configurable response behavior, including fault injection.** Concretely: + respond with a correct frame, respond with a corrupted checksum, respond + with a mismatched PID, don't respond at all (timeout), or respond after an + injected delay. This is what actually exercises `master.Node.OnError`, + `MasterBus.SendHeader`'s `ErrNoResponse` path, and (once E2E is layered on + top) `safety.Receiver`'s CRC/sequence-gap detection under conditions a + correct-by-construction bus like `virtual.Bus` can never produce. +3. **Multi-slave conflict detection.** Two simulated slaves claiming the same + frame ID — or a simulated slave answering an ID the LDF assigns to a + different publisher — should be observable/reportable by the simulator, + not silently resolved by map overwrite. +4. **A transport decision**: does the simulator stay in-process (extend + `virtual`-style semantics, just with real goroutines per slave and a + fault-injection layer), or does it also get a cross-process transport + (Unix domain socket, or a shared-memory ring buffer for lower latency) so + two separate OS processes can be the master and the slaves? The two are + not the same amount of work, and the phasing below treats them as + separate deliverables. + +### Proposed shape + +A new top-level `simulator/` package, additive alongside `virtual` rather +than a replacement for it — `virtual` keeps its documented job (zero-dependency, +deterministic, single-struct mock used by `mock.New()` and the quickstart) per +Guiding Principle 4. `simulator` targets a different job: a bus with +independently-behaving, individually-faultable slave nodes. + +Sketch, in this repo's idiom (interfaces + goroutines/channels, matching +`virtual.Bus`'s use of `sync.RWMutex` + `atomic` counters and `lin.Err*` +sentinel wrapping): + +```go +// SlaveBehavior decides how a simulated slave answers a header for id. +// ok=false means the slave stays silent this cycle (unpowered, busy, +// deliberately non-responsive) — distinct from "no response ever +// registered," which virtual.Bus already models via ErrNoResponse. +type SlaveBehavior interface { + Respond(id uint8) (data []byte, ok bool) +} + +// FaultBehavior wraps a SlaveBehavior to corrupt its output, mirroring the +// go-DDS record.FaultPublisher wrapper pattern (a decorator around a +// well-behaved implementation, not a fork of it). +type FaultBehavior struct { + Inner SlaveBehavior + WrongChecksum bool + WrongPID bool + Silent bool // overrides Inner entirely: never answer + ResponseDelay time.Duration // injected latency before the header is answered +} + +// Slave runs one simulated slave node in its own goroutine, reacting to +// header-request events from a Bus. +type Slave struct { + ID uint8 // node address, for logging/conflict reports + Frames []uint8 // frame IDs this slave claims + Behavior SlaveBehavior +} + +// Bus is the simulator transport. Unlike virtual.Bus, header requests are +// *events* that registered slaves race to answer, not a map lookup — this +// is what makes independent per-slave goroutines and fault injection +// possible. Bus still implements lin.MasterBus, so master.Node.Run works +// against it unmodified. +type Bus struct { /* ... */ } + +func (b *Bus) AddSlave(s *Slave) error +func (b *Bus) Conflicts() []Conflict // frame IDs claimed by >1 slave +``` + +Cross-process reach, if pursued (see Phase 4 below), extends the same +`lin.MasterBus`/`lin.Bus` interfaces over a Unix domain socket — e.g. +`simulator/ipc.Dial(path)` / `simulator/ipc.Listen(path)` returning something +satisfying `lin.MasterBus` on the master side and `lin.Bus` on each slave-side +connection, framing `lin.Frame` (already JSON-tagged) plus a header-request +event type over the socket. No interface changes are required anywhere else +in the repo — this is exactly what Guiding Principle 6 ("interface-first API +— transports are always swappable") is for. + +### Phasing + +- **Phase 1 — minimal useful first cut (in-process, well-behaved slaves).** + `simulator.Bus` + `simulator.Slave` with event-driven (not map-driven) + header handling, each slave in its own goroutine, default `SlaveBehavior` + that answers correctly. Reuse `master.Node.Run` unmodified against + `simulator.Bus`. This alone is already more realistic than `virtual.Bus` + for schedule-table testing: N independently-scheduled slaves, "slave + present but silent this cycle" as a real per-cycle decision, and multi-slave + conflict detection. Optionally drivable straight from a parsed + `ldf.Database` (nodes → `Slave`s, `db.Schedule(name)` → `master.Node.SetSchedule`). +- **Phase 2 — fault injection.** `FaultBehavior` wrapper: wrong checksum, + wrong PID, dropped response, injected response delay — deterministic or + probabilistic. This is what actually lets `master.Node.OnError`, + `ErrNoResponse`, and `safety.Receiver`'s CRC/sequence-gap detection be + exercised against realistic bus faults instead of only hand-constructed + byte slices in unit tests. +- **Phase 3 — multi-slave scheduling conflicts.** `Bus.Conflicts()` and + LDF cross-validation: flag frame IDs claimed by more than one simulated + slave, and flag a simulated slave answering an ID the LDF assigns to a + different node's publisher. This is a correctness-of-the-simulation + concern (catching *test setup* bugs), not fault injection of the bus + itself. +- **Phase 4 — cross-process transport (stretch goal).** `simulator/ipc` over + a Unix domain socket (shared memory only if latency numbers from the + socket prove insufficient — no evidence of that need yet). This is what + would let two separate `go-LIN` processes run master and slaves as real + OS processes instead of goroutines in one binary, and — because the wire + framing is just `lin.Frame` plus a header-request event — is a plausible + basis for real go-LIN/cpp-LIN/rust-LIN interop testing, the same role + `vcan`+`can-utils` plays for go-CAN and CycloneDDS (or rust-DDS's + two-process self-interop harness) plays for the DDS family. This phase is + explicitly **not a prerequisite** for the simulator's core value — Phases + 1–3 deliver a fully useful fault-injecting dev/test simulator entirely + in-process, with zero IPC. Phase 4 is only worth doing once Phases 1–3 are + proven useful, and only if cross-implementation LIN interop testing + becomes an active goal rather than a hypothetical one.