forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ 0798119
This commit is contained in:
0
iqdbc_repo/iqdbc/car/volkswagen/tests/__init__.py
Normal file
0
iqdbc_repo/iqdbc/car/volkswagen/tests/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from iqdbc.car.volkswagen import mqbcan, pqcan
|
||||
from iqdbc.car.volkswagen.carcontroller import CarController
|
||||
from iqdbc.car.volkswagen.values import CAR, MQB_A0_CARS
|
||||
|
||||
|
||||
def test_is_mqb_a0_car_matches_expected_platforms():
|
||||
assert CAR.VOLKSWAGEN_POLO_MK6 in MQB_A0_CARS
|
||||
assert CAR.VOLKSWAGEN_TCROSS_MK1 in MQB_A0_CARS
|
||||
assert CAR.SKODA_FABIA_MK4 in MQB_A0_CARS
|
||||
assert CAR.SKODA_KAMIQ_MK1 in MQB_A0_CARS
|
||||
assert CarController._is_mqb_a0_car(CAR.VOLKSWAGEN_POLO_MK6)
|
||||
assert CarController._is_mqb_a0_car(CAR.VOLKSWAGEN_TCROSS_MK1)
|
||||
assert CarController._is_mqb_a0_car(CAR.SKODA_FABIA_MK4)
|
||||
assert CarController._is_mqb_a0_car(CAR.SKODA_KAMIQ_MK1)
|
||||
assert not CarController._is_mqb_a0_car(CAR.VOLKSWAGEN_GOLF_MK7)
|
||||
|
||||
|
||||
def test_mqb_steering_torque_scale_only_changes_when_toggle_enabled():
|
||||
controller = object.__new__(CarController)
|
||||
controller.CCS = mqbcan
|
||||
assert controller._get_mqb_steering_torque_scale(0.4, False) == 1.0
|
||||
|
||||
assert controller._get_mqb_steering_torque_scale(0.4, True) == 0.8
|
||||
assert controller._get_mqb_steering_torque_scale(4.0, True) == 1.0
|
||||
|
||||
controller.CCS = pqcan
|
||||
assert controller._get_mqb_steering_torque_scale(0.4, True) == 1.0
|
||||
|
||||
|
||||
def test_mqb_a0_resume_spam_requires_toggle_platform_and_window():
|
||||
controller = object.__new__(CarController)
|
||||
controller.CCS = mqbcan
|
||||
controller.is_mqb_a0 = True
|
||||
controller.frame = 10
|
||||
cs = SimpleNamespace(out=SimpleNamespace(standstill=True))
|
||||
|
||||
assert controller._should_spam_mqb_a0_resume(cs, True)
|
||||
|
||||
controller.frame = 20
|
||||
assert not controller._should_spam_mqb_a0_resume(cs, True)
|
||||
|
||||
controller.frame = 10
|
||||
controller.is_mqb_a0 = False
|
||||
assert not controller._should_spam_mqb_a0_resume(cs, True)
|
||||
|
||||
controller.is_mqb_a0 = True
|
||||
assert not controller._should_spam_mqb_a0_resume(cs, False)
|
||||
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
"""
|
||||
import math
|
||||
from types import SimpleNamespace
|
||||
|
||||
from iqdbc.car.volkswagen.carcontroller import MQBStandstillManager
|
||||
|
||||
|
||||
def _pitch(grade_pct):
|
||||
return math.atan(grade_pct / 100.0)
|
||||
|
||||
|
||||
def _mgr():
|
||||
return MQBStandstillManager(vehicle_mass=1540.0, accel_min=-3.5)
|
||||
|
||||
|
||||
def _cs(*, esp_hold_confirmation=False, esp_stopping=False, rolling_backward=False,
|
||||
rolling_forward=False, brake_pressed=False, gas_pressed=False, standstill=True, v_ego=0.0,
|
||||
sum_wegimpulse=0):
|
||||
out = SimpleNamespace(brakePressed=brake_pressed, gasPressed=gas_pressed, standstill=standstill, vEgo=v_ego)
|
||||
return SimpleNamespace(out=out, esp_hold_confirmation=esp_hold_confirmation,
|
||||
esp_stopping=esp_stopping, rolling_backward=rolling_backward,
|
||||
rolling_forward=rolling_forward, sum_wegimpulse=sum_wegimpulse)
|
||||
|
||||
|
||||
def _run(mgr, cs, *, long_active=True, accel=0.0, stopping=False, starting=False,
|
||||
max_planned_speed=0.0, grade_pct=0.0, tsk_brake_torque=0.0):
|
||||
return mgr.update(cs, long_active, accel, stopping, starting, max_planned_speed,
|
||||
_pitch(grade_pct), tsk_brake_torque)
|
||||
|
||||
|
||||
def _safe_speed(mgr, grade_pct, brake_torque=0.0):
|
||||
return mgr.get_safe_speed_for_brake_torque(_pitch(grade_pct), brake_torque)
|
||||
|
||||
|
||||
# ── brake press ──────────────────────────────────────────────────────────────
|
||||
|
||||
def test_brake_pressed_disables_long_active():
|
||||
mgr = _mgr()
|
||||
long_active, *_ = _run(mgr, _cs(brake_pressed=True), long_active=True)
|
||||
assert not long_active
|
||||
|
||||
|
||||
# ── rollback detection ───────────────────────────────────────────────────────
|
||||
|
||||
def test_rollback_detected_on_rolling_backward():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(rolling_backward=True))
|
||||
assert mgr.rollback_detected
|
||||
|
||||
|
||||
def test_rollback_clears_on_rolling_forward():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(rolling_backward=True))
|
||||
assert mgr.rollback_detected
|
||||
_run(mgr, _cs(rolling_forward=True))
|
||||
assert not mgr.rollback_detected
|
||||
|
||||
|
||||
def test_rollback_forces_brake():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(rolling_backward=True))
|
||||
_, accel, *_ = _run(mgr, _cs(), accel=-0.5)
|
||||
assert accel == mgr.accel_min
|
||||
|
||||
|
||||
def test_rollback_cleared_when_rolling_forward_passes_through_accel():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(rolling_backward=True))
|
||||
_, accel, *_ = _run(mgr, _cs(rolling_forward=True, standstill=False), accel=-0.5)
|
||||
assert accel == -0.5
|
||||
|
||||
|
||||
def test_rollback_forces_brake_with_positive_accel():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(rolling_backward=True))
|
||||
_, accel, stopping, starting, *_ = _run(mgr, _cs(), accel=0.5)
|
||||
assert accel == mgr.accel_min
|
||||
assert stopping
|
||||
assert not starting
|
||||
|
||||
|
||||
# ── safe speed braking ───────────────────────────────────────────────────────
|
||||
|
||||
def test_flat_ground_passes_raw_accel():
|
||||
mgr = _mgr()
|
||||
_, accel, stopping, starting, *_ = _run(mgr, _cs(v_ego=0.0), accel=-0.55,
|
||||
stopping=True, starting=False, grade_pct=0.0)
|
||||
assert accel == -0.55
|
||||
assert stopping
|
||||
assert not starting
|
||||
|
||||
|
||||
def test_current_brake_torque_reduces_safe_speed():
|
||||
mgr = _mgr()
|
||||
zero_brake_safe_speed = _safe_speed(mgr, 20.0)
|
||||
current_brake_safe_speed = _safe_speed(mgr, 20.0, 500.0)
|
||||
assert 0.0 < current_brake_safe_speed < zero_brake_safe_speed
|
||||
|
||||
|
||||
def test_current_brake_torque_can_eliminate_safe_speed():
|
||||
mgr = _mgr()
|
||||
assert _safe_speed(mgr, 20.0, 10000.0) == 0.0
|
||||
|
||||
|
||||
def test_safe_speed_is_capped_at_10_kph():
|
||||
mgr = _mgr()
|
||||
assert _safe_speed(mgr, 100.0) == MQBStandstillManager.MAX_SAFE_STOPPING_SPEED
|
||||
|
||||
|
||||
def test_esp_stopping_passes_raw_accel_on_flat():
|
||||
mgr = _mgr()
|
||||
_, accel, stopping, starting, esp_starting_override, esp_stopping_override = _run(
|
||||
mgr, _cs(esp_stopping=True, v_ego=0.2), accel=-0.55, stopping=True, starting=False,
|
||||
)
|
||||
assert accel == -0.55
|
||||
assert stopping
|
||||
assert not starting
|
||||
assert esp_starting_override is True
|
||||
assert esp_stopping_override is False
|
||||
|
||||
|
||||
def test_below_safe_speed_blends_brake():
|
||||
mgr = _mgr()
|
||||
grade = 20.0
|
||||
safe_speed = _safe_speed(mgr, grade)
|
||||
required_torque = mgr.get_hill_hold_decel_deficit(_pitch(grade), 0.0) * mgr.vehicle_mass * mgr.ASSUMED_WHEEL_RADIUS
|
||||
_, accel, stopping, starting, *_ = _run(
|
||||
mgr, _cs(v_ego=safe_speed * 0.5), accel=-0.55, grade_pct=grade,
|
||||
tsk_brake_torque=required_torque * 0.5,
|
||||
)
|
||||
assert mgr.accel_min < accel < -0.55
|
||||
assert stopping
|
||||
assert not starting
|
||||
|
||||
|
||||
def test_sufficient_brake_torque_passes_raw_accel():
|
||||
mgr = _mgr()
|
||||
grade = 20.0
|
||||
safe_speed = _safe_speed(mgr, grade)
|
||||
_, accel, stopping, starting, *_ = _run(
|
||||
mgr, _cs(v_ego=safe_speed * 0.5), accel=-0.2, stopping=True, starting=False,
|
||||
grade_pct=grade, tsk_brake_torque=10000.0,
|
||||
)
|
||||
assert accel == -0.2
|
||||
assert stopping
|
||||
assert not starting
|
||||
|
||||
|
||||
def test_below_safe_speed_with_low_planned_speed_uses_blended_braking():
|
||||
mgr = _mgr()
|
||||
grade = 20.0
|
||||
safe_speed = _safe_speed(mgr, grade)
|
||||
_, accel, stopping, starting, *_ = _run(
|
||||
mgr, _cs(v_ego=safe_speed * 0.5), accel=-0.55,
|
||||
max_planned_speed=safe_speed * 0.5, grade_pct=grade,
|
||||
)
|
||||
assert mgr.accel_min <= accel < -0.55
|
||||
assert stopping
|
||||
assert not starting
|
||||
|
||||
|
||||
def test_below_safe_speed_with_high_planned_speed_and_negative_accel_uses_blended_braking():
|
||||
mgr = _mgr()
|
||||
grade = 20.0
|
||||
safe_speed = _safe_speed(mgr, grade)
|
||||
_, accel, stopping, starting, *_ = _run(
|
||||
mgr, _cs(v_ego=safe_speed * 0.5), accel=-0.55,
|
||||
max_planned_speed=safe_speed * 2.0, grade_pct=grade,
|
||||
)
|
||||
assert mgr.accel_min <= accel < -0.55
|
||||
assert stopping
|
||||
assert not starting
|
||||
|
||||
|
||||
def test_below_safe_speed_with_high_planned_speed_and_positive_accel_uses_hill_takeoff():
|
||||
mgr = _mgr()
|
||||
grade = 20.0
|
||||
safe_speed = _safe_speed(mgr, grade)
|
||||
_, accel, stopping, starting, *_ = _run(
|
||||
mgr, _cs(v_ego=safe_speed * 0.5), accel=0.1,
|
||||
max_planned_speed=safe_speed * 2.0, grade_pct=grade,
|
||||
)
|
||||
assert accel == max(0.1, 0.1 * grade, 0.2)
|
||||
assert starting
|
||||
assert not stopping
|
||||
|
||||
|
||||
# ── start commit ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_start_commit_on_pre_enable():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(esp_hold_confirmation=True))
|
||||
assert mgr.start_commit_active
|
||||
|
||||
|
||||
def test_start_commit_forces_accel():
|
||||
mgr = _mgr()
|
||||
grade = 10.0
|
||||
expected_min = max(0.1 * grade, 0.2)
|
||||
_run(mgr, _cs(esp_hold_confirmation=True), grade_pct=grade)
|
||||
_, accel, stopping, starting, *_ = _run(
|
||||
mgr, _cs(esp_hold_confirmation=True), accel=0.0, grade_pct=grade,
|
||||
)
|
||||
assert accel >= expected_min
|
||||
assert starting
|
||||
assert not stopping
|
||||
|
||||
|
||||
def test_start_commit_clears_above_safe_speed_while_moving():
|
||||
mgr = _mgr()
|
||||
grade = 20.0
|
||||
safe_speed = _safe_speed(mgr, grade)
|
||||
_run(mgr, _cs(esp_hold_confirmation=True), grade_pct=grade)
|
||||
assert mgr.start_commit_active
|
||||
_run(mgr, _cs(v_ego=safe_speed * 2.0, standstill=True), grade_pct=grade)
|
||||
assert not mgr.start_commit_active
|
||||
|
||||
|
||||
def test_start_commit_persists_below_safe_speed():
|
||||
mgr = _mgr()
|
||||
grade = 20.0
|
||||
safe_speed = _safe_speed(mgr, grade)
|
||||
_run(mgr, _cs(esp_hold_confirmation=True), grade_pct=grade)
|
||||
assert mgr.start_commit_active
|
||||
_, accel, stopping, starting, *_ = _run(
|
||||
mgr, _cs(v_ego=safe_speed * 0.5), accel=-0.55, grade_pct=grade,
|
||||
)
|
||||
assert mgr.start_commit_active
|
||||
assert accel == max(-0.55, 0.1 * grade, 0.2)
|
||||
assert starting
|
||||
assert not stopping
|
||||
|
||||
|
||||
# ── can_stop_forever / ESP override ──────────────────────────────────────────
|
||||
|
||||
def test_can_stop_forever_latches_on_esp_stopping():
|
||||
mgr = _mgr()
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(esp_stopping=True), accel=-1.0, stopping=True)
|
||||
assert mgr.can_stop_forever
|
||||
assert esp_start is True
|
||||
assert esp_stop is False
|
||||
|
||||
|
||||
def test_can_stop_forever_persists():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(esp_stopping=True), accel=-1.0, stopping=True)
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(), accel=-1.0, stopping=True)
|
||||
assert mgr.can_stop_forever
|
||||
assert esp_start is True
|
||||
assert esp_stop is False
|
||||
|
||||
|
||||
def test_can_stop_forever_cleared_by_hold_confirmation():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(esp_stopping=True), accel=-1.0, stopping=True)
|
||||
assert mgr.can_stop_forever
|
||||
_run(mgr, _cs(esp_hold_confirmation=True), accel=-1.0, stopping=True)
|
||||
assert not mgr.can_stop_forever
|
||||
|
||||
|
||||
def test_hold_confirmation_recovers_stop_forever_after_launch_commit():
|
||||
mgr = _mgr()
|
||||
grade = 20.0
|
||||
safe_speed = _safe_speed(mgr, grade)
|
||||
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(esp_hold_confirmation=True, sum_wegimpulse=0),
|
||||
accel=0.0, grade_pct=grade)
|
||||
assert mgr.start_commit_active
|
||||
assert mgr.hold_recovery_active
|
||||
assert esp_start is True
|
||||
assert esp_stop is False
|
||||
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(v_ego=safe_speed * 2.0, standstill=False, sum_wegimpulse=1),
|
||||
accel=0.0, grade_pct=grade)
|
||||
assert not mgr.start_commit_active
|
||||
assert mgr.hold_recovery_active
|
||||
assert esp_start is False
|
||||
assert esp_stop is True
|
||||
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(esp_stopping=True, v_ego=safe_speed * 2.0, standstill=False,
|
||||
sum_wegimpulse=2), accel=0.0, grade_pct=grade)
|
||||
assert mgr.can_stop_forever
|
||||
assert not mgr.hold_recovery_active
|
||||
assert esp_start is True
|
||||
assert esp_stop is False
|
||||
|
||||
|
||||
def test_can_stop_forever_cleared_when_moving():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(esp_stopping=True), accel=-1.0, stopping=True)
|
||||
assert mgr.can_stop_forever
|
||||
_run(mgr, _cs(v_ego=MQBStandstillManager.ESP_OVERRIDE_SPEED + 0.01), accel=0.5)
|
||||
assert not mgr.can_stop_forever
|
||||
|
||||
|
||||
def test_can_stop_forever_cleared_when_long_inactive():
|
||||
mgr = _mgr()
|
||||
_run(mgr, _cs(esp_stopping=True), accel=-1.0, stopping=True)
|
||||
assert mgr.can_stop_forever
|
||||
_run(mgr, _cs(), long_active=False)
|
||||
assert not mgr.can_stop_forever
|
||||
|
||||
|
||||
def test_esp_override_stop_at_standstill():
|
||||
mgr = _mgr()
|
||||
esp_start = esp_stop = None
|
||||
for _ in range(MQBStandstillManager.WEGIMPULSE_STILLNESS_FRAMES + 1):
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(sum_wegimpulse=0), accel=-1.0, stopping=True)
|
||||
assert esp_start is False
|
||||
assert esp_stop is True
|
||||
|
||||
|
||||
def test_esp_override_stop_persists_at_standstill():
|
||||
mgr = _mgr()
|
||||
for _ in range(MQBStandstillManager.WEGIMPULSE_STILLNESS_FRAMES + 1):
|
||||
_run(mgr, _cs(sum_wegimpulse=0), accel=-1.0, stopping=True)
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(sum_wegimpulse=0), accel=-1.0, stopping=True)
|
||||
assert esp_start is False
|
||||
assert esp_stop is True
|
||||
|
||||
|
||||
def test_esp_override_stop_not_sent_while_esp_stopping():
|
||||
mgr = _mgr()
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(esp_stopping=True, sum_wegimpulse=0),
|
||||
accel=-1.0, stopping=True)
|
||||
assert mgr.can_stop_forever
|
||||
assert esp_start is True
|
||||
assert esp_stop is False
|
||||
|
||||
|
||||
def test_esp_override_stop_not_requested_while_moving():
|
||||
mgr = _mgr()
|
||||
esp_start = esp_stop = None
|
||||
for sum_wegimpulse in range(MQBStandstillManager.WEGIMPULSE_STILLNESS_FRAMES + 1):
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(sum_wegimpulse=sum_wegimpulse), accel=-1.0, stopping=True)
|
||||
assert esp_start is True
|
||||
assert esp_stop is False
|
||||
|
||||
|
||||
def test_esp_override_default_below_grant_speed_when_inactive():
|
||||
mgr = _mgr()
|
||||
*_, esp_start, esp_stop = _run(mgr, _cs(), long_active=False)
|
||||
assert esp_start is True
|
||||
assert esp_stop is False
|
||||
|
||||
|
||||
def test_wegimpulse_at_standstill_after_stillness_frames():
|
||||
mgr = _mgr()
|
||||
for _ in range(MQBStandstillManager.WEGIMPULSE_STILLNESS_FRAMES + 1):
|
||||
_run(mgr, _cs(sum_wegimpulse=0))
|
||||
assert mgr.frames_since_last_wheel_pulse >= MQBStandstillManager.WEGIMPULSE_STILLNESS_FRAMES
|
||||
|
||||
|
||||
def test_wegimpulse_resets_on_change():
|
||||
mgr = _mgr()
|
||||
for _ in range(MQBStandstillManager.WEGIMPULSE_STILLNESS_FRAMES):
|
||||
_run(mgr, _cs(sum_wegimpulse=0))
|
||||
_run(mgr, _cs(sum_wegimpulse=1))
|
||||
assert mgr.frames_since_last_wheel_pulse == 0
|
||||
|
||||
|
||||
186
iqdbc_repo/iqdbc/car/volkswagen/tests/test_pq_can_valid.py
Normal file
186
iqdbc_repo/iqdbc/car/volkswagen/tests/test_pq_can_valid.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Regression guard for the class of bug fixed in carstate.py's Diagnose_1 (cluster clock,
|
||||
removed) and EPB_1 (stop-and-go hold, now gated on PQ_SNG_ECD): reading a CAN message via
|
||||
`some_cp.vl["MsgName"]["Signal"]` without declaring it in get_can_parsers_pq() lazily adds
|
||||
it via VLDict.__getitem__ -> CANParser._add_message(key, freq=None), which is NOT the same
|
||||
as ignore-alive (that's what math.nan is for -- see
|
||||
iqdbc/can/tests/test_packer_parser.py::test_lazy_add_not_ignore_alive). An undeclared
|
||||
message defaults to "assume ~1Hz, must be seen within ~10s", so if the real car never sends
|
||||
it, CarState.canValid gets stuck False forever.
|
||||
|
||||
Rather than fuzzing every VolkswagenFlagsIQ combination (most are unreachable through
|
||||
interface.py's real detection logic), each fixture below is the CarParams captured from a
|
||||
real konn3kt route for a real car of that variant. We run CarState.update() once (no CAN
|
||||
data needs to be fed -- .vl[...] lazily adds and returns default-zero values regardless of
|
||||
whether the parser has ever seen a real frame) while recording every message name accessed,
|
||||
then assert that set is a subset of what that real car actually transmits (per its captured
|
||||
CAN fingerprint). A message accessed but not in the real fingerprint is exactly the bug
|
||||
class this guards against.
|
||||
"""
|
||||
from iqdbc.can.parser import VLDict
|
||||
from iqdbc.car import structs
|
||||
from iqdbc.car.volkswagen.carstate import CarState
|
||||
from iqdbc.car.volkswagen.values import CAR, Bus
|
||||
|
||||
|
||||
class _RecordingVLDict(VLDict):
|
||||
"""Records every message name read via .vl[...], including ones lazily added for
|
||||
messages never declared in get_can_parsers_pq's message list. Applied by reclassing
|
||||
a live CANParser.vl instance in place (rather than replacing it with a fresh object)
|
||||
so any messages already registered at CANParser construction time, and the parser
|
||||
back-reference _add_message needs, are preserved."""
|
||||
accessed: set[str]
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, str):
|
||||
self.accessed.add(key)
|
||||
return super().__getitem__(key)
|
||||
|
||||
|
||||
def _make_car_params(car_fingerprint, flags, network_location, transmission_type, enable_bsm, pcm_cruise):
|
||||
CP = structs.CarParams.new_message()
|
||||
CP.carFingerprint = car_fingerprint
|
||||
CP.flags = flags
|
||||
CP.radarUnavailable = True
|
||||
CP.networkLocation = network_location
|
||||
CP.transmissionType = transmission_type
|
||||
CP.enableBsm = enable_bsm
|
||||
CP.pcmCruise = pcm_cruise
|
||||
CP.minSteerSpeed = 0.0
|
||||
CP_IQ = structs.IQCarParams()
|
||||
return CP, CP_IQ
|
||||
|
||||
|
||||
# Real per-variant CarParams captured from konn3kt routes (not hand-derived), so each
|
||||
# fixture reflects an actual car rather than a guess at which flag combos are reachable.
|
||||
# known_bus1/known_bus2 are the exact message names present in that car's real CAN
|
||||
# fingerprint. Bus.pt and Bus.aux both listen on physical bus 1 for PQ (CanBus.powertrain
|
||||
# == CanBus.aux == 1); Bus.cam listens on physical bus 2 (CanBus.cam == 2) -- see CanBus in
|
||||
# iqdbc/car/volkswagen/values.py. UNK_* (unrecognized DBC addresses) are dropped.
|
||||
FIXTURES = [
|
||||
dict(
|
||||
name="jetta_mk6_base",
|
||||
route="a0d99b85ff06857b|0000003a--135c88414f",
|
||||
car_fingerprint=CAR.VOLKSWAGEN_JETTA_MK6,
|
||||
flags=2,
|
||||
network_location="gateway",
|
||||
transmission_type="automatic",
|
||||
enable_bsm=False,
|
||||
pcm_cruise=True,
|
||||
known_bus1={
|
||||
'ACC_GRA_Anzeige', 'ACC_System', 'AWV', 'Airbag_1', 'Airbag_2', 'BSG_Last', 'Bremse_1',
|
||||
'Bremse_10', 'Bremse_11', 'Bremse_2', 'Bremse_3', 'Bremse_4', 'Bremse_5', 'Bremse_8',
|
||||
'Bremse_9', 'Diagnose_1', 'Einheiten_1', 'GRA_Neu', 'Gate_Komf_1', 'Gate_Komf_2',
|
||||
'Getriebe_1', 'Getriebe_2', 'Getriebe_4', 'Ident', 'Klima_1', 'Kombi_1', 'Kombi_2',
|
||||
'Kombi_3', 'Lenkhilfe_1', 'Lenkhilfe_2', 'Lenkhilfe_3', 'Lenkwinkel_1', 'Motor_1',
|
||||
'Motor_10', 'Motor_12', 'Motor_2', 'Motor_3', 'Motor_5', 'Motor_6', 'Motor_7', 'Motor_8',
|
||||
'Motor_Bremse', 'Motor_Flexia', 'Soll_Verbauliste_neu', 'Systeminfo_1', 'Waehlhebel_1',
|
||||
},
|
||||
known_bus2={
|
||||
'ACC_GRA_Anzeige', 'ACC_System', 'AWV', 'Airbag_1', 'BSG_Last', 'Bremse_1', 'Bremse_10',
|
||||
'Bremse_11', 'Bremse_2', 'Bremse_3', 'Bremse_5', 'Bremse_8', 'Diagnose_1', 'Einheiten_1',
|
||||
'GRA_Neu', 'Gate_Komf_1', 'Gate_Komf_2', 'Getriebe_1', 'Ident', 'Kombi_1', 'Kombi_2',
|
||||
'Kombi_3', 'Lenkhilfe_2', 'Lenkhilfe_3', 'Lenkwinkel_1', 'Motor_1', 'Motor_10', 'Motor_2',
|
||||
'Motor_3', 'Motor_5', 'Motor_Flexia', 'Soll_Verbauliste_neu', 'Systeminfo_1',
|
||||
},
|
||||
),
|
||||
dict(
|
||||
name="passat_nms_sng_ecd",
|
||||
route="0f53129ed44f6920|00000031--0c1aea511e",
|
||||
car_fingerprint=CAR.VOLKSWAGEN_PASSAT_NMS,
|
||||
flags=524418, # PQ | IQ_LVBS_ALC_MODULE | IQ_PQ_SNG_ECD
|
||||
network_location="gateway",
|
||||
transmission_type="automatic",
|
||||
enable_bsm=False,
|
||||
pcm_cruise=False,
|
||||
known_bus1={
|
||||
'ACC_GRA_Anzeige', 'ACC_System', 'AWV', 'Airbag_1', 'Airbag_2', 'BSG_Last', 'Bremse_1',
|
||||
'Bremse_10', 'Bremse_11', 'Bremse_2', 'Bremse_3', 'Bremse_4', 'Bremse_5', 'Bremse_8',
|
||||
'Bremse_9', 'Diagnose_1', 'EPB_1', 'EPB_2', 'Einheiten_1', 'GRA_Neu', 'Gate_Komf_1',
|
||||
'Gate_Komf_2', 'Getriebe_1', 'Getriebe_2', 'Ident', 'Klima_1', 'Kombi_1', 'Kombi_2',
|
||||
'Kombi_3', 'Lenkhilfe_1', 'Lenkhilfe_2', 'Lenkhilfe_3', 'Lenkwinkel_1', 'Motor_1',
|
||||
'Motor_10', 'Motor_12', 'Motor_13', 'Motor_2', 'Motor_3', 'Motor_5', 'Motor_6', 'Motor_7',
|
||||
'Motor_8', 'Motor_Bremse', 'Motor_Flexia', 'Soll_Verbauliste_neu', 'Systeminfo_1',
|
||||
},
|
||||
known_bus2={
|
||||
'ACC_GRA_Anzeige', 'ACC_System', 'AWV', 'Airbag_1', 'BSG_Last', 'Bremse_1', 'Bremse_10',
|
||||
'Bremse_11', 'Bremse_2', 'Bremse_3', 'Bremse_5', 'Bremse_8', 'Diagnose_1', 'EPB_1',
|
||||
'Einheiten_1', 'GRA_Neu', 'Gate_Komf_1', 'Gate_Komf_2', 'Getriebe_1', 'Ident', 'Kombi_1',
|
||||
'Kombi_2', 'Kombi_3', 'Lenkhilfe_2', 'Lenkhilfe_3', 'Lenkwinkel_1', 'Motor_1', 'Motor_10',
|
||||
'Motor_2', 'Motor_3', 'Motor_5', 'Motor_Flexia', 'Soll_Verbauliste_neu', 'Systeminfo_1',
|
||||
},
|
||||
),
|
||||
dict(
|
||||
name="passat_b7_acc_fts_epb",
|
||||
route="20e3cd4f0d5f39d1|0000008e--274d762bac",
|
||||
car_fingerprint=CAR.VOLKSWAGEN_PASSAT_B7,
|
||||
flags=262146, # PQ | IQ_PQ_ACC_FTS_EPB
|
||||
network_location="gateway",
|
||||
transmission_type="automatic",
|
||||
enable_bsm=True,
|
||||
pcm_cruise=False,
|
||||
known_bus1={
|
||||
'ACC_GRA_Anzeige', 'ACC_System', 'AWV', 'Airbag_1', 'Airbag_2', 'BSG_Last', 'Bremse_1',
|
||||
'Bremse_10', 'Bremse_11', 'Bremse_2', 'Bremse_3', 'Bremse_4', 'Bremse_5', 'Bremse_8',
|
||||
'Bremse_9', 'Daempfer_1', 'Diagnose_1', 'EPB_1', 'Einheiten_1', 'GRA_Neu', 'Gate_Komf_1',
|
||||
'Gate_Komf_2', 'Getriebe_1', 'Getriebe_2', 'Getriebe_4', 'HCA_1', 'Ident', 'Klima_1',
|
||||
'Kombi_1', 'Kombi_2', 'Kombi_3', 'Lenkhilfe_1', 'Lenkhilfe_2', 'Lenkhilfe_3',
|
||||
'Lenkwinkel_1', 'Motor_1', 'Motor_10', 'Motor_12', 'Motor_2', 'Motor_3', 'Motor_5',
|
||||
'Motor_6', 'Motor_7', 'Motor_8', 'Motor_Bremse', 'Motor_Flexia', 'Parkhilfe_01',
|
||||
'Soll_Verbauliste_neu', 'Systeminfo_1', 'Waehlhebel_1',
|
||||
},
|
||||
known_bus2={
|
||||
'ACC_GRA_Anzeige', 'ACC_System', 'AWV', 'Airbag_1', 'BSG_Last', 'Bremse_1', 'Bremse_10',
|
||||
'Bremse_11', 'Bremse_2', 'Bremse_3', 'Bremse_5', 'Bremse_8', 'Diagnose_1', 'EPB_1',
|
||||
'Einheiten_1', 'GRA_Neu', 'Gate_Komf_1', 'Gate_Komf_2', 'Getriebe_1', 'HCA_1', 'Ident',
|
||||
'Kombi_1', 'Kombi_2', 'Kombi_3', 'LDW_Status', 'Lenkhilfe_2', 'Lenkhilfe_3',
|
||||
'Lenkwinkel_1', 'Motor_1', 'Motor_10', 'Motor_2', 'Motor_3', 'Motor_5', 'Motor_Flexia',
|
||||
'Parkhilfe_01', 'RDK_Status', 'SWA_1', 'Soll_Verbauliste_neu', 'Systeminfo_1',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class TestVolkswagenPqCanValid:
|
||||
def test_only_reads_messages_the_real_car_sends(self, subtests):
|
||||
for fixture in FIXTURES:
|
||||
with subtests.test(car=fixture["name"]):
|
||||
CP, CP_IQ = _make_car_params(
|
||||
fixture["car_fingerprint"], fixture["flags"], fixture["network_location"],
|
||||
fixture["transmission_type"], fixture["enable_bsm"], fixture["pcm_cruise"],
|
||||
)
|
||||
|
||||
CS = CarState(CP, CP_IQ)
|
||||
can_parsers = CS.get_can_parsers(CP, CP_IQ)
|
||||
|
||||
recorders = {}
|
||||
for bus, parser in can_parsers.items():
|
||||
if parser is None:
|
||||
continue
|
||||
parser.vl.__class__ = _RecordingVLDict
|
||||
parser.vl.accessed = set()
|
||||
recorders[bus] = parser.vl
|
||||
|
||||
# no CAN data is fed: .vl[...] lazily adds and returns default-zero values
|
||||
# regardless of whether the parser has ever seen a real frame, so this alone
|
||||
# is enough to harvest every message name update_pq() touches for this variant
|
||||
CS.update(can_parsers)
|
||||
|
||||
accessed_bus1 = recorders[Bus.pt].accessed | recorders[Bus.aux].accessed
|
||||
accessed_bus2 = recorders[Bus.cam].accessed
|
||||
|
||||
extra_bus1 = accessed_bus1 - fixture["known_bus1"]
|
||||
extra_bus2 = accessed_bus2 - fixture["known_bus2"]
|
||||
|
||||
assert not extra_bus1, (
|
||||
f"{fixture['name']}: code reads {sorted(extra_bus1)} on bus 1 (pt/aux) but the "
|
||||
f"real car (route {fixture['route']}) never transmits it -- this needs to be "
|
||||
f"gated behind whatever flag makes it optional, or declared with math.nan if it's "
|
||||
f"genuinely on-demand/rare, not read unconditionally"
|
||||
)
|
||||
assert not extra_bus2, (
|
||||
f"{fixture['name']}: code reads {sorted(extra_bus2)} on bus 2 (cam) but the "
|
||||
f"real car (route {fixture['route']}) never transmits it -- this needs to be "
|
||||
f"gated behind whatever flag makes it optional, or declared with math.nan if it's "
|
||||
f"genuinely on-demand/rare, not read unconditionally"
|
||||
)
|
||||
33
iqdbc_repo/iqdbc/car/volkswagen/tests/test_stopping.py
Normal file
33
iqdbc_repo/iqdbc/car/volkswagen/tests/test_stopping.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
|
||||
from iqdbc.car.volkswagen.interface import CarInterface
|
||||
from iqdbc.car.volkswagen.values import (
|
||||
CAR, PASSAT_B7_STOP_ACCEL, PASSAT_B7_STOPPING_SPEED, PQ_STOPPING_SPEED,
|
||||
VolkswagenFlags, apply_pq_stopping_accel, get_longitudinal_stopping_speed_override,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("candidate, flags, expected", [
|
||||
(CAR.VOLKSWAGEN_PASSAT_B7, VolkswagenFlags.PQ, PASSAT_B7_STOPPING_SPEED),
|
||||
(CAR.VOLKSWAGEN_JETTA_MK6, VolkswagenFlags.PQ, PQ_STOPPING_SPEED),
|
||||
(CAR.VOLKSWAGEN_GOLF_MK7, 0, 0.0),
|
||||
(CAR.VOLKSWAGEN_ID4_MK1, VolkswagenFlags.MEB, 0.0),
|
||||
])
|
||||
def test_stopping_speed_override(candidate, flags, expected):
|
||||
assert get_longitudinal_stopping_speed_override(candidate, flags) == expected
|
||||
|
||||
|
||||
def test_passat_b7_stop_accel_is_exact():
|
||||
assert apply_pq_stopping_accel(CAR.VOLKSWAGEN_PASSAT_B7, -0.2, True) == PASSAT_B7_STOP_ACCEL == -0.55
|
||||
assert apply_pq_stopping_accel(CAR.VOLKSWAGEN_PASSAT_B7, -0.2, False) == -0.2
|
||||
assert apply_pq_stopping_accel(CAR.VOLKSWAGEN_JETTA_MK6, -0.2, True) == -0.2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("candidate", [
|
||||
CAR.VOLKSWAGEN_JETTA_MK6,
|
||||
CAR.VOLKSWAGEN_PASSAT_NMS,
|
||||
])
|
||||
def test_pq_longitudinal_feedforward_uses_active_schema_field(candidate):
|
||||
fingerprint = {bus: {} for bus in range(8)}
|
||||
cp = CarInterface.get_params(candidate, fingerprint, [], alpha_long=True, is_release=False, docs=False)
|
||||
assert cp.longitudinalTuning.kf == pytest.approx(1.2)
|
||||
129
iqdbc_repo/iqdbc/car/volkswagen/tests/test_volkswagen.py
Normal file
129
iqdbc_repo/iqdbc/car/volkswagen/tests/test_volkswagen.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import random
|
||||
import re
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from iqdbc.car import structs
|
||||
from iqdbc.car.volkswagen.carstate import CarState
|
||||
from iqdbc.car.structs import CarParams
|
||||
from iqdbc.car.volkswagen.values import CAR, FW_QUERY_CONFIG, WMI
|
||||
from iqdbc.car.volkswagen.fingerprints import FW_VERSIONS
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
|
||||
CHASSIS_CODE_PATTERN = re.compile('[A-Z0-9]{2}')
|
||||
# TODO: determine the unknown groups
|
||||
SPARE_PART_FW_PATTERN = re.compile(b'\xf1\x87(?P<gateway>[0-9][0-9A-Z]{2})(?P<unknown>[0-9][0-9A-Z][0-9])(?P<unknown2>[0-9A-Z]{2}[0-9])([A-Z0-9]| )')
|
||||
|
||||
|
||||
class TestVolkswagenPlatformConfigs:
|
||||
def test_spare_part_fw_pattern(self, subtests):
|
||||
# Relied on for determining if a FW is likely VW
|
||||
for platform, ecus in FW_VERSIONS.items():
|
||||
with subtests.test(platform=platform.value):
|
||||
for fws in ecus.values():
|
||||
for fw in fws:
|
||||
assert SPARE_PART_FW_PATTERN.match(fw) is not None, f"Bad FW: {fw}"
|
||||
|
||||
def test_chassis_codes(self, subtests):
|
||||
for platform in CAR:
|
||||
with subtests.test(platform=platform.value):
|
||||
assert len(platform.config.wmis) > 0, "WMIs not set"
|
||||
assert len(platform.config.chassis_codes) > 0, "Chassis codes not set"
|
||||
assert all(CHASSIS_CODE_PATTERN.match(cc) for cc in
|
||||
platform.config.chassis_codes), "Bad chassis codes"
|
||||
|
||||
# No two platforms should share chassis codes
|
||||
for comp in CAR:
|
||||
if platform == comp:
|
||||
continue
|
||||
|
||||
shared_chassis_codes = platform.config.chassis_codes & comp.config.chassis_codes
|
||||
if len(shared_chassis_codes) == 0:
|
||||
continue
|
||||
|
||||
# A shared chassis code is unambiguous when the VIN WMI separates the candidates.
|
||||
if platform.config.wmis.isdisjoint(comp.config.wmis):
|
||||
continue
|
||||
|
||||
platform_model_years = getattr(platform.config, "model_years", set())
|
||||
comp_model_years = getattr(comp.config, "model_years", set())
|
||||
if platform_model_years and comp_model_years and platform_model_years.isdisjoint(comp_model_years):
|
||||
continue
|
||||
|
||||
assert set() == shared_chassis_codes, f"Shared chassis codes: {comp}"
|
||||
|
||||
def test_custom_fuzzy_fingerprinting(self, subtests):
|
||||
all_radar_fw = list({fw for ecus in FW_VERSIONS.values() for fw in ecus.get((Ecu.fwdRadar, 0x757, None), [])})
|
||||
|
||||
for platform in CAR:
|
||||
with subtests.test(platform=platform.name):
|
||||
# Fuzzy matching keys off the radar ECU (CHECK_FUZZY_ECUS), so a platform that
|
||||
# declares no radar ECU can never be VIN-matched and should never be expected to match.
|
||||
platform_has_radar = (Ecu.fwdRadar, 0x757, None) in FW_VERSIONS.get(platform, {})
|
||||
for wmi in WMI:
|
||||
for chassis_code in platform.config.chassis_codes | {"00"}:
|
||||
platform_model_years = getattr(platform.config, "model_years", set())
|
||||
model_years = platform_model_years if platform_model_years else {"0"}
|
||||
for model_year in model_years | {"0"}:
|
||||
vin = ["0"] * 17
|
||||
vin[0:3] = wmi
|
||||
vin[6:8] = chassis_code
|
||||
vin[9] = model_year
|
||||
vin = "".join(vin)
|
||||
|
||||
# Check a few FW cases - expected, unexpected
|
||||
for radar_fw in random.sample(all_radar_fw, 5) + [b'\xf1\x875Q0907572G \xf1\x890571', b'\xf1\x877H9907572AA\xf1\x890396']:
|
||||
model_year_match = len(platform_model_years) == 0 or model_year in platform_model_years
|
||||
should_match = ((wmi in platform.config.wmis and chassis_code in platform.config.chassis_codes and model_year_match) and
|
||||
radar_fw in all_radar_fw and platform_has_radar)
|
||||
|
||||
live_fws = {(0x757, None): [radar_fw]}
|
||||
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fws, vin, FW_VERSIONS)
|
||||
|
||||
expected_matches = {platform} if should_match else set()
|
||||
assert expected_matches == matches, "Bad match"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("button_type", (
|
||||
structs.CarState.ButtonEvent.Type.setCruise,
|
||||
structs.CarState.ButtonEvent.Type.resumeCruise,
|
||||
))
|
||||
def test_button_enable_is_blocked_while_cruise_fault_lateral_mode_is_still_faulted(button_type):
|
||||
state = object.__new__(CarState)
|
||||
state.CP = SimpleNamespace(pcmCruise=False)
|
||||
state.cruise_fault_lateral_active = True
|
||||
state.cruise_faulted = True
|
||||
|
||||
button_events = [structs.CarState.ButtonEvent(pressed=False, type=button_type)]
|
||||
|
||||
assert not state.update_button_enable(button_events)
|
||||
|
||||
|
||||
def test_button_enable_recovers_once_cruise_fault_clears():
|
||||
state = object.__new__(CarState)
|
||||
state.CP = SimpleNamespace(pcmCruise=False)
|
||||
state.cruise_fault_lateral_active = True
|
||||
state.cruise_faulted = False
|
||||
|
||||
button_events = [structs.CarState.ButtonEvent(
|
||||
pressed=False,
|
||||
type=structs.CarState.ButtonEvent.Type.setCruise,
|
||||
)]
|
||||
|
||||
assert state.update_button_enable(button_events)
|
||||
|
||||
|
||||
def test_pq_hca_ready_does_not_complete_eps_initialization():
|
||||
state = object.__new__(CarState)
|
||||
state.eps_init_complete = False
|
||||
|
||||
state.frame = 0
|
||||
assert state.update_hca_state("READY", ready_confirms_init=False) == (True, False)
|
||||
|
||||
state.frame = 317
|
||||
assert state.update_hca_state("FAULT", ready_confirms_init=False) == (True, False)
|
||||
|
||||
state.frame = 1001
|
||||
assert state.update_hca_state("FAULT", ready_confirms_init=False) == (False, True)
|
||||
Reference in New Issue
Block a user