544 lines
15 KiB
Python

#!/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()