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
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<br/>AlmReqA.send&#40;fio, ...&#41;<br/>compile-time name check"]
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"]
end
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
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