Add MUM support in the testing framework

This commit is contained in:
Hosam-Eldin Mostafa 2026-04-28 23:37:53 +02:00
parent 58aa7350e6
commit b8f52bea39
84 changed files with 8860 additions and 4851 deletions

View File

@ -1,11 +1,12 @@
# ECU Tests Framework
Python-based ECU testing framework built on pytest, with a pluggable LIN communication layer (Mock and 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, MUM, and legacy BabyLIN), configuration via YAML, and enhanced HTML/XML reporting with rich test metadata.
## Highlights
- **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
- Real BabyLIN adapter using the SDK's official Python wrapper (BabyLIN_library.py)
- BabyLIN adapter (legacy) using the vendor SDK's Python wrapper
- Hex flashing scaffold you can wire to UDS
- Rich pytest fixtures and example tests
- Self-contained HTML report with Title, Requirements, Steps, and Expected Results extracted from test docstrings
@ -15,8 +16,9 @@ Python-based ECU testing framework built on pytest, with a pluggable LIN communi
- Using the framework (common runs, markers, CI, Pi): `docs/12_using_the_framework.md`
- Plugin overview (reporting, hooks, artifacts): `docs/11_conftest_plugin_overview.md`
- Power supply (Owon) usage and troubleshooting: `docs/14_power_supply.md`
- Report properties cheatsheet (standard keys): `docs/15_report_properties_cheatsheet.md`
- Power supply (Owon) usage and troubleshooting: `docs/14_power_supply.md`
- Report properties cheatsheet (standard keys): `docs/15_report_properties_cheatsheet.md`
- MUM source scripts (vendor reference): [vendor/automated_lin_test/README.md](vendor/automated_lin_test/README.md)
## TL;DR quick start (copy/paste)
@ -26,7 +28,15 @@ Mock (no hardware):
python -m venv .venv; .\.venv\Scripts\Activate.ps1; pip install -r requirements.txt; pytest -m "not hardware" -v
```
Hardware (BabyLIN SDK):
Hardware via MUM (current default):
```powershell
# 1. Install Melexis 'pylin' and 'pymumclient' (see vendor/automated_lin_test/install_packages.sh)
# 2. Make sure the MUM is reachable (default IP 192.168.7.2)
$env:ECU_TESTS_CONFIG = ".\config\mum.example.yaml"; pytest -m "hardware and mum" -v
```
Hardware via BabyLIN (legacy):
```powershell
# Place BabyLIN_library.py and native libs under .\vendor per vendor/README.md first
@ -97,17 +107,48 @@ Default config is `config/test_config.yaml`. Override via the `ECU_TESTS_CONFIG`
$env:ECU_TESTS_CONFIG = (Resolve-Path .\config\test_config.yaml)
```
BabyLIN configuration template: `config/babylin.example.yaml`
### MUM configuration (default for hardware)
Template: `config/mum.example.yaml`
```yaml
interface:
type: babylin # or "mock"
type: mum
host: 192.168.7.2 # MUM IP (USB-RNDIS default)
lin_device: lin0 # MUM LIN device name
power_device: power_out0 # MUM power-control device (built-in PSU)
bitrate: 19200 # LIN baudrate
boot_settle_seconds: 0.5 # Wait after power-up before sending the first frame
frame_lengths:
0x0A: 8 # ALM_Req_A
0x11: 4 # ALM_Status
```
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-
voltage scenarios but is independent of the LIN interface.
### BabyLIN configuration (legacy)
Template: `config/babylin.example.yaml`
```yaml
interface:
type: babylin # or "mock", or "mum"
channel: 0 # Channel index used by the SDK wrapper
bitrate: 19200 # Usually determined by SDF
sdf_path: ./vendor/Example.sdf
schedule_nr: 0 # Start this schedule on connect
schedule_nr: 0 # Start this schedule on connect (-1 to skip)
```
### LIN adapter capabilities
| Adapter | Power control | Diagnostic frames (Classic checksum) | Passive listen |
| --- | --- | --- | --- |
| `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 |
| `babylin` | external (Owon PSU) | via SDF / `BLC_sendCommand` | yes (frame queue) |
Switch to hardware profile and run only hardware tests:
```powershell

29
config/mum.example.yaml Normal file
View File

@ -0,0 +1,29 @@
# MUM (Melexis Universal Master) interface example.
# Copy to test_config.yaml or point ECU_TESTS_CONFIG at this file.
#
# Prerequisites:
# - MUM is reachable over IP (default 192.168.7.2 over USB-RNDIS).
# - Melexis Python packages 'pylin' and 'pymumclient' are importable.
# See vendor/automated_lin_test/install_packages.sh.
interface:
type: mum
host: 192.168.7.2 # MUM IP address
lin_device: lin0 # MUM LIN device name
power_device: power_out0 # MUM power-control device
bitrate: 19200 # LIN baudrate
boot_settle_seconds: 0.5 # Delay after power-up before first frame
# Optional: per-frame-id data lengths. Defaults cover the 4SEVEN library
# (ALM_Status=4, ALM_Req_A=8, etc.) — only override if your ECU differs.
frame_lengths:
0x0A: 8 # ALM_Req_A
0x11: 4 # ALM_Status
flash:
enabled: false
hex_path:
# The Owon PSU is unused on the MUM flow (MUM provides power on power_out0).
# Leave disabled unless you also want to drive the Owon for a separate test.
power_supply:
enabled: false

View File

@ -14,5 +14,5 @@ dsrdtr: false
# Optional assertions/behavior
idn_substr: OWON # require this substring in *IDN?
do_set: true # briefly set V/I and toggle output
set_voltage: 10.0 # volts when do_set is true
set_current: 0.1 # amps when do_set is true
set_voltage: 13.0 # volts when do_set is true
set_current: 1.0 # amps when do_set is true (raise above ECU draw to stay in CV mode)

View File

@ -1,18 +1,34 @@
interface:
type: mock
channel: 1
bitrate: 19200
# MUM (Melexis Universal Master) is the current default. Switch type to
# 'babylin' for the legacy SDK flow, or 'mock' for hardware-free runs.
type: mum
host: 192.168.7.2 # MUM IP (USB-RNDIS default)
lin_device: lin0 # MUM LIN device name
power_device: power_out0 # MUM power-control device (built-in PSU)
bitrate: 19200 # LIN baudrate
boot_settle_seconds: 0.5 # Wait after power-up before sending the first frame
frame_lengths:
0x0A: 8 # ALM_Req_A (master-published, RGB control)
0x11: 4 # ALM_Status (slave-published)
# --- BabyLIN (legacy) settings, used only when type: babylin ---
channel: 0
node_name: ECU_TEST_NODE
sdf_path: .\vendor\4SEVEN_color_lib_test.sdf
schedule_nr: -1 # -1 = don't auto-start a schedule
flash:
enabled: false
hex_path:
# Optional: central power supply config used by hardware tests/demos
# You can also place machine-specific values in config/owon_psu.yaml or set OWON_PSU_CONFIG
# Owon PSU is independent of the LIN interface. The MUM provides its own
# power on power_out0, so leave the PSU disabled unless you specifically
# need to drive an external supply for over/under-voltage scenarios.
power_supply:
enabled: true
enabled: false
# port: COM4
baudrate: 115200
timeout: 1.0
timeout: 2.0
eol: "\n"
parity: N
stopbits: 1
@ -21,5 +37,5 @@ power_supply:
dsrdtr: false
# idn_substr: OWON
do_set: false
set_voltage: 1.0
set_current: 0.1
set_voltage: 13.0
set_current: 1.0

View File

@ -9,7 +9,7 @@ This document walks through the exact order of operations when you run the frame
3. Test discovery collects tests under `tests/`
4. Session fixtures run:
- `config()` loads YAML configuration
- `lin()` selects and connects the LIN interface (Mock or BabyLin)
- `lin()` selects and connects the LIN interface (Mock, MUM, or legacy BabyLIN)
- `flash_ecu()` optionally flashes the ECU (if enabled)
5. Tests execute using fixtures and call interface methods
6. Our plugin extracts test metadata (Title, Requirements, Steps) from docstrings
@ -28,7 +28,7 @@ sequenceDiagram
participant F as Fixtures (conftest.py)
participant C as Config Loader (ecu_framework/config.py)
participant PS as Power Supply (optional)
participant L as LIN Adapter (mock/BabyLIN SDK)
participant L as LIN Adapter (mock/MUM/BabyLIN)
participant X as HexFlasher (optional)
participant R as Reports (HTML/JUnit)
@ -39,7 +39,7 @@ sequenceDiagram
P->>F: Init session fixtures
F->>C: load_config(workspace_root)
C-->>F: EcuTestConfig (merged dataclasses)
F->>L: Create interface (mock or BabyLIN SDK)
F->>L: Create interface (mock, MUM, or BabyLIN SDK)
L-->>F: Instance ready
F->>L: connect()
alt flash.enabled and hex_path provided
@ -77,8 +77,10 @@ Session fixture: config()
Session fixture: lin(config)
→ chooses interface by config.interface.type
- mock → ecu_framework.lin.mock.MockBabyLinInterface(...)
- babylin → ecu_framework.lin.babylin.BabyLinInterface(...)
- mum → ecu_framework.lin.mum.MumLinInterface(host, lin_device, power_device, ...)
- babylin → ecu_framework.lin.babylin.BabyLinInterface(...) [legacy]
→ lin.connect()
- MUM connect() also powers up the ECU via power_out0 and waits boot_settle_seconds
Optional session fixture: flash_ecu(config, lin)
→ if config.flash.enabled and hex_path set
@ -112,13 +114,16 @@ Reports written
- `ecu_framework/config.py`: loads and merges configuration into dataclasses
- `ecu_framework/lin/base.py`: abstract LIN interface contract and frame shape
- `ecu_framework/lin/mock.py`: mock behavior for send/receive/request
- `ecu_framework/lin/babylin.py`: BabyLIN SDK wrapper adapter (real hardware via BabyLIN_library.py)
- `ecu_framework/lin/mum.py`: MUM adapter (Melexis Universal Master via pylin + pymumclient)
- `ecu_framework/lin/babylin.py`: BabyLIN SDK wrapper adapter (legacy real hardware via BabyLIN_library.py)
- `ecu_framework/flashing/hex_flasher.py`: placeholder flashing logic
- `conftest_plugin.py`: report customization and metadata extraction
## Edge cases and behavior
- If `interface.type` is `babylin` but the SDK wrapper or libraries cannot be loaded, hardware tests are skipped
- If `interface.type` is `mum` but `pylin` / `pymumclient` aren't importable, or `interface.host` is unset, hardware tests are skipped with a clear message
- If `flash.enabled` is true but `hex_path` is missing, flashing fixture skips
- Timeouts are honored in `receive()` and `request()` implementations
- Invalid frame IDs (outside 0x000x3F) or data > 8 bytes will raise in `LinFrame`
- MUM `receive()` is master-driven: it requires a frame ID; `receive(id=None)` raises NotImplementedError. Diagnostic frames needing LIN 1.x Classic checksum should use `MumLinInterface.send_raw()`.

View File

@ -15,13 +15,18 @@ From highest to lowest precedence:
- `EcuTestConfig`
- `interface: InterfaceConfig`
- `type`: `mock` or `babylin`
- `channel`: LIN channel index (0-based in SDK wrapper)
- `bitrate`: LIN bitrate (e.g., 19200); usually defined by SDF
- `sdf_path`: Path to SDF file (hardware; required for typical operation)
- `schedule_nr`: Schedule number to start on connect (hardware)
- `type`: `mock`, `mum`, or `babylin`
- `channel`: LIN channel index (0-based in SDK wrapper) — BabyLIN-specific
- `bitrate`: LIN baudrate (e.g., 19200). The MUM uses this directly; BabyLIN typically takes it from the SDF
- `sdf_path`: Path to SDF file (BabyLIN; required for typical operation)
- `schedule_nr`: Schedule number to start on connect (BabyLIN). `-1` = skip
- `node_name`: Optional node identifier (informational)
- `dll_path`, `func_names`: Legacy fields from the old ctypes adapter; not used with the SDK wrapper
- `host`: MUM IP address (MUM-only). Required when `type: mum`
- `lin_device`: MUM LIN device name (MUM-only, default `lin0`)
- `power_device`: MUM power-control device (MUM-only, default `power_out0`)
- `boot_settle_seconds`: Delay after MUM power-up before sending the first frame (default 0.5)
- `frame_lengths`: Optional `{frame_id: data_length}` map for the MUM adapter to drive slave-published reads. Hex keys like `0x0A` are supported in YAML
- `flash: FlashConfig`
- `enabled`: whether to flash before tests
- `hex_path`: path to HEX file
@ -48,6 +53,23 @@ flash:
enabled: false
```
Hardware via MUM (current default) — see also `config/mum.example.yaml`:
```yaml
interface:
type: mum
host: 192.168.7.2 # MUM IP address (USB-RNDIS default)
lin_device: lin0 # MUM LIN device name
power_device: power_out0 # MUM power-control device
bitrate: 19200 # LIN baudrate
boot_settle_seconds: 0.5 # Delay after power-up before first frame
frame_lengths:
0x0A: 8 # ALM_Req_A
0x11: 4 # ALM_Status
flash:
enabled: false
```
Hardware (BabyLIN SDK wrapper) configuration:
```yaml
@ -105,9 +127,10 @@ central defaults in `config/test_config.yaml`.
## How tests and adapters consume config
- `lin` fixture picks `mock` or `babylin` based on `interface.type`
- `lin` fixture picks `mock`, `mum`, or `babylin` based on `interface.type`
- Mock adapter uses `bitrate` and `channel` to simulate timing/behavior
- BabyLIN adapter (SDK wrapper) uses `sdf_path`, `schedule_nr`, `channel` to open the device, load the SDF, and start a schedule. `bitrate` is informational unless explicitly applied via commands/SDF.
- MUM adapter uses `host`, `lin_device`, `power_device`, `bitrate`, `boot_settle_seconds`, and `frame_lengths` to open the MUM, set up the LIN bus, and power up the ECU on connect
- BabyLIN adapter (SDK wrapper) uses `sdf_path`, `schedule_nr`, `channel` to open the device, load the SDF, and start a schedule. `bitrate` is informational unless explicitly applied via commands/SDF
- `flash_ecu` uses `flash.enabled` and `flash.hex_path`
- PSU-related tests or utilities read `config.power_supply` for serial parameters
and optional actions (IDN assertions, on/off toggle, set/measure). The reference

View File

@ -1,6 +1,6 @@
# LIN Interface Call Flow
This document explains how LIN operations flow through the abstraction for both Mock and BabyLin adapters.
This document explains how LIN operations flow through the abstraction for the Mock, MUM, and legacy BabyLIN adapters.
## Contract (base)
@ -30,6 +30,37 @@ Use cases:
- Fast local dev, deterministic responses, no hardware
- Timeout and boundary behavior validation
## MUM adapter flow (Melexis Universal Master)
File: `ecu_framework/lin/mum.py`
The MUM is a networked LIN master (default IP `192.168.7.2`) with built-in
power control on `power_out0`. It is **master-driven**: there is no passive
listen — to read a slave-published frame, the master triggers a header on
that frame ID. Diagnostic frames (BSM-SNPD, service ID 0xB5) require LIN 1.x
**Classic** checksum and are sent through the transport layer's
`ld_put_raw`, not the regular `send_message`.
- `connect()`: lazy-imports `pymumclient` + `pylin`; opens MUM
(`MelexisUniversalMaster.open_all(host)`), gets the LIN device
(`linmaster`) and power device (`power_control`), runs `linmaster.setup()`,
builds `LinBusManager` + `LinDevice22`, sets `lin_dev.baudrate`, fetches
the transport layer (`get_device("bus/transport_layer")`), and finally
`power_control.power_up()` followed by a `boot_settle_seconds` sleep
- `send(frame)`: `lin_dev.send_message(master_to_slave=True, frame_id, data_length, data)`
- `receive(id, timeout)`: `lin_dev.send_message(master_to_slave=False, frame_id=id, data_length=frame_lengths.get(id, default_data_length))`
— pylin returns the response bytes (or raises on timeout, which we treat as `None`).
`id=None` raises `NotImplementedError` because the MUM cannot listen passively.
- `disconnect()`: best-effort `power_control.power_down()` followed by `linmaster.teardown()`
- MUM-only extras: `send_raw(bytes)` (Classic checksum via `ld_put_raw`),
`power_up()`, `power_down()`, `power_cycle(wait)`
Configuration:
- `interface.host` is required; `interface.lin_device` and `interface.power_device` default to MUM conventions
- `interface.bitrate` is the actual LIN baudrate the MUM drives
- `interface.frame_lengths` lets you map slave frame IDs to their fixed data lengths so `receive(id)` can fetch the correct number of bytes; built-in defaults cover ALM_Status (4) and ALM_Req_A (8)
## BabyLIN adapter flow (SDK wrapper)
File: `ecu_framework/lin/babylin.py`

View File

@ -9,7 +9,8 @@ This document provides a high-level view of the frameworks components and how
- Config Loader — `ecu_framework/config.py` (YAML → dataclasses)
- LIN Abstraction — `ecu_framework/lin/base.py` (`LinInterface`, `LinFrame`)
- Mock LIN Adapter — `ecu_framework/lin/mock.py`
- BabyLIN Adapter — `ecu_framework/lin/babylin.py` (SDK wrapper → BabyLIN_library.py)
- MUM LIN Adapter — `ecu_framework/lin/mum.py` (Melexis Universal Master via `pylin` + `pymumclient`)
- BabyLIN Adapter — `ecu_framework/lin/babylin.py` (SDK wrapper → BabyLIN_library.py; legacy)
- Flasher — `ecu_framework/flashing/hex_flasher.py`
- Power Supply (PSU) control — `ecu_framework/power/owon_psu.py` (serial SCPI)
- PSU quick demo script — `vendor/Owon/owon_psu_quick_demo.py`
@ -30,6 +31,7 @@ flowchart TB
CFG[ecu_framework/config.py]
BASE[ecu_framework/lin/base.py]
MOCK[ecu_framework/lin/mock.py]
MUM[ecu_framework/lin/mum.py]
BABY[ecu_framework/lin/babylin.py]
FLASH[ecu_framework/flashing/hex_flasher.py]
POWER[ecu_framework/power/owon_psu.py]
@ -37,8 +39,9 @@ flowchart TB
subgraph Artifacts
REP[reports/report.html<br/>reports/junit.xml]
YAML[config/*.yaml<br/>babylin.example.yaml<br/>test_config.yaml]
YAML[config/*.yaml<br/>test_config.yaml<br/>mum.example.yaml<br/>babylin.example.yaml]
PSU_YAML[config/owon_psu.yaml<br/>OWON_PSU_CONFIG]
MELEXIS[Melexis pylin + pymumclient<br/>MUM @ 192.168.7.2]
SDK[vendor/BabyLIN_library.py<br/>platform-specific libs]
OWON[vendor/Owon/owon_psu_quick_demo.py]
end
@ -47,6 +50,7 @@ flowchart TB
CF --> CFG
CF --> BASE
CF --> MOCK
CF --> MUM
CF --> BABY
CF --> FLASH
T --> POWER
@ -54,6 +58,7 @@ flowchart TB
CFG --> YAML
CFG --> PSU_YAML
MUM --> MELEXIS
BABY --> SDK
T --> OWON
T --> REP

View File

@ -16,7 +16,7 @@ sequenceDiagram
participant P as pytest
participant F as flash_ecu fixture
participant H as HexFlasher
participant L as LinInterface (mock/babylin)
participant L as LinInterface (mock/mum/babylin)
participant E as ECU
P->>F: Evaluate flashing precondition

View File

@ -1,11 +1,15 @@
# Raspberry Pi Deployment Guide
This guide explains how to run the ECU testing framework on a Raspberry Pi (Debian/Raspberry Pi OS). It covers environment setup, optional BabyLin hardware integration, running tests headless, and installing as a systemd service.
This guide explains how to run the ECU testing framework on a Raspberry Pi (Debian/Raspberry Pi OS). It covers environment setup, hardware integration via MUM (recommended) or BabyLin (legacy), running tests headless, and installing as a systemd service.
> Note: If you plan to use BabyLin hardware on a Pi, verify vendor driver support for ARM Linux. If BabyLin provides only Windows DLLs, use the Mock interface on Pi or deploy a different hardware interface that supports Linux/ARM.
> Note: The MUM (Melexis Universal Master) is **networked**, so the Pi only
> needs IP reachability to the MUM (default `192.168.7.2`) — there are no
> Pi-side native libs to worry about. BabyLin needs ARM Linux native
> libraries; if those aren't available, use Mock or MUM on the Pi instead.
## 1) Choose your interface
- **MUM (recommended for hardware on Pi)**: `interface.type: mum`. Requires Melexis `pylin` + `pymumclient` (see `vendor/automated_lin_test/install_packages.sh`) and IP reachability to the MUM device.
- Mock (recommended for headless/dev on Pi): `interface.type: mock`
- BabyLIN (only if ARM/Linux support is available): `interface.type: babylin` and ensure the SDK's `BabyLIN_library.py` and corresponding Linux/ARM shared libraries are available under `vendor/` or on PYTHONPATH/LD_LIBRARY_PATH.
@ -54,7 +58,29 @@ Optionally point to another config file via env var:
export ECU_TESTS_CONFIG=$(pwd)/config/test_config.yaml
```
If using the MUM on the Pi, set:
```yaml
interface:
type: mum
host: 192.168.7.2 # adjust to your MUM IP
lin_device: lin0
power_device: power_out0
bitrate: 19200
boot_settle_seconds: 0.5
frame_lengths:
0x0A: 8
0x11: 4
```
Confirm reachability before running tests:
```bash
ping -c 2 192.168.7.2
```
If using BabyLIN on Linux/ARM with the SDK wrapper, set:
```yaml
interface:
type: babylin
@ -140,5 +166,6 @@ systemctl status ecu-tests.service
## 8) Tips
- Use the mock interface on Pi for quick smoke tests and documentation/report generation
- For full HIL, ensure vendor SDK supports Linux/ARM and provide a shared object (`.so`) and headers
- If only Windows is supported, run the hardware suite on a Windows host and use the Pi for lightweight tasks (archiving, reporting, quick checks)
- For full HIL on Pi, the **MUM is the easiest path** — it's IP-reachable so the Pi doesn't need vendor-specific native libraries, just the Melexis Python packages (`pylin`, `pymumclient`)
- For BabyLIN HIL, ensure vendor SDK supports Linux/ARM and provide a shared object (`.so`) and headers
- If only Windows is supported by your hardware path, run the hardware suite on a Windows host and use the Pi for lightweight tasks (archiving, reporting, quick checks)

View File

@ -2,7 +2,13 @@
This guide walks you through building your own Raspberry Pi OS image that already contains this framework, dependencies, config, and services. It uses the official pi-gen tool (used by Raspberry Pi OS) or the simpler pi-gen-lite alternatives.
> Important: BabyLin support on ARM/Linux depends on vendor SDKs. If no `.so` is provided for ARM, either use the Mock interface on the Pi, or keep hardware tests on Windows.
> Important: For full HIL on the Pi, the **MUM (Melexis Universal Master)** is
> the recommended hardware path — it's IP-reachable so the Pi only needs the
> Melexis Python packages (`pylin`, `pymumclient`), no native libraries. Bake
> those into the image's site-packages from the Melexis IDE bundle. BabyLin
> support on ARM/Linux depends on vendor SDKs; if no `.so` is provided for
> ARM, either use the Mock or MUM interface on the Pi, or keep BabyLIN
> hardware tests on Windows.
## Approach A: Using pi-gen (official)

View File

@ -6,7 +6,8 @@ This guide shows common ways to run the test framework: from fast local mock run
- Python 3.x and a virtual environment
- Dependencies installed (see `requirements.txt`)
- Optional: BabyLIN SDK files placed under `vendor/` as described in `vendor/README.md` when running hardware tests
- For MUM hardware: Melexis `pylin` and `pymumclient` Python packages on `PYTHONPATH` (see `vendor/automated_lin_test/install_packages.sh`) plus a reachable MUM (default IP `192.168.7.2`)
- For BabyLIN (legacy) hardware: SDK files placed under `vendor/` as described in `vendor/README.md`
## Configuring tests
@ -19,8 +20,11 @@ Example PowerShell:
# Use a mock-only config for fast local runs
$env:ECU_TESTS_CONFIG = ".\config\mock.yml"
# Use a hardware config with BabyLIN SDK wrapper
$env:ECU_TESTS_CONFIG = ".\config\hardware_babylin.yml"
# Use a hardware config with the MUM (current default)
$env:ECU_TESTS_CONFIG = ".\config\mum.example.yaml"
# Use a hardware config with the BabyLIN SDK wrapper (legacy)
$env:ECU_TESTS_CONFIG = ".\config\babylin.example.yaml"
```
Quick try with provided examples:
@ -30,8 +34,8 @@ Quick try with provided examples:
$env:ECU_TESTS_CONFIG = ".\config\examples.yaml"
# The 'active' section defaults to the mock profile; run non-hardware tests
pytest -m "not hardware" -v
# Edit 'active' to the babylin profile (or point to babylin.example.yaml) and run hardware tests
```
# Edit 'active' to the mum or babylin profile (or point to mum.example.yaml /
# babylin.example.yaml) and run hardware tests
```
## Running locally (mock interface)
@ -59,14 +63,34 @@ Open the HTML report on Windows:
start .\reports\report.html
```
## Running on hardware (BabyLIN SDK wrapper)
## Running on hardware (MUM — current default)
1) Install Melexis `pylin` and `pymumclient` (see `vendor/automated_lin_test/install_packages.sh` — on Windows, point `pip` at a wheel or extend `PYTHONPATH` to the Melexis IDE site-packages).
2) Make sure the MUM is reachable: `ping 192.168.7.2`.
3) Select a config that defines `interface.type: mum` plus `host`/`lin_device`/`power_device`.
```powershell
$env:ECU_TESTS_CONFIG = ".\config\mum.example.yaml"
# Run only the MUM-marked hardware tests
pytest -m "hardware and mum" -v
# Run a single MUM test by file
pytest tests\hardware\test_e2e_mum_led_activate.py -q
```
Tips:
- The MUM owns ECU power on `power_out0`; it powers up automatically in `connect()` and powers down on `disconnect()`. The Owon PSU is independent and can be left disabled (`power_supply.enabled: false`).
- The MUM is master-driven: `lin.receive(id)` requires a frame ID. The default `frame_lengths` covers ALM_Status (4 B) and ALM_Req_A (8 B); add others in YAML when you need slave-published frames at non-standard lengths.
- For BSM-SNPD diagnostic frames (service ID 0xB5), use `lin.send_raw(bytes)` — it routes through the transport layer's `ld_put_raw`, which uses LIN 1.x **Classic** checksum. `send()` uses Enhanced and the firmware will reject these frames.
## Running on hardware (BabyLIN SDK wrapper — legacy)
1) Place SDK files per `vendor/README.md`.
2) Select a config that defines `interface.type: babylin`, `sdf_path`, and `schedule_nr`.
3) Markers allow restricting to hardware tests.
```powershell
# Example environment selection
$env:ECU_TESTS_CONFIG = ".\config\babylin.example.yaml"
# Run only hardware tests
@ -80,13 +104,17 @@ Tips:
- If multiple devices are attached, update your config to select the desired port (future enhancement) or keep only one connected.
- On timeout, tests often accept None to avoid flakiness; increase timeouts if your bus is slow.
- Master request behavior: the adapter prefers `BLC_sendRawMasterRequest(channel, id, length)`; it falls back to the bytes variant or a header+receive strategy as needed. The mock covers both forms.
- `interface.schedule_nr: -1` defers schedule start to the test code (useful when the test wants to pick a specific schedule by name via `lin.start_schedule("CCO")`).
## Selecting tests with markers
Markers in use:
- `smoke`: quick confidence tests
- `hardware`: needs real device
- `babylin`: targets the BabyLIN SDK adapter
- `hardware`: needs real device (any LIN master)
- `mum`: targets the Melexis Universal Master adapter (current default)
- `babylin`: targets the legacy BabyLIN SDK adapter
- `unit`: pure unit tests (no hardware, no external I/O)
- `req_XXX`: requirement mapping (e.g., `@pytest.mark.req_001`)
Examples:
@ -163,11 +191,15 @@ pytest -m smoke --maxfail=1 -q
- For a golden image approach, see `docs/10_build_custom_image.md`
Running tests headless via systemd typically involves:
- A service that sets `ECU_TESTS_CONFIG` to a hardware YAML
- Running `pytest -m "hardware and babylin"` on boot or via timer
- Running `pytest -m "hardware and mum"` (or `"hardware and babylin"`) on boot or via timer
## Troubleshooting quick hits
- ImportError for `pylin` / `pymumclient`: install Melexis packages (`vendor/automated_lin_test/install_packages.sh`); the MUM adapter raises a clear error pointing at this script.
- "interface.host is required when interface.type == 'mum'": set `interface.host` in YAML.
- MUM unreachable: `ping 192.168.7.2`; check the USB-RNDIS link.
- ImportError for `BabyLIN_library`: verify placement under `vendor/` and native library presence.
- No BabyLIN devices found: check USB connection, drivers, and permissions.
- Timeouts on receive: increase `timeout` or verify schedule activity and SDF correctness.

