ecu-tests/tests/hardware/swe5/test_com_management.py

322 lines
13 KiB
Python

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