This commit is contained in:
infiniteCable2
2026-04-27 18:38:58 +02:00
14 changed files with 176 additions and 46 deletions

View File

@@ -31,7 +31,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: tests.yml
workflow_conclusion: ''
pr: ${{ github.event.number }}
commit: ${{ github.event.pull_request.head.sha }}
name: car_diff_${{ github.event.number }}
path: .
allow_forks: true

View File

@@ -1,3 +1,31 @@
Version 0.3.1 (2026-04-22)
========================
* Ship DBCs, capnp schemas, and torque data in the wheel
Version 0.3.0 (2026-04-22)
========================
* Supported car count: 345 → 397
* Tesla Model 3, Model Y, and Model X (HW3 and HW4) support thanks to lukasloetkolben and greatgitsby!
* Rivian R1S and R1T (Gen 1 and Gen 2) support thanks to lukasloetkolben!
* Porsche Macan and Audi Q5 (VW MLB) support thanks to jyoung8607 and Dennis-NL!
* VW MEB (ID.x) architecture support thanks to jyoung8607!
* PSA AEE2010_R3 platform support thanks to elkoled!
* Honda Accord 2023-25, CR-V 2023-25, Pilot 2023-25, Passport 2026, and Acura MDX 2025 support thanks to vanillagorillaa and MVL!
* Honda Odyssey 2021-25 support thanks to csouers and MVL!
* Acura TLX 2021 support thanks to MVL!
* Honda City 2023 support thanks to vanillagorillaa and drFritz!
* Honda N-Box 2018 support thanks to miettal!
* Ford F-150, F-150 Hybrid, Mach-E, and Ranger support
* Ford Escape 2023-24 and Kuga 2024 support thanks to incognitojam!
* Hyundai Nexo 2021 support thanks to sunnyhaibin!
* Kia K7 2017 support thanks to royjr!
* Lexus LS 2018 support thanks to Hacheoy!
* Lexus RC 2023 support thanks to nelsonjchen!
* CANParser and CANPacker rewritten in pure Python — the installed package is pure Python, no compilation required
* Safety code, `isotp.py`/`ccp.py`/`xcp.py`, and `vehicle_model.py` moved into opendbc from panda/openpilot — opendbc is now the self-contained car API package
* `mull` replaced with a faster custom mutation test runner
* Safety hardening: per-brand message-block config, missing RX checks, relay-malfunction config
Version 0.2.1 (2025-02-10)
========================
* Fix missing files making car/ package not importable

View File

@@ -1,8 +1,10 @@
import unittest
from opendbc.can import CANDefine
from opendbc.can.tests import ALL_DBCS
class TestCANDefine:
class TestCANDefine(unittest.TestCase):
def test_civic(self):
dbc_file = "honda_civic_touring_2016_can_generated"
@@ -20,8 +22,8 @@ class TestCANDefine:
0: 'NORMAL'}
}
def test_all_dbcs(self, subtests):
def test_all_dbcs(self):
# Asserts no exceptions on all DBCs
for dbc in ALL_DBCS:
with subtests.test(dbc=dbc):
with self.subTest(dbc=dbc):
CANDefine(dbc)

View File

@@ -1,4 +1,5 @@
import re
import unittest
from opendbc.car.honda.fingerprints import FW_VERSIONS
from opendbc.car.honda.values import HONDA_BOSCH, HONDA_BOSCH_TJA_CONTROL
@@ -6,7 +7,7 @@ from opendbc.car.honda.values import HONDA_BOSCH, HONDA_BOSCH_TJA_CONTROL
HONDA_FW_VERSION_RE = br"[A-Z0-9]{5}(-|,)[A-Z0-9]{3}(-|,)[A-Z0-9]{4}(\x00){2}$"
class TestHondaFingerprint:
class TestHondaFingerprint(unittest.TestCase):
def test_fw_version_format(self):
# Asserts all FW versions follow an expected format
for fw_by_ecu in FW_VERSIONS.values():

View File

