ecu-tests/docs/16_mum_internals.md

169 lines
7.4 KiB
Markdown

# 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)
A->>PL: 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(bytes)
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.