ecu-tests/docs/04_lin_interface_call_flow.md
Hosam-Eldin Mostafa e121b617a5 docs(lin): expand LinInterface base contract and __post_init__ flow
Adds a deeper "Contract (base)" section to 04_lin_interface_call_flow.md:
LinFrame field validation, LinInterface abstract vs default methods, the
list of concrete adapters / consumers, and a "How __post_init__ runs"
subsection explaining the dataclass-generated __init__ hook chain and the
inheritance caveat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:42:05 +02:00

8.2 KiB
Raw Blame History

LIN Interface Call Flow

This document explains how LIN operations flow through the abstraction for the Mock, MUM, and the deprecated BabyLIN adapters.

Contract (base)

File: ecu_framework/lin/base.py

This module is the polymorphism boundary for LIN I/O. Everything above it (tests, fixtures, helpers like FrameIO/AlmTester) depends only on these abstractions, so the same code can run against a mock in CI or against real hardware on a Pi by swapping the concrete adapter.

LinFrame (dataclass)

A validated representation of a single LIN frame:

  • id: int — frame identifier; enforced to 0x000x3F (classic 6-bit LIN ID range)
  • data: bytes — payload; coerced from list-of-ints to bytes if needed, capped at 8 bytes

Validation runs in __post_init__, so any malformed frame fails fast at construction rather than surfacing as a confusing error deeper in the stack:

  • ID outside 0x000x3FValueError
  • data not bytes-like and not coercible → TypeError
  • len(data) > 8ValueError

How __post_init__ runs

__post_init__ is never called explicitly in base.py — it is a dataclass hook invoked automatically by the __init__ that @dataclass generates. For LinFrame, that generated __init__ is effectively:

def __init__(self, id, data):
    self.id = id
    self.data = data
    self.__post_init__()   # auto-appended because __post_init__ is defined

So when someone writes LinFrame(id=0x10, data=[1, 2, 3]):

  1. The generated __init__ assigns self.id and self.data exactly as passed in (no validation, no coercion).
  2. It then calls self.__post_init__() with no arguments.
  3. __post_init__ re-reads self.id / self.data, validates the ID range, coerces self.data to bytes (which is why a list of ints is accepted even though the annotation says bytes), and validates length.
  4. If validation fails, the raised exception propagates out of the LinFrame(...) call — so a bad frame never becomes a live object. There is no half-constructed LinFrame for callers to observe.

Two consequences worth knowing:

  • Order matters. Type coercion of data happens inside __post_init__, so between the field assignment and the __post_init__ call there is a brief moment where self.data may be a list rather than bytes. Nothing observes that window, but it's why the type annotation is slightly optimistic.
  • Inheritance. Any subclass of LinFrame that overrides __post_init__ must call super().__post_init__() or it loses the validation. No subclass exists today; this is purely a future-proofing note.

LinInterface (abstract base class)

The contract every concrete LIN adapter must implement. Abstract methods:

  • connect() — open the interface connection
  • disconnect() — close the interface connection
  • send(frame: LinFrame) — transmit a LIN frame
  • receive(id: int | None = None, timeout: float = 1.0) -> LinFrame | None — receive a frame, optionally filtered by ID; returns None on timeout

Non-abstract methods with default implementations (overridable):

  • request(id: int, length: int, timeout: float = 1.0) -> LinFrame | None — default implementation just calls receive(id=id, timeout=timeout). Adapters that need to send a header first (e.g. BabyLIN, MUM) override this.
  • flush() — no-op hook for clearing RX buffers.

Concrete implementors

  • ecu_framework/lin/mock.py — in-memory mock for unit tests and CI
  • ecu_framework/lin/mum.py — Melexis Universal Master (current hardware path)
  • ecu_framework/lin/babylin.py — BabyLIN SDK wrapper (deprecated)

Consumers

LinFrame and LinInterface are imported across the framework — the conftest plugin (tests/conftest.py), hardware helpers (tests/hardware/frame_io.py), unit tests (tests/unit/test_linframe.py, tests/unit/test_hex_flasher.py), and the bulk of tests/hardware/* test cases. Tests never import a concrete adapter directly; the lin fixture in conftest.py resolves the adapter based on configuration, which is what makes the same test file runnable against mock or hardware.

Mock adapter flow

File: ecu_framework/lin/mock.py

  • connect(): initialize buffers and state
  • send(frame): enqueues the frame and (for echo behavior) schedules it for RX
  • receive(timeout): waits up to timeout for a frame in RX buffer
  • request(id, length, timeout): synthesizes a deterministic response of the given length for predictability
  • disconnect(): clears state

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) — DEPRECATED

Retained only so existing BabyLIN rigs can keep running. New work should use the MUM flow above.

File: ecu_framework/lin/babylin.py (emits DeprecationWarning on instantiation)

  • connect(): import SDK BabyLIN_library.py, discover ports, open first, optionally BLC_loadSDF, get channel handle, and BLC_sendCommand("start schedule N;")
  • send(frame): calls BLC_mon_set_xmit(channelHandle, frameId, data, slotTime=0)
  • receive(timeout): calls BLC_getNextFrameTimeout(channelHandle, timeout_ms) and converts returned BLC_FRAME to LinFrame
  • request(id, length, timeout): prefers BLC_sendRawMasterRequest(channel, id, length); falls back to (channel, id, bytes); if unavailable, sends a header and waits on receive()
  • disconnect(): calls BLC_closeAll()
  • Error handling: uses BLC_getDetailedErrorString (if available)

Configuration:

  • interface.sdf_path locates the SDF to load
  • interface.schedule_nr sets the schedule to start upon connect
  • interface.channel selects the channel index

Edge considerations

  • Ensure the correct architecture (x86/x64) of the DLL matches Python
  • Channel/bitrate must match your network configuration
  • Some SDKs require initialization/scheduling steps before transmit/receive
  • Time synchronization and timestamp units vary per SDK — convert as needed

Note on master requests:

  • Our mock wrapper returns a deterministic byte pattern when called with the length signature.
  • When only the bytes signature is available, zeros of the requested length are used in tests.