IQ.Pilot Prebuilt Release @ 7a91404

This commit is contained in:
IQ.Lvbs CI [bot]
2026-08-31 23:04:09 -05:00
commit e2b219bcf7
2545 changed files with 677873 additions and 0 deletions

View File

@@ -0,0 +1,323 @@
#!/usr/bin/env python3
import argparse
import os
import pickle
import re
import subprocess
import sys
import tempfile
import traceback
import zstandard as zstd
from tqdm import tqdm
from tqdm.contrib.concurrent import process_map
from urllib.request import urlopen
from collections import defaultdict
from pathlib import Path
from typing import Any
from iqdbc.car import structs
from iqdbc.car.can_definitions import CanData
from iqdbc.car.car_helpers import can_fingerprint, interfaces
from iqdbc.car.logreader import LogReader, decompress_stream
TOLERANCE = 1e-4
DIFF_BUCKET = "car_diff"
IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"]
PADDING = 5
Diff = tuple[str, int, tuple[Any, Any], int]
Ref = tuple[int, structs.CarState]
Result = tuple[str, str, list[Diff], list[Ref] | None, list[structs.CarState] | None, str | None]
def dict_diff(d1: dict[str, Any], d2: dict[str, Any], path: str = "", ignore: list[str] | None = None, tolerance: float = 0) -> list[tuple]:
ignore = ignore or []
diffs = []
for key in d1.keys() | d2.keys():
if key in ignore:
continue
full_path = f"{path}.{key}" if path else key
v1, v2 = d1.get(key), d2.get(key)
if isinstance(v1, dict) and isinstance(v2, dict):
diffs.extend(dict_diff(v1, v2, full_path, ignore, tolerance))
elif isinstance(v1, (int, float)) and isinstance(v2, (int, float)):
if abs(v1 - v2) > tolerance:
diffs.append(("change", full_path, (v1, v2)))
elif v1 != v2:
diffs.append(("change", full_path, (v1, v2)))
return diffs
def load_can_messages(seg: str) -> list[Any]:
from comma_car_segments import get_url
parts = seg.split("/")
url = get_url(f"{parts[0]}/{parts[1]}", parts[2])
msgs = LogReader(url, only_union_types=True, sort_by_time=True)
return [m for m in msgs if m.which() == 'can']
def replay_segment(platform: str, can_msgs: list[Any]) -> tuple[structs.CarParams, list[structs.CarState], list[int]]:
_can_msgs = ([CanData(can.address, can.dat, can.src) for can in m.can] for m in can_msgs)
def can_recv(wait_for_one: bool = False) -> list[list[CanData]]:
return [next(_can_msgs, [])]
_, fingerprint = can_fingerprint(can_recv)
CarInterface = interfaces[platform]
CP = CarInterface.get_params(platform, fingerprint, [], False, False, False)
CP_IQ = CarInterface.get_params_iq(CP, platform, fingerprint, [], False, False, False)
CI = CarInterface(CP, CP_IQ)
CC = structs.CarControl().as_reader()
states, timestamps = [], []
for msg in can_msgs:
frames = [CanData(c.address, c.dat, c.src) for c in msg.can]
states.append(CI.update([(msg.logMonoTime, frames)]))
CI.apply(CC, msg.logMonoTime)
timestamps.append(msg.logMonoTime)
return CP, states, timestamps
def process_segment(args: tuple) -> Result:
platform, seg, ref_path, update = args
try:
can_msgs = load_can_messages(seg)
CP, states, timestamps = replay_segment(platform, can_msgs)
ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst"
if update:
data = {"cp": CP.to_dict(), "frames": list(zip(timestamps, states, strict=True))}
ref_file.write_bytes(zstd.compress(pickle.dumps(data), 10))
return (platform, seg, [], None, None, None)
if not ref_file.exists():
return (platform, seg, [], None, None, "no ref")
ref_data = pickle.loads(decompress_stream(ref_file.read_bytes()))
cp: dict[str, Any] = ref_data["cp"]
ref: list[Ref] = ref_data["frames"]
diffs = []
for diff in dict_diff(cp, CP.to_dict(), path="carParams", ignore=IGNORE_FIELDS, tolerance=TOLERANCE):
diffs.append((diff[1], -1, diff[2], 0))
for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)):
for diff in dict_diff(ref_state.to_dict(), state.to_dict(), ignore=IGNORE_FIELDS, tolerance=TOLERANCE):
diffs.append((diff[1], i, diff[2], ts))
return (platform, seg, diffs, ref, states, None)
except Exception:
return (platform, seg, [], None, None, traceback.format_exc())
def get_changed_platforms(cwd: Path, database: dict[str, Any], interfaces: dict[str, Any]) -> list[str]:
git_ref = os.environ.get("GIT_REF", "origin/master")
changed = subprocess.check_output(["git", "diff", "--name-only", f"{git_ref}...HEAD"], cwd=cwd, encoding='utf8').strip()
brands = set()
patterns = [r"iqdbc/car/(\w+)/", r"iqdbc/dbc/(\w+?)_", r"iqdbc/dbc/generator/(\w+)", r"iqdbc/safety/modes/(\w+?)[_.]"]
for line in changed.splitlines():
for pattern in patterns:
m = re.search(pattern, line)
if m:
brands.add(m.group(1).lower())
return [p for p in interfaces if any(b in p.lower() for b in brands) and p in database]
def download_refs(ref_path: Path, platforms: list[str], segments: dict[str, list[str]]) -> None:
base_url = f"https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/{DIFF_BUCKET}"
for platform in tqdm(platforms):
for seg in segments.get(platform, []):
filename = f"{platform}_{seg.replace('/', '_')}.zst"
with urlopen(f"{base_url}/{filename}") as resp:
(Path(ref_path) / filename).write_bytes(resp.read())
def run_replay(platforms: list[str], segments: dict[str, list[str]], ref_path: Path, update: bool, workers: int = 4) -> list[Result]:
work = [(platform, seg, ref_path, update)
for platform in platforms for seg in segments.get(platform, [])]
return process_map(process_segment, work, max_workers=workers)
# ASCII waveforms helpers
def find_edges(vals: list[bool]) -> tuple[list[int], list[int]]:
rises = []
falls = []
prev = vals[0]
for i, val in enumerate(vals):
if val and not prev:
rises.append(i)
if not val and prev:
falls.append(i)
prev = val
return rises, falls
def render_waveform(label: str, vals: list[bool]) -> str:
wave = {(False, False): "_", (True, True): "", (False, True): "/", (True, False): "\\"}
line = f" {label}:".ljust(12)
prev = vals[0]
for val in vals:
line += wave[(prev, val)]
prev = val
if len(line) > 80:
line = line[:80] + "..."
return line
def format_timing(edge_type: str, master_edges: list[int], pr_edges: list[int], ms_per_frame: float) -> str | None:
if not master_edges or not pr_edges:
return None
delta = pr_edges[0] - master_edges[0]
if delta == 0:
return None
direction = "lags" if delta > 0 else "leads"
ms = int(abs(delta) * ms_per_frame)
return " " * 12 + f"{edge_type}: PR {direction} by {abs(delta)} frames ({ms}ms)"
def group_frames(diffs: list[Diff], max_gap: int = 15) -> list[list[Diff]]:
groups = []
current = [diffs[0]]
for diff in diffs[1:]:
_, frame, _, _ = diff
_, prev_frame, _, _ = current[-1]
if frame <= prev_frame + max_gap:
current.append(diff)
else:
groups.append(current)
current = [diff]
groups.append(current)
return groups
def build_signals(group: list[Diff], ref: list[Ref], states: list[structs.CarState], field: str) -> tuple[list[Any], list[Any], int, int]:
_, first_frame, _, _ = group[0]
_, last_frame, _, _ = group[-1]
start = max(0, first_frame - PADDING)
end = min(last_frame + PADDING + 1, len(ref))
master_vals = []
pr_vals = []
for frame in range(start, end):
mval = ref[frame][1].to_dict()
pval = states[frame].to_dict()
for k in field.split("."):
mval = mval.get(k) if isinstance(mval, dict) else None
pval = pval.get(k) if isinstance(pval, dict) else None
master_vals.append(mval)
pr_vals.append(pval)
return master_vals, pr_vals, start, end
def format_numeric_diffs(diffs: list[Diff]) -> list[str]:
lines = []
for _, frame, (old_val, new_val), _ in diffs[:10]:
lines.append(f" frame {frame}: {old_val} -> {new_val}")
if len(diffs) > 10:
lines.append(f" (... {len(diffs) - 10} more)")
return lines
def format_boolean_diffs(diffs: list[Diff], ref: list[Ref], states: list[structs.CarState], field: str) -> list[str]:
_, first_frame, _, first_ts = diffs[0]
_, last_frame, _, last_ts = diffs[-1]
frame_time = last_frame - first_frame
time_ms = (last_ts - first_ts) / 1e6
ms = time_ms / frame_time if frame_time else 10.0
lines = []
for group in group_frames(diffs):
master_vals, pr_vals, start, end = build_signals(group, ref, states, field)
master_rises, master_falls = find_edges(master_vals)
pr_rises, pr_falls = find_edges(pr_vals)
lines.append(f"\n frames {start}-{end - 1}")
lines.append(render_waveform("master", master_vals))
lines.append(render_waveform("PR", pr_vals))
for edge_type, master_edges, pr_edges in [("rise", master_rises, pr_rises), ("fall", master_falls, pr_falls)]:
msg = format_timing(edge_type, master_edges, pr_edges, ms)
if msg:
lines.append(msg)
return lines
def format_diff(diffs: list[Diff], ref: list[Ref], states: list[structs.CarState], field: str) -> list[str]:
if not diffs:
return []
_, _, (old, new), _ = diffs[0]
is_bool = isinstance(old, bool) and isinstance(new, bool)
if is_bool:
return format_boolean_diffs(diffs, ref, states, field)
return format_numeric_diffs(diffs)
def main(platform: str | None = None, segments_per_platform: int = 10, update_refs: bool = False, all_platforms: bool = False) -> int:
from comma_car_segments import get_comma_car_segments_database
cwd = Path(__file__).resolve().parents[3]
ref_path = cwd / DIFF_BUCKET
if not update_refs:
ref_path = Path(tempfile.mkdtemp())
ref_path.mkdir(exist_ok=True)
database = get_comma_car_segments_database()
if all_platforms:
print("Running all platforms...")
platforms = [p for p in interfaces if p in database]
elif platform and platform in interfaces:
platforms = [platform]
else:
platforms = get_changed_platforms(cwd, database, interfaces)
print("## Car behavior report")
print("Replays driving segments through this PR and compares the behavior to master.")
print("Please review any changes carefully to ensure they are expected.\n")
if not platforms:
print("✅ No changes detected")
return 0
segments = {p: database.get(p, [])[:segments_per_platform] for p in platforms}
n_segments = sum(len(s) for s in segments.values())
print(f"{'Generating' if update_refs else 'Testing'} {n_segments} segments for: {', '.join(platforms)}")
if update_refs:
results = run_replay(platforms, segments, ref_path, update=True)
errors = [e for _, _, _, _, _, e in results if e]
assert len(errors) == 0, f"Segment failures: {errors}"
print(f"Generated {n_segments} refs to {ref_path}")
return 0
download_refs(ref_path, platforms, segments)
results = run_replay(platforms, segments, ref_path, update=False)
with_diffs = [(platform, seg, diffs, ref, states)
for platform, seg, diffs, ref, states, err in results if diffs]
errors = [(platform, seg, err) for platform, seg, diffs, ref, states, err in results if err]
n_passed = len(results) - len(with_diffs) - len(errors)
icon = "⚠️" if with_diffs else ""
print(f"\n{icon} {len(with_diffs)} changed, {n_passed} passed, {len(errors)} errors")
for plat, seg, err in errors:
print(f"\nERROR {plat} - {seg}: {err}")
if with_diffs:
print("<details><summary><b>Show changes</b></summary>\n\n```")
for plat, seg, diffs, ref, states in with_diffs:
print(f"\n{plat} - {seg}")
by_field = defaultdict(list)
for d in diffs:
by_field[d[0]].append(d)
for field, fd in sorted(by_field.items()):
print(f"\n {field} ({len(fd)} diffs)")
for line in format_diff(fd, ref, states, field):
print(line)
print("```\n</details>")
return 1 if errors else 0
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--platform", help="diff single platform")
parser.add_argument("--segments-per-platform", type=int, default=10, help="number of segments to diff per platform")
parser.add_argument("--update-refs", action="store_true", help="update refs based on current commit")
parser.add_argument("--all", action="store_true", help="run diff on all platforms")
args = parser.parse_args()
sys.exit(main(args.platform, args.segments_per_platform, args.update_refs, args.all))

View File

