add ldf parser
This commit is contained in:
parent
0656f3a0e1
commit
a10187844a
@ -13,11 +13,12 @@ interface:
|
||||
power_device: power_out0 # MUM power-control device
|
||||
bitrate: 19200 # LIN baudrate
|
||||
boot_settle_seconds: 0.5 # Delay after power-up before first frame
|
||||
# Optional: per-frame-id data lengths. Defaults cover the 4SEVEN library
|
||||
# (ALM_Status=4, ALM_Req_A=8, etc.) — only override if your ECU differs.
|
||||
frame_lengths:
|
||||
0x0A: 8 # ALM_Req_A
|
||||
0x11: 4 # ALM_Status
|
||||
# Path to an LDF; auto-populates frame_lengths and is exposed to tests
|
||||
# via the `ldf` fixture (db.frame("ALM_Req_A").pack(...) etc.).
|
||||
ldf_path: ./vendor/4SEVEN_color_lib_test.ldf
|
||||
# Optional per-frame-id data lengths. When ldf_path is set, anything here
|
||||
# only acts as an override on top of the LDF lengths.
|
||||
frame_lengths: {}
|
||||
|
||||
flash:
|
||||
enabled: false
|
||||
|
||||
@ -7,9 +7,12 @@ interface:
|
||||
power_device: power_out0 # MUM power-control device (built-in PSU)
|
||||
bitrate: 19200 # LIN baudrate
|
||||
boot_settle_seconds: 0.5 # Wait after power-up before sending the first frame
|
||||
frame_lengths:
|
||||
0x0A: 8 # ALM_Req_A (master-published, RGB control)
|
||||
0x11: 4 # ALM_Status (slave-published)
|
||||
# Path to an LDF (LIN description file). When set, tests can use the
|
||||
# `ldf` fixture to pack/unpack frames by signal name, and the MUM adapter
|
||||
# auto-populates frame_lengths from the LDF (any keys you add below
|
||||
# override the LDF on a per-frame-id basis).
|
||||
ldf_path: ./vendor/4SEVEN_color_lib_test.ldf
|
||||
frame_lengths: {} # leave empty unless you need a non-LDF override
|
||||
|
||||
# --- BabyLIN (legacy) settings, used only when type: babylin ---
|
||||
channel: 0
|
||||
|
||||
@ -26,7 +26,8 @@ From highest to lowest precedence:
|
||||
- `lin_device`: MUM LIN device name (MUM-only, default `lin0`)
|
||||
- `power_device`: MUM power-control device (MUM-only, default `power_out0`)
|
||||
- `boot_settle_seconds`: Delay after MUM power-up before sending the first frame (default 0.5)
|
||||
- `frame_lengths`: Optional `{frame_id: data_length}` map for the MUM adapter to drive slave-published reads. Hex keys like `0x0A` are supported in YAML
|
||||
- `frame_lengths`: Optional `{frame_id: data_length}` map for the MUM adapter to drive slave-published reads. Hex keys like `0x0A` are supported in YAML. When `ldf_path` is set, this acts as an override on top of LDF-derived lengths.
|
||||
- `ldf_path`: Optional path to a `.ldf` file. Tests can request the `ldf` fixture to obtain an `LdfDatabase` for per-frame `pack`/`unpack`; the MUM adapter additionally inherits frame lengths from the LDF. Relative paths resolve against the workspace root
|
||||
- `flash: FlashConfig`
|
||||
- `enabled`: whether to flash before tests
|
||||
- `hex_path`: path to HEX file
|
||||
|
||||
@ -11,6 +11,7 @@ This document provides a high-level view of the framework’s components and how
|
||||
- Mock LIN Adapter — `ecu_framework/lin/mock.py`
|
||||
- MUM LIN Adapter — `ecu_framework/lin/mum.py` (Melexis Universal Master via `pylin` + `pymumclient`)
|
||||
- BabyLIN Adapter — `ecu_framework/lin/babylin.py` (SDK wrapper → BabyLIN_library.py; legacy)
|
||||
- LDF Database — `ecu_framework/lin/ldf.py` (`LdfDatabase`/`Frame` over `ldfparser`; per-frame `pack`/`unpack`)
|
||||
- Flasher — `ecu_framework/flashing/hex_flasher.py`
|
||||
- Power Supply (PSU) control — `ecu_framework/power/owon_psu.py` (serial SCPI)
|
||||
- PSU quick demo script — `vendor/Owon/owon_psu_quick_demo.py`
|
||||
@ -33,6 +34,7 @@ flowchart TB
|
||||
MOCK[ecu_framework/lin/mock.py]
|
||||
MUM[ecu_framework/lin/mum.py]
|
||||
BABY[ecu_framework/lin/babylin.py]
|
||||
LDF[ecu_framework/lin/ldf.py]
|
||||
FLASH[ecu_framework/flashing/hex_flasher.py]
|
||||
POWER[ecu_framework/power/owon_psu.py]
|
||||
end
|
||||
@ -44,6 +46,8 @@ flowchart TB
|
||||
MELEXIS[Melexis pylin + pymumclient<br/>MUM @ 192.168.7.2]
|
||||
SDK[vendor/BabyLIN_library.py<br/>platform-specific libs]
|
||||
OWON[vendor/Owon/owon_psu_quick_demo.py]
|
||||
LDFFILE[vendor/*.ldf]
|
||||
LDFLIB[ldfparser PyPI]
|
||||
end
|
||||
|
||||
T --> CF
|
||||
@ -54,12 +58,15 @@ flowchart TB
|
||||
CF --> BABY
|
||||
CF --> FLASH
|
||||
T --> POWER
|
||||
T --> LDF
|
||||
PL --> REP
|
||||
|
||||
CFG --> YAML
|
||||
CFG --> PSU_YAML
|
||||
MUM --> MELEXIS
|
||||
BABY --> SDK
|
||||
LDF --> LDFLIB
|
||||
LDF --> LDFFILE
|
||||
T --> OWON
|
||||
T --> REP
|
||||
```
|
||||
|
||||
@ -12,13 +12,15 @@ A guided tour of the ECU testing framework. Start here:
|
||||
8. `07_flash_sequence.md` — ECU flashing workflow and sequence diagram
|
||||
9. `08_babylin_internals.md` — BabyLIN SDK wrapper internals and call flow (legacy)
|
||||
10. `16_mum_internals.md` — MUM (Melexis Universal Master) adapter internals and call flow
|
||||
11. `DEVELOPER_COMMIT_GUIDE.md` — What to commit vs ignore, commands
|
||||
12. `09_raspberry_pi_deployment.md` — Run on Raspberry Pi (venv, service, hardware notes)
|
||||
13. `10_build_custom_image.md` — Build a custom Raspberry Pi OS image with the framework baked in
|
||||
14. `12_using_the_framework.md` — Practical usage: local, hardware (MUM/BabyLIN), CI, and Pi
|
||||
15. `13_unit_testing_guide.md` — Unit tests layout, markers, coverage, and tips
|
||||
16. `14_power_supply.md` — Owon PSU control, configuration, tests, and quick demo script
|
||||
17. `15_report_properties_cheatsheet.md` — Standardized keys for record_property/rp across suites
|
||||
11. `17_ldf_parser.md` — LDF parser, `ldf` fixture, and per-frame `pack`/`unpack` helpers
|
||||
12. `18_test_catalog.md` — Per-test catalog: purpose, markers, hardware needs, expected result
|
||||
13. `DEVELOPER_COMMIT_GUIDE.md` — What to commit vs ignore, commands
|
||||
14. `09_raspberry_pi_deployment.md` — Run on Raspberry Pi (venv, service, hardware notes)
|
||||
15. `10_build_custom_image.md` — Build a custom Raspberry Pi OS image with the framework baked in
|
||||
16. `12_using_the_framework.md` — Practical usage: local, hardware (MUM/BabyLIN), CI, and Pi
|
||||
17. `13_unit_testing_guide.md` — Unit tests layout, markers, coverage, and tips
|
||||
18. `14_power_supply.md` — Owon PSU control, configuration, tests, and quick demo script
|
||||
19. `15_report_properties_cheatsheet.md` — Standardized keys for record_property/rp across suites
|
||||
|
||||
Related references:
|
||||
|
||||
|
||||
@ -56,6 +56,9 @@ class InterfaceConfig:
|
||||
power_device: str = "power_out0"
|
||||
boot_settle_seconds: float = 0.5
|
||||
frame_lengths: Dict[int, int] = field(default_factory=dict)
|
||||
# Optional LDF path; when set, tests/fixtures can load an LdfDatabase
|
||||
# and the MUM adapter auto-merges the LDF's frame lengths into its map.
|
||||
ldf_path: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -157,6 +160,7 @@ def _to_dataclass(cfg: Dict[str, Any]) -> EcuTestConfig:
|
||||
power_device=str(iface.get("power_device", "power_out0")),
|
||||
boot_settle_seconds=float(iface.get("boot_settle_seconds", 0.5)),
|
||||
frame_lengths=frame_lengths,
|
||||
ldf_path=iface.get("ldf_path"),
|
||||
),
|
||||
flash=FlashConfig(
|
||||
enabled=bool(flash.get("enabled", False)), # Coerce to bool
|
||||
|
||||
@ -26,6 +26,7 @@ markers =
|
||||
req_004: REQ-004 - Mock interface shall handle timeout scenarios gracefully
|
||||
smoke: Basic functionality validation tests
|
||||
boundary: Boundary condition and edge case tests
|
||||
slow: Slow tests (>5s typical); selectable via -m "slow" or excludable via -m "not slow"
|
||||
|
||||
# testpaths: Where pytest looks for tests by default.
|
||||
testpaths = tests
|
||||
|
||||
@ -11,6 +11,9 @@ pytest-xdist>=3.6,<4 # Parallel test execution (e.g., pytest -n auto)
|
||||
pytest-html>=4,<5 # Generate HTML test reports for CI and sharing
|
||||
pytest-cov>=5,<6 # Coverage reports for Python packages
|
||||
|
||||
# LDF parsing (LIN description file → frame/signal database for tests)
|
||||
ldfparser>=0.26,<1 # Pure-Python LDF 1.x/2.x parser; pulls in lark + bitstruct
|
||||
|
||||
# Logging and config extras
|
||||
configparser>=6,<7 # Optional INI-based config support if you add .ini configs later
|
||||
colorlog>=6,<7 # Colored logging output for readable test logs
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
import pytest
|
||||
@ -50,13 +51,26 @@ def lin(config: EcuTestConfig) -> t.Iterator[LinInterface]:
|
||||
pytest.skip("MUM interface not available in this environment")
|
||||
if not config.interface.host:
|
||||
pytest.skip("interface.host is required when interface.type == 'mum'")
|
||||
# Merge frame lengths: LDF (if any) provides defaults; YAML
|
||||
# `frame_lengths` overrides on a per-id basis.
|
||||
merged_lengths: dict = {}
|
||||
if config.interface.ldf_path:
|
||||
try:
|
||||
from ecu_framework.lin.ldf import LdfDatabase
|
||||
merged_lengths.update(LdfDatabase(config.interface.ldf_path).frame_lengths())
|
||||
except Exception as e:
|
||||
# Don't fail connect just because the LDF couldn't be parsed —
|
||||
# the `ldf` fixture will surface the real error if a test asks.
|
||||
sys.stderr.write(f"[lin fixture] LDF load failed, ignoring: {e!r}\n")
|
||||
if config.interface.frame_lengths:
|
||||
merged_lengths.update(config.interface.frame_lengths)
|
||||
lin = MumLinInterface(
|
||||
host=config.interface.host,
|
||||
lin_device=config.interface.lin_device,
|
||||
power_device=config.interface.power_device,
|
||||
baudrate=config.interface.bitrate,
|
||||
boot_settle_seconds=config.interface.boot_settle_seconds,
|
||||
frame_lengths=config.interface.frame_lengths or None,
|
||||
frame_lengths=merged_lengths or None,
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"Unknown interface type: {iface_type}")
|
||||
@ -66,6 +80,29 @@ def lin(config: EcuTestConfig) -> t.Iterator[LinInterface]:
|
||||
lin.disconnect()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def ldf(config: EcuTestConfig):
|
||||
"""Session-scoped LDF database loaded from `interface.ldf_path`.
|
||||
|
||||
Tests that depend on LDF-defined frames request this fixture; tests that
|
||||
don't need it can ignore it. Skips with a clear message if `ldf_path`
|
||||
isn't set or the file isn't parseable.
|
||||
"""
|
||||
if not config.interface.ldf_path:
|
||||
pytest.skip("interface.ldf_path is not set in config")
|
||||
# Resolve relative paths against the workspace root for convenience.
|
||||
p = pathlib.Path(config.interface.ldf_path)
|
||||
if not p.is_absolute():
|
||||
p = (WORKSPACE_ROOT / p).resolve()
|
||||
if not p.is_file():
|
||||
pytest.skip(f"LDF file not found: {p}")
|
||||
try:
|
||||
from ecu_framework.lin.ldf import LdfDatabase
|
||||
except Exception as e:
|
||||
pytest.skip(f"ldfparser not available: {e!r}")
|
||||
return LdfDatabase(p)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=False)
|
||||
def flash_ecu(config: EcuTestConfig, lin: LinInterface) -> None:
|
||||
if not config.flash.enabled:
|
||||
|
||||
@ -1,21 +1,11 @@
|
||||
"""End-to-end hardware test on the MUM (Melexis Universal Master).
|
||||
|
||||
Power the ECU via MUM's built-in power output, then activate the RGB LED via
|
||||
the master-published ALM_Req_A frame (ID 0x0A) and verify the slave responds
|
||||
on ALM_Status (ID 0x11).
|
||||
|
||||
Frame layout (from vendor/4SEVEN_color_lib_test.ldf, ALM_Req_A @ 0x0A, 8B):
|
||||
byte 0 AmbLightColourRed (0..255)
|
||||
byte 1 AmbLightColourGreen (0..255)
|
||||
byte 2 AmbLightColourBlue (0..255)
|
||||
byte 3 AmbLightIntensity (0..255)
|
||||
byte 4 AmbLightUpdate (bits 0-1) | AmbLightMode (bits 2-7)
|
||||
byte 5 AmbLightDuration
|
||||
byte 6 AmbLightLIDFrom
|
||||
byte 7 AmbLightLIDTo
|
||||
|
||||
The ECU answers ALM_Req_A only when AmbLightLIDFrom <= ALMNadNo <= LIDTo, so
|
||||
we read the current NAD from ALM_Status first and target that NAD exactly.
|
||||
Powers the ECU via MUM's built-in power output, reads ALM_Status to discover
|
||||
the slave's NAD, then activates the RGB LED via the master-published
|
||||
ALM_Req_A frame targeting that NAD with full white at full intensity. Frame
|
||||
layouts are taken from the LDF at runtime via the `ldf` fixture, so signal
|
||||
names and bit positions stay in sync with `vendor/4SEVEN_color_lib_test.ldf`
|
||||
without manual byte building.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@ -27,35 +17,10 @@ from ecu_framework.lin.base import LinFrame, LinInterface
|
||||
|
||||
pytestmark = [pytest.mark.hardware, pytest.mark.mum]
|
||||
|
||||
ALM_REQ_A_ID = 0x0A
|
||||
ALM_STATUS_ID = 0x11
|
||||
|
||||
DEFAULT_RGB = (0xFF, 0xFF, 0xFF)
|
||||
DEFAULT_INTENSITY = 0xFF
|
||||
|
||||
|
||||
def _build_alm_req_a_payload(
|
||||
r: int, g: int, b: int,
|
||||
intensity: int = DEFAULT_INTENSITY,
|
||||
update: int = 0,
|
||||
mode: int = 0,
|
||||
duration: int = 0,
|
||||
lid_from: int = 0x01,
|
||||
lid_to: int = 0xFF,
|
||||
) -> bytes:
|
||||
"""Pack RGB+mode signals into the 8-byte ALM_Req_A payload."""
|
||||
byte4 = (update & 0x03) | ((mode & 0x3F) << 2)
|
||||
return bytes([
|
||||
r & 0xFF, g & 0xFF, b & 0xFF,
|
||||
intensity & 0xFF,
|
||||
byte4 & 0xFF,
|
||||
duration & 0xFF,
|
||||
lid_from & 0xFF,
|
||||
lid_to & 0xFF,
|
||||
])
|
||||
|
||||
|
||||
def test_mum_e2e_power_on_then_led_activate(config: EcuTestConfig, lin: LinInterface, rp):
|
||||
def test_mum_e2e_power_on_then_led_activate(
|
||||
config: EcuTestConfig, lin: LinInterface, ldf, rp
|
||||
):
|
||||
"""
|
||||
Title: MUM E2E - Power ECU, Read NAD, Activate RGB LED
|
||||
|
||||
@ -65,54 +30,64 @@ def test_mum_e2e_power_on_then_led_activate(config: EcuTestConfig, lin: LinInter
|
||||
up the LIN bus. This test reads ALM_Status to discover the slave's
|
||||
NAD, publishes ALM_Req_A targeting that NAD with full white at full
|
||||
intensity, and re-reads ALM_Status to confirm the bus is alive.
|
||||
Frame layouts come from the LDF database, not hand-coded byte
|
||||
positions.
|
||||
|
||||
Requirements: REQ-MUM-LED-ACTIVATE
|
||||
|
||||
Test Steps:
|
||||
1. Skip unless interface.type == 'mum'
|
||||
2. Read ALM_Status (0x11) and extract ALMNadNo (byte 0 lower 8 bits)
|
||||
3. Build ALM_Req_A payload with RGB=(0xFF,0xFF,0xFF), intensity=0xFF,
|
||||
targeting LIDFrom=LIDTo=current_nad
|
||||
2. Read ALM_Status; decode signals via the LDF; extract ALMNadNo
|
||||
3. Build the ALM_Req_A payload via ldf.frame("ALM_Req_A").pack(...),
|
||||
targeting LIDFrom=LIDTo=current_nad with full-white RGB
|
||||
4. Publish ALM_Req_A via lin.send()
|
||||
5. Re-read ALM_Status and assert it still returns a valid frame
|
||||
5. Re-read ALM_Status and confirm the bus still returns a valid frame
|
||||
|
||||
Expected Result:
|
||||
- First ALM_Status read returns a 4-byte frame with a NAD in 0x01..0xFE
|
||||
- First ALM_Status decode yields ALMNadNo in 0x01..0xFE
|
||||
- lin.send() of the LDF-packed frame succeeds
|
||||
- Second ALM_Status read returns a frame (bus still alive after Tx)
|
||||
"""
|
||||
if config.interface.type != "mum":
|
||||
pytest.skip("interface.type must be 'mum' for this test")
|
||||
|
||||
# Step 2: read current NAD from ALM_Status
|
||||
status = lin.receive(id=ALM_STATUS_ID, timeout=1.0)
|
||||
assert status is not None, "No ALM_Status received — check MUM/ECU wiring and power"
|
||||
assert len(status.data) >= 1, f"ALM_Status too short: {status.data!r}"
|
||||
current_nad = status.data[0]
|
||||
rp("alm_status_data_hex", bytes(status.data).hex())
|
||||
req_a = ldf.frame("ALM_Req_A")
|
||||
status = ldf.frame("ALM_Status")
|
||||
rp("ldf_path", str(ldf.path))
|
||||
rp("req_a_id", f"0x{req_a.id:02X}")
|
||||
rp("status_id", f"0x{status.id:02X}")
|
||||
|
||||
# Step 2: read ALM_Status and decode it via the LDF.
|
||||
rx = lin.receive(id=status.id, timeout=1.0)
|
||||
assert rx is not None, "No ALM_Status received — check MUM/ECU wiring and power"
|
||||
decoded = status.unpack(bytes(rx.data))
|
||||
current_nad = int(decoded["ALMNadNo"])
|
||||
rp("alm_status_decoded", decoded)
|
||||
rp("current_nad", f"0x{current_nad:02X}")
|
||||
assert 0x01 <= current_nad <= 0xFE, (
|
||||
f"ALMNadNo {current_nad:#x} is out of valid range; ECU may be unconfigured"
|
||||
)
|
||||
|
||||
# Step 3 + 4: target the discovered NAD with full white
|
||||
payload = _build_alm_req_a_payload(
|
||||
*DEFAULT_RGB,
|
||||
intensity=DEFAULT_INTENSITY,
|
||||
lid_from=current_nad,
|
||||
lid_to=current_nad,
|
||||
# Step 3 + 4: target the discovered NAD with full white at full intensity.
|
||||
payload = req_a.pack(
|
||||
AmbLightColourRed=0xFF,
|
||||
AmbLightColourGreen=0xFF,
|
||||
AmbLightColourBlue=0xFF,
|
||||
AmbLightIntensity=0xFF,
|
||||
AmbLightUpdate=0, # 0 = Immediate color update
|
||||
AmbLightMode=0, # 0 = Immediate Setpoint
|
||||
AmbLightDuration=0,
|
||||
AmbLightLIDFrom=current_nad,
|
||||
AmbLightLIDTo=current_nad,
|
||||
)
|
||||
rp("tx_id", f"0x{ALM_REQ_A_ID:02X}")
|
||||
rp("tx_data_hex", payload.hex())
|
||||
rp("rgb", list(DEFAULT_RGB))
|
||||
rp("intensity", DEFAULT_INTENSITY)
|
||||
lin.send(LinFrame(id=req_a.id, data=payload))
|
||||
|
||||
lin.send(LinFrame(id=ALM_REQ_A_ID, data=payload))
|
||||
|
||||
# Step 5: confirm bus liveness after the activation frame
|
||||
status_after = lin.receive(id=ALM_STATUS_ID, timeout=1.0)
|
||||
rp("post_status_present", status_after is not None)
|
||||
if status_after is not None:
|
||||
rp("post_status_data_hex", bytes(status_after.data).hex())
|
||||
assert status_after is not None, (
|
||||
# Step 5: confirm bus liveness after the activation frame.
|
||||
rx_after = lin.receive(id=status.id, timeout=1.0)
|
||||
rp("post_status_present", rx_after is not None)
|
||||
if rx_after is not None:
|
||||
rp("post_status_decoded", status.unpack(bytes(rx_after.data)))
|
||||
assert rx_after is not None, (
|
||||
"ALM_Status not received after publishing ALM_Req_A — ECU may have reset"
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user