Files
dragonpilot/selfdrive/controls/tests/test_alka.py
2026-02-13 16:14:14 +08:00

286 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Tests for ALKA (Always-on Lane Keeping Assist) Python layer.
Tests the controlsd logic for computing latActive when ALKA is enabled.
Matches the logic in selfdrive/controls/controlsd.py.
"""
import unittest
from cereal import log
from opendbc.safety import ALTERNATIVE_EXPERIENCE
class TestALKAAlternativeExperience(unittest.TestCase):
"""Test ALTERNATIVE_EXPERIENCE.ALKA constant."""
def test_alka_constant_value(self):
"""ALKA constant should be 1024 (2^10)."""
self.assertEqual(ALTERNATIVE_EXPERIENCE.ALKA, 1024)
def test_alka_flag_bitwise(self):
"""ALKA flag should work with bitwise operations."""
# Test setting ALKA flag
exp = ALTERNATIVE_EXPERIENCE.DEFAULT | ALTERNATIVE_EXPERIENCE.ALKA
self.assertTrue(exp & ALTERNATIVE_EXPERIENCE.ALKA)
# Test combining with other flags
exp = ALTERNATIVE_EXPERIENCE.DISABLE_STOCK_AEB | ALTERNATIVE_EXPERIENCE.ALKA
self.assertTrue(exp & ALTERNATIVE_EXPERIENCE.ALKA)
self.assertTrue(exp & ALTERNATIVE_EXPERIENCE.DISABLE_STOCK_AEB)
# Test without ALKA
exp = ALTERNATIVE_EXPERIENCE.DISABLE_STOCK_AEB
self.assertFalse(exp & ALTERNATIVE_EXPERIENCE.ALKA)
class TestALKALatActive(unittest.TestCase):
"""Test latActive computation with ALKA.
This mirrors the logic in controlsd.py:
alka_enabled = (self.CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALKA) != 0
lkas_on = self.sm['carStateExt'].lkasOn
calibrated = self.sm['liveCalibration'].calStatus == log.LiveCalibrationData.Status.calibrated
gear_ok = CS.gearShifter not in (park, neutral, reverse)
alka_active = lkas_on and gear_ok and calibrated and not CS.seatbeltUnlatched and not CS.doorOpen
CC.latActive = (self.sm['selfdriveState'].active or alka_active) and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \
(not standstill or self.CP.steerAtStandstill)
"""
def _compute_alka_active(self, alka_enabled, lkas_on, gear_ok, calibrated, seatbelt_unlatched, door_open):
"""Compute alka_active matching controlsd.py logic."""
return alka_enabled and lkas_on and gear_ok and calibrated and \
not seatbelt_unlatched and not door_open
def _compute_lat_active(self, selfdrive_active, alka_active, steer_fault_temp, steer_fault_perm, standstill, steer_at_standstill=False):
"""Compute latActive matching controlsd.py logic."""
return (selfdrive_active or alka_active) and not steer_fault_temp and not steer_fault_perm and \
(not standstill or steer_at_standstill)
def test_lat_active_normal_mode(self):
"""Without ALKA, latActive should follow selfdriveState.active."""
alka_active = self._compute_alka_active(
alka_enabled=False, lkas_on=True, gear_ok=True,
calibrated=True, seatbelt_unlatched=False, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=True, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=False)
self.assertTrue(lat_active)
# When selfdrive not active, lat should be inactive
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=False)
self.assertFalse(lat_active)
def test_lat_active_alka_mode(self):
"""With ALKA, latActive can be true even when selfdriveState.active is false."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=True, gear_ok=True,
calibrated=True, seatbelt_unlatched=False, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=False)
self.assertTrue(lat_active)
def test_lat_active_alka_requires_lkas_on(self):
"""ALKA requires lkasOn (ACC Main ON)."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=False, gear_ok=True,
calibrated=True, seatbelt_unlatched=False, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=False)
self.assertFalse(lat_active)
def test_lat_active_alka_requires_gear_ok(self):
"""ALKA requires gear not in P/N/R."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=True, gear_ok=False,
calibrated=True, seatbelt_unlatched=False, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=False)
self.assertFalse(lat_active)
def test_lat_active_alka_requires_calibration(self):
"""ALKA requires calibration to be complete."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=True, gear_ok=True,
calibrated=False, seatbelt_unlatched=False, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=False)
self.assertFalse(lat_active)
def test_lat_active_alka_requires_seatbelt(self):
"""ALKA requires seatbelt to be latched."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=True, gear_ok=True,
calibrated=True, seatbelt_unlatched=True, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=False)
self.assertFalse(lat_active)
def test_lat_active_alka_requires_doors_closed(self):
"""ALKA requires all doors to be closed."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=True, gear_ok=True,
calibrated=True, seatbelt_unlatched=False, door_open=True)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=False)
self.assertFalse(lat_active)
def test_lat_active_blocked_by_steer_fault_temporary(self):
"""Temporary steer fault should block lateral control regardless of ALKA."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=True, gear_ok=True,
calibrated=True, seatbelt_unlatched=False, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=True, steer_fault_perm=False, standstill=False)
self.assertFalse(lat_active)
def test_lat_active_blocked_by_steer_fault_permanent(self):
"""Permanent steer fault should block lateral control regardless of ALKA."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=True, gear_ok=True,
calibrated=True, seatbelt_unlatched=False, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=True, standstill=False)
self.assertFalse(lat_active)
def test_lat_active_blocked_by_standstill(self):
"""ALKA should be blocked at standstill (latActive checks standstill)."""
alka_active = self._compute_alka_active(
alka_enabled=True, lkas_on=True, gear_ok=True,
calibrated=True, seatbelt_unlatched=False, door_open=False)
lat_active = self._compute_lat_active(
selfdrive_active=False, alka_active=alka_active,
steer_fault_temp=False, steer_fault_perm=False, standstill=True)
self.assertFalse(lat_active)
class TestALKASettings(unittest.TestCase):
"""Test ALKA settings configuration."""
def test_alka_setting_in_lateral_section(self):
"""ALKA setting should be in the Lateral section."""
from dragonpilot.settings import SETTINGS
lateral_section = None
for section in SETTINGS:
if section["title"] == "Lateral":
lateral_section = section
break
self.assertIsNotNone(lateral_section, "Lateral section not found in SETTINGS")
# Find ALKA setting
alka_setting = None
for setting in lateral_section["settings"]:
if setting.get("key") == "dp_lat_alka":
alka_setting = setting
break
self.assertIsNotNone(alka_setting, "dp_lat_alka setting not found")
def test_alka_setting_has_brands(self):
"""ALKA setting brands should match safety modes with alka_allowed=true."""
from dragonpilot.settings import SETTINGS
from opendbc.car.structs import CarParams
from opendbc.safety.tests.libsafety import libsafety_py
# Map of brand names to their safety modes
brand_to_safety_mode = {
"toyota": CarParams.SafetyModel.toyota,
"hyundai": CarParams.SafetyModel.hyundai,
"honda": CarParams.SafetyModel.hondaNidec,
"volkswagen": CarParams.SafetyModel.volkswagen,
"subaru": CarParams.SafetyModel.subaru,
"mazda": CarParams.SafetyModel.mazda,
"nissan": CarParams.SafetyModel.nissan,
"ford": CarParams.SafetyModel.ford,
"chrysler": CarParams.SafetyModel.chrysler,
}
# Find ALKA setting
alka_setting = None
for section in SETTINGS:
for setting in section.get("settings", []):
if setting.get("key") == "dp_lat_alka":
alka_setting = setting
break
self.assertIsNotNone(alka_setting)
self.assertIn("brands", alka_setting)
self.assertIsInstance(alka_setting["brands"], list)
# Verify each brand in settings has alka_allowed=true in safety mode
safety = libsafety_py.libsafety
for brand in alka_setting["brands"]:
self.assertIn(brand, brand_to_safety_mode, f"Unknown brand: {brand}")
safety_mode = brand_to_safety_mode[brand]
safety.set_safety_hooks(safety_mode, 0)
safety.init_tests()
self.assertTrue(safety.get_alka_allowed(), f"Brand {brand} should have alka_allowed=true")
class TestALKAAllConditions(unittest.TestCase):
"""Comprehensive truth table tests for all ALKA conditions."""
def test_alka_all_conditions_truth_table(self):
"""Test all combinations of ALKA conditions."""
# Test cases: (alka_enabled, lkas_on, gear_ok, calibrated, seatbelt_unlatched, door_open) -> expected_alka_active
test_cases = [
# All conditions met
(True, True, True, True, False, False, True),
# Missing one condition each
(False, True, True, True, False, False, False), # ALKA disabled
(True, False, True, True, False, False, False), # lkas_on false
(True, True, False, True, False, False, False), # gear not ok (P/N/R)
(True, True, True, False, False, False, False), # Not calibrated
(True, True, True, True, True, False, False), # Seatbelt unlatched
(True, True, True, True, False, True, False), # Door open
# Multiple conditions missing
(True, False, False, True, False, False, False), # No lkas_on + bad gear
(True, True, True, False, True, True, False), # Not calibrated + seatbelt + door
]
for alka_enabled, lkas_on, gear_ok, calibrated, seatbelt_unlatched, door_open, expected in test_cases:
alka_active = alka_enabled and lkas_on and gear_ok and calibrated and \
not seatbelt_unlatched and not door_open
self.assertEqual(alka_active, expected,
f"Failed for alka_enabled={alka_enabled}, lkas_on={lkas_on}, gear_ok={gear_ok}, "
f"calibrated={calibrated}, seatbelt_unlatched={seatbelt_unlatched}, "
f"door_open={door_open}")
def test_lat_active_truth_table(self):
"""Test latActive computation with various inputs."""
# Test cases: (selfdrive_active, alka_active, steer_fault, standstill) -> expected_lat_active
test_cases = [
(False, False, False, False, False), # Nothing active
(True, False, False, False, True), # Selfdrive only
(False, True, False, False, True), # ALKA only
(True, True, False, False, True), # Both active
(True, False, True, False, False), # Selfdrive but fault
(False, True, True, False, False), # ALKA but fault
(True, False, False, True, False), # Selfdrive but standstill (no steerAtStandstill)
(False, True, False, True, False), # ALKA but standstill
]
for selfdrive_active, alka_active, steer_fault, standstill, expected in test_cases:
lat_active = (selfdrive_active or alka_active) and not steer_fault and not standstill
self.assertEqual(lat_active, expected,
f"Failed for selfdrive_active={selfdrive_active}, alka_active={alka_active}, "
f"steer_fault={steer_fault}, standstill={standstill}")
if __name__ == "__main__":
unittest.main()