"""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))