# 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.
