180 lines
6.9 KiB
Markdown
180 lines
6.9 KiB
Markdown
# 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
|