View File

@ -14,6 +14,7 @@ This guide explains how the project's unit tests are organized, how to run them
- `test_config_loader.py` — config precedence and defaults
- `test_linframe.py``LinFrame` validation
- `test_babylin_adapter_mocked.py` — BabyLIN adapter error paths with a mocked SDK wrapper
- `test_mum_adapter_mocked.py` — MUM adapter (`MumLinInterface`) plumbing exercised through fake `pylin` / `pymumclient` modules
- `test_hex_flasher.py` — flashing scaffold against a stub LIN interface
- `tests/plugin/` — plugin self-tests using `pytester`
- `test_conftest_plugin_artifacts.py` — verifies JSON coverage and summary artifacts
@ -69,6 +70,20 @@ lin.connect()
# exercise send/receive/request
```
- For MUM adapter logic, inject `mum_module` and `pylin_module` with fakes
(see `tests/unit/test_mum_adapter_mocked.py` for a full example):
```python
from ecu_framework.lin.mum import MumLinInterface
# fake_mum exposes MelexisUniversalMaster() returning an object with
# open_all(host) and get_device(name)
# fake_pylin exposes LinBusManager(linmaster) and LinDevice22(lin_bus)
lin = MumLinInterface(host="10.0.0.1", mum_module=fake_mum, pylin_module=fake_pylin)
lin.connect()
# exercise send / receive / send_raw / power_*
```
- To simulate specific SDK signatures, use a thin shim (see `_MockBytesOnly` in `tests/test_babylin_wrapper_mock.py`).
- Include a docstring with Title/Description/Requirements/Steps/Expected Result so the reporting plugin can extract metadata (this also helps the HTML report).
- When testing the plugin itself, use the `pytester` fixture to generate a temporary test run and validate artifacts exist and contain expected entries.
@ -115,7 +130,7 @@ start .\reports\report.html
- Run `-m unit` and `tests/plugin` on every PR
- Optionally run mock integration/smoke on PR
- Run hardware test matrix on a nightly or on-demand basis (`-m "hardware and babylin"`)
- Run hardware test matrix on a nightly or on-demand basis (`-m "hardware and mum"` or `-m "hardware and babylin"`)
- Publish artifacts from `reports/`: HTML/JUnit/coverage JSON/summary MD
## Troubleshooting

View File

@ -2,6 +2,13 @@
This guide covers using the Owon bench power supply via SCPI over serial with the framework.
> **MUM users**: the Melexis Universal Master has its own power output on
> `power_out0` and the MUM adapter calls `power_up()` / `power_down()` in
> `connect()` / `disconnect()` automatically. The Owon PSU is **not required**
> for the standard MUM flow — leave `power_supply.enabled: false`. The Owon
> remains useful for over/under-voltage scenarios, separate-rail tests, or
> when running with the legacy BabyLIN adapter (which has no built-in power).
- Library: `ecu_framework/power/owon_psu.py`
- Hardware test: `tests/hardware/test_owon_psu.py`
- quick demo script: `vendor/Owon/owon_psu_quick_demo.py`

167
docs/16_mum_internals.md Normal file
View File

@ -0,0 +1,167 @@
# MUM Adapter Internals (Melexis Universal Master)
This document describes how the `MumLinInterface` adapter wraps the Melexis
`pymumclient` and `pylin` packages, how frames flow across the LIN bus, and
which MUM-specific behaviors callers need to understand.
## Overview
- Location: `ecu_framework/lin/mum.py`
- Vendor reference scripts: `vendor/automated_lin_test/` (`test_led_control.py`, `test_auto_addressing.py`, `power_cycle.py`)
- Default MUM endpoint: `192.168.7.2` over USB-RNDIS
- LIN device name on MUM: `lin0`
- Power-control device on MUM: `power_out0`
- Required Python packages: `pylin`, `pymumclient` (Melexis-supplied; not on PyPI). See `vendor/automated_lin_test/install_packages.sh`.
## What the MUM gives you that BabyLIN doesn't
- **Built-in power control** on `power_out0` — the adapter calls `power_up()` in `connect()` and `power_down()` in `disconnect()`. No external Owon PSU needed for the standard flow.
- **Network access**: the MUM is IP-reachable, so the host machine (Windows, Linux, Pi) does not need vendor native libraries — only the two Python packages.
- **Direct transport-layer access** for sending raw frames with LIN 1.x **Classic** checksum (required for BSM-SNPD diagnostic frames).
## What it doesn't give you
- **No passive listen.** The MUM is master-driven. To "receive" a slave-published frame, the master sends a header on that frame ID and the slave must respond. `MumLinInterface.receive(id=None)` raises `NotImplementedError` for that reason.
- **No SDF / schedule manager.** The adapter does not run a schedule; tests publish frames explicitly (or pull slave frames explicitly) on each call.
## Mermaid: connect / receive / send
```mermaid
sequenceDiagram
autonumber
participant T as Test/Fixture
participant A as MumLinInterface
participant MM as pymumclient (MelexisUniversalMaster)
participant PL as pylin (LinDevice22 / TransportLayer)
participant E as ECU
T->>A: connect()
A->>MM: MelexisUniversalMaster()
A->>MM: open_all(host)
A->>MM: get_device('power_out0')
A->>MM: get_device('lin0')
A->>MM: linmaster.setup()
A->>PL: LinBusManager(linmaster)
A->>PL: LinDevice22(lin_bus); set baudrate
A->>PL: get_device('bus/transport_layer')
A->>MM: power_control.power_up()
Note over A: sleep(boot_settle_seconds)
A-->>T: connected
T->>A: receive(id=0x11)
A->>PL: send_message(master_to_slave=False, frame_id=0x11, data_length=4)
PL->>E: header for 0x11
E-->>PL: response bytes
PL-->>A: bytes
A-->>T: LinFrame(id=0x11, data=...)
T->>A: send(LinFrame(0x0A, payload))
A->>PL: send_message(master_to_slave=True, frame_id=0x0A, data_length=8, data=payload)
PL->>E: header + payload (Enhanced checksum)
T->>A: send_raw(b"\x7F\x06\xB5...")
A->>PL: transport_layer.ld_put_raw(data, baudrate)
Note over PL,E: LIN 1.x Classic checksum (required for BSM-SNPD)
T->>A: disconnect()
A->>MM: power_control.power_down()
A->>MM: linmaster.teardown()
```
## Public API
`MumLinInterface(host, lin_device='lin0', power_device='power_out0', baudrate=19200, frame_lengths=None, default_data_length=8, boot_settle_seconds=0.5)`
LinInterface contract (matches Mock and BabyLIN adapters):
- `connect()` — opens MUM, sets up LIN, **and powers up the ECU**
- `disconnect()` — powers down and tears down (best-effort)
- `send(frame: LinFrame)` — publishes a master-to-slave frame using Enhanced checksum
- `receive(id: int, timeout: float = 1.0) -> LinFrame | None` — triggers a slave read for `id`. The `timeout` argument is informational; the underlying `pylin` call is synchronous. Any pylin exception is treated as "no data" and returns `None`. Passing `id=None` raises `NotImplementedError`.
MUM-only extras:
- `send_raw(bytes)` — sends a raw LIN frame using **Classic** checksum via the transport layer's `ld_put_raw`. Use this for BSM-SNPD diagnostic frames; the firmware will reject them if Enhanced is used.
- `power_up()` / `power_down()` — direct control over `power_out0`
- `power_cycle(wait=2.0)` — convenience: `power_down()`, sleep, `power_up()`, then `boot_settle_seconds` sleep
## Frame-length resolution
Because the MUM is master-driven, every receive needs to know how many bytes
to ask for. The adapter resolves this from `frame_lengths`:
1. Built-in defaults for the 4SEVEN library (ALM_Status=4, ALM_Req_A=8, ConfigFrame=3, PWM_Frame=8, VF_Frame=8, Tj_Frame=8, PWM_wo_Comp=8, NVM_Debug=8).
2. Anything in the constructor's `frame_lengths` argument **overrides** the defaults.
3. If a frame ID isn't in the map, `default_data_length` (default 8) is used.
In YAML, hex keys work:
```yaml
interface:
type: mum
frame_lengths:
0x0A: 8
0x11: 4
```
The config loader coerces hex strings (`"0x0A"`) and integers alike.
## Diagnostic frames (BSM-SNPD)
The vendor's `test_auto_addressing.py` flow runs LIN 2.1 BSM-SNPD via raw
frames on `0x3C` (MasterReq). The framework supports the same flow:
```python
# inside a test that already has the MUM 'lin' fixture
data = bytearray([
0x7F, # NAD broadcast
0x06, # PCI: 6 data bytes
0xB5, # SID: BSM-SNPD
0xFF, # Supplier ID LSB
0x7F, # Supplier ID MSB
0x01, # subfunction (INIT)
0x02, # param 1
0xFF, # param 2
])
lin.send_raw(bytes(data))
```
`send_raw()` calls `transport_layer.ld_put_raw(data=..., baudrate=...)`
which uses LIN 1.x Classic checksum. Using `lin.send()` for these frames
would compute Enhanced checksum and the firmware would discard the frame.
## Error surfaces
- **`pymumclient is not installed`** / **`pylin is not installed`** — raised on `connect()` if the Melexis packages aren't importable. The error message points at `vendor/automated_lin_test/install_packages.sh`.
- **`MUM not connected`** — calling `send` / `receive` / `send_raw` before `connect()` (or after `disconnect()`).
- **`MUM transport layer not available`** — raised by `send_raw` when the LIN device didn't expose `bus/transport_layer`. Practically always available on MUM firmware that supports diagnostic frames.
- **pylin exceptions during `receive`** — converted to `None` (treated as a timeout / no-data). Use this to drive timeout-tolerant tests without try/except in the test body.
## Unit testing without hardware
The adapter accepts `mum_module=` and `pylin_module=` constructor arguments
that bypass the real package imports. Tests in
`tests/unit/test_mum_adapter_mocked.py` use simple in-memory fakes to drive
the connect / send / receive / send_raw / power-cycle paths end to end. See
that file for a complete shim implementation.
```python
from ecu_framework.lin.mum import MumLinInterface
iface = MumLinInterface(
host="10.0.0.1",
boot_settle_seconds=0.0,
mum_module=fake_mum,
pylin_module=fake_pylin,
)
iface.connect()
# ... assertions ...
iface.disconnect()
```
## Notes and pitfalls
- **Boot settling**: After `power_up()` the adapter sleeps `boot_settle_seconds` (default 0.5 s) so the ECU has time to come up before the first frame. Increase if your ECU boots slowly.
- **Owon PSU coexistence**: the MUM provides power on `power_out0` independently of `ecu_framework/power/`. Leave `power_supply.enabled: false` for the standard MUM flow; enable it only for over/under-voltage scenarios that need a separate, programmable rail.
- **Networking**: USB-RNDIS bring-up can take a few seconds after plugging in the MUM. If `connect()` fails with a connection-refused, `ping 192.168.7.2` first.
- **Multiple MUMs**: only one MUM is supported per `MumLinInterface` instance. Different `host` addresses can run different fixture sessions side-by-side.

View File

@ -6,21 +6,24 @@ A guided tour of the ECU testing framework. Start here:
2. `02_configuration_resolution.md` — How configuration is loaded and merged
3. `03_reporting_and_metadata.md` — How test documentation becomes report metadata
4. `11_conftest_plugin_overview.md` — Custom pytest plugin: hooks, call sequence, and artifacts
5. `04_lin_interface_call_flow.md` — LIN abstraction and adapter behavior (Mock vs BabyLIN SDK wrapper)
5. `04_lin_interface_call_flow.md` — LIN abstraction and adapter behavior (Mock, MUM, legacy BabyLIN)
6. `05_architecture_overview.md` — High-level architecture and components
7. `06_requirement_traceability.md` — Requirement markers and coverage visuals
8. `07_flash_sequence.md` — ECU flashing workflow and sequence diagram
9. `08_babylin_internals.md` — BabyLIN SDK wrapper internals and call flow
9. `DEVELOPER_COMMIT_GUIDE.md` — What to commit vs ignore, commands
10. `09_raspberry_pi_deployment.md` — Run on Raspberry Pi (venv, service, hardware notes)
11. `10_build_custom_image.md` — Build a custom Raspberry Pi OS image with the framework baked in
12. `12_using_the_framework.md` — Practical usage: local, hardware, CI, and Pi
13. `13_unit_testing_guide.md` — Unit tests layout, markers, coverage, and tips
14. `14_power_supply.md` — Owon PSU control, configuration, tests, and quick demo script
15. `15_report_properties_cheatsheet.md` — Standardized keys for record_property/rp across suites
9. `08_babylin_internals.md` — BabyLIN SDK wrapper internals and call flow (legacy)
10. `16_mum_internals.md` — MUM (Melexis Universal Master) adapter internals and call flow
11. `DEVELOPER_COMMIT_GUIDE.md` — What to commit vs ignore, commands
12. `09_raspberry_pi_deployment.md` — Run on Raspberry Pi (venv, service, hardware notes)
13. `10_build_custom_image.md` — Build a custom Raspberry Pi OS image with the framework baked in
14. `12_using_the_framework.md` — Practical usage: local, hardware (MUM/BabyLIN), CI, and Pi
15. `13_unit_testing_guide.md` — Unit tests layout, markers, coverage, and tips
16. `14_power_supply.md` — Owon PSU control, configuration, tests, and quick demo script
17. `15_report_properties_cheatsheet.md` — Standardized keys for record_property/rp across suites
Related references:
- Root project guide: `../README.md`
- Full framework guide: `../TESTING_FRAMEWORK_GUIDE.md`
- BabyLIN placement and integration: `../vendor/README.md`
- PSU quick demo and scripts: `../vendor/Owon/`
- MUM source scripts and protocol details: `../vendor/automated_lin_test/README.md`
- PSU quick demo and scripts: `../vendor/Owon/`

View File

@ -24,25 +24,38 @@ class FlashConfig:
class InterfaceConfig:
"""LIN interface configuration.
type: Adapter type name: "mock" for the simulated adapter, "babylin" for real hardware via SDK.
channel: Channel index to use (0-based in most SDKs); default chosen by project convention.
bitrate: Informational; typically SDF/schedule defines effective bitrate for BabyLIN.
type: Adapter type "mock" (simulated), "babylin" (legacy BabyLIN SDK), or "mum"
(Melexis Universal Master).
channel: Channel index to use (0-based in most SDKs); BabyLIN-specific.
bitrate: Effective LIN bitrate; the MUM uses this directly, the BabyLIN SDF may override.
dll_path: Legacy/optional pointer to vendor DLLs when using ctypes (not used by SDK wrapper).
node_name: Optional friendly name for display/logging.
func_names: Legacy mapping for ctypes function names; ignored by SDK wrapper.
sdf_path: Path to the SDF to load on connect (BabyLIN only).
schedule_nr: Schedule index to start after connect (BabyLIN only).
schedule_nr: Schedule index to start after connect (BabyLIN only). -1 = skip.
host: MUM IP address (MUM only).
lin_device: MUM LIN device name (MUM only, default 'lin0').
power_device: MUM power-control device name (MUM only, default 'power_out0').
boot_settle_seconds: Delay after MUM power-up before sending the first frame.
frame_lengths: Optional map of frame_id (int) -> data length (int) used by the
MUM adapter when receiving slave-published frames.
"""
type: str = "mock" # "mock" or "babylin"
channel: int = 1 # Default channel index (project-specific default)
bitrate: int = 19200 # Typical LIN bitrate; SDF may override
dll_path: Optional[str] = None # Legacy ctypes option; not used with SDK wrapper
node_name: Optional[str] = None # Optional label for node/adapter
func_names: Dict[str, str] = field(default_factory=dict) # Legacy ctypes mapping; safe to leave empty
# SDK wrapper options
sdf_path: Optional[str] = None # Path to SDF file to load (BabyLIN)
schedule_nr: int = 0 # Schedule number to start after connect (BabyLIN)
type: str = "mock" # "mock", "babylin", or "mum"
channel: int = 1
bitrate: int = 19200
dll_path: Optional[str] = None
node_name: Optional[str] = None
func_names: Dict[str, str] = field(default_factory=dict)
# BabyLIN-specific
sdf_path: Optional[str] = None
schedule_nr: int = 0
# MUM-specific
host: Optional[str] = None
lin_device: str = "lin0"
power_device: str = "power_out0"
boot_settle_seconds: float = 0.5
frame_lengths: Dict[int, int] = field(default_factory=dict)
@dataclass
@ -117,16 +130,33 @@ def _to_dataclass(cfg: Dict[str, Any]) -> EcuTestConfig:
iface = cfg.get("interface", {}) # Sub-config for interface
flash = cfg.get("flash", {}) # Sub-config for flashing
psu = cfg.get("power_supply", {}) # Sub-config for power supply
# Coerce frame_lengths keys to int (YAML may parse numeric keys as int already,
# but accept hex strings like "0x0A: 8" too).
raw_fl = iface.get("frame_lengths", {}) or {}
frame_lengths: Dict[int, int] = {}
if isinstance(raw_fl, dict):
for k, v in raw_fl.items():
try:
key = int(k, 0) if isinstance(k, str) else int(k)
frame_lengths[key] = int(v)
except (TypeError, ValueError):
continue
return EcuTestConfig(
interface=InterfaceConfig(
type=str(iface.get("type", "mock")).lower(), # Normalize to lowercase
channel=int(iface.get("channel", 1)), # Coerce to int
bitrate=int(iface.get("bitrate", 19200)), # Coerce to int
dll_path=iface.get("dll_path"), # Optional legacy field
node_name=iface.get("node_name"), # Optional friendly name
func_names=dict(iface.get("func_names", {}) or {}), # Ensure a dict
sdf_path=iface.get("sdf_path"), # Optional SDF path
schedule_nr=int(iface.get("schedule_nr", 0)), # Coerce to int
type=str(iface.get("type", "mock")).lower(),
channel=int(iface.get("channel", 1)),
bitrate=int(iface.get("bitrate", 19200)),
dll_path=iface.get("dll_path"),
node_name=iface.get("node_name"),
func_names=dict(iface.get("func_names", {}) or {}),
sdf_path=iface.get("sdf_path"),
schedule_nr=int(iface.get("schedule_nr", 0)),
host=iface.get("host"),
lin_device=str(iface.get("lin_device", "lin0")),
power_device=str(iface.get("power_device", "power_out0")),
boot_settle_seconds=float(iface.get("boot_settle_seconds", 0.5)),
frame_lengths=frame_lengths,
),
flash=FlashConfig(
enabled=bool(flash.get("enabled", False)), # Coerce to bool

View File

@ -5,7 +5,10 @@ Exports:
- LinInterface, LinFrame: core abstraction and frame type
- MockBabyLinInterface: mock implementation for fast, hardware-free tests
Real hardware adapter (BabyLIN) is available in babylin.py.
Real hardware adapters live in their own modules and are imported by the
fixture only when selected by config:
- babylin.BabyLinInterface (legacy; needs the BabyLIN SDK + native libs)
- mum.MumLinInterface (current; needs Melexis pylin + pymumclient)
"""
from .base import LinInterface, LinFrame
from .mock import MockBabyLinInterface

View File

@ -71,19 +71,99 @@ class BabyLinInterface(LinInterface):
self._channel_handle = None # Per-channel handle returned by BLC_getChannelHandle
self._connected = False # Internal connection state flag
def _err(self, rc: int) -> None:
def _detail_for(self, rc) -> str:
"""Look up a human-readable SDK error message; never raises.
Tries (in order):
1. BLC_getLastError(channel_handle) device-side last error (best detail)
2. BLC_getErrorString(rc) simple rc lookup
3. BLC_getDetailedErrorString(rc, 0) detailed lookup (rc + report_param)
Returns the first non-empty message, or "".
"""
parts = []
# 1. Device-side last error — usually the most informative.
# BLC_getLastError takes the device connection handle; fall back to the
# channel handle if the device handle isn't set yet.
for h in (self._handle, self._channel_handle):
if h is None:
continue
try:
fn = getattr(self._BabyLIN, 'BLC_getLastError', None)
if fn is not None:
s = fn(h)
if isinstance(s, bytes):
s = s.decode('utf-8', errors='ignore')
if s:
parts.append(str(s))
break
except Exception:
continue
if rc is None:
return " | ".join(parts)
# 2. Simple error string by rc
try:
fn = getattr(self._BabyLIN, 'BLC_getErrorString', None)
if fn is not None:
s = fn(int(rc))
if isinstance(s, bytes):
s = s.decode('utf-8', errors='ignore')
if s:
parts.append(str(s))
except Exception:
pass
# 3. Detailed string (rc + report_parameter)
try:
fn = getattr(self._BabyLIN, 'BLC_getDetailedErrorString', None)
if fn is not None:
s = fn(int(rc), 0)
if isinstance(s, bytes):
s = s.decode('utf-8', errors='ignore')
if s:
parts.append(str(s))
except Exception:
pass
return " | ".join(parts)
def _err(self, rc: int, context: str = "") -> None:
"""Raise a RuntimeError with a readable SDK error message for rc != BL_OK."""
if rc == self._BabyLIN.BL_OK:
return
# Prefer a human-friendly error string if the SDK provides it
msg = self._detail_for(rc) or f"rc={rc}"
prefix = f"BabyLIN error{(' (' + context + ')') if context else ''}"
raise RuntimeError(f"{prefix}: {msg} (rc={rc})")
def _exec_command(self, cmd: str) -> None:
"""Run a BLC_sendCommand on the channel handle, surfacing detailed errors.
The SDK's wrapper raises BabyLINException for any non-zero rc. We catch
that and re-raise a RuntimeError that includes BLC_getDetailedErrorString,
so callers see e.g. "schedule index out of range" instead of opaque "303".
"""
if self._channel_handle is None:
raise RuntimeError("BabyLIN not connected")
try:
get_str = getattr(self._BabyLIN, 'BLC_getDetailedErrorString', None)
msg = get_str(rc) if get_str else f"rc={rc}"
if not isinstance(msg, str):
msg = str(msg)
except Exception:
msg = f"rc={rc}"
raise RuntimeError(f"BabyLIN error: {msg}")
rc = self._bl_call('BLC_sendCommand', self._channel_handle, cmd)
except Exception as e:
rc = getattr(e, 'errorCode', None)
if rc is None:
# Try common alternate attributes used by SDK exception types
for attr in ('rc', 'returncode', 'code'):
rc = getattr(e, attr, None)
if rc is not None:
break
detail = self._detail_for(rc) if rc is not None else ""
rc_part = f"rc={rc}" if rc is not None else "rc=?"
extra = f"{detail}" if detail else ""
raise RuntimeError(
f"BabyLIN command failed: {cmd!r} ({rc_part}){extra}"
) from e
if rc != self._BabyLIN.BL_OK:
self._err(rc, context=f"command {cmd!r}")
def connect(self) -> None:
"""Open device, optionally load SDF, select channel, and start schedule."""
@ -102,24 +182,117 @@ class BabyLinInterface(LinInterface):
if rc != self._BabyLIN.BL_OK:
self._err(rc)
# Get channel count and pick the configured channel index (default 0)
# Get channel count and resolve the channel handle.
# A BabyLIN device may expose multiple channel types (LIN/CAN/...).
# When the SDK supports BLC_getChannelInfo, we filter by info.type==0
# to find LIN channels (mirrors vendor/BLCInterfaceExample.py).
# Without it (older SDKs, mock wrappers), we fall back to honoring
# the configured index and validating the handle.
ch_count = self._bl_call('BLC_getChannelCount', self._handle)
if ch_count <= 0:
raise RuntimeError("No channels reported by device")
ch_idx = int(self.channel_index)
if ch_idx < 0 or ch_idx >= ch_count:
ch_idx = 0
# Resolve a channel handle used for all subsequent Tx/Rx commands
configured_idx = int(self.channel_index)
get_info = getattr(self._BabyLIN, 'BLC_getChannelInfo', None)
if get_info is not None:
lin_channels = [] # [(idx, handle, info)] for type==0 channels
seen = [] # diagnostics if no LIN channel is found
for idx in range(int(ch_count)):
h = self._bl_call('BLC_getChannelHandle', self._handle, idx)
if not h:
seen.append((idx, None, None))
continue
try:
info = get_info(h)
except Exception:
info = None
seen.append((idx, h, info))
if info is not None and getattr(info, 'type', None) == 0:
lin_channels.append((idx, h, info))
if not lin_channels:
details = ", ".join(
f"idx={i} handle={'ok' if h else 'None'} "
f"type={getattr(info, 'type', '?') if info is not None else '?'} "
f"name={getattr(info, 'name', b'').decode('utf-8', errors='ignore') if info is not None else ''}"
for i, h, info in seen
)
raise RuntimeError(
f"No LIN channel (type==0) found on device. Channels seen: [{details}]"
)
# Prefer the configured index if it is a LIN channel; otherwise the first LIN channel.
chosen = next((t for t in lin_channels if t[0] == configured_idx), lin_channels[0])
ch_idx, self._channel_handle, _ = chosen
else:
ch_idx = configured_idx if 0 <= configured_idx < int(ch_count) else 0
self._channel_handle = self._bl_call('BLC_getChannelHandle', self._handle, ch_idx)
# Start a schedule if configured (common requirement for regular polling/masters)
if self.schedule_nr is not None:
cmd = f"start schedule {int(self.schedule_nr)};"
rc = self._bl_call('BLC_sendCommand', self._channel_handle, cmd)
if rc != self._BabyLIN.BL_OK:
self._err(rc)
if not self._channel_handle:
raise RuntimeError(f"BLC_getChannelHandle returned invalid handle for channel {ch_idx}")
self._connected = True # Mark interface as connected
# Mark connected before any sendCommand so send_command()/_exec_command()
# accept the call. Auto-start a schedule only if a non-negative index is set;
# use -1 (or None) in config to defer starting to the test/caller.
self._connected = True
if self.schedule_nr is not None and int(self.schedule_nr) >= 0:
self._exec_command(f"start schedule {int(self.schedule_nr)};")
def send_command(self, cmd: str) -> None:
"""Send a raw BabyLIN SDK command via BLC_sendCommand on the channel handle.
Useful for actions that don't fit the abstract LinInterface, e.g.:
send_command("stop;")
send_command("setsig 0 255;")
Note: BabyLIN firmware accepts 'start schedule <index>;' but not the
schedule name. Use start_schedule() for name-or-index lookup.
"""
if not self._connected:
raise RuntimeError("BabyLIN not connected")
self._exec_command(cmd)
def schedule_nr_for_name(self, name: str) -> int:
"""Return the schedule index matching `name` from the loaded SDF.
Tries BLC_SDF_getScheduleNr first; falls back to enumerating with
BLC_SDF_getNumSchedules + BLC_SDF_getScheduleName for older SDKs.
Raises RuntimeError if the schedule isn't found.
"""
if self._channel_handle is None:
raise RuntimeError("BabyLIN not connected")
get_nr = getattr(self._BabyLIN, 'BLC_SDF_getScheduleNr', None)
if get_nr is not None:
try:
return int(get_nr(self._channel_handle, name))
except Exception:
pass # fall through to enumeration
get_count = getattr(self._BabyLIN, 'BLC_SDF_getNumSchedules', None)
get_name = getattr(self._BabyLIN, 'BLC_SDF_getScheduleName', None)
if get_count is None or get_name is None:
raise RuntimeError(
f"SDK does not expose schedule lookup; cannot resolve schedule {name!r}"
)
count = int(get_count(self._channel_handle))
names = []
for i in range(count):
try:
n = get_name(self._channel_handle, i)
except Exception:
n = ""
names.append(n)
if n == name:
return i
raise RuntimeError(
f"Schedule {name!r} not found in SDF. Available: {names}"
)
def start_schedule(self, name_or_nr) -> int:
"""Start a schedule by name (str) or index (int). Returns the index used."""
nr = name_or_nr if isinstance(name_or_nr, int) else self.schedule_nr_for_name(str(name_or_nr))
self.send_command(f"start schedule {int(nr)};")
return int(nr)
def disconnect(self) -> None:
"""Close device handles and reset internal state (best-effort)."""

220
ecu_framework/lin/mum.py Normal file
View File

@ -0,0 +1,220 @@
"""LIN adapter that uses the Melexis Universal Master (MUM) over the network.
Wraps the vendor's `pylin` + `pymumclient` packages so test code can talk to
the MUM through the same `LinInterface` abstraction used by the BabyLIN and
mock adapters. The MUM is a BeagleBone-based LIN master reachable over IP
(default 192.168.7.2) with built-in power control on `power_out0`.
The MUM is master-driven: a slave frame is fetched by issuing a request via
`send_message(master_to_slave=False, frame_id, data_length)`, so `receive()`
requires a frame ID. Per-frame `data_length` is taken from the constructor's
`frame_lengths` map; ALM_Status (0x11, 4 bytes) and ALM_Req_A (0x0A, 8 bytes)
have built-in defaults so the common cases work out of the box.
Diagnostic frames (BSM-SNPD) need the LIN 1.x **Classic** checksum, which
`send_message` does not produce. Use `send_raw()` (which calls the transport
layer's `ld_put_raw`) for those frames.
"""
from __future__ import annotations
import time
from typing import Dict, Optional
from .base import LinInterface, LinFrame
# Sensible defaults for the 4SEVEN_color_lib_test ECU. Callers can extend or
# override these via the `frame_lengths` constructor argument.
_DEFAULT_FRAME_LENGTHS: Dict[int, int] = {
0x0A: 8, # ALM_Req_A (master-published, RGB control)
0x11: 4, # ALM_Status (slave-published)
0x06: 3, # ConfigFrame (master-published)
0x12: 8, # PWM_Frame (slave-published)
0x13: 8, # VF_Frame (slave-published)
0x14: 8, # Tj_Frame (slave-published)
0x15: 8, # PWM_wo_Comp (slave-published)
0x16: 8, # NVM_Debug (slave-published)
}
class MumLinInterface(LinInterface):
"""LIN adapter for the Melexis Universal Master."""
def __init__(
self,
host: str = "192.168.7.2",
lin_device: str = "lin0",
power_device: str = "power_out0",
baudrate: int = 19200,
frame_lengths: Optional[Dict[int, int]] = None,
default_data_length: int = 8,
boot_settle_seconds: float = 0.5,
# Test seam: inject pre-built modules to bypass real hardware.
mum_module: object = None,
pylin_module: object = None,
) -> None:
self.host = host
self.lin_device = lin_device
self.power_device = power_device
self.baudrate = int(baudrate)
self.boot_settle_seconds = float(boot_settle_seconds)
self.default_data_length = int(default_data_length)
self.frame_lengths = dict(_DEFAULT_FRAME_LENGTHS)
if frame_lengths:
self.frame_lengths.update({int(k): int(v) for k, v in frame_lengths.items()})
self._mum_module = mum_module
self._pylin_module = pylin_module
self._mum = None
self._linmaster = None
self._power_control = None
self._lin_dev = None
self._transport_layer = None
self._connected = False
# -----------------------------
# Lifecycle
# -----------------------------
def _resolve_modules(self):
"""Lazy-import MUM stack so the framework still loads without it."""
if self._mum_module is None:
try:
import pymumclient # type: ignore
except Exception as e:
raise RuntimeError(
"pymumclient is not installed. The MUM adapter requires Melexis "
"packages 'pymumclient' and 'pylin'. See "
"vendor/automated_lin_test/install_packages.sh."
) from e
self._mum_module = pymumclient
if self._pylin_module is None:
try:
import pylin # type: ignore
except Exception as e:
raise RuntimeError(
"pylin is not installed. The MUM adapter requires Melexis "
"packages 'pymumclient' and 'pylin'. See "
"vendor/automated_lin_test/install_packages.sh."
) from e
self._pylin_module = pylin
return self._mum_module, self._pylin_module
def connect(self) -> None:
"""Open MUM, set up LIN master, attach LIN bus, and power up the ECU."""
pymumclient, pylin = self._resolve_modules()
self._mum = pymumclient.MelexisUniversalMaster()
self._mum.open_all(self.host)
self._power_control = self._mum.get_device(self.power_device)
self._linmaster = self._mum.get_device(self.lin_device)
self._linmaster.setup()
lin_bus = pylin.LinBusManager(self._linmaster)
self._lin_dev = pylin.LinDevice22(lin_bus)
self._lin_dev.baudrate = self.baudrate
# Transport layer is needed for Classic-checksum diagnostic frames.
try:
self._transport_layer = self._lin_dev.get_device("bus/transport_layer")
except Exception:
self._transport_layer = None
# Power up and let the ECU boot before the first frame.
self._power_control.power_up()
if self.boot_settle_seconds > 0:
time.sleep(self.boot_settle_seconds)
self._connected = True
def disconnect(self) -> None:
"""Power down the ECU and tear down the MUM connection (best-effort)."""
if self._power_control is not None:
try:
self._power_control.power_down()
except Exception:
pass
if self._linmaster is not None:
try:
self._linmaster.teardown()
except Exception:
pass
self._connected = False
self._mum = None
self._linmaster = None
self._power_control = None
self._lin_dev = None
self._transport_layer = None
# -----------------------------
# LinInterface contract
# -----------------------------
def send(self, frame: LinFrame) -> None:
"""Publish a master-to-slave frame using Enhanced checksum."""
if not self._connected or self._lin_dev is None:
raise RuntimeError("MUM not connected")
self._lin_dev.send_message(
master_to_slave=True,
frame_id=int(frame.id),
data_length=len(frame.data),
data=list(frame.data),
)
def receive(self, id: Optional[int] = None, timeout: float = 1.0) -> Optional[LinFrame]:
"""Trigger a slave-to-master read for `id` and return the response.
The MUM is master-driven, so a frame ID is required; passing None
raises NotImplementedError. `timeout` is informational only the
underlying pylin call is synchronous and uses its own timing.
"""
if not self._connected or self._lin_dev is None:
raise RuntimeError("MUM not connected")
if id is None:
raise NotImplementedError(
"MUM receive requires a frame ID; passive listen is not supported"
)
length = self.frame_lengths.get(int(id), self.default_data_length)
try:
response = self._lin_dev.send_message(
master_to_slave=False,
frame_id=int(id),
data_length=int(length),
data=None,
)
except Exception:
return None # treat any pylin exception as a timeout / no-data
if not response:
return None
return LinFrame(id=int(id) & 0x3F, data=bytes(response[: int(length)]))
# -----------------------------
# MUM-specific extras
# -----------------------------
def send_raw(self, data: bytes) -> None:
"""Send a raw LIN frame using LIN 1.x **Classic** checksum.
Required for BSM-SNPD diagnostic frames (service ID 0xB5) the
firmware rejects these if Enhanced checksum is used.
"""
if not self._connected or self._transport_layer is None:
raise RuntimeError("MUM transport layer not available")
self._transport_layer.ld_put_raw(data=bytearray(data), baudrate=self.baudrate)
def power_up(self) -> None:
if self._power_control is None:
raise RuntimeError("MUM not connected")
self._power_control.power_up()
def power_down(self) -> None:
if self._power_control is None:
raise RuntimeError("MUM not connected")
self._power_control.power_down()
def power_cycle(self, wait: float = 2.0) -> None:
"""Power the ECU down, wait `wait` seconds, then back up."""
self.power_down()
time.sleep(wait)
self.power_up()
if self.boot_settle_seconds > 0:
time.sleep(self.boot_settle_seconds)

View File

@ -16,8 +16,9 @@ addopts = -ra --junitxml=reports/junit.xml --html=reports/report.html --self-con
# markers: Document all custom markers so pytest doesn't warn and so usage is clear.
# Use with: pytest -m "markername"
markers =
hardware: requires real hardware (BabyLIN device and ECU); excluded by default in mock runs
babylin: tests that use the BabyLIN interface (may require hardware)
hardware: requires real hardware (LIN master + ECU); excluded by default in mock runs
babylin: tests that use the legacy BabyLIN interface (may require hardware)
mum: tests that use the Melexis Universal Master (MUM) interface (requires hardware)
unit: fast, isolated tests (no hardware, no external I/O)
req_001: REQ-001 - Mock interface shall echo transmitted frames for local testing
req_002: REQ-002 - Mock interface shall synthesize deterministic responses for request operations

View File

@ -13,6 +13,11 @@ try:
except Exception:
BabyLinInterface = None # type: ignore
try:
from ecu_framework.lin.mum import MumLinInterface # type: ignore
except Exception:
MumLinInterface = None # type: ignore
WORKSPACE_ROOT = pathlib.Path(__file__).resolve().parents[1]
@ -40,6 +45,19 @@ def lin(config: EcuTestConfig) -> t.Iterator[LinInterface]:
sdf_path=config.interface.sdf_path,
schedule_nr=config.interface.schedule_nr,
)
elif iface_type == "mum":
if MumLinInterface is None:
pytest.skip("MUM interface not available in this environment")
if not config.interface.host:
pytest.skip("interface.host is required when interface.type == 'mum'")
lin = MumLinInterface(
host=config.interface.host,
lin_device=config.interface.lin_device,
power_device=config.interface.power_device,
baudrate=config.interface.bitrate,
boot_settle_seconds=config.interface.boot_settle_seconds,
frame_lengths=config.interface.frame_lengths or None,
)
else:
raise RuntimeError(f"Unknown interface type: {iface_type}")

View File

@ -0,0 +1,118 @@
"""End-to-end hardware test on the MUM (Melexis Universal Master).
Power the ECU via MUM's built-in power output, then activate the RGB LED via
the master-published ALM_Req_A frame (ID 0x0A) and verify the slave responds
on ALM_Status (ID 0x11).
Frame layout (from vendor/4SEVEN_color_lib_test.ldf, ALM_Req_A @ 0x0A, 8B):
byte 0 AmbLightColourRed (0..255)
byte 1 AmbLightColourGreen (0..255)
byte 2 AmbLightColourBlue (0..255)
byte 3 AmbLightIntensity (0..255)
byte 4 AmbLightUpdate (bits 0-1) | AmbLightMode (bits 2-7)
byte 5 AmbLightDuration
byte 6 AmbLightLIDFrom
byte 7 AmbLightLIDTo
The ECU answers ALM_Req_A only when AmbLightLIDFrom <= ALMNadNo <= LIDTo, so
we read the current NAD from ALM_Status first and target that NAD exactly.
"""
from __future__ import annotations
import pytest
from ecu_framework.config import EcuTestConfig
from ecu_framework.lin.base import LinFrame, LinInterface
pytestmark = [pytest.mark.hardware, pytest.mark.mum]
ALM_REQ_A_ID = 0x0A
ALM_STATUS_ID = 0x11
DEFAULT_RGB = (0xFF, 0xFF, 0xFF)
DEFAULT_INTENSITY = 0xFF
def _build_alm_req_a_payload(
r: int, g: int, b: int,
intensity: int = DEFAULT_INTENSITY,
update: int = 0,
mode: int = 0,
duration: int = 0,
lid_from: int = 0x01,
lid_to: int = 0xFF,
) -> bytes:
"""Pack RGB+mode signals into the 8-byte ALM_Req_A payload."""
byte4 = (update & 0x03) | ((mode & 0x3F) << 2)
return bytes([
r & 0xFF, g & 0xFF, b & 0xFF,
intensity & 0xFF,
byte4 & 0xFF,
duration & 0xFF,
lid_from & 0xFF,
lid_to & 0xFF,
])
def test_mum_e2e_power_on_then_led_activate(config: EcuTestConfig, lin: LinInterface, rp):
"""
Title: MUM E2E - Power ECU, Read NAD, Activate RGB LED
Description:
Drives the full hardware path through the Melexis Universal Master:
the `lin` fixture has already powered the ECU via power_out0 and set
up the LIN bus. This test reads ALM_Status to discover the slave's
NAD, publishes ALM_Req_A targeting that NAD with full white at full
intensity, and re-reads ALM_Status to confirm the bus is alive.
Requirements: REQ-MUM-LED-ACTIVATE
Test Steps:
1. Skip unless interface.type == 'mum'
2. Read ALM_Status (0x11) and extract ALMNadNo (byte 0 lower 8 bits)
3. Build ALM_Req_A payload with RGB=(0xFF,0xFF,0xFF), intensity=0xFF,
targeting LIDFrom=LIDTo=current_nad
4. Publish ALM_Req_A via lin.send()
5. Re-read ALM_Status and assert it still returns a valid frame
Expected Result:
- First ALM_Status read returns a 4-byte frame with a NAD in 0x01..0xFE
- Second ALM_Status read returns a frame (bus still alive after Tx)
"""
if config.interface.type != "mum":
pytest.skip("interface.type must be 'mum' for this test")
# Step 2: read current NAD from ALM_Status
status = lin.receive(id=ALM_STATUS_ID, timeout=1.0)
assert status is not None, "No ALM_Status received — check MUM/ECU wiring and power"
assert len(status.data) >= 1, f"ALM_Status too short: {status.data!r}"
current_nad = status.data[0]
rp("alm_status_data_hex", bytes(status.data).hex())
rp("current_nad", f"0x{current_nad:02X}")
assert 0x01 <= current_nad <= 0xFE, (
f"ALMNadNo {current_nad:#x} is out of valid range; ECU may be unconfigured"
)
# Step 3 + 4: target the discovered NAD with full white
payload = _build_alm_req_a_payload(
*DEFAULT_RGB,
intensity=DEFAULT_INTENSITY,
lid_from=current_nad,
lid_to=current_nad,
)
rp("tx_id", f"0x{ALM_REQ_A_ID:02X}")
rp("tx_data_hex", payload.hex())
rp("rgb", list(DEFAULT_RGB))
rp("intensity", DEFAULT_INTENSITY)
lin.send(LinFrame(id=ALM_REQ_A_ID, data=payload))
# Step 5: confirm bus liveness after the activation frame
status_after = lin.receive(id=ALM_STATUS_ID, timeout=1.0)
rp("post_status_present", status_after is not None)
if status_after is not None:
rp("post_status_data_hex", bytes(status_after.data).hex())
assert status_after is not None, (
"ALM_Status not received after publishing ALM_Req_A — ECU may have reset"
)

View File

@ -0,0 +1,235 @@
"""End-to-end hardware test: power the ECU on via Owon PSU, switch to the
'CCO' schedule, and publish an RGB activation frame on ALM_Req_A (ID 0x0A).
Frame layout (from vendor/4SEVEN_color_lib_test.ldf, ALM_Req_A @ ID 0x0A, 8B):
byte 0 AmbLightColourRed (0..255)
byte 1 AmbLightColourGreen (0..255)
byte 2 AmbLightColourBlue (0..255)
byte 3 AmbLightIntensity (0..255)
byte 4 AmbLightUpdate (bits 0-1) | AmbLightMode (bits 2-7)
byte 5 AmbLightDuration
byte 6 AmbLightLIDFrom
byte 7 AmbLightLIDTo
Schedule 'CCO' polls ALM_Req_A every 10 ms (LDF line 252-263). Updating the
master-published frame data via BLC_mon_set_xmit makes the next CCO slot
publish the new RGB values. The slave answers ALM_Status (ID 0x11) which we
use as evidence the bus is alive.
"""
from __future__ import annotations
import time
import pytest
import serial
from ecu_framework.config import EcuTestConfig
from ecu_framework.lin.base import LinFrame, LinInterface
from ecu_framework.power import OwonPSU, SerialParams
pytestmark = [pytest.mark.hardware, pytest.mark.babylin]
# Frame IDs from the LDF
ALM_REQ_A_ID = 0x0A # master-published RGB control frame
ALM_STATUS_ID = 0x11 # slave-published status frame
# Default RGB activation: full white at full intensity, immediate setpoint.
DEFAULT_RGB = (0xFF, 0xFF, 0xFF)
DEFAULT_INTENSITY = 0xFF
_PARITY_MAP = {
"N": serial.PARITY_NONE,
"E": serial.PARITY_EVEN,
"O": serial.PARITY_ODD,
}
_STOPBITS_MAP = {
1: serial.STOPBITS_ONE,
2: serial.STOPBITS_TWO,
}
def _build_serial_params(psu_cfg) -> SerialParams:
return SerialParams(
baudrate=int(psu_cfg.baudrate),
timeout=float(psu_cfg.timeout),
parity=_PARITY_MAP.get(str(psu_cfg.parity or "N").upper(), serial.PARITY_NONE),
stopbits=_STOPBITS_MAP.get(int(float(psu_cfg.stopbits or 1)), serial.STOPBITS_ONE),
xonxoff=bool(psu_cfg.xonxoff),
rtscts=bool(psu_cfg.rtscts),
dsrdtr=bool(psu_cfg.dsrdtr),
)
def _build_alm_req_a_payload(
r: int, g: int, b: int,
intensity: int = DEFAULT_INTENSITY,
update: int = 0, # 0 = Immediate color update
mode: int = 0, # 0 = Immediate Setpoint
duration: int = 0,
lid_from: int = 0,
lid_to: int = 0,
) -> bytes:
"""Pack RGB-activation signals into the 8-byte ALM_Req_A payload."""
# byte 4 packs Update (2 bits, LSB) and Mode (6 bits) per the LDF offsets.
byte4 = (update & 0x03) | ((mode & 0x3F) << 2)
return bytes([
r & 0xFF, g & 0xFF, b & 0xFF,
intensity & 0xFF,
byte4 & 0xFF,
duration & 0xFF,
lid_from & 0xFF,
lid_to & 0xFF,
])
def test_e2e_power_on_then_cco_rgb_activate(config: EcuTestConfig, lin: LinInterface, rp):
"""
Title: E2E - Power ECU, Switch to CCO Schedule, Activate RGB
Description:
Powers the ECU via the Owon PSU, switches the BabyLIN master to the
'CCO' schedule (which polls ALM_Req_A every 10 ms per the LDF), and
publishes an RGB activation payload on ALM_Req_A (ID 0x0A). Captures
bus traffic for a short window to confirm activity (typically the
slave-published ALM_Status at ID 0x11 will appear).
Requirements: REQ-E2E-CCO-RGB
Test Steps:
1. Skip unless interface.type == 'babylin'
2. Skip unless power_supply is enabled and a port is configured
3. Open the PSU, IDN check, set V/I, enable output
4. Wait for ECU boot (boot_settle_seconds, default 1.5 s)
5. Stop any current schedule and start schedule 'CCO'
6. Build the ALM_Req_A payload from RGB+intensity+mode+update
7. Publish the payload via lin.send(LinFrame(0x0A, ...))
8. Drain RX briefly and collect frames seen during the activation window
9. Assert at least one frame was observed; report IDs/lengths
10. Disable PSU output (always)
Expected Result:
- PSU comes up, ECU boots, CCO starts without SDK errors
- At least one LIN frame is observed on the bus during the window
- PSU output is disabled at end of test
"""
# Step 1 / 2: gate on hardware availability
if config.interface.type != "babylin":
pytest.skip("interface.type must be 'babylin' for this E2E test")
psu_cfg = config.power_supply
if not psu_cfg.enabled:
pytest.skip("Power supply disabled in config.power_supply.enabled")
if not psu_cfg.port:
pytest.skip("No power supply 'port' configured (config.power_supply.port)")
set_v = float(psu_cfg.set_voltage)
print(f"Debug: set_v={set_v}, type={type(set_v)}")
set_i = float(psu_cfg.set_current)
print(f"Debug: set_i={set_i}, type={type(set_i)}")
eol = psu_cfg.eol or "\n"
port = str(psu_cfg.port).strip()
boot_settle_s = float(getattr(psu_cfg, "boot_settle_seconds", 1.5))
activation_window_s = float(getattr(psu_cfg, "activation_window", 1.0))
# The adapter is hardware-only here; the test is gated on interface.type=='babylin'.
send_command = getattr(lin, "send_command", None)
start_schedule = getattr(lin, "start_schedule", None)
if send_command is None or start_schedule is None:
pytest.skip("LIN adapter does not expose send_command/start_schedule (need BabyLinInterface)")
rgb = (DEFAULT_RGB[0], DEFAULT_RGB[1], DEFAULT_RGB[2])
rp("interface_type", config.interface.type)
rp("psu_port", port)
rp("set_voltage", set_v)
rp("set_current", set_i)
rp("schedule", "CCO")
rp("rgb", list(rgb))
rp("intensity", DEFAULT_INTENSITY)
sparams = _build_serial_params(psu_cfg)
with OwonPSU(port, sparams, eol=eol) as psu:
# Step 3: bring up PSU
idn = psu.idn()
rp("psu_idn", idn)
assert isinstance(idn, str) and idn != "", "PSU *IDN? returned empty"
if psu_cfg.idn_substr:
assert str(psu_cfg.idn_substr).lower() in idn.lower(), (
f"PSU IDN does not contain expected substring "
f"{psu_cfg.idn_substr!r}; got {idn!r}"
)
psu.set_voltage(1, set_v)
psu.set_current(1, set_i)
try:
psu.set_output(True)
# Step 4: let ECU boot
time.sleep(boot_settle_s)
try:
rp("measured_voltage", psu.measure_voltage())
rp("measured_current", psu.measure_current())
except Exception as meas_err:
rp("measure_error", repr(meas_err))
# Step 5: switch to schedule CCO. The BabyLIN firmware only accepts
# 'start schedule <index>;', so we resolve the name to its SDF index
# via BLC_SDF_getScheduleNr (handled inside start_schedule).
try:
send_command("stop;")
except Exception as e:
rp("stop_error", repr(e))
cco_idx = start_schedule("CCO")
rp("schedule_index", cco_idx)
# Step 6 + 7: build and publish the RGB activation frame.
payload = _build_alm_req_a_payload(*rgb, intensity=DEFAULT_INTENSITY)
rp("tx_id", f"0x{ALM_REQ_A_ID:02X}")
rp("tx_data_hex", payload.hex())
lin.send(LinFrame(id=ALM_REQ_A_ID, data=payload))
# Step 8: collect frames over the activation window. CCO publishes
# ALM_Req_A (0x0A) and ALM_Status (0x11) every ~10 ms each.
try:
lin.flush()
except Exception:
pass
seen = []
deadline = time.monotonic() + activation_window_s
while time.monotonic() < deadline:
rx = lin.receive(timeout=0.1)
if rx is None:
continue
seen.append((rx.id, bytes(rx.data)))
ids = sorted({fid for fid, _ in seen})
rp("rx_count", len(seen))
rp("rx_ids", [f"0x{i:02X}" for i in ids])
if seen:
last_id, last_data = seen[-1]
rp("rx_last_id", f"0x{last_id:02X}")
rp("rx_last_data_hex", last_data.hex())
# Step 9: minimal liveness assertion. We don't require ALM_Status
# specifically because absence-of-slave is a separate failure mode
# to diagnose; we just want to know the bus moved at all.
assert seen, (
f"No LIN frames observed during {activation_window_s:.2f}s on schedule CCO. "
f"Check wiring, SDF, and that 'CCO' exists in the loaded SDF."
)
if ALM_STATUS_ID in ids:
rp("alm_status_seen", True)
else:
# Not asserted, but logged so the report shows it clearly.
rp("alm_status_seen", False)
finally:
# Step 10: always cut power
try:
psu.set_output(False)
except Exception as off_err:
rp("set_output_off_error", repr(off_err))

View File

@ -27,7 +27,7 @@ def test_babylin_connect_receive_timeout(lin, rp):
- Return value is None (timeout) or an object with an 'id' attribute
"""
# Step 2: Perform a short receive to verify operability
rx = lin.receive(timeout=0.2)
rx = lin.receive(timeout=1.0) # 1 second timeout
rp("receive_result", "timeout" if rx is None else "frame")
# Step 3: Accept either a timeout (None) or a frame-like object

View File

@ -1,5 +1,17 @@
import pytest
from ecu_framework.lin.base import LinFrame
from ecu_framework.lin.mock import MockBabyLinInterface
@pytest.fixture(scope="module")
def lin():
"""Module-local override: these tests are explicitly mock-only and must
not depend on whatever real-hardware interface the central config selects."""
iface = MockBabyLinInterface(bitrate=19200, channel=0)
iface.connect()
yield iface
iface.disconnect()
class TestMockLinInterface:

View File

@ -0,0 +1,242 @@
"""Unit tests for the MUM LIN adapter using fake pylin/pymumclient modules.
These tests don't talk to real hardware — they inject lightweight fakes via
the adapter's `mum_module` / `pylin_module` constructor args to validate the
adapter's plumbing (connect/disconnect, send, receive, send_raw, power_*).
"""
from __future__ import annotations
import pytest
from ecu_framework.lin.base import LinFrame
from ecu_framework.lin.mum import MumLinInterface
# ---- fakes ---------------------------------------------------------------
class _FakePower:
def __init__(self):
self.up_calls = 0
self.down_calls = 0
def power_up(self):
self.up_calls += 1
def power_down(self):
self.down_calls += 1
class _FakeTransport:
def __init__(self):
self.raw_frames = []
def ld_put_raw(self, data, baudrate):
self.raw_frames.append((bytes(data), int(baudrate)))
class _FakeLinDev:
def __init__(self, transport):
self.baudrate = 0
self.tx = []
self._transport = transport
# Pre-canned slave responses keyed by frame_id
self.slave_responses = {0x11: [0x07, 0x00, 0x00, 0x00]}
self.fail_on_recv_id = None
def get_device(self, name):
if name == "bus/transport_layer":
return self._transport
raise KeyError(name)
def send_message(self, master_to_slave, frame_id, data_length, data=None):
if master_to_slave:
self.tx.append((int(frame_id), int(data_length), list(data or [])))
return None
# slave-to-master
if self.fail_on_recv_id == int(frame_id):
raise RuntimeError("simulated rx timeout")
return self.slave_responses.get(int(frame_id))
class _FakeLinMaster:
def __init__(self):
self.setup_calls = 0
self.teardown_calls = 0
def setup(self):
self.setup_calls += 1
def teardown(self):
self.teardown_calls += 1
class _FakeMUM:
"""Stand-in for pymumclient.MelexisUniversalMaster()."""
def __init__(self):
self.opened_with = None
self._lin_master = _FakeLinMaster()
self._power = _FakePower()
self._transport = _FakeTransport()
self._lin_dev = _FakeLinDev(self._transport)
def open_all(self, host):
self.opened_with = host
def get_device(self, name):
if name == "lin0":
return self._lin_master
if name == "power_out0":
return self._power
raise KeyError(name)
class _FakeMumModule:
def __init__(self):
self.last = None
def MelexisUniversalMaster(self): # noqa: N802 - matches vendor API
self.last = _FakeMUM()
return self.last
class _FakePylinModule:
"""Stand-in for pylin: provides LinBusManager and LinDevice22."""
def __init__(self, lin_dev_factory):
# lin_dev_factory(lin_bus) returns an object with the .get_device,
# .send_message and .baudrate API used by MumLinInterface.
self._lin_dev_factory = lin_dev_factory
def LinBusManager(self, linmaster): # noqa: N802
return ("bus_for", linmaster)
def LinDevice22(self, lin_bus): # noqa: N802
return self._lin_dev_factory(lin_bus)
# ---- helpers -------------------------------------------------------------
def _build_iface(boot_settle=0.0):
"""Construct a MumLinInterface wired to fake modules; return (iface, fakes)."""
mum_mod = _FakeMumModule()
# Pylin's LinDevice22 should hand back the same FakeLinDev that's
# attached to the MUM instance for this test, so assertions can read tx.
captured = {}
def lin_dev_factory(lin_bus):
# The mum module's get_device('lin0') will be called from connect();
# but pylin.LinDevice22(lin_bus) just needs to expose the same API.
# We pull the FakeLinDev off the FakeMUM that was constructed.
captured["lin_dev"] = mum_mod.last._lin_dev
return mum_mod.last._lin_dev
pylin_mod = _FakePylinModule(lin_dev_factory)
iface = MumLinInterface(
host="10.0.0.1",
boot_settle_seconds=boot_settle,
mum_module=mum_mod,
pylin_module=pylin_mod,
)
return iface, mum_mod, captured
# ---- tests ---------------------------------------------------------------
@pytest.mark.unit
def test_connect_opens_mum_and_powers_up():
iface, mum_mod, _ = _build_iface()
iface.connect()
try:
assert mum_mod.last.opened_with == "10.0.0.1"
assert mum_mod.last._lin_master.setup_calls == 1
assert mum_mod.last._power.up_calls == 1
assert iface._lin_dev.baudrate == 19200
finally:
iface.disconnect()
@pytest.mark.unit
def test_disconnect_powers_down_and_tears_down():
iface, mum_mod, _ = _build_iface()
iface.connect()
iface.disconnect()
assert mum_mod.last._power.down_calls == 1
assert mum_mod.last._lin_master.teardown_calls == 1
@pytest.mark.unit
def test_send_publishes_master_frame():
iface, mum_mod, _ = _build_iface()
iface.connect()
try:
iface.send(LinFrame(id=0x0A, data=bytes([1, 2, 3, 4, 5, 6, 7, 8])))
tx = mum_mod.last._lin_dev.tx
assert tx == [(0x0A, 8, [1, 2, 3, 4, 5, 6, 7, 8])]
finally:
iface.disconnect()
@pytest.mark.unit
def test_receive_uses_frame_lengths_default():
iface, _, _ = _build_iface()
iface.connect()
try:
frame = iface.receive(id=0x11, timeout=0.1)
assert frame is not None
assert frame.id == 0x11
# Default frame_lengths maps 0x11 -> 4
assert len(frame.data) == 4
assert frame.data[0] == 0x07
finally:
iface.disconnect()
@pytest.mark.unit
def test_receive_returns_none_on_pylin_exception():
iface, mum_mod, _ = _build_iface()
iface.connect()
try:
mum_mod.last._lin_dev.fail_on_recv_id = 0x11
assert iface.receive(id=0x11, timeout=0.1) is None
finally:
iface.disconnect()
@pytest.mark.unit
def test_receive_without_id_raises():
iface, _, _ = _build_iface()
iface.connect()
try:
with pytest.raises(NotImplementedError):
iface.receive(id=None)
finally:
iface.disconnect()
@pytest.mark.unit
def test_send_raw_uses_classic_checksum_path():
iface, mum_mod, _ = _build_iface()
iface.connect()
try:
iface.send_raw(b"\x7f\x06\xb5\xff\x7f\x01\x02\xff")
raw = mum_mod.last._transport.raw_frames
assert len(raw) == 1
assert raw[0][0] == b"\x7f\x06\xb5\xff\x7f\x01\x02\xff"
assert raw[0][1] == 19200
finally:
iface.disconnect()
@pytest.mark.unit
def test_power_cycle_calls_down_then_up():
iface, mum_mod, _ = _build_iface()
iface.connect()
try:
iface.power_cycle(wait=0.0)
finally:
iface.disconnect()
assert mum_mod.last._power.up_calls >= 2 # initial connect + cycle
assert mum_mod.last._power.down_calls >= 1

405
vendor/4SEVEN_color_lib_test.ldf vendored Normal file
View File

@ -0,0 +1,405 @@
LIN_description_file;
LIN_protocol_version = "2.1";
LIN_language_version = "2.1";
LIN_speed = 19.2 kbps;
Nodes {
Master: Master_Node, 5 ms, 0.5 ms ;
Slaves: ALM_Node ;
}
Signals {
AmbLightColourRed:8,0x00,Master_Node,ALM_Node;
AmbLightColourGreen:8,0x00,Master_Node,ALM_Node;
AmbLightColourBlue:8,0x00,Master_Node,ALM_Node;
AmbLightIntensity:8,0x00,Master_Node,ALM_Node;
AmbLightUpdate:2,0x0,Master_Node,ALM_Node;
AmbLightMode:6,0x0,Master_Node,ALM_Node;
AmbLightDuration:8,0x00,Master_Node,ALM_Node;
AmbLightLIDFrom:8,0x00,Master_Node,ALM_Node;
AmbLightLIDTo:8,0x00,Master_Node,ALM_Node;
ALMNVMStatus:4,0x0,ALM_Node,Master_Node;
ALMThermalStatus:4,0x0,ALM_Node,Master_Node;
ALMNadNo:8,0x00,ALM_Node,Master_Node;
SigCommErr:1,0x0,ALM_Node,Master_Node;
ALMVoltageStatus:4,0x0,ALM_Node,Master_Node;
ALMLEDState:2,0x0,ALM_Node,Master_Node;
ColorConfigFrameRed_X: 16, 5665, Master_Node, ALM_Node ;
ColorConfigFrameRed_Y: 16, 2396, Master_Node, ALM_Node ;
ColorConfigFrameRed_Z: 16, 0, Master_Node, ALM_Node ;
ColorConfigFrameGreen_X: 16, 1094, Master_Node, ALM_Node ;
ColorConfigFrameGreen_Y: 16, 5534, Master_Node, ALM_Node ;
ColorConfigFrameGreen_Z: 16, 996, Master_Node, ALM_Node ;
ColorConfigFrameBlue_X: 16, 9618, Master_Node, ALM_Node ;
ColorConfigFrameBlue_Y: 16, 0, Master_Node, ALM_Node ;
ColorConfigFrameBlue_Z: 16, 51922, Master_Node, ALM_Node ;
PWM_Frame_Red: 16, 0, ALM_Node, Master_Node ;
PWM_Frame_Green: 16, 0, ALM_Node, Master_Node ;
PWM_Frame_Blue1: 16, 0, ALM_Node, Master_Node ;
ConfigFrame_Calibration: 1, 0, Master_Node, ALM_Node ;
PWM_Frame_Blue2: 16, 0, ALM_Node, Master_Node ;
ColorConfigFrameRed_Vf_Cal: 16, 2031, Master_Node, ALM_Node ;
ColorConfigFrameGreen_VfCal: 16, 2903, Master_Node, ALM_Node ;
ColorConfigFrameBlue_VfCal: 16, 2950, Master_Node, ALM_Node ;
VF_Frame_Red_VF: 16, 0, ALM_Node, Master_Node ;
VF_Frame_Green_VF: 16, 0, ALM_Node, Master_Node ;
VF_Frame_Blue1_VF: 16, 0, ALM_Node, Master_Node ;
VF_Frame_VLED: 16, 0, ALM_Node, Master_Node ;
VF_Frame_VS: 16, 0, ALM_Node, Master_Node ;
Tj_Frame_Red: 16, 0, ALM_Node, Master_Node ;
Tj_Frame_Green: 16, 0, ALM_Node, Master_Node ;
Tj_Frame_Blue: 16, 0, ALM_Node, Master_Node ;
ConfigFrame_MaxLM: 16, 3840, Master_Node, ALM_Node ;
Calibration_status: 1, 0, ALM_Node, Master_Node ;
Tj_Frame_NTC: 15, 0, ALM_Node, Master_Node ;
PWM_wo_Comp_Red: 16, 0, ALM_Node, Master_Node ;
PWM_wo_Comp_Green: 16, 0, ALM_Node, Master_Node ;
PWM_wo_Comp_Blue: 16, 0, ALM_Node, Master_Node ;
NVM_Static_Valid: 16, 0, ALM_Node, Master_Node ;
NVM_Static_Rev: 16, 0, ALM_Node, Master_Node ;
NVM_Calib_Version: 8, 0, ALM_Node, Master_Node ;
NVM_OADCCAL: 8, 0, ALM_Node, Master_Node ;
NVM_GainADCLowCal: 8, 0, ALM_Node, Master_Node ;
NVM_GainADCHighCal: 8, 0, ALM_Node, Master_Node ;
ConfigFrame_EnableDerating: 1, 1, Master_Node, ALM_Node ;
ConfigFrame_EnableCompensation: 1, 1, Master_Node, ALM_Node ;
}
Diagnostic_signals {
MasterReqB0: 8, 0 ;
MasterReqB1: 8, 0 ;
MasterReqB2: 8, 0 ;
MasterReqB3: 8, 0 ;
MasterReqB4: 8, 0 ;
MasterReqB5: 8, 0 ;
MasterReqB6: 8, 0 ;
MasterReqB7: 8, 0 ;
SlaveRespB0: 8, 0 ;
SlaveRespB1: 8, 0 ;
SlaveRespB2: 8, 0 ;
SlaveRespB3: 8, 0 ;
SlaveRespB4: 8, 0 ;
SlaveRespB5: 8, 0 ;
SlaveRespB6: 8, 0 ;
SlaveRespB7: 8, 0 ;
}
Frames {
ALM_Req_A:0x0A,Master_Node,8{
AmbLightColourRed,0;
AmbLightColourGreen,8;
AmbLightColourBlue,16;
AmbLightIntensity,24;
AmbLightUpdate,32;
AmbLightMode,34;
AmbLightDuration,40;
AmbLightLIDFrom,48;
AmbLightLIDTo,56;
}
ALM_Status:0x11,ALM_Node,4{
ALMNVMStatus,16;
SigCommErr,24;
ALMLEDState,20;
ALMVoltageStatus,8;
ALMNadNo,0;
ALMThermalStatus,12;
}
ColorConfigFrameRed: 3, Master_Node, 8 {
ColorConfigFrameRed_X, 0 ;
ColorConfigFrameRed_Y, 16 ;
ColorConfigFrameRed_Z, 32 ;
ColorConfigFrameRed_Vf_Cal, 48 ;
}
ColorConfigFrameGreen: 4, Master_Node, 8 {
ColorConfigFrameGreen_X, 0 ;
ColorConfigFrameGreen_Y, 16 ;
ColorConfigFrameGreen_Z, 32 ;
ColorConfigFrameGreen_VfCal, 48 ;
}
ColorConfigFrameBlue: 5, Master_Node, 8 {
ColorConfigFrameBlue_X, 0 ;
ColorConfigFrameBlue_Y, 16 ;
ColorConfigFrameBlue_Z, 32 ;
ColorConfigFrameBlue_VfCal, 48 ;
}
PWM_Frame: 18, ALM_Node, 8 {
PWM_Frame_Red, 0 ;
PWM_Frame_Green, 16 ;
PWM_Frame_Blue1, 32 ;
PWM_Frame_Blue2, 48 ;
}
ConfigFrame: 6, Master_Node, 3 {
ConfigFrame_Calibration, 0 ;
ConfigFrame_MaxLM, 3 ;
ConfigFrame_EnableDerating, 1 ;
ConfigFrame_EnableCompensation, 2 ;
}
VF_Frame: 19, ALM_Node, 8 {
VF_Frame_Red_VF, 0 ;
VF_Frame_Green_VF, 16 ;
VF_Frame_Blue1_VF, 32 ;
VF_Frame_VLED, 48 ;
}
Tj_Frame: 20, ALM_Node, 8 {
Tj_Frame_Red, 0 ;
Tj_Frame_Green, 16 ;
Tj_Frame_Blue, 32 ;
Calibration_status, 63 ;
Tj_Frame_NTC, 48 ;
}
PWM_wo_Comp: 21, ALM_Node, 8 {
PWM_wo_Comp_Red, 0 ;
PWM_wo_Comp_Green, 16 ;
PWM_wo_Comp_Blue, 32 ;
VF_Frame_VS, 48 ;
}
NVM_Debug: 22, ALM_Node, 8 {
NVM_Static_Valid, 0 ;
NVM_Static_Rev, 16 ;
NVM_Calib_Version, 32 ;
NVM_OADCCAL, 40 ;
NVM_GainADCLowCal, 48 ;
NVM_GainADCHighCal, 56 ;
}
}
Diagnostic_frames {
MasterReq: 0x3c {
MasterReqB0, 0 ;
MasterReqB1, 8 ;
MasterReqB2, 16 ;
MasterReqB3, 24 ;
MasterReqB4, 32 ;
MasterReqB5, 40 ;
MasterReqB6, 48 ;
MasterReqB7, 56 ;
}
SlaveResp: 0x3d {
SlaveRespB0, 0 ;
SlaveRespB1, 8 ;
SlaveRespB2, 16 ;
SlaveRespB3, 24 ;
SlaveRespB4, 32 ;
SlaveRespB5, 40 ;
SlaveRespB6, 48 ;
SlaveRespB7, 56 ;
}
}
Node_attributes {
ALM_Node {
LIN_protocol = 2.1 ;
configured_NAD = 0x01 ;
initial_NAD = 0x02 ;
product_id = 0x0013, 0x0003, 1 ;
response_error = SigCommErr ;
P2_min = 50.0000 ms ;
ST_min = 20.0000 ms ;
configurable_frames {
ALM_Req_A;
ALM_Status;
ColorConfigFrameRed ;
ColorConfigFrameGreen ;
ColorConfigFrameBlue ;
PWM_Frame ;
ConfigFrame ;
VF_Frame ;
Tj_Frame ;
PWM_wo_Comp ;
NVM_Debug ;
}
}
}
Schedule_tables {
LIN_AA {
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x1, 0x2, 0xFF } delay 50 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x1 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x2 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x3 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x4 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x5 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x6 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x7 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x8 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x9 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0xA } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0xB } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0xC } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0xD } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0xE } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0xF } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x2, 0x2, 0x10 } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x3, 0x2, 0xFF } delay 20 ms ;
FreeFormat { 0x7F, 0x6, 0xB5, 0xFF, 0x7F, 0x4, 0x2, 0xFF } delay 20 ms ;
}
User_serv {
ALM_Req_A delay 10.0000 ms ;
}
Pub_serv {
ALM_Status delay 20.0000 ms ;
}
RequestResponse {
ALM_Req_A delay 10 ms ;
ALM_Status delay 10 ms ;
}
CCO {
ALM_Req_A delay 10 ms ;
ALM_Status delay 10 ms ;
ConfigFrame delay 10 ms ;
ColorConfigFrameRed delay 10 ms ;
ColorConfigFrameGreen delay 10 ms ;
ColorConfigFrameBlue delay 10 ms ;
VF_Frame delay 10 ms ;
PWM_Frame delay 10 ms ;
Tj_Frame delay 10 ms ;
PWM_wo_Comp delay 10 ms ;
}
calib {
NVM_Debug delay 10 ms ;
}
}
Signal_encoding_types {
Red {
physical_value,0,255,1.0000,0.0000,"Red" ;
}
Green {
physical_value,0,255,1.0000,0.0000,"Green" ;
}
Blue {
physical_value,0,255,1.0000,0.0000,"Blue" ;
}
Intensity {
physical_value,0,255,1.0000,0.0000,"Intensity" ;
}
Update {
logical_value,0x00,"Immediate color Update" ;
logical_value,0x01,"Color memorization" ;
logical_value,0x02,"Apply memorized color" ;
logical_value,0x03,"Discard memorized color" ;
}
Mode {
logical_value,0x00,"Immediate Setpoint" ;
logical_value,0x01,"Fading effect 1 (color and intensity fade)" ;
logical_value,0x02,"Fading effect 2 (intensity fade only; color changes immediately)" ;
logical_value,0x03,"TBD" ;
logical_value,0x04,"TBD" ;
physical_value,5,63,1.0000,0.0000,"Not Used" ;
}
Duration {
physical_value,0,255,0.2000,0.0000,"s" ;
}
ModuleID {
physical_value,0,255,1.0000,0.0000,"ModuleID" ;
}
NVMStatus {
logical_value,0x00,"NVM OK" ;
logical_value,0x01,"NVM NOK" ;
logical_value,0x02,"Reserved" ;
logical_value,0x03,"Reserved" ;
logical_value,0x04,"Reserved" ;
logical_value,0x05,"Reserved" ;
logical_value,0x06,"Reserved" ;
logical_value,0x07,"Reserved" ;
logical_value,0x08,"Reserved" ;
logical_value,0x09,"Reserved" ;
logical_value,0x0A,"Reserved" ;
logical_value,0x0B,"Reserved" ;
logical_value,0x0C,"Reserved" ;
logical_value,0x0D,"Reserved" ;
logical_value,0x0E,"Reserved" ;
logical_value,0x0F,"Reserved" ;
}
VoltageStatus {
logical_value,0x00,"Normal Voltage" ;
logical_value,0x01,"Power UnderVoltage" ;
logical_value,0x02,"Power OverVoltage" ;
logical_value,0x03,"Reserved" ;
logical_value,0x04,"Reserved" ;
logical_value,0x05,"Reserved" ;
logical_value,0x06,"Reserved" ;
logical_value,0x07,"Reserved" ;
logical_value,0x08,"Reserved" ;
logical_value,0x09,"Reserved" ;
logical_value,0x0A,"Reserved" ;
logical_value,0x0B,"Reserved" ;
logical_value,0x0C,"Reserved" ;
logical_value,0x0D,"Reserved" ;
logical_value,0x0E,"Reserved" ;
logical_value,0x0F,"Reserved" ;
}
ThermalStatus {
logical_value,0x00,"Normal Temperature" ;
logical_value,0x01,"Thermal derating" ;
logical_value,0x02,"Thermal shutdown" ;
logical_value,0x03,"Reserved" ;
logical_value,0x04,"Reserved" ;
logical_value,0x05,"Reserved" ;
logical_value,0x06,"Reserved" ;
logical_value,0x07,"Reserved" ;
logical_value,0x08,"Reserved" ;
logical_value,0x09,"Reserved" ;
logical_value,0x0A,"Reserved" ;
logical_value,0x0B,"Reserved" ;
logical_value,0x0C,"Reserved" ;
logical_value,0x0D,"Reserved" ;
logical_value,0x0E,"Reserved" ;
logical_value,0x0F,"Reserved" ;
}
LED_State {
logical_value,0x00,"LED OFF" ;
logical_value,0x01,"LED ANIMATING" ;
logical_value,0x02,"LED ON" ;
logical_value,0x03,"Reserved" ;
}
NVM_Static_Valid_Encoding {
logical_value, 0, "NVM Corrupted/Zero" ;
logical_value, 42331, "NVM Valid (0xA55B)" ;
logical_value, 65535, "NVM Empty/Erased" ;
}
NVM_Static_Rev_Encoding {
logical_value, 0, "Invalid Revision" ;
logical_value, 1, "Revision 1 (Current)" ;
logical_value, 65535, "Not Programmed" ;
}
NVM_Calib_Version_Encoding {
physical_value, 0, 255, 1, 0, "Factory Calib Version (>=1 valid)" ;
}
NVM_OADCCAL_Encoding {
physical_value, 0, 255, 1, 0, "ADC Offset Cal (signed 8-bit)" ;
}
NVM_GainADCLowCal_Encoding {
physical_value, 0, 255, 1, 0, "ADC Gain Low Temp (signed 8-bit)" ;
}
NVM_GainADCHighCal_Encoding {
physical_value, 0, 255, 1, 0, "ADC Gain High Temp (signed 8-bit)" ;
}
}
Signal_representation {
Red:AmbLightColourRed;
Green:AmbLightColourGreen;
Blue:AmbLightColourBlue;
Intensity:AmbLightIntensity;
Update:AmbLightUpdate;
Mode:AmbLightMode;
Duration:AmbLightDuration;
ModuleID:AmbLightLIDFrom,AmbLightLIDTo;
NVMStatus:ALMNVMStatus;
LED_State:ALMLEDState;
NVM_Calib_Version_Encoding: NVM_Calib_Version ;
NVM_GainADCHighCal_Encoding: NVM_GainADCHighCal ;
NVM_GainADCLowCal_Encoding: NVM_GainADCLowCal ;
NVM_OADCCAL_Encoding: NVM_OADCCAL ;
NVM_Static_Rev_Encoding: NVM_Static_Rev ;
NVM_Static_Valid_Encoding: NVM_Static_Valid ;
}

Binary file not shown.

BIN
vendor/4SEVEN_color_lib_test.sdf vendored Normal file

Binary file not shown.

Binary file not shown.

321
vendor/automated_lin_test/README.md vendored Normal file
View File

@ -0,0 +1,321 @@
# LIN Automated Test Scripts
Automated test scripts for LIN bus communication and auto-addressing functionality using the Melexis Universal Master (MUM) hardware.
## Purpose
This folder contains Python scripts to automate LIN bus testing without requiring manual tool switching between MUM and babylin. The scripts provide:
- **LIN Auto-Addressing Test**: Automated BSM-SNPD (Bus Shunt Method - Slave Node Position Detection) auto-addressing
- **LED Control Test**: Verify LIN communication by controlling the board LED
- **Power Cycle Utility**: Power cycle the ECU through MUM
- **Dependency Installation**: Automated setup of required Python packages
## Hardware Setup
### Required Hardware
1. **Melexis Universal Master (MUM)**
- BeagleBone-based LIN master device
- Default IP: 192.168.7.2
- LIN interface: lin0
- Power control: power_out0
2. **ALM Platform MLX81124 Board**
- Target ECU with LIN auto-addressing support
- RGB LED for visual feedback
### Hardware Connections
```
┌─────────────────┐ ┌──────────────────┐
│ MUM │ │ ALM Platform │
│ (192.168.7.2) │ │ MLX81124 │
├─────────────────┤ ├──────────────────┤
│ │ │ │
│ LIN (lin0) ├────────────────────┤ LIN │
│ │ │ │
│ Power ├────────────────────┤ VCC/GND │
│ (power_out0) │ │ │
│ │ │ RGB LED │
└─────────────────┘ └──────────────────┘
```
### Connection Details
1. **LIN Bus**: Connect MUM LIN0 to ALM Platform LIN pin
2. **Power**: Connect MUM power_out0 to ALM Platform power (controlled by scripts)
3. **Ground**: Common ground between MUM and ALM Platform
## Files
### Scripts
- **`test_auto_addressing.py`** - Main auto-addressing test
- **`test_led_control.py`** - LED control verification test
- **`power_cycle.py`** - ECU power cycle utility
- **`install_packages.sh`** - Dependency installer
### Configuration
- **`config.py`** - Hardware and protocol configuration
- MUM connection settings
- LIN bus parameters
- BSM-SNPD protocol constants
- Test defaults
## Dependencies
### Python Packages
The scripts require these Python packages:
- `pylin` - LIN bus communication library
- `pymumclient` - Melexis Universal Master client library
### Installation
Run the installer script to set up dependencies:
```bash
./install_packages.sh
```
Or manually install:
```bash
pip3 install pylin pymumclient
```
## Usage
### 1. Auto-Addressing Test
Tests LIN auto-addressing using BSM-SNPD protocol. Automatically selects a target NAD different from the current NAD.
**Basic usage:**
```bash
python3 test_auto_addressing.py
```
**With options:**
```bash
python3 test_auto_addressing.py --iterations 1 --check-interval 1
```
**Parameters:**
- `--host` - MUM IP address (default: 192.168.7.2)
- `--iterations` - Number of auto-addressing iterations (default: 1)
- `--check-interval` - Check status every N iterations (0 = only at end)
**What it does:**
1. Connects to MUM
2. Reads current NAD from ECU
3. Selects target NAD (automatically different from current)
4. Sends BSM-SNPD sequence:
- INIT (0x01) - Initialize auto-addressing
- ASSIGN (0x02) - Assign NAD (16 frames)
- STORE (0x03) - Store to NVM
- FINALIZE (0x04) - Exit auto-addressing mode
5. Polls status frames between iterations
6. Verifies NAD change
**Expected output:**
```
Initial NAD: 0x07
Target NAD: 0x01
SUCCESS! NAD changed from 0x07 to 0x01
```
### 2. LED Control Test
Verifies LIN communication by controlling the RGB LED through color fades.
**Basic usage:**
```bash
python3 test_led_control.py
```
**With options:**
```bash
python3 test_led_control.py --nad 0x02 --cycles 3 --duration 3.0
```
**Parameters:**
- `--host` - MUM IP address (default: 192.168.7.2)
- `--nad` - Node address to control (default: 0x01)
- `--cycles` - Number of fade cycles (default: 3)
- `--duration` - Duration per color in seconds (default: 3.0)
**What it does:**
1. Connects to MUM
2. Reads current NAD from ECU
3. Fades LED through Red → Green → Blue
4. Each color fades in and out smoothly
**Expected output:**
```
Current NAD: 0x02
Fading Red...
Fading Green...
Fading Blue...
LED test complete
```
### 3. Power Cycle Utility
Power cycles the ECU through MUM power control.
**Basic usage:**
```bash
python3 power_cycle.py
```
**With options:**
```bash
python3 power_cycle.py --wait 3.0
```
**Parameters:**
- `--host` - MUM IP address (default: 192.168.7.2)
- `--wait` - Wait time after power down/up in seconds (default: 2.0)
**What it does:**
1. Powers down ECU
2. Waits specified duration
3. Powers up ECU
4. Waits for ECU to boot
## Configuration
All hardware-specific settings are centralized in [`config.py`](config.py). Edit this file to match your setup:
### Common Settings to Modify
```python
# MUM Configuration
MUM_HOST = '192.168.7.2' # Change if MUM has different IP
# LIN Bus Configuration
LIN_BAUDRATE = 19200 # Change if using different baudrate
# Test Parameters
AUTOADDRESSING_DEFAULT_ITERATIONS = 1 # Default test iterations
LED_DEFAULT_NAD = 0x01 # Default NAD for LED test
```
## Firmware Requirements
The firmware must have auto-addressing enabled with twist detection disabled for single-node MUM testing:
**File:** `02-Software/02-Source-Code/code/src/03-HAL/LAA/cfg/HAL_LAA_cfg.h`
```c
#define HAL_LAA_LINAATWISTDETECTDISABLE (1u)
```
This allows the `LASTSLAVE` flag to be set directly without requiring multi-node hardware setup.
## Troubleshooting
### MUM Connection Issues
**Problem:** Cannot connect to MUM
```
Error: Connection to 192.168.7.2 failed
```
**Solution:**
1. Check MUM is powered and connected via USB
2. Verify IP address with `ip addr show` or `ifconfig`
3. Ping MUM: `ping 192.168.7.2`
4. Check USB connection is recognized: `lsusb`
### No Response from ECU
**Problem:** ECU not responding to LIN frames
```
Error: S2M frame receiving failed with error code: 3 - Rx timeout error
```
**Solution:**
1. Check LIN bus connections
2. Verify ECU is powered (use power_cycle.py)
3. Check baudrate matches (19200)
4. Verify NAD is correct
### NAD Not Changing
**Problem:** Auto-addressing completes but NAD doesn't change
**Solution:**
1. Verify firmware has `HAL_LAA_LINAATWISTDETECTDISABLE = 1`
2. Rebuild and flash firmware
3. Check initial NAD is in valid range (0x01-0x10)
4. Run test with `--check-interval 1` to see intermediate status
### LED Not Changing
**Problem:** LED control test doesn't change LED color
**Solution:**
1. Verify NAD parameter matches ECU NAD
2. Check `ALM_Req_A` frame ID is 0x01 in LDF
3. Run auto-addressing test first to verify communication
4. Check LED connections on hardware
## Integration with Build/Flash Pipeline
These tests integrate with the automated firmware development pipeline:
```bash
# 1. Modify firmware
vim 02-Software/02-Source-Code/code/src/...
# 2. Build
./00-Tools/migrate_mlx_tools_linux/build_linux.sh
# 3. Flash
./00-Tools/migrate_mlx_tools_linux/flash_linux.sh
# 4. Test auto-addressing
python3 00-Tools/automated_lin_test/test_auto_addressing.py
# 5. Verify LED control
python3 00-Tools/automated_lin_test/test_led_control.py
```
## Technical Details
### LIN Frame IDs
- `0x3C` - MasterReq (diagnostic frames)
- `0x11` - ALM_Status (4 bytes, contains NAD in byte 0)
- `0x01` - ALM_Req_A (8 bytes, LED control)
### BSM-SNPD Protocol
Auto-addressing uses diagnostic service 0xB5 with subfunctions:
- `0x01` - INIT: Enable auto-addressing mode
- `0x02` - ASSIGN: Assign NAD to node
- `0x03` - STORE: Save NAD to NVM
- `0x04` - FINALIZE: Exit auto-addressing mode
Frame structure:
```
Byte 0: NAD = 0x7F (broadcast)
Byte 1: PCI = 0x06 (6 data bytes)
Byte 2: SID = 0xB5 (BSM-SNPD service)
Byte 3: Supplier ID LSB = 0xFF
Byte 4: Supplier ID MSB = 0x7F
Byte 5: Subfunction
Byte 6: Parameter 1
Byte 7: Parameter 2
```
### Checksum Requirements
**Critical:** BSM frames must use **LIN 1.x Classic checksum**. The scripts use `ld_put_raw()` to ensure Classic checksum. Using `send_message()` with Enhanced checksum will cause frames to be rejected by firmware.
## License
Part of the ALM Platform MLX81124 project.

Binary file not shown.

190
vendor/automated_lin_test/config.py vendored Normal file
View File

@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
Configuration file for LIN automated test scripts
This file contains all hardware-specific settings and tool dependencies.
Modify these values to match your test setup.
"""
# ============================================================================
# Hardware Configuration
# ============================================================================
# MUM (Melexis Universal Master) Configuration
MUM_HOST = '192.168.7.2' # Default MUM IP address on BeagleBone
MUM_LIN_DEVICE = 'lin0' # LIN interface name on MUM
MUM_POWER_DEVICE = 'power_out0' # Power control device name
# LIN Bus Configuration
LIN_BAUDRATE = 19200 # LIN bus baudrate in bps
# Valid NAD range for auto-addressing
VALID_NAD_RANGE = range(0x01, 0x11) # NADs 0x01 through 0x10
# ============================================================================
# External Tool Dependencies
# ============================================================================
# Python packages required (install with: pip3 install <package>)
REQUIRED_PACKAGES = [
'pylin', # LIN bus communication library
'pymumclient', # Melexis Universal Master client library
]
# ============================================================================
# Test Parameters
# ============================================================================
# Auto-addressing test defaults
AUTOADDRESSING_DEFAULT_ITERATIONS = 1 # Number of BSM iterations
AUTOADDRESSING_POLL_DURATION = 2.0 # Status polling duration between iterations (seconds)
AUTOADDRESSING_STATUS_POLL_INTERVAL = 0.020 # Status frame poll interval (20ms)
# LED control test defaults
LED_DEFAULT_NAD = 0x01 # Default NAD for LED control test
# Power cycle defaults
POWER_CYCLE_WAIT_TIME = 2.0 # Wait time after power down/up (seconds)
# ============================================================================
# Frame IDs (from 4SEVEN_color_lib_test.ldf)
# ============================================================================
LIN_FRAME_ID_MASTERREQ = 0x3C # Diagnostic master request frame
LIN_FRAME_ID_ALM_STATUS = 0x11 # ALM_Status (slave-to-master, 4 bytes)
LIN_FRAME_ID_ALM_REQ_A = 0x0A # ALM_Req_A (master-to-slave, 8 bytes, LED control)
LIN_FRAME_ID_CONFIG_FRAME = 0x06 # ConfigFrame (master-to-slave, 3 bytes)
LIN_FRAME_ID_VF_FRAME = 0x13 # VF_Frame (slave-to-master, 8 bytes, LED forward voltages + VLED)
LIN_FRAME_ID_PWM_WO_COMP = 0x15 # PWM_wo_Comp (slave-to-master, 8 bytes, PWM values + VS)
# ============================================================================
# Frame Definitions (from 4SEVEN_color_lib_test.ldf)
# ============================================================================
# Each entry mirrors the LDF Frames section. The signal tuple is:
# 'SignalName': (start_bit, width_in_bits)
# where start_bit comes from the LDF Frames block and width comes from
# the LDF Signals section. To update after an LDF change, copy the new
# Frames entry here and adjust widths from the Signals section.
#
# NAD selection for ALM_Req_A:
# node responds if AmbLightLIDFrom <= ALMNadNo <= AmbLightLIDTo
# single node -> set both to the target NAD
# broadcast -> AmbLightLIDFrom=0x01, AmbLightLIDTo=0xFF
# ALM_Req_A: 0x0A, Master_Node, 8
ALM_REQ_A_FRAME = {
'frame_id': LIN_FRAME_ID_ALM_REQ_A,
'length': 8,
'signals': {
'AmbLightColourRed': (0, 8), # AmbLightColourRed, 0;
'AmbLightColourGreen': (8, 8), # AmbLightColourGreen, 8;
'AmbLightColourBlue': (16, 8), # AmbLightColourBlue, 16;
'AmbLightIntensity': (24, 8), # AmbLightIntensity, 24;
'AmbLightUpdate': (32, 2), # AmbLightUpdate, 32;
'AmbLightMode': (34, 6), # AmbLightMode, 34;
'AmbLightDuration': (40, 8), # AmbLightDuration, 40;
'AmbLightLIDFrom': (48, 8), # AmbLightLIDFrom, 48;
'AmbLightLIDTo': (56, 8), # AmbLightLIDTo, 56;
},
}
# ALM_Status: 0x11, ALM_Node, 4
ALM_STATUS_FRAME = {
'frame_id': LIN_FRAME_ID_ALM_STATUS,
'length': 4,
'signals': {
'ALMNadNo': (0, 8), # ALMNadNo, 0;
'ALMVoltageStatus': (8, 4), # ALMVoltageStatus, 8;
'ALMThermalStatus': (12, 4), # ALMThermalStatus, 12;
'ALMNVMStatus': (16, 4), # ALMNVMStatus, 16;
'ALMLEDState': (20, 2), # ALMLEDState, 20;
'SigCommErr': (24, 1), # SigCommErr, 24;
},
}
# ConfigFrame: 6, Master_Node, 3
CONFIG_FRAME = {
'frame_id': LIN_FRAME_ID_CONFIG_FRAME,
'length': 3,
'signals': {
'ConfigFrame_Calibration': (0, 1), # ConfigFrame_Calibration, 0;
'ConfigFrame_EnableDerating': (1, 1), # ConfigFrame_EnableDerating, 1;
'ConfigFrame_EnableCompensation': (2, 1), # ConfigFrame_EnableCompensation, 2;
'ConfigFrame_MaxLM': (3, 16), # ConfigFrame_MaxLM, 3;
},
}
# VF_Frame: 19 (0x13), ALM_Node, 8
VF_FRAME = {
'frame_id': LIN_FRAME_ID_VF_FRAME,
'length': 8,
'signals': {
'VF_Frame_Red_VF': (0, 16), # VF_Frame_Red_VF, 0;
'VF_Frame_Green_VF': (16, 16), # VF_Frame_Green_VF, 16;
'VF_Frame_Blue1_VF': (32, 16), # VF_Frame_Blue1_VF, 32;
'VF_Frame_VLED': (48, 16), # VF_Frame_VLED, 48;
},
}
# PWM_wo_Comp: 21 (0x15), ALM_Node, 8
PWM_WO_COMP_FRAME = {
'frame_id': LIN_FRAME_ID_PWM_WO_COMP,
'length': 8,
'signals': {
'PWM_wo_Comp_Red': (0, 16), # PWM_wo_Comp_Red, 0;
'PWM_wo_Comp_Green': (16, 16), # PWM_wo_Comp_Green, 16;
'PWM_wo_Comp_Blue': (32, 16), # PWM_wo_Comp_Blue, 32;
'VF_Frame_VS': (48, 16), # VF_Frame_VS, 48;
},
}
def pack_frame(frame_def, **signals):
"""Pack signal values into a byte list using a frame definition.
Unlisted signals default to 0. Bit ordering follows the LDF/LIN
convention: bit 0 of the signal sits at start_bit in the frame,
packed little-endian within each byte.
"""
data = bytearray(frame_def['length'])
for name, value in signals.items():
start_bit, width = frame_def['signals'][name]
value = int(value) & ((1 << width) - 1)
for i in range(width):
bit_pos = start_bit + i
if value & (1 << i):
data[bit_pos // 8] |= 1 << (bit_pos % 8)
return list(data)
def unpack_frame(frame_def, data):
"""Unpack a received byte sequence into a dict of signal values."""
result = {}
for name, (start_bit, width) in frame_def['signals'].items():
value = 0
for i in range(width):
bit_pos = start_bit + i
if data[bit_pos // 8] & (1 << (bit_pos % 8)):
value |= 1 << i
result[name] = value
return result
# ============================================================================
# BSM-SNPD Protocol Constants
# ============================================================================
BSM_NAD_BROADCAST = 0x7F # Broadcast NAD for BSM frames
BSM_PCI = 0x06 # Protocol Control Information (6 data bytes)
BSM_SID = 0xB5 # Service ID for BSM-SNPD
BSM_SUPPLIER_ID_LSB = 0xFF # Supplier ID LSB (broadcast)
BSM_SUPPLIER_ID_MSB = 0x7F # Supplier ID MSB (broadcast)
# BSM Subfunctions
BSM_SUBF_INIT = 0x01 # Initialize auto-addressing
BSM_SUBF_ASSIGN = 0x02 # Assign NAD
BSM_SUBF_STORE = 0x03 # Store to NVM
BSM_SUBF_FINALIZE = 0x04 # Finalize auto-addressing
# Timing parameters (matching babylin behavior)
BSM_INIT_DELAY = 0.050 # Delay after INIT subfunction (50ms)
BSM_FRAME_DELAY = 0.020 # Delay between frames (20ms)

Binary file not shown.

View File

@ -0,0 +1,71 @@
#!/bin/bash
# Install Melexis Python packages to system Python
echo "Installing Melexis LIN packages to system Python..."
MELEXIS_SITE_PACKAGES="/mnt/WINDRV/InstalledPrograms/Melexis IDE/plugins/com.melexis.mlxide.python_1.2.0.202408130945/python/Lib/site-packages"
# Try to install from Melexis packages
if [ -d "$MELEXIS_SITE_PACKAGES" ]; then
echo "Found Melexis packages at: $MELEXIS_SITE_PACKAGES"
# Copy packages to system site-packages
SYSTEM_SITE_PACKAGES=$(python3 -c "import site; print(site.getsitepackages()[0])" 2>/dev/null)
if [ -z "$SYSTEM_SITE_PACKAGES" ]; then
echo "Error: Could not determine system site-packages directory"
exit 1
fi
echo "System site-packages: $SYSTEM_SITE_PACKAGES"
# Check if we have write permissions
if [ ! -w "$SYSTEM_SITE_PACKAGES" ]; then
echo "Note: You may need sudo to install packages system-wide"
SUDO="sudo"
else
SUDO=""
fi
# Copy packages
echo "Copying pylin..."
$SUDO cp -r "$MELEXIS_SITE_PACKAGES/pylin" "$SYSTEM_SITE_PACKAGES/"
$SUDO cp -r "$MELEXIS_SITE_PACKAGES/pylin-"*".dist-info" "$SYSTEM_SITE_PACKAGES/"
echo "Copying pylinframe..."
$SUDO cp -r "$MELEXIS_SITE_PACKAGES/pylinframe" "$SYSTEM_SITE_PACKAGES/"
$SUDO cp -r "$MELEXIS_SITE_PACKAGES/pylinframe-"*".dist-info" "$SYSTEM_SITE_PACKAGES/"
echo "Copying pymumclient..."
$SUDO cp -r "$MELEXIS_SITE_PACKAGES/pymumclient" "$SYSTEM_SITE_PACKAGES/"
$SUDO cp -r "$MELEXIS_SITE_PACKAGES/pymumclient-"*".dist-info" "$SYSTEM_SITE_PACKAGES/"
# Copy all dependencies
echo "Copying all Melexis dependencies..."
for pkg_dir in "$MELEXIS_SITE_PACKAGES"/*; do
pkg=$(basename "$pkg_dir")
# Skip dist-info directories and __pycache__
if [[ "$pkg" == *".dist-info" ]] || [[ "$pkg" == "__pycache__" ]]; then
continue
fi
# Only copy directories (packages)
if [ -d "$pkg_dir" ]; then
echo " - $pkg"
$SUDO cp -r "$pkg_dir" "$SYSTEM_SITE_PACKAGES/"
# Copy corresponding .dist-info if exists
$SUDO cp -r "$MELEXIS_SITE_PACKAGES/${pkg}-"*".dist-info" "$SYSTEM_SITE_PACKAGES/" 2>/dev/null || true
fi
done
echo ""
echo "Installation complete!"
echo ""
echo "Verifying installation..."
python3 -c "import pylin; import pymumclient; print('✓ Packages imported successfully')" && echo "Success!" || echo "Failed - some packages missing"
else
echo "Error: Melexis packages not found"
exit 1
fi

Binary file not shown.

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""
Power cycle the ECU via MUM
"""
import argparse
import time
from pymumclient import MelexisUniversalMaster
from config import MUM_HOST, MUM_POWER_DEVICE, POWER_CYCLE_WAIT_TIME
def main():
parser = argparse.ArgumentParser(description='Power cycle ECU via MUM')
parser.add_argument('--host', default=MUM_HOST,
help=f'MUM IP address (default: {MUM_HOST})')
parser.add_argument('--wait', type=float, default=POWER_CYCLE_WAIT_TIME,
help=f'Wait time in seconds (default: {POWER_CYCLE_WAIT_TIME})')
args = parser.parse_args()
print(f"Connecting to MUM at {args.host}...")
mum = MelexisUniversalMaster()
mum.open_all(args.host)
power_control = mum.get_device(MUM_POWER_DEVICE)
print("Powering down ECU...")
power_control.power_down()
print(f"Waiting {args.wait} seconds...")
time.sleep(args.wait)
print("Powering up ECU...")
power_control.power_up()
print(f"Waiting {args.wait} seconds for ECU to boot...")
time.sleep(args.wait)
print("Power cycle complete!")
if __name__ == "__main__":
main()

Binary file not shown.

View File

@ -0,0 +1,493 @@
#!/usr/bin/env python3
"""
LIN ADC Measurement Verification Test
This test reads ADC measurement values from the ECU over LIN and verifies
they are within expected ranges across multiple LED states.
Test cases:
1. All LEDs off
2. Only Red on (color=255, intensity=255)
3. Only Green on (color=255, intensity=255)
4. Only Blue on (color=255, intensity=255)
5. All LEDs on (color=255, intensity=255)
Verified signals:
- VF_Frame_VS: Supply voltage (expected ~12V = ~12000 mV)
- VF_Frame_VLED: DC-DC converter output voltage feeding LEDs (expected ~5V = ~5000 mV)
- VF_Frame_Red_VF: Red LED forward voltage (0 when off, ~1500-3500 mV when on)
- VF_Frame_Green_VF: Green LED forward voltage (0 when off, ~1500-3500 mV when on)
- VF_Frame_Blue1_VF: Blue LED forward voltage (0 when off, ~1500-3500 mV when on)
Frame structures:
ALM_Req_A (ID=0x0A, master-to-slave, 8 bytes):
- Byte 0: AmbLightColourRed (0-255)
- Byte 1: AmbLightColourGreen (0-255)
- Byte 2: AmbLightColourBlue (0-255)
- Byte 3: AmbLightIntensity (0-255)
- Byte 4: AmbLightUpdate[1:0] | (AmbLightMode[5:0] << 2)
- Byte 5: AmbLightDuration (0-255)
- Byte 6: AmbLightLIDFrom (NAD range start set equal to LIDTo to target one node)
- Byte 7: AmbLightLIDTo (NAD range end)
PWM_wo_Comp (ID=0x15, slave-to-master, 8 bytes):
- Byte 0-1: PWM_wo_Comp_Red (16-bit, little-endian)
- Byte 2-3: PWM_wo_Comp_Green (16-bit, little-endian)
- Byte 4-5: PWM_wo_Comp_Blue (16-bit, little-endian)
- Byte 6-7: VF_Frame_VS (16-bit, little-endian, value in mV)
VF_Frame (ID=0x13, slave-to-master, 8 bytes):
- Byte 0-1: VF_Frame_Red_VF (16-bit, little-endian, value in mV)
- Byte 2-3: VF_Frame_Green_VF (16-bit, little-endian, value in mV)
- Byte 4-5: VF_Frame_Blue1_VF (16-bit, little-endian, value in mV)
- Byte 6-7: VF_Frame_VLED (16-bit, little-endian, value in mV)
"""
import argparse
import logging
import time
import sys
from pylin import LinBusManager, LinDevice22
from pymumclient import MelexisUniversalMaster
from config import *
logging.basicConfig(
level=logging.INFO,
format='%(asctime)-15s %(levelname)-8s %(message)s'
)
logger = logging.getLogger(__name__)
# ADC measurement expected ranges (in mV)
VS_EXPECTED_MIN_MV = 10000 # 10.0V minimum
VS_EXPECTED_MAX_MV = 14000 # 14.0V maximum
VS_EXPECTED_NOMINAL_MV = 12000 # 12.0V nominal
VLED_EXPECTED_MIN_MV = 4000 # 4.0V minimum
VLED_EXPECTED_MAX_MV = 6000 # 6.0V maximum
VLED_EXPECTED_NOMINAL_MV = 5000 # 5.0V nominal
# LED forward voltage ranges when LEDs are off
LED_VF_OFF_MIN_MV = 0 # 0V minimum (off)
LED_VF_OFF_MAX_MV = 500 # 0.5V maximum (off, allowing some noise)
# LED forward voltage ranges when LEDs are on
LED_VF_ON_MIN_MV = 1500 # 1.5V minimum (on)
LED_VF_ON_MAX_MV = 3500 # 3.5V maximum (on)
# Settle time after changing LED state (seconds)
LED_SETTLE_TIME = 1.0
def read_alm_status(lin_dev):
"""Read ALM_Status frame and return (ALMNadNo, raw_bytes)."""
try:
response = lin_dev.send_message(
master_to_slave=False,
frame_id=ALM_STATUS_FRAME['frame_id'],
data_length=ALM_STATUS_FRAME['length'],
data=None,
)
if response and len(response) >= ALM_STATUS_FRAME['length']:
parsed = unpack_frame(ALM_STATUS_FRAME, response)
return parsed['ALMNadNo'], response
return None, None
except Exception as e:
logger.error(f"Failed to read ALM_Status: {e}")
return None, None
def send_config_frame(lin_dev, calibration=0, enable_derating=1,
enable_compensation=1, max_lm=3840):
"""Send ConfigFrame to configure calibration, derating and compensation."""
data = pack_frame(CONFIG_FRAME,
ConfigFrame_Calibration=calibration,
ConfigFrame_EnableDerating=enable_derating,
ConfigFrame_EnableCompensation=enable_compensation,
ConfigFrame_MaxLM=max_lm,
)
lin_dev.send_message(
master_to_slave=True,
frame_id=CONFIG_FRAME['frame_id'],
data_length=CONFIG_FRAME['length'],
data=data,
)
def set_led_color(lin_dev, nad, red, green, blue, intensity):
"""Set LED color and intensity via ALM_Req_A frame."""
data = pack_frame(ALM_REQ_A_FRAME,
AmbLightColourRed=red,
AmbLightColourGreen=green,
AmbLightColourBlue=blue,
AmbLightIntensity=intensity,
AmbLightLIDFrom=nad,
AmbLightLIDTo=nad,
)
lin_dev.send_message(
master_to_slave=True,
frame_id=ALM_REQ_A_FRAME['frame_id'],
data_length=ALM_REQ_A_FRAME['length'],
data=data,
)
def read_pwm_wo_comp_frame(lin_dev):
"""
Read PWM_wo_Comp frame from slave.
Returns:
tuple: (raw_bytes, parsed_dict) or (None, None) on failure.
parsed_dict keys: pwm_red, pwm_green, pwm_blue, vs_mv
"""
try:
response = lin_dev.send_message(
master_to_slave=False,
frame_id=PWM_WO_COMP_FRAME['frame_id'],
data_length=PWM_WO_COMP_FRAME['length'],
data=None,
)
if response and len(response) >= PWM_WO_COMP_FRAME['length']:
s = unpack_frame(PWM_WO_COMP_FRAME, response)
return response, {
'pwm_red': s['PWM_wo_Comp_Red'],
'pwm_green': s['PWM_wo_Comp_Green'],
'pwm_blue': s['PWM_wo_Comp_Blue'],
'vs_mv': s['VF_Frame_VS'],
}
return None, None
except Exception as e:
logger.error(f"Failed to read PWM_wo_Comp frame: {e}")
return None, None
def read_vf_frame(lin_dev):
"""
Read VF_Frame from slave.
Returns:
tuple: (raw_bytes, parsed_dict) or (None, None) on failure.
parsed_dict keys: red_vf_mv, green_vf_mv, blue_vf_mv, vled_mv
"""
try:
response = lin_dev.send_message(
master_to_slave=False,
frame_id=VF_FRAME['frame_id'],
data_length=VF_FRAME['length'],
data=None,
)
if response and len(response) >= VF_FRAME['length']:
s = unpack_frame(VF_FRAME, response)
return response, {
'red_vf_mv': s['VF_Frame_Red_VF'],
'green_vf_mv': s['VF_Frame_Green_VF'],
'blue_vf_mv': s['VF_Frame_Blue1_VF'],
'vled_mv': s['VF_Frame_VLED'],
}
return None, None
except Exception as e:
logger.error(f"Failed to read VF_Frame: {e}")
return None, None
def sample_signal(lin_dev, signal_name, read_func, signal_key,
expected_min, expected_max, num_samples=5, sample_interval=0.1):
"""
Read a signal multiple times and verify it is within expected range.
Args:
lin_dev: LinDevice22 instance
signal_name: Display name for the signal
read_func: Function to call to read the frame (returns raw, parsed)
signal_key: Key in parsed dict to extract the signal value
expected_min: Minimum expected value in mV
expected_max: Maximum expected value in mV
num_samples: Number of samples to read
sample_interval: Delay between samples in seconds
Returns:
tuple: (passed, avg_voltage_mv, samples)
"""
samples = []
passed = True
for i in range(num_samples):
raw, parsed = read_func(lin_dev)
if parsed is None:
logger.warning(f" Sample {i+1}/{num_samples}: No response")
continue
value_mv = parsed[signal_key]
samples.append(value_mv)
in_range = expected_min <= value_mv <= expected_max
status = "OK" if in_range else "FAIL"
logger.info(f" Sample {i+1}/{num_samples}: {signal_name} = {value_mv} mV ({value_mv/1000:.2f} V) [{status}]")
if not in_range:
passed = False
if i < num_samples - 1:
time.sleep(sample_interval)
if len(samples) == 0:
logger.error(f" No valid samples received for {signal_name}")
return False, 0, samples
avg_mv = sum(samples) / len(samples)
return passed, avg_mv, samples
def log_signal_summary(signal_name, passed, avg_mv, samples):
"""Log summary statistics for a verified signal."""
if len(samples) > 0:
s_min = min(samples)
s_max = max(samples)
logger.info(f" Average: {avg_mv:.0f} mV ({avg_mv/1000:.2f} V)")
logger.info(f" Min: {s_min} mV ({s_min/1000:.2f} V)")
logger.info(f" Max: {s_max} mV ({s_max/1000:.2f} V)")
logger.info(f" Result: {'PASS' if passed else 'FAIL'}")
else:
logger.error(f" Result: FAIL (no data)")
def verify_adc_signals(lin_dev, num_samples, sample_interval,
expected_red_vf, expected_green_vf, expected_blue_vf):
"""
Verify all ADC signals (VS, VLED, Red_VF, Green_VF, Blue_VF) for the current LED state.
Args:
lin_dev: LinDevice22 instance
num_samples: Number of samples per signal
sample_interval: Delay between samples in seconds
expected_red_vf: Tuple (min_mv, max_mv) for Red forward voltage
expected_green_vf: Tuple (min_mv, max_mv) for Green forward voltage
expected_blue_vf: Tuple (min_mv, max_mv) for Blue forward voltage
Returns:
bool: True if all signals pass, False otherwise
"""
all_passed = True
logger.info(f" --- VS (Supply Voltage) ---")
vs_passed, vs_avg, vs_samples = sample_signal(
lin_dev, "VS", read_pwm_wo_comp_frame, 'vs_mv',
VS_EXPECTED_MIN_MV, VS_EXPECTED_MAX_MV,
num_samples=num_samples, sample_interval=sample_interval
)
log_signal_summary("VS", vs_passed, vs_avg, vs_samples)
if not vs_passed:
all_passed = False
logger.info(f" --- VLED (DC-DC Voltage) ---")
vled_passed, vled_avg, vled_samples = sample_signal(
lin_dev, "VLED", read_vf_frame, 'vled_mv',
VLED_EXPECTED_MIN_MV, VLED_EXPECTED_MAX_MV,
num_samples=num_samples, sample_interval=sample_interval
)
log_signal_summary("VLED", vled_passed, vled_avg, vled_samples)
if not vled_passed:
all_passed = False
led_checks = [
("Red_VF", 'red_vf_mv', expected_red_vf),
("Green_VF", 'green_vf_mv', expected_green_vf),
("Blue_VF", 'blue_vf_mv', expected_blue_vf),
]
for signal_name, signal_key, (exp_min, exp_max) in led_checks:
logger.info(f" --- {signal_name} (expected {exp_min}-{exp_max} mV) ---")
led_passed, led_avg, led_samples = sample_signal(
lin_dev, signal_name, read_vf_frame, signal_key,
exp_min, exp_max,
num_samples=num_samples, sample_interval=sample_interval
)
log_signal_summary(signal_name, led_passed, led_avg, led_samples)
if not led_passed:
all_passed = False
return all_passed
def main():
parser = argparse.ArgumentParser(description='LIN ADC Measurement Verification Test')
parser.add_argument('--host', default=MUM_HOST,
help=f'MUM IP address (default: {MUM_HOST})')
parser.add_argument('--nad', type=lambda x: int(x, 0), default=LED_DEFAULT_NAD,
help=f'Node address (default: 0x{LED_DEFAULT_NAD:02X})')
parser.add_argument('--samples', type=int, default=10,
help='Number of samples to read per signal (default: 10)')
parser.add_argument('--interval', type=float, default=0.1,
help='Interval between samples in seconds (default: 0.1)')
parser.add_argument('--settle-time', type=float, default=LED_SETTLE_TIME,
help=f'Settle time after LED state change (default: {LED_SETTLE_TIME}s)')
args = parser.parse_args()
# Define test cases: (name, red, green, blue, intensity,
# expected_red_vf, expected_green_vf, expected_blue_vf)
test_cases = [
(
"All LEDs OFF",
0, 0, 0, 0,
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
),
(
"Red ON (255/255)",
255, 0, 0, 255,
(LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
),
(
"Green ON (255/255)",
0, 255, 0, 255,
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
(LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
),
(
"Blue ON (255/255)",
0, 0, 255, 255,
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
(LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV),
(LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
),
(
"All LEDs ON (255/255)",
255, 255, 255, 255,
(LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
(LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
(LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV),
),
]
test_results = {}
nad = args.nad # may be updated below after reading ALM_Status
try:
logger.info(f"Connecting to MUM at {args.host}...")
mum = MelexisUniversalMaster()
mum.open_all(args.host)
power_control = mum.get_device(MUM_POWER_DEVICE)
linmaster = mum.get_device(MUM_LIN_DEVICE)
linmaster.setup()
lin_bus = LinBusManager(linmaster)
lin_dev = LinDevice22(lin_bus)
lin_dev.baudrate = LIN_BAUDRATE
lin_dev.nad = args.nad
power_control.power_up()
time.sleep(0.5)
logger.info("MUM connected and LIN bus ready")
logger.info("=" * 70)
logger.info("ADC MEASUREMENT VERIFICATION TEST")
logger.info(f"Samples: {args.samples}, Interval: {args.interval}s, "
f"Settle: {args.settle_time}s")
logger.info("=" * 70)
# Wait for ADC to settle after power-up
logger.info("Waiting for ADC to settle after power-up...")
time.sleep(1.0)
# Read the actual NAD from the node. Using args.nad directly risks
# a silent miss if the node was assigned a different NAD (e.g. via
# auto-addressing), because AmbLightLIDFrom/LIDTo must equal ALMNadNo.
logger.info("Reading node NAD from ALM_Status...")
detected_nad, status_data = read_alm_status(lin_dev)
if detected_nad is not None:
nad = detected_nad
data_hex = ' '.join(f'{b:02X}' for b in status_data)
logger.info(f"Detected NAD: 0x{nad:02X} (Status frame: {data_hex})")
else:
nad = args.nad
logger.warning(f"Could not read NAD, falling back to 0x{nad:02X}")
logger.info("=" * 70)
# Configure: disable derating and compensation so PWM output directly
# reflects the requested color/brightness.
logger.info("Sending ConfigFrame: Calibration=1, Derating=0, Compensation=0")
send_config_frame(lin_dev, calibration=1, enable_derating=0,
enable_compensation=0)
time.sleep(0.1)
# Ensure LEDs are off before starting
set_led_color(lin_dev, nad, 0, 0, 0, 0)
time.sleep(args.settle_time)
for idx, (name, red, green, blue, intensity,
exp_red, exp_green, exp_blue) in enumerate(test_cases, 1):
logger.info("")
logger.info("-" * 70)
logger.info(f"TEST {idx}/{len(test_cases)}: {name}")
logger.info(f" Command: R={red} G={green} B={blue} I={intensity}"
f" -> NAD 0x{nad:02X}")
logger.info("-" * 70)
# Set LED state
set_led_color(lin_dev, nad, red, green, blue, intensity)
logger.info(f" Waiting {args.settle_time}s for ADC to settle...")
time.sleep(args.settle_time)
# Verify all ADC signals
passed = verify_adc_signals(
lin_dev, args.samples, args.interval,
exp_red, exp_green, exp_blue
)
test_results[name] = passed
logger.info(f" >> TEST {idx} {'PASS' if passed else 'FAIL'}")
# Turn LEDs off at the end
set_led_color(lin_dev, nad, 0, 0, 0, 0)
# Summary
logger.info("")
logger.info("=" * 70)
logger.info("TEST SUMMARY")
logger.info("=" * 70)
all_passed = True
for name, passed in test_results.items():
status = "PASS" if passed else "FAIL"
logger.info(f" {status} - {name}")
if not passed:
all_passed = False
logger.info("-" * 70)
if all_passed:
logger.info("RESULT: ALL TESTS PASSED")
else:
logger.info("RESULT: SOME TESTS FAILED")
logger.info("=" * 70)
logger.info("Tearing down...")
linmaster.teardown()
logger.info("Done (ECU still powered)")
sys.exit(0 if all_passed else 1)
except KeyboardInterrupt:
logger.info("")
logger.info("Interrupted by user")
try:
set_led_color(lin_dev, nad, 0, 0, 0, 0)
linmaster.teardown()
except:
pass
sys.exit(130)
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()

Binary file not shown.

View File

@ -0,0 +1,543 @@
#!/usr/bin/env python3
"""
Interactive BABYLIN animation validation for ALM_Req_A.
This script executes the requirement-oriented checks step-by-step and pauses
after each action so the tester can verify physical LED behavior.
Covered checks:
1) AmbLightMode behavior (0 immediate, 1 fade RGBI, 2 immediate color + fade I)
2) AmbLightUpdate save/apply/discard
3) AmbLightDuration scaling (0.2 s/LSB)
4) LID range selection (single-node, broadcast, invalid From>To)
"""
import argparse
import logging
import time
from pylin import LinBusManager, LinDevice22
from pymumclient import MelexisUniversalMaster
from config import *
logging.basicConfig(
level=logging.INFO,
format="%(asctime)-15s %(levelname)-8s %(message)s",
)
logger = logging.getLogger(__name__)
SEPARATOR = "=" * 78
SUB = "-" * 78
# ALM_Status.ALMLedState values
LED_STATE_OFF = 0
LED_STATE_ANIMATING = 1
LED_STATE_ON = 2
LED_STATE_NAMES = {
LED_STATE_OFF: "OFF",
LED_STATE_ANIMATING: "ANIMATING",
LED_STATE_ON: "ON",
}
def pause(msg):
print()
input(f">>> {msg}")
print()
def banner(title):
logger.info(SEPARATOR)
logger.info(title)
logger.info(SEPARATOR)
def section(title):
logger.info(SUB)
logger.info(title)
logger.info(SUB)
def read_alm_status(lin_dev):
"""Return (parsed_dict, raw_bytes) or (None, None)."""
try:
response = lin_dev.send_message(
master_to_slave=False,
frame_id=ALM_STATUS_FRAME["frame_id"],
data_length=ALM_STATUS_FRAME["length"],
data=None,
)
if response and len(response) >= ALM_STATUS_FRAME["length"]:
return unpack_frame(ALM_STATUS_FRAME, response), response
return None, None
except Exception as exc:
logger.error("Failed reading ALM_Status: %s", exc)
return None, None
def read_led_state(lin_dev):
parsed, _ = read_alm_status(lin_dev)
if parsed is None:
return -1
return parsed.get("ALMLEDState", -1)
def read_nad(lin_dev, fallback):
parsed, raw = read_alm_status(lin_dev)
if parsed is None:
logger.warning("Could not read ALM_Status, fallback NAD=0x%02X", fallback)
return fallback
nad = parsed.get("ALMNadNo", fallback)
logger.info("Detected ALMNadNo=0x%02X (raw: %s)", nad, " ".join(f"{b:02X}" for b in raw))
return nad
def send_req(
lin_dev,
*,
red,
green,
blue,
intensity,
update,
mode,
duration,
lid_from,
lid_to,
):
data = pack_frame(
ALM_REQ_A_FRAME,
AmbLightColourRed=red,
AmbLightColourGreen=green,
AmbLightColourBlue=blue,
AmbLightIntensity=intensity,
AmbLightUpdate=update,
AmbLightMode=mode,
AmbLightDuration=duration,
AmbLightLIDFrom=lid_from,
AmbLightLIDTo=lid_to,
)
lin_dev.send_message(
master_to_slave=True,
frame_id=ALM_REQ_A_FRAME["frame_id"],
data_length=ALM_REQ_A_FRAME["length"],
data=data,
)
def observe_state(lin_dev, seconds):
"""Poll status slowly and print changes."""
logger.info("Observing for %.1f s...", seconds)
end_t = time.time() + seconds
last = None
while time.time() < end_t:
st = read_led_state(lin_dev)
if st != last:
name = LED_STATE_NAMES.get(st, f"UNKNOWN({st})")
logger.info(" ALMLEDState -> %s", name)
last = st
time.sleep(0.25)
def guided_step(lin_dev, title, expectation_lines, command_kwargs, observe_s):
section(title)
logger.info("What you should see:")
for line in expectation_lines:
logger.info(" - %s", line)
pause("Press Enter to send this command...")
send_req(lin_dev, **command_kwargs)
observe_state(lin_dev, observe_s)
pause("Verify visually, then press Enter for the next step...")
def main():
parser = argparse.ArgumentParser(description="Interactive ALM animation checks for BABYLIN")
parser.add_argument("--host", default=MUM_HOST, help=f"MUM IP (default: {MUM_HOST})")
parser.add_argument(
"--nad",
type=lambda x: int(x, 0),
default=LED_DEFAULT_NAD,
help=f"Fallback NAD if ALM_Status read fails (default: 0x{LED_DEFAULT_NAD:02X})",
)
parser.add_argument(
"--slow-factor",
type=float,
default=1.0,
help="Multiply wait/observe durations (default: 1.0)",
)
args = parser.parse_args()
mum = None
linmaster = None
lin_dev = None
try:
banner("Connecting to MUM / LIN")
mum = MelexisUniversalMaster()
mum.open_all(args.host)
power_control = mum.get_device(MUM_POWER_DEVICE)
linmaster = mum.get_device(MUM_LIN_DEVICE)
linmaster.setup()
lin_bus = LinBusManager(linmaster)
lin_dev = LinDevice22(lin_bus)
lin_dev.baudrate = LIN_BAUDRATE
lin_dev.nad = args.nad
power_control.power_up()
time.sleep(0.5 * args.slow_factor)
nad = read_nad(lin_dev, args.nad)
lin_dev.nad = nad
banner("Interactive Requirement Validation")
logger.info("Target NAD: 0x%02X", nad)
logger.info("Slow factor: %.2f", args.slow_factor)
logger.info("You will be prompted before and after every test step.")
pause("Press Enter to start from a known OFF baseline...")
# Step 0: Baseline OFF
guided_step(
lin_dev,
"Step 0 - Baseline OFF",
[
"LED should turn OFF quickly.",
"ALMLEDState should become OFF.",
],
{
"red": 0,
"green": 0,
"blue": 0,
"intensity": 0,
"update": 0,
"mode": 0,
"duration": 0,
"lid_from": nad,
"lid_to": nad,
},
1.0 * args.slow_factor,
)
# 1) Mode behavior checks
guided_step(
lin_dev,
"Step 1 - Mode 0 Immediate Setpoint",
[
"Color/intensity should change immediately.",
"No visible fade; direct jump to requested setpoint.",
],
{
"red": 0,
"green": 180,
"blue": 80,
"intensity": 200,
"update": 0,
"mode": 0,
"duration": 10,
"lid_from": nad,
"lid_to": nad,
},
1.0 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 2 - Mode 1 Fade RGB + Intensity (2.0 s)",
[
"RGB and intensity should both transition smoothly.",
"Transition duration should be close to 2.0 s (Duration=10).",
],
{
"red": 255,
"green": 40,
"blue": 0,
"intensity": 220,
"update": 0,
"mode": 1,
"duration": 10,
"lid_from": nad,
"lid_to": nad,
},
3.0 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 3 - Mode 2 Immediate Color + Faded Intensity (2.0 s)",
[
"Color should jump immediately to the new RGB target.",
"Only intensity should ramp over ~2.0 s.",
],
{
"red": 0,
"green": 0,
"blue": 255,
"intensity": 50,
"update": 0,
"mode": 2,
"duration": 10,
"lid_from": nad,
"lid_to": nad,
},
3.0 * args.slow_factor,
)
# 2) Update save/apply/discard checks
guided_step(
lin_dev,
"Step 4 - Update=1 Save (must NOT apply)",
[
"LED output should remain unchanged after this command.",
"No visible color/intensity change should occur.",
],
{
"red": 0,
"green": 255,
"blue": 0,
"intensity": 255,
"update": 1,
"mode": 1,
"duration": 10,
"lid_from": nad,
"lid_to": nad,
},
1.5 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 5 - Update=2 Apply Saved",
[
"Saved command from Step 4 should execute now.",
"Payload in this Apply frame should be ignored by ECU logic.",
"You should see saved behavior (mode/duration/RGBI from Step 4).",
],
{
"red": 7,
"green": 7,
"blue": 7,
"intensity": 7,
"update": 2,
"mode": 0,
"duration": 0,
"lid_from": nad,
"lid_to": nad,
},
3.0 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 6 - Update=3 Discard Saved",
[
"Saved buffer should be cleared.",
"This discard command itself should not change output.",
],
{
"red": 0,
"green": 0,
"blue": 0,
"intensity": 0,
"update": 3,
"mode": 0,
"duration": 0,
"lid_from": nad,
"lid_to": nad,
},
1.5 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 7 - Update=2 After Discard",
[
"No saved command should exist now.",
"Apply should behave like a no-op (no new visible action).",
],
{
"red": 123,
"green": 12,
"blue": 45,
"intensity": 200,
"update": 2,
"mode": 1,
"duration": 5,
"lid_from": nad,
"lid_to": nad,
},
2.0 * args.slow_factor,
)
# 3) Duration scaling checks
guided_step(
lin_dev,
"Step 8 - Duration=1 (expect ~0.2 s)",
[
"Transition should complete very quickly (~0.2 s).",
],
{
"red": 255,
"green": 0,
"blue": 0,
"intensity": 200,
"update": 0,
"mode": 1,
"duration": 1,
"lid_from": nad,
"lid_to": nad,
},
1.5 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 9 - Duration=5 (expect ~1.0 s)",
[
"Transition should take around 1.0 s.",
"Visibly slower than Step 8.",
],
{
"red": 0,
"green": 255,
"blue": 0,
"intensity": 200,
"update": 0,
"mode": 1,
"duration": 5,
"lid_from": nad,
"lid_to": nad,
},
2.0 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 10 - Duration=10 (expect ~2.0 s)",
[
"Transition should take around 2.0 s.",
"Visibly slower than Step 9.",
],
{
"red": 0,
"green": 0,
"blue": 255,
"intensity": 200,
"update": 0,
"mode": 1,
"duration": 10,
"lid_from": nad,
"lid_to": nad,
},
3.0 * args.slow_factor,
)
# 4) LID selection checks
guided_step(
lin_dev,
"Step 11 - LID Single-Node Select (From=To=NAD)",
[
"This node should react (it is explicitly selected).",
],
{
"red": 255,
"green": 120,
"blue": 0,
"intensity": 180,
"update": 0,
"mode": 0,
"duration": 0,
"lid_from": nad,
"lid_to": nad,
},
1.0 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 12 - LID Broadcast Select (From=0, To=255)",
[
"This node should react (broadcast range).",
],
{
"red": 120,
"green": 0,
"blue": 255,
"intensity": 180,
"update": 0,
"mode": 0,
"duration": 0,
"lid_from": 0,
"lid_to": 255,
},
1.0 * args.slow_factor,
)
guided_step(
lin_dev,
"Step 13 - LID Invalid Range (From > To)",
[
"Node should ignore this command.",
"No visible output change is expected.",
],
{
"red": 255,
"green": 255,
"blue": 255,
"intensity": 255,
"update": 0,
"mode": 0,
"duration": 0,
"lid_from": 20,
"lid_to": 10,
},
1.5 * args.slow_factor,
)
pause("All checks done. Press Enter to send final OFF cleanup...")
send_req(
lin_dev,
red=0,
green=0,
blue=0,
intensity=0,
update=0,
mode=0,
duration=0,
lid_from=nad,
lid_to=nad,
)
observe_state(lin_dev, 1.0 * args.slow_factor)
banner("Test sequence completed")
except KeyboardInterrupt:
logger.info("Interrupted by user")
finally:
try:
if lin_dev is not None:
# Best effort: leave node OFF
send_req(
lin_dev,
red=0,
green=0,
blue=0,
intensity=0,
update=0,
mode=0,
duration=0,
lid_from=lin_dev.nad,
lid_to=lin_dev.nad,
)
except Exception:
pass
try:
if linmaster is not None:
linmaster.teardown()
except Exception:
pass
if __name__ == "__main__":
main()

Binary file not shown.

View File

@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""
LIN Auto-Addressing Test - Matching babylin behavior
This test replicates the exact babylin sequence that successfully changed NAD.
Key observations from babylin log:
1. Uses FreeFormat frame (ID 0x3C)
2. Frame structure: [NAD, PCI, SID, SupID_LSB, SupID_MSB, Subf, Param1, Param2]
3. Uses LIN 1.x Classic checksum
4. Loops the auto-addressing schedule multiple times (6+ iterations in babylin log)
5. NAD change happens after several iterations
"""
import argparse
import logging
import time
from pylin import LinBusManager, LinDevice22
from pymumclient import MelexisUniversalMaster
from config import *
logging.basicConfig(
level=logging.INFO,
format='%(asctime)-15s %(levelname)-8s %(message)s'
)
logger = logging.getLogger(__name__)
def read_status(lin_dev):
"""Read ALM_Status frame"""
try:
response = lin_dev.send_message(
master_to_slave=False,
frame_id=LIN_FRAME_ID_ALM_STATUS,
data_length=4,
data=None
)
if response and len(response) > 0:
return response[0], response
return None, None
except Exception as e:
logger.error(f"Failed to read status: {e}")
return None, None
def send_bsm_frame(transport_layer, subfunction, param1, param2):
"""
Send BSM-SNPD diagnostic frame with Classic checksum.
Uses ld_put_raw() which sends with LIN 1.x Classic checksum (like babylin).
send_message() uses Enhanced checksum which causes firmware to reject frames.
"""
try:
data = bytearray([
BSM_NAD_BROADCAST,
BSM_PCI,
BSM_SID,
BSM_SUPPLIER_ID_LSB,
BSM_SUPPLIER_ID_MSB,
subfunction,
param1,
param2
])
transport_layer.ld_put_raw(data=data, baudrate=LIN_BAUDRATE)
return True
except Exception as e:
logger.error(f"Failed to send BSM frame: {e}")
return False
def poll_status_frames(lin_dev, duration_seconds=AUTOADDRESSING_POLL_DURATION):
"""
Poll status frames for a specified duration, matching babylin's Pub_serv schedule.
This acts as a keepalive and gives the ECU time to process.
"""
start_time = time.time()
poll_count = 0
while (time.time() - start_time) < duration_seconds:
try:
lin_dev.send_message(
master_to_slave=False,
frame_id=LIN_FRAME_ID_ALM_STATUS,
data_length=4,
data=[]
)
poll_count += 1
time.sleep(AUTOADDRESSING_STATUS_POLL_INTERVAL)
except Exception:
# Ignore timeout errors during polling
time.sleep(AUTOADDRESSING_STATUS_POLL_INTERVAL)
logger.debug(f" Polled status {poll_count} times over {duration_seconds:.1f}s")
def run_auto_addressing_sequence(transport_layer, target_nad):
"""
Run one complete auto-addressing sequence matching babylin.
Babylin sequence:
1. INIT (subf 0x01)
2. Wait 50ms
3. 16x NAD assignments (subf 0x02) with 20ms delays
4. STORE (subf 0x03)
5. FINALIZE (subf 0x04)
Args:
transport_layer: LIN transport layer for sending frames
target_nad: Target NAD to assign (will be placed first in sequence)
"""
# Step 1: Initialize auto-addressing mode
logger.debug(" INIT (0x01)")
if not send_bsm_frame(transport_layer, BSM_SUBF_INIT, 0x02, 0xFF):
return False
time.sleep(BSM_INIT_DELAY)
# Step 2: Send 16 NAD assignment frames
# Put target NAD first in sequence to ensure it gets assigned
nad_sequence = list(VALID_NAD_RANGE)
# Move target_nad to the front of the sequence
if target_nad in nad_sequence:
nad_sequence.remove(target_nad)
nad_sequence.insert(0, target_nad)
for nad in nad_sequence:
logger.debug(f" ASSIGN NAD 0x{nad:02X} (0x02)")
if not send_bsm_frame(transport_layer, BSM_SUBF_ASSIGN, 0x02, nad):
return False
time.sleep(BSM_FRAME_DELAY)
# Step 3: Store configuration
logger.debug(" STORE (0x03)")
if not send_bsm_frame(transport_layer, BSM_SUBF_STORE, 0x02, 0xFF):
return False
time.sleep(BSM_FRAME_DELAY)
# Step 4: Finalize
logger.debug(" FINALIZE (0x04)")
if not send_bsm_frame(transport_layer, BSM_SUBF_FINALIZE, 0x02, 0xFF):
return False
time.sleep(BSM_FRAME_DELAY)
return True
def main():
parser = argparse.ArgumentParser(description='LIN Auto-Addressing Test')
parser.add_argument('--host', default=MUM_HOST,
help=f'MUM IP address (default: {MUM_HOST})')
parser.add_argument('--iterations', type=int, default=AUTOADDRESSING_DEFAULT_ITERATIONS,
help=f'Number of auto-addressing iterations (default: {AUTOADDRESSING_DEFAULT_ITERATIONS})')
parser.add_argument('--check-interval', type=int, default=0,
help='Check status every N iterations (0=only at end)')
args = parser.parse_args()
try:
logger.info(f"Connecting to MUM at {args.host}...")
# Initialize MUM
mum = MelexisUniversalMaster()
mum.open_all(args.host)
power_control = mum.get_device(MUM_POWER_DEVICE)
linmaster = mum.get_device(MUM_LIN_DEVICE)
linmaster.setup()
# Initialize LIN
lin_bus = LinBusManager(linmaster)
lin_dev = LinDevice22(lin_bus)
lin_dev.baudrate = LIN_BAUDRATE
# Get transport layer for sending with Classic checksum
transport_layer = lin_dev.get_device("bus/transport_layer")
# Power up
power_control.power_up()
time.sleep(0.5)
logger.info("=" * 70)
logger.info("MUM connected, LIN bus ready")
# Read initial status
initial_nad, _ = read_status(lin_dev)
if initial_nad:
logger.info(f"Initial NAD: 0x{initial_nad:02X}")
# Calculate target NAD (different from initial NAD)
valid_nads = list(VALID_NAD_RANGE)
if initial_nad and initial_nad in valid_nads:
valid_nads.remove(initial_nad)
target_nad = valid_nads[0] # Pick the first available NAD
logger.info(f"Target NAD: 0x{target_nad:02X}")
logger.info("=" * 70)
logger.info(f"Running {args.iterations} auto-addressing iterations...")
logger.info("(Like babylin: iterate multiple times, then check result)")
logger.info("=" * 70)
# Run iterations with status polling (like babylin's schedule switching)
for iteration in range(1, args.iterations + 1):
logger.info(f"Iteration {iteration}/{args.iterations}")
# Run BSM sequence (like babylin's LIN_AA schedule)
if not run_auto_addressing_sequence(transport_layer, target_nad):
logger.error("Auto-addressing sequence failed")
break
# Poll status frames between iterations (like babylin's Pub_serv schedule)
# This gives ECU time to process and keeps communication alive
logger.debug(f" Status polling between iterations...")
poll_status_frames(lin_dev, duration_seconds=2.0)
# Check status at intervals if requested
if args.check_interval > 0 and iteration % args.check_interval == 0:
nad, _ = read_status(lin_dev)
if nad:
logger.info(f" After iteration {iteration}: NAD = 0x{nad:02X}")
if initial_nad and nad != initial_nad:
logger.info("=" * 70)
logger.info(f"SUCCESS! NAD changed from 0x{initial_nad:02X} to 0x{nad:02X}")
logger.info(f"Change occurred after {iteration} iterations")
logger.info("=" * 70)
break
# Final status check
logger.info("=" * 70)
logger.info("Checking final status...")
time.sleep(1.0)
final_nad, final_data = read_status(lin_dev)
if final_nad:
data_hex = ' '.join(f'{b:02X}' for b in final_data)
logger.info(f"Final NAD: 0x{final_nad:02X}, Data: {data_hex}")
if initial_nad and final_nad != initial_nad:
logger.info("=" * 70)
logger.info(f"SUCCESS! NAD changed from 0x{initial_nad:02X} to 0x{final_nad:02X}")
logger.info("=" * 70)
else:
logger.info(f"NAD unchanged (still 0x{final_nad:02X})")
logger.info("=" * 70)
linmaster.teardown()
logger.info("Done")
except KeyboardInterrupt:
logger.info("\nInterrupted by user")
try:
linmaster.teardown()
except:
pass
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
if __name__ == "__main__":
main()

Binary file not shown.

View File

@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""
LIN LED Control Test
This test verifies LIN communication by controlling the LED on the board.
It will fade through different colors (Red, Green, Blue) to verify that
frames are being received correctly.
Frame structure (ALM_Req_A, ID=0x0A, 8 bytes):
- Byte 0: AmbLightColourRed (0-255)
- Byte 1: AmbLightColourGreen (0-255)
- Byte 2: AmbLightColourBlue (0-255)
- Byte 3: AmbLightIntensity (0-255)
- Byte 4: AmbLightUpdate[1:0] | (AmbLightMode[5:0] << 2)
- Byte 5: AmbLightDuration (0-255)
- Byte 6: AmbLightLIDFrom (NAD range start set equal to LIDTo to target one node)
- Byte 7: AmbLightLIDTo (NAD range end)
"""
import argparse
import logging
import time
import math
from pylin import LinBusManager, LinDevice22
from pymumclient import MelexisUniversalMaster
from config import *
logging.basicConfig(
level=logging.INFO,
format='%(asctime)-15s %(levelname)-8s %(message)s'
)
logger = logging.getLogger(__name__)
def read_alm_status(lin_dev):
"""Read ALM_Status frame and return (ALMNadNo, raw_bytes)."""
try:
response = lin_dev.send_message(
master_to_slave=False,
frame_id=ALM_STATUS_FRAME['frame_id'],
data_length=ALM_STATUS_FRAME['length'],
data=None
)
if response and len(response) >= ALM_STATUS_FRAME['length']:
parsed = unpack_frame(ALM_STATUS_FRAME, response)
return parsed['ALMNadNo'], response
return None, None
except Exception as e:
logger.error(f"Failed to read ALM_Status: {e}")
return None, None
def set_led_color(lin_dev, nad, red, green, blue, intensity,
update=0, mode=0, duration=0):
"""
Set LED color and intensity via ALM_Req_A frame.
The node responds only if AmbLightLIDFrom <= ALMNadNo <= AmbLightLIDTo.
Setting both to the same NAD targets a single node.
"""
try:
data = pack_frame(ALM_REQ_A_FRAME,
AmbLightColourRed=red,
AmbLightColourGreen=green,
AmbLightColourBlue=blue,
AmbLightIntensity=intensity,
AmbLightUpdate=update,
AmbLightMode=mode,
AmbLightDuration=duration,
AmbLightLIDFrom=nad,
AmbLightLIDTo=nad,
)
lin_dev.send_message(
master_to_slave=True,
frame_id=ALM_REQ_A_FRAME['frame_id'],
data_length=ALM_REQ_A_FRAME['length'],
data=data,
)
return True
except Exception as e:
logger.error(f"Failed to set LED color: {e}")
return False
def fade_test(lin_dev, nad, duration_per_color=5.0):
"""
Fade through Red, Green, and Blue colors.
Args:
lin_dev: LinDevice22 instance
nad: Node address
duration_per_color: How long to fade each color (seconds)
"""
colors = [
("Red", 255, 0, 0),
("Green", 0, 255, 0),
("Blue", 0, 0, 255),
]
steps = 50 # Number of fade steps
delay = duration_per_color / steps
for color_name, r_max, g_max, b_max in colors:
logger.info(f"Fading {color_name}...")
# Fade in
for step in range(steps + 1):
progress = step / steps
# Use sine wave for smoother fade
brightness = math.sin(progress * math.pi / 2)
red = int(r_max * brightness)
green = int(g_max * brightness)
blue = int(b_max * brightness)
intensity = int(100 * brightness)
set_led_color(lin_dev, nad, red, green, blue, intensity)
time.sleep(delay)
# Fade out
for step in range(steps, -1, -1):
progress = step / steps
brightness = math.sin(progress * math.pi / 2)
red = int(r_max * brightness)
green = int(g_max * brightness)
blue = int(b_max * brightness)
intensity = int(100 * brightness)
set_led_color(lin_dev, nad, red, green, blue, intensity)
time.sleep(delay)
def main():
parser = argparse.ArgumentParser(description='LIN LED Control Test')
parser.add_argument('--host', default=MUM_HOST,
help=f'MUM IP address (default: {MUM_HOST})')
parser.add_argument('--nad', type=lambda x: int(x,0), default=LED_DEFAULT_NAD,
help=f'Node address to control (default: 0x{LED_DEFAULT_NAD:02X})')
parser.add_argument('--cycles', type=int, default=3,
help='Number of fade cycles (default: 3)')
parser.add_argument('--duration', type=float, default=3.0,
help='Duration per color in seconds (default: 3.0)')
args = parser.parse_args()
try:
logger.info(f"Connecting to MUM at {args.host}...")
# Setup MUM and LIN
mum = MelexisUniversalMaster()
mum.open_all(args.host)
power_control = mum.get_device(MUM_POWER_DEVICE)
linmaster = mum.get_device(MUM_LIN_DEVICE)
linmaster.setup()
lin_bus = LinBusManager(linmaster)
lin_dev = LinDevice22(lin_bus)
lin_dev.baudrate = LIN_BAUDRATE
lin_dev.nad = args.nad
power_control.power_up()
time.sleep(0.5)
logger.info("MUM connected and LIN bus ready")
logger.info("=" * 70)
# Read current NAD
logger.info("Reading current NAD from ALM_Status...")
current_nad, status_data = read_alm_status(lin_dev)
if current_nad is not None:
data_hex = ' '.join(f'{b:02X}' for b in status_data)
logger.info(f"Current NAD: 0x{current_nad:02X}")
logger.info(f"Full status data: {data_hex}")
else:
logger.warning("Could not read NAD, using command-line NAD")
current_nad = args.nad
logger.info("=" * 70)
logger.info(f"LED FADE TEST")
logger.info(f"Controlling NAD: 0x{current_nad:02X}")
logger.info(f"LIDFrom: 0x{current_nad:02X}, LIDTo: 0x{current_nad:02X}")
logger.info(f"Fade cycles: {args.cycles}")
logger.info(f"Duration per color: {args.duration}s")
logger.info("=" * 70)
# Turn LED off initially
logger.info("Turning LED off...")
set_led_color(lin_dev, current_nad, 0, 0, 0, 0)
time.sleep(1.0)
# Run fade test
for cycle in range(1, args.cycles + 1):
logger.info(f"\nCycle {cycle}/{args.cycles}")
fade_test(lin_dev, current_nad, args.duration)
if cycle < args.cycles:
logger.info("Pausing between cycles...")
time.sleep(1.0)
# Turn LED off at the end
logger.info("\nTurning LED off...")
set_led_color(lin_dev, current_nad, 0, 0, 0, 0)
logger.info("=" * 70)
logger.info("✓ LED TEST COMPLETED")
logger.info("=" * 70)
logger.info("Tearing down...")
linmaster.teardown()
logger.info("Done (ECU still powered)")
except KeyboardInterrupt:
logger.info("")
logger.info("Interrupted by user")
logger.info("Turning LED off...")
try:
set_led_color(lin_dev, args.nad, 0, 0, 0, 0)
linmaster.teardown()
except:
pass
except Exception as e:
logger.error(f"Error: {e}", exc_info=True)
if __name__ == "__main__":
main()

Binary file not shown.