"""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)