Add MUM support in the testing framework
This commit is contained in:
parent
58aa7350e6
commit
b8f52bea39
57
README.md
57
README.md
@ -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
29
config/mum.example.yaml
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -76,9 +76,11 @@ 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(...)
|
||||
- mock → ecu_framework.lin.mock.MockBabyLinInterface(...)
|
||||
- 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 0x00–0x3F) 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()`.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -9,7 +9,8 @@ This document provides a high-level view of the framework’s 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
167
docs/16_mum_internals.md
Normal 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.
|
||||
@ -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/`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
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)
|
||||
configured_idx = int(self.channel_index)
|
||||
get_info = getattr(self._BabyLIN, 'BLC_getChannelInfo', None)
|
||||
|
||||
self._connected = True # Mark interface as connected
|
||||
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)
|
||||
|
||||
if not self._channel_handle:
|
||||
raise RuntimeError(f"BLC_getChannelHandle returned invalid handle for channel {ch_idx}")
|
||||
|
||||
# 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
220
ecu_framework/lin/mum.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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}")
|
||||
|
||||
|
||||
118
tests/hardware/test_e2e_mum_led_activate.py
Normal file
118
tests/hardware/test_e2e_mum_led_activate.py
Normal 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"
|
||||
)
|
||||
235
tests/hardware/test_e2e_power_on_lin_smoke.py
Normal file
235
tests/hardware/test_e2e_power_on_lin_smoke.py
Normal 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))
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
242
tests/unit/test_mum_adapter_mocked.py
Normal file
242
tests/unit/test_mum_adapter_mocked.py
Normal 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
405
vendor/4SEVEN_color_lib_test.ldf
vendored
Normal 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 ;
|
||||
}
|
||||
BIN
vendor/4SEVEN_color_lib_test.ldf:Zone.Identifier
vendored
Normal file
BIN
vendor/4SEVEN_color_lib_test.ldf:Zone.Identifier
vendored
Normal file
Binary file not shown.
BIN
vendor/4SEVEN_color_lib_test.sdf
vendored
Normal file
BIN
vendor/4SEVEN_color_lib_test.sdf
vendored
Normal file
Binary file not shown.
BIN
vendor/4SEVEN_color_lib_test.sdf:Zone.Identifier
vendored
Normal file
BIN
vendor/4SEVEN_color_lib_test.sdf:Zone.Identifier
vendored
Normal file
Binary file not shown.
321
vendor/automated_lin_test/README.md
vendored
Normal file
321
vendor/automated_lin_test/README.md
vendored
Normal 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.
|
||||
BIN
vendor/automated_lin_test/README.md:Zone.Identifier
vendored
Normal file
BIN
vendor/automated_lin_test/README.md:Zone.Identifier
vendored
Normal file
Binary file not shown.
190
vendor/automated_lin_test/config.py
vendored
Normal file
190
vendor/automated_lin_test/config.py
vendored
Normal 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)
|
||||
BIN
vendor/automated_lin_test/config.py:Zone.Identifier
vendored
Normal file
BIN
vendor/automated_lin_test/config.py:Zone.Identifier
vendored
Normal file
Binary file not shown.
71
vendor/automated_lin_test/install_packages.sh
vendored
Normal file
71
vendor/automated_lin_test/install_packages.sh
vendored
Normal 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
|
||||
BIN
vendor/automated_lin_test/install_packages.sh:Zone.Identifier
vendored
Normal file
BIN
vendor/automated_lin_test/install_packages.sh:Zone.Identifier
vendored
Normal file
Binary file not shown.
37
vendor/automated_lin_test/power_cycle.py
vendored
Normal file
37
vendor/automated_lin_test/power_cycle.py
vendored
Normal 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()
|
||||
BIN
vendor/automated_lin_test/power_cycle.py:Zone.Identifier
vendored
Normal file
BIN
vendor/automated_lin_test/power_cycle.py:Zone.Identifier
vendored
Normal file
Binary file not shown.
493
vendor/automated_lin_test/test_adc_measurements.py
vendored
Normal file
493
vendor/automated_lin_test/test_adc_measurements.py
vendored
Normal 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()
|
||||
BIN
vendor/automated_lin_test/test_adc_measurements.py:Zone.Identifier
vendored
Normal file
BIN
vendor/automated_lin_test/test_adc_measurements.py:Zone.Identifier
vendored
Normal file
Binary file not shown.
543
vendor/automated_lin_test/test_animation.py
vendored
Normal file
543
vendor/automated_lin_test/test_animation.py
vendored
Normal 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()
|
||||
BIN
vendor/automated_lin_test/test_animation.py:Zone.Identifier
vendored
Normal file
BIN
vendor/automated_lin_test/test_animation.py:Zone.Identifier
vendored
Normal file
Binary file not shown.
260
vendor/automated_lin_test/test_auto_addressing.py
vendored
Normal file
260
vendor/automated_lin_test/test_auto_addressing.py
vendored
Normal 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()
|
||||
BIN
vendor/automated_lin_test/test_auto_addressing.py:Zone.Identifier
vendored
Normal file
BIN
vendor/automated_lin_test/test_auto_addressing.py:Zone.Identifier
vendored
Normal file
Binary file not shown.
230
vendor/automated_lin_test/test_led_control.py
vendored
Normal file
230
vendor/automated_lin_test/test_led_control.py
vendored
Normal 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()
|
||||
BIN
vendor/automated_lin_test/test_led_control.py:Zone.Identifier
vendored
Normal file
BIN
vendor/automated_lin_test/test_led_control.py:Zone.Identifier
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user