- Virtual bus with Classic and Enhanced checksum support
- Protected Identifier (PID) computation and verification
- LDF parser (nodes, signals, frames, schedule tables, signal decode)
- Master node with schedule table runner (
std::stop_token) - Slave node with per-ID response registration
- E2E safety: Protector and Receiver (CRC-16/CCITT-FALSE, 10-byte header)
- RELAY v1.10 adapter (
relay::INode) - CLI binary (
cpp-lin-cli) for relay conform/interop - ISO 26262 ASIL-B requirements traceability
- CI pipeline: build, test, coverage, sanitizers, RELAY conform, cpfusa
- Docker multi-stage image
- TARA (ISO/SAE 21434, IEC 62443-4-1 SL-2): threats, controls,
tara.json - FMEA (ISO 26262-9) linked to hazards,
fmea.json - SAS (ISO 26262-6 §7.4): 9-module architecture specification
- Boundary diagram (ISO 26262-6 §7.4.1):
boundary.mermaid - Safety Manual (ISO 26262-6 §7, Part 10 §9):
SAFETY_MANUAL.md -
SECURITY.md+INCIDENT-RESPONSE.md: coordinated disclosure, ISN process - IEC 62443: expanded
.fusa-iec62443.jsonsecurity requirements - Full requirements traceability closure in
.fusa-reqs.json - CI:
cpfusa lint/cyberhard gates; ThreadSanitizer gate added
-
kSpecVersionbumped 1.10 → 1.11 (relay.hpp,lin.hpp) - CLI version/capabilities JSON report
spec_version1.11 -
RELAY_BUILD_CLICMake alias (§17.7, since removed — see v0.4.0) - Test vectors updated to spec_version 1.11
- LIN-specific error category (
lin::Errc::invalid_frame) distinct fromErrPayloadTooLargefor out-of-range frame IDs (spec §5.3) -
Adapt()subscription thread no longer captures a rawthis— fixes a use-after-free if the adapter is dropped before the bus is closed -
convertwrites the correctErrInvalidFramesentinel name to stderr -
capabilitiesreports an actual transport backend (virtual), not the protocol name -
version/capabilities/statusself-report the real release version (driven byPROJECT_VERSION, not a second hard-coded literal) -
version/statussupport--format text|jsonwith exit2on an unrecognized format or unknown argument (spec §11.1) -
toolfield lowercased tocpp-lin(spec §13.2 CLI-binary-name convention), matching peers likego-can -
lin::mockcanonical module (spec §13.7.1), aliasinglin::virt::Bus
- Declared RELAY spec version bumped 1.11 → 2.0
-
verify_checksum()added; HARA SG-03 downgraded from "Implemented" to "Partial" pending a verified-checksum path in a real (non-virtual) bus backend -
lin::Errc::no_responsedistinct sentinel replaces the generic timeout previously returned bysend_headeron no slave response -
from_messagerejects unrecognizedlin.checksum_typeinstead of silently defaulting to Classic -
master::Node::set_scheduleaccepts an empty schedule (spec §8.3) - DropOldest back-pressure counts an eviction as a drop, not a delivery
-
Frame::checksum_typedefaults to Classic (enum zero value) - E2E
Receiver::unwrapno longer resyncs its sequence counter after a single rejected frame - Requirement/HARA traceability corrections:
REQ-CLI-001..006/REQ-SEC-013defined, three misattributed SG rationale references fixed, HARA §15→§5 citation fixed -
CHANGELOG.mdadded
-
virtual::Bus::publish()forces classic checksum for diagnostic frames 0x3C/0x3D (LIN 2.2A §2.3.1.5 / RELAY §15.3) - RELAY adapter
LinAdapter::send()honourslin.checksum_type/ diagnostic-ID classic-checksum override instead of always upgrading bridged frames to enhanced -
master::Node::run()treats an empty schedule as a no-op success (spec §8.3), notinvalid_frame - LDF parser clamps
Signal::bit_widthto[0, 64]at parse time (defence-in-depth against a malformed/adversarial LDF drivingDB::decode()'s bit-extraction loop into UB) - CI: third-party actions (
ilammy/msvc-dev-cmd,softprops/action-gh-release) pinned to commit SHA - CI:
cpfusa init/cpfusa hara initsteps hard-fail instead of being masked with|| true
- Sleep/wake frame support (LIN 2.x go-to-sleep command)
- Break field and sync field simulation in virtual bus
- Configurable baud rate metadata on virtual bus
-
IMasterBus::set_schedulehot-swap without restartingrun() -
IBus::unsubscribeto cancel an active subscription
- ISO 15765-3 / LIN Transport Layer (segmented diagnostic frames)
- UDS (ISO 14229) over LIN TP adapter
- Diagnostic session management in master node
- Serial/UART hardware bus implementation (Linux
termios) - SocketLIN adapter (if Linux kernel support is available)
- PEAK LIN hardware adapter
- LIN Interface (LINIf) / LIN Driver (LINDrv) API-compatible shim
- COM-stack signal packing / unpacking with endianness support
- PDU router integration point
- ASIL-D dual-channel redundancy pattern example
- Formal verification of checksum and PID algorithms (SPARK/Frama-C)
- DO-178C DAL-C qualification evidence package
- MISRA C++ 2023 compliance report
- ISO/SAE 21434 TARA update (v1.0 threat model)
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<ScheduleEntry>) 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 singlestd::map<uint8_t, ResponseEntry>keyed by frame ID. Any number ofslave::Nodeinstances can share oneBus, but they all write into the same map — two "slaves" registering the same ID silently clobber each other (lastpublish()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 callsprotect_id()andcalc_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 badFramestructs by hand; they never exercisemaster::Node::on_errororlin::safety::Receiver::unwrap'sE2EErrorpath against a wire-realistic fault.- It is strictly in-process.
Bus::create()returns astd::shared_ptr<Bus>that only means anything inside one process's address space. There is no transport that lets two separate OS processes — twocpp-lin-cliinstances, or acpp-linmaster talking to ago-linorrust-linslave — exchange frames at all.
The smallest cut that is genuinely useful on its own, with no transport work:
- New
lin::simnamespace (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<Frame> on_header(uint8_t id, uint8_t pid) = 0;, giving each simulated ECU a real identity instead of a shared map entry.sim::StaticSlaveBehaviorships as the default implementation and internally delegates tolin::slave::Node::set_response(), so existing callers ofslave::Nodeare not obsoleted — they become the trivial case of a behavior-driven slave.sim::Simulator— an RAII owner (constructor wires ashared_ptr<virt::Bus>, amaster::Node, and N registered behaviors together; destructor callsbus->close()) withstd::error_code add_slave(uint8_t id, std::unique_ptr<ISlaveBehavior>). Unlike today'svirt::Bus::responses_,add_slavefails 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
MasterSchedulehelper adding an expliciton_slot_completehook aroundmaster::Node::run(), so a test can step one schedule cycle and assert on it, rather than only being able to run the loop until anstd::atomic<bool>flag flips (run()'s only stop mechanism today). - Tests under
tests/test_sim.cpp; links against the existingcpp-linCMake target, no new build option required.
sim::FaultInjector, a decorator implementingISlaveBehaviorand wrapping any other behavior (composition, not a change to theISlaveBehaviorinterface), adding: no response (returnstd::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 avirt::Bus(orsim-owned) entry point that accepts a fully-formed, un-recomputedFramefor 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_errorandlin::safety::Receiver::unwrap'sE2EErrorthrow path be exercised against realistic simulated wire faults, not just hand-built badFramevalues in unit tests.
- 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, sincemaster::Node::run()just sleeps a fixeddelay_msafter eachsend_header()with no deadline enforcement), and diagnostic ID (kLINDiagRequestID0x3C /kLINDiagResponseID0x3D) arbitration when more than one simulated ECU is configured to answer a diagnostic request. - Surface these as
on_errorconditions or asim::ConflictReport, so a schedule table parsed straight out of an.ldffile (lin::ldf::DB::schedule()) can be validated against a simulated slave population before it ever reaches real hardware.
- A second
IMasterBus/IBusimplementation,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), framingFramerecords — reusingcli/json.hpp's existing JSON helpers for the wire encoding would keep it consistent with the CLI's ownconvertcommand rather than inventing a second format. Gated behind a new CMake option following this repo's existingCPPLIN_BUILD_CLI/RELAY_BUILD_CLInaming convention, e.g.CPPLIN_SIM_IPC(defaultOFF). - With that in place, two separate processes — a
sim-masterand asim-slavebinary, or subcommands added tocpp-lin-cli— opening the same socket path would give cpp-LIN its own two-process self-interop test, mirroring rust-DDS'srtps-interop-peerpattern (one proven binary run twice as independent OS processes) and the shape of go-DDS/cpp-DDS's CycloneDDS-peer harness (opt-ininterop/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-linspeaking 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::Busis a real LIN transport — real LIN is a single-wire UART-level serial bus (see v0.7.0's plannedtermios/SocketLIN/PEAK hardware work for that). This is a test/interop simulation transport crossing a process boundary, the same rolevirt::Busalready 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.