494 lines
17 KiB
Python
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()
|