Skip to content
Merged
22 changes: 11 additions & 11 deletions .fusa-hara.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
],
Expand All @@ -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."
}
]
Expand Down
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion HARA.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
5 changes: 4 additions & 1 deletion adapt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/lintool/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion ldf/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)})
Expand Down
11 changes: 11 additions & 0 deletions lin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
6 changes: 3 additions & 3 deletions lin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
14 changes: 7 additions & 7 deletions master/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand Down
29 changes: 26 additions & 3 deletions master/master_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand All @@ -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) {
Expand Down
15 changes: 6 additions & 9 deletions sas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | ✓ |
Expand All @@ -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_
2 changes: 1 addition & 1 deletion seooc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions slave/slave.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions virtual/bus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion virtual/bus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading