docs: hardware test infrastructure, session-managed PSU, settle-then-validate
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>
This commit is contained in:
parent
11b5402b14
commit
afd9da8206
265
README.md
265
README.md
@ -1,12 +1,14 @@
|
|||||||
# ECU Tests Framework
|
# ECU Tests Framework
|
||||||
|
|
||||||
Python-based ECU testing framework built on pytest, with a pluggable LIN communication layer (Mock, MUM, and legacy BabyLIN), configuration via YAML, and enhanced HTML/XML reporting with rich test metadata.
|
Python-based ECU testing framework built on pytest, with a pluggable LIN communication layer (Mock and MUM, with the deprecated BabyLIN adapter retained for backward compatibility), configuration via YAML, and enhanced HTML/XML reporting with rich test metadata.
|
||||||
|
|
||||||
|
> **Heads-up:** the BabyLIN adapter is **deprecated**. New tests and deployments should target MUM. BabyLIN is documented below only so existing setups can keep running while they migrate.
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **MUM (Melexis Universal Master) adapter** — current default for hardware tests; networked LIN master with built-in power control
|
- **MUM (Melexis Universal Master) adapter** — current default for hardware tests; networked LIN master with built-in power control
|
||||||
- Mock LIN adapter for fast, hardware-free development
|
- Mock LIN adapter for fast, hardware-free development
|
||||||
- BabyLIN adapter (legacy) using the vendor SDK's Python wrapper
|
- BabyLIN adapter (DEPRECATED) using the vendor SDK's Python wrapper
|
||||||
- Hex flashing scaffold you can wire to UDS
|
- Hex flashing scaffold you can wire to UDS
|
||||||
- Rich pytest fixtures and example tests
|
- Rich pytest fixtures and example tests
|
||||||
- Self-contained HTML report with Title, Requirements, Steps, and Expected Results extracted from test docstrings
|
- Self-contained HTML report with Title, Requirements, Steps, and Expected Results extracted from test docstrings
|
||||||
@ -36,7 +38,7 @@ Hardware via MUM (current default):
|
|||||||
$env:ECU_TESTS_CONFIG = ".\config\mum.example.yaml"; pytest -m "hardware and mum" -v
|
$env:ECU_TESTS_CONFIG = ".\config\mum.example.yaml"; pytest -m "hardware and mum" -v
|
||||||
```
|
```
|
||||||
|
|
||||||
Hardware via BabyLIN (legacy):
|
Hardware via BabyLIN (DEPRECATED — kept for existing rigs only):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Place BabyLIN_library.py and native libs under .\vendor per vendor/README.md first
|
# Place BabyLIN_library.py and native libs under .\vendor per vendor/README.md first
|
||||||
@ -66,6 +68,96 @@ python.exe -m pytest -m "not hardware" -v
|
|||||||
|
|
||||||
Tip: You can change output via `--html` and `--junitxml` CLI options.
|
Tip: You can change output via `--html` and `--junitxml` CLI options.
|
||||||
|
|
||||||
|
## Quick start (WSL on Windows)
|
||||||
|
|
||||||
|
Use this approach when running from **Windows Subsystem for Linux** instead of PowerShell.
|
||||||
|
|
||||||
|
### 1. Open a WSL terminal and navigate to the project
|
||||||
|
|
||||||
|
Clone or access the repo from within WSL. If the project lives on the Windows filesystem (e.g. `C:\Users\you\ecu-tests`), it is available at:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/c/Users/<your-username>/ecu-tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create a virtual environment and install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run the mock test suite (no hardware needed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest -m "not hardware" -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Install Melexis packages into the venv (required for hardware tests)
|
||||||
|
|
||||||
|
`pylin`, `pymumclient`, and `pylinframe` are not on PyPI — they ship with the Melexis IDE.
|
||||||
|
On Windows they live at:
|
||||||
|
|
||||||
|
```
|
||||||
|
C:\Program Files\Melexis\Melexis IDE\plugins\com.melexis.mlxide.python_1.2.0.202408130945\python\Lib\site-packages
|
||||||
|
```
|
||||||
|
|
||||||
|
which WSL exposes at `/mnt/c/Program Files/Melexis/Melexis IDE/...`.
|
||||||
|
|
||||||
|
**With your venv already activated**, copy the packages directly into it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate # skip if already active
|
||||||
|
|
||||||
|
MELEXIS_SITE="/mnt/c/Program Files/Melexis/Melexis IDE/plugins/com.melexis.mlxide.python_1.2.0.202408130945/python/Lib/site-packages"
|
||||||
|
VENV_SITE=$(python -c "import site; print(site.getsitepackages()[0])")
|
||||||
|
|
||||||
|
cp -r "$MELEXIS_SITE/pylin" "$VENV_SITE/"
|
||||||
|
cp -r "$MELEXIS_SITE/pymumclient" "$VENV_SITE/"
|
||||||
|
cp -r "$MELEXIS_SITE/pylinframe" "$VENV_SITE/"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "import pylin; import pymumclient; print('OK')"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Alternative:** You can also run `bash vendor/automated_lin_test/install_packages.sh` after updating the `MELEXIS_SITE_PACKAGES` path in that script — but the commands above are simpler and target the venv directly.
|
||||||
|
|
||||||
|
### 5. Run hardware tests via MUM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ECU_TESTS_CONFIG=./config/mum.example.yaml
|
||||||
|
python -m pytest -m "hardware and mum" -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Run hardware tests via BabyLIN (DEPRECATED)
|
||||||
|
|
||||||
|
> **Deprecated.** The BabyLIN adapter is kept for backward compatibility only; new work should target MUM (step 5). The BabyLIN SDK also ships Windows-only native libraries (`.dll`), so these tests cannot run under WSL unless you have a Linux-compatible `.so` build of the SDK.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ECU_TESTS_CONFIG=./config/babylin.example.yaml
|
||||||
|
python -m pytest -m "hardware and babylin" -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. View reports
|
||||||
|
|
||||||
|
Open the HTML report directly in Windows from the WSL terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
explorer.exe reports/report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from PowerShell/CMD:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
start .\reports\report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Reporting: Metadata in HTML
|
## Reporting: Metadata in HTML
|
||||||
|
|
||||||
We extract these fields from each test’s docstring and render them in the HTML report:
|
We extract these fields from each test’s docstring and render them in the HTML report:
|
||||||
@ -128,13 +220,15 @@ The MUM has its own power output, so `power_supply.enabled: false` is the
|
|||||||
typical setting when using MUM. The Owon PSU support remains for over/under-
|
typical setting when using MUM. The Owon PSU support remains for over/under-
|
||||||
voltage scenarios but is independent of the LIN interface.
|
voltage scenarios but is independent of the LIN interface.
|
||||||
|
|
||||||
### BabyLIN configuration (legacy)
|
### BabyLIN configuration (DEPRECATED)
|
||||||
|
|
||||||
|
> Retained for backward compatibility. Prefer the MUM configuration above.
|
||||||
|
|
||||||
Template: `config/babylin.example.yaml`
|
Template: `config/babylin.example.yaml`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
interface:
|
interface:
|
||||||
type: babylin # or "mock", or "mum"
|
type: babylin # deprecated; prefer "mum" or "mock"
|
||||||
channel: 0 # Channel index used by the SDK wrapper
|
channel: 0 # Channel index used by the SDK wrapper
|
||||||
bitrate: 19200 # Usually determined by SDF
|
bitrate: 19200 # Usually determined by SDF
|
||||||
sdf_path: ./vendor/Example.sdf
|
sdf_path: ./vendor/Example.sdf
|
||||||
@ -147,12 +241,12 @@ interface:
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `mock` | n/a | n/a | yes (queue-based) |
|
| `mock` | n/a | n/a | yes (queue-based) |
|
||||||
| `mum` | yes (`power_out0`) | yes (`MumLinInterface.send_raw()` → `ld_put_raw`) | no — `receive(id)` triggers a slave read |
|
| `mum` | yes (`power_out0`) | yes (`MumLinInterface.send_raw()` → `ld_put_raw`) | no — `receive(id)` triggers a slave read |
|
||||||
| `babylin` | external (Owon PSU) | via SDF / `BLC_sendCommand` | yes (frame queue) |
|
| `babylin` (deprecated) | external (Owon PSU) | via SDF / `BLC_sendCommand` | yes (frame queue) |
|
||||||
|
|
||||||
Switch to hardware profile and run only hardware tests:
|
Switch to hardware profile and run only hardware tests (MUM example):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:ECU_TESTS_CONFIG = (Resolve-Path .\config\babylin.example.yaml)
|
$env:ECU_TESTS_CONFIG = (Resolve-Path .\config\mum.example.yaml)
|
||||||
python.exe -m pytest -m hardware -v
|
python.exe -m pytest -m hardware -v
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -160,50 +254,127 @@ python.exe -m pytest -m hardware -v
|
|||||||
|
|
||||||
```
|
```
|
||||||
ecu_tests/
|
ecu_tests/
|
||||||
├── ecu_framework/
|
├── ecu_framework/ # Core framework package
|
||||||
│ ├── config.py # YAML config loader
|
│ ├── config.py # YAML config loader → typed dataclasses
|
||||||
│ ├── power/
|
|
||||||
│ │ └── owon_psu.py # Owon PSU serial SCPI controller (library)
|
|
||||||
│ ├── lin/
|
│ ├── lin/
|
||||||
│ │ ├── base.py # LinInterface + LinFrame
|
│ │ ├── base.py # LinInterface + LinFrame contract
|
||||||
│ │ ├── mock.py # Mock LIN adapter
|
│ │ ├── mock.py # Mock LIN adapter (no hardware)
|
||||||
│ │ └── babylin.py # BabyLIN SDK-wrapper adapter (uses BabyLIN_library.py)
|
│ │ ├── mum.py # MUM adapter (current default; Melexis pylin/pymumclient)
|
||||||
|
│ │ ├── ldf.py # LdfDatabase wrapper around ldfparser
|
||||||
|
│ │ └── babylin.py # DEPRECATED BabyLIN SDK-wrapper adapter
|
||||||
|
│ ├── power/
|
||||||
|
│ │ └── owon_psu.py # Owon PSU SCPI controller + cross-platform port resolver
|
||||||
│ └── flashing/
|
│ └── flashing/
|
||||||
│ └── hex_flasher.py # Hex flashing scaffold
|
│ └── hex_flasher.py # Hex flashing scaffold
|
||||||
|
│
|
||||||
├── tests/
|
├── tests/
|
||||||
│ ├── conftest.py # Shared fixtures
|
│ ├── conftest.py # Project-wide fixtures: config, lin, ldf, flash_ecu, rp
|
||||||
│ ├── test_smoke_mock.py # Mock interface smoke and boundary tests
|
│ │
|
||||||
│ ├── test_babylin_hardware_smoke.py # Hardware smoke tests
|
│ ├── unit/ # Pure-logic tests (no hardware)
|
||||||
│ ├── test_babylin_hardware_schedule_smoke.py # Hardware schedule flow
|
│ │ ├── test_config_loader.py
|
||||||
│ ├── test_babylin_wrapper_mock.py # SDK adapter with mock wrapper
|
│ │ ├── test_linframe.py
|
||||||
|
│ │ ├── test_ldf_database.py
|
||||||
|
│ │ ├── test_hex_flasher.py
|
||||||
|
│ │ ├── test_mum_adapter_mocked.py
|
||||||
|
│ │ └── test_babylin_adapter_mocked.py # deprecated path
|
||||||
|
│ │
|
||||||
│ ├── plugin/
|
│ ├── plugin/
|
||||||
│ │ └── test_conftest_plugin_artifacts.py # Plugin self-test (reports artifacts)
|
│ │ └── test_conftest_plugin_artifacts.py # reporting plugin self-test
|
||||||
│ ├── unit/
|
│ │
|
||||||
│ │ ├── test_config_loader.py # Config loader unit tests
|
│ ├── hardware/ # Real-bench tests (MUM / PSU / ECU)
|
||||||
│ │ ├── test_linframe.py # LIN frame dataclass/logic
|
│ │ ├── conftest.py # Session-scoped autouse PSU fixture (powers the ECU)
|
||||||
│ │ ├── test_hex_flasher.py # Hex flasher scaffolding
|
│ │ ├── frame_io.py # FrameIO — generic LDF-driven send/receive/pack/unpack
|
||||||
│ │ └── test_babylin_adapter_mocked.py # BabyLIN adapter with mocks
|
│ │ ├── alm_helpers.py # AlmTester — ALM_Node domain helpers + constants
|
||||||
│ └── hardware/
|
│ │ ├── psu_helpers.py # apply_voltage_and_settle — measure-rail-then-validate
|
||||||
│ └── test_owon_psu.py # Owon PSU hardware test (uses central config)
|
│ │ ├── _test_case_template.py # ALM-only test starting point (not collected)
|
||||||
|
│ │ ├── _test_case_template_psu_lin.py # PSU + LIN test starting point (not collected)
|
||||||
|
│ │ ├── test_mum_alm_animation.py # ALM mode/update/LID checks via MUM
|
||||||
|
│ │ ├── test_mum_auto_addressing.py # BSM auto-addressing (NAD)
|
||||||
|
│ │ ├── test_e2e_mum_led_activate.py # MUM end-to-end power+activate
|
||||||
|
│ │ ├── test_overvolt.py # Voltage-tolerance (over/under/sweep)
|
||||||
|
│ │ ├── test_psu_voltage_settling.py # PSU settling-time characterization (-m psu_settling)
|
||||||
|
│ │ ├── test_owon_psu.py # PSU IDN + measurements (read-only)
|
||||||
|
│ │ └── test_e2e_power_on_lin_smoke.py # DEPRECATED BabyLIN E2E
|
||||||
|
│ │
|
||||||
|
│ ├── test_smoke_mock.py # Mock interface smoke + boundary
|
||||||
|
│ ├── test_babylin_hardware_smoke.py # DEPRECATED BabyLIN hardware
|
||||||
|
│ ├── test_babylin_hardware_schedule_smoke.py # DEPRECATED BabyLIN schedule flow
|
||||||
|
│ ├── test_babylin_wrapper_mock.py # DEPRECATED BabyLIN adapter w/ mock wrapper
|
||||||
|
│ └── test_hardware_placeholder.py
|
||||||
|
│
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── test_config.yaml # Default config
|
│ ├── test_config.yaml # Default config (MUM by default)
|
||||||
│ ├── babylin.example.yaml # BabyLIN hardware template
|
│ ├── mum.example.yaml # MUM hardware profile
|
||||||
│ ├── owon_psu.example.yaml # Owon PSU example (copy to owon_psu.yaml)
|
│ ├── owon_psu.example.yaml # PSU profile (copy to owon_psu.yaml)
|
||||||
│ └── owon_psu.yaml # Optional machine-specific PSU config
|
│ ├── owon_psu.yaml # Optional per-machine PSU override
|
||||||
├── vendor/ # Place SDK wrapper and platform libs here
|
│ ├── examples.yaml # Combined mock/babylin profiles
|
||||||
|
│ └── babylin.example.yaml # DEPRECATED BabyLIN profile
|
||||||
|
│
|
||||||
|
├── docs/
|
||||||
|
│ ├── README.md # Documentation index
|
||||||
|
│ ├── 01_run_sequence.md # End-to-end run sequence
|
||||||
|
│ ├── 02_configuration_resolution.md
|
||||||
|
│ ├── 03_reporting_and_metadata.md
|
||||||
|
│ ├── 04_lin_interface_call_flow.md
|
||||||
|
│ ├── 05_architecture_overview.md
|
||||||
|
│ ├── 06_requirement_traceability.md
|
||||||
|
│ ├── 07_flash_sequence.md
|
||||||
|
│ ├── 08_babylin_internals.md # DEPRECATED
|
||||||
|
│ ├── 09_raspberry_pi_deployment.md
|
||||||
|
│ ├── 10_build_custom_image.md
|
||||||
|
│ ├── 11_conftest_plugin_overview.md
|
||||||
|
│ ├── 12_using_the_framework.md
|
||||||
|
│ ├── 13_unit_testing_guide.md
|
||||||
|
│ ├── 14_power_supply.md # PSU controller, resolver, session-managed power
|
||||||
|
│ ├── 15_report_properties_cheatsheet.md
|
||||||
|
│ ├── 16_mum_internals.md
|
||||||
|
│ ├── 17_ldf_parser.md
|
||||||
|
│ ├── 18_test_catalog.md
|
||||||
|
│ ├── 19_frame_io_and_alm_helpers.md # Hardware test helpers + four-phase pattern
|
||||||
|
│ └── DEVELOPER_COMMIT_GUIDE.md
|
||||||
|
│
|
||||||
|
├── vendor/ # Third-party + project assets
|
||||||
|
│ ├── 4SEVEN_color_lib_test.ldf # LDF used by the LIN tests
|
||||||
|
│ ├── 4SEVEN_color_lib_test.sdf # SDF for the deprecated BabyLIN path
|
||||||
|
│ ├── rgb_to_pwm.py # RGB → PWM calculator (used by ALM PWM assertions)
|
||||||
|
│ ├── led_platform.py # Platform-specific LED helpers
|
||||||
│ ├── Owon/
|
│ ├── Owon/
|
||||||
│ │ └── owon_psu_quick_demo.py # Quick PSU demo using the library & YAML
|
│ │ └── owon_psu_quick_demo.py # Standalone PSU demo
|
||||||
│ ├── BabyLIN_library.py # Official SDK Python wrapper
|
│ ├── automated_lin_test/ # Reference scripts (test_animation.py etc.)
|
||||||
│ └── BabyLIN library/ # Platform-specific binaries from SDK (DLL/.so)
|
│ │ ├── README.md
|
||||||
├── reports/ # Generated reports
|
│ │ ├── install_packages.sh # Installs Melexis pylin/pymumclient into the venv
|
||||||
|
│ │ └── (test_*.py reference scripts)
|
||||||
|
│ ├── BabyLIN_library.py # DEPRECATED official BabyLIN SDK Python wrapper
|
||||||
|
│ ├── BLCInterfaceExample.py # DEPRECATED vendor example
|
||||||
|
│ └── BabyLIN library/ # DEPRECATED platform binaries (DLL/.so)
|
||||||
|
│
|
||||||
|
├── reports/ # Generated per-run (HTML, JUnit, summary, coverage)
|
||||||
│ ├── report.html
|
│ ├── report.html
|
||||||
│ └── junit.xml
|
│ ├── junit.xml
|
||||||
├── conftest_plugin.py # HTML metadata extraction & rendering
|
│ ├── summary.md
|
||||||
├── pytest.ini # Markers and default addopts
|
│ └── requirements_coverage.json
|
||||||
|
│
|
||||||
|
├── scripts/
|
||||||
|
│ ├── pi_install.sh # Raspberry Pi installer
|
||||||
|
│ ├── ecu-tests.service # systemd unit
|
||||||
|
│ ├── ecu-tests.timer # systemd timer
|
||||||
|
│ ├── run_tests.sh # Convenience runner
|
||||||
|
│ ├── run_two_reports.ps1 # Split unit/non-unit report runs (Windows)
|
||||||
|
│ └── 99-babylin.rules # DEPRECATED udev rule
|
||||||
|
│
|
||||||
|
├── conftest_plugin.py # HTML metadata extraction + report customization
|
||||||
|
├── pytest.ini # Markers, addopts, junit_family=legacy
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
└── README.md
|
├── README.md # ← you are here
|
||||||
|
└── TESTING_FRAMEWORK_GUIDE.md # Deep dive companion to this README
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For the hardware-test layer specifically, see
|
||||||
|
[`docs/19_frame_io_and_alm_helpers.md`](docs/19_frame_io_and_alm_helpers.md)
|
||||||
|
(FrameIO + AlmTester + the four-phase test pattern) and
|
||||||
|
[`docs/14_power_supply.md`](docs/14_power_supply.md) §5
|
||||||
|
(session-managed PSU lifecycle).
|
||||||
|
|
||||||
## Usage recipes
|
## Usage recipes
|
||||||
|
|
||||||
- Run everything (mock and any non-hardware tests):
|
- Run everything (mock and any non-hardware tests):
|
||||||
@ -237,9 +408,11 @@ python -m pytest tests\plugin\test_conftest_plugin_artifacts.py -q
|
|||||||
./scripts/run_two_reports.ps1
|
./scripts/run_two_reports.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
## BabyLIN adapter notes
|
## BabyLIN adapter notes (DEPRECATED)
|
||||||
|
|
||||||
The `ecu_framework/lin/babylin.py` implementation uses the official `BabyLIN_library.py` wrapper from the SDK. Put `BabyLIN_library.py` under `vendor/` (or on `PYTHONPATH`) along with the SDK's platform-specific libraries. Configure `sdf_path` and `schedule_nr` to load an SDF and start a schedule during connect. The adapter sends frames via `BLC_mon_set_xmit` and receives via `BLC_getNextFrameTimeout`.
|
> Kept for backward compatibility. New work should target the MUM adapter.
|
||||||
|
|
||||||
|
The `ecu_framework/lin/babylin.py` implementation uses the official `BabyLIN_library.py` wrapper from the SDK. Put `BabyLIN_library.py` under `vendor/` (or on `PYTHONPATH`) along with the SDK's platform-specific libraries. Configure `sdf_path` and `schedule_nr` to load an SDF and start a schedule during connect. The adapter sends frames via `BLC_mon_set_xmit` and receives via `BLC_getNextFrameTimeout`. Instantiating `BabyLinInterface` emits a `DeprecationWarning`.
|
||||||
|
|
||||||
## Docs and references
|
## Docs and references
|
||||||
|
|
||||||
@ -248,8 +421,8 @@ The `ecu_framework/lin/babylin.py` implementation uses the official `BabyLIN_lib
|
|||||||
- CI summary: `reports/summary.md` (machine-friendly run summary)
|
- CI summary: `reports/summary.md` (machine-friendly run summary)
|
||||||
- Requirements coverage: `reports/requirements_coverage.json` (requirement → tests mapping)
|
- Requirements coverage: `reports/requirements_coverage.json` (requirement → tests mapping)
|
||||||
- Tip: Open the HTML report on Windows with: `start .\reports\report.html`
|
- Tip: Open the HTML report on Windows with: `start .\reports\report.html`
|
||||||
- Configs: `config/test_config.yaml`, `config/babylin.example.yaml` (copy and modify for your environment)
|
- Configs: `config/test_config.yaml`, `config/mum.example.yaml`, `config/babylin.example.yaml` (deprecated) — copy and modify for your environment
|
||||||
- BabyLIN SDK placement and notes: `vendor/README.md`
|
- BabyLIN SDK placement and notes: `vendor/README.md` (deprecated; only relevant for legacy BabyLIN rigs)
|
||||||
- Docs index: `docs/README.md` (run sequence, config resolution, reporting, call flows)
|
- Docs index: `docs/README.md` (run sequence, config resolution, reporting, call flows)
|
||||||
- Raspberry Pi deployment: `docs/09_raspberry_pi_deployment.md`
|
- Raspberry Pi deployment: `docs/09_raspberry_pi_deployment.md`
|
||||||
- Build custom Pi image: `docs/10_build_custom_image.md`
|
- Build custom Pi image: `docs/10_build_custom_image.md`
|
||||||
@ -258,7 +431,7 @@ The `ecu_framework/lin/babylin.py` implementation uses the official `BabyLIN_lib
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- HTML report missing columns: ensure `pytest.ini` includes `-p conftest_plugin` in `addopts`.
|
- HTML report missing columns: ensure `pytest.ini` includes `-p conftest_plugin` in `addopts`.
|
||||||
- ImportError for BabyLIN_library: verify `vendor/BabyLIN_library.py` placement and that required native libraries (DLL/.so) from the SDK are available on PATH/LD_LIBRARY_PATH.
|
- ImportError for BabyLIN_library (DEPRECATED path): verify `vendor/BabyLIN_library.py` placement and that required native libraries (DLL/.so) from the SDK are available on PATH/LD_LIBRARY_PATH. Consider migrating to the MUM adapter, which avoids vendor DLLs entirely.
|
||||||
- Permission errors in PowerShell: run the venv's full Python path or adjust ExecutionPolicy for scripts.
|
- Permission errors in PowerShell: run the venv's full Python path or adjust ExecutionPolicy for scripts.
|
||||||
- Import errors: activate the venv and reinstall `requirements.txt`.
|
- Import errors: activate the venv and reinstall `requirements.txt`.
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,12 @@ def test_mock_send_receive_echo(self, mock_interface):
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
|
The hardware-test layer is split across three modules — see
|
||||||
|
[`docs/19_frame_io_and_alm_helpers.md`](docs/19_frame_io_and_alm_helpers.md)
|
||||||
|
for the API and the four-phase test pattern, and
|
||||||
|
[`docs/14_power_supply.md`](docs/14_power_supply.md) §5 for the
|
||||||
|
session-managed PSU lifecycle.
|
||||||
|
|
||||||
```
|
```
|
||||||
ecu_tests/
|
ecu_tests/
|
||||||
├── ecu_framework/ # Core framework package
|
├── ecu_framework/ # Core framework package
|
||||||
@ -79,27 +85,75 @@ ecu_tests/
|
|||||||
│ ├── lin/ # LIN communication interfaces
|
│ ├── lin/ # LIN communication interfaces
|
||||||
│ │ ├── base.py # Abstract LinInterface definition
|
│ │ ├── base.py # Abstract LinInterface definition
|
||||||
│ │ ├── mock.py # Mock interface for development
|
│ │ ├── mock.py # Mock interface for development
|
||||||
|
│ │ ├── mum.py # MUM adapter (current default; pylin/pymumclient)
|
||||||
|
│ │ ├── ldf.py # LdfDatabase wrapper around ldfparser
|
||||||
│ │ └── babylin.py # DEPRECATED BabyLin hardware interface (kept for legacy rigs)
|
│ │ └── babylin.py # DEPRECATED BabyLin hardware interface (kept for legacy rigs)
|
||||||
|
│ ├── power/ # Bench power supply control
|
||||||
|
│ │ └── owon_psu.py # Owon PSU SCPI controller + cross-platform resolver
|
||||||
│ └── flashing/ # Hex file flashing capabilities
|
│ └── flashing/ # Hex file flashing capabilities
|
||||||
│ └── hex_flasher.py # ECU flash programming
|
│ └── hex_flasher.py # ECU flash programming
|
||||||
|
│
|
||||||
├── tests/ # Test suite
|
├── tests/ # Test suite
|
||||||
│ ├── conftest.py # pytest fixtures and configuration
|
│ ├── conftest.py # Project-wide fixtures: config, lin, ldf, flash_ecu, rp
|
||||||
│ ├── test_smoke_mock.py # Mock interface validation tests
|
│ ├── test_smoke_mock.py # Mock interface validation tests
|
||||||
│ ├── test_babylin_hardware_smoke.py # Hardware smoke tests (deprecated BabyLIN)
|
│ ├── test_babylin_hardware_smoke.py # Hardware smoke tests (deprecated BabyLIN)
|
||||||
│ └── test_hardware_placeholder.py # Future hardware tests
|
│ ├── test_hardware_placeholder.py # Placeholder
|
||||||
|
│ ├── unit/ # Pure-logic tests (no hardware)
|
||||||
|
│ │ ├── test_config_loader.py
|
||||||
|
│ │ ├── test_linframe.py
|
||||||
|
│ │ ├── test_ldf_database.py
|
||||||
|
│ │ ├── test_hex_flasher.py
|
||||||
|
│ │ ├── test_mum_adapter_mocked.py
|
||||||
|
│ │ └── test_babylin_adapter_mocked.py # deprecated path
|
||||||
|
│ ├── plugin/
|
||||||
|
│ │ └── test_conftest_plugin_artifacts.py
|
||||||
|
│ └── hardware/ # Real-bench tests (MUM / PSU / ECU)
|
||||||
|
│ ├── conftest.py # Session-scoped autouse PSU fixture (powers the ECU)
|
||||||
|
│ ├── frame_io.py # FrameIO — generic LDF-driven send/receive/pack/unpack
|
||||||
|
│ ├── alm_helpers.py # AlmTester — ALM_Node domain helpers + constants
|
||||||
|
│ ├── psu_helpers.py # apply_voltage_and_settle — measure-rail-then-validate
|
||||||
|
│ ├── _test_case_template.py # ALM-only test starting point
|
||||||
|
│ ├── _test_case_template_psu_lin.py # PSU + LIN test starting point
|
||||||
|
│ ├── test_mum_alm_animation.py # ALM mode/update/LID checks
|
||||||
|
│ ├── test_mum_auto_addressing.py # BSM auto-addressing
|
||||||
|
│ ├── test_e2e_mum_led_activate.py # MUM end-to-end power+activate
|
||||||
|
│ ├── test_overvolt.py # Voltage-tolerance suite
|
||||||
|
│ ├── test_psu_voltage_settling.py # PSU settling-time (opt-in: -m psu_settling)
|
||||||
|
│ ├── test_owon_psu.py # PSU IDN + measurements (read-only)
|
||||||
|
│ └── test_e2e_power_on_lin_smoke.py # DEPRECATED BabyLIN E2E
|
||||||
|
│
|
||||||
├── config/ # Configuration files
|
├── config/ # Configuration files
|
||||||
│ ├── test_config.yaml # Main test configuration
|
│ ├── test_config.yaml # Main test configuration (MUM by default)
|
||||||
│ └── babylin.example.yaml # DEPRECATED BabyLin configuration template
|
│ ├── mum.example.yaml # MUM hardware profile
|
||||||
├── vendor/ # SDK placement (BabyLIN paths are deprecated)
|
│ ├── owon_psu.example.yaml # PSU profile (copy to owon_psu.yaml)
|
||||||
| ├── BabyLIN_library.py # Official SDK Python wrapper (deprecated)
|
│ ├── owon_psu.yaml # Optional per-machine PSU override
|
||||||
| └── platform libs # OS-specific native libs (DLL/.so/.dylib) — deprecated
|
│ ├── examples.yaml # Combined mock/babylin profiles
|
||||||
├── reports/ # Generated test reports
|
│ └── babylin.example.yaml # DEPRECATED BabyLIN profile
|
||||||
│ ├── report.html # Enhanced HTML report
|
│
|
||||||
│ └── junit.xml # JUnit XML report
|
├── docs/ # Deep-dive guides (numbered for reading order)
|
||||||
|
│ ├── 01_run_sequence.md … 18_test_catalog.md
|
||||||
|
│ └── 19_frame_io_and_alm_helpers.md # Hardware test helpers + four-phase pattern
|
||||||
|
│
|
||||||
|
├── vendor/ # Third-party assets and project resources
|
||||||
|
│ ├── 4SEVEN_color_lib_test.ldf # LDF used by the LIN tests
|
||||||
|
│ ├── rgb_to_pwm.py # RGB → PWM calculator (ALM PWM assertions)
|
||||||
|
│ ├── Owon/owon_psu_quick_demo.py # Standalone PSU demo
|
||||||
|
│ ├── automated_lin_test/ # Reference scripts + Melexis package installer
|
||||||
|
│ ├── BabyLIN_library.py # DEPRECATED official SDK Python wrapper
|
||||||
|
│ └── platform libs # DEPRECATED OS-specific native libs (DLL/.so)
|
||||||
|
│
|
||||||
|
├── reports/ # Generated test reports per run
|
||||||
|
│ ├── report.html # Enhanced HTML with metadata
|
||||||
|
│ ├── junit.xml # JUnit XML for CI
|
||||||
|
│ ├── summary.md # Machine-readable run summary
|
||||||
|
│ └── requirements_coverage.json
|
||||||
|
│
|
||||||
|
├── scripts/ # Pi/CI helpers (pi_install.sh, ecu-tests.service, …)
|
||||||
├── conftest_plugin.py # Custom pytest plugin for enhanced reporting
|
├── conftest_plugin.py # Custom pytest plugin for enhanced reporting
|
||||||
├── pytest.ini # pytest configuration with custom markers
|
├── pytest.ini # Markers, addopts, junit_family=legacy
|
||||||
├── requirements.txt # Python dependencies
|
├── requirements.txt # Python dependencies
|
||||||
└── README.md # Project documentation
|
├── README.md # Quick start + project overview
|
||||||
|
└── TESTING_FRAMEWORK_GUIDE.md # ← you are here
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|||||||
@ -4,8 +4,7 @@ This document provides a high-level view of the framework’s components and how
|
|||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
- Tests (pytest) — test modules and functions under `tests/`
|
### Framework core (`ecu_framework/`)
|
||||||
- Fixtures — defined in `tests/conftest.py` (config, lin, flash_ecu)
|
|
||||||
- Config Loader — `ecu_framework/config.py` (YAML → dataclasses)
|
- Config Loader — `ecu_framework/config.py` (YAML → dataclasses)
|
||||||
- LIN Abstraction — `ecu_framework/lin/base.py` (`LinInterface`, `LinFrame`)
|
- LIN Abstraction — `ecu_framework/lin/base.py` (`LinInterface`, `LinFrame`)
|
||||||
- Mock LIN Adapter — `ecu_framework/lin/mock.py`
|
- Mock LIN Adapter — `ecu_framework/lin/mock.py`
|
||||||
@ -13,21 +12,41 @@ This document provides a high-level view of the framework’s components and how
|
|||||||
- BabyLIN Adapter — `ecu_framework/lin/babylin.py` (SDK wrapper → BabyLIN_library.py; **DEPRECATED**, kept for legacy rigs only)
|
- BabyLIN Adapter — `ecu_framework/lin/babylin.py` (SDK wrapper → BabyLIN_library.py; **DEPRECATED**, kept for legacy rigs only)
|
||||||
- LDF Database — `ecu_framework/lin/ldf.py` (`LdfDatabase`/`Frame` over `ldfparser`; per-frame `pack`/`unpack`)
|
- LDF Database — `ecu_framework/lin/ldf.py` (`LdfDatabase`/`Frame` over `ldfparser`; per-frame `pack`/`unpack`)
|
||||||
- Flasher — `ecu_framework/flashing/hex_flasher.py`
|
- Flasher — `ecu_framework/flashing/hex_flasher.py`
|
||||||
- Power Supply (PSU) control — `ecu_framework/power/owon_psu.py` (serial SCPI)
|
- Power Supply (PSU) control — `ecu_framework/power/owon_psu.py` (serial SCPI + cross-platform port resolver)
|
||||||
- PSU quick demo script — `vendor/Owon/owon_psu_quick_demo.py`
|
- PSU quick demo script — `vendor/Owon/owon_psu_quick_demo.py`
|
||||||
|
|
||||||
|
### Hardware test layer (`tests/hardware/`)
|
||||||
|
- Project-wide fixtures — `tests/conftest.py` (config, lin, ldf, flash_ecu, rp)
|
||||||
|
- Hardware-suite fixtures — `tests/hardware/conftest.py` (session-scoped, autouse PSU; the bench is powered up once at session start and stays on for every test in the suite)
|
||||||
|
- Generic LDF I/O — `tests/hardware/frame_io.py` (`FrameIO` — send/receive/pack/unpack for any LDF frame plus raw-bus escape hatches)
|
||||||
|
- ALM domain helpers — `tests/hardware/alm_helpers.py` (`AlmTester` — force_off / wait_for_state / measure_animating_window / assert_pwm_*)
|
||||||
|
- PSU settle helpers — `tests/hardware/psu_helpers.py` (`wait_until_settled`, `apply_voltage_and_settle` — measured-rail-then-validation pattern shared by all voltage-changing tests)
|
||||||
|
- RGB→PWM calculator — `vendor/rgb_to_pwm.py` (consumed by `AlmTester.assert_pwm_*`)
|
||||||
|
- Test templates (not collected) — `tests/hardware/_test_case_template.py`, `tests/hardware/_test_case_template_psu_lin.py`
|
||||||
|
|
||||||
|
### Tests, reporting, artifacts
|
||||||
|
- Tests (pytest) — modules under `tests/{,unit,plugin,hardware}/`
|
||||||
- Reporting Plugin — `conftest_plugin.py` (docstring → report metadata)
|
- Reporting Plugin — `conftest_plugin.py` (docstring → report metadata)
|
||||||
- Reports — `reports/report.html`, `reports/junit.xml`
|
- Reports — `reports/report.html`, `reports/junit.xml`, `reports/summary.md`, `reports/requirements_coverage.json`
|
||||||
|
|
||||||
## Mermaid architecture diagram
|
## Mermaid architecture diagram
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
subgraph Tests & Pytest
|
subgraph Tests_and_Pytest [Tests & Pytest]
|
||||||
T[tests/*]
|
T[tests/* (test bodies)]
|
||||||
CF[tests/conftest.py]
|
CF[tests/conftest.py<br/>config, lin, ldf, flash_ecu, rp]
|
||||||
|
HCF[tests/hardware/conftest.py<br/>SESSION psu (autouse)]
|
||||||
PL[conftest_plugin.py]
|
PL[conftest_plugin.py]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
subgraph Hardware_Helpers [Hardware-test helpers]
|
||||||
|
FIO[tests/hardware/frame_io.py<br/>FrameIO]
|
||||||
|
ALM[tests/hardware/alm_helpers.py<br/>AlmTester]
|
||||||
|
RGB[vendor/rgb_to_pwm.py]
|
||||||
|
TPL[tests/hardware/_test_case_template*.py<br/>not collected]
|
||||||
|
end
|
||||||
|
|
||||||
subgraph Framework
|
subgraph Framework
|
||||||
CFG[ecu_framework/config.py]
|
CFG[ecu_framework/config.py]
|
||||||
BASE[ecu_framework/lin/base.py]
|
BASE[ecu_framework/lin/base.py]
|
||||||
@ -36,29 +55,35 @@ flowchart TB
|
|||||||
BABY[ecu_framework/lin/babylin.py<br/>DEPRECATED]
|
BABY[ecu_framework/lin/babylin.py<br/>DEPRECATED]
|
||||||
LDF[ecu_framework/lin/ldf.py]
|
LDF[ecu_framework/lin/ldf.py]
|
||||||
FLASH[ecu_framework/flashing/hex_flasher.py]
|
FLASH[ecu_framework/flashing/hex_flasher.py]
|
||||||
POWER[ecu_framework/power/owon_psu.py]
|
POWER[ecu_framework/power/owon_psu.py<br/>SerialParams, OwonPSU,<br/>resolve_port]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Artifacts
|
subgraph Artifacts
|
||||||
REP[reports/report.html<br/>reports/junit.xml]
|
REP[reports/report.html<br/>reports/junit.xml<br/>reports/summary.md]
|
||||||
YAML[config/*.yaml<br/>test_config.yaml<br/>mum.example.yaml<br/>babylin.example.yaml — deprecated]
|
YAML[config/*.yaml<br/>test_config.yaml<br/>mum.example.yaml<br/>babylin.example.yaml — deprecated]
|
||||||
PSU_YAML[config/owon_psu.yaml<br/>OWON_PSU_CONFIG]
|
PSU_YAML[config/owon_psu.yaml<br/>OWON_PSU_CONFIG]
|
||||||
MELEXIS[Melexis pylin + pymumclient<br/>MUM @ 192.168.7.2]
|
MELEXIS[Melexis pylin + pymumclient<br/>MUM @ 192.168.7.2]
|
||||||
SDK[vendor/BabyLIN_library.py<br/>platform-specific libs<br/>DEPRECATED]
|
SDK[vendor/BabyLIN_library.py<br/>platform libs<br/>DEPRECATED]
|
||||||
OWON[vendor/Owon/owon_psu_quick_demo.py]
|
OWON[vendor/Owon/owon_psu_quick_demo.py]
|
||||||
LDFFILE[vendor/*.ldf]
|
LDFFILE[vendor/*.ldf]
|
||||||
LDFLIB[ldfparser PyPI]
|
LDFLIB[ldfparser PyPI]
|
||||||
end
|
end
|
||||||
|
|
||||||
T --> CF
|
T --> CF
|
||||||
|
T --> HCF
|
||||||
CF --> CFG
|
CF --> CFG
|
||||||
CF --> BASE
|
CF --> BASE
|
||||||
CF --> MOCK
|
CF --> MOCK
|
||||||
CF --> MUM
|
CF --> MUM
|
||||||
CF --> BABY
|
CF --> BABY
|
||||||
CF --> FLASH
|
CF --> FLASH
|
||||||
T --> POWER
|
HCF --> POWER
|
||||||
T --> LDF
|
T --> FIO
|
||||||
|
T --> ALM
|
||||||
|
ALM --> FIO
|
||||||
|
ALM --> RGB
|
||||||
|
TPL -.copy & edit.-> T
|
||||||
|
|
||||||
PL --> REP
|
PL --> REP
|
||||||
|
|
||||||
CFG --> YAML
|
CFG --> YAML
|
||||||
@ -67,6 +92,7 @@ flowchart TB
|
|||||||
BABY --> SDK
|
BABY --> SDK
|
||||||
LDF --> LDFLIB
|
LDF --> LDFLIB
|
||||||
LDF --> LDFFILE
|
LDF --> LDFFILE
|
||||||
|
POWER --> PSU_YAML
|
||||||
T --> OWON
|
T --> OWON
|
||||||
T --> REP
|
T --> REP
|
||||||
```
|
```
|
||||||
@ -76,14 +102,29 @@ flowchart TB
|
|||||||
- Tests use fixtures to obtain config and a connected LIN adapter
|
- Tests use fixtures to obtain config and a connected LIN adapter
|
||||||
- Config loader reads YAML (or env override), returns typed dataclasses
|
- Config loader reads YAML (or env override), returns typed dataclasses
|
||||||
- LIN calls are routed through the interface abstraction to the selected adapter
|
- LIN calls are routed through the interface abstraction to the selected adapter
|
||||||
- Flasher (optional) uses the same interface to program the ECU
|
- Hardware tests sit on top of two helpers: `FrameIO` (LDF-driven send /
|
||||||
- Power supply control (optional) uses `ecu_framework/power/owon_psu.py` and reads
|
receive / pack / unpack for any frame) and `AlmTester` (ALM_Node domain
|
||||||
`config.power_supply` (merged with `config/owon_psu.yaml` or `OWON_PSU_CONFIG` when present);
|
patterns built on `FrameIO`). Both are imported as siblings from
|
||||||
the quick demo script under `vendor/Owon/` provides a quick manual flow
|
`tests/hardware/` — see `docs/19_frame_io_and_alm_helpers.md`
|
||||||
|
- The hardware-suite `tests/hardware/conftest.py` defines a **session-scoped,
|
||||||
|
autouse** `psu` fixture: on benches where the Owon PSU powers the ECU,
|
||||||
|
the supply is opened once at session start, parked at
|
||||||
|
`config.power_supply.set_voltage` / `set_current`, and left enabled
|
||||||
|
for every test. Voltage-tolerance tests perturb voltage and restore
|
||||||
|
in `finally`; they never toggle output. See `docs/14_power_supply.md` §5.
|
||||||
|
- Flasher (optional) uses the same `LinInterface` to program the ECU
|
||||||
|
- Power supply control (optional) uses `ecu_framework/power/owon_psu.py`
|
||||||
|
and reads `config.power_supply` (merged with `config/owon_psu.yaml`
|
||||||
|
or `OWON_PSU_CONFIG` when present). The quick demo script under
|
||||||
|
`vendor/Owon/` provides a quick manual flow
|
||||||
- Reporting plugin parses docstrings and enriches the HTML report
|
- Reporting plugin parses docstrings and enriches the HTML report
|
||||||
|
|
||||||
## Extending the architecture
|
## Extending the architecture
|
||||||
|
|
||||||
- Add new bus adapters by implementing `LinInterface`
|
- Add new bus adapters by implementing `LinInterface`
|
||||||
|
- Add new ECU-domain helpers next to `AlmTester` (e.g. `BcmTester`)
|
||||||
|
on top of `FrameIO`; share fixtures via `tests/hardware/conftest.py`
|
||||||
|
- Add new bench instrument controllers next to `OwonPSU` under
|
||||||
|
`ecu_framework/power/` or a new `ecu_framework/instruments/` package,
|
||||||
|
expose them as session-scoped fixtures
|
||||||
- Add new report sinks (e.g., JSON or a DB) by extending the plugin
|
- Add new report sinks (e.g., JSON or a DB) by extending the plugin
|
||||||
- Add new fixtures for diagnostics or measurement tools (Scopes, power supplies, etc.)
|
|
||||||
|
|||||||
@ -1,110 +1,486 @@
|
|||||||
# Power Supply (Owon) — control, configuration, tests, and quick demo
|
# Power Supply (Owon) — control, configuration, tests, and quick demo
|
||||||
|
|
||||||
This guide covers using the Owon bench power supply via SCPI over serial with the framework.
|
This guide covers driving the Owon bench power supply via SCPI over a
|
||||||
|
serial link, plus the cross-platform port resolver and the safety
|
||||||
|
guarantees the controller class provides.
|
||||||
|
|
||||||
> **MUM users**: the Melexis Universal Master has its own power output on
|
> **MUM users**: the Melexis Universal Master has its own power output
|
||||||
> `power_out0` and the MUM adapter calls `power_up()` / `power_down()` in
|
> on `power_out0` and the MUM adapter calls `power_up()` /
|
||||||
> `connect()` / `disconnect()` automatically. The Owon PSU is **not required**
|
> `power_down()` in `connect()` / `disconnect()` automatically. The
|
||||||
> for the standard MUM flow — leave `power_supply.enabled: false`. The Owon
|
> Owon PSU is **not required** for the standard MUM flow — leave
|
||||||
> remains useful for over/under-voltage scenarios, separate-rail tests, or
|
> `power_supply.enabled: false`. The Owon remains useful for
|
||||||
> when running with the deprecated BabyLIN adapter (which has no built-in power).
|
> over/under-voltage scenarios, separate-rail tests, or when running
|
||||||
|
> with the deprecated BabyLIN adapter (which has no built-in power).
|
||||||
|
|
||||||
- Library: `ecu_framework/power/owon_psu.py`
|
| Artifact | Path |
|
||||||
- Hardware test: `tests/hardware/test_owon_psu.py`
|
|---|---|
|
||||||
- quick demo script: `vendor/Owon/owon_psu_quick_demo.py`
|
| Controller library | [`ecu_framework/power/owon_psu.py`](../ecu_framework/power/owon_psu.py) |
|
||||||
- Configuration: `config/test_config.yaml` (`power_supply`), optionally merged from `config/owon_psu.yaml` or env `OWON_PSU_CONFIG`
|
| Hardware test | [`tests/hardware/test_owon_psu.py`](../tests/hardware/test_owon_psu.py) |
|
||||||
|
| Quick demo script | [`vendor/Owon/owon_psu_quick_demo.py`](../vendor/Owon/owon_psu_quick_demo.py) |
|
||||||
|
| Central config | [`config/test_config.yaml`](../config/test_config.yaml) → `power_supply` |
|
||||||
|
| Per-machine override | `config/owon_psu.yaml` or env `OWON_PSU_CONFIG` |
|
||||||
|
|
||||||
## Install dependencies
|
---
|
||||||
|
|
||||||
|
## 1. Install dependencies
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
pip install -r .\requirements.txt
|
pip install -r .\requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configure
|
`pyserial` is the only non-stdlib dep used by the controller.
|
||||||
|
|
||||||
You can keep PSU settings centrally or in a machine-specific YAML.
|
---
|
||||||
|
|
||||||
- Central: `config/test_config.yaml` → `power_supply` section
|
## 2. Configure
|
||||||
- Separate: `config/owon_psu.yaml` (or `OWON_PSU_CONFIG` env var)
|
|
||||||
|
|
||||||
Supported keys:
|
Settings can live centrally in `config/test_config.yaml` or be peeled
|
||||||
|
out into a machine-specific `config/owon_psu.yaml` (or any path set
|
||||||
|
via `OWON_PSU_CONFIG`). The loader merges the per-machine file into
|
||||||
|
the central `power_supply` section.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
power_supply:
|
power_supply:
|
||||||
enabled: true
|
enabled: true
|
||||||
port: COM4 # e.g., COM4 (Windows) or /dev/ttyUSB0 (Linux)
|
port: COM7 # see §3 for cross-platform behaviour
|
||||||
baudrate: 115200
|
baudrate: 115200
|
||||||
timeout: 1.0
|
timeout: 1.0
|
||||||
eol: "\n" # or "\r\n" if required
|
eol: "\n" # or "\r\n" if your device requires CRLF
|
||||||
parity: N # N|E|O
|
parity: N # N|E|O
|
||||||
stopbits: 1 # 1|2
|
stopbits: 1 # 1|1.5|2
|
||||||
xonxoff: false
|
xonxoff: false
|
||||||
rtscts: false
|
rtscts: false
|
||||||
dsrdtr: false
|
dsrdtr: false
|
||||||
idn_substr: OWON
|
idn_substr: OWON # optional — see §4 (auto-detection)
|
||||||
do_set: false
|
do_set: false
|
||||||
set_voltage: 5.0
|
set_voltage: 5.0
|
||||||
set_current: 0.1
|
set_current: 0.1
|
||||||
```
|
```
|
||||||
|
|
||||||
The central config loader automatically merges `config/owon_psu.yaml` (or the path in `OWON_PSU_CONFIG`) into `power_supply`.
|
### Field reference
|
||||||
|
|
||||||
## Run the hardware test
|
| Field | Default | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `enabled` | `false` | Master gate. Tests/utilities skip when `false`. |
|
||||||
|
| `port` | `null` | Bench port name. See §3 — works for `COM7` *or* `/dev/ttyUSB0` and translates between them. |
|
||||||
|
| `baudrate` | `115200` | Serial bit rate. |
|
||||||
|
| `timeout` | `1.0` | Read timeout in seconds. |
|
||||||
|
| `eol` | `"\n"` | Line terminator appended to every command and expected on every response. |
|
||||||
|
| `parity` | `"N"` | One of `N`, `E`, `O`. Translated to `pyserial` constants by `SerialParams.from_config()`. |
|
||||||
|
| `stopbits` | `1` | One of `1`, `1.5`, `2`. |
|
||||||
|
| `xonxoff` / `rtscts` / `dsrdtr` | `false` | Flow control flags. |
|
||||||
|
| `idn_substr` | `null` | Optional substring (case-insensitive) the device's `*IDN?` must contain to be accepted. Used as the filter when scanning ports for auto-detection. |
|
||||||
|
| `do_set` | `false` | If `true`, the hardware test runs the set/measure cycle (sets V/I, enables output briefly, measures, disables). |
|
||||||
|
| `set_voltage` / `set_current` | `5.0` / `0.1` | Setpoints used when `do_set: true`. |
|
||||||
|
|
||||||
Skips unless `power_supply.enabled` is true and `port` is set.
|
---
|
||||||
|
|
||||||
|
## 3. Cross-platform port resolution
|
||||||
|
|
||||||
|
A bench config typically names the port the way Windows sees it
|
||||||
|
(`COM7`). The resolver lets the **same config** work on Windows,
|
||||||
|
Linux, and WSL by trying multiple candidates in priority order.
|
||||||
|
|
||||||
|
### What the resolver does
|
||||||
|
|
||||||
|
`resolve_port(configured, *, idn_substr, params)` walks four phases
|
||||||
|
and returns the first port whose `*IDN?` response is non-empty
|
||||||
|
(filtered by `idn_substr` if given):
|
||||||
|
|
||||||
|
| Phase | What's tried | Use case |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `configured` verbatim | Windows native — `COM7` opens directly. |
|
||||||
|
| 2 | Cross-platform translation | `COM7` ↔ `/dev/ttyS6` on WSL1; `/dev/ttyS6` ↔ `COM7` on Windows. |
|
||||||
|
| 3 | Linux USB-serial paths | `/dev/ttyUSB*` and `/dev/ttyACM*` — covers WSL2 with `usbipd-win` plus generic Linux USB adapters. Linux/WSL only. |
|
||||||
|
| 4 | Full `scan_ports()` | Last resort — probes every serial port `pyserial` reports. |
|
||||||
|
|
||||||
|
Linux device files that don't exist on disk are skipped without an
|
||||||
|
open attempt, so the resolver is fast even on machines with many
|
||||||
|
phantom `ttyS*` entries.
|
||||||
|
|
||||||
|
### What works on each platform with `port: COM7`
|
||||||
|
|
||||||
|
| Host | What happens |
|
||||||
|
|---|---|
|
||||||
|
| **Windows native** | Phase 1 hits `COM7` directly. |
|
||||||
|
| **WSL1** | Phase 1 fails on `COM7`, Phase 2 finds `/dev/ttyS6` (the COM7 mapping). |
|
||||||
|
| **WSL2 + `usbipd-win`** | Phase 1+2 fail, Phase 3 finds the attached adapter at `/dev/ttyUSB0`. |
|
||||||
|
| **Linux native (USB adapter)** | Phases 1+2 fail, Phase 3 finds `/dev/ttyUSB0`. |
|
||||||
|
|
||||||
|
The resolved port is recorded in the JUnit testsuite properties as
|
||||||
|
`psu_resolved_port` (and the IDN as `psu_resolved_idn`), so report
|
||||||
|
viewers can see which path was used.
|
||||||
|
|
||||||
|
### Translation helpers
|
||||||
|
|
||||||
|
Useful as building blocks if you need to do the mapping yourself:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ecu_framework.power import (
|
||||||
|
windows_com_to_linux, linux_serial_to_windows,
|
||||||
|
candidate_ports, resolve_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
windows_com_to_linux("COM7") # → "/dev/ttyS6"
|
||||||
|
windows_com_to_linux("com10") # → "/dev/ttyS9"
|
||||||
|
linux_serial_to_windows("/dev/ttyS6") # → "COM7"
|
||||||
|
|
||||||
|
# What resolve_port will try, in order, for port="COM7" on Linux:
|
||||||
|
candidate_ports("COM7")
|
||||||
|
# → ['COM7', '/dev/ttyS6', '/dev/ttyUSB0', '/dev/ttyUSB1', '/dev/ttyACM0', ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Auto-detection
|
||||||
|
|
||||||
|
Leave `port` empty and set `idn_substr` to let the resolver scan:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
power_supply:
|
||||||
|
enabled: true
|
||||||
|
port: # ← empty
|
||||||
|
idn_substr: OWON # ← required so we don't grab a different SCPI device
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
With no `port`, Phase 1 and Phase 2 short-circuit; Phase 3 (Linux USB
|
||||||
|
paths) and Phase 4 (full scan) do the work. The first port whose IDN
|
||||||
|
contains `OWON` (case-insensitive) wins.
|
||||||
|
|
||||||
|
> **Tip:** without `idn_substr`, *any* device that responds to `*IDN?`
|
||||||
|
> on any port is accepted — fine when the PSU is the only SCPI thing
|
||||||
|
> attached, risky otherwise. Always set `idn_substr` if your bench has
|
||||||
|
> other SCPI hardware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Session-managed power (the bench powers the ECU through the PSU)
|
||||||
|
|
||||||
|
On benches where the **Owon PSU powers the ECU** (the MUM only carries
|
||||||
|
LIN traffic), the PSU output must stay on for the *entire* test
|
||||||
|
session — not just the duration of an individual PSU test. Otherwise
|
||||||
|
every test that runs after a closed PSU connection would brown out
|
||||||
|
the ECU and fail.
|
||||||
|
|
||||||
|
The hardware-suite conftest
|
||||||
|
([`tests/hardware/conftest.py`](../tests/hardware/conftest.py))
|
||||||
|
implements this with three session-scoped fixtures:
|
||||||
|
|
||||||
|
| Fixture | Scope | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `_psu_or_none` | session | Tolerant: opens the PSU once, parks at `set_voltage` / `set_current`, enables output. Yields the live `OwonPSU` or `None` if unreachable. Closes (with `output 0`) at session end. |
|
||||||
|
| `_psu_powers_bench` | session, **autouse** | Realizes `_psu_or_none`. Every hardware test triggers PSU power-up at session start, even tests that don't request `psu` by name. |
|
||||||
|
| `psu` | session | Public fixture for tests that read measurements or perturb voltage. Skips cleanly when the PSU isn't available. |
|
||||||
|
|
||||||
|
### What this means for tests
|
||||||
|
|
||||||
|
Tests **should**:
|
||||||
|
|
||||||
|
- Request `psu` if they need to read measurements or change the supply voltage.
|
||||||
|
- Always restore nominal voltage in their `finally` block — the session fixture won't restore it between tests.
|
||||||
|
|
||||||
|
Tests **must not**:
|
||||||
|
|
||||||
|
- Call `psu.set_output(False)` — this kills ECU power for every later test in the same session.
|
||||||
|
- Call `psu.close()` — the session fixture owns the lifecycle.
|
||||||
|
|
||||||
|
### What changed in the existing tests
|
||||||
|
|
||||||
|
- **`tests/hardware/test_owon_psu.py`** is now read-only: it queries `*IDN?`, `output?`, and the parsed measurement helpers, but doesn't toggle the output. The previous toggle-and-restore cycle has been deleted because it would brown out the bench mid-session.
|
||||||
|
- **`tests/hardware/_test_case_template_psu_lin.py`** drops its local `psu` fixture and uses the conftest's. Its autouse `_park_at_nominal` only restores voltage between tests — it never toggles output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Run the hardware test
|
||||||
|
|
||||||
|
Skips cleanly unless `power_supply.enabled` is true, a port can be
|
||||||
|
resolved, and the device responds to `*IDN?`.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
pytest -k test_owon_psu_idn_and_optional_set -m hardware -q
|
pytest -k test_owon_psu_idn_and_optional_set -m hardware -q
|
||||||
```
|
```
|
||||||
|
|
||||||
What it does:
|
What it does:
|
||||||
- Opens serial with your configured line params
|
|
||||||
- Queries `*IDN?` (checks `idn_substr` if provided)
|
|
||||||
- If `do_set` is true, sets voltage/current, enables output briefly, then disables
|
|
||||||
|
|
||||||
## Use the library programmatically
|
1. Resolves a working port via `resolve_port(...)` (cross-platform,
|
||||||
|
IDN-verified).
|
||||||
|
2. Queries `*IDN?` and the initial `output?` state.
|
||||||
|
3. If `do_set` is true: sets V/I, enables output, waits, measures,
|
||||||
|
disables output. The measure/disable pair lives in an inner
|
||||||
|
`try`/`finally` so the disable runs even if measurement raises.
|
||||||
|
4. Records IDN, before/after output state, setpoints, and parsed
|
||||||
|
measurements as report properties.
|
||||||
|
5. The fixture's `safe_off_on_close=True` is a backstop — it will
|
||||||
|
send `output 0` once more when the port closes.
|
||||||
|
|
||||||
|
The test follows the four-phase
|
||||||
|
[SETUP / PROCEDURE / ASSERT / TEARDOWN pattern from the template](19_frame_io_and_alm_helpers.md#72-the-four-phase-test-pattern)
|
||||||
|
because it mutates real bench state.
|
||||||
|
|
||||||
|
### The settle-then-validate pattern (recommended for any voltage-changing test)
|
||||||
|
|
||||||
|
Voltage changes go through two delays — and confusing them is the
|
||||||
|
single most common source of flaky tests:
|
||||||
|
|
||||||
|
| Delay | Source | Bench-dependent? |
|
||||||
|
|---|---|---|
|
||||||
|
| **PSU settling** | Owon needs time to slew its output to the new setpoint | **Yes** — depends on PSU model, load, cable drop. Different up-step / down-step times in practice. |
|
||||||
|
| **ECU validation** | Firmware samples its supply rail, debounces, and republishes status on its 10 ms LIN cycle | No (firmware-dependent, but constant for a given build) |
|
||||||
|
|
||||||
|
The shared helper [`tests/hardware/psu_helpers.py`](../tests/hardware/psu_helpers.py)
|
||||||
|
exposes `apply_voltage_and_settle()` which separates the two cleanly:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from ecu_framework.power import OwonPSU, SerialParams
|
from psu_helpers import apply_voltage_and_settle
|
||||||
|
|
||||||
params = SerialParams(baudrate=115200, timeout=1.0)
|
result = apply_voltage_and_settle(
|
||||||
with OwonPSU("COM4", params, eol="\n") as psu:
|
psu, OVERVOLTAGE_V,
|
||||||
print(psu.idn())
|
validation_time=ECU_VALIDATION_TIME_S, # firmware budget
|
||||||
psu.set_voltage(1, 5.0)
|
)
|
||||||
psu.set_current(1, 0.1)
|
# By here:
|
||||||
psu.set_output(True)
|
# - PSU output is measurably at OVERVOLTAGE_V (within ±0.10 V)
|
||||||
# ... measure, etc.
|
# - validation_time has elapsed since the rail settled
|
||||||
psu.set_output(False)
|
# So a single status read is unambiguous:
|
||||||
|
status = fio.read_signal("ALM_Status", "ALMVoltageStatus")
|
||||||
|
assert status == VOLTAGE_STATUS_OVER
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
What `apply_voltage_and_settle` does internally:
|
||||||
- Commands use newline-terminated writes; reads use `readline()`
|
|
||||||
- SCPI forms: `SOUR:VOLT`, `SOUR:CURR`, `MEAS:VOLT?`, `MEAS:CURR?`, `output 0/1`, `output?`
|
|
||||||
|
|
||||||
## quick demo script
|
1. `psu.set_voltage(1, target_v)` — issue the setpoint.
|
||||||
|
2. Polls `measure_voltage_v()` every 50 ms until the rail is within
|
||||||
|
±100 mV of target (or raises `AssertionError` on timeout).
|
||||||
|
3. `time.sleep(validation_time)` — hold the steady rail.
|
||||||
|
4. Returns `{settled_s, validation_s, final_v, trace}` for reporting.
|
||||||
|
|
||||||
The quick demo reads `OWON_PSU_CONFIG` or `config/owon_psu.yaml` and performs a small sequence.
|
The poll-the-meter approach means the function works on any bench
|
||||||
|
without re-tuning sleeps. Up-step and down-step are handled
|
||||||
|
identically — each waits as long as that *specific* transition takes.
|
||||||
|
|
||||||
|
To pick `ECU_VALIDATION_TIME_S`, run the characterization in §6.1
|
||||||
|
to learn your PSU's slew time, then add a margin for the firmware's
|
||||||
|
detection-and-debounce window. Default `1.0 s` is conservative for
|
||||||
|
most automotive ECUs. Tests that change voltage many times should
|
||||||
|
use the smallest validation time their firmware tolerates.
|
||||||
|
|
||||||
|
### Characterizing PSU settling time
|
||||||
|
|
||||||
|
Voltage-tolerance tests need to wait long enough after a setpoint
|
||||||
|
change for the PSU's output to actually reach the new voltage. The
|
||||||
|
right wait depends on the PSU model and the load. To extract real
|
||||||
|
numbers, run the dedicated characterization test:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pytest -m psu_settling -s
|
||||||
|
```
|
||||||
|
|
||||||
|
`tests/hardware/test_psu_voltage_settling.py` walks four
|
||||||
|
transitions (`13 V↔18 V`, `13 V↔7 V`), polls `measure_voltage_v()`
|
||||||
|
every 50 ms until the rail is within ±100 mV of target, and records
|
||||||
|
`settling_time_s` plus a downsampled voltage trace per case. The
|
||||||
|
test is marked `psu_settling` + `slow` so it doesn't run on every
|
||||||
|
`-m hardware` invocation — it's meant for periodic re-tuning, not
|
||||||
|
every CI run.
|
||||||
|
|
||||||
|
Use the recorded settling times to size constants like
|
||||||
|
`VOLTAGE_DETECT_TIMEOUT` in `test_overvolt.py`: the timeout has to
|
||||||
|
exceed *both* the PSU's settling time *and* the ECU's detection
|
||||||
|
delay, so add a margin to the larger of the two.
|
||||||
|
|
||||||
|
### Writing a PSU+LIN test (over/undervoltage etc.)
|
||||||
|
|
||||||
|
For tests that *combine* PSU control with LIN observation — e.g.
|
||||||
|
overvoltage / undervoltage tolerance — there's a dedicated
|
||||||
|
copy-paste-ready template at
|
||||||
|
[`tests/hardware/_test_case_template_psu_lin.py`](../tests/hardware/_test_case_template_psu_lin.py).
|
||||||
|
It contains:
|
||||||
|
|
||||||
|
- The three module-scoped fixtures (`fio`, `alm`, `psu`) wired with
|
||||||
|
cross-platform port resolution and `safe_off_on_close=True`.
|
||||||
|
- An autouse `_park_at_nominal` fixture that parks the PSU at
|
||||||
|
`NOMINAL_VOLTAGE` and the LED OFF before AND after every test, so
|
||||||
|
failures don't leak supply state between tests.
|
||||||
|
- A `wait_for_voltage_status(fio, target, …)` helper that polls
|
||||||
|
`ALM_Status.ALMVoltageStatus` until it matches.
|
||||||
|
- Three flavors:
|
||||||
|
| Flavor | Demonstrates |
|
||||||
|
|---|---|
|
||||||
|
| A | Overvoltage detection — drive PSU above OV threshold, expect `ALMVoltageStatus = 0x02`, restore. |
|
||||||
|
| B | Undervoltage detection — symmetric for UV (`0x01`). |
|
||||||
|
| C | Parametrized voltage sweep walking `(V, expected_status)` tuples. |
|
||||||
|
|
||||||
|
Tune the four constants at the top of the file
|
||||||
|
(`NOMINAL_VOLTAGE`, `OVERVOLTAGE_V`, `UNDERVOLTAGE_V`,
|
||||||
|
`SET_CURRENT_A`) to your ECU's datasheet before running on real
|
||||||
|
hardware. The defaults are conservative automotive ranges.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Library API
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ecu_framework.power import (
|
||||||
|
SerialParams, OwonPSU, resolve_port,
|
||||||
|
scan_ports, auto_detect, try_idn_on_port,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `SerialParams`
|
||||||
|
|
||||||
|
Plain dataclass for serial-port settings. Build directly, or from the
|
||||||
|
project's PSU config:
|
||||||
|
|
||||||
|
```python
|
||||||
|
params = SerialParams(baudrate=115200, timeout=1.0)
|
||||||
|
# or
|
||||||
|
params = SerialParams.from_config(config.power_supply) # translates 'N'/'1' → pyserial constants
|
||||||
|
```
|
||||||
|
|
||||||
|
### `OwonPSU`
|
||||||
|
|
||||||
|
Context-managed controller. Two construction paths:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Manual:
|
||||||
|
psu = OwonPSU(port="COM4", params=params, eol="\n")
|
||||||
|
|
||||||
|
# From central config (recommended):
|
||||||
|
psu = OwonPSU.from_config(config.power_supply)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then either use as a context manager or call `open()` / `close()` by
|
||||||
|
hand. Both forms send `output 0` before closing the port if
|
||||||
|
`safe_off_on_close=True` (the default).
|
||||||
|
|
||||||
|
```python
|
||||||
|
with OwonPSU.from_config(cfg) as psu:
|
||||||
|
print(psu.idn()) # *IDN?
|
||||||
|
psu.set_voltage(1, 5.0) # SOUR:VOLT 5.000
|
||||||
|
psu.set_current(1, 0.1) # SOUR:CURR 0.100
|
||||||
|
psu.set_output(True) # output 1
|
||||||
|
v = psu.measure_voltage_v() # MEAS:VOLT? → float
|
||||||
|
i = psu.measure_current_a() # MEAS:CURR? → float
|
||||||
|
is_on = psu.output_is_on() # output? → True/False/None
|
||||||
|
# safe_off_on_close=True turned the output OFF before the port closed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Method reference
|
||||||
|
|
||||||
|
| Method | SCPI sent | Returns |
|
||||||
|
|---|---|---|
|
||||||
|
| `idn()` | `*IDN?` | `str` |
|
||||||
|
| `set_voltage(channel, volts)` | `SOUR:VOLT <V>` | `None`. `channel` is currently ignored — placeholder for multi-channel firmware. |
|
||||||
|
| `set_current(channel, amps)` | `SOUR:CURR <A>` | `None` |
|
||||||
|
| `set_output(on)` | `output 1`/`output 0` | `None`. Note: dialect uses *lowercase* `output`, not `OUTP ON`. |
|
||||||
|
| `output_status()` | `output?` | Raw `str` (`'ON'`/`'OFF'`/`'1'`/`'0'`). |
|
||||||
|
| `output_is_on()` | `output?` | `bool` (or `None` if unparseable). |
|
||||||
|
| `measure_voltage()` | `MEAS:VOLT?` | Raw `str`. |
|
||||||
|
| `measure_voltage_v()` | `MEAS:VOLT?` | `float` (V) or `None`. |
|
||||||
|
| `measure_current()` | `MEAS:CURR?` | Raw `str`. |
|
||||||
|
| `measure_current_a()` | `MEAS:CURR?` | `float` (A) or `None`. |
|
||||||
|
| `query(s)` | `s` | Single-line `str` response (with newline stripped). |
|
||||||
|
| `write(s)` | `s` | `None`. No response read. |
|
||||||
|
|
||||||
|
#### Safety: `safe_off_on_close`
|
||||||
|
|
||||||
|
`OwonPSU(safe_off_on_close=True)` (the default) sends `output 0`
|
||||||
|
before the serial port closes. This protects against leaving the
|
||||||
|
bench powered on after an aborted test, an exception in user code, or
|
||||||
|
a forgotten manual close. Errors during the safe-off attempt are
|
||||||
|
swallowed so the close itself always completes.
|
||||||
|
|
||||||
|
Pass `safe_off_on_close=False` only when you specifically need the
|
||||||
|
output to stay enabled across context-manager boundaries. The
|
||||||
|
discovery helper `try_idn_on_port` opts out by default since it
|
||||||
|
shouldn't drive the bench in either direction.
|
||||||
|
|
||||||
|
### Discovery helpers
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Probe one port, return its IDN (or "" on failure):
|
||||||
|
try_idn_on_port("COM7", params)
|
||||||
|
|
||||||
|
# Scan every serial port; returns [(port, idn), ...] for responders:
|
||||||
|
scan_ports(params)
|
||||||
|
|
||||||
|
# Pick the first responder matching idn_substr (or first responder if no substring):
|
||||||
|
auto_detect(params, idn_substr="OWON")
|
||||||
|
|
||||||
|
# Cross-platform resolver (recommended): tries the configured port,
|
||||||
|
# its translation, USB-serial paths, then a full scan. Returns
|
||||||
|
# (port, idn) or None.
|
||||||
|
resolve_port("COM7", idn_substr="OWON", params=params)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Quick demo script
|
||||||
|
|
||||||
|
The quick demo reads `OWON_PSU_CONFIG` or `config/owon_psu.yaml` and
|
||||||
|
performs a short sequence using the same library.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python .\vendor\Owon\owon_psu_quick_demo.py
|
python .\vendor\Owon\owon_psu_quick_demo.py
|
||||||
```
|
```
|
||||||
|
|
||||||
It also scans ports with `*IDN?` using `scan_ports()`.
|
It also scans ports with `*IDN?` via `scan_ports()` to help confirm
|
||||||
|
which port the device is on before you commit it to the YAML.
|
||||||
|
|
||||||
## Troubleshooting
|
---
|
||||||
|
|
||||||
- Empty `*IDN?` or timeouts:
|
## 9. Troubleshooting
|
||||||
- Verify COM port and exclusivity (no other program holding it)
|
|
||||||
- Try `eol: "\r\n"`
|
|
||||||
- Adjust `parity` and `stopbits` per your device manual
|
|
||||||
- Windows COM > 9:
|
|
||||||
- Most Python code accepts `COM10` directly; if needed in other tools, use `\\.\\COM10`
|
|
||||||
- Flow control:
|
|
||||||
- Keep `xonxoff`, `rtscts`, `dsrdtr` false unless required
|
|
||||||
|
|
||||||
## Related files
|
### Empty `*IDN?` / timeouts
|
||||||
|
|
||||||
- `ecu_framework/power/owon_psu.py` — PSU controller (pyserial)
|
- Verify the port and exclusivity — no other program may hold it open.
|
||||||
- `tests/hardware/test_owon_psu.py` — Hardware test using central config
|
- Try `eol: "\r\n"` if your firmware revision expects CRLF.
|
||||||
- `vendor/Owon/owon_psu_quick_demo.py` — Quick demo runner
|
- Adjust `parity` and `stopbits` per your device manual.
|
||||||
- `config/owon_psu.example.yaml` — Example machine-specific YAML
|
- Power-cycle the PSU and re-attempt — some firmware revisions need
|
||||||
|
a fresh boot before they accept SCPI.
|
||||||
|
|
||||||
|
### `Could not find a working PSU port`
|
||||||
|
|
||||||
|
The fixture skips with this message when `resolve_port` returns
|
||||||
|
`None`. Things to check, in order:
|
||||||
|
|
||||||
|
1. Is the device powered and connected?
|
||||||
|
2. Does another process (Putty, Owon's own tool, an old test session)
|
||||||
|
still hold the port?
|
||||||
|
3. Does your user have permission to open the device file? On
|
||||||
|
Debian-style systems: `sudo usermod -aG dialout $USER` and re-login.
|
||||||
|
4. **WSL2 specifically**: USB-serial adapters need
|
||||||
|
[`usbipd-win`](https://learn.microsoft.com/en-us/windows/wsl/connect-usb)
|
||||||
|
to bind the device into the Linux side. Once attached they appear
|
||||||
|
at `/dev/ttyUSB0` and the resolver's Phase 3 picks them up
|
||||||
|
automatically.
|
||||||
|
5. **WSL1**: COMx → /dev/ttySn mapping is automatic. If `/dev/ttyS6`
|
||||||
|
doesn't exist for `COM7`, the bench probably has Windows COM port
|
||||||
|
numbering you weren't expecting — list with
|
||||||
|
`ls /dev/ttyS*` and try `linux_serial_to_windows()` to confirm.
|
||||||
|
|
||||||
|
### Windows COM > 9
|
||||||
|
|
||||||
|
Most Python tooling (including `pyserial`) accepts `COM10` directly.
|
||||||
|
If a third-party tool needs the long form, use `\\.\COM10`. The
|
||||||
|
translator in this repo accepts any positive integer.
|
||||||
|
|
||||||
|
### Flow control
|
||||||
|
|
||||||
|
Keep `xonxoff`, `rtscts`, `dsrdtr` set to `false` unless your specific
|
||||||
|
PSU model requires otherwise — the Owon family used in this project
|
||||||
|
doesn't.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Related files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `ecu_framework/power/owon_psu.py` | Controller library (`SerialParams`, `OwonPSU`, resolver helpers). |
|
||||||
|
| `tests/hardware/test_owon_psu.py` | Hardware test wired to central config. |
|
||||||
|
| `vendor/Owon/owon_psu_quick_demo.py` | Quick demo runner. |
|
||||||
|
| `config/owon_psu.example.yaml` | Example per-machine YAML. |
|
||||||
|
| `tests/hardware/_test_case_template.py` | Copyable starting point for new hardware tests. |
|
||||||
|
| [`docs/19_frame_io_and_alm_helpers.md`](19_frame_io_and_alm_helpers.md) | The four-phase test pattern and the FrameIO / AlmTester helpers. |
|
||||||
|
| [`docs/15_report_properties_cheatsheet.md`](15_report_properties_cheatsheet.md) | Standard `rp(...)` keys including the PSU ones (`psu_idn`, `psu_resolved_port`, …). |
|
||||||
|
|||||||
@ -27,14 +27,19 @@ This keeps reports consistent and easy to scan across suites.
|
|||||||
- expected_data: list of ints
|
- expected_data: list of ints
|
||||||
|
|
||||||
## Power supply (PSU)
|
## Power supply (PSU)
|
||||||
|
|
||||||
|
Per-test (function-scoped `rp`):
|
||||||
- psu_idn: string from `*IDN?`
|
- psu_idn: string from `*IDN?`
|
||||||
- output_status_before: bool
|
- output_status_before: string ('ON'/'OFF'/'1'/'0'; raw `output?` response)
|
||||||
- output_status_after: bool
|
- output_status_after: string (same, after the test toggled output)
|
||||||
- set_voltage: float (V)
|
- set_voltage: float (V)
|
||||||
- set_current: float (A)
|
- set_current: float (A)
|
||||||
- measured_voltage: float (V)
|
- measured_voltage: float | None (V) — parsed via `measure_voltage_v()`
|
||||||
- measured_current: float (A)
|
- measured_current: float | None (A) — parsed via `measure_current_a()`
|
||||||
- psu_port: e.g., COM4 or /dev/ttyUSB0 (if helpful)
|
|
||||||
|
Module-scoped (testsuite property — emitted once per file by the `psu` fixture):
|
||||||
|
- psu_resolved_port: string — the port `resolve_port` actually opened (e.g. `'COM7'`, `'/dev/ttyS6'`, `'/dev/ttyUSB0'`)
|
||||||
|
- psu_resolved_idn: string — the IDN response captured during resolution
|
||||||
|
|
||||||
## Flashing
|
## Flashing
|
||||||
- hex_path: string
|
- hex_path: string
|
||||||
|
|||||||
@ -13,9 +13,21 @@ Generated by hand from the source files; rerun
|
|||||||
| Mock-loopback smoke | 2 | 6 | none |
|
| Mock-loopback smoke | 2 | 6 | none |
|
||||||
| Plugin self-test | 1 | 1 | none |
|
| Plugin self-test | 1 | 1 | none |
|
||||||
| Hardware – MUM | 4 | 12 | MUM + ECU |
|
| Hardware – MUM | 4 | 12 | MUM + ECU |
|
||||||
| Hardware – BabyLIN (DEPRECATED) | 4 | 4 | BabyLIN + ECU + Owon PSU |
|
| Hardware – Voltage tolerance | 1 | 5 | MUM + ECU + Owon PSU |
|
||||||
| Hardware – Owon PSU | 1 | 1 | Owon PSU |
|
| Hardware – Owon PSU | 1 | 1 | Owon PSU |
|
||||||
| **Total** | **18** | **52** | mixed |
|
| Hardware – PSU settling (opt-in) | 1 | 4 | Owon PSU |
|
||||||
|
| Hardware – BabyLIN (DEPRECATED) | 4 | 4 | BabyLIN + ECU + Owon PSU |
|
||||||
|
| **Total** | **20** | **61** | mixed |
|
||||||
|
|
||||||
|
**Infrastructure (not collected as tests):**
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `tests/hardware/conftest.py` | Session-scoped autouse PSU fixture (powers the ECU once at session start) + the public `psu` fixture |
|
||||||
|
| `tests/hardware/frame_io.py` | `FrameIO` class — generic LDF-driven I/O |
|
||||||
|
| `tests/hardware/alm_helpers.py` | `AlmTester` class + ALM constants and tolerance utilities |
|
||||||
|
| `tests/hardware/_test_case_template.py` | ALM-only test starting point (leading underscore → not collected) |
|
||||||
|
| `tests/hardware/_test_case_template_psu_lin.py` | PSU + LIN test starting point (leading underscore → not collected) |
|
||||||
|
|
||||||
The numbers count the cases pytest reports when collecting. Some tests are
|
The numbers count the cases pytest reports when collecting. Some tests are
|
||||||
`@parametrize`-expanded (e.g. `test_linframe_invalid_id_raises[-1]`,
|
`@parametrize`-expanded (e.g. `test_linframe_invalid_id_raises[-1]`,
|
||||||
@ -28,7 +40,8 @@ pytest -m "unit" # pure unit tests
|
|||||||
pytest -m "not hardware" # everything except hardware (≈ 35 cases)
|
pytest -m "not hardware" # everything except hardware (≈ 35 cases)
|
||||||
pytest -m "hardware and mum" # MUM-only hardware tests
|
pytest -m "hardware and mum" # MUM-only hardware tests
|
||||||
pytest -m "hardware and babylin" # DEPRECATED BabyLIN hardware tests (legacy rigs only)
|
pytest -m "hardware and babylin" # DEPRECATED BabyLIN hardware tests (legacy rigs only)
|
||||||
pytest -m "hardware and not slow" # hardware excluding the slow auto-addressing test
|
pytest -m "hardware and not slow" # hardware excluding the slow tests
|
||||||
|
pytest -m psu_settling # PSU voltage-settling characterization (opt-in)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -297,15 +310,125 @@ Source: [tests/hardware/test_owon_psu.py](tests/hardware/test_owon_psu.py)
|
|||||||
|
|
||||||
| Test | Markers | Purpose |
|
| Test | Markers | Purpose |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `test_owon_psu_idn_and_optional_set` | `hardware` | Independent of any LIN adapter. Skips unless `power_supply.enabled: true` and `power_supply.port` is set. Opens the configured serial port, queries `*IDN?` (asserts non-empty; optionally checks `idn_substr`), reads `output?`, and — if `do_set: true` — sets V/I, briefly enables output, measures back, then disables. All values are recorded as report properties. |
|
| `test_owon_psu_idn_and_measurements` | `hardware` | Read-only smoke against the **session-managed** PSU (opened by `tests/hardware/conftest.py`). Queries `*IDN?` (asserts non-empty; checks `idn_substr` if configured), `output?` (asserts ON — the session fixture parked it that way), and the parsed-numeric helpers `measure_voltage_v()` / `measure_current_a()`. Verifies measured voltage is within ±10% of `cfg.set_voltage`. |
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
- Useful as a pure-PSU bench check before running any LIN E2E test.
|
- Does **not** toggle the output — that would brown out the ECU and break every test that follows in the same session. The toggle path is exercised once at session start by the conftest fixture.
|
||||||
- Settings can live in `config/test_config.yaml` (central) or `config/owon_psu.yaml` (per-machine override; the latter wins).
|
- Settings can live in `config/test_config.yaml` (central) or `config/owon_psu.yaml` (per-machine override; the latter wins).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 7. Hardware – Voltage tolerance (PSU + LIN)
|
||||||
|
|
||||||
|
### 7.1 `test_overvolt.py`
|
||||||
|
|
||||||
|
Source: [tests/hardware/test_overvolt.py](tests/hardware/test_overvolt.py)
|
||||||
|
|
||||||
|
Drives the bench supply through known thresholds and observes
|
||||||
|
`ALM_Status.ALMVoltageStatus` on the LIN bus. All cases use the
|
||||||
|
SETUP / PROCEDURE / ASSERT / TEARDOWN four-phase pattern with a
|
||||||
|
`try`/`finally` that restores nominal voltage. The session-scoped
|
||||||
|
PSU stays open across every case; voltage is perturbed but output
|
||||||
|
is never toggled.
|
||||||
|
|
||||||
|
**Pattern (settle then validate).** Each PROCEDURE goes through
|
||||||
|
`apply_voltage_and_settle()` from `psu_helpers`: set the target,
|
||||||
|
**poll the PSU meter** until the rail is actually there, then hold
|
||||||
|
for `ECU_VALIDATION_TIME_S` so the firmware can detect and republish
|
||||||
|
status. After that, a single deterministic read of
|
||||||
|
`ALMVoltageStatus` gives the answer — no polling-the-bus race. See
|
||||||
|
`docs/14_power_supply.md` for the full pattern reference.
|
||||||
|
|
||||||
|
| Test | Markers | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `test_template_overvoltage_status` | `hardware mum` | Confirm baseline `ALMVoltageStatus == Normal`, then `apply_voltage_and_settle(OVERVOLTAGE_V, ECU_VALIDATION_TIME_S)`, single read of status, assert `OverVoltage` (`0x02`). Restore nominal and verify recovery to `Normal`. |
|
||||||
|
| `test_template_undervoltage_status` | `hardware mum` | Symmetric: apply `UNDERVOLTAGE_V`, settle + validation hold, assert `UnderVoltage` (`0x01`), restore. Failure message hints when the slave browns out before tripping the UV flag. |
|
||||||
|
| `test_template_voltage_status_parametrized[nominal\|overvoltage\|undervoltage]` | `hardware mum` | One parametrized walk over `(voltage, expected_status, label)`. Each row runs SETUP/PROCEDURE/ASSERT/TEARDOWN independently via the autouse `_park_at_nominal` fixture. |
|
||||||
|
|
||||||
|
**Report properties recorded per case:**
|
||||||
|
- `psu_setpoint_v` — requested voltage
|
||||||
|
- `psu_settled_s` — measured PSU slew time (bench-dependent)
|
||||||
|
- `psu_final_v` — last measured voltage
|
||||||
|
- `validation_time_s` — firmware-side hold (`ECU_VALIDATION_TIME_S`)
|
||||||
|
- `voltage_status_after` — single status read used for the assertion
|
||||||
|
- `voltage_trace` — downsampled `(elapsed_s, v)` trace from the settle phase
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- **Tune the constants at the top of the file** to your firmware spec: `NOMINAL_VOLTAGE`, `OVERVOLTAGE_V`, `UNDERVOLTAGE_V`, `ECU_VALIDATION_TIME_S`.
|
||||||
|
- The autouse `_park_at_nominal` fixture also uses `apply_voltage_and_settle`, so the rail is *measurably* back at nominal before AND after every test — voltage cannot leak between cases.
|
||||||
|
- `cfg.power_supply.do_set` is no longer required (the session fixture owns the PSU lifecycle); `enabled: true` and a reachable port are sufficient.
|
||||||
|
|
||||||
|
### 7.2 `test_psu_voltage_settling.py` *(opt-in: `-m psu_settling`)*
|
||||||
|
|
||||||
|
Source: [tests/hardware/test_psu_voltage_settling.py](tests/hardware/test_psu_voltage_settling.py)
|
||||||
|
|
||||||
|
Characterization test — extracts how long the bench Owon PSU takes
|
||||||
|
to actually deliver a new voltage at its terminals after a setpoint
|
||||||
|
change. Other voltage-tolerance tests use the result to budget their
|
||||||
|
detect timeouts. Marked `psu_settling` + `slow` so it stays out of
|
||||||
|
default `-m hardware` runs unless explicitly selected.
|
||||||
|
|
||||||
|
| Test | Markers | Purpose |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `test_psu_voltage_settling_time[13_to_18_OV]` | `hardware psu_settling slow` | Park PSU at 13 V (un-timed), then `set_voltage(18)` and poll `measure_voltage_v()` every 50 ms until within ±0.10 V of target or 10 s timeout. Records `settling_time_s` and a downsampled voltage trace. |
|
||||||
|
| `test_psu_voltage_settling_time[18_to_13_back]` | same | The return path: 18 V → 13 V. Slewing down often differs from slewing up; both numbers are useful for budgeting. |
|
||||||
|
| `test_psu_voltage_settling_time[13_to_7_UV]` | same | Nominal → undervoltage. |
|
||||||
|
| `test_psu_voltage_settling_time[7_to_13_back]` | same | Undervoltage → nominal. |
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
|
||||||
|
- Run via `pytest -m psu_settling -s` to see the per-case timing in stdout.
|
||||||
|
- Per-case report properties: `settling_time_s`, `final_voltage_v`, `sample_count`, `voltage_trace` (downsampled to ~30 entries), plus the inputs (`start_voltage_v`, `target_voltage_v`, `voltage_tol_v`).
|
||||||
|
- Each case ends by restoring `NOMINAL_V` (13 V) so subsequent tests don't inherit a perturbed setpoint.
|
||||||
|
- Tune the four module-level constants (`VOLTAGE_TOL_V`, `POLL_INTERVAL_S`, `MAX_SETTLE_TIME_S`, `NOMINAL_V`) to your bench if defaults don't fit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Hardware-test infrastructure (not collected as tests)
|
||||||
|
|
||||||
|
These files support the suite but are not test bodies:
|
||||||
|
|
||||||
|
### 8.1 `tests/hardware/conftest.py`
|
||||||
|
|
||||||
|
Session-scoped fixtures:
|
||||||
|
|
||||||
|
- `_psu_or_none` — opens the Owon PSU once via `resolve_port()` (cross-platform), parks at `cfg.power_supply.set_voltage` / `set_current`, enables output. Yields `OwonPSU` or `None` (tolerant: never raises out of fixture).
|
||||||
|
- `_psu_powers_bench` — `autouse=True`. Realizes `_psu_or_none` so even tests that don't request `psu` by name benefit from the session power-up.
|
||||||
|
- `psu` — public; skips cleanly when the PSU isn't available.
|
||||||
|
|
||||||
|
Tests **must not** call `psu.set_output(False)` or `psu.close()` — the conftest owns the lifecycle. See `docs/14_power_supply.md` §5.
|
||||||
|
|
||||||
|
### 8.2 `tests/hardware/frame_io.py` — `FrameIO`
|
||||||
|
|
||||||
|
Generic LDF-driven I/O. Three layers (`send`/`receive`/`read_signal`, `pack`/`unpack`, `send_raw`/`receive_raw`) plus introspection (`frame_id`, `frame_length`). Reusable for any frame in any LDF — no ALM-specific knowledge.
|
||||||
|
|
||||||
|
### 8.3 `tests/hardware/alm_helpers.py` — `AlmTester` + constants
|
||||||
|
|
||||||
|
ALM_Node domain helpers built on `FrameIO`: `force_off`, `wait_for_state`, `measure_animating_window`, `read_led_state`, `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.
|
||||||
|
|
||||||
|
### 8.4 `tests/hardware/psu_helpers.py` — settle-then-validate primitives
|
||||||
|
|
||||||
|
Shared PSU helpers used by every test that changes the supply voltage:
|
||||||
|
|
||||||
|
- `wait_until_settled(psu, target_v, *, tol, interval, timeout)` — polls `psu.measure_voltage_v()` until within `tol` of `target_v`, returns `(elapsed_s, trace)` or `(None, trace)` on timeout.
|
||||||
|
- `apply_voltage_and_settle(psu, target_v, *, validation_time, ...)` — composite: issues the setpoint, calls `wait_until_settled`, then sleeps `validation_time` so the firmware-side observer can detect and republish. Returns `{settled_s, validation_s, final_v, trace}`. Raises `AssertionError` if the PSU can't reach the target.
|
||||||
|
- `downsample_trace(trace, max_samples=30)` — utility to keep poll traces in report properties readable.
|
||||||
|
|
||||||
|
Module-level defaults: `DEFAULT_VOLTAGE_TOL_V = 0.10`, `DEFAULT_POLL_INTERVAL_S = 0.05`, `DEFAULT_SETTLE_TIMEOUT_S = 10.0`, `DEFAULT_VALIDATION_TIME_S = 1.0`.
|
||||||
|
|
||||||
|
Used by `test_overvolt.py`, `test_psu_voltage_settling.py`, and the `_test_case_template_psu_lin.py` template.
|
||||||
|
|
||||||
|
### 8.5 Test starting points (leading underscore → not collected)
|
||||||
|
|
||||||
|
- `tests/hardware/_test_case_template.py` — three flavors (minimal / with isolation / single-signal probe) for ALM-touching MUM tests.
|
||||||
|
- `tests/hardware/_test_case_template_psu_lin.py` — three flavors (overvoltage / undervoltage / parametrized sweep) for tests that drive the PSU and observe the LIN bus.
|
||||||
|
|
||||||
|
Both contain pedagogical inline comments explaining fixture scopes, autouse, `yield`, the four-phase test pattern, and per-flavor when-to-use guidance. Copy to `test_<feature>.py` and edit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test naming conventions
|
## Test naming conventions
|
||||||
|
|
||||||
When adding new tests, follow these patterns so the catalog stays scannable:
|
When adding new tests, follow these patterns so the catalog stays scannable:
|
||||||
|
|||||||
422
docs/19_frame_io_and_alm_helpers.md
Normal file
422
docs/19_frame_io_and_alm_helpers.md
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
# Hardware Test Helpers — `FrameIO` and `AlmTester`
|
||||||
|
|
||||||
|
Hardware tests under `tests/hardware/` use two helper modules to keep test
|
||||||
|
bodies focused on intent rather than bus mechanics:
|
||||||
|
|
||||||
|
| Module | Scope | What it gives you |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [`tests/hardware/frame_io.py`](../tests/hardware/frame_io.py) | **Generic LDF I/O** | `FrameIO` class — send/receive any LDF-defined frame by name, plus pack/unpack and raw-bus escape hatches. Knows nothing about ALM. |
|
||||||
|
| [`tests/hardware/alm_helpers.py`](../tests/hardware/alm_helpers.py) | **ALM_Node domain** | `AlmTester` class + constants + pure utilities. Encodes the test patterns specific to the ALM_Req_A / ALM_Status / PWM_Frame / PWM_wo_Comp / Tj_Frame / ConfigFrame set. Built on `FrameIO`. |
|
||||||
|
|
||||||
|
The split lets the same `FrameIO` class be reused by future test suites for
|
||||||
|
other ECUs while keeping ALM-specific knowledge in one place.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Three layers of access
|
||||||
|
|
||||||
|
`FrameIO` exposes the same bus three ways. A test picks whichever layer
|
||||||
|
matches its intent.
|
||||||
|
|
||||||
|
### 1.1 High level — by frame and signal name
|
||||||
|
|
||||||
|
This is the default for almost every test. The LDF carries the frame ID,
|
||||||
|
length, and signal layout, so the test code never mentions any of those.
|
||||||
|
|
||||||
|
```python
|
||||||
|
fio.send(
|
||||||
|
"ALM_Req_A",
|
||||||
|
AmbLightColourRed=255, AmbLightColourGreen=0, AmbLightColourBlue=0,
|
||||||
|
AmbLightIntensity=255,
|
||||||
|
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=10,
|
||||||
|
AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
|
||||||
|
)
|
||||||
|
|
||||||
|
decoded = fio.receive("ALM_Status") # full dict of decoded signals
|
||||||
|
nad = fio.read_signal("ALM_Status", "ALMNadNo") # one signal
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Mid level — pack / unpack without I/O
|
||||||
|
|
||||||
|
Use this when you want to build a payload, inspect or modify it, and then
|
||||||
|
send it (often via the low-level path).
|
||||||
|
|
||||||
|
```python
|
||||||
|
data = bytearray(fio.pack("ALM_Req_A", AmbLightColourRed=255, ...))
|
||||||
|
data[7] |= 0x80 # tweak a bit by hand
|
||||||
|
fio.send_raw(fio.frame_id("ALM_Req_A"), bytes(data))
|
||||||
|
|
||||||
|
# Decode raw bytes you already have:
|
||||||
|
decoded = fio.unpack("PWM_Frame", b"\x12\x34..." )
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Low level — raw bus, bypass the LDF
|
||||||
|
|
||||||
|
For cases the LDF doesn't describe, or when you need full control.
|
||||||
|
|
||||||
|
```python
|
||||||
|
fio.send_raw(0x12, bytes([0x00] * 8))
|
||||||
|
rx = fio.receive_raw(0x11, timeout=0.5) # returns LinFrame | None
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 Introspection
|
||||||
|
|
||||||
|
```python
|
||||||
|
fio.frame_id("PWM_Frame") # 0x12
|
||||||
|
fio.frame_length("PWM_Frame") # 8
|
||||||
|
fio.frame("PWM_Frame") # raw ldfparser Frame object (cached)
|
||||||
|
fio.lin # underlying LinInterface
|
||||||
|
fio.ldf # LdfDatabase
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `FrameIO` API reference
|
||||||
|
|
||||||
|
```python
|
||||||
|
class FrameIO:
|
||||||
|
def __init__(self, lin: LinInterface, ldf): ...
|
||||||
|
|
||||||
|
# high level
|
||||||
|
def send(self, frame_name: str, **signals) -> None
|
||||||
|
def receive(self, frame_name: str, timeout: float = 1.0) -> dict | None
|
||||||
|
def read_signal(self, frame_name: str, signal_name: str, *,
|
||||||
|
timeout: float = 1.0, default=None) -> Any
|
||||||
|
|
||||||
|
# mid level
|
||||||
|
def pack(self, frame_name: str, **signals) -> bytes
|
||||||
|
def unpack(self, frame_name: str, data: bytes) -> dict
|
||||||
|
|
||||||
|
# low level
|
||||||
|
def send_raw(self, frame_id: int, data: bytes) -> None
|
||||||
|
def receive_raw(self, frame_id: int, timeout: float = 1.0) -> LinFrame | None
|
||||||
|
|
||||||
|
# introspection
|
||||||
|
def frame(self, name: str)
|
||||||
|
def frame_id(self, name: str) -> int
|
||||||
|
def frame_length(self, name: str) -> int
|
||||||
|
|
||||||
|
# injected refs
|
||||||
|
@property
|
||||||
|
def lin(self) -> LinInterface
|
||||||
|
@property
|
||||||
|
def ldf(self)
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `send()` / `pack()` require **every** signal in the frame; ldfparser
|
||||||
|
raises if one is missing. Use `receive()` first if you want to merge a
|
||||||
|
change into the current state.
|
||||||
|
- `receive()` returns `None` on timeout (rather than raising), so polling
|
||||||
|
loops stay simple.
|
||||||
|
- All frame lookups are cached per `FrameIO` instance — repeated calls to
|
||||||
|
`send`/`receive`/`frame` for the same name don't re-walk the LDF.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. `AlmTester` API reference
|
||||||
|
|
||||||
|
`AlmTester` bundles a `FrameIO` and a NAD, and exposes ALM-specific test
|
||||||
|
patterns. Build it once in a fixture and pass it into tests.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AlmTester:
|
||||||
|
def __init__(self, fio: FrameIO, nad: int): ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fio(self) -> FrameIO # the underlying FrameIO
|
||||||
|
@property
|
||||||
|
def nad(self) -> int # bound node NAD
|
||||||
|
|
||||||
|
# ALM_Status polling
|
||||||
|
def read_led_state(self, timeout: float = STATE_RECEIVE_TIMEOUT) -> int
|
||||||
|
def wait_for_state(self, target: int, timeout: float
|
||||||
|
) -> tuple[bool, float, list[int]]
|
||||||
|
def measure_animating_window(self, max_wait: float
|
||||||
|
) -> tuple[float | None, list[int]]
|
||||||
|
|
||||||
|
# LED control
|
||||||
|
def force_off(self) -> None # drives mode=0, intensity=0; sleeps to settle
|
||||||
|
|
||||||
|
# PWM assertions (use rgb_to_pwm.compute_pwm() under the hood)
|
||||||
|
def assert_pwm_matches_rgb(self, rp, r, g, b, *, label: str = "") -> None
|
||||||
|
def assert_pwm_wo_comp_matches_rgb(self, rp, r, g, b, *, label: str = "") -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
The `assert_pwm_*` helpers:
|
||||||
|
|
||||||
|
- Read `Tj_Frame_NTC` (Kelvin), convert to °C, and pass it to `compute_pwm`
|
||||||
|
so temperature compensation matches what the ECU is applying.
|
||||||
|
- Sleep `PWM_SETTLE_SECONDS` (10 LIN frame periods) before reading PWM
|
||||||
|
frames so the slave's TX buffer has time to refresh.
|
||||||
|
- Record both expected and actual values as report properties via the
|
||||||
|
`rp(...)` helper from `tests/conftest.py`. The optional `label`
|
||||||
|
parameter lets you append a suffix when you assert PWM more than once
|
||||||
|
in the same test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Constants and utilities (in `alm_helpers`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ALMLEDState (from LDF Signal_encoding_types: LED_State)
|
||||||
|
LED_STATE_OFF = 0
|
||||||
|
LED_STATE_ANIMATING = 1
|
||||||
|
LED_STATE_ON = 2
|
||||||
|
|
||||||
|
# Test pacing — chosen against the 10 ms LIN frame periodicity
|
||||||
|
STATE_POLL_INTERVAL = 0.05 # 50 ms between polls (5 LIN periods)
|
||||||
|
STATE_RECEIVE_TIMEOUT = 0.2 # per-poll receive timeout
|
||||||
|
STATE_TIMEOUT_DEFAULT = 1.0 # default wait_for_state ceiling
|
||||||
|
PWM_SETTLE_SECONDS = 0.1 # let the slave refresh PWM_Frame TX buffer
|
||||||
|
DURATION_LSB_SECONDS = 0.2 # AmbLightDuration scale: 1 LSB = 200 ms
|
||||||
|
FORCE_OFF_SETTLE_SECONDS = 0.4 # pause after the OFF command
|
||||||
|
|
||||||
|
# PWM tolerances
|
||||||
|
KELVIN_TO_CELSIUS_OFFSET = 273.15
|
||||||
|
PWM_ABS_TOL = 3277 # ±5% of 16-bit full scale
|
||||||
|
PWM_REL_TOL = 0.05 # ±5% of expected, whichever is larger
|
||||||
|
|
||||||
|
# Pure utilities
|
||||||
|
def ntc_kelvin_to_celsius(ntc_raw: int) -> float
|
||||||
|
def pwm_within_tol(actual: int, expected: int) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Fixture wiring
|
||||||
|
|
||||||
|
`tests/hardware/test_mum_alm_animation.py` defines two module-scoped
|
||||||
|
fixtures plus an autouse reset. The same pattern applies to any new
|
||||||
|
hardware test file targeting MUM.
|
||||||
|
|
||||||
|
```python
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def fio(config: EcuTestConfig, lin: LinInterface, ldf) -> FrameIO:
|
||||||
|
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:
|
||||||
|
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 OFF before and after each test so state doesn't leak."""
|
||||||
|
alm.force_off()
|
||||||
|
yield
|
||||||
|
alm.force_off()
|
||||||
|
```
|
||||||
|
|
||||||
|
The `lin`, `ldf`, and `config` fixtures are provided globally by
|
||||||
|
`tests/conftest.py` — see [docs/02_configuration_resolution.md](02_configuration_resolution.md)
|
||||||
|
for how they are wired.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Cookbook
|
||||||
|
|
||||||
|
### Drive the LED to a color and verify both PWM frames
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_red_at_full(fio, alm, rp):
|
||||||
|
r, g, b = 255, 0, 0
|
||||||
|
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, _, history = alm.wait_for_state(LED_STATE_ON, timeout=1.0)
|
||||||
|
assert reached, history
|
||||||
|
alm.assert_pwm_matches_rgb(rp, r, g, b)
|
||||||
|
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toggle a single ConfigFrame bit and restore it
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_with_compensation_off(fio, alm, rp):
|
||||||
|
try:
|
||||||
|
fio.send("ConfigFrame",
|
||||||
|
ConfigFrame_Calibration=0,
|
||||||
|
ConfigFrame_EnableDerating=1,
|
||||||
|
ConfigFrame_EnableCompensation=0,
|
||||||
|
ConfigFrame_MaxLM=3840)
|
||||||
|
time.sleep(0.2)
|
||||||
|
# ... drive the LED, observe non-compensated PWM ...
|
||||||
|
finally:
|
||||||
|
fio.send("ConfigFrame",
|
||||||
|
ConfigFrame_Calibration=0,
|
||||||
|
ConfigFrame_EnableDerating=1,
|
||||||
|
ConfigFrame_EnableCompensation=1,
|
||||||
|
ConfigFrame_MaxLM=3840)
|
||||||
|
time.sleep(0.2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read one signal periodically
|
||||||
|
|
||||||
|
```python
|
||||||
|
nad = fio.read_signal("ALM_Status", "ALMNadNo", timeout=0.5, default=None)
|
||||||
|
if nad is None:
|
||||||
|
pytest.skip("ECU silent")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build a malformed payload and send it raw
|
||||||
|
|
||||||
|
```python
|
||||||
|
data = bytearray(fio.pack("ALM_Req_A",
|
||||||
|
AmbLightColourRed=0, AmbLightColourGreen=0,
|
||||||
|
AmbLightColourBlue=0, AmbLightIntensity=0,
|
||||||
|
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
|
||||||
|
AmbLightLIDFrom=0, AmbLightLIDTo=0))
|
||||||
|
data[2] = 0xFF # corrupt one byte
|
||||||
|
fio.send_raw(fio.frame_id("ALM_Req_A"), bytes(data))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Writing a new test
|
||||||
|
|
||||||
|
### 7.1 Starting point
|
||||||
|
|
||||||
|
A heavily-annotated, copyable template lives at
|
||||||
|
[`tests/hardware/_test_case_template.py`](../tests/hardware/_test_case_template.py).
|
||||||
|
The leading underscore stops pytest from collecting it, so the example
|
||||||
|
bodies don't run on the bench.
|
||||||
|
|
||||||
|
Copy it to a new file named `test_<feature>.py` under `tests/hardware/`
|
||||||
|
and edit. The template includes:
|
||||||
|
|
||||||
|
- The standard imports for `frame_io` and `alm_helpers`
|
||||||
|
- The three module-level fixtures (`fio`, `alm`, `_reset_to_off`) with
|
||||||
|
inline explanations of fixture scope, `autouse`, and `yield`
|
||||||
|
- Three skeleton bodies (one per common shape — see §7.3)
|
||||||
|
- An appendix listing the most-reached-for patterns
|
||||||
|
|
||||||
|
### 7.2 The four-phase test pattern
|
||||||
|
|
||||||
|
Every hardware test that mutates ECU state beyond just the LED should
|
||||||
|
follow a **SETUP / PROCEDURE / ASSERT / TEARDOWN** structure with a
|
||||||
|
`try`/`finally` so the teardown runs even when an assertion fails.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_xyz(fio, alm, rp):
|
||||||
|
"""..."""
|
||||||
|
# ── SETUP ──────────────────────────────────────
|
||||||
|
# Bring the ECU to the exact state THIS test needs, beyond what the
|
||||||
|
# autouse reset already gave us. Anything you change here MUST be
|
||||||
|
# undone in TEARDOWN below.
|
||||||
|
fio.send("ConfigFrame", ConfigFrame_EnableCompensation=0, ...)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ── PROCEDURE ──────────────────────────────
|
||||||
|
# The actions whose effects you are validating.
|
||||||
|
fio.send("ALM_Req_A", ...)
|
||||||
|
reached, _, history = alm.wait_for_state(LED_STATE_ON, timeout=1.0)
|
||||||
|
|
||||||
|
# ── ASSERT ─────────────────────────────────
|
||||||
|
# Bus-observable expectations. Use `rp("key", value)` to attach
|
||||||
|
# diagnostics to the report, then assert.
|
||||||
|
rp("led_state_history", history)
|
||||||
|
assert reached, history
|
||||||
|
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# ── TEARDOWN ───────────────────────────────
|
||||||
|
# Always runs. Restores anything SETUP perturbed.
|
||||||
|
fio.send("ConfigFrame", ConfigFrame_EnableCompensation=1, ...)
|
||||||
|
time.sleep(0.2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why this gives you test independence
|
||||||
|
|
||||||
|
Pytest runs tests in a deterministic order (the order they appear in the
|
||||||
|
file). Without strict teardown, a failure midway through one test can
|
||||||
|
leave the ECU in a non-default state that breaks every subsequent test
|
||||||
|
— turning a single bug into a cascade. The four-phase pattern prevents
|
||||||
|
that with two layers:
|
||||||
|
|
||||||
|
| Layer | What it covers | Where it lives |
|
||||||
|
|---|---|---|
|
||||||
|
| Common baseline | LED → OFF | autouse `_reset_to_off` fixture |
|
||||||
|
| Per-test specifics | ConfigFrame, schedules, mode flags, anything else | the test's own `try`/`finally` |
|
||||||
|
|
||||||
|
The autouse fixture handles the universal baseline so individual tests
|
||||||
|
don't have to think about it; the per-test `try`/`finally` handles
|
||||||
|
whatever that specific test mutated.
|
||||||
|
|
||||||
|
### When you can skip the four phases
|
||||||
|
|
||||||
|
If your test only sends a frame and observes the LED state (i.e. the
|
||||||
|
*only* mutable state involved is something the autouse reset already
|
||||||
|
restores), the explicit SETUP/TEARDOWN sections are dead weight — just
|
||||||
|
write the procedure straight through. Flavor A in the template
|
||||||
|
illustrates this minimal shape.
|
||||||
|
|
||||||
|
### 7.3 Three flavors in the template
|
||||||
|
|
||||||
|
| Flavor | When to use it |
|
||||||
|
|---|---|
|
||||||
|
| **A — minimal** | Test only drives the LED and asserts on PWM/state. The autouse reset is enough. |
|
||||||
|
| **B — with isolation** | Test changes any persistent ECU state (ConfigFrame, schedules, NAD, …). Use the `try`/`finally` pattern. |
|
||||||
|
| **C — single-signal probe** | "Ask the ECU one thing and check the answer." Uses `fio.read_signal(...)`, no state mutation. |
|
||||||
|
|
||||||
|
Pick the closest one, delete the others, rename the function and fill
|
||||||
|
in the docstring.
|
||||||
|
|
||||||
|
### 7.4 Tests that drive the PSU and observe the LIN bus
|
||||||
|
|
||||||
|
For *combined* PSU + LIN scenarios (overvoltage / undervoltage
|
||||||
|
tolerance, brown-out behaviour, supply transients) there is a
|
||||||
|
dedicated template at
|
||||||
|
[`tests/hardware/_test_case_template_psu_lin.py`](../tests/hardware/_test_case_template_psu_lin.py).
|
||||||
|
It adds a `psu` fixture (cross-platform port resolution + safe-off
|
||||||
|
on close), an autouse `_park_at_nominal` fixture, a
|
||||||
|
`wait_for_voltage_status` polling helper, and three flavors:
|
||||||
|
|
||||||
|
| Flavor | Demonstrates |
|
||||||
|
|---|---|
|
||||||
|
| A — overvoltage | Drive PSU above the OV threshold, expect `ALMVoltageStatus = 0x02`, restore. |
|
||||||
|
| B — undervoltage | Symmetric for UV (`0x01`). |
|
||||||
|
| C — sweep | Parametrized walk over `(V, expected_status)` tuples. |
|
||||||
|
|
||||||
|
For the *settling time* characterization that feeds these tests'
|
||||||
|
detect timeouts, see `tests/hardware/test_psu_voltage_settling.py`
|
||||||
|
(opt-in via `pytest -m psu_settling`).
|
||||||
|
|
||||||
|
See [`docs/14_power_supply.md` §6](14_power_supply.md#6-run-the-hardware-test) and [§5 (session-managed power)](14_power_supply.md#5-session-managed-power-the-bench-powers-the-ecu-through-the-psu)
|
||||||
|
for the full reference and the constants to tune for your firmware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Related docs
|
||||||
|
|
||||||
|
- [`04_lin_interface_call_flow.md`](04_lin_interface_call_flow.md) — what
|
||||||
|
`LinInterface.send`/`receive` does under the hood for each adapter.
|
||||||
|
- [`16_mum_internals.md`](16_mum_internals.md) — MUM-specific behaviour
|
||||||
|
the helpers rely on (master-driven receive, frame-length map, …).
|
||||||
|
- [`17_ldf_parser.md`](17_ldf_parser.md) — how the LDF is loaded and how
|
||||||
|
`pack` / `unpack` are implemented.
|
||||||
|
- [`13_unit_testing_guide.md`](13_unit_testing_guide.md) — unit-test
|
||||||
|
conventions, markers, coverage.
|
||||||
|
- [`15_report_properties_cheatsheet.md`](15_report_properties_cheatsheet.md)
|
||||||
|
— the standard `rp("key", value)` keys these helpers emit.
|
||||||
@ -21,6 +21,7 @@ A guided tour of the ECU testing framework. Start here:
|
|||||||
17. `13_unit_testing_guide.md` — Unit tests layout, markers, coverage, and tips
|
17. `13_unit_testing_guide.md` — Unit tests layout, markers, coverage, and tips
|
||||||
18. `14_power_supply.md` — Owon PSU control, configuration, tests, and quick demo script
|
18. `14_power_supply.md` — Owon PSU control, configuration, tests, and quick demo script
|
||||||
19. `15_report_properties_cheatsheet.md` — Standardized keys for record_property/rp across suites
|
19. `15_report_properties_cheatsheet.md` — Standardized keys for record_property/rp across suites
|
||||||
|
20. `19_frame_io_and_alm_helpers.md` — Hardware-test helpers: `FrameIO` (generic LDF I/O) and `AlmTester` (ALM_Node domain), plus the `tests/hardware/_test_case_template.py` starting point
|
||||||
|
|
||||||
Related references:
|
Related references:
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user