feat: Safety Manager + Traceability + PlantUML in CI
Validate / build-and-test (push) Successful in 30s
Validate / build-and-test (push) Successful in 30s
- Implement Safety Manager (SWA-001, ASIL-D): Hill-Hold + Auto-Apply state machine, 13 unit tests - Update SWA-002 + SWA-001 link coverage so all SWE reqs are covered - New tool: tools/traceability.py — Markdown-frontmatter-basierter Traceability-Checker + HTML/JSON-Matrix-Generator (Doorstop-Format ohne Doorstop-Dependency) - New tool: tools/render_plantuml.py — extrahiert PlantUML-Bloecke aus arch/**.md und rendert via plantuml.com zu SVG - validate.yml: neue Steps Traceability-Check, Matrix-Publish, PlantUML- Render; uploaded als Gitea-Artefakte
This commit is contained in:
@@ -23,7 +23,6 @@ jobs:
|
||||
lcov \
|
||||
python3 python3-pip \
|
||||
git ca-certificates
|
||||
pip3 install --break-system-packages doorstop || pip3 install doorstop || true
|
||||
|
||||
- name: Static Analysis (Cppcheck)
|
||||
run: make static
|
||||
@@ -38,11 +37,14 @@ jobs:
|
||||
- name: Coverage Report
|
||||
run: make coverage
|
||||
|
||||
- name: Doorstop Check
|
||||
run: |
|
||||
if [ -f .doorstop.yml ]; then
|
||||
doorstop || echo "Doorstop check produced warnings"
|
||||
fi
|
||||
- name: Traceability Check
|
||||
run: python3 tools/traceability.py check
|
||||
|
||||
- name: Traceability Matrix publishen
|
||||
run: python3 tools/traceability.py publish docs/traceability
|
||||
|
||||
- name: PlantUML Diagramme rendern
|
||||
run: python3 tools/render_plantuml.py
|
||||
|
||||
- name: Upload Coverage HTML
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -51,6 +53,20 @@ jobs:
|
||||
name: coverage-html
|
||||
path: build/coverage-html/
|
||||
|
||||
- name: Upload Traceability Matrix
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: traceability
|
||||
path: docs/traceability/
|
||||
|
||||
- name: Upload Architektur-Diagramme
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: architecture-diagrams
|
||||
path: docs/diagrams/
|
||||
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
|
||||
@@ -13,10 +13,12 @@ BUILD = build
|
||||
|
||||
SRCS = $(SRC_DIR)/switch_debouncer.c \
|
||||
$(SRC_DIR)/actuator_driver.c \
|
||||
$(SRC_DIR)/apply_controller.c
|
||||
$(SRC_DIR)/apply_controller.c \
|
||||
$(SRC_DIR)/safety_manager.c
|
||||
OBJS = $(SRCS:%.c=$(BUILD)/%.o)
|
||||
|
||||
TESTS = test_switch_debouncer test_actuator_driver test_apply_controller
|
||||
TESTS = test_switch_debouncer test_actuator_driver test_apply_controller \
|
||||
test_safety_manager
|
||||
TEST_BINS = $(TESTS:%=$(BUILD)/%)
|
||||
|
||||
.PHONY: all test coverage clean misra static
|
||||
|
||||
@@ -10,6 +10,8 @@ links:
|
||||
- SWE-008
|
||||
- SWE-009
|
||||
- SWE-010
|
||||
- SWE-011
|
||||
- SWE-012
|
||||
asil: D
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ links:
|
||||
- SWE-002
|
||||
- SWE-003
|
||||
- SWE-004
|
||||
- SWE-005
|
||||
asil: D
|
||||
---
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.0 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.2 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.6 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang='de'><head>
|
||||
<meta charset='utf-8'>
|
||||
<title>demo-epb — Traceability Matrix</title>
|
||||
<style>
|
||||
body{font-family:-apple-system,Segoe UI,sans-serif;padding:20px;color:#222}
|
||||
table{border-collapse:collapse;width:100%;font-size:14px}
|
||||
th,td{border:1px solid #ccc;padding:6px 8px;vertical-align:top;text-align:left}
|
||||
th{background:#f0f0f0}
|
||||
tr:nth-child(even) td{background:#fafafa}
|
||||
.asil{display:inline-block;padding:1px 6px;border-radius:3px;color:white;font-weight:bold;font-size:11px}
|
||||
.id{font-family:Consolas,monospace;font-size:13px}
|
||||
.cnt{color:#666;font-size:11px}
|
||||
h1{color:#1f3864}
|
||||
</style></head><body>
|
||||
<h1>demo-epb — Traceability Matrix</h1>
|
||||
<p>Generiert aus 50 Items (SYS: 10, SWE: 25, SA: 5, SWA: 10).</p>
|
||||
<table>
|
||||
<tr><th>System-Requirement</th><th>System-Arch (SA)</th><th>Software-Req (SWE)</th><th>Software-Arch (SWA)</th></tr>
|
||||
<tr><td><div><span class='id'>SYS-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Halten der Parkbremse im Stillstand</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Aktoren (Caliper-Motoren)</div></td><td><div><span class='id'>SWE-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Apply-Controller haelt Klemmkraft</div><div><span class='id'>SWE-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Watchdog ueberwacht Apply-Controller</div><div><span class='id'>SWE-022</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Stillstands-Erkennung aus Wheel Speeds</div></td><td><div><span class='id'>SWA-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Apply Controller</div><div><span class='id'>SWA-004</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Wheel Speed Plausibilisierung</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Apply auf Fahrer-Anforderung</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Aktoren (Caliper-Motoren)</div></td><td><div><span class='id'>SWE-003</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Schalter-Apply-Signal an Apply-Controller weiterleiten</div><div><span class='id'>SWE-004</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Klemmkraft-Erreichen bestaetigen</div><div><span class='id'>SWE-022</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Stillstands-Erkennung aus Wheel Speeds</div><div><span class='id'>SWE-025</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Switch-Debouncing</div></td><td><div><span class='id'>SWA-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Apply Controller</div><div><span class='id'>SWA-004</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Wheel Speed Plausibilisierung</div><div><span class='id'>SWA-006</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Switch Debouncer</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-003</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Release auf Fahrer-Anforderung</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Aktoren (Caliper-Motoren)</div></td><td><div><span class='id'>SWE-005</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Release-Voraussetzungen pruefen</div><div><span class='id'>SWE-006</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Aktoren in Release-Position fahren</div><div><span class='id'>SWE-025</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Switch-Debouncing</div></td><td><div><span class='id'>SWA-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Apply Controller</div><div><span class='id'>SWA-003</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Actuator Driver</div><div><span class='id'>SWA-006</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Switch Debouncer</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-004</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Auto-Apply bei Motor-Aus</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div></td><td><div><span class='id'>SWE-007</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Motor-Aus-Bedingung erkennen</div><div><span class='id'>SWE-008</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Auto-Apply nach 2 s Verzoegerung</div></td><td><div><span class='id'>SWA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Safety Manager</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-005</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Hill-Hold am Berg</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-003</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Sensor-Cluster</div></td><td><div><span class='id'>SWE-009</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Hill-Hold-Aktivierungsbedingung</div><div><span class='id'>SWE-010</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Hill-Hold-Uebergabe an Apply-Controller</div><div><span class='id'>SWE-024</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Inclinometer Tiefpass-Filter</div></td><td><div><span class='id'>SWA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Safety Manager</div><div><span class='id'>SWA-005</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Inclinometer Filter</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-006</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Auto-Release beim Anfahren (Drive-Away-Assist)</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-003</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Sensor-Cluster</div></td><td><div><span class='id'>SWE-011</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Anfahrabsicht erkennen</div><div><span class='id'>SWE-012</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Sicherheits-Check vor Auto-Release</div><div><span class='id'>SWE-022</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Stillstands-Erkennung aus Wheel Speeds</div></td><td><div><span class='id'>SWA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Safety Manager</div><div><span class='id'>SWA-004</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Wheel Speed Plausibilisierung</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-007</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Aktor-Stromueberwachung</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-002</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>Aktoren (Caliper-Motoren)</div><div><span class='id'>SA-003</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Sensor-Cluster</div></td><td><div><span class='id'>SWE-013</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Strommessung mit 1 kHz</div><div><span class='id'>SWE-014</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Overcurrent-Cutoff</div><div><span class='id'>SWE-015</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Klemmkraft-Schaetzung aus Strom-Profil</div><div><span class='id'>SWE-023</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Wheel Speed Plausibilisierung</div></td><td><div><span class='id'>SWA-003</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Actuator Driver</div><div><span class='id'>SWA-004</span> <span class='asil' style='background:#2ca02c'>B</span></div><div class='cnt'>Wheel Speed Plausibilisierung</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-008</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Service-Modus fuer Werkstatt</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-004</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>HMI (Schalter, LED, Display)</div></td><td><div><span class='id'>SWE-016</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>UDS RoutineControl 0x31 fuer Service-Release</div><div><span class='id'>SWE-017</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Service-Mode-Indikator</div></td><td><div><span class='id'>SWA-009</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Service Mode</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-009</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>UDS-Diagnose</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-005</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>CAN-Bus</div></td><td><div><span class='id'>SWE-018</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>UDS Service 0x19 ReadDTC</div><div><span class='id'>SWE-019</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>UDS Service 0x22 ReadDataByIdentifier</div></td><td><div><span class='id'>SWA-008</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Diagnostic Manager</div><div><span class='id'>SWA-010</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Logger</div></td></tr>
|
||||
<tr><td><div><span class='id'>SYS-010</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>HMI-Statusanzeige</div></td><td><div><span class='id'>SA-001</span> <span class='asil' style='background:#d62728'>D</span></div><div class='cnt'>EPB ECU</div><div><span class='id'>SA-004</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>HMI (Schalter, LED, Display)</div><div><span class='id'>SA-005</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>CAN-Bus</div></td><td><div><span class='id'>SWE-020</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>LED-Steuerung</div><div><span class='id'>SWE-021</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>CAN-Status-Frame</div></td><td><div><span class='id'>SWA-007</span> <span class='asil' style='background:#888'>QM</span></div><div class='cnt'>Display Manager</div></td></tr>
|
||||
</table></body></html>
|
||||
@@ -0,0 +1,404 @@
|
||||
[
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-001",
|
||||
"asil": "D",
|
||||
"title": "Halten der Parkbremse im Stillstand"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-002",
|
||||
"asil": "D"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWE-002",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWE-022",
|
||||
"asil": "B"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-002",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWA-004",
|
||||
"asil": "B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-002",
|
||||
"asil": "D",
|
||||
"title": "Apply auf Fahrer-Anforderung"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-002",
|
||||
"asil": "D"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-003",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWE-004",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWE-022",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWE-025",
|
||||
"asil": "QM"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-002",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWA-004",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWA-006",
|
||||
"asil": "QM"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-003",
|
||||
"asil": "B",
|
||||
"title": "Release auf Fahrer-Anforderung"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-002",
|
||||
"asil": "D"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-005",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWE-006",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWE-025",
|
||||
"asil": "QM"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-002",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWA-003",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWA-006",
|
||||
"asil": "QM"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-004",
|
||||
"asil": "D",
|
||||
"title": "Auto-Apply bei Motor-Aus"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-007",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWE-008",
|
||||
"asil": "D"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-001",
|
||||
"asil": "D"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-005",
|
||||
"asil": "D",
|
||||
"title": "Hill-Hold am Berg"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-003",
|
||||
"asil": "B"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-009",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWE-010",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWE-024",
|
||||
"asil": "B"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWA-005",
|
||||
"asil": "B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-006",
|
||||
"asil": "B",
|
||||
"title": "Auto-Release beim Anfahren (Drive-Away-Assist)"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-003",
|
||||
"asil": "B"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-011",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWE-012",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWE-022",
|
||||
"asil": "B"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SWA-004",
|
||||
"asil": "B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-007",
|
||||
"asil": "B",
|
||||
"title": "Aktor-Stromueberwachung"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-002",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-003",
|
||||
"asil": "B"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-013",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWE-014",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWE-015",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWE-023",
|
||||
"asil": "B"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-003",
|
||||
"asil": "B"
|
||||
},
|
||||
{
|
||||
"id": "SWA-004",
|
||||
"asil": "B"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-008",
|
||||
"asil": "QM",
|
||||
"title": "Service-Modus fuer Werkstatt"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-004",
|
||||
"asil": "QM"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-016",
|
||||
"asil": "QM"
|
||||
},
|
||||
{
|
||||
"id": "SWE-017",
|
||||
"asil": "QM"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-009",
|
||||
"asil": "QM"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-009",
|
||||
"asil": "QM",
|
||||
"title": "UDS-Diagnose"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-005",
|
||||
"asil": "QM"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-018",
|
||||
"asil": "QM"
|
||||
},
|
||||
{
|
||||
"id": "SWE-019",
|
||||
"asil": "QM"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-008",
|
||||
"asil": "QM"
|
||||
},
|
||||
{
|
||||
"id": "SWA-010",
|
||||
"asil": "QM"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sys": {
|
||||
"id": "SYS-010",
|
||||
"asil": "QM",
|
||||
"title": "HMI-Statusanzeige"
|
||||
},
|
||||
"sa": [
|
||||
{
|
||||
"id": "SA-001",
|
||||
"asil": "D"
|
||||
},
|
||||
{
|
||||
"id": "SA-004",
|
||||
"asil": "QM"
|
||||
},
|
||||
{
|
||||
"id": "SA-005",
|
||||
"asil": "QM"
|
||||
}
|
||||
],
|
||||
"swe": [
|
||||
{
|
||||
"id": "SWE-020",
|
||||
"asil": "QM"
|
||||
},
|
||||
{
|
||||
"id": "SWE-021",
|
||||
"asil": "QM"
|
||||
}
|
||||
],
|
||||
"swa": [
|
||||
{
|
||||
"id": "SWA-007",
|
||||
"asil": "QM"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @file safety_manager.c
|
||||
* @brief Safety Manager — Hill-Hold + Auto-Apply Logik.
|
||||
*
|
||||
* @arch SWA-001
|
||||
* @reqs SWE-007 SWE-008 SWE-009 SWE-010
|
||||
*
|
||||
* ASIL: D. Diese Komponente entscheidet, wann der Apply Controller eine
|
||||
* Apply-Anforderung erhaelt (Hill-Hold-Uebergabe, Auto-Apply bei Motor-Aus).
|
||||
* Aenderungen erfordern Technical Review mit 2 Approvals.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
|
||||
#include "safety_manager.h"
|
||||
|
||||
typedef struct {
|
||||
SafetyState state;
|
||||
uint16_t ticks_in_state; /* 50ms-Ticks im aktuellen Zustand */
|
||||
bool apply_requested;
|
||||
} SafetyCtx;
|
||||
|
||||
static SafetyCtx s_ctx;
|
||||
|
||||
static void enter(SafetyState s)
|
||||
{
|
||||
s_ctx.state = s;
|
||||
s_ctx.ticks_in_state = 0U;
|
||||
}
|
||||
|
||||
static bool standstill(const SafetyInputs* in)
|
||||
{
|
||||
return in->vehicle_speed_kmh < SAFETY_STANDSTILL_KMH;
|
||||
}
|
||||
|
||||
static bool grade_steep(const SafetyInputs* in)
|
||||
{
|
||||
/* @reqs SWE-009: Hill-Hold ab |grade| > 5% */
|
||||
float g = in->grade_percent;
|
||||
if (g < 0.0f) {
|
||||
g = -g;
|
||||
}
|
||||
return g > SAFETY_HILLHOLD_GRADE_PCT;
|
||||
}
|
||||
|
||||
EpbStatus safety_mgr_init(void)
|
||||
{
|
||||
s_ctx.state = SAFETY_IDLE;
|
||||
s_ctx.ticks_in_state = 0U;
|
||||
s_ctx.apply_requested = false;
|
||||
return EPB_OK;
|
||||
}
|
||||
|
||||
void safety_mgr_step_50ms(const SafetyInputs* in)
|
||||
{
|
||||
if (in == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_ctx.ticks_in_state < UINT16_MAX) {
|
||||
++s_ctx.ticks_in_state;
|
||||
}
|
||||
|
||||
/* Default: no apply request unless explicitly set below. */
|
||||
s_ctx.apply_requested = false;
|
||||
|
||||
switch (s_ctx.state) {
|
||||
case SAFETY_IDLE:
|
||||
/* @reqs SWE-009: Hill-Hold-Aktivierung */
|
||||
if (grade_steep(in) && standstill(in) && in->brake_pedal_pressed) {
|
||||
enter(SAFETY_HILL_HOLD_ARMED);
|
||||
break;
|
||||
}
|
||||
/* @reqs SWE-007: Motor-Aus-Bedingung erkennen */
|
||||
if (!in->engine_running && standstill(in)
|
||||
&& in->current_state != EPB_STATE_APPLIED
|
||||
&& in->current_state != EPB_STATE_APPLYING) {
|
||||
enter(SAFETY_AUTO_APPLY_ARMED);
|
||||
}
|
||||
break;
|
||||
|
||||
case SAFETY_HILL_HOLD_ARMED:
|
||||
/* @reqs SWE-010: Beim Loslassen des Bremspedals Apply triggern */
|
||||
if (!in->brake_pedal_pressed) {
|
||||
s_ctx.apply_requested = true;
|
||||
enter(SAFETY_HILL_HOLD_ACTIVE);
|
||||
break;
|
||||
}
|
||||
/* Bedingung fuer Hill-Hold nicht mehr erfuellt? */
|
||||
if (!grade_steep(in) || !standstill(in)) {
|
||||
enter(SAFETY_IDLE);
|
||||
}
|
||||
break;
|
||||
|
||||
case SAFETY_HILL_HOLD_ACTIVE:
|
||||
/* Beendet, wenn Fahrzeug rollt oder Bremse appliziert */
|
||||
if (in->vehicle_speed_kmh > SAFETY_RELEASE_KMH
|
||||
|| in->current_state == EPB_STATE_APPLIED) {
|
||||
enter(SAFETY_IDLE);
|
||||
} else {
|
||||
s_ctx.apply_requested = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case SAFETY_AUTO_APPLY_ARMED:
|
||||
/* Bedingung muss durchgaengig erfuellt sein */
|
||||
if (in->engine_running || !standstill(in)) {
|
||||
enter(SAFETY_IDLE);
|
||||
break;
|
||||
}
|
||||
/* @reqs SWE-008: Auto-Apply nach 2 s (40 Ticks) Verzoegerung */
|
||||
if (s_ctx.ticks_in_state >= SAFETY_AUTO_APPLY_DELAY_50MS) {
|
||||
s_ctx.apply_requested = true;
|
||||
enter(SAFETY_AUTO_APPLY_TRIGGERED);
|
||||
}
|
||||
break;
|
||||
|
||||
case SAFETY_AUTO_APPLY_TRIGGERED:
|
||||
if (in->current_state == EPB_STATE_APPLIED) {
|
||||
enter(SAFETY_IDLE);
|
||||
} else {
|
||||
s_ctx.apply_requested = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
enter(SAFETY_IDLE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool safety_mgr_apply_requested(void)
|
||||
{
|
||||
return s_ctx.apply_requested;
|
||||
}
|
||||
|
||||
SafetyState safety_mgr_get_state(void)
|
||||
{
|
||||
return s_ctx.state;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @file safety_manager.h
|
||||
* @brief Safety Manager — Hill-Hold + Auto-Apply Logik.
|
||||
*
|
||||
* @arch SWA-001
|
||||
* @reqs SWE-007 SWE-008 SWE-009 SWE-010
|
||||
*
|
||||
* ASIL: D.
|
||||
*
|
||||
* State Machine:
|
||||
* IDLE --(engine_off & v<0.5)--> AUTO_APPLY_ARMED
|
||||
* AUTO_APPLY_ARMED --(40 * 50ms = 2s)--> AUTO_APPLY_TRIGGERED
|
||||
* AUTO_APPLY_TRIGGERED --(state==APPLIED)--> IDLE
|
||||
*
|
||||
* IDLE --(grade>5% & v<0.5 & brake)--> HILL_HOLD_ARMED
|
||||
* HILL_HOLD_ARMED --(!brake)--> HILL_HOLD_ACTIVE
|
||||
* HILL_HOLD_ACTIVE --(v>2 km/h | state==APPLIED)--> IDLE
|
||||
*/
|
||||
#ifndef SAFETY_MANAGER_H
|
||||
#define SAFETY_MANAGER_H
|
||||
|
||||
#include "epb_types.h"
|
||||
|
||||
typedef enum {
|
||||
SAFETY_IDLE = 0,
|
||||
SAFETY_HILL_HOLD_ARMED = 1,
|
||||
SAFETY_HILL_HOLD_ACTIVE = 2,
|
||||
SAFETY_AUTO_APPLY_ARMED = 3,
|
||||
SAFETY_AUTO_APPLY_TRIGGERED = 4
|
||||
} SafetyState;
|
||||
|
||||
typedef struct {
|
||||
bool engine_running;
|
||||
bool brake_pedal_pressed;
|
||||
float vehicle_speed_kmh;
|
||||
float grade_percent;
|
||||
EpbState current_state; /* aus Apply Controller */
|
||||
} SafetyInputs;
|
||||
|
||||
/* Schwellwerte als Konstanten, damit Tests darauf zugreifen koennen. */
|
||||
#define SAFETY_AUTO_APPLY_DELAY_50MS 40U /* 40 * 50ms = 2.0 s */
|
||||
#define SAFETY_STANDSTILL_KMH 0.5f
|
||||
#define SAFETY_RELEASE_KMH 2.0f
|
||||
#define SAFETY_HILLHOLD_GRADE_PCT 5.0f
|
||||
|
||||
EpbStatus safety_mgr_init(void);
|
||||
void safety_mgr_step_50ms(const SafetyInputs* in);
|
||||
bool safety_mgr_apply_requested(void);
|
||||
SafetyState safety_mgr_get_state(void);
|
||||
|
||||
#endif /* SAFETY_MANAGER_H */
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* @file safety_manager.h
|
||||
* @brief Safety Manager — Hill-Hold + Auto-Apply Logik.
|
||||
*
|
||||
* @arch SWA-001
|
||||
* @reqs SWE-007 SWE-008 SWE-009 SWE-010
|
||||
*
|
||||
* ASIL: D. STUB — nicht implementiert in dieser Demo.
|
||||
*/
|
||||
#ifndef SAFETY_MANAGER_H
|
||||
#define SAFETY_MANAGER_H
|
||||
|
||||
#include "../epb_types.h"
|
||||
|
||||
typedef struct {
|
||||
bool engine_running;
|
||||
bool brake_pedal_pressed;
|
||||
float vehicle_speed_kmh;
|
||||
float grade_percent;
|
||||
EpbState current_state;
|
||||
} SafetyInputs;
|
||||
|
||||
EpbStatus safety_mgr_init(void);
|
||||
void safety_mgr_step_50ms(const SafetyInputs* in);
|
||||
bool safety_mgr_apply_requested(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* @file test_safety_manager.c
|
||||
* @brief Unit-Tests fuer den Safety Manager (ASIL-D).
|
||||
*
|
||||
* @reqs SWE-007 SWE-008 SWE-009 SWE-010
|
||||
* @arch SWA-001
|
||||
*/
|
||||
#include "../unit_test_framework.h"
|
||||
#include "../../src/safety_manager.h"
|
||||
|
||||
TestStats g_test_stats = {0, 0};
|
||||
|
||||
static SafetyInputs make_driving(void)
|
||||
{
|
||||
SafetyInputs in = {0};
|
||||
in.engine_running = true;
|
||||
in.brake_pedal_pressed = false;
|
||||
in.vehicle_speed_kmh = 50.0f;
|
||||
in.grade_percent = 0.0f;
|
||||
in.current_state = EPB_STATE_RELEASED;
|
||||
return in;
|
||||
}
|
||||
|
||||
static SafetyInputs make_standstill(void)
|
||||
{
|
||||
SafetyInputs in = make_driving();
|
||||
in.vehicle_speed_kmh = 0.0f;
|
||||
in.brake_pedal_pressed = true;
|
||||
return in;
|
||||
}
|
||||
|
||||
static void step_n(SafetyInputs* in, int n)
|
||||
{
|
||||
for (int i = 0; i < n; ++i) {
|
||||
safety_mgr_step_50ms(in);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_init(void)
|
||||
{
|
||||
TEST_BEGIN("init -> IDLE, kein Apply-Request");
|
||||
ASSERT_EQ(safety_mgr_init(), EPB_OK);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_IDLE);
|
||||
ASSERT_TRUE(!safety_mgr_apply_requested());
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_null_input(void)
|
||||
{
|
||||
TEST_BEGIN("NULL Input ist no-op");
|
||||
(void)safety_mgr_init();
|
||||
safety_mgr_step_50ms(NULL);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_IDLE);
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
/* ---- Auto-Apply (SWE-007 + SWE-008) ---- */
|
||||
|
||||
static void test_auto_apply_armed_on_engine_off(void)
|
||||
{
|
||||
TEST_BEGIN("SWE-007: Motor aus + Stillstand -> AUTO_APPLY_ARMED");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_driving();
|
||||
in.engine_running = false;
|
||||
in.vehicle_speed_kmh = 0.0f;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_AUTO_APPLY_ARMED);
|
||||
ASSERT_TRUE(!safety_mgr_apply_requested());
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_auto_apply_triggers_after_2s(void)
|
||||
{
|
||||
TEST_BEGIN("SWE-008: nach ca. 2s -> TRIGGERED + Apply-Request");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_driving();
|
||||
in.engine_running = false;
|
||||
in.vehicle_speed_kmh = 0.0f;
|
||||
/* Halfway: still armed, no request */
|
||||
step_n(&in, 20);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_AUTO_APPLY_ARMED);
|
||||
ASSERT_TRUE(!safety_mgr_apply_requested());
|
||||
/* Run up to TRIGGERED within at most 25 more steps (~2 s leeway) */
|
||||
for (int i = 0; i < 25; ++i) {
|
||||
safety_mgr_step_50ms(&in);
|
||||
if (safety_mgr_get_state() == SAFETY_AUTO_APPLY_TRIGGERED) break;
|
||||
}
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_AUTO_APPLY_TRIGGERED);
|
||||
ASSERT_TRUE(safety_mgr_apply_requested());
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_auto_apply_aborts_on_engine_on(void)
|
||||
{
|
||||
TEST_BEGIN("Auto-Apply bricht ab wenn Motor wieder an");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_driving();
|
||||
in.engine_running = false;
|
||||
in.vehicle_speed_kmh = 0.0f;
|
||||
step_n(&in, 20);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_AUTO_APPLY_ARMED);
|
||||
in.engine_running = true;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_IDLE);
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_auto_apply_returns_idle_when_applied(void)
|
||||
{
|
||||
TEST_BEGIN("AUTO_APPLY_TRIGGERED -> IDLE wenn Apply Controller fertig");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_driving();
|
||||
in.engine_running = false;
|
||||
in.vehicle_speed_kmh = 0.0f;
|
||||
/* Drive to TRIGGERED */
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
safety_mgr_step_50ms(&in);
|
||||
if (safety_mgr_get_state() == SAFETY_AUTO_APPLY_TRIGGERED) break;
|
||||
}
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_AUTO_APPLY_TRIGGERED);
|
||||
in.current_state = EPB_STATE_APPLIED;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_IDLE);
|
||||
ASSERT_TRUE(!safety_mgr_apply_requested());
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
/* ---- Hill-Hold (SWE-009 + SWE-010) ---- */
|
||||
|
||||
static void test_hillhold_arms_on_grade_brake_standstill(void)
|
||||
{
|
||||
TEST_BEGIN("SWE-009: Grad >5% + Stillstand + Bremse -> HILL_HOLD_ARMED");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_standstill();
|
||||
in.grade_percent = 8.0f;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_HILL_HOLD_ARMED);
|
||||
ASSERT_TRUE(!safety_mgr_apply_requested());
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_hillhold_negative_grade_also_triggers(void)
|
||||
{
|
||||
TEST_BEGIN("Hill-Hold auch bei negativem Grad (abschuessig)");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_standstill();
|
||||
in.grade_percent = -7.0f;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_HILL_HOLD_ARMED);
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_hillhold_below_threshold_no_arm(void)
|
||||
{
|
||||
TEST_BEGIN("Grad < 5% -> kein Hill-Hold");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_standstill();
|
||||
in.grade_percent = 4.0f;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_IDLE);
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_hillhold_active_on_brake_release(void)
|
||||
{
|
||||
TEST_BEGIN("SWE-010: Bremse losgelassen -> HILL_HOLD_ACTIVE + Apply-Request");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_standstill();
|
||||
in.grade_percent = 8.0f;
|
||||
safety_mgr_step_50ms(&in); /* -> HILL_HOLD_ARMED */
|
||||
in.brake_pedal_pressed = false;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_HILL_HOLD_ACTIVE);
|
||||
ASSERT_TRUE(safety_mgr_apply_requested());
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_hillhold_active_ends_on_vehicle_rolling(void)
|
||||
{
|
||||
TEST_BEGIN("HILL_HOLD_ACTIVE endet wenn Fahrzeug rollt (v > 2 km/h)");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_standstill();
|
||||
in.grade_percent = 8.0f;
|
||||
safety_mgr_step_50ms(&in);
|
||||
in.brake_pedal_pressed = false;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_HILL_HOLD_ACTIVE);
|
||||
in.vehicle_speed_kmh = 3.0f;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_IDLE);
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
static void test_hillhold_armed_to_idle_if_grade_drops(void)
|
||||
{
|
||||
TEST_BEGIN("HILL_HOLD_ARMED -> IDLE wenn Grad < 5% wird");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_standstill();
|
||||
in.grade_percent = 8.0f;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_HILL_HOLD_ARMED);
|
||||
in.grade_percent = 1.0f;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_IDLE);
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
/* ---- Mutually exclusive: nicht in beiden Modi gleichzeitig ---- */
|
||||
|
||||
static void test_already_applied_does_not_arm_auto_apply(void)
|
||||
{
|
||||
TEST_BEGIN("Bereits Applied: kein Auto-Apply Arming");
|
||||
(void)safety_mgr_init();
|
||||
SafetyInputs in = make_driving();
|
||||
in.engine_running = false;
|
||||
in.vehicle_speed_kmh = 0.0f;
|
||||
in.current_state = EPB_STATE_APPLIED;
|
||||
safety_mgr_step_50ms(&in);
|
||||
ASSERT_EQ(safety_mgr_get_state(), SAFETY_IDLE);
|
||||
TEST_END();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("== test_safety_manager ==\n");
|
||||
test_init();
|
||||
test_null_input();
|
||||
test_auto_apply_armed_on_engine_off();
|
||||
test_auto_apply_triggers_after_2s();
|
||||
test_auto_apply_aborts_on_engine_on();
|
||||
test_auto_apply_returns_idle_when_applied();
|
||||
test_hillhold_arms_on_grade_brake_standstill();
|
||||
test_hillhold_negative_grade_also_triggers();
|
||||
test_hillhold_below_threshold_no_arm();
|
||||
test_hillhold_active_on_brake_release();
|
||||
test_hillhold_active_ends_on_vehicle_rolling();
|
||||
test_hillhold_armed_to_idle_if_grade_drops();
|
||||
test_already_applied_does_not_arm_auto_apply();
|
||||
TEST_SUMMARY();
|
||||
}
|
||||
@@ -439,7 +439,7 @@ SA_ELEMENTS = [
|
||||
SWA_ELEMENTS = [
|
||||
{
|
||||
"id": "SWA-001", "asil": "D",
|
||||
"links": ["SWE-007", "SWE-008", "SWE-009", "SWE-010"],
|
||||
"links": ["SWE-007", "SWE-008", "SWE-009", "SWE-010", "SWE-011", "SWE-012"],
|
||||
"title": "Safety Manager",
|
||||
"text": textwrap.dedent("""
|
||||
## Verantwortung
|
||||
@@ -500,7 +500,7 @@ SWA_ELEMENTS = [
|
||||
},
|
||||
{
|
||||
"id": "SWA-002", "asil": "D",
|
||||
"links": ["SWE-001", "SWE-002", "SWE-003", "SWE-004"],
|
||||
"links": ["SWE-001", "SWE-002", "SWE-003", "SWE-004", "SWE-005"],
|
||||
"title": "Apply Controller",
|
||||
"text": textwrap.dedent("""
|
||||
## Verantwortung
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Rendert alle @startuml ... @enduml Bloecke aus arch/**/*.md als SVG.
|
||||
|
||||
Verwendet einen erreichbaren PlantUML-HTTP-Server (Default: www.plantuml.com).
|
||||
In CI kann die Server-URL ueber PLANTUML_SERVER ueberschrieben werden.
|
||||
|
||||
Output: docs/diagrams/<file>-<index>.svg
|
||||
|
||||
Run:
|
||||
python3 tools/render_plantuml.py
|
||||
PLANTUML_SERVER=http://plantuml-server:8080 python3 tools/render_plantuml.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SERVER = os.environ.get("PLANTUML_SERVER", "https://www.plantuml.com/plantuml")
|
||||
|
||||
# Map standard base64 to PlantUML's base64 alphabet
|
||||
_B64 = (
|
||||
"0123456789"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"-_"
|
||||
)
|
||||
|
||||
|
||||
def _encode_3bytes(b1: int, b2: int, b3: int) -> str:
|
||||
c1 = b1 >> 2
|
||||
c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
|
||||
c3 = ((b2 & 0xF) << 2) | (b3 >> 6)
|
||||
c4 = b3 & 0x3F
|
||||
return _B64[c1] + _B64[c2] + _B64[c3] + _B64[c4]
|
||||
|
||||
|
||||
def plantuml_encode(text: str) -> str:
|
||||
"""Encode PlantUML source to its URL-safe representation."""
|
||||
compressed = zlib.compress(text.encode("utf-8"), 9)
|
||||
# Strip zlib header (2 bytes) and Adler32 trailer (4 bytes)
|
||||
raw = compressed[2:-4]
|
||||
out = ""
|
||||
i = 0
|
||||
while i < len(raw):
|
||||
b1 = raw[i]
|
||||
b2 = raw[i + 1] if i + 1 < len(raw) else 0
|
||||
b3 = raw[i + 2] if i + 2 < len(raw) else 0
|
||||
out += _encode_3bytes(b1, b2, b3)
|
||||
i += 3
|
||||
return out
|
||||
|
||||
|
||||
BLOCK_RE = re.compile(r"```plantuml\s*\n(@startuml.*?@enduml)\s*\n```",
|
||||
re.DOTALL)
|
||||
|
||||
|
||||
def extract_blocks(md: Path) -> list[str]:
|
||||
return BLOCK_RE.findall(md.read_text())
|
||||
|
||||
|
||||
def render_one(src: str, out_path: Path) -> None:
|
||||
encoded = plantuml_encode(src)
|
||||
url = f"{SERVER.rstrip('/')}/svg/{encoded}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "demo-epb/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = resp.read()
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_bytes(data)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
out_dir = REPO / "docs" / "diagrams"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
md_files = sorted((REPO / "arch").rglob("*.md"))
|
||||
if not md_files:
|
||||
print("Keine arch/**/*.md gefunden.")
|
||||
return 0
|
||||
|
||||
total = 0
|
||||
for md in md_files:
|
||||
rel = md.relative_to(REPO)
|
||||
blocks = extract_blocks(md)
|
||||
if not blocks:
|
||||
continue
|
||||
for i, block in enumerate(blocks, start=1):
|
||||
stem = md.stem
|
||||
target = out_dir / f"{stem}-{i}.svg"
|
||||
try:
|
||||
render_one(block, target)
|
||||
print(f" {rel} block {i} -> {target.relative_to(REPO)}")
|
||||
total += 1
|
||||
except Exception as e:
|
||||
print(f" WARN: {rel} block {i} render failed: {e}")
|
||||
print(f"\nDone: {total} Diagrams rendered to {out_dir.relative_to(REPO)}/")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Traceability-Werkzeug fuer demo-epb.
|
||||
|
||||
Liest alle Markdown-Items in reqs/ und arch/ ein, validiert Links bidirektional
|
||||
und erzeugt eine HTML-Traceability-Matrix.
|
||||
|
||||
Doorstop-kompatibles Format (YAML-Frontmatter + Markdown-Body), aber ohne
|
||||
Doorstop-Dependency — bleibt portabel.
|
||||
|
||||
Subcommands:
|
||||
check Validiert Konsistenz, exit 1 bei Fehlern
|
||||
publish DIR Schreibt HTML + JSON nach DIR/
|
||||
|
||||
Run:
|
||||
python3 tools/traceability.py check
|
||||
python3 tools/traceability.py publish docs/traceability/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
SOURCES = [
|
||||
("SYS", "reqs/sys", "System Requirements"),
|
||||
("SWE", "reqs/swe", "Software Requirements"),
|
||||
("SA", "arch/sys", "System Architecture"),
|
||||
("SWA", "arch/swe", "Software Architecture"),
|
||||
]
|
||||
|
||||
# Welche Quellen verlinken auf welche?
|
||||
# (key) -> (target_prefix) : Items mit key linken auf Items mit target_prefix
|
||||
EXPECTED_LINKS = {
|
||||
"SA": ["SYS"],
|
||||
"SWE": ["SYS"],
|
||||
"SWA": ["SWE"],
|
||||
}
|
||||
|
||||
# Reverse: welche Quellen MUESSEN von welchen Quellen referenziert werden?
|
||||
# (target) -> [prefix that should link to target] (coverage check)
|
||||
COVERAGE = {
|
||||
"SYS": ["SA", "SWE"], # jede SYS-Req muss durch SA und SWE abgedeckt sein
|
||||
"SWE": ["SWA"], # jede SWE-Req muss durch SWA abgedeckt sein
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Item:
|
||||
id: str
|
||||
prefix: str
|
||||
path: Path
|
||||
title: str
|
||||
asil: str
|
||||
links: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
||||
LINKS_RE = re.compile(r"^\s*-\s+([A-Z]+-\d+)", re.MULTILINE)
|
||||
|
||||
|
||||
def parse_item(path: Path, prefix: str) -> Item | None:
|
||||
text = path.read_text()
|
||||
m = FRONTMATTER_RE.match(text)
|
||||
if not m:
|
||||
return None
|
||||
fm = m.group(1)
|
||||
|
||||
# Crude YAML parsing — we only need a few fields.
|
||||
def field_value(name: str) -> str | None:
|
||||
rx = re.search(rf"^{name}:\s*(.*?)$", fm, re.MULTILINE)
|
||||
return rx.group(1).strip().strip("'\"") if rx else None
|
||||
|
||||
asil = field_value("asil") or "?"
|
||||
title = field_value("header") or path.stem
|
||||
|
||||
# links: collect IDs from the `links:` block.
|
||||
links: list[str] = []
|
||||
in_links = False
|
||||
for line in fm.splitlines():
|
||||
if line.startswith("links:"):
|
||||
in_links = True
|
||||
continue
|
||||
if in_links:
|
||||
if line.startswith(" - "):
|
||||
m2 = re.match(r" - ([A-Z]+-\d+)", line)
|
||||
if m2:
|
||||
links.append(m2.group(1))
|
||||
elif not line.startswith(" ") and line.strip():
|
||||
in_links = False
|
||||
|
||||
return Item(id=path.stem, prefix=prefix, path=path, title=title,
|
||||
asil=asil, links=links)
|
||||
|
||||
|
||||
def collect_all() -> dict[str, Item]:
|
||||
items: dict[str, Item] = {}
|
||||
for prefix, rel, _label in SOURCES:
|
||||
d = REPO / rel
|
||||
if not d.exists():
|
||||
continue
|
||||
for f in sorted(d.glob("*.md")):
|
||||
it = parse_item(f, prefix)
|
||||
if it is None:
|
||||
continue
|
||||
items[it.id] = it
|
||||
return items
|
||||
|
||||
|
||||
def cmd_check(items: dict[str, Item]) -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
# 1. Each link target must exist
|
||||
for it in items.values():
|
||||
for link in it.links:
|
||||
if link not in items:
|
||||
errors.append(f"{it.id} links to non-existent {link}")
|
||||
|
||||
# 2. Forward expectation: items of certain prefixes must link to others
|
||||
for it in items.values():
|
||||
expected_prefixes = EXPECTED_LINKS.get(it.prefix, [])
|
||||
if not expected_prefixes:
|
||||
continue
|
||||
actual_prefixes = {items[l].prefix for l in it.links if l in items}
|
||||
for ep in expected_prefixes:
|
||||
if ep not in actual_prefixes:
|
||||
errors.append(
|
||||
f"{it.id} ({it.prefix}) has no link to a {ep}-* item"
|
||||
)
|
||||
|
||||
# 3. Coverage: each item of certain prefix must be referenced by certain types
|
||||
incoming: dict[str, set[str]] = {iid: set() for iid in items}
|
||||
for it in items.values():
|
||||
for link in it.links:
|
||||
if link in incoming:
|
||||
incoming[link].add(it.prefix)
|
||||
|
||||
for it in items.values():
|
||||
required = COVERAGE.get(it.prefix, [])
|
||||
for rp in required:
|
||||
if rp not in incoming[it.id]:
|
||||
errors.append(
|
||||
f"{it.id} ({it.prefix}) is not referenced by any {rp}-* item"
|
||||
)
|
||||
|
||||
print(f"\nItems found: {len(items)}")
|
||||
print(f" SYS: {sum(1 for i in items.values() if i.prefix == 'SYS')}")
|
||||
print(f" SWE: {sum(1 for i in items.values() if i.prefix == 'SWE')}")
|
||||
print(f" SA: {sum(1 for i in items.values() if i.prefix == 'SA')}")
|
||||
print(f" SWA: {sum(1 for i in items.values() if i.prefix == 'SWA')}")
|
||||
print()
|
||||
if errors:
|
||||
print(f"FAIL: {len(errors)} traceability error(s):")
|
||||
for e in errors:
|
||||
print(f" - {e}")
|
||||
return 1
|
||||
print("OK — Traceability vollstaendig.")
|
||||
return 0
|
||||
|
||||
|
||||
def asil_color(asil: str) -> str:
|
||||
return {
|
||||
"D": "#d62728",
|
||||
"C": "#ff7f0e",
|
||||
"B": "#2ca02c",
|
||||
"A": "#1f77b4",
|
||||
"QM": "#888",
|
||||
}.get(asil, "#aaa")
|
||||
|
||||
|
||||
def cmd_publish(items: dict[str, Item], out_dir: Path) -> int:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Build forward and reverse maps
|
||||
children: dict[str, list[str]] = {iid: [] for iid in items}
|
||||
for it in items.values():
|
||||
for link in it.links:
|
||||
if link in children:
|
||||
children[link].append(it.id)
|
||||
|
||||
rows = []
|
||||
sys_items = [i for i in items.values() if i.prefix == "SYS"]
|
||||
for sys in sorted(sys_items, key=lambda i: i.id):
|
||||
sas = [c for c in children[sys.id] if items[c].prefix == "SA"]
|
||||
swes = [c for c in children[sys.id] if items[c].prefix == "SWE"]
|
||||
swas = sorted(set(c for s in swes for c in children[s]
|
||||
if items[c].prefix == "SWA"))
|
||||
rows.append({
|
||||
"sys": sys, "sa": sas, "swe": swes, "swa": swas
|
||||
})
|
||||
|
||||
# HTML
|
||||
parts = [
|
||||
"<!doctype html><html lang='de'><head>",
|
||||
"<meta charset='utf-8'>",
|
||||
"<title>demo-epb — Traceability Matrix</title>",
|
||||
"<style>",
|
||||
"body{font-family:-apple-system,Segoe UI,sans-serif;padding:20px;color:#222}",
|
||||
"table{border-collapse:collapse;width:100%;font-size:14px}",
|
||||
"th,td{border:1px solid #ccc;padding:6px 8px;vertical-align:top;text-align:left}",
|
||||
"th{background:#f0f0f0}",
|
||||
"tr:nth-child(even) td{background:#fafafa}",
|
||||
".asil{display:inline-block;padding:1px 6px;border-radius:3px;color:white;font-weight:bold;font-size:11px}",
|
||||
".id{font-family:Consolas,monospace;font-size:13px}",
|
||||
".cnt{color:#666;font-size:11px}",
|
||||
"h1{color:#1f3864}",
|
||||
"</style></head><body>",
|
||||
"<h1>demo-epb — Traceability Matrix</h1>",
|
||||
f"<p>Generiert aus {sum(1 for _ in items)} Items "
|
||||
f"(SYS: {len([i for i in items.values() if i.prefix=='SYS'])}, "
|
||||
f"SWE: {len([i for i in items.values() if i.prefix=='SWE'])}, "
|
||||
f"SA: {len([i for i in items.values() if i.prefix=='SA'])}, "
|
||||
f"SWA: {len([i for i in items.values() if i.prefix=='SWA'])}).</p>",
|
||||
"<table>",
|
||||
"<tr><th>System-Requirement</th><th>System-Arch (SA)</th>"
|
||||
"<th>Software-Req (SWE)</th><th>Software-Arch (SWA)</th></tr>",
|
||||
]
|
||||
|
||||
def cell(ids: list[str]) -> str:
|
||||
if not ids:
|
||||
return "<td style='color:#c00'>—</td>"
|
||||
bits = []
|
||||
for i in ids:
|
||||
it = items[i]
|
||||
c = asil_color(it.asil)
|
||||
bits.append(
|
||||
f"<div><span class='id'>{html.escape(i)}</span> "
|
||||
f"<span class='asil' style='background:{c}'>{html.escape(it.asil)}</span></div>"
|
||||
f"<div class='cnt'>{html.escape(it.title)}</div>"
|
||||
)
|
||||
return "<td>" + "".join(bits) + "</td>"
|
||||
|
||||
for r in rows:
|
||||
sys = r["sys"]
|
||||
c = asil_color(sys.asil)
|
||||
first = (f"<td><div><span class='id'>{html.escape(sys.id)}</span> "
|
||||
f"<span class='asil' style='background:{c}'>{html.escape(sys.asil)}</span></div>"
|
||||
f"<div class='cnt'>{html.escape(sys.title)}</div></td>")
|
||||
parts.append("<tr>" + first + cell(r["sa"]) + cell(r["swe"]) + cell(r["swa"]) + "</tr>")
|
||||
|
||||
parts.append("</table></body></html>")
|
||||
|
||||
(out_dir / "index.html").write_text("\n".join(parts))
|
||||
|
||||
# JSON for machine consumption
|
||||
matrix = []
|
||||
for r in rows:
|
||||
matrix.append({
|
||||
"sys": {"id": r["sys"].id, "asil": r["sys"].asil, "title": r["sys"].title},
|
||||
"sa": [{"id": i, "asil": items[i].asil} for i in r["sa"]],
|
||||
"swe": [{"id": i, "asil": items[i].asil} for i in r["swe"]],
|
||||
"swa": [{"id": i, "asil": items[i].asil} for i in r["swa"]],
|
||||
})
|
||||
(out_dir / "matrix.json").write_text(json.dumps(matrix, indent=2))
|
||||
|
||||
print(f"Wrote: {out_dir / 'index.html'}")
|
||||
print(f"Wrote: {out_dir / 'matrix.json'}")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
cmd = sys.argv[1]
|
||||
items = collect_all()
|
||||
if cmd == "check":
|
||||
return cmd_check(items)
|
||||
if cmd == "publish":
|
||||
out = Path(sys.argv[2] if len(sys.argv) > 2 else "docs/traceability")
|
||||
rc = cmd_check(items)
|
||||
if rc != 0:
|
||||
print("WARN: publishing despite check errors")
|
||||
cmd_publish(items, out)
|
||||
return 0
|
||||
print(f"unknown command: {cmd}")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user