Extends ``tests/hardware/alm_helpers.py`` into the full surface that
hardware tests use, so contributors write intent (``alm.send_color``,
``alm.read_led_state``, ``alm.wait_for_led_on``) and never touch
``fio.send("ALM_Req_A", AmbLight…=…)`` or LDF schema details.
What landed:
- AlmTester gains ~16 methods:
read_nad, read_voltage_status, read_thermal_status, read_nvm_status,
read_sig_comm_err, read_ntc_kelvin, read_ntc_celsius, read_pwm,
read_pwm_wo_comp, send_color, send_color_broadcast, save_color,
apply_saved_color, discard_saved_color, send_config, plus
wait_for_led_on / wait_for_led_off / wait_for_animating wrappers.
- The six IntEnum classes that ALM tests need (LedState, Mode, Update,
NVMStatus, VoltageStatus, ThermalStatus) are defined directly in
alm_helpers.py — tests get them via `from alm_helpers import …`.
- All ALM test files migrated:
test_mum_alm_animation.py, test_mum_alm_cases.py, test_overvolt.py,
swe5/test_anm_management.py, swe5/test_com_management.py
each now go through AlmTester for every common pattern.
- swe6/test_com_management.py: stays on `fio` (these tests probe
schema features not in the current production LDF and skip when
the LDF doesn't declare them) — change limited to LedState enum.
- test_mum_alm_animation_generated.py deleted — its "no-AlmTester"
demonstration loses its point now that AlmTester is the
recommended path.
- docs/19_frame_io_and_alm_helpers.md reframed: AlmTester is the
contributor surface; FrameIO is implementation detail. New API
reference + Cookbook examples + a note that the maintenance pact
is "LDF changes → AlmTester updates".
Verified: pytest --collect-only collects 87 tests cleanly; 40 unit
+ mock smoke tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
472 lines
20 KiB
Python
472 lines
20 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)
|
||
|
||
Test bodies go through :class:`AlmTester` exclusively — frame names and
|
||
signal kwargs live in :mod:`alm_helpers`, not here.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
|
||
import pytest
|
||
|
||
from alm_helpers import (
|
||
AlmTester,
|
||
LedState, Mode, Update,
|
||
STATE_POLL_INTERVAL, STATE_TIMEOUT_DEFAULT,
|
||
)
|
||
|
||
|
||
pytestmark = [pytest.mark.ANM]
|
||
|
||
|
||
# Fixtures (fio, alm, _reset_to_off) and the MUM gate come from
|
||
# tests/hardware/mum/conftest.py — see that file for scope rationale.
|
||
|
||
|
||
# --- tests: AmbLightMode behavior ------------------------------------------
|
||
|
||
|
||
def test_mode0_immediate_setpoint_drives_led_on(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)
|
||
|
||
Requirements: REQ-MODE0-IMMEDIATE
|
||
|
||
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 ──────────────────────────────────────────────────────
|
||
alm.send_color(red=r, green=g, blue=b, duration=10)
|
||
reached, elapsed, history = alm.wait_for_state(LedState.LED_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(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.
|
||
|
||
Requirements: REQ-MODE1-FADE
|
||
|
||
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.
|
||
alm.send_config(enable_compensation=0)
|
||
time.sleep(0.2) # let the ECU latch the new config
|
||
|
||
try:
|
||
# ── PROCEDURE ──────────────────────────────────────────────────
|
||
alm.send_color(red=r, green=g, blue=b, mode=Mode.FADING_EFFECT_1, duration=10)
|
||
# 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(LedState.LED_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", LedState.LED_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.
|
||
alm.send_config(enable_compensation=1)
|
||
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(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.
|
||
|
||
Requirements: REQ-101
|
||
|
||
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 ──────────────────────────────────────────────────────
|
||
alm.save_color(red=0, green=255, blue=0, mode=Mode.FADING_EFFECT_1, duration=10)
|
||
# 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 LedState.LED_ANIMATING not in history, (
|
||
f"Save (update=1) unexpectedly triggered ANIMATING: {history}"
|
||
)
|
||
assert LedState.LED_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(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.
|
||
|
||
Requirements: REQ-LID-BROADCAST, REQ-LID-LED-RESPONSE
|
||
|
||
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 ──────────────────────────────────────────────────────
|
||
alm.send_color_broadcast(red=r, green=g, blue=b)
|
||
reached, elapsed, history = alm.wait_for_state(LedState.LED_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(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.
|
||
|
||
Requirements: REQ-LID-INVALID
|
||
|
||
Expected Result: LEDState stays OFF.
|
||
"""
|
||
# Flavor A — minimal: no per-test SETUP/TEARDOWN.
|
||
|
||
# ── PROCEDURE ──────────────────────────────────────────────────────
|
||
alm.send_color(
|
||
red=255, green=255, blue=255,
|
||
lid_from=0x14, lid_to=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 LedState.LED_ANIMATING not in history, (
|
||
f"Invalid LID range animated unexpectedly: {history}"
|
||
)
|
||
assert LedState.LED_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(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.
|
||
|
||
Requirements: REQ-CONFIG-COMP
|
||
|
||
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.
|
||
alm.send_config(enable_compensation=0)
|
||
time.sleep(0.2) # let the ECU latch the new config
|
||
|
||
try:
|
||
# ── PROCEDURE ──────────────────────────────────────────────────
|
||
alm.send_color(red=r, green=g, blue=b, duration=10)
|
||
reached, elapsed, history = alm.wait_for_state(LedState.LED_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.
|
||
alm.send_config(enable_compensation=1)
|
||
time.sleep(0.2)
|