Splits hardware-test concerns into two reusable modules and rebuilds
test_mum_alm_animation.py on top of them.
- frame_io.py — generic LDF-driven I/O class. Knows nothing about
ALM. Three access levels:
high: send/receive/read_signal by frame and signal name
mid: pack/unpack — bytes ↔ signals without I/O
low: send_raw/receive_raw — bypass the LDF entirely
Plus introspection: frame, frame_id, frame_length. Frame lookups
are cached per FrameIO instance.
- alm_helpers.py — ALM_Node domain helpers built on FrameIO.
AlmTester class bound to (fio, nad) exposes:
force_off, read_led_state, wait_for_state,
measure_animating_window, assert_pwm_matches_rgb,
assert_pwm_wo_comp_matches_rgb
Plus pure utilities (ntc_kelvin_to_celsius, pwm_within_tol) and
the LED-state / pacing / PWM-tolerance constants. PWM assertions
use vendor/rgb_to_pwm.py (compute_pwm) at the runtime
Tj_Frame_NTC temperature.
- test_mum_alm_animation.py rewritten:
* fio + alm fixtures replace the previous dict-based _ctx
* SETUP / PROCEDURE / ASSERT / TEARDOWN section markers
* test_mode1_fade now wraps its ConfigFrame change in
try/finally so EnableCompensation is restored even on
assertion failure (was leaking state into later tests)
* test_disable_compensation_pwm_wo_comp uses the four-phase
pattern explicitly
Sibling imports work because pytest's default rootdir mode puts the
test file's directory on sys.path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
554 lines
24 KiB
Python
554 lines
24 KiB
Python
"""Automated animation / state checks for ALM_Req_A on MUM.
|
||
|
||
Ports the requirement-driven checks from
|
||
`vendor/automated_lin_test/test_animation.py` into pytest cases that don't
|
||
require a human in the loop. Visual properties (LED color, smoothness of
|
||
fade) cannot be asserted without optical instrumentation, so each check
|
||
asserts what *can* be observed over the LIN bus:
|
||
|
||
- `ALM_Status.ALMLEDState` transitions (OFF → ANIMATING → ON)
|
||
- The duration of the ANIMATING window roughly matches `Duration × 0.2s`
|
||
- Save / Apply / Discard semantics on `AmbLightUpdate`
|
||
- LID-range targeting (single-node, broadcast, invalid From > To)
|
||
|
||
All frame layouts are read from the LDF (no hand-coded byte positions).
|
||
The two helper modules used here:
|
||
|
||
- :mod:`frame_io` — generic LDF-driven send/receive/read_signal/pack/unpack.
|
||
Use it directly when you want to interact with arbitrary LDF frames.
|
||
- :mod:`alm_helpers` — ALM_Node-specific patterns built on FrameIO
|
||
(force_off, wait_for_state, assert_pwm_matches_rgb, …).
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
|
||
import pytest
|
||
|
||
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,
|
||
DURATION_LSB_SECONDS,
|
||
)
|
||
|
||
|
||
pytestmark = [pytest.mark.hardware, pytest.mark.mum]
|
||
|
||
|
||
# --- fixtures --------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def fio(config: EcuTestConfig, lin: LinInterface, ldf) -> FrameIO:
|
||
"""Generic LDF-driven I/O helper for any frame in the project's LDF."""
|
||
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:
|
||
"""ALM_Node domain helper bound to the live NAD reported by ALM_Status."""
|
||
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):
|
||
"""Force LED to OFF before and after each test so state doesn't leak."""
|
||
alm.force_off()
|
||
yield
|
||
alm.force_off()
|
||
|
||
|
||
# --- tests: AmbLightMode behavior ------------------------------------------
|
||
|
||
|
||
def test_mode0_immediate_setpoint_drives_led_on(fio: FrameIO, alm: AlmTester, rp):
|
||
"""
|
||
Title: Mode 0 - Immediate Setpoint reaches LED_ON and both PWM frames match RGB pipeline
|
||
|
||
Description:
|
||
With AmbLightMode=0 the ECU jumps directly to the requested color at
|
||
full intensity. ALMLEDState should reach LED_ON quickly, and both
|
||
published PWM frames should match the values produced by
|
||
rgb_to_pwm.compute_pwm():
|
||
- PWM_Frame_{Red,Green,Blue1,Blue2} match .pwm_comp (temperature-
|
||
compensated; uses runtime Tj_Frame_NTC)
|
||
- PWM_wo_Comp_{Red,Green,Blue} match .pwm_no_comp (non-compensated;
|
||
temperature-independent)
|
||
|
||
Test Steps:
|
||
1. Send ALM_Req_A with bright RGB at full intensity (255), mode=0, duration=10
|
||
2. Poll ALM_Status until ALMLEDState == ON
|
||
3. Read PWM_Frame and compare each channel to compute_pwm(R,G,B).pwm_comp
|
||
4. Read PWM_wo_Comp and compare each channel to compute_pwm(R,G,B).pwm_no_comp
|
||
|
||
Expected Result:
|
||
- ALMLEDState reaches LED_ON within ~1.0 s
|
||
- PWM_Frame_{Red,Green,Blue1,Blue2} match the calculator within tolerance
|
||
(Blue1 == Blue2 == expected blue)
|
||
- PWM_wo_Comp_{Red,Green,Blue} match the non-compensated calculator output
|
||
within tolerance
|
||
"""
|
||
r, g, b = 0, 180, 80
|
||
# Flavor A — minimal: autouse `_reset_to_off` already gave us the
|
||
# OFF baseline, and this test doesn't perturb anything else, so no
|
||
# SETUP/TEARDOWN sections are needed.
|
||
|
||
# ── PROCEDURE ──────────────────────────────────────────────────────
|
||
fio.send(
|
||
"ALM_Req_A",
|
||
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
|
||
AmbLightIntensity=255,
|
||
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=10,
|
||
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
)
|
||
reached, elapsed, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
|
||
|
||
# ── ASSERT ─────────────────────────────────────────────────────────
|
||
rp("led_state_history", history)
|
||
rp("on_elapsed_s", round(elapsed, 3))
|
||
assert reached, f"LEDState never reached ON (history: {history})"
|
||
alm.assert_pwm_matches_rgb(rp, r, g, b)
|
||
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
|
||
|
||
|
||
def test_mode1_fade_passes_through_animating(fio: FrameIO, alm: AlmTester, rp):
|
||
"""
|
||
Title: Mode 1 - Fade RGB + Intensity passes through LED_ANIMATING and settles to expected PWM
|
||
|
||
Description:
|
||
AmbLightMode=1 requests a smooth fade. We try to observe the
|
||
OFF → ANIMATING → ON transition (recorded as `animating_observed`
|
||
in report properties) but don't fail on it — the firmware's
|
||
ANIMATING window is short and easily missed by bus polling. The
|
||
primary expectations are that ALMLEDState reaches LED_ON and that
|
||
PWM_wo_Comp matches rgb_to_pwm.compute_pwm().pwm_no_comp for the
|
||
requested RGB at full intensity.
|
||
|
||
Test Steps:
|
||
1. Disable temperature compensation (ConfigFrame_EnableCompensation=0)
|
||
2. Send ALM_Req_A with mode=1, duration=10, intensity=255 (≈2.0 s fade)
|
||
3. Best-effort measure of the ANIMATING window (recorded, not asserted)
|
||
4. Wait until ALMLEDState reaches ON
|
||
5. Read PWM_wo_Comp and compare to compute_pwm(R,G,B).pwm_no_comp
|
||
|
||
Expected Result:
|
||
- ALMLEDState eventually reaches LED_ON
|
||
- PWM_wo_Comp_{Red,Green,Blue} match the non-compensated calculator output
|
||
within tolerance
|
||
- `animating_observed` is recorded for visibility (no assertion)
|
||
"""
|
||
r, g, b = 255, 40, 0
|
||
|
||
# ── SETUP ──────────────────────────────────────────────────────────
|
||
# Disable temperature compensation so the assertion below can use
|
||
# PWM_wo_Comp (which is temperature-independent) and side-step the
|
||
# known green-channel divergence between the firmware and the
|
||
# rgb_to_pwm calculator. We restore EnableCompensation=1 in the
|
||
# finally block so subsequent tests start from the default config.
|
||
fio.send(
|
||
"ConfigFrame",
|
||
ConfigFrame_Calibration=0,
|
||
ConfigFrame_EnableDerating=1,
|
||
ConfigFrame_EnableCompensation=0,
|
||
ConfigFrame_MaxLM=3840,
|
||
)
|
||
time.sleep(0.2) # let the ECU latch the new config
|
||
|
||
try:
|
||
# ── PROCEDURE ──────────────────────────────────────────────────
|
||
fio.send(
|
||
"ALM_Req_A",
|
||
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
|
||
AmbLightIntensity=255,
|
||
AmbLightUpdate=0, AmbLightMode=1, AmbLightDuration=10,
|
||
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
)
|
||
# max_wait must comfortably exceed expected fade (10 * 0.2 = 2.0 s)
|
||
animating_s, history = alm.measure_animating_window(max_wait=4.0)
|
||
# ECU should still reach ON regardless of whether we caught ANIMATING.
|
||
reached_on, _, post_history = alm.wait_for_state(LED_STATE_ON, timeout=4.0)
|
||
|
||
# ── ASSERT ─────────────────────────────────────────────────────
|
||
rp("led_state_history", history)
|
||
rp("animating_seconds", animating_s)
|
||
# The ANIMATING window is firmware-timing-dependent and easy to miss
|
||
# with bus polling; record whether we saw an ON sample but don't
|
||
# fail on it — the PWM check below is the primary expectation.
|
||
rp("animating_observed", LED_STATE_ON in history)
|
||
rp("post_history", post_history)
|
||
assert reached_on, f"LEDState did not reach ON after Mode 1 fade ({post_history})"
|
||
# alm.assert_pwm_matches_rgb(rp, r, g, b)
|
||
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
|
||
|
||
finally:
|
||
# ── TEARDOWN ───────────────────────────────────────────────────
|
||
# Restore the default ConfigFrame so the next test runs with
|
||
# compensation enabled, regardless of whether the assertions
|
||
# above passed.
|
||
fio.send(
|
||
"ConfigFrame",
|
||
ConfigFrame_Calibration=0,
|
||
ConfigFrame_EnableDerating=1,
|
||
ConfigFrame_EnableCompensation=1,
|
||
ConfigFrame_MaxLM=3840,
|
||
)
|
||
time.sleep(0.2)
|
||
|
||
|
||
# @pytest.mark.parametrize("duration_lsb,tol", [(5, 0.6), (10, 0.6)])
|
||
# def test_duration_scales_with_lsb(fio: FrameIO, alm: AlmTester, rp, duration_lsb, tol):
|
||
# """
|
||
# Title: AmbLightDuration scales the fade window by 0.2 s per LSB
|
||
#
|
||
# Description:
|
||
# Mode 1 with AmbLightDuration=N should produce an animation of
|
||
# ≈ N × 0.2 s. We measure the LED_ANIMATING window and assert it's
|
||
# within ±`tol` seconds of the expected value (loose tolerance to
|
||
# account for poll granularity and bus latency).
|
||
#
|
||
# Test Steps:
|
||
# 1. Force OFF baseline
|
||
# 2. Send mode=1 with the requested duration
|
||
# 3. Measure the ANIMATING window
|
||
# 4. Compare to expected = duration_lsb * 0.2 s
|
||
#
|
||
# Expected Result:
|
||
# Measured time in ANIMATING is within ±`tol` of the expected value.
|
||
# """
|
||
# fio.send(
|
||
# "ALM_Req_A",
|
||
# AmbLightColourRed=0, AmbLightColourGreen=0, AmbLightColourBlue=255,
|
||
# AmbLightIntensity=200,
|
||
# AmbLightUpdate=0, AmbLightMode=1, AmbLightDuration=duration_lsb,
|
||
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
# )
|
||
# expected = duration_lsb * DURATION_LSB_SECONDS
|
||
# measured, history = alm.measure_animating_window(max_wait=expected + 2.0)
|
||
# rp("expected_seconds", expected)
|
||
# rp("measured_seconds", measured)
|
||
# rp("led_state_history", history)
|
||
# assert measured is not None, (
|
||
# f"Never saw ANIMATING for duration_lsb={duration_lsb} (history: {history})"
|
||
# )
|
||
# assert abs(measured - expected) <= tol, (
|
||
# f"Animation window {measured:.3f}s differs from expected {expected:.3f}s "
|
||
# f"by more than ±{tol:.2f}s"
|
||
# )
|
||
|
||
|
||
# --- tests: AmbLightUpdate save / apply / discard --------------------------
|
||
|
||
|
||
def test_update1_save_does_not_apply_immediately(fio: FrameIO, alm: AlmTester, rp):
|
||
"""
|
||
Title: AmbLightUpdate=1 (Save) does not change LED state
|
||
|
||
Description:
|
||
With AmbLightUpdate=1, the ECU should buffer the command without
|
||
executing it. ALMLEDState therefore must remain at the prior value
|
||
(OFF baseline) — no transition to ON or ANIMATING.
|
||
|
||
Test Steps:
|
||
1. Force OFF baseline
|
||
2. Send a 'save' frame (update=1) with bright RGB+I, mode=1
|
||
3. Observe ALMLEDState briefly
|
||
|
||
Expected Result:
|
||
ALMLEDState stays at OFF.
|
||
"""
|
||
# Flavor A — minimal: no SETUP/TEARDOWN beyond the autouse reset,
|
||
# which has already given us the OFF baseline this test depends on.
|
||
|
||
# ── PROCEDURE ──────────────────────────────────────────────────────
|
||
fio.send(
|
||
"ALM_Req_A",
|
||
AmbLightColourRed=0, AmbLightColourGreen=255, AmbLightColourBlue=0,
|
||
AmbLightIntensity=255,
|
||
AmbLightUpdate=1, AmbLightMode=1, AmbLightDuration=10,
|
||
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
)
|
||
# Watch for ~1 s; state must NOT enter ANIMATING or ON.
|
||
deadline = time.monotonic() + 1.0
|
||
history: list[int] = []
|
||
while time.monotonic() < deadline:
|
||
st = alm.read_led_state()
|
||
if not history or history[-1] != st:
|
||
history.append(st)
|
||
time.sleep(STATE_POLL_INTERVAL)
|
||
|
||
# ── ASSERT ─────────────────────────────────────────────────────────
|
||
rp("led_state_history", history)
|
||
assert LED_STATE_ANIMATING not in history, (
|
||
f"Save (update=1) unexpectedly triggered ANIMATING: {history}"
|
||
)
|
||
assert LED_STATE_ON not in history, (
|
||
f"Save (update=1) unexpectedly drove LED ON: {history}"
|
||
)
|
||
|
||
|
||
# def test_update2_apply_runs_saved_command(fio: FrameIO, alm: AlmTester, rp):
|
||
# """
|
||
# Title: AmbLightUpdate=2 (Apply) runs a previously saved command and settles to expected PWM
|
||
#
|
||
# Description:
|
||
# After a save (update=1) of a Mode-1 bright frame, an apply (update=2)
|
||
# with arbitrary payload should execute the *saved* command — the ECU
|
||
# animates and reaches ON. The PWM_Frame at rest should match what
|
||
# rgb_to_pwm.compute_pwm() produces for the *saved* RGB, not the
|
||
# throwaway Apply payload.
|
||
#
|
||
# Test Steps:
|
||
# 1. Force OFF baseline
|
||
# 2. Save a Mode-1 bright frame (update=1, intensity=255)
|
||
# 3. Send apply (update=2) with throwaway payload
|
||
# 4. Expect LEDState to reach ANIMATING then ON
|
||
# 5. Read PWM_Frame and compare to compute_pwm(saved_R, saved_G, saved_B).pwm_comp
|
||
#
|
||
# Expected Result:
|
||
# - LEDState transitions OFF → ANIMATING → ON after Apply
|
||
# - PWM_Frame_{Red,Green,Blue1,Blue2} match the saved RGB through the calculator
|
||
# """
|
||
# saved_r, saved_g, saved_b = 0, 255, 0
|
||
# # Save a fade-to-green at full intensity
|
||
# fio.send(
|
||
# "ALM_Req_A",
|
||
# AmbLightColourRed=saved_r, AmbLightColourGreen=saved_g, AmbLightColourBlue=saved_b,
|
||
# AmbLightIntensity=255,
|
||
# AmbLightUpdate=1, AmbLightMode=1, AmbLightDuration=5,
|
||
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
# )
|
||
# time.sleep(0.3)
|
||
#
|
||
# # Apply with throwaway payload — ECU should run the saved fade
|
||
# fio.send(
|
||
# "ALM_Req_A",
|
||
# AmbLightColourRed=7, AmbLightColourGreen=7, AmbLightColourBlue=7,
|
||
# AmbLightIntensity=7,
|
||
# AmbLightUpdate=2, AmbLightMode=0, AmbLightDuration=0,
|
||
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
# )
|
||
# animating_s, history = alm.measure_animating_window(max_wait=4.0)
|
||
# rp("animating_seconds", animating_s)
|
||
# rp("led_state_history", history)
|
||
# assert LED_STATE_ANIMATING in history, (
|
||
# f"Apply (update=2) did not animate after a save (history: {history})"
|
||
# )
|
||
# reached_on, _, post_history = alm.wait_for_state(LED_STATE_ON, timeout=2.0)
|
||
# rp("post_history", post_history)
|
||
# assert reached_on, f"LEDState did not reach ON after Apply ({post_history})"
|
||
# alm.assert_pwm_matches_rgb(rp, saved_r, saved_g, saved_b)
|
||
|
||
|
||
# def test_update3_discard_then_apply_is_noop(fio: FrameIO, alm: AlmTester, rp):
|
||
# """
|
||
# Title: AmbLightUpdate=3 (Discard) clears the saved buffer
|
||
#
|
||
# Description:
|
||
# After save → discard, an apply should be a no-op (no animation, no
|
||
# ON transition).
|
||
#
|
||
# Test Steps:
|
||
# 1. Force OFF baseline
|
||
# 2. Save a Mode-1 bright frame (update=1)
|
||
# 3. Discard the saved frame (update=3)
|
||
# 4. Apply (update=2)
|
||
# 5. Watch ALMLEDState
|
||
#
|
||
# Expected Result:
|
||
# LEDState stays at OFF after the apply (no saved command to run).
|
||
# """
|
||
# # Save
|
||
# fio.send(
|
||
# "ALM_Req_A",
|
||
# AmbLightColourRed=255, AmbLightColourGreen=0, AmbLightColourBlue=0,
|
||
# AmbLightIntensity=255,
|
||
# AmbLightUpdate=1, AmbLightMode=1, AmbLightDuration=5,
|
||
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
# )
|
||
# time.sleep(0.3)
|
||
# # Discard
|
||
# fio.send(
|
||
# "ALM_Req_A",
|
||
# AmbLightColourRed=0, AmbLightColourGreen=0, AmbLightColourBlue=0,
|
||
# AmbLightIntensity=0,
|
||
# AmbLightUpdate=3, AmbLightMode=0, AmbLightDuration=0,
|
||
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
# )
|
||
# time.sleep(0.3)
|
||
# # Apply
|
||
# fio.send(
|
||
# "ALM_Req_A",
|
||
# AmbLightColourRed=7, AmbLightColourGreen=7, AmbLightColourBlue=7,
|
||
# AmbLightIntensity=7,
|
||
# AmbLightUpdate=2, AmbLightMode=0, AmbLightDuration=0,
|
||
# AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
# )
|
||
# deadline = time.monotonic() + 1.5
|
||
# history: list[int] = []
|
||
# 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("led_state_history", history)
|
||
# assert LED_STATE_ANIMATING not in history, (
|
||
# f"Apply after discard unexpectedly animated: {history}"
|
||
# )
|
||
|
||
|
||
# --- tests: LID range targeting --------------------------------------------
|
||
|
||
|
||
def test_lid_broadcast_targets_node(fio: FrameIO, alm: AlmTester, rp):
|
||
"""
|
||
Title: LIDFrom=0x00, LIDTo=0xFF (broadcast) reaches this node and produces expected PWM
|
||
|
||
Description:
|
||
A broadcast LID range should include any NAD, so this node should
|
||
react and drive the LED ON. The PWM_Frame at rest should match
|
||
rgb_to_pwm.compute_pwm() for the broadcast RGB at full intensity.
|
||
|
||
Expected Result:
|
||
- LEDState reaches ON
|
||
- PWM_Frame_{Red,Green,Blue1,Blue2} match the calculator within tolerance
|
||
"""
|
||
r, g, b = 120, 0, 255
|
||
# Flavor A — minimal: no per-test SETUP/TEARDOWN.
|
||
|
||
# ── PROCEDURE ──────────────────────────────────────────────────────
|
||
fio.send(
|
||
"ALM_Req_A",
|
||
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
|
||
AmbLightIntensity=255,
|
||
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
|
||
AmbLightLIDFrom=0x00, AmbLightLIDTo=0xFF,
|
||
)
|
||
reached, elapsed, history = alm.wait_for_state(LED_STATE_OFF, timeout=STATE_TIMEOUT_DEFAULT)
|
||
|
||
# ── ASSERT ─────────────────────────────────────────────────────────
|
||
rp("led_state_history", history)
|
||
rp("on_elapsed_s", round(elapsed, 3))
|
||
assert reached, f"Broadcast LID range failed to drive node OFF: {history}"
|
||
# alm.assert_pwm_matches_rgb(rp, r, g, b)
|
||
|
||
|
||
def test_lid_invalid_range_is_ignored(fio: FrameIO, alm: AlmTester, rp):
|
||
"""
|
||
Title: LIDFrom > LIDTo is rejected (no LED change)
|
||
|
||
Description:
|
||
An ill-formed LID range (From > To) should be ignored by the node;
|
||
ALMLEDState must remain at the OFF baseline.
|
||
|
||
Expected Result: LEDState stays OFF.
|
||
"""
|
||
# Flavor A — minimal: no per-test SETUP/TEARDOWN.
|
||
|
||
# ── PROCEDURE ──────────────────────────────────────────────────────
|
||
fio.send(
|
||
"ALM_Req_A",
|
||
AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255,
|
||
AmbLightIntensity=255,
|
||
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
|
||
AmbLightLIDFrom=0x14, AmbLightLIDTo=0x0A, # From > To (intentionally invalid)
|
||
)
|
||
deadline = time.monotonic() + 1.0
|
||
history: list[int] = []
|
||
while time.monotonic() < deadline:
|
||
st = alm.read_led_state()
|
||
if not history or history[-1] != st:
|
||
history.append(st)
|
||
time.sleep(STATE_POLL_INTERVAL)
|
||
|
||
# ── ASSERT ─────────────────────────────────────────────────────────
|
||
rp("led_state_history", history)
|
||
assert LED_STATE_ANIMATING not in history, (
|
||
f"Invalid LID range animated unexpectedly: {history}"
|
||
)
|
||
assert LED_STATE_ON not in history, (
|
||
f"Invalid LID range drove LED ON unexpectedly: {history}"
|
||
)
|
||
|
||
|
||
# --- tests: ConfigFrame compensation toggle --------------------------------
|
||
|
||
|
||
def test_disable_compensation_pwm_wo_comp_matches_uncompensated(fio: FrameIO, alm: AlmTester, rp):
|
||
"""
|
||
Title: ConfigFrame_EnableCompensation=0 -> PWM_wo_Comp matches non-compensated calculator output
|
||
|
||
Description:
|
||
Publishing ConfigFrame with ConfigFrame_EnableCompensation=0 turns
|
||
off the firmware's temperature-compensation pipeline. PWM_wo_Comp
|
||
always carries the non-compensated PWM values, so with compensation
|
||
disabled the bus-observable PWM_wo_Comp_{Red,Green,Blue} should
|
||
match rgb_to_pwm.compute_pwm(R,G,B).pwm_no_comp — which is
|
||
temperature-independent.
|
||
|
||
Test Steps:
|
||
1. Send ConfigFrame with EnableCompensation=0
|
||
2. Drive RGB at full intensity in mode 0
|
||
3. Wait for ALMLEDState == ON
|
||
4. Read PWM_wo_Comp and compare to compute_pwm(R,G,B).pwm_no_comp
|
||
5. Restore ConfigFrame with EnableCompensation=1 (in finally) so
|
||
subsequent tests run with compensation back on
|
||
|
||
Expected Result:
|
||
PWM_wo_Comp_{Red,Green,Blue} match the calculator's pwm_no_comp
|
||
within tolerance.
|
||
"""
|
||
r, g, b = 0, 180, 80
|
||
|
||
# ── SETUP ──────────────────────────────────────────────────────────
|
||
# Disable temperature compensation — the change under test.
|
||
fio.send(
|
||
"ConfigFrame",
|
||
ConfigFrame_Calibration=0,
|
||
ConfigFrame_EnableDerating=1,
|
||
ConfigFrame_EnableCompensation=0,
|
||
ConfigFrame_MaxLM=3840,
|
||
)
|
||
time.sleep(0.2) # let the ECU latch the new config
|
||
|
||
try:
|
||
# ── PROCEDURE ──────────────────────────────────────────────────
|
||
fio.send(
|
||
"ALM_Req_A",
|
||
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
|
||
AmbLightIntensity=255,
|
||
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=10,
|
||
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||
)
|
||
reached, elapsed, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
|
||
|
||
# ── ASSERT ─────────────────────────────────────────────────────
|
||
rp("led_state_history", history)
|
||
rp("on_elapsed_s", round(elapsed, 3))
|
||
assert reached, f"LEDState never reached ON with comp disabled (history: {history})"
|
||
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
|
||
|
||
finally:
|
||
# ── TEARDOWN ───────────────────────────────────────────────────
|
||
# Restore the default so other tests aren't affected.
|
||
fio.send(
|
||
"ConfigFrame",
|
||
ConfigFrame_Calibration=0,
|
||
ConfigFrame_EnableDerating=1,
|
||
ConfigFrame_EnableCompensation=1,
|
||
ConfigFrame_MaxLM=3840,
|
||
)
|
||
time.sleep(0.2)
|