ecu-tests/vendor/automated_lin_test/test_adc_measurements.py

494 lines
17 KiB
Python

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