Voltage-changing tests can't sleep a fixed amount and assume the
rail is there — Owon settling is bench-dependent and typically
asymmetric (up-step ≠ down-step). New shared helpers and tests use
the rail's measured value to drive timing.
- tests/hardware/psu_helpers.py:
wait_until_settled(psu, target_v, ...)
polls measure_voltage_v() until within tol, returns
(elapsed_s, trace) or (None, trace) on timeout
apply_voltage_and_settle(psu, target_v, validation_time, ...)
composite: set setpoint → wait until measured matches →
sleep validation_time so the firmware-side observer can
detect and republish status. Raises on settle timeout.
downsample_trace, plus DEFAULT_VOLTAGE_TOL_V (0.10),
DEFAULT_POLL_INTERVAL_S (0.05), DEFAULT_SETTLE_TIMEOUT_S (10.0),
DEFAULT_VALIDATION_TIME_S (1.0).
- test_overvolt.py: voltage-tolerance suite. Each test (over,
under, parametrized sweep) uses apply_voltage_and_settle for the
procedure, the autouse _park_at_nominal fixture (also via the
helper), and a single deterministic ALM_Status read after the
validation hold instead of polling-the-bus.
- test_psu_voltage_settling.py: characterization test, opt-in via
the new psu_settling marker. Walks four (start_v, target_v)
transitions and records settling_time_s + voltage_trace per case.
Values feed directly into test_overvolt's ECU_VALIDATION_TIME_S
budgeting.
- pytest.ini:
junit_family = legacy → record_property() entries now actually
appear in reports/junit.xml (the default xunit2 silently
dropped them with a collect-time warning, breaking the
conftest plugin's metadata round-trip)
psu_settling marker registered
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
6.8 KiB
Python
183 lines
6.8 KiB
Python
"""Shared PSU helpers for hardware tests.
|
|
|
|
The Owon PSU does not slew instantaneously, and the slew time depends on
|
|
the bench (PSU model, load, cable drop). Tests that change supply
|
|
voltage must therefore *measure* the rail before assuming the new
|
|
voltage is present, instead of waiting a fixed sleep.
|
|
|
|
This module provides two layers:
|
|
|
|
- :func:`wait_until_settled` — primitive: poll
|
|
``psu.measure_voltage_v()`` until it falls within a tolerance band
|
|
of the target. Returns the elapsed time and the full poll trace.
|
|
|
|
- :func:`apply_voltage_and_settle` — composite: write a setpoint,
|
|
wait for the rail to actually be there, then hold for a
|
|
configurable ``validation_time`` so any downstream observer (an
|
|
ECU monitoring its supply rail and reporting status over LIN) has
|
|
time to detect and react. Returns a structured dict that callers
|
|
record to the report.
|
|
|
|
The pattern in tests is:
|
|
|
|
apply_voltage_and_settle(psu, OVERVOLTAGE_V,
|
|
validation_time=ECU_VALIDATION_TIME_S)
|
|
status = fio.read_signal("ALM_Status", "ALMVoltageStatus")
|
|
assert status == VOLTAGE_STATUS_OVER
|
|
|
|
— a single deterministic status read instead of polling the bus
|
|
hoping the ECU has caught up.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Optional
|
|
|
|
from ecu_framework.power import OwonPSU
|
|
|
|
|
|
# ── tunable defaults (override per call when needed) ─────────────────────
|
|
# Tolerance band for "the PSU has reached the target". 100 mV is well
|
|
# within typical Owon regulation accuracy and tight enough that we're
|
|
# really measuring the slewed voltage, not loop noise.
|
|
DEFAULT_VOLTAGE_TOL_V = 0.10
|
|
|
|
# Polling interval. The serial round-trip is ~10 ms; 50 ms gives clean
|
|
# samples without saturating the link.
|
|
DEFAULT_POLL_INTERVAL_S = 0.05
|
|
|
|
# Maximum time to wait for the PSU to settle. Owon settling on small
|
|
# steps is sub-second; on big steps a few seconds. 10 s is a generous
|
|
# fence that surfaces a real bench problem if exceeded.
|
|
DEFAULT_SETTLE_TIMEOUT_S = 10.0
|
|
|
|
# Default time to hold after the PSU settles before the test reads any
|
|
# downstream status. This is the **firmware-dependent** budget — how
|
|
# long the ECU needs to detect the new voltage and republish status.
|
|
# Tune to your firmware spec.
|
|
DEFAULT_VALIDATION_TIME_S = 1.0
|
|
|
|
|
|
# ── primitive: poll until settled ────────────────────────────────────────
|
|
|
|
|
|
def wait_until_settled(
|
|
psu: OwonPSU,
|
|
target_v: float,
|
|
*,
|
|
tol: float = DEFAULT_VOLTAGE_TOL_V,
|
|
interval: float = DEFAULT_POLL_INTERVAL_S,
|
|
timeout: float = DEFAULT_SETTLE_TIMEOUT_S,
|
|
) -> tuple[Optional[float], list[tuple[float, Optional[float]]]]:
|
|
"""Poll ``psu.measure_voltage_v()`` until within ``tol`` of ``target_v``.
|
|
|
|
The caller is responsible for issuing the setpoint **just before**
|
|
calling this — the timer starts on the function's first instruction
|
|
so the recorded duration includes the bus latency of the setpoint
|
|
being applied.
|
|
|
|
Returns ``(elapsed_seconds, trace)`` when settled, or
|
|
``(None, trace)`` if ``timeout`` expired. ``trace`` is the full list
|
|
of ``(elapsed_seconds, measured_voltage)`` tuples; the
|
|
``measured_voltage`` may be ``None`` for samples that failed to
|
|
parse (rare; surfaces firmware response anomalies).
|
|
"""
|
|
trace: list[tuple[float, Optional[float]]] = []
|
|
start = time.monotonic()
|
|
deadline = start + timeout
|
|
while time.monotonic() < deadline:
|
|
v = psu.measure_voltage_v()
|
|
elapsed = time.monotonic() - start
|
|
trace.append((round(elapsed, 4), v))
|
|
if v is not None and abs(v - target_v) <= tol:
|
|
return elapsed, trace
|
|
time.sleep(interval)
|
|
return None, trace
|
|
|
|
|
|
def downsample_trace(
|
|
trace: list[tuple[float, Optional[float]]],
|
|
max_samples: int = 30,
|
|
) -> list[tuple[float, Optional[float]]]:
|
|
"""Reduce a trace to at most ``max_samples`` evenly-spaced entries.
|
|
|
|
Keeps the first and last samples so the start/end of the curve are
|
|
always visible, then strides through the middle. Useful for
|
|
attaching a poll trace to a JUnit/HTML report without bloating it.
|
|
"""
|
|
n = len(trace)
|
|
if n <= max_samples:
|
|
return list(trace)
|
|
step = max(1, n // max_samples)
|
|
sampled = trace[::step]
|
|
if sampled[-1] != trace[-1]:
|
|
sampled.append(trace[-1])
|
|
return sampled
|
|
|
|
|
|
# ── composite: apply, wait for rail, hold for ECU ───────────────────────
|
|
|
|
|
|
def apply_voltage_and_settle(
|
|
psu: OwonPSU,
|
|
target_v: float,
|
|
*,
|
|
validation_time: float = DEFAULT_VALIDATION_TIME_S,
|
|
tol: float = DEFAULT_VOLTAGE_TOL_V,
|
|
interval: float = DEFAULT_POLL_INTERVAL_S,
|
|
settle_timeout: float = DEFAULT_SETTLE_TIMEOUT_S,
|
|
) -> dict:
|
|
"""Set ``target_v``, wait for the rail to actually be there, then hold.
|
|
|
|
Steps:
|
|
1. ``psu.set_voltage(1, target_v)`` — issue the setpoint.
|
|
2. :func:`wait_until_settled` — poll the PSU meter until measured
|
|
voltage is within ``tol`` of ``target_v`` (or raise on timeout).
|
|
3. ``time.sleep(validation_time)`` — give the firmware-side
|
|
observer (e.g. ECU voltage monitor) time to detect the new
|
|
voltage and update its status frame.
|
|
|
|
By the time this function returns the rail is at ``target_v`` and
|
|
the ECU has had ``validation_time`` to react. A single status read
|
|
afterwards is unambiguous — no polling-on-the-bus race.
|
|
|
|
Returns a dict with diagnostic data:
|
|
|
|
{
|
|
"settled_s": float, # PSU slewing time to within tol
|
|
"validation_s": float, # validation_time as passed
|
|
"final_v": float, # last measured voltage
|
|
"trace": list, # full (elapsed_s, v) trace
|
|
}
|
|
|
|
Raises:
|
|
AssertionError: PSU did not reach ``target_v`` within
|
|
``settle_timeout`` seconds (last measured voltage and
|
|
tolerance band included in the message).
|
|
"""
|
|
psu.set_voltage(1, target_v)
|
|
elapsed, trace = wait_until_settled(
|
|
psu, target_v,
|
|
tol=tol, interval=interval, timeout=settle_timeout,
|
|
)
|
|
|
|
final_v = trace[-1][1] if trace else None
|
|
if elapsed is None:
|
|
raise AssertionError(
|
|
f"PSU did not settle to {target_v} V within {settle_timeout} s "
|
|
f"(last measured: {final_v} V, ±{tol} V tolerance). "
|
|
f"Either the PSU can't slew this far, the load is misbehaving, "
|
|
f"or the timeout is too tight for this transition."
|
|
)
|
|
|
|
# Hold the rail steady so the ECU can detect and republish status.
|
|
if validation_time > 0:
|
|
time.sleep(validation_time)
|
|
|
|
return {
|
|
"settled_s": elapsed,
|
|
"validation_s": validation_time,
|
|
"final_v": final_v,
|
|
"trace": trace,
|
|
}
|