@@ -0,0 +1,413 @@
from typing import NamedTuple
from iqdbc.car.chrysler.values import CAR as CHRYSLER
from iqdbc.car.gm.values import CAR as GM
from iqdbc.car.ford.values import CAR as FORD
from iqdbc.car.honda.values import CAR as HONDA
from iqdbc.car.hyundai.values import CAR as HYUNDAI
from iqdbc.car.nissan.values import CAR as NISSAN
from iqdbc.car.mazda.values import CAR as MAZDA
from iqdbc.car.mock.values import CAR as MOCK
from iqdbc.car.rivian.values import CAR as RIVIAN
from iqdbc.car.subaru.values import CAR as SUBARU
from iqdbc.car.tesla.values import CAR as TESLA
from iqdbc.car.toyota.values import CAR as TOYOTA
from iqdbc.car.values import Platform
from iqdbc.car.volkswagen.values import CAR as VOLKSWAGEN
from iqdbc.car.body.values import CAR as COMMA
from iqdbc.car.psa.values import CAR as PSA
# FIXME: add routes for these cars
route_exempt_cars = [
MOCK.MOCK,
GM.CADILLAC_ATS,
GM.HOLDEN_ASTRA,
GM.CHEVROLET_MALIBU,
HYUNDAI.GENESIS_G90,
VOLKSWAGEN.VOLKSWAGEN_CRAFTER_MK2, # need a route from an ACC-equipped Crafter
SUBARU.SUBARU_FORESTER_HYBRID,
# Honda/Acura test routes below expired, replace when CI bucket sync is fixed
HONDA.ACURA_TLX_2G,
HONDA.HONDA_NBOX_2G,
HONDA.ACURA_MDX_4G_MMR,
HONDA.HONDA_CITY_7G,
# These had their DSUs unplugged, need new routes
# TOYOTA.LEXUS_ES # hybrid
TOYOTA.TOYOTA_COROLLA,
TOYOTA.TOYOTA_RAV4H,
HONDA.HONDA_CLARITY,
GM.CHEVROLET_BOLT_NON_ACC,
GM.CHEVROLET_BOLT_NON_ACC_1ST_GEN,
GM.CHEVROLET_BOLT_NON_ACC_2ND_GEN,
GM.CHEVROLET_EQUINOX_NON_ACC_3RD_GEN,
GM.CHEVROLET_SUBURBAN_NON_ACC_11TH_GEN,
GM.CADILLAC_CT6_NON_ACC_1ST_GEN,
GM.CHEVROLET_TRAILBLAZER_NON_ACC_2ND_GEN,
GM.CHEVROLET_MALIBU_NON_ACC_9TH_GEN,
GM.CADILLAC_XT5_NON_ACC_1ST_GEN,
HYUNDAI.HYUNDAI_IONIQ_5_PE,
HYUNDAI.HYUNDAI_IONIQ_5_N,
HYUNDAI.HYUNDAI_IONIQ_9,
HYUNDAI.HYUNDAI_CASPER,
HYUNDAI.HYUNDAI_CASPER_EV,
HYUNDAI.HYUNDAI_PORTER_II_EV,
HYUNDAI.HYUNDAI_SANTAFE_MX5,
HYUNDAI.HYUNDAI_SANTAFE_MX5_HEV,
HYUNDAI.KIA_K5_DL3_24,
HYUNDAI.KIA_K5_DL3_24_HEV,
HYUNDAI.KIA_EV4,
HYUNDAI.KIA_EV6_PE,
HYUNDAI.GENESIS_GV70_EV_1ST_GEN,
HYUNDAI.HYUNDAI_GRANDEUR_IG,
HYUNDAI.HYUNDAI_GRANDEUR_IG_HEV,
HYUNDAI.GENESIS_EQ900,
HYUNDAI.GENESIS_EQ900_L,
HYUNDAI.GENESIS_G90_2019,
HYUNDAI.HYUNDAI_NEXO,
HYUNDAI.KIA_MOHAVE,
HYUNDAI.KIA_K5,
HYUNDAI.KIA_K5_HEV,
HYUNDAI.KIA_K5_HEV_2022,
HYUNDAI.KIA_K7,
HYUNDAI.KIA_K7_HEV,
HYUNDAI.KIA_K7_PE,
HYUNDAI.KIA_K7_HEV_PE,
HYUNDAI.KIA_K9,
HYUNDAI.KIA_EV_SK3,
HYUNDAI.KIA_EV9,
HYUNDAI.KIA_EV3,
HYUNDAI.KIA_PV5,
HYUNDAI.KIA_RAY_EV,
HYUNDAI.HYUNDAI_KONA_HEV_2ND_GEN,
HYUNDAI.HYUNDAI_SONATA_2024,
HYUNDAI.HYUNDAI_AZERA_7TH_GEN,
VOLKSWAGEN.SEAT_ALHAMBRA_MK1,
VOLKSWAGEN.AUDI_Q4_MK1,
VOLKSWAGEN.AUDI_Q4_MK2,
VOLKSWAGEN.SEAT_LEON_MK4,
VOLKSWAGEN.CUPRA_BORN_MK1,
VOLKSWAGEN.SKODA_ENYAQ_MK1,
VOLKSWAGEN.SKODA_ENYAQ_MK2,
]
class CarTestRoute(NamedTuple):
route: str
car_model: Platform | None
segment: int | None = None
routes = [
CarTestRoute("efdf9af95e71cd84/2022-05-13--19-03-31", COMMA.COMMA_BODY),
CarTestRoute("0c94aa1e1296d7c6/2021-05-05--19-48-37", CHRYSLER.JEEP_GRAND_CHEROKEE),
CarTestRoute("91dfedae61d7bd75/2021-05-22--20-07-52", CHRYSLER.JEEP_GRAND_CHEROKEE_2019),
CarTestRoute("420a8e183f1aed48/2020-03-05--07-15-29", CHRYSLER.CHRYSLER_PACIFICA_2018_HYBRID), # 2017
CarTestRoute("43a685a66291579b/2021-05-27--19-47-29", CHRYSLER.CHRYSLER_PACIFICA_2018),
CarTestRoute("378472f830ee7395/2021-05-28--07-38-43", CHRYSLER.CHRYSLER_PACIFICA_2018_HYBRID),
CarTestRoute("8190c7275a24557b/2020-01-29--08-33-58", CHRYSLER.CHRYSLER_PACIFICA_2019_HYBRID),
CarTestRoute("3d84727705fecd04/2021-05-25--08-38-56", CHRYSLER.CHRYSLER_PACIFICA_2020),
CarTestRoute("221c253375af4ee9/2022-06-15--18-38-24", CHRYSLER.RAM_1500_5TH_GEN),
CarTestRoute("8fb5eabf914632ae/2022-08-04--17-28-53", CHRYSLER.RAM_HD_5TH_GEN, segment=6),
CarTestRoute("3379c85aeedc8285/2023-12-07--17-49-39", CHRYSLER.DODGE_DURANGO),
CarTestRoute("54827bf84c38b14f/2023-01-25--14-14-11", FORD.FORD_BRONCO_SPORT_MK1),
CarTestRoute("f8eaaccd2a90aef8/2023-05-04--15-10-09", FORD.FORD_ESCAPE_MK4),
CarTestRoute("56574443e0c3783c/00000002--c00bd0fe69", FORD.FORD_ESCAPE_MK4_5),
CarTestRoute("62241b0c7fea4589/2022-09-01--15-32-49", FORD.FORD_EXPLORER_MK6),
CarTestRoute("e886087f430e7fe7/2023-06-16--23-06-36", FORD.FORD_FOCUS_MK4),
CarTestRoute("bd37e43731e5964b/2023-04-30--10-42-26", FORD.FORD_MAVERICK_MK1),
CarTestRoute("112e4d6e0cad05e1/2023-11-14--08-21-43", FORD.FORD_F_150_LIGHTNING_MK1),
CarTestRoute("e36b272d5679115f/00000369--a3e8499a85", FORD.FORD_F_150_MK14),
CarTestRoute("83a4e056c7072678/2023-11-13--16-51-33", FORD.FORD_MUSTANG_MACH_E_MK1),
CarTestRoute("37998aa0fade36ab/00000000--48f927c4f5", FORD.FORD_RANGER_MK2),
CarTestRoute("61a1b9e7a4eae0f6/00000000--79d85d1315", FORD.FORD_EXPEDITION_MK4),
#TestRoute("f1b4c567731f4a1b/2018-04-30--10-15-35", FORD.FUSION),
CarTestRoute("7cc2a8365b4dd8a9/2018-12-02--12-10-44", GM.GMC_ACADIA),
CarTestRoute("aa20e335f61ba898/2019-02-05--16-59-04", GM.BUICK_REGAL),
CarTestRoute("75a6bcb9b8b40373/2023-03-11--22-47-33", GM.BUICK_LACROSSE),
CarTestRoute("e746f59bc96fd789/2024-01-31--22-25-58", GM.CHEVROLET_EQUINOX),
CarTestRoute("ef8f2185104d862e/2023-02-09--18-37-13", GM.CADILLAC_ESCALADE),
CarTestRoute("46460f0da08e621e/2021-10-26--07-21-46", GM.CADILLAC_ESCALADE_ESV),
CarTestRoute("168f8b3be57f66ae/2023-09-12--21-44-42", GM.CADILLAC_ESCALADE_ESV_2019),
CarTestRoute("c950e28c26b5b168/2018-05-30--22-03-41", GM.CHEVROLET_VOLT),
CarTestRoute("f08912a233c1584f/2022-08-11--18-02-41", GM.CHEVROLET_BOLT_EUV, segment=1),
CarTestRoute("555d4087cf86aa91/2022-12-02--12-15-07", GM.CHEVROLET_BOLT_EUV, segment=14), # Bolt EV
CarTestRoute("38aa7da107d5d252/2022-08-15--16-01-12", GM.CHEVROLET_SILVERADO),
CarTestRoute("5085c761395d1fe6/2023-04-07--18-20-06", GM.CHEVROLET_TRAILBLAZER),
CarTestRoute("162796f1469f2f1b/00000005--6f334eda14", GM.CADILLAC_XT4),
CarTestRoute("477dd485611d1e6e/00000009--85fc06e10a", GM.CHEVROLET_VOLT_2019),
CarTestRoute("a40976dc9f28ba62/0000001f--160e210119", GM.CHEVROLET_TRAVERSE),
CarTestRoute("36c62b5da6f08154/00000052--983c17c5b2", GM.GMC_YUKON),
CarTestRoute("0e7a2ba168465df5/2020-10-18--14-14-22", HONDA.ACURA_RDX_3G),
CarTestRoute("a74b011b32b51b56/2020-07-26--17-09-36", HONDA.HONDA_CIVIC),
CarTestRoute("a859a044a447c2b0/2020-03-03--18-42-45", HONDA.HONDA_CRV_EU),
CarTestRoute("68aac44ad69f838e/2021-05-18--20-40-52", HONDA.HONDA_CRV),
CarTestRoute("14fed2e5fa0aa1a5/2021-05-25--14-59-42", HONDA.HONDA_CRV_HYBRID),
CarTestRoute("52f3e9ae60c0d886/2021-05-23--15-59-43", HONDA.HONDA_FIT),
CarTestRoute("2c4292a5cd10536c/2021-08-19--21-32-15", HONDA.HONDA_FREED),
CarTestRoute("03be5f2fd5c508d1/2020-04-19--18-44-15", HONDA.HONDA_HRV),
CarTestRoute("320098ff6c5e4730/2023-04-13--17-47-46", HONDA.HONDA_HRV_3G),
CarTestRoute("147613502316e718/00000001--dd141a3140", HONDA.HONDA_HRV_3G), # Brazilian model
CarTestRoute("1e4baee1aa2687a0/00000001--74c4cc0b23", HONDA.HONDA_HRV_3G), # Thailand model use ALT_GEAR
CarTestRoute("917b074700869333/2021-05-24--20-40-20", HONDA.ACURA_ILX),
CarTestRoute("08a3deb07573f157/2020-03-06--16-11-19", HONDA.HONDA_ACCORD), # 1.5T
CarTestRoute("1da5847ac2488106/2021-05-24--19-31-50", HONDA.HONDA_ACCORD), # 2.0T
CarTestRoute("085ac1d942c35910/2021-03-25--20-11-15", HONDA.HONDA_ACCORD), # 2021 with new style HUD msgs
CarTestRoute("07585b0da3c88459/2021-05-26--18-52-04", HONDA.HONDA_ACCORD), # hybrid
CarTestRoute("f29e2b57a55e7ad5/2021-03-24--20-52-38", HONDA.HONDA_ACCORD), # hybrid, 2021 with new style HUD msgs
CarTestRoute("1ad763dd22ef1a0e/2020-02-29--18-37-03", HONDA.HONDA_CRV_5G),
CarTestRoute("0a96f86fcfe35964/2020-02-05--07-25-51", HONDA.HONDA_ODYSSEY),
CarTestRoute("7817fe954aff07b8/00000001--fdaaf36c4f", HONDA.HONDA_ODYSSEY_TWN),
CarTestRoute("d7233a428eb7d0b5/00000001--9b99b04d43", HONDA.HONDA_ODYSSEY_5G_MMR),
CarTestRoute("d83f36766f8012a5/2020-02-05--18-42-21", HONDA.HONDA_CIVIC_BOSCH_DIESEL),
CarTestRoute("f0890d16a07a236b/2021-05-25--17-27-22", HONDA.HONDA_INSIGHT),
CarTestRoute("07d37d27996096b6/2020-03-04--21-57-27", HONDA.HONDA_PILOT),
CarTestRoute("684e8f96bd491a0e/2021-11-03--11-08-42", HONDA.HONDA_PILOT), # Passport
CarTestRoute("0a78dfbacc8504ef/2020-03-04--13-29-55", HONDA.HONDA_CIVIC_BOSCH),
CarTestRoute("f34a60d68d83b1e5/2020-10-06--14-35-55", HONDA.ACURA_RDX),
CarTestRoute("54fd8451b3974762/2021-04-01--14-50-10", HONDA.HONDA_RIDGELINE),
CarTestRoute("2d5808fae0b38ac6/2021-09-01--17-14-11", HONDA.HONDA_E),
CarTestRoute("f44aa96ace22f34a/2021-12-22--06-22-31", HONDA.HONDA_CIVIC_2022),
CarTestRoute("1f032f5173c8ad99/00000006--573b3fcaf5", HONDA.HONDA_CIVIC_2022), # Civic Type R with manual transmission
CarTestRoute("b1c832ad56b6bc9d/00000010--debfcf5867", HONDA.HONDA_CIVIC_2022), # 2025 Civic Hatch Hybrid with new eCVT transmission
CarTestRoute("f9c43864cf057d05/2024-01-15--23-01-20", HONDA.HONDA_PILOT_4G), # TODO: Replace with a newer route
CarTestRoute("f39cf149898833ff/0000002b--54f3fae045", HONDA.HONDA_ACCORD_11G),
# CarTestRoute("56b2cf1dacdcd033/00000017--d24ffdb376", HONDA.HONDA_CITY_7G), # Brazilian model
CarTestRoute("2dc4489d7e1410ca/00000001--bbec3f5117", HONDA.HONDA_CRV_6G),
CarTestRoute("a703d058f4e05aeb/00000008--f169423024", HONDA.HONDA_PASSPORT_4G),
CarTestRoute("ad9840558640c31d/000001f2--026c4f6275", HONDA.ACURA_TLX_2G_MMR),
CarTestRoute("87d7f06ade479c2e/2023-09-11--23-30-11", HYUNDAI.HYUNDAI_AZERA_6TH_GEN),
CarTestRoute("66189dd8ec7b50e6/2023-09-20--07-02-12", HYUNDAI.HYUNDAI_AZERA_HEV_6TH_GEN),
CarTestRoute("6fe86b4e410e4c37/2020-07-22--16-27-13", HYUNDAI.HYUNDAI_GENESIS),
CarTestRoute("b5d6dc830ad63071/2022-12-12--21-28-25", HYUNDAI.GENESIS_GV60_EV_1ST_GEN, segment=12),
CarTestRoute("70c5bec28ec8e345/2020-08-08--12-22-23", HYUNDAI.GENESIS_G70),
CarTestRoute("ca4de5b12321bd98/2022-10-18--21-15-59", HYUNDAI.GENESIS_GV70_1ST_GEN),
CarTestRoute("afe09b9f5d3f3548/00000011--15fefe1c50", HYUNDAI.GENESIS_GV70_ELECTRIFIED_1ST_GEN),
CarTestRoute("afe09b9f5d3f3548/0000001b--a1129a4a15", HYUNDAI.GENESIS_GV70_ELECTRIFIED_1ST_GEN), # openpilot longitudinal enabled
CarTestRoute("6b301bf83f10aa90/2020-11-22--16-45-07", HYUNDAI.GENESIS_G80),
CarTestRoute("66eaa6c3b6b2afc6/00000009--3a5199aabe", HYUNDAI.GENESIS_G80_2ND_GEN_FL), # LKA steering
CarTestRoute("0bbe367c98fa1538/2023-09-16--00-16-49", HYUNDAI.HYUNDAI_CUSTIN_1ST_GEN),
CarTestRoute("f0709d2bc6ca451f/2022-10-15--08-13-54", HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN),
CarTestRoute("4dbd55df87507948/2022-03-01--09-45-38", HYUNDAI.HYUNDAI_SANTA_FE),
CarTestRoute("bf43d9df2b660eb0/2021-09-23--14-16-37", HYUNDAI.HYUNDAI_SANTA_FE_2022),
CarTestRoute("37398f32561a23ad/2021-11-18--00-11-35", HYUNDAI.HYUNDAI_SANTA_FE_HEV_2022),
CarTestRoute("656ac0d830792fcc/2021-12-28--14-45-56", HYUNDAI.HYUNDAI_SANTA_FE_PHEV_2022, segment=1),
CarTestRoute("de59124955b921d8/2023-06-24--00-12-50", HYUNDAI.KIA_CARNIVAL_4TH_GEN),
CarTestRoute("409c9409979a8abc/2023-07-11--09-06-44", HYUNDAI.KIA_CARNIVAL_4TH_GEN), # Chinese model
CarTestRoute("e0e98335f3ebc58f/2021-03-07--16-38-29", HYUNDAI.KIA_CEED),
CarTestRoute("7653b2bce7bcfdaa/2020-03-04--15-34-32", HYUNDAI.KIA_OPTIMA_G4),
CarTestRoute("018654717bc93d7d/2022-09-19--23-11-10", HYUNDAI.KIA_OPTIMA_G4_FL, segment=0),
CarTestRoute("f9716670b2481438/2023-08-23--14-49-50", HYUNDAI.KIA_OPTIMA_H),
CarTestRoute("6a42c1197b2a8179/2023-09-21--10-23-44", HYUNDAI.KIA_OPTIMA_H_G4_FL),
CarTestRoute("c75a59efa0ecd502/2021-03-11--20-52-55", HYUNDAI.KIA_SELTOS),
CarTestRoute("5b7c365c50084530/2020-04-15--16-13-24", HYUNDAI.HYUNDAI_SONATA),
CarTestRoute("b2a38c712dcf90bd/2020-05-18--18-12-48", HYUNDAI.HYUNDAI_SONATA_LF),
CarTestRoute("c344fd2492c7a9d2/2023-12-11--09-03-23", HYUNDAI.HYUNDAI_STARIA_4TH_GEN),
CarTestRoute("fb3fd42f0baaa2f8/2022-03-30--15-25-05", HYUNDAI.HYUNDAI_TUCSON),
CarTestRoute("db68bbe12250812c/2022-12-05--00-54-12", HYUNDAI.HYUNDAI_TUCSON_4TH_GEN), # 2023
CarTestRoute("36e10531feea61a4/2022-07-25--13-37-42", HYUNDAI.HYUNDAI_TUCSON_4TH_GEN), # hybrid
CarTestRoute("5875672fc1d4bf57/2020-07-23--21-33-28", HYUNDAI.KIA_SORENTO),
CarTestRoute("1d0d000db3370fd0/2023-01-04--22-28-42", HYUNDAI.KIA_SORENTO_4TH_GEN, segment=5),
CarTestRoute("fc19648042eb6896/2023-08-16--11-43-27", HYUNDAI.KIA_SORENTO_HEV_4TH_GEN, segment=14),
CarTestRoute("628935d7d3e5f4f7/2022-11-30--01-12-46", HYUNDAI.KIA_SORENTO_HEV_4TH_GEN), # plug-in hybrid
CarTestRoute("9c917ba0d42ffe78/2020-04-17--12-43-19", HYUNDAI.HYUNDAI_PALISADE),
CarTestRoute("05a8f0197fdac372/2022-10-19--14-14-09", HYUNDAI.HYUNDAI_IONIQ_5), # LKA steering
CarTestRoute("eb4eae1476647463/2023-08-26--18-07-04", HYUNDAI.HYUNDAI_IONIQ_6, segment=6), # LKA steering
CarTestRoute("3f29334d6134fcd4/2022-03-30--22-00-50", HYUNDAI.HYUNDAI_IONIQ_PHEV_2019),
CarTestRoute("fa8db5869167f821/2021-06-10--22-50-10", HYUNDAI.HYUNDAI_IONIQ_PHEV),
CarTestRoute("e1107f9d04dfb1e2/2023-09-05--22-32-12", HYUNDAI.HYUNDAI_IONIQ_PHEV), # openpilot longitudinal enabled
CarTestRoute("2c5cf2dd6102e5da/2020-12-17--16-06-44", HYUNDAI.HYUNDAI_IONIQ_EV_2020),
CarTestRoute("610ebb9faaad6b43/2020-06-13--15-28-36", HYUNDAI.HYUNDAI_IONIQ_EV_LTD),
CarTestRoute("2c5cf2dd6102e5da/2020-06-26--16-00-08", HYUNDAI.HYUNDAI_IONIQ),
CarTestRoute("012c95f06918eca4/2023-01-15--11-19-36", HYUNDAI.HYUNDAI_IONIQ), # openpilot longitudinal enabled
CarTestRoute("ab59fe909f626921/2021-10-18--18-34-28", HYUNDAI.HYUNDAI_IONIQ_HEV_2022),
CarTestRoute("22d955b2cd499c22/2020-08-10--19-58-21", HYUNDAI.HYUNDAI_KONA),
CarTestRoute("0099bdb24d82951b/00000005--c38d940b04", HYUNDAI.HYUNDAI_KONA_2022),
CarTestRoute("efc48acf44b1e64d/2021-05-28--21-05-04", HYUNDAI.HYUNDAI_KONA_EV),
CarTestRoute("f90d3cd06caeb6fa/2023-09-06--17-15-47", HYUNDAI.HYUNDAI_KONA_EV), # openpilot longitudinal enabled
CarTestRoute("ff973b941a69366f/2022-07-28--22-01-19", HYUNDAI.HYUNDAI_KONA_EV_2022, segment=11),
CarTestRoute("1618132d68afc876/2023-08-27--09-32-14", HYUNDAI.HYUNDAI_KONA_EV_2ND_GEN, segment=13),
CarTestRoute("49f3c13141b6bc87/2021-07-28--08-05-13", HYUNDAI.HYUNDAI_KONA_HEV),
CarTestRoute("a74afe0cf708748f/0000000e--a2885a9a71", HYUNDAI.HYUNDAI_NEXO_1ST_GEN),
CarTestRoute("a74afe0cf708748f/0000000c--b476a8fd00", HYUNDAI.HYUNDAI_NEXO_1ST_GEN), # openpilot longitudinal enabled
CarTestRoute("5dddcbca6eb66c62/2020-07-26--13-24-19", HYUNDAI.KIA_STINGER),
CarTestRoute("5b50b883a4259afb/2022-11-09--15-00-42", HYUNDAI.KIA_STINGER_2022),
CarTestRoute("d624b3d19adce635/2020-08-01--14-59-12", HYUNDAI.HYUNDAI_VELOSTER),
CarTestRoute("d545129f3ca90f28/2022-10-19--09-22-54", HYUNDAI.KIA_EV6), # LKA steering
CarTestRoute("68d6a96e703c00c9/2022-09-10--16-09-39", HYUNDAI.KIA_EV6), # LFA steering
CarTestRoute("9b25e8c1484a1b67/2023-04-13--10-41-45", HYUNDAI.KIA_EV6),
CarTestRoute("007d5e4ad9f86d13/2021-09-30--15-09-23", HYUNDAI.KIA_K5_2021),
CarTestRoute("c58dfc9fc16590e0/2023-01-14--13-51-48", HYUNDAI.KIA_K5_HEV_2020),
CarTestRoute("78ad5150de133637/2023-09-13--16-15-57", HYUNDAI.KIA_K8_HEV_1ST_GEN),
CarTestRoute("50c6c9b85fd1ff03/2020-10-26--17-56-06", HYUNDAI.KIA_NIRO_EV),
CarTestRoute("b153671049a867b3/2023-04-05--10-00-30", HYUNDAI.KIA_NIRO_EV_2ND_GEN),
CarTestRoute("173219cf50acdd7b/2021-07-05--10-27-41", HYUNDAI.KIA_NIRO_PHEV),
CarTestRoute("23349923ba5c4e3b/2023-12-02--08-51-54", HYUNDAI.KIA_NIRO_PHEV_2022),
CarTestRoute("34a875f29f69841a/2021-07-29--13-02-09", HYUNDAI.KIA_NIRO_HEV_2021),
CarTestRoute("db04d2c63990e3ba/2023-02-08--16-52-39", HYUNDAI.KIA_NIRO_HEV_2ND_GEN),
CarTestRoute("50a2212c41f65c7b/2021-05-24--16-22-06", HYUNDAI.KIA_FORTE),
CarTestRoute("192283cdbb7a58c2/2022-10-15--01-43-18", HYUNDAI.KIA_SPORTAGE_5TH_GEN),
CarTestRoute("09559f1fcaed4704/2023-11-16--02-24-57", HYUNDAI.KIA_SPORTAGE_5TH_GEN, segment=0), # openpilot longitudinal
CarTestRoute("b3537035ffe6a7d6/2022-10-17--15-23-49", HYUNDAI.KIA_SPORTAGE_5TH_GEN), # hybrid
CarTestRoute("c5ac319aa9583f83/2021-06-01--18-18-31", HYUNDAI.HYUNDAI_ELANTRA),
CarTestRoute("734ef96182ddf940/2022-10-02--16-41-44", HYUNDAI.HYUNDAI_ELANTRA_GT_I30),
CarTestRoute("82e9cdd3f43bf83e/2021-05-15--02-42-51", HYUNDAI.HYUNDAI_ELANTRA_2021),
CarTestRoute("715ac05b594e9c59/2021-06-20--16-21-07", HYUNDAI.HYUNDAI_ELANTRA_HEV_2021),
CarTestRoute("7120aa90bbc3add7/2021-08-02--07-12-31", HYUNDAI.HYUNDAI_SONATA_HYBRID),
CarTestRoute("715ac05b594e9c59/2021-10-27--23-24-56", HYUNDAI.GENESIS_G70_2020),
CarTestRoute("6b0d44d22df18134/2023-05-06--10-36-55", HYUNDAI.GENESIS_GV80),
CarTestRoute("00c829b1b7613dea/2021-06-24--09-10-10", TOYOTA.TOYOTA_ALPHARD_TSS2),
CarTestRoute("912119ebd02c7a42/2022-03-19--07-24-50", TOYOTA.TOYOTA_ALPHARD_TSS2), # hybrid
CarTestRoute("000cf3730200c71c/2021-05-24--10-42-05", TOYOTA.TOYOTA_AVALON),
CarTestRoute("0bb588106852abb7/2021-05-26--12-22-01", TOYOTA.TOYOTA_AVALON_2019),
CarTestRoute("87bef2930af86592/2021-05-30--09-40-54", TOYOTA.TOYOTA_AVALON_2019), # hybrid
CarTestRoute("e9966711cfb04ce3/2022-01-11--07-59-43", TOYOTA.TOYOTA_AVALON_TSS2),
CarTestRoute("eca1080a91720a54/2022-03-17--13-32-29", TOYOTA.TOYOTA_AVALON_TSS2), # hybrid
CarTestRoute("6cdecc4728d4af37/2020-02-23--15-44-18", TOYOTA.TOYOTA_CAMRY),
CarTestRoute("2f37c007683e85ba/2023-09-02--14-39-44", TOYOTA.TOYOTA_CAMRY), # openpilot longitudinal, with radar CAN filter
CarTestRoute("54034823d30962f5/2021-05-24--06-37-34", TOYOTA.TOYOTA_CAMRY), # hybrid
CarTestRoute("3456ad0cd7281b24/2020-12-13--17-45-56", TOYOTA.TOYOTA_CAMRY_TSS2),
CarTestRoute("ffccc77938ddbc44/2021-01-04--16-55-41", TOYOTA.TOYOTA_CAMRY_TSS2), # hybrid
# CarTestRoute("4e45c89c38e8ec4d/2021-05-02--02-49-28", TOYOTA.TOYOTA_COROLLA),
CarTestRoute("5f5afb36036506e4/2019-05-14--02-09-54", TOYOTA.TOYOTA_COROLLA_TSS2),
CarTestRoute("5ceff72287a5c86c/2019-10-19--10-59-02", TOYOTA.TOYOTA_COROLLA_TSS2), # hybrid
CarTestRoute("d2525c22173da58b/2021-04-25--16-47-04", TOYOTA.TOYOTA_PRIUS),
CarTestRoute("b14c5b4742e6fc85/2020-07-28--19-50-11", TOYOTA.TOYOTA_RAV4),
# CarTestRoute("32a7df20486b0f70/2020-02-06--16-06-50", TOYOTA.TOYOTA_RAV4H),
CarTestRoute("cdf2f7de565d40ae/2019-04-25--03-53-41", TOYOTA.TOYOTA_RAV4_TSS2),
CarTestRoute("a5c341bb250ca2f0/2022-05-18--16-05-17", TOYOTA.TOYOTA_RAV4_TSS2_2022),
CarTestRoute("ad5a3fa719bc2f83/2023-10-17--19-48-42", TOYOTA.TOYOTA_RAV4_TSS2_2023),
CarTestRoute("7e34a988419b5307/2019-12-18--19-13-30", TOYOTA.TOYOTA_RAV4_TSS2), # hybrid
CarTestRoute("2475fb3eb2ffcc2e/2022-04-29--12-46-23", TOYOTA.TOYOTA_RAV4_TSS2_2022), # hybrid
CarTestRoute("20ba9ade056a8c7b/2021-02-08--21-57-35", TOYOTA.TOYOTA_RAV4_PRIME), # SecOC
CarTestRoute("41ba5b181f29435d/00000001--e3ae76382f", TOYOTA.TOYOTA_RAV4_PRIME), # SecOC longitudinal
CarTestRoute("8bfb000e03b2a257/00000004--f9eee5f52e", TOYOTA.TOYOTA_SIENNA_4TH_GEN), # SecOC
CarTestRoute("0b54d0594d924cd9/00000057--b6206a3205", TOYOTA.TOYOTA_YARIS), # SecOC
CarTestRoute("7a31f030957b9c85/2023-04-01--14-12-51", TOYOTA.LEXUS_ES),
# CarTestRoute("37041c500fd30100/2020-12-30--12-17-24", TOYOTA.LEXUS_ES), # hybrid
CarTestRoute("e6a24be49a6cd46e/2019-10-29--10-52-42", TOYOTA.LEXUS_ES_TSS2),
CarTestRoute("f49e8041283f2939/2019-05-30--11-51-51", TOYOTA.LEXUS_ES_TSS2), # hybrid
CarTestRoute("da23c367491f53e2/2021-05-21--09-09-11", TOYOTA.LEXUS_CTH, segment=3),
CarTestRoute("32696cea52831b02/2021-11-19--18-13-30", TOYOTA.LEXUS_RC),
CarTestRoute("7f8f479cfa6f392a/00000001--9a84b69c9d", TOYOTA.LEXUS_RC_TSS2),
CarTestRoute("ab9b64a5e5960cba/2023-10-24--17-32-08", TOYOTA.LEXUS_GS_F),
CarTestRoute("886fcd8408d570e9/2020-01-29--02-18-55", TOYOTA.LEXUS_RX),
CarTestRoute("d27ad752e9b08d4f/2021-05-26--19-39-51", TOYOTA.LEXUS_RX), # hybrid
CarTestRoute("01b22eb2ed121565/2020-02-02--11-25-51", TOYOTA.LEXUS_RX_TSS2),
CarTestRoute("b74758c690a49668/2020-05-20--15-58-57", TOYOTA.LEXUS_RX_TSS2), # hybrid
CarTestRoute("964c09eb11ca8089/2020-11-03--22-04-00", TOYOTA.LEXUS_NX),
CarTestRoute("ec429c0f37564e3c/2020-02-01--17-28-12", TOYOTA.LEXUS_NX), # hybrid
CarTestRoute("3fd5305f8b6ca765/2021-04-28--19-26-49", TOYOTA.LEXUS_NX_TSS2),
CarTestRoute("09ae96064ed85a14/2022-06-09--12-22-31", TOYOTA.LEXUS_NX_TSS2), # hybrid
CarTestRoute("4765fbbf59e3cd88/2024-02-06--17-45-32", TOYOTA.LEXUS_LC_TSS2),
CarTestRoute("5afee5161ea1dcde/00000003--b8ae16aae0", TOYOTA.LEXUS_LS),
CarTestRoute("0a302ffddbb3e3d3/2020-02-08--16-19-08", TOYOTA.TOYOTA_HIGHLANDER_TSS2),
CarTestRoute("437e4d2402abf524/2021-05-25--07-58-50", TOYOTA.TOYOTA_HIGHLANDER_TSS2), # hybrid
CarTestRoute("3183cd9b021e89ce/2021-05-25--10-34-44", TOYOTA.TOYOTA_HIGHLANDER),
CarTestRoute("80d16a262e33d57f/2021-05-23--20-01-43", TOYOTA.TOYOTA_HIGHLANDER), # hybrid
CarTestRoute("eb6acd681135480d/2019-06-20--20-00-00", TOYOTA.TOYOTA_SIENNA),
CarTestRoute("2e07163a1ba9a780/2019-08-25--13-15-13", TOYOTA.LEXUS_IS),
CarTestRoute("649bf2997ada6e3a/2023-08-08--18-04-22", TOYOTA.LEXUS_IS_TSS2),
CarTestRoute("0a0de17a1e6a2d15/2020-09-21--21-24-41", TOYOTA.TOYOTA_PRIUS_TSS2),
CarTestRoute("9b36accae406390e/2021-03-30--10-41-38", TOYOTA.TOYOTA_MIRAI),
CarTestRoute("cd9cff4b0b26c435/2021-05-13--15-12-39", TOYOTA.TOYOTA_CHR),
CarTestRoute("57858ede0369a261/2021-05-18--20-34-20", TOYOTA.TOYOTA_CHR), # hybrid
CarTestRoute("ea8fbe72b96a185c/2023-02-08--15-11-46", TOYOTA.TOYOTA_CHR_TSS2),
CarTestRoute("ea8fbe72b96a185c|2023-02-22--09-20-34", TOYOTA.TOYOTA_CHR_TSS2), # openpilot longitudinal, with smartDSU
CarTestRoute("6719965b0e1d1737/2023-02-09--22-44-05", TOYOTA.TOYOTA_CHR_TSS2), # hybrid
CarTestRoute("6719965b0e1d1737/2023-08-29--06-40-05", TOYOTA.TOYOTA_CHR_TSS2), # hybrid, openpilot longitudinal, radar disabled
CarTestRoute("14623aae37e549f3/2021-10-24--01-20-49", TOYOTA.TOYOTA_PRIUS_V),
CarTestRoute("202c40641158a6e5/2021-09-21--09-43-24", VOLKSWAGEN.VOLKSWAGEN_ARTEON_MK1),
CarTestRoute("2c68dda277d887ac/2021-05-11--15-22-20", VOLKSWAGEN.VOLKSWAGEN_ATLAS_MK1),
CarTestRoute("ffcd23abbbd02219/2024-02-28--14-59-38", VOLKSWAGEN.VOLKSWAGEN_CADDY_MK3),
CarTestRoute("cae14e88932eb364/2021-03-26--14-43-28", VOLKSWAGEN.VOLKSWAGEN_GOLF_MK7), # Stock ACC
CarTestRoute("3cfdec54aa035f3f/2022-10-13--14-58-58", VOLKSWAGEN.VOLKSWAGEN_GOLF_MK7), # openpilot longitudinal
CarTestRoute("578742b26807f756|00000010--41ee3e5bec", VOLKSWAGEN.VOLKSWAGEN_JETTA_MK6),
CarTestRoute("58a7d3b707987d65/2021-03-25--17-26-37", VOLKSWAGEN.VOLKSWAGEN_JETTA_MK7),
CarTestRoute("4d134e099430fba2/2021-03-26--00-26-06", VOLKSWAGEN.VOLKSWAGEN_PASSAT_MK8),
CarTestRoute("b29ee8c5a0a735d1|000000dc--a384e9083e", VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS, segment=0),
CarTestRoute("0f53129ed44f6920|00000287--3efbddeb96", VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS, segment=0),
CarTestRoute("cecfd0ca8552b71c|00000057--a1a1986651", VOLKSWAGEN.PORSCHE_MACAN_MK1, segment=0),
CarTestRoute("0cd0b7f7e31a3853/2021-11-03--19-30-22", VOLKSWAGEN.VOLKSWAGEN_POLO_MK6),
CarTestRoute("064d1816e448f8eb/2022-09-29--15-32-34", VOLKSWAGEN.VOLKSWAGEN_SHARAN_MK2),
CarTestRoute("7d82b2f3a9115f1f/2021-10-21--15-39-42", VOLKSWAGEN.VOLKSWAGEN_TAOS_MK1),
CarTestRoute("2744c89a8dda9a51/2021-07-24--21-28-06", VOLKSWAGEN.VOLKSWAGEN_TCROSS_MK1),
CarTestRoute("2cef8a0b898f331a/2021-03-25--20-13-57", VOLKSWAGEN.VOLKSWAGEN_TIGUAN_MK2),
CarTestRoute("a589dcc642fdb10a/2021-06-14--20-54-26", VOLKSWAGEN.VOLKSWAGEN_TOURAN_MK2),
CarTestRoute("a459f4556782eba1/2021-09-19--09-48-00", VOLKSWAGEN.VOLKSWAGEN_TRANSPORTER_T61),
CarTestRoute("0cd0b7f7e31a3853/2021-11-18--00-38-32", VOLKSWAGEN.VOLKSWAGEN_TROC_MK1),
CarTestRoute("59c5dc50499d9d08/00000003--98586d2894", VOLKSWAGEN.VOLKSWAGEN_GOLF_MK8),
CarTestRoute("2b0e2b387c87c150/0000008f--7e47fb87d9", VOLKSWAGEN.VOLKSWAGEN_ID3_MK1),
CarTestRoute("db724585e1a2cceb/00000020--dbc57cf788", VOLKSWAGEN.VOLKSWAGEN_ID3_MK2),
CarTestRoute("fc76cf2b65550db6/00000091--5b1f61e718", VOLKSWAGEN.VOLKSWAGEN_ID4_MK1),
CarTestRoute("db724585e1a2cceb/00000124--f458af698a", VOLKSWAGEN.VOLKSWAGEN_ID4_MK2),
CarTestRoute("20e3cd4f0d5f39d1/00000091--d240bf0b44", VOLKSWAGEN.VOLKSWAGEN_PASSAT_B7),
CarTestRoute("79b0a26c081d6d6c/00000083--8407f96a1e", VOLKSWAGEN.VOLKSWAGEN_PASSAT_MK7),
CarTestRoute("cc7e1fc0ce4ec686/00000004--dddc5bf4b6", VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS_PLUS),
CarTestRoute("07667b885add75fd/2021-01-23--19-48-42", VOLKSWAGEN.AUDI_A3_MK3),
CarTestRoute("c8b7d4cd76391e07|00000000--c42dead9c6", VOLKSWAGEN.AUDI_A4_MK4, segment=29),
CarTestRoute("5432d2499e17e646/000000e8--9ce8cf6d58", VOLKSWAGEN.AUDI_Q5_MK1),
CarTestRoute("6c6b466346192818/2021-06-06--14-17-47", VOLKSWAGEN.AUDI_Q2_MK1),
CarTestRoute("0cd0b7f7e31a3853/2021-12-03--03-12-05", VOLKSWAGEN.AUDI_Q3_MK2),
CarTestRoute("8f205bdd11bcbb65/2021-03-26--01-00-17", VOLKSWAGEN.SEAT_ATECA_MK1),
CarTestRoute("fc6b6c9a3471c846/2021-05-27--13-39-56", VOLKSWAGEN.SEAT_ATECA_MK1), # Leon
CarTestRoute("0bbe367c98fa1538/2023-03-04--17-46-11", VOLKSWAGEN.SKODA_FABIA_MK4),
CarTestRoute("12d6ae3057c04b0d/2021-09-15--00-04-07", VOLKSWAGEN.SKODA_KAMIQ_MK1),
CarTestRoute("12d6ae3057c04b0d/2021-09-04--21-21-21", VOLKSWAGEN.SKODA_KAROQ_MK1),
CarTestRoute("90434ff5d7c8d603/2021-03-15--12-07-31", VOLKSWAGEN.SKODA_KODIAQ_MK1),
CarTestRoute("66e5edc3a16459c5/2021-05-25--19-00-29", VOLKSWAGEN.SKODA_OCTAVIA_MK3),
CarTestRoute("026b6d18fba6417f/2021-03-26--09-17-04", VOLKSWAGEN.SKODA_KAMIQ_MK1), # Scala
CarTestRoute("b2e9858e29db492b/2021-03-26--16-58-42", VOLKSWAGEN.SKODA_SUPERB_MK3),
CarTestRoute("3c8f0c502e119c1c/2020-06-30--12-58-02", SUBARU.SUBARU_ASCENT),
CarTestRoute("c321c6b697c5a5ff/2020-06-23--11-04-33", SUBARU.SUBARU_FORESTER),
CarTestRoute("791340bc01ed993d/2019-03-10--16-28-08", SUBARU.SUBARU_IMPREZA),
CarTestRoute("8bf7e79a3ce64055/2021-05-24--09-36-27", SUBARU.SUBARU_IMPREZA_2020),
CarTestRoute("8de015561e1ea4a0/2023-08-29--17-08-31", SUBARU.SUBARU_IMPREZA), # openpilot longitudinal
# CarTestRoute("c3d1ccb52f5f9d65/2023-07-22--01-23-20", SUBARU.OUTBACK, segment=9), # gen2 longitudinal, eyesight disabled
CarTestRoute("1bbe6bf2d62f58a8/2022-07-14--17-11-43", SUBARU.SUBARU_OUTBACK, segment=10),
CarTestRoute("c56e69bbc74b8fad/2022-08-18--09-43-51", SUBARU.SUBARU_LEGACY, segment=3),
CarTestRoute("f4e3a0c511a076f4/2022-08-04--16-16-48", SUBARU.SUBARU_CROSSTREK_HYBRID, segment=2),
CarTestRoute("7fd1e4f3a33c1673/2022-12-04--15-09-53", SUBARU.SUBARU_FORESTER_2022, segment=4),
CarTestRoute("f3b34c0d2632aa83/2023-07-23--20-43-25", SUBARU.SUBARU_OUTBACK_2023, segment=7),
CarTestRoute("99437cef6d5ff2ee/2023-03-13--21-21-38", SUBARU.SUBARU_ASCENT_2023, segment=7),
# Pre-global, dashcam
CarTestRoute("95441c38ae8c130e/2020-06-08--12-10-17", SUBARU.SUBARU_FORESTER_PREGLOBAL),
CarTestRoute("df5ca7660000fba8/2020-06-16--17-37-19", SUBARU.SUBARU_LEGACY_PREGLOBAL),
CarTestRoute("5ab784f361e19b78/2020-06-08--16-30-41", SUBARU.SUBARU_OUTBACK_PREGLOBAL),
CarTestRoute("e19eb5d5353b1ac1/2020-08-09--14-37-56", SUBARU.SUBARU_OUTBACK_PREGLOBAL_2018),
CarTestRoute("fbbfa6af821552b9/2020-03-03--08-09-43", NISSAN.NISSAN_XTRAIL),
CarTestRoute("5b7c365c50084530/2020-03-25--22-10-13", NISSAN.NISSAN_LEAF),
CarTestRoute("22c3dcce2dd627eb/2020-12-30--16-38-48", NISSAN.NISSAN_LEAF_IC),
CarTestRoute("059ab9162e23198e/2020-05-30--09-41-01", NISSAN.NISSAN_ROGUE),
CarTestRoute("b72d3ec617c0a90f/2020-12-11--15-38-17", NISSAN.NISSAN_ALTIMA),
CarTestRoute("32a319f057902bb3/2020-04-27--15-18-58", MAZDA.MAZDA_CX5),
CarTestRoute("10b5a4b380434151/2020-08-26--17-11-45", MAZDA.MAZDA_CX9),
CarTestRoute("74f1038827005090/2020-08-26--20-05-50", MAZDA.MAZDA_3),
CarTestRoute("fb53c640f499b73d/2021-06-01--04-17-56", MAZDA.MAZDA_6),
CarTestRoute("f6d5b1a9d7a1c92e/2021-07-08--06-56-59", MAZDA.MAZDA_CX9_2021),
CarTestRoute("a4af1602d8e668ac/2022-02-03--12-17-07", MAZDA.MAZDA_CX5_2022),
CarTestRoute("6a7075a4fdd765ee/0000004e--1f612006dd", PSA.PSA_PEUGEOT_208),
CarTestRoute("bc095dc92e101734/000000db--ee9fe46e57", RIVIAN.RIVIAN_R1_GEN1),
CarTestRoute("7dc058789994da80/00000112--adb970f6a8", TESLA.TESLA_MODEL_3),
CarTestRoute("c8a98e58647765ad/00000002--84e4746136", TESLA.TESLA_MODEL_Y),
CarTestRoute("2c912ca5de3b1ee9/0000025d--6eb6bcbca4", TESLA.TESLA_MODEL_Y, segment=4),
CarTestRoute("bdda168c0c35fad7/00000001--5c5a36ec06", TESLA.TESLA_MODEL_X), # openpilot longitudinal
# Segments that test specific issues
# Controls mismatch due to standstill threshold
CarTestRoute("bec2dcfde6a64235/2022-04-08--14-21-32", HONDA.HONDA_CRV_HYBRID, segment=22),
]

