ecu-tests/tests/hardware/test_mum_auto_addressing.py

189 lines
6.5 KiB
Python
Raw 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.

"""LIN auto-addressing (BSM-SNPD) test on the MUM.
Ports the BSM-SNPD sequence from `vendor/automated_lin_test/test_auto_addressing.py`
into pytest. The flow:
1. INIT subf=0x01, params=(0x02, 0xFF) wait 50 ms
2. ASSIGN subf=0x02, params=(0x02, target_nad) x 16 frames, 20 ms apart
(target_nad placed first, then NADs 0x01..0x10 cycle)
3. STORE subf=0x03, params=(0x02, 0xFF) wait 20 ms
4. FINALIZE subf=0x04, params=(0x02, 0xFF) wait 20 ms
Each frame is 8 bytes:
byte 0 NAD = 0x7F (broadcast)
byte 1 PCI = 0x06 (6 data bytes)
byte 2 SID = 0xB5 (BSM-SNPD)
byte 3 Supplier ID LSB = 0xFF
byte 4 Supplier ID MSB = 0x7F
byte 5 subfunction
byte 6 param 1
byte 7 param 2
Critically, BSM frames must be sent with **LIN 1.x Classic checksum**, which
the ECU firmware checks. `MumLinInterface.send_raw()` routes through the
transport layer's `ld_put_raw`, which uses Classic; `lin.send()` would use
Enhanced and frames would be silently rejected.
The test changes the ECU's NAD, asserts the change, and restores the original
NAD in `finally` so it leaves the bench in the state it found it.
"""
from __future__ import annotations
import time
from typing import Iterable
import pytest
from ecu_framework.config import EcuTestConfig
from ecu_framework.lin.base import LinInterface
pytestmark = [pytest.mark.hardware, pytest.mark.mum, pytest.mark.slow]
# BSM-SNPD constants
BSM_NAD_BROADCAST = 0x7F
BSM_PCI = 0x06
BSM_SID = 0xB5
BSM_SUPPLIER_ID_LSB = 0xFF
BSM_SUPPLIER_ID_MSB = 0x7F
BSM_SUBF_INIT = 0x01
BSM_SUBF_ASSIGN = 0x02
BSM_SUBF_STORE = 0x03
BSM_SUBF_FINALIZE = 0x04
BSM_INIT_DELAY = 0.050
BSM_FRAME_DELAY = 0.020
VALID_NAD_RANGE: Iterable[int] = range(0x01, 0x11) # 0x01..0x10 inclusive
# Time to wait after FINALIZE for the ECU to commit and resume normal traffic
POST_FINALIZE_SETTLE = 1.0
def _bsm_frame(subfunction: int, param1: int, param2: int) -> bytes:
"""Build the 8-byte BSM-SNPD raw payload."""
return bytes([
BSM_NAD_BROADCAST,
BSM_PCI,
BSM_SID,
BSM_SUPPLIER_ID_LSB,
BSM_SUPPLIER_ID_MSB,
subfunction & 0xFF,
param1 & 0xFF,
param2 & 0xFF,
])
def _read_nad(lin: LinInterface, status_frame, attempts: int = 5) -> int | None:
"""Read ALM_Status a few times, return ALMNadNo or None if no response."""
for _ in range(attempts):
rx = lin.receive(id=status_frame.id, timeout=0.5)
if rx is not None:
decoded = status_frame.unpack(bytes(rx.data))
return int(decoded["ALMNadNo"])
time.sleep(0.1)
return None
def _run_bsm_sequence(lin: LinInterface, target_nad: int) -> None:
"""Drive one full INIT→ASSIGN×16→STORE→FINALIZE cycle, target NAD first."""
# 1. INIT
lin.send_raw(_bsm_frame(BSM_SUBF_INIT, 0x02, 0xFF))
time.sleep(BSM_INIT_DELAY)
# 2. 16x ASSIGN, target_nad placed first
nad_sequence = list(VALID_NAD_RANGE)
if target_nad in nad_sequence:
nad_sequence.remove(target_nad)
nad_sequence.insert(0, target_nad)
for nad in nad_sequence:
lin.send_raw(_bsm_frame(BSM_SUBF_ASSIGN, 0x02, nad))
time.sleep(BSM_FRAME_DELAY)
# 3. STORE
lin.send_raw(_bsm_frame(BSM_SUBF_STORE, 0x02, 0xFF))
time.sleep(BSM_FRAME_DELAY)
# 4. FINALIZE
lin.send_raw(_bsm_frame(BSM_SUBF_FINALIZE, 0x02, 0xFF))
time.sleep(BSM_FRAME_DELAY)
def test_bsm_auto_addressing_changes_nad(
config: EcuTestConfig, lin: LinInterface, ldf, rp
):
"""
Title: BSM-SNPD auto-addressing assigns a new NAD and ALM_Status reflects it
Description:
Runs the full BSM-SNPD sequence (INIT, 16x ASSIGN, STORE, FINALIZE)
with a target NAD different from the ECU's current NAD, then reads
ALM_Status and asserts ALMNadNo equals the target. Restores the
original NAD in a finally block to leave the bench unchanged.
Requirements: REQ-MUM-BSM-AUTOADDR
Test Steps:
1. Skip unless interface.type == 'mum'
2. Read initial NAD from ALM_Status
3. Pick a target NAD in 0x01..0x10 different from initial
4. Run BSM sequence with target_nad first
5. Read ALM_Status; assert ALMNadNo == target_nad
6. Run BSM sequence again to restore initial NAD
7. Read ALM_Status; record the final NAD
Expected Result:
- Initial NAD is in 0x01..0xFE
- After BSM sequence, ALM_Status.ALMNadNo == target_nad
- After restore sequence, ALM_Status.ALMNadNo == initial_nad
"""
if config.interface.type != "mum":
pytest.skip("interface.type must be 'mum' for this test")
# send_raw is MUM-only; gate on capability so the failure mode is clean
if not hasattr(lin, "send_raw"):
pytest.skip("LIN adapter does not expose send_raw() (need MumLinInterface)")
status = ldf.frame("ALM_Status")
rp("ldf_path", str(ldf.path))
# Step 2: read current NAD
initial_nad = _read_nad(lin, status)
assert initial_nad is not None, "ECU not responding on ALM_Status — wiring/power?"
rp("initial_nad", f"0x{initial_nad:02X}")
assert 0x01 <= initial_nad <= 0xFE, f"ECU initial NAD {initial_nad:#x} is out of range"
# Step 3: pick a target NAD different from current
candidates = [n for n in VALID_NAD_RANGE if n != initial_nad]
target_nad = candidates[0]
rp("target_nad", f"0x{target_nad:02X}")
try:
# Step 4: run the BSM sequence
_run_bsm_sequence(lin, target_nad)
time.sleep(POST_FINALIZE_SETTLE)
# Step 5: verify
new_nad = _read_nad(lin, status)
rp("post_bsm_nad", f"0x{new_nad:02X}" if new_nad is not None else "no_response")
assert new_nad == target_nad, (
f"NAD did not change to target: expected 0x{target_nad:02X}, "
f"got {new_nad if new_nad is None else f'0x{new_nad:02X}'}"
)
finally:
# Step 6 + 7: restore the original NAD so the bench is left as we found it
try:
_run_bsm_sequence(lin, initial_nad)
time.sleep(POST_FINALIZE_SETTLE)
restored_nad = _read_nad(lin, status)
rp("restored_nad", f"0x{restored_nad:02X}" if restored_nad is not None else "no_response")
if restored_nad != initial_nad:
# Don't fail the test on restore failure (the original assertion is
# what we care about), but make it visible.
rp("restore_warning", f"failed to restore initial NAD ({restored_nad})")
except Exception as e:
rp("restore_error", repr(e))