A reader asked where FrameIO gets its list of known frame names from —
because looking at `fio.send("ALM_Req_A", ...)` it seems like the class
must hold a registry somewhere. It doesn't: FrameIO is a broker that
forwards an incoming string to the LDF object it was constructed with,
and the string lives either in the test source (Path A) or in the
generated wrapper class (Path B).
Adds section 2 "How frame names reach FrameIO" to
docs/19_frame_io_and_alm_helpers.md, between the "Three layers of
access" overview (section 1) and the API reference (formerly section 2,
now section 3). The new section contains:
- A table of where the names actually live: LDF file on disk,
LdfDatabase after parsing, caller source code. FrameIO is explicitly
NOT in that table.
- The FrameIO class skeleton showing the empty _frames cache.
- A concrete ASCII call trace of `fio.send("ALM_Req_A", ...)` from
test source -> FrameIO -> LdfDatabase -> ldfparser -> byte layout.
- Path A (stringly-typed) vs Path B (typed wrapper from gen_lin_api),
with the trade-off (typo caught at runtime vs at import time).
- The cache lifecycle (starts empty, fills lazily, one entry per
unique frame name passed in).
- A "mental model" summary calling FrameIO a generic glue layer.
Sections 3-9 renumbered to make room (3->4, 4->5, ..., 8->9). The 7.x
sub-sections under "Writing a new test" become 8.x. Updates the
stale anchor link in 14_power_supply.md
(#72-the-four-phase-test-pattern -> #82-the-four-phase-test-pattern).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit fixed the FrameIO/LDF diagram by labeling the
ldf-lookup edge as "duck-typed" without defining the term. This commit
adds a dedicated section explaining what duck typing means in this
codebase, why both architectural seams (FrameIO's ldf injection and the
lin fixture's adapter swap) rely on it, and the Python idioms behind it.
Content covers:
- The "walks like a duck" slogan and what it means in code: shape of
used methods is the contract, not the class.
- Example 1 — FrameIO and the untyped `ldf` parameter: shows the
contract (single .frame() call) and the absence of any
`from ecu_framework.lin.ldf import LdfDatabase`. Includes the
counter-example of what nominal typing would have meant for
module dependencies and testability.
- Example 2 — the lin fixture and adapter polymorphism: same idiom,
with LinInterface providing the nominal anchor.
- EAFP ("Easier to Ask Forgiveness than Permission") as the supporting
Python idiom, contrasted with LBYL.
- The trade-off section: implicit contracts and runtime-only errors,
and how the codebase mitigates them.
Cross-linked from 24_test_wiring.md's `lin` polymorphism-boundary
discussion so readers of either doc can navigate to the explanation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous ASCII pipeline implied a single linear stack from gen_lin_api
down through FrameIO down through ecu_framework/lin/ldf.py — and showed
a static dependency from FrameIO to that module. Both are wrong.
What the code actually says (tests/hardware/frame_io.py:34):
from ecu_framework.lin.base import LinFrame, LinInterface
That's the only ecu_framework import in FrameIO. The `ldf` constructor
parameter is duck-typed — FrameIO never imports LdfDatabase and would
work against any object exposing `.frame(name)`. So `frame_io → lin/ldf`
is an injected runtime call, not a module dependency.
Replace the linear ASCII diagram with a Mermaid parallel-paths diagram
that surfaces the three independent ways a tester can address a frame:
- gen_lin_api typed wrapper (compile-time name check)
- FrameIO stringly-typed I/O (with raw send_raw/receive_raw escape
hatches that don't touch the ldf object at all)
- LdfDatabase used directly (schema-only — pack to bytes, no I/O)
…all converging at LinInterface. The prose around the diagram is
rewritten to match: each path's affordance, and what concrete capability
is lost by removing any of the three.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a typed layer between the LDF and hardware tests so frame /
signal / enum-value typos become import errors instead of runtime
KeyErrors. This complements the runtime ``LdfDatabase`` in
``ecu_framework/lin/ldf.py`` rather than replacing it.
- scripts/gen_lin_api.py: LDF → Python generator. Reads an LDF via
ldfparser and emits one ``IntEnum`` per logical-valued
Signal_encoding_types block, one class per pure-physical encoding
type, and one class per frame with NAME / FRAME_ID / LENGTH /
PUBLISHER / SIGNALS / SIGNAL_LAYOUT plus ``send`` / ``receive`` /
``read_signal`` classmethods that delegate to a caller-supplied
``FrameIO``. Output starts with a "DO NOT EDIT — re-run" header and
the source-LDF SHA-256 prefix for traceability.
- tests/hardware/_generated/__init__.py + lin_api.py: the generated
output for vendor/4SEVEN_color_lib_test.ldf. Already consumed by
tests/hardware/mum/test_mum_alm_animation_generated.py to demonstrate
the "no AlmTester anywhere" pattern.
- docs/22_generated_lin_api.md: design doc covering the generation
rules, the build-time-vs-runtime layering with LdfDatabase, the
rationale for keeping AlmTester-style helpers above this layer, and
worked before/after examples.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces BuildKit's `--mount=type=secret` with `--mount=type=bind,from=…`
backed by a named build context. Secrets are capped at 500 KiB and are
meant for keys, not blobs — the Melexis tarball routinely exceeds that.
A named context overriding a `FROM scratch AS melexis-bundle` stub stage
gives "optional, file-of-any-size, never-in-image" semantics without
polluting the default build context.
- docker/Dockerfile: add the scratch stub stage, change the install step
to `--mount=type=bind,from=melexis-bundle,target=/melexis-bundle`,
update the usage header to show the new `--build-context` invocation,
fail loudly with a clear message when INCLUDE_MELEXIS=1 but no bundle
is bound.
- docker/README.md: document the new build flow, the rationale for the
bind-mount vs secret tradeoff, and bench instructions.
- .dockerignore: ignore the new `melexis-bundle/` directory at the repo
root (named build contexts respect a .dockerignore at THEIR own root,
not the default one — so this entry only prevents accidental inclusion
via the default context).
- requirements.txt: pin the Melexis stack's transitive PyPI deps
(pyparsing, natsort, intelhex, pygdbmi, crcmod, packaging, zeroconf)
unconditionally so mock and hw images share a single venv layout. The
size delta in the mock image is a few MB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- Replace ecu_framework/config.py with ecu_framework/config/ package
(loader.py + __init__.py re-exports). Public surface unchanged — every
call site already uses 'from ecu_framework.config import ...' which
works identically for a module and a package. Brings config into the
same shape as lin/, power/, flashing/.
- Enrich loader.py with module-level design notes (pipeline diagram,
precedence rationale, "known wart" callout) and inline "why" comments:
the EcuTestConfig forward-reference quirk, the int(k, 0) hex-key trick,
_deep_update's mutate-in-place semantics, and the reason the in-memory
overrides are applied last despite being precedence #1.
- Add docs/23_config_loader_internals.md covering the merge semantics,
type-coercion philosophy, dataclass ordering quirks, PSU side-channel,
and the test-surface checklist (four places to touch when adding a
new config field).
- Fix the now-stale ecu_framework/config.py path in 01_run_sequence.md
and DEVELOPER_COMMIT_GUIDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add pyproject.toml (hatchling backend, version 0.1.0, name ecu-framework).
Runtime deps split out from requirements.txt; test extras and the
Melexis-transitive bundle are opt-in.
- Add CHANGELOG.md (Keep-A-Changelog format), seeding [Unreleased] with the
installable shift and a [0.1.0] entry for the existing baseline.
- ecu_framework/__init__.py: resolve __version__ from importlib.metadata
with a "0.0.0+local" fallback for source checkouts. Add power and
flashing to __all__ and the docstring (previously stale).
- Drop per-subpackage __version__ from lin/ and power/. A single
pyproject.toml version is the source of truth; subpackage-level
__version__ strings drift and nothing consumed them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a deeper "Contract (base)" section to 04_lin_interface_call_flow.md:
LinFrame field validation, LinInterface abstract vs default methods, the
list of concrete adapters / consumers, and a "How __post_init__ runs"
subsection explaining the dataclass-generated __init__ hook chain and the
inheritance caveat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous version described the pre-refactor flow only — no
hardware-suite conftest, no helper layer, no PSU resolver, no
settle-then-validate pattern, no junit_family note. Rewritten so it
reflects the current architecture without losing the original
sequence-diagram + text-flow shape.
What's new in the doc:
- Two-layer fixture model (project-wide vs hardware-suite) called
out at the top.
- Mermaid sequence diagram now shows the session-scoped autouse PSU
power-up, the helper layer (FrameIO / AlmTester / psu_helpers),
and the safe-off-on-close at session teardown.
- Text-flow split into PROJECT-WIDE / HARDWARE-SUITE / TEST-BODIES
sections; describes resolve_port's fallback chain and the
settle-then-validate behaviour of apply_voltage_and_settle.
- "Where information is fetched from" gains the LDF, rgb_to_pwm,
and per-machine PSU override paths.
- "Key components" split into project-wide / hardware-suite, listing
every helper and template file.
- Edge cases gain PSU-side entries: cross-platform port resolution,
the must-not list (no set_output(False), no close()),
apply_voltage_and_settle's timeout behaviour, and the
junit_family=legacy requirement for record_property round-trips.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the new layers introduced over the past several commits.
- docs/19_frame_io_and_alm_helpers.md (new): full reference for the
FrameIO and AlmTester helpers — three access levels (high/mid/low),
full API tables, fixture wiring, cookbook patterns, and §7
describing the four-phase SETUP/PROCEDURE/ASSERT/TEARDOWN test
pattern with the three template flavors plus a §7.4 link to the
PSU+LIN template.
- docs/14_power_supply.md: rewritten and expanded.
§3 cross-platform port resolution (Windows / WSL1 / WSL2 +
usbipd-win / Linux native compatibility table)
§4 auto-detection via idn_substr
§5 session-managed power: contract for tests, must-not list,
what changed in the existing tests
§6 the settle-then-validate pattern: two-delays table (PSU
bench-dependent vs ECU firmware-dependent), copy-paste
example, tuning guidance for ECU_VALIDATION_TIME_S
§6 PSU settling characterization (-m psu_settling)
§7 library API reference table + safe_off_on_close
§9 troubleshooting expanded with WSL2 usbipd-win + dialout
- docs/18_test_catalog.md: voltage-tolerance section refreshed for
the settle-then-validate shape, new "Hardware – PSU settling
(opt-in)" category, new §8 "Hardware-test infrastructure"
documenting conftest.py, frame_io.py, alm_helpers.py,
psu_helpers.py, and both templates.
- docs/05_architecture_overview.md: components list split into
framework core / hardware test layer / artifacts. Mermaid diagram
gained a Hardware-test helpers subgraph showing FrameIO,
AlmTester, rgb_to_pwm, and the templates. Data/control flow
summary describes the session-managed PSU and the helper layer.
- docs/15_report_properties_cheatsheet.md: PSU section split into
per-test (function-scoped rp) and module-scoped (testsuite
property) blocks; added psu_resolved_port, psu_resolved_idn,
psu_settled_s, validation_time_s.
- docs/README.md: links to the new doc 19.
- README.md, TESTING_FRAMEWORK_GUIDE.md: project-structure trees
expanded to show the full current layout — every file and
directory under tests/hardware/ (conftest, helpers, templates,
tests), tests/unit/, config/, docs/, scripts/, and vendor/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two starting-point files for new hardware tests. Leading underscore
in the filenames keeps pytest from collecting them.
- _test_case_template.py — for ALM_Node-touching MUM tests.
Three flavors with full SETUP / PROCEDURE / ASSERT / TEARDOWN
section markers:
A) minimal: relies on the autouse _reset_to_off (LED OFF
baseline) — no per-test setup/teardown
B) with isolation: try/finally pattern for tests that mutate
persistent ECU state (e.g. ConfigFrame)
C) single-signal probe: fio.read_signal one-shot
Inline comments explain pytest fundamentals (fixture, scope,
autouse, yield, rp), the four-phase pattern, and the
must/must-not contract.
- _test_case_template_psu_lin.py — for tests that drive the PSU
AND observe the LIN bus (over/undervoltage tolerance, brown-out,
supply transients). Three flavors:
A) overvoltage: apply OV via apply_voltage_and_settle, single
status read after validation hold, assert OverVoltage
B) undervoltage: symmetric for UV
C) parametrized voltage sweep
Documents the three-layer safety guarantee (session
safe_off_on_close / autouse _park_at_nominal / per-test
try/finally) and the rule that tests never call set_output(False)
or close() — the session fixture owns the PSU lifecycle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
On benches where the Owon PSU powers the ECU, every per-file PSU
fixture that closed the port (sending 'output 0' on close) browned
out the bench between modules — every MUM test that ran after a
closed PSU connection failed with "ECU not responding".
New tests/hardware/conftest.py provides three session-scoped
fixtures:
- _psu_or_none: tolerant. Opens the Owon PSU once via resolve_port,
parks at config.power_supply.set_voltage / set_current, enables
output. Yields the live OwonPSU or None. Closes (with
safe_off_on_close=True) at session end — the bench ends safely
de-energized.
- _psu_powers_bench: autouse=True. Realizes _psu_or_none so even
tests that don't request `psu` by name benefit from the
session-level power-up. No-op if PSU isn't configured.
- psu: public. Skips cleanly when the PSU isn't reachable.
Contract for tests:
- request `psu` if you need to read measurements or change voltage
- restore nominal voltage in your finally block
- MUST NOT call psu.set_output(False) (would brown out the bench)
- MUST NOT call psu.close() (the session fixture owns it)
test_owon_psu.py becomes read-only:
- removed the local module-scoped psu fixture
- removed the set_output toggle (would have killed the session)
- now validates IDN, output_is_on(), and parsed measurements
against the always-on PSU. Renamed to
test_owon_psu_idn_and_measurements to reflect the new shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
owon_psu.py upgrades (all backward-compatible):
- SerialParams.from_config() and OwonPSU.from_config() factories that
translate the YAML power_supply block (parity 'N', stopbits 1.0)
into pyserial constants — eliminates the boilerplate every test
was duplicating.
- Parsed-numeric measurement helpers: measure_voltage_v(),
measure_current_a(), output_is_on(). Tests can now assert on
floats / bools instead of regex-ing strings.
- safe_off_on_close=True (new ctor kwarg, default on) — close()
sends 'output 0' before closing the port. Last-ditch protection
against leaving the bench powered on after an aborted test.
Keyword-only so the historical positional ctor signature is
preserved.
- Cross-platform port resolver: windows_com_to_linux,
linux_serial_to_windows, candidate_ports, resolve_port. The
resolver tries the configured port verbatim, then its
cross-platform translation (COM7 ↔ /dev/ttyS6 on WSL1), then
Linux USB-serial paths (/dev/ttyUSB*, /dev/ttyACM*), then a full
scan_ports() with optional idn_substr filter. One bench config
works on Windows, WSL1, WSL2 + usbipd-win, and native Linux.
- try_idn_on_port refactored to use OwonPSU internally, removing
~25 lines of duplicated serial-port plumbing.
ecu_framework/power/__init__.py re-exports the new helpers so tests
can do `from ecu_framework.power import resolve_port, ...`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-Python port of the Input Sheet RGB→PWM pipeline (color management
+ luminance management + temperature compensation) used by the ALM
firmware. Exposes compute_pwm(r, g, b, temp_c) returning both the
non-compensated and the temperature-compensated 16-bit PWM tuples.
Imported by tests/hardware/alm_helpers.py to predict expected PWM
values from RGB inputs in PWM-validation assertions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MUM (Melexis Universal Master) adapter is the current default; the
BabyLIN SDK adapter is retained only for backward compatibility with
existing rigs.
Code:
- Emit DeprecationWarning when BabyLinInterface is instantiated and
when tests/conftest.py routes interface.type=='babylin' to it.
- Update module/class docstrings in ecu_framework/{__init__,config,
lin/__init__,lin/babylin}.py to label BabyLIN-specific fields and
paths as deprecated.
Config / scripts / pytest:
- pytest.ini: relabel the babylin marker as deprecated.
- config/{babylin.example,examples,test_config}.yaml: add deprecation
banners and field comments.
- scripts/99-babylin.rules and scripts/pi_install.sh: annotate the
udev-rule install block as legacy-only.
Documentation:
- TESTING_FRAMEWORK_GUIDE.md, docs/08_babylin_internals.md, and
vendor/README.md: prepend explicit "DEPRECATED" banners.
- docs/{README,01,02,04,05,07,09,10,12,13,14,15,18,DEVELOPER_COMMIT_
GUIDE}.md: relabel "legacy" to "deprecated" where babylin is
mentioned, present MUM as the primary path, and steer new work
toward the MUM examples.
No tests, configs, or modules were deleted; existing BabyLIN setups
keep working but now produce a clear DeprecationWarning at runtime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>