docs(architecture): fix FrameIO / LDF / gen_lin_api layering

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) <noreply@anthropic.com>
This commit is contained in:
Hosam-Eldin Mostafa 2026-05-14 20:15:41 +02:00
parent 7cf74312d6
commit ec218bd5fe

View File

@ -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 loaded LDF — and worse, the test bench would happily ship bytes encoded
against a *stale* LDF baked into the generator's last run. 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
``` A tester has three legitimate ways to drive the bus, all converging at
test code `LinInterface`. They are **parallel paths**, not a single nested stack —
| `FrameIO` deliberately has no static dependency on `ecu_framework/lin/ldf.py`
| AlmReqA.send(fio, AmbLightColourRed=0, ...) (its only `ecu_framework` import is `LinInterface` + `LinFrame` from
v `lin/base.py`), so the `ldf` it receives can be any object with a
tests/hardware/_generated/lin_api.py <-- typed names, compile-time check `.frame(name)` method.
|
| fio.send("ALM_Req_A", AmbLightColourRed=0, ...) ```mermaid
v flowchart TB
tests/hardware/frame_io.py <-- per-instance frame cache T[test code]
|
| ldf.frame("ALM_Req_A").pack(AmbLightColourRed=0, ...) subgraph Paths[three independent ways to address a frame]
v GEN["gen_lin_api typed wrapper<br/>AlmReqA.send&#40;fio, ...&#41;<br/>compile-time name check"]
ecu_framework/lin/ldf.py <-- runtime pack/unpack FIO["FrameIO stringly-typed<br/>fio.send&#40;'ALM_Req_A', ...&#41;<br/>per-instance frame cache"]
| LDFDIRECT["LdfDatabase directly<br/>ldf.frame&#40;'ALM_Req_A'&#41;.pack&#40;...&#41;<br/>returns bytes, no I/O"]
| raw_frame.encode_raw({...}) end
v
ldfparser <-- bit-level layout from LDF on disk T --> GEN
T --> FIO
T --> LDFDIRECT
GEN -.delegates.-> FIO
FIO -.duck-typed lookup.-> LDFOBJ[ldf-like object<br/>currently LdfDatabase]
LDFDIRECT --> LDFOBJ
LDFOBJ --> LDFPARSER[ldfparser - bit layout]
FIO --> LIN[LinInterface.send / receive]
LDFDIRECT -->|caller invokes lin.send<br/>with the packed bytes| LIN
LIN --> WIRE[wire]
``` ```
Each layer's responsibility is unique to that layer; removing either What each path buys you:
collapses a distinct kind of check (compile-time name validation, or
runtime LDF-driven byte layout) that the other layer cannot provide. - **`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 ## Extending the architecture