View File

@@ -0,0 +1,55 @@
import pytest
from iqdbc.car.can_definitions import CanData
from iqdbc.car.car_helpers import FRAME_FINGERPRINT, can_fingerprint
from iqdbc.car.fingerprints import _FINGERPRINTS as FINGERPRINTS
class TestCanFingerprint:
@pytest.mark.parametrize("car_model, fingerprints", FINGERPRINTS.items())
def test_can_fingerprint(self, car_model, fingerprints):
"""Tests online fingerprinting function on offline fingerprints"""
for fingerprint in fingerprints: # can have multiple fingerprints for each platform
can = [CanData(address=address, dat=b'\x00' * length, src=src)
for address, length in fingerprint.items() for src in (0, 1)]
fingerprint_iter = iter([can])
car_fingerprint, finger = can_fingerprint(lambda **kwargs: [next(fingerprint_iter, [])]) # noqa: B023
assert car_fingerprint == car_model
assert finger[0] == fingerprint
assert finger[1] == fingerprint
assert finger[2] == {}
def test_timing(self, subtests):
# just pick any CAN fingerprinting car
car_model = "CHEVROLET_BOLT_EUV"
fingerprint = FINGERPRINTS[car_model][0]
cases = []
# case 1 - one match, make sure we keep going for 100 frames
can = [CanData(address=address, dat=b'\x00' * length, src=src)
for address, length in fingerprint.items() for src in (0, 1)]
cases.append((FRAME_FINGERPRINT, car_model, can))
# case 2 - no matches, make sure we keep going for 100 frames
can = [CanData(address=1, dat=b'\x00' * 1, src=src) for src in (0, 1)] # uncommon address
cases.append((FRAME_FINGERPRINT, None, can))
# case 3 - multiple matches, make sure we keep going for 200 frames to try to eliminate some
can = [CanData(address=2016, dat=b'\x00' * 8, src=src) for src in (0, 1)] # common address
cases.append((FRAME_FINGERPRINT * 2, None, can))
for expected_frames, car_model, can in cases:
with subtests.test(expected_frames=expected_frames, car_model=car_model):
frames = 0
def can_recv(**kwargs):
nonlocal frames
frames += 1
return [can] # noqa: B023
car_fingerprint, _ = can_fingerprint(can_recv)
assert car_fingerprint == car_model
assert frames == expected_frames + 2 # TODO: fix extra frames

