"""Migrated from SWE5 COM Management Integration Test Plan. Source: ``25IMR003_ForSeven_RGB-SWITD_03-COM Management (Integration Test results).xlsm`` Translation strategy -------------------- The COM tests are about LIN communication: NAD addressing, LDF/baudrate, ALM_Req_A signal layout, LID-range targeting, and frame periodicity. - ``Watch color table`` (firmware lookup) is exercised end-to-end: drive a known RGB at full intensity and verify ``PWM_Frame`` matches the ``rgb_to_pwm.compute_pwm`` calculator (which encodes the same color table). - NAD: read ``ALM_Status.ALMNadNo`` and confirm it falls inside the valid NAD range declared by the LDF. - Baudrate: physical-layer; not measurable from inside the test runner (requires a scope) → the step is recorded for traceability and skipped. - ``ALM_Req_A`` byte-mapping: send a frame with distinctive RGB+I values and confirm the ECU's response (LED reaches ON, PWM matches) — that proves byte-level interpretation end-to-end. - LID-range flag: drive a frame inside vs. outside the node's range and observe whether the LED reacts. - 5 ms periodicity: a master-side LIN-master scheduling property that the slave does not echo back; documented as not directly observable. Marker: ``COM`` — see ``pytest.ini``. Run only this module: pytest -m "COM" tests/hardware/swe5/test_com_management.py """ from __future__ import annotations import sys import time from pathlib import Path import pytest _HW_DIR = Path(__file__).resolve().parent.parent if str(_HW_DIR) not in sys.path: sys.path.insert(0, str(_HW_DIR)) from ecu_framework.config import EcuTestConfig from ecu_framework.lin.base import LinInterface from frame_io import FrameIO from alm_helpers import ( AlmTester, LED_STATE_OFF, LED_STATE_ANIMATING, LED_STATE_ON, STATE_POLL_INTERVAL, STATE_TIMEOUT_DEFAULT, ) pytestmark = [pytest.mark.COM] # --- fixtures -------------------------------------------------------------- @pytest.fixture(scope="module") def fio(config: EcuTestConfig, lin: LinInterface, ldf) -> FrameIO: if config.interface.type != "mum": pytest.skip("interface.type must be 'mum' for this suite") return FrameIO(lin, ldf) @pytest.fixture(scope="module") def alm(fio: FrameIO) -> AlmTester: decoded = fio.receive("ALM_Status", timeout=1.0) if decoded is None: pytest.skip("ECU not responding on ALM_Status — check wiring/power") nad = int(decoded["ALMNadNo"]) if not (0x01 <= nad <= 0xFE): pytest.skip(f"ECU reports invalid NAD {nad:#x} — auto-addressing first") return AlmTester(fio, nad) @pytest.fixture(autouse=True) def _reset_to_off(alm: AlmTester): alm.force_off() yield alm.force_off() # --- tests ----------------------------------------------------------------- def test_com_itd_0001(fio: FrameIO, alm: AlmTester, rp): """ Title: LED color table is configured per spec — PWM_Frame matches the calculator Description: Verify the SW configures the LED color table as required by [SWRS_LIN_0001]. The firmware's color table feeds rgb_to_pwm.compute_pwm(). Drive a known RGB at full intensity, wait for LED_ON, then assert PWM_Frame matches compute_pwm(R,G,B,temp_c=Tj_NTC).pwm_comp within tolerance. Mismatch implies the on-ECU table differs from the spec. Requirements: SWRS_LIN_0001 Test ID: COM_ITD_0001 """ r, g, b = 0, 180, 80 # Step: Drive ALM_Req_A mode=0 RGB at full intensity to this NAD fio.send( "ALM_Req_A", AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b, AmbLightIntensity=255, AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0, AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad, ) # Step: Wait for ALMLEDState == LED_ON reached, elapsed, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT) rp("led_state_history", history) rp("on_elapsed_s", round(elapsed, 3)) assert reached, f"LED_ON never reached: {history}" # Step: Assert PWM_Frame matches the rgb_to_pwm calculator (color-table proxy) alm.assert_pwm_matches_rgb(rp, r, g, b) def test_com_itd_0002(fio: FrameIO, alm: AlmTester, rp, ldf): """ Title: LDF implementation — NAD and baudrate match the LDF Description: Verify the SW implements the LDF from "4SEVEN_LDF.ldf" — confirm the NAD and the bus baudrate. NAD: read ALMNadNo via ALM_Status; confirm it is a valid LIN slave NAD (0x01..0xFE) — i.e. matches the value the LDF declares for ALM_Node. Baudrate: physical-layer property of the LIN bus; verifying it requires an oscilloscope on the LIN line, so the step is recorded for traceability but cannot be asserted from inside the test runner. Requirements: SWRS_LIN_0001, SWRS_LIN_0002, SWRS_LIN_0003 Test ID: COM_ITD_0002 """ # Step: Read ALM_Status and confirm ALMNadNo is a valid LIN slave NAD nad = fio.read_signal("ALM_Status", "ALMNadNo") assert nad is not None, "ALM_Status not received within timeout" rp("alm_nad", int(nad)) assert 0x01 <= int(nad) <= 0xFE, ( f"ALMNadNo {int(nad):#x} is outside the valid LIN slave range 0x01..0xFE" ) # Step: Confirm the LDF declares the same NAD for ALM_Node (introspection) # The LdfDatabase wrapper exposes the parsed ldf via .ldf in some # implementations; fall back to attribute access otherwise. ldf_nad = None try: for node in getattr(ldf.ldf, "slaves", []) or []: # ldfparser slaves carry .name and .configured_nad if getattr(node, "name", "").lower().startswith("alm"): ldf_nad = int(getattr(node, "configured_nad", 0)) break except Exception as e: # pragma: no cover — best effort rp("ldf_introspection_error", repr(e)) rp("ldf_declared_nad", ldf_nad) if ldf_nad is not None: assert int(nad) == ldf_nad, ( f"Runtime NAD {int(nad):#x} != LDF-declared NAD {ldf_nad:#x}" ) # Step: Baudrate verification requires an external scope on the LIN bus. # LIN baudrate is a physical-layer parameter; the master configures # it from the LDF when opening the interface, but it is not echoed # back in any frame the slave publishes. Recording for traceability. rp("baudrate_check", "requires oscilloscope — not asserted in software") def test_com_itd_0003(fio: FrameIO, alm: AlmTester, rp): """ Title: ALM_Req_A interpretation — distinctive RGBI bytes drive the expected PWM Description: Verify the SW correctly interprets the bytes of ALM_Req_A (AmbLightLIDFrom/To, AmbLightColourRed/Green/Blue, AmbLightIntensity). Per-byte verification at the firmware level (Byte_3 == AmbLightColourRed, etc.) is not LIN-observable. Instead, send a frame with distinctive R/G/B values, addressed to this node's NAD only, then verify (a) the LED reaches ON (LIDFrom/To were honoured) and (b) PWM_wo_Comp matches the calculator for those R/G/B at full intensity (Red/Green/Blue/Intensity bytes were interpreted correctly). Requirements: SWRS_LIN_0004, SWRS_LIN_0012, SWRS_LIN_0013, SWRS_LIN_0014, SWRS_LIN_0015 Test ID: COM_ITD_0003 """ # Distinctive values so a swap of two bytes would be detected. r, g, b, intensity = 0xA0, 0x40, 0x10, 0xFF # Step: Disable temperature compensation so PWM_wo_Comp == calculator fio.send( "ConfigFrame", ConfigFrame_Calibration=0, ConfigFrame_EnableDerating=1, ConfigFrame_EnableCompensation=0, ConfigFrame_MaxLM=3840, ) time.sleep(0.2) try: # Step: Send ALM_Req_A LIDFrom=LIDTo=alm.nad, RGBI distinctive values fio.send( "ALM_Req_A", AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b, AmbLightIntensity=intensity, AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0, AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad, ) # Step: LIDFrom/To honoured — LED reaches ON reached, elapsed, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT) rp("led_state_history", history) rp("on_elapsed_s", round(elapsed, 3)) assert reached, ( f"ECU did not respond to a frame addressed to its own NAD: {history}" ) # Step: RGB+Intensity bytes correctly interpreted — PWM_wo_Comp matches calculator alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b) finally: # Step: Restore EnableCompensation=1 fio.send( "ConfigFrame", ConfigFrame_Calibration=0, ConfigFrame_EnableDerating=1, ConfigFrame_EnableCompensation=1, ConfigFrame_MaxLM=3840, ) time.sleep(0.2) def test_com_itd_0006(fio: FrameIO, alm: AlmTester, rp): """ Title: LID range targeting — broadcast hits, out-of-range frame is ignored Description: Verify the SW respects the AmbLightLIDFrom/To range — frames whose range covers this node's NAD are processed; out-of-range frames are ignored. Note: the workbook step ``Watch AmbLightLIDFrom/AmbLightLIDTo range flag`` refers to a firmware-internal flag that is not echoed on the LIN bus. The LIN-observable proxy is whether the LED reacts (ON) or stays at OFF. Requirements: SWRS_LIN_0053 Test ID: COM_ITD_0006 """ # Step: Broadcast LIDFrom=0x00, LIDTo=0xFF — node is in range, must react fio.send( "ALM_Req_A", AmbLightColourRed=120, AmbLightColourGreen=0, AmbLightColourBlue=255, AmbLightIntensity=255, AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0, AmbLightLIDFrom=0x00, AmbLightLIDTo=0xFF, ) reached_on, elapsed, h_in = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT) rp("in_range_history", h_in) rp("in_range_elapsed_s", round(elapsed, 3)) assert reached_on, f"Node ignored an in-range broadcast: {h_in}" alm.force_off() # Step: Out-of-range LID (range that excludes this NAD) — must be ignored # Pick a non-trivial range that intentionally excludes this node. if alm.nad <= 0x10: lid_from, lid_to = 0x80, 0xFE else: lid_from, lid_to = 0x01, max(0x02, alm.nad - 1) fio.send( "ALM_Req_A", AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255, AmbLightIntensity=255, AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0, AmbLightLIDFrom=lid_from, AmbLightLIDTo=lid_to, ) deadline = time.monotonic() + 1.0 history = [] while time.monotonic() < deadline: st = alm.read_led_state() if not history or history[-1] != st: history.append(st) time.sleep(STATE_POLL_INTERVAL) rp("out_of_range_lid", (lid_from, lid_to)) rp("out_of_range_history", history) assert LED_STATE_ON not in history and LED_STATE_ANIMATING not in history, ( f"Out-of-range LID frame [{lid_from:#x}..{lid_to:#x}] (NAD={alm.nad:#x}) " f"unexpectedly drove the LED: {history}" ) def test_com_itd_0001_b(fio: FrameIO, alm: AlmTester, rp): """ Title: Input frame reading periodicity (5 ms) — master-side scheduling, scope-only Description: Verify the SW respects 5 ms periodicity for reading input frames. 5 ms is the LIN master's schedule cadence: it is configured by the master (MUM/BabyLin) when the schedule table is loaded from the LDF, and is not echoed back by the slave. Confirming the actual on-bus inter-frame gap requires bus-tracing hardware (oscilloscope or LIN analyzer). This step is recorded for traceability. Requirements: SWRS_LIN_0001 Test ID: COM_ITD_0001_b """ # Step: Document: 5 ms scheduling is master-side and not asserted from the slave rp("note", ( "5 ms periodicity is set by the LIN master's schedule table " "(loaded from the LDF). Slave-side timing of inter-frame gaps " "requires an external LIN bus analyzer or oscilloscope to " "verify; pytest cannot observe it directly." )) # Sanity: confirm we can at least round-trip a frame within a few # schedule periods, which verifies the bus is up at the configured # baudrate (a coarse sanity check, not a 5 ms timing assertion). decoded = fio.receive("ALM_Status", timeout=1.0) assert decoded is not None, "ALM_Status not received — bus may be down" pytest.skip( "5 ms inter-frame periodicity is master-side / physical-layer; " "verify with a LIN bus analyzer or oscilloscope, not pytest." )