IQ.Pilot Prebuilt Release @ 7a91404
This commit is contained in:
323
artifacts/package_runtime/iqdbc/car/tests/car_diff.py
Normal file
323
artifacts/package_runtime/iqdbc/car/tests/car_diff.py
Normal 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))
|
||||
413
artifacts/package_runtime/iqdbc/car/tests/routes.py
Normal file
413
artifacts/package_runtime/iqdbc/car/tests/routes.py
Normal 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),
|
||||
]
|
||||
@@ -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
|
||||
200
artifacts/package_runtime/iqdbc/car/tests/test_car_interfaces.py
Normal file
200
artifacts/package_runtime/iqdbc/car/tests/test_car_interfaces.py
Normal 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)
|
||||
@@ -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
|
||||
75
artifacts/package_runtime/iqdbc/car/tests/test_docs.py
Normal file
75
artifacts/package_runtime/iqdbc/car/tests/test_docs.py
Normal 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}"
|
||||
355
artifacts/package_runtime/iqdbc/car/tests/test_fw_fingerprint.py
Normal file
355
artifacts/package_runtime/iqdbc/car/tests/test_fw_fingerprint.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
18
artifacts/package_runtime/iqdbc/car/tests/test_routes.py
Normal file
18
artifacts/package_runtime/iqdbc/car/tests/test_routes.py
Normal 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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user