View File

@@ -0,0 +1,200 @@
import inspect
import math
import os
import subprocess
import sys
import hypothesis.strategies as st
import pytest
from functools import cache
from hypothesis import Phase, given, settings
from collections.abc import Callable
from typing import Any
from iqdbc.car import DT_CTRL, CanData, structs
from iqdbc.car.car_helpers import interfaces
from iqdbc.car.fingerprints import FW_VERSIONS
from iqdbc.car.fw_versions import FW_QUERY_CONFIGS
from iqdbc.car.interfaces import CarInterfaceBase, get_interface_attr
from iqdbc.car.mock.values import CAR as MOCK
from iqdbc.car.values import PLATFORMS
DrawType = Callable[[st.SearchStrategy], Any]
ALL_ECUS = {ecu for ecus in FW_VERSIONS.values() for ecu in ecus.keys()}
ALL_ECUS |= {ecu for config in FW_QUERY_CONFIGS.values() for ecu in config.extra_ecus}
ALL_REQUESTS = {tuple(r.request) for config in FW_QUERY_CONFIGS.values() for r in config.requests}
# From panda/python/__init__.py
DLC_TO_LEN = [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64]
MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '15'))
SIGNED_RUNTIME_BRANDS = {"tesla", "volkswagen"}
SIGNED_RUNTIME_PLATFORMS = tuple(car_name for car_name in sorted(PLATFORMS)
if interfaces[car_name].__module__.split(".")[-2] in SIGNED_RUNTIME_BRANDS)
PUBLIC_INTERFACE_PLATFORMS = tuple(car_name for car_name in sorted(PLATFORMS)
if car_name not in SIGNED_RUNTIME_PLATFORMS)
@cache
def get_fuzzy_strategy():
# Fuzzy CAN fingerprints and FW versions to test more states of the CarInterface
fingerprint_strategy = st.fixed_dictionaries({0: st.dictionaries(st.integers(min_value=0, max_value=0x800),
st.sampled_from(DLC_TO_LEN))})
# only pick from possible ecus to reduce search space
car_fw_strategy = st.lists(st.builds(
lambda fw, req: structs.CarParams.CarFw(ecu=fw[0], address=fw[1], subAddress=fw[2] or 0, request=req),
st.sampled_from(sorted(ALL_ECUS)),
st.sampled_from(sorted(ALL_REQUESTS)),
))
params_strategy = st.fixed_dictionaries({
'fingerprints': fingerprint_strategy,
'car_fw': car_fw_strategy,
'alpha_long': st.booleans(),
})
return params_strategy
def get_fuzzy_car_interface(car_name: str, draw: DrawType) -> CarInterfaceBase:
params: dict = draw(get_fuzzy_strategy())
# reduce search space by duplicating CAN fingerprints across all buses
params['fingerprints'] |= {key + 1: params['fingerprints'][0] for key in range(6)}
# initialize car interface
CarInterface = interfaces[car_name]
car_params = CarInterface.get_params(car_name, params['fingerprints'], params['car_fw'],
alpha_long=params['alpha_long'], is_release=False, docs=False)
car_params_iq = CarInterface.get_params_iq(car_params, car_name, params['fingerprints'], params['car_fw'],
alpha_long=params['alpha_long'], is_release_iq=False, docs=False)
return CarInterface(car_params, car_params_iq)
class TestCarInterfaces:
def test_init_contract(self):
for car_interface in set(interfaces.values()):
parameters = tuple(inspect.signature(car_interface.init).parameters)
assert parameters[:4] == ("CP", "CP_IQ", "can_recv", "can_send")
# FIXME: Due to the lists used in carParams, Phase.target is very slow and will cause
# many generated examples to overrun when max_examples > ~20, don't use it
@pytest.mark.parametrize("car_name", PUBLIC_INTERFACE_PLATFORMS)
@settings(max_examples=MAX_EXAMPLES, deadline=None,
phases=(Phase.reuse, Phase.generate, Phase.shrink))
@given(data=st.data())
def test_car_interfaces(self, car_name, data):
car_interface = get_fuzzy_car_interface(car_name, data.draw)
car_params = car_interface.CP.as_reader()
car_params_iq = car_interface.CP_IQ
assert car_params.mass > 1
assert car_params.wheelbase > 0
# centerToFront is center of gravity to front wheels, assert a reasonable range
assert car_params.wheelbase * 0.3 < car_params.centerToFront < car_params.wheelbase * 0.7
assert car_params.maxLateralAccel > 0
# Longitudinal sanity checks
assert len(car_params.longitudinalTuning.kpV) == len(car_params.longitudinalTuning.kpBP)
assert len(car_params.longitudinalTuning.kiV) == len(car_params.longitudinalTuning.kiBP)
# If we're using the interceptor for gasPressed, we should be commanding gas with it
if car_params_iq.enableGasInterceptor:
assert car_params.openpilotLongitudinalControl
# Lateral sanity checks
if car_params.steerControlType not in (structs.CarParams.SteerControlType.angle,
structs.CarParams.SteerControlType.curvatureDEPRECATED):
tune = car_params.lateralTuning
if tune.which() == 'pid':
if car_name != MOCK.MOCK:
assert not math.isnan(tune.pid.kf) and tune.pid.kf > 0
assert len(tune.pid.kpV) > 0 and len(tune.pid.kpV) == len(tune.pid.kpBP)
assert len(tune.pid.kiV) > 0 and len(tune.pid.kiV) == len(tune.pid.kiBP)
elif tune.which() == 'torque':
assert not math.isnan(tune.torque.latAccelFactor) and tune.torque.latAccelFactor > 0
assert not math.isnan(tune.torque.friction) and tune.torque.friction > 0
# Run car interface
# TODO: use hypothesis to generate random messages
now_nanos = 0
CC = structs.CarControl().as_reader()
CC_IQ = structs.IQCarControl()
for _ in range(10):
car_interface.update([])
car_interface.apply(CC, CC_IQ, now_nanos)
now_nanos += DT_CTRL * 1e9 # 10 ms
CC = structs.CarControl()
CC.enabled = True
CC.latActive = True
CC.longActive = True
CC = CC.as_reader()
for _ in range(10):
car_interface.update([])
car_interface.apply(CC, CC_IQ, now_nanos)
now_nanos += DT_CTRL * 1e9 # 10ms
# Test radar interface
radar_interface = car_interface.RadarInterface(car_params, car_params_iq)
assert radar_interface
# Run radar interface once
radar_interface.update([])
if not car_params.radarUnavailable and radar_interface.rcp is not None and \
hasattr(radar_interface, '_update') and hasattr(radar_interface, 'trigger_msg'):
radar_interface._update([radar_interface.trigger_msg])
# Test radar fault
if not car_params.radarUnavailable and radar_interface.rcp is not None:
cans = [(0, [CanData(0, b'', 0) for _ in range(5)])]
rr = radar_interface.update(cans)
assert rr is None or len(rr.errors) > 0
@pytest.mark.parametrize("car_name", SIGNED_RUNTIME_PLATFORMS)
def test_signed_runtime_params(self, car_name):
car_interface = interfaces[car_name]
fingerprints = {bus: {} for bus in range(7)}
car_params = car_interface.get_params(car_name, fingerprints, [], alpha_long=False, is_release=False, docs=False)
car_params_iq = car_interface.get_params_iq(car_params, car_name, fingerprints, [],
alpha_long=False, is_release_iq=False, docs=False)
assert car_params.mass > 1
assert car_params.wheelbase > 0
assert car_params.maxLateralAccel > 0
assert car_params_iq is not None
def test_interface_attrs(self):
"""Asserts basic behavior of interface attribute getter"""
num_brands = len(get_interface_attr('CAR'))
assert num_brands >= 12
# Should return value for all brands when not combining, even if attribute doesn't exist
ret = get_interface_attr('FAKE_ATTR')
assert len(ret) == num_brands
# Make sure we can combine dicts
ret = get_interface_attr('DBC', combine_brands=True)
assert len(ret) >= 160
# We don't support combining non-dicts
ret = get_interface_attr('CAR', combine_brands=True)
assert len(ret) == 0
# If brand has None value, it shouldn't return when ignore_none=True is specified
none_brands = {b for b, v in get_interface_attr('FINGERPRINTS').items() if v is None}
assert len(none_brands) >= 1
ret = get_interface_attr('FINGERPRINTS', ignore_none=True)
none_brands_in_ret = none_brands.intersection(ret)
assert len(none_brands_in_ret) == 0, f'Brands with None values in ignore_none=True result: {none_brands_in_ret}'
def test_interface_discovery_does_not_load_private_runtime():
code = "; ".join((
"import sys",
"sys.modules['iqpilot.system.proprietary_runtime._verified_import'] = None",
"from iqdbc.car.car_helpers import interfaces",
"assert len(interfaces) >= 300",
))
subprocess.run([sys.executable, "-c", code], check=True)

