From ec218bd5fe9b2941c7b92d1097a682bc58ab9983 Mon Sep 17 00:00:00 2001 From: Hosam-Eldin Mostafa Date: Thu, 14 May 2026 20:15:41 +0200 Subject: [PATCH] docs(architecture): fix FrameIO / LDF / gen_lin_api layering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous ASCII pipeline implied a single linear stack from gen_lin_api down through FrameIO down through ecu_framework/lin/ldf.py — and showed a static dependency from FrameIO to that module. Both are wrong. What the code actually says (tests/hardware/frame_io.py:34): from ecu_framework.lin.base import LinFrame, LinInterface That's the only ecu_framework import in FrameIO. The `ldf` constructor parameter is duck-typed — FrameIO never imports LdfDatabase and would work against any object exposing `.frame(name)`. So `frame_io → lin/ldf` is an injected runtime call, not a module dependency. Replace the linear ASCII diagram with a Mermaid parallel-paths diagram that surfaces the three independent ways a tester can address a frame: - gen_lin_api typed wrapper (compile-time name check) - FrameIO stringly-typed I/O (with raw send_raw/receive_raw escape hatches that don't touch the ldf object at all) - LdfDatabase used directly (schema-only — pack to bytes, no I/O) …all converging at LinInterface. The prose around the diagram is rewritten to match: each path's affordance, and what concrete capability is lost by removing any of the three. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/05_architecture_overview.md | 77 +++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/docs/05_architecture_overview.md b/docs/05_architecture_overview.md index 081e470..2d329d5 100644 --- a/docs/05_architecture_overview.md +++ b/docs/05_architecture_overview.md @@ -181,31 +181,64 @@ have no path from a frame name to the actual byte layout for the currently loaded LDF — and worse, the test bench would happily ship bytes encoded against a *stale* LDF baked into the generator's last run. -Concretely, a single `fio.send` call traverses both layers: +### Three independent entry points, one wire -``` -test code - | - | AlmReqA.send(fio, AmbLightColourRed=0, ...) - v -tests/hardware/_generated/lin_api.py <-- typed names, compile-time check - | - | fio.send("ALM_Req_A", AmbLightColourRed=0, ...) - v -tests/hardware/frame_io.py <-- per-instance frame cache - | - | ldf.frame("ALM_Req_A").pack(AmbLightColourRed=0, ...) - v -ecu_framework/lin/ldf.py <-- runtime pack/unpack - | - | raw_frame.encode_raw({...}) - v -ldfparser <-- bit-level layout from LDF on disk +A tester has three legitimate ways to drive the bus, all converging at +`LinInterface`. They are **parallel paths**, not a single nested stack — +`FrameIO` deliberately has no static dependency on `ecu_framework/lin/ldf.py` +(its only `ecu_framework` import is `LinInterface` + `LinFrame` from +`lin/base.py`), so the `ldf` it receives can be any object with a +`.frame(name)` method. + +```mermaid +flowchart TB + T[test code] + + subgraph Paths[three independent ways to address a frame] + GEN["gen_lin_api typed wrapper
AlmReqA.send(fio, ...)
compile-time name check"] + FIO["FrameIO stringly-typed
fio.send('ALM_Req_A', ...)
per-instance frame cache"] + LDFDIRECT["LdfDatabase directly
ldf.frame('ALM_Req_A').pack(...)
returns bytes, no I/O"] + end + + T --> GEN + T --> FIO + T --> LDFDIRECT + + GEN -.delegates.-> FIO + FIO -.duck-typed lookup.-> LDFOBJ[ldf-like object
currently LdfDatabase] + LDFDIRECT --> LDFOBJ + LDFOBJ --> LDFPARSER[ldfparser - bit layout] + + FIO --> LIN[LinInterface.send / receive] + LDFDIRECT -->|caller invokes lin.send
with the packed bytes| LIN + LIN --> WIRE[wire] ``` -Each layer's responsibility is unique to that layer; removing either -collapses a distinct kind of check (compile-time name validation, or -runtime LDF-driven byte layout) that the other layer cannot provide. +What each path buys you: + +- **`gen_lin_api`** — compile-time name validation. Typo a frame or signal + name and the IDE / mypy / pytest collection rejects it before any LDF + is read. Delegates the actual packing to `fio.send`. +- **`FrameIO`** — stringly-typed I/O over the wire. Caches frame + lookups, supports raw escape hatches (`send_raw` / `receive_raw`) that + bypass the LDF object entirely. +- **`LdfDatabase` directly** — schema-only access. Useful when a test + wants to inspect frame layout, pack a buffer without sending, or hand + the bytes to a non-FrameIO transport. + +The LDF object (currently `LdfDatabase`) is consumed by both `FrameIO` +and any direct-use code path. `FrameIO`'s use is via injection — it +never imports `LdfDatabase` and can be tested against a stub. + +Removing any of the three entry points collapses a distinct affordance: + +- Drop `gen_lin_api` → tests keep stringly-typed `fio.send("ALM_Req_A", …)` + and hand-copied state constants, both of which silently drift when the + LDF changes. +- Drop `FrameIO` → every test that wants high-level I/O has to wire + `LinInterface` + LDF lookup + pack/unpack itself. +- Drop direct `LdfDatabase` usage → tests can no longer pack a frame + without sending it, or inspect frame metadata without an I/O attempt. ## Extending the architecture