6.9 KiB
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:
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, intests/conftest.py) - Underlying library:
ldfparser(pure-Python, MIT) - LDF location is read from
interface.ldf_pathin 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):
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
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
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:
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:
- Reads
interface.ldf_pathfrom config. - Resolves it against the workspace root if relative.
- Skips the test cleanly with a clear message if the path is missing,
the file isn't there, or
ldfparserisn't installed. - Returns an
LdfDatabase.
A test that needs LDF-defined frames simply requests it:
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:
LinFramevalidates IDs as 0x00..0x3F (LIN classic 6-bit).ldfparserreturns 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-rolledvendor/automated_lin_test/config.py:pack_frame()byte-for-byte for the 4SEVEN file. encodevsencode_raw: ldfparser'sencode()insists on encoded values ("Immediate color Update"not0). OurFrame.pack()usesencode_raw()instead, so kwargs are integers. If you need encoded names, useFrame.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:
# 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_pathschemadocs/04_lin_interface_call_flow.md— how MUM usesframe_lengthsdocs/16_mum_internals.md— MUM adapter internals (theldffixture is the recommended source forframe_lengthsrather than hand-maintained YAML)vendor/4SEVEN_color_lib_test.ldf— the LDF used as test fixture