View File

@@ -0,0 +1,93 @@
from iqdbc.car.can_definitions import CanData
from iqdbc.car.disable_ecu import (CLEAR_DTC_ISOTP_SF, CLEAR_DTC_REQUEST, EXT_DIAG_REQUEST,
FUNCTIONAL_ADDR_29BIT, clear_all_dtcs, clear_ecu_dtcs, disable_ecu)
RADAR_ADDR = 0x18DAB0F1
COM_CONT_REQUEST = b'\x28\x83\x03'
class QueryRecorder:
def __init__(self):
self.requests = []
def make_fake_query(self):
recorder = self
class FakeIsoTpParallelQuery:
def __init__(self, can_send, can_recv, bus, addrs, requests, responses, response_offset=0x8):
self.bus = bus
self.addrs = addrs
self.request = requests[0]
recorder.requests.append((bus, addrs[0][0], requests[0]))
def get_data(self, timeout):
return {(self.addrs[0][0], None): b''}
return FakeIsoTpParallelQuery
def test_clear_all_dtcs_broadcasts_single_frame():
sent = []
clear_all_dtcs(lambda msgs: sent.extend(msgs), [0, 2])
assert sent == [
CanData(FUNCTIONAL_ADDR_29BIT, CLEAR_DTC_ISOTP_SF, 0),
CanData(FUNCTIONAL_ADDR_29BIT, CLEAR_DTC_ISOTP_SF, 2),
]
def test_clear_dtc_isotp_framing():
assert len(CLEAR_DTC_ISOTP_SF) == 8
assert CLEAR_DTC_ISOTP_SF[0] == len(CLEAR_DTC_REQUEST)
assert CLEAR_DTC_ISOTP_SF[1:1 + len(CLEAR_DTC_REQUEST)] == CLEAR_DTC_REQUEST
assert CLEAR_DTC_REQUEST == b'\x14\xff\xff\xff'
def test_clear_ecu_dtcs_sequence(mocker):
recorder = QueryRecorder()
mocker.patch("iqdbc.car.disable_ecu.IsoTpParallelQuery", recorder.make_fake_query())
assert clear_ecu_dtcs(None, None, bus=0, addr=RADAR_ADDR)
assert recorder.requests == [
(0, RADAR_ADDR, EXT_DIAG_REQUEST),
(0, RADAR_ADDR, CLEAR_DTC_REQUEST),
]
def test_disable_ecu_sequence(mocker):
recorder = QueryRecorder()
mocker.patch("iqdbc.car.disable_ecu.IsoTpParallelQuery", recorder.make_fake_query())
assert disable_ecu(None, None, bus=1, addr=RADAR_ADDR, com_cont_req=COM_CONT_REQUEST)
assert recorder.requests == [
(1, RADAR_ADDR, EXT_DIAG_REQUEST),
(1, RADAR_ADDR, COM_CONT_REQUEST),
]
def test_disable_ecu_clears_dtcs_before_comm_control(mocker):
recorder = QueryRecorder()
mocker.patch("iqdbc.car.disable_ecu.IsoTpParallelQuery", recorder.make_fake_query())
assert disable_ecu(None, None, bus=1, addr=RADAR_ADDR, com_cont_req=COM_CONT_REQUEST, clear_dtc=True)
assert recorder.requests == [
(1, RADAR_ADDR, EXT_DIAG_REQUEST),
(1, RADAR_ADDR, CLEAR_DTC_REQUEST),
(1, RADAR_ADDR, COM_CONT_REQUEST),
]
def test_disable_ecu_retries_then_fails(mocker):
attempts = []
class NoResponseQuery:
def __init__(self, can_send, can_recv, bus, addrs, requests, responses, response_offset=0x8):
attempts.append(requests[0])
def get_data(self, timeout):
return {}
mocker.patch("iqdbc.car.disable_ecu.IsoTpParallelQuery", NoResponseQuery)
assert not disable_ecu(None, None, addr=RADAR_ADDR, retry=3)
assert attempts == [EXT_DIAG_REQUEST] * 3

