544 lines
15 KiB
Python
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()
|