ecu-tests/docs/17_ldf_parser.md

180 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LDF Parser & Frame Helpers
The framework parses your LDF (LIN Description File) at session start and
exposes a typed `LdfDatabase` to tests. Tests then build and decode frames
by **signal name**, never by hand-counting bit positions.
## Why
Hard-coded frame layouts (the `ALM_REQ_A_FRAME = {...}` style in
`vendor/automated_lin_test/config.py`) drift the moment the LDF changes.
Loading the LDF directly removes the drift and gives you a pleasant API:
```python
def test_x(lin, ldf):
req = ldf.frame("ALM_Req_A")
payload = req.pack(
AmbLightColourRed=0xFF, AmbLightColourGreen=0xFF,
AmbLightColourBlue=0xFF, AmbLightIntensity=0xFF,
AmbLightLIDFrom=nad, AmbLightLIDTo=nad,
)
lin.send(LinFrame(id=req.id, data=payload))
raw = lin.receive(id=ldf.frame("ALM_Status").id, timeout=1.0)
sig = ldf.frame("ALM_Status").unpack(bytes(raw.data))
assert sig["ALMNadNo"] == nad
```
## Where it lives
- Parser wrapper: `ecu_framework/lin/ldf.py`
- Test fixture: `ldf` (session-scoped, in `tests/conftest.py`)
- Underlying library: [`ldfparser`](https://pypi.org/project/ldfparser/) (pure-Python, MIT)
- LDF location is read from `interface.ldf_path` in YAML
- Unit tests against `vendor/4SEVEN_color_lib_test.ldf`: `tests/unit/test_ldf_database.py`
## Configuration
Set `interface.ldf_path` (relative paths resolve against the workspace root):
```yaml
interface:
type: mum
host: 192.168.7.2
bitrate: 19200
ldf_path: ./vendor/4SEVEN_color_lib_test.ldf
# frame_lengths is optional: any keys here override the LDF on a
# per-frame-id basis. Leave empty to inherit everything from the LDF.
frame_lengths: {}
```
When `ldf_path` is set, the `lin` fixture also feeds the LDF's
`{frame_id: length}` map into `MumLinInterface(frame_lengths=...)`, so
`lin.receive(id=...)` knows the right number of bytes to ask for **for
every frame in the LDF** — no per-id bookkeeping required.
## API
### `LdfDatabase`
```python
from ecu_framework.lin.ldf import LdfDatabase
db = LdfDatabase("./vendor/4SEVEN_color_lib_test.ldf")
db.protocol_version # "2.1"
db.baudrate # 19200
db.frame("ALM_Req_A") # by name
db.frame(0x0A) # by frame_id
db.frames() # list[Frame]
db.frame_lengths() # {frame_id: length} — drop into MumLinInterface
db.signal_names("ALM_Req_A") # ['AmbLightColourRed', ...]
```
`db.frame(...)` raises `FrameNotFound` (a `KeyError` subclass) if the name
or ID isn't present; missing files raise `FileNotFoundError`.
### `Frame`
```python
frame = db.frame("ALM_Req_A")
frame.id # 0x0A (int)
frame.name # "ALM_Req_A"
frame.length # 8
frame.signal_names() # ['AmbLightColourRed', ...]
frame.signal_layout() # [(start_bit, name, width), ...]
# Raw integer pack/unpack — use this for tests that work in raw values.
payload = frame.pack(AmbLightColourRed=255, AmbLightColourGreen=128)
payload = frame.pack({"AmbLightColourRed": 255}) # dict form is fine too
decoded = frame.unpack(payload) # {'AmbLightColourRed': 255, ...}
# Encoding-aware variant (logical/physical values from the LDF) — use this
# if you want to write `AmbLightUpdate="Immediate color Update"`:
encoded = frame.encode({"AmbLightUpdate": "Immediate color Update", ...})
decoded = frame.decode(encoded)
```
### Default values
`pack()` doesn't require every signal — anything you omit takes the
**`init_value` declared in the LDF**. For example, `ColorConfigFrameRed`'s
`_X` signal has `init_value = 5665`, so `frame.pack()` with no kwargs
produces a payload that decodes back to that value:
```python
db.frame("ColorConfigFrameRed").unpack(db.frame("ColorConfigFrameRed").pack())
# → {'ColorConfigFrameRed_X': 5665, 'ColorConfigFrameRed_Y': 2396, ...}
```
This means you can usually pass only the signals the test cares about and
let the LDF supply sensible defaults for the rest.
## The `ldf` fixture
`tests/conftest.py` provides a session-scoped `ldf` fixture that:
1. Reads `interface.ldf_path` from config.
2. Resolves it against the workspace root if relative.
3. Skips the test cleanly with a clear message if the path is missing,
the file isn't there, or `ldfparser` isn't installed.
4. Returns an `LdfDatabase`.
A test that needs LDF-defined frames simply requests it:
```python
def test_thing(lin, ldf):
payload = ldf.frame("ALM_Req_A").pack(AmbLightColourRed=0xFF)
lin.send(LinFrame(id=ldf.frame("ALM_Req_A").id, data=payload))
```
Tests that don't need LDF can ignore the fixture; nothing is loaded
unless the fixture is requested.
## Switching between raw and encoded values
| Use this | When |
| --- | --- |
| `frame.pack(**raw_ints) / frame.unpack(bytes)` | You're writing test logic against numeric signal values (most assertions). |
| `frame.encode(values_dict) / frame.decode(bytes)` | You want LDF logical names (`"Immediate color Update"`) or scaled physical values (e.g. `AmbLightDuration` is `value × 0.2 s`). |
Both round-trip through the same byte representation; the difference is
purely how the values look in Python.
## Common pitfalls
- **Frame ID ranges**: `LinFrame` validates IDs as 0x00..0x3F (LIN classic 6-bit). `ldfparser` returns IDs in this range for normal frames; diagnostic frames (`MasterReq=0x3C`, `SlaveResp=0x3D`) are also accepted. If you ever see an out-of-range ID, you're probably looking at an event-triggered frame's collision resolution table — not a real bus ID.
- **Bit ordering**: LDF and ldfparser both use the LIN-standard little-endian bit ordering within bytes. The framework's `Frame.pack()` matches the existing hand-rolled `vendor/automated_lin_test/config.py:pack_frame()` byte-for-byte for the 4SEVEN file.
- **`encode` vs `encode_raw`**: ldfparser's `encode()` insists on encoded values (`"Immediate color Update"` not `0`). Our `Frame.pack()` uses `encode_raw()` instead, so kwargs are integers. If you need encoded names, use `Frame.encode(dict)` explicitly.
## Migration from hardcoded frames
If you have tests that import the dicts in `vendor/automated_lin_test/config.py`
(`ALM_REQ_A_FRAME`, etc.) and call its `pack_frame` / `unpack_frame`, they
keep working — the new system is additive. To migrate a test:
```python
# Before
from config import ALM_REQ_A_FRAME, pack_frame
data = pack_frame(ALM_REQ_A_FRAME, AmbLightColourRed=255, ...)
lin.send_message(master_to_slave=True, frame_id=ALM_REQ_A_FRAME['frame_id'],
data_length=ALM_REQ_A_FRAME['length'], data=data)
# After
def test(lin, ldf):
f = ldf.frame("ALM_Req_A")
lin.send(LinFrame(id=f.id, data=f.pack(AmbLightColourRed=255, ...)))
```
## Related
- `docs/02_configuration_resolution.md``interface.ldf_path` schema
- `docs/04_lin_interface_call_flow.md` — how MUM uses `frame_lengths`
- `docs/16_mum_internals.md` — MUM adapter internals (the `ldf` fixture is the recommended source for `frame_lengths` rather than hand-maintained YAML)
- `vendor/4SEVEN_color_lib_test.ldf` — the LDF used as test fixture