diff --git a/.fusa-hara.json b/.fusa-hara.json index 14a015c..7636bef 100644 --- a/.fusa-hara.json +++ b/.fusa-hara.json @@ -24,42 +24,42 @@ "id": "H-01", "description": "Master transmits a header for the wrong frame ID — an unintended slave actuates the wrong actuator.", "situations": ["OS-001", "OS-004"], - "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B"}, + "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-A"}, "safetyGoals": ["SG-01", "SG-05"] }, { "id": "H-02", "description": "A corrupted LIN frame payload is received without error detection and an incorrect command is delivered to an actuator.", "situations": ["OS-001", "OS-004"], - "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B"}, + "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-A"}, "safetyGoals": ["SG-02"] }, { "id": "H-03", "description": "Incorrect PID parity allows a wrong frame ID to be accepted as valid.", "situations": ["OS-001", "OS-002"], - "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B"}, + "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-A"}, "safetyGoals": ["SG-01"] }, { "id": "H-04", "description": "A checksum error is not detected and corrupted data is passed to the application.", "situations": ["OS-001", "OS-004"], - "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B"}, + "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-A"}, "safetyGoals": ["SG-02"] }, { "id": "H-05", "description": "An LDF signal is decoded with wrong bit offsets and an actuator is set to an out-of-range value.", "situations": ["OS-001", "OS-004"], - "risk": {"severity": "S1", "exposure": "E3", "controllability": "C2", "asil": "ASIL-A"}, + "risk": {"severity": "S1", "exposure": "E3", "controllability": "C2", "asil": "QM"}, "safetyGoals": ["SG-03"] }, { "id": "H-06", "description": "An E2E sequence-counter gap is not detected — a replayed or lost safety frame goes unnoticed.", "situations": ["OS-001", "OS-003", "OS-004"], - "risk": {"severity": "S2", "exposure": "E2", "controllability": "C2", "asil": "ASIL-A"}, + "risk": {"severity": "S2", "exposure": "E2", "controllability": "C2", "asil": "QM"}, "safetyGoals": ["SG-04"] } ], @@ -68,35 +68,35 @@ "id": "SG-01", "description": "go-LIN shall correctly identify frame IDs using PID parity computation and verification.", "hazards": ["H-01", "H-03"], - "asil": "ASIL-B", + "asil": "ASIL-A", "safeState": "Frame with an unverifiable or mismatched PID is rejected and not delivered to the application." }, { "id": "SG-02", "description": "go-LIN shall detect frame payload corruption using the LIN checksum algorithm.", "hazards": ["H-02", "H-04"], - "asil": "ASIL-B", + "asil": "ASIL-A", "safeState": "Frame failing checksum verification is rejected and reported as an error rather than delivered." }, { "id": "SG-03", "description": "go-LIN shall correctly parse LDF signal definitions and decode frame payloads without offset errors.", "hazards": ["H-05"], - "asil": "ASIL-A", + "asil": "QM", "safeState": "Malformed LDF input is rejected at parse time with a descriptive error; no decode occurs." }, { "id": "SG-04", "description": "go-LIN shall detect E2E sequence gaps and CRC mismatches.", "hazards": ["H-06"], - "asil": "ASIL-A", + "asil": "QM", "safeState": "E2E-protected frame with a sequence gap or CRC mismatch is surfaced as an E2EError and not treated as valid." }, { "id": "SG-05", "description": "go-LIN shall validate all frame IDs and data lengths at API boundaries.", "hazards": ["H-01"], - "asil": "ASIL-B", + "asil": "ASIL-A", "safeState": "Out-of-range frame ID or data length is rejected by ValidateFrame before transmission or processing." } ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fac27ae..bbbb55f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,7 +58,6 @@ with Matt Jones. The DCO sign-off transfers no copyright — it only affirms you | `ldf/` | LIN Description File (LDF) parser | | `master/` | LIN master node — schedule execution and header transmission | | `slave/` | LIN slave node — response publisher | -| `transport/` | Physical-layer abstraction (serial/UART) | | `safety/` | E2E protection header (CRC, sequence counter) | | `cmd/lintool/` | CLI tool — `send`, `dump`, `ldf` subcommands | | `examples/quickstart/` | Docker quickstart | diff --git a/HARA.md b/HARA.md index 8c21e39..657e493 100644 --- a/HARA.md +++ b/HARA.md @@ -46,7 +46,7 @@ SEOOC (Safety Element Out Of Context) in automotive and industrial LIN bus syste | SG | Requirement IDs | |---|---| | SG-01 | REQ-LIN-003, REQ-LIN-004 | -| SG-02 | REQ-LIN-004 | +| SG-02 | REQ-LIN-008, REQ-LIN-009, REQ-LIN-010 | | SG-03 | REQ-LDF-002, REQ-LDF-003 | | SG-04 | REQ-SAFETY-004, REQ-SAFETY-005 | | SG-05 | REQ-LIN-001 | diff --git a/adapt.go b/adapt.go index 9769dff..1e23143 100644 --- a/adapt.go +++ b/adapt.go @@ -39,7 +39,10 @@ func (n *linNode) Protocol() relay.Protocol { func (n *linNode) Send(ctx context.Context, msg relay.Message) error { id, err := strconv.ParseUint(msg.ID, 10, 8) if err != nil || id > LINMaxID { - return fmt.Errorf("lin: invalid frame ID %q: %w", msg.ID, ErrNotConnected) + return fmt.Errorf("lin: invalid frame ID %q: %w", msg.ID, ErrInvalidFrame) + } + if len(msg.Payload) > LINMaxDataLen { + return fmt.Errorf("lin: payload length %d exceeds maximum %d: %w", len(msg.Payload), LINMaxDataLen, ErrPayloadTooLarge) } return n.bus.Publish(uint8(id), msg.Payload) } diff --git a/cmd/lintool/main.go b/cmd/lintool/main.go index 5233aae..065d024 100644 --- a/cmd/lintool/main.go +++ b/cmd/lintool/main.go @@ -149,7 +149,7 @@ func parseID(s string) uint8 { } else { v, err = strconv.ParseUint(s, 10, 8) } - if err != nil || v > lin.MaxID { + if err != nil || v > lin.LINMaxID { fatal("invalid LIN frame ID %q (must be 0x00–0x3F)", s) } return uint8(v) @@ -161,8 +161,8 @@ func parseHex(s string) []byte { if err != nil { fatal("invalid hex data %q: %v", s, err) } - if len(b) == 0 || len(b) > lin.MaxDataLen { - fatal("data length %d is not in range 1–%d", len(b), lin.MaxDataLen) + if len(b) == 0 || len(b) > lin.LINMaxDataLen { + fatal("data length %d is not in range 1–%d", len(b), lin.LINMaxDataLen) } return b } diff --git a/ldf/parser.go b/ldf/parser.go index bc2f86e..711b03a 100644 --- a/ldf/parser.go +++ b/ldf/parser.go @@ -533,7 +533,7 @@ func (p *ldfParser) parseScheduleTables(db *DB) error { delay, _ := parseUint(delayStr) // resolve frame name → ID id := frameIDByName(db, parts[0]) - if id > lin.MaxID { + if id > lin.LINMaxID { continue } entries = append(entries, lin.ScheduleEntry{ID: id, DelayMs: uint32(delay)}) diff --git a/lin.go b/lin.go index 89f485c..7b6f360 100644 --- a/lin.go +++ b/lin.go @@ -174,6 +174,17 @@ type Bus interface { //fusa:req REQ-LIN-019 Publish(id uint8, data []byte) error + // PublishFrame registers a fully-formed response Frame for f.ID, + // preserving f.ChecksumType. Unlike Publish (which always uses the + // enhanced checksum), PublishFrame lets callers select the checksum + // type — required for diagnostic frames 0x3C/0x3D, which ISO 17987 / + // LIN 2.x §4.2.3 mandate carry the classic checksum. Passing a Frame + // with nil Data removes a previously registered response. + // + //fusa:req REQ-LIN-011 + //fusa:req REQ-LIN-019 + PublishFrame(f Frame) error + // Subscribe returns a channel that delivers frames matching any of the // supplied filters. Pass nil to receive all frames. // opts configures channel delivery (depth, back-pressure per relay §14). diff --git a/lin_test.go b/lin_test.go index dde2a3a..027074b 100644 --- a/lin_test.go +++ b/lin_test.go @@ -37,7 +37,7 @@ func TestProtectID_P0(t *testing.T) { } func TestProtectID_allIDs(t *testing.T) { - for id := uint8(0); id <= lin.MaxID; id++ { + for id := uint8(0); id <= lin.LINMaxID; id++ { pid := lin.ProtectID(id) if pid&0x3F != id { t.Errorf("ProtectID(0x%02X): lower 6 bits 0x%02X != id", id, pid&0x3F) @@ -51,7 +51,7 @@ func TestProtectID_allIDs(t *testing.T) { //fusa:test REQ-LIN-007 func TestVerifyPID_valid(t *testing.T) { - for id := uint8(0); id <= lin.MaxID; id++ { + for id := uint8(0); id <= lin.LINMaxID; id++ { pid := lin.ProtectID(id) got, err := lin.VerifyPID(pid) if err != nil { @@ -319,7 +319,7 @@ func TestFilterMatches_exact(t *testing.T) { func TestFilterMatches_all(t *testing.T) { flt := lin.Filter{All: true} - for id := uint8(0); id <= lin.MaxID; id++ { + for id := uint8(0); id <= lin.LINMaxID; id++ { if !flt.Matches(lin.Frame{ID: id, Data: []byte{1}}) { t.Errorf("all-filter should match ID 0x%02X", id) } diff --git a/master/master.go b/master/master.go index 8e824c5..5cf9e9b 100644 --- a/master/master.go +++ b/master/master.go @@ -129,7 +129,7 @@ func (n *Node) Diagnostics(ctx context.Context, req lin.MasterRequestFrame) (lin if err != nil { return lin.SlaveResponseFrame{}, fmt.Errorf("master: diagnostics: %w", err) } - if err := n.bus.Publish(f.ID, f.Data); err != nil { + if err := n.bus.PublishFrame(f); err != nil { return lin.SlaveResponseFrame{}, fmt.Errorf("master: diagnostics: publish request: %w", err) } if _, err := n.bus.SendHeader(ctx, lin.LINDiagRequestID); err != nil { @@ -161,12 +161,12 @@ func (n *Node) Diagnostics(ctx context.Context, req lin.MasterRequestFrame) (lin // //fusa:req REQ-MASTER-016 func (n *Node) SetSporadicGroup(slotID uint8, candidates []uint8) error { - if slotID > lin.MaxID { - return fmt.Errorf("master: sporadic slot ID 0x%02X exceeds maximum 0x%02X", slotID, lin.MaxID) + if slotID > lin.LINMaxID { + return fmt.Errorf("master: sporadic slot ID 0x%02X exceeds maximum 0x%02X", slotID, lin.LINMaxID) } for i, id := range candidates { - if id > lin.MaxID { - return fmt.Errorf("master: sporadic candidate %d: ID 0x%02X exceeds maximum 0x%02X", i, id, lin.MaxID) + if id > lin.LINMaxID { + return fmt.Errorf("master: sporadic candidate %d: ID 0x%02X exceeds maximum 0x%02X", i, id, lin.LINMaxID) } } @@ -283,8 +283,8 @@ func (n *Node) Run(ctx context.Context) error { //fusa:req REQ-MASTER-011 func validateSchedule(entries []lin.ScheduleEntry) error { for i, e := range entries { - if e.ID > lin.MaxID { - return fmt.Errorf("master: schedule entry %d: ID 0x%02X exceeds maximum 0x%02X", i, e.ID, lin.MaxID) + if e.ID > lin.LINMaxID { + return fmt.Errorf("master: schedule entry %d: ID 0x%02X exceeds maximum 0x%02X", i, e.ID, lin.LINMaxID) } } return nil diff --git a/master/master_test.go b/master/master_test.go index 8b25870..6a18140 100644 --- a/master/master_test.go +++ b/master/master_test.go @@ -347,11 +347,12 @@ func TestDiagnostics_requestResponseRoundTrip(t *testing.T) { if err != nil { t.Fatalf("resp.ToFrame: %v", err) } - if err := bus.Publish(lin.LINDiagResponseID, respFrame.Data); err != nil { - t.Fatalf("Publish(0x3D): %v", err) + if err := bus.PublishFrame(respFrame); err != nil { + t.Fatalf("PublishFrame(0x3D): %v", err) } ch, _ := bus.Subscribe([]lin.Filter{{ID: lin.LINDiagRequestID}}) + respCh, _ := bus.Subscribe([]lin.Filter{{ID: lin.LINDiagResponseID}}) n := master.New(bus) req := lin.MasterRequestFrame{NAD: 0x01, SID: 0xB2, Data: []byte{0x01, 0x02}} @@ -363,16 +364,38 @@ func TestDiagnostics_requestResponseRoundTrip(t *testing.T) { t.Errorf("Diagnostics response = %+v, want %+v", got, resp) } - // The request itself must actually have been transmitted on 0x3C. + // The request itself must actually have been transmitted on 0x3C, and + // diagnostic frames must carry the classic checksum (ISO 17987 §4.2.3). select { case sent := <-ch: reqFrame, _ := req.ToFrame() if string(sent.Data) != string(reqFrame.Data) { t.Errorf("transmitted request data = % X, want % X", sent.Data, reqFrame.Data) } + if sent.ChecksumType != lin.ClassicChecksum { + t.Errorf("0x3C request ChecksumType = %v, want ClassicChecksum", sent.ChecksumType) + } + wantCS := lin.CalcChecksum(lin.ProtectID(lin.LINDiagRequestID), sent.Data, lin.ClassicChecksum) + if sent.Checksum != wantCS { + t.Errorf("0x3C request Checksum = 0x%02X, want 0x%02X", sent.Checksum, wantCS) + } case <-time.After(time.Second): t.Fatal("timed out waiting for the request frame to be broadcast") } + + // The 0x3D response frame must likewise carry the classic checksum. + select { + case sent := <-respCh: + if sent.ChecksumType != lin.ClassicChecksum { + t.Errorf("0x3D response ChecksumType = %v, want ClassicChecksum", sent.ChecksumType) + } + wantCS := lin.CalcChecksum(lin.ProtectID(lin.LINDiagResponseID), sent.Data, lin.ClassicChecksum) + if sent.Checksum != wantCS { + t.Errorf("0x3D response Checksum = 0x%02X, want 0x%02X", sent.Checksum, wantCS) + } + case <-time.After(time.Second): + t.Fatal("timed out waiting for the response frame to be broadcast") + } } func TestDiagnostics_noResponseRegistered(t *testing.T) { diff --git a/sas.md b/sas.md index a08ff96..aced12b 100644 --- a/sas.md +++ b/sas.md @@ -14,14 +14,14 @@ ## Evidence Summary -**14 / 20** lifecycle data items present. +**17 / 20** lifecycle data items present. | Item | File | Present | |---|---|:---:| | Software Development Plan | `SAFETY_PLAN.md` | ✓ | -| Software Verification Plan | `SVP.md` | ✗ | -| Software Configuration Management Plan | `SCMP.md` | ✗ | -| Software Quality Assurance Plan | `SQAP.md` | ✗ | +| Software Verification Plan | `SVP.md` | ✓ | +| Software Configuration Management Plan | `SCMP.md` | ✓ | +| Software Quality Assurance Plan | `SQAP.md` | ✓ | | Requirements Manifest | `.fusa-reqs.json` | ✓ | | Traceability Matrix | `.fusa-reqs.json` | ✓ | | Test Evidence Bundle | `.fusa-evidence.json` | ✓ | @@ -39,18 +39,15 @@ | Problem Reports | `.fusa-problems.json` | ✓ | | Audit Pack | `audit-pack.zip` | ✗ | -## Gaps (6) +## Gaps (3) -- Software Verification Plan (SVP.md) — not found -- Software Configuration Management Plan (SCMP.md) — not found -- Software Quality Assurance Plan (SQAP.md) — not found - Tool Qualification Report (qualify-report.json) — not found - DO-178C Gap Report (do178-gap-report.json) — not found - Audit Pack (audit-pack.zip) — not found ## Assertion -Software Accomplishment Summary INCOMPLETE — 6 lifecycle data item(s) are absent. See gaps list. Address all gaps before submitting for DER review. +Software Accomplishment Summary INCOMPLETE — 3 lifecycle data item(s) are absent. See gaps list. Address all gaps before submitting for DER review. --- _Generated by go-FuSa v0.18.0 — DO-178C §11.20_ diff --git a/seooc_test.go b/seooc_test.go index 6651157..5ad6520 100644 --- a/seooc_test.go +++ b/seooc_test.go @@ -135,7 +135,7 @@ func TestSEOOC_LDFScheduleIDsValid(t *testing.T) { frames := db.Frames() for i, entry := range sched { - if entry.ID > lin.MaxID { + if entry.ID > lin.LINMaxID { t.Errorf("schedule entry %d: ID 0x%02X exceeds MaxID", i, entry.ID) } if frames[entry.ID] == nil { diff --git a/slave/slave.go b/slave/slave.go index 9802d84..7ba0439 100644 --- a/slave/slave.go +++ b/slave/slave.go @@ -60,8 +60,8 @@ func New(bus lin.Bus) *Node { //fusa:req REQ-SLAVE-004 //fusa:req REQ-SLAVE-008 func (n *Node) SetResponse(id uint8, data []byte) error { - if id > lin.MaxID { - return fmt.Errorf("slave: frame ID 0x%02X exceeds maximum 0x%02X", id, lin.MaxID) + if id > lin.LINMaxID { + return fmt.Errorf("slave: frame ID 0x%02X exceeds maximum 0x%02X", id, lin.LINMaxID) } if err := n.bus.Publish(id, data); err != nil { return fmt.Errorf("slave: Publish: %w", err) diff --git a/virtual/bus.go b/virtual/bus.go index 9dcc1d0..b9f8264 100644 --- a/virtual/bus.go +++ b/virtual/bus.go @@ -100,6 +100,9 @@ func (b *Bus) Publish(id uint8, data []byte) error { if id > lin.LINMaxID { return fmt.Errorf("lin/virtual: frame ID 0x%02X exceeds maximum 0x%02X", id, lin.LINMaxID) } + if len(data) > lin.LINMaxDataLen { + return fmt.Errorf("lin/virtual: payload length %d exceeds maximum %d: %w", len(data), lin.LINMaxDataLen, lin.ErrPayloadTooLarge) + } b.mu.Lock() defer b.mu.Unlock() if b.closed { @@ -137,6 +140,17 @@ func (b *Bus) PublishClassic(id uint8, data []byte) error { return nil } +// PublishFrame registers f.Data for f.ID, preserving f.ChecksumType so that +// callers can select the classic checksum required for diagnostic frames +// 0x3C/0x3D (ISO 17987 / LIN 2.x §4.2.3). A Frame with nil Data removes a +// previously registered response. +func (b *Bus) PublishFrame(f lin.Frame) error { + if f.ChecksumType == lin.ClassicChecksum { + return b.PublishClassic(f.ID, f.Data) + } + return b.Publish(f.ID, f.Data) +} + // SendHeader drives a frame exchange for the given ID. // It looks up any registered slave response, synthesises the Frame with the // correct PID and checksum, broadcasts it to all matching subscribers, and diff --git a/virtual/bus_test.go b/virtual/bus_test.go index 67a9a68..6ded7b0 100644 --- a/virtual/bus_test.go +++ b/virtual/bus_test.go @@ -395,7 +395,7 @@ func FuzzSendHeader(f *testing.F) { t.Fatal(err) } defer b.Close() - if id > lin.MaxID || len(data) == 0 || len(data) > lin.MaxDataLen { + if id > lin.LINMaxID || len(data) == 0 || len(data) > lin.LINMaxDataLen { return } _ = b.Publish(id, data)