#!/usr/bin/env python3 """ Interactive BABYLIN animation validation for ALM_Req_A. This script executes the requirement-oriented checks step-by-step and pauses after each action so the tester can verify physical LED behavior. Covered checks: 1) AmbLightMode behavior (0 immediate, 1 fade RGBI, 2 immediate color + fade I) 2) AmbLightUpdate save/apply/discard 3) AmbLightDuration scaling (0.2 s/LSB) 4) LID range selection (single-node, broadcast, invalid From>To) """ import argparse import logging import time from pylin import LinBusManager, LinDevice22 from pymumclient import MelexisUniversalMaster from config import * logging.basicConfig( level=logging.INFO, format="%(asctime)-15s %(levelname)-8s %(message)s", ) logger = logging.getLogger(__name__) SEPARATOR = "=" * 78 SUB = "-" * 78 # ALM_Status.ALMLedState values LED_STATE_OFF = 0 LED_STATE_ANIMATING = 1 LED_STATE_ON = 2 LED_STATE_NAMES = { LED_STATE_OFF: "OFF", LED_STATE_ANIMATING: "ANIMATING", LED_STATE_ON: "ON", } def pause(msg): print() input(f">>> {msg}") print() def banner(title): logger.info(SEPARATOR) logger.info(title) logger.info(SEPARATOR) def section(title): logger.info(SUB) logger.info(title) logger.info(SUB) def read_alm_status(lin_dev): """Return (parsed_dict, raw_bytes) or (None, None).""" try: response = lin_dev.send_message( master_to_slave=False, frame_id=ALM_STATUS_FRAME["frame_id"], data_length=ALM_STATUS_FRAME["length"], data=None, ) if response and len(response) >= ALM_STATUS_FRAME["length"]: return unpack_frame(ALM_STATUS_FRAME, response), response return None, None except Exception as exc: logger.error("Failed reading ALM_Status: %s", exc) return None, None def read_led_state(lin_dev): parsed, _ = read_alm_status(lin_dev) if parsed is None: return -1 return parsed.get("ALMLEDState", -1) def read_nad(lin_dev, fallback): parsed, raw = read_alm_status(lin_dev) if parsed is None: logger.warning("Could not read ALM_Status, fallback NAD=0x%02X", fallback) return fallback nad = parsed.get("ALMNadNo", fallback) logger.info("Detected ALMNadNo=0x%02X (raw: %s)", nad, " ".join(f"{b:02X}" for b in raw)) return nad def send_req( lin_dev, *, red, green, blue, intensity, update, mode, duration, lid_from, lid_to, ): data = pack_frame( ALM_REQ_A_FRAME, AmbLightColourRed=red, AmbLightColourGreen=green, AmbLightColourBlue=blue, AmbLightIntensity=intensity, AmbLightUpdate=update, AmbLightMode=mode, AmbLightDuration=duration, AmbLightLIDFrom=lid_from, AmbLightLIDTo=lid_to, ) lin_dev.send_message( master_to_slave=True, frame_id=ALM_REQ_A_FRAME["frame_id"], data_length=ALM_REQ_A_FRAME["length"], data=data, ) def observe_state(lin_dev, seconds): """Poll status slowly and print changes.""" logger.info("Observing for %.1f s...", seconds) end_t = time.time() + seconds last = None while time.time() < end_t: st = read_led_state(lin_dev) if st != last: name = LED_STATE_NAMES.get(st, f"UNKNOWN({st})") logger.info(" ALMLEDState -> %s", name) last = st time.sleep(0.25) def guided_step(lin_dev, title, expectation_lines, command_kwargs, observe_s): section(title) logger.info("What you should see:") for line in expectation_lines: logger.info(" - %s", line) pause("Press Enter to send this command...") send_req(lin_dev, **command_kwargs) observe_state(lin_dev, observe_s) pause("Verify visually, then press Enter for the next step...") def main(): parser = argparse.ArgumentParser(description="Interactive ALM animation checks for BABYLIN") parser.add_argument("--host", default=MUM_HOST, help=f"MUM IP (default: {MUM_HOST})") parser.add_argument( "--nad", type=lambda x: int(x, 0), default=LED_DEFAULT_NAD, help=f"Fallback NAD if ALM_Status read fails (default: 0x{LED_DEFAULT_NAD:02X})", ) parser.add_argument( "--slow-factor", type=float, default=1.0, help="Multiply wait/observe durations (default: 1.0)", ) args = parser.parse_args() mum = None linmaster = None lin_dev = None try: banner("Connecting to MUM / LIN") mum = MelexisUniversalMaster() mum.open_all(args.host) power_control = mum.get_device(MUM_POWER_DEVICE) linmaster = mum.get_device(MUM_LIN_DEVICE) linmaster.setup() lin_bus = LinBusManager(linmaster) lin_dev = LinDevice22(lin_bus) lin_dev.baudrate = LIN_BAUDRATE lin_dev.nad = args.nad power_control.power_up() time.sleep(0.5 * args.slow_factor) nad = read_nad(lin_dev, args.nad) lin_dev.nad = nad banner("Interactive Requirement Validation") logger.info("Target NAD: 0x%02X", nad) logger.info("Slow factor: %.2f", args.slow_factor) logger.info("You will be prompted before and after every test step.") pause("Press Enter to start from a known OFF baseline...") # Step 0: Baseline OFF guided_step( lin_dev, "Step 0 - Baseline OFF", [ "LED should turn OFF quickly.", "ALMLEDState should become OFF.", ], { "red": 0, "green": 0, "blue": 0, "intensity": 0, "update": 0, "mode": 0, "duration": 0, "lid_from": nad, "lid_to": nad, }, 1.0 * args.slow_factor, ) # 1) Mode behavior checks guided_step( lin_dev, "Step 1 - Mode 0 Immediate Setpoint", [ "Color/intensity should change immediately.", "No visible fade; direct jump to requested setpoint.", ], { "red": 0, "green": 180, "blue": 80, "intensity": 200, "update": 0, "mode": 0, "duration": 10, "lid_from": nad, "lid_to": nad, }, 1.0 * args.slow_factor, ) guided_step( lin_dev, "Step 2 - Mode 1 Fade RGB + Intensity (2.0 s)", [ "RGB and intensity should both transition smoothly.", "Transition duration should be close to 2.0 s (Duration=10).", ], { "red": 255, "green": 40, "blue": 0, "intensity": 220, "update": 0, "mode": 1, "duration": 10, "lid_from": nad, "lid_to": nad, }, 3.0 * args.slow_factor, ) guided_step( lin_dev, "Step 3 - Mode 2 Immediate Color + Faded Intensity (2.0 s)", [ "Color should jump immediately to the new RGB target.", "Only intensity should ramp over ~2.0 s.", ], { "red": 0, "green": 0, "blue": 255, "intensity": 50, "update": 0, "mode": 2, "duration": 10, "lid_from": nad, "lid_to": nad, }, 3.0 * args.slow_factor, ) # 2) Update save/apply/discard checks guided_step( lin_dev, "Step 4 - Update=1 Save (must NOT apply)", [ "LED output should remain unchanged after this command.", "No visible color/intensity change should occur.", ], { "red": 0, "green": 255, "blue": 0, "intensity": 255, "update": 1, "mode": 1, "duration": 10, "lid_from": nad, "lid_to": nad, }, 1.5 * args.slow_factor, ) guided_step( lin_dev, "Step 5 - Update=2 Apply Saved", [ "Saved command from Step 4 should execute now.", "Payload in this Apply frame should be ignored by ECU logic.", "You should see saved behavior (mode/duration/RGBI from Step 4).", ], { "red": 7, "green": 7, "blue": 7, "intensity": 7, "update": 2, "mode": 0, "duration": 0, "lid_from": nad, "lid_to": nad, }, 3.0 * args.slow_factor, ) guided_step( lin_dev, "Step 6 - Update=3 Discard Saved", [ "Saved buffer should be cleared.", "This discard command itself should not change output.", ], { "red": 0, "green": 0, "blue": 0, "intensity": 0, "update": 3, "mode": 0, "duration": 0, "lid_from": nad, "lid_to": nad, }, 1.5 * args.slow_factor, ) guided_step( lin_dev, "Step 7 - Update=2 After Discard", [ "No saved command should exist now.", "Apply should behave like a no-op (no new visible action).", ], { "red": 123, "green": 12, "blue": 45, "intensity": 200, "update": 2, "mode": 1, "duration": 5, "lid_from": nad, "lid_to": nad, }, 2.0 * args.slow_factor, ) # 3) Duration scaling checks guided_step( lin_dev, "Step 8 - Duration=1 (expect ~0.2 s)", [ "Transition should complete very quickly (~0.2 s).", ], { "red": 255, "green": 0, "blue": 0, "intensity": 200, "update": 0, "mode": 1, "duration": 1, "lid_from": nad, "lid_to": nad, }, 1.5 * args.slow_factor, ) guided_step( lin_dev, "Step 9 - Duration=5 (expect ~1.0 s)", [ "Transition should take around 1.0 s.", "Visibly slower than Step 8.", ], { "red": 0, "green": 255, "blue": 0, "intensity": 200, "update": 0, "mode": 1, "duration": 5, "lid_from": nad, "lid_to": nad, }, 2.0 * args.slow_factor, ) guided_step( lin_dev, "Step 10 - Duration=10 (expect ~2.0 s)", [ "Transition should take around 2.0 s.", "Visibly slower than Step 9.", ], { "red": 0, "green": 0, "blue": 255, "intensity": 200, "update": 0, "mode": 1, "duration": 10, "lid_from": nad, "lid_to": nad, }, 3.0 * args.slow_factor, ) # 4) LID selection checks guided_step( lin_dev, "Step 11 - LID Single-Node Select (From=To=NAD)", [ "This node should react (it is explicitly selected).", ], { "red": 255, "green": 120, "blue": 0, "intensity": 180, "update": 0, "mode": 0, "duration": 0, "lid_from": nad, "lid_to": nad, }, 1.0 * args.slow_factor, ) guided_step( lin_dev, "Step 12 - LID Broadcast Select (From=0, To=255)", [ "This node should react (broadcast range).", ], { "red": 120, "green": 0, "blue": 255, "intensity": 180, "update": 0, "mode": 0, "duration": 0, "lid_from": 0, "lid_to": 255, }, 1.0 * args.slow_factor, ) guided_step( lin_dev, "Step 13 - LID Invalid Range (From > To)", [ "Node should ignore this command.", "No visible output change is expected.", ], { "red": 255, "green": 255, "blue": 255, "intensity": 255, "update": 0, "mode": 0, "duration": 0, "lid_from": 20, "lid_to": 10, }, 1.5 * args.slow_factor, ) pause("All checks done. Press Enter to send final OFF cleanup...") send_req( lin_dev, red=0, green=0, blue=0, intensity=0, update=0, mode=0, duration=0, lid_from=nad, lid_to=nad, ) observe_state(lin_dev, 1.0 * args.slow_factor) banner("Test sequence completed") except KeyboardInterrupt: logger.info("Interrupted by user") finally: try: if lin_dev is not None: # Best effort: leave node OFF send_req( lin_dev, red=0, green=0, blue=0, intensity=0, update=0, mode=0, duration=0, lid_from=lin_dev.nad, lid_to=lin_dev.nad, ) except Exception: pass try: if linmaster is not None: linmaster.teardown() except Exception: pass if __name__ == "__main__": main()