#!/usr/bin/env python3 """ LIN ADC Measurement Verification Test This test reads ADC measurement values from the ECU over LIN and verifies they are within expected ranges across multiple LED states. Test cases: 1. All LEDs off 2. Only Red on (color=255, intensity=255) 3. Only Green on (color=255, intensity=255) 4. Only Blue on (color=255, intensity=255) 5. All LEDs on (color=255, intensity=255) Verified signals: - VF_Frame_VS: Supply voltage (expected ~12V = ~12000 mV) - VF_Frame_VLED: DC-DC converter output voltage feeding LEDs (expected ~5V = ~5000 mV) - VF_Frame_Red_VF: Red LED forward voltage (0 when off, ~1500-3500 mV when on) - VF_Frame_Green_VF: Green LED forward voltage (0 when off, ~1500-3500 mV when on) - VF_Frame_Blue1_VF: Blue LED forward voltage (0 when off, ~1500-3500 mV when on) Frame structures: ALM_Req_A (ID=0x0A, master-to-slave, 8 bytes): - Byte 0: AmbLightColourRed (0-255) - Byte 1: AmbLightColourGreen (0-255) - Byte 2: AmbLightColourBlue (0-255) - Byte 3: AmbLightIntensity (0-255) - Byte 4: AmbLightUpdate[1:0] | (AmbLightMode[5:0] << 2) - Byte 5: AmbLightDuration (0-255) - Byte 6: AmbLightLIDFrom (NAD range start — set equal to LIDTo to target one node) - Byte 7: AmbLightLIDTo (NAD range end) PWM_wo_Comp (ID=0x15, slave-to-master, 8 bytes): - Byte 0-1: PWM_wo_Comp_Red (16-bit, little-endian) - Byte 2-3: PWM_wo_Comp_Green (16-bit, little-endian) - Byte 4-5: PWM_wo_Comp_Blue (16-bit, little-endian) - Byte 6-7: VF_Frame_VS (16-bit, little-endian, value in mV) VF_Frame (ID=0x13, slave-to-master, 8 bytes): - Byte 0-1: VF_Frame_Red_VF (16-bit, little-endian, value in mV) - Byte 2-3: VF_Frame_Green_VF (16-bit, little-endian, value in mV) - Byte 4-5: VF_Frame_Blue1_VF (16-bit, little-endian, value in mV) - Byte 6-7: VF_Frame_VLED (16-bit, little-endian, value in mV) """ import argparse import logging import time import sys 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__) # ADC measurement expected ranges (in mV) VS_EXPECTED_MIN_MV = 10000 # 10.0V minimum VS_EXPECTED_MAX_MV = 14000 # 14.0V maximum VS_EXPECTED_NOMINAL_MV = 12000 # 12.0V nominal VLED_EXPECTED_MIN_MV = 4000 # 4.0V minimum VLED_EXPECTED_MAX_MV = 6000 # 6.0V maximum VLED_EXPECTED_NOMINAL_MV = 5000 # 5.0V nominal # LED forward voltage ranges when LEDs are off LED_VF_OFF_MIN_MV = 0 # 0V minimum (off) LED_VF_OFF_MAX_MV = 500 # 0.5V maximum (off, allowing some noise) # LED forward voltage ranges when LEDs are on LED_VF_ON_MIN_MV = 1500 # 1.5V minimum (on) LED_VF_ON_MAX_MV = 3500 # 3.5V maximum (on) # Settle time after changing LED state (seconds) LED_SETTLE_TIME = 1.0 def read_alm_status(lin_dev): """Read ALM_Status frame and return (ALMNadNo, raw_bytes).""" 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']: parsed = unpack_frame(ALM_STATUS_FRAME, response) return parsed['ALMNadNo'], response return None, None except Exception as e: logger.error(f"Failed to read ALM_Status: {e}") return None, None def send_config_frame(lin_dev, calibration=0, enable_derating=1, enable_compensation=1, max_lm=3840): """Send ConfigFrame to configure calibration, derating and compensation.""" data = pack_frame(CONFIG_FRAME, ConfigFrame_Calibration=calibration, ConfigFrame_EnableDerating=enable_derating, ConfigFrame_EnableCompensation=enable_compensation, ConfigFrame_MaxLM=max_lm, ) lin_dev.send_message( master_to_slave=True, frame_id=CONFIG_FRAME['frame_id'], data_length=CONFIG_FRAME['length'], data=data, ) def set_led_color(lin_dev, nad, red, green, blue, intensity): """Set LED color and intensity via ALM_Req_A frame.""" data = pack_frame(ALM_REQ_A_FRAME, AmbLightColourRed=red, AmbLightColourGreen=green, AmbLightColourBlue=blue, AmbLightIntensity=intensity, AmbLightLIDFrom=nad, AmbLightLIDTo=nad, ) 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 read_pwm_wo_comp_frame(lin_dev): """ Read PWM_wo_Comp frame from slave. Returns: tuple: (raw_bytes, parsed_dict) or (None, None) on failure. parsed_dict keys: pwm_red, pwm_green, pwm_blue, vs_mv """ try: response = lin_dev.send_message( master_to_slave=False, frame_id=PWM_WO_COMP_FRAME['frame_id'], data_length=PWM_WO_COMP_FRAME['length'], data=None, ) if response and len(response) >= PWM_WO_COMP_FRAME['length']: s = unpack_frame(PWM_WO_COMP_FRAME, response) return response, { 'pwm_red': s['PWM_wo_Comp_Red'], 'pwm_green': s['PWM_wo_Comp_Green'], 'pwm_blue': s['PWM_wo_Comp_Blue'], 'vs_mv': s['VF_Frame_VS'], } return None, None except Exception as e: logger.error(f"Failed to read PWM_wo_Comp frame: {e}") return None, None def read_vf_frame(lin_dev): """ Read VF_Frame from slave. Returns: tuple: (raw_bytes, parsed_dict) or (None, None) on failure. parsed_dict keys: red_vf_mv, green_vf_mv, blue_vf_mv, vled_mv """ try: response = lin_dev.send_message( master_to_slave=False, frame_id=VF_FRAME['frame_id'], data_length=VF_FRAME['length'], data=None, ) if response and len(response) >= VF_FRAME['length']: s = unpack_frame(VF_FRAME, response) return response, { 'red_vf_mv': s['VF_Frame_Red_VF'], 'green_vf_mv': s['VF_Frame_Green_VF'], 'blue_vf_mv': s['VF_Frame_Blue1_VF'], 'vled_mv': s['VF_Frame_VLED'], } return None, None except Exception as e: logger.error(f"Failed to read VF_Frame: {e}") return None, None def sample_signal(lin_dev, signal_name, read_func, signal_key, expected_min, expected_max, num_samples=5, sample_interval=0.1): """ Read a signal multiple times and verify it is within expected range. Args: lin_dev: LinDevice22 instance signal_name: Display name for the signal read_func: Function to call to read the frame (returns raw, parsed) signal_key: Key in parsed dict to extract the signal value expected_min: Minimum expected value in mV expected_max: Maximum expected value in mV num_samples: Number of samples to read sample_interval: Delay between samples in seconds Returns: tuple: (passed, avg_voltage_mv, samples) """ samples = [] passed = True for i in range(num_samples): raw, parsed = read_func(lin_dev) if parsed is None: logger.warning(f" Sample {i+1}/{num_samples}: No response") continue value_mv = parsed[signal_key] samples.append(value_mv) in_range = expected_min <= value_mv <= expected_max status = "OK" if in_range else "FAIL" logger.info(f" Sample {i+1}/{num_samples}: {signal_name} = {value_mv} mV ({value_mv/1000:.2f} V) [{status}]") if not in_range: passed = False if i < num_samples - 1: time.sleep(sample_interval) if len(samples) == 0: logger.error(f" No valid samples received for {signal_name}") return False, 0, samples avg_mv = sum(samples) / len(samples) return passed, avg_mv, samples def log_signal_summary(signal_name, passed, avg_mv, samples): """Log summary statistics for a verified signal.""" if len(samples) > 0: s_min = min(samples) s_max = max(samples) logger.info(f" Average: {avg_mv:.0f} mV ({avg_mv/1000:.2f} V)") logger.info(f" Min: {s_min} mV ({s_min/1000:.2f} V)") logger.info(f" Max: {s_max} mV ({s_max/1000:.2f} V)") logger.info(f" Result: {'PASS' if passed else 'FAIL'}") else: logger.error(f" Result: FAIL (no data)") def verify_adc_signals(lin_dev, num_samples, sample_interval, expected_red_vf, expected_green_vf, expected_blue_vf): """ Verify all ADC signals (VS, VLED, Red_VF, Green_VF, Blue_VF) for the current LED state. Args: lin_dev: LinDevice22 instance num_samples: Number of samples per signal sample_interval: Delay between samples in seconds expected_red_vf: Tuple (min_mv, max_mv) for Red forward voltage expected_green_vf: Tuple (min_mv, max_mv) for Green forward voltage expected_blue_vf: Tuple (min_mv, max_mv) for Blue forward voltage Returns: bool: True if all signals pass, False otherwise """ all_passed = True logger.info(f" --- VS (Supply Voltage) ---") vs_passed, vs_avg, vs_samples = sample_signal( lin_dev, "VS", read_pwm_wo_comp_frame, 'vs_mv', VS_EXPECTED_MIN_MV, VS_EXPECTED_MAX_MV, num_samples=num_samples, sample_interval=sample_interval ) log_signal_summary("VS", vs_passed, vs_avg, vs_samples) if not vs_passed: all_passed = False logger.info(f" --- VLED (DC-DC Voltage) ---") vled_passed, vled_avg, vled_samples = sample_signal( lin_dev, "VLED", read_vf_frame, 'vled_mv', VLED_EXPECTED_MIN_MV, VLED_EXPECTED_MAX_MV, num_samples=num_samples, sample_interval=sample_interval ) log_signal_summary("VLED", vled_passed, vled_avg, vled_samples) if not vled_passed: all_passed = False led_checks = [ ("Red_VF", 'red_vf_mv', expected_red_vf), ("Green_VF", 'green_vf_mv', expected_green_vf), ("Blue_VF", 'blue_vf_mv', expected_blue_vf), ] for signal_name, signal_key, (exp_min, exp_max) in led_checks: logger.info(f" --- {signal_name} (expected {exp_min}-{exp_max} mV) ---") led_passed, led_avg, led_samples = sample_signal( lin_dev, signal_name, read_vf_frame, signal_key, exp_min, exp_max, num_samples=num_samples, sample_interval=sample_interval ) log_signal_summary(signal_name, led_passed, led_avg, led_samples) if not led_passed: all_passed = False return all_passed def main(): parser = argparse.ArgumentParser(description='LIN ADC Measurement Verification Test') parser.add_argument('--host', default=MUM_HOST, help=f'MUM IP address (default: {MUM_HOST})') parser.add_argument('--nad', type=lambda x: int(x, 0), default=LED_DEFAULT_NAD, help=f'Node address (default: 0x{LED_DEFAULT_NAD:02X})') parser.add_argument('--samples', type=int, default=10, help='Number of samples to read per signal (default: 10)') parser.add_argument('--interval', type=float, default=0.1, help='Interval between samples in seconds (default: 0.1)') parser.add_argument('--settle-time', type=float, default=LED_SETTLE_TIME, help=f'Settle time after LED state change (default: {LED_SETTLE_TIME}s)') args = parser.parse_args() # Define test cases: (name, red, green, blue, intensity, # expected_red_vf, expected_green_vf, expected_blue_vf) test_cases = [ ( "All LEDs OFF", 0, 0, 0, 0, (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), ), ( "Red ON (255/255)", 255, 0, 0, 255, (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV), (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), ), ( "Green ON (255/255)", 0, 255, 0, 255, (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV), (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), ), ( "Blue ON (255/255)", 0, 0, 255, 255, (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), (LED_VF_OFF_MIN_MV, LED_VF_OFF_MAX_MV), (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV), ), ( "All LEDs ON (255/255)", 255, 255, 255, 255, (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV), (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV), (LED_VF_ON_MIN_MV, LED_VF_ON_MAX_MV), ), ] test_results = {} nad = args.nad # may be updated below after reading ALM_Status try: logger.info(f"Connecting to MUM at {args.host}...") 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) logger.info("MUM connected and LIN bus ready") logger.info("=" * 70) logger.info("ADC MEASUREMENT VERIFICATION TEST") logger.info(f"Samples: {args.samples}, Interval: {args.interval}s, " f"Settle: {args.settle_time}s") logger.info("=" * 70) # Wait for ADC to settle after power-up logger.info("Waiting for ADC to settle after power-up...") time.sleep(1.0) # Read the actual NAD from the node. Using args.nad directly risks # a silent miss if the node was assigned a different NAD (e.g. via # auto-addressing), because AmbLightLIDFrom/LIDTo must equal ALMNadNo. logger.info("Reading node NAD from ALM_Status...") detected_nad, status_data = read_alm_status(lin_dev) if detected_nad is not None: nad = detected_nad data_hex = ' '.join(f'{b:02X}' for b in status_data) logger.info(f"Detected NAD: 0x{nad:02X} (Status frame: {data_hex})") else: nad = args.nad logger.warning(f"Could not read NAD, falling back to 0x{nad:02X}") logger.info("=" * 70) # Configure: disable derating and compensation so PWM output directly # reflects the requested color/brightness. logger.info("Sending ConfigFrame: Calibration=1, Derating=0, Compensation=0") send_config_frame(lin_dev, calibration=1, enable_derating=0, enable_compensation=0) time.sleep(0.1) # Ensure LEDs are off before starting set_led_color(lin_dev, nad, 0, 0, 0, 0) time.sleep(args.settle_time) for idx, (name, red, green, blue, intensity, exp_red, exp_green, exp_blue) in enumerate(test_cases, 1): logger.info("") logger.info("-" * 70) logger.info(f"TEST {idx}/{len(test_cases)}: {name}") logger.info(f" Command: R={red} G={green} B={blue} I={intensity}" f" -> NAD 0x{nad:02X}") logger.info("-" * 70) # Set LED state set_led_color(lin_dev, nad, red, green, blue, intensity) logger.info(f" Waiting {args.settle_time}s for ADC to settle...") time.sleep(args.settle_time) # Verify all ADC signals passed = verify_adc_signals( lin_dev, args.samples, args.interval, exp_red, exp_green, exp_blue ) test_results[name] = passed logger.info(f" >> TEST {idx} {'PASS' if passed else 'FAIL'}") # Turn LEDs off at the end set_led_color(lin_dev, nad, 0, 0, 0, 0) # Summary logger.info("") logger.info("=" * 70) logger.info("TEST SUMMARY") logger.info("=" * 70) all_passed = True for name, passed in test_results.items(): status = "PASS" if passed else "FAIL" logger.info(f" {status} - {name}") if not passed: all_passed = False logger.info("-" * 70) if all_passed: logger.info("RESULT: ALL TESTS PASSED") else: logger.info("RESULT: SOME TESTS FAILED") logger.info("=" * 70) logger.info("Tearing down...") linmaster.teardown() logger.info("Done (ECU still powered)") sys.exit(0 if all_passed else 1) except KeyboardInterrupt: logger.info("") logger.info("Interrupted by user") try: set_led_color(lin_dev, nad, 0, 0, 0, 0) linmaster.teardown() except: pass sys.exit(130) except Exception as e: logger.error(f"Error: {e}", exc_info=True) sys.exit(1) if __name__ == "__main__": main()