@@ -23,6 +23,12 @@ MAX_ANGLE = 85
MAX_ANGLE_FRAMES = 89
MAX_ANGLE_CONSECUTIVE_FRAMES = 2
# On some HKG CAN and CAN FD non-CANFD_ALT_BUTTONS, the cancel button (CF_Clu_CruiseSwState / CRUISE_BUTTONS = 4) is
# a pause/resume toggle, not a dedicated cancel. Firing it mid-brake inadvertently can cause a re-enable attempt
# and triggers the "SCC Conditions Not Met" alert. Delaying the button send lets factory SCC disengage
# naturally on brake press. We send ~100 ms later if it fails to do so, or if we want to cancel for another reason.
CANCEL_BUTTON_DELAY_FRAMES = 10
def process_hud_alert(enabled, fingerprint, hud_control):
sys_warning = (hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw))
@@ -66,6 +72,7 @@ class CarController(CarControllerBase, EsccCarController, LeadDataCarController,
self.apply_torque_last = 0
self.car_fingerprint = CP.carFingerprint
self.last_button_frame = 0
self.cancel_counter = 0
def update(self, CC, CC_SP, CS, now_nanos):
EsccCarController.update(self, CS)
@@ -113,6 +120,10 @@ class CarController(CarControllerBase, EsccCarController, LeadDataCarController,
if self.CP.flags & HyundaiFlags.CANFD_ENABLE_BLINKERS:
can_sends.append(make_tester_present_msg(0x7b1, self.CAN.ECAN, suppress_response=True))
# Delay the cancel button send so the brake can disengage factory SCC first.
# Reset whenever openpilot is no longer requesting cancel.
self.cancel_counter = self.cancel_counter + 1 if CC.cruiseControl.cancel else 0
# *** CAN/CAN FD specific ***
if self.CP.flags & HyundaiFlags.CANFD:
can_sends.extend(self.create_canfd_msgs(apply_steer_req, apply_torque, set_speed_in_units, accel,
@@ -151,7 +162,7 @@ class CarController(CarControllerBase, EsccCarController, LeadDataCarController,
# Button messages
if not self.CP.openpilotLongitudinalControl:
if CC.cruiseControl.cancel:
if self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES:
can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL, self.CP))
elif CC.cruiseControl.resume:
# send resume at a max freq of 10Hz
@@ -220,10 +231,11 @@ class CarController(CarControllerBase, EsccCarController, LeadDataCarController,
if (self.frame - self.last_button_frame) * DT_CTRL > 0.25:
# cruise cancel
if CC.cruiseControl.cancel:
# Here we send ACC message to cancel, not buttons. Don't delay
if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS:
can_sends.append(hyundaicanfd.create_acc_cancel(self.packer, self.CP, self.CAN, CS.cruise_info))
self.last_button_frame = self.frame
else:
elif self.cancel_counter > CANCEL_BUTTON_DELAY_FRAMES:
for _ in range(20):
can_sends.append(hyundaicanfd.create_buttons(self.packer, self.CP, self.CAN, CS.buttons_counter + 1, Buttons.CANCEL))
self.last_button_frame = self.frame

View File

@@ -1,11 +1,13 @@
import unittest
from opendbc.car.rivian.fingerprints import FW_VERSIONS
from opendbc.car.rivian.values import CAR, FW_QUERY_CONFIG, WMI, ModelLine, ModelYear
class TestRivian:
def test_custom_fuzzy_fingerprinting(self, subtests):
class TestRivian(unittest.TestCase):
def test_custom_fuzzy_fingerprinting(self):
for platform in CAR:
with subtests.test(platform=platform.name):
with self.subTest(platform=platform.name):
for wmi in WMI:
for line in ModelLine:
for year in ModelYear:

View File

@@ -1,7 +1,9 @@
import unittest
from opendbc.car.subaru.fingerprints import FW_VERSIONS
class TestSubaruFingerprint:
class TestSubaruFingerprint(unittest.TestCase):
def test_fw_version_format(self):
for platform, fws_per_ecu in FW_VERSIONS.items():
for (ecu, _, _), fws in fws_per_ecu.items():

View File

@@ -12,6 +12,7 @@ FW_VERSIONS = {
b'TeM3_E014p10_0.0.0 (16),E014.17.00',
b'TeM3_E014p10_0.0.0 (16),EL014.17.00',
b'TeM3_ES014p11_0.0.0 (25),ES014.19.0',
b'TeM3_E014p10_0.0.0 (24),E014.20.2',
b'TeMYG4_DCS_Update_0.0.0 (13),E4014.28.1',
b'TeMYG4_DCS_Update_0.0.0 (9),E4014.26.0',
b'TeMYG4_Legacy3Y_0.0.0 (2),E4015.02.0',
@@ -24,12 +25,14 @@ FW_VERSIONS = {
b'TeMYG4_Main_0.0.0 (77),E4HP015.04.5',
b'TeMYG4_Main_0.0.0 (78),E4HP015.05.0',
b'TeMYG4_SingleECU_0.0.0 (33),E4S014.27',
b'TeMYG4_Main_0.0.0 (78),E4H015.05.0',
],
},
CAR.TESLA_MODEL_Y: {
(Ecu.eps, 0x730, None): [
b'TeM3_E014p10_0.0.0 (16),Y002.18.00',
b'TeM3_E014p10_0.0.0 (16),YP002.18.00',
b'TeM3_E014p10_0.0.0 (24),YP002.21.2',
b'TeM3_ES014p11_0.0.0 (16),YS002.17',
b'TeM3_ES014p11_0.0.0 (25),YS002.19.0',
b'TeMYG4_DCS_Update_0.0.0 (13),Y4002.27.1',

View File

@@ -1,3 +1,6 @@
import re
import unittest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.structs import CarParams
from opendbc.car.tesla.interface import CarInterface
@@ -7,23 +10,80 @@ from opendbc.car.tesla.values import CAR, FSD_14_FW
Ecu = CarParams.Ecu
# Fields prefixed unknown_* we observe structurally but don't know the meaning of.
# Only `platform` has evidence-backed semantic meaning (matches car_model in FW_VERSIONS).
#
# unknown_prefix is everything before the comma; we don't split it because we don't know what its
# parts mean, but observed shape is: <family>_<package>_<triplet> (<build>), e.g.
# TeMYG4 _ Main _ 0.0.0 (78) or TeM3 _ SP_XP002p2 _ 0.0.0 (23)
# family package triplet build family package triplet build
#
# After the comma, the version string decomposes into:
# platform : E/Y/X = car model (Model 3 / Y / X). The only field with known meaning.
# variant_code : differentiator WITHIN a platform — hardware/trim/calibration bits packed
# into <digit?><letters?><3-digit series>, e.g. '4HP015', '4003', 'L014',
# 'PR003'. We don't fully know what the parts mean individually, but the
# whole string identifies a specific variant within the car model.
# software_major/minor : numeric components after the first '.' — conventional release numbers.
# minor is optional (e.g. 'E4S014.27' has no minor).
#
# Suspected (not confirmed): for M3/MY, `TeM3_*` outer + no-leading-digit variant_code == HW3, and
# `TeMYG4_*` outer + leading-'4' variant_code == HW4 (the 'G4' in TeMYG4 likely denotes Gen 4).
#
# Example full parse of 'TeMYG4_Main_0.0.0 (78),E4HP015.05.0':
# unknown_prefix='TeMYG4_Main_0.0.0 (78)'
# platform=E variant_code=4HP015 software_major=05 software_minor=0
FW_RE = re.compile(
rb'^(?P<unknown_prefix>.+),' +
rb'(?P<platform>[EYX])' +
rb'(?P<variant_code>\d?[A-Z]*\d{3})' +
rb'\.(?P<software_major>\d+)' +
rb'(?:\.(?P<software_minor>\d+))?$'
)
class TestTeslaFingerprint:
def test_fsd_14_fw(self):
PLATFORM_TO_CAR = {
b'E': CAR.TESLA_MODEL_3,
b'Y': CAR.TESLA_MODEL_Y,
b'X': CAR.TESLA_MODEL_X,
}
# Hypothesized FSD 14 profile, in terms of variant_code bookends (given software_major >= 4):
# M3: variant_code starts with '4H', ends with '015'
# MY: variant_code starts with '4', ends with '003'
# Older series (M3 '014', MY '002') are never FSD 14.
FSD_14_FW_RULE = {
CAR.TESLA_MODEL_3: (b'4H', b'015'),
CAR.TESLA_MODEL_Y: (b'4', b'003'),
}
class TestTeslaFingerprint(unittest.TestCase):
def test_fw_platform_code(self):
# Every EPS FW must parse and its platform letter must match the car it's filed under.
for car_model, ecus in FW_VERSIONS.items():
for fw in ecus.get((Ecu.eps, 0x730, None), []):
_, _, version = fw.partition(b',')
is_fsd_14 = fw in FSD_14_FW.get(car_model, [])
m = FW_RE.match(fw)
if car_model == CAR.TESLA_MODEL_3:
# Model 3: FSD 14 FW has 'P' in the Highland suffix (E4HP vs E4H)
assert is_fsd_14 == version.startswith(b'E4HP'), f"{fw}"
elif car_model == CAR.TESLA_MODEL_Y:
# Model Y: FSD 14 FW is Y4x version >= 003.04
prefix, _, ver = version.partition(b'.')
y4_003 = prefix.startswith(b'Y4') and prefix.endswith(b'003') # Y4=HW4, 003=version series (002 is never FSD 14)
high_version = y4_003 and int(ver.split(b'.')[0]) >= 4
assert is_fsd_14 == high_version, f"{fw}"
assert m is not None, f"Unparsable FW: {fw}"
assert PLATFORM_TO_CAR[m['platform']] == car_model, f"Platform letter {m['platform']!r} != {car_model.value}: {fw}"
def test_fsd_14_fw(self):
for car_model, ecus in FW_VERSIONS.items():
if car_model not in FSD_14_FW_RULE:
continue
variant_prefix, variant_suffix = FSD_14_FW_RULE[car_model]
for fw in ecus.get((Ecu.eps, 0x730, None), []):
m = FW_RE.match(fw)
assert m is not None, f"Unparsable FW: {fw}"
is_fsd_14 = fw in FSD_14_FW.get(car_model, [])
expected = (
m['variant_code'].startswith(variant_prefix)
and m['variant_code'].endswith(variant_suffix)
and int(m['software_major']) >= 4
)
assert is_fsd_14 == expected, f"{fw}"
def test_radar_detection(self):
# Test radar availability detection for cars with radar DBC defined

View File

@@ -85,6 +85,7 @@ FSD_14_FW = {
b'TeMYG4_Main_0.0.0 (77),E4HP015.04.5',
b'TeMYG4_Main_0.0.0 (78),E4HP015.05.0',
b'TeMYG4_Main_0.0.0 (77),E4H015.04.5',
b'TeMYG4_Main_0.0.0 (78),E4H015.05.0',
],
CAR.TESLA_MODEL_Y: [
b'TeMYG4_Legacy3Y_0.0.0 (6),Y4003.04.0',

View File

@@ -1,3 +1,5 @@
import unittest
from hypothesis import given, settings, strategies as st
from opendbc.car import Bus
@@ -16,7 +18,7 @@ def check_fw_version(fw_version: bytes) -> bool:
return b'?' not in fw_version and b'!' not in fw_version
class TestToyotaInterfaces:
class TestToyotaInterfaces(unittest.TestCase):
def test_car_sets(self):
assert len(ANGLE_CONTROL_CAR - TSS2_CAR) == 0
assert len(RADAR_ACC_CAR - TSS2_CAR) == 0
@@ -32,11 +34,11 @@ class TestToyotaInterfaces:
if car_model in TSS2_CAR and car_model not in SECOC_CAR:
assert dbc[Bus.pt] == "toyota_nodsu_pt_generated"
def test_essential_ecus(self, subtests):
def test_essential_ecus(self):
# Asserts standard ECUs exist for each platform
common_ecus = {Ecu.fwdRadar, Ecu.fwdCamera}
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
present_ecus = {ecu[0] for ecu in ecus}
missing_ecus = common_ecus - present_ecus
assert len(missing_ecus) == 0
@@ -52,19 +54,19 @@ class TestToyotaInterfaces:
assert Ecu.eps in present_ecus
class TestToyotaFingerprint:
def test_non_essential_ecus(self, subtests):
class TestToyotaFingerprint(unittest.TestCase):
def test_non_essential_ecus(self):
# Ensures only the cars that have multiple engine ECUs are in the engine non-essential ECU list
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
engine_ecus = {ecu for ecu in ecus if ecu[0] == Ecu.engine}
assert (len(engine_ecus) > 1) == (car_model in FW_QUERY_CONFIG.non_essential_ecus[Ecu.engine]), \
f"Car model unexpectedly {'not ' if len(engine_ecus) > 1 else ''}in non-essential list"
def test_valid_fw_versions(self, subtests):
def test_valid_fw_versions(self):
# Asserts all FW versions are valid
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
for fws in ecus.values():
for fw in fws:
assert check_fw_version(fw), fw
@@ -78,10 +80,10 @@ class TestToyotaFingerprint:
fws = data.draw(fw_strategy)
get_platform_codes(fws)
def test_platform_code_ecus_available(self, subtests):
def test_platform_code_ecus_available(self):
# Asserts ECU keys essential for fuzzy fingerprinting are available on all platforms
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
for platform_code_ecu in PLATFORM_CODE_ECUS:
if platform_code_ecu == Ecu.eps and car_model in (CAR.TOYOTA_PRIUS_V, CAR.LEXUS_CTH,):
continue
@@ -89,14 +91,14 @@ class TestToyotaFingerprint:
continue
assert platform_code_ecu in [e[0] for e in ecus]
def test_fw_format(self, subtests):
def test_fw_format(self):
# Asserts:
# - every supported ECU FW version returns one platform code
# - every supported ECU FW version has a part number
# - expected parsing of ECU sub-versions
for car_model, ecus in FW_VERSIONS.items():
with subtests.test(car_model=car_model.value):
with self.subTest(car_model=car_model.value):
for ecu, fws in ecus.items():
if ecu[0] not in PLATFORM_CODE_ECUS:
continue

View File

@@ -1,6 +1,6 @@
[project]
name = "opendbc"
version = "0.2.1"
version = "0.3.1"
description = "a Python API for your car"
license = "MIT"
authors = [{ name = "Vehicle Researcher", email = "user@comma.ai" }]
@@ -18,7 +18,6 @@ dependencies = [
[project.optional-dependencies]
testing = [
"comma-car-segments @ https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl",
"cffi",
"tree-sitter",
"tree-sitter-c",
@@ -33,7 +32,6 @@ testing = [
"lefthook",
"cpplint",
"codespell",
"cppcheck @ git+https://github.com/commaai/dependencies.git@release-cppcheck#subdirectory=cppcheck",
]
docs = [
"Jinja2",
@@ -42,6 +40,15 @@ examples = [
"inputs",
]
# Dependency groups (PEP 735) are not included in the published wheel.
# Direct URL dependencies must live here because PyPI rejects uploads whose
# `Requires-Dist` contains direct URLs.
[dependency-groups]
testing = [
"comma-car-segments @ https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl",
"cppcheck @ git+https://github.com/commaai/dependencies.git@release-cppcheck#subdirectory=cppcheck",
]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
@@ -127,4 +134,6 @@ too-many-positional-arguments = "ignore"
include-package-data = true
[tool.setuptools.package-data]
"opendbc.safety" = ["*.h", "board/*.h", "board/drivers/*.h", "modes/*.h"]
"opendbc.car" = ["**/*.capnp", "**/*.toml"]
"opendbc.dbc" = ["**/*.dbc"]
"opendbc.safety" = ["*.h", "modes/*.h"]

View File

@@ -18,5 +18,5 @@ if ! command -v uv &>/dev/null; then
fi
export UV_PROJECT_ENVIRONMENT="$BASEDIR/.venv"
uv sync --all-extras --inexact
uv sync --all-extras --all-groups --inexact
source "$PYTHONPATH/.venv/bin/activate"

18
uv.lock generated
View File

@@ -382,7 +382,7 @@ wheels = [
[[package]]
name = "opendbc"
version = "0.2.1"
version = "0.3.1"
source = { editable = "." }
dependencies = [
{ name = "numpy" },
@@ -401,8 +401,6 @@ examples = [
testing = [
{ name = "cffi" },
{ name = "codespell" },
{ name = "comma-car-segments" },
{ name = "cppcheck" },
{ name = "cpplint" },
{ name = "gcovr" },
{ name = "hypothesis" },
@@ -415,12 +413,16 @@ testing = [
{ name = "zstandard" },
]
[package.dev-dependencies]
testing = [
{ name = "comma-car-segments" },
{ name = "cppcheck" },
]
[package.metadata]
requires-dist = [
{ name = "cffi", marker = "extra == 'testing'" },
{ name = "codespell", marker = "extra == 'testing'" },
{ name = "comma-car-segments", marker = "extra == 'testing'", url = "https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl" },
{ name = "cppcheck", marker = "extra == 'testing'", git = "https://github.com/commaai/dependencies.git?subdirectory=cppcheck&rev=release-cppcheck" },
{ name = "cpplint", marker = "extra == 'testing'" },
{ name = "gcovr", marker = "extra == 'testing'" },
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
@@ -440,6 +442,12 @@ requires-dist = [
]
provides-extras = ["testing", "docs", "examples"]
[package.metadata.requires-dev]
testing = [
{ name = "comma-car-segments", url = "https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl" },
{ name = "cppcheck", git = "https://github.com/commaai/dependencies.git?subdirectory=cppcheck&rev=release-cppcheck" },
]
[[package]]
name = "pycapnp"
version = "2.1.0"