View File

@@ -0,0 +1,75 @@
from collections import defaultdict
from iqdbc.car.car_helpers import interfaces
from iqdbc.car.docs import get_all_car_docs
from iqdbc.car.docs_definitions import Cable, Column, PartType, Star, SupportType
from iqdbc.car.honda.values import CAR as HONDA
from iqdbc.car.values import PLATFORMS
class TestCarDocs:
@classmethod
def setup_class(cls):
cls.all_cars = get_all_car_docs()
def test_duplicate_years(self, subtests):
make_model_years = defaultdict(list)
for car in self.all_cars:
if car.support_type != SupportType.UPSTREAM:
continue
with subtests.test(car_docs_name=car.name):
make_model = (car.make, car.model)
for year in car.year_list:
assert year not in make_model_years[make_model], f"{car.name}: Duplicate model year"
make_model_years[make_model].append(year)
def test_missing_car_docs(self, subtests):
all_car_docs_platforms = [name for name, config in PLATFORMS.items()]
for platform in sorted(interfaces.keys()):
with subtests.test(platform=platform):
assert platform in all_car_docs_platforms, f"Platform: {platform} doesn't have a CarDocs entry"
def test_naming_conventions(self, subtests):
# Asserts market-standard car naming conventions by brand
for car in self.all_cars:
with subtests.test(car=car.name):
tokens = car.model.lower().split(" ")
if car.brand == "hyundai":
assert "phev" not in tokens, "Use `Plug-in Hybrid`"
assert "hev" not in tokens, "Use `Hybrid`"
if "plug-in hybrid" in car.model.lower():
assert "Plug-in Hybrid" in car.model, "Use correct capitalization"
if car.make != "Kia":
assert "ev" not in tokens, "Use `Electric`"
elif car.brand == "toyota":
if "rav4" in tokens:
assert "RAV4" in car.model, "Use correct capitalization"
def test_torque_star(self, subtests):
# Asserts brand-specific assumptions around steering torque star
for car in self.all_cars:
with subtests.test(car=car.name):
# honda sanity check, it's the definition of a no torque star
if car.car_fingerprint in (HONDA.HONDA_ACCORD, HONDA.HONDA_CIVIC, HONDA.HONDA_CRV, HONDA.HONDA_ODYSSEY, HONDA.HONDA_ODYSSEY_TWN, HONDA.HONDA_PILOT):
assert car.row[Column.STEERING_TORQUE] == Star.EMPTY, f"{car.name} has full torque star"
elif car.brand in ("toyota", "hyundai"):
assert car.row[Column.STEERING_TORQUE] != Star.EMPTY, f"{car.name} has no torque star"
def test_year_format(self, subtests):
for car in self.all_cars:
if car.name == "comma body":
continue
with subtests.test(car=car.name):
assert car.years and car.year_list, f"Format years correctly: {car.name}"
def test_harnesses(self, subtests):
for car in self.all_cars:
if car.name == "comma body" or car.support_type != SupportType.UPSTREAM:
continue
with subtests.test(car=car.name):
car_part_type = [p.part_type for p in car.car_parts.all_parts()]
car_parts = list(car.car_parts.all_parts())
assert len(car_parts) > 0, f"Need to specify car parts: {car.name}"
assert car_part_type.count(PartType.connector) == 1, f"Need to specify one harness connector: {car.name}"
assert car_part_type.count(PartType.mount) == 1, f"Need to specify one mount: {car.name}"
assert Cable.obd_c_cable_2ft in car_parts, f"Need to specify an OBD-C cable (2ft): {car.name}"

