Restructures tests/hardware/ so that fixture access is controlled by
directory layout — pytest only walks upward through conftest.py files,
so a PSU test physically cannot request fio/alm/nad.
Layout:
- tests/hardware/conftest.py (unchanged: PSU fixtures)
- tests/hardware/mum/conftest.py NEW: _require_mum (session autouse),
fio (session), nad (session),
alm (session), _reset_to_off
(function autouse)
- tests/hardware/mum/** MUM tests + swe5/ + swe6/
- tests/hardware/psu/** PSU-only tests
- tests/hardware/babylin/** deprecated BabyLIN E2E
What this removes (was duplicated before):
- 7 verbatim copies of the `fio` fixture
- 6 copies of the `alm` fixture
- 6 copies of the `_reset_to_off` autouse
- 9 inline `if config.interface.type != "mum": pytest.skip(...)` gates
What this changes by design:
- fio / alm / nad scope: module → session. NAD discovery happens once
per run instead of once per module. The helpers are immutable beyond
their constructor args, so sharing them is safe; per-test state is
reset by the autouse `_reset_to_off`.
- test_overvolt.py: `_park_at_nominal` is now `_reset_to_off`, which
cleanly overrides the conftest's LED-only version (PSU + LED reset).
- test_mum_alm_animation_generated.py keeps a local `_reset_to_off` +
`_force_off` so its "no AlmTester anywhere" demonstration is preserved
via fixture override; the local `nad` is also retained because it
uses the typed `AlmStatus.receive` API.
Docs:
- docs/24_test_wiring.md NEW — describes the three-layer fixture
topology, lifecycle sequence diagram, helper class wiring, and the
playbook for adding a new framework component.
- docs/05_architecture_overview.md: add MCF (mum conftest) node to the
Mermaid diagram + mention it in the components list.
- docs/19_frame_io_and_alm_helpers.md: replace the per-module
fixture-wiring example with a request-fixtures-by-name snippet plus
the override pattern.
- Path references swept across docs/02, docs/14, docs/18, docs/20,
docs/README to point at the new locations.
Verified: pytest --collect-only collects 93 tests with no errors;
30 unit tests and 10 mock-only smoke tests pass; fixture-per-test
output shows PSU tests cannot see fio/alm/nad.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
537 lines
21 KiB
Python
537 lines
21 KiB
Python
"""Animation tests using only the generated LIN API + FrameIO.
|
|
|
|
Parallels :mod:`test_mum_alm_animation` but imports **nothing** from
|
|
``alm_helpers`` — frame and signal names, state values, encoding-type
|
|
constants, and tolerances all come from the generated ``_generated.lin_api``
|
|
module (or are declared locally in this file).
|
|
|
|
Why this file exists:
|
|
|
|
- It's a worked example of what tests look like when they go straight
|
|
through the generated layer.
|
|
- It makes the trade-off concrete. The patterns ``AlmTester`` provides
|
|
(``force_off``, ``wait_for_state``, ``measure_animating_window``,
|
|
``assert_pwm_matches_rgb``) reappear in this file as module-level
|
|
helpers because they can't be derived from the LDF — they're test
|
|
intent, not schema.
|
|
- It serves as a reference for "what does the generated layer give you
|
|
on its own" before deciding whether a future ECU needs its own
|
|
``<ecu>_helpers.py``.
|
|
|
|
If you're writing a *new* ALM test that needs these patterns, prefer the
|
|
``alm_helpers.AlmTester`` path — the patterns are reused across the suite
|
|
and belong in one place. This file deliberately duplicates them to
|
|
demonstrate the seam.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Optional
|
|
|
|
import pytest
|
|
|
|
|
|
from frame_io import FrameIO
|
|
from vendor.rgb_to_pwm import compute_pwm
|
|
|
|
from _generated.lin_api import (
|
|
AlmReqA,
|
|
AlmStatus,
|
|
ConfigFrame,
|
|
PwmFrame,
|
|
PwmWoComp,
|
|
TjFrame,
|
|
LedState,
|
|
Mode,
|
|
Update,
|
|
)
|
|
|
|
|
|
pytestmark = [pytest.mark.ANM]
|
|
|
|
|
|
# --- cadences / tolerances (not in the LDF) --------------------------------
|
|
# These are test-bench choices, not schema. They mirror the values in
|
|
# alm_helpers.py:40-53 and exist here only because this file is a worked
|
|
# example of avoiding the alm_helpers import.
|
|
STATE_POLL_INTERVAL = 0.05 # 50 ms (5 LIN periods)
|
|
STATE_TIMEOUT_DEFAULT = 1.0
|
|
PWM_SETTLE_SECONDS = 0.1 # 100 ms — TX-buffer refresh
|
|
FORCE_OFF_SETTLE_SECONDS = 0.4
|
|
KELVIN_TO_CELSIUS_OFFSET = 273.15
|
|
PWM_ABS_TOL = 3277 # ±5% of 16-bit full scale
|
|
PWM_REL_TOL = 0.05
|
|
|
|
|
|
# --- module-local semantic helpers -----------------------------------------
|
|
# These mirror AlmTester's methods. They live here only because this file
|
|
# is the "no alm_helpers" reference. New code should use AlmTester instead.
|
|
|
|
|
|
def _force_off(fio: FrameIO, nad: int) -> None:
|
|
AlmReqA.send(
|
|
fio,
|
|
AmbLightColourRed=0, AmbLightColourGreen=0, AmbLightColourBlue=0,
|
|
AmbLightIntensity=0,
|
|
AmbLightUpdate=Update.IMMEDIATE_COLOR_UPDATE,
|
|
AmbLightMode=Mode.IMMEDIATE_SETPOINT,
|
|
AmbLightDuration=0,
|
|
AmbLightLIDFrom=nad, AmbLightLIDTo=nad,
|
|
)
|
|
time.sleep(FORCE_OFF_SETTLE_SECONDS)
|
|
|
|
|
|
def _read_led_state(fio: FrameIO, timeout: float = 0.2) -> int:
|
|
"""Read ALM_Status.ALMLEDState; -1 on timeout."""
|
|
decoded = AlmStatus.receive(fio, timeout=timeout)
|
|
if decoded is None:
|
|
return -1
|
|
return int(decoded.get("ALMLEDState", -1))
|
|
|
|
|
|
def _wait_for_state(
|
|
fio: FrameIO, target: int, timeout: float
|
|
) -> tuple[bool, float, list[int]]:
|
|
seen: list[int] = []
|
|
start = time.monotonic()
|
|
deadline = start + timeout
|
|
while time.monotonic() < deadline:
|
|
st = _read_led_state(fio)
|
|
if not seen or seen[-1] != st:
|
|
seen.append(st)
|
|
if st == target:
|
|
return True, time.monotonic() - start, seen
|
|
time.sleep(STATE_POLL_INTERVAL)
|
|
return False, time.monotonic() - start, seen
|
|
|
|
|
|
def _measure_animating_window(
|
|
fio: FrameIO, max_wait: float
|
|
) -> tuple[Optional[float], list[int]]:
|
|
seen: list[int] = []
|
|
started_at: Optional[float] = None
|
|
deadline = time.monotonic() + max_wait
|
|
while time.monotonic() < deadline:
|
|
st = _read_led_state(fio)
|
|
if not seen or seen[-1] != st:
|
|
seen.append(st)
|
|
if started_at is None and st == LedState.LED_ANIMATING:
|
|
started_at = time.monotonic()
|
|
elif started_at is not None and st != LedState.LED_ANIMATING:
|
|
return time.monotonic() - started_at, seen
|
|
time.sleep(STATE_POLL_INTERVAL)
|
|
return None, seen
|
|
|
|
|
|
def _pwm_within_tol(actual: int, expected: int) -> bool:
|
|
return abs(actual - expected) <= max(PWM_ABS_TOL, abs(expected) * PWM_REL_TOL)
|
|
|
|
|
|
def _band(expected: int) -> int:
|
|
return max(PWM_ABS_TOL, int(abs(expected) * PWM_REL_TOL))
|
|
|
|
|
|
def _assert_pwm_matches_rgb(fio: FrameIO, rp, r: int, g: int, b: int) -> None:
|
|
"""PWM_Frame_{Red,Green,Blue1,Blue2} match compute_pwm(...).pwm_comp."""
|
|
ntc_raw = TjFrame.read_signal(fio, "Tj_Frame_NTC")
|
|
assert ntc_raw is not None, "Tj_Frame not received within timeout"
|
|
temp_c = float(ntc_raw) - KELVIN_TO_CELSIUS_OFFSET
|
|
rp("ntc_raw_kelvin", int(ntc_raw))
|
|
rp("temp_c_used", round(temp_c, 2))
|
|
|
|
exp_r, exp_g, exp_b = compute_pwm(r, g, b, temp_c=temp_c).pwm_comp
|
|
rp("expected_pwm", {
|
|
"red": exp_r, "green": exp_g, "blue": exp_b,
|
|
"rgb_in": (r, g, b), "temp_c_used": round(temp_c, 2),
|
|
})
|
|
|
|
time.sleep(PWM_SETTLE_SECONDS)
|
|
decoded = PwmFrame.receive(fio)
|
|
assert decoded is not None, "PWM_Frame not received within timeout"
|
|
actual_r = int(decoded["PWM_Frame_Red"])
|
|
actual_g = int(decoded["PWM_Frame_Green"])
|
|
actual_b1 = int(decoded["PWM_Frame_Blue1"])
|
|
actual_b2 = int(decoded["PWM_Frame_Blue2"])
|
|
rp("actual_pwm", {
|
|
"red": actual_r, "green": actual_g,
|
|
"blue1": actual_b1, "blue2": actual_b2,
|
|
})
|
|
|
|
assert _pwm_within_tol(actual_r, exp_r), (
|
|
f"PWM_Frame_Red {actual_r} differs from expected {exp_r} "
|
|
f"by more than ±{_band(exp_r)} (rgb_in={(r, g, b)})"
|
|
)
|
|
assert _pwm_within_tol(actual_g, exp_g), (
|
|
f"PWM_Frame_Green {actual_g} differs from expected {exp_g} "
|
|
f"by more than ±{_band(exp_g)} (rgb_in={(r, g, b)})"
|
|
)
|
|
assert _pwm_within_tol(actual_b1, exp_b), (
|
|
f"PWM_Frame_Blue1 {actual_b1} differs from expected {exp_b} "
|
|
f"by more than ±{_band(exp_b)} (rgb_in={(r, g, b)})"
|
|
)
|
|
assert _pwm_within_tol(actual_b2, exp_b), (
|
|
f"PWM_Frame_Blue2 {actual_b2} differs from expected {exp_b} "
|
|
f"by more than ±{_band(exp_b)} (rgb_in={(r, g, b)})"
|
|
)
|
|
|
|
|
|
def _assert_pwm_wo_comp_matches_rgb(fio: FrameIO, rp, r: int, g: int, b: int) -> None:
|
|
"""PWM_wo_Comp_{Red,Green,Blue} match compute_pwm(...).pwm_no_comp."""
|
|
exp_r, exp_g, exp_b = compute_pwm(r, g, b).pwm_no_comp
|
|
rp("expected_pwm_wo_comp", {
|
|
"red": exp_r, "green": exp_g, "blue": exp_b, "rgb_in": (r, g, b),
|
|
})
|
|
rp("ntc_raw_kelvin", TjFrame.read_signal(fio, "Tj_Frame_NTC"))
|
|
|
|
time.sleep(PWM_SETTLE_SECONDS)
|
|
decoded = PwmWoComp.receive(fio)
|
|
assert decoded is not None, "PWM_wo_Comp not received within timeout"
|
|
actual_r = int(decoded["PWM_wo_Comp_Red"])
|
|
actual_g = int(decoded["PWM_wo_Comp_Green"])
|
|
actual_b = int(decoded["PWM_wo_Comp_Blue"])
|
|
rp("actual_pwm_wo_comp", {
|
|
"red": actual_r, "green": actual_g, "blue": actual_b,
|
|
})
|
|
|
|
assert _pwm_within_tol(actual_r, exp_r), (
|
|
f"PWM_wo_Comp_Red {actual_r} differs from expected {exp_r} "
|
|
f"by more than ±{_band(exp_r)} (rgb_in={(r, g, b)})"
|
|
)
|
|
assert _pwm_within_tol(actual_g, exp_g), (
|
|
f"PWM_wo_Comp_Green {actual_g} differs from expected {exp_g} "
|
|
f"by more than ±{_band(exp_g)} (rgb_in={(r, g, b)})"
|
|
)
|
|
assert _pwm_within_tol(actual_b, exp_b), (
|
|
f"PWM_wo_Comp_Blue {actual_b} differs from expected {exp_b} "
|
|
f"by more than ±{_band(exp_b)} (rgb_in={(r, g, b)})"
|
|
)
|
|
|
|
|
|
# --- fixtures --------------------------------------------------------------
|
|
#
|
|
# ``fio`` comes from ``tests/hardware/mum/conftest.py``. We deliberately
|
|
# keep local ``nad`` and ``_reset_to_off`` overrides here so that this
|
|
# module continues to demonstrate the "no AlmTester anywhere" path — the
|
|
# typed ``AlmStatus.receive`` / ``AlmReqA.send`` calls (via ``_force_off``)
|
|
# replace what AlmTester would do.
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def nad(fio: FrameIO) -> int:
|
|
"""Live NAD reported by ALM_Status; used as LIDFrom/LIDTo in unicast sends.
|
|
|
|
Overrides the conftest's stringly-typed ``nad`` fixture to use the
|
|
generated typed ``AlmStatus.receive`` API instead.
|
|
"""
|
|
decoded = AlmStatus.receive(fio, timeout=1.0)
|
|
if decoded is None:
|
|
pytest.skip("ECU not responding on ALM_Status — check wiring/power")
|
|
n = int(decoded["ALMNadNo"])
|
|
if not (0x01 <= n <= 0xFE):
|
|
pytest.skip(f"ECU reports invalid NAD {n:#x} — auto-addressing first")
|
|
return n
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_to_off(fio: FrameIO, nad: int):
|
|
"""Force LED to OFF before and after each test using only the generated API.
|
|
|
|
Overrides the conftest's AlmTester-based ``_reset_to_off`` to keep this
|
|
module's "no AlmTester" demonstration intact.
|
|
"""
|
|
_force_off(fio, nad)
|
|
yield
|
|
_force_off(fio, nad)
|
|
|
|
|
|
# --- tests: AmbLightMode behavior ------------------------------------------
|
|
|
|
|
|
def test_mode0_immediate_setpoint_drives_led_on(fio: FrameIO, nad: int, rp):
|
|
"""
|
|
Title: Mode 0 - Immediate Setpoint reaches LED_ON and both PWM frames match RGB pipeline
|
|
|
|
Description:
|
|
With AmbLightMode=IMMEDIATE_SETPOINT 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
|
|
"""
|
|
r, g, b = 0, 180, 80
|
|
|
|
# ── PROCEDURE ──────────────────────────────────────────────────────
|
|
AlmReqA.send(
|
|
fio,
|
|
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
|
|
AmbLightIntensity=255,
|
|
AmbLightUpdate=Update.IMMEDIATE_COLOR_UPDATE,
|
|
AmbLightMode=Mode.IMMEDIATE_SETPOINT,
|
|
AmbLightDuration=10,
|
|
AmbLightLIDFrom=nad, AmbLightLIDTo=nad,
|
|
)
|
|
reached, elapsed, history = _wait_for_state(
|
|
fio, 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 LED_ON (history: {history})"
|
|
_assert_pwm_matches_rgb(fio, rp, r, g, b)
|
|
_assert_pwm_wo_comp_matches_rgb(fio, rp, r, g, b)
|
|
|
|
|
|
def test_mode1_fade_passes_through_animating(fio: FrameIO, nad: int, rp):
|
|
"""
|
|
Title: Mode 1 - Fade RGB + Intensity passes through LED_ANIMATING and settles to expected PWM
|
|
|
|
Description:
|
|
AmbLightMode=FADING_EFFECT_1 requests a smooth fade. We try to
|
|
observe the LED_OFF -> LED_ANIMATING -> LED_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 expectation is 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
|
|
"""
|
|
r, g, b = 255, 40, 0
|
|
|
|
# ── SETUP ──────────────────────────────────────────────────────────
|
|
# Disable temperature compensation so the assertion can use PWM_wo_Comp
|
|
# (which is temperature-independent). Restore in finally.
|
|
ConfigFrame.send(
|
|
fio,
|
|
ConfigFrame_Calibration=0,
|
|
ConfigFrame_EnableDerating=1,
|
|
ConfigFrame_EnableCompensation=0,
|
|
ConfigFrame_MaxLM=3840,
|
|
)
|
|
time.sleep(0.2)
|
|
|
|
try:
|
|
# ── PROCEDURE ──────────────────────────────────────────────────
|
|
AlmReqA.send(
|
|
fio,
|
|
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
|
|
AmbLightIntensity=255,
|
|
AmbLightUpdate=Update.IMMEDIATE_COLOR_UPDATE,
|
|
AmbLightMode=Mode.FADING_EFFECT_1,
|
|
AmbLightDuration=10,
|
|
AmbLightLIDFrom=nad, AmbLightLIDTo=nad,
|
|
)
|
|
animating_s, history = _measure_animating_window(fio, max_wait=4.0)
|
|
reached_on, _, post_history = _wait_for_state(
|
|
fio, LedState.LED_ON, timeout=4.0
|
|
)
|
|
|
|
# ── ASSERT ─────────────────────────────────────────────────────
|
|
rp("led_state_history", history)
|
|
rp("animating_seconds", animating_s)
|
|
rp("animating_observed", LedState.LED_ON in history)
|
|
rp("post_history", post_history)
|
|
assert reached_on, (
|
|
f"LEDState did not reach LED_ON after Mode 1 fade ({post_history})"
|
|
)
|
|
_assert_pwm_wo_comp_matches_rgb(fio, rp, r, g, b)
|
|
|
|
finally:
|
|
# ── TEARDOWN ───────────────────────────────────────────────────
|
|
ConfigFrame.send(
|
|
fio,
|
|
ConfigFrame_Calibration=0,
|
|
ConfigFrame_EnableDerating=1,
|
|
ConfigFrame_EnableCompensation=1,
|
|
ConfigFrame_MaxLM=3840,
|
|
)
|
|
time.sleep(0.2)
|
|
|
|
|
|
# --- tests: AmbLightUpdate save / apply / discard --------------------------
|
|
|
|
|
|
def test_update1_save_does_not_apply_immediately(fio: FrameIO, nad: int, rp):
|
|
"""
|
|
Title: AmbLightUpdate=COLOR_MEMORIZATION does not change LED state
|
|
|
|
Description:
|
|
With AmbLightUpdate=COLOR_MEMORIZATION the ECU should buffer the
|
|
command without executing it. ALMLEDState therefore must remain at
|
|
the prior value (LED_OFF baseline) — no transition to LED_ON or
|
|
LED_ANIMATING.
|
|
|
|
Requirements: REQ-101
|
|
"""
|
|
# ── PROCEDURE ──────────────────────────────────────────────────────
|
|
AlmReqA.send(
|
|
fio,
|
|
AmbLightColourRed=0, AmbLightColourGreen=255, AmbLightColourBlue=0,
|
|
AmbLightIntensity=255,
|
|
AmbLightUpdate=Update.COLOR_MEMORIZATION,
|
|
AmbLightMode=Mode.FADING_EFFECT_1,
|
|
AmbLightDuration=10,
|
|
AmbLightLIDFrom=nad, AmbLightLIDTo=nad,
|
|
)
|
|
deadline = time.monotonic() + 1.0
|
|
history: list[int] = []
|
|
while time.monotonic() < deadline:
|
|
st = _read_led_state(fio)
|
|
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.COLOR_MEMORIZATION) unexpectedly triggered ANIMATING: {history}"
|
|
)
|
|
assert LedState.LED_ON not in history, (
|
|
f"Save (Update.COLOR_MEMORIZATION) unexpectedly drove LED ON: {history}"
|
|
)
|
|
|
|
|
|
# --- tests: LID range targeting --------------------------------------------
|
|
|
|
|
|
def test_lid_broadcast_targets_node(fio: FrameIO, nad: int, rp):
|
|
"""
|
|
Title: LIDFrom=0x00, LIDTo=0xFF (broadcast) reaches this node
|
|
|
|
Description:
|
|
A broadcast LID range should include any NAD, so this node should
|
|
react. We assert against LED_OFF here (matches the parallel test
|
|
in test_mum_alm_animation.py:447 — note that test compares against
|
|
OFF, not ON; preserving the same behavior).
|
|
|
|
Requirements: REQ-LID-BROADCAST, REQ-LID-LED-RESPONSE
|
|
"""
|
|
r, g, b = 120, 0, 255
|
|
|
|
# ── PROCEDURE ──────────────────────────────────────────────────────
|
|
AlmReqA.send(
|
|
fio,
|
|
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
|
|
AmbLightIntensity=255,
|
|
AmbLightUpdate=Update.IMMEDIATE_COLOR_UPDATE,
|
|
AmbLightMode=Mode.IMMEDIATE_SETPOINT,
|
|
AmbLightDuration=0,
|
|
AmbLightLIDFrom=0x00, AmbLightLIDTo=0xFF,
|
|
)
|
|
reached, elapsed, history = _wait_for_state(
|
|
fio, 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}"
|
|
|
|
|
|
def test_lid_invalid_range_is_ignored(fio: FrameIO, nad: int, 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 LED_OFF baseline.
|
|
|
|
Requirements: REQ-LID-INVALID
|
|
"""
|
|
# ── PROCEDURE ──────────────────────────────────────────────────────
|
|
AlmReqA.send(
|
|
fio,
|
|
AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255,
|
|
AmbLightIntensity=255,
|
|
AmbLightUpdate=Update.IMMEDIATE_COLOR_UPDATE,
|
|
AmbLightMode=Mode.IMMEDIATE_SETPOINT,
|
|
AmbLightDuration=0,
|
|
AmbLightLIDFrom=0x14, AmbLightLIDTo=0x0A, # From > To (invalid)
|
|
)
|
|
deadline = time.monotonic() + 1.0
|
|
history: list[int] = []
|
|
while time.monotonic() < deadline:
|
|
st = _read_led_state(fio)
|
|
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(fio: FrameIO, nad: int, 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
|
|
"""
|
|
r, g, b = 0, 180, 80
|
|
|
|
# ── SETUP ──────────────────────────────────────────────────────────
|
|
ConfigFrame.send(
|
|
fio,
|
|
ConfigFrame_Calibration=0,
|
|
ConfigFrame_EnableDerating=1,
|
|
ConfigFrame_EnableCompensation=0,
|
|
ConfigFrame_MaxLM=3840,
|
|
)
|
|
time.sleep(0.2)
|
|
|
|
try:
|
|
# ── PROCEDURE ──────────────────────────────────────────────────
|
|
AlmReqA.send(
|
|
fio,
|
|
AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
|
|
AmbLightIntensity=255,
|
|
AmbLightUpdate=Update.IMMEDIATE_COLOR_UPDATE,
|
|
AmbLightMode=Mode.IMMEDIATE_SETPOINT,
|
|
AmbLightDuration=10,
|
|
AmbLightLIDFrom=nad, AmbLightLIDTo=nad,
|
|
)
|
|
reached, elapsed, history = _wait_for_state(
|
|
fio, 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 LED_ON with comp disabled (history: {history})"
|
|
)
|
|
_assert_pwm_wo_comp_matches_rgb(fio, rp, r, g, b)
|
|
|
|
finally:
|
|
# ── TEARDOWN ───────────────────────────────────────────────────
|
|
ConfigFrame.send(
|
|
fio,
|
|
ConfigFrame_Calibration=0,
|
|
ConfigFrame_EnableDerating=1,
|
|
ConfigFrame_EnableCompensation=1,
|
|
ConfigFrame_MaxLM=3840,
|
|
)
|
|
time.sleep(0.2)
|