189 lines
6.5 KiB
Python
189 lines
6.5 KiB
Python
"""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))
|