From 880b043506bf0e0453859b10a97d8077d819a730 Mon Sep 17 00:00:00 2001 From: BFE Engineering Date: Tue, 25 Aug 2026 15:22:57 -0600 Subject: [PATCH] docs+tools: workflow flowchart harness (P4) + drivers Tier-1/Tier-2 split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools/flowgen.py: generates docs/workflows/.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 Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf --- docs/workflows/README.md | 11 ++ docs/workflows/cru-reset-ladder.md | 15 +++ docs/workflows/freshness-contract.md | 19 +++ docs/workflows/hpmcu-watchdog.md | 17 +++ docs/workflows/modbus-read-holding.md | 18 +++ docs/workflows/relay-drive.md | 20 +++ docs/workflows/rga-offload.md | 15 +++ drivers/README.md | 41 +++++++ tools/flowgen.py | 169 ++++++++++++++++++++++++++ 9 files changed, 325 insertions(+) create mode 100644 docs/workflows/README.md create mode 100644 docs/workflows/cru-reset-ladder.md create mode 100644 docs/workflows/freshness-contract.md create mode 100644 docs/workflows/hpmcu-watchdog.md create mode 100644 docs/workflows/modbus-read-holding.md create mode 100644 docs/workflows/relay-drive.md create mode 100644 docs/workflows/rga-offload.md create mode 100644 drivers/README.md create mode 100644 tools/flowgen.py diff --git a/docs/workflows/README.md b/docs/workflows/README.md new file mode 100644 index 0000000..6c4ddd3 --- /dev/null +++ b/docs/workflows/README.md @@ -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) diff --git a/docs/workflows/cru-reset-ladder.md b/docs/workflows/cru-reset-ladder.md new file mode 100644 index 0000000..0752226 --- /dev/null +++ b/docs/workflows/cru-reset-ladder.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] +``` diff --git a/docs/workflows/freshness-contract.md b/docs/workflows/freshness-contract.md new file mode 100644 index 0000000..2467cbd --- /dev/null +++ b/docs/workflows/freshness-contract.md @@ -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 +``` diff --git a/docs/workflows/hpmcu-watchdog.md b/docs/workflows/hpmcu-watchdog.md new file mode 100644 index 0000000..be7721b --- /dev/null +++ b/docs/workflows/hpmcu-watchdog.md @@ -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] +``` diff --git a/docs/workflows/modbus-read-holding.md b/docs/workflows/modbus-read-holding.md new file mode 100644 index 0000000..127c781 --- /dev/null +++ b/docs/workflows/modbus-read-holding.md @@ -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] +``` diff --git a/docs/workflows/relay-drive.md b/docs/workflows/relay-drive.md new file mode 100644 index 0000000..e45cb62 --- /dev/null +++ b/docs/workflows/relay-drive.md @@ -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] +``` diff --git a/docs/workflows/rga-offload.md b/docs/workflows/rga-offload.md new file mode 100644 index 0000000..727ae17 --- /dev/null +++ b/docs/workflows/rga-offload.md @@ -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 +``` diff --git a/drivers/README.md b/drivers/README.md new file mode 100644 index 0000000..dba9780 --- /dev/null +++ b/drivers/README.md @@ -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 `.{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. diff --git a/tools/flowgen.py b/tools/flowgen.py new file mode 100644 index 0000000..330a466 --- /dev/null +++ b/tools/flowgen.py @@ -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/.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()