221 lines
8.5 KiB
Python
221 lines
8.5 KiB
Python
"""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)
|