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
|
power_device: power_out0 # MUM power-control device
|
||||||
bitrate: 19200 # LIN baudrate
|
bitrate: 19200 # LIN baudrate
|
||||||
boot_settle_seconds: 0.5 # Delay after power-up before first frame
|
boot_settle_seconds: 0.5 # Delay after power-up before first frame
|
||||||
# Optional: per-frame-id data lengths. Defaults cover the 4SEVEN library
|
# Path to an LDF; auto-populates frame_lengths and is exposed to tests
|
||||||
# (ALM_Status=4, ALM_Req_A=8, etc.) — only override if your ECU differs.
|
# via the `ldf` fixture (db.frame("ALM_Req_A").pack(...) etc.).
|
||||||
frame_lengths:
|
ldf_path: ./vendor/4SEVEN_color_lib_test.ldf
|
||||||
0x0A: 8 # ALM_Req_A
|
# Optional per-frame-id data lengths. When ldf_path is set, anything here
|
||||||
0x11: 4 # ALM_Status
|
# only acts as an override on top of the LDF lengths.
|
||||||
|
frame_lengths: {}
|
||||||
|
|
||||||
flash:
|
flash:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@ -7,9 +7,12 @@ interface:
|
|||||||
power_device: power_out0 # MUM power-control device (built-in PSU)
|
power_device: power_out0 # MUM power-control device (built-in PSU)
|
||||||
bitrate: 19200 # LIN baudrate
|
bitrate: 19200 # LIN baudrate
|
||||||
boot_settle_seconds: 0.5 # Wait after power-up before sending the first frame
|
boot_settle_seconds: 0.5 # Wait after power-up before sending the first frame
|
||||||
frame_lengths:
|
# Path to an LDF (LIN description file). When set, tests can use the
|
||||||
0x0A: 8 # ALM_Req_A (master-published, RGB control)
|
# `ldf` fixture to pack/unpack frames by signal name, and the MUM adapter
|
||||||
0x11: 4 # ALM_Status (slave-published)
|
# 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 ---
|
# --- BabyLIN (legacy) settings, used only when type: babylin ---
|
||||||
channel: 0
|
channel: 0
|
||||||
|
|||||||
@ -26,7 +26,8 @@ From highest to lowest precedence:
|
|||||||
- `lin_device`: MUM LIN device name (MUM-only, default `lin0`)
|
- `lin_device`: MUM LIN device name (MUM-only, default `lin0`)
|
||||||
- `power_device`: MUM power-control device (MUM-only, default `power_out0`)
|
- `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)
|
- `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`
|
- `flash: FlashConfig`
|
||||||
- `enabled`: whether to flash before tests
|
- `enabled`: whether to flash before tests
|
||||||
- `hex_path`: path to HEX file
|
- `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`
|
- Mock LIN Adapter — `ecu_framework/lin/mock.py`
|
||||||
- MUM LIN Adapter — `ecu_framework/lin/mum.py` (Melexis Universal Master via `pylin` + `pymumclient`)
|
- 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)
|
- 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`
|
- Flasher — `ecu_framework/flashing/hex_flasher.py`
|
||||||
- Power Supply (PSU) control — `ecu_framework/power/owon_psu.py` (serial SCPI)
|
- Power Supply (PSU) control — `ecu_framework/power/owon_psu.py` (serial SCPI)
|
||||||
- PSU quick demo script — `vendor/Owon/owon_psu_quick_demo.py`
|
- PSU quick demo script — `vendor/Owon/owon_psu_quick_demo.py`
|
||||||
@ -33,6 +34,7 @@ flowchart TB
|
|||||||
MOCK[ecu_framework/lin/mock.py]
|
MOCK[ecu_framework/lin/mock.py]
|
||||||
MUM[ecu_framework/lin/mum.py]
|
MUM[ecu_framework/lin/mum.py]
|
||||||
BABY[ecu_framework/lin/babylin.py]
|
BABY[ecu_framework/lin/babylin.py]
|
||||||
|
LDF[ecu_framework/lin/ldf.py]
|
||||||
FLASH[ecu_framework/flashing/hex_flasher.py]
|
FLASH[ecu_framework/flashing/hex_flasher.py]
|
||||||
POWER[ecu_framework/power/owon_psu.py]
|
POWER[ecu_framework/power/owon_psu.py]
|
||||||
end
|
end
|
||||||
@ -44,6 +46,8 @@ flowchart TB
|
|||||||
MELEXIS[Melexis pylin + pymumclient<br/>MUM @ 192.168.7.2]
|
MELEXIS[Melexis pylin + pymumclient<br/>MUM @ 192.168.7.2]
|
||||||
SDK[vendor/BabyLIN_library.py<br/>platform-specific libs]
|
SDK[vendor/BabyLIN_library.py<br/>platform-specific libs]
|
||||||
OWON[vendor/Owon/owon_psu_quick_demo.py]
|
OWON[vendor/Owon/owon_psu_quick_demo.py]
|
||||||
|
LDFFILE[vendor/*.ldf]
|
||||||
|
LDFLIB[ldfparser PyPI]
|
||||||
end
|
end
|
||||||
|
|
||||||
T --> CF
|
T --> CF
|
||||||
@ -54,12 +58,15 @@ flowchart TB
|
|||||||
CF --> BABY
|
CF --> BABY
|
||||||
CF --> FLASH
|
CF --> FLASH
|
||||||
T --> POWER
|
T --> POWER
|
||||||
|
T --> LDF
|
||||||
PL --> REP
|
PL --> REP
|
||||||
|
|
||||||
CFG --> YAML
|
CFG --> YAML
|
||||||
CFG --> PSU_YAML
|
CFG --> PSU_YAML
|
||||||
MUM --> MELEXIS
|
MUM --> MELEXIS
|
||||||
BABY --> SDK
|
BABY --> SDK
|
||||||
|
LDF --> LDFLIB
|
||||||
|
LDF --> LDFFILE
|
||||||
T --> OWON
|
T --> OWON
|
||||||
T --> REP
|
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
|
8. `07_flash_sequence.md` — ECU flashing workflow and sequence diagram
|
||||||
9. `08_babylin_internals.md` — BabyLIN SDK wrapper internals and call flow (legacy)
|
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
|
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
|
11. `17_ldf_parser.md` — LDF parser, `ldf` fixture, and per-frame `pack`/`unpack` helpers
|
||||||
12. `09_raspberry_pi_deployment.md` — Run on Raspberry Pi (venv, service, hardware notes)
|
12. `18_test_catalog.md` — Per-test catalog: purpose, markers, hardware needs, expected result
|
||||||
13. `10_build_custom_image.md` — Build a custom Raspberry Pi OS image with the framework baked in
|
13. `DEVELOPER_COMMIT_GUIDE.md` — What to commit vs ignore, commands
|
||||||
14. `12_using_the_framework.md` — Practical usage: local, hardware (MUM/BabyLIN), CI, and Pi
|
14. `09_raspberry_pi_deployment.md` — Run on Raspberry Pi (venv, service, hardware notes)
|
||||||
15. `13_unit_testing_guide.md` — Unit tests layout, markers, coverage, and tips
|
15. `10_build_custom_image.md` — Build a custom Raspberry Pi OS image with the framework baked in
|
||||||
16. `14_power_supply.md` — Owon PSU control, configuration, tests, and quick demo script
|
16. `12_using_the_framework.md` — Practical usage: local, hardware (MUM/BabyLIN), CI, and Pi
|
||||||
17. `15_report_properties_cheatsheet.md` — Standardized keys for record_property/rp across suites
|
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:
|
Related references:
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,9 @@ class InterfaceConfig:
|
|||||||
power_device: str = "power_out0"
|
power_device: str = "power_out0"
|
||||||
boot_settle_seconds: float = 0.5
|
boot_settle_seconds: float = 0.5
|
||||||
frame_lengths: Dict[int, int] = field(default_factory=dict)
|
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
|
@dataclass
|
||||||
@ -157,6 +160,7 @@ def _to_dataclass(cfg: Dict[str, Any]) -> EcuTestConfig:
|
|||||||
power_device=str(iface.get("power_device", "power_out0")),
|
power_device=str(iface.get("power_device", "power_out0")),
|
||||||
boot_settle_seconds=float(iface.get("boot_settle_seconds", 0.5)),
|
boot_settle_seconds=float(iface.get("boot_settle_seconds", 0.5)),
|
||||||
frame_lengths=frame_lengths,
|
frame_lengths=frame_lengths,
|
||||||
|
ldf_path=iface.get("ldf_path"),
|
||||||
),
|
),
|
||||||
flash=FlashConfig(
|
flash=FlashConfig(
|
||||||
enabled=bool(flash.get("enabled", False)), # Coerce to bool
|
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
|
req_004: REQ-004 - Mock interface shall handle timeout scenarios gracefully
|
||||||
smoke: Basic functionality validation tests
|
smoke: Basic functionality validation tests
|
||||||
boundary: Boundary condition and edge case 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: Where pytest looks for tests by default.
|
||||||
testpaths = tests
|
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-html>=4,<5 # Generate HTML test reports for CI and sharing
|
||||||
pytest-cov>=5,<6 # Coverage reports for Python packages
|
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
|
# Logging and config extras
|
||||||
configparser>=6,<7 # Optional INI-based config support if you add .ini configs later
|
configparser>=6,<7 # Optional INI-based config support if you add .ini configs later
|
||||||
colorlog>=6,<7 # Colored logging output for readable test logs
|
colorlog>=6,<7 # Colored logging output for readable test logs
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -50,13 +51,26 @@ def lin(config: EcuTestConfig) -> t.Iterator[LinInterface]:
|
|||||||
pytest.skip("MUM interface not available in this environment")
|
pytest.skip("MUM interface not available in this environment")
|
||||||
if not config.interface.host:
|
if not config.interface.host:
|
||||||
pytest.skip("interface.host is required when interface.type == 'mum'")
|
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(
|
lin = MumLinInterface(
|
||||||
host=config.interface.host,
|
host=config.interface.host,
|
||||||
lin_device=config.interface.lin_device,
|
lin_device=config.interface.lin_device,
|
||||||
power_device=config.interface.power_device,
|
power_device=config.interface.power_device,
|
||||||
baudrate=config.interface.bitrate,
|
baudrate=config.interface.bitrate,
|
||||||
boot_settle_seconds=config.interface.boot_settle_seconds,
|
boot_settle_seconds=config.interface.boot_settle_seconds,
|
||||||
frame_lengths=config.interface.frame_lengths or None,
|
frame_lengths=merged_lengths or None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"Unknown interface type: {iface_type}")
|
raise RuntimeError(f"Unknown interface type: {iface_type}")
|
||||||
@ -66,6 +80,29 @@ def lin(config: EcuTestConfig) -> t.Iterator[LinInterface]:
|
|||||||
lin.disconnect()
|
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)
|
@pytest.fixture(scope="session", autouse=False)
|
||||||
def flash_ecu(config: EcuTestConfig, lin: LinInterface) -> None:
|
def flash_ecu(config: EcuTestConfig, lin: LinInterface) -> None:
|
||||||
if not config.flash.enabled:
|
if not config.flash.enabled:
|
||||||
|
|||||||
@ -1,21 +1,11 @@
|
|||||||
"""End-to-end hardware test on the MUM (Melexis Universal Master).
|
"""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
|
Powers the ECU via MUM's built-in power output, reads ALM_Status to discover
|
||||||
the master-published ALM_Req_A frame (ID 0x0A) and verify the slave responds
|
the slave's NAD, then activates the RGB LED via the master-published
|
||||||
on ALM_Status (ID 0x11).
|
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
|
||||||
Frame layout (from vendor/4SEVEN_color_lib_test.ldf, ALM_Req_A @ 0x0A, 8B):
|
names and bit positions stay in sync with `vendor/4SEVEN_color_lib_test.ldf`
|
||||||
byte 0 AmbLightColourRed (0..255)
|
without manual byte building.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -27,35 +17,10 @@ from ecu_framework.lin.base import LinFrame, LinInterface
|
|||||||
|
|
||||||
pytestmark = [pytest.mark.hardware, pytest.mark.mum]
|
pytestmark = [pytest.mark.hardware, pytest.mark.mum]
|
||||||
|
|
||||||
ALM_REQ_A_ID = 0x0A
|
|
||||||
ALM_STATUS_ID = 0x11
|
|
||||||
|
|
||||||
DEFAULT_RGB = (0xFF, 0xFF, 0xFF)
|
def test_mum_e2e_power_on_then_led_activate(
|
||||||
DEFAULT_INTENSITY = 0xFF
|
config: EcuTestConfig, lin: LinInterface, ldf, rp
|
||||||
|
):
|
||||||
|
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
Title: MUM E2E - Power ECU, Read NAD, Activate RGB LED
|
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
|
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
|
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.
|
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
|
Requirements: REQ-MUM-LED-ACTIVATE
|
||||||
|
|
||||||
Test Steps:
|
Test Steps:
|
||||||
1. Skip unless interface.type == 'mum'
|
1. Skip unless interface.type == 'mum'
|
||||||
2. Read ALM_Status (0x11) and extract ALMNadNo (byte 0 lower 8 bits)
|
2. Read ALM_Status; decode signals via the LDF; extract ALMNadNo
|
||||||
3. Build ALM_Req_A payload with RGB=(0xFF,0xFF,0xFF), intensity=0xFF,
|
3. Build the ALM_Req_A payload via ldf.frame("ALM_Req_A").pack(...),
|
||||||
targeting LIDFrom=LIDTo=current_nad
|
targeting LIDFrom=LIDTo=current_nad with full-white RGB
|
||||||
4. Publish ALM_Req_A via lin.send()
|
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:
|
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)
|
- Second ALM_Status read returns a frame (bus still alive after Tx)
|
||||||
"""
|
"""
|
||||||
if config.interface.type != "mum":
|
if config.interface.type != "mum":
|
||||||
pytest.skip("interface.type must be 'mum' for this test")
|
pytest.skip("interface.type must be 'mum' for this test")
|
||||||
|
|
||||||
# Step 2: read current NAD from ALM_Status
|
req_a = ldf.frame("ALM_Req_A")
|
||||||
status = lin.receive(id=ALM_STATUS_ID, timeout=1.0)
|
status = ldf.frame("ALM_Status")
|
||||||
assert status is not None, "No ALM_Status received — check MUM/ECU wiring and power"
|
rp("ldf_path", str(ldf.path))
|
||||||
assert len(status.data) >= 1, f"ALM_Status too short: {status.data!r}"
|
rp("req_a_id", f"0x{req_a.id:02X}")
|
||||||
current_nad = status.data[0]
|
rp("status_id", f"0x{status.id:02X}")
|
||||||
rp("alm_status_data_hex", bytes(status.data).hex())
|
|
||||||
|
# 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}")
|
rp("current_nad", f"0x{current_nad:02X}")
|
||||||
assert 0x01 <= current_nad <= 0xFE, (
|
assert 0x01 <= current_nad <= 0xFE, (
|
||||||
f"ALMNadNo {current_nad:#x} is out of valid range; ECU may be unconfigured"
|
f"ALMNadNo {current_nad:#x} is out of valid range; ECU may be unconfigured"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 3 + 4: target the discovered NAD with full white
|
# Step 3 + 4: target the discovered NAD with full white at full intensity.
|
||||||
payload = _build_alm_req_a_payload(
|
payload = req_a.pack(
|
||||||
*DEFAULT_RGB,
|
AmbLightColourRed=0xFF,
|
||||||
intensity=DEFAULT_INTENSITY,
|
AmbLightColourGreen=0xFF,
|
||||||
lid_from=current_nad,
|
AmbLightColourBlue=0xFF,
|
||||||
lid_to=current_nad,
|
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("tx_data_hex", payload.hex())
|
||||||
rp("rgb", list(DEFAULT_RGB))
|
lin.send(LinFrame(id=req_a.id, data=payload))
|
||||||
rp("intensity", DEFAULT_INTENSITY)
|
|
||||||
|
|
||||||
lin.send(LinFrame(id=ALM_REQ_A_ID, data=payload))
|
# Step 5: confirm bus liveness after the activation frame.
|
||||||
|
rx_after = lin.receive(id=status.id, timeout=1.0)
|
||||||
# Step 5: confirm bus liveness after the activation frame
|
rp("post_status_present", rx_after is not None)
|
||||||
status_after = lin.receive(id=ALM_STATUS_ID, timeout=1.0)
|
if rx_after is not None:
|
||||||
rp("post_status_present", status_after is not None)
|
rp("post_status_decoded", status.unpack(bytes(rx_after.data)))
|
||||||
if status_after is not None:
|
assert rx_after is not None, (
|
||||||
rp("post_status_data_hex", bytes(status_after.data).hex())
|
|
||||||
assert status_after is not None, (
|
|
||||||
"ALM_Status not received after publishing ALM_Req_A — ECU may have reset"
|
"ALM_Status not received after publishing ALM_Req_A — ECU may have reset"
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user