docs+tools: workflow flowchart harness (P4) + drivers Tier-1/Tier-2 split
- tools/flowgen.py: generates docs/workflows/<name>.md — one outcome-first mermaid flowchart per workflow the SDK tests (hpmcu watchdog, modbus read, cru ladder, rga offload, relay drive, freshness contract), each stamped with its benchmark ns/op or 100% MC/DC result. Deterministic; reads the cargo-bench trend json. - drivers/README.md: honest Tier-1 (relays + freshness, 100% MC/DC here now) vs Tier-2 (modbus/rga — serious testing + fault-injection + benchmarks via sim models; driver sources migrate in with the flare-edge unification) per ADR-0002/0005. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c7c1c70219
commit
880b043506
@@ -0,0 +1,11 @@
|
|||||||
|
# Workflow flowcharts
|
||||||
|
|
||||||
|
Generated by `tools/flowgen.py` from the modelled decision paths.
|
||||||
|
Each is an outcome-first flowchart of a workflow the SDK tests, with its benchmark or MC/DC metric.
|
||||||
|
|
||||||
|
- [HPMCU watchdog: arm → beat → fire](hpmcu-watchdog.md)
|
||||||
|
- [Modbus RTU: read-holding-registers round trip](modbus-read-holding.md)
|
||||||
|
- [CRU reset ladder: cause + boot-mode survival](cru-reset-ladder.md)
|
||||||
|
- [RGA 2D offload dispatch](rga-offload.md)
|
||||||
|
- [Relay drive (Tier-1, 100% MC/DC)](relay-drive.md)
|
||||||
|
- [UI freshness contract (Tier-1, 100% MC/DC)](freshness-contract.md)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# CRU reset ladder: cause + boot-mode survival
|
||||||
|
|
||||||
|
> **Outcome tested:** Reset cause is attributable and the boot-mode register survives a warm reset.
|
||||||
|
|
||||||
|
**Benchmark** (`cru_poll`): 23.0 ns/op
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[poll] --> B{global reset asserted?}
|
||||||
|
B -- no --> A
|
||||||
|
B -- yes --> C[record cause]
|
||||||
|
C --> D{power-on vs warm?}
|
||||||
|
D -- POR --> E[boot-mode cleared]
|
||||||
|
D -- warm --> F[boot-mode preserved]
|
||||||
|
```
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# UI freshness contract (Tier-1, 100% MC/DC)
|
||||||
|
|
||||||
|
> **Outcome tested:** The UI never shows a stale number: it holds briefly, then marks unknown.
|
||||||
|
|
||||||
|
**Coverage**: freshness.c — 66/66 conditions, 100% MC/DC (CI-enforced)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[produce] --> B{result}
|
||||||
|
B -- OK --> V[render value, save last]
|
||||||
|
B -- SAME --> C{showing unknown?}
|
||||||
|
C -- yes --> V
|
||||||
|
C -- no --> N[no change]
|
||||||
|
B -- UNKNOWN --> D{ever had a value?}
|
||||||
|
D -- no --> U[render UNKNOWN mark]
|
||||||
|
D -- yes --> E{age > max_stale?}
|
||||||
|
E -- yes --> U
|
||||||
|
E -- no --> N
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# HPMCU watchdog: arm → beat → fire
|
||||||
|
|
||||||
|
> **Outcome tested:** A hung A7/flared ends in a counted reset, not a dark panel.
|
||||||
|
|
||||||
|
**Benchmark** (`hpmcu_tick`): 1.8 ns/op
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[flared loads SCR1 fw, releases core] --> B[MCU tick]
|
||||||
|
B --> C{mailbox magic == DISARM?}
|
||||||
|
C -- yes --> D[disarmed: never fire]
|
||||||
|
C -- no --> E{magic == ARMED?}
|
||||||
|
E -- no --> B
|
||||||
|
E -- yes --> F{beat counter advanced\nwithin deadline?}
|
||||||
|
F -- yes --> B
|
||||||
|
F -- no --> G[fire CRU global reset]
|
||||||
|
```
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Modbus RTU: read-holding-registers round trip
|
||||||
|
|
||||||
|
> **Outcome tested:** A well-formed request yields the right registers; a bad one a defined fault.
|
||||||
|
|
||||||
|
**Benchmark** (`modbus_read_holding`): 88.0 ns/op
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[frame in] --> B{addr == mine\nor broadcast?}
|
||||||
|
B -- no --> Z[ignore]
|
||||||
|
B -- yes --> C{CRC ok?}
|
||||||
|
C -- no --> Z
|
||||||
|
C -- yes --> D{function code}
|
||||||
|
D -- 0x03 read-holding --> E{range in bounds?}
|
||||||
|
E -- no --> X[exception 0x02]
|
||||||
|
E -- yes --> R[registers response + CRC]
|
||||||
|
D -- unsupported --> X2[exception 0x01]
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Relay drive (Tier-1, 100% MC/DC)
|
||||||
|
|
||||||
|
> **Outcome tested:** A relay is exported transparently and driven without disturbing a held contact.
|
||||||
|
|
||||||
|
**Coverage**: relays.c — 40/40 conditions, 100% MC/DC (CI-enforced)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[warden_relay_set idx,on] --> B{idx < COUNT?}
|
||||||
|
B -- no --> Z[no-op]
|
||||||
|
B -- yes --> C{exported?}
|
||||||
|
C -- no --> D[write export] --> E{exported now?}
|
||||||
|
E -- no --> Z2[give up]
|
||||||
|
E -- yes --> F
|
||||||
|
C -- yes --> F[read direction]
|
||||||
|
F --> G{dir == out?}
|
||||||
|
G -- no --> H[preserve level: read value,\nwrite high/low]
|
||||||
|
G -- yes --> I
|
||||||
|
H --> I[write value = on?1:0]
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# RGA 2D offload dispatch
|
||||||
|
|
||||||
|
> **Outcome tested:** Blits go to the RGA when it succeeds, and fall back to the CPU when it doesn't.
|
||||||
|
|
||||||
|
**Benchmark** (`rga_improcess`): 7.2 ns/op
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[draw request] --> B{RGA compiled in\n(#if WARDEN_USE_RGA)?}
|
||||||
|
B -- no --> C[LVGL software draw]
|
||||||
|
B -- yes --> D[improcess src,dst,rects IM_SYNC]
|
||||||
|
D --> E{IM_STATUS == SUCCESS?}
|
||||||
|
E -- yes --> F[done on RGA]
|
||||||
|
E -- no --> C
|
||||||
|
```
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# drivers/ — our own hardened, hardware-facing drivers
|
||||||
|
|
||||||
|
Per **ADR-0002** (tiered MC/DC) and **ADR-0005** (source-of-truth), our own
|
||||||
|
hardware-facing code migrates here behind a HAL seam and is hardened. "100% MC/DC on
|
||||||
|
100% of drivers" is infeasible (≈97% of kernel-driver LOC is vendor blobs — AIC8800
|
||||||
|
alone is 88.5K lines); the realistic, honest target is tiered.
|
||||||
|
|
||||||
|
## Tier 1 — real 100% MC/DC (here now, CI-enforced)
|
||||||
|
|
||||||
|
Self-contained logic with a clean seam, measured to **100% MC/DC** (gcc-14
|
||||||
|
`-fcondition-coverage`) by the CI `mcdc` job (`make -C drivers/*/test check`):
|
||||||
|
|
||||||
|
| Driver | Seam | MC/DC |
|
||||||
|
|---|---|---|
|
||||||
|
| `relays/` | `relay_io` vtable (sysfs backend + in-memory fake) | 40/40 conditions, 100% |
|
||||||
|
| `freshness/` | produce/render callbacks (the "no stale numbers" guard) | 66/66 conditions, 100% |
|
||||||
|
|
||||||
|
**Adding a Tier-1 driver:** copy `<name>.{c,h}` here, put the hardware/OS calls behind
|
||||||
|
a small injectable seam, then mirror `relays/test/` (a fake backend for the logic
|
||||||
|
branches + a real backend over a scratch tree for the plumbing) and
|
||||||
|
`enforce-mcdc.sh`. The CI job picks up any `drivers/*/test/Makefile` automatically.
|
||||||
|
|
||||||
|
## Tier 2 — serious testing + fault-injection + benchmarks
|
||||||
|
|
||||||
|
Drivers too large or too vendor/UI-coupled for literal MC/DC get fault-injection,
|
||||||
|
branch coverage, and benchmarks against the simulator instead. Their **hardware side
|
||||||
|
is already modelled and tested here** in `../sim/`:
|
||||||
|
|
||||||
|
| Driver | Serious-testing status | SDK model |
|
||||||
|
|---|---|---|
|
||||||
|
| `modbus_engine.c` (RS485 master) | 11 pty scenarios + fault-injection + a compiled corpus walk (flare-edge `tools/modbus-sim/`, green) | `sim::modbus` RTU slave (11 tests, silent-drop/forced-NAK faults) + `modbus_read_holding` benchmark |
|
||||||
|
| `warden_rga.c` (RGA offload) | offload-dispatch + CPU-fallback logic | `sim::rga` recording `improcess` fake (programmable IM_STATUS) + `rga_improcess` benchmark |
|
||||||
|
| HPMCU supervisor (`hpmcu.rs`) | arm/beat/fire + boot-grace safety property | `sim::hpmcu` (8 tests) + `hpmcu_tick` benchmark |
|
||||||
|
|
||||||
|
**Why the Tier-2 *source* isn't vendored here yet:** `modbus_engine.c` and
|
||||||
|
`warden_rga.c` pull in shared UI headers (`platform.h`, `settings.h`, `lv_*`) and
|
||||||
|
librga. Copying those in would duplicate exactly the shared surface the
|
||||||
|
**flare-edge↔warden-sdk unification** (ADR-0003/0005, a separate [maintainer]-gated step) is
|
||||||
|
meant to resolve cleanly. So the Tier-2 *models* (the hardware ends) live here now;
|
||||||
|
the Tier-2 *driver sources* migrate in with the unification, at which point their
|
||||||
|
existing flare-edge harnesses point at this repo.
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""flowgen — generate mermaid flowcharts for the workflows the SDK tests.
|
||||||
|
|
||||||
|
future-features-2 asks that the test harness "produce flowcharts of every workflow
|
||||||
|
and process that it tests so a user can understand them better", each carrying its
|
||||||
|
benchmark. This emits one `docs/workflows/<name>.md` per workflow: an outcome-first
|
||||||
|
flowchart (from the modelled decision path) plus the workflow's metric — a benchmark
|
||||||
|
ns/op for the sim-modelled hardware workflows, or the MC/DC result for the Tier-1
|
||||||
|
driver workflows.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
tools/flowgen.py [bench.json] # bench.json = the `cargo bench` stderr trend
|
||||||
|
Reads the ns/op trend JSON if given (or sim/bench.json if present) and stamps it in.
|
||||||
|
Deterministic: same inputs -> same output (safe to run in CI and diff).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
REPO = os.path.dirname(HERE)
|
||||||
|
OUT = os.path.join(REPO, "docs", "workflows")
|
||||||
|
|
||||||
|
# Each workflow: an outcome, the mermaid body, and its metric source.
|
||||||
|
# metric = ("bench", key) -> ns/op from the trend json
|
||||||
|
# metric = ("mcdc", text) -> a Tier-1 MC/DC result line
|
||||||
|
WORKFLOWS = [
|
||||||
|
{
|
||||||
|
"name": "hpmcu-watchdog",
|
||||||
|
"title": "HPMCU watchdog: arm → beat → fire",
|
||||||
|
"outcome": "A hung A7/flared ends in a counted reset, not a dark panel.",
|
||||||
|
"metric": ("bench", "hpmcu_tick"),
|
||||||
|
"mermaid": """flowchart TD
|
||||||
|
A[flared loads SCR1 fw, releases core] --> B[MCU tick]
|
||||||
|
B --> C{mailbox magic == DISARM?}
|
||||||
|
C -- yes --> D[disarmed: never fire]
|
||||||
|
C -- no --> E{magic == ARMED?}
|
||||||
|
E -- no --> B
|
||||||
|
E -- yes --> F{beat counter advanced\\nwithin deadline?}
|
||||||
|
F -- yes --> B
|
||||||
|
F -- no --> G[fire CRU global reset]""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "modbus-read-holding",
|
||||||
|
"title": "Modbus RTU: read-holding-registers round trip",
|
||||||
|
"outcome": "A well-formed request yields the right registers; a bad one a defined fault.",
|
||||||
|
"metric": ("bench", "modbus_read_holding"),
|
||||||
|
"mermaid": """flowchart TD
|
||||||
|
A[frame in] --> B{addr == mine\\nor broadcast?}
|
||||||
|
B -- no --> Z[ignore]
|
||||||
|
B -- yes --> C{CRC ok?}
|
||||||
|
C -- no --> Z
|
||||||
|
C -- yes --> D{function code}
|
||||||
|
D -- 0x03 read-holding --> E{range in bounds?}
|
||||||
|
E -- no --> X[exception 0x02]
|
||||||
|
E -- yes --> R[registers response + CRC]
|
||||||
|
D -- unsupported --> X2[exception 0x01]""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cru-reset-ladder",
|
||||||
|
"title": "CRU reset ladder: cause + boot-mode survival",
|
||||||
|
"outcome": "Reset cause is attributable and the boot-mode register survives a warm reset.",
|
||||||
|
"metric": ("bench", "cru_poll"),
|
||||||
|
"mermaid": """flowchart TD
|
||||||
|
A[poll] --> B{global reset asserted?}
|
||||||
|
B -- no --> A
|
||||||
|
B -- yes --> C[record cause]
|
||||||
|
C --> D{power-on vs warm?}
|
||||||
|
D -- POR --> E[boot-mode cleared]
|
||||||
|
D -- warm --> F[boot-mode preserved]""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rga-offload",
|
||||||
|
"title": "RGA 2D offload dispatch",
|
||||||
|
"outcome": "Blits go to the RGA when it succeeds, and fall back to the CPU when it doesn't.",
|
||||||
|
"metric": ("bench", "rga_improcess"),
|
||||||
|
"mermaid": """flowchart TD
|
||||||
|
A[draw request] --> B{RGA compiled in\\n(#if WARDEN_USE_RGA)?}
|
||||||
|
B -- no --> C[LVGL software draw]
|
||||||
|
B -- yes --> D[improcess src,dst,rects IM_SYNC]
|
||||||
|
D --> E{IM_STATUS == SUCCESS?}
|
||||||
|
E -- yes --> F[done on RGA]
|
||||||
|
E -- no --> C""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "relay-drive",
|
||||||
|
"title": "Relay drive (Tier-1, 100% MC/DC)",
|
||||||
|
"outcome": "A relay is exported transparently and driven without disturbing a held contact.",
|
||||||
|
"metric": ("mcdc", "relays.c — 40/40 conditions, 100% MC/DC (CI-enforced)"),
|
||||||
|
"mermaid": """flowchart TD
|
||||||
|
A[warden_relay_set idx,on] --> B{idx < COUNT?}
|
||||||
|
B -- no --> Z[no-op]
|
||||||
|
B -- yes --> C{exported?}
|
||||||
|
C -- no --> D[write export] --> E{exported now?}
|
||||||
|
E -- no --> Z2[give up]
|
||||||
|
E -- yes --> F
|
||||||
|
C -- yes --> F[read direction]
|
||||||
|
F --> G{dir == out?}
|
||||||
|
G -- no --> H[preserve level: read value,\\nwrite high/low]
|
||||||
|
G -- yes --> I
|
||||||
|
H --> I[write value = on?1:0]""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "freshness-contract",
|
||||||
|
"title": "UI freshness contract (Tier-1, 100% MC/DC)",
|
||||||
|
"outcome": "The UI never shows a stale number: it holds briefly, then marks unknown.",
|
||||||
|
"metric": ("mcdc", "freshness.c — 66/66 conditions, 100% MC/DC (CI-enforced)"),
|
||||||
|
"mermaid": """flowchart TD
|
||||||
|
A[produce] --> B{result}
|
||||||
|
B -- OK --> V[render value, save last]
|
||||||
|
B -- SAME --> C{showing unknown?}
|
||||||
|
C -- yes --> V
|
||||||
|
C -- no --> N[no change]
|
||||||
|
B -- UNKNOWN --> D{ever had a value?}
|
||||||
|
D -- no --> U[render UNKNOWN mark]
|
||||||
|
D -- yes --> E{age > max_stale?}
|
||||||
|
E -- yes --> U
|
||||||
|
E -- no --> N""",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_bench(argv):
|
||||||
|
path = argv[1] if len(argv) > 1 else os.path.join(REPO, "sim", "bench.json")
|
||||||
|
out = {}
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("{") and '"bench"' in line:
|
||||||
|
try:
|
||||||
|
d = json.loads(line)
|
||||||
|
out[d["bench"]] = d["ns_per_op"]
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def metric_line(metric, bench):
|
||||||
|
kind, val = metric
|
||||||
|
if kind == "bench":
|
||||||
|
ns = bench.get(val)
|
||||||
|
shown = f"{ns:.1f} ns/op" if ns is not None else "(run `cargo bench` to populate)"
|
||||||
|
return f"**Benchmark** (`{val}`): {shown}"
|
||||||
|
return f"**Coverage**: {val}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
bench = load_bench(sys.argv)
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
index = ["# Workflow flowcharts", "",
|
||||||
|
"Generated by `tools/flowgen.py` from the modelled decision paths.",
|
||||||
|
"Each is an outcome-first flowchart of a workflow the SDK tests, with its"
|
||||||
|
" benchmark or MC/DC metric.", ""]
|
||||||
|
for w in WORKFLOWS:
|
||||||
|
body = (f"# {w['title']}\n\n"
|
||||||
|
f"> **Outcome tested:** {w['outcome']}\n\n"
|
||||||
|
f"{metric_line(w['metric'], bench)}\n\n"
|
||||||
|
f"```mermaid\n{w['mermaid']}\n```\n")
|
||||||
|
with open(os.path.join(OUT, w["name"] + ".md"), "w") as f:
|
||||||
|
f.write(body)
|
||||||
|
index.append(f"- [{w['title']}]({w['name']}.md)")
|
||||||
|
with open(os.path.join(OUT, "README.md"), "w") as f:
|
||||||
|
f.write("\n".join(index) + "\n")
|
||||||
|
print(f"wrote {len(WORKFLOWS)} flowcharts + index to {os.path.relpath(OUT, REPO)}/")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user