try no scons (#3194)

* try no scons

* lil more

* lil more

* lazy

* fix ty
This commit is contained in:
Adeeb Shihadeh
2026-03-11 11:23:38 -07:00
committed by GitHub
parent 2d52887bee
commit ddeba888a3
22 changed files with 252 additions and 274 deletions

View File

@@ -49,7 +49,6 @@ jobs:
- name: Run mutation tests - name: Run mutation tests
run: | run: |
source setup.sh source setup.sh
scons -j8
python opendbc/safety/tests/mutation.py python opendbc/safety/tests/mutation.py
car_diff: car_diff:
@@ -61,10 +60,6 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Build opendbc
run: |
source setup.sh
scons -j8
- name: Test car diff - name: Test car diff
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
run: source setup.sh && python opendbc/car/tests/car_diff.py | tee diff.txt run: source setup.sh && python opendbc/car/tests/car_diff.py | tee diff.txt

View File

@@ -15,7 +15,6 @@ jobs:
- name: Generate Car Docs - name: Generate Car Docs
run: | run: |
pip install -e . pip install -e .
scons -c && scons -j$(nproc)
python -m pip install jinja2==3.1.4 python -m pip install jinja2==3.1.4
python opendbc/car/docs.py python opendbc/car/docs.py
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 - uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842

5
.gitignore vendored
View File

@@ -9,7 +9,6 @@
*.dylib *.dylib
.*.swp .*.swp
.DS_Store .DS_Store
.sconsign.dblite
.hypothesis .hypothesis
*.egg-info/ *.egg-info/
*.html *.html
@@ -21,14 +20,10 @@
.vscode/ .vscode/
__pycache__/ __pycache__/
*.profraw *.profraw
.sconf_temp/
opendbc/can/build/ opendbc/can/build/
opendbc/can/obj/ opendbc/can/obj/
opendbc/dbc/*_generated.dbc
cppcheck-addon-ctu-file-list cppcheck-addon-ctu-file-list
opendbc/safety/tests/coverage-out opendbc/safety/tests/coverage-out
compile_commands.json
.mull/ .mull/

View File

@@ -1,5 +0,0 @@
SConscript(['opendbc/dbc/SConscript'])
# test files
if GetOption('extras'):
SConscript('opendbc/safety/tests/libsafety/SConscript')

View File

@@ -1,7 +0,0 @@
AddOption('--minimal',
action='store_false',
dest='extras',
default=True,
help='the minimum build. no tests, tools, etc.')
SConscript(['SConscript'])

View File

@@ -4,3 +4,14 @@ DBC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dbc')
# -I include path for e.g. "#include <opendbc/safety/safety.h>" # -I include path for e.g. "#include <opendbc/safety/safety.h>"
INCLUDE_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")) INCLUDE_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../"))
_generated_dbc_cache: dict[str, str] | None = None
def get_generated_dbcs() -> dict[str, str]:
"""Lazily generate all *_generated DBC content in memory.
Returns {name: content} where name has no .dbc extension."""
global _generated_dbc_cache
if _generated_dbc_cache is None:
from opendbc.dbc.generator.generator import generate_all
_generated_dbc_cache = generate_all()
return _generated_dbc_cache

View File

@@ -4,7 +4,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from functools import cache from functools import cache
from opendbc import DBC_PATH from opendbc import DBC_PATH, get_generated_dbcs
# TODO: these should just be passed in along with the DBC file # TODO: these should just be passed in along with the DBC file
from opendbc.car.honda.hondacan import honda_checksum from opendbc.car.honda.hondacan import honda_checksum
@@ -77,16 +77,32 @@ VAL_SPLIT_RE = re.compile(r'["]+')
@cache @cache
class DBC: class DBC:
def __init__(self, name: str): def __init__(self, name: str):
dbc_path = name if os.path.exists(name):
if not os.path.exists(dbc_path): self._parse_file(name)
else:
dbc_path = os.path.join(DBC_PATH, name + ".dbc") dbc_path = os.path.join(DBC_PATH, name + ".dbc")
if os.path.exists(dbc_path):
self._parse_file(dbc_path)
else:
# try in-memory generated DBC
generated = get_generated_dbcs()
content = generated.get(name)
if content is None:
raise FileNotFoundError(f"DBC not found: {name}")
self._parse_content(name, content)
self._parse(dbc_path) def _parse_file(self, path: str):
def _parse(self, path: str):
self.name = os.path.basename(path).replace(".dbc", "") self.name = os.path.basename(path).replace(".dbc", "")
with open(path) as f: with open(path) as f:
lines = f.readlines() lines = f.readlines()
self._parse_lines(lines)
def _parse_content(self, name: str, content: str):
self.name = name
lines = content.splitlines(keepends=True)
self._parse_lines(lines)
def _parse_lines(self, lines: list[str]):
checksum_state = get_checksum_state(self.name) checksum_state = get_checksum_state(self.name)
be_bits = [j + i * 8 for i in range(64) for j in range(7, -1, -1)] be_bits = [j + i * 8 for i in range(64) for j in range(7, -1, -1)]

View File

@@ -1,8 +1,9 @@
import glob import glob
import os import os
from opendbc import DBC_PATH from opendbc import DBC_PATH, get_generated_dbcs
ALL_DBCS = [os.path.basename(dbc).split('.')[0] for dbc in static_dbcs = [os.path.basename(dbc).split('.')[0] for dbc in
glob.glob(f"{DBC_PATH}/*.dbc")] glob.glob(f"{DBC_PATH}/*.dbc")]
ALL_DBCS = sorted(set(static_dbcs + list(get_generated_dbcs().keys())))
TEST_DBC = os.path.abspath(os.path.join(os.path.dirname(__file__), "test.dbc")) TEST_DBC = os.path.abspath(os.path.join(os.path.dirname(__file__), "test.dbc"))

View File

@@ -1,31 +0,0 @@
import os
from pathlib import Path
env = Environment(ENV=os.environ)
generator = File("generator/generator.py")
source_files = [
File(str(f))
for f in Path("generator").rglob("*")
if f.is_file() and f.suffix in {".py", ".dbc"}
]
output_files = [
f.name.replace(".dbc", "_generated.dbc")
for f in Path("generator").rglob("*.dbc")
if not f.name.startswith("_")
]
# include DBCs generated by python scripts
output_files += [
f.name.replace(".py", "_generated.dbc")
for f in Path("generator").rglob("*.py")
if not f.name.startswith(("_", "test_")) and f.name != "generator.py"
]
generated = env.Command(
target=list(set(output_files)),
source=[generator] + source_files,
action="python3 ${SOURCES[0]}",
)

View File

@@ -31,13 +31,15 @@ chrysler_to_ram = {
}, },
} }
if __name__ == "__main__":
def generate():
src = '_stellantis_common.dbc' src = '_stellantis_common.dbc'
chrysler_path = os.path.dirname(os.path.realpath(__file__)) chrysler_path = os.path.dirname(os.path.realpath(__file__))
result = {}
for out, addr_lookup in chrysler_to_ram.items(): for out, addr_lookup in chrysler_to_ram.items():
with open(os.path.join(chrysler_path, src), encoding='utf-8') as in_f, open(os.path.join(chrysler_path, out), 'w', encoding='utf-8') as out_f: with open(os.path.join(chrysler_path, src), encoding='utf-8') as in_f:
out_f.write(f'CM_ "Generated from {src}"\n\n') parts = [f'CM_ "Generated from {src}"\n\n']
wrote_addrs = set() wrote_addrs = set()
for line in in_f.readlines(): for line in in_f.readlines():
@@ -45,10 +47,13 @@ if __name__ == "__main__":
sl = line.split(' ') sl = line.split(' ')
addr = int(sl[1]) addr = int(sl[1])
wrote_addrs.add(addr) wrote_addrs.add(addr)
sl[1] = str(addr_lookup.get(addr, addr)) sl[1] = str(addr_lookup.get(addr, addr))
line = ' '.join(sl) line = ' '.join(sl)
out_f.write(line) parts.append(line)
missing_addrs = set(addr_lookup.keys()) - wrote_addrs missing_addrs = set(addr_lookup.keys()) - wrote_addrs
assert len(missing_addrs) == 0, f"Missing addrs from {src}: {missing_addrs}" assert len(missing_addrs) == 0, f"Missing addrs from {src}: {missing_addrs}"
result[out] = ''.join(parts)
return result

View File

@@ -1,65 +1,92 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import importlib
import os import os
import re import re
import glob from pathlib import Path
import subprocess
generator_path = os.path.dirname(os.path.realpath(__file__)) generator_path = os.path.dirname(os.path.realpath(__file__))
opendbc_root = os.path.join(generator_path, '../')
include_pattern = re.compile(r'CM_ "IMPORT (.*?)";\n') include_pattern = re.compile(r'CM_ "IMPORT (.*?)";\n')
generated_suffix = '_generated.dbc'
def read_dbc(src_dir: str, filename: str) -> str: def _read_dbc(src_dir: str, filename: str, extra_files: dict[str, str] | None = None) -> str:
if extra_files and filename in extra_files:
return extra_files[filename]
with open(os.path.join(src_dir, filename), encoding='utf-8') as file_in: with open(os.path.join(src_dir, filename), encoding='utf-8') as file_in:
return file_in.read() return file_in.read()
def create_dbc(src_dir: str, filename: str, output_path: str): def _create_dbc_content(src_dir: str, filename: str, extra_files: dict[str, str] | None = None) -> str:
dbc_file_in = read_dbc(src_dir, filename) dbc_file_in = _read_dbc(src_dir, filename, extra_files)
includes = include_pattern.findall(dbc_file_in) includes = include_pattern.findall(dbc_file_in)
output_filename = filename.replace('.dbc', generated_suffix) parts = ['CM_ "AUTOGENERATED FILE, DO NOT EDIT";\n']
output_file_location = os.path.join(output_path, output_filename) for include_filename in includes:
parts.append(f'\n\nCM_ "Imported file {include_filename} starts here";\n')
parts.append(_read_dbc(src_dir, include_filename, extra_files))
with open(output_file_location, 'w', encoding='utf-8') as dbc_file_out: parts.append(f'\nCM_ "{filename} starts here";\n')
dbc_file_out.write('CM_ "AUTOGENERATED FILE, DO NOT EDIT";\n') core_dbc = include_pattern.sub('', dbc_file_in)
parts.append(core_dbc)
for include_filename in includes: return ''.join(parts)
include_file_header = f'\n\nCM_ "Imported file {include_filename} starts here";\n'
dbc_file_out.write(include_file_header)
include_file = read_dbc(src_dir, include_filename)
dbc_file_out.write(include_file)
dbc_file_out.write(f'\nCM_ "{filename} starts here";\n')
core_dbc = include_pattern.sub('', dbc_file_in)
dbc_file_out.write(core_dbc)
def create_all(output_path: str): def _collect_script_outputs() -> dict[str, dict[str, str]]:
# clear out old DBCs """Import and call generate() from each sub-generator script.
for f in glob.glob(f"{output_path}/*{generated_suffix}"): Returns {dir_name: {filename: content}}."""
os.remove(f) outputs: dict[str, dict[str, str]] = {}
# run python generator scripts first for py_file in sorted(Path(generator_path).rglob("*.py")):
for f in glob.glob(f"{generator_path}/*/*.py"): if py_file.name.startswith("test_") or py_file.name == "generator.py":
subprocess.check_call(f) continue
dir_name = py_file.parent.name
module_name = f"opendbc.dbc.generator.{dir_name}.{py_file.stem}"
mod = importlib.import_module(module_name)
if hasattr(mod, 'generate'):
outputs.setdefault(dir_name, {}).update(mod.generate())
return outputs
def generate_all() -> dict[str, str]:
"""Generate all DBC content in memory. Returns {name: content} where name has no .dbc extension."""
script_outputs = _collect_script_outputs()
result = {}
for src_dir, _, filenames in os.walk(generator_path): for src_dir, _, filenames in os.walk(generator_path):
if src_dir == generator_path: if src_dir == generator_path:
continue continue
#print(src_dir) dir_name = os.path.basename(src_dir)
for filename in filenames: extra = script_outputs.get(dir_name, {})
if filename.startswith('_') or not filename.endswith('.dbc'):
continue
#print(filename) # all non-_ .dbc files: on-disk templates + script-generated
create_dbc(src_dir, filename, output_path) all_dbc_files = {f for f in filenames if f.endswith('.dbc') and not f.startswith('_')}
all_dbc_files |= {f for f in extra if not f.startswith('_')}
for filename in sorted(all_dbc_files):
output_name = filename.replace('.dbc', '_generated')
content = _create_dbc_content(src_dir, filename, extra)
result[output_name] = content
return result
def create_all(output_path: str):
"""Generate all DBC files and write them to output_path (for backward compatibility)."""
import glob
generated_suffix = '_generated.dbc'
# clear out old generated DBCs
for f in glob.glob(os.path.join(output_path, f"*{generated_suffix}")):
os.remove(f)
for name, content in generate_all().items():
with open(os.path.join(output_path, name + '.dbc'), 'w', encoding='utf-8') as f:
f.write(content)
if __name__ == "__main__": if __name__ == "__main__":
opendbc_root = os.path.join(generator_path, '../')
create_all(opendbc_root) create_all(opendbc_root)

View File

@@ -1,12 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from collections import namedtuple from collections import namedtuple
import os
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc") def generate():
hyundai_path = os.path.dirname(os.path.realpath(__file__)) parts = []
with open(os.path.join(hyundai_path, dbc_name), "w", encoding='utf-8') as f: parts.append("""
f.write("""
VERSION "" VERSION ""
@@ -45,8 +43,8 @@ BS_:
BU_: XXX BU_: XXX
""") """)
for a in [0x100, 0x200]: for a in [0x100, 0x200]:
f.write(f""" parts.append(f"""
BO_ {a} RADAR_POINTS_METADATA_0x{a:x}: 64 RADAR BO_ {a} RADAR_POINTS_METADATA_0x{a:x}: 64 RADAR
SG_ SIGNAL_1 : 0|32@1+ (1,0) [0|255] "" XXX SG_ SIGNAL_1 : 0|32@1+ (1,0) [0|255] "" XXX
SG_ SIGNAL_2 : 32|32@1+ (1,0) [0|65535] "" XXX SG_ SIGNAL_2 : 32|32@1+ (1,0) [0|65535] "" XXX
@@ -108,63 +106,65 @@ BO_ {a} RADAR_POINTS_METADATA_0x{a:x}: 64 RADAR
SG_ SIGNAL_58 : 418|1@1+ (1,0) [0|3] "" XXX SG_ SIGNAL_58 : 418|1@1+ (1,0) [0|3] "" XXX
""") """)
# radar points are sent at 20 Hz in groups of 1 to 13 messages # radar points are sent at 20 Hz in groups of 1 to 13 messages
# each message has 5 radar points for a total of 65 points max # each message has 5 radar points for a total of 65 points max
# each radar point is 101 bits so the alignment is not consistent # each radar point is 101 bits so the alignment is not consistent
RadarPointSignal = namedtuple("RadarPointSignal", ["name", "start", "length", "scale", "offset"]) RadarPointSignal = namedtuple("RadarPointSignal", ["name", "start", "length", "scale", "offset"])
radar_point_signals = ( radar_point_signals = (
RadarPointSignal("DISTANCE", 7, 14, 1/64, 0), RadarPointSignal("DISTANCE", 7, 14, 1/64, 0),
RadarPointSignal("", 21, 2, 1, 0), RadarPointSignal("", 21, 2, 1, 0),
RadarPointSignal("", 23, 8, 1/512, -127/512), RadarPointSignal("", 23, 8, 1/512, -127/512),
RadarPointSignal("REL_VELOCITY", 31, 13, 1/32, -66), RadarPointSignal("REL_VELOCITY", 31, 13, 1/32, -66),
RadarPointSignal("", 44, 2, 1, 0), RadarPointSignal("", 44, 2, 1, 0),
RadarPointSignal("", 46, 2, 1, 0), RadarPointSignal("", 46, 2, 1, 0),
RadarPointSignal("AZIMUTH", 48, 12, 1/512, -2047/512), RadarPointSignal("AZIMUTH", 48, 12, 1/512, -2047/512),
RadarPointSignal("", 60, 2, 1, 0), RadarPointSignal("", 60, 2, 1, 0),
RadarPointSignal("", 62, 1, 1, 0), RadarPointSignal("", 62, 1, 1, 0),
RadarPointSignal("", 63, 7, 1, 0), RadarPointSignal("", 63, 7, 1, 0),
RadarPointSignal("", 70, 1, 1, 0), RadarPointSignal("", 70, 1, 1, 0),
RadarPointSignal("", 71, 6, 1, 0), RadarPointSignal("", 71, 6, 1, 0),
RadarPointSignal("", 77, 2, 1, 0), RadarPointSignal("", 77, 2, 1, 0),
RadarPointSignal("", 79, 8, 1/512, -127/512), RadarPointSignal("", 79, 8, 1/512, -127/512),
RadarPointSignal("", 87, 1, 1, 0), RadarPointSignal("", 87, 1, 1, 0),
RadarPointSignal("", 88, 2, 1, 0), RadarPointSignal("", 88, 2, 1, 0),
RadarPointSignal("", 90, 3, 1, 0), RadarPointSignal("", 90, 3, 1, 0),
# last 15 bits are controlled by LAYOUT_ID (seems to always zero, so below is layout 0) # last 15 bits are controlled by LAYOUT_ID (seems to always zero, so below is layout 0)
RadarPointSignal("", 93, 6, 1, 0), RadarPointSignal("", 93, 6, 1, 0),
RadarPointSignal("", 99, 8, 1, 0), RadarPointSignal("", 99, 8, 1, 0),
RadarPointSignal("", 107, 1, 1, 0), RadarPointSignal("", 107, 1, 1, 0),
) )
radar_point_bit_count = sum([s.length for s in radar_point_signals]) radar_point_bit_count = sum([s.length for s in radar_point_signals])
for a in [0x101, 0x201]: for a in [0x101, 0x201]:
f.write(f""" parts.append(f"""
BO_ {a} RADAR_POINTS_0x{a:x}: 64 RADAR BO_ {a} RADAR_POINTS_0x{a:x}: 64 RADAR
SG_ MESSAGE_ID : 0|5@1+ (1,0) [0|31] "" XXX SG_ MESSAGE_ID : 0|5@1+ (1,0) [0|31] "" XXX
SG_ LAYOUT_ID : 5|2@1+ (1,0) [0|3] "" XXX SG_ LAYOUT_ID : 5|2@1+ (1,0) [0|3] "" XXX
""") """)
bit_idx = radar_point_signals[0].start bit_idx = radar_point_signals[0].start
for i in range(5): for i in range(5):
signal_idx = 1 signal_idx = 1
for sig in radar_point_signals: for sig in radar_point_signals:
if sig.name: if sig.name:
sig_name = f"POINT_{i+1}_{sig.name}" sig_name = f"POINT_{i+1}_{sig.name}"
else: else:
sig_name = f"POINT_{i+1}_SIGNAL_{signal_idx}" sig_name = f"POINT_{i+1}_SIGNAL_{signal_idx}"
signal_idx += 1 signal_idx += 1
sig_start_idx = i * radar_point_bit_count + sig.start sig_start_idx = i * radar_point_bit_count + sig.start
assert bit_idx == sig_start_idx, f"signal overlap or gap!!! {bit_idx} != {sig_start_idx}" assert bit_idx == sig_start_idx, f"signal overlap or gap!!! {bit_idx} != {sig_start_idx}"
min_val = round(sig.offset, 10) min_val = round(sig.offset, 10)
max_val = round((2**sig.length - 1) * sig.scale + sig.offset, 10) max_val = round((2**sig.length - 1) * sig.scale + sig.offset, 10)
f.write(f" SG_ {sig_name} : {sig_start_idx}|{sig.length}@1+ ({sig.scale},{sig.offset}) [{min_val}|{max_val}] \"\" XXX\n") parts.append(f" SG_ {sig_name} : {sig_start_idx}|{sig.length}@1+ ({sig.scale},{sig.offset}) [{min_val}|{max_val}] \"\" XXX\n")
bit_idx += sig.length bit_idx += sig.length
# checksum is across a group of 0x100/200 and 0x101/201 messages (no checksums inside the other messages) # checksum is across a group of 0x100/200 and 0x101/201 messages (no checksums inside the other messages)
# ccitt_crc16 = mkCrcFun(0x11021, initCrc=0xffff, xorOut=0x0000, rev=False) # ccitt_crc16 = mkCrcFun(0x11021, initCrc=0xffff, xorOut=0x0000, rev=False)
for a in [0x104, 0x204]: for a in [0x104, 0x204]:
f.write(f""" parts.append(f"""
BO_ {a} RADAR_POINTS_CHECKSUM_0x{a:x}: 3 RADAR BO_ {a} RADAR_POINTS_CHECKSUM_0x{a:x}: 3 RADAR
SG_ CRC16 : 0|16@1+ (1,0) [0|65535] "" XXX SG_ CRC16 : 0|16@1+ (1,0) [0|65535] "" XXX
""") """)
return {"hyundai_kia_mando_corner_radar.dbc": "".join(parts)}

View File

@@ -1,11 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc") def generate():
hyundai_path = os.path.dirname(os.path.realpath(__file__)) parts = []
with open(os.path.join(hyundai_path, dbc_name), "w", encoding='utf-8') as f: parts.append("""
f.write("""
VERSION "" VERSION ""
@@ -44,9 +42,9 @@ BS_:
BU_: XXX BU_: XXX
""") """)
# note: 0x501/0x502 seem to be special in 0x5XX range # note: 0x501/0x502 seem to be special in 0x5XX range
for a in range(0x500, 0x500 + 32): for a in range(0x500, 0x500 + 32):
f.write(f""" parts.append(f"""
BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR
SG_ UNKNOWN_1 : 7|8@0- (1,0) [-128|127] "" XXX SG_ UNKNOWN_1 : 7|8@0- (1,0) [-128|127] "" XXX
SG_ AZIMUTH : 12|10@0- (0.2,0) [-102.4|102.2] "" XXX SG_ AZIMUTH : 12|10@0- (0.2,0) [-102.4|102.2] "" XXX
@@ -59,3 +57,5 @@ BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR
SG_ REL_SPEED : 53|14@0- (0.01,0) [-81.92|81.92] "" XXX SG_ REL_SPEED : 53|14@0- (0.01,0) [-81.92|81.92] "" XXX
SG_ STATE_2 : 55|2@0+ (1,0) [0|3] "" XXX SG_ STATE_2 : 55|2@0+ (1,0) [0|3] "" XXX
""") """)
return {"hyundai_kia_mando_front_radar.dbc": "".join(parts)}

View File

@@ -1,11 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc") def generate():
rivian_path = os.path.dirname(os.path.realpath(__file__)) parts = []
with open(os.path.join(rivian_path, dbc_name), "w", encoding='utf-8') as f: parts.append("""
f.write("""
VERSION "" VERSION ""
@@ -44,8 +42,8 @@ BS_:
BU_: XXX BU_: XXX
""") """)
for a in range(0x500, 0x500 + 32): for a in range(0x500, 0x500 + 32):
f.write(f""" parts.append(f"""
BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR
SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX SG_ CHECKSUM : 0|8@1+ (1,0) [0|255] "" XXX
SG_ COUNTER : 11|4@0+ (1,0) [0|15] "" XXX SG_ COUNTER : 11|4@0+ (1,0) [0|15] "" XXX
@@ -57,8 +55,10 @@ BO_ {a} RADAR_TRACK_{a:x}: 8 RADAR
SG_ REL_SPEED : 53|14@0- (0.01,0) [-81.92|81.92] "m/s" XXX SG_ REL_SPEED : 53|14@0- (0.01,0) [-81.92|81.92] "m/s" XXX
""") """)
for a in range(0x500, 0x500 + 32): for a in range(0x500, 0x500 + 32):
f.write(f""" parts.append(f"""
VAL_ {a} STATE 0 "Empty" 1 "New" 2 "New_updated" 3 "Updated" 4 "Coasting" 7 "New_coasting" ; VAL_ {a} STATE 0 "Empty" 1 "New" 2 "New_updated" 3 "Updated" 4 "Coasting" 7 "New_coasting" ;
VAL_ {a} MODE 0 "None" 1 "SRR" 2 "LRR" 3 "SRR_and_LRR" ; VAL_ {a} MODE 0 "None" 1 "SRR" 2 "LRR" 3 "SRR_and_LRR" ;
""") """)
return {"rivian_mando_front_radar.dbc": "".join(parts)}

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
from opendbc.dbc.generator.tesla._radar_common import get_radar_point_definition, get_val_definition from opendbc.dbc.generator.tesla._radar_common import get_radar_point_definition, get_val_definition
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc") def generate():
tesla_path = os.path.dirname(os.path.realpath(__file__)) parts = []
with open(os.path.join(tesla_path, dbc_name), "w", encoding='utf-8') as f: parts.append("""
f.write("""
VERSION "" VERSION ""
NS_ : NS_ :
@@ -133,15 +131,15 @@ BO_ 1281 TeslaRadarAlertMatrix: 8 Radar
SG_ unused62 : 62|2@1+ (1,0) [0|3] "" Autopilot SG_ unused62 : 62|2@1+ (1,0) [0|3] "" Autopilot
""") """)
M_RANGE = range(0x310, 0x36D + 1, 3) M_RANGE = range(0x310, 0x36D + 1, 3)
for i, base_id in enumerate(M_RANGE): for i, base_id in enumerate(M_RANGE):
f.write(get_radar_point_definition(base_id, f"RadarPoint{i}")) parts.append(get_radar_point_definition(base_id, f"RadarPoint{i}"))
L_RANGE = range(0x371, 0x37D + 1, 3) L_RANGE = range(0x371, 0x37D + 1, 3)
for i, base_id in enumerate(L_RANGE): for i, base_id in enumerate(L_RANGE):
f.write(get_radar_point_definition(base_id, f"ProcessedRadarPoint{i+1}")) parts.append(get_radar_point_definition(base_id, f"ProcessedRadarPoint{i+1}"))
f.write(""" parts.append("""
BO_ 697 VIN_VIP_405HS: 8 Autopilot BO_ 697 VIN_VIP_405HS: 8 Autopilot
SG_ VIN_MuxID M : 0|8@1+ (1,0) [0|0] "" Radar SG_ VIN_MuxID M : 0|8@1+ (1,0) [0|0] "" Radar
SG_ VIN_Part1 m16 : 47|24@0+ (1,0) [0|16777215] "" Radar SG_ VIN_Part1 m16 : 47|24@0+ (1,0) [0|16777215] "" Radar
@@ -278,5 +276,7 @@ BA_ "GenMsgCycleTime" BO_ 729 1000;
VAL_ 681 Msg2A9_FourWheelDrive 3 "SNA" 2 "UNUSED" 1 "4WD" 0 "2WD" ;""") VAL_ 681 Msg2A9_FourWheelDrive 3 "SNA" 2 "UNUSED" 1 "4WD" 0 "2WD" ;""")
for base_id in list(M_RANGE) + list(L_RANGE): for base_id in list(M_RANGE) + list(L_RANGE):
f.write(get_val_definition(base_id)) parts.append(get_val_definition(base_id))
return {"tesla_radar_bosch.dbc": "".join(parts)}

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
from opendbc.dbc.generator.tesla._radar_common import get_radar_point_definition, get_val_definition from opendbc.dbc.generator.tesla._radar_common import get_radar_point_definition, get_val_definition
if __name__ == "__main__":
dbc_name = os.path.basename(__file__).replace(".py", ".dbc") def generate():
tesla_path = os.path.dirname(os.path.realpath(__file__)) parts = []
with open(os.path.join(tesla_path, dbc_name), "w", encoding='utf-8') as f: parts.append("""
f.write("""
VERSION "" VERSION ""
NS_ : NS_ :
@@ -66,12 +64,14 @@ BO_ 1601 UDS_radcRequest: 8 Diag
SG_ UDS_radcRequestData : 7|64@0+ (1,0) [0|1.84467e+19] "" Radar SG_ UDS_radcRequestData : 7|64@0+ (1,0) [0|1.84467e+19] "" Radar
""") """)
POINT_RANGE = range(0x410, 0x45E + 1, 2) POINT_RANGE = range(0x410, 0x45E + 1, 2)
for i, base_id in enumerate(POINT_RANGE): for i, base_id in enumerate(POINT_RANGE):
f.write(get_radar_point_definition(base_id, f"RadarPoint{i}")) parts.append(get_radar_point_definition(base_id, f"RadarPoint{i}"))
f.write(""" parts.append("""
VAL_ 1025 lowPowerMode 1 "COMMANDED_LOW_POWER" 0 "DEFAULT_LOW_POWER" 2 "NORMAL_POWER" 3 "SNA";""") VAL_ 1025 lowPowerMode 1 "COMMANDED_LOW_POWER" 0 "DEFAULT_LOW_POWER" 2 "NORMAL_POWER" 3 "SNA";""")
for base_id in list(POINT_RANGE): for base_id in list(POINT_RANGE):
f.write(get_val_definition(base_id)) parts.append(get_val_definition(base_id))
return {"tesla_radar_continental.dbc": "".join(parts)}

View File

@@ -1,45 +0,0 @@
import os
import platform
system = platform.system()
env = Environment(
CFLAGS=[
'-Wall',
"-Wextra",
'-Werror',
'-nostdlib',
'-fno-builtin',
'-std=gnu11',
'-Wfatal-errors',
'-Wno-pointer-to-int-cast',
'-g',
'-O0',
'-fno-omit-frame-pointer',
'-DALLOW_DEBUG',
],
LINKFLAGS=[
'-fsanitize=undefined',
'-fno-sanitize-recover=undefined',
],
CPPPATH=["#"],
tools=["default", "compilation_db"],
)
# add coverage if available
# Use TryCompile (not TryLink) because -nostdlib in CFLAGS breaks the link probe.
conf = Configure(env, log_file=os.devnull)
prev = env['CFLAGS'][:]
env.Append(CFLAGS=['-fprofile-arcs', '-ftest-coverage'])
has_coverage = conf.TryCompile('int x;\n', '.c')
env['CFLAGS'] = prev
if has_coverage:
env.Append(CFLAGS=['-fprofile-arcs', '-ftest-coverage'])
env.Append(LINKFLAGS=['-fprofile-arcs', '-ftest-coverage'])
env = conf.Finish()
safety = env.SharedObject("safety.os", "safety.c")
libsafety = env.SharedLibrary("libsafety.so", [safety])
# GCC-style note file is generated by compiler, allow scons to clean it up
env.SideEffect("safety.gcno", safety)

View File

@@ -1,10 +1,40 @@
import os import os
import subprocess
import tempfile
from pathlib import Path
from cffi import FFI from cffi import FFI
from opendbc.safety import LEN_TO_DLC from opendbc.safety import LEN_TO_DLC
libsafety_dir = os.path.dirname(os.path.abspath(__file__)) libsafety_dir = os.path.dirname(os.path.abspath(__file__))
def _build_libsafety() -> str:
"""Compile libsafety.so to a temp file and return its path."""
root = str(Path(libsafety_dir).parents[3])
safety_c = os.path.join(libsafety_dir, "safety.c")
safety_os = os.path.join(libsafety_dir, "safety.os")
cflags = [
'-Wall', '-Wextra', '-Werror', '-nostdlib', '-fno-builtin',
'-std=gnu11', '-Wfatal-errors', '-Wno-pointer-to-int-cast',
'-g', '-O0', '-fno-omit-frame-pointer', '-DALLOW_DEBUG',
'-fprofile-arcs', '-ftest-coverage',
]
ldflags = [
'-fsanitize=undefined', '-fno-sanitize-recover=undefined',
'-fprofile-arcs', '-ftest-coverage',
]
fd, libsafety_so = tempfile.mkstemp(suffix='.so')
os.close(fd)
subprocess.check_call(['cc', '-fPIC', *cflags, '-I', root, '-c', safety_c, '-o', safety_os])
subprocess.check_call(['cc', '-shared', safety_os, '-o', libsafety_so, *ldflags])
return libsafety_so
ffi = FFI() ffi = FFI()
ffi.cdef(""" ffi.cdef("""
@@ -83,12 +113,18 @@ int mutation_get_active_mutant(void);
class LibSafety: class LibSafety:
pass pass
libsafety: LibSafety = ffi.dlopen(os.path.join(libsafety_dir, "libsafety.so")) libsafety: LibSafety
def load(path): def load(path):
global libsafety global libsafety
libsafety = ffi.dlopen(str(path)) libsafety = ffi.dlopen(str(path))
def __getattr__(name):
if name == "libsafety":
load(_build_libsafety())
return libsafety
raise AttributeError(name)
def make_CANPacket(addr: int, bus: int, dat): def make_CANPacket(addr: int, bus: int, dat):
ret = ffi.new('CANPacket_t *') ret = ffi.new('CANPacket_t *')
ret[0].extended = 1 if addr >= 0x800 else 0 ret[0].extended = 1 if addr >= 0x800 else 0

View File

@@ -6,9 +6,8 @@ cd $DIR
source ../../../setup.sh source ../../../setup.sh
# reset coverage data and generate gcc note file # reset coverage data
rm -f ./libsafety/*.gcda rm -f ./libsafety/*.gcda
scons -j$(nproc) -D
# run safety tests and generate coverage data # run safety tests and generate coverage data
python -m unittest discover -s . -p 'test_*.py' -t ../../../ python -m unittest discover -s . -p 'test_*.py' -t ../../../

View File

@@ -10,7 +10,6 @@ requires-python = ">=3.11,<3.13" # pycapnp doesn't work with 3.13
urls = { "homepage" = "https://github.com/commaai/opendbc" } urls = { "homepage" = "https://github.com/commaai/opendbc" }
dependencies = [ dependencies = [
"scons",
"numpy", "numpy",
"tqdm", "tqdm",
"pycapnp==2.1.0", "pycapnp==2.1.0",
@@ -90,9 +89,6 @@ ignore = [
] ]
flake8-implicit-str-concat.allow-multiline=false flake8-implicit-str-concat.allow-multiline=false
[tool.ruff.lint.per-file-ignores]
"site_scons/*" = ["ALL"]
[tool.ruff.lint.flake8-tidy-imports.banned-api] [tool.ruff.lint.flake8-tidy-imports.banned-api]
"numpy.mean".msg = "Sum and divide. np.mean is slow" "numpy.mean".msg = "Sum and divide. np.mean is slow"

View File

@@ -9,13 +9,10 @@ source ./setup.sh
# *** uv lockfile check *** # *** uv lockfile check ***
uv lock --check uv lock --check
# *** build ***
scons -j8
# *** lint + test *** # *** lint + test ***
lefthook run test lefthook run test
# *** all done *** # *** all done ***
GREEN='\033[0;32m' GREEN='\033[0;32m'
NC='\033[0m' NC='\033[0m'
printf "\n${GREEN}All good!${NC} Finished build, lint, and test in ${SECONDS}s\n" printf "\n${GREEN}All good!${NC} Finished lint and test in ${SECONDS}s\n"

11
uv.lock generated
View File

@@ -384,7 +384,6 @@ dependencies = [
{ name = "numpy" }, { name = "numpy" },
{ name = "pycapnp" }, { name = "pycapnp" },
{ name = "pycryptodome" }, { name = "pycryptodome" },
{ name = "scons" },
{ name = "tqdm" }, { name = "tqdm" },
] ]
@@ -428,7 +427,6 @@ requires-dist = [
{ name = "pycapnp", specifier = "==2.1.0" }, { name = "pycapnp", specifier = "==2.1.0" },
{ name = "pycryptodome" }, { name = "pycryptodome" },
{ name = "ruff", marker = "extra == 'testing'" }, { name = "ruff", marker = "extra == 'testing'" },
{ name = "scons" },
{ name = "tqdm" }, { name = "tqdm" },
{ name = "tree-sitter", marker = "extra == 'testing'" }, { name = "tree-sitter", marker = "extra == 'testing'" },
{ name = "tree-sitter-c", marker = "extra == 'testing'" }, { name = "tree-sitter-c", marker = "extra == 'testing'" },
@@ -547,15 +545,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
] ]
[[package]]
name = "scons"
version = "4.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" },
]
[[package]] [[package]]
name = "sortedcontainers" name = "sortedcontainers"
version = "2.4.0" version = "2.4.0"