From 1794c9854521169159602831d403a6446489f6fc Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 8 Jan 2026 15:37:05 -0800 Subject: [PATCH 001/117] Opendbc replay (#6) * add replay POC * compare HEAD to master * split in compare and worker * copy worker.py * cleanup * fix * apply tesla brake PR * fix * fix mutation test * add panda * add timestamps * simplify * add falling edge * add remaining carstate * simple lines * bump * rename * use zst as ref * ref commit * cleanup * formatting * no failing * fix git * clean * use pool.map * fix docker build * clean * Revert "fix docker build" This reverts commit da3577441eaa4b7fdc5763f73e779dd32c03a3c3. * use flat paths * format * use azure test instance * print full diff * no stderr output * use GIT_REF * Revert "apply tesla brake PR" This reverts commit f377e21f3865c574db5d618be20b4efa86fba66f. * allow overwrite blobs * supress stderr --- .github/workflows/tests.yml | 35 +++++++- opendbc/car/tests/replay/__init__.py | 0 opendbc/car/tests/replay/car_replay.py | 111 +++++++++++++++++++++++++ opendbc/car/tests/replay/worker.py | 101 ++++++++++++++++++++++ opendbc/safety/tests/test_tesla.py | 14 ++++ 5 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 opendbc/car/tests/replay/__init__.py create mode 100644 opendbc/car/tests/replay/car_replay.py create mode 100644 opendbc/car/tests/replay/worker.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f04f489d35..8a12a885f5d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,7 +60,40 @@ jobs: scons -j8 cd opendbc/safety/tests && ./mutation.sh - # TODO: this test needs to move to opendbc + car_replay: + name: car replay + runs-on: ${{ github.repository == 'commaai/opendbc' && 'namespace-profile-amd64-8x16' || 'ubuntu-latest' }} + env: + BASE_IMAGE: openpilot-base + GIT_REF: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.before || format('origin/{0}', github.event.repository.default_branch) }} + RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONPATH=/tmp/openpilot -e AZURE_TOKEN=$AZURE_TOKEN -e GIT_REF=$GIT_REF -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/bash -c + steps: + - uses: actions/checkout@v4 + with: + repository: 'commaai/openpilot' + ref: 'master' + submodules: true + - run: rm -rf opendbc_repo/ + - uses: actions/checkout@v4 + with: + path: opendbc_repo + fetch-depth: 0 + - run: cd opendbc_repo && git fetch origin master + - uses: ./.github/workflows/setup-with-retry + - name: Build docker + run: selfdrive/test/docker_build.sh base + - name: Build + run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" + - name: Test + if: github.event_name == 'pull_request' + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py 2>/dev/null" + - name: Update refs + if: github.ref == 'refs/heads/master' + env: + AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py --update-refs 2>/dev/null" + + # TODO: this needs to move to opendbc test_models: name: test models runs-on: ${{ github.repository == 'commaai/opendbc' && 'namespace-profile-amd64-8x16' || 'ubuntu-latest' }} diff --git a/opendbc/car/tests/replay/__init__.py b/opendbc/car/tests/replay/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py new file mode 100644 index 00000000000..721b8f52e7a --- /dev/null +++ b/opendbc/car/tests/replay/car_replay.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +import argparse +import os +import re +import requests +import sys +import tempfile +from collections import defaultdict +from concurrent.futures import ProcessPoolExecutor +from pathlib import Path + + +def get_changed_platforms(cwd, database): + from openpilot.common.utils import run_cmd + git_ref = os.environ.get("GIT_REF", "origin/master") + changed = run_cmd(["git", "diff", "--name-only", f"{git_ref}...HEAD"], cwd=cwd) + brands = set() + for line in changed.splitlines(): + if m := re.search(r"opendbc/car/(\w+)/", line): + brands.add(m.group(1)) + if m := re.search(r"opendbc/dbc/(\w+?)_", line): + brands.add(m.group(1).lower()) + return [p for p in database if any(b.upper() in p for b in brands)] + + +def download_refs(ref_path, platforms, segments): + BASE_URL = "https://elkoled.blob.core.windows.net/openpilotci/" + for platform in platforms: + for seg in segments.get(platform, []): + filename = f"{platform}_{seg.replace('/', '_')}.zst" + resp = requests.get(f"{BASE_URL}car_replay/{filename}") + if resp.status_code == 200: + (Path(ref_path) / filename).write_bytes(resp.content) + + +def upload_refs(ref_path, platforms, segments): + from openpilot.tools.lib.azure_container import AzureContainer + container = AzureContainer("elkoled", "openpilotci") + for platform in platforms: + for seg in segments.get(platform, []): + filename = f"{platform}_{seg.replace('/', '_')}.zst" + local_path = Path(ref_path) / filename + if local_path.exists(): + container.upload_file(str(local_path), f"car_replay/{filename}", overwrite=True) + + +def format_diff(diffs): + return [f" {d[1]}: {d[2]} → {d[3]}" for d in diffs] + + +def run_replay(platforms, segments, ref_path, update, workers=8): + from opendbc.car.tests.replay.worker import process_segment + work = [(platform, seg, ref_path, update) + for platform in platforms for seg in segments.get(platform, [])] + with ProcessPoolExecutor(max_workers=workers) as pool: + return list(pool.map(process_segment, work)) + + +def main(platform=None, segments_per_platform=10, update_refs=False): + from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database + + cwd = Path(__file__).resolve().parents[4] + ref_path = tempfile.mkdtemp(prefix="car_ref_") + database = get_comma_car_segments_database() + platforms = [platform] if platform else get_changed_platforms(cwd, database) + + if not platforms: + print("No platforms detected from changes") + 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: + run_replay(platforms, segments, ref_path, update=True) + upload_refs(ref_path, platforms, segments) + return 0 + + download_refs(ref_path, platforms, segments) + results = run_replay(platforms, segments, ref_path, update=False) + + with_diffs = [(p, s, d) for p, s, d, e in results if d] + errors = [(p, s, e) for p, s, d, e in results if e] + n_passed = len(results) - len(with_diffs) - len(errors) + + print(f"\nResults: {n_passed} passed, {len(with_diffs)} with diffs, {len(errors)} errors") + + for plat, seg, err in errors: + print(f"\nERROR {plat} - {seg}: {err}") + + for plat, seg, diffs 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" {field} (frame: master → PR)") + for line in format_diff(fd): + print(line) + + return 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--platform") + parser.add_argument("--segments-per-platform", type=int, default=10) + parser.add_argument("--update-refs", action="store_true") + args = parser.parse_args() + sys.exit(main(args.platform, args.segments_per_platform, args.update_refs)) diff --git a/opendbc/car/tests/replay/worker.py b/opendbc/car/tests/replay/worker.py new file mode 100644 index 00000000000..4f27532fd9a --- /dev/null +++ b/opendbc/car/tests/replay/worker.py @@ -0,0 +1,101 @@ +import pickle +import zstandard as zstd +from pathlib import Path + +CARSTATE_FIELDS = [ + "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", + "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", + "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", + "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", + "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", + "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", + "cruiseState.nonAdaptive", "cruiseState.speedCluster", + "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", + "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", + "canValid", "canTimeout", +] + + +def get_value(obj, field): + for p in field.split("."): + obj = getattr(obj, p, None) + return obj.raw if hasattr(obj, "raw") else obj + + +def differs(v1, v2): + if isinstance(v1, float) and isinstance(v2, float): + return abs(v1 - v2) > 1e-3 + return v1 != v2 + + +def save_ref(path, states, timestamps): + data = list(zip(timestamps, states, strict=True)) + Path(path).write_bytes(zstd.compress(pickle.dumps(data))) + + +def load_ref(path): + return pickle.loads(zstd.decompress(Path(path).read_bytes())) + + +def load_can_messages(seg): + from opendbc.car.can_definitions import CanData + from openpilot.selfdrive.pandad import can_capnp_to_list + from openpilot.tools.lib.logreader import _LogFileReader + from openpilot.tools.lib.comma_car_segments import get_url + + parts = seg.split("/") + url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) + + can_msgs = [] + for msg in _LogFileReader(url): + if msg.which() == "can": + ts, data = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] + can_msgs.append((ts, [CanData(*x) for x in data])) + return can_msgs + + +def replay_segment(platform, can_msgs): + from opendbc.car import gen_empty_fingerprint, structs + from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces + + fingerprint = gen_empty_fingerprint() + for _, frames in can_msgs[:FRAME_FINGERPRINT]: + for msg in frames: + if msg.src < 64: + fingerprint[msg.src][msg.address] = len(msg.dat) + + CarInterface = interfaces[platform] + car_interface = CarInterface(CarInterface.get_params(platform, fingerprint, [], False, False, False)) + car_control = structs.CarControl().as_reader() + + states = [] + timestamps = [] + for ts, frames in can_msgs: + states.append(car_interface.update([(ts, frames)])) + car_interface.apply(car_control, ts) + timestamps.append(ts) + return states, timestamps + + +def process_segment(args): + platform, seg, ref_path, update = args + try: + can_msgs = load_can_messages(seg) + states, timestamps = replay_segment(platform, can_msgs) + ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" + + if update: + save_ref(ref_file, states, timestamps) + return (platform, seg, [], None) + + if not ref_file.exists(): + return (platform, seg, [], "no ref") + + ref = load_ref(ref_file) + diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) + for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) + for field in CARSTATE_FIELDS + if differs(get_value(state, field), get_value(ref_state, field))] + return (platform, seg, diffs, None) + except Exception as e: + return (platform, seg, [], str(e)) diff --git a/opendbc/safety/tests/test_tesla.py b/opendbc/safety/tests/test_tesla.py index 52390f81aa8..bd9c85a3b6c 100755 --- a/opendbc/safety/tests/test_tesla.py +++ b/opendbc/safety/tests/test_tesla.py @@ -165,6 +165,20 @@ def test_rx_hook(self): self.assertEqual(should_rx, self._rx(msg)) self.assertEqual(should_rx, self.safety.get_controls_allowed()) + def test_checksum(self): + msgs = [ + self._speed_msg(0), + self._user_gas_msg(0), + self._user_brake_msg(False), + self._pcm_status_msg(False), + self.packer.make_can_msg_safety("UI_warning", 0, {}), + ] + for msg in msgs: + self.assertTrue(self._rx(msg)) + # invalidate checksum + msg[0].data[0] = (msg[0].data[0] + 1) & 0xFF + self.assertFalse(self._rx(msg)) + def test_vehicle_speed_measurements(self): # OVERRIDDEN: 79.1667 is the max speed in m/s self._common_measurement_test(self._speed_msg, 0, 285 / 3.6, 1, From 066d6ad577c5cb9048035b1213861904e4cb3bf6 Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 8 Jan 2026 18:52:14 -0800 Subject: [PATCH 002/117] add error on failed generation (#8) --- opendbc/car/tests/replay/car_replay.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index 721b8f52e7a..ab4be710e79 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -73,7 +73,9 @@ def main(platform=None, segments_per_platform=10, update_refs=False): print(f"{'Generating' if update_refs else 'Testing'} {n_segments} segments for: {', '.join(platforms)}") if update_refs: - run_replay(platforms, segments, ref_path, update=True) + 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}" upload_refs(ref_path, platforms, segments) return 0 From d3015fd48bd768c617adfd1e2312788679957b24 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 14:39:58 -0800 Subject: [PATCH 003/117] Opendbc replay (#129) * add replay POC * compare HEAD to master * split in compare and worker * copy worker.py * cleanup * fix * apply tesla brake PR * fix * fix mutation test * add panda * add timestamps * simplify * add falling edge * add remaining carstate * simple lines * bump * rename * use zst as ref * ref commit * cleanup * formatting * no failing * fix git * clean * use pool.map * fix docker build * clean * Revert "fix docker build" This reverts commit da3577441eaa4b7fdc5763f73e779dd32c03a3c3. * use flat paths * format * use azure test instance * print full diff * no stderr output * use GIT_REF * Revert "apply tesla brake PR" This reverts commit f377e21f3865c574db5d618be20b4efa86fba66f. * allow overwrite blobs * supress stderr * print upload refs * add error on failed generation * Revert "use azure test instance" This reverts commit ba969ccda452a4841934440d3ced22829861f70f. * Revert "fix mutation test" This reverts commit 8f5e01de7743ee38c340df90751cf370634fe2b2. * detect safety changes * use logReader * only use valid platforms * use local database * Revert "use local database" This reverts commit 4ebe9fe0800fb1631efe8672817a8c21b8780b65. * Reapply "use azure test instance" This reverts commit e9968a04e4ddee00b69174fd30ee1cdcdfa506fb. --- opendbc/car/tests/replay/car_replay.py | 8 ++++++-- opendbc/car/tests/replay/worker.py | 12 +++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index ab4be710e79..0b99ac3e34b 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -20,7 +20,9 @@ def get_changed_platforms(cwd, database): brands.add(m.group(1)) if m := re.search(r"opendbc/dbc/(\w+?)_", line): brands.add(m.group(1).lower()) - return [p for p in database if any(b.upper() in p for b in brands)] + if m := re.search(r"opendbc/safety/modes/(\w+?)[_.]", line): + brands.add(m.group(1).lower()) + return [p for p in interfaces if any(b.upper() in p for b in brands) and p in database] def download_refs(ref_path, platforms, segments): @@ -57,12 +59,13 @@ def run_replay(platforms, segments, ref_path, update, workers=8): def main(platform=None, segments_per_platform=10, update_refs=False): + from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database cwd = Path(__file__).resolve().parents[4] ref_path = tempfile.mkdtemp(prefix="car_ref_") database = get_comma_car_segments_database() - platforms = [platform] if platform else get_changed_platforms(cwd, database) + platforms = [platform] if platform and platform in interfaces else get_changed_platforms(cwd, database) if not platforms: print("No platforms detected from changes") @@ -77,6 +80,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False): errors = [e for _, _, _, e in results if e] assert len(errors) == 0, f"Segment failures: {errors}" upload_refs(ref_path, platforms, segments) + print(f"Uploaded {n_segments} refs") return 0 download_refs(ref_path, platforms, segments) diff --git a/opendbc/car/tests/replay/worker.py b/opendbc/car/tests/replay/worker.py index 4f27532fd9a..41ebd7241d9 100644 --- a/opendbc/car/tests/replay/worker.py +++ b/opendbc/car/tests/replay/worker.py @@ -2,6 +2,8 @@ import zstandard as zstd from pathlib import Path +TOLERANCE = 1e-3 + CARSTATE_FIELDS = [ "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", @@ -24,7 +26,7 @@ def get_value(obj, field): def differs(v1, v2): if isinstance(v1, float) and isinstance(v2, float): - return abs(v1 - v2) > 1e-3 + return abs(v1 - v2) > TOLERANCE return v1 != v2 @@ -40,17 +42,17 @@ def load_ref(path): def load_can_messages(seg): from opendbc.car.can_definitions import CanData from openpilot.selfdrive.pandad import can_capnp_to_list - from openpilot.tools.lib.logreader import _LogFileReader + from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.comma_car_segments import get_url parts = seg.split("/") url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) can_msgs = [] - for msg in _LogFileReader(url): + for msg in LogReader(url): if msg.which() == "can": - ts, data = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] - can_msgs.append((ts, [CanData(*x) for x in data])) + can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] + can_msgs.append((can[0], [CanData(*c) for c in can[1]])) return can_msgs From e491031faf3ea7758f6506f295f6ba73f1249c5a Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 14:44:45 -0800 Subject: [PATCH 004/117] cutoff diff --- opendbc/car/tests/replay/car_replay.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index 0b99ac3e34b..07483316dde 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -98,12 +98,14 @@ def main(platform=None, segments_per_platform=10, update_refs=False): for plat, seg, diffs in with_diffs: print(f"\n{plat} - {seg}") by_field = defaultdict(list) - for d in diffs: + for d in diffs[:100]: by_field[d[0]].append(d) for field, fd in sorted(by_field.items()): print(f" {field} (frame: master → PR)") for line in format_diff(fd): print(line) + if len(diffs) > 100: + print(f" ... ({len(diffs) - 100} more)") return 0 From 77a42d391d1f3d6b42a60d7ceb89e833efe39c5f Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 15:04:33 -0800 Subject: [PATCH 005/117] parallel downloads --- opendbc/car/tests/replay/car_replay.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index 07483316dde..c75446c8b80 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -26,13 +26,17 @@ def get_changed_platforms(cwd, database): def download_refs(ref_path, platforms, segments): + from concurrent.futures import ThreadPoolExecutor BASE_URL = "https://elkoled.blob.core.windows.net/openpilotci/" - for platform in platforms: - for seg in segments.get(platform, []): - filename = f"{platform}_{seg.replace('/', '_')}.zst" - resp = requests.get(f"{BASE_URL}car_replay/{filename}") - if resp.status_code == 200: - (Path(ref_path) / filename).write_bytes(resp.content) + def fetch(item): + platform, seg = item + filename = f"{platform}_{seg.replace('/', '_')}.zst" + resp = requests.get(f"{BASE_URL}car_replay/{filename}") + if resp.status_code == 200: + (Path(ref_path) / filename).write_bytes(resp.content) + work = [(p, s) for p in platforms for s in segments.get(p, [])] + with ThreadPoolExecutor(max_workers=8) as pool: + list(pool.map(fetch, work)) def upload_refs(ref_path, platforms, segments): From 3a4bfba8fd4c3d7243f23db218eca4aef0053a5f Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 15:50:18 -0800 Subject: [PATCH 006/117] enable stderr --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8a12a885f5d..f0f3a220bd3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -86,12 +86,12 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test if: github.event_name == 'pull_request' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py 2>/dev/null" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py" - name: Update refs if: github.ref == 'refs/heads/master' env: AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py --update-refs 2>/dev/null" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py --update-refs" # TODO: this needs to move to opendbc test_models: From 1f752b3ecc3b160e8c431f50f9fcb0786e966ab3 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 15:54:46 -0800 Subject: [PATCH 007/117] fix interface --- opendbc/car/tests/replay/car_replay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index c75446c8b80..acb56cea816 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -10,7 +10,7 @@ from pathlib import Path -def get_changed_platforms(cwd, database): +def get_changed_platforms(cwd, database, interfaces): from openpilot.common.utils import run_cmd git_ref = os.environ.get("GIT_REF", "origin/master") changed = run_cmd(["git", "diff", "--name-only", f"{git_ref}...HEAD"], cwd=cwd) @@ -69,7 +69,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False): cwd = Path(__file__).resolve().parents[4] ref_path = tempfile.mkdtemp(prefix="car_ref_") database = get_comma_car_segments_database() - platforms = [platform] if platform and platform in interfaces else get_changed_platforms(cwd, database) + platforms = [platform] if platform and platform in interfaces else get_changed_platforms(cwd, database, interfaces) if not platforms: print("No platforms detected from changes") From 15a040aca1d1efaaecdeb9d1e7da7204049bb2da Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 16:54:34 -0800 Subject: [PATCH 008/117] logprint errors --- opendbc/car/tests/replay/car_replay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index acb56cea816..95f19108df9 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -9,6 +9,7 @@ from concurrent.futures import ProcessPoolExecutor from pathlib import Path +os.environ['LOGPRINT'] = 'ERROR' def get_changed_platforms(cwd, database, interfaces): from openpilot.common.utils import run_cmd From 196eda0be6bc7718f27de61381fa25e39a82ed87 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 17:09:52 -0800 Subject: [PATCH 009/117] add comment to PR --- .github/workflows/tests.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f0f3a220bd3..27a3c72c158 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -86,7 +86,13 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test if: github.event_name == 'pull_request' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py" + run: | + ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py" 2>&1 | tee output.txt + - name: Comment PR + if: github.event_name == 'pull_request' && always() + env: + GH_TOKEN: ${{ github.token }} + run: gh pr comment ${{ github.event.pull_request.number }} --body "$(cat output.txt)" - name: Update refs if: github.ref == 'refs/heads/master' env: From f775a7e1b5262daeb8227620805081dc660407bc Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 17:48:00 -0800 Subject: [PATCH 010/117] fix --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27a3c72c158..61b2b64006f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -92,7 +92,7 @@ jobs: if: github.event_name == 'pull_request' && always() env: GH_TOKEN: ${{ github.token }} - run: gh pr comment ${{ github.event.pull_request.number }} --body "$(cat output.txt)" + run: gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "$(cat output.txt)" - name: Update refs if: github.ref == 'refs/heads/master' env: From 5cdd4c567f0440874a74678ca7cb3b6f909bc0ba Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 18:10:40 -0800 Subject: [PATCH 011/117] FORMAT: lines --- opendbc/car/tests/replay/car_replay.py | 91 +++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index 95f19108df9..38d1569c6b9 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -52,7 +52,96 @@ def upload_refs(ref_path, platforms, segments): def format_diff(diffs): - return [f" {d[1]}: {d[2]} → {d[3]}" for d in diffs] + if not diffs: + return [] + if not all(isinstance(d[2], bool) and isinstance(d[3], bool) for d in diffs): + return [f" frame {d[1]}: {d[2]} -> {d[3]}" for d in diffs[:10]] + + lines = [] + ranges, cur = [], [diffs[0]] + for d in diffs[1:]: + if d[1] <= cur[-1][1] + 15: + cur.append(d) + else: + ranges.append(cur) + cur = [d] + ranges.append(cur) + + for rdiffs in ranges: + t0, t1 = max(0, rdiffs[0][1] - 5), rdiffs[-1][1] + 6 + diff_map = {d[1]: d for d in rdiffs} + + b_vals, m_vals, ts_map = [], [], {} + first, last = rdiffs[0], rdiffs[-1] + if first[2] and not first[3]: + b_st, m_st = False, False + elif not first[2] and first[3]: + b_st, m_st = True, True + else: + b_st, m_st = False, False + + converge_frame = last[1] + 1 + converge_val = last[2] + + for f in range(t0, t1): + if f in diff_map: + b_st, m_st = diff_map[f][2], diff_map[f][3] + if len(diff_map[f]) > 4: + ts_map[f] = diff_map[f][4] + elif f >= converge_frame: + b_st = m_st = converge_val + b_vals.append(b_st) + m_vals.append(m_st) + + ts_start = ts_map.get(t0, rdiffs[0][4] if len(rdiffs[0]) > 4 else 0) + ts_end = ts_map.get(t1 - 1, rdiffs[-1][4] if len(rdiffs[-1]) > 4 else 0) + t0_sec = ts_start / 1e9 + t1_sec = ts_end / 1e9 + + # ms per frame from timestamps + if len(ts_map) >= 2: + ts_vals = sorted(ts_map.items()) + frame_ms = (ts_vals[-1][1] - ts_vals[0][1]) / 1e6 / (ts_vals[-1][0] - ts_vals[0][0]) + else: + frame_ms = 10 + + lines.append(f"\n frames {t0}-{t1-1}") + pad = 12 + init_b = not (first[2] and not first[3]) + init_m = not first[2] and first[3] + for label, vals, init in [("master", b_vals, init_b), ("PR", m_vals, init_m)]: + line = f" {label}:".ljust(pad) + for i, v in enumerate(vals): + pv = vals[i - 1] if i > 0 else init + if v and not pv: + line += "/" + elif not v and pv: + line += "\\" + elif v: + line += "‾" + else: + line += "_" + lines.append(line) + + b_rises = [i for i, v in enumerate(b_vals) if v and (i == 0 or not b_vals[i - 1])] + m_rises = [i for i, v in enumerate(m_vals) if v and (i == 0 or not m_vals[i - 1])] + b_falls = [i for i, v in enumerate(b_vals) if not v and i > 0 and b_vals[i - 1]] + m_falls = [i for i, v in enumerate(m_vals) if not v and i > 0 and m_vals[i - 1]] + + if b_rises and m_rises: + delta = m_rises[0] - b_rises[0] + if delta: + ms = int(abs(delta) * frame_ms) + direction = "lags" if delta > 0 else "leads" + lines.append(" " * pad + f"rise: PR {direction} by {abs(delta)} frames ({ms}ms)") + if b_falls and m_falls: + delta = m_falls[0] - b_falls[0] + if delta: + ms = int(abs(delta) * frame_ms) + direction = "lags" if delta > 0 else "leads" + lines.append(" " * pad + f"fall: PR {direction} by {abs(delta)} frames ({ms}ms)") + + return lines def run_replay(platforms, segments, ref_path, update, workers=8): From 314417db1fb4c01206d8b3880f240a44d70b5a7e Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 19:10:10 -0800 Subject: [PATCH 012/117] FORMAT: plots --- .github/workflows/tests.yml | 6 +- opendbc/car/tests/replay/car_replay.py | 148 ++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 61b2b64006f..a5ef1de83ea 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -86,13 +86,15 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test if: github.event_name == 'pull_request' + env: + AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} run: | - ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py" 2>&1 | tee output.txt + ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py --plot" 2>&1 | tee output.txt - name: Comment PR if: github.event_name == 'pull_request' && always() env: GH_TOKEN: ${{ github.token }} - run: gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "$(cat output.txt)" + run: gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body-file output.txt - name: Update refs if: github.ref == 'refs/heads/master' env: diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index 38d1569c6b9..868323ec6ac 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -9,6 +9,8 @@ from concurrent.futures import ProcessPoolExecutor from pathlib import Path +import matplotlib.pyplot as plt + os.environ['LOGPRINT'] = 'ERROR' def get_changed_platforms(cwd, database, interfaces): @@ -144,6 +146,104 @@ def format_diff(diffs): return lines +def plot_diff(field, diffs, output_dir): + if not diffs: + return None + + diff_map = {d[1]: (d[2], d[3]) for d in diffs} + frames = sorted(diff_map.keys()) + is_bool = all(isinstance(d[2], bool) and isinstance(d[3], bool) for d in diffs) + + # group into edge regions (gaps > 15 frames = new region) + regions, cur = [], [frames[0]] + for f in frames[1:]: + if f - cur[-1] > 15: + regions.append(cur) + cur = [f] + else: + cur.append(f) + regions.append(cur) + + if is_bool: + n_regions = min(len(regions), 6) + fig, axes = plt.subplots(n_regions, 1, figsize=(8, 1.5 * n_regions), squeeze=False) + + for idx in range(n_regions): + region_frames = regions[idx] + ax = axes[idx, 0] + f_min, f_max = min(region_frames) - 5, max(region_frames) + 5 + x_vals = list(range(f_min, f_max + 1)) + + first_m, first_p = diff_map[region_frames[0]] + if first_m and not first_p: + m_state, p_state = False, False + elif not first_m and first_p: + m_state, p_state = True, True + else: + m_state, p_state = False, False + + last_frame = region_frames[-1] + converge_val = diff_map[last_frame][0] + + m_vals, p_vals = [], [] + for x in x_vals: + if x in diff_map: + m_state, p_state = diff_map[x] + elif x > last_frame: + m_state = p_state = converge_val + m_vals.append(int(m_state)) + p_vals.append(int(p_state)) + + ax.step(x_vals, m_vals, where='post', label='master', color='#1f77b4', linewidth=2) + ax.step(x_vals, p_vals, where='post', label='PR', color='#ff7f0e', linewidth=2) + + m_rises = [i for i, v in enumerate(m_vals) if v and (i == 0 or not m_vals[i - 1])] + p_rises = [i for i, v in enumerate(p_vals) if v and (i == 0 or not p_vals[i - 1])] + m_falls = [i for i, v in enumerate(m_vals) if not v and i > 0 and m_vals[i - 1]] + p_falls = [i for i, v in enumerate(p_vals) if not v and i > 0 and p_vals[i - 1]] + + ts_list = [(d[1], d[4]) for d in diffs if d[1] in region_frames and len(d) > 4] + frame_ms = 10 + if len(ts_list) >= 2: + ts_list.sort() + frame_ms = (ts_list[-1][1] - ts_list[0][1]) / 1e6 / (ts_list[-1][0] - ts_list[0][0]) + + annotation = "" + if m_rises and p_rises and (delta := p_rises[0] - m_rises[0]): + annotation = f"PR {'lags' if delta > 0 else 'leads'} by {int(abs(delta) * frame_ms)} ms" + if m_falls and p_falls and (delta := p_falls[0] - m_falls[0]): + annotation = f"PR {'lags' if delta > 0 else 'leads'} by {int(abs(delta) * frame_ms)} ms" + + ax.set_yticks([0, 1]) + ax.set_yticklabels(['F', 'T']) + ax.set_ylim(-0.1, 1.1) + ax.set_xlim(f_min, f_max) + ax.tick_params(axis='x', labelsize=8) + if annotation: + ax.text(0.02, 0.5, annotation, transform=ax.transAxes, fontsize=9, ha='left', va='center') + if idx == 0: + ax.legend(loc='upper right', fontsize=8) + axes[0, 0].set_title(field, fontsize=10) + + else: + fig, ax = plt.subplots(figsize=(8, 2.5)) + m_vals = [diff_map[f][0] for f in frames] + p_vals = [diff_map[f][1] for f in frames] + pad = max(10, (max(frames) - min(frames)) // 10) + ax.plot(frames, m_vals, '-', label='master', color='#1f77b4', linewidth=1.5) + ax.plot(frames, p_vals, '-', label='PR', color='#ff7f0e', linewidth=1.5) + ax.set_xlim(min(frames) - pad, max(frames) + pad) + ax.set_title(field, fontsize=10) + ax.legend(loc='upper right', fontsize=8) + ax.tick_params(axis='x', labelsize=8) + + plt.tight_layout() + plot_path = output_dir / f'{field.replace(".", "_")}.png' + plt.savefig(plot_path, dpi=120, facecolor='white') + plt.close() + return plot_path + + def run_replay(platforms, segments, ref_path, update, workers=8): from opendbc.car.tests.replay.worker import process_segment work = [(platform, seg, ref_path, update) @@ -152,7 +252,7 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) -def main(platform=None, segments_per_platform=10, update_refs=False): +def main(platform=None, segments_per_platform=10, update_refs=False, plot=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database @@ -189,17 +289,52 @@ def main(platform=None, segments_per_platform=10, update_refs=False): for plat, seg, err in errors: print(f"\nERROR {plat} - {seg}: {err}") + plot_dir = Path(tempfile.mkdtemp(prefix="car_replay_plots_")) if plot else None + upload = plot and os.environ.get("AZURE_TOKEN") + if upload: + from azure.storage.blob import BlobClient, ContentSettings + from openpilot.tools.lib.azure_container import get_azure_credential + account_url = "https://elkoled.blob.core.windows.net" + container_name = "openpilotci" + image_urls = [] + + if with_diffs: + print("```") # open code block for ASCII output + for plat, seg, diffs in with_diffs: print(f"\n{plat} - {seg}") by_field = defaultdict(list) - for d in diffs[:100]: + for d in diffs: by_field[d[0]].append(d) + + if plot_dir: + seg_dir = plot_dir / f"{plat}_{seg.replace('/', '_')}" + seg_dir.mkdir(exist_ok=True) + for field, fd in sorted(by_field.items()): print(f" {field} (frame: master → PR)") - for line in format_diff(fd): + for line in format_diff(fd[:100]): print(line) - if len(diffs) > 100: - print(f" ... ({len(diffs) - 100} more)") + if len(fd) > 100: + print(f" ... ({len(fd) - 100} more)") + if plot_dir: + plot_path = plot_diff(field, fd, seg_dir) + if plot_path and upload: + blob_name = f"car_replay_plots/{seg_dir.name}/{plot_path.name}" + blob = BlobClient(account_url, container_name, blob_name, credential=get_azure_credential()) + with open(plot_path, "rb") as f: + blob.upload_blob(f, overwrite=True, content_settings=ContentSettings(content_type="image/png")) + image_urls.append((plat, seg, field, f"{account_url}/{container_name}/{blob_name}")) + + if with_diffs: + print("```") # close code block + + # print images outside code block so markdown renders them + if image_urls: + print("\n**Plots:**") + for plat, seg, field, url in image_urls: + print(f"\n{plat} - {field}") + print(f"![{field}]({url})") return 0 @@ -209,5 +344,6 @@ def main(platform=None, segments_per_platform=10, update_refs=False): parser.add_argument("--platform") parser.add_argument("--segments-per-platform", type=int, default=10) parser.add_argument("--update-refs", action="store_true") + parser.add_argument("--plot", action="store_true") args = parser.parse_args() - sys.exit(main(args.platform, args.segments_per_platform, args.update_refs)) + sys.exit(main(args.platform, args.segments_per_platform, args.update_refs, args.plot)) From 0262b1fad446691d594adef6baf284938d2d115d Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 9 Jan 2026 20:03:58 -0800 Subject: [PATCH 013/117] FORMAT: fix --- opendbc/car/tests/replay/car_replay.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index 868323ec6ac..608c53dd25a 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -113,7 +113,7 @@ def format_diff(diffs): init_m = not first[2] and first[3] for label, vals, init in [("master", b_vals, init_b), ("PR", m_vals, init_m)]: line = f" {label}:".ljust(pad) - for i, v in enumerate(vals): + for i, v in enumerate(vals[:100]): pv = vals[i - 1] if i > 0 else init if v and not pv: line += "/" @@ -231,7 +231,7 @@ def plot_diff(field, diffs, output_dir): p_vals = [diff_map[f][1] for f in frames] pad = max(10, (max(frames) - min(frames)) // 10) ax.plot(frames, m_vals, '-', label='master', color='#1f77b4', linewidth=1.5) - ax.plot(frames, p_vals, '-', label='PR', color='#ff7f0e', linewidth=1.5) + ax.plot(frames, p_vals, '--', label='PR', color='#ff7f0e', linewidth=1.5) ax.set_xlim(min(frames) - pad, max(frames) + pad) ax.set_title(field, fontsize=10) ax.legend(loc='upper right', fontsize=8) @@ -313,10 +313,8 @@ def main(platform=None, segments_per_platform=10, update_refs=False, plot=False) for field, fd in sorted(by_field.items()): print(f" {field} (frame: master → PR)") - for line in format_diff(fd[:100]): + for line in format_diff(fd): print(line) - if len(fd) > 100: - print(f" ... ({len(fd) - 100} more)") if plot_dir: plot_path = plot_diff(field, fd, seg_dir) if plot_path and upload: From d4c10e68f2d0dcfbb6e01e180f5f2dac36686a56 Mon Sep 17 00:00:00 2001 From: elkoled Date: Sun, 11 Jan 2026 14:56:54 -0800 Subject: [PATCH 014/117] modules --- opendbc/car/tests/replay/car_replay.py | 2 +- opendbc/car/tests/replay/compare.py | 26 +++++++ opendbc/car/tests/replay/loader.py | 29 +++++++ opendbc/car/tests/replay/replay.py | 51 ++++++++++++ opendbc/car/tests/replay/worker.py | 103 ------------------------- 5 files changed, 107 insertions(+), 104 deletions(-) create mode 100644 opendbc/car/tests/replay/compare.py create mode 100644 opendbc/car/tests/replay/loader.py create mode 100644 opendbc/car/tests/replay/replay.py delete mode 100644 opendbc/car/tests/replay/worker.py diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index 608c53dd25a..490b8890ec1 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -245,7 +245,7 @@ def plot_diff(field, diffs, output_dir): def run_replay(platforms, segments, ref_path, update, workers=8): - from opendbc.car.tests.replay.worker import process_segment + from opendbc.car.tests.replay.replay import process_segment work = [(platform, seg, ref_path, update) for platform in platforms for seg in segments.get(platform, [])] with ProcessPoolExecutor(max_workers=workers) as pool: diff --git a/opendbc/car/tests/replay/compare.py b/opendbc/car/tests/replay/compare.py new file mode 100644 index 00000000000..6b9993be8df --- /dev/null +++ b/opendbc/car/tests/replay/compare.py @@ -0,0 +1,26 @@ +TOLERANCE = 1e-3 + +CARSTATE_FIELDS = [ + "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", + "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", + "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", + "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", + "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", + "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", + "cruiseState.nonAdaptive", "cruiseState.speedCluster", + "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", + "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", + "canValid", "canTimeout", +] + + +def get_value(obj, field): + for p in field.split("."): + obj = getattr(obj, p, None) + return obj.raw if hasattr(obj, "raw") else obj + + +def differs(v1, v2): + if isinstance(v1, float) and isinstance(v2, float): + return abs(v1 - v2) > TOLERANCE + return v1 != v2 diff --git a/opendbc/car/tests/replay/loader.py b/opendbc/car/tests/replay/loader.py new file mode 100644 index 00000000000..0a6b15d12f6 --- /dev/null +++ b/opendbc/car/tests/replay/loader.py @@ -0,0 +1,29 @@ +import pickle +import zstandard as zstd +from pathlib import Path + + +def save_ref(path, states, timestamps): + data = list(zip(timestamps, states, strict=True)) + Path(path).write_bytes(zstd.compress(pickle.dumps(data))) + + +def load_ref(path): + return pickle.loads(zstd.decompress(Path(path).read_bytes())) + + +def load_can_messages(seg): + from opendbc.car.can_definitions import CanData + from openpilot.selfdrive.pandad import can_capnp_to_list + from openpilot.tools.lib.logreader import LogReader + from openpilot.tools.lib.comma_car_segments import get_url + + parts = seg.split("/") + url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) + + can_msgs = [] + for msg in LogReader(url): + if msg.which() == "can": + can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] + can_msgs.append((can[0], [CanData(*c) for c in can[1]])) + return can_msgs diff --git a/opendbc/car/tests/replay/replay.py b/opendbc/car/tests/replay/replay.py new file mode 100644 index 00000000000..df4d8fc078d --- /dev/null +++ b/opendbc/car/tests/replay/replay.py @@ -0,0 +1,51 @@ +from pathlib import Path + +from opendbc.car.tests.replay.compare import CARSTATE_FIELDS, get_value, differs +from opendbc.car.tests.replay.loader import load_can_messages, load_ref, save_ref + + +def replay_segment(platform, can_msgs): + from opendbc.car import gen_empty_fingerprint, structs + from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces + + fingerprint = gen_empty_fingerprint() + for _, frames in can_msgs[:FRAME_FINGERPRINT]: + for msg in frames: + if msg.src < 64: + fingerprint[msg.src][msg.address] = len(msg.dat) + + CarInterface = interfaces[platform] + car_interface = CarInterface(CarInterface.get_params(platform, fingerprint, [], False, False, False)) + car_control = structs.CarControl().as_reader() + + states = [] + timestamps = [] + for ts, frames in can_msgs: + states.append(car_interface.update([(ts, frames)])) + car_interface.apply(car_control, ts) + timestamps.append(ts) + return states, timestamps + + +def process_segment(args): + platform, seg, ref_path, update = args + try: + can_msgs = load_can_messages(seg) + states, timestamps = replay_segment(platform, can_msgs) + ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" + + if update: + save_ref(ref_file, states, timestamps) + return (platform, seg, [], None) + + if not ref_file.exists(): + return (platform, seg, [], "no ref") + + ref = load_ref(ref_file) + diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) + for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) + for field in CARSTATE_FIELDS + if differs(get_value(state, field), get_value(ref_state, field))] + return (platform, seg, diffs, None) + except Exception as e: + return (platform, seg, [], str(e)) diff --git a/opendbc/car/tests/replay/worker.py b/opendbc/car/tests/replay/worker.py deleted file mode 100644 index 41ebd7241d9..00000000000 --- a/opendbc/car/tests/replay/worker.py +++ /dev/null @@ -1,103 +0,0 @@ -import pickle -import zstandard as zstd -from pathlib import Path - -TOLERANCE = 1e-3 - -CARSTATE_FIELDS = [ - "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", - "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", - "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", - "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", - "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", - "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", - "cruiseState.nonAdaptive", "cruiseState.speedCluster", - "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", - "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", - "canValid", "canTimeout", -] - - -def get_value(obj, field): - for p in field.split("."): - obj = getattr(obj, p, None) - return obj.raw if hasattr(obj, "raw") else obj - - -def differs(v1, v2): - if isinstance(v1, float) and isinstance(v2, float): - return abs(v1 - v2) > TOLERANCE - return v1 != v2 - - -def save_ref(path, states, timestamps): - data = list(zip(timestamps, states, strict=True)) - Path(path).write_bytes(zstd.compress(pickle.dumps(data))) - - -def load_ref(path): - return pickle.loads(zstd.decompress(Path(path).read_bytes())) - - -def load_can_messages(seg): - from opendbc.car.can_definitions import CanData - from openpilot.selfdrive.pandad import can_capnp_to_list - from openpilot.tools.lib.logreader import LogReader - from openpilot.tools.lib.comma_car_segments import get_url - - parts = seg.split("/") - url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) - - can_msgs = [] - for msg in LogReader(url): - if msg.which() == "can": - can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] - can_msgs.append((can[0], [CanData(*c) for c in can[1]])) - return can_msgs - - -def replay_segment(platform, can_msgs): - from opendbc.car import gen_empty_fingerprint, structs - from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces - - fingerprint = gen_empty_fingerprint() - for _, frames in can_msgs[:FRAME_FINGERPRINT]: - for msg in frames: - if msg.src < 64: - fingerprint[msg.src][msg.address] = len(msg.dat) - - CarInterface = interfaces[platform] - car_interface = CarInterface(CarInterface.get_params(platform, fingerprint, [], False, False, False)) - car_control = structs.CarControl().as_reader() - - states = [] - timestamps = [] - for ts, frames in can_msgs: - states.append(car_interface.update([(ts, frames)])) - car_interface.apply(car_control, ts) - timestamps.append(ts) - return states, timestamps - - -def process_segment(args): - platform, seg, ref_path, update = args - try: - can_msgs = load_can_messages(seg) - states, timestamps = replay_segment(platform, can_msgs) - ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" - - if update: - save_ref(ref_file, states, timestamps) - return (platform, seg, [], None) - - if not ref_file.exists(): - return (platform, seg, [], "no ref") - - ref = load_ref(ref_file) - diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) - for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) - for field in CARSTATE_FIELDS - if differs(get_value(state, field), get_value(ref_state, field))] - return (platform, seg, diffs, None) - except Exception as e: - return (platform, seg, [], str(e)) From b7f7cb55b4ef3749af9047f8b4f7e9db23b4e438 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 14:32:22 -0800 Subject: [PATCH 015/117] Revert "use azure test instance" This reverts commit ba969ccda452a4841934440d3ced22829861f70f. --- .github/workflows/tests.yml | 2 -- opendbc/car/tests/replay/car_replay.py | 23 +++++++++-------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a5ef1de83ea..506a2473453 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,8 +80,6 @@ jobs: fetch-depth: 0 - run: cd opendbc_repo && git fetch origin master - uses: ./.github/workflows/setup-with-retry - - name: Build docker - run: selfdrive/test/docker_build.sh base - name: Build run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index 490b8890ec1..bcb427e3290 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -29,28 +29,23 @@ def get_changed_platforms(cwd, database, interfaces): def download_refs(ref_path, platforms, segments): - from concurrent.futures import ThreadPoolExecutor - BASE_URL = "https://elkoled.blob.core.windows.net/openpilotci/" - def fetch(item): - platform, seg = item - filename = f"{platform}_{seg.replace('/', '_')}.zst" - resp = requests.get(f"{BASE_URL}car_replay/{filename}") - if resp.status_code == 200: - (Path(ref_path) / filename).write_bytes(resp.content) - work = [(p, s) for p in platforms for s in segments.get(p, [])] - with ThreadPoolExecutor(max_workers=8) as pool: - list(pool.map(fetch, work)) + from openpilot.tools.lib.openpilotci import BASE_URL + for platform in platforms: + for seg in segments.get(platform, []): + filename = f"{platform}_{seg.replace('/', '_')}.zst" + resp = requests.get(f"{BASE_URL}car_replay/{filename}") + if resp.status_code == 200: + (Path(ref_path) / filename).write_bytes(resp.content) def upload_refs(ref_path, platforms, segments): - from openpilot.tools.lib.azure_container import AzureContainer - container = AzureContainer("elkoled", "openpilotci") + from openpilot.tools.lib.openpilotci import upload_file for platform in platforms: for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" local_path = Path(ref_path) / filename if local_path.exists(): - container.upload_file(str(local_path), f"car_replay/{filename}", overwrite=True) + upload_file(str(local_path), f"car_replay/{filename}") def format_diff(diffs): From f9d23e4f4d93f462e0138637351ea81c13fed5a4 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 14:34:47 -0800 Subject: [PATCH 016/117] build on forks --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 506a2473453..a5ef1de83ea 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,6 +80,8 @@ jobs: fetch-depth: 0 - run: cd opendbc_repo && git fetch origin master - uses: ./.github/workflows/setup-with-retry + - name: Build docker + run: selfdrive/test/docker_build.sh base - name: Build run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test From e82047fdf89873e79a807ba75e324f8349138256 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 14:40:07 -0800 Subject: [PATCH 017/117] clean --- opendbc/car/tests/replay/car_replay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/replay/car_replay.py index bcb427e3290..e7832882b15 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/replay/car_replay.py @@ -21,9 +21,9 @@ def get_changed_platforms(cwd, database, interfaces): for line in changed.splitlines(): if m := re.search(r"opendbc/car/(\w+)/", line): brands.add(m.group(1)) - if m := re.search(r"opendbc/dbc/(\w+?)_", line): + if m := re.search(r"opendbc/dbc/(\w+)_", line): brands.add(m.group(1).lower()) - if m := re.search(r"opendbc/safety/modes/(\w+?)[_.]", line): + if m := re.search(r"opendbc/safety/modes/(\w+)[_.]", line): brands.add(m.group(1).lower()) return [p for p in interfaces if any(b.upper() in p for b in brands) and p in database] From 132af7f50b5b3d6f3e38b5aea6070490b0d7f418 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 14:47:01 -0800 Subject: [PATCH 018/117] rename --- .github/workflows/tests.yml | 8 ++++---- opendbc/car/tests/{replay => diff}/__init__.py | 0 .../car/tests/{replay/car_replay.py => diff/car_diff.py} | 2 +- opendbc/car/tests/{replay => diff}/compare.py | 0 opendbc/car/tests/{replay => diff}/loader.py | 0 opendbc/car/tests/{replay => diff}/replay.py | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) rename opendbc/car/tests/{replay => diff}/__init__.py (100%) rename opendbc/car/tests/{replay/car_replay.py => diff/car_diff.py} (99%) rename opendbc/car/tests/{replay => diff}/compare.py (100%) rename opendbc/car/tests/{replay => diff}/loader.py (100%) rename opendbc/car/tests/{replay => diff}/replay.py (90%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a5ef1de83ea..2f911d7b952 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,8 +60,8 @@ jobs: scons -j8 cd opendbc/safety/tests && ./mutation.sh - car_replay: - name: car replay + car_diff: + name: car diff runs-on: ${{ github.repository == 'commaai/opendbc' && 'namespace-profile-amd64-8x16' || 'ubuntu-latest' }} env: BASE_IMAGE: openpilot-base @@ -89,7 +89,7 @@ jobs: env: AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} run: | - ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py --plot" 2>&1 | tee output.txt + ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" 2>&1 | tee output.txt - name: Comment PR if: github.event_name == 'pull_request' && always() env: @@ -99,7 +99,7 @@ jobs: if: github.ref == 'refs/heads/master' env: AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/replay/car_replay.py --update-refs" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py --update-refs" # TODO: this needs to move to opendbc test_models: diff --git a/opendbc/car/tests/replay/__init__.py b/opendbc/car/tests/diff/__init__.py similarity index 100% rename from opendbc/car/tests/replay/__init__.py rename to opendbc/car/tests/diff/__init__.py diff --git a/opendbc/car/tests/replay/car_replay.py b/opendbc/car/tests/diff/car_diff.py similarity index 99% rename from opendbc/car/tests/replay/car_replay.py rename to opendbc/car/tests/diff/car_diff.py index e7832882b15..471a164cd7b 100644 --- a/opendbc/car/tests/replay/car_replay.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -240,7 +240,7 @@ def plot_diff(field, diffs, output_dir): def run_replay(platforms, segments, ref_path, update, workers=8): - from opendbc.car.tests.replay.replay import process_segment + from opendbc.car.tests.diff.replay import process_segment work = [(platform, seg, ref_path, update) for platform in platforms for seg in segments.get(platform, [])] with ProcessPoolExecutor(max_workers=workers) as pool: diff --git a/opendbc/car/tests/replay/compare.py b/opendbc/car/tests/diff/compare.py similarity index 100% rename from opendbc/car/tests/replay/compare.py rename to opendbc/car/tests/diff/compare.py diff --git a/opendbc/car/tests/replay/loader.py b/opendbc/car/tests/diff/loader.py similarity index 100% rename from opendbc/car/tests/replay/loader.py rename to opendbc/car/tests/diff/loader.py diff --git a/opendbc/car/tests/replay/replay.py b/opendbc/car/tests/diff/replay.py similarity index 90% rename from opendbc/car/tests/replay/replay.py rename to opendbc/car/tests/diff/replay.py index df4d8fc078d..c0a0376955d 100644 --- a/opendbc/car/tests/replay/replay.py +++ b/opendbc/car/tests/diff/replay.py @@ -1,7 +1,7 @@ from pathlib import Path -from opendbc.car.tests.replay.compare import CARSTATE_FIELDS, get_value, differs -from opendbc.car.tests.replay.loader import load_can_messages, load_ref, save_ref +from opendbc.car.tests.diff.compare import CARSTATE_FIELDS, get_value, differs +from opendbc.car.tests.diff.loader import load_can_messages, load_ref, save_ref def replay_segment(platform, can_msgs): From ca35829b19e4ddc77dbab58594d5e9d982db142d Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 15:13:03 -0800 Subject: [PATCH 019/117] replace azure with ci-artifacts --- .github/workflows/tests.yml | 4 ++-- opendbc/car/tests/diff/car_diff.py | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2f911d7b952..d55ba3f5267 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -66,7 +66,7 @@ jobs: env: BASE_IMAGE: openpilot-base GIT_REF: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.before || format('origin/{0}', github.event.repository.default_branch) }} - RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONPATH=/tmp/openpilot -e AZURE_TOKEN=$AZURE_TOKEN -e GIT_REF=$GIT_REF -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/bash -c + RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONPATH=/tmp/openpilot -e GIT_REF -e GITHUB_TOKEN -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/bash -c steps: - uses: actions/checkout@v4 with: @@ -98,7 +98,7 @@ jobs: - name: Update refs if: github.ref == 'refs/heads/master' env: - AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.CI_ARTIFACTS_TOKEN }} run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py --update-refs" # TODO: this needs to move to opendbc diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index 471a164cd7b..de33e9389bc 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -13,6 +13,9 @@ os.environ['LOGPRINT'] = 'ERROR' +DIFF_BUCKET = "car_diff" + + def get_changed_platforms(cwd, database, interfaces): from openpilot.common.utils import run_cmd git_ref = os.environ.get("GIT_REF", "origin/master") @@ -29,23 +32,28 @@ def get_changed_platforms(cwd, database, interfaces): def download_refs(ref_path, platforms, segments): - from openpilot.tools.lib.openpilotci import BASE_URL + from openpilot.tools.lib.github_utils import GithubUtils + gh = GithubUtils(None, None) + base_url = gh.get_bucket_link(DIFF_BUCKET) for platform in platforms: for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" - resp = requests.get(f"{BASE_URL}car_replay/{filename}") + resp = requests.get(f"{base_url}/{filename}") if resp.status_code == 200: (Path(ref_path) / filename).write_bytes(resp.content) def upload_refs(ref_path, platforms, segments): - from openpilot.tools.lib.openpilotci import upload_file + from openpilot.tools.lib.github_utils import GithubUtils + gh = GithubUtils(None, os.environ.get("GITHUB_TOKEN")) + files = [] for platform in platforms: for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" local_path = Path(ref_path) / filename if local_path.exists(): - upload_file(str(local_path), f"car_replay/{filename}") + files.append((filename, str(local_path))) + gh.upload_files(DIFF_BUCKET, files) def format_diff(diffs): From 52740b5f0cef069b458aaf41235117f7819d42e0 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 16:33:40 -0800 Subject: [PATCH 020/117] auto bootstrap --- opendbc/car/tests/diff/car_diff.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index de33e9389bc..299fefe7ed2 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -16,6 +16,11 @@ DIFF_BUCKET = "car_diff" +def bucket_is_empty(): + from openpilot.tools.lib.github_utils import GithubUtils + return requests.head(GithubUtils(None, None).get_bucket_link(DIFF_BUCKET)).status_code != 200 + + def get_changed_platforms(cwd, database, interfaces): from openpilot.common.utils import run_cmd git_ref = os.environ.get("GIT_REF", "origin/master") @@ -262,7 +267,15 @@ def main(platform=None, segments_per_platform=10, update_refs=False, plot=False) cwd = Path(__file__).resolve().parents[4] ref_path = tempfile.mkdtemp(prefix="car_ref_") database = get_comma_car_segments_database() - platforms = [platform] if platform and platform in interfaces else get_changed_platforms(cwd, database, interfaces) + + if update_refs and bucket_is_empty(): + print("Bootstrapping all platforms...") + platforms = [p for p in interfaces if p in database] + elif platform and platform in interfaces: + platforms = [platform] + else: + # auto detect platform changes by default + platforms = get_changed_platforms(cwd, database, interfaces) if not platforms: print("No platforms detected from changes") @@ -342,9 +355,8 @@ def main(platform=None, segments_per_platform=10, update_refs=False, plot=False) if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--platform") - parser.add_argument("--segments-per-platform", type=int, default=10) - parser.add_argument("--update-refs", action="store_true") - parser.add_argument("--plot", action="store_true") + parser.add_argument("--platform", help="run diff on a single platform only") + parser.add_argument("--segments-per-platform", type=int, default=10, help="number of segments to test per platform") + parser.add_argument("--update-refs", action="store_true", help="update refs to the current commit") args = parser.parse_args() sys.exit(main(args.platform, args.segments_per_platform, args.update_refs, args.plot)) From 3ddb39abe536a9e30438cf4b0cbc7481f7588f6e Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 16:41:37 -0800 Subject: [PATCH 021/117] help --- opendbc/car/tests/diff/car_diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index 299fefe7ed2..e0d4f962bde 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -355,8 +355,8 @@ def main(platform=None, segments_per_platform=10, update_refs=False, plot=False) if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--platform", help="run diff on a single platform only") - parser.add_argument("--segments-per-platform", type=int, default=10, help="number of segments to test per platform") - parser.add_argument("--update-refs", action="store_true", help="update refs to the current commit") + 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") args = parser.parse_args() sys.exit(main(args.platform, args.segments_per_platform, args.update_refs, args.plot)) From fc691b500c58e76a41c8f5785b6acf89ff4e1573 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 16:46:12 -0800 Subject: [PATCH 022/117] FORK TEST --- opendbc/car/tests/diff/car_diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index e0d4f962bde..f31018d5e57 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -18,7 +18,7 @@ def bucket_is_empty(): from openpilot.tools.lib.github_utils import GithubUtils - return requests.head(GithubUtils(None, None).get_bucket_link(DIFF_BUCKET)).status_code != 200 + return requests.head(GithubUtils(None, None, "elkoled").get_bucket_link(DIFF_BUCKET)).status_code != 200 def get_changed_platforms(cwd, database, interfaces): @@ -38,7 +38,7 @@ def get_changed_platforms(cwd, database, interfaces): def download_refs(ref_path, platforms, segments): from openpilot.tools.lib.github_utils import GithubUtils - gh = GithubUtils(None, None) + gh = GithubUtils(None, None, "elkoled") base_url = gh.get_bucket_link(DIFF_BUCKET) for platform in platforms: for seg in segments.get(platform, []): @@ -50,7 +50,7 @@ def download_refs(ref_path, platforms, segments): def upload_refs(ref_path, platforms, segments): from openpilot.tools.lib.github_utils import GithubUtils - gh = GithubUtils(None, os.environ.get("GITHUB_TOKEN")) + gh = GithubUtils(None, os.environ.get("GITHUB_TOKEN"), "elkoled") files = [] for platform in platforms: for seg in segments.get(platform, []): From 309e5c946d677c21504c4965c1d12ea4370dfed5 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 16:51:51 -0800 Subject: [PATCH 023/117] Revert "FORMAT: fix" This reverts commit 0262b1fad446691d594adef6baf284938d2d115d. --- opendbc/car/tests/diff/car_diff.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index f31018d5e57..322fd168d11 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -121,7 +121,7 @@ def format_diff(diffs): init_m = not first[2] and first[3] for label, vals, init in [("master", b_vals, init_b), ("PR", m_vals, init_m)]: line = f" {label}:".ljust(pad) - for i, v in enumerate(vals[:100]): + for i, v in enumerate(vals): pv = vals[i - 1] if i > 0 else init if v and not pv: line += "/" @@ -239,7 +239,7 @@ def plot_diff(field, diffs, output_dir): p_vals = [diff_map[f][1] for f in frames] pad = max(10, (max(frames) - min(frames)) // 10) ax.plot(frames, m_vals, '-', label='master', color='#1f77b4', linewidth=1.5) - ax.plot(frames, p_vals, '--', label='PR', color='#ff7f0e', linewidth=1.5) + ax.plot(frames, p_vals, '-', label='PR', color='#ff7f0e', linewidth=1.5) ax.set_xlim(min(frames) - pad, max(frames) + pad) ax.set_title(field, fontsize=10) ax.legend(loc='upper right', fontsize=8) @@ -329,8 +329,10 @@ def main(platform=None, segments_per_platform=10, update_refs=False, plot=False) for field, fd in sorted(by_field.items()): print(f" {field} (frame: master → PR)") - for line in format_diff(fd): + for line in format_diff(fd[:100]): print(line) + if len(fd) > 100: + print(f" ... ({len(fd) - 100} more)") if plot_dir: plot_path = plot_diff(field, fd, seg_dir) if plot_path and upload: From 75ca90203dab6d7c15eb4c9a877684ea505f615d Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 16:52:29 -0800 Subject: [PATCH 024/117] Revert "FORMAT: plots" This reverts commit 314417db1fb4c01206d8b3880f240a44d70b5a7e. --- .github/workflows/tests.yml | 4 +- opendbc/car/tests/diff/car_diff.py | 147 ++--------------------------- 2 files changed, 7 insertions(+), 144 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d55ba3f5267..8e56b02d212 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -86,15 +86,13 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test if: github.event_name == 'pull_request' - env: - AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} run: | ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" 2>&1 | tee output.txt - name: Comment PR if: github.event_name == 'pull_request' && always() env: GH_TOKEN: ${{ github.token }} - run: gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body-file output.txt + run: gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "$(cat output.txt)" - name: Update refs if: github.ref == 'refs/heads/master' env: diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index 322fd168d11..beb34840c69 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -9,8 +9,6 @@ from concurrent.futures import ProcessPoolExecutor from pathlib import Path -import matplotlib.pyplot as plt - os.environ['LOGPRINT'] = 'ERROR' DIFF_BUCKET = "car_diff" @@ -154,104 +152,6 @@ def format_diff(diffs): return lines -def plot_diff(field, diffs, output_dir): - if not diffs: - return None - - diff_map = {d[1]: (d[2], d[3]) for d in diffs} - frames = sorted(diff_map.keys()) - is_bool = all(isinstance(d[2], bool) and isinstance(d[3], bool) for d in diffs) - - # group into edge regions (gaps > 15 frames = new region) - regions, cur = [], [frames[0]] - for f in frames[1:]: - if f - cur[-1] > 15: - regions.append(cur) - cur = [f] - else: - cur.append(f) - regions.append(cur) - - if is_bool: - n_regions = min(len(regions), 6) - fig, axes = plt.subplots(n_regions, 1, figsize=(8, 1.5 * n_regions), squeeze=False) - - for idx in range(n_regions): - region_frames = regions[idx] - ax = axes[idx, 0] - f_min, f_max = min(region_frames) - 5, max(region_frames) + 5 - x_vals = list(range(f_min, f_max + 1)) - - first_m, first_p = diff_map[region_frames[0]] - if first_m and not first_p: - m_state, p_state = False, False - elif not first_m and first_p: - m_state, p_state = True, True - else: - m_state, p_state = False, False - - last_frame = region_frames[-1] - converge_val = diff_map[last_frame][0] - - m_vals, p_vals = [], [] - for x in x_vals: - if x in diff_map: - m_state, p_state = diff_map[x] - elif x > last_frame: - m_state = p_state = converge_val - m_vals.append(int(m_state)) - p_vals.append(int(p_state)) - - ax.step(x_vals, m_vals, where='post', label='master', color='#1f77b4', linewidth=2) - ax.step(x_vals, p_vals, where='post', label='PR', color='#ff7f0e', linewidth=2) - - m_rises = [i for i, v in enumerate(m_vals) if v and (i == 0 or not m_vals[i - 1])] - p_rises = [i for i, v in enumerate(p_vals) if v and (i == 0 or not p_vals[i - 1])] - m_falls = [i for i, v in enumerate(m_vals) if not v and i > 0 and m_vals[i - 1]] - p_falls = [i for i, v in enumerate(p_vals) if not v and i > 0 and p_vals[i - 1]] - - ts_list = [(d[1], d[4]) for d in diffs if d[1] in region_frames and len(d) > 4] - frame_ms = 10 - if len(ts_list) >= 2: - ts_list.sort() - frame_ms = (ts_list[-1][1] - ts_list[0][1]) / 1e6 / (ts_list[-1][0] - ts_list[0][0]) - - annotation = "" - if m_rises and p_rises and (delta := p_rises[0] - m_rises[0]): - annotation = f"PR {'lags' if delta > 0 else 'leads'} by {int(abs(delta) * frame_ms)} ms" - if m_falls and p_falls and (delta := p_falls[0] - m_falls[0]): - annotation = f"PR {'lags' if delta > 0 else 'leads'} by {int(abs(delta) * frame_ms)} ms" - - ax.set_yticks([0, 1]) - ax.set_yticklabels(['F', 'T']) - ax.set_ylim(-0.1, 1.1) - ax.set_xlim(f_min, f_max) - ax.tick_params(axis='x', labelsize=8) - if annotation: - ax.text(0.02, 0.5, annotation, transform=ax.transAxes, fontsize=9, ha='left', va='center') - if idx == 0: - ax.legend(loc='upper right', fontsize=8) - axes[0, 0].set_title(field, fontsize=10) - - else: - fig, ax = plt.subplots(figsize=(8, 2.5)) - m_vals = [diff_map[f][0] for f in frames] - p_vals = [diff_map[f][1] for f in frames] - pad = max(10, (max(frames) - min(frames)) // 10) - ax.plot(frames, m_vals, '-', label='master', color='#1f77b4', linewidth=1.5) - ax.plot(frames, p_vals, '-', label='PR', color='#ff7f0e', linewidth=1.5) - ax.set_xlim(min(frames) - pad, max(frames) + pad) - ax.set_title(field, fontsize=10) - ax.legend(loc='upper right', fontsize=8) - ax.tick_params(axis='x', labelsize=8) - - plt.tight_layout() - plot_path = output_dir / f'{field.replace(".", "_")}.png' - plt.savefig(plot_path, dpi=120, facecolor='white') - plt.close() - return plot_path - - def run_replay(platforms, segments, ref_path, update, workers=8): from opendbc.car.tests.diff.replay import process_segment work = [(platform, seg, ref_path, update) @@ -260,7 +160,7 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) -def main(platform=None, segments_per_platform=10, update_refs=False, plot=False): +def main(platform=None, segments_per_platform=10, update_refs=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database @@ -305,52 +205,17 @@ def main(platform=None, segments_per_platform=10, update_refs=False, plot=False) for plat, seg, err in errors: print(f"\nERROR {plat} - {seg}: {err}") - plot_dir = Path(tempfile.mkdtemp(prefix="car_replay_plots_")) if plot else None - upload = plot and os.environ.get("AZURE_TOKEN") - if upload: - from azure.storage.blob import BlobClient, ContentSettings - from openpilot.tools.lib.azure_container import get_azure_credential - account_url = "https://elkoled.blob.core.windows.net" - container_name = "openpilotci" - image_urls = [] - - if with_diffs: - print("```") # open code block for ASCII output - for plat, seg, diffs in with_diffs: print(f"\n{plat} - {seg}") by_field = defaultdict(list) - for d in diffs: + for d in diffs[:100]: by_field[d[0]].append(d) - - if plot_dir: - seg_dir = plot_dir / f"{plat}_{seg.replace('/', '_')}" - seg_dir.mkdir(exist_ok=True) - for field, fd in sorted(by_field.items()): print(f" {field} (frame: master → PR)") - for line in format_diff(fd[:100]): + for line in format_diff(fd): print(line) - if len(fd) > 100: - print(f" ... ({len(fd) - 100} more)") - if plot_dir: - plot_path = plot_diff(field, fd, seg_dir) - if plot_path and upload: - blob_name = f"car_replay_plots/{seg_dir.name}/{plot_path.name}" - blob = BlobClient(account_url, container_name, blob_name, credential=get_azure_credential()) - with open(plot_path, "rb") as f: - blob.upload_blob(f, overwrite=True, content_settings=ContentSettings(content_type="image/png")) - image_urls.append((plat, seg, field, f"{account_url}/{container_name}/{blob_name}")) - - if with_diffs: - print("```") # close code block - - # print images outside code block so markdown renders them - if image_urls: - print("\n**Plots:**") - for plat, seg, field, url in image_urls: - print(f"\n{plat} - {field}") - print(f"![{field}]({url})") + if len(diffs) > 100: + print(f" ... ({len(diffs) - 100} more)") return 0 @@ -361,4 +226,4 @@ def main(platform=None, segments_per_platform=10, update_refs=False, plot=False) 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") args = parser.parse_args() - sys.exit(main(args.platform, args.segments_per_platform, args.update_refs, args.plot)) + sys.exit(main(args.platform, args.segments_per_platform, args.update_refs)) From c122c4f5517f22c759456ea44a5f0d2ac4159f34 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 16:52:46 -0800 Subject: [PATCH 025/117] Revert "FORMAT: lines" This reverts commit 5cdd4c567f0440874a74678ca7cb3b6f909bc0ba. --- opendbc/car/tests/diff/car_diff.py | 91 +----------------------------- 1 file changed, 1 insertion(+), 90 deletions(-) diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index beb34840c69..2e4f583d791 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -60,96 +60,7 @@ def upload_refs(ref_path, platforms, segments): def format_diff(diffs): - if not diffs: - return [] - if not all(isinstance(d[2], bool) and isinstance(d[3], bool) for d in diffs): - return [f" frame {d[1]}: {d[2]} -> {d[3]}" for d in diffs[:10]] - - lines = [] - ranges, cur = [], [diffs[0]] - for d in diffs[1:]: - if d[1] <= cur[-1][1] + 15: - cur.append(d) - else: - ranges.append(cur) - cur = [d] - ranges.append(cur) - - for rdiffs in ranges: - t0, t1 = max(0, rdiffs[0][1] - 5), rdiffs[-1][1] + 6 - diff_map = {d[1]: d for d in rdiffs} - - b_vals, m_vals, ts_map = [], [], {} - first, last = rdiffs[0], rdiffs[-1] - if first[2] and not first[3]: - b_st, m_st = False, False - elif not first[2] and first[3]: - b_st, m_st = True, True - else: - b_st, m_st = False, False - - converge_frame = last[1] + 1 - converge_val = last[2] - - for f in range(t0, t1): - if f in diff_map: - b_st, m_st = diff_map[f][2], diff_map[f][3] - if len(diff_map[f]) > 4: - ts_map[f] = diff_map[f][4] - elif f >= converge_frame: - b_st = m_st = converge_val - b_vals.append(b_st) - m_vals.append(m_st) - - ts_start = ts_map.get(t0, rdiffs[0][4] if len(rdiffs[0]) > 4 else 0) - ts_end = ts_map.get(t1 - 1, rdiffs[-1][4] if len(rdiffs[-1]) > 4 else 0) - t0_sec = ts_start / 1e9 - t1_sec = ts_end / 1e9 - - # ms per frame from timestamps - if len(ts_map) >= 2: - ts_vals = sorted(ts_map.items()) - frame_ms = (ts_vals[-1][1] - ts_vals[0][1]) / 1e6 / (ts_vals[-1][0] - ts_vals[0][0]) - else: - frame_ms = 10 - - lines.append(f"\n frames {t0}-{t1-1}") - pad = 12 - init_b = not (first[2] and not first[3]) - init_m = not first[2] and first[3] - for label, vals, init in [("master", b_vals, init_b), ("PR", m_vals, init_m)]: - line = f" {label}:".ljust(pad) - for i, v in enumerate(vals): - pv = vals[i - 1] if i > 0 else init - if v and not pv: - line += "/" - elif not v and pv: - line += "\\" - elif v: - line += "‾" - else: - line += "_" - lines.append(line) - - b_rises = [i for i, v in enumerate(b_vals) if v and (i == 0 or not b_vals[i - 1])] - m_rises = [i for i, v in enumerate(m_vals) if v and (i == 0 or not m_vals[i - 1])] - b_falls = [i for i, v in enumerate(b_vals) if not v and i > 0 and b_vals[i - 1]] - m_falls = [i for i, v in enumerate(m_vals) if not v and i > 0 and m_vals[i - 1]] - - if b_rises and m_rises: - delta = m_rises[0] - b_rises[0] - if delta: - ms = int(abs(delta) * frame_ms) - direction = "lags" if delta > 0 else "leads" - lines.append(" " * pad + f"rise: PR {direction} by {abs(delta)} frames ({ms}ms)") - if b_falls and m_falls: - delta = m_falls[0] - b_falls[0] - if delta: - ms = int(abs(delta) * frame_ms) - direction = "lags" if delta > 0 else "leads" - lines.append(" " * pad + f"fall: PR {direction} by {abs(delta)} frames ({ms}ms)") - - return lines + return [f" {d[1]}: {d[2]} → {d[3]}" for d in diffs] def run_replay(platforms, segments, ref_path, update, workers=8): From 968794fbb69d414f6ba604cdcdd08705608b4755 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 16:58:19 -0800 Subject: [PATCH 026/117] clean --- .github/workflows/tests.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8e56b02d212..4d9a6cb5d33 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,6 +65,7 @@ jobs: runs-on: ${{ github.repository == 'commaai/opendbc' && 'namespace-profile-amd64-8x16' || 'ubuntu-latest' }} env: BASE_IMAGE: openpilot-base + BUILD: selfdrive/test/docker_build.sh base GIT_REF: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.before || format('origin/{0}', github.event.repository.default_branch) }} RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONPATH=/tmp/openpilot -e GIT_REF -e GITHUB_TOKEN -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/bash -c steps: @@ -80,11 +81,9 @@ jobs: fetch-depth: 0 - run: cd opendbc_repo && git fetch origin master - uses: ./.github/workflows/setup-with-retry - - name: Build docker - run: selfdrive/test/docker_build.sh base - - name: Build + - name: Build openpilot run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - - name: Test + - name: Test car diff if: github.event_name == 'pull_request' run: | ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" 2>&1 | tee output.txt From 8cd4801438abfe503bfc59310f4b022a838784fa Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 17:23:30 -0800 Subject: [PATCH 027/117] no comment on empty output --- .github/workflows/tests.yml | 5 ++--- opendbc/car/tests/diff/car_diff.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d9a6cb5d33..1f44f87e605 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,13 +85,12 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test car diff if: github.event_name == 'pull_request' - run: | - ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" 2>&1 | tee output.txt + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" - name: Comment PR if: github.event_name == 'pull_request' && always() env: GH_TOKEN: ${{ github.token }} - run: gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "$(cat output.txt)" + run: '[ -f output.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F output.txt || true' - name: Update refs if: github.ref == 'refs/heads/master' env: diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index 2e4f583d791..df86020591b 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -89,7 +89,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False): platforms = get_changed_platforms(cwd, database, interfaces) if not platforms: - print("No platforms detected from changes") + print("No car changes detected", file=sys.stderr) return 0 segments = {p: database.get(p, [])[:segments_per_platform] for p in platforms} From d763d666e207d04a22dfb5f0689526a362ef1547 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 18:43:16 -0800 Subject: [PATCH 028/117] output errors --- opendbc/car/tests/diff/car_diff.py | 6 +++--- opendbc/car/tests/diff/replay.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index df86020591b..0743e0d98d6 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 -import argparse import os +os.environ['LOGPRINT'] = 'ERROR' + +import argparse import re import requests import sys @@ -9,8 +11,6 @@ from concurrent.futures import ProcessPoolExecutor from pathlib import Path -os.environ['LOGPRINT'] = 'ERROR' - DIFF_BUCKET = "car_diff" diff --git a/opendbc/car/tests/diff/replay.py b/opendbc/car/tests/diff/replay.py index c0a0376955d..3c8c83ac784 100644 --- a/opendbc/car/tests/diff/replay.py +++ b/opendbc/car/tests/diff/replay.py @@ -1,3 +1,6 @@ +import os +os.environ['LOGPRINT'] = 'ERROR' + from pathlib import Path from opendbc.car.tests.diff.compare import CARSTATE_FIELDS, get_value, differs From 58f47498c32c5ec148a2130233ec090393a3db83 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 21:10:07 -0800 Subject: [PATCH 029/117] fix pr comment --- .github/workflows/tests.yml | 6 +++--- opendbc/car/tests/diff/car_diff.py | 4 ++-- opendbc/car/tests/diff/replay.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f44f87e605..00eb5779767 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,12 +85,12 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test car diff if: github.event_name == 'pull_request' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" 2>&1 | tee diff.txt - name: Comment PR - if: github.event_name == 'pull_request' && always() + if: github.event_name == 'pull_request' env: GH_TOKEN: ${{ github.token }} - run: '[ -f output.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F output.txt || true' + run: '[ -f diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs if: github.ref == 'refs/heads/master' env: diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index 0743e0d98d6..12f044edc5e 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import os -os.environ['LOGPRINT'] = 'ERROR' +os.environ['LOGPRINT'] = 'CRITICAL' import argparse import re @@ -126,7 +126,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False): for line in format_diff(fd): print(line) if len(diffs) > 100: - print(f" ... ({len(diffs) - 100} more)") + print(f" ... ({len(set(d[1] for d in diffs))} more)") return 0 diff --git a/opendbc/car/tests/diff/replay.py b/opendbc/car/tests/diff/replay.py index 3c8c83ac784..45a79caa887 100644 --- a/opendbc/car/tests/diff/replay.py +++ b/opendbc/car/tests/diff/replay.py @@ -1,5 +1,5 @@ import os -os.environ['LOGPRINT'] = 'ERROR' +os.environ['LOGPRINT'] = 'CRITICAL' from pathlib import Path From 55f2ebd844ec507138f4bcd4e310ad350fca5dbc Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 21:20:47 -0800 Subject: [PATCH 030/117] fix diff more --- opendbc/car/tests/diff/car_diff.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index 12f044edc5e..70b2492534f 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -119,14 +119,14 @@ def main(platform=None, segments_per_platform=10, update_refs=False): for plat, seg, diffs in with_diffs: print(f"\n{plat} - {seg}") by_field = defaultdict(list) - for d in diffs[:100]: + for d in diffs: by_field[d[0]].append(d) for field, fd in sorted(by_field.items()): - print(f" {field} (frame: master → PR)") - for line in format_diff(fd): + print(f" {field} ({len(fd)} diffs)") + for line in format_diff(fd[:10]): print(line) - if len(diffs) > 100: - print(f" ... ({len(set(d[1] for d in diffs))} more)") + if len(fd) > 10: + print(f" ... ({len(fd) - 10} more)") return 0 From 804008f19d92415bbd15fc37b3345f5702e62730 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 21:24:22 -0800 Subject: [PATCH 031/117] increase tolerance --- opendbc/car/tests/diff/compare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/diff/compare.py b/opendbc/car/tests/diff/compare.py index 6b9993be8df..058cba59338 100644 --- a/opendbc/car/tests/diff/compare.py +++ b/opendbc/car/tests/diff/compare.py @@ -1,4 +1,4 @@ -TOLERANCE = 1e-3 +TOLERANCE = 1e-2 CARSTATE_FIELDS = [ "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", From a716f46a6868432da505d674b6c48594e76d618b Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 22:33:19 -0800 Subject: [PATCH 032/117] less files --- opendbc/car/tests/diff/car_diff.py | 4 +-- opendbc/car/tests/diff/compare.py | 26 -------------- opendbc/car/tests/diff/loader.py | 29 ---------------- opendbc/car/tests/diff/replay.py | 56 ++++++++++++++++++++++++++++-- 4 files changed, 55 insertions(+), 60 deletions(-) delete mode 100644 opendbc/car/tests/diff/compare.py delete mode 100644 opendbc/car/tests/diff/loader.py diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index 70b2492534f..b1d2e63d13c 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 -import os -os.environ['LOGPRINT'] = 'CRITICAL' - import argparse +import os import re import requests import sys diff --git a/opendbc/car/tests/diff/compare.py b/opendbc/car/tests/diff/compare.py deleted file mode 100644 index 058cba59338..00000000000 --- a/opendbc/car/tests/diff/compare.py +++ /dev/null @@ -1,26 +0,0 @@ -TOLERANCE = 1e-2 - -CARSTATE_FIELDS = [ - "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", - "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", - "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", - "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", - "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", - "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", - "cruiseState.nonAdaptive", "cruiseState.speedCluster", - "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", - "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", - "canValid", "canTimeout", -] - - -def get_value(obj, field): - for p in field.split("."): - obj = getattr(obj, p, None) - return obj.raw if hasattr(obj, "raw") else obj - - -def differs(v1, v2): - if isinstance(v1, float) and isinstance(v2, float): - return abs(v1 - v2) > TOLERANCE - return v1 != v2 diff --git a/opendbc/car/tests/diff/loader.py b/opendbc/car/tests/diff/loader.py deleted file mode 100644 index 0a6b15d12f6..00000000000 --- a/opendbc/car/tests/diff/loader.py +++ /dev/null @@ -1,29 +0,0 @@ -import pickle -import zstandard as zstd -from pathlib import Path - - -def save_ref(path, states, timestamps): - data = list(zip(timestamps, states, strict=True)) - Path(path).write_bytes(zstd.compress(pickle.dumps(data))) - - -def load_ref(path): - return pickle.loads(zstd.decompress(Path(path).read_bytes())) - - -def load_can_messages(seg): - from opendbc.car.can_definitions import CanData - from openpilot.selfdrive.pandad import can_capnp_to_list - from openpilot.tools.lib.logreader import LogReader - from openpilot.tools.lib.comma_car_segments import get_url - - parts = seg.split("/") - url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) - - can_msgs = [] - for msg in LogReader(url): - if msg.which() == "can": - can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] - can_msgs.append((can[0], [CanData(*c) for c in can[1]])) - return can_msgs diff --git a/opendbc/car/tests/diff/replay.py b/opendbc/car/tests/diff/replay.py index 45a79caa887..2387da89d15 100644 --- a/opendbc/car/tests/diff/replay.py +++ b/opendbc/car/tests/diff/replay.py @@ -1,10 +1,62 @@ import os os.environ['LOGPRINT'] = 'CRITICAL' +import pickle +import zstandard as zstd from pathlib import Path -from opendbc.car.tests.diff.compare import CARSTATE_FIELDS, get_value, differs -from opendbc.car.tests.diff.loader import load_can_messages, load_ref, save_ref +TOLERANCE = 1e-2 + +CARSTATE_FIELDS = [ + "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", + "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", + "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", + "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", + "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", + "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", + "cruiseState.nonAdaptive", "cruiseState.speedCluster", + "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", + "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", + "canValid", "canTimeout", +] + + +def get_value(obj, field): + for p in field.split("."): + obj = getattr(obj, p, None) + return obj.raw if hasattr(obj, "raw") else obj + + +def differs(v1, v2): + if isinstance(v1, float) and isinstance(v2, float): + return abs(v1 - v2) > TOLERANCE + return v1 != v2 + + +def save_ref(path, states, timestamps): + data = list(zip(timestamps, states, strict=True)) + Path(path).write_bytes(zstd.compress(pickle.dumps(data))) + + +def load_ref(path): + return pickle.loads(zstd.decompress(Path(path).read_bytes())) + + +def load_can_messages(seg): + from opendbc.car.can_definitions import CanData + from openpilot.selfdrive.pandad import can_capnp_to_list + from openpilot.tools.lib.logreader import LogReader + from openpilot.tools.lib.comma_car_segments import get_url + + parts = seg.split("/") + url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) + + can_msgs = [] + for msg in LogReader(url): + if msg.which() == "can": + can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] + can_msgs.append((can[0], [CanData(*c) for c in can[1]])) + return can_msgs def replay_segment(platform, can_msgs): From bc8c3027815ef89b7a15e6fd9fca79224745132c Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 22:54:23 -0800 Subject: [PATCH 033/117] one file --- opendbc/car/tests/diff/car_diff.py | 106 ++++++++++++++++++++++++++++- opendbc/car/tests/diff/replay.py | 106 ----------------------------- 2 files changed, 104 insertions(+), 108 deletions(-) delete mode 100644 opendbc/car/tests/diff/replay.py diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index b1d2e63d13c..f68835c4724 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -1,15 +1,118 @@ #!/usr/bin/env python3 -import argparse import os +os.environ['LOGPRINT'] = 'CRITICAL' + +import argparse +import pickle import re import requests import sys import tempfile +import zstandard as zstd from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path DIFF_BUCKET = "car_diff" +TOLERANCE = 1e-2 + +CARSTATE_FIELDS = [ + "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", + "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", + "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", + "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", + "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", + "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", + "cruiseState.nonAdaptive", "cruiseState.speedCluster", + "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", + "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", + "canValid", "canTimeout", +] + + +def get_value(obj, field): + for p in field.split("."): + obj = getattr(obj, p, None) + return obj.raw if hasattr(obj, "raw") else obj + + +def differs(v1, v2): + if isinstance(v1, float) and isinstance(v2, float): + return abs(v1 - v2) > TOLERANCE + return v1 != v2 + + +def save_ref(path, states, timestamps): + data = list(zip(timestamps, states, strict=True)) + Path(path).write_bytes(zstd.compress(pickle.dumps(data))) + + +def load_ref(path): + return pickle.loads(zstd.decompress(Path(path).read_bytes())) + + +def load_can_messages(seg): + from opendbc.car.can_definitions import CanData + from openpilot.selfdrive.pandad import can_capnp_to_list + from openpilot.tools.lib.logreader import LogReader + from openpilot.tools.lib.comma_car_segments import get_url + + parts = seg.split("/") + url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) + + can_msgs = [] + for msg in LogReader(url): + if msg.which() == "can": + can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] + can_msgs.append((can[0], [CanData(*c) for c in can[1]])) + return can_msgs + + +def replay_segment(platform, can_msgs): + from opendbc.car import gen_empty_fingerprint, structs + from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces + + fingerprint = gen_empty_fingerprint() + for _, frames in can_msgs[:FRAME_FINGERPRINT]: + for msg in frames: + if msg.src < 64: + fingerprint[msg.src][msg.address] = len(msg.dat) + + CarInterface = interfaces[platform] + car_interface = CarInterface(CarInterface.get_params(platform, fingerprint, [], False, False, False)) + car_control = structs.CarControl().as_reader() + + states = [] + timestamps = [] + for ts, frames in can_msgs: + states.append(car_interface.update([(ts, frames)])) + car_interface.apply(car_control, ts) + timestamps.append(ts) + return states, timestamps + + +def process_segment(args): + platform, seg, ref_path, update = args + try: + can_msgs = load_can_messages(seg) + states, timestamps = replay_segment(platform, can_msgs) + ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" + + if update: + save_ref(ref_file, states, timestamps) + return (platform, seg, [], None) + + if not ref_file.exists(): + return (platform, seg, [], "no ref") + + ref = load_ref(ref_file) + diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) + for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) + for field in CARSTATE_FIELDS + if differs(get_value(state, field), get_value(ref_state, field))] + return (platform, seg, diffs, None) + except Exception as e: + return (platform, seg, [], str(e)) def bucket_is_empty(): @@ -62,7 +165,6 @@ def format_diff(diffs): def run_replay(platforms, segments, ref_path, update, workers=8): - from opendbc.car.tests.diff.replay import process_segment work = [(platform, seg, ref_path, update) for platform in platforms for seg in segments.get(platform, [])] with ProcessPoolExecutor(max_workers=workers) as pool: diff --git a/opendbc/car/tests/diff/replay.py b/opendbc/car/tests/diff/replay.py deleted file mode 100644 index 2387da89d15..00000000000 --- a/opendbc/car/tests/diff/replay.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -os.environ['LOGPRINT'] = 'CRITICAL' - -import pickle -import zstandard as zstd -from pathlib import Path - -TOLERANCE = 1e-2 - -CARSTATE_FIELDS = [ - "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", - "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", - "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", - "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", - "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", - "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", - "cruiseState.nonAdaptive", "cruiseState.speedCluster", - "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", - "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", - "canValid", "canTimeout", -] - - -def get_value(obj, field): - for p in field.split("."): - obj = getattr(obj, p, None) - return obj.raw if hasattr(obj, "raw") else obj - - -def differs(v1, v2): - if isinstance(v1, float) and isinstance(v2, float): - return abs(v1 - v2) > TOLERANCE - return v1 != v2 - - -def save_ref(path, states, timestamps): - data = list(zip(timestamps, states, strict=True)) - Path(path).write_bytes(zstd.compress(pickle.dumps(data))) - - -def load_ref(path): - return pickle.loads(zstd.decompress(Path(path).read_bytes())) - - -def load_can_messages(seg): - from opendbc.car.can_definitions import CanData - from openpilot.selfdrive.pandad import can_capnp_to_list - from openpilot.tools.lib.logreader import LogReader - from openpilot.tools.lib.comma_car_segments import get_url - - parts = seg.split("/") - url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) - - can_msgs = [] - for msg in LogReader(url): - if msg.which() == "can": - can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] - can_msgs.append((can[0], [CanData(*c) for c in can[1]])) - return can_msgs - - -def replay_segment(platform, can_msgs): - from opendbc.car import gen_empty_fingerprint, structs - from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces - - fingerprint = gen_empty_fingerprint() - for _, frames in can_msgs[:FRAME_FINGERPRINT]: - for msg in frames: - if msg.src < 64: - fingerprint[msg.src][msg.address] = len(msg.dat) - - CarInterface = interfaces[platform] - car_interface = CarInterface(CarInterface.get_params(platform, fingerprint, [], False, False, False)) - car_control = structs.CarControl().as_reader() - - states = [] - timestamps = [] - for ts, frames in can_msgs: - states.append(car_interface.update([(ts, frames)])) - car_interface.apply(car_control, ts) - timestamps.append(ts) - return states, timestamps - - -def process_segment(args): - platform, seg, ref_path, update = args - try: - can_msgs = load_can_messages(seg) - states, timestamps = replay_segment(platform, can_msgs) - ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" - - if update: - save_ref(ref_file, states, timestamps) - return (platform, seg, [], None) - - if not ref_file.exists(): - return (platform, seg, [], "no ref") - - ref = load_ref(ref_file) - diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) - for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) - for field in CARSTATE_FIELDS - if differs(get_value(state, field), get_value(ref_state, field))] - return (platform, seg, diffs, None) - except Exception as e: - return (platform, seg, [], str(e)) From 021eec421acd80866cb3eb05c42327b7d0233fa3 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 16:46:12 -0800 Subject: [PATCH 034/117] Revert "FORK TEST" This reverts commit fc691b500c58e76a41c8f5785b6acf89ff4e1573. --- opendbc/car/tests/diff/car_diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index f68835c4724..a12bf4156e9 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -117,7 +117,7 @@ def process_segment(args): def bucket_is_empty(): from openpilot.tools.lib.github_utils import GithubUtils - return requests.head(GithubUtils(None, None, "elkoled").get_bucket_link(DIFF_BUCKET)).status_code != 200 + return requests.head(GithubUtils(None, None).get_bucket_link(DIFF_BUCKET)).status_code != 200 def get_changed_platforms(cwd, database, interfaces): @@ -137,7 +137,7 @@ def get_changed_platforms(cwd, database, interfaces): def download_refs(ref_path, platforms, segments): from openpilot.tools.lib.github_utils import GithubUtils - gh = GithubUtils(None, None, "elkoled") + gh = GithubUtils(None, None) base_url = gh.get_bucket_link(DIFF_BUCKET) for platform in platforms: for seg in segments.get(platform, []): @@ -149,7 +149,7 @@ def download_refs(ref_path, platforms, segments): def upload_refs(ref_path, platforms, segments): from openpilot.tools.lib.github_utils import GithubUtils - gh = GithubUtils(None, os.environ.get("GITHUB_TOKEN"), "elkoled") + gh = GithubUtils(None, os.environ.get("GITHUB_TOKEN")) files = [] for platform in platforms: for seg in segments.get(platform, []): From 2b51f63645854f24ddeb56a68fe93fea1792f7d7 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 23:25:54 -0800 Subject: [PATCH 035/117] Revert "fix mutation test" This reverts commit 8f5e01de7743ee38c340df90751cf370634fe2b2. --- opendbc/safety/tests/test_tesla.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/opendbc/safety/tests/test_tesla.py b/opendbc/safety/tests/test_tesla.py index bd9c85a3b6c..52390f81aa8 100755 --- a/opendbc/safety/tests/test_tesla.py +++ b/opendbc/safety/tests/test_tesla.py @@ -165,20 +165,6 @@ def test_rx_hook(self): self.assertEqual(should_rx, self._rx(msg)) self.assertEqual(should_rx, self.safety.get_controls_allowed()) - def test_checksum(self): - msgs = [ - self._speed_msg(0), - self._user_gas_msg(0), - self._user_brake_msg(False), - self._pcm_status_msg(False), - self.packer.make_can_msg_safety("UI_warning", 0, {}), - ] - for msg in msgs: - self.assertTrue(self._rx(msg)) - # invalidate checksum - msg[0].data[0] = (msg[0].data[0] + 1) & 0xFF - self.assertFalse(self._rx(msg)) - def test_vehicle_speed_measurements(self): # OVERRIDDEN: 79.1667 is the max speed in m/s self._common_measurement_test(self._speed_msg, 0, 285 / 3.6, 1, From c4dc49a63462609b581d56da450f7840b5b529df Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 11:53:08 -0800 Subject: [PATCH 036/117] fix comment --- .github/workflows/tests.yml | 4 ++-- opendbc/car/tests/diff/car_diff.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 00eb5779767..b6856150fb0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,12 +85,12 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test car diff if: github.event_name == 'pull_request' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" 2>&1 | tee diff.txt + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" | tee diff.txt - name: Comment PR if: github.event_name == 'pull_request' env: GH_TOKEN: ${{ github.token }} - run: '[ -f diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' + run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs if: github.ref == 'refs/heads/master' env: diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index a12bf4156e9..c6935e94c7b 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -137,8 +137,7 @@ def get_changed_platforms(cwd, database, interfaces): def download_refs(ref_path, platforms, segments): from openpilot.tools.lib.github_utils import GithubUtils - gh = GithubUtils(None, None) - base_url = gh.get_bucket_link(DIFF_BUCKET) + base_url = GithubUtils(None, None).get_bucket_link(DIFF_BUCKET) for platform in platforms: for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" @@ -160,10 +159,6 @@ def upload_refs(ref_path, platforms, segments): gh.upload_files(DIFF_BUCKET, files) -def format_diff(diffs): - return [f" {d[1]}: {d[2]} → {d[3]}" for d in diffs] - - def run_replay(platforms, segments, ref_path, update, workers=8): work = [(platform, seg, ref_path, update) for platform in platforms for seg in segments.get(platform, [])] @@ -171,6 +166,10 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) +def format_diff(diffs): + return [f" {d[1]}: {d[2]} -> {d[3]}" for d in diffs] + + def main(platform=None, segments_per_platform=10, update_refs=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database From 11c5d08fbdf21d355c06fa25ddcaea7714c30a39 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 11:53:08 -0800 Subject: [PATCH 037/117] fix comment --- .github/workflows/tests.yml | 4 ++-- opendbc/car/tests/diff/car_diff.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 00eb5779767..b6856150fb0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,12 +85,12 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test car diff if: github.event_name == 'pull_request' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" 2>&1 | tee diff.txt + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" | tee diff.txt - name: Comment PR if: github.event_name == 'pull_request' env: GH_TOKEN: ${{ github.token }} - run: '[ -f diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' + run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs if: github.ref == 'refs/heads/master' env: diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/diff/car_diff.py index f68835c4724..b0e248e9fc8 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/diff/car_diff.py @@ -137,8 +137,7 @@ def get_changed_platforms(cwd, database, interfaces): def download_refs(ref_path, platforms, segments): from openpilot.tools.lib.github_utils import GithubUtils - gh = GithubUtils(None, None, "elkoled") - base_url = gh.get_bucket_link(DIFF_BUCKET) + base_url = GithubUtils(None, None, "elkoled").get_bucket_link(DIFF_BUCKET) for platform in platforms: for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" @@ -160,10 +159,6 @@ def upload_refs(ref_path, platforms, segments): gh.upload_files(DIFF_BUCKET, files) -def format_diff(diffs): - return [f" {d[1]}: {d[2]} → {d[3]}" for d in diffs] - - def run_replay(platforms, segments, ref_path, update, workers=8): work = [(platform, seg, ref_path, update) for platform in platforms for seg in segments.get(platform, [])] @@ -171,6 +166,10 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) +def format_diff(diffs): + return [f" {d[1]}: {d[2]} -> {d[3]}" for d in diffs] + + def main(platform=None, segments_per_platform=10, update_refs=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database From a96a243aaec3eb652a1989ec16301e94b0a400fd Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 14:04:30 -0800 Subject: [PATCH 038/117] move car_diff.py --- .github/workflows/tests.yml | 4 ++-- opendbc/car/tests/{diff => }/car_diff.py | 5 ++--- opendbc/car/tests/diff/__init__.py | 0 3 files changed, 4 insertions(+), 5 deletions(-) rename opendbc/car/tests/{diff => }/car_diff.py (99%) delete mode 100644 opendbc/car/tests/diff/__init__.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b6856150fb0..f6d6b41a8da 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,7 +85,7 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test car diff if: github.event_name == 'pull_request' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" | tee diff.txt + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py" | tee diff.txt - name: Comment PR if: github.event_name == 'pull_request' env: @@ -95,7 +95,7 @@ jobs: if: github.ref == 'refs/heads/master' env: GITHUB_TOKEN: ${{ secrets.CI_ARTIFACTS_TOKEN }} - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py --update-refs" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs" # TODO: this needs to move to opendbc test_models: diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/car_diff.py similarity index 99% rename from opendbc/car/tests/diff/car_diff.py rename to opendbc/car/tests/car_diff.py index b0e248e9fc8..9a49d558428 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -82,8 +82,7 @@ def replay_segment(platform, can_msgs): car_interface = CarInterface(CarInterface.get_params(platform, fingerprint, [], False, False, False)) car_control = structs.CarControl().as_reader() - states = [] - timestamps = [] + states, timestamps = [], [] for ts, frames in can_msgs: states.append(car_interface.update([(ts, frames)])) car_interface.apply(car_control, ts) @@ -174,7 +173,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database - cwd = Path(__file__).resolve().parents[4] + cwd = Path(__file__).resolve().parents[3] ref_path = tempfile.mkdtemp(prefix="car_ref_") database = get_comma_car_segments_database() diff --git a/opendbc/car/tests/diff/__init__.py b/opendbc/car/tests/diff/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From 5eeae96c29ead1c5a6c3bd0c1c812df30c19dc5b Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 12 Jan 2026 23:25:54 -0800 Subject: [PATCH 039/117] Revert "fix mutation test" This reverts commit 8f5e01de7743ee38c340df90751cf370634fe2b2. --- opendbc/safety/tests/test_tesla.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/opendbc/safety/tests/test_tesla.py b/opendbc/safety/tests/test_tesla.py index bd9c85a3b6c..52390f81aa8 100755 --- a/opendbc/safety/tests/test_tesla.py +++ b/opendbc/safety/tests/test_tesla.py @@ -165,20 +165,6 @@ def test_rx_hook(self): self.assertEqual(should_rx, self._rx(msg)) self.assertEqual(should_rx, self.safety.get_controls_allowed()) - def test_checksum(self): - msgs = [ - self._speed_msg(0), - self._user_gas_msg(0), - self._user_brake_msg(False), - self._pcm_status_msg(False), - self.packer.make_can_msg_safety("UI_warning", 0, {}), - ] - for msg in msgs: - self.assertTrue(self._rx(msg)) - # invalidate checksum - msg[0].data[0] = (msg[0].data[0] + 1) & 0xFF - self.assertFalse(self._rx(msg)) - def test_vehicle_speed_measurements(self): # OVERRIDDEN: 79.1667 is the max speed in m/s self._common_measurement_test(self._speed_msg, 0, 285 / 3.6, 1, From 171d2e490867baf13006a8cfdbe834a19814d42e Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 14:04:30 -0800 Subject: [PATCH 040/117] move car_diff.py --- .github/workflows/tests.yml | 4 ++-- opendbc/car/tests/{diff => }/car_diff.py | 5 ++--- opendbc/car/tests/diff/__init__.py | 0 3 files changed, 4 insertions(+), 5 deletions(-) rename opendbc/car/tests/{diff => }/car_diff.py (99%) delete mode 100644 opendbc/car/tests/diff/__init__.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b6856150fb0..f6d6b41a8da 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -85,7 +85,7 @@ jobs: run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" - name: Test car diff if: github.event_name == 'pull_request' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py" | tee diff.txt + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py" | tee diff.txt - name: Comment PR if: github.event_name == 'pull_request' env: @@ -95,7 +95,7 @@ jobs: if: github.ref == 'refs/heads/master' env: GITHUB_TOKEN: ${{ secrets.CI_ARTIFACTS_TOKEN }} - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/diff/car_diff.py --update-refs" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs" # TODO: this needs to move to opendbc test_models: diff --git a/opendbc/car/tests/diff/car_diff.py b/opendbc/car/tests/car_diff.py similarity index 99% rename from opendbc/car/tests/diff/car_diff.py rename to opendbc/car/tests/car_diff.py index c6935e94c7b..849949d3eda 100644 --- a/opendbc/car/tests/diff/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -82,8 +82,7 @@ def replay_segment(platform, can_msgs): car_interface = CarInterface(CarInterface.get_params(platform, fingerprint, [], False, False, False)) car_control = structs.CarControl().as_reader() - states = [] - timestamps = [] + states, timestamps = [], [] for ts, frames in can_msgs: states.append(car_interface.update([(ts, frames)])) car_interface.apply(car_control, ts) @@ -174,7 +173,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database - cwd = Path(__file__).resolve().parents[4] + cwd = Path(__file__).resolve().parents[3] ref_path = tempfile.mkdtemp(prefix="car_ref_") database = get_comma_car_segments_database() diff --git a/opendbc/car/tests/diff/__init__.py b/opendbc/car/tests/diff/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From 8dab7800410f6d622cb87b101fa196edc56489a5 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 15:15:12 -0800 Subject: [PATCH 041/117] inline helpers --- opendbc/car/tests/car_diff.py | 39 ++++++++++++----------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 849949d3eda..69f98ebc44b 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -33,7 +33,7 @@ def get_value(obj, field): for p in field.split("."): obj = getattr(obj, p, None) - return obj.raw if hasattr(obj, "raw") else obj + return getattr(obj, "raw", obj) def differs(v1, v2): @@ -42,15 +42,6 @@ def differs(v1, v2): return v1 != v2 -def save_ref(path, states, timestamps): - data = list(zip(timestamps, states, strict=True)) - Path(path).write_bytes(zstd.compress(pickle.dumps(data))) - - -def load_ref(path): - return pickle.loads(zstd.decompress(Path(path).read_bytes())) - - def load_can_messages(seg): from opendbc.car.can_definitions import CanData from openpilot.selfdrive.pandad import can_capnp_to_list @@ -98,15 +89,16 @@ def process_segment(args): ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" if update: - save_ref(ref_file, states, timestamps) + data = list(zip(timestamps, states)) + ref_file.write_bytes(zstd.compress(pickle.dumps(data))) return (platform, seg, [], None) if not ref_file.exists(): return (platform, seg, [], "no ref") - ref = load_ref(ref_file) + ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) - for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) + for i, ((ts, ref_state), state) in enumerate(zip(ref, states)) for field in CARSTATE_FIELDS if differs(get_value(state, field), get_value(ref_state, field))] return (platform, seg, diffs, None) @@ -124,14 +116,13 @@ def get_changed_platforms(cwd, database, interfaces): git_ref = os.environ.get("GIT_REF", "origin/master") changed = run_cmd(["git", "diff", "--name-only", f"{git_ref}...HEAD"], cwd=cwd) brands = set() + patterns = [r"opendbc/car/(\w+)/", r"opendbc/dbc/(\w+)_", r"opendbc/safety/modes/(\w+)[_.]"] for line in changed.splitlines(): - if m := re.search(r"opendbc/car/(\w+)/", line): - brands.add(m.group(1)) - if m := re.search(r"opendbc/dbc/(\w+)_", line): - brands.add(m.group(1).lower()) - if m := re.search(r"opendbc/safety/modes/(\w+)[_.]", line): - brands.add(m.group(1).lower()) - return [p for p in interfaces if any(b.upper() in p for b in brands) and p in database] + 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, platforms, segments): @@ -165,10 +156,6 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) -def format_diff(diffs): - return [f" {d[1]}: {d[2]} -> {d[3]}" for d in diffs] - - def main(platform=None, segments_per_platform=10, update_refs=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database @@ -221,8 +208,8 @@ def main(platform=None, segments_per_platform=10, update_refs=False): by_field[d[0]].append(d) for field, fd in sorted(by_field.items()): print(f" {field} ({len(fd)} diffs)") - for line in format_diff(fd[:10]): - print(line) + for d in fd[:10]: + print(f" {d[1]}: {d[2]} -> {d[3]}") if len(fd) > 10: print(f" ... ({len(fd) - 10} more)") From 159f8cb86c5c4e000b706a65864cdf0cb552bf9e Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 15:15:12 -0800 Subject: [PATCH 042/117] inline helpers --- opendbc/car/tests/car_diff.py | 39 ++++++++++++----------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 9a49d558428..636e195c9fb 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -33,7 +33,7 @@ def get_value(obj, field): for p in field.split("."): obj = getattr(obj, p, None) - return obj.raw if hasattr(obj, "raw") else obj + return getattr(obj, "raw", obj) def differs(v1, v2): @@ -42,15 +42,6 @@ def differs(v1, v2): return v1 != v2 -def save_ref(path, states, timestamps): - data = list(zip(timestamps, states, strict=True)) - Path(path).write_bytes(zstd.compress(pickle.dumps(data))) - - -def load_ref(path): - return pickle.loads(zstd.decompress(Path(path).read_bytes())) - - def load_can_messages(seg): from opendbc.car.can_definitions import CanData from openpilot.selfdrive.pandad import can_capnp_to_list @@ -98,15 +89,16 @@ def process_segment(args): ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" if update: - save_ref(ref_file, states, timestamps) + data = list(zip(timestamps, states)) + ref_file.write_bytes(zstd.compress(pickle.dumps(data))) return (platform, seg, [], None) if not ref_file.exists(): return (platform, seg, [], "no ref") - ref = load_ref(ref_file) + ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) - for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) + for i, ((ts, ref_state), state) in enumerate(zip(ref, states)) for field in CARSTATE_FIELDS if differs(get_value(state, field), get_value(ref_state, field))] return (platform, seg, diffs, None) @@ -124,14 +116,13 @@ def get_changed_platforms(cwd, database, interfaces): git_ref = os.environ.get("GIT_REF", "origin/master") changed = run_cmd(["git", "diff", "--name-only", f"{git_ref}...HEAD"], cwd=cwd) brands = set() + patterns = [r"opendbc/car/(\w+)/", r"opendbc/dbc/(\w+)_", r"opendbc/safety/modes/(\w+)[_.]"] for line in changed.splitlines(): - if m := re.search(r"opendbc/car/(\w+)/", line): - brands.add(m.group(1)) - if m := re.search(r"opendbc/dbc/(\w+)_", line): - brands.add(m.group(1).lower()) - if m := re.search(r"opendbc/safety/modes/(\w+)[_.]", line): - brands.add(m.group(1).lower()) - return [p for p in interfaces if any(b.upper() in p for b in brands) and p in database] + 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, platforms, segments): @@ -165,10 +156,6 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) -def format_diff(diffs): - return [f" {d[1]}: {d[2]} -> {d[3]}" for d in diffs] - - def main(platform=None, segments_per_platform=10, update_refs=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database @@ -221,8 +208,8 @@ def main(platform=None, segments_per_platform=10, update_refs=False): by_field[d[0]].append(d) for field, fd in sorted(by_field.items()): print(f" {field} ({len(fd)} diffs)") - for line in format_diff(fd[:10]): - print(line) + for d in fd[:10]: + print(f" {d[1]}: {d[2]} -> {d[3]}") if len(fd) > 10: print(f" ... ({len(fd) - 10} more)") From 4da42ac8a233e5930945807c320dddf385cfa6e1 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 15:40:29 -0800 Subject: [PATCH 043/117] fix ruff --- opendbc/car/tests/car_diff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 636e195c9fb..0462827a19b 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -89,7 +89,7 @@ def process_segment(args): ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" if update: - data = list(zip(timestamps, states)) + data = list(zip(timestamps, states, strict=True)) ref_file.write_bytes(zstd.compress(pickle.dumps(data))) return (platform, seg, [], None) @@ -98,7 +98,7 @@ def process_segment(args): ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) - for i, ((ts, ref_state), state) in enumerate(zip(ref, states)) + for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) for field in CARSTATE_FIELDS if differs(get_value(state, field), get_value(ref_state, field))] return (platform, seg, diffs, None) From 7927248c2ec0f09ce7bc3bd7ede1feaf4b94aa6d Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 15:40:29 -0800 Subject: [PATCH 044/117] fix ruff --- opendbc/car/tests/car_diff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 69f98ebc44b..bd455f30688 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -89,7 +89,7 @@ def process_segment(args): ref_file = Path(ref_path) / f"{platform}_{seg.replace('/', '_')}.zst" if update: - data = list(zip(timestamps, states)) + data = list(zip(timestamps, states, strict=True)) ref_file.write_bytes(zstd.compress(pickle.dumps(data))) return (platform, seg, [], None) @@ -98,7 +98,7 @@ def process_segment(args): ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) - for i, ((ts, ref_state), state) in enumerate(zip(ref, states)) + for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) for field in CARSTATE_FIELDS if differs(get_value(state, field), get_value(ref_state, field))] return (platform, seg, diffs, None) From f4f684e10750894017b02855b51751285a195640 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 16:44:38 -0800 Subject: [PATCH 045/117] use all carstate fields --- opendbc/car/tests/car_diff.py | 38 ++++++++--------------------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 0462827a19b..73e36b843d5 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -9,37 +9,14 @@ import sys import tempfile import zstandard as zstd +import dictdiffer from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path DIFF_BUCKET = "car_diff" TOLERANCE = 1e-2 - -CARSTATE_FIELDS = [ - "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", - "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", - "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", - "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", - "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", - "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", - "cruiseState.nonAdaptive", "cruiseState.speedCluster", - "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", - "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", - "canValid", "canTimeout", -] - - -def get_value(obj, field): - for p in field.split("."): - obj = getattr(obj, p, None) - return getattr(obj, "raw", obj) - - -def differs(v1, v2): - if isinstance(v1, float) and isinstance(v2, float): - return abs(v1 - v2) > TOLERANCE - return v1 != v2 +IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"] def load_can_messages(seg): @@ -97,10 +74,11 @@ def process_segment(args): return (platform, seg, [], "no ref") ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) - diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) - for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) - for field in CARSTATE_FIELDS - if differs(get_value(state, field), get_value(ref_state, field))] + diffs = [] + for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)): + for diff in dictdiffer.diff(ref_state.to_dict(), state.to_dict(), ignore=IGNORE_FIELDS, tolerance=TOLERANCE): + if diff[0] == "change": # ignore add/remove from schema changes + diffs.append((str(diff[1]), i, diff[2], ts)) return (platform, seg, diffs, None) except Exception as e: return (platform, seg, [], str(e)) @@ -209,7 +187,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False): for field, fd in sorted(by_field.items()): print(f" {field} ({len(fd)} diffs)") for d in fd[:10]: - print(f" {d[1]}: {d[2]} -> {d[3]}") + print(f" {d[1]}: {d[2][0]} -> {d[2][1]}") if len(fd) > 10: print(f" ... ({len(fd) - 10} more)") From 833ff1e2dc8ae380f0993b2121849c82d9d0e3f5 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 17:08:44 -0800 Subject: [PATCH 046/117] add --all --- opendbc/car/tests/car_diff.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 73e36b843d5..c5059f5df32 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -84,11 +84,6 @@ def process_segment(args): return (platform, seg, [], str(e)) -def bucket_is_empty(): - from openpilot.tools.lib.github_utils import GithubUtils - return requests.head(GithubUtils(None, None, "elkoled").get_bucket_link(DIFF_BUCKET)).status_code != 200 - - def get_changed_platforms(cwd, database, interfaces): from openpilot.common.utils import run_cmd git_ref = os.environ.get("GIT_REF", "origin/master") @@ -134,7 +129,7 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) -def main(platform=None, segments_per_platform=10, update_refs=False): +def main(platform=None, segments_per_platform=10, update_refs=False, all_platforms=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database @@ -142,13 +137,12 @@ def main(platform=None, segments_per_platform=10, update_refs=False): ref_path = tempfile.mkdtemp(prefix="car_ref_") database = get_comma_car_segments_database() - if update_refs and bucket_is_empty(): - print("Bootstrapping all platforms...") + 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: - # auto detect platform changes by default platforms = get_changed_platforms(cwd, database, interfaces) if not platforms: @@ -199,5 +193,6 @@ def main(platform=None, segments_per_platform=10, update_refs=False): 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)) + sys.exit(main(args.platform, args.segments_per_platform, args.update_refs, args.all)) From 4d0ee9afb2f418d0643c62a4b102f21171e23335 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 16:44:38 -0800 Subject: [PATCH 047/117] use all carstate fields --- opendbc/car/tests/car_diff.py | 38 ++++++++--------------------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index bd455f30688..0d19ac68379 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -9,37 +9,14 @@ import sys import tempfile import zstandard as zstd +import dictdiffer from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path DIFF_BUCKET = "car_diff" TOLERANCE = 1e-2 - -CARSTATE_FIELDS = [ - "vEgo", "aEgo", "vEgoRaw", "yawRate", "standstill", - "gasPressed", "brake", "brakePressed", "regenBraking", "parkingBrake", "brakeHoldActive", - "steeringAngleDeg", "steeringAngleOffsetDeg", "steeringRateDeg", "steeringTorque", "steeringTorqueEps", - "steeringPressed", "steerFaultTemporary", "steerFaultPermanent", - "stockAeb", "stockFcw", "stockLkas", "espDisabled", "espActive", "accFaulted", - "cruiseState.enabled", "cruiseState.available", "cruiseState.speed", "cruiseState.standstill", - "cruiseState.nonAdaptive", "cruiseState.speedCluster", - "gearShifter", "leftBlinker", "rightBlinker", "genericToggle", - "doorOpen", "seatbeltUnlatched", "leftBlindspot", "rightBlindspot", - "canValid", "canTimeout", -] - - -def get_value(obj, field): - for p in field.split("."): - obj = getattr(obj, p, None) - return getattr(obj, "raw", obj) - - -def differs(v1, v2): - if isinstance(v1, float) and isinstance(v2, float): - return abs(v1 - v2) > TOLERANCE - return v1 != v2 +IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"] def load_can_messages(seg): @@ -97,10 +74,11 @@ def process_segment(args): return (platform, seg, [], "no ref") ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) - diffs = [(field, i, get_value(ref_state, field), get_value(state, field), ts) - for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)) - for field in CARSTATE_FIELDS - if differs(get_value(state, field), get_value(ref_state, field))] + diffs = [] + for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)): + for diff in dictdiffer.diff(ref_state.to_dict(), state.to_dict(), ignore=IGNORE_FIELDS, tolerance=TOLERANCE): + if diff[0] == "change": # ignore add/remove from schema changes + diffs.append((str(diff[1]), i, diff[2], ts)) return (platform, seg, diffs, None) except Exception as e: return (platform, seg, [], str(e)) @@ -209,7 +187,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False): for field, fd in sorted(by_field.items()): print(f" {field} ({len(fd)} diffs)") for d in fd[:10]: - print(f" {d[1]}: {d[2]} -> {d[3]}") + print(f" {d[1]}: {d[2][0]} -> {d[2][1]}") if len(fd) > 10: print(f" ... ({len(fd) - 10} more)") From 2c2d2903cfed4e3ced00b2034da2ce33a2a78230 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 17:08:44 -0800 Subject: [PATCH 048/117] add --all --- opendbc/car/tests/car_diff.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 0d19ac68379..65f3279a38d 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -84,11 +84,6 @@ def process_segment(args): return (platform, seg, [], str(e)) -def bucket_is_empty(): - from openpilot.tools.lib.github_utils import GithubUtils - return requests.head(GithubUtils(None, None).get_bucket_link(DIFF_BUCKET)).status_code != 200 - - def get_changed_platforms(cwd, database, interfaces): from openpilot.common.utils import run_cmd git_ref = os.environ.get("GIT_REF", "origin/master") @@ -134,7 +129,7 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) -def main(platform=None, segments_per_platform=10, update_refs=False): +def main(platform=None, segments_per_platform=10, update_refs=False, all_platforms=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database @@ -142,13 +137,12 @@ def main(platform=None, segments_per_platform=10, update_refs=False): ref_path = tempfile.mkdtemp(prefix="car_ref_") database = get_comma_car_segments_database() - if update_refs and bucket_is_empty(): - print("Bootstrapping all platforms...") + 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: - # auto detect platform changes by default platforms = get_changed_platforms(cwd, database, interfaces) if not platforms: @@ -199,5 +193,6 @@ def main(platform=None, segments_per_platform=10, update_refs=False): 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)) + sys.exit(main(args.platform, args.segments_per_platform, args.update_refs, args.all)) From a9d19bdc4a43c8e24a1ff9063efaee74f56745aa Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 20:09:20 -0800 Subject: [PATCH 049/117] bootstrap --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f6d6b41a8da..d061c0b77d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -92,10 +92,10 @@ jobs: GH_TOKEN: ${{ github.token }} run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' || github.head_ref == 'car_diff' env: GITHUB_TOKEN: ${{ secrets.CI_ARTIFACTS_TOKEN }} - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs --all" # TODO: this needs to move to opendbc test_models: From c3c2ba41e8d1dbdeb74cef8498caf9fd13325fe3 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 20:09:20 -0800 Subject: [PATCH 050/117] bootstrap --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f6d6b41a8da..69d4cc25523 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -92,10 +92,10 @@ jobs: GH_TOKEN: ${{ github.token }} run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' || github.head_ref == 'opendbc_replay' env: GITHUB_TOKEN: ${{ secrets.CI_ARTIFACTS_TOKEN }} - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs --all" # TODO: this needs to move to opendbc test_models: From 71482ea607d534a1da97cee2a1e99dfb8fa94f5f Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 20:59:06 -0800 Subject: [PATCH 051/117] Revert "bootstrap" This reverts commit c3c2ba41e8d1dbdeb74cef8498caf9fd13325fe3. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 69d4cc25523..f6d6b41a8da 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -92,10 +92,10 @@ jobs: GH_TOKEN: ${{ github.token }} run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs - if: github.ref == 'refs/heads/master' || github.head_ref == 'opendbc_replay' + if: github.ref == 'refs/heads/master' env: GITHUB_TOKEN: ${{ secrets.CI_ARTIFACTS_TOKEN }} - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs --all" + run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs" # TODO: this needs to move to opendbc test_models: From 85e55c0f77a276d9209d5bf1d1470a6c90d497f2 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 21:02:48 -0800 Subject: [PATCH 052/117] add line formatting --- opendbc/car/tests/car_diff.py | 99 +++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 65f3279a38d..63ebc3b58d3 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -129,6 +129,99 @@ def run_replay(platforms, segments, ref_path, update, workers=8): return list(pool.map(process_segment, work)) +def format_diff(diffs): + if not diffs: + return [] + if not all(isinstance(d[2], bool) and isinstance(d[3], bool) for d in diffs): + return [f" frame {d[1]}: {d[2]} -> {d[3]}" for d in diffs[:10]] + + lines = [] + ranges, cur = [], [diffs[0]] + for d in diffs[1:]: + if d[1] <= cur[-1][1] + 15: + cur.append(d) + else: + ranges.append(cur) + cur = [d] + ranges.append(cur) + + for rdiffs in ranges: + t0, t1 = max(0, rdiffs[0][1] - 5), rdiffs[-1][1] + 6 + diff_map = {d[1]: d for d in rdiffs} + + b_vals, m_vals, ts_map = [], [], {} + first, last = rdiffs[0], rdiffs[-1] + if first[2] and not first[3]: + b_st, m_st = False, False + elif not first[2] and first[3]: + b_st, m_st = True, True + else: + b_st, m_st = False, False + + converge_frame = last[1] + 1 + converge_val = last[2] + + for f in range(t0, t1): + if f in diff_map: + b_st, m_st = diff_map[f][2], diff_map[f][3] + if len(diff_map[f]) > 4: + ts_map[f] = diff_map[f][4] + elif f >= converge_frame: + b_st = m_st = converge_val + b_vals.append(b_st) + m_vals.append(m_st) + + ts_start = ts_map.get(t0, rdiffs[0][4] if len(rdiffs[0]) > 4 else 0) + ts_end = ts_map.get(t1 - 1, rdiffs[-1][4] if len(rdiffs[-1]) > 4 else 0) + t0_sec = ts_start / 1e9 + t1_sec = ts_end / 1e9 + + # ms per frame from timestamps + if len(ts_map) >= 2: + ts_vals = sorted(ts_map.items()) + frame_ms = (ts_vals[-1][1] - ts_vals[0][1]) / 1e6 / (ts_vals[-1][0] - ts_vals[0][0]) + else: + frame_ms = 10 + + lines.append(f"\n frames {t0}-{t1-1}") + pad = 12 + init_b = not (first[2] and not first[3]) + init_m = not first[2] and first[3] + for label, vals, init in [("master", b_vals, init_b), ("PR", m_vals, init_m)]: + line = f" {label}:".ljust(pad) + for i, v in enumerate(vals): + pv = vals[i - 1] if i > 0 else init + if v and not pv: + line += "/" + elif not v and pv: + line += "\\" + elif v: + line += "‾" + else: + line += "_" + lines.append(line) + + b_rises = [i for i, v in enumerate(b_vals) if v and (i == 0 or not b_vals[i - 1])] + m_rises = [i for i, v in enumerate(m_vals) if v and (i == 0 or not m_vals[i - 1])] + b_falls = [i for i, v in enumerate(b_vals) if not v and i > 0 and b_vals[i - 1]] + m_falls = [i for i, v in enumerate(m_vals) if not v and i > 0 and m_vals[i - 1]] + + if b_rises and m_rises: + delta = m_rises[0] - b_rises[0] + if delta: + ms = int(abs(delta) * frame_ms) + direction = "lags" if delta > 0 else "leads" + lines.append(" " * pad + f"rise: PR {direction} by {abs(delta)} frames ({ms}ms)") + if b_falls and m_falls: + delta = m_falls[0] - b_falls[0] + if delta: + ms = int(abs(delta) * frame_ms) + direction = "lags" if delta > 0 else "leads" + lines.append(" " * pad + f"fall: PR {direction} by {abs(delta)} frames ({ms}ms)") + + return lines + + def main(platform=None, segments_per_platform=10, update_refs=False, all_platforms=False): from opendbc.car.car_helpers import interfaces from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database @@ -180,10 +273,8 @@ def main(platform=None, segments_per_platform=10, update_refs=False, all_platfor by_field[d[0]].append(d) for field, fd in sorted(by_field.items()): print(f" {field} ({len(fd)} diffs)") - for d in fd[:10]: - print(f" {d[1]}: {d[2][0]} -> {d[2][1]}") - if len(fd) > 10: - print(f" ... ({len(fd) - 10} more)") + for line in format_diff(fd): + print(line) return 0 From e32715bc470c07005a0e3272b9247b3ee9729699 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 21:57:53 -0800 Subject: [PATCH 053/117] commit workflow --- .github/workflows/tests.yml | 21 +++++++++++++++++++-- opendbc/car/tests/car_diff.py | 22 ++++++---------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f6d6b41a8da..560b4d9b7fd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -93,9 +93,26 @@ jobs: run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs if: github.ref == 'refs/heads/master' - env: - GITHUB_TOKEN: ${{ secrets.CI_ARTIFACTS_TOKEN }} run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs" + - name: Checkout ci-artifacts + if: github.ref == 'refs/heads/master' + uses: actions/checkout@v4 + with: + repository: commaai/ci-artifacts + ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} + path: ${{ github.workspace }}/ci-artifacts + - name: Push refs + if: github.ref == 'refs/heads/master' + working-directory: ${{ github.workspace }}/ci-artifacts + run: | + git config user.name "GitHub Actions Bot" + git config user.email "<>" + git fetch origin car_diff + git checkout car_diff + cp ${{ github.workspace }}/opendbc_repo/car_diff/*.zst . + git add *.zst + git commit -m "car_diff refs for ${{ github.sha }}" || echo "No changes to commit" + git push origin car_diff # TODO: this needs to move to opendbc test_models: diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 63ebc3b58d3..e3cd73864ee 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -99,8 +99,7 @@ def get_changed_platforms(cwd, database, interfaces): def download_refs(ref_path, platforms, segments): - from openpilot.tools.lib.github_utils import GithubUtils - base_url = GithubUtils(None, None).get_bucket_link(DIFF_BUCKET) + base_url = f"https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/{DIFF_BUCKET}" for platform in platforms: for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" @@ -109,17 +108,6 @@ def download_refs(ref_path, platforms, segments): (Path(ref_path) / filename).write_bytes(resp.content) -def upload_refs(ref_path, platforms, segments): - from openpilot.tools.lib.github_utils import GithubUtils - gh = GithubUtils(None, os.environ.get("GITHUB_TOKEN")) - files = [] - for platform in platforms: - for seg in segments.get(platform, []): - filename = f"{platform}_{seg.replace('/', '_')}.zst" - local_path = Path(ref_path) / filename - if local_path.exists(): - files.append((filename, str(local_path))) - gh.upload_files(DIFF_BUCKET, files) def run_replay(platforms, segments, ref_path, update, workers=8): @@ -227,7 +215,10 @@ def main(platform=None, segments_per_platform=10, update_refs=False, all_platfor from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database cwd = Path(__file__).resolve().parents[3] - ref_path = tempfile.mkdtemp(prefix="car_ref_") + 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: @@ -250,8 +241,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False, all_platfor 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}" - upload_refs(ref_path, platforms, segments) - print(f"Uploaded {n_segments} refs") + print(f"Generated {n_segments} refs to {ref_path}") return 0 download_refs(ref_path, platforms, segments) From 3b14d64b53935adc3c037af7c2e2aa517a3dcf2e Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 22:09:29 -0800 Subject: [PATCH 054/117] adjust formatting --- opendbc/car/tests/car_diff.py | 39 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index e3cd73864ee..38765e63518 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -138,16 +138,10 @@ def format_diff(diffs): diff_map = {d[1]: d for d in rdiffs} b_vals, m_vals, ts_map = [], [], {} - first, last = rdiffs[0], rdiffs[-1] - if first[2] and not first[3]: - b_st, m_st = False, False - elif not first[2] and first[3]: - b_st, m_st = True, True - else: - b_st, m_st = False, False - + last = rdiffs[-1] converge_frame = last[1] + 1 converge_val = last[2] + b_st = m_st = not converge_val # before divergence, both had opposite of converge value for f in range(t0, t1): if f in diff_map: @@ -173,9 +167,9 @@ def format_diff(diffs): lines.append(f"\n frames {t0}-{t1-1}") pad = 12 - init_b = not (first[2] and not first[3]) - init_m = not first[2] and first[3] - for label, vals, init in [("master", b_vals, init_b), ("PR", m_vals, init_m)]: + max_width = 60 + init_val = not converge_val + for label, vals, init in [("master", b_vals, init_val), ("PR", m_vals, init_val)]: line = f" {label}:".ljust(pad) for i, v in enumerate(vals): pv = vals[i - 1] if i > 0 else init @@ -187,6 +181,8 @@ def format_diff(diffs): line += "‾" else: line += "_" + if len(line) > pad + max_width: + line = line[:pad + max_width] + "..." lines.append(line) b_rises = [i for i, v in enumerate(b_vals) if v and (i == 0 or not b_vals[i - 1])] @@ -256,15 +252,18 @@ def main(platform=None, segments_per_platform=10, update_refs=False, all_platfor for plat, seg, err in errors: print(f"\nERROR {plat} - {seg}: {err}") - for plat, seg, diffs 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" {field} ({len(fd)} diffs)") - for line in format_diff(fd): - print(line) + if with_diffs: + print("```") + for plat, seg, diffs 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" {field} ({len(fd)} diffs)") + for line in format_diff(fd): + print(line) + print("```") return 0 From 644054cba384046f54474518bb5696b349dd9ef4 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 22:17:22 -0800 Subject: [PATCH 055/117] return err --- opendbc/car/tests/car_diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 38765e63518..7fac487037d 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -265,7 +265,7 @@ def main(platform=None, segments_per_platform=10, update_refs=False, all_platfor print(line) print("```") - return 0 + return 1 if errors else 0 if __name__ == "__main__": From 0a84552c4793d86eed91b459a4788693543e7e57 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 22:21:48 -0800 Subject: [PATCH 056/117] subprocess --- opendbc/car/tests/car_diff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 7fac487037d..8cb060a0d42 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -6,6 +6,7 @@ import pickle import re import requests +import subprocess import sys import tempfile import zstandard as zstd @@ -85,9 +86,8 @@ def process_segment(args): def get_changed_platforms(cwd, database, interfaces): - from openpilot.common.utils import run_cmd git_ref = os.environ.get("GIT_REF", "origin/master") - changed = run_cmd(["git", "diff", "--name-only", f"{git_ref}...HEAD"], cwd=cwd) + changed = subprocess.check_output(["git", "diff", "--name-only", f"{git_ref}...HEAD"], cwd=cwd, encoding='utf8').strip() brands = set() patterns = [r"opendbc/car/(\w+)/", r"opendbc/dbc/(\w+)_", r"opendbc/safety/modes/(\w+)[_.]"] for line in changed.splitlines(): From 48297232c020ebb73bdbeb1b5570301bf2531920 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 22:36:41 -0800 Subject: [PATCH 057/117] comma_car_segments --- opendbc/car/tests/car_diff.py | 74 ++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 8cb060a0d42..be66678c7ca 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -19,12 +19,79 @@ TOLERANCE = 1e-2 IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"] +# commaCarSegments helpers +COMMA_CAR_SEGMENTS_REPO = os.environ.get("COMMA_CAR_SEGMENTS_REPO", "https://huggingface.co/datasets/commaai/commaCarSegments") +COMMA_CAR_SEGMENTS_BRANCH = os.environ.get("COMMA_CAR_SEGMENTS_BRANCH", "main") +COMMA_CAR_SEGMENTS_LFS_INSTANCE = os.environ.get("COMMA_CAR_SEGMENTS_LFS_INSTANCE", COMMA_CAR_SEGMENTS_REPO) + + +def get_repo_raw_url(path): + if "huggingface" in COMMA_CAR_SEGMENTS_REPO: + return f"{COMMA_CAR_SEGMENTS_REPO}/raw/{COMMA_CAR_SEGMENTS_BRANCH}/{path}" + + +def parse_lfs_pointer(text): + header, lfs_version = text.splitlines()[0].split(" ") + assert header == "version" + assert lfs_version == "https://git-lfs.github.com/spec/v1" + + header, oid_raw = text.splitlines()[1].split(" ") + assert header == "oid" + header, oid = oid_raw.split(":") + assert header == "sha256" + + header, size = text.splitlines()[2].split(" ") + assert header == "size" + + return oid, size + + +def get_lfs_file_url(oid, size): + data = { + "operation": "download", + "transfers": ["basic"], + "objects": [{"oid": oid, "size": int(size)}], + "hash_algo": "sha256" + } + headers = { + "Accept": "application/vnd.git-lfs+json", + "Content-Type": "application/vnd.git-lfs+json" + } + response = requests.post(f"{COMMA_CAR_SEGMENTS_LFS_INSTANCE}.git/info/lfs/objects/batch", json=data, headers=headers) + assert response.ok + obj = response.json()["objects"][0] + assert "error" not in obj, obj + return obj["actions"]["download"]["href"] + + +def get_repo_url(path): + response = requests.head(get_repo_raw_url(path)) + if "text/plain" in response.headers.get("content-type"): + response = requests.get(get_repo_raw_url(path)) + assert response.status_code == 200 + oid, size = parse_lfs_pointer(response.text) + return get_lfs_file_url(oid, size) + else: + return get_repo_raw_url(path) + + +def get_url(route, segment, file="rlog.zst"): + return get_repo_url(f"segments/{route.replace('|', '/')}/{segment}/{file}") + + +def get_comma_car_segments_database(): + from opendbc.car.fingerprints import MIGRATION + database = requests.get(get_repo_raw_url("database.json")).json() + ret = {} + for platform in database: + ret[MIGRATION.get(platform, platform)] = [s.rstrip('/s') for s in database[platform]] + return ret + def load_can_messages(seg): from opendbc.car.can_definitions import CanData from openpilot.selfdrive.pandad import can_capnp_to_list from openpilot.tools.lib.logreader import LogReader - from openpilot.tools.lib.comma_car_segments import get_url parts = seg.split("/") url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) @@ -108,8 +175,6 @@ def download_refs(ref_path, platforms, segments): (Path(ref_path) / filename).write_bytes(resp.content) - - def run_replay(platforms, segments, ref_path, update, workers=8): work = [(platform, seg, ref_path, update) for platform in platforms for seg in segments.get(platform, [])] @@ -141,7 +206,7 @@ def format_diff(diffs): last = rdiffs[-1] converge_frame = last[1] + 1 converge_val = last[2] - b_st = m_st = not converge_val # before divergence, both had opposite of converge value + b_st = m_st = not converge_val for f in range(t0, t1): if f in diff_map: @@ -208,7 +273,6 @@ def format_diff(diffs): def main(platform=None, segments_per_platform=10, update_refs=False, all_platforms=False): from opendbc.car.car_helpers import interfaces - from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database cwd = Path(__file__).resolve().parents[3] ref_path = cwd / DIFF_BUCKET From 98cdbca2836f4869a660f747917786d19ece72b2 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 23:03:15 -0800 Subject: [PATCH 058/117] dead code --- opendbc/car/tests/car_diff.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index be66678c7ca..ddbdbfbb957 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -218,11 +218,6 @@ def format_diff(diffs): b_vals.append(b_st) m_vals.append(m_st) - ts_start = ts_map.get(t0, rdiffs[0][4] if len(rdiffs[0]) > 4 else 0) - ts_end = ts_map.get(t1 - 1, rdiffs[-1][4] if len(rdiffs[-1]) > 4 else 0) - t0_sec = ts_start / 1e9 - t1_sec = ts_end / 1e9 - # ms per frame from timestamps if len(ts_map) >= 2: ts_vals = sorted(ts_map.items()) From 078c0e7a071ddaf83ccf5e3bff491daba98c376f Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 23:06:16 -0800 Subject: [PATCH 059/117] minimal logreader --- opendbc/car/tests/car_diff.py | 26 ++++++++++++++++++++------ opendbc/car/tests/rlog.capnp | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 opendbc/car/tests/rlog.capnp diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index ddbdbfbb957..b925fdf35a4 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -88,19 +88,33 @@ def get_comma_car_segments_database(): return ret +def logreader_from_url(url): + import capnp + + response = requests.get(url) + assert response.status_code == 200, f"Failed to download {url}: {response.status_code}" + + data = response.content + if data.startswith(b'\x28\xB5\x2F\xFD'): # zstd magic + data = zstd.decompress(data) + + rlog = capnp.load(str(Path(__file__).parent / "rlog.capnp")) + return rlog.Event.read_multiple_bytes(data) + + def load_can_messages(seg): from opendbc.car.can_definitions import CanData - from openpilot.selfdrive.pandad import can_capnp_to_list - from openpilot.tools.lib.logreader import LogReader parts = seg.split("/") url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) can_msgs = [] - for msg in LogReader(url): - if msg.which() == "can": - can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] - can_msgs.append((can[0], [CanData(*c) for c in can[1]])) + for evt in logreader_from_url(url): + try: + if evt.which() == "can": + can_msgs.append((evt.logMonoTime, [CanData(c.address, c.dat, c.src) for c in evt.can])) + except Exception: + pass return can_msgs diff --git a/opendbc/car/tests/rlog.capnp b/opendbc/car/tests/rlog.capnp new file mode 100644 index 00000000000..ee0e3a4825a --- /dev/null +++ b/opendbc/car/tests/rlog.capnp @@ -0,0 +1,23 @@ +@0xe3aae94257cf750f; + +# Minimal schema for parsing rlog CAN messages +# Subset of cereal/log.capnp + +struct CanData { + address @0 :UInt32; + busTimeDEPRECATED @1 :UInt16; + dat @2 :Data; + src @3 :UInt8; +} + +struct Event { + logMonoTime @0 :UInt64; + + union { + initData @1 :Void; + frame @2 :Void; + gpsNMEA @3 :Void; + sensorEventDEPRECATED @4 :Void; + can @5 :List(CanData); + } +} From 968aa55a9d020815171fcfd0cc3e00964dfdeb18 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 23:14:41 -0800 Subject: [PATCH 060/117] remove openpilot --- .github/workflows/tests.yml | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 560b4d9b7fd..d83e74d0fe8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,28 +64,19 @@ jobs: name: car diff runs-on: ${{ github.repository == 'commaai/opendbc' && 'namespace-profile-amd64-8x16' || 'ubuntu-latest' }} env: - BASE_IMAGE: openpilot-base - BUILD: selfdrive/test/docker_build.sh base GIT_REF: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.before || format('origin/{0}', github.event.repository.default_branch) }} - RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONPATH=/tmp/openpilot -e GIT_REF -e GITHUB_TOKEN -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/bash -c steps: - uses: actions/checkout@v4 with: - repository: 'commaai/openpilot' - ref: 'master' - submodules: true - - run: rm -rf opendbc_repo/ - - uses: actions/checkout@v4 - with: - path: opendbc_repo fetch-depth: 0 - - run: cd opendbc_repo && git fetch origin master - - uses: ./.github/workflows/setup-with-retry - - name: Build openpilot - run: ${{ env.RUN }} "scons -j$(nproc) selfdrive/pandad/ opendbc_repo/opendbc/car opendbc_repo/opendbc/dbc" + - uses: ./.github/workflows/cache + - name: Build opendbc + run: | + source setup.sh + scons -j8 - name: Test car diff if: github.event_name == 'pull_request' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py" | tee diff.txt + run: python opendbc/car/tests/car_diff.py | tee diff.txt - name: Comment PR if: github.event_name == 'pull_request' env: @@ -93,7 +84,7 @@ jobs: run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs if: github.ref == 'refs/heads/master' - run: ${{ env.RUN }} "git config --global --add safe.directory '*' && python opendbc_repo/opendbc/car/tests/car_diff.py --update-refs" + run: python opendbc/car/tests/car_diff.py --update-refs - name: Checkout ci-artifacts if: github.ref == 'refs/heads/master' uses: actions/checkout@v4 @@ -109,7 +100,7 @@ jobs: git config user.email "<>" git fetch origin car_diff git checkout car_diff - cp ${{ github.workspace }}/opendbc_repo/car_diff/*.zst . + cp ${{ github.workspace }}/car_diff/*.zst . git add *.zst git commit -m "car_diff refs for ${{ github.sha }}" || echo "No changes to commit" git push origin car_diff From 598c28a6a5a3e25afc54f75ba0766a523adf8c8b Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 23:19:00 -0800 Subject: [PATCH 061/117] add deps --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 1a0ace169d8..ac5ed920b10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,11 @@ docs = [ "Jinja2", "natsort", ] +car_diff = [ + "requests", + "zstandard", + "dictdiffer", +] examples = [ "inputs", "matplotlib", From fa57731d293694321a328fcd99b3e5be7dac1549 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 23:25:59 -0800 Subject: [PATCH 062/117] remove all deps --- opendbc/car/tests/car_diff.py | 100 +++++++++++++++++++++++++--------- pyproject.toml | 5 -- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index b925fdf35a4..1703288bda0 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -3,14 +3,14 @@ os.environ['LOGPRINT'] = 'CRITICAL' import argparse +import json import pickle import re -import requests import subprocess import sys import tempfile -import zstandard as zstd -import dictdiffer +import urllib.error +import urllib.request from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path @@ -19,12 +19,57 @@ TOLERANCE = 1e-2 IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"] -# commaCarSegments helpers COMMA_CAR_SEGMENTS_REPO = os.environ.get("COMMA_CAR_SEGMENTS_REPO", "https://huggingface.co/datasets/commaai/commaCarSegments") COMMA_CAR_SEGMENTS_BRANCH = os.environ.get("COMMA_CAR_SEGMENTS_BRANCH", "main") COMMA_CAR_SEGMENTS_LFS_INSTANCE = os.environ.get("COMMA_CAR_SEGMENTS_LFS_INSTANCE", COMMA_CAR_SEGMENTS_REPO) +def http_get(url, headers=None): + req = urllib.request.Request(url, headers=headers or {}) + with urllib.request.urlopen(req) as resp: + return resp.read(), resp.status, dict(resp.headers) + + +def http_post_json(url, data, headers=None): + req = urllib.request.Request(url, data=json.dumps(data).encode(), headers=headers or {}, method='POST') + with urllib.request.urlopen(req) as resp: + return json.load(resp), resp.status + + +def http_head(url): + req = urllib.request.Request(url, method='HEAD') + with urllib.request.urlopen(req) as resp: + return resp.status, dict(resp.headers) + + +def zstd_decompress(data): + proc = subprocess.run(['zstd', '-d'], input=data, capture_output=True, check=True) + return proc.stdout + + +def zstd_compress(data): + proc = subprocess.run(['zstd', '-c'], input=data, capture_output=True, check=True) + return proc.stdout + + +def dict_diff(d1, d2, path="", ignore=None, tolerance=0): + ignore = ignore or [] + diffs = [] + for key in set(list(d1.keys()) + list(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 get_repo_raw_url(path): if "huggingface" in COMMA_CAR_SEGMENTS_REPO: return f"{COMMA_CAR_SEGMENTS_REPO}/raw/{COMMA_CAR_SEGMENTS_BRANCH}/{path}" @@ -57,19 +102,19 @@ def get_lfs_file_url(oid, size): "Accept": "application/vnd.git-lfs+json", "Content-Type": "application/vnd.git-lfs+json" } - response = requests.post(f"{COMMA_CAR_SEGMENTS_LFS_INSTANCE}.git/info/lfs/objects/batch", json=data, headers=headers) - assert response.ok - obj = response.json()["objects"][0] + resp, status = http_post_json(f"{COMMA_CAR_SEGMENTS_LFS_INSTANCE}.git/info/lfs/objects/batch", data, headers) + assert status == 200 + obj = resp["objects"][0] assert "error" not in obj, obj return obj["actions"]["download"]["href"] def get_repo_url(path): - response = requests.head(get_repo_raw_url(path)) - if "text/plain" in response.headers.get("content-type"): - response = requests.get(get_repo_raw_url(path)) - assert response.status_code == 200 - oid, size = parse_lfs_pointer(response.text) + status, headers = http_head(get_repo_raw_url(path)) + if "text/plain" in headers.get("Content-Type", ""): + content, status, _ = http_get(get_repo_raw_url(path)) + assert status == 200 + oid, size = parse_lfs_pointer(content.decode()) return get_lfs_file_url(oid, size) else: return get_repo_raw_url(path) @@ -81,7 +126,9 @@ def get_url(route, segment, file="rlog.zst"): def get_comma_car_segments_database(): from opendbc.car.fingerprints import MIGRATION - database = requests.get(get_repo_raw_url("database.json")).json() + content, status, _ = http_get(get_repo_raw_url("database.json")) + assert status == 200 + database = json.loads(content) ret = {} for platform in database: ret[MIGRATION.get(platform, platform)] = [s.rstrip('/s') for s in database[platform]] @@ -91,12 +138,11 @@ def get_comma_car_segments_database(): def logreader_from_url(url): import capnp - response = requests.get(url) - assert response.status_code == 200, f"Failed to download {url}: {response.status_code}" + data, status, _ = http_get(url) + assert status == 200, f"Failed to download {url}: {status}" - data = response.content - if data.startswith(b'\x28\xB5\x2F\xFD'): # zstd magic - data = zstd.decompress(data) + if data.startswith(b'\x28\xB5\x2F\xFD'): # zstd magic + data = zstd_decompress(data) rlog = capnp.load(str(Path(__file__).parent / "rlog.capnp")) return rlog.Event.read_multiple_bytes(data) @@ -149,18 +195,17 @@ def process_segment(args): if update: data = list(zip(timestamps, states, strict=True)) - ref_file.write_bytes(zstd.compress(pickle.dumps(data))) + ref_file.write_bytes(zstd_compress(pickle.dumps(data))) return (platform, seg, [], None) if not ref_file.exists(): return (platform, seg, [], "no ref") - ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) + ref = pickle.loads(zstd_decompress(ref_file.read_bytes())) diffs = [] for i, ((ts, ref_state), state) in enumerate(zip(ref, states, strict=True)): - for diff in dictdiffer.diff(ref_state.to_dict(), state.to_dict(), ignore=IGNORE_FIELDS, tolerance=TOLERANCE): - if diff[0] == "change": # ignore add/remove from schema changes - diffs.append((str(diff[1]), i, diff[2], ts)) + 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, None) except Exception as e: return (platform, seg, [], str(e)) @@ -184,9 +229,12 @@ def download_refs(ref_path, platforms, segments): for platform in platforms: for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" - resp = requests.get(f"{base_url}/{filename}") - if resp.status_code == 200: - (Path(ref_path) / filename).write_bytes(resp.content) + try: + content, status, _ = http_get(f"{base_url}/{filename}") + if status == 200: + (Path(ref_path) / filename).write_bytes(content) + except urllib.error.HTTPError: + pass def run_replay(platforms, segments, ref_path, update, workers=8): diff --git a/pyproject.toml b/pyproject.toml index ac5ed920b10..1a0ace169d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,11 +44,6 @@ docs = [ "Jinja2", "natsort", ] -car_diff = [ - "requests", - "zstandard", - "dictdiffer", -] examples = [ "inputs", "matplotlib", From 39a5680565d7a69c6d03a0ea05f0f9a132b261a4 Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 13 Jan 2026 23:48:48 -0800 Subject: [PATCH 063/117] fix source --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d83e74d0fe8..ad4163a1d95 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -76,7 +76,7 @@ jobs: scons -j8 - name: Test car diff if: github.event_name == 'pull_request' - run: python opendbc/car/tests/car_diff.py | tee diff.txt + run: source setup.sh && python opendbc/car/tests/car_diff.py | tee diff.txt - name: Comment PR if: github.event_name == 'pull_request' env: @@ -84,7 +84,7 @@ jobs: run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs if: github.ref == 'refs/heads/master' - run: python opendbc/car/tests/car_diff.py --update-refs + run: source setup.sh && python opendbc/car/tests/car_diff.py --update-refs - name: Checkout ci-artifacts if: github.ref == 'refs/heads/master' uses: actions/checkout@v4 From 16afc6682e92b67d856823801de9b097936ea53d Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 00:54:25 -0800 Subject: [PATCH 064/117] request --- opendbc/car/tests/car_diff.py | 65 ++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 1703288bda0..b53ac92e2d3 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -9,8 +9,9 @@ import subprocess import sys import tempfile -import urllib.error -import urllib.request +import http.client +import time +import urllib.parse from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path @@ -18,28 +19,68 @@ DIFF_BUCKET = "car_diff" TOLERANCE = 1e-2 IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"] +RETRY_STATUS = (409, 429, 503, 504) COMMA_CAR_SEGMENTS_REPO = os.environ.get("COMMA_CAR_SEGMENTS_REPO", "https://huggingface.co/datasets/commaai/commaCarSegments") COMMA_CAR_SEGMENTS_BRANCH = os.environ.get("COMMA_CAR_SEGMENTS_BRANCH", "main") COMMA_CAR_SEGMENTS_LFS_INSTANCE = os.environ.get("COMMA_CAR_SEGMENTS_LFS_INSTANCE", COMMA_CAR_SEGMENTS_REPO) +_connections = {} + + +def _reset_connections(): + global _connections + for conn in _connections.values(): + conn.close() + _connections = {} + + +def _get_connection(host, port=443): + key = (host, port) + if key not in _connections: + _connections[key] = http.client.HTTPSConnection(host, port) + return _connections[key] + + +os.register_at_fork(after_in_child=_reset_connections) + + +def _request(method, url, headers=None, body=None): + parsed = urllib.parse.urlparse(url) + path = parsed.path + ("?" + parsed.query if parsed.query else "") + conn = _get_connection(parsed.hostname, parsed.port or 443) + + for attempt in range(5): + try: + conn.request(method, path, body=body, headers=headers or {}) + resp = conn.getresponse() + data = resp.read() + if resp.status in RETRY_STATUS and attempt < 4: + time.sleep(0.5 * (2 ** attempt)) + continue + return data, resp.status, dict(resp.getheaders()) + except (http.client.HTTPException, OSError): + conn.close() + _connections.pop((parsed.hostname, parsed.port or 443), None) + conn = _get_connection(parsed.hostname, parsed.port or 443) + if attempt == 4: + raise + def http_get(url, headers=None): - req = urllib.request.Request(url, headers=headers or {}) - with urllib.request.urlopen(req) as resp: - return resp.read(), resp.status, dict(resp.headers) + return _request("GET", url, headers) def http_post_json(url, data, headers=None): - req = urllib.request.Request(url, data=json.dumps(data).encode(), headers=headers or {}, method='POST') - with urllib.request.urlopen(req) as resp: - return json.load(resp), resp.status + hdrs = {"Content-Type": "application/json", **(headers or {})} + body = json.dumps(data).encode() + data, status, resp_headers = _request("POST", url, hdrs, body) + return json.loads(data), status def http_head(url): - req = urllib.request.Request(url, method='HEAD') - with urllib.request.urlopen(req) as resp: - return resp.status, dict(resp.headers) + _, status, headers = _request("HEAD", url) + return status, headers def zstd_decompress(data): @@ -233,7 +274,7 @@ def download_refs(ref_path, platforms, segments): content, status, _ = http_get(f"{base_url}/{filename}") if status == 200: (Path(ref_path) / filename).write_bytes(content) - except urllib.error.HTTPError: + except Exception: pass From 564f5d8de5557473e5b7657d6d92eaf6ce944a14 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 03:12:51 -0800 Subject: [PATCH 065/117] less parallel --- opendbc/car/tests/car_diff.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index b53ac92e2d3..6a6dc819aef 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -48,23 +48,22 @@ def _get_connection(host, port=443): def _request(method, url, headers=None, body=None): parsed = urllib.parse.urlparse(url) path = parsed.path + ("?" + parsed.query if parsed.query else "") - conn = _get_connection(parsed.hostname, parsed.port or 443) + key = (parsed.hostname, parsed.port or 443) for attempt in range(5): try: + conn = _get_connection(*key) conn.request(method, path, body=body, headers=headers or {}) resp = conn.getresponse() data = resp.read() - if resp.status in RETRY_STATUS and attempt < 4: - time.sleep(0.5 * (2 ** attempt)) - continue - return data, resp.status, dict(resp.getheaders()) - except (http.client.HTTPException, OSError): - conn.close() - _connections.pop((parsed.hostname, parsed.port or 443), None) - conn = _get_connection(parsed.hostname, parsed.port or 443) + if resp.status not in RETRY_STATUS: + return data, resp.status, dict(resp.getheaders()) + raise OSError(f"HTTP {resp.status}") + except (http.client.HTTPException, OSError) as e: + _connections.pop(key, None) if attempt == 4: - raise + raise OSError(f"{method} {url}: {e}") from None + time.sleep(1.0 * (2 ** attempt)) def http_get(url, headers=None): @@ -278,7 +277,7 @@ def download_refs(ref_path, platforms, segments): pass -def run_replay(platforms, segments, ref_path, update, workers=8): +def run_replay(platforms, segments, ref_path, update, workers=4): work = [(platform, seg, ref_path, update) for platform in platforms for seg in segments.get(platform, [])] with ProcessPoolExecutor(max_workers=workers) as pool: From 04852c1e559f72525aee4e5d16dee7efe09ae3d1 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 03:15:50 -0800 Subject: [PATCH 066/117] fix push --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad4163a1d95..a272304581f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -98,8 +98,8 @@ jobs: run: | git config user.name "GitHub Actions Bot" git config user.email "<>" - git fetch origin car_diff - git checkout car_diff + git fetch origin car_diff || true + git checkout car_diff 2>/dev/null || git checkout --orphan car_diff cp ${{ github.workspace }}/car_diff/*.zst . git add *.zst git commit -m "car_diff refs for ${{ github.sha }}" || echo "No changes to commit" From b4af0d3bbd31f119ed3f18933329dcc64c9cf634 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 03:16:45 -0800 Subject: [PATCH 067/117] bootstrap --- .github/workflows/tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a272304581f..4336f006b8d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -83,17 +83,14 @@ jobs: GH_TOKEN: ${{ github.token }} run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs - if: github.ref == 'refs/heads/master' - run: source setup.sh && python opendbc/car/tests/car_diff.py --update-refs + run: source setup.sh && python opendbc/car/tests/car_diff.py --update-refs --all - name: Checkout ci-artifacts - if: github.ref == 'refs/heads/master' uses: actions/checkout@v4 with: repository: commaai/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} path: ${{ github.workspace }}/ci-artifacts - name: Push refs - if: github.ref == 'refs/heads/master' working-directory: ${{ github.workspace }}/ci-artifacts run: | git config user.name "GitHub Actions Bot" From d58761a3c0c8fd8caad52ca768fc3c0823d3e245 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 03:38:50 -0800 Subject: [PATCH 068/117] Revert "bootstrap" This reverts commit b4af0d3bbd31f119ed3f18933329dcc64c9cf634. --- .github/workflows/tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4336f006b8d..a272304581f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -83,14 +83,17 @@ jobs: GH_TOKEN: ${{ github.token }} run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs - run: source setup.sh && python opendbc/car/tests/car_diff.py --update-refs --all + if: github.ref == 'refs/heads/master' + run: source setup.sh && python opendbc/car/tests/car_diff.py --update-refs - name: Checkout ci-artifacts + if: github.ref == 'refs/heads/master' uses: actions/checkout@v4 with: repository: commaai/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} path: ${{ github.workspace }}/ci-artifacts - name: Push refs + if: github.ref == 'refs/heads/master' working-directory: ${{ github.workspace }}/ci-artifacts run: | git config user.name "GitHub Actions Bot" From d1fa3753728b1ebd5125c07742eca27dbecff6da Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 03:47:57 -0800 Subject: [PATCH 069/117] fix graph --- opendbc/car/tests/car_diff.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 6a6dc819aef..a864bfeb0aa 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -287,8 +287,9 @@ def run_replay(platforms, segments, ref_path, update, workers=4): def format_diff(diffs): if not diffs: return [] - if not all(isinstance(d[2], bool) and isinstance(d[3], bool) for d in diffs): - return [f" frame {d[1]}: {d[2]} -> {d[3]}" for d in diffs[:10]] + old, new = diffs[0][2] + if not (isinstance(old, bool) and isinstance(new, bool)): + return [f" frame {d[1]}: {d[2][0]} -> {d[2][1]}" for d in diffs[:10]] lines = [] ranges, cur = [], [diffs[0]] @@ -307,14 +308,13 @@ def format_diff(diffs): b_vals, m_vals, ts_map = [], [], {} last = rdiffs[-1] converge_frame = last[1] + 1 - converge_val = last[2] + converge_val = last[2][1] b_st = m_st = not converge_val for f in range(t0, t1): if f in diff_map: - b_st, m_st = diff_map[f][2], diff_map[f][3] - if len(diff_map[f]) > 4: - ts_map[f] = diff_map[f][4] + b_st, m_st = diff_map[f][2] + ts_map[f] = diff_map[f][3] elif f >= converge_frame: b_st = m_st = converge_val b_vals.append(b_st) From d8eac39076b11e4b427d578a886d840556d51d0b Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 03:54:28 -0800 Subject: [PATCH 070/117] push --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a272304581f..8974b84991c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -96,6 +96,7 @@ jobs: if: github.ref == 'refs/heads/master' working-directory: ${{ github.workspace }}/ci-artifacts run: | + ls ${{ github.workspace }}/car_diff/*.zst 2>/dev/null || exit 0 git config user.name "GitHub Actions Bot" git config user.email "<>" git fetch origin car_diff || true From 7705f500fbe043135659887b862402a496dfdf9e Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 15:02:18 -0800 Subject: [PATCH 071/117] regex --- opendbc/car/tests/car_diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index a864bfeb0aa..4e2af08687f 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -255,7 +255,7 @@ def get_changed_platforms(cwd, database, interfaces): 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"opendbc/car/(\w+)/", r"opendbc/dbc/(\w+)_", r"opendbc/safety/modes/(\w+)[_.]"] + patterns = [r"opendbc/car/(\w+)/", r"opendbc/dbc/(\w+?)_", r"opendbc/dbc/generator/(\w+)", r"opendbc/safety/modes/(\w+?)[_.]"] for line in changed.splitlines(): for pattern in patterns: m = re.search(pattern, line) From 451afa8195c261dbf016b7d85860b48c8cbb769e Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 15:39:44 -0800 Subject: [PATCH 072/117] optimize dict_diff --- opendbc/car/tests/car_diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 4e2af08687f..0dbf0ebb718 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -95,7 +95,7 @@ def zstd_compress(data): def dict_diff(d1, d2, path="", ignore=None, tolerance=0): ignore = ignore or [] diffs = [] - for key in set(list(d1.keys()) + list(d2.keys())): + for key in d1.keys() | d2.keys(): if key in ignore: continue full_path = f"{path}.{key}" if path else key From 8c52ddc28fd4aec38e4f98b6d3c88f0dfd23e3a5 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 19:17:01 -0800 Subject: [PATCH 073/117] reduce tolerance --- opendbc/car/tests/car_diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 0dbf0ebb718..9408fdd7def 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -17,7 +17,7 @@ from pathlib import Path DIFF_BUCKET = "car_diff" -TOLERANCE = 1e-2 +TOLERANCE = 1e-4 IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"] RETRY_STATUS = (409, 429, 503, 504) From 01d7b879a961ea2bdaf2dbefea0ae03a7e2f8415 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 20:09:50 -0800 Subject: [PATCH 074/117] use comma_car_segments --- opendbc/car/tests/car_diff.py | 69 ++--------------------------------- 1 file changed, 3 insertions(+), 66 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 9408fdd7def..ba2325282c8 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -3,83 +3,20 @@ os.environ['LOGPRINT'] = 'CRITICAL' import argparse -import json import pickle import re import subprocess import sys import tempfile -import http.client -import time -import urllib.parse +import requests from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path +from comma_car_segments import get_comma_car_segments_database, get_url -DIFF_BUCKET = "car_diff" TOLERANCE = 1e-4 +DIFF_BUCKET = "car_diff" IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"] -RETRY_STATUS = (409, 429, 503, 504) - -COMMA_CAR_SEGMENTS_REPO = os.environ.get("COMMA_CAR_SEGMENTS_REPO", "https://huggingface.co/datasets/commaai/commaCarSegments") -COMMA_CAR_SEGMENTS_BRANCH = os.environ.get("COMMA_CAR_SEGMENTS_BRANCH", "main") -COMMA_CAR_SEGMENTS_LFS_INSTANCE = os.environ.get("COMMA_CAR_SEGMENTS_LFS_INSTANCE", COMMA_CAR_SEGMENTS_REPO) - -_connections = {} - - -def _reset_connections(): - global _connections - for conn in _connections.values(): - conn.close() - _connections = {} - - -def _get_connection(host, port=443): - key = (host, port) - if key not in _connections: - _connections[key] = http.client.HTTPSConnection(host, port) - return _connections[key] - - -os.register_at_fork(after_in_child=_reset_connections) - - -def _request(method, url, headers=None, body=None): - parsed = urllib.parse.urlparse(url) - path = parsed.path + ("?" + parsed.query if parsed.query else "") - key = (parsed.hostname, parsed.port or 443) - - for attempt in range(5): - try: - conn = _get_connection(*key) - conn.request(method, path, body=body, headers=headers or {}) - resp = conn.getresponse() - data = resp.read() - if resp.status not in RETRY_STATUS: - return data, resp.status, dict(resp.getheaders()) - raise OSError(f"HTTP {resp.status}") - except (http.client.HTTPException, OSError) as e: - _connections.pop(key, None) - if attempt == 4: - raise OSError(f"{method} {url}: {e}") from None - time.sleep(1.0 * (2 ** attempt)) - - -def http_get(url, headers=None): - return _request("GET", url, headers) - - -def http_post_json(url, data, headers=None): - hdrs = {"Content-Type": "application/json", **(headers or {})} - body = json.dumps(data).encode() - data, status, resp_headers = _request("POST", url, hdrs, body) - return json.loads(data), status - - -def http_head(url): - _, status, headers = _request("HEAD", url) - return status, headers def zstd_decompress(data): From 9c630f0c544fdcc5d91081265efbbd30f1a384c3 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 20:11:23 -0800 Subject: [PATCH 075/117] remove --- opendbc/car/tests/car_diff.py | 65 ----------------------------------- 1 file changed, 65 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index ba2325282c8..e4d161914f7 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -47,71 +47,6 @@ def dict_diff(d1, d2, path="", ignore=None, tolerance=0): return diffs -def get_repo_raw_url(path): - if "huggingface" in COMMA_CAR_SEGMENTS_REPO: - return f"{COMMA_CAR_SEGMENTS_REPO}/raw/{COMMA_CAR_SEGMENTS_BRANCH}/{path}" - - -def parse_lfs_pointer(text): - header, lfs_version = text.splitlines()[0].split(" ") - assert header == "version" - assert lfs_version == "https://git-lfs.github.com/spec/v1" - - header, oid_raw = text.splitlines()[1].split(" ") - assert header == "oid" - header, oid = oid_raw.split(":") - assert header == "sha256" - - header, size = text.splitlines()[2].split(" ") - assert header == "size" - - return oid, size - - -def get_lfs_file_url(oid, size): - data = { - "operation": "download", - "transfers": ["basic"], - "objects": [{"oid": oid, "size": int(size)}], - "hash_algo": "sha256" - } - headers = { - "Accept": "application/vnd.git-lfs+json", - "Content-Type": "application/vnd.git-lfs+json" - } - resp, status = http_post_json(f"{COMMA_CAR_SEGMENTS_LFS_INSTANCE}.git/info/lfs/objects/batch", data, headers) - assert status == 200 - obj = resp["objects"][0] - assert "error" not in obj, obj - return obj["actions"]["download"]["href"] - - -def get_repo_url(path): - status, headers = http_head(get_repo_raw_url(path)) - if "text/plain" in headers.get("Content-Type", ""): - content, status, _ = http_get(get_repo_raw_url(path)) - assert status == 200 - oid, size = parse_lfs_pointer(content.decode()) - return get_lfs_file_url(oid, size) - else: - return get_repo_raw_url(path) - - -def get_url(route, segment, file="rlog.zst"): - return get_repo_url(f"segments/{route.replace('|', '/')}/{segment}/{file}") - - -def get_comma_car_segments_database(): - from opendbc.car.fingerprints import MIGRATION - content, status, _ = http_get(get_repo_raw_url("database.json")) - assert status == 200 - database = json.loads(content) - ret = {} - for platform in database: - ret[MIGRATION.get(platform, platform)] = [s.rstrip('/s') for s in database[platform]] - return ret - - def logreader_from_url(url): import capnp From 22e4324e8d0d8fc93d67f646fd1e29f93bb33925 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 20:11:54 -0800 Subject: [PATCH 076/117] use requests --- opendbc/car/tests/car_diff.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index e4d161914f7..7e42949673d 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -50,8 +50,9 @@ def dict_diff(d1, d2, path="", ignore=None, tolerance=0): def logreader_from_url(url): import capnp - data, status, _ = http_get(url) - assert status == 200, f"Failed to download {url}: {status}" + resp = requests.get(url) + assert resp.status_code == 200, f"Failed to download {url}: {resp.status_code}" + data = resp.content if data.startswith(b'\x28\xB5\x2F\xFD'): # zstd magic data = zstd_decompress(data) @@ -142,9 +143,9 @@ def download_refs(ref_path, platforms, segments): for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" try: - content, status, _ = http_get(f"{base_url}/{filename}") - if status == 200: - (Path(ref_path) / filename).write_bytes(content) + resp = requests.get(f"{base_url}/{filename}") + if resp.status_code == 200: + (Path(ref_path) / filename).write_bytes(resp.content) except Exception: pass From 37bc9209374edf98efd1ff26c5f0d777252f169f Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 20:13:09 -0800 Subject: [PATCH 077/117] timestamps --- opendbc/car/tests/car_diff.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 7e42949673d..5f9d7de070e 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -194,11 +194,7 @@ def format_diff(diffs): m_vals.append(m_st) # ms per frame from timestamps - if len(ts_map) >= 2: - ts_vals = sorted(ts_map.items()) - frame_ms = (ts_vals[-1][1] - ts_vals[0][1]) / 1e6 / (ts_vals[-1][0] - ts_vals[0][0]) - else: - frame_ms = 10 + frame_ms = (rdiffs[-1][3] - rdiffs[0][3]) / 1e6 / max(1, rdiffs[-1][1] - rdiffs[0][1]) lines.append(f"\n frames {t0}-{t1-1}") pad = 12 From b3aa7cba78d24c6add52c011fe1d34b97d7b0503 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 20:31:19 -0800 Subject: [PATCH 078/117] frame_ms --- opendbc/car/tests/car_diff.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 5f9d7de070e..26da76e10be 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -174,11 +174,15 @@ def format_diff(diffs): cur = [d] ranges.append(cur) + # ms per frame from timestamps, fallback to 10ms on single diff + frame_diff = diffs[-1][1] - diffs[0][1] + frame_ms = (diffs[-1][3] - diffs[0][3]) / 1e6 / frame_diff if frame_diff else 10 + for rdiffs in ranges: t0, t1 = max(0, rdiffs[0][1] - 5), rdiffs[-1][1] + 6 diff_map = {d[1]: d for d in rdiffs} - b_vals, m_vals, ts_map = [], [], {} + b_vals, m_vals = [], [] last = rdiffs[-1] converge_frame = last[1] + 1 converge_val = last[2][1] @@ -187,15 +191,11 @@ def format_diff(diffs): for f in range(t0, t1): if f in diff_map: b_st, m_st = diff_map[f][2] - ts_map[f] = diff_map[f][3] elif f >= converge_frame: b_st = m_st = converge_val b_vals.append(b_st) m_vals.append(m_st) - # ms per frame from timestamps - frame_ms = (rdiffs[-1][3] - rdiffs[0][3]) / 1e6 / max(1, rdiffs[-1][1] - rdiffs[0][1]) - lines.append(f"\n frames {t0}-{t1-1}") pad = 12 max_width = 60 From a9e013d599402514122aa3f8e4c7de34c7468095 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 21:04:21 -0800 Subject: [PATCH 079/117] id --- opendbc/car/tests/rlog.capnp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/rlog.capnp b/opendbc/car/tests/rlog.capnp index ee0e3a4825a..50cdd68b5eb 100644 --- a/opendbc/car/tests/rlog.capnp +++ b/opendbc/car/tests/rlog.capnp @@ -1,4 +1,4 @@ -@0xe3aae94257cf750f; +@0xce500edaaae36b0e; # Minimal schema for parsing rlog CAN messages # Subset of cereal/log.capnp From de34876b01205f413d7c50726cc0e86de204d554 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 21:35:19 -0800 Subject: [PATCH 080/117] logreader --- opendbc/car/logreader.py | 37 ++++++++++++++++++++++++++++++ opendbc/car/{tests => }/rlog.capnp | 0 2 files changed, 37 insertions(+) create mode 100644 opendbc/car/logreader.py rename opendbc/car/{tests => }/rlog.capnp (100%) diff --git a/opendbc/car/logreader.py b/opendbc/car/logreader.py new file mode 100644 index 00000000000..d862b09e7af --- /dev/null +++ b/opendbc/car/logreader.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +import subprocess +from pathlib import Path +from urllib.request import urlopen +import capnp + + +def _zstd_decompress(dat): + proc = subprocess.run(['zstd', '-d'], input=dat, capture_output=True, check=True) + return proc.stdout + + +class LogReader: + def __init__(self, fn, sort_by_time=False): + if fn.startswith("http"): + with urlopen(fn) as f: + dat = f.read() + else: + dat = Path(fn).read_bytes() + + if dat.startswith(b'\x28\xB5\x2F\xFD'): + dat = _zstd_decompress(dat) + + rlog = capnp.load(str(Path(__file__).parent / "rlog.capnp")) + self._ents = list(rlog.Event.read_multiple_bytes(dat)) + + if sort_by_time: + self._ents.sort(key=lambda x: x.logMonoTime) + + def __iter__(self): + yield from self._ents + + def filter(self, msg_type: str): + return (getattr(m, m.which()) for m in filter(lambda m: m.which() == msg_type, self)) + + def first(self, msg_type: str): + return next(self.filter(msg_type), None) diff --git a/opendbc/car/tests/rlog.capnp b/opendbc/car/rlog.capnp similarity index 100% rename from opendbc/car/tests/rlog.capnp rename to opendbc/car/rlog.capnp From 23b74331c430d23b0e148f2dfb2550bc5cd82adb Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 21:51:25 -0800 Subject: [PATCH 081/117] align logreader --- opendbc/car/logreader.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/opendbc/car/logreader.py b/opendbc/car/logreader.py index d862b09e7af..397574e096e 100644 --- a/opendbc/car/logreader.py +++ b/opendbc/car/logreader.py @@ -1,34 +1,52 @@ #!/usr/bin/env python3 +import os import subprocess -from pathlib import Path +import urllib.parse +import warnings from urllib.request import urlopen + import capnp +from opendbc.car.common.basedir import BASEDIR + +capnp_log = capnp.load(os.path.join(BASEDIR, "rlog.capnp")) + -def _zstd_decompress(dat): - proc = subprocess.run(['zstd', '-d'], input=dat, capture_output=True, check=True) +def decompress_stream(data: bytes) -> bytes: + proc = subprocess.run(['zstd', '-d'], input=data, capture_output=True, check=True) return proc.stdout class LogReader: - def __init__(self, fn, sort_by_time=False): + def __init__(self, fn: str, sort_by_time: bool = False): + _, ext = os.path.splitext(urllib.parse.urlparse(fn).path) + if fn.startswith("http"): with urlopen(fn) as f: dat = f.read() else: - dat = Path(fn).read_bytes() + with open(fn, "rb") as f: + dat = f.read() + + if ext == ".zst" or dat.startswith(b'\x28\xB5\x2F\xFD'): + # https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#zstandard-frames + dat = decompress_stream(dat) - if dat.startswith(b'\x28\xB5\x2F\xFD'): - dat = _zstd_decompress(dat) + ents = capnp_log.Event.read_multiple_bytes(dat) - rlog = capnp.load(str(Path(__file__).parent / "rlog.capnp")) - self._ents = list(rlog.Event.read_multiple_bytes(dat)) + self._ents = [] + try: + for e in ents: + self._ents.append(e) + except capnp.KjException: + warnings.warn("Corrupted events detected", RuntimeWarning, stacklevel=1) if sort_by_time: self._ents.sort(key=lambda x: x.logMonoTime) def __iter__(self): - yield from self._ents + for ent in self._ents: + yield ent def filter(self, msg_type: str): return (getattr(m, m.which()) for m in filter(lambda m: m.which() == msg_type, self)) From 6cb14ccbf1c2d36dc9b6bc02ddfdb21995b003fe Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 21:53:12 -0800 Subject: [PATCH 082/117] use logreader --- opendbc/car/tests/car_diff.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 26da76e10be..96f5963b4e5 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -12,6 +12,8 @@ from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path +from opendbc.car.logreader import LogReader + from comma_car_segments import get_comma_car_segments_database, get_url TOLERANCE = 1e-4 @@ -47,28 +49,15 @@ def dict_diff(d1, d2, path="", ignore=None, tolerance=0): return diffs -def logreader_from_url(url): - import capnp - - resp = requests.get(url) - assert resp.status_code == 200, f"Failed to download {url}: {resp.status_code}" - data = resp.content - - if data.startswith(b'\x28\xB5\x2F\xFD'): # zstd magic - data = zstd_decompress(data) - - rlog = capnp.load(str(Path(__file__).parent / "rlog.capnp")) - return rlog.Event.read_multiple_bytes(data) - - def load_can_messages(seg): from opendbc.car.can_definitions import CanData parts = seg.split("/") url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) + lr = LogReader(url) can_msgs = [] - for evt in logreader_from_url(url): + for evt in lr: try: if evt.which() == "can": can_msgs.append((evt.logMonoTime, [CanData(c.address, c.dat, c.src) for c in evt.can])) From 6e6127ae82d348f14d2820938bdedc17643b3083 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 21:53:34 -0800 Subject: [PATCH 083/117] remove requests --- opendbc/car/tests/car_diff.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 96f5963b4e5..34bd3c0958e 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -8,7 +8,7 @@ import subprocess import sys import tempfile -import requests +from urllib.request import urlopen from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path @@ -132,9 +132,8 @@ def download_refs(ref_path, platforms, segments): for seg in segments.get(platform, []): filename = f"{platform}_{seg.replace('/', '_')}.zst" try: - resp = requests.get(f"{base_url}/{filename}") - if resp.status_code == 200: - (Path(ref_path) / filename).write_bytes(resp.content) + with urlopen(f"{base_url}/{filename}") as resp: + (Path(ref_path) / filename).write_bytes(resp.read()) except Exception: pass From 2f3af7cbcf5b19395c1efd991c88980dde2863a0 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 22:09:22 -0800 Subject: [PATCH 084/117] return --- opendbc/car/logreader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/logreader.py b/opendbc/car/logreader.py index 397574e096e..46d62aab868 100644 --- a/opendbc/car/logreader.py +++ b/opendbc/car/logreader.py @@ -12,13 +12,13 @@ capnp_log = capnp.load(os.path.join(BASEDIR, "rlog.capnp")) -def decompress_stream(data: bytes) -> bytes: +def decompress_stream(data: bytes): proc = subprocess.run(['zstd', '-d'], input=data, capture_output=True, check=True) return proc.stdout class LogReader: - def __init__(self, fn: str, sort_by_time: bool = False): + def __init__(self, fn, sort_by_time=False): _, ext = os.path.splitext(urllib.parse.urlparse(fn).path) if fn.startswith("http"): From 95a5e5c631af58fa1056e107a67f2a5734c72c8e Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 22:19:57 -0800 Subject: [PATCH 085/117] more line --- opendbc/car/tests/car_diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 34bd3c0958e..2662773423f 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -186,7 +186,7 @@ def format_diff(diffs): lines.append(f"\n frames {t0}-{t1-1}") pad = 12 - max_width = 60 + max_width = 80 init_val = not converge_val for label, vals, init in [("master", b_vals, init_val), ("PR", m_vals, init_val)]: line = f" {label}:".ljust(pad) From b6bc789fa95868035a07da70c1b5e58b7763fc34 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 22:30:14 -0800 Subject: [PATCH 086/117] ruff --- opendbc/car/logreader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opendbc/car/logreader.py b/opendbc/car/logreader.py index 46d62aab868..5f72bc89723 100644 --- a/opendbc/car/logreader.py +++ b/opendbc/car/logreader.py @@ -45,8 +45,7 @@ def __init__(self, fn, sort_by_time=False): self._ents.sort(key=lambda x: x.logMonoTime) def __iter__(self): - for ent in self._ents: - yield ent + yield from self._ents def filter(self, msg_type: str): return (getattr(m, m.which()) for m in filter(lambda m: m.which() == msg_type, self)) From 02761501ca82d41867a1d7cecfd978a9e160e6e9 Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 19:31:08 -0800 Subject: [PATCH 087/117] use test pip package --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1a0ace169d8..6a10e92538b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ [project.optional-dependencies] testing = [ + "comma-car-segments-test @ https://test-files.pythonhosted.org/packages/13/58/1a458fb78d2f9fde9f57b26ad7e0a86b9f2371d33436ef0df9512a5ba8b2/comma_car_segments_test-0.1.0-py3-none-any.whl", "cffi", "gcovr", # FIXME: pytest 9.0.0 doesn't support unittest.SkipTest From ca5a18eb1a1d69974dc0107033c8106a4330236b Mon Sep 17 00:00:00 2001 From: elkoled Date: Wed, 14 Jan 2026 22:37:24 -0800 Subject: [PATCH 088/117] push on commaai --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8974b84991c..f86d77e14df 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -83,17 +83,17 @@ jobs: GH_TOKEN: ${{ github.token }} run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' - name: Update refs - if: github.ref == 'refs/heads/master' + if: github.repository == 'commaai/opendbc' && github.ref == 'refs/heads/master' run: source setup.sh && python opendbc/car/tests/car_diff.py --update-refs - name: Checkout ci-artifacts - if: github.ref == 'refs/heads/master' + if: github.repository == 'commaai/opendbc' && github.ref == 'refs/heads/master' uses: actions/checkout@v4 with: repository: commaai/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} path: ${{ github.workspace }}/ci-artifacts - name: Push refs - if: github.ref == 'refs/heads/master' + if: github.repository == 'commaai/opendbc' && github.ref == 'refs/heads/master' working-directory: ${{ github.workspace }}/ci-artifacts run: | ls ${{ github.workspace }}/car_diff/*.zst 2>/dev/null || exit 0 From 0108c6fc9b755d3d47ca4c80f7fd8e3bc8c97725 Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 15 Jan 2026 11:54:14 -0800 Subject: [PATCH 089/117] use zstandard lib --- opendbc/car/logreader.py | 14 +++++++++----- opendbc/car/tests/car_diff.py | 19 ++++++------------- pyproject.toml | 1 + 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/opendbc/car/logreader.py b/opendbc/car/logreader.py index 5f72bc89723..e80bd6348a5 100644 --- a/opendbc/car/logreader.py +++ b/opendbc/car/logreader.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 import os -import subprocess +import capnp import urllib.parse import warnings from urllib.request import urlopen - -import capnp +import zstandard as zstd from opendbc.car.common.basedir import BASEDIR @@ -13,8 +12,13 @@ def decompress_stream(data: bytes): - proc = subprocess.run(['zstd', '-d'], input=data, capture_output=True, check=True) - return proc.stdout + dctx = zstd.ZstdDecompressor() + decompressed_data = b"" + + with dctx.stream_reader(data) as reader: + decompressed_data = reader.read() + + return decompressed_data class LogReader: diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 2662773423f..f38e010034b 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -8,29 +8,22 @@ import subprocess import sys import tempfile +import zstandard as zstd from urllib.request import urlopen from collections import defaultdict from concurrent.futures import ProcessPoolExecutor from pathlib import Path -from opendbc.car.logreader import LogReader from comma_car_segments import get_comma_car_segments_database, get_url +from opendbc.car.logreader import LogReader + + TOLERANCE = 1e-4 DIFF_BUCKET = "car_diff" IGNORE_FIELDS = ["cumLagMs", "canErrorCounter"] -def zstd_decompress(data): - proc = subprocess.run(['zstd', '-d'], input=data, capture_output=True, check=True) - return proc.stdout - - -def zstd_compress(data): - proc = subprocess.run(['zstd', '-c'], input=data, capture_output=True, check=True) - return proc.stdout - - def dict_diff(d1, d2, path="", ignore=None, tolerance=0): ignore = ignore or [] diffs = [] @@ -97,13 +90,13 @@ def process_segment(args): if update: data = list(zip(timestamps, states, strict=True)) - ref_file.write_bytes(zstd_compress(pickle.dumps(data))) + ref_file.write_bytes(zstd.compress(pickle.dumps(data), 10)) return (platform, seg, [], None) if not ref_file.exists(): return (platform, seg, [], "no ref") - ref = pickle.loads(zstd_decompress(ref_file.read_bytes())) + ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) diffs = [] 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): diff --git a/pyproject.toml b/pyproject.toml index 6a10e92538b..666f1b3f362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ testing = [ "pytest-subtests", "hypothesis==6.47.*", "parameterized>=0.8,<0.9", + "zstandard", # static analysis "ruff", From 47145c4a9bc028376452cbc97a6918abc57c359f Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 15 Jan 2026 14:23:14 -0800 Subject: [PATCH 090/117] clean --- opendbc/car/tests/car_diff.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index f38e010034b..191483d0e95 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -70,13 +70,14 @@ def replay_segment(platform, can_msgs): fingerprint[msg.src][msg.address] = len(msg.dat) CarInterface = interfaces[platform] - car_interface = CarInterface(CarInterface.get_params(platform, fingerprint, [], False, False, False)) - car_control = structs.CarControl().as_reader() + CP = CarInterface.get_params(platform, fingerprint, [], False, False, False) + CI = CarInterface(CP) + CC = structs.CarControl().as_reader() states, timestamps = [], [] for ts, frames in can_msgs: - states.append(car_interface.update([(ts, frames)])) - car_interface.apply(car_control, ts) + states.append(CI.update([(ts, frames)])) + CI.apply(CC, ts) timestamps.append(ts) return states, timestamps @@ -260,13 +261,13 @@ def main(platform=None, segments_per_platform=10, update_refs=False, all_platfor print(f"\nResults: {n_passed} passed, {len(with_diffs)} with diffs, {len(errors)} errors") - for plat, seg, err in errors: - print(f"\nERROR {plat} - {seg}: {err}") + for platform, seg, err in errors: + print(f"\nERROR {platform} - {seg}: {err}") if with_diffs: print("```") - for plat, seg, diffs in with_diffs: - print(f"\n{plat} - {seg}") + for platform, seg, diffs in with_diffs: + print(f"\n{platform} - {seg}") by_field = defaultdict(list) for d in diffs: by_field[d[0]].append(d) From 0b32b0607392a880db9d9c3338712081adae7005 Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 15 Jan 2026 14:35:42 -0800 Subject: [PATCH 091/117] rename --- opendbc/car/tests/car_diff.py | 104 +++++++++++++++++----------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 191483d0e95..a79ebfc670d 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -142,79 +142,81 @@ def run_replay(platforms, segments, ref_path, update, workers=4): def format_diff(diffs): if not diffs: return [] + + frame_group = 15 + frame_pad = 5 + graph_pad = 12 + graph_width = 80 + wave = {(False, True): "/", (True, False): "\\", (True, True): "‾", (False, False): "_"} + old, new = diffs[0][2] if not (isinstance(old, bool) and isinstance(new, bool)): - return [f" frame {d[1]}: {d[2][0]} -> {d[2][1]}" for d in diffs[:10]] - - lines = [] - ranges, cur = [], [diffs[0]] - for d in diffs[1:]: - if d[1] <= cur[-1][1] + 15: - cur.append(d) + lines = [f" frame {diff[1]}: {diff[2][0]} -> {diff[2][1]}" for diff in diffs[:10]] + if len(diffs) > 10: + lines.append(f" (... {len(diffs) - 10} more)") + return lines + + ranges, current = [], [diffs[0]] + for diff in diffs[1:]: + if diff[1] <= current[-1][1] + frame_group: + current.append(diff) else: - ranges.append(cur) - cur = [d] - ranges.append(cur) + ranges.append(current) + current = [diff] + ranges.append(current) # ms per frame from timestamps, fallback to 10ms on single diff frame_diff = diffs[-1][1] - diffs[0][1] frame_ms = (diffs[-1][3] - diffs[0][3]) / 1e6 / frame_diff if frame_diff else 10 - for rdiffs in ranges: - t0, t1 = max(0, rdiffs[0][1] - 5), rdiffs[-1][1] + 6 - diff_map = {d[1]: d for d in rdiffs} + lines = [] + for range_diffs in ranges: + start, end = max(0, range_diffs[0][1] - frame_pad), range_diffs[-1][1] + frame_pad + 1 + diff_map = {diff[1]: diff for diff in range_diffs} - b_vals, m_vals = [], [] - last = rdiffs[-1] + last = range_diffs[-1] converge_frame = last[1] + 1 - converge_val = last[2][1] - b_st = m_st = not converge_val - - for f in range(t0, t1): - if f in diff_map: - b_st, m_st = diff_map[f][2] - elif f >= converge_frame: - b_st = m_st = converge_val - b_vals.append(b_st) + converge_val = last[2][1] + m_st = pr_st = not converge_val + + m_vals, pr_vals = [], [] + for frame in range(start, end): + if frame in diff_map: + m_st, pr_st = diff_map[frame][2] + elif frame >= converge_frame: + m_st = pr_st = converge_val m_vals.append(m_st) + pr_vals.append(pr_st) - lines.append(f"\n frames {t0}-{t1-1}") - pad = 12 - max_width = 80 + lines.append(f"\n frames {start}-{end - 1}") init_val = not converge_val - for label, vals, init in [("master", b_vals, init_val), ("PR", m_vals, init_val)]: - line = f" {label}:".ljust(pad) - for i, v in enumerate(vals): - pv = vals[i - 1] if i > 0 else init - if v and not pv: - line += "/" - elif not v and pv: - line += "\\" - elif v: - line += "‾" - else: - line += "_" - if len(line) > pad + max_width: - line = line[:pad + max_width] + "..." + for label, vals in [("master", m_vals), ("PR", pr_vals)]: + line = f" {label}:".ljust(graph_pad) + for i, val in enumerate(vals): + prev = vals[i - 1] if i else init_val + line += wave[(prev, val)] + if len(line) > graph_pad + graph_width: + line = line[:graph_pad + graph_width] + "..." lines.append(line) - b_rises = [i for i, v in enumerate(b_vals) if v and (i == 0 or not b_vals[i - 1])] - m_rises = [i for i, v in enumerate(m_vals) if v and (i == 0 or not m_vals[i - 1])] - b_falls = [i for i, v in enumerate(b_vals) if not v and i > 0 and b_vals[i - 1]] - m_falls = [i for i, v in enumerate(m_vals) if not v and i > 0 and m_vals[i - 1]] + m_rises = [i for i, v in enumerate(m_vals) if v and (i == 0 or not m_vals[i - 1])] + pr_rises = [i for i, v in enumerate(pr_vals) if v and (i == 0 or not pr_vals[i - 1])] + m_falls = [i for i, v in enumerate(m_vals) if not v and i > 0 and m_vals[i - 1]] + pr_falls = [i for i, v in enumerate(pr_vals) if not v and i > 0 and pr_vals[i - 1]] - if b_rises and m_rises: - delta = m_rises[0] - b_rises[0] + if m_rises and pr_rises: + delta = pr_rises[0] - m_rises[0] if delta: ms = int(abs(delta) * frame_ms) direction = "lags" if delta > 0 else "leads" - lines.append(" " * pad + f"rise: PR {direction} by {abs(delta)} frames ({ms}ms)") - if b_falls and m_falls: - delta = m_falls[0] - b_falls[0] + lines.append(" " * graph_pad + f"rise: PR {direction} by {abs(delta)} frames ({ms}ms)") + + if m_falls and pr_falls: + delta = pr_falls[0] - m_falls[0] if delta: ms = int(abs(delta) * frame_ms) direction = "lags" if delta > 0 else "leads" - lines.append(" " * pad + f"fall: PR {direction} by {abs(delta)} frames ({ms}ms)") + lines.append(" " * graph_pad + f"fall: PR {direction} by {abs(delta)} frames ({ms}ms)") return lines From 8537c4799035124c79a2acccaa37aef5d8a18589 Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 15 Jan 2026 15:34:36 -0800 Subject: [PATCH 092/117] use decompress --- opendbc/car/tests/car_diff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index a79ebfc670d..b24cb313cef 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -16,7 +16,7 @@ from comma_car_segments import get_comma_car_segments_database, get_url -from opendbc.car.logreader import LogReader +from opendbc.car.logreader import LogReader, decompress_stream TOLERANCE = 1e-4 @@ -97,7 +97,7 @@ def process_segment(args): if not ref_file.exists(): return (platform, seg, [], "no ref") - ref = pickle.loads(zstd.decompress(ref_file.read_bytes())) + ref = pickle.loads(decompress_stream(ref_file.read_bytes())) diffs = [] 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): From ff8a182ac1740f9dec43815908fc30fd9ae3c15d Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 15 Jan 2026 15:38:35 -0800 Subject: [PATCH 093/117] fix --- opendbc/car/tests/car_diff.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index b24cb313cef..4fc3af600e7 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -263,13 +263,13 @@ def main(platform=None, segments_per_platform=10, update_refs=False, all_platfor print(f"\nResults: {n_passed} passed, {len(with_diffs)} with diffs, {len(errors)} errors") - for platform, seg, err in errors: - print(f"\nERROR {platform} - {seg}: {err}") + for plat, seg, err in errors: + print(f"\nERROR {plat} - {seg}: {err}") if with_diffs: print("```") - for platform, seg, diffs in with_diffs: - print(f"\n{platform} - {seg}") + for plat, seg, diffs in with_diffs: + print(f"\n{plat} - {seg}") by_field = defaultdict(list) for d in diffs: by_field[d[0]].append(d) From a0e73eb8f027dee54551dadcae34cf3d70cf1e25 Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 15 Jan 2026 16:21:45 -0800 Subject: [PATCH 094/117] index --- opendbc/car/tests/car_diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 4fc3af600e7..79932d654c2 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -176,7 +176,7 @@ def format_diff(diffs): last = range_diffs[-1] converge_frame = last[1] + 1 - converge_val = last[2][1] + converge_val = last[2][0] m_st = pr_st = not converge_val m_vals, pr_vals = [], [] From aa7de954a0f33bd905759eedff55b9c01b6b9bb5 Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 15 Jan 2026 22:49:08 -0800 Subject: [PATCH 095/117] lr filter --- opendbc/car/tests/car_diff.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 79932d654c2..e5d22d2afc1 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -43,31 +43,22 @@ def dict_diff(d1, d2, path="", ignore=None, tolerance=0): def load_can_messages(seg): - from opendbc.car.can_definitions import CanData - parts = seg.split("/") url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) - lr = LogReader(url) - can_msgs = [] - for evt in lr: - try: - if evt.which() == "can": - can_msgs.append((evt.logMonoTime, [CanData(c.address, c.dat, c.src) for c in evt.can])) - except Exception: - pass - return can_msgs + return list(lr.filter('can')) def replay_segment(platform, can_msgs): from opendbc.car import gen_empty_fingerprint, structs + from opendbc.car.can_definitions import CanData from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces fingerprint = gen_empty_fingerprint() - for _, frames in can_msgs[:FRAME_FINGERPRINT]: - for msg in frames: - if msg.src < 64: - fingerprint[msg.src][msg.address] = len(msg.dat) + for can in can_msgs[:FRAME_FINGERPRINT]: + for m in can: + if m.src < 64: + fingerprint[m.src][m.address] = len(m.dat) CarInterface = interfaces[platform] CP = CarInterface.get_params(platform, fingerprint, [], False, False, False) @@ -75,10 +66,12 @@ def replay_segment(platform, can_msgs): CC = structs.CarControl().as_reader() states, timestamps = [], [] - for ts, frames in can_msgs: - states.append(CI.update([(ts, frames)])) - CI.apply(CC, ts) - timestamps.append(ts) + for i, can in enumerate(can_msgs): + t = int(0.01 * i * 1e9) + frames = [CanData(m.address, m.dat, m.src) for m in can] + states.append(CI.update([(t, frames)])) + CI.apply(CC, t) + timestamps.append(t) return states, timestamps From 749a7c33faab64c7678ff1cbfb6e1ebb2b23f879 Mon Sep 17 00:00:00 2001 From: elkoled Date: Thu, 15 Jan 2026 23:39:14 -0800 Subject: [PATCH 096/117] union types --- opendbc/car/logreader.py | 13 +++++++++++-- opendbc/car/tests/car_diff.py | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/opendbc/car/logreader.py b/opendbc/car/logreader.py index e80bd6348a5..93b2359fb5b 100644 --- a/opendbc/car/logreader.py +++ b/opendbc/car/logreader.py @@ -22,7 +22,8 @@ def decompress_stream(data: bytes): class LogReader: - def __init__(self, fn, sort_by_time=False): + def __init__(self, fn, only_union_types=False, sort_by_time=False): + self._only_union_types = only_union_types _, ext = os.path.splitext(urllib.parse.urlparse(fn).path) if fn.startswith("http"): @@ -49,7 +50,15 @@ def __init__(self, fn, sort_by_time=False): self._ents.sort(key=lambda x: x.logMonoTime) def __iter__(self): - yield from self._ents + for ent in self._ents: + if self._only_union_types: + try: + ent.which() + yield ent + except capnp.lib.capnp.KjException: + pass + else: + yield ent def filter(self, msg_type: str): return (getattr(m, m.which()) for m in filter(lambda m: m.which() == msg_type, self)) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index e5d22d2afc1..d77086f1cae 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -45,7 +45,7 @@ def dict_diff(d1, d2, path="", ignore=None, tolerance=0): def load_can_messages(seg): parts = seg.split("/") url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) - lr = LogReader(url) + lr = LogReader(url, only_union_types=True) return list(lr.filter('can')) From 910932b9bf1bb28441d85c5542e3af1c5774df69 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 15:38:41 -0800 Subject: [PATCH 097/117] find edges --- opendbc/car/tests/car_diff.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index d77086f1cae..3ce8bc811e5 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -132,6 +132,19 @@ def run_replay(platforms, segments, ref_path, update, workers=4): return list(pool.map(process_segment, work)) +def find_edges(vals, init): + rises = [] + falls = [] + prev = init + 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 format_diff(diffs): if not diffs: return [] @@ -192,10 +205,8 @@ def format_diff(diffs): line = line[:graph_pad + graph_width] + "..." lines.append(line) - m_rises = [i for i, v in enumerate(m_vals) if v and (i == 0 or not m_vals[i - 1])] - pr_rises = [i for i, v in enumerate(pr_vals) if v and (i == 0 or not pr_vals[i - 1])] - m_falls = [i for i, v in enumerate(m_vals) if not v and i > 0 and m_vals[i - 1]] - pr_falls = [i for i, v in enumerate(pr_vals) if not v and i > 0 and pr_vals[i - 1]] + m_rises, m_falls = find_edges(m_vals, init_val) + pr_rises, pr_falls = find_edges(pr_vals, init_val) if m_rises and pr_rises: delta = pr_rises[0] - m_rises[0] From 3cc0d33d3b8df93ad17ea36546514768906f3cc2 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 15:39:06 -0800 Subject: [PATCH 098/117] group frames --- opendbc/car/tests/car_diff.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 3ce8bc811e5..91f6a77592b 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -145,11 +145,25 @@ def find_edges(vals, init): return rises, falls +def group_frames(diffs, max_gap=15): + 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 format_diff(diffs): if not diffs: return [] - frame_group = 15 frame_pad = 5 graph_pad = 12 graph_width = 80 @@ -162,14 +176,7 @@ def format_diff(diffs): lines.append(f" (... {len(diffs) - 10} more)") return lines - ranges, current = [], [diffs[0]] - for diff in diffs[1:]: - if diff[1] <= current[-1][1] + frame_group: - current.append(diff) - else: - ranges.append(current) - current = [diff] - ranges.append(current) + ranges = group_frames(diffs) # ms per frame from timestamps, fallback to 10ms on single diff frame_diff = diffs[-1][1] - diffs[0][1] From 7b16053f86f879d5c755f76fbe9d55a501e7ac80 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 15:40:07 -0800 Subject: [PATCH 099/117] render waveform --- opendbc/car/tests/car_diff.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 91f6a77592b..ad6bc5c7ab4 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -145,6 +145,18 @@ def find_edges(vals, init): return rises, falls +def render_waveform(label, vals, init): + wave = {(False, False): "_", (True, True): "‾", (False, True): "/", (True, False): "\\"} + line = f" {label}:".ljust(12) + prev = init + for val in vals: + line += wave[(prev, val)] + prev = val + if len(line) > 80: + line = line[:80] + "..." + return line + + def group_frames(diffs, max_gap=15): groups = [] current = [diffs[0]] @@ -166,8 +178,6 @@ def format_diff(diffs): frame_pad = 5 graph_pad = 12 - graph_width = 80 - wave = {(False, True): "/", (True, False): "\\", (True, True): "‾", (False, False): "_"} old, new = diffs[0][2] if not (isinstance(old, bool) and isinstance(new, bool)): @@ -203,14 +213,8 @@ def format_diff(diffs): lines.append(f"\n frames {start}-{end - 1}") init_val = not converge_val - for label, vals in [("master", m_vals), ("PR", pr_vals)]: - line = f" {label}:".ljust(graph_pad) - for i, val in enumerate(vals): - prev = vals[i - 1] if i else init_val - line += wave[(prev, val)] - if len(line) > graph_pad + graph_width: - line = line[:graph_pad + graph_width] + "..." - lines.append(line) + lines.append(render_waveform("master", m_vals, init_val)) + lines.append(render_waveform("PR", pr_vals, init_val)) m_rises, m_falls = find_edges(m_vals, init_val) pr_rises, pr_falls = find_edges(pr_vals, init_val) From 06eab0d395daf8339ec5ee8f1330506a6b6f24b8 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 15:41:57 -0800 Subject: [PATCH 100/117] format timing --- opendbc/car/tests/car_diff.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index ad6bc5c7ab4..d0dd1a5383a 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -157,6 +157,17 @@ def render_waveform(label, vals, init): return line +def format_timing(edge_type, master_edges, pr_edges, ms_per_frame): + 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, max_gap=15): groups = [] current = [diffs[0]] @@ -177,7 +188,6 @@ def format_diff(diffs): return [] frame_pad = 5 - graph_pad = 12 old, new = diffs[0][2] if not (isinstance(old, bool) and isinstance(new, bool)): @@ -219,19 +229,10 @@ def format_diff(diffs): m_rises, m_falls = find_edges(m_vals, init_val) pr_rises, pr_falls = find_edges(pr_vals, init_val) - if m_rises and pr_rises: - delta = pr_rises[0] - m_rises[0] - if delta: - ms = int(abs(delta) * frame_ms) - direction = "lags" if delta > 0 else "leads" - lines.append(" " * graph_pad + f"rise: PR {direction} by {abs(delta)} frames ({ms}ms)") - - if m_falls and pr_falls: - delta = pr_falls[0] - m_falls[0] - if delta: - ms = int(abs(delta) * frame_ms) - direction = "lags" if delta > 0 else "leads" - lines.append(" " * graph_pad + f"fall: PR {direction} by {abs(delta)} frames ({ms}ms)") + for edge_type, master_edges, pr_edges in [("rise", m_rises, pr_rises), ("fall", m_falls, pr_falls)]: + msg = format_timing(edge_type, master_edges, pr_edges, frame_ms) + if msg: + lines.append(msg) return lines From cba48abd6c807f6974c244fb992c5c4dd451181b Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 15:42:39 -0800 Subject: [PATCH 101/117] signals --- opendbc/car/tests/car_diff.py | 51 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index d0dd1a5383a..47fb06457da 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -183,12 +183,31 @@ def group_frames(diffs, max_gap=15): return groups +def build_signals(group): + _, first_frame, _, _ = group[0] + _, last_frame, (final_master, _), _ = group[-1] + start = max(0, first_frame - 5) + end = last_frame + 6 + init = not final_master + diff_at = {frame: (m, p) for _, frame, (m, p), _ in group} + master_vals = [] + pr_vals = [] + master = init + pr = init + for frame in range(start, end): + if frame in diff_at: + master, pr = diff_at[frame] + elif frame > last_frame: + master = pr = final_master + master_vals.append(master) + pr_vals.append(pr) + return master_vals, pr_vals, init, start, end + + def format_diff(diffs): if not diffs: return [] - frame_pad = 5 - old, new = diffs[0][2] if not (isinstance(old, bool) and isinstance(new, bool)): lines = [f" frame {diff[1]}: {diff[2][0]} -> {diff[2][1]}" for diff in diffs[:10]] @@ -203,31 +222,15 @@ def format_diff(diffs): frame_ms = (diffs[-1][3] - diffs[0][3]) / 1e6 / frame_diff if frame_diff else 10 lines = [] - for range_diffs in ranges: - start, end = max(0, range_diffs[0][1] - frame_pad), range_diffs[-1][1] + frame_pad + 1 - diff_map = {diff[1]: diff for diff in range_diffs} - - last = range_diffs[-1] - converge_frame = last[1] + 1 - converge_val = last[2][0] - m_st = pr_st = not converge_val - - m_vals, pr_vals = [], [] - for frame in range(start, end): - if frame in diff_map: - m_st, pr_st = diff_map[frame][2] - elif frame >= converge_frame: - m_st = pr_st = converge_val - m_vals.append(m_st) - pr_vals.append(pr_st) + for group in ranges: + master_vals, pr_vals, init, start, end = build_signals(group) lines.append(f"\n frames {start}-{end - 1}") - init_val = not converge_val - lines.append(render_waveform("master", m_vals, init_val)) - lines.append(render_waveform("PR", pr_vals, init_val)) + lines.append(render_waveform("master", master_vals, init)) + lines.append(render_waveform("PR", pr_vals, init)) - m_rises, m_falls = find_edges(m_vals, init_val) - pr_rises, pr_falls = find_edges(pr_vals, init_val) + m_rises, m_falls = find_edges(master_vals, init) + pr_rises, pr_falls = find_edges(pr_vals, init) for edge_type, master_edges, pr_edges in [("rise", m_rises, pr_rises), ("fall", m_falls, pr_falls)]: msg = format_timing(edge_type, master_edges, pr_edges, frame_ms) From 357f1eabb238b544e60fc23b6a32a4e9f8455c6a Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 15:43:02 -0800 Subject: [PATCH 102/117] numeric diff --- opendbc/car/tests/car_diff.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 47fb06457da..2db96c62124 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -204,16 +204,22 @@ def build_signals(group): return master_vals, pr_vals, init, start, end +def format_numeric_diffs(diffs): + 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_diff(diffs): if not diffs: return [] - old, new = diffs[0][2] + _, _, (old, new), _ = diffs[0] if not (isinstance(old, bool) and isinstance(new, bool)): - lines = [f" frame {diff[1]}: {diff[2][0]} -> {diff[2][1]}" for diff in diffs[:10]] - if len(diffs) > 10: - lines.append(f" (... {len(diffs) - 10} more)") - return lines + return format_numeric_diffs(diffs) ranges = group_frames(diffs) From 37585e5bf1608ebe16ac80b35b45616e26b36f69 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 15:43:36 -0800 Subject: [PATCH 103/117] boolean diff --- opendbc/car/tests/car_diff.py | 42 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 2db96c62124..a4759cc7805 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -213,39 +213,37 @@ def format_numeric_diffs(diffs): return lines -def format_diff(diffs): - if not diffs: - return [] - - _, _, (old, new), _ = diffs[0] - if not (isinstance(old, bool) and isinstance(new, bool)): - return format_numeric_diffs(diffs) - - ranges = group_frames(diffs) - - # ms per frame from timestamps, fallback to 10ms on single diff - frame_diff = diffs[-1][1] - diffs[0][1] - frame_ms = (diffs[-1][3] - diffs[0][3]) / 1e6 / frame_diff if frame_diff else 10 - +def format_boolean_diffs(diffs): + _, 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 ranges: + for group in group_frames(diffs): master_vals, pr_vals, init, start, end = build_signals(group) - lines.append(f"\n frames {start}-{end - 1}") lines.append(render_waveform("master", master_vals, init)) lines.append(render_waveform("PR", pr_vals, init)) - - m_rises, m_falls = find_edges(master_vals, init) + master_rises, master_falls = find_edges(master_vals, init) pr_rises, pr_falls = find_edges(pr_vals, init) - - for edge_type, master_edges, pr_edges in [("rise", m_rises, pr_rises), ("fall", m_falls, pr_falls)]: - msg = format_timing(edge_type, master_edges, pr_edges, frame_ms) + 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): + 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) + return format_numeric_diffs(diffs) + + def main(platform=None, segments_per_platform=10, update_refs=False, all_platforms=False): from opendbc.car.car_helpers import interfaces From 1a418973954b1f289d827e199753fcaedcdafd82 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 17:06:30 -0800 Subject: [PATCH 104/117] real timestamp --- opendbc/car/tests/car_diff.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index a4759cc7805..a3581cf95f0 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -45,8 +45,8 @@ def dict_diff(d1, d2, path="", ignore=None, tolerance=0): def load_can_messages(seg): parts = seg.split("/") url = get_url(f"{parts[0]}/{parts[1]}", parts[2]) - lr = LogReader(url, only_union_types=True) - return list(lr.filter('can')) + msgs = LogReader(url, only_union_types=True) + return [m for m in msgs if m.which() == 'can'] def replay_segment(platform, can_msgs): @@ -55,8 +55,8 @@ def replay_segment(platform, can_msgs): from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces fingerprint = gen_empty_fingerprint() - for can in can_msgs[:FRAME_FINGERPRINT]: - for m in can: + for msg in can_msgs[:FRAME_FINGERPRINT]: + for m in msg.can: if m.src < 64: fingerprint[m.src][m.address] = len(m.dat) @@ -66,12 +66,11 @@ def replay_segment(platform, can_msgs): CC = structs.CarControl().as_reader() states, timestamps = [], [] - for i, can in enumerate(can_msgs): - t = int(0.01 * i * 1e9) - frames = [CanData(m.address, m.dat, m.src) for m in can] - states.append(CI.update([(t, frames)])) - CI.apply(CC, t) - timestamps.append(t) + 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 states, timestamps From 692e34c7f8df6540c26eca79799a8eac47f352bc Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 16:21:20 -0800 Subject: [PATCH 105/117] impreza fingerprint --- opendbc/car/docs_definitions.py | 23 +++++++++---------- opendbc/car/extra_cars.py | 24 ++++++++++---------- opendbc/car/subaru/fingerprints.py | 5 +++++ opendbc/car/tesla/carcontroller.py | 2 +- opendbc/car/tesla/carstate.py | 7 +++++- opendbc/car/tesla/fingerprints.py | 2 ++ opendbc/car/tesla/interface.py | 9 ++++++-- opendbc/car/tesla/teslacan.py | 19 +++++++++++++--- opendbc/car/tesla/values.py | 10 +++++++++ opendbc/car/toyota/carcontroller.py | 15 +++++++------ opendbc/safety/modes/tesla.h | 26 +++++++++++++++++---- opendbc/safety/tests/test_tesla.py | 35 ++++++++++++++++++----------- setup.sh | 2 +- 13 files changed, 123 insertions(+), 56 deletions(-) diff --git a/opendbc/car/docs_definitions.py b/opendbc/car/docs_definitions.py index 2f49b834f61..c77573930a8 100644 --- a/opendbc/car/docs_definitions.py +++ b/opendbc/car/docs_definitions.py @@ -78,7 +78,6 @@ class Cable(EnumBase): long_obdc_cable = BasePart("long OBD-C cable (9.5 ft)") usb_a_2_a_cable = BasePart("USB A-A cable") usbc_otg_cable = BasePart("USB C OTG cable") - usbc_coupler = BasePart("USB-C coupler") obd_c_cable_2ft = BasePart("OBD-C cable (2 ft)") @@ -112,7 +111,7 @@ class CarHarness(EnumBase): fca = BaseCarHarness("FCA connector") ram = BaseCarHarness("Ram connector") vw_a = BaseCarHarness("VW A connector") - vw_j533 = BaseCarHarness("VW J533 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) + vw_j533 = BaseCarHarness("VW J533 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) hyundai_a = BaseCarHarness("Hyundai A connector") hyundai_b = BaseCarHarness("Hyundai B connector") hyundai_c = BaseCarHarness("Hyundai C connector") @@ -132,18 +131,18 @@ class CarHarness(EnumBase): hyundai_q = BaseCarHarness("Hyundai Q connector") hyundai_r = BaseCarHarness("Hyundai R connector") custom = BaseCarHarness("Developer connector") - obd_ii = BaseCarHarness("OBD-II connector", parts=[Cable.long_obdc_cable, Cable.usbc_coupler], has_connector=False) + obd_ii = BaseCarHarness("OBD-II connector", parts=[Cable.long_obdc_cable], has_connector=False) gm = BaseCarHarness("GM connector", parts=[Accessory.harness_box]) - gmsdgm = BaseCarHarness("GM SDGM connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) - nissan_a = BaseCarHarness("Nissan A connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) - nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) + gmsdgm = BaseCarHarness("GM SDGM connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) + nissan_a = BaseCarHarness("Nissan A connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) + nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) mazda = BaseCarHarness("Mazda connector") ford_q3 = BaseCarHarness("Ford Q3 connector") - ford_q4 = BaseCarHarness("Ford Q4 connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) - rivian = BaseCarHarness("Rivian A connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) - tesla_a = BaseCarHarness("Tesla A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) - tesla_b = BaseCarHarness("Tesla B connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) - psa_a = BaseCarHarness("PSA A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) + ford_q4 = BaseCarHarness("Ford Q4 connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) + rivian = BaseCarHarness("Rivian A connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) + tesla_a = BaseCarHarness("Tesla A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) + tesla_b = BaseCarHarness("Tesla B connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) + psa_a = BaseCarHarness("PSA A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) class Device(EnumBase): @@ -394,7 +393,7 @@ def get_extra_cars_column(self, column: ExtraCarsColumn) -> str: @dataclass class ExtraCarDocs(CarDocs): - package: str = "Any" + package: str = "All" merged: bool = False support_type: SupportType = SupportType.INCOMPATIBLE support_link: str | None = "#incompatible" diff --git a/opendbc/car/extra_cars.py b/opendbc/car/extra_cars.py index 5ffa42bf4a3..fed2f5a4914 100644 --- a/opendbc/car/extra_cars.py +++ b/opendbc/car/extra_cars.py @@ -37,19 +37,19 @@ class CAR(Platforms): EXTRA_HONDA = ExtraPlatformConfig( [ - CommunityCarDocs("Acura Integra 2023-25", "All"), + CommunityCarDocs("Acura Integra 2023-25"), CommunityCarDocs("Acura MDX 2015-16", "Advance Package"), - CommunityCarDocs("Acura MDX 2017-20", "All"), - CommunityCarDocs("Acura MDX 2022-24", "All"), - CommunityCarDocs("Acura RDX 2022-25", "All"), + CommunityCarDocs("Acura MDX 2017-20"), + CommunityCarDocs("Acura MDX 2022-24"), + CommunityCarDocs("Acura RDX 2022-25"), CommunityCarDocs("Acura RLX 2017", "Advance Package or Technology Package"), CommunityCarDocs("Acura TLX 2015-17", "Advance Package"), - CommunityCarDocs("Acura TLX 2018-20", "All"), - CommunityCarDocs("Acura TLX 2022-23", "All"), - GMSecurityCarDocs("Acura ZDX 2024", "All"), + CommunityCarDocs("Acura TLX 2018-20"), + CommunityCarDocs("Acura TLX 2022-23"), + GMSecurityCarDocs("Acura ZDX 2024"), CommunityCarDocs("Honda Accord 2016-17", "Honda Sensing"), - CommunityCarDocs("Honda Clarity 2018-21", "All"), - GMSecurityCarDocs("Honda Prologue 2024-25", "All"), + CommunityCarDocs("Honda Clarity 2018-21"), + GMSecurityCarDocs("Honda Prologue 2024-25"), ], ) @@ -78,8 +78,8 @@ class CAR(Platforms): EXTRA_VOLKSWAGEN = ExtraPlatformConfig( [ - FlexRayCarDocs("Audi A4 2016-24", package="All"), - FlexRayCarDocs("Audi A5 2016-24", package="All"), - FlexRayCarDocs("Audi Q5 2017-24", package="All"), + FlexRayCarDocs("Audi A4 2016-24"), + FlexRayCarDocs("Audi A5 2016-24"), + FlexRayCarDocs("Audi Q5 2017-24"), ], ) diff --git a/opendbc/car/subaru/fingerprints.py b/opendbc/car/subaru/fingerprints.py index 64158446bd3..9addabaa1e9 100644 --- a/opendbc/car/subaru/fingerprints.py +++ b/opendbc/car/subaru/fingerprints.py @@ -97,6 +97,7 @@ b'\xa2 \x193\x00', b'\xa2 \x194\x00', b'\xa2 \x19`\x00', + b'\xa2 5\x00', ], (Ecu.eps, 0x746, None): [ b'z\xc0\x00\x00', @@ -106,6 +107,7 @@ b'z\xc0\x0c\x00', b'\x8a\xc0\x00\x00', b'\x8a\xc0\x10\x00', + b'\x9a\xc0\x08\x00', ], (Ecu.fwdCamera, 0x787, None): [ b'\x00\x00c\xf4\x00\x00\x00\x00', @@ -122,6 +124,7 @@ b'\x00\x00e\x1c\x1f@ \x14', b'\x00\x00e+\x00\x00\x00\x00', b'\x00\x00e+\x1f@ \x14', + b'\x00\x00eq\x1f@ "', ], (Ecu.engine, 0x7e0, None): [ b'\xaa\x00Bu\x07', @@ -145,6 +148,7 @@ b'\xc5!as\x07', b'\xc5!dr\x07', b'\xc5!ds\x07', + b'\xca\x01b0\x07', ], (Ecu.transmission, 0x7e1, None): [ b'\xe3\xd0\x081\x00', @@ -163,6 +167,7 @@ b'\xe5\xf5\x04\x00\x00', b'\xe5\xf5$\x00\x00', b'\xe5\xf5B\x00\x00', + b'\xe6\xd5\x041\x00', ], }, CAR.SUBARU_IMPREZA_2020: { diff --git a/opendbc/car/tesla/carcontroller.py b/opendbc/car/tesla/carcontroller.py index 986897f8494..8b7d9989c44 100644 --- a/opendbc/car/tesla/carcontroller.py +++ b/opendbc/car/tesla/carcontroller.py @@ -20,7 +20,7 @@ def __init__(self, dbc_names, CP): super().__init__(dbc_names, CP) self.apply_angle_last = 0 self.packer = CANPacker(dbc_names[Bus.party]) - self.tesla_can = TeslaCAN(self.packer) + self.tesla_can = TeslaCAN(CP, self.packer) # Vehicle model used for lateral limiting self.VM = VehicleModel(get_safety_CP()) diff --git a/opendbc/car/tesla/carstate.py b/opendbc/car/tesla/carstate.py index fa16116b3b4..2e56a8b033e 100644 --- a/opendbc/car/tesla/carstate.py +++ b/opendbc/car/tesla/carstate.py @@ -3,6 +3,7 @@ from opendbc.car import Bus, structs from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarStateBase +from opendbc.car.tesla.teslacan import get_steer_ctrl_type from opendbc.car.tesla.values import DBC, CANBUS, GEAR_MAP, STEER_THRESHOLD, CAR ButtonType = structs.CarState.ButtonEvent.Type @@ -105,7 +106,11 @@ def update(self, can_parsers) -> structs.CarState: ret.stockAeb = cp_ap_party.vl["DAS_control"]["DAS_aebEvent"] == 1 # LKAS - ret.stockLkas = cp_ap_party.vl["DAS_steeringControl"]["DAS_steeringControlType"] == 2 # LANE_KEEP_ASSIST + # On FSD 14+, ANGLE_CONTROL behavior changed to allow user winddown while actuating. + # FSD switched from using ANGLE_CONTROL to LANE_KEEP_ASSIST to likely keep the old steering override disengage logic. + # LKAS switched from LANE_KEEP_ASSIST to ANGLE_CONTROL to likely allow overriding LKAS events smoothly + lkas_ctrl_type = get_steer_ctrl_type(self.CP.flags, 2) + ret.stockLkas = cp_ap_party.vl["DAS_steeringControl"]["DAS_steeringControlType"] == lkas_ctrl_type # LANE_KEEP_ASSIST # Stock Autosteer should be off (includes FSD) if self.CP.carFingerprint in (CAR.TESLA_MODEL_3, CAR.TESLA_MODEL_Y): diff --git a/opendbc/car/tesla/fingerprints.py b/opendbc/car/tesla/fingerprints.py index 54705385a6e..2ebaef0b8fb 100644 --- a/opendbc/car/tesla/fingerprints.py +++ b/opendbc/car/tesla/fingerprints.py @@ -36,6 +36,8 @@ b'TeMYG4_Legacy3Y_0.0.0 (5),Y4P003.03.2', b'TeMYG4_SingleECU_0.0.0 (28),Y4S002.23.0', b'TeMYG4_SingleECU_0.0.0 (33),Y4S002.26', + b'TeMYG4_Legacy3Y_0.0.0 (6),Y4003.04.0', + b'TeMYG4_Main_0.0.0 (77),Y4003.05.4', ], }, CAR.TESLA_MODEL_X: { diff --git a/opendbc/car/tesla/interface.py b/opendbc/car/tesla/interface.py index baf1593238a..c53ad621937 100644 --- a/opendbc/car/tesla/interface.py +++ b/opendbc/car/tesla/interface.py @@ -2,7 +2,7 @@ from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.tesla.carcontroller import CarController from opendbc.car.tesla.carstate import CarState -from opendbc.car.tesla.values import TeslaSafetyFlags, CAR +from opendbc.car.tesla.values import TeslaSafetyFlags, TeslaFlags, CAR, FSD_14_FW, Ecu class CarInterface(CarInterfaceBase): @@ -31,6 +31,11 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.vEgoStarting = 0.1 ret.stoppingDecelRate = 0.3 - ret.dashcamOnly = candidate in (CAR.TESLA_MODEL_X) # dashcam only, pending find invalidLkasSetting signal + fsd_14 = any(fw.ecu == Ecu.eps and fw.fwVersion in FSD_14_FW.get(candidate, []) for fw in car_fw) + if fsd_14: + ret.flags |= TeslaFlags.FSD_14.value + ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.FSD_14.value + + ret.dashcamOnly = candidate in (CAR.TESLA_MODEL_X,) # dashcam only, pending find invalidLkasSetting signal return ret diff --git a/opendbc/car/tesla/teslacan.py b/opendbc/car/tesla/teslacan.py index 4bfc67a4e36..7449b961f17 100644 --- a/opendbc/car/tesla/teslacan.py +++ b/opendbc/car/tesla/teslacan.py @@ -1,16 +1,29 @@ from opendbc.car.common.conversions import Conversions as CV -from opendbc.car.tesla.values import CANBUS, CarControllerParams +from opendbc.car.tesla.values import CANBUS, CarControllerParams, TeslaFlags + + +def get_steer_ctrl_type(flags: int, ctrl_type: int) -> int: + # Returns the flipped signal value for DAS_steeringControlType on FSD 14 + if flags & TeslaFlags.FSD_14: + return {1: 2, 2: 1}.get(ctrl_type, ctrl_type) + else: + return ctrl_type class TeslaCAN: - def __init__(self, packer): + def __init__(self, CP, packer): + self.CP = CP self.packer = packer def create_steering_control(self, angle, enabled): + # On FSD 14+, ANGLE_CONTROL behavior changed to allow user winddown while actuating. + # with openpilot, after overriding w/ ANGLE_CONTROL the wheel snaps back to the original angle abruptly + # so we now use LANE_KEEP_ASSIST to match stock FSD. + # see carstate.py for more details values = { "DAS_steeringAngleRequest": -angle, "DAS_steeringHapticRequest": 0, - "DAS_steeringControlType": 1 if enabled else 0, + "DAS_steeringControlType": get_steer_ctrl_type(self.CP.flags, 1 if enabled else 0), } return self.packer.make_can_msg("DAS_steeringControl", CANBUS.party, values) diff --git a/opendbc/car/tesla/values.py b/opendbc/car/tesla/values.py index 6fedfaac027..92a08d2a87f 100644 --- a/opendbc/car/tesla/values.py +++ b/opendbc/car/tesla/values.py @@ -72,6 +72,14 @@ class CAR(Platforms): ] ) +# Cars with this EPS FW have FSD 14 and use TeslaFlags.FSD_14 +FSD_14_FW = { + CAR.TESLA_MODEL_Y: [ + b'TeMYG4_Legacy3Y_0.0.0 (6),Y4003.04.0', + b'TeMYG4_Main_0.0.0 (77),Y4003.05.4', + ] +} + class CANBUS: party = 0 @@ -119,10 +127,12 @@ class CarControllerParams: class TeslaSafetyFlags(IntFlag): LONG_CONTROL = 1 + FSD_14 = 2 class TeslaFlags(IntFlag): LONG_CONTROL = 1 + FSD_14 = 2 DBC = CAR.create_dbc_map() diff --git a/opendbc/car/toyota/carcontroller.py b/opendbc/car/toyota/carcontroller.py index 627a2e1bfeb..380c64b3af9 100644 --- a/opendbc/car/toyota/carcontroller.py +++ b/opendbc/car/toyota/carcontroller.py @@ -182,13 +182,14 @@ def update(self, CC, CS, now_nanos): # if user engages at a stop with foot on brake, PCM starts in a special cruise standstill mode. on resume press, # brakes can take a while to ramp up causing a lurch forward. prevent resume press until planner wants to move. # don't use CC.cruiseControl.resume since it is gated on CS.cruiseState.standstill which goes false for 3s after resume press - # TODO: hybrids do not have this issue and can stay stopped after resume press, whitelist them - should_resume = actuators.accel > 0 - if should_resume: - self.standstill_req = False - - if not should_resume and CS.out.cruiseState.standstill: - self.standstill_req = True + # whitelist hybrids as they do not have this issue and can stay stopped after resume press + if not self.CP.flags & ToyotaFlags.HYBRID.value: + should_resume = actuators.accel > 0 + if should_resume: + self.standstill_req = False + + if not should_resume and CS.out.cruiseState.standstill: + self.standstill_req = True self.last_standstill = CS.out.standstill diff --git a/opendbc/safety/modes/tesla.h b/opendbc/safety/modes/tesla.h index b8f41f0454b..33f5e14ee58 100644 --- a/opendbc/safety/modes/tesla.h +++ b/opendbc/safety/modes/tesla.h @@ -3,6 +3,7 @@ #include "opendbc/safety/declarations.h" static bool tesla_longitudinal = false; +static bool tesla_fsd_14 = false; static bool tesla_stock_aeb = false; // Only rising edges while controls are not allowed are considered for these systems: @@ -91,6 +92,20 @@ static bool tesla_get_quality_flag_valid(const CANPacket_t *msg) { return valid; } +static int tesla_get_steer_ctrl_type(const int ctrl_type) { + // Returns ANGLE_CONTROL-equivalent control type for FSD 14 + int steer_ctrl_type = ctrl_type; + if (tesla_fsd_14) { + if (ctrl_type == 1) { + steer_ctrl_type = 2; + } else if (ctrl_type == 2) { + steer_ctrl_type = 1; + } else { + } + } + return steer_ctrl_type; +} + static void tesla_rx_hook(const CANPacket_t *msg) { if (msg->bus == 0U) { @@ -176,7 +191,7 @@ static void tesla_rx_hook(const CANPacket_t *msg) { // DAS_steeringControl if (msg->addr == 0x488U) { int steering_control_type = msg->data[2] >> 6; - bool tesla_stock_lkas_now = steering_control_type == 2; // "LANE_KEEP_ASSIST" + bool tesla_stock_lkas_now = steering_control_type == tesla_get_steer_ctrl_type(2); // "LANE_KEEP_ASSIST" // Only consider rising edges while controls are not allowed if (tesla_stock_lkas_now && !tesla_stock_lkas_prev && !controls_allowed) { @@ -225,14 +240,15 @@ static bool tesla_tx_hook(const CANPacket_t *msg) { int raw_angle_can = ((msg->data[0] & 0x7FU) << 8) | msg->data[1]; int desired_angle = raw_angle_can - 16384; int steer_control_type = msg->data[2] >> 6; - bool steer_control_enabled = steer_control_type == 1; // ANGLE_CONTROL + const int angle_ctrl_type = tesla_get_steer_ctrl_type(1); + bool steer_control_enabled = steer_control_type == angle_ctrl_type; // ANGLE_CONTROL if (steer_angle_cmd_checks_vm(desired_angle, steer_control_enabled, TESLA_STEERING_LIMITS, TESLA_STEERING_PARAMS)) { violation = true; } bool valid_steer_control_type = (steer_control_type == 0) || // NONE - (steer_control_type == 1); // ANGLE_CONTROL + (steer_control_type == angle_ctrl_type); // ANGLE_CONTROL if (!valid_steer_control_type) { violation = true; } @@ -328,7 +344,9 @@ static safety_config tesla_init(uint16_t param) { {0x27D, 0, 3, .check_relay = true, .disable_static_blocking = true}, // APS_eacMonitor }; - SAFETY_UNUSED(param); + const uint16_t TESLA_FLAG_FSD_14 = 2; + tesla_fsd_14 = GET_FLAG(param, TESLA_FLAG_FSD_14); + #ifdef ALLOW_DEBUG const uint16_t TESLA_FLAG_LONGITUDINAL_CONTROL = 1; tesla_longitudinal = GET_FLAG(param, TESLA_FLAG_LONGITUDINAL_CONTROL); diff --git a/opendbc/safety/tests/test_tesla.py b/opendbc/safety/tests/test_tesla.py index 52390f81aa8..a6b9265ba50 100755 --- a/opendbc/safety/tests/test_tesla.py +++ b/opendbc/safety/tests/test_tesla.py @@ -4,7 +4,8 @@ import numpy as np from opendbc.car.lateral import get_max_angle_delta_vm, get_max_angle_vm -from opendbc.car.tesla.values import CarControllerParams, TeslaSafetyFlags +from opendbc.car.tesla.teslacan import get_steer_ctrl_type +from opendbc.car.tesla.values import CarControllerParams, TeslaSafetyFlags, TeslaFlags from opendbc.car.tesla.carcontroller import get_safety_CP from opendbc.car.structs import CarParams from opendbc.car.vehicle_model import VehicleModel @@ -26,6 +27,8 @@ def round_angle(apply_angle, can_offset=0): class TestTeslaSafetyBase(common.CarSafetyTest, common.AngleSteeringSafetyTest, common.LongitudinalAccelSafetyTest): + SAFETY_PARAM = 0 + RELAY_MALFUNCTION_ADDRS = {0: (MSG_DAS_steeringControl, MSG_APS_eacMonitor)} FWD_BLACKLISTED_ADDRS = {2: [MSG_DAS_steeringControl, MSG_APS_eacMonitor]} TX_MSGS = [[MSG_DAS_steeringControl, 0], [MSG_APS_eacMonitor, 0], [MSG_DAS_Control, 0]] @@ -69,7 +72,15 @@ def setUp(self): self.steer_control_types = {d: v for v, d in self.define.dv["DAS_steeringControl"]["DAS_steeringControlType"].items()} + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.tesla, self.SAFETY_PARAM) + self.safety.init_tests() + def _angle_cmd_msg(self, angle: float, state: bool | int, increment_timer: bool = True, bus: int = 0): + # If FSD 14, translate steer control type to new flipped definition + if self.safety.get_current_safety_param() & TeslaSafetyFlags.FSD_14: + state = get_steer_ctrl_type(TeslaFlags.FSD_14, int(state)) + values = {"DAS_steeringAngleRequest": angle, "DAS_steeringControlType": state} if increment_timer: self.safety.set_timer(self.cnt_angle_cmd * int(1e6 / self.LATERAL_FREQUENCY)) @@ -358,12 +369,6 @@ class TestTeslaStockSafety(TestTeslaSafetyBase): LONGITUDINAL = False - def setUp(self): - super().setUp() - self.safety = libsafety_py.libsafety - self.safety.set_safety_hooks(CarParams.SafetyModel.tesla, 0) - self.safety.init_tests() - def test_cancel(self): for acc_state in range(16): self.safety.set_controls_allowed(True) @@ -395,16 +400,16 @@ def test_stock_aeb_no_cancel(self): self.assertFalse(self._tx(no_aeb_msg)) +class TestTeslaFSD14StockSafety(TestTeslaStockSafety): + SAFETY_PARAM = TeslaSafetyFlags.FSD_14 + + class TestTeslaLongitudinalSafety(TestTeslaSafetyBase): + SAFETY_PARAM = TeslaSafetyFlags.LONG_CONTROL + RELAY_MALFUNCTION_ADDRS = {0: (MSG_DAS_steeringControl, MSG_APS_eacMonitor, MSG_DAS_Control)} FWD_BLACKLISTED_ADDRS = {2: [MSG_DAS_steeringControl, MSG_APS_eacMonitor, MSG_DAS_Control]} - def setUp(self): - super().setUp() - self.safety = libsafety_py.libsafety - self.safety.set_safety_hooks(CarParams.SafetyModel.tesla, TeslaSafetyFlags.LONG_CONTROL) - self.safety.init_tests() - def test_no_aeb(self): for aeb_event in range(4): self.assertEqual(self._tx(self._long_control_msg(10, aeb_event=aeb_event)), aeb_event == 0) @@ -447,5 +452,9 @@ def test_prevent_reverse(self): self.assertFalse(self._tx(self._long_control_msg(set_speed=0, accel_limits=(-0.1, -0.1)))) +class TestTeslaFSD14LongitudinalSafety(TestTeslaLongitudinalSafety): + SAFETY_PARAM = TeslaSafetyFlags.LONG_CONTROL | TeslaSafetyFlags.FSD_14 + + if __name__ == "__main__": unittest.main() diff --git a/setup.sh b/setup.sh index 0234610f89a..4aee2d16de0 100755 --- a/setup.sh +++ b/setup.sh @@ -39,7 +39,7 @@ if ! command -v uv &>/dev/null; then fi export UV_PROJECT_ENVIRONMENT="$BASEDIR/.venv" -uv sync --all-extras +uv sync --all-extras --inexact source "$PYTHONPATH/.venv/bin/activate" $BASEDIR/opendbc/safety/tests/misra/install.sh From 3fb0d394e72582479ef7bd32f38fe08a577ee20e Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 18:56:35 -0800 Subject: [PATCH 106/117] Revert "impreza fingerprint" This reverts commit 692e34c7f8df6540c26eca79799a8eac47f352bc. --- opendbc/car/docs_definitions.py | 23 ++++++++++--------- opendbc/car/extra_cars.py | 24 ++++++++++---------- opendbc/car/subaru/fingerprints.py | 5 ----- opendbc/car/tesla/carcontroller.py | 2 +- opendbc/car/tesla/carstate.py | 7 +----- opendbc/car/tesla/fingerprints.py | 2 -- opendbc/car/tesla/interface.py | 9 ++------ opendbc/car/tesla/teslacan.py | 19 +++------------- opendbc/car/tesla/values.py | 10 --------- opendbc/car/toyota/carcontroller.py | 15 ++++++------- opendbc/safety/modes/tesla.h | 26 ++++----------------- opendbc/safety/tests/test_tesla.py | 35 +++++++++++------------------ setup.sh | 2 +- 13 files changed, 56 insertions(+), 123 deletions(-) diff --git a/opendbc/car/docs_definitions.py b/opendbc/car/docs_definitions.py index c77573930a8..2f49b834f61 100644 --- a/opendbc/car/docs_definitions.py +++ b/opendbc/car/docs_definitions.py @@ -78,6 +78,7 @@ class Cable(EnumBase): long_obdc_cable = BasePart("long OBD-C cable (9.5 ft)") usb_a_2_a_cable = BasePart("USB A-A cable") usbc_otg_cable = BasePart("USB C OTG cable") + usbc_coupler = BasePart("USB-C coupler") obd_c_cable_2ft = BasePart("OBD-C cable (2 ft)") @@ -111,7 +112,7 @@ class CarHarness(EnumBase): fca = BaseCarHarness("FCA connector") ram = BaseCarHarness("Ram connector") vw_a = BaseCarHarness("VW A connector") - vw_j533 = BaseCarHarness("VW J533 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) + vw_j533 = BaseCarHarness("VW J533 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) hyundai_a = BaseCarHarness("Hyundai A connector") hyundai_b = BaseCarHarness("Hyundai B connector") hyundai_c = BaseCarHarness("Hyundai C connector") @@ -131,18 +132,18 @@ class CarHarness(EnumBase): hyundai_q = BaseCarHarness("Hyundai Q connector") hyundai_r = BaseCarHarness("Hyundai R connector") custom = BaseCarHarness("Developer connector") - obd_ii = BaseCarHarness("OBD-II connector", parts=[Cable.long_obdc_cable], has_connector=False) + obd_ii = BaseCarHarness("OBD-II connector", parts=[Cable.long_obdc_cable, Cable.usbc_coupler], has_connector=False) gm = BaseCarHarness("GM connector", parts=[Accessory.harness_box]) - gmsdgm = BaseCarHarness("GM SDGM connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) - nissan_a = BaseCarHarness("Nissan A connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) - nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) + gmsdgm = BaseCarHarness("GM SDGM connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) + nissan_a = BaseCarHarness("Nissan A connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) + nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) mazda = BaseCarHarness("Mazda connector") ford_q3 = BaseCarHarness("Ford Q3 connector") - ford_q4 = BaseCarHarness("Ford Q4 connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) - rivian = BaseCarHarness("Rivian A connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable]) - tesla_a = BaseCarHarness("Tesla A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) - tesla_b = BaseCarHarness("Tesla B connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) - psa_a = BaseCarHarness("PSA A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable]) + ford_q4 = BaseCarHarness("Ford Q4 connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) + rivian = BaseCarHarness("Rivian A connector", parts=[Accessory.harness_box, Accessory.comma_power, Cable.long_obdc_cable, Cable.usbc_coupler]) + tesla_a = BaseCarHarness("Tesla A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) + tesla_b = BaseCarHarness("Tesla B connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) + psa_a = BaseCarHarness("PSA A connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler]) class Device(EnumBase): @@ -393,7 +394,7 @@ def get_extra_cars_column(self, column: ExtraCarsColumn) -> str: @dataclass class ExtraCarDocs(CarDocs): - package: str = "All" + package: str = "Any" merged: bool = False support_type: SupportType = SupportType.INCOMPATIBLE support_link: str | None = "#incompatible" diff --git a/opendbc/car/extra_cars.py b/opendbc/car/extra_cars.py index fed2f5a4914..5ffa42bf4a3 100644 --- a/opendbc/car/extra_cars.py +++ b/opendbc/car/extra_cars.py @@ -37,19 +37,19 @@ class CAR(Platforms): EXTRA_HONDA = ExtraPlatformConfig( [ - CommunityCarDocs("Acura Integra 2023-25"), + CommunityCarDocs("Acura Integra 2023-25", "All"), CommunityCarDocs("Acura MDX 2015-16", "Advance Package"), - CommunityCarDocs("Acura MDX 2017-20"), - CommunityCarDocs("Acura MDX 2022-24"), - CommunityCarDocs("Acura RDX 2022-25"), + CommunityCarDocs("Acura MDX 2017-20", "All"), + CommunityCarDocs("Acura MDX 2022-24", "All"), + CommunityCarDocs("Acura RDX 2022-25", "All"), CommunityCarDocs("Acura RLX 2017", "Advance Package or Technology Package"), CommunityCarDocs("Acura TLX 2015-17", "Advance Package"), - CommunityCarDocs("Acura TLX 2018-20"), - CommunityCarDocs("Acura TLX 2022-23"), - GMSecurityCarDocs("Acura ZDX 2024"), + CommunityCarDocs("Acura TLX 2018-20", "All"), + CommunityCarDocs("Acura TLX 2022-23", "All"), + GMSecurityCarDocs("Acura ZDX 2024", "All"), CommunityCarDocs("Honda Accord 2016-17", "Honda Sensing"), - CommunityCarDocs("Honda Clarity 2018-21"), - GMSecurityCarDocs("Honda Prologue 2024-25"), + CommunityCarDocs("Honda Clarity 2018-21", "All"), + GMSecurityCarDocs("Honda Prologue 2024-25", "All"), ], ) @@ -78,8 +78,8 @@ class CAR(Platforms): EXTRA_VOLKSWAGEN = ExtraPlatformConfig( [ - FlexRayCarDocs("Audi A4 2016-24"), - FlexRayCarDocs("Audi A5 2016-24"), - FlexRayCarDocs("Audi Q5 2017-24"), + FlexRayCarDocs("Audi A4 2016-24", package="All"), + FlexRayCarDocs("Audi A5 2016-24", package="All"), + FlexRayCarDocs("Audi Q5 2017-24", package="All"), ], ) diff --git a/opendbc/car/subaru/fingerprints.py b/opendbc/car/subaru/fingerprints.py index 9addabaa1e9..64158446bd3 100644 --- a/opendbc/car/subaru/fingerprints.py +++ b/opendbc/car/subaru/fingerprints.py @@ -97,7 +97,6 @@ b'\xa2 \x193\x00', b'\xa2 \x194\x00', b'\xa2 \x19`\x00', - b'\xa2 5\x00', ], (Ecu.eps, 0x746, None): [ b'z\xc0\x00\x00', @@ -107,7 +106,6 @@ b'z\xc0\x0c\x00', b'\x8a\xc0\x00\x00', b'\x8a\xc0\x10\x00', - b'\x9a\xc0\x08\x00', ], (Ecu.fwdCamera, 0x787, None): [ b'\x00\x00c\xf4\x00\x00\x00\x00', @@ -124,7 +122,6 @@ b'\x00\x00e\x1c\x1f@ \x14', b'\x00\x00e+\x00\x00\x00\x00', b'\x00\x00e+\x1f@ \x14', - b'\x00\x00eq\x1f@ "', ], (Ecu.engine, 0x7e0, None): [ b'\xaa\x00Bu\x07', @@ -148,7 +145,6 @@ b'\xc5!as\x07', b'\xc5!dr\x07', b'\xc5!ds\x07', - b'\xca\x01b0\x07', ], (Ecu.transmission, 0x7e1, None): [ b'\xe3\xd0\x081\x00', @@ -167,7 +163,6 @@ b'\xe5\xf5\x04\x00\x00', b'\xe5\xf5$\x00\x00', b'\xe5\xf5B\x00\x00', - b'\xe6\xd5\x041\x00', ], }, CAR.SUBARU_IMPREZA_2020: { diff --git a/opendbc/car/tesla/carcontroller.py b/opendbc/car/tesla/carcontroller.py index 8b7d9989c44..986897f8494 100644 --- a/opendbc/car/tesla/carcontroller.py +++ b/opendbc/car/tesla/carcontroller.py @@ -20,7 +20,7 @@ def __init__(self, dbc_names, CP): super().__init__(dbc_names, CP) self.apply_angle_last = 0 self.packer = CANPacker(dbc_names[Bus.party]) - self.tesla_can = TeslaCAN(CP, self.packer) + self.tesla_can = TeslaCAN(self.packer) # Vehicle model used for lateral limiting self.VM = VehicleModel(get_safety_CP()) diff --git a/opendbc/car/tesla/carstate.py b/opendbc/car/tesla/carstate.py index 2e56a8b033e..fa16116b3b4 100644 --- a/opendbc/car/tesla/carstate.py +++ b/opendbc/car/tesla/carstate.py @@ -3,7 +3,6 @@ from opendbc.car import Bus, structs from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarStateBase -from opendbc.car.tesla.teslacan import get_steer_ctrl_type from opendbc.car.tesla.values import DBC, CANBUS, GEAR_MAP, STEER_THRESHOLD, CAR ButtonType = structs.CarState.ButtonEvent.Type @@ -106,11 +105,7 @@ def update(self, can_parsers) -> structs.CarState: ret.stockAeb = cp_ap_party.vl["DAS_control"]["DAS_aebEvent"] == 1 # LKAS - # On FSD 14+, ANGLE_CONTROL behavior changed to allow user winddown while actuating. - # FSD switched from using ANGLE_CONTROL to LANE_KEEP_ASSIST to likely keep the old steering override disengage logic. - # LKAS switched from LANE_KEEP_ASSIST to ANGLE_CONTROL to likely allow overriding LKAS events smoothly - lkas_ctrl_type = get_steer_ctrl_type(self.CP.flags, 2) - ret.stockLkas = cp_ap_party.vl["DAS_steeringControl"]["DAS_steeringControlType"] == lkas_ctrl_type # LANE_KEEP_ASSIST + ret.stockLkas = cp_ap_party.vl["DAS_steeringControl"]["DAS_steeringControlType"] == 2 # LANE_KEEP_ASSIST # Stock Autosteer should be off (includes FSD) if self.CP.carFingerprint in (CAR.TESLA_MODEL_3, CAR.TESLA_MODEL_Y): diff --git a/opendbc/car/tesla/fingerprints.py b/opendbc/car/tesla/fingerprints.py index 2ebaef0b8fb..54705385a6e 100644 --- a/opendbc/car/tesla/fingerprints.py +++ b/opendbc/car/tesla/fingerprints.py @@ -36,8 +36,6 @@ b'TeMYG4_Legacy3Y_0.0.0 (5),Y4P003.03.2', b'TeMYG4_SingleECU_0.0.0 (28),Y4S002.23.0', b'TeMYG4_SingleECU_0.0.0 (33),Y4S002.26', - b'TeMYG4_Legacy3Y_0.0.0 (6),Y4003.04.0', - b'TeMYG4_Main_0.0.0 (77),Y4003.05.4', ], }, CAR.TESLA_MODEL_X: { diff --git a/opendbc/car/tesla/interface.py b/opendbc/car/tesla/interface.py index c53ad621937..baf1593238a 100644 --- a/opendbc/car/tesla/interface.py +++ b/opendbc/car/tesla/interface.py @@ -2,7 +2,7 @@ from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.tesla.carcontroller import CarController from opendbc.car.tesla.carstate import CarState -from opendbc.car.tesla.values import TeslaSafetyFlags, TeslaFlags, CAR, FSD_14_FW, Ecu +from opendbc.car.tesla.values import TeslaSafetyFlags, CAR class CarInterface(CarInterfaceBase): @@ -31,11 +31,6 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.vEgoStarting = 0.1 ret.stoppingDecelRate = 0.3 - fsd_14 = any(fw.ecu == Ecu.eps and fw.fwVersion in FSD_14_FW.get(candidate, []) for fw in car_fw) - if fsd_14: - ret.flags |= TeslaFlags.FSD_14.value - ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.FSD_14.value - - ret.dashcamOnly = candidate in (CAR.TESLA_MODEL_X,) # dashcam only, pending find invalidLkasSetting signal + ret.dashcamOnly = candidate in (CAR.TESLA_MODEL_X) # dashcam only, pending find invalidLkasSetting signal return ret diff --git a/opendbc/car/tesla/teslacan.py b/opendbc/car/tesla/teslacan.py index 7449b961f17..4bfc67a4e36 100644 --- a/opendbc/car/tesla/teslacan.py +++ b/opendbc/car/tesla/teslacan.py @@ -1,29 +1,16 @@ from opendbc.car.common.conversions import Conversions as CV -from opendbc.car.tesla.values import CANBUS, CarControllerParams, TeslaFlags - - -def get_steer_ctrl_type(flags: int, ctrl_type: int) -> int: - # Returns the flipped signal value for DAS_steeringControlType on FSD 14 - if flags & TeslaFlags.FSD_14: - return {1: 2, 2: 1}.get(ctrl_type, ctrl_type) - else: - return ctrl_type +from opendbc.car.tesla.values import CANBUS, CarControllerParams class TeslaCAN: - def __init__(self, CP, packer): - self.CP = CP + def __init__(self, packer): self.packer = packer def create_steering_control(self, angle, enabled): - # On FSD 14+, ANGLE_CONTROL behavior changed to allow user winddown while actuating. - # with openpilot, after overriding w/ ANGLE_CONTROL the wheel snaps back to the original angle abruptly - # so we now use LANE_KEEP_ASSIST to match stock FSD. - # see carstate.py for more details values = { "DAS_steeringAngleRequest": -angle, "DAS_steeringHapticRequest": 0, - "DAS_steeringControlType": get_steer_ctrl_type(self.CP.flags, 1 if enabled else 0), + "DAS_steeringControlType": 1 if enabled else 0, } return self.packer.make_can_msg("DAS_steeringControl", CANBUS.party, values) diff --git a/opendbc/car/tesla/values.py b/opendbc/car/tesla/values.py index 92a08d2a87f..6fedfaac027 100644 --- a/opendbc/car/tesla/values.py +++ b/opendbc/car/tesla/values.py @@ -72,14 +72,6 @@ class CAR(Platforms): ] ) -# Cars with this EPS FW have FSD 14 and use TeslaFlags.FSD_14 -FSD_14_FW = { - CAR.TESLA_MODEL_Y: [ - b'TeMYG4_Legacy3Y_0.0.0 (6),Y4003.04.0', - b'TeMYG4_Main_0.0.0 (77),Y4003.05.4', - ] -} - class CANBUS: party = 0 @@ -127,12 +119,10 @@ class CarControllerParams: class TeslaSafetyFlags(IntFlag): LONG_CONTROL = 1 - FSD_14 = 2 class TeslaFlags(IntFlag): LONG_CONTROL = 1 - FSD_14 = 2 DBC = CAR.create_dbc_map() diff --git a/opendbc/car/toyota/carcontroller.py b/opendbc/car/toyota/carcontroller.py index 380c64b3af9..627a2e1bfeb 100644 --- a/opendbc/car/toyota/carcontroller.py +++ b/opendbc/car/toyota/carcontroller.py @@ -182,14 +182,13 @@ def update(self, CC, CS, now_nanos): # if user engages at a stop with foot on brake, PCM starts in a special cruise standstill mode. on resume press, # brakes can take a while to ramp up causing a lurch forward. prevent resume press until planner wants to move. # don't use CC.cruiseControl.resume since it is gated on CS.cruiseState.standstill which goes false for 3s after resume press - # whitelist hybrids as they do not have this issue and can stay stopped after resume press - if not self.CP.flags & ToyotaFlags.HYBRID.value: - should_resume = actuators.accel > 0 - if should_resume: - self.standstill_req = False - - if not should_resume and CS.out.cruiseState.standstill: - self.standstill_req = True + # TODO: hybrids do not have this issue and can stay stopped after resume press, whitelist them + should_resume = actuators.accel > 0 + if should_resume: + self.standstill_req = False + + if not should_resume and CS.out.cruiseState.standstill: + self.standstill_req = True self.last_standstill = CS.out.standstill diff --git a/opendbc/safety/modes/tesla.h b/opendbc/safety/modes/tesla.h index 33f5e14ee58..b8f41f0454b 100644 --- a/opendbc/safety/modes/tesla.h +++ b/opendbc/safety/modes/tesla.h @@ -3,7 +3,6 @@ #include "opendbc/safety/declarations.h" static bool tesla_longitudinal = false; -static bool tesla_fsd_14 = false; static bool tesla_stock_aeb = false; // Only rising edges while controls are not allowed are considered for these systems: @@ -92,20 +91,6 @@ static bool tesla_get_quality_flag_valid(const CANPacket_t *msg) { return valid; } -static int tesla_get_steer_ctrl_type(const int ctrl_type) { - // Returns ANGLE_CONTROL-equivalent control type for FSD 14 - int steer_ctrl_type = ctrl_type; - if (tesla_fsd_14) { - if (ctrl_type == 1) { - steer_ctrl_type = 2; - } else if (ctrl_type == 2) { - steer_ctrl_type = 1; - } else { - } - } - return steer_ctrl_type; -} - static void tesla_rx_hook(const CANPacket_t *msg) { if (msg->bus == 0U) { @@ -191,7 +176,7 @@ static void tesla_rx_hook(const CANPacket_t *msg) { // DAS_steeringControl if (msg->addr == 0x488U) { int steering_control_type = msg->data[2] >> 6; - bool tesla_stock_lkas_now = steering_control_type == tesla_get_steer_ctrl_type(2); // "LANE_KEEP_ASSIST" + bool tesla_stock_lkas_now = steering_control_type == 2; // "LANE_KEEP_ASSIST" // Only consider rising edges while controls are not allowed if (tesla_stock_lkas_now && !tesla_stock_lkas_prev && !controls_allowed) { @@ -240,15 +225,14 @@ static bool tesla_tx_hook(const CANPacket_t *msg) { int raw_angle_can = ((msg->data[0] & 0x7FU) << 8) | msg->data[1]; int desired_angle = raw_angle_can - 16384; int steer_control_type = msg->data[2] >> 6; - const int angle_ctrl_type = tesla_get_steer_ctrl_type(1); - bool steer_control_enabled = steer_control_type == angle_ctrl_type; // ANGLE_CONTROL + bool steer_control_enabled = steer_control_type == 1; // ANGLE_CONTROL if (steer_angle_cmd_checks_vm(desired_angle, steer_control_enabled, TESLA_STEERING_LIMITS, TESLA_STEERING_PARAMS)) { violation = true; } bool valid_steer_control_type = (steer_control_type == 0) || // NONE - (steer_control_type == angle_ctrl_type); // ANGLE_CONTROL + (steer_control_type == 1); // ANGLE_CONTROL if (!valid_steer_control_type) { violation = true; } @@ -344,9 +328,7 @@ static safety_config tesla_init(uint16_t param) { {0x27D, 0, 3, .check_relay = true, .disable_static_blocking = true}, // APS_eacMonitor }; - const uint16_t TESLA_FLAG_FSD_14 = 2; - tesla_fsd_14 = GET_FLAG(param, TESLA_FLAG_FSD_14); - + SAFETY_UNUSED(param); #ifdef ALLOW_DEBUG const uint16_t TESLA_FLAG_LONGITUDINAL_CONTROL = 1; tesla_longitudinal = GET_FLAG(param, TESLA_FLAG_LONGITUDINAL_CONTROL); diff --git a/opendbc/safety/tests/test_tesla.py b/opendbc/safety/tests/test_tesla.py index a6b9265ba50..52390f81aa8 100755 --- a/opendbc/safety/tests/test_tesla.py +++ b/opendbc/safety/tests/test_tesla.py @@ -4,8 +4,7 @@ import numpy as np from opendbc.car.lateral import get_max_angle_delta_vm, get_max_angle_vm -from opendbc.car.tesla.teslacan import get_steer_ctrl_type -from opendbc.car.tesla.values import CarControllerParams, TeslaSafetyFlags, TeslaFlags +from opendbc.car.tesla.values import CarControllerParams, TeslaSafetyFlags from opendbc.car.tesla.carcontroller import get_safety_CP from opendbc.car.structs import CarParams from opendbc.car.vehicle_model import VehicleModel @@ -27,8 +26,6 @@ def round_angle(apply_angle, can_offset=0): class TestTeslaSafetyBase(common.CarSafetyTest, common.AngleSteeringSafetyTest, common.LongitudinalAccelSafetyTest): - SAFETY_PARAM = 0 - RELAY_MALFUNCTION_ADDRS = {0: (MSG_DAS_steeringControl, MSG_APS_eacMonitor)} FWD_BLACKLISTED_ADDRS = {2: [MSG_DAS_steeringControl, MSG_APS_eacMonitor]} TX_MSGS = [[MSG_DAS_steeringControl, 0], [MSG_APS_eacMonitor, 0], [MSG_DAS_Control, 0]] @@ -72,15 +69,7 @@ def setUp(self): self.steer_control_types = {d: v for v, d in self.define.dv["DAS_steeringControl"]["DAS_steeringControlType"].items()} - self.safety = libsafety_py.libsafety - self.safety.set_safety_hooks(CarParams.SafetyModel.tesla, self.SAFETY_PARAM) - self.safety.init_tests() - def _angle_cmd_msg(self, angle: float, state: bool | int, increment_timer: bool = True, bus: int = 0): - # If FSD 14, translate steer control type to new flipped definition - if self.safety.get_current_safety_param() & TeslaSafetyFlags.FSD_14: - state = get_steer_ctrl_type(TeslaFlags.FSD_14, int(state)) - values = {"DAS_steeringAngleRequest": angle, "DAS_steeringControlType": state} if increment_timer: self.safety.set_timer(self.cnt_angle_cmd * int(1e6 / self.LATERAL_FREQUENCY)) @@ -369,6 +358,12 @@ class TestTeslaStockSafety(TestTeslaSafetyBase): LONGITUDINAL = False + def setUp(self): + super().setUp() + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.tesla, 0) + self.safety.init_tests() + def test_cancel(self): for acc_state in range(16): self.safety.set_controls_allowed(True) @@ -400,16 +395,16 @@ def test_stock_aeb_no_cancel(self): self.assertFalse(self._tx(no_aeb_msg)) -class TestTeslaFSD14StockSafety(TestTeslaStockSafety): - SAFETY_PARAM = TeslaSafetyFlags.FSD_14 - - class TestTeslaLongitudinalSafety(TestTeslaSafetyBase): - SAFETY_PARAM = TeslaSafetyFlags.LONG_CONTROL - RELAY_MALFUNCTION_ADDRS = {0: (MSG_DAS_steeringControl, MSG_APS_eacMonitor, MSG_DAS_Control)} FWD_BLACKLISTED_ADDRS = {2: [MSG_DAS_steeringControl, MSG_APS_eacMonitor, MSG_DAS_Control]} + def setUp(self): + super().setUp() + self.safety = libsafety_py.libsafety + self.safety.set_safety_hooks(CarParams.SafetyModel.tesla, TeslaSafetyFlags.LONG_CONTROL) + self.safety.init_tests() + def test_no_aeb(self): for aeb_event in range(4): self.assertEqual(self._tx(self._long_control_msg(10, aeb_event=aeb_event)), aeb_event == 0) @@ -452,9 +447,5 @@ def test_prevent_reverse(self): self.assertFalse(self._tx(self._long_control_msg(set_speed=0, accel_limits=(-0.1, -0.1)))) -class TestTeslaFSD14LongitudinalSafety(TestTeslaLongitudinalSafety): - SAFETY_PARAM = TeslaSafetyFlags.LONG_CONTROL | TeslaSafetyFlags.FSD_14 - - if __name__ == "__main__": unittest.main() diff --git a/setup.sh b/setup.sh index 4aee2d16de0..0234610f89a 100755 --- a/setup.sh +++ b/setup.sh @@ -39,7 +39,7 @@ if ! command -v uv &>/dev/null; then fi export UV_PROJECT_ENVIRONMENT="$BASEDIR/.venv" -uv sync --all-extras --inexact +uv sync --all-extras source "$PYTHONPATH/.venv/bin/activate" $BASEDIR/opendbc/safety/tests/misra/install.sh From 8cef72d54e0ace5bf54083f61de94daf7a7ebf8d Mon Sep 17 00:00:00 2001 From: elkoled Date: Tue, 6 Jan 2026 20:30:30 -0800 Subject: [PATCH 107/117] apply tesla brake PR --- opendbc/car/tesla/carstate.py | 2 +- opendbc/safety/modes/tesla.h | 20 ++++++++++---------- opendbc/safety/tests/test_tesla.py | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/opendbc/car/tesla/carstate.py b/opendbc/car/tesla/carstate.py index fa16116b3b4..fa51cd9e66c 100644 --- a/opendbc/car/tesla/carstate.py +++ b/opendbc/car/tesla/carstate.py @@ -44,7 +44,7 @@ def update(self, can_parsers) -> structs.CarState: # Brake pedal ret.brake = 0 - ret.brakePressed = cp_party.vl["IBST_status"]["IBST_driverBrakeApply"] == 2 + ret.brakePressed = cp_party.vl["ESP_status"]["ESP_driverBrakeApply"] == 2 # Steering wheel epas_status = cp_party.vl["EPAS3S_sysStatus"] diff --git a/opendbc/safety/modes/tesla.h b/opendbc/safety/modes/tesla.h index b8f41f0454b..481270077a7 100644 --- a/opendbc/safety/modes/tesla.h +++ b/opendbc/safety/modes/tesla.h @@ -23,8 +23,8 @@ static uint8_t tesla_get_counter(const CANPacket_t *msg) { } else if (msg->addr == 0x488U) { // Signal: DAS_steeringControlCounter cnt = msg->data[2] & 0x0FU; - } else if ((msg->addr == 0x257U) || (msg->addr == 0x118U) || (msg->addr == 0x39dU) || (msg->addr == 0x286U) || (msg->addr == 0x311U)) { - // Signal: DI_speedCounter, DI_systemStatusCounter, IBST_statusCounter, DI_locStatusCounter, UI_warningCounter + } else if ((msg->addr == 0x257U) || (msg->addr == 0x118U) || (msg->addr == 0x145U) || (msg->addr == 0x286U) || (msg->addr == 0x311U)) { + // Signal: DI_speedCounter, DI_systemStatusCounter, ESP_statusCounter, DI_locStatusCounter, UI_warningCounter cnt = msg->data[1] & 0x0FU; } else if (msg->addr == 0x155U) { // Signal: ESP_wheelRotationCounter @@ -45,8 +45,8 @@ static int _tesla_get_checksum_byte(const int addr) { } else if (addr == 0x488) { // Signal: DAS_steeringControlChecksum checksum_byte = 3; - } else if ((addr == 0x257) || (addr == 0x118) || (addr == 0x39d) || (addr == 0x286) || (addr == 0x311)) { - // Signal: DI_speedChecksum, DI_systemStatusChecksum, IBST_statusChecksum, DI_locStatusChecksum, UI_warningChecksum + } else if ((addr == 0x257) || (addr == 0x118) || (addr == 0x145) || (addr == 0x286) || (addr == 0x311)) { + // Signal: DI_speedChecksum, DI_systemStatusChecksum, ESP_statusChecksum, DI_locStatusChecksum, UI_warningChecksum checksum_byte = 0; } else { } @@ -83,9 +83,9 @@ static bool tesla_get_quality_flag_valid(const CANPacket_t *msg) { bool valid = false; if (msg->addr == 0x155U) { valid = (msg->data[5] & 0x1U) == 0x1U; // ESP_wheelSpeedsQF - } else if (msg->addr == 0x39dU) { - int user_brake_status = msg->data[2] & 0x03U; - valid = (user_brake_status != 0) && (user_brake_status != 3); // IBST_driverBrakeApply=NOT_INIT_OR_OFF, FAULT + } else if (msg->addr == 0x145U) { + int user_brake_status = (msg->data[3] >> 5) & 0x03U; + valid = (user_brake_status != 0) && (user_brake_status != 3); // ESP_driverBrakeApply=NotInit_orOff, Faulty_SNA } else { } return valid; @@ -128,8 +128,8 @@ static void tesla_rx_hook(const CANPacket_t *msg) { } // Brake pressed - if (msg->addr == 0x39dU) { - brake_pressed = (msg->data[2] & 0x03U) == 2U; + if (msg->addr == 0x145U) { + brake_pressed = ((msg->data[3] >> 5) & 0x03U) == 2U; } // Cruise and Autopark/Summon state @@ -349,7 +349,7 @@ static safety_config tesla_init(uint16_t param) { {.msg = {{0x155, 0, 8, 50U, .max_counter = 15U}, { 0 }, { 0 }}}, // ESP_B (2nd speed in kph) {.msg = {{0x370, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // EPAS3S_sysStatus (steering angle) {.msg = {{0x118, 0, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // DI_systemStatus (gas pedal) - {.msg = {{0x39d, 0, 5, 25U, .max_counter = 15U}, { 0 }, { 0 }}}, // IBST_status (brakes) + {.msg = {{0x145, 0, 8, 50U, .max_counter = 15U}, { 0 }, { 0 }}}, // ESP_status (brakes) {.msg = {{0x286, 0, 8, 10U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // DI_state (acc state) {.msg = {{0x311, 0, 7, 10U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, // UI_warning (blinkers, buckle switch & doors) }; diff --git a/opendbc/safety/tests/test_tesla.py b/opendbc/safety/tests/test_tesla.py index 52390f81aa8..518b594d05d 100755 --- a/opendbc/safety/tests/test_tesla.py +++ b/opendbc/safety/tests/test_tesla.py @@ -84,10 +84,10 @@ def _angle_meas_msg(self, angle: float, hands_on_level: int = 0, eac_status: int return self.packer.make_can_msg_safety("EPAS3S_sysStatus", 0, values) def _user_brake_msg(self, brake, quality_flag: bool = True): - values = {"IBST_driverBrakeApply": 2 if brake else 1} + values = {"ESP_driverBrakeApply": 2 if brake else 1} if not quality_flag: - values["IBST_driverBrakeApply"] = random.choice((0, 3)) # NOT_INIT_OR_OFF, FAULT - return self.packer.make_can_msg_safety("IBST_status", 0, values) + values["ESP_driverBrakeApply"] = random.choice((0, 3)) # NotInit_orOff, Faulty_SNA + return self.packer.make_can_msg_safety("ESP_status", 0, values) def _speed_msg(self, speed): values = {"DI_vehicleSpeed": speed * 3.6} From 91c4a5d3c896cf0936c5cf00a484127b9665d21c Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 19:29:09 -0800 Subject: [PATCH 108/117] honda clear startup fault --- opendbc/car/honda/carstate.py | 17 ++++++++++++++++- opendbc/car/honda/fingerprints.py | 8 ++++++++ opendbc/car/honda/interface.py | 5 +++++ opendbc/car/honda/values.py | 5 +++++ opendbc/car/tests/routes.py | 1 + opendbc/car/torque_data/override.toml | 1 + 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/opendbc/car/honda/carstate.py b/opendbc/car/honda/carstate.py index 334491968fe..69539d456a4 100644 --- a/opendbc/car/honda/carstate.py +++ b/opendbc/car/honda/carstate.py @@ -2,7 +2,7 @@ from collections import defaultdict from opendbc.can import CANDefine, CANParser -from opendbc.car import Bus, create_button_events, structs +from opendbc.car import Bus, create_button_events, structs, DT_CTRL from opendbc.car.common.conversions import Conversions as CV from opendbc.car.honda.hondacan import CanBus from opendbc.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, HONDA_BOSCH_ALT_RADAR, HONDA_BOSCH_CANFD, \ @@ -49,6 +49,9 @@ def __init__(self, CP): # However, on cars without a digital speedometer this is not always present (HRV, FIT, CRV 2016, ILX and RDX) self.dash_speed_seen = False + self.initial_accFault_cleared = False + self.initial_accFault_cleared_timer = int(10 / DT_CTRL) # 10 seconds after startup for initial faults to clear + def update(self, can_parsers) -> structs.CarState: cp = can_parsers[Bus.pt] cp_cam = can_parsers[Bus.cam] @@ -187,6 +190,18 @@ def update(self, can_parsers) -> structs.CarState: ret.cruiseState.enabled = cp.vl["POWERTRAIN_DATA"]["ACC_STATUS"] != 0 ret.cruiseState.available = bool(cp.vl[self.car_state_scm_msg]["MAIN_ON"]) + # Bosch cars take a few minutes after startup to clear prior faults + if ret.accFaulted: + if (self.CP.carFingerprint in HONDA_BOSCH) and not self.initial_accFault_cleared: + # block via cruiseState since accFaulted is not reversible until offroad + ret.accFaulted = False + ret.cruiseState.available = False + elif self.initial_accFault_cleared_timer == 0: + self.initial_accFault_cleared = True + + if self.initial_accFault_cleared_timer > 0: + self.initial_accFault_cleared_timer -= 1 + # Gets rid of Pedal Grinding noise when brake is pressed at slow speeds for some models if self.CP.carFingerprint in (CAR.HONDA_PILOT, CAR.HONDA_RIDGELINE): if ret.brake > 0.1: diff --git a/opendbc/car/honda/fingerprints.py b/opendbc/car/honda/fingerprints.py index 9b5b68992e2..3eb54045f78 100644 --- a/opendbc/car/honda/fingerprints.py +++ b/opendbc/car/honda/fingerprints.py @@ -1067,4 +1067,12 @@ b'36161-TGV-A030\x00\x00', ], }, + CAR.ACURA_TLX_2G_MMR: { + (Ecu.fwdRadar, 0x18dab0f1, None): [ + b'8S302-TGV-A030\x00\x00', + ], + (Ecu.fwdCamera, 0x18dab5f1, None): [ + b'8S102-TGV-A030\x00\x00', + ], + }, } diff --git a/opendbc/car/honda/interface.py b/opendbc/car/honda/interface.py index 59e300c4f07..18fd2fbc8c4 100755 --- a/opendbc/car/honda/interface.py +++ b/opendbc/car/honda/interface.py @@ -191,6 +191,11 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo # When using stock ACC, the radar intercepts and filters steering commands the EPS would otherwise accept ret.minSteerSpeed = 70. * CV.KPH_TO_MS + elif candidate == CAR.ACURA_TLX_2G_MMR: + ret.steerActuatorDelay = 0.15 + ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]] + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + else: ret.steerActuatorDelay = 0.15 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]] diff --git a/opendbc/car/honda/values.py b/opendbc/car/honda/values.py index 3cdccc1eb24..01362b31acc 100644 --- a/opendbc/car/honda/values.py +++ b/opendbc/car/honda/values.py @@ -277,6 +277,11 @@ class CAR(Platforms): {Bus.pt: 'honda_civic_hatchback_ex_2017_can_generated'}, flags=HondaFlags.BOSCH_ALT_RADAR, ) + # mid-model refresh + ACURA_TLX_2G_MMR = HondaBoschCANFDPlatformConfig( + [HondaCarDocs("Acura TLX 2025", "All")], + CarSpecs(mass=3990 * CV.LB_TO_KG, wheelbase=2.87, centerToFrontRatio=0.43, steerRatio=14.2), + ) # Nidec Cars ACURA_ILX = HondaNidecPlatformConfig( diff --git a/opendbc/car/tests/routes.py b/opendbc/car/tests/routes.py index 3fef40ef93a..51a08629db2 100644 --- a/opendbc/car/tests/routes.py +++ b/opendbc/car/tests/routes.py @@ -127,6 +127,7 @@ class CarTestRoute(NamedTuple): # 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/000001e3--597b055ad7", 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), diff --git a/opendbc/car/torque_data/override.toml b/opendbc/car/torque_data/override.toml index 69cc6048d66..f55889e5291 100644 --- a/opendbc/car/torque_data/override.toml +++ b/opendbc/car/torque_data/override.toml @@ -91,6 +91,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "HONDA_ODYSSEY_5G_MMR" = [0.9, 0.9, 0.2] "HONDA_NBOX_2G" = [1.2, 1.2, 0.2] "ACURA_TLX_2G" = [1.2, 1.2, 0.15] +"ACURA_TLX_2G_MMR" = [1.6, 1.6, 0.14] "PORSCHE_MACAN_MK1" = [2.0, 2.0, 0.2] # Dashcam or fallback configured as ideal car From 661513eecd75fa9311884f2f23c67e6d386587a0 Mon Sep 17 00:00:00 2001 From: elkoled Date: Fri, 16 Jan 2026 19:30:31 -0800 Subject: [PATCH 109/117] Revert "honda clear startup fault" This reverts commit 91c4a5d3c896cf0936c5cf00a484127b9665d21c. --- opendbc/car/honda/carstate.py | 17 +---------------- opendbc/car/honda/fingerprints.py | 8 -------- opendbc/car/honda/interface.py | 5 ----- opendbc/car/honda/values.py | 5 ----- opendbc/car/tests/routes.py | 1 - opendbc/car/torque_data/override.toml | 1 - 6 files changed, 1 insertion(+), 36 deletions(-) diff --git a/opendbc/car/honda/carstate.py b/opendbc/car/honda/carstate.py index 69539d456a4..334491968fe 100644 --- a/opendbc/car/honda/carstate.py +++ b/opendbc/car/honda/carstate.py @@ -2,7 +2,7 @@ from collections import defaultdict from opendbc.can import CANDefine, CANParser -from opendbc.car import Bus, create_button_events, structs, DT_CTRL +from opendbc.car import Bus, create_button_events, structs from opendbc.car.common.conversions import Conversions as CV from opendbc.car.honda.hondacan import CanBus from opendbc.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, HONDA_BOSCH_ALT_RADAR, HONDA_BOSCH_CANFD, \ @@ -49,9 +49,6 @@ def __init__(self, CP): # However, on cars without a digital speedometer this is not always present (HRV, FIT, CRV 2016, ILX and RDX) self.dash_speed_seen = False - self.initial_accFault_cleared = False - self.initial_accFault_cleared_timer = int(10 / DT_CTRL) # 10 seconds after startup for initial faults to clear - def update(self, can_parsers) -> structs.CarState: cp = can_parsers[Bus.pt] cp_cam = can_parsers[Bus.cam] @@ -190,18 +187,6 @@ def update(self, can_parsers) -> structs.CarState: ret.cruiseState.enabled = cp.vl["POWERTRAIN_DATA"]["ACC_STATUS"] != 0 ret.cruiseState.available = bool(cp.vl[self.car_state_scm_msg]["MAIN_ON"]) - # Bosch cars take a few minutes after startup to clear prior faults - if ret.accFaulted: - if (self.CP.carFingerprint in HONDA_BOSCH) and not self.initial_accFault_cleared: - # block via cruiseState since accFaulted is not reversible until offroad - ret.accFaulted = False - ret.cruiseState.available = False - elif self.initial_accFault_cleared_timer == 0: - self.initial_accFault_cleared = True - - if self.initial_accFault_cleared_timer > 0: - self.initial_accFault_cleared_timer -= 1 - # Gets rid of Pedal Grinding noise when brake is pressed at slow speeds for some models if self.CP.carFingerprint in (CAR.HONDA_PILOT, CAR.HONDA_RIDGELINE): if ret.brake > 0.1: diff --git a/opendbc/car/honda/fingerprints.py b/opendbc/car/honda/fingerprints.py index 3eb54045f78..9b5b68992e2 100644 --- a/opendbc/car/honda/fingerprints.py +++ b/opendbc/car/honda/fingerprints.py @@ -1067,12 +1067,4 @@ b'36161-TGV-A030\x00\x00', ], }, - CAR.ACURA_TLX_2G_MMR: { - (Ecu.fwdRadar, 0x18dab0f1, None): [ - b'8S302-TGV-A030\x00\x00', - ], - (Ecu.fwdCamera, 0x18dab5f1, None): [ - b'8S102-TGV-A030\x00\x00', - ], - }, } diff --git a/opendbc/car/honda/interface.py b/opendbc/car/honda/interface.py index 18fd2fbc8c4..59e300c4f07 100755 --- a/opendbc/car/honda/interface.py +++ b/opendbc/car/honda/interface.py @@ -191,11 +191,6 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo # When using stock ACC, the radar intercepts and filters steering commands the EPS would otherwise accept ret.minSteerSpeed = 70. * CV.KPH_TO_MS - elif candidate == CAR.ACURA_TLX_2G_MMR: - ret.steerActuatorDelay = 0.15 - ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]] - CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) - else: ret.steerActuatorDelay = 0.15 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]] diff --git a/opendbc/car/honda/values.py b/opendbc/car/honda/values.py index 01362b31acc..3cdccc1eb24 100644 --- a/opendbc/car/honda/values.py +++ b/opendbc/car/honda/values.py @@ -277,11 +277,6 @@ class CAR(Platforms): {Bus.pt: 'honda_civic_hatchback_ex_2017_can_generated'}, flags=HondaFlags.BOSCH_ALT_RADAR, ) - # mid-model refresh - ACURA_TLX_2G_MMR = HondaBoschCANFDPlatformConfig( - [HondaCarDocs("Acura TLX 2025", "All")], - CarSpecs(mass=3990 * CV.LB_TO_KG, wheelbase=2.87, centerToFrontRatio=0.43, steerRatio=14.2), - ) # Nidec Cars ACURA_ILX = HondaNidecPlatformConfig( diff --git a/opendbc/car/tests/routes.py b/opendbc/car/tests/routes.py index 51a08629db2..3fef40ef93a 100644 --- a/opendbc/car/tests/routes.py +++ b/opendbc/car/tests/routes.py @@ -127,7 +127,6 @@ class CarTestRoute(NamedTuple): # 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/000001e3--597b055ad7", 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), diff --git a/opendbc/car/torque_data/override.toml b/opendbc/car/torque_data/override.toml index f55889e5291..69cc6048d66 100644 --- a/opendbc/car/torque_data/override.toml +++ b/opendbc/car/torque_data/override.toml @@ -91,7 +91,6 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "HONDA_ODYSSEY_5G_MMR" = [0.9, 0.9, 0.2] "HONDA_NBOX_2G" = [1.2, 1.2, 0.2] "ACURA_TLX_2G" = [1.2, 1.2, 0.15] -"ACURA_TLX_2G_MMR" = [1.6, 1.6, 0.14] "PORSCHE_MACAN_MK1" = [2.0, 2.0, 0.2] # Dashcam or fallback configured as ideal car From c72f64a4897644de83cdb8ca797d76e640ce267b Mon Sep 17 00:00:00 2001 From: elkoled Date: Sun, 18 Jan 2026 14:30:06 -0800 Subject: [PATCH 110/117] skip unrelated edges --- opendbc/car/tests/car_diff.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index a3581cf95f0..904dc1f1315 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -221,11 +221,13 @@ def format_boolean_diffs(diffs): lines = [] for group in group_frames(diffs): master_vals, pr_vals, init, start, end = build_signals(group) + master_rises, master_falls = find_edges(master_vals, init) + pr_rises, pr_falls = find_edges(pr_vals, init) + if bool(master_rises) != bool(pr_rises): + continue lines.append(f"\n frames {start}-{end - 1}") lines.append(render_waveform("master", master_vals, init)) lines.append(render_waveform("PR", pr_vals, init)) - master_rises, master_falls = find_edges(master_vals, init) - pr_rises, pr_falls = find_edges(pr_vals, init) 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: From 5366516c0b21004c334a787a80ed0fdd2abd0b75 Mon Sep 17 00:00:00 2001 From: elkoled Date: Sun, 18 Jan 2026 18:12:24 -0800 Subject: [PATCH 111/117] fix exit --- .github/workflows/tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f86d77e14df..b1d0691dcec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -76,7 +76,12 @@ jobs: scons -j8 - name: Test car diff if: github.event_name == 'pull_request' - run: source setup.sh && python opendbc/car/tests/car_diff.py | tee diff.txt + run: | + source setup.sh + ret=0 + python opendbc/car/tests/car_diff.py > diff.txt || ret=$? + cat diff.txt + exit $ret - name: Comment PR if: github.event_name == 'pull_request' env: From 2ba7e6c045fad1b389c9ee9a6cf29a49649494bb Mon Sep 17 00:00:00 2001 From: elkoled Date: Sun, 18 Jan 2026 19:45:22 -0800 Subject: [PATCH 112/117] fix down edge --- opendbc/car/tests/car_diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 904dc1f1315..7938b06949b 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -223,7 +223,7 @@ def format_boolean_diffs(diffs): master_vals, pr_vals, init, start, end = build_signals(group) master_rises, master_falls = find_edges(master_vals, init) pr_rises, pr_falls = find_edges(pr_vals, init) - if bool(master_rises) != bool(pr_rises): + if bool(master_rises) != bool(pr_rises) or bool(master_falls) != bool(pr_falls): continue lines.append(f"\n frames {start}-{end - 1}") lines.append(render_waveform("master", master_vals, init)) From 5643d25aa4dcdf7c2f6a739f0d45322470634223 Mon Sep 17 00:00:00 2001 From: elkoled Date: Sun, 18 Jan 2026 19:45:40 -0800 Subject: [PATCH 113/117] use hf pip package --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 666f1b3f362..d0d3cd27065 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ [project.optional-dependencies] testing = [ - "comma-car-segments-test @ https://test-files.pythonhosted.org/packages/13/58/1a458fb78d2f9fde9f57b26ad7e0a86b9f2371d33436ef0df9512a5ba8b2/comma_car_segments_test-0.1.0-py3-none-any.whl", + "comma-car-segments @ https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl", "cffi", "gcovr", # FIXME: pytest 9.0.0 doesn't support unittest.SkipTest From 05da4f6dc870e0b5312ea417f103641376a5e249 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 19 Jan 2026 00:38:33 -0800 Subject: [PATCH 114/117] comment on fail --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b1d0691dcec..5df63972c19 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -83,7 +83,7 @@ jobs: cat diff.txt exit $ret - name: Comment PR - if: github.event_name == 'pull_request' + if: ${{ !cancelled() && github.event_name == 'pull_request' }} env: GH_TOKEN: ${{ github.token }} run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' From 49dfefbbb9d0989b186ecfa72879e11da82c8740 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 19 Jan 2026 01:00:45 -0800 Subject: [PATCH 115/117] fix --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5df63972c19..172cf960315 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -83,7 +83,7 @@ jobs: cat diff.txt exit $ret - name: Comment PR - if: ${{ !cancelled() && github.event_name == 'pull_request' }} + if: always() && github.event_name == 'pull_request' env: GH_TOKEN: ${{ github.token }} run: '[ -s diff.txt ] && gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} -F diff.txt || true' From 2135514a7bf6df3805232cefb0e91bea999de52b Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 19 Jan 2026 01:14:11 -0800 Subject: [PATCH 116/117] comment --- opendbc/car/tests/car_diff.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index 7938b06949b..a335744d556 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -131,6 +131,7 @@ def run_replay(platforms, segments, ref_path, update, workers=4): return list(pool.map(process_segment, work)) +# ASCII waveforms helpers def find_edges(vals, init): rises = [] falls = [] From 939af7567d7c645c3a0c01d7fe282c88704ceac8 Mon Sep 17 00:00:00 2001 From: elkoled Date: Mon, 19 Jan 2026 13:39:40 -0800 Subject: [PATCH 117/117] no exit 1 in CI --- .github/workflows/tests.yml | 7 +------ opendbc/car/tests/car_diff.py | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 172cf960315..5ebeceed5d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -76,12 +76,7 @@ jobs: scons -j8 - name: Test car diff if: github.event_name == 'pull_request' - run: | - source setup.sh - ret=0 - python opendbc/car/tests/car_diff.py > diff.txt || ret=$? - cat diff.txt - exit $ret + run: source setup.sh && python opendbc/car/tests/car_diff.py | tee diff.txt - name: Comment PR if: always() && github.event_name == 'pull_request' env: diff --git a/opendbc/car/tests/car_diff.py b/opendbc/car/tests/car_diff.py index a335744d556..a75b8f95dff 100644 --- a/opendbc/car/tests/car_diff.py +++ b/opendbc/car/tests/car_diff.py @@ -1,8 +1,5 @@ -#!/usr/bin/env python3 -import os -os.environ['LOGPRINT'] = 'CRITICAL' - import argparse +import os import pickle import re import subprocess