View File

@@ -0,0 +1,355 @@
import pytest
import random
import time
from collections import defaultdict
from iqdbc.car.can_definitions import CanData
from iqdbc.car.car_helpers import interfaces
from iqdbc.car.structs import CarParams
from iqdbc.car.fingerprints import FW_VERSIONS
from iqdbc.car.fw_versions import FW_QUERY_CONFIGS, FUZZY_EXCLUDE_ECUS, VERSIONS, build_fw_dict, \
match_fw_to_car, get_brand_ecu_matches, get_fw_versions, get_present_ecus
from iqdbc.car.vin import get_vin
CarFw = CarParams.CarFw
Ecu = CarParams.Ecu
ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()}
VALID_VERSION_SETS = [
(brand, car_model, ecus)
for brand, brand_versions in VERSIONS.items()
for car_model, ecus in brand_versions.items()
if ecus and all(fw_versions for fw_versions in ecus.values())
]
CUSTOM_FUZZY_VERSION_SETS = [
(brand, car_model, ecus)
for brand, car_model, ecus in VALID_VERSION_SETS
if FW_QUERY_CONFIGS[brand].match_fw_to_car_fuzzy is not None
]
FUZZY_VERSION_SETS = [
(brand, car_model, ecus)
for brand, car_model, ecus in VALID_VERSION_SETS
if any(ecu[0] not in FUZZY_EXCLUDE_ECUS for ecu in ecus)
]
def representative_vin(car_model):
config = getattr(car_model, "config", None)
wmis = getattr(config, "wmis", set())
chassis_codes = getattr(config, "chassis_codes", set())
if not wmis or not chassis_codes:
return ""
model_years = getattr(config, "model_years", set())
vin = ["0"] * 17
vin[0:3] = str(sorted(wmis, key=str)[0])
vin[6:8] = sorted(chassis_codes)[0]
vin[9] = sorted(model_years)[0] if model_years else "0"
return "".join(vin)
class TestFwFingerprint:
def assertFingerprints(self, candidates, expected):
candidates = list(candidates)
assert len(candidates) == 1, f"got more than one candidate: {candidates}"
assert candidates[0] == expected
@pytest.mark.parametrize("brand, car_model, ecus, test_non_essential",
[(*version_set, test_non_essential)
for version_set in VALID_VERSION_SETS for test_non_essential in (True, False)])
def test_exact_match(self, brand, car_model, ecus, test_non_essential):
config = FW_QUERY_CONFIGS[brand]
CP = CarParams(carVin=representative_vin(car_model))
for _ in range(20):
fw = []
for ecu, fw_versions in ecus.items():
# Assume non-essential ECUs apply to all cars, so we catch cases where Car A with
# missing ECUs won't match to Car B where only Car B has labeled non-essential ECUs
if ecu[0] in config.non_essential_ecus and test_non_essential:
continue
ecu_name, addr, sub_addr = ecu
fw.append(CarFw(ecu=ecu_name, fwVersion=random.choice(fw_versions), brand=brand,
address=addr, subAddress=0 if sub_addr is None else sub_addr))
CP.carFw = fw
_, matches = match_fw_to_car(CP.carFw, CP.carVin, allow_fuzzy=False)
if not test_non_essential:
self.assertFingerprints(matches, car_model)
else:
# if we're removing ECUs we expect some match loss, but it shouldn't mismatch
if len(matches) != 0:
self.assertFingerprints(matches, car_model)
@pytest.mark.parametrize("brand, car_model, ecus", CUSTOM_FUZZY_VERSION_SETS)
def test_custom_fuzzy_match(self, brand, car_model, ecus):
config = FW_QUERY_CONFIGS[brand]
CP = CarParams()
for _ in range(5):
fw = []
for ecu, fw_versions in ecus.items():
ecu_name, addr, sub_addr = ecu
fw.append(CarFw(ecu=ecu_name, fwVersion=random.choice(fw_versions), brand=brand,
address=addr, subAddress=0 if sub_addr is None else sub_addr))
CP.carFw = fw
_, matches = match_fw_to_car(CP.carFw, CP.carVin, allow_exact=False, log=False)
brand_matches = config.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, VERSIONS[brand])
# If both have matches, they must agree
if len(matches) == 1 and len(brand_matches) == 1:
assert matches == brand_matches
@pytest.mark.parametrize("brand, car_model, ecus", FUZZY_VERSION_SETS)
def test_fuzzy_match_ecu_count(self, brand, car_model, ecus):
valid_ecus = [e for e in ecus if e[0] not in FUZZY_EXCLUDE_ECUS]
fw = []
for ecu in valid_ecus:
ecu_name, addr, sub_addr = ecu
for _ in range(5):
# Add multiple FW versions to simulate ECU returning to multiple queries in a brand
fw.append(CarFw(ecu=ecu_name, fwVersion=random.choice(ecus[ecu]), brand=brand,
address=addr, subAddress=0 if sub_addr is None else sub_addr))
CP = CarParams(carFw=fw)
_, matches = match_fw_to_car(CP.carFw, CP.carVin, allow_exact=False, log=False)
# Assert no match if there are not enough unique ECUs
unique_ecus = {(f.address, f.subAddress) for f in fw}
if len(unique_ecus) < 2:
assert len(matches) == 0, car_model
# There won't always be a match due to shared FW, but if there is it should be correct
elif len(matches):
self.assertFingerprints(matches, car_model)
def test_fw_version_lists(self, subtests):
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
for ecu, ecu_fw in ecus.items():
with subtests.test(ecu):
duplicates = {fw for fw in ecu_fw if ecu_fw.count(fw) > 1}
assert not len(duplicates), f'{car_model}: Duplicate FW versions: Ecu.{ecu[0]}, {duplicates}'
assert len(ecu_fw) > 0, f'{car_model}: No FW versions: Ecu.{ecu[0]}'
def test_all_addrs_map_to_one_ecu(self):
for brand, cars in VERSIONS.items():
addr_to_ecu = defaultdict(set)
for ecus in cars.values():
for ecu_type, addr, sub_addr in ecus.keys():
addr_to_ecu[(addr, sub_addr)].add(ecu_type)
ecus_for_addr = addr_to_ecu[(addr, sub_addr)]
ecu_strings = ", ".join([f'Ecu.{ecu}' for ecu in ecus_for_addr])
assert len(ecus_for_addr) <= 1, f"{brand} has multiple ECUs that map to one address: {ecu_strings} -> ({hex(addr)}, {sub_addr})"
def test_data_collection_ecus(self, subtests):
# Asserts no extra ECUs are in the fingerprinting database
for brand, config in FW_QUERY_CONFIGS.items():
for car_model, ecus in VERSIONS[brand].items():
bad_ecus = set(ecus).intersection(config.extra_ecus)
with subtests.test(car_model=car_model.value):
assert not len(bad_ecus), f'{car_model}: Fingerprints contain ECUs added for data collection: {bad_ecus}'
def test_blacklisted_ecus(self, subtests):
blacklisted_addrs = (0x7c4, 0x7d0) # includes A/C ecu and an unknown ecu
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
CP = interfaces[car_model].get_non_essential_params(car_model)
if CP.brand == 'subaru':
for ecu in ecus.keys():
assert ecu[1] not in blacklisted_addrs, f'{car_model}: Blacklisted ecu: (Ecu.{ecu[0]}, {hex(ecu[1])})'
elif CP.brand == "chrysler":
# Some HD trucks have a combined TCM and ECM
if CP.carFingerprint.startswith("RAM_HD"):
for ecu in ecus.keys():
assert ecu[0] != Ecu.transmission, f"{car_model}: Blacklisted ecu: (Ecu.{ecu[0]}, {hex(ecu[1])})"
def test_missing_versions_and_configs(self, subtests):
brand_versions = set(VERSIONS.keys())
brand_configs = set(FW_QUERY_CONFIGS.keys())
if len(brand_configs - brand_versions):
with subtests.test():
pytest.fail(f"Brands do not implement FW_VERSIONS: {brand_configs - brand_versions}")
if len(brand_versions - brand_configs):
with subtests.test():
pytest.fail(f"Brands do not implement FW_QUERY_CONFIG: {brand_versions - brand_configs}")
# Ensure each brand has at least 1 ECU to query, and extra ECU retrieval
for brand, config in FW_QUERY_CONFIGS.items():
assert len(config.get_all_ecus({}, include_extra_ecus=False)) == 0
assert config.get_all_ecus({}) == set(config.extra_ecus)
if len(VERSIONS[brand]) > 0:
assert len(config.get_all_ecus(VERSIONS[brand])) > 0
def test_fw_request_ecu_whitelist(self, subtests):
for brand, config in FW_QUERY_CONFIGS.items():
with subtests.test(brand=brand):
whitelisted_ecus = {ecu for r in config.requests for ecu in r.whitelist_ecus}
brand_ecus = {fw[0] for car_fw in VERSIONS[brand].values() for fw in car_fw}
brand_ecus |= {ecu[0] for ecu in config.extra_ecus}
# each ecu in brand's fw versions + extra ecus needs to be whitelisted at least once
ecus_not_whitelisted = brand_ecus - whitelisted_ecus
ecu_strings = ", ".join([f'Ecu.{ecu}' for ecu in ecus_not_whitelisted])
assert not (len(whitelisted_ecus) and len(ecus_not_whitelisted)), \
f'{brand.title()}: ECUs not in any FW query whitelists: {ecu_strings}'
def test_request_ecus_in_versions(self):
# All ECUs in requests should be in the brand's FW versions
for brand, config in FW_QUERY_CONFIGS.items():
request_ecus = {ecu for r in config.requests for ecu in r.whitelist_ecus} - {ecu[0] for ecu in config.extra_ecus}
print(brand, request_ecus)
version_ecus = config.get_all_ecus(VERSIONS[brand], include_extra_ecus=False)
for request_ecu in request_ecus:
assert request_ecu in {e for e, _, _ in version_ecus}, f"Ecu.{ECU_NAME[request_ecu]} not in {brand} FW versions"
def test_brand_ecu_matches(self):
brand_matches = get_brand_ecu_matches(set())
assert len(brand_matches) > 0
assert all(len(e) and not any(e) for e in brand_matches.values())
# we ignore bus
brand_matches = get_brand_ecu_matches({(0x758, 0xf, 99)})
assert True in brand_matches['toyota']
assert not any(any(e) for b, e in brand_matches.items() if b != 'toyota')
class TestFwFingerprintTiming:
N: int = 5
TOL: float = 0.05
# for patched functions
current_obd_multiplexing: bool
total_time: float
@staticmethod
def fake_can_send(msgs):
pass
@staticmethod
def fake_can_recv(wait_for_one: bool = False) -> list[list[CanData]]:
return ([[CanData(random.randint(0x600, 0x800), b'\x00' * 8, 0)]]
if random.uniform(0, 1) > 0.5 else [])
def fake_set_obd_multiplexing(self, obd_multiplexing):
"""The 10Hz blocking params loop adds on average 50ms to the query time for each OBD multiplexing change"""
if obd_multiplexing != self.current_obd_multiplexing:
self.current_obd_multiplexing = obd_multiplexing
self.total_time += 0.1 / 2
def fake_get_data(self, timeout):
self.total_time += timeout
return {}
def _benchmark_brand(self, brand, num_pandas, mocker):
self.total_time = 0
mocker.patch("iqdbc.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)
for _ in range(self.N):
# Treat each brand as the most likely (aka, the first) brand with OBD multiplexing initially on
self.current_obd_multiplexing = True
t = time.perf_counter()
get_fw_versions(self.fake_can_recv, self.fake_can_send, self.fake_set_obd_multiplexing, brand, num_pandas=num_pandas)
self.total_time += time.perf_counter() - t
return self.total_time / self.N
def _assert_timing(self, avg_time, ref_time):
assert avg_time < ref_time + self.TOL
assert avg_time > ref_time - self.TOL, "Performance seems to have improved, update test refs."
def test_startup_timing(self, subtests, mocker):
# Tests worse-case VIN query time and typical present ECU query time
vin_ref_times = {'worst': 1.6, 'best': 0.8} # best assumes we go through all queries to get a match
present_ecu_ref_time = 0.45
def fake_get_ecu_addrs(*_, timeout):
self.total_time += timeout
return set()
self.total_time = 0.0
mocker.patch("iqdbc.car.fw_versions.get_ecu_addrs", fake_get_ecu_addrs)
for _ in range(self.N):
self.current_obd_multiplexing = True
get_present_ecus(self.fake_can_recv, self.fake_can_send, self.fake_set_obd_multiplexing, num_pandas=2)
self._assert_timing(self.total_time / self.N, present_ecu_ref_time)
print(f'get_present_ecus, query time={self.total_time / self.N} seconds')
for name, args in (('worst', {}), ('best', {'retry': 1})):
with subtests.test(name=name):
self.total_time = 0.0
mocker.patch("iqdbc.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)
for _ in range(self.N):
get_vin(self.fake_can_recv, self.fake_can_send, (0, 1), **args)
self._assert_timing(self.total_time / self.N, vin_ref_times[name])
print(f'get_vin {name} case, query time={self.total_time / self.N} seconds')
def test_fw_query_timing(self, subtests, mocker):
total_ref_time = {1: 7.7, 2: 8.3}
brand_ref_times = {
1: {
'gm': 1.0,
'body': 0.1,
'chrysler': 0.3,
'ford': 1.5,
'honda': 0.45,
'hyundai': 0.65,
'mazda': 0.1,
'nissan': 0.8,
'subaru': 0.65,
'tesla': 0.1,
'toyota': 0.7,
'volkswagen': 0.95,
'rivian': 0.3,
'psa': 0.1,
},
2: {
'ford': 1.6,
'hyundai': 1.15,
}
}
total_times = {1: 0.0, 2: 0.0}
for num_pandas in (1, 2):
for brand, config in FW_QUERY_CONFIGS.items():
with subtests.test(brand=brand, num_pandas=num_pandas):
avg_time = self._benchmark_brand(brand, num_pandas, mocker)
total_times[num_pandas] += avg_time
avg_time = round(avg_time, 2)
ref_time = brand_ref_times[num_pandas].get(brand)
if ref_time is None:
# ref time should be same as 1 panda if no aux queries
ref_time = brand_ref_times[num_pandas - 1][brand]
self._assert_timing(avg_time, ref_time)
print(f'{brand=}, {num_pandas=}, {len(config.requests)=}, avg FW query time={avg_time} seconds')
for num_pandas in (1, 2):
with subtests.test(brand='all_brands', num_pandas=num_pandas):
total_time = round(total_times[num_pandas], 2)
self._assert_timing(total_time, total_ref_time[num_pandas])
print(f'all brands, total FW query time={total_time} seconds')
def test_get_fw_versions(self, subtests, mocker):
# some coverage on IsoTpParallelQuery and panda UDS library
# TODO: replace this with full fingerprint simulation testing
# https://github.com/commaai/panda/pull/1329
def fake_carlog_exception(*args, **kwargs):
raise
t = 0
def fake_monotonic():
nonlocal t
t += 0.0001
return t
mocker.patch("iqdbc.car.carlog.carlog.exception", fake_carlog_exception)
mocker.patch("time.monotonic", fake_monotonic)
for brand in FW_QUERY_CONFIGS.keys():
with subtests.test(brand=brand):
get_fw_versions(self.fake_can_recv, self.fake_can_send, lambda obd: None, brand)

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
from collections import defaultdict
import importlib
from parameterized import parameterized_class
import pytest
import sys
from iqdbc.car import DT_CTRL, structs
from iqdbc.car.car_helpers import interfaces
from iqdbc.car.interfaces import get_torque_params
from iqdbc.car.lateral import ISO_LATERAL_ACCEL
from iqdbc.car.values import PLATFORMS
# ISO 11270 - allowed up jerk is strictly lower than recommended limits
MAX_LAT_JERK_UP = 2.5 # m/s^3
MAX_LAT_JERK_DOWN = 5.0 # m/s^3
MAX_LAT_JERK_UP_TOLERANCE = 0.5 # m/s^3
# jerk is measured over half a second
JERK_MEAS_T = 0.5
def torque_platforms():
platforms = []
for car_model in sorted(PLATFORMS):
CP = interfaces[car_model].get_non_essential_params(car_model)
if car_model != 'MOCK' and CP.steerControlType == structs.CarParams.SteerControlType.torque and not CP.notCar:
platforms.append(car_model)
return platforms
@parameterized_class('car_model', [(car_model,) for car_model in torque_platforms()])
class TestLateralLimits:
car_model: str
@classmethod
def setup_class(cls):
CarInterface = interfaces[cls.car_model]
CP = CarInterface.get_non_essential_params(cls.car_model)
CarControllerParams = importlib.import_module(f'iqdbc.car.{CP.brand}.values').CarControllerParams
cls.control_params = CarControllerParams(CP)
cls.torque_params = get_torque_params()[cls.car_model]
@staticmethod
def calculate_0_5s_jerk(control_params, torque_params):
steer_step = control_params.STEER_STEP
max_lat_accel = torque_params['MAX_LAT_ACCEL_MEASURED']
# Steer up/down delta per 10ms frame, in percentage of max torque
steer_up_per_frame = control_params.STEER_DELTA_UP / control_params.STEER_MAX / steer_step
steer_down_per_frame = control_params.STEER_DELTA_DOWN / control_params.STEER_MAX / steer_step
# Lateral acceleration reached in 0.5 seconds, clipping to max torque
accel_up_0_5_sec = min(steer_up_per_frame * JERK_MEAS_T / DT_CTRL, 1.0) * max_lat_accel
accel_down_0_5_sec = min(steer_down_per_frame * JERK_MEAS_T / DT_CTRL, 1.0) * max_lat_accel
# Convert to m/s^3
return accel_up_0_5_sec / JERK_MEAS_T, accel_down_0_5_sec / JERK_MEAS_T
def test_jerk_limits(self):
up_jerk, down_jerk = self.calculate_0_5s_jerk(self.control_params, self.torque_params)
assert up_jerk <= MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE
assert down_jerk <= MAX_LAT_JERK_DOWN
def test_max_lateral_accel(self):
assert self.torque_params["MAX_LAT_ACCEL_MEASURED"] <= ISO_LATERAL_ACCEL
class LatAccelReport:
car_model_jerks: defaultdict[str, dict[str, float]] = defaultdict(dict)
def pytest_sessionfinish(self):
print(f"\n\n---- Lateral limit report ({len(PLATFORMS)} cars) ----\n")
max_car_model_len = max([len(car_model) for car_model in self.car_model_jerks])
for car_model, _jerks in sorted(self.car_model_jerks.items(), key=lambda i: i[1]['up_jerk'], reverse=True):
violation = _jerks["up_jerk"] > MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE or \
_jerks["down_jerk"] > MAX_LAT_JERK_DOWN
violation_str = " - VIOLATION" if violation else ""
print(f"{car_model:{max_car_model_len}} - up jerk: {round(_jerks['up_jerk'], 2):5} " +
f"m/s^3, down jerk: {round(_jerks['down_jerk'], 2):5} m/s^3{violation_str}")
@pytest.fixture(scope="class", autouse=True)
def class_setup(self, request):
yield
cls = request.cls
if hasattr(cls, "control_params"):
up_jerk, down_jerk = TestLateralLimits.calculate_0_5s_jerk(cls.control_params, cls.torque_params)
self.car_model_jerks[cls.car_model] = {"up_jerk": up_jerk, "down_jerk": down_jerk}
if __name__ == '__main__':
sys.exit(pytest.main([__file__, '-n0', '--no-summary'], plugins=[LatAccelReport()])) # noqa: TID251

View File

@@ -0,0 +1,17 @@
from iqdbc.car.values import PLATFORMS
class TestPlatformConfigs:
def test_configs(self, subtests):
for name, platform in PLATFORMS.items():
with subtests.test(platform=str(platform)):
assert platform.config._frozen
if platform != "MOCK":
assert len(platform.config.dbc_dict) > 0
assert len(platform.config.platform_str) > 0
assert name == platform.config.platform_str
assert platform.config.specs is not None

View File

@@ -0,0 +1,18 @@
import pytest
from iqdbc.car.values import PLATFORMS
from iqdbc.car.tests.routes import route_exempt_cars, routes
@pytest.mark.parametrize("platform", PLATFORMS.keys())
def test_test_route_present(platform):
tested_platforms = [r.car_model for r in routes]
assert platform in set(tested_platforms) | set(route_exempt_cars), \
f"Missing test route for {platform}. Add a route to iqdbc/car/tests/routes.py"
def test_route_exemptions_are_current():
tested_platforms = {r.car_model for r in routes}
assert len(route_exempt_cars) == len(set(route_exempt_cars))
assert not tested_platforms & set(route_exempt_cars)
assert set(route_exempt_cars) <= set(PLATFORMS)

View File

@@ -0,0 +1,67 @@
import pytest
import math
import numpy as np
from iqdbc.car.honda.interface import CarInterface
from iqdbc.car.honda.values import CAR
from iqdbc.car.vehicle_model import VehicleModel, dyn_ss_sol, create_dyn_state_matrices
class TestVehicleModel:
def setup_method(self):
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
self.VM = VehicleModel(CP)
def test_round_trip_yaw_rate(self):
# TODO: fix VM to work at zero speed
for u in np.linspace(1, 30, num=10):
for roll in np.linspace(math.radians(-20), math.radians(20), num=11):
for sa in np.linspace(math.radians(-20), math.radians(20), num=11):
yr = self.VM.yaw_rate(sa, u, roll)
new_sa = self.VM.get_steer_from_yaw_rate(yr, u, roll)
assert sa == pytest.approx(new_sa)
def test_dyn_ss_sol_against_yaw_rate(self):
"""Verify that the yaw_rate helper function matches the results
from the state space model."""
for roll in np.linspace(math.radians(-20), math.radians(20), num=11):
for u in np.linspace(1, 30, num=10):
for sa in np.linspace(math.radians(-20), math.radians(20), num=11):
# Compute yaw rate based on state space model
_, yr1 = dyn_ss_sol(sa, u, roll, self.VM)
# Compute yaw rate using direct computations
yr2 = self.VM.yaw_rate(sa, u, roll)
assert float(yr1[0]) == pytest.approx(yr2)
def test_syn_ss_sol_simulate(self):
"""Verifies that dyn_ss_sol matches a simulation"""
for roll in np.linspace(math.radians(-20), math.radians(20), num=11):
for u in np.linspace(1, 30, num=10):
A, B = create_dyn_state_matrices(u, self.VM)
# Convert to discrete time system
dt = 0.01
top = np.hstack((A, B))
full = np.vstack((top, np.zeros_like(top))) * dt
Md = sum([np.linalg.matrix_power(full, k) / math.factorial(k) for k in range(25)])
Ad = Md[:A.shape[0], :A.shape[1]]
Bd = Md[:A.shape[0], A.shape[1]:]
for sa in np.linspace(math.radians(-20), math.radians(20), num=11):
inp = np.array([[sa], [roll]])
# Simulate for 1 second
x1 = np.zeros((2, 1))
for _ in range(100):
x1 = Ad @ x1 + Bd @ inp
# Compute steady state solution directly
x2 = dyn_ss_sol(sa, u, roll, self.VM)
np.testing.assert_almost_equal(x1, x2, decimal=3)