diff --git a/.github/labeler.yaml b/.github/labeler.yaml
new file mode 100644
index 0000000000..9b71460ed1
--- /dev/null
+++ b/.github/labeler.yaml
@@ -0,0 +1,68 @@
+CI / testing:
+ - changed-files:
+ - any-glob-to-any-file: '.github/**'
+
+car:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/**'
+
+can:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/can/**'
+
+DBC signals:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/dbc/**'
+
+
+body:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/body/**'
+
+chrysler:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/chrysler/**'
+
+ford:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/ford/**'
+
+gm:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/gm/**'
+
+honda:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/honda/**'
+
+hyundai:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/hyundai/**'
+
+mazda:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/mazda/**'
+
+nissan:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/nissan/**'
+
+subaru:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/subaru/**'
+
+tesla:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/tesla/**'
+
+toyota:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/toyota/**'
+
+volkswagen:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/volkswagen/**'
+
+fingerprint:
+ - changed-files:
+ - any-glob-to-any-file: 'opendbc/car/*/fingerprints.py'
diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml
new file mode 100644
index 0000000000..4585b98244
--- /dev/null
+++ b/.github/workflows/auto_pr_review.yaml
@@ -0,0 +1,49 @@
+name: "PR review"
+on:
+ pull_request_target:
+ types: [opened, reopened, synchronize, edited, edited]
+
+jobs:
+ labeler:
+ name: review
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # Label PRs
+ - uses: actions/labeler@v5.0.0
+ with:
+ dot: true
+ configuration-path: .github/labeler.yaml
+
+ # Check PR target branch
+ - name: check branch
+ uses: Vankka/pr-target-branch-action@def32ec9d93514138d6ac0132ee62e120a72aed5
+ if: github.repository == 'commaai/opendbc'
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ target: /^(?!master$).*/
+ exclude: /commaai:.*/
+ change-to: ${{ github.base_ref }}
+ already-exists-action: close_this
+ already-exists-comment: "Your PR should be made against the `master` branch"
+
+ # Welcome comment
+ - name: "First timers PR"
+ uses: actions/first-interaction@v1
+ if: github.event.pull_request.head.repo.full_name != 'commaai/opendbc'
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ pr-message: |
+
+ Thanks for contributing to opendbc! In order for us to review your PR as quickly as possible, check the following:
+ * Convert your PR to a draft unless it's ready to review
+ * Read the [contributing docs](https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md)
+ * Before marking as "ready for review", ensure:
+ * the goal is clearly stated in the description
+ * all the tests are passing
+ * include a route or your device' dongle ID if relevant
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 711059405b..493a5de0ec 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,48 +1,52 @@
name: tests
-on: [push, pull_request]
+on:
+ push:
+ branches:
+ - master
+ pull_request:
-env:
- RUN: docker run -e PYTHONWARNINGS="error,default::DeprecationWarning" --shm-size 1G --rm opendbc /bin/bash -c
- BUILD: docker buildx build --pull --load --cache-to type=inline --cache-from type=registry,ref=ghcr.io/commaai/opendbc:latest -t opendbc -f Dockerfile .
- PYTHONWARNINGS: error
+# TODO: enable this
+#env:
+# PYTHONWARNINGS: error,default::DeprecationWarning
jobs:
unit-tests:
name: unit tests
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
+ timeout-minutes: 1
#strategy:
# fail-fast: false
# matrix:
# run: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
steps:
- uses: actions/checkout@v4
- - name: Build Docker image
- run: eval "$BUILD"
- - name: Unit tests
- run: ${{ env.RUN }} "pytest -n logical --durations=0"
+ - name: setup python
+ run: |
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+ uv venv
+ # TODO: remove setuptools once https://github.com/cython/cython/issues/5751 is fixed
+ uv pip install -e .[testing,docs] setuptools
+ - run: uv run scons -j$(nproc)
+ - run: uv run pytest .
static-analysis:
name: static analysis
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Build Docker image
- run: eval "$BUILD"
- - name: pre-commit
- # TODO: a package pre-commit installs has a warning, remove the unset once that's fixed
- run: ${{ env.RUN }} "git init && git add -A && unset PYTHONWARNINGS && pre-commit run --all"
-
- docker-push:
- name: docker push
- runs-on: ubuntu-latest
- if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/opendbc'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 1
steps:
- uses: actions/checkout@v4
- - name: Build Docker image
- run: eval "$BUILD"
- - name: Push to dockerhub
+ - run: sudo apt install --no-install-recommends -y cppcheck
+ - name: setup python
run: |
- docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
- docker tag opendbc ghcr.io/commaai/opendbc:latest
- docker push ghcr.io/commaai/opendbc:latest
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+ uv venv
+ uv pip install -e . pre-commit
+ - name: cache pre-commit environments
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pre-commit
+ key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-pre-commit-
+ - run: uv run pre-commit run --all
diff --git a/.gitignore b/.gitignore
index 3b68262069..d66bd7dcb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/build/
.mypy_cache/
*.pyc
*.os
@@ -7,6 +8,10 @@
.*.swp
.DS_Store
.sconsign.dblite
+.hypothesis
+*.egg-info/
+*.html
+uv.lock
opendbc/can/*.so
opendbc/can/*.a
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4d0e67ec84..df68399b4f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,19 +9,16 @@ repos:
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- repo: https://github.com/codespell-project/codespell
- rev: v2.2.6
+ rev: v2.3.0
hooks:
- id: codespell
exclude: '\.dbc$'
- args:
- - --check-hidden
- - --builtins clear,rare,informal,usage,code,names,en-GB_to_en-US
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.9.0
+ rev: v1.11.1
hooks:
- id: mypy
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.3.2
+ rev: v0.5.7
hooks:
- id: ruff
- repo: https://github.com/MarcoGorelli/cython-lint
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 7cbf7c522d..0000000000
--- a/Dockerfile
+++ /dev/null
@@ -1,47 +0,0 @@
-FROM ubuntu:24.04
-
-ENV DEBIAN_FRONTEND=noninteractive
-RUN apt-get update && apt-get install -y --no-install-recommends \
- autoconf \
- build-essential \
- ca-certificates \
- capnproto \
- clang \
- cppcheck \
- curl \
- git \
- libtool \
- make \
- libbz2-dev \
- libffi-dev \
- libcapnp-dev \
- liblzma-dev \
- libncurses5-dev \
- libncursesw5-dev \
- libreadline-dev \
- libssl-dev \
- libsqlite3-dev \
- libzmq3-dev \
- llvm \
- ocl-icd-opencl-dev \
- opencl-headers \
- tk-dev \
- python3-pip \
- python3-dev \
- python3-openssl \
- python-is-python3 \
- xz-utils \
- zlib1g-dev \
- cmake \
- && rm -rf /var/lib/apt/lists/*
-
-COPY requirements.txt /tmp/
-RUN pip3 install --break-system-packages --no-cache-dir -r /tmp/requirements.txt
-RUN pip3 install --break-system-packages --no-cache-dir pre-commit==2.15.0 pylint==2.17.4
-
-WORKDIR /project/opendbc
-ENV PYTHONPATH=/project/opendbc
-
-COPY . .
-RUN ls && rm -rf .git && \
- scons -c && scons -j$(nproc) \
diff --git a/README.md b/README.md
index 20e31a9034..1aa116fa09 100644
--- a/README.md
+++ b/README.md
@@ -1,49 +1,28 @@
-## DBC file basics
-
-A DBC file encodes, in a humanly readable way, the information needed to understand a vehicle's CAN bus traffic. A vehicle might have multiple CAN buses and every CAN bus is represented by its own dbc file.
-Wondering what's the DBC file format? [Here](http://www.socialledge.com/sjsu/index.php?title=DBC_Format) and [Here](https://github.com/stefanhoelzl/CANpy/blob/master/docs/DBC_Specification.md) a couple of good overviews.
-
-## How to start reverse engineering cars
-
-[opendbc](https://github.com/commaai/opendbc) is integrated with [cabana](https://github.com/commaai/openpilot/tree/master/tools/cabana).
-
-Use [panda](https://github.com/commaai/panda) to connect your car to a computer.
-
-## How to use reverse engineered DBC
-To create custom CAN simulations or send reverse engineered signals back to the car you can use [CANdevStudio](https://github.com/GENIVI/CANdevStudio) project.
-
-## DBC file preprocessor
-
-DBC files for different models of the same brand have a lot of overlap. Therefore, we wrote a preprocessor to create DBC files from a brand DBC file and a model specific DBC file. The source DBC files can be found in the generator folder. After changing one of the files run the generator.py script to regenerate the output files. These output files will be placed in the root of the opendbc repository and are suffixed by _generated.
-
-## Good practices for contributing to opendbc
-
-- Comments: the best way to store comments is to add them directly to the DBC files. For example:
- ```
- CM_ SG_ 490 LONG_ACCEL "wheel speed derivative, noisy and zero snapping";
- ```
- is a comment that refers to signal `LONG_ACCEL` in message `490`. Using comments is highly recommended, especially for doubts and uncertainties. [cabana](https://community.comma.ai/cabana/) can easily display/add/edit comments to signals and messages.
-
-- Units: when applicable, it's recommended to convert signals into physical units, by using a proper signal factor. Using a SI unit is preferred, unless a non-SI unit rounds the signal factor much better.
-For example:
- ```
- SG_ VEHICLE_SPEED : 7|15@0+ (0.00278,0) [0|70] "m/s" PCM
- ```
- is better than:
- ```
- SG_ VEHICLE_SPEED : 7|15@0+ (0.00620,0) [0|115] "mph" PCM
- ```
- However, the cleanest option is really:
- ```
- SG_ VEHICLE_SPEED : 7|15@0+ (0.01,0) [0|250] "kph" PCM
- ```
-
-- Signal size: always use the smallest amount of bits possible. For example, let's say I'm reverse engineering the gas pedal position and I've determined that it's in a 3 bytes message. For 0% pedal position I read a message value of `0x00 0x00 0x00`, while for 100% of pedal position I read `0x64 0x00 0x00`: clearly, the gas pedal position is within the first byte of the message and I might be tempted to define the signal `GAS_POS` as:
- ```
- SG_ GAS_POS : 7|8@0+ (1,0) [0|100] "%" PCM
- ```
- However, I can't be sure that the very first bit of the message is referred to the pedal position: I haven't seen it changing! Therefore, a safer way of defining the signal is:
- ```
- SG_ GAS_POS : 6|7@0+ (1,0) [0|100] "%" PCM
- ```
- which leaves the first bit unallocated. This prevents from very erroneous reading of the gas pedal position, in case the first bit is indeed used for something else.
+# opendbc
+
+opendbc is a Python API for your car. Read the speed, steering angle, and more. Send gas, braking, and steering commands.
+
+## Structure
+* [`opendbc/dbc/`](opendbc/dbc/) is a repository of [DBC](https://en.wikipedia.org/wiki/CAN_bus#DBC) files
+* [`opendbc/can/`](opendbc/can/) is a library for parsing and building CAN messages from DBC files
+* [`opendbc/car/`](opendbc/car/) is a high-level library for interfacing with cars using Python
+
+## Quick start
+
+```bash
+git clone https://github.com/commaai/opendbc.git
+
+cd opendbc
+
+# Install the dependencies
+pip3 install -e .[testing,docs]
+
+# Build
+scons -j8
+
+# Run the tests
+pytest .
+
+# Run the linter
+pre-commit run --all-files
+```
diff --git a/SConstruct b/SConstruct
index 2401720f17..932ff26e8b 100644
--- a/SConstruct
+++ b/SConstruct
@@ -3,7 +3,6 @@ import subprocess
import sysconfig
import numpy as np
-zmq = 'zmq'
arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip()
python_path = sysconfig.get_paths()['include']
@@ -28,8 +27,8 @@ ldflags_asan = ["-fsanitize=address"] if GetOption('asan') else []
env = Environment(
ENV=os.environ,
- CC='clang',
- CXX='clang++',
+ CC='gcc',
+ CXX='g++',
CCFLAGS=[
"-g",
"-fPIC",
@@ -38,6 +37,7 @@ env = Environment(
"-Werror",
"-Wshadow",
"-Wno-vla-cxx-extension",
+ "-Wno-unknown-warning-option", # for compatibility across compiler versions
] + ccflags_asan,
LDFLAGS=ldflags_asan,
LINKFLAGS=ldflags_asan,
@@ -52,7 +52,7 @@ env = Environment(
)
common = ''
-Export('env', 'zmq', 'arch', 'common')
+Export('env', 'arch', 'common')
envCython = env.Clone()
envCython["CPPPATH"] += [np.get_include()]
diff --git a/examples/joystick.py b/examples/joystick.py
new file mode 100755
index 0000000000..9a2e1c08be
--- /dev/null
+++ b/examples/joystick.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+import time
+import threading
+import argparse
+import numpy as np
+from inputs import get_gamepad
+
+from kbhit import KBHit
+
+from opendbc.car.structs import CarControl
+from opendbc.car.panda_runner import PandaRunner
+from opendbc.car.can_definitions import CanData
+
+class Keyboard:
+ def __init__(self):
+ self.kb = KBHit()
+ self.axis_increment = 0.05 # 5% of full actuation each key press
+ self.axes_map = {'w': 'gb', 's': 'gb',
+ 'a': 'steer', 'd': 'steer'}
+ self.axes_values = {'gb': 0., 'steer': 0.}
+ self.axes_order = ['gb', 'steer']
+ self.cancel = False
+
+ def update(self):
+ key = self.kb.getch().lower()
+ print(key)
+ self.cancel = False
+ if key == 'r':
+ self.axes_values = {ax: 0. for ax in self.axes_values}
+ elif key == 'c':
+ self.cancel = True
+ elif key in self.axes_map:
+ axis = self.axes_map[key]
+ incr = self.axis_increment if key in ['w', 'a'] else -self.axis_increment
+ self.axes_values[axis] = float(np.clip(self.axes_values[axis] + incr, -1, 1))
+ else:
+ return False
+ return True
+
+class Joystick:
+ def __init__(self, gamepad=False):
+ # TODO: find a way to get this from API, perhaps "inputs" doesn't support it
+ if gamepad:
+ self.cancel_button = 'BTN_NORTH' # (BTN_NORTH=X, ABS_RZ=Right Trigger)
+ accel_axis = 'ABS_Y'
+ steer_axis = 'ABS_RX'
+ else:
+ self.cancel_button = 'BTN_TRIGGER'
+ accel_axis = 'ABS_Y'
+ steer_axis = 'ABS_RX'
+ self.min_axis_value = {accel_axis: 0., steer_axis: 0.}
+ self.max_axis_value = {accel_axis: 255., steer_axis: 255.}
+ self.axes_values = {accel_axis: 0., steer_axis: 0.}
+ self.axes_order = [accel_axis, steer_axis]
+ self.cancel = False
+
+ def update(self):
+ joystick_event = get_gamepad()[0]
+ event = (joystick_event.code, joystick_event.state)
+ if event[0] == self.cancel_button:
+ if event[1] == 1:
+ self.cancel = True
+ elif event[1] == 0: # state 0 is falling edge
+ self.cancel = False
+ elif event[0] in self.axes_values:
+ self.max_axis_value[event[0]] = max(event[1], self.max_axis_value[event[0]])
+ self.min_axis_value[event[0]] = min(event[1], self.min_axis_value[event[0]])
+
+ norm = -float(np.interp(event[1], [self.min_axis_value[event[0]], self.max_axis_value[event[0]]], [-1., 1.]))
+ self.axes_values[event[0]] = norm if abs(norm) > 0.05 else 0. # center can be noisy, deadzone of 5%
+ else:
+ return False
+ return True
+
+def joystick_thread(joystick):
+ while True:
+ joystick.update()
+
+def main(joystick):
+ threading.Thread(target=joystick_thread, args=(joystick,), daemon=True).start()
+ with PandaRunner() as (p, CI):
+ CC = CarControl(enabled=False)
+ while True:
+ cd = [CanData(addr, dat, bus) for addr, dat, bus in p.can_recv()]
+ CI.update([0, cd])
+
+ CC.actuators.accel = float(4.0*np.clip(joystick.axes_values['gb'], -1, 1))
+ CC.actuators.steer = float(np.clip(joystick.axes_values['steer'], -1, 1))
+
+ from pprint import pprint
+ pprint(CC)
+
+ _, can_sends = CI.apply(CC)
+ p.can_send_many(can_sends, timeout=1000)
+
+ # 100Hz
+ time.sleep(0.01)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Test the car interface with a joystick. Uses keyboard by default.',
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+
+ parser.add_argument('--mode', choices=['keyboard', 'gamepad', 'joystick'], default='keyboard')
+ args = parser.parse_args()
+
+ print()
+ joystick: Keyboard | Joystick
+ if args.mode == 'keyboard':
+ print('Gas/brake control: `W` and `S` keys')
+ print('Steering control: `A` and `D` keys')
+ print('Buttons')
+ print('- `R`: Resets axes')
+ print('- `C`: Cancel cruise control')
+ joystick = Keyboard()
+ else:
+ joystick = Joystick(gamepad=(args.mode == 'gamepad'))
+ main(joystick)
\ No newline at end of file
diff --git a/examples/kbhit.py b/examples/kbhit.py
new file mode 100755
index 0000000000..35f67b4771
--- /dev/null
+++ b/examples/kbhit.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+import sys
+import termios
+import atexit
+from select import select
+
+STDIN_FD = sys.stdin.fileno()
+
+class KBHit:
+ def __init__(self) -> None:
+ self.set_kbhit_terminal()
+
+ def set_kbhit_terminal(self) -> None:
+ # Save the terminal settings
+ self.old_term = termios.tcgetattr(STDIN_FD)
+ self.new_term = self.old_term.copy()
+
+ # New terminal setting unbuffered
+ self.new_term[3] &= ~(termios.ICANON | termios.ECHO)
+ termios.tcsetattr(STDIN_FD, termios.TCSAFLUSH, self.new_term)
+
+ # Support normal-terminal reset at exit
+ atexit.register(self.set_normal_term)
+
+ def set_normal_term(self) -> None:
+ termios.tcsetattr(STDIN_FD, termios.TCSAFLUSH, self.old_term)
+
+ @staticmethod
+ def getch() -> str:
+ return sys.stdin.read(1)
+
+ @staticmethod
+ def getarrow() -> int:
+ c = sys.stdin.read(3)[2]
+ vals = [65, 67, 66, 68]
+ return vals.index(ord(c))
+
+ @staticmethod
+ def kbhit():
+ ''' Returns True if keyboard character was hit, False otherwise.
+ '''
+ return select([sys.stdin], [], [], 0)[0] != []
+
+
+if __name__ == "__main__":
+
+ kb = KBHit()
+
+ print('Hit any key, or ESC to exit')
+
+ while True:
+
+ if kb.kbhit():
+ c = kb.getch()
+ if c == '\x1b': # ESC
+ break
+ print(c)
+
+ kb.set_normal_term()
diff --git a/examples/longitudinal-profiles.py b/examples/longitudinal-profiles.py
new file mode 100755
index 0000000000..e7a366d1e2
--- /dev/null
+++ b/examples/longitudinal-profiles.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+import io
+import os
+import time
+import base64
+import argparse
+import numpy as np
+import matplotlib.pyplot as plt
+from collections import defaultdict
+from dataclasses import dataclass, asdict
+from pathlib import Path
+
+from opendbc.car.structs import CarControl
+from opendbc.car.panda_runner import PandaRunner
+from opendbc.car.common.conversions import Conversions
+
+DT = 0.01 # step time (s)
+
+# TODOs
+# - support lateral maneuvers
+# - setup: show countdown?
+
+
+class Ratekeeper:
+ def __init__(self, rate: float) -> None:
+ self.interval = 1. / rate
+ self.next_frame_time = time.monotonic() + self.interval
+
+ def keep_time(self) -> bool:
+ lagged = False
+ remaining = self.next_frame_time - time.monotonic()
+ self.next_frame_time += self.interval
+ if remaining < -0.1:
+ print(f"lagging by {-remaining * 1000:.2f} ms")
+ lagged = True
+
+ if remaining > 0:
+ time.sleep(remaining)
+ return lagged
+
+@dataclass
+class Action:
+ accel: float # m/s^2
+ duration: float # seconds
+ longControlState: CarControl.Actuators.LongControlState = CarControl.Actuators.LongControlState.pid
+
+ def get_msgs(self):
+ return [
+ (t, CarControl(
+ enabled=True,
+ longActive=True,
+ actuators=CarControl.Actuators(
+ accel=self.accel,
+ longControlState=self.longControlState,
+ ),
+ ))
+ for t in np.linspace(0, self.duration, int(self.duration/DT))
+ ]
+
+@dataclass
+class Maneuver:
+ description: str
+ actions: list[Action]
+ repeat: int = 1
+ initial_speed: float = 0. # m/s
+
+ def get_msgs(self):
+ t0 = 0
+ for action in self.actions:
+ for lt, msg in action.get_msgs():
+ yield lt + t0, msg
+ t0 += lt
+
+MANEUVERS = [
+ Maneuver(
+ "creep: alternate between +1m/ss and -1m/ss",
+ [
+ Action(1, 2), Action(-1, 2),
+ Action(1, 2), Action(-1, 2),
+ Action(1, 2), Action(-1, 2),
+ ],
+ repeat=2,
+ initial_speed=0.,
+ ),
+ Maneuver(
+ "brake step response: -1m/ss from 20mph",
+ [Action(0, 2), Action(-1, 3)],
+ repeat=3,
+ initial_speed=20. * Conversions.MPH_TO_MS,
+ ),
+ Maneuver(
+ "brake step response: -4m/ss from 20mph",
+ [Action(0, 2), Action(-4, 3)],
+ repeat=3,
+ initial_speed=20. * Conversions.MPH_TO_MS,
+ ),
+ Maneuver(
+ "gas step response: +1m/ss from 20mph",
+ [Action(0, 2), Action(1, 3)],
+ repeat=3,
+ initial_speed=20. * Conversions.MPH_TO_MS,
+ ),
+ Maneuver(
+ "gas step response: +4m/ss from 20mph",
+ [Action(0, 2), Action(4, 3)],
+ repeat=3,
+ initial_speed=20. * Conversions.MPH_TO_MS,
+ ),
+]
+
+def report(args, logs, fp):
+ output_path = Path(__file__).resolve().parent / "longitudinal_reports"
+ output_fn = args.output or output_path / f"{fp}_{time.strftime('%Y%m%d-%H_%M_%S')}.html"
+ output_path.mkdir(exist_ok=True)
+ with open(output_fn, "w") as f:
+ f.write("
Longitudinal maneuver report
\n")
+ f.write(f"{fp}
\n")
+ if args.desc:
+ f.write(f"{args.desc}
")
+ for description, runs in logs.items():
+ f.write("\n")
+ f.write(f"{description}
\n")
+ for run, log in runs.items():
+ f.write(f"Run #{int(run)+1}
\n")
+ plt.rcParams['font.size'] = 40
+ fig = plt.figure(figsize=(30, 25))
+ ax = fig.subplots(4, 1, sharex=True, gridspec_kw={'hspace': 0, 'height_ratios': [5, 3, 1, 1]})
+
+ ax[0].grid(linewidth=4)
+ ax[0].plot(log["t"], log["carControl.actuators.accel"], label='accel command', linewidth=6)
+ ax[0].plot(log["t"], log["carState.aEgo"], label='aEgo', linewidth=6)
+ ax[0].set_ylabel('Acceleration (m/s^2)')
+ #ax[0].set_ylim(-6.5, 6.5)
+ ax[0].legend()
+
+ ax[1].grid(linewidth=4)
+ ax[1].plot(log["t"], log["carState.vEgo"], 'g', label='vEgo', linewidth=6)
+ ax[1].set_ylabel('Velocity (m/s)')
+ ax[1].legend()
+
+ ax[2].plot(log["t"], log["carControl.enabled"], label='enabled', linewidth=6)
+ ax[3].plot(log["t"], log["carState.gasPressed"], label='gasPressed', linewidth=6)
+ ax[3].plot(log["t"], log["carState.brakePressed"], label='brakePressed', linewidth=6)
+ for i in (2, 3):
+ ax[i].set_yticks([0, 1], minor=False)
+ ax[i].set_ylim(-1, 2)
+ ax[i].legend()
+
+ ax[-1].set_xlabel("Time (s)")
+ fig.tight_layout()
+
+ buffer = io.BytesIO()
+ fig.savefig(buffer, format='png')
+ buffer.seek(0)
+ f.write(f"\n")
+
+ import json
+ f.write(f"{json.dumps(logs)}
")
+ print(f"\nReport written to {output_fn}\n")
+
+def main(args):
+ with PandaRunner() as p:
+ print("\n\n")
+
+ maneuvers = MANEUVERS
+ if len(args.maneuvers):
+ maneuvers = [MANEUVERS[i-1] for i in set(args.maneuvers)]
+
+ logs = {}
+ rk = Ratekeeper(int(1./DT))
+ for i, m in enumerate(maneuvers):
+ logs[m.description] = {}
+ print(f"Running {i+1}/{len(MANEUVERS)} '{m.description}'")
+ for run in range(m.repeat):
+ print(f"- run #{run}")
+ print("- setting up, engage cruise")
+ ready_cnt = 0
+ for _ in range(int(2*60./DT)):
+ cs = p.read(strict=False)
+ cc = CarControl(
+ enabled=True,
+ longActive=True,
+ actuators=CarControl.Actuators(
+ accel=(m.initial_speed - cs.vEgo)*0.8,
+ longControlState=CarControl.Actuators.LongControlState.pid,
+ ),
+ )
+ if m.initial_speed < 0.1:
+ cc.actuators.accel = -2
+ cc.actuators.longControlState = CarControl.Actuators.LongControlState.stopping
+ p.write(cc)
+
+ ready = cs.cruiseState.enabled and not cs.cruiseState.standstill and ((m.initial_speed - 0.6) < cs.vEgo < (m.initial_speed + 0.6))
+ ready_cnt = (ready_cnt+1) if ready else 0
+ if ready_cnt > (2./DT):
+ break
+ rk.keep_time()
+ else:
+ print("ERROR: failed to setup")
+ continue
+
+ print("- executing maneuver")
+ logs[m.description][run] = defaultdict(list)
+ for t, cc in m.get_msgs():
+ cs = p.read()
+ p.write(cc)
+
+ logs[m.description][run]["t"].append(t)
+ to_log = {"carControl": cc, "carState": cs, "carControl.actuators": cc.actuators,
+ "carControl.cruiseControl": cc.cruiseControl, "carState.cruiseState": cs.cruiseState}
+ for k, v in to_log.items():
+ for k2, v2 in asdict(v).items():
+ logs[m.description][run][f"{k}.{k2}"].append(v2)
+
+ rk.keep_time()
+
+ print("writing out report")
+ with open('/tmp/logs.json', 'w') as f:
+ import json
+ json.dump(logs, f, indent=2)
+ report(args, logs, p.CI.CP.carFingerprint)
+
+
+if __name__ == "__main__":
+ maneuver_help = "\n".join([f"{i+1}. {m.description}" for i, m in enumerate(MANEUVERS)])
+ parser = argparse.ArgumentParser(description="A tool for longitudinal control testing.",
+ formatter_class=argparse.RawTextHelpFormatter)
+ parser.add_argument('--desc', help="Extra description to include in report.")
+ parser.add_argument('--output', help="Write out report to this file.", default=None)
+ parser.add_argument('maneuvers', nargs='*', type=int, default=None, help=f'Deafult is all.\n{maneuver_help}')
+ args = parser.parse_args()
+ print(args)
+
+ if "REPORT_TEST" in os.environ:
+ with open(os.environ["REPORT_TEST"]) as f:
+ import json
+ logs = json.loads(f.read().split("none'>")[1].split('')[0])
+ report(args, logs, "testing")
+ exit()
+
+ assert args.output is None or args.output.endswith(".html"), "Output filename must end with '.html'"
+
+ main(args)
diff --git a/opendbc/can/SConscript b/opendbc/can/SConscript
index 1c1629d584..9f6825a93f 100644
--- a/opendbc/can/SConscript
+++ b/opendbc/can/SConscript
@@ -1,30 +1,22 @@
Import('env', 'envCython', 'common', 'arch')
-import os
-
envDBC = env.Clone()
dbc_file_path = '-DDBC_FILE_PATH=\'"%s"\'' % (envDBC.Dir("../dbc").abspath)
envDBC['CXXFLAGS'] += [dbc_file_path]
src = ["dbc.cc", "parser.cc", "packer.cc", "common.cc"]
-libs = [common, "zmq"]
# shared library for openpilot
LINKFLAGS = envDBC["LINKFLAGS"]
if arch == "Darwin":
LINKFLAGS += ["-Wl,-install_name,@loader_path/libdbc.dylib"]
-libdbc = envDBC.SharedLibrary('libdbc', src, LIBS=libs, LINKFLAGS=LINKFLAGS)
-
-# static library for tools like cabana
-envDBC.Library('libdbc_static', src, LIBS=libs)
+libdbc = envDBC.SharedLibrary('libdbc', src, LIBS=[common, ], LINKFLAGS=LINKFLAGS)
# Build packer and parser
lenv = envCython.Clone()
-lenv["LINKFLAGS"] += [libdbc[0].get_labspath()]
-parser = lenv.Program('parser_pyx.so', 'parser_pyx.pyx')
-packer = lenv.Program('packer_pyx.so', 'packer_pyx.pyx')
-
-lenv.Depends(parser, libdbc)
-lenv.Depends(packer, libdbc)
+lenv["LIBPATH"].append(Dir("."))
+lenv["RPATH"] = [libdbc[0].dir.abspath, ]
+parser = lenv.Program('parser_pyx.so', 'parser_pyx.pyx', LIBS=[common, libdbc[0].name])
+packer = lenv.Program('packer_pyx.so', 'packer_pyx.pyx', LIBS=[common, libdbc[0].name])
opendbc_python = Alias("opendbc_python", [parser, packer])
diff --git a/opendbc/can/packer_pyx.pyx b/opendbc/can/packer_pyx.pyx
index 84a7e4544e..4e8ae7d564 100644
--- a/opendbc/can/packer_pyx.pyx
+++ b/opendbc/can/packer_pyx.pyx
@@ -50,4 +50,4 @@ cdef class CANPacker:
pass
cdef vector[uint8_t] val = self.pack(addr, values)
- return [addr, (&val[0])[:val.size()], bus]
+ return addr, (&val[0])[:val.size()], bus
diff --git a/opendbc/can/tests/test_checksums.py b/opendbc/can/tests/test_checksums.py
index c347f3a68b..e584ba30a5 100644
--- a/opendbc/can/tests/test_checksums.py
+++ b/opendbc/can/tests/test_checksums.py
@@ -22,7 +22,7 @@ def test_honda_checksum(self):
checksum_std = [11, 10, 9, 8]
checksum_ext = [4, 3, 2, 1]
- for std, ext in zip(checksum_std, checksum_ext):
+ for std, ext in zip(checksum_std, checksum_ext, strict=True):
msgs = [
packer.make_can_msg("LKAS_HUD", 0, values),
packer.make_can_msg("LKAS_HUD_A", 0, values),
diff --git a/opendbc/can/tests/test_packer_parser.py b/opendbc/can/tests/test_packer_parser.py
index acb5acebe9..2abb8f0578 100644
--- a/opendbc/can/tests/test_packer_parser.py
+++ b/opendbc/can/tests/test_packer_parser.py
@@ -107,8 +107,9 @@ def rx_steering_msg(values, bad_checksum=False):
msg = packer.make_can_msg("STEERING_CONTROL", 0, values)
if bad_checksum:
# add 1 to checksum
- msg[1] = bytearray(msg[1])
- msg[1][4] = (msg[1][4] & 0xF0) | ((msg[1][4] & 0x0F) + 1)
+ dat = bytearray(msg[1])
+ dat[4] = (dat[4] & 0xF0) | ((dat[4] & 0x0F) + 1)
+ msg = (msg[0], bytes(dat), msg[2])
parser.update_strings([0, [msg]])
@@ -350,6 +351,6 @@ def test_allow_undefined_msgs(self):
# discovery tests in openpilot first
packer = CANPacker("toyota_nodsu_pt_generated")
- assert packer.make_can_msg("ACC_CONTROL", 0, {"UNKNOWN_SIGNAL": 0}) == [835, b'\x00\x00\x00\x00\x00\x00\x00N', 0]
- assert packer.make_can_msg("UNKNOWN_MESSAGE", 0, {"UNKNOWN_SIGNAL": 0}) == [0, b'', 0]
- assert packer.make_can_msg(0, 0, {"UNKNOWN_SIGNAL": 0}) == [0, b'', 0]
+ assert packer.make_can_msg("ACC_CONTROL", 0, {"UNKNOWN_SIGNAL": 0}) == (835, b'\x00\x00\x00\x00\x00\x00\x00N', 0)
+ assert packer.make_can_msg("UNKNOWN_MESSAGE", 0, {"UNKNOWN_SIGNAL": 0}) == (0, b'', 0)
+ assert packer.make_can_msg(0, 0, {"UNKNOWN_SIGNAL": 0}) == (0, b'', 0)
diff --git a/opendbc/car/__init__.py b/opendbc/car/__init__.py
new file mode 100644
index 0000000000..455d8c3803
--- /dev/null
+++ b/opendbc/car/__init__.py
@@ -0,0 +1,323 @@
+# functions common among cars
+import logging
+from collections import namedtuple
+from dataclasses import dataclass
+from enum import IntFlag, ReprEnum, EnumType
+from dataclasses import replace
+
+from panda import uds
+from opendbc.car import structs
+from opendbc.car.can_definitions import CanData
+from opendbc.car.docs_definitions import CarDocs
+from opendbc.car.common.numpy_fast import clip, interp
+
+# set up logging
+carlog = logging.getLogger('carlog')
+carlog.setLevel(logging.INFO)
+carlog.propagate = False
+
+DT_CTRL = 0.01 # car state and control loop timestep (s)
+
+# kg of standard extra cargo to count for drive, gas, etc...
+STD_CARGO_KG = 136.
+
+ButtonType = structs.CarState.ButtonEvent.Type
+AngleRateLimit = namedtuple('AngleRateLimit', ['speed_bp', 'angle_v'])
+
+
+def apply_hysteresis(val: float, val_steady: float, hyst_gap: float) -> float:
+ if val > val_steady + hyst_gap:
+ val_steady = val - hyst_gap
+ elif val < val_steady - hyst_gap:
+ val_steady = val + hyst_gap
+ return val_steady
+
+
+def create_button_events(cur_btn: int, prev_btn: int, buttons_dict: dict[int, structs.CarState.ButtonEvent.Type],
+ unpressed_btn: int = 0) -> list[structs.CarState.ButtonEvent]:
+ events: list[structs.CarState.ButtonEvent] = []
+
+ if cur_btn == prev_btn:
+ return events
+
+ # Add events for button presses, multiple when a button switches without going to unpressed
+ for pressed, btn in ((False, prev_btn), (True, cur_btn)):
+ if btn != unpressed_btn:
+ events.append(structs.CarState.ButtonEvent(pressed=pressed,
+ type=buttons_dict.get(btn, ButtonType.unknown)))
+ return events
+
+
+def gen_empty_fingerprint():
+ return {i: {} for i in range(8)}
+
+
+# these params were derived for the Civic and used to calculate params for other cars
+class VehicleDynamicsParams:
+ MASS = 1326. + STD_CARGO_KG
+ WHEELBASE = 2.70
+ CENTER_TO_FRONT = WHEELBASE * 0.4
+ CENTER_TO_REAR = WHEELBASE - CENTER_TO_FRONT
+ ROTATIONAL_INERTIA = 2500
+ TIRE_STIFFNESS_FRONT = 192150
+ TIRE_STIFFNESS_REAR = 202500
+
+
+# TODO: get actual value, for now starting with reasonable value for
+# civic and scaling by mass and wheelbase
+def scale_rot_inertia(mass, wheelbase):
+ return VehicleDynamicsParams.ROTATIONAL_INERTIA * mass * wheelbase ** 2 / (VehicleDynamicsParams.MASS * VehicleDynamicsParams.WHEELBASE ** 2)
+
+
+# TODO: start from empirically derived lateral slip stiffness for the civic and scale by
+# mass and CG position, so all cars will have approximately similar dyn behaviors
+def scale_tire_stiffness(mass, wheelbase, center_to_front, tire_stiffness_factor):
+ center_to_rear = wheelbase - center_to_front
+ tire_stiffness_front = (VehicleDynamicsParams.TIRE_STIFFNESS_FRONT * tire_stiffness_factor) * mass / VehicleDynamicsParams.MASS * \
+ (center_to_rear / wheelbase) / (VehicleDynamicsParams.CENTER_TO_REAR / VehicleDynamicsParams.WHEELBASE)
+
+ tire_stiffness_rear = (VehicleDynamicsParams.TIRE_STIFFNESS_REAR * tire_stiffness_factor) * mass / VehicleDynamicsParams.MASS * \
+ (center_to_front / wheelbase) / (VehicleDynamicsParams.CENTER_TO_FRONT / VehicleDynamicsParams.WHEELBASE)
+
+ return tire_stiffness_front, tire_stiffness_rear
+
+
+DbcDict = dict[str, str]
+
+
+def dbc_dict(pt_dbc, radar_dbc, chassis_dbc=None, body_dbc=None) -> DbcDict:
+ return {'pt': pt_dbc, 'radar': radar_dbc, 'chassis': chassis_dbc, 'body': body_dbc}
+
+
+def apply_driver_steer_torque_limits(apply_torque, apply_torque_last, driver_torque, LIMITS):
+
+ # limits due to driver torque
+ driver_max_torque = LIMITS.STEER_MAX + (LIMITS.STEER_DRIVER_ALLOWANCE + driver_torque * LIMITS.STEER_DRIVER_FACTOR) * LIMITS.STEER_DRIVER_MULTIPLIER
+ driver_min_torque = -LIMITS.STEER_MAX + (-LIMITS.STEER_DRIVER_ALLOWANCE + driver_torque * LIMITS.STEER_DRIVER_FACTOR) * LIMITS.STEER_DRIVER_MULTIPLIER
+ max_steer_allowed = max(min(LIMITS.STEER_MAX, driver_max_torque), 0)
+ min_steer_allowed = min(max(-LIMITS.STEER_MAX, driver_min_torque), 0)
+ apply_torque = clip(apply_torque, min_steer_allowed, max_steer_allowed)
+
+ # slow rate if steer torque increases in magnitude
+ if apply_torque_last > 0:
+ apply_torque = clip(apply_torque, max(apply_torque_last - LIMITS.STEER_DELTA_DOWN, -LIMITS.STEER_DELTA_UP),
+ apply_torque_last + LIMITS.STEER_DELTA_UP)
+ else:
+ apply_torque = clip(apply_torque, apply_torque_last - LIMITS.STEER_DELTA_UP,
+ min(apply_torque_last + LIMITS.STEER_DELTA_DOWN, LIMITS.STEER_DELTA_UP))
+
+ return int(round(float(apply_torque)))
+
+
+def apply_dist_to_meas_limits(val, val_last, val_meas,
+ STEER_DELTA_UP, STEER_DELTA_DOWN,
+ STEER_ERROR_MAX, STEER_MAX):
+ # limits due to comparison of commanded val VS measured val (torque/angle/curvature)
+ max_lim = min(max(val_meas + STEER_ERROR_MAX, STEER_ERROR_MAX), STEER_MAX)
+ min_lim = max(min(val_meas - STEER_ERROR_MAX, -STEER_ERROR_MAX), -STEER_MAX)
+
+ val = clip(val, min_lim, max_lim)
+
+ # slow rate if val increases in magnitude
+ if val_last > 0:
+ val = clip(val,
+ max(val_last - STEER_DELTA_DOWN, -STEER_DELTA_UP),
+ val_last + STEER_DELTA_UP)
+ else:
+ val = clip(val,
+ val_last - STEER_DELTA_UP,
+ min(val_last + STEER_DELTA_DOWN, STEER_DELTA_UP))
+
+ return float(val)
+
+
+def apply_meas_steer_torque_limits(apply_torque, apply_torque_last, motor_torque, LIMITS):
+ return int(round(apply_dist_to_meas_limits(apply_torque, apply_torque_last, motor_torque,
+ LIMITS.STEER_DELTA_UP, LIMITS.STEER_DELTA_DOWN,
+ LIMITS.STEER_ERROR_MAX, LIMITS.STEER_MAX)))
+
+
+def apply_std_steer_angle_limits(apply_angle, apply_angle_last, v_ego, LIMITS):
+ # pick angle rate limits based on wind up/down
+ steer_up = apply_angle_last * apply_angle >= 0. and abs(apply_angle) > abs(apply_angle_last)
+ rate_limits = LIMITS.ANGLE_RATE_LIMIT_UP if steer_up else LIMITS.ANGLE_RATE_LIMIT_DOWN
+
+ angle_rate_lim = interp(v_ego, rate_limits.speed_bp, rate_limits.angle_v)
+ return clip(apply_angle, apply_angle_last - angle_rate_lim, apply_angle_last + angle_rate_lim)
+
+
+def common_fault_avoidance(fault_condition: bool, request: bool, above_limit_frames: int,
+ max_above_limit_frames: int, max_mismatching_frames: int = 1):
+ """
+ Several cars have the ability to work around their EPS limits by cutting the
+ request bit of their LKAS message after a certain number of frames above the limit.
+ """
+
+ # Count up to max_above_limit_frames, at which point we need to cut the request for above_limit_frames to avoid a fault
+ if request and fault_condition:
+ above_limit_frames += 1
+ else:
+ above_limit_frames = 0
+
+ # Once we cut the request bit, count additionally to max_mismatching_frames before setting the request bit high again.
+ # Some brands do not respect our workaround without multiple messages on the bus, for example
+ if above_limit_frames > max_above_limit_frames:
+ request = False
+
+ if above_limit_frames >= max_above_limit_frames + max_mismatching_frames:
+ above_limit_frames = 0
+
+ return above_limit_frames, request
+
+
+def apply_center_deadzone(error, deadzone):
+ if (error > - deadzone) and (error < deadzone):
+ error = 0.
+ return error
+
+
+def rate_limit(new_value, last_value, dw_step, up_step):
+ return clip(new_value, last_value + dw_step, last_value + up_step)
+
+
+def get_friction(lateral_accel_error: float, lateral_accel_deadzone: float, friction_threshold: float,
+ torque_params: structs.CarParams.LateralTorqueTuning, friction_compensation: bool) -> float:
+ friction_interp = interp(
+ apply_center_deadzone(lateral_accel_error, lateral_accel_deadzone),
+ [-friction_threshold, friction_threshold],
+ [-torque_params.friction, torque_params.friction]
+ )
+ friction = float(friction_interp) if friction_compensation else 0.0
+ return friction
+
+
+def make_tester_present_msg(addr, bus, subaddr=None, suppress_response=False):
+ dat = [0x02, uds.SERVICE_TYPE.TESTER_PRESENT]
+ if subaddr is not None:
+ dat.insert(0, subaddr)
+ dat.append(0x80 if suppress_response else 0x0) # sub-function
+
+ dat.extend([0x0] * (8 - len(dat)))
+ return CanData(addr, bytes(dat), bus)
+
+
+def get_safety_config(safety_model: structs.CarParams.SafetyModel, safety_param: int = None) -> structs.CarParams.SafetyConfig:
+ ret = structs.CarParams.SafetyConfig()
+ ret.safetyModel = safety_model
+ if safety_param is not None:
+ ret.safetyParam = safety_param
+ return ret
+
+
+class CanBusBase:
+ offset: int
+
+ def __init__(self, CP, fingerprint: dict[int, dict[int, int]] | None) -> None:
+ if CP is None:
+ assert fingerprint is not None
+ num = max([k for k, v in fingerprint.items() if len(v)], default=0) // 4 + 1
+ else:
+ num = len(CP.safetyConfigs)
+ self.offset = 4 * (num - 1)
+
+
+class CanSignalRateCalculator:
+ """
+ Calculates the instantaneous rate of a CAN signal by using the counter
+ variable and the known frequency of the CAN message that contains it.
+ """
+ def __init__(self, frequency):
+ self.frequency = frequency
+ self.previous_counter = 0
+ self.previous_value = 0
+ self.rate = 0
+
+ def update(self, current_value, current_counter):
+ if current_counter != self.previous_counter:
+ self.rate = (current_value - self.previous_value) * self.frequency
+
+ self.previous_counter = current_counter
+ self.previous_value = current_value
+
+ return self.rate
+
+
+@dataclass(frozen=True, kw_only=True)
+class CarSpecs:
+ mass: float # kg, curb weight
+ wheelbase: float # meters
+ steerRatio: float
+ centerToFrontRatio: float = 0.5
+ minSteerSpeed: float = 0.0 # m/s
+ minEnableSpeed: float = -1.0 # m/s
+ tireStiffnessFactor: float = 1.0
+
+ def override(self, **kwargs):
+ return replace(self, **kwargs)
+
+
+class Freezable:
+ _frozen: bool = False
+
+ def freeze(self):
+ if not self._frozen:
+ self._frozen = True
+
+ def __setattr__(self, *args, **kwargs):
+ if self._frozen:
+ raise Exception("cannot modify frozen object")
+ super().__setattr__(*args, **kwargs)
+
+
+@dataclass(order=True)
+class PlatformConfig(Freezable):
+ car_docs: list[CarDocs]
+ specs: CarSpecs
+
+ dbc_dict: DbcDict
+
+ flags: int = 0
+
+ platform_str: str | None = None
+
+ def __hash__(self) -> int:
+ return hash(self.platform_str)
+
+ def override(self, **kwargs):
+ return replace(self, **kwargs)
+
+ def init(self):
+ pass
+
+ def __post_init__(self):
+ self.init()
+
+
+class PlatformsType(EnumType):
+ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **kwds):
+ for key in classdict._member_names.keys():
+ cfg: PlatformConfig = classdict[key]
+ cfg.platform_str = key
+ cfg.freeze()
+ return super().__new__(metacls, cls, bases, classdict, boundary=boundary, _simple=_simple, **kwds)
+
+
+class Platforms(str, ReprEnum, metaclass=PlatformsType):
+ config: PlatformConfig
+
+ def __new__(cls, platform_config: PlatformConfig):
+ member = str.__new__(cls, platform_config.platform_str)
+ member.config = platform_config
+ member._value_ = platform_config.platform_str
+ return member
+
+ def __repr__(self):
+ return f"<{self.__class__.__name__}.{self.name}>"
+
+ @classmethod
+ def create_dbc_map(cls) -> dict[str, DbcDict]:
+ return {p: p.config.dbc_dict for p in cls}
+
+ @classmethod
+ def with_flags(cls, flags: IntFlag) -> set['Platforms']:
+ return {p for p in cls if p.config.flags & flags}
diff --git a/opendbc/car/body/__init__.py b/opendbc/car/body/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/body/bodycan.py b/opendbc/car/body/bodycan.py
new file mode 100644
index 0000000000..580e5025ad
--- /dev/null
+++ b/opendbc/car/body/bodycan.py
@@ -0,0 +1,7 @@
+def create_control(packer, torque_l, torque_r):
+ values = {
+ "TORQUE_L": torque_l,
+ "TORQUE_R": torque_r,
+ }
+
+ return packer.make_can_msg("TORQUE_CMD", 0, values)
diff --git a/opendbc/car/body/carcontroller.py b/opendbc/car/body/carcontroller.py
new file mode 100644
index 0000000000..a311304ed0
--- /dev/null
+++ b/opendbc/car/body/carcontroller.py
@@ -0,0 +1,142 @@
+import copy
+import numpy as np
+from numbers import Number
+
+from opendbc.can.packer import CANPacker
+from opendbc.car import DT_CTRL
+from opendbc.car.common.numpy_fast import clip, interp
+from opendbc.car.body import bodycan
+from opendbc.car.body.values import SPEED_FROM_RPM
+from opendbc.car.interfaces import CarControllerBase
+
+
+class PIController:
+ def __init__(self, k_p, k_i, pos_limit=1e308, neg_limit=-1e308, rate=100):
+ self._k_p = k_p
+ self._k_i = k_i
+ if isinstance(self._k_p, Number):
+ self._k_p = [[0], [self._k_p]]
+ if isinstance(self._k_i, Number):
+ self._k_i = [[0], [self._k_i]]
+
+ self.pos_limit = pos_limit
+ self.neg_limit = neg_limit
+
+ self.i_unwind_rate = 0.3 / rate
+ self.i_rate = 1.0 / rate
+ self.speed = 0.0
+
+ self.reset()
+
+ @property
+ def k_p(self):
+ return interp(self.speed, self._k_p[0], self._k_p[1])
+
+ @property
+ def k_i(self):
+ return interp(self.speed, self._k_i[0], self._k_i[1])
+
+ @property
+ def error_integral(self):
+ return self.i/self.k_i
+
+ def reset(self):
+ self.p = 0.0
+ self.i = 0.0
+ self.control = 0
+
+ def update(self, error, speed=0.0, freeze_integrator=False):
+ self.speed = speed
+
+ self.p = float(error) * self.k_p
+
+ i = self.i + error * self.k_i * self.i_rate
+ control = self.p + i
+
+ # Update when changing i will move the control away from the limits
+ # or when i will move towards the sign of the error
+ if ((error >= 0 and (control <= self.pos_limit or i < 0.0)) or
+ (error <= 0 and (control >= self.neg_limit or i > 0.0))) and \
+ not freeze_integrator:
+ self.i = i
+
+ control = self.p + self.i
+
+ self.control = clip(control, self.neg_limit, self.pos_limit)
+ return self.control
+
+
+MAX_TORQUE = 500
+MAX_TORQUE_RATE = 50
+MAX_ANGLE_ERROR = np.radians(7)
+MAX_POS_INTEGRATOR = 0.2 # meters
+MAX_TURN_INTEGRATOR = 0.1 # meters
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.packer = CANPacker(dbc_name)
+
+ # PIDs
+ self.turn_pid = PIController(110, k_i=11.5, rate=1/DT_CTRL)
+ self.wheeled_speed_pid = PIController(110, k_i=11.5, rate=1/DT_CTRL)
+
+ self.torque_r_filtered = 0.
+ self.torque_l_filtered = 0.
+
+ @staticmethod
+ def deadband_filter(torque, deadband):
+ if torque > 0:
+ torque += deadband
+ else:
+ torque -= deadband
+ return torque
+
+ def update(self, CC, CS, now_nanos):
+
+ torque_l = 0
+ torque_r = 0
+
+ llk_valid = len(CC.orientationNED) > 1 and len(CC.angularVelocity) > 1
+ if CC.enabled and llk_valid:
+ # Read these from the joystick
+ # TODO: this isn't acceleration, okay?
+ speed_desired = CC.actuators.accel / 5.
+ speed_diff_desired = -CC.actuators.steer / 2.
+
+ speed_measured = SPEED_FROM_RPM * (CS.out.wheelSpeeds.fl + CS.out.wheelSpeeds.fr) / 2.
+ speed_error = speed_desired - speed_measured
+
+ torque = self.wheeled_speed_pid.update(speed_error, freeze_integrator=False)
+
+ speed_diff_measured = SPEED_FROM_RPM * (CS.out.wheelSpeeds.fl - CS.out.wheelSpeeds.fr)
+ turn_error = speed_diff_measured - speed_diff_desired
+ freeze_integrator = ((turn_error < 0 and self.turn_pid.error_integral <= -MAX_TURN_INTEGRATOR) or
+ (turn_error > 0 and self.turn_pid.error_integral >= MAX_TURN_INTEGRATOR))
+ torque_diff = self.turn_pid.update(turn_error, freeze_integrator=freeze_integrator)
+
+ # Combine 2 PIDs outputs
+ torque_r = torque + torque_diff
+ torque_l = torque - torque_diff
+
+ # Torque rate limits
+ self.torque_r_filtered = np.clip(self.deadband_filter(torque_r, 10),
+ self.torque_r_filtered - MAX_TORQUE_RATE,
+ self.torque_r_filtered + MAX_TORQUE_RATE)
+ self.torque_l_filtered = np.clip(self.deadband_filter(torque_l, 10),
+ self.torque_l_filtered - MAX_TORQUE_RATE,
+ self.torque_l_filtered + MAX_TORQUE_RATE)
+ torque_r = int(np.clip(self.torque_r_filtered, -MAX_TORQUE, MAX_TORQUE))
+ torque_l = int(np.clip(self.torque_l_filtered, -MAX_TORQUE, MAX_TORQUE))
+
+ can_sends = []
+ can_sends.append(bodycan.create_control(self.packer, torque_l, torque_r))
+
+ new_actuators = copy.copy(CC.actuators)
+ new_actuators.accel = torque_l
+ new_actuators.steer = torque_r
+ new_actuators.steerOutputCan = torque_r
+
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/body/carstate.py b/opendbc/car/body/carstate.py
new file mode 100644
index 0000000000..68cfb2072f
--- /dev/null
+++ b/opendbc/car/body/carstate.py
@@ -0,0 +1,39 @@
+from opendbc.can.parser import CANParser
+from opendbc.car import structs
+from opendbc.car.interfaces import CarStateBase
+from opendbc.car.body.values import DBC
+
+
+class CarState(CarStateBase):
+ def update(self, cp, *_) -> structs.CarState:
+ ret = structs.CarState()
+
+ ret.wheelSpeeds.fl = cp.vl['MOTORS_DATA']['SPEED_L']
+ ret.wheelSpeeds.fr = cp.vl['MOTORS_DATA']['SPEED_R']
+
+ ret.vEgoRaw = ((ret.wheelSpeeds.fl + ret.wheelSpeeds.fr) / 2.) * self.CP.wheelSpeedFactor
+
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.standstill = False
+
+ ret.steerFaultPermanent = any([cp.vl['VAR_VALUES']['MOTOR_ERR_L'], cp.vl['VAR_VALUES']['MOTOR_ERR_R'],
+ cp.vl['VAR_VALUES']['FAULT']])
+
+ ret.charging = cp.vl["BODY_DATA"]["CHARGER_CONNECTED"] == 1
+ ret.fuelGauge = cp.vl["BODY_DATA"]["BATT_PERCENTAGE"] / 100
+
+ # irrelevant for non-car
+ ret.gearShifter = structs.CarState.GearShifter.drive
+ ret.cruiseState.enabled = True
+ ret.cruiseState.available = True
+
+ return ret
+
+ @staticmethod
+ def get_can_parser(CP):
+ messages = [
+ ("MOTORS_DATA", 100),
+ ("VAR_VALUES", 10),
+ ("BODY_DATA", 1),
+ ]
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0)
diff --git a/opendbc/car/body/fingerprints.py b/opendbc/car/body/fingerprints.py
new file mode 100644
index 0000000000..3f5665029b
--- /dev/null
+++ b/opendbc/car/body/fingerprints.py
@@ -0,0 +1,28 @@
+# ruff: noqa: E501
+from opendbc.car.structs import CarParams
+from opendbc.car.body.values import CAR
+
+Ecu = CarParams.Ecu
+
+# debug ecu fw version is the git hash of the firmware
+
+
+FINGERPRINTS = {
+ CAR.COMMA_BODY: [{
+ 513: 8, 516: 8, 514: 3, 515: 4
+ }],
+}
+
+FW_VERSIONS = {
+ CAR.COMMA_BODY: {
+ (Ecu.engine, 0x720, None): [
+ b'0.0.01',
+ b'0.3.00a',
+ b'02/27/2022',
+ ],
+ (Ecu.debug, 0x721, None): [
+ b'166bd860',
+ b'dc780f85',
+ ],
+ },
+}
diff --git a/opendbc/car/body/interface.py b/opendbc/car/body/interface.py
new file mode 100644
index 0000000000..f720c4a7d6
--- /dev/null
+++ b/opendbc/car/body/interface.py
@@ -0,0 +1,25 @@
+import math
+from opendbc.car import get_safety_config, structs
+from opendbc.car.interfaces import CarInterfaceBase
+from opendbc.car.body.values import SPEED_FROM_RPM
+
+
+class CarInterface(CarInterfaceBase):
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.notCar = True
+ ret.carName = "body"
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.body)]
+
+ ret.minSteerSpeed = -math.inf
+ ret.maxLateralAccel = math.inf # TODO: set to a reasonable value
+ ret.steerLimitTimer = 1.0
+ ret.steerActuatorDelay = 0.
+
+ ret.wheelSpeedFactor = SPEED_FROM_RPM
+
+ ret.radarUnavailable = True
+ ret.openpilotLongitudinalControl = True
+ ret.steerControlType = structs.CarParams.SteerControlType.angle
+
+ return ret
diff --git a/opendbc/car/body/radar_interface.py b/opendbc/car/body/radar_interface.py
new file mode 100644
index 0000000000..6e552bf618
--- /dev/null
+++ b/opendbc/car/body/radar_interface.py
@@ -0,0 +1,4 @@
+from opendbc.car.interfaces import RadarInterfaceBase
+
+class RadarInterface(RadarInterfaceBase):
+ pass
diff --git a/opendbc/car/body/values.py b/opendbc/car/body/values.py
new file mode 100644
index 0000000000..3c221548c3
--- /dev/null
+++ b/opendbc/car/body/values.py
@@ -0,0 +1,40 @@
+from opendbc.car import CarSpecs, PlatformConfig, Platforms, dbc_dict
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarDocs
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
+
+Ecu = CarParams.Ecu
+
+SPEED_FROM_RPM = 0.008587
+
+
+class CarControllerParams:
+ ANGLE_DELTA_BP = [0., 5., 15.]
+ ANGLE_DELTA_V = [5., .8, .15] # windup limit
+ ANGLE_DELTA_VU = [5., 3.5, 0.4] # unwind limit
+ LKAS_MAX_TORQUE = 1 # A value of 1 is easy to overpower
+ STEER_THRESHOLD = 1.0
+
+ def __init__(self, CP):
+ pass
+
+
+class CAR(Platforms):
+ COMMA_BODY = PlatformConfig(
+ [CarDocs("comma body", package="All")],
+ CarSpecs(mass=9, wheelbase=0.406, steerRatio=0.5, centerToFrontRatio=0.44),
+ dbc_dict('comma_body', None),
+ )
+
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[
+ Request(
+ [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.UDS_VERSION_REQUEST],
+ [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.UDS_VERSION_RESPONSE],
+ bus=0,
+ ),
+ ],
+)
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/can_definitions.py b/opendbc/car/can_definitions.py
new file mode 100644
index 0000000000..2dba7f26a8
--- /dev/null
+++ b/opendbc/car/can_definitions.py
@@ -0,0 +1,15 @@
+from collections.abc import Callable
+from typing import NamedTuple, Protocol
+
+
+class CanData(NamedTuple):
+ address: int
+ dat: bytes
+ src: int
+
+
+CanSendCallable = Callable[[list[CanData]], None]
+
+
+class CanRecvCallable(Protocol):
+ def __call__(self, wait_for_one: bool = False) -> list[list[CanData]]: ...
diff --git a/opendbc/car/car_helpers.py b/opendbc/car/car_helpers.py
new file mode 100644
index 0000000000..f94c88ba8e
--- /dev/null
+++ b/opendbc/car/car_helpers.py
@@ -0,0 +1,178 @@
+import os
+import time
+
+from opendbc.car import carlog, gen_empty_fingerprint
+from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable
+from opendbc.car.structs import CarParams
+from opendbc.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars
+from opendbc.car.fw_versions import ObdCallback, get_fw_versions_ordered, get_present_ecus, match_fw_to_car
+from opendbc.car.interfaces import get_interface_attr
+from opendbc.car.mock.values import CAR as MOCK
+from opendbc.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
+
+FRAME_FINGERPRINT = 100 # 1s
+
+
+def load_interfaces(brand_names):
+ ret = {}
+ for brand_name in brand_names:
+ path = f'opendbc.car.{brand_name}'
+ CarInterface = __import__(path + '.interface', fromlist=['CarInterface']).CarInterface
+ CarState = __import__(path + '.carstate', fromlist=['CarState']).CarState
+ CarController = __import__(path + '.carcontroller', fromlist=['CarController']).CarController
+ for model_name in brand_names[brand_name]:
+ ret[model_name] = (CarInterface, CarController, CarState)
+ return ret
+
+
+def _get_interface_names() -> dict[str, list[str]]:
+ # returns a dict of brand name and its respective models
+ brand_names = {}
+ for brand_name, brand_models in get_interface_attr("CAR").items():
+ brand_names[brand_name] = [model.value for model in brand_models]
+
+ return brand_names
+
+
+# imports from directory opendbc/car//
+interface_names = _get_interface_names()
+interfaces = load_interfaces(interface_names)
+
+
+def can_fingerprint(can_recv: CanRecvCallable) -> tuple[str | None, dict[int, dict]]:
+ finger = gen_empty_fingerprint()
+ candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1
+ frame = 0
+ car_fingerprint = None
+ done = False
+
+ while not done:
+ # can_recv(wait_for_one=True) may return zero or multiple packets, so we increment frame for each one we receive
+ can_packets = can_recv(wait_for_one=True)
+ for can_packet in can_packets:
+ for can in can_packet:
+ # The fingerprint dict is generated for all buses, this way the car interface
+ # can use it to detect a (valid) multipanda setup and initialize accordingly
+ if can.src < 128:
+ if can.src not in finger:
+ finger[can.src] = {}
+ finger[can.src][can.address] = len(can.dat)
+
+ for b in candidate_cars:
+ # Ignore extended messages and VIN query response.
+ if can.src == b and can.address < 0x800 and can.address not in (0x7df, 0x7e0, 0x7e8):
+ candidate_cars[b] = eliminate_incompatible_cars(can, candidate_cars[b])
+
+ # if we only have one car choice and the time since we got our first
+ # message has elapsed, exit
+ for b in candidate_cars:
+ if len(candidate_cars[b]) == 1 and frame > FRAME_FINGERPRINT:
+ # fingerprint done
+ car_fingerprint = candidate_cars[b][0]
+
+ # bail if no cars left or we've been waiting for more than 2s
+ failed = (all(len(cc) == 0 for cc in candidate_cars.values()) and frame > FRAME_FINGERPRINT) or frame > 200
+ succeeded = car_fingerprint is not None
+ done = failed or succeeded
+
+ frame += 1
+
+ return car_fingerprint, finger
+
+
+# **** for use live only ****
+def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, num_pandas: int,
+ cached_params: CarParams | None) -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
+ fixed_fingerprint = os.environ.get('FINGERPRINT', "")
+ skip_fw_query = os.environ.get('SKIP_FW_QUERY', False)
+ disable_fw_cache = os.environ.get('DISABLE_FW_CACHE', False)
+ ecu_rx_addrs = set()
+
+ start_time = time.monotonic()
+ if not skip_fw_query:
+ if cached_params is not None and cached_params.carName != "mock" and len(cached_params.carFw) > 0 and \
+ cached_params.carVin is not VIN_UNKNOWN and not disable_fw_cache:
+ carlog.warning("Using cached CarParams")
+ vin_rx_addr, vin_rx_bus, vin = -1, -1, cached_params.carVin
+ car_fw = list(cached_params.carFw)
+ cached = True
+ else:
+ carlog.warning("Getting VIN & FW versions")
+ # enable OBD multiplexing for VIN query
+ # NOTE: this takes ~0.1s and is relied on to allow sendcan subscriber to connect in time
+ set_obd_multiplexing(True)
+ # VIN query only reliably works through OBDII
+ vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0, 1))
+ ecu_rx_addrs = get_present_ecus(can_recv, can_send, set_obd_multiplexing, num_pandas=num_pandas)
+ car_fw = get_fw_versions_ordered(can_recv, can_send, set_obd_multiplexing, vin, ecu_rx_addrs, num_pandas=num_pandas)
+ cached = False
+
+ exact_fw_match, fw_candidates = match_fw_to_car(car_fw, vin)
+ else:
+ vin_rx_addr, vin_rx_bus, vin = -1, -1, VIN_UNKNOWN
+ exact_fw_match, fw_candidates, car_fw = True, set(), []
+ cached = False
+
+ if not is_valid_vin(vin):
+ carlog.error({"event": "Malformed VIN", "vin": vin})
+ vin = VIN_UNKNOWN
+ carlog.warning("VIN %s", vin)
+
+ # disable OBD multiplexing for CAN fingerprinting and potential ECU knockouts
+ set_obd_multiplexing(False)
+
+ fw_query_time = time.monotonic() - start_time
+
+ # CAN fingerprint
+ # drain CAN socket so we get the latest messages
+ can_recv()
+ car_fingerprint, finger = can_fingerprint(can_recv)
+
+ exact_match = True
+ source = CarParams.FingerprintSource.can
+
+ # If FW query returns exactly 1 candidate, use it
+ if len(fw_candidates) == 1:
+ car_fingerprint = list(fw_candidates)[0]
+ source = CarParams.FingerprintSource.fw
+ exact_match = exact_fw_match
+
+ if fixed_fingerprint:
+ car_fingerprint = fixed_fingerprint
+ source = CarParams.FingerprintSource.fixed
+
+ carlog.error({"event": "fingerprinted", "car_fingerprint": str(car_fingerprint), "source": source, "fuzzy": not exact_match,
+ "cached": cached, "fw_count": len(car_fw), "ecu_responses": list(ecu_rx_addrs), "vin_rx_addr": vin_rx_addr,
+ "vin_rx_bus": vin_rx_bus, "fingerprints": repr(finger), "fw_query_time": fw_query_time})
+
+ return car_fingerprint, finger, vin, car_fw, source, exact_match
+
+
+def get_car_interface(CP: CarParams):
+ CarInterface, CarController, CarState = interfaces[CP.carFingerprint]
+ return CarInterface(CP, CarController, CarState)
+
+
+def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, experimental_long_allowed: bool,
+ num_pandas: int = 1, cached_params: CarParams | None = None):
+ candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, num_pandas, cached_params)
+
+ if candidate is None:
+ carlog.error({"event": "car doesn't match any fingerprints", "fingerprints": repr(fingerprints)})
+ candidate = "MOCK"
+
+ CarInterface, _, _ = interfaces[candidate]
+ CP: CarParams = CarInterface.get_params(candidate, fingerprints, car_fw, experimental_long_allowed, docs=False)
+ CP.carVin = vin
+ CP.carFw = car_fw
+ CP.fingerprintSource = source
+ CP.fuzzyFingerprint = not exact_match
+
+ return get_car_interface(CP)
+
+
+def get_demo_car_params():
+ platform = MOCK.MOCK
+ CarInterface, _, _ = interfaces[platform]
+ CP = CarInterface.get_non_essential_params(platform)
+ return CP
diff --git a/opendbc/car/chrysler/__init__.py b/opendbc/car/chrysler/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/chrysler/carcontroller.py b/opendbc/car/chrysler/carcontroller.py
new file mode 100644
index 0000000000..4bc35d3397
--- /dev/null
+++ b/opendbc/car/chrysler/carcontroller.py
@@ -0,0 +1,84 @@
+import copy
+from opendbc.can.packer import CANPacker
+from opendbc.car import DT_CTRL, apply_meas_steer_torque_limits
+from opendbc.car.chrysler import chryslercan
+from opendbc.car.chrysler.values import RAM_CARS, CarControllerParams, ChryslerFlags
+from opendbc.car.interfaces import CarControllerBase
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.apply_steer_last = 0
+
+ self.hud_count = 0
+ self.last_lkas_falling_edge = 0
+ self.lkas_control_bit_prev = False
+ self.last_button_frame = 0
+
+ self.packer = CANPacker(dbc_name)
+ self.params = CarControllerParams(CP)
+
+ def update(self, CC, CS, now_nanos):
+ can_sends = []
+
+ lkas_active = CC.latActive and self.lkas_control_bit_prev
+
+ # cruise buttons
+ if (self.frame - self.last_button_frame)*DT_CTRL > 0.05:
+ das_bus = 2 if self.CP.carFingerprint in RAM_CARS else 0
+
+ # ACC cancellation
+ if CC.cruiseControl.cancel:
+ self.last_button_frame = self.frame
+ can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, cancel=True))
+
+ # ACC resume from standstill
+ elif CC.cruiseControl.resume:
+ self.last_button_frame = self.frame
+ can_sends.append(chryslercan.create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, resume=True))
+
+ # HUD alerts
+ if self.frame % 25 == 0:
+ if CS.lkas_car_model != -1:
+ can_sends.append(chryslercan.create_lkas_hud(self.packer, self.CP, lkas_active, CC.hudControl.visualAlert,
+ self.hud_count, CS.lkas_car_model, CS.auto_high_beam))
+ self.hud_count += 1
+
+ # steering
+ if self.frame % self.params.STEER_STEP == 0:
+
+ # TODO: can we make this more sane? why is it different for all the cars?
+ lkas_control_bit = self.lkas_control_bit_prev
+ if CS.out.vEgo > self.CP.minSteerSpeed:
+ lkas_control_bit = True
+ elif self.CP.flags & ChryslerFlags.HIGHER_MIN_STEERING_SPEED:
+ if CS.out.vEgo < (self.CP.minSteerSpeed - 3.0):
+ lkas_control_bit = False
+ elif self.CP.carFingerprint in RAM_CARS:
+ if CS.out.vEgo < (self.CP.minSteerSpeed - 0.5):
+ lkas_control_bit = False
+
+ # EPS faults if LKAS re-enables too quickly
+ lkas_control_bit = lkas_control_bit and (self.frame - self.last_lkas_falling_edge > 200)
+
+ if not lkas_control_bit and self.lkas_control_bit_prev:
+ self.last_lkas_falling_edge = self.frame
+ self.lkas_control_bit_prev = lkas_control_bit
+
+ # steer torque
+ new_steer = int(round(CC.actuators.steer * self.params.STEER_MAX))
+ apply_steer = apply_meas_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorqueEps, self.params)
+ if not lkas_active or not lkas_control_bit:
+ apply_steer = 0
+ self.apply_steer_last = apply_steer
+
+ can_sends.append(chryslercan.create_lkas_command(self.packer, self.CP, int(apply_steer), lkas_control_bit))
+
+ self.frame += 1
+
+ new_actuators = copy.copy(CC.actuators)
+ new_actuators.steer = self.apply_steer_last / self.params.STEER_MAX
+ new_actuators.steerOutputCan = self.apply_steer_last
+
+ return new_actuators, can_sends
diff --git a/opendbc/car/chrysler/carstate.py b/opendbc/car/chrysler/carstate.py
new file mode 100644
index 0000000000..ab37008204
--- /dev/null
+++ b/opendbc/car/chrysler/carstate.py
@@ -0,0 +1,159 @@
+from opendbc.can.parser import CANParser
+from opendbc.can.can_define import CANDefine
+from opendbc.car import create_button_events, structs
+from opendbc.car.chrysler.values import DBC, STEER_THRESHOLD, RAM_CARS
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.interfaces import CarStateBase
+
+ButtonType = structs.CarState.ButtonEvent.Type
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ self.CP = CP
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+
+ self.auto_high_beam = 0
+ self.button_counter = 0
+ self.lkas_car_model = -1
+
+ if CP.carFingerprint in RAM_CARS:
+ self.shifter_values = can_define.dv["Transmission_Status"]["Gear_State"]
+ else:
+ self.shifter_values = can_define.dv["GEAR"]["PRNDL"]
+
+ self.distance_button = 0
+
+ def update(self, cp, cp_cam, *_) -> structs.CarState:
+
+ ret = structs.CarState()
+
+ prev_distance_button = self.distance_button
+ self.distance_button = cp.vl["CRUISE_BUTTONS"]["ACC_Distance_Dec"]
+
+ # lock info
+ ret.doorOpen = any([cp.vl["BCM_1"]["DOOR_OPEN_FL"],
+ cp.vl["BCM_1"]["DOOR_OPEN_FR"],
+ cp.vl["BCM_1"]["DOOR_OPEN_RL"],
+ cp.vl["BCM_1"]["DOOR_OPEN_RR"]])
+ ret.seatbeltUnlatched = cp.vl["ORC_1"]["SEATBELT_DRIVER_UNLATCHED"] == 1
+
+ # brake pedal
+ ret.brake = 0
+ ret.brakePressed = cp.vl["ESP_1"]['Brake_Pedal_State'] == 1 # Physical brake pedal switch
+
+ # gas pedal
+ ret.gas = cp.vl["ECM_5"]["Accelerator_Position"]
+ ret.gasPressed = ret.gas > 1e-5
+
+ # car speed
+ if self.CP.carFingerprint in RAM_CARS:
+ ret.vEgoRaw = cp.vl["ESP_8"]["Vehicle_Speed"] * CV.KPH_TO_MS
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["Transmission_Status"]["Gear_State"], None))
+ else:
+ ret.vEgoRaw = (cp.vl["SPEED_1"]["SPEED_LEFT"] + cp.vl["SPEED_1"]["SPEED_RIGHT"]) / 2.
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["GEAR"]["PRNDL"], None))
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.standstill = not ret.vEgoRaw > 0.001
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ cp.vl["ESP_6"]["WHEEL_SPEED_FL"],
+ cp.vl["ESP_6"]["WHEEL_SPEED_FR"],
+ cp.vl["ESP_6"]["WHEEL_SPEED_RL"],
+ cp.vl["ESP_6"]["WHEEL_SPEED_RR"],
+ unit=1,
+ )
+
+ # button presses
+ ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_stalk(200, cp.vl["STEERING_LEVERS"]["TURN_SIGNALS"] == 1,
+ cp.vl["STEERING_LEVERS"]["TURN_SIGNALS"] == 2)
+ ret.genericToggle = cp.vl["STEERING_LEVERS"]["HIGH_BEAM_PRESSED"] == 1
+
+ # steering wheel
+ ret.steeringAngleDeg = cp.vl["STEERING"]["STEERING_ANGLE"] + cp.vl["STEERING"]["STEERING_ANGLE_HP"]
+ ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"]
+ ret.steeringTorque = cp.vl["EPS_2"]["COLUMN_TORQUE"]
+ ret.steeringTorqueEps = cp.vl["EPS_2"]["EPS_TORQUE_MOTOR"]
+ ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD
+
+ # cruise state
+ cp_cruise = cp_cam if self.CP.carFingerprint in RAM_CARS else cp
+
+ ret.cruiseState.available = cp_cruise.vl["DAS_3"]["ACC_AVAILABLE"] == 1
+ ret.cruiseState.enabled = cp_cruise.vl["DAS_3"]["ACC_ACTIVE"] == 1
+ ret.cruiseState.speed = cp_cruise.vl["DAS_4"]["ACC_SET_SPEED_KPH"] * CV.KPH_TO_MS
+ ret.cruiseState.nonAdaptive = cp_cruise.vl["DAS_4"]["ACC_STATE"] in (1, 2) # 1 NormalCCOn and 2 NormalCCSet
+ ret.cruiseState.standstill = cp_cruise.vl["DAS_3"]["ACC_STANDSTILL"] == 1
+ ret.accFaulted = cp_cruise.vl["DAS_3"]["ACC_FAULTED"] != 0
+
+ if self.CP.carFingerprint in RAM_CARS:
+ # Auto High Beam isn't Located in this message on chrysler or jeep currently located in 729 message
+ self.auto_high_beam = cp_cam.vl["DAS_6"]['AUTO_HIGH_BEAM_ON']
+ ret.steerFaultTemporary = cp.vl["EPS_3"]["DASM_FAULT"] == 1
+ else:
+ ret.steerFaultTemporary = cp.vl["EPS_2"]["LKAS_TEMPORARY_FAULT"] == 1
+ ret.steerFaultPermanent = cp.vl["EPS_2"]["LKAS_STATE"] == 4
+
+ # blindspot sensors
+ if self.CP.enableBsm:
+ ret.leftBlindspot = cp.vl["BSM_1"]["LEFT_STATUS"] == 1
+ ret.rightBlindspot = cp.vl["BSM_1"]["RIGHT_STATUS"] == 1
+
+ self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"]
+ self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"]
+
+ ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
+
+ return ret
+
+ @staticmethod
+ def get_cruise_messages():
+ messages = [
+ ("DAS_3", 50),
+ ("DAS_4", 50),
+ ]
+ return messages
+
+ @staticmethod
+ def get_can_parser(CP):
+ messages = [
+ # sig_address, frequency
+ ("ESP_1", 50),
+ ("EPS_2", 100),
+ ("ESP_6", 50),
+ ("STEERING", 100),
+ ("ECM_5", 50),
+ ("CRUISE_BUTTONS", 50),
+ ("STEERING_LEVERS", 10),
+ ("ORC_1", 2),
+ ("BCM_1", 1),
+ ]
+
+ if CP.enableBsm:
+ messages.append(("BSM_1", 2))
+
+ if CP.carFingerprint in RAM_CARS:
+ messages += [
+ ("ESP_8", 50),
+ ("EPS_3", 50),
+ ("Transmission_Status", 50),
+ ]
+ else:
+ messages += [
+ ("GEAR", 50),
+ ("SPEED_1", 100),
+ ]
+ messages += CarState.get_cruise_messages()
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ messages = [
+ ("DAS_6", 4),
+ ]
+
+ if CP.carFingerprint in RAM_CARS:
+ messages += CarState.get_cruise_messages()
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 2)
diff --git a/opendbc/car/chrysler/chryslercan.py b/opendbc/car/chrysler/chryslercan.py
new file mode 100644
index 0000000000..8a0755ac5d
--- /dev/null
+++ b/opendbc/car/chrysler/chryslercan.py
@@ -0,0 +1,71 @@
+from opendbc.car import structs
+from opendbc.car.chrysler.values import RAM_CARS
+
+GearShifter = structs.CarState.GearShifter
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+
+def create_lkas_hud(packer, CP, lkas_active, hud_alert, hud_count, car_model, auto_high_beam):
+ # LKAS_HUD - Controls what lane-keeping icon is displayed
+
+ # == Color ==
+ # 0 hidden?
+ # 1 white
+ # 2 green
+ # 3 ldw
+
+ # == Lines ==
+ # 03 white Lines
+ # 04 grey lines
+ # 09 left lane close
+ # 0A right lane close
+ # 0B left Lane very close
+ # 0C right Lane very close
+ # 0D left cross cross
+ # 0E right lane cross
+
+ # == Alerts ==
+ # 7 Normal
+ # 6 lane departure place hands on wheel
+
+ color = 2 if lkas_active else 1
+ lines = 3 if lkas_active else 0
+ alerts = 7 if lkas_active else 0
+
+ if hud_count < (1 * 4): # first 3 seconds, 4Hz
+ alerts = 1
+
+ if hud_alert in (VisualAlert.ldw, VisualAlert.steerRequired):
+ color = 4
+ lines = 0
+ alerts = 6
+
+ values = {
+ "LKAS_ICON_COLOR": color,
+ "CAR_MODEL": car_model,
+ "LKAS_LANE_LINES": lines,
+ "LKAS_ALERTS": alerts,
+ }
+
+ if CP.carFingerprint in RAM_CARS:
+ values['AUTO_HIGH_BEAM_ON'] = auto_high_beam
+
+ return packer.make_can_msg("DAS_6", 0, values)
+
+
+def create_lkas_command(packer, CP, apply_steer, lkas_control_bit):
+ # LKAS_COMMAND Lane-keeping signal to turn the wheel
+ enabled_val = 2 if CP.carFingerprint in RAM_CARS else 1
+ values = {
+ "STEERING_TORQUE": apply_steer,
+ "LKAS_CONTROL_BIT": enabled_val if lkas_control_bit else 0,
+ }
+ return packer.make_can_msg("LKAS_COMMAND", 0, values)
+
+
+def create_cruise_buttons(packer, frame, bus, cancel=False, resume=False):
+ values = {
+ "ACC_Cancel": cancel,
+ "ACC_Resume": resume,
+ "COUNTER": frame % 0x10,
+ }
+ return packer.make_can_msg("CRUISE_BUTTONS", bus, values)
diff --git a/opendbc/car/chrysler/fingerprints.py b/opendbc/car/chrysler/fingerprints.py
new file mode 100644
index 0000000000..072eafb1d0
--- /dev/null
+++ b/opendbc/car/chrysler/fingerprints.py
@@ -0,0 +1,716 @@
+from opendbc.car.structs import CarParams
+from opendbc.car.chrysler.values import CAR
+
+Ecu = CarParams.Ecu
+
+FW_VERSIONS = {
+ CAR.CHRYSLER_PACIFICA_2018: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68227902AF',
+ b'68227902AG',
+ b'68227902AH',
+ b'68227905AG',
+ b'68360252AC',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68211617AF',
+ b'68211617AG',
+ b'68358974AC',
+ b'68405937AA',
+ ],
+ (Ecu.abs, 0x747, None): [
+ b'68222747AG',
+ b'68330876AA',
+ b'68330876AB',
+ b'68352227AA',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'04672758AA',
+ b'04672758AB',
+ b'68226356AF',
+ b'68226356AH',
+ b'68226356AI',
+ ],
+ (Ecu.eps, 0x75a, None): [
+ b'68288891AE',
+ b'68378884AA',
+ b'68525338AA',
+ b'68525338AB',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'68267018AO ',
+ b'68267020AJ ',
+ b'68303534AG ',
+ b'68303534AJ ',
+ b'68340762AD ',
+ b'68340764AD ',
+ b'68352652AE ',
+ b'68352654AE ',
+ b'68366851AH ',
+ b'68366853AE ',
+ b'68366853AG ',
+ b'68372861AF ',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'68277370AJ',
+ b'68277370AM',
+ b'68277372AD',
+ b'68277372AE',
+ b'68277372AN',
+ b'68277374AA',
+ b'68277374AB',
+ b'68277374AD',
+ b'68277374AN',
+ b'68367471AC',
+ b'68367471AD',
+ b'68380571AB',
+ ],
+ },
+ CAR.CHRYSLER_PACIFICA_2020: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68405327AC',
+ b'68436233AB',
+ b'68436233AC',
+ b'68436234AB',
+ b'68436250AE',
+ b'68529067AA',
+ b'68594993AB',
+ b'68594994AB',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68405565AB',
+ b'68405565AC',
+ b'68444299AC',
+ b'68480707AC',
+ b'68480708AC',
+ b'68526663AB',
+ ],
+ (Ecu.abs, 0x747, None): [
+ b'68397394AA',
+ b'68433480AB',
+ b'68453575AF',
+ b'68577676AA',
+ b'68593395AA',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'04672758AA',
+ b'04672758AB',
+ b'68417813AF',
+ b'68540436AA',
+ b'68540436AC',
+ b'68540436AD',
+ b'68598670AB',
+ b'68598670AC',
+ ],
+ (Ecu.eps, 0x75a, None): [
+ b'68416742AA',
+ b'68460393AA',
+ b'68460393AB',
+ b'68494461AB',
+ b'68494461AC',
+ b'68524936AA',
+ b'68524936AB',
+ b'68525338AB',
+ b'68594337AB',
+ b'68594340AB',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'68413871AD ',
+ b'68413871AE ',
+ b'68413871AH ',
+ b'68413871AI ',
+ b'68413873AH ',
+ b'68413873AI ',
+ b'68443120AE ',
+ b'68443123AC ',
+ b'68443125AC ',
+ b'68496647AI ',
+ b'68496647AJ ',
+ b'68496650AH ',
+ b'68496650AI ',
+ b'68496652AH ',
+ b'68526752AD ',
+ b'68526752AE ',
+ b'68526754AE ',
+ b'68536264AE ',
+ b'68700304AB ',
+ b'68700306AB ',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'68414271AC',
+ b'68414271AD',
+ b'68414275AC',
+ b'68414275AD',
+ b'68443154AB',
+ b'68443155AC',
+ b'68443158AB',
+ b'68501050AD',
+ b'68501051AD',
+ b'68501055AD',
+ b'68527221AB',
+ b'68527223AB',
+ b'68586231AD',
+ b'68586233AD',
+ ],
+ },
+ CAR.CHRYSLER_PACIFICA_2018_HYBRID: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68239262AH',
+ b'68239262AI',
+ b'68239262AJ',
+ b'68239263AH',
+ b'68239263AJ',
+ b'68358439AE',
+ b'68358439AG',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68238840AH',
+ b'68358990AC',
+ b'68405939AA',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'04672758AA',
+ b'68226356AI',
+ ],
+ (Ecu.eps, 0x75a, None): [
+ b'68288309AC',
+ b'68288309AD',
+ b'68525339AA',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'68277480AV ',
+ b'68277480AX ',
+ b'68277480AZ ',
+ b'68366580AI ',
+ b'68366580AK ',
+ b'68366580AM ',
+ ],
+ (Ecu.hybrid, 0x7e2, None): [
+ b'05190175BF',
+ b'05190175BH',
+ b'05190226AI',
+ b'05190226AK',
+ b'05190226AM',
+ ],
+ },
+ CAR.CHRYSLER_PACIFICA_2019_HYBRID: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68405292AC',
+ b'68434956AC',
+ b'68434956AD',
+ b'68434960AE',
+ b'68434960AF',
+ b'68529064AB',
+ b'68594990AB',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68405567AB',
+ b'68405567AC',
+ b'68453076AD',
+ b'68480710AC',
+ b'68526665AB',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'04672758AB',
+ b'68417813AF',
+ b'68540436AA',
+ b'68540436AB',
+ b'68540436AC',
+ b'68540436AD',
+ b'68598670AB',
+ b'68598670AC',
+ b'68645752AA',
+ ],
+ (Ecu.eps, 0x75a, None): [
+ b'68416741AA',
+ b'68460392AA',
+ b'68525339AA',
+ b'68525339AB',
+ b'68594341AB',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'68416680AE ',
+ b'68416680AF ',
+ b'68416680AG ',
+ b'68444228AC ',
+ b'68444228AD ',
+ b'68444228AE ',
+ b'68444228AF ',
+ b'68499122AD ',
+ b'68499122AE ',
+ b'68499122AF ',
+ b'68526772AD ',
+ b'68526772AH ',
+ b'68599493AC ',
+ b'68657433AA ',
+ ],
+ (Ecu.hybrid, 0x7e2, None): [
+ b'05185116AF',
+ b'05185116AJ',
+ b'05185116AK',
+ b'05190240AP',
+ b'05190240AQ',
+ b'05190240AR',
+ b'05190265AG',
+ b'05190265AH',
+ b'05190289AE',
+ b'68540977AH',
+ b'68540977AK',
+ b'68597647AE',
+ b'68632416AB',
+ ],
+ },
+ CAR.JEEP_GRAND_CHEROKEE: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68243549AG',
+ b'68302211AC',
+ b'68302212AD',
+ b'68302223AC',
+ b'68302246AC',
+ b'68331511AC',
+ b'68331574AC',
+ b'68331687AC',
+ b'68331690AC',
+ b'68340272AD',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68309533AA',
+ b'68316742AB',
+ b'68355363AB',
+ ],
+ (Ecu.abs, 0x747, None): [
+ b'68252642AG',
+ b'68306178AD',
+ b'68336275AB',
+ b'68336276AB',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'04672627AB',
+ b'68251506AF',
+ b'68332015AB',
+ ],
+ (Ecu.eps, 0x75a, None): [
+ b'68276201AG',
+ b'68321644AB',
+ b'68321644AC',
+ b'68321646AC',
+ b'68321648AC',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'05035920AE ',
+ b'68252272AG ',
+ b'68284455AI ',
+ b'68284456AI ',
+ b'68284477AF ',
+ b'68325564AH ',
+ b'68325564AI ',
+ b'68325565AH ',
+ b'68325565AI ',
+ b'68325618AD ',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'05035517AH',
+ b'68253222AF',
+ b'68311218AC',
+ b'68311223AF',
+ b'68311223AG',
+ b'68361911AE',
+ b'68361911AF',
+ b'68361911AH',
+ b'68361916AD',
+ ],
+ },
+ CAR.JEEP_GRAND_CHEROKEE_2019: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68402703AB',
+ b'68402704AB',
+ b'68402708AB',
+ b'68402971AD',
+ b'68454144AD',
+ b'68454145AB',
+ b'68454152AB',
+ b'68454156AB',
+ b'68516650AB',
+ b'68516651AB',
+ b'68516669AB',
+ b'68516671AB',
+ b'68516683AB',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68355363AB',
+ b'68355364AB',
+ ],
+ (Ecu.abs, 0x747, None): [
+ b'68408639AC',
+ b'68408639AD',
+ b'68499978AB',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'04672788AA',
+ b'68456722AC',
+ ],
+ (Ecu.eps, 0x75a, None): [
+ b'68417279AA',
+ b'68417280AA',
+ b'68417281AA',
+ b'68453431AA',
+ b'68453433AA',
+ b'68453435AA',
+ b'68499171AA',
+ b'68499171AB',
+ b'68501183AA',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'05035674AB ',
+ b'68412635AG ',
+ b'68412660AD ',
+ b'68422860AB',
+ b'68449435AE ',
+ b'68496223AA ',
+ b'68504959AD ',
+ b'68504959AE ',
+ b'68504960AD ',
+ b'68504993AC ',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'05035707AA',
+ b'68419672AC',
+ b'68419678AB',
+ b'68423905AB',
+ b'68449258AC',
+ b'68495807AA',
+ b'68495807AB',
+ b'68503641AC',
+ b'68503664AC',
+ ],
+ },
+ CAR.RAM_1500_5TH_GEN: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68294051AG',
+ b'68294051AI',
+ b'68294052AG',
+ b'68294052AH',
+ b'68294059AI',
+ b'68294063AG',
+ b'68294063AH',
+ b'68294063AI',
+ b'68434846AC',
+ b'68434847AC',
+ b'68434849AC',
+ b'68434856AC',
+ b'68434858AC',
+ b'68434859AC',
+ b'68434860AC',
+ b'68453471AD',
+ b'68453483AC',
+ b'68453483AD',
+ b'68453487AD',
+ b'68453491AC',
+ b'68453491AD',
+ b'68453499AD',
+ b'68453503AC',
+ b'68453503AD',
+ b'68453505AC',
+ b'68453505AD',
+ b'68453511AC',
+ b'68453513AC',
+ b'68453513AD',
+ b'68453514AD',
+ b'68505633AB',
+ b'68510277AG',
+ b'68510277AH',
+ b'68510280AG',
+ b'68510282AG',
+ b'68510282AH',
+ b'68510283AG',
+ b'68527346AE',
+ b'68527361AD',
+ b'68527375AD',
+ b'68527381AD',
+ b'68527381AE',
+ b'68527382AE',
+ b'68527383AD',
+ b'68527383AE',
+ b'68527387AE',
+ b'68527397AD',
+ b'68527403AC',
+ b'68527403AD',
+ b'68546047AF',
+ b'68631938AA',
+ b'68631939AA',
+ b'68631940AA',
+ b'68631940AB',
+ b'68631942AA',
+ b'68631943AB',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68428609AB',
+ b'68441329AB',
+ b'68473844AB',
+ b'68490898AA',
+ b'68500728AA',
+ b'68615033AA',
+ b'68615034AA',
+ ],
+ (Ecu.abs, 0x747, None): [
+ b'68292406AG',
+ b'68292406AH',
+ b'68432418AB',
+ b'68432418AC',
+ b'68432418AD',
+ b'68436004AD',
+ b'68436004AE',
+ b'68438454AC',
+ b'68438454AD',
+ b'68438456AE',
+ b'68438456AF',
+ b'68535469AB',
+ b'68535470AC',
+ b'68548900AB',
+ b'68586307AB',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'04672892AB',
+ b'04672932AB',
+ b'04672932AC',
+ b'22DTRHD_AA',
+ b'68320950AH',
+ b'68320950AI',
+ b'68320950AJ',
+ b'68320950AL',
+ b'68320950AM',
+ b'68454268AB',
+ b'68475160AE',
+ b'68475160AF',
+ b'68475160AG',
+ ],
+ (Ecu.eps, 0x75a, None): [
+ b'21590101AA',
+ b'21590101AB',
+ b'68273275AF',
+ b'68273275AG',
+ b'68273275AH',
+ b'68312176AE',
+ b'68312176AF',
+ b'68312176AG',
+ b'68440789AC',
+ b'68466110AA',
+ b'68466110AB',
+ b'68466113AA',
+ b'68469901AA',
+ b'68469907AA',
+ b'68522583AA',
+ b'68522583AB',
+ b'68522584AA',
+ b'68522585AB',
+ b'68552788AA',
+ b'68552789AA',
+ b'68552790AA',
+ b'68552791AB',
+ b'68552794AA',
+ b'68552794AD',
+ b'68585106AB',
+ b'68585107AB',
+ b'68585108AB',
+ b'68585109AB',
+ b'68585112AB',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'05035699AG ',
+ b'05035841AC ',
+ b'05035841AD ',
+ b'05036026AB ',
+ b'05036065AE ',
+ b'05036066AE ',
+ b'05036067AE ',
+ b'05036193AA ',
+ b'05149368AA ',
+ b'05149374AA ',
+ b'05149591AD ',
+ b'05149591AE ',
+ b'05149592AE ',
+ b'05149599AE ',
+ b'05149600AD ',
+ b'05149600AE ',
+ b'05149605AE ',
+ b'05149846AA ',
+ b'05149848AA ',
+ b'05149848AC ',
+ b'05190341AD',
+ b'68378695AJ ',
+ b'68378696AJ ',
+ b'68378696AK ',
+ b'68378701AI ',
+ b'68378702AI ',
+ b'68378710AL ',
+ b'68378742AI ',
+ b'68378742AK ',
+ b'68378743AM ',
+ b'68378748AL ',
+ b'68378758AM ',
+ b'68448163AJ',
+ b'68448163AK',
+ b'68448163AL',
+ b'68448165AG',
+ b'68448165AK',
+ b'68455111AC ',
+ b'68455119AC ',
+ b'68455145AC ',
+ b'68455145AE ',
+ b'68455146AC ',
+ b'68460927AA ',
+ b'68467915AC ',
+ b'68467916AC ',
+ b'68467936AC ',
+ b'68500630AD',
+ b'68500630AE',
+ b'68500631AE',
+ b'68502719AC ',
+ b'68502722AC ',
+ b'68502733AC ',
+ b'68502734AF ',
+ b'68502740AF ',
+ b'68502741AF ',
+ b'68502742AC ',
+ b'68502742AF ',
+ b'68539650AD',
+ b'68539650AF',
+ b'68539651AD',
+ b'68586101AA ',
+ b'68586102AA ',
+ b'68586105AB ',
+ b'68629919AC ',
+ b'68629922AC ',
+ b'68629925AC ',
+ b'68629926AC ',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'05035706AD',
+ b'05035842AB',
+ b'05036069AA',
+ b'05036181AA',
+ b'05149536AC',
+ b'05149537AC',
+ b'05149543AC',
+ b'68360078AL',
+ b'68360080AL',
+ b'68360080AM',
+ b'68360081AM',
+ b'68360085AJ',
+ b'68360085AL',
+ b'68360086AH',
+ b'68360086AK',
+ b'68360086AN',
+ b'68384328AD',
+ b'68384332AD',
+ b'68445531AC',
+ b'68445533AB',
+ b'68445536AB',
+ b'68445537AB',
+ b'68466081AB',
+ b'68466087AB',
+ b'68484466AC',
+ b'68484467AC',
+ b'68484471AC',
+ b'68502994AD',
+ b'68502996AD',
+ b'68520867AE',
+ b'68520867AF',
+ b'68520870AC',
+ b'68520871AC',
+ b'68528325AE',
+ b'68540431AB',
+ b'68540433AB',
+ b'68551676AA',
+ b'68629935AB',
+ b'68629936AC',
+ ],
+ },
+ CAR.RAM_HD_5TH_GEN: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68361606AH',
+ b'68437735AC',
+ b'68492693AD',
+ b'68525485AB',
+ b'68525487AB',
+ b'68525498AB',
+ b'68528791AF',
+ b'68628474AB',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68399794AC',
+ b'68428503AA',
+ b'68428505AA',
+ b'68428507AA',
+ ],
+ (Ecu.abs, 0x747, None): [
+ b'68334977AH',
+ b'68455481AC',
+ b'68504022AA',
+ b'68504022AB',
+ b'68504022AC',
+ b'68530686AB',
+ b'68530686AC',
+ b'68544596AC',
+ b'68641704AA',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'04672895AB',
+ b'04672934AB',
+ b'56029827AG',
+ b'56029827AH',
+ b'68462657AE',
+ b'68484694AD',
+ b'68484694AE',
+ b'68615489AB',
+ ],
+ (Ecu.eps, 0x761, None): [
+ b'68421036AC',
+ b'68507906AB',
+ b'68534023AC',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'52370131AF',
+ b'52370231AF',
+ b'52370231AG',
+ b'52370491AA',
+ b'52370931CT',
+ b'52401032AE',
+ b'52421132AF',
+ b'52421332AF',
+ b'68527616AD ',
+ b'M2370131MB',
+ b'M2421132MB',
+ ],
+ },
+ CAR.DODGE_DURANGO: {
+ (Ecu.combinationMeter, 0x742, None): [
+ b'68454261AD',
+ b'68471535AE',
+ ],
+ (Ecu.srs, 0x744, None): [
+ b'68355362AB',
+ b'68492238AD',
+ ],
+ (Ecu.abs, 0x747, None): [
+ b'68408639AD',
+ b'68499978AB',
+ ],
+ (Ecu.fwdRadar, 0x753, None): [
+ b'68440581AE',
+ b'68456722AC',
+ ],
+ (Ecu.eps, 0x75a, None): [
+ b'68453435AA',
+ b'68498477AA',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'05035786AE ',
+ b'68449476AE ',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'05035826AC',
+ b'68449265AC',
+ ],
+ },
+}
diff --git a/opendbc/car/chrysler/interface.py b/opendbc/car/chrysler/interface.py
new file mode 100755
index 0000000000..a5c7f5bd2c
--- /dev/null
+++ b/opendbc/car/chrysler/interface.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+from panda import Panda
+from opendbc.car import get_safety_config, structs
+from opendbc.car.chrysler.values import CAR, RAM_HD, RAM_DT, RAM_CARS, ChryslerFlags
+from opendbc.car.interfaces import CarInterfaceBase
+
+
+class CarInterface(CarInterfaceBase):
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "chrysler"
+ ret.dashcamOnly = candidate in RAM_HD
+
+ # radar parsing needs some work, see https://github.com/commaai/openpilot/issues/26842
+ ret.radarUnavailable = True # DBC[candidate]['radar'] is None
+ ret.steerActuatorDelay = 0.1
+ ret.steerLimitTimer = 0.4
+
+ # safety config
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.chrysler)]
+ if candidate in RAM_HD:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_CHRYSLER_RAM_HD
+ elif candidate in RAM_DT:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_CHRYSLER_RAM_DT
+
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+ if candidate not in RAM_CARS:
+ # Newer FW versions standard on the following platforms, or flashed by a dealer onto older platforms have a higher minimum steering speed.
+ new_eps_platform = candidate in (CAR.CHRYSLER_PACIFICA_2019_HYBRID, CAR.CHRYSLER_PACIFICA_2020, CAR.JEEP_GRAND_CHEROKEE_2019, CAR.DODGE_DURANGO)
+ new_eps_firmware = any(fw.ecu == 'eps' and fw.fwVersion[:4] >= b"6841" for fw in car_fw)
+ if new_eps_platform or new_eps_firmware:
+ ret.flags |= ChryslerFlags.HIGHER_MIN_STEERING_SPEED.value
+
+ # Chrysler
+ if candidate in (CAR.CHRYSLER_PACIFICA_2018, CAR.CHRYSLER_PACIFICA_2018_HYBRID, CAR.CHRYSLER_PACIFICA_2019_HYBRID,
+ CAR.CHRYSLER_PACIFICA_2020, CAR.DODGE_DURANGO):
+ ret.lateralTuning.init('pid')
+ ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]]
+ ret.lateralTuning.pid.kf = 0.00006
+
+ # Jeep
+ elif candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
+ ret.steerActuatorDelay = 0.2
+
+ ret.lateralTuning.init('pid')
+ ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]]
+ ret.lateralTuning.pid.kf = 0.00006
+
+ # Ram
+ elif candidate == CAR.RAM_1500_5TH_GEN:
+ ret.steerActuatorDelay = 0.2
+ ret.wheelbase = 3.88
+ # Older EPS FW allow steer to zero
+ if any(fw.ecu == 'eps' and b"68" < fw.fwVersion[:4] <= b"6831" for fw in car_fw):
+ ret.minSteerSpeed = 0.
+
+ elif candidate == CAR.RAM_HD_5TH_GEN:
+ ret.steerActuatorDelay = 0.2
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, 1.0, False)
+
+ else:
+ raise ValueError(f"Unsupported car: {candidate}")
+
+ if ret.flags & ChryslerFlags.HIGHER_MIN_STEERING_SPEED:
+ # TODO: allow these cars to steer down to 13 m/s if already engaged.
+ # TODO: Durango 2020 may be able to steer to zero once above 38 kph
+ ret.minSteerSpeed = 17.5 # m/s 17 on the way up, 13 on the way down once engaged.
+
+ ret.centerToFront = ret.wheelbase * 0.44
+ ret.enableBsm = 720 in fingerprint[0]
+
+ return ret
diff --git a/opendbc/car/chrysler/radar_interface.py b/opendbc/car/chrysler/radar_interface.py
new file mode 100755
index 0000000000..3528373f8d
--- /dev/null
+++ b/opendbc/car/chrysler/radar_interface.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+from opendbc.can.parser import CANParser
+from opendbc.car import structs
+from opendbc.car.interfaces import RadarInterfaceBase
+from opendbc.car.chrysler.values import DBC
+
+RADAR_MSGS_C = list(range(0x2c2, 0x2d4+2, 2)) # c_ messages 706,...,724
+RADAR_MSGS_D = list(range(0x2a2, 0x2b4+2, 2)) # d_ messages
+LAST_MSG = max(RADAR_MSGS_C + RADAR_MSGS_D)
+NUMBER_MSGS = len(RADAR_MSGS_C) + len(RADAR_MSGS_D)
+
+def _create_radar_can_parser(car_fingerprint):
+ dbc = DBC[car_fingerprint]['radar']
+ if dbc is None:
+ return None
+
+ msg_n = len(RADAR_MSGS_C)
+ # list of [(signal name, message name or number), (...)]
+ # [('RADAR_STATE', 1024),
+ # ('LONG_DIST', 1072),
+ # ('LONG_DIST', 1073),
+ # ('LONG_DIST', 1074),
+ # ('LONG_DIST', 1075),
+
+ messages = list(zip(RADAR_MSGS_C +
+ RADAR_MSGS_D,
+ [20] * msg_n + # 20Hz (0.05s)
+ [20] * msg_n, strict=True)) # 20Hz (0.05s)
+
+ return CANParser(DBC[car_fingerprint]['radar'], messages, 1)
+
+def _address_to_track(address):
+ if address in RADAR_MSGS_C:
+ return (address - RADAR_MSGS_C[0]) // 2
+ if address in RADAR_MSGS_D:
+ return (address - RADAR_MSGS_D[0]) // 2
+ raise ValueError("radar received unexpected address %d" % address)
+
+class RadarInterface(RadarInterfaceBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ self.rcp = _create_radar_can_parser(CP.carFingerprint)
+ self.updated_messages = set()
+ self.trigger_msg = LAST_MSG
+
+ def update(self, can_strings):
+ if self.rcp is None or self.CP.radarUnavailable:
+ return super().update(None)
+
+ vls = self.rcp.update_strings(can_strings)
+ self.updated_messages.update(vls)
+
+ if self.trigger_msg not in self.updated_messages:
+ return None
+
+ ret = structs.RadarData()
+ errors = []
+ if not self.rcp.can_valid:
+ errors.append("canError")
+ ret.errors = errors
+
+ for ii in self.updated_messages: # ii should be the message ID as a number
+ cpt = self.rcp.vl[ii]
+ trackId = _address_to_track(ii)
+
+ if trackId not in self.pts:
+ self.pts[trackId] = structs.RadarData.RadarPoint()
+ self.pts[trackId].trackId = trackId
+ self.pts[trackId].aRel = float('nan')
+ self.pts[trackId].yvRel = float('nan')
+ self.pts[trackId].measured = True
+
+ if 'LONG_DIST' in cpt: # c_* message
+ self.pts[trackId].dRel = cpt['LONG_DIST'] # from front of car
+ # our lat_dist is positive to the right in car's frame.
+ # TODO what does yRel want?
+ self.pts[trackId].yRel = cpt['LAT_DIST'] # in car frame's y axis, left is positive
+ else: # d_* message
+ self.pts[trackId].vRel = cpt['REL_SPEED']
+
+ # We want a list, not a dictionary. Filter out LONG_DIST==0 because that means it's not valid.
+ ret.points = [x for x in self.pts.values() if x.dRel != 0]
+
+ self.updated_messages.clear()
+ return ret
diff --git a/opendbc/car/chrysler/values.py b/opendbc/car/chrysler/values.py
new file mode 100644
index 0000000000..472b707f9d
--- /dev/null
+++ b/opendbc/car/chrysler/values.py
@@ -0,0 +1,152 @@
+from enum import IntFlag
+from dataclasses import dataclass, field
+
+from panda import uds
+from opendbc.car import CarSpecs, DbcDict, PlatformConfig, Platforms, dbc_dict
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarHarness, CarDocs, CarParts
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, p16
+
+Ecu = CarParams.Ecu
+
+
+class ChryslerFlags(IntFlag):
+ # Detected flags
+ HIGHER_MIN_STEERING_SPEED = 1
+
+@dataclass
+class ChryslerCarDocs(CarDocs):
+ package: str = "Adaptive Cruise Control (ACC)"
+ car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.fca]))
+
+
+@dataclass
+class ChryslerPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'))
+
+
+@dataclass(frozen=True)
+class ChryslerCarSpecs(CarSpecs):
+ minSteerSpeed: float = 3.8 # m/s
+
+
+class CAR(Platforms):
+ # Chrysler
+ CHRYSLER_PACIFICA_2018_HYBRID = ChryslerPlatformConfig(
+ [ChryslerCarDocs("Chrysler Pacifica Hybrid 2017-18")],
+ ChryslerCarSpecs(mass=2242., wheelbase=3.089, steerRatio=16.2),
+ )
+ CHRYSLER_PACIFICA_2019_HYBRID = ChryslerPlatformConfig(
+ [ChryslerCarDocs("Chrysler Pacifica Hybrid 2019-24")],
+ CHRYSLER_PACIFICA_2018_HYBRID.specs,
+ )
+ CHRYSLER_PACIFICA_2018 = ChryslerPlatformConfig(
+ [ChryslerCarDocs("Chrysler Pacifica 2017-18")],
+ CHRYSLER_PACIFICA_2018_HYBRID.specs,
+ )
+ CHRYSLER_PACIFICA_2020 = ChryslerPlatformConfig(
+ [
+ ChryslerCarDocs("Chrysler Pacifica 2019-20"),
+ ChryslerCarDocs("Chrysler Pacifica 2021-23", package="All"),
+ ],
+ CHRYSLER_PACIFICA_2018_HYBRID.specs,
+ )
+
+ # Dodge
+ DODGE_DURANGO = ChryslerPlatformConfig(
+ [ChryslerCarDocs("Dodge Durango 2020-21")],
+ CHRYSLER_PACIFICA_2018_HYBRID.specs,
+ )
+
+ # Jeep
+ JEEP_GRAND_CHEROKEE = ChryslerPlatformConfig( # includes 2017 Trailhawk
+ [ChryslerCarDocs("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk")],
+ ChryslerCarSpecs(mass=1778., wheelbase=2.71, steerRatio=16.7),
+ )
+
+ JEEP_GRAND_CHEROKEE_2019 = ChryslerPlatformConfig( # includes 2020 Trailhawk
+ [ChryslerCarDocs("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4")],
+ JEEP_GRAND_CHEROKEE.specs,
+ )
+
+ # Ram
+ RAM_1500_5TH_GEN = ChryslerPlatformConfig(
+ [ChryslerCarDocs("Ram 1500 2019-24", car_parts=CarParts.common([CarHarness.ram]))],
+ ChryslerCarSpecs(mass=2493., wheelbase=3.88, steerRatio=16.3, minSteerSpeed=14.5),
+ dbc_dict('chrysler_ram_dt_generated', None),
+ )
+ RAM_HD_5TH_GEN = ChryslerPlatformConfig(
+ [
+ ChryslerCarDocs("Ram 2500 2020-24", car_parts=CarParts.common([CarHarness.ram])),
+ ChryslerCarDocs("Ram 3500 2019-22", car_parts=CarParts.common([CarHarness.ram])),
+ ],
+ ChryslerCarSpecs(mass=3405., wheelbase=3.785, steerRatio=15.61, minSteerSpeed=16.),
+ dbc_dict('chrysler_ram_hd_generated', None),
+ )
+
+
+class CarControllerParams:
+ def __init__(self, CP):
+ self.STEER_STEP = 2 # 50 Hz
+ self.STEER_ERROR_MAX = 80
+ if CP.carFingerprint in RAM_HD:
+ self.STEER_DELTA_UP = 14
+ self.STEER_DELTA_DOWN = 14
+ self.STEER_MAX = 361 # higher than this faults the EPS
+ elif CP.carFingerprint in RAM_DT:
+ self.STEER_DELTA_UP = 6
+ self.STEER_DELTA_DOWN = 6
+ self.STEER_MAX = 261 # EPS allows more, up to 350?
+ else:
+ self.STEER_DELTA_UP = 3
+ self.STEER_DELTA_DOWN = 3
+ self.STEER_MAX = 261 # higher than this faults the EPS
+
+
+STEER_THRESHOLD = 120
+
+RAM_DT = {CAR.RAM_1500_5TH_GEN, }
+RAM_HD = {CAR.RAM_HD_5TH_GEN, }
+RAM_CARS = RAM_DT | RAM_HD
+
+
+CHRYSLER_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(0xf132)
+CHRYSLER_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+ p16(0xf132)
+
+CHRYSLER_SOFTWARE_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.SYSTEM_SUPPLIER_ECU_SOFTWARE_NUMBER)
+CHRYSLER_SOFTWARE_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.SYSTEM_SUPPLIER_ECU_SOFTWARE_NUMBER)
+
+CHRYSLER_RX_OFFSET = -0x280
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[
+ Request(
+ [CHRYSLER_VERSION_REQUEST],
+ [CHRYSLER_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.abs, Ecu.eps, Ecu.srs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.combinationMeter],
+ rx_offset=CHRYSLER_RX_OFFSET,
+ bus=0,
+ ),
+ Request(
+ [CHRYSLER_VERSION_REQUEST],
+ [CHRYSLER_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.abs, Ecu.hybrid, Ecu.engine, Ecu.transmission],
+ bus=0,
+ ),
+ Request(
+ [CHRYSLER_SOFTWARE_VERSION_REQUEST],
+ [CHRYSLER_SOFTWARE_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.engine, Ecu.transmission],
+ bus=0,
+ ),
+ ],
+ extra_ecus=[
+ (Ecu.abs, 0x7e4, None), # alt address for abs on hybrids, NOTE: not on all hybrid platforms
+ ],
+)
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/common/__init__.py b/opendbc/car/common/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/common/basedir.py b/opendbc/car/common/basedir.py
new file mode 100644
index 0000000000..6b4811e53c
--- /dev/null
+++ b/opendbc/car/common/basedir.py
@@ -0,0 +1,4 @@
+import os
+
+
+BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../"))
diff --git a/opendbc/car/common/conversions.py b/opendbc/car/common/conversions.py
new file mode 100644
index 0000000000..b02b33c625
--- /dev/null
+++ b/opendbc/car/common/conversions.py
@@ -0,0 +1,19 @@
+import numpy as np
+
+class Conversions:
+ # Speed
+ MPH_TO_KPH = 1.609344
+ KPH_TO_MPH = 1. / MPH_TO_KPH
+ MS_TO_KPH = 3.6
+ KPH_TO_MS = 1. / MS_TO_KPH
+ MS_TO_MPH = MS_TO_KPH * KPH_TO_MPH
+ MPH_TO_MS = MPH_TO_KPH * KPH_TO_MS
+ MS_TO_KNOTS = 1.9438
+ KNOTS_TO_MS = 1. / MS_TO_KNOTS
+
+ # Angle
+ DEG_TO_RAD = np.pi / 180.
+ RAD_TO_DEG = 1. / DEG_TO_RAD
+
+ # Mass
+ LB_TO_KG = 0.453592
diff --git a/opendbc/car/common/filter_simple.py b/opendbc/car/common/filter_simple.py
new file mode 100644
index 0000000000..0ec7a51562
--- /dev/null
+++ b/opendbc/car/common/filter_simple.py
@@ -0,0 +1,18 @@
+class FirstOrderFilter:
+ # first order filter
+ def __init__(self, x0, rc, dt, initialized=True):
+ self.x = x0
+ self.dt = dt
+ self.update_alpha(rc)
+ self.initialized = initialized
+
+ def update_alpha(self, rc):
+ self.alpha = self.dt / (rc + self.dt)
+
+ def update(self, x):
+ if self.initialized:
+ self.x = (1. - self.alpha) * self.x + self.alpha * x
+ else:
+ self.initialized = True
+ self.x = x
+ return self.x
diff --git a/opendbc/car/common/numpy_fast.py b/opendbc/car/common/numpy_fast.py
new file mode 100644
index 0000000000..81f6d6a6bc
--- /dev/null
+++ b/opendbc/car/common/numpy_fast.py
@@ -0,0 +1,22 @@
+def clip(x, lo, hi):
+ return max(lo, min(hi, x))
+
+
+def interp(x, xp, fp):
+ N = len(xp)
+
+ def get_interp(xv):
+ hi = 0
+ while hi < N and xv > xp[hi]:
+ hi += 1
+ low = hi - 1
+ return fp[-1] if hi == N and xv > xp[low] else (
+ fp[0] if hi == 0 else
+ (xv - xp[low]) * (fp[hi] - fp[low]) / (xp[hi] - xp[low]) + fp[low])
+
+ return [get_interp(v) for v in x] if hasattr(x, '__iter__') else get_interp(x)
+
+
+def mean(x):
+ return sum(x) / len(x)
+
diff --git a/opendbc/car/common/simple_kalman.py b/opendbc/car/common/simple_kalman.py
new file mode 100644
index 0000000000..194b27204b
--- /dev/null
+++ b/opendbc/car/common/simple_kalman.py
@@ -0,0 +1,54 @@
+import numpy as np
+
+
+def get_kalman_gain(dt, A, C, Q, R, iterations=100):
+ P = np.zeros_like(Q)
+ for _ in range(iterations):
+ P = A.dot(P).dot(A.T) + dt * Q
+ S = C.dot(P).dot(C.T) + R
+ K = P.dot(C.T).dot(np.linalg.inv(S))
+ P = (np.eye(len(P)) - K.dot(C)).dot(P)
+ return K
+
+
+class KF1D:
+ # this EKF assumes constant covariance matrix, so calculations are much simpler
+ # the Kalman gain also needs to be precomputed using the control module
+
+ def __init__(self, x0, A, C, K):
+ self.x0_0 = x0[0][0]
+ self.x1_0 = x0[1][0]
+ self.A0_0 = A[0][0]
+ self.A0_1 = A[0][1]
+ self.A1_0 = A[1][0]
+ self.A1_1 = A[1][1]
+ self.C0_0 = C[0]
+ self.C0_1 = C[1]
+ self.K0_0 = K[0][0]
+ self.K1_0 = K[1][0]
+
+ self.A_K_0 = self.A0_0 - self.K0_0 * self.C0_0
+ self.A_K_1 = self.A0_1 - self.K0_0 * self.C0_1
+ self.A_K_2 = self.A1_0 - self.K1_0 * self.C0_0
+ self.A_K_3 = self.A1_1 - self.K1_0 * self.C0_1
+
+ # K matrix needs to be pre-computed as follow:
+ # import control
+ # (x, l, K) = control.dare(np.transpose(self.A), np.transpose(self.C), Q, R)
+ # self.K = np.transpose(K)
+
+ def update(self, meas):
+ #self.x = np.dot(self.A_K, self.x) + np.dot(self.K, meas)
+ x0_0 = self.A_K_0 * self.x0_0 + self.A_K_1 * self.x1_0 + self.K0_0 * meas
+ x1_0 = self.A_K_2 * self.x0_0 + self.A_K_3 * self.x1_0 + self.K1_0 * meas
+ self.x0_0 = x0_0
+ self.x1_0 = x1_0
+ return [self.x0_0, self.x1_0]
+
+ @property
+ def x(self):
+ return [[self.x0_0], [self.x1_0]]
+
+ def set_x(self, x):
+ self.x0_0 = x[0][0]
+ self.x1_0 = x[1][0]
diff --git a/opendbc/car/disable_ecu.py b/opendbc/car/disable_ecu.py
new file mode 100644
index 0000000000..1e3f412449
--- /dev/null
+++ b/opendbc/car/disable_ecu.py
@@ -0,0 +1,36 @@
+from opendbc.car import carlog
+from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
+
+EXT_DIAG_REQUEST = b'\x10\x03'
+EXT_DIAG_RESPONSE = b'\x50\x03'
+
+COM_CONT_RESPONSE = b''
+
+
+def disable_ecu(can_recv, can_send, bus=0, addr=0x7d0, sub_addr=None, com_cont_req=b'\x28\x83\x01', timeout=0.1, retry=10, debug=False):
+ """Silence an ECU by disabling sending and receiving messages using UDS 0x28.
+ The ECU will stay silent as long as openpilot keeps sending Tester Present.
+
+ This is used to disable the radar in some cars. Openpilot will emulate the radar.
+ WARNING: THIS DISABLES AEB!"""
+ carlog.warning(f"ecu disable {hex(addr), sub_addr} ...")
+
+ for i in range(retry):
+ try:
+ query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [EXT_DIAG_REQUEST], [EXT_DIAG_RESPONSE], debug=debug)
+
+ for _, _ in query.get_data(timeout).items():
+ carlog.warning("communication control disable tx/rx ...")
+
+ query = IsoTpParallelQuery(can_send, can_recv, bus, [(addr, sub_addr)], [com_cont_req], [COM_CONT_RESPONSE], debug=debug)
+ query.get_data(0)
+
+ carlog.warning("ecu disabled")
+ return True
+
+ except Exception:
+ carlog.exception("ecu disable exception")
+
+ carlog.error(f"ecu disable retry ({i + 1}) ...")
+ carlog.error("ecu disable failed")
+ return False
diff --git a/opendbc/car/docs.py b/opendbc/car/docs.py
new file mode 100644
index 0000000000..b44c873c3c
--- /dev/null
+++ b/opendbc/car/docs.py
@@ -0,0 +1,59 @@
+from collections import defaultdict
+import jinja2
+from enum import Enum
+from natsort import natsorted
+
+from opendbc.car import gen_empty_fingerprint
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarDocs, Column, CommonFootnote, PartType
+from opendbc.car.car_helpers import interfaces, get_interface_attr
+from opendbc.car.values import PLATFORMS
+
+
+def get_all_footnotes() -> dict[Enum, int]:
+ all_footnotes = list(CommonFootnote)
+ for footnotes in get_interface_attr("Footnote", ignore_none=True).values():
+ all_footnotes.extend(footnotes)
+ return {fn: idx + 1 for idx, fn in enumerate(all_footnotes)}
+
+
+def get_all_car_docs() -> list[CarDocs]:
+ all_car_docs: list[CarDocs] = []
+ footnotes = get_all_footnotes()
+ for model, platform in PLATFORMS.items():
+ car_docs = platform.config.car_docs
+ # If available, uses experimental longitudinal limits for the docs
+ CP = interfaces[model][0].get_params(platform, fingerprint=gen_empty_fingerprint(),
+ car_fw=[CarParams.CarFw(ecu=CarParams.Ecu.unknown)], experimental_long=True, docs=True)
+
+ if CP.dashcamOnly or not len(car_docs):
+ continue
+
+ # A platform can include multiple car models
+ for _car_docs in car_docs:
+ if not hasattr(_car_docs, "row"):
+ _car_docs.init_make(CP)
+ _car_docs.init(CP, footnotes)
+ all_car_docs.append(_car_docs)
+
+ # Sort cars by make and model + year
+ sorted_cars: list[CarDocs] = natsorted(all_car_docs, key=lambda car: car.name.lower())
+ return sorted_cars
+
+
+def group_by_make(all_car_docs: list[CarDocs]) -> dict[str, list[CarDocs]]:
+ sorted_car_docs = defaultdict(list)
+ for car_docs in all_car_docs:
+ sorted_car_docs[car_docs.make].append(car_docs)
+ return dict(sorted_car_docs)
+
+
+def generate_cars_md(all_car_docs: list[CarDocs], template_fn: str) -> str:
+ with open(template_fn) as f:
+ template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True)
+
+ footnotes = [fn.value.text for fn in get_all_footnotes()]
+ cars_md: str = template.render(all_car_docs=all_car_docs, PartType=PartType,
+ group_by_make=group_by_make, footnotes=footnotes,
+ Column=Column)
+ return cars_md
diff --git a/opendbc/car/docs_definitions.py b/opendbc/car/docs_definitions.py
new file mode 100644
index 0000000000..5e0fa28a00
--- /dev/null
+++ b/opendbc/car/docs_definitions.py
@@ -0,0 +1,370 @@
+import re
+from collections import namedtuple
+import copy
+from dataclasses import dataclass, field
+from enum import Enum
+
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.structs import CarParams
+
+GOOD_TORQUE_THRESHOLD = 1.0 # m/s^2
+MODEL_YEARS_RE = r"(?<= )((\d{4}-\d{2})|(\d{4}))(,|$)"
+
+
+class Column(Enum):
+ MAKE = "Make"
+ MODEL = "Model"
+ PACKAGE = "Supported Package"
+ LONGITUDINAL = "ACC"
+ FSR_LONGITUDINAL = "No ACC accel below"
+ FSR_STEERING = "No ALC below"
+ STEERING_TORQUE = "Steering Torque"
+ AUTO_RESUME = "Resume from stop"
+ HARDWARE = "Hardware Needed"
+ VIDEO = "Video"
+
+
+class Star(Enum):
+ FULL = "full"
+ HALF = "half"
+ EMPTY = "empty"
+
+
+# A part + its comprised parts
+@dataclass
+class BasePart:
+ name: str
+ parts: list[Enum] = field(default_factory=list)
+
+ def all_parts(self):
+ # Recursively get all parts
+ _parts = 'parts'
+ parts = []
+ parts.extend(getattr(self, _parts))
+ for part in getattr(self, _parts):
+ parts.extend(part.value.all_parts())
+
+ return parts
+
+
+class EnumBase(Enum):
+ @property
+ def part_type(self):
+ return PartType(self.__class__)
+
+
+class Mount(EnumBase):
+ mount = BasePart("mount")
+ angled_mount_8_degrees = BasePart("angled mount (8 degrees)")
+
+
+class Cable(EnumBase):
+ rj45_cable_7ft = BasePart("RJ45 cable (7 ft)")
+ long_obdc_cable = BasePart("long OBD-C cable")
+ 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_1_5ft = BasePart("OBD-C cable (1.5 ft)")
+ right_angle_obd_c_cable_1_5ft = BasePart("right angle OBD-C cable (1.5 ft)")
+
+
+class Accessory(EnumBase):
+ harness_box = BasePart("harness box")
+ comma_power_v2 = BasePart("comma power v2")
+
+
+@dataclass
+class BaseCarHarness(BasePart):
+ parts: list[Enum] = field(default_factory=lambda: [Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft])
+ has_connector: bool = True # without are hidden on the harness connector page
+
+
+class CarHarness(EnumBase):
+ nidec = BaseCarHarness("Honda Nidec connector")
+ bosch_a = BaseCarHarness("Honda Bosch A connector")
+ bosch_b = BaseCarHarness("Honda Bosch B connector")
+ toyota_a = BaseCarHarness("Toyota A connector")
+ toyota_b = BaseCarHarness("Toyota B connector")
+ subaru_a = BaseCarHarness("Subaru A connector")
+ subaru_b = BaseCarHarness("Subaru B connector")
+ subaru_c = BaseCarHarness("Subaru C connector")
+ subaru_d = BaseCarHarness("Subaru D connector")
+ 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])
+ hyundai_a = BaseCarHarness("Hyundai A connector")
+ hyundai_b = BaseCarHarness("Hyundai B connector")
+ hyundai_c = BaseCarHarness("Hyundai C connector")
+ hyundai_d = BaseCarHarness("Hyundai D connector")
+ hyundai_e = BaseCarHarness("Hyundai E connector")
+ hyundai_f = BaseCarHarness("Hyundai F connector")
+ hyundai_g = BaseCarHarness("Hyundai G connector")
+ hyundai_h = BaseCarHarness("Hyundai H connector")
+ hyundai_i = BaseCarHarness("Hyundai I connector")
+ hyundai_j = BaseCarHarness("Hyundai J connector")
+ hyundai_k = BaseCarHarness("Hyundai K connector")
+ hyundai_l = BaseCarHarness("Hyundai L connector")
+ hyundai_m = BaseCarHarness("Hyundai M connector")
+ hyundai_n = BaseCarHarness("Hyundai N connector")
+ hyundai_o = BaseCarHarness("Hyundai O connector")
+ hyundai_p = BaseCarHarness("Hyundai P connector")
+ 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.long_obdc_cable], has_connector=False)
+ gm = BaseCarHarness("GM connector", parts=[Accessory.harness_box])
+ gmsdgm = BaseCarHarness("GM SDGM connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable,
+ Cable.usbc_coupler, Accessory.comma_power_v2])
+ nissan_a = BaseCarHarness("Nissan A connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler])
+ nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, 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_v2, Cable.rj45_cable_7ft, Cable.long_obdc_cable,
+ Cable.usbc_coupler])
+
+
+class Device(EnumBase):
+ threex = BasePart("comma 3X", parts=[Mount.mount, Cable.right_angle_obd_c_cable_1_5ft])
+ # variant of comma 3X with angled mounts
+ threex_angled_mount = BasePart("comma 3X", parts=[Mount.angled_mount_8_degrees, Cable.right_angle_obd_c_cable_1_5ft])
+ red_panda = BasePart("red panda")
+
+
+class Kit(EnumBase):
+ red_panda_kit = BasePart("CAN FD panda kit", parts=[Device.red_panda, Accessory.harness_box,
+ Cable.usb_a_2_a_cable, Cable.usbc_otg_cable, Cable.obd_c_cable_1_5ft])
+
+
+class Tool(EnumBase):
+ socket_8mm_deep = BasePart("Socket Wrench 8mm or 5/16\" (deep)")
+ pry_tool = BasePart("Pry Tool")
+
+
+class PartType(Enum):
+ accessory = Accessory
+ cable = Cable
+ connector = CarHarness
+ device = Device
+ kit = Kit
+ mount = Mount
+ tool = Tool
+
+
+DEFAULT_CAR_PARTS: list[EnumBase] = [Device.threex]
+
+
+@dataclass
+class CarParts:
+ parts: list[EnumBase] = field(default_factory=list)
+
+ def __call__(self):
+ return copy.deepcopy(self)
+
+ @classmethod
+ def common(cls, add: list[EnumBase] = None, remove: list[EnumBase] = None):
+ p = [part for part in (add or []) + DEFAULT_CAR_PARTS if part not in (remove or [])]
+ return cls(p)
+
+ def all_parts(self):
+ parts = []
+ for part in self.parts:
+ parts.extend(part.value.all_parts())
+ return self.parts + parts
+
+
+CarFootnote = namedtuple("CarFootnote", ["text", "column", "docs_only", "shop_footnote"], defaults=(False, False))
+
+
+class CommonFootnote(Enum):
+ EXP_LONG_AVAIL = CarFootnote(
+ "openpilot Longitudinal Control (Alpha) is available behind a toggle; " +
+ "the toggle is only available in non-release branches such as `devel` or `master-ci`.",
+ Column.LONGITUDINAL, docs_only=True)
+ EXP_LONG_DSU = CarFootnote(
+ "By default, this car will use the stock Adaptive Cruise Control (ACC) for longitudinal control. " +
+ "If the Driver Support Unit (DSU) is disconnected, openpilot ACC will replace " +
+ "stock ACC. NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).",
+ Column.LONGITUDINAL)
+
+
+def get_footnotes(footnotes: list[Enum], column: Column) -> list[Enum]:
+ # Returns applicable footnotes given current column
+ return [fn for fn in footnotes if fn.value.column == column]
+
+
+# TODO: store years as a list
+def get_year_list(years):
+ years_list = []
+ if len(years) == 0:
+ return years_list
+
+ for year in years.split(','):
+ year = year.strip()
+ if len(year) == 4:
+ years_list.append(str(year))
+ elif "-" in year and len(year) == 7:
+ start, end = year.split("-")
+ years_list.extend(map(str, range(int(start), int(f"20{end}") + 1)))
+ else:
+ raise Exception(f"Malformed year string: {years}")
+ return years_list
+
+
+def split_name(name: str) -> tuple[str, str, str]:
+ make, model = name.split(" ", 1)
+ years = ""
+ match = re.search(MODEL_YEARS_RE, model)
+ if match is not None:
+ years = model[match.start():]
+ model = model[:match.start() - 1]
+ return make, model, years
+
+
+@dataclass
+class CarDocs:
+ # make + model + model years
+ name: str
+
+ # Example for Toyota Corolla MY20
+ # requirements: Lane Tracing Assist (LTA) and Dynamic Radar Cruise Control (DRCC)
+ # US Market reference: "All", since all Corolla in the US come standard with LTA and DRCC
+
+ # the simplest description of the requirements for the US market
+ package: str
+
+ # the minimum compatibility requirements for this model, regardless
+ # of market. can be a package, trim, or list of features
+ requirements: str | None = None
+
+ video_link: str | None = None
+ footnotes: list[Enum] = field(default_factory=list)
+ min_steer_speed: float | None = None
+ min_enable_speed: float | None = None
+ auto_resume: bool | None = None
+
+ # all the parts needed for the supported car
+ car_parts: CarParts = field(default_factory=CarParts)
+
+ def __post_init__(self):
+ self.make, self.model, self.years = split_name(self.name)
+ self.year_list = get_year_list(self.years)
+
+ def init(self, CP: CarParams, all_footnotes: dict[Enum, int]):
+ self.car_name = CP.carName
+ self.car_fingerprint = CP.carFingerprint
+
+ # longitudinal column
+ op_long = "Stock"
+ if CP.experimentalLongitudinalAvailable or CP.enableDsu:
+ op_long = "openpilot available"
+ if CP.enableDsu:
+ self.footnotes.append(CommonFootnote.EXP_LONG_DSU)
+ else:
+ self.footnotes.append(CommonFootnote.EXP_LONG_AVAIL)
+ elif CP.openpilotLongitudinalControl and not CP.enableDsu:
+ op_long = "openpilot"
+
+ # min steer & enable speed columns
+ # TODO: set all the min steer speeds in carParams and remove this
+ if self.min_steer_speed is not None:
+ assert CP.minSteerSpeed < 0.5, f"{CP.carFingerprint}: Minimum steer speed set in both CarDocs and CarParams"
+ else:
+ self.min_steer_speed = CP.minSteerSpeed
+
+ # TODO: set all the min enable speeds in carParams correctly and remove this
+ if self.min_enable_speed is None:
+ self.min_enable_speed = CP.minEnableSpeed
+
+ if self.auto_resume is None:
+ self.auto_resume = CP.autoResumeSng and self.min_enable_speed <= 0
+
+ # hardware column
+ hardware_col = "None"
+ if self.car_parts.parts:
+ model_years = self.model + (' ' + self.years if self.years else '')
+ buy_link = f'Buy Here'
+
+ tools_docs = [part for part in self.car_parts.all_parts() if isinstance(part, Tool)]
+ parts_docs = [part for part in self.car_parts.all_parts() if not isinstance(part, Tool)]
+
+ def display_func(parts):
+ return '
'.join([f"- {parts.count(part)} {part.value.name}" for part in sorted(set(parts), key=lambda part: str(part.value.name))])
+
+ hardware_col = f'Parts
{display_func(parts_docs)}
{buy_link} '
+ if len(tools_docs):
+ hardware_col += f'Tools
{display_func(tools_docs)} '
+
+ self.row: dict[Enum, str | Star] = {
+ Column.MAKE: self.make,
+ Column.MODEL: self.model,
+ Column.PACKAGE: self.package,
+ Column.LONGITUDINAL: op_long,
+ Column.FSR_LONGITUDINAL: f"{max(self.min_enable_speed * CV.MS_TO_MPH, 0):.0f} mph",
+ Column.FSR_STEERING: f"{max(self.min_steer_speed * CV.MS_TO_MPH, 0):.0f} mph",
+ Column.STEERING_TORQUE: Star.EMPTY,
+ Column.AUTO_RESUME: Star.FULL if self.auto_resume else Star.EMPTY,
+ Column.HARDWARE: hardware_col,
+ Column.VIDEO: self.video_link if self.video_link is not None else "", # replaced with an image and link from template in get_column
+ }
+
+ # Set steering torque star from max lateral acceleration
+ assert CP.maxLateralAccel > 0.1
+ if CP.maxLateralAccel >= GOOD_TORQUE_THRESHOLD:
+ self.row[Column.STEERING_TORQUE] = Star.FULL
+
+ self.all_footnotes = all_footnotes
+ self.detail_sentence = self.get_detail_sentence(CP)
+
+ return self
+
+ def init_make(self, CP: CarParams):
+ """CarDocs subclasses can add make-specific logic for harness selection, footnotes, etc."""
+
+ def get_detail_sentence(self, CP):
+ if not CP.notCar:
+ sentence_builder = "openpilot upgrades your {car_model} with automated lane centering{alc} and adaptive cruise control{acc}."
+
+ if self.min_steer_speed > self.min_enable_speed:
+ alc = f" above {self.min_steer_speed * CV.MS_TO_MPH:.0f} mph," if self.min_steer_speed > 0 else " at all speeds,"
+ else:
+ alc = ""
+
+ # Exception for cars which do not auto-resume yet
+ acc = ""
+ if self.min_enable_speed > 0:
+ acc = f" while driving above {self.min_enable_speed * CV.MS_TO_MPH:.0f} mph"
+ elif self.auto_resume:
+ acc = " that automatically resumes from a stop"
+
+ if self.row[Column.STEERING_TORQUE] != Star.FULL:
+ sentence_builder += " This car may not be able to take tight turns on its own."
+
+ # experimental mode
+ exp_link = "Experimental mode"
+ if CP.openpilotLongitudinalControl and not CP.experimentalLongitudinalAvailable:
+ sentence_builder += f" Traffic light and stop sign handling is also available in {exp_link}."
+
+ return sentence_builder.format(car_model=f"{self.make} {self.model}", alc=alc, acc=acc)
+
+ else:
+ if CP.carFingerprint == "COMMA_BODY":
+ return "The body is a robotics dev kit that can run openpilot. Learn more."
+ else:
+ raise Exception(f"This notCar does not have a detail sentence: {CP.carFingerprint}")
+
+ def get_column(self, column: Column, star_icon: str, video_icon: str, footnote_tag: str) -> str:
+ item: str | Star = self.row[column]
+ if isinstance(item, Star):
+ item = star_icon.format(item.value)
+ elif column == Column.MODEL and len(self.years):
+ item += f" {self.years}"
+ elif column == Column.VIDEO and len(item) > 0:
+ item = video_icon.format(item)
+
+ footnotes = get_footnotes(self.footnotes, column)
+ if len(footnotes):
+ sups = sorted([self.all_footnotes[fn] for fn in footnotes])
+ item += footnote_tag.format(f'{",".join(map(str, sups))}')
+
+ return item
diff --git a/opendbc/car/ecu_addrs.py b/opendbc/car/ecu_addrs.py
new file mode 100644
index 0000000000..3d1b83f0c6
--- /dev/null
+++ b/opendbc/car/ecu_addrs.py
@@ -0,0 +1,56 @@
+import time
+
+from panda import uds
+from opendbc.car import make_tester_present_msg, carlog
+from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
+from opendbc.car.fw_query_definitions import EcuAddrBusType
+
+
+def _is_tester_present_response(msg: CanData, subaddr: int = None) -> bool:
+ # ISO-TP messages are always padded to 8 bytes
+ # tester present response is always a single frame
+ dat_offset = 1 if subaddr is not None else 0
+ if len(msg.dat) == 8 and 1 <= msg.dat[dat_offset] <= 7:
+ # success response
+ if msg.dat[dat_offset + 1] == (uds.SERVICE_TYPE.TESTER_PRESENT + 0x40):
+ return True
+ # error response
+ if msg.dat[dat_offset + 1] == 0x7F and msg.dat[dat_offset + 2] == uds.SERVICE_TYPE.TESTER_PRESENT:
+ return True
+ return False
+
+
+def get_all_ecu_addrs(can_recv: CanRecvCallable, can_send: CanSendCallable, bus: int, timeout: float = 1, debug: bool = True) -> set[EcuAddrBusType]:
+ addr_list = [0x700 + i for i in range(256)] + [0x18da00f1 + (i << 8) for i in range(256)]
+ queries: set[EcuAddrBusType] = {(addr, None, bus) for addr in addr_list}
+ responses = queries
+ return get_ecu_addrs(can_recv, can_send, queries, responses, timeout=timeout, debug=debug)
+
+
+def get_ecu_addrs(can_recv: CanRecvCallable, can_send: CanSendCallable, queries: set[EcuAddrBusType],
+ responses: set[EcuAddrBusType], timeout: float = 1, debug: bool = False) -> set[EcuAddrBusType]:
+ ecu_responses: set[EcuAddrBusType] = set() # set((addr, subaddr, bus),)
+ try:
+ msgs = [make_tester_present_msg(addr, bus, subaddr) for addr, subaddr, bus in queries]
+
+ can_recv()
+ can_send(msgs)
+ start_time = time.monotonic()
+ while time.monotonic() - start_time < timeout:
+ can_packets = can_recv(wait_for_one=True)
+ for packet in can_packets:
+ for msg in packet:
+ if not len(msg.dat):
+ carlog.warning("ECU addr scan: skipping empty remote frame")
+ continue
+
+ subaddr = None if (msg.address, None, msg.src) in responses else msg.dat[0]
+ if (msg.address, subaddr, msg.src) in responses and _is_tester_present_response(msg, subaddr):
+ if debug:
+ print(f"CAN-RX: {hex(msg.address)} - 0x{bytes.hex(msg.dat)}")
+ if (msg.address, subaddr, msg.src) in ecu_responses:
+ print(f"Duplicate ECU address: {hex(msg.address)}")
+ ecu_responses.add((msg.address, subaddr, msg.src))
+ except Exception:
+ carlog.exception("ECU addr scan exception")
+ return ecu_responses
diff --git a/opendbc/car/fingerprints.py b/opendbc/car/fingerprints.py
new file mode 100644
index 0000000000..1670ac6342
--- /dev/null
+++ b/opendbc/car/fingerprints.py
@@ -0,0 +1,344 @@
+from opendbc.car.interfaces import get_interface_attr
+from opendbc.car.body.values import CAR as BODY
+from opendbc.car.chrysler.values import CAR as CHRYSLER
+from opendbc.car.ford.values import CAR as FORD
+from opendbc.car.gm.values import CAR as GM
+from opendbc.car.honda.values import CAR as HONDA
+from opendbc.car.hyundai.values import CAR as HYUNDAI
+from opendbc.car.mazda.values import CAR as MAZDA
+from opendbc.car.mock.values import CAR as MOCK
+from opendbc.car.nissan.values import CAR as NISSAN
+from opendbc.car.subaru.values import CAR as SUBARU
+from opendbc.car.toyota.values import CAR as TOYOTA
+from opendbc.car.volkswagen.values import CAR as VW
+
+FW_VERSIONS = get_interface_attr('FW_VERSIONS', combine_brands=True, ignore_none=True)
+_FINGERPRINTS = get_interface_attr('FINGERPRINTS', combine_brands=True, ignore_none=True)
+
+_DEBUG_ADDRESS = {1880: 8} # reserved for debug purposes
+
+
+def is_valid_for_fingerprint(msg, car_fingerprint: dict[int, int]):
+ adr = msg.address
+ # ignore addresses that are more than 11 bits
+ return (adr in car_fingerprint and car_fingerprint[adr] == len(msg.dat)) or adr >= 0x800
+
+
+def eliminate_incompatible_cars(msg, candidate_cars):
+ """Removes cars that could not have sent msg.
+
+ Inputs:
+ msg: A cereal/log CanData message from the car.
+ candidate_cars: A list of cars to consider.
+
+ Returns:
+ A list containing the subset of candidate_cars that could have sent msg.
+ """
+ compatible_cars = []
+
+ for car_name in candidate_cars:
+ car_fingerprints = _FINGERPRINTS[car_name]
+
+ for fingerprint in car_fingerprints:
+ # add alien debug address
+ if is_valid_for_fingerprint(msg, fingerprint | _DEBUG_ADDRESS):
+ compatible_cars.append(car_name)
+ break
+
+ return compatible_cars
+
+
+def all_known_cars():
+ """Returns a list of all known car strings."""
+ return list({*FW_VERSIONS.keys(), *_FINGERPRINTS.keys()})
+
+
+def all_legacy_fingerprint_cars():
+ """Returns a list of all known car strings, FPv1 only."""
+ return list(_FINGERPRINTS.keys())
+
+
+# A dict that maps old platform strings to their latest representations
+MIGRATION = {
+ "ACURA ILX 2016 ACURAWATCH PLUS": HONDA.ACURA_ILX,
+ "ACURA RDX 2018 ACURAWATCH PLUS": HONDA.ACURA_RDX,
+ "ACURA RDX 2020 TECH": HONDA.ACURA_RDX_3G,
+ "AUDI A3": VW.AUDI_A3_MK3,
+ "HONDA ACCORD 2018 HYBRID TOURING": HONDA.HONDA_ACCORD,
+ "HONDA ACCORD 1.5T 2018": HONDA.HONDA_ACCORD,
+ "HONDA ACCORD 2018 LX 1.5T": HONDA.HONDA_ACCORD,
+ "HONDA ACCORD 2018 SPORT 2T": HONDA.HONDA_ACCORD,
+ "HONDA ACCORD 2T 2018": HONDA.HONDA_ACCORD,
+ "HONDA ACCORD HYBRID 2018": HONDA.HONDA_ACCORD,
+ "HONDA CIVIC 2016 TOURING": HONDA.HONDA_CIVIC,
+ "HONDA CIVIC HATCHBACK 2017 SEDAN/COUPE 2019": HONDA.HONDA_CIVIC_BOSCH,
+ "HONDA CIVIC SEDAN 1.6 DIESEL": HONDA.HONDA_CIVIC_BOSCH_DIESEL,
+ "HONDA CR-V 2016 EXECUTIVE": HONDA.HONDA_CRV_EU,
+ "HONDA CR-V 2016 TOURING": HONDA.HONDA_CRV,
+ "HONDA CR-V 2017 EX": HONDA.HONDA_CRV_5G,
+ "HONDA CR-V 2019 HYBRID": HONDA.HONDA_CRV_HYBRID,
+ "HONDA FIT 2018 EX": HONDA.HONDA_FIT,
+ "HONDA HRV 2019 TOURING": HONDA.HONDA_HRV,
+ "HONDA INSIGHT 2019 TOURING": HONDA.HONDA_INSIGHT,
+ "HONDA ODYSSEY 2018 EX-L": HONDA.HONDA_ODYSSEY,
+ "HONDA ODYSSEY 2019 EXCLUSIVE CHN": HONDA.HONDA_ODYSSEY_CHN,
+ "HONDA PILOT 2017 TOURING": HONDA.HONDA_PILOT,
+ "HONDA PILOT 2019 ELITE": HONDA.HONDA_PILOT,
+ "HONDA PILOT 2019": HONDA.HONDA_PILOT,
+ "HONDA PASSPORT 2021": HONDA.HONDA_PILOT,
+ "HONDA RIDGELINE 2017 BLACK EDITION": HONDA.HONDA_RIDGELINE,
+ "HYUNDAI ELANTRA LIMITED ULTIMATE 2017": HYUNDAI.HYUNDAI_ELANTRA,
+ "HYUNDAI SANTA FE LIMITED 2019": HYUNDAI.HYUNDAI_SANTA_FE,
+ "HYUNDAI TUCSON DIESEL 2019": HYUNDAI.HYUNDAI_TUCSON,
+ "KIA OPTIMA 2016": HYUNDAI.KIA_OPTIMA_G4,
+ "KIA OPTIMA 2019": HYUNDAI.KIA_OPTIMA_G4_FL,
+ "KIA OPTIMA SX 2019 & 2016": HYUNDAI.KIA_OPTIMA_G4_FL,
+ "LEXUS CT 200H 2018": TOYOTA.LEXUS_CTH,
+ "LEXUS ES 300H 2018": TOYOTA.LEXUS_ES,
+ "LEXUS ES 300H 2019": TOYOTA.LEXUS_ES_TSS2,
+ "LEXUS IS300 2018": TOYOTA.LEXUS_IS,
+ "LEXUS NX300 2018": TOYOTA.LEXUS_NX,
+ "LEXUS NX300H 2018": TOYOTA.LEXUS_NX,
+ "LEXUS RX 350 2016": TOYOTA.LEXUS_RX,
+ "LEXUS RX350 2020": TOYOTA.LEXUS_RX_TSS2,
+ "LEXUS RX450 HYBRID 2020": TOYOTA.LEXUS_RX_TSS2,
+ "TOYOTA SIENNA XLE 2018": TOYOTA.TOYOTA_SIENNA,
+ "TOYOTA C-HR HYBRID 2018": TOYOTA.TOYOTA_CHR,
+ "TOYOTA COROLLA HYBRID TSS2 2019": TOYOTA.TOYOTA_COROLLA_TSS2,
+ "TOYOTA RAV4 HYBRID 2019": TOYOTA.TOYOTA_RAV4_TSS2,
+ "LEXUS ES HYBRID 2019": TOYOTA.LEXUS_ES_TSS2,
+ "LEXUS NX HYBRID 2018": TOYOTA.LEXUS_NX,
+ "LEXUS NX HYBRID 2020": TOYOTA.LEXUS_NX_TSS2,
+ "LEXUS RX HYBRID 2020": TOYOTA.LEXUS_RX_TSS2,
+ "TOYOTA ALPHARD HYBRID 2021": TOYOTA.TOYOTA_ALPHARD_TSS2,
+ "TOYOTA AVALON HYBRID 2019": TOYOTA.TOYOTA_AVALON_2019,
+ "TOYOTA AVALON HYBRID 2022": TOYOTA.TOYOTA_AVALON_TSS2,
+ "TOYOTA CAMRY HYBRID 2018": TOYOTA.TOYOTA_CAMRY,
+ "TOYOTA CAMRY HYBRID 2021": TOYOTA.TOYOTA_CAMRY_TSS2,
+ "TOYOTA C-HR HYBRID 2022": TOYOTA.TOYOTA_CHR_TSS2,
+ "TOYOTA HIGHLANDER HYBRID 2020": TOYOTA.TOYOTA_HIGHLANDER_TSS2,
+ "TOYOTA RAV4 HYBRID 2022": TOYOTA.TOYOTA_RAV4_TSS2_2022,
+ "TOYOTA RAV4 HYBRID 2023": TOYOTA.TOYOTA_RAV4_TSS2_2023,
+ "TOYOTA HIGHLANDER HYBRID 2018": TOYOTA.TOYOTA_HIGHLANDER,
+ "LEXUS ES HYBRID 2018": TOYOTA.LEXUS_ES,
+ "LEXUS RX HYBRID 2017": TOYOTA.LEXUS_RX,
+ "HYUNDAI TUCSON HYBRID 4TH GEN": HYUNDAI.HYUNDAI_TUCSON_4TH_GEN,
+ "KIA SPORTAGE HYBRID 5TH GEN": HYUNDAI.KIA_SPORTAGE_5TH_GEN,
+ "KIA SORENTO PLUG-IN HYBRID 4TH GEN": HYUNDAI.KIA_SORENTO_HEV_4TH_GEN,
+ "CADILLAC ESCALADE ESV PLATINUM 2019": GM.CADILLAC_ESCALADE_ESV_2019,
+
+ # Removal of platform_str, see https://github.com/commaai/openpilot/pull/31868/
+ "COMMA BODY": BODY.COMMA_BODY,
+ "CHRYSLER PACIFICA HYBRID 2017": CHRYSLER.CHRYSLER_PACIFICA_2018_HYBRID,
+ "CHRYSLER_PACIFICA_2017_HYBRID": CHRYSLER.CHRYSLER_PACIFICA_2018_HYBRID,
+ "CHRYSLER PACIFICA HYBRID 2018": CHRYSLER.CHRYSLER_PACIFICA_2018_HYBRID,
+ "CHRYSLER PACIFICA HYBRID 2019": CHRYSLER.CHRYSLER_PACIFICA_2019_HYBRID,
+ "CHRYSLER PACIFICA 2018": CHRYSLER.CHRYSLER_PACIFICA_2018,
+ "CHRYSLER PACIFICA 2020": CHRYSLER.CHRYSLER_PACIFICA_2020,
+ "DODGE DURANGO 2021": CHRYSLER.DODGE_DURANGO,
+ "JEEP GRAND CHEROKEE V6 2018": CHRYSLER.JEEP_GRAND_CHEROKEE,
+ "JEEP GRAND CHEROKEE 2019": CHRYSLER.JEEP_GRAND_CHEROKEE_2019,
+ "RAM 1500 5TH GEN": CHRYSLER.RAM_1500_5TH_GEN,
+ "RAM HD 5TH GEN": CHRYSLER.RAM_HD_5TH_GEN,
+ "FORD BRONCO SPORT 1ST GEN": FORD.FORD_BRONCO_SPORT_MK1,
+ "FORD ESCAPE 4TH GEN": FORD.FORD_ESCAPE_MK4,
+ "FORD EXPLORER 6TH GEN": FORD.FORD_EXPLORER_MK6,
+ "FORD F-150 14TH GEN": FORD.FORD_F_150_MK14,
+ "FORD F-150 LIGHTNING 1ST GEN": FORD.FORD_F_150_LIGHTNING_MK1,
+ "FORD FOCUS 4TH GEN": FORD.FORD_FOCUS_MK4,
+ "FORD MAVERICK 1ST GEN": FORD.FORD_MAVERICK_MK1,
+ "FORD MUSTANG MACH-E 1ST GEN": FORD.FORD_MUSTANG_MACH_E_MK1,
+ "HOLDEN ASTRA RS-V BK 2017": GM.HOLDEN_ASTRA,
+ "CHEVROLET VOLT PREMIER 2017": GM.CHEVROLET_VOLT,
+ "CADILLAC ATS Premium Performance 2018": GM.CADILLAC_ATS,
+ "CHEVROLET MALIBU PREMIER 2017": GM.CHEVROLET_MALIBU,
+ "GMC ACADIA DENALI 2018": GM.GMC_ACADIA,
+ "BUICK LACROSSE 2017": GM.BUICK_LACROSSE,
+ "BUICK REGAL ESSENCE 2018": GM.BUICK_REGAL,
+ "CADILLAC ESCALADE 2017": GM.CADILLAC_ESCALADE,
+ "CADILLAC ESCALADE ESV 2016": GM.CADILLAC_ESCALADE_ESV,
+ "CADILLAC ESCALADE ESV 2019": GM.CADILLAC_ESCALADE_ESV_2019,
+ "CHEVROLET BOLT EUV 2022": GM.CHEVROLET_BOLT_EUV,
+ "CHEVROLET SILVERADO 1500 2020": GM.CHEVROLET_SILVERADO,
+ "CHEVROLET EQUINOX 2019": GM.CHEVROLET_EQUINOX,
+ "CHEVROLET TRAILBLAZER 2021": GM.CHEVROLET_TRAILBLAZER,
+ "HONDA ACCORD 2018": HONDA.HONDA_ACCORD,
+ "HONDA CIVIC (BOSCH) 2019": HONDA.HONDA_CIVIC_BOSCH,
+ "HONDA CIVIC SEDAN 1.6 DIESEL 2019": HONDA.HONDA_CIVIC_BOSCH_DIESEL,
+ "HONDA CIVIC 2022": HONDA.HONDA_CIVIC_2022,
+ "HONDA CR-V 2017": HONDA.HONDA_CRV_5G,
+ "HONDA CR-V HYBRID 2019": HONDA.HONDA_CRV_HYBRID,
+ "HONDA HR-V 2023": HONDA.HONDA_HRV_3G,
+ "ACURA RDX 2020": HONDA.ACURA_RDX_3G,
+ "HONDA INSIGHT 2019": HONDA.HONDA_INSIGHT,
+ "HONDA E 2020": HONDA.HONDA_E,
+ "ACURA ILX 2016": HONDA.ACURA_ILX,
+ "HONDA CR-V 2016": HONDA.HONDA_CRV,
+ "HONDA CR-V EU 2016": HONDA.HONDA_CRV_EU,
+ "HONDA FIT 2018": HONDA.HONDA_FIT,
+ "HONDA FREED 2020": HONDA.HONDA_FREED,
+ "HONDA HRV 2019": HONDA.HONDA_HRV,
+ "HONDA ODYSSEY 2018": HONDA.HONDA_ODYSSEY,
+ "HONDA ODYSSEY CHN 2019": HONDA.HONDA_ODYSSEY_CHN,
+ "ACURA RDX 2018": HONDA.ACURA_RDX,
+ "HONDA PILOT 2017": HONDA.HONDA_PILOT,
+ "HONDA RIDGELINE 2017": HONDA.HONDA_RIDGELINE,
+ "HONDA CIVIC 2016": HONDA.HONDA_CIVIC,
+ "HYUNDAI AZERA 6TH GEN": HYUNDAI.HYUNDAI_AZERA_6TH_GEN,
+ "HYUNDAI AZERA HYBRID 6TH GEN": HYUNDAI.HYUNDAI_AZERA_HEV_6TH_GEN,
+ "HYUNDAI ELANTRA 2017": HYUNDAI.HYUNDAI_ELANTRA,
+ "HYUNDAI I30 N LINE 2019 & GT 2018 DCT": HYUNDAI.HYUNDAI_ELANTRA_GT_I30,
+ "HYUNDAI ELANTRA 2021": HYUNDAI.HYUNDAI_ELANTRA_2021,
+ "HYUNDAI ELANTRA HYBRID 2021": HYUNDAI.HYUNDAI_ELANTRA_HEV_2021,
+ "HYUNDAI GENESIS 2015-2016": HYUNDAI.HYUNDAI_GENESIS,
+ "HYUNDAI IONIQ HYBRID 2017-2019": HYUNDAI.HYUNDAI_IONIQ,
+ "HYUNDAI IONIQ HYBRID 2020-2022": HYUNDAI.HYUNDAI_IONIQ_HEV_2022,
+ "HYUNDAI IONIQ ELECTRIC LIMITED 2019": HYUNDAI.HYUNDAI_IONIQ_EV_LTD,
+ "HYUNDAI IONIQ ELECTRIC 2020": HYUNDAI.HYUNDAI_IONIQ_EV_2020,
+ "HYUNDAI IONIQ PLUG-IN HYBRID 2019": HYUNDAI.HYUNDAI_IONIQ_PHEV_2019,
+ "HYUNDAI IONIQ PHEV 2020": HYUNDAI.HYUNDAI_IONIQ_PHEV,
+ "HYUNDAI KONA 2020": HYUNDAI.HYUNDAI_KONA,
+ "HYUNDAI KONA ELECTRIC 2019": HYUNDAI.HYUNDAI_KONA_EV,
+ "HYUNDAI KONA ELECTRIC 2022": HYUNDAI.HYUNDAI_KONA_EV_2022,
+ "HYUNDAI KONA ELECTRIC 2ND GEN": HYUNDAI.HYUNDAI_KONA_EV_2ND_GEN,
+ "HYUNDAI KONA HYBRID 2020": HYUNDAI.HYUNDAI_KONA_HEV,
+ "HYUNDAI SANTA FE 2019": HYUNDAI.HYUNDAI_SANTA_FE,
+ "HYUNDAI SANTA FE 2022": HYUNDAI.HYUNDAI_SANTA_FE_2022,
+ "HYUNDAI SANTA FE HYBRID 2022": HYUNDAI.HYUNDAI_SANTA_FE_HEV_2022,
+ "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": HYUNDAI.HYUNDAI_SANTA_FE_PHEV_2022,
+ "HYUNDAI SONATA 2020": HYUNDAI.HYUNDAI_SONATA,
+ "HYUNDAI SONATA 2019": HYUNDAI.HYUNDAI_SONATA_LF,
+ "HYUNDAI STARIA 4TH GEN": HYUNDAI.HYUNDAI_STARIA_4TH_GEN,
+ "HYUNDAI TUCSON 2019": HYUNDAI.HYUNDAI_TUCSON,
+ "HYUNDAI PALISADE 2020": HYUNDAI.HYUNDAI_PALISADE,
+ "HYUNDAI VELOSTER 2019": HYUNDAI.HYUNDAI_VELOSTER,
+ "HYUNDAI SONATA HYBRID 2021": HYUNDAI.HYUNDAI_SONATA_HYBRID,
+ "HYUNDAI IONIQ 5 2022": HYUNDAI.HYUNDAI_IONIQ_5,
+ "HYUNDAI IONIQ 6 2023": HYUNDAI.HYUNDAI_IONIQ_6,
+ "HYUNDAI TUCSON 4TH GEN": HYUNDAI.HYUNDAI_TUCSON_4TH_GEN,
+ "HYUNDAI SANTA CRUZ 1ST GEN": HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN,
+ "HYUNDAI CUSTIN 1ST GEN": HYUNDAI.HYUNDAI_CUSTIN_1ST_GEN,
+ "KIA FORTE E 2018 & GT 2021": HYUNDAI.KIA_FORTE,
+ "KIA K5 2021": HYUNDAI.KIA_K5_2021,
+ "KIA K5 HYBRID 2020": HYUNDAI.KIA_K5_HEV_2020,
+ "KIA K8 HYBRID 1ST GEN": HYUNDAI.KIA_K8_HEV_1ST_GEN,
+ "KIA NIRO EV 2020": HYUNDAI.KIA_NIRO_EV,
+ "KIA NIRO EV 2ND GEN": HYUNDAI.KIA_NIRO_EV_2ND_GEN,
+ "KIA NIRO HYBRID 2019": HYUNDAI.KIA_NIRO_PHEV,
+ "KIA NIRO PLUG-IN HYBRID 2022": HYUNDAI.KIA_NIRO_PHEV_2022,
+ "KIA NIRO HYBRID 2021": HYUNDAI.KIA_NIRO_HEV_2021,
+ "KIA NIRO HYBRID 2ND GEN": HYUNDAI.KIA_NIRO_HEV_2ND_GEN,
+ "KIA OPTIMA 4TH GEN": HYUNDAI.KIA_OPTIMA_G4,
+ "KIA OPTIMA 4TH GEN FACELIFT": HYUNDAI.KIA_OPTIMA_G4_FL,
+ "KIA OPTIMA HYBRID 2017 & SPORTS 2019": HYUNDAI.KIA_OPTIMA_H,
+ "KIA OPTIMA HYBRID 4TH GEN FACELIFT": HYUNDAI.KIA_OPTIMA_H_G4_FL,
+ "KIA SELTOS 2021": HYUNDAI.KIA_SELTOS,
+ "KIA SPORTAGE 5TH GEN": HYUNDAI.KIA_SPORTAGE_5TH_GEN,
+ "KIA SORENTO GT LINE 2018": HYUNDAI.KIA_SORENTO,
+ "KIA SORENTO 4TH GEN": HYUNDAI.KIA_SORENTO_4TH_GEN,
+ "KIA SORENTO HYBRID 4TH GEN": HYUNDAI.KIA_SORENTO_HEV_4TH_GEN,
+ "KIA STINGER GT2 2018": HYUNDAI.KIA_STINGER,
+ "KIA STINGER 2022": HYUNDAI.KIA_STINGER_2022,
+ "KIA CEED INTRO ED 2019": HYUNDAI.KIA_CEED,
+ "KIA EV6 2022": HYUNDAI.KIA_EV6,
+ "KIA CARNIVAL 4TH GEN": HYUNDAI.KIA_CARNIVAL_4TH_GEN,
+ "GENESIS GV60 ELECTRIC 1ST GEN": HYUNDAI.GENESIS_GV60_EV_1ST_GEN,
+ "GENESIS G70 2018": HYUNDAI.GENESIS_G70,
+ "GENESIS G70 2020": HYUNDAI.GENESIS_G70_2020,
+ "GENESIS GV70 1ST GEN": HYUNDAI.GENESIS_GV70_1ST_GEN,
+ "GENESIS G80 2017": HYUNDAI.GENESIS_G80,
+ "GENESIS G90 2017": HYUNDAI.GENESIS_G90,
+ "GENESIS GV80 2023": HYUNDAI.GENESIS_GV80,
+ "MAZDA CX-5": MAZDA.MAZDA_CX5,
+ "MAZDA CX-9": MAZDA.MAZDA_CX9,
+ "MAZDA 3": MAZDA.MAZDA_3,
+ "MAZDA 6": MAZDA.MAZDA_6,
+ "MAZDA CX-9 2021": MAZDA.MAZDA_CX9_2021,
+ "MAZDA CX-5 2022": MAZDA.MAZDA_CX5_2022,
+ "NISSAN X-TRAIL 2017": NISSAN.NISSAN_XTRAIL,
+ "NISSAN LEAF 2018": NISSAN.NISSAN_LEAF,
+ "NISSAN LEAF 2018 Instrument Cluster": NISSAN.NISSAN_LEAF_IC,
+ "NISSAN ROGUE 2019": NISSAN.NISSAN_ROGUE,
+ "NISSAN ALTIMA 2020": NISSAN.NISSAN_ALTIMA,
+ "SUBARU ASCENT LIMITED 2019": SUBARU.SUBARU_ASCENT,
+ "SUBARU OUTBACK 6TH GEN": SUBARU.SUBARU_OUTBACK,
+ "SUBARU LEGACY 7TH GEN": SUBARU.SUBARU_LEGACY,
+ "SUBARU IMPREZA LIMITED 2019": SUBARU.SUBARU_IMPREZA,
+ "SUBARU IMPREZA SPORT 2020": SUBARU.SUBARU_IMPREZA_2020,
+ "SUBARU CROSSTREK HYBRID 2020": SUBARU.SUBARU_CROSSTREK_HYBRID,
+ "SUBARU FORESTER 2019": SUBARU.SUBARU_FORESTER,
+ "SUBARU FORESTER HYBRID 2020": SUBARU.SUBARU_FORESTER_HYBRID,
+ "SUBARU FORESTER 2017 - 2018": SUBARU.SUBARU_FORESTER_PREGLOBAL,
+ "SUBARU LEGACY 2015 - 2018": SUBARU.SUBARU_LEGACY_PREGLOBAL,
+ "SUBARU OUTBACK 2015 - 2017": SUBARU.SUBARU_OUTBACK_PREGLOBAL,
+ "SUBARU OUTBACK 2018 - 2019": SUBARU.SUBARU_OUTBACK_PREGLOBAL_2018,
+ "SUBARU FORESTER 2022": SUBARU.SUBARU_FORESTER_2022,
+ "SUBARU OUTBACK 7TH GEN": SUBARU.SUBARU_OUTBACK_2023,
+ "SUBARU ASCENT 2023": SUBARU.SUBARU_ASCENT_2023,
+ "TOYOTA ALPHARD 2020": TOYOTA.TOYOTA_ALPHARD_TSS2,
+ "TOYOTA AVALON 2016": TOYOTA.TOYOTA_AVALON,
+ "TOYOTA AVALON 2019": TOYOTA.TOYOTA_AVALON_2019,
+ "TOYOTA AVALON 2022": TOYOTA.TOYOTA_AVALON_TSS2,
+ "TOYOTA CAMRY 2018": TOYOTA.TOYOTA_CAMRY,
+ "TOYOTA CAMRY 2021": TOYOTA.TOYOTA_CAMRY_TSS2,
+ "TOYOTA C-HR 2018": TOYOTA.TOYOTA_CHR,
+ "TOYOTA C-HR 2021": TOYOTA.TOYOTA_CHR_TSS2,
+ "TOYOTA COROLLA 2017": TOYOTA.TOYOTA_COROLLA,
+ "TOYOTA COROLLA TSS2 2019": TOYOTA.TOYOTA_COROLLA_TSS2,
+ "TOYOTA HIGHLANDER 2017": TOYOTA.TOYOTA_HIGHLANDER,
+ "TOYOTA HIGHLANDER 2020": TOYOTA.TOYOTA_HIGHLANDER_TSS2,
+ "TOYOTA PRIUS 2017": TOYOTA.TOYOTA_PRIUS,
+ "TOYOTA PRIUS v 2017": TOYOTA.TOYOTA_PRIUS_V,
+ "TOYOTA PRIUS TSS2 2021": TOYOTA.TOYOTA_PRIUS_TSS2,
+ "TOYOTA RAV4 2017": TOYOTA.TOYOTA_RAV4,
+ "TOYOTA RAV4 HYBRID 2017": TOYOTA.TOYOTA_RAV4H,
+ "TOYOTA RAV4 2019": TOYOTA.TOYOTA_RAV4_TSS2,
+ "TOYOTA RAV4 2022": TOYOTA.TOYOTA_RAV4_TSS2_2022,
+ "TOYOTA RAV4 2023": TOYOTA.TOYOTA_RAV4_TSS2_2023,
+ "TOYOTA MIRAI 2021": TOYOTA.TOYOTA_MIRAI,
+ "TOYOTA SIENNA 2018": TOYOTA.TOYOTA_SIENNA,
+ "LEXUS CT HYBRID 2018": TOYOTA.LEXUS_CTH,
+ "LEXUS ES 2018": TOYOTA.LEXUS_ES,
+ "LEXUS ES 2019": TOYOTA.LEXUS_ES_TSS2,
+ "LEXUS IS 2018": TOYOTA.LEXUS_IS,
+ "LEXUS IS 2023": TOYOTA.LEXUS_IS_TSS2,
+ "LEXUS NX 2018": TOYOTA.LEXUS_NX,
+ "LEXUS NX 2020": TOYOTA.LEXUS_NX_TSS2,
+ "LEXUS LC 2024": TOYOTA.LEXUS_LC_TSS2,
+ "LEXUS RC 2020": TOYOTA.LEXUS_RC,
+ "LEXUS RX 2016": TOYOTA.LEXUS_RX,
+ "LEXUS RX 2020": TOYOTA.LEXUS_RX_TSS2,
+ "LEXUS GS F 2016": TOYOTA.LEXUS_GS_F,
+ "VOLKSWAGEN ARTEON 1ST GEN": VW.VOLKSWAGEN_ARTEON_MK1,
+ "VOLKSWAGEN ATLAS 1ST GEN": VW.VOLKSWAGEN_ATLAS_MK1,
+ "VOLKSWAGEN CADDY 3RD GEN": VW.VOLKSWAGEN_CADDY_MK3,
+ "VOLKSWAGEN CRAFTER 2ND GEN": VW.VOLKSWAGEN_CRAFTER_MK2,
+ "VOLKSWAGEN GOLF 7TH GEN": VW.VOLKSWAGEN_GOLF_MK7,
+ "VOLKSWAGEN JETTA 7TH GEN": VW.VOLKSWAGEN_JETTA_MK7,
+ "VOLKSWAGEN PASSAT 8TH GEN": VW.VOLKSWAGEN_PASSAT_MK8,
+ "VOLKSWAGEN PASSAT NMS": VW.VOLKSWAGEN_PASSAT_NMS,
+ "VOLKSWAGEN POLO 6TH GEN": VW.VOLKSWAGEN_POLO_MK6,
+ "VOLKSWAGEN SHARAN 2ND GEN": VW.VOLKSWAGEN_SHARAN_MK2,
+ "VOLKSWAGEN TAOS 1ST GEN": VW.VOLKSWAGEN_TAOS_MK1,
+ "VOLKSWAGEN T-CROSS 1ST GEN": VW.VOLKSWAGEN_TCROSS_MK1,
+ "VOLKSWAGEN TIGUAN 2ND GEN": VW.VOLKSWAGEN_TIGUAN_MK2,
+ "VOLKSWAGEN TOURAN 2ND GEN": VW.VOLKSWAGEN_TOURAN_MK2,
+ "VOLKSWAGEN TRANSPORTER T6.1": VW.VOLKSWAGEN_TRANSPORTER_T61,
+ "VOLKSWAGEN T-ROC 1ST GEN": VW.VOLKSWAGEN_TROC_MK1,
+ "AUDI A3 3RD GEN": VW.AUDI_A3_MK3,
+ "AUDI Q2 1ST GEN": VW.AUDI_Q2_MK1,
+ "AUDI Q3 2ND GEN": VW.AUDI_Q3_MK2,
+ "SEAT ATECA 1ST GEN": VW.SEAT_ATECA_MK1,
+ "SEAT LEON 3RD GEN": VW.SEAT_ATECA_MK1,
+ "SEAT_LEON_MK3": VW.SEAT_ATECA_MK1,
+ "SKODA FABIA 4TH GEN": VW.SKODA_FABIA_MK4,
+ "SKODA KAMIQ 1ST GEN": VW.SKODA_KAMIQ_MK1,
+ "SKODA KAROQ 1ST GEN": VW.SKODA_KAROQ_MK1,
+ "SKODA KODIAQ 1ST GEN": VW.SKODA_KODIAQ_MK1,
+ "SKODA OCTAVIA 3RD GEN": VW.SKODA_OCTAVIA_MK3,
+ "SKODA SCALA 1ST GEN": VW.SKODA_KAMIQ_MK1,
+ "SKODA_SCALA_MK1": VW.SKODA_KAMIQ_MK1,
+ "SKODA SUPERB 3RD GEN": VW.SKODA_SUPERB_MK3,
+
+ "mock": MOCK.MOCK,
+}
diff --git a/opendbc/car/ford/__init__.py b/opendbc/car/ford/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/ford/carcontroller.py b/opendbc/car/ford/carcontroller.py
new file mode 100644
index 0000000000..2556d44ad6
--- /dev/null
+++ b/opendbc/car/ford/carcontroller.py
@@ -0,0 +1,118 @@
+import copy
+from opendbc.can.packer import CANPacker
+from opendbc.car import apply_std_steer_angle_limits, structs
+from opendbc.car.ford import fordcan
+from opendbc.car.ford.values import CarControllerParams, FordFlags
+from opendbc.car.common.numpy_fast import clip
+from opendbc.car.interfaces import CarControllerBase, V_CRUISE_MAX
+
+LongCtrlState = structs.CarControl.Actuators.LongControlState
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+
+
+def apply_ford_curvature_limits(apply_curvature, apply_curvature_last, current_curvature, v_ego_raw):
+ # No blending at low speed due to lack of torque wind-up and inaccurate current curvature
+ if v_ego_raw > 9:
+ apply_curvature = clip(apply_curvature, current_curvature - CarControllerParams.CURVATURE_ERROR,
+ current_curvature + CarControllerParams.CURVATURE_ERROR)
+
+ # Curvature rate limit after driver torque limit
+ apply_curvature = apply_std_steer_angle_limits(apply_curvature, apply_curvature_last, v_ego_raw, CarControllerParams)
+
+ return clip(apply_curvature, -CarControllerParams.CURVATURE_MAX, CarControllerParams.CURVATURE_MAX)
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.packer = CANPacker(dbc_name)
+ self.CAN = fordcan.CanBus(CP)
+
+ self.apply_curvature_last = 0
+ self.main_on_last = False
+ self.lkas_enabled_last = False
+ self.steer_alert_last = False
+ self.lead_distance_bars_last = None
+
+ def update(self, CC, CS, now_nanos):
+ can_sends = []
+
+ actuators = CC.actuators
+ hud_control = CC.hudControl
+
+ main_on = CS.out.cruiseState.available
+ steer_alert = hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw)
+ fcw_alert = hud_control.visualAlert == VisualAlert.fcw
+
+ ### acc buttons ###
+ if CC.cruiseControl.cancel:
+ can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, cancel=True))
+ can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.main, CS.buttons_stock_values, cancel=True))
+ elif CC.cruiseControl.resume and (self.frame % CarControllerParams.BUTTONS_STEP) == 0:
+ can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, resume=True))
+ can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.main, CS.buttons_stock_values, resume=True))
+ # if stock lane centering isn't off, send a button press to toggle it off
+ # the stock system checks for steering pressed, and eventually disengages cruise control
+ elif CS.acc_tja_status_stock_values["Tja_D_Stat"] != 0 and (self.frame % CarControllerParams.ACC_UI_STEP) == 0:
+ can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, tja_toggle=True))
+
+ ### lateral control ###
+ # send steer msg at 20Hz
+ if (self.frame % CarControllerParams.STEER_STEP) == 0:
+ if CC.latActive:
+ # apply rate limits, curvature error limit, and clip to signal range
+ current_curvature = -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
+ apply_curvature = apply_ford_curvature_limits(actuators.curvature, self.apply_curvature_last, current_curvature, CS.out.vEgoRaw)
+ else:
+ apply_curvature = 0.
+
+ self.apply_curvature_last = apply_curvature
+
+ if self.CP.flags & FordFlags.CANFD:
+ # TODO: extended mode
+ mode = 1 if CC.latActive else 0
+ counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
+ can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -apply_curvature, 0., counter))
+ else:
+ can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, CC.latActive, 0., 0., -apply_curvature, 0.))
+
+ # send lka msg at 33Hz
+ if (self.frame % CarControllerParams.LKA_STEP) == 0:
+ can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
+
+ ### longitudinal control ###
+ # send acc msg at 50Hz
+ if self.CP.openpilotLongitudinalControl and (self.frame % CarControllerParams.ACC_CONTROL_STEP) == 0:
+ # Both gas and accel are in m/s^2, accel is used solely for braking
+ accel = clip(actuators.accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX)
+ gas = accel
+ if not CC.longActive or gas < CarControllerParams.MIN_GAS:
+ gas = CarControllerParams.INACTIVE_GAS
+ stopping = CC.actuators.longControlState == LongCtrlState.stopping
+ # TODO: look into using the actuators packet to send the desired speed
+ can_sends.append(fordcan.create_acc_msg(self.packer, self.CAN, CC.longActive, gas, accel, stopping, v_ego_kph=V_CRUISE_MAX))
+
+ ### ui ###
+ send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != CC.latActive) or (self.steer_alert_last != steer_alert)
+ # send lkas ui msg at 1Hz or if ui state changes
+ if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
+ can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values))
+
+ # send acc ui msg at 5Hz or if ui state changes
+ if hud_control.leadDistanceBars != self.lead_distance_bars_last:
+ send_ui = True
+ if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
+ can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, CC.latActive,
+ fcw_alert, CS.out.cruiseState.standstill, hud_control,
+ CS.acc_tja_status_stock_values))
+
+ self.main_on_last = main_on
+ self.lkas_enabled_last = CC.latActive
+ self.steer_alert_last = steer_alert
+ self.lead_distance_bars_last = hud_control.leadDistanceBars
+
+ new_actuators = copy.copy(actuators)
+ new_actuators.curvature = self.apply_curvature_last
+
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/ford/carstate.py b/opendbc/car/ford/carstate.py
new file mode 100644
index 0000000000..8b631e8dee
--- /dev/null
+++ b/opendbc/car/ford/carstate.py
@@ -0,0 +1,174 @@
+from opendbc.can.can_define import CANDefine
+from opendbc.can.parser import CANParser
+from opendbc.car import create_button_events, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.ford.fordcan import CanBus
+from opendbc.car.ford.values import DBC, CarControllerParams, FordFlags
+from opendbc.car.interfaces import CarStateBase
+
+ButtonType = structs.CarState.ButtonEvent.Type
+GearShifter = structs.CarState.GearShifter
+TransmissionType = structs.CarParams.TransmissionType
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+ if CP.transmissionType == TransmissionType.automatic:
+ self.shifter_values = can_define.dv["PowertrainData_10"]["TrnRng_D_Rq"]
+
+ self.distance_button = 0
+
+ def update(self, cp, cp_cam, *_) -> structs.CarState:
+ ret = structs.CarState()
+
+ # Occasionally on startup, the ABS module recalibrates the steering pinion offset, so we need to block engagement
+ # The vehicle usually recovers out of this state within a minute of normal driving
+ ret.vehicleSensorsInvalid = cp.vl["SteeringPinion_Data"]["StePinCompAnEst_D_Qf"] != 3
+
+ # car speed
+ ret.vEgoRaw = cp.vl["BrakeSysFeatures"]["Veh_V_ActlBrk"] * CV.KPH_TO_MS
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.yawRate = cp.vl["Yaw_Data_FD1"]["VehYaw_W_Actl"]
+ ret.standstill = cp.vl["DesiredTorqBrk"]["VehStop_D_Stat"] == 1
+
+ # gas pedal
+ ret.gas = cp.vl["EngVehicleSpThrottle"]["ApedPos_Pc_ActlArb"] / 100.
+ ret.gasPressed = ret.gas > 1e-6
+
+ # brake pedal
+ ret.brake = cp.vl["BrakeSnData_4"]["BrkTot_Tq_Actl"] / 32756. # torque in Nm
+ ret.brakePressed = cp.vl["EngBrakeData"]["BpedDrvAppl_D_Actl"] == 2
+ ret.parkingBrake = cp.vl["DesiredTorqBrk"]["PrkBrkStatus"] in (1, 2)
+
+ # steering wheel
+ ret.steeringAngleDeg = cp.vl["SteeringPinion_Data"]["StePinComp_An_Est"]
+ ret.steeringTorque = cp.vl["EPAS_INFO"]["SteeringColumnTorque"]
+ ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CarControllerParams.STEER_DRIVER_ALLOWANCE, 5)
+ ret.steerFaultTemporary = cp.vl["EPAS_INFO"]["EPAS_Failure"] == 1
+ ret.steerFaultPermanent = cp.vl["EPAS_INFO"]["EPAS_Failure"] in (2, 3)
+ ret.espDisabled = cp.vl["Cluster_Info1_FD1"]["DrvSlipCtlMde_D_Rq"] != 0 # 0 is default mode
+
+ if self.CP.flags & FordFlags.CANFD:
+ # this signal is always 0 on non-CAN FD cars
+ ret.steerFaultTemporary |= cp.vl["Lane_Assist_Data3_FD1"]["LatCtlSte_D_Stat"] not in (1, 2, 3)
+
+ # cruise state
+ is_metric = cp.vl["INSTRUMENT_PANEL"]["METRIC_UNITS"] == 1 if not self.CP.flags & FordFlags.CANFD else False
+ ret.cruiseState.speed = cp.vl["EngBrakeData"]["Veh_V_DsplyCcSet"] * (CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS)
+ ret.cruiseState.enabled = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (4, 5)
+ ret.cruiseState.available = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (3, 4, 5)
+ ret.cruiseState.nonAdaptive = cp.vl["Cluster_Info1_FD1"]["AccEnbl_B_RqDrv"] == 0
+ ret.cruiseState.standstill = cp.vl["EngBrakeData"]["AccStopMde_D_Rq"] == 3
+ ret.accFaulted = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (1, 2)
+ if not self.CP.openpilotLongitudinalControl:
+ ret.accFaulted = ret.accFaulted or cp_cam.vl["ACCDATA"]["CmbbDeny_B_Actl"] == 1
+
+ # gear
+ if self.CP.transmissionType == TransmissionType.automatic:
+ gear = self.shifter_values.get(cp.vl["PowertrainData_10"]["TrnRng_D_Rq"])
+ ret.gearShifter = self.parse_gear_shifter(gear)
+ elif self.CP.transmissionType == TransmissionType.manual:
+ ret.clutchPressed = cp.vl["Engine_Clutch_Data"]["CluPdlPos_Pc_Meas"] > 0
+ if bool(cp.vl["BCM_Lamp_Stat_FD1"]["RvrseLghtOn_B_Stat"]):
+ ret.gearShifter = GearShifter.reverse
+ else:
+ ret.gearShifter = GearShifter.drive
+
+ # safety
+ ret.stockFcw = bool(cp_cam.vl["ACCDATA_3"]["FcwVisblWarn_B_Rq"])
+ ret.stockAeb = bool(cp_cam.vl["ACCDATA_2"]["CmbbBrkDecel_B_Rq"])
+
+ # button presses
+ ret.leftBlinker = cp.vl["Steering_Data_FD1"]["TurnLghtSwtch_D_Stat"] == 1
+ ret.rightBlinker = cp.vl["Steering_Data_FD1"]["TurnLghtSwtch_D_Stat"] == 2
+ # TODO: block this going to the camera otherwise it will enable stock TJA
+ ret.genericToggle = bool(cp.vl["Steering_Data_FD1"]["TjaButtnOnOffPress"])
+ prev_distance_button = self.distance_button
+ self.distance_button = cp.vl["Steering_Data_FD1"]["AccButtnGapTogglePress"]
+
+ # lock info
+ ret.doorOpen = any([cp.vl["BodyInfo_3_FD1"]["DrStatDrv_B_Actl"], cp.vl["BodyInfo_3_FD1"]["DrStatPsngr_B_Actl"],
+ cp.vl["BodyInfo_3_FD1"]["DrStatRl_B_Actl"], cp.vl["BodyInfo_3_FD1"]["DrStatRr_B_Actl"]])
+ ret.seatbeltUnlatched = cp.vl["RCMStatusMessage2_FD1"]["FirstRowBuckleDriver"] == 2
+
+ # blindspot sensors
+ if self.CP.enableBsm:
+ cp_bsm = cp_cam if self.CP.flags & FordFlags.CANFD else cp
+ ret.leftBlindspot = cp_bsm.vl["Side_Detect_L_Stat"]["SodDetctLeft_D_Stat"] != 0
+ ret.rightBlindspot = cp_bsm.vl["Side_Detect_R_Stat"]["SodDetctRight_D_Stat"] != 0
+
+ # Stock steering buttons so that we can passthru blinkers etc.
+ self.buttons_stock_values = cp.vl["Steering_Data_FD1"]
+ # Stock values from IPMA so that we can retain some stock functionality
+ self.acc_tja_status_stock_values = cp_cam.vl["ACCDATA_3"]
+ self.lkas_status_stock_values = cp_cam.vl["IPMA_Data"]
+
+ ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
+
+ return ret
+
+ @staticmethod
+ def get_can_parser(CP):
+ messages = [
+ # sig_address, frequency
+ ("VehicleOperatingModes", 100),
+ ("BrakeSysFeatures", 50),
+ ("Yaw_Data_FD1", 100),
+ ("DesiredTorqBrk", 50),
+ ("EngVehicleSpThrottle", 100),
+ ("BrakeSnData_4", 50),
+ ("EngBrakeData", 10),
+ ("Cluster_Info1_FD1", 10),
+ ("SteeringPinion_Data", 100),
+ ("EPAS_INFO", 50),
+ ("Steering_Data_FD1", 10),
+ ("BodyInfo_3_FD1", 2),
+ ("RCMStatusMessage2_FD1", 10),
+ ]
+
+ if CP.flags & FordFlags.CANFD:
+ messages += [
+ ("Lane_Assist_Data3_FD1", 33),
+ ]
+ else:
+ messages += [
+ ("INSTRUMENT_PANEL", 1),
+ ]
+
+ if CP.transmissionType == TransmissionType.automatic:
+ messages += [
+ ("PowertrainData_10", 10),
+ ]
+ elif CP.transmissionType == TransmissionType.manual:
+ messages += [
+ ("Engine_Clutch_Data", 33),
+ ("BCM_Lamp_Stat_FD1", 1),
+ ]
+
+ if CP.enableBsm and not (CP.flags & FordFlags.CANFD):
+ messages += [
+ ("Side_Detect_L_Stat", 5),
+ ("Side_Detect_R_Stat", 5),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus(CP).main)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ messages = [
+ # sig_address, frequency
+ ("ACCDATA", 50),
+ ("ACCDATA_2", 50),
+ ("ACCDATA_3", 5),
+ ("IPMA_Data", 1),
+ ]
+
+ if CP.enableBsm and CP.flags & FordFlags.CANFD:
+ messages += [
+ ("Side_Detect_L_Stat", 5),
+ ("Side_Detect_R_Stat", 5),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus(CP).camera)
diff --git a/opendbc/car/ford/fingerprints.py b/opendbc/car/ford/fingerprints.py
new file mode 100644
index 0000000000..c14c836669
--- /dev/null
+++ b/opendbc/car/ford/fingerprints.py
@@ -0,0 +1,166 @@
+from opendbc.car.structs import CarParams
+from opendbc.car.ford.values import CAR
+
+Ecu = CarParams.Ecu
+
+FW_VERSIONS = {
+ CAR.FORD_BRONCO_SPORT_MK1: {
+ (Ecu.eps, 0x730, None): [
+ b'LX6C-14D003-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-14D003-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'LX6C-2D053-RD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-2D053-RE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-2D053-RF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'LB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'M1PT-14F397-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'M1PT-14F397-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.FORD_ESCAPE_MK4: {
+ (Ecu.eps, 0x730, None): [
+ b'LX6C-14D003-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-14D003-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-14D003-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'LX6C-2D053-NS\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-2D053-NT\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-2D053-NY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-2D053-SA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LX6C-2D053-SD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'LB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'LJ6T-14F397-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LJ6T-14F397-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LV4T-14F397-GG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.FORD_EXPLORER_MK6: {
+ (Ecu.eps, 0x730, None): [
+ b'L1MC-14D003-AJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'L1MC-14D003-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'L1MC-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'M1MC-14D003-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'M1MC-14D003-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'P1MC-14D003-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'L1MC-2D053-AJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'L1MC-2D053-BA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'L1MC-2D053-BB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'L1MC-2D053-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'L1MC-2D053-BF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'L1MC-2D053-BJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'L1MC-2D053-KB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'LB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'LB5T-14F397-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LB5T-14F397-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LB5T-14F397-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LC5T-14F397-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LC5T-14F397-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.FORD_F_150_MK14: {
+ (Ecu.eps, 0x730, None): [
+ b'ML3V-14D003-BC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'PL34-2D053-CA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'ML3T-14H102-ABR\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.FORD_F_150_LIGHTNING_MK1: {
+ (Ecu.abs, 0x760, None): [
+ b'PL38-2D053-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'ML3T-14H102-ABT\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.FORD_MUSTANG_MACH_E_MK1: {
+ (Ecu.eps, 0x730, None): [
+ b'LJ9C-14D003-AM\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'LJ9C-14D003-CC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'LK9C-2D053-CK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'ML3T-14H102-ABS\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.FORD_FOCUS_MK4: {
+ (Ecu.eps, 0x730, None): [
+ b'JX6C-14D003-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'JX61-2D053-CJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'JX7T-14D049-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'JX7T-14F397-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.FORD_MAVERICK_MK1: {
+ (Ecu.eps, 0x730, None): [
+ b'NZ6C-14D003-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'NZ6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'NZ6C-2D053-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'NZ6C-2D053-AG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PZ6C-2D053-ED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PZ6C-2D053-EE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PZ6C-2D053-EF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'NZ6T-14D049-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'NZ6T-14F397-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.FORD_RANGER_MK2: {
+ (Ecu.eps, 0x730, None): [
+ b'NL14-14D003-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'PB3C-2D053-ZD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+}
diff --git a/opendbc/car/ford/fordcan.py b/opendbc/car/ford/fordcan.py
new file mode 100644
index 0000000000..61263099c7
--- /dev/null
+++ b/opendbc/car/ford/fordcan.py
@@ -0,0 +1,339 @@
+from opendbc.car import CanBusBase, structs
+
+HUDControl = structs.CarControl.HUDControl
+
+
+class CanBus(CanBusBase):
+ def __init__(self, CP=None, fingerprint=None) -> None:
+ super().__init__(CP, fingerprint)
+
+ @property
+ def main(self) -> int:
+ return self.offset
+
+ @property
+ def radar(self) -> int:
+ return self.offset + 1
+
+ @property
+ def camera(self) -> int:
+ return self.offset + 2
+
+
+def calculate_lat_ctl2_checksum(mode: int, counter: int, dat: bytearray) -> int:
+ curvature = (dat[2] << 3) | ((dat[3]) >> 5)
+ curvature_rate = (dat[6] << 3) | ((dat[7]) >> 5)
+ path_angle = ((dat[3] & 0x1F) << 6) | ((dat[4]) >> 2)
+ path_offset = ((dat[4] & 0x3) << 8) | dat[5]
+
+ checksum = mode + counter
+ for sig_val in (curvature, curvature_rate, path_angle, path_offset):
+ checksum += sig_val + (sig_val >> 8)
+
+ return 0xFF - (checksum & 0xFF)
+
+
+def create_lka_msg(packer, CAN: CanBus):
+ """
+ Creates an empty CAN message for the Ford LKA Command.
+
+ This command can apply "Lane Keeping Aid" maneuvers, which are subject to the PSCM lockout.
+
+ Frequency is 33Hz.
+ """
+
+ return packer.make_can_msg("Lane_Assist_Data1", CAN.main, {})
+
+
+def create_lat_ctl_msg(packer, CAN: CanBus, lat_active: bool, path_offset: float, path_angle: float, curvature: float,
+ curvature_rate: float):
+ """
+ Creates a CAN message for the Ford TJA/LCA Command.
+
+ This command can apply "Lane Centering" maneuvers: continuous lane centering for traffic jam assist and highway
+ driving. It is not subject to the PSCM lockout.
+
+ Ford lane centering command uses a third order polynomial to describe the road centerline. The polynomial is defined
+ by the following coefficients:
+ c0: lateral offset between the vehicle and the centerline (positive is right)
+ c1: heading angle between the vehicle and the centerline (positive is right)
+ c2: curvature of the centerline (positive is left)
+ c3: rate of change of curvature of the centerline
+ As the PSCM combines this information with other sensor data, such as the vehicle's yaw rate and speed, the steering
+ angle cannot be easily controlled.
+
+ The PSCM should be configured to accept TJA/LCA commands before these commands will be processed. This can be done
+ using tools such as Forscan.
+
+ Frequency is 20Hz.
+ """
+
+ values = {
+ "LatCtlRng_L_Max": 0, # Unknown [0|126] meter
+ "HandsOffCnfm_B_Rq": 0, # Unknown: 0=Inactive, 1=Active [0|1]
+ "LatCtl_D_Rq": 1 if lat_active else 0, # Mode: 0=None, 1=ContinuousPathFollowing, 2=InterventionLeft,
+ # 3=InterventionRight, 4-7=NotUsed [0|7]
+ "LatCtlRampType_D_Rq": 0, # Ramp speed: 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
+ # Makes no difference with curvature control
+ "LatCtlPrecision_D_Rq": 1, # Precision: 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
+ # The stock system always uses comfortable
+ "LatCtlPathOffst_L_Actl": path_offset, # Path offset [-5.12|5.11] meter
+ "LatCtlPath_An_Actl": path_angle, # Path angle [-0.5|0.5235] radians
+ "LatCtlCurv_NoRate_Actl": curvature_rate, # Curvature rate [-0.001024|0.00102375] 1/meter^2
+ "LatCtlCurv_No_Actl": curvature, # Curvature [-0.02|0.02094] 1/meter
+ }
+ return packer.make_can_msg("LateralMotionControl", CAN.main, values)
+
+
+def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, path_offset: float, path_angle: float, curvature: float,
+ curvature_rate: float, counter: int):
+ """
+ Create a CAN message for the new Ford Lane Centering command.
+
+ This message is used on the CAN FD platform and replaces the old LateralMotionControl message. It is similar but has
+ additional signals for a counter and checksum.
+
+ Frequency is 20Hz.
+ """
+
+ values = {
+ "LatCtl_D2_Rq": mode, # Mode: 0=None, 1=PathFollowingLimitedMode, 2=PathFollowingExtendedMode,
+ # 3=SafeRampOut, 4-7=NotUsed [0|7]
+ "LatCtlRampType_D_Rq": 0, # 0=Slow, 1=Medium, 2=Fast, 3=Immediate [0|3]
+ "LatCtlPrecision_D_Rq": 1, # 0=Comfortable, 1=Precise, 2/3=NotUsed [0|3]
+ "LatCtlPathOffst_L_Actl": path_offset, # [-5.12|5.11] meter
+ "LatCtlPath_An_Actl": path_angle, # [-0.5|0.5235] radians
+ "LatCtlCurv_No_Actl": curvature, # [-0.02|0.02094] 1/meter
+ "LatCtlCrv_NoRate2_Actl": curvature_rate, # [-0.001024|0.001023] 1/meter^2
+ "HandsOffCnfm_B_Rq": 0, # 0=Inactive, 1=Active [0|1]
+ "LatCtlPath_No_Cnt": counter, # [0|15]
+ "LatCtlPath_No_Cs": 0, # [0|255]
+ }
+
+ # calculate checksum
+ dat = packer.make_can_msg("LateralMotionControl2", 0, values)[1]
+ values["LatCtlPath_No_Cs"] = calculate_lat_ctl2_checksum(mode, counter, dat)
+
+ return packer.make_can_msg("LateralMotionControl2", CAN.main, values)
+
+
+def create_acc_msg(packer, CAN: CanBus, long_active: bool, gas: float, accel: float, stopping: bool, v_ego_kph: float):
+ """
+ Creates a CAN message for the Ford ACC Command.
+
+ This command can be used to enable ACC, to set the ACC gas/brake/decel values
+ and to disable ACC.
+
+ Frequency is 50Hz.
+ """
+ decel = accel < 0 and long_active
+ values = {
+ "AccBrkTot_A_Rq": accel, # Brake total accel request: [-20|11.9449] m/s^2
+ "Cmbb_B_Enbl": 1 if long_active else 0, # Enabled: 0=No, 1=Yes
+ "AccPrpl_A_Rq": gas, # Acceleration request: [-5|5.23] m/s^2
+ "AccPrpl_A_Pred": -5.0, # Acceleration request: [-5|5.23] m/s^2
+ "AccResumEnbl_B_Rq": 1 if long_active else 0,
+ "AccVeh_V_Trg": v_ego_kph, # Target speed: [0|255] km/h
+ # TODO: we may be able to improve braking response by utilizing pre-charging better
+ "AccBrkPrchg_B_Rq": 1 if decel else 0, # Pre-charge brake request: 0=No, 1=Yes
+ "AccBrkDecel_B_Rq": 1 if decel else 0, # Deceleration request: 0=Inactive, 1=Active
+ "AccStopStat_B_Rq": 1 if stopping else 0,
+ }
+ return packer.make_can_msg("ACCDATA", CAN.main, values)
+
+
+def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw_alert: bool, standstill: bool,
+ hud_control, stock_values: dict):
+ """
+ Creates a CAN message for the Ford IPC adaptive cruise, forward collision warning and traffic jam
+ assist status.
+
+ Stock functionality is maintained by passing through unmodified signals.
+
+ Frequency is 5Hz.
+ """
+
+ # Tja_D_Stat
+ if enabled:
+ if hud_control.leftLaneDepart:
+ status = 3 # ActiveInterventionLeft
+ elif hud_control.rightLaneDepart:
+ status = 4 # ActiveInterventionRight
+ else:
+ status = 2 # Active
+ elif main_on:
+ if hud_control.leftLaneDepart:
+ status = 5 # ActiveWarningLeft
+ elif hud_control.rightLaneDepart:
+ status = 6 # ActiveWarningRight
+ else:
+ status = 1 # Standby
+ else:
+ status = 0 # Off
+
+ values = {s: stock_values[s] for s in [
+ "HaDsply_No_Cs",
+ "HaDsply_No_Cnt",
+ "AccStopStat_D_Dsply", # ACC stopped status message
+ "AccTrgDist2_D_Dsply", # ACC target distance
+ "AccStopRes_B_Dsply",
+ "TjaWarn_D_Rq", # TJA warning
+ "TjaMsgTxt_D_Dsply", # TJA text
+ "IaccLamp_D_Rq", # iACC status icon
+ "AccMsgTxt_D2_Rq", # ACC text
+ "FcwDeny_B_Dsply", # FCW disabled
+ "FcwMemStat_B_Actl", # FCW enabled setting
+ "AccTGap_B_Dsply", # ACC time gap display setting
+ "CadsAlignIncplt_B_Actl",
+ "AccFllwMde_B_Dsply", # ACC follow mode display setting
+ "CadsRadrBlck_B_Actl",
+ "CmbbPostEvnt_B_Dsply", # AEB event status
+ "AccStopMde_B_Dsply", # ACC stop mode display setting
+ "FcwMemSens_D_Actl", # FCW sensitivity setting
+ "FcwMsgTxt_D_Rq", # FCW text
+ "AccWarn_D_Dsply", # ACC warning
+ "FcwVisblWarn_B_Rq", # FCW visible alert
+ "FcwAudioWarn_B_Rq", # FCW audio alert
+ "AccTGap_D_Dsply", # ACC time gap
+ "AccMemEnbl_B_RqDrv", # ACC adaptive/normal setting
+ "FdaMem_B_Stat", # FDA enabled setting
+ ]}
+
+ values.update({
+ "Tja_D_Stat": status, # TJA status
+ })
+
+ if CP.openpilotLongitudinalControl:
+ values.update({
+ "AccStopStat_D_Dsply": 2 if standstill else 0, # Stopping status text
+ "AccMsgTxt_D2_Rq": 0, # ACC text
+ "AccTGap_B_Dsply": 0, # Show time gap control UI
+ "AccFllwMde_B_Dsply": 1 if hud_control.leadVisible else 0, # Lead indicator
+ "AccStopMde_B_Dsply": 1 if standstill else 0,
+ "AccWarn_D_Dsply": 0, # ACC warning
+ "AccTGap_D_Dsply": hud_control.leadDistanceBars, # Time gap
+ })
+
+ # Forwards FCW alert from IPMA
+ if fcw_alert:
+ values["FcwVisblWarn_B_Rq"] = 1 # FCW visible alert
+
+ return packer.make_can_msg("ACCDATA_3", CAN.main, values)
+
+
+def create_lkas_ui_msg(packer, CAN: CanBus, main_on: bool, enabled: bool, steer_alert: bool, hud_control,
+ stock_values: dict):
+ """
+ Creates a CAN message for the Ford IPC IPMA/LKAS status.
+
+ Show the LKAS status with the "driver assist" lines in the IPC.
+
+ Stock functionality is maintained by passing through unmodified signals.
+
+ Frequency is 1Hz.
+ """
+
+ # LaActvStats_D_Dsply
+ # R Intvn Warn Supprs Avail No
+ # L
+ # Intvn 24 19 14 9 4
+ # Warn 23 18 13 8 3
+ # Supprs 22 17 12 7 2
+ # Avail 21 16 11 6 1
+ # No 20 15 10 5 0
+ #
+ # TODO: test suppress state
+ if enabled:
+ lines = 0 # NoLeft_NoRight
+ if hud_control.leftLaneDepart:
+ lines += 4
+ elif hud_control.leftLaneVisible:
+ lines += 1
+ if hud_control.rightLaneDepart:
+ lines += 20
+ elif hud_control.rightLaneVisible:
+ lines += 5
+ elif main_on:
+ lines = 0
+ else:
+ if hud_control.leftLaneDepart:
+ lines = 3 # WarnLeft_NoRight
+ elif hud_control.rightLaneDepart:
+ lines = 15 # NoLeft_WarnRight
+ else:
+ lines = 30 # LA_Off
+
+ hands_on_wheel_dsply = 1 if steer_alert else 0
+
+ values = {s: stock_values[s] for s in [
+ "FeatConfigIpmaActl",
+ "FeatNoIpmaActl",
+ "PersIndexIpma_D_Actl",
+ "AhbcRampingV_D_Rq", # AHB ramping
+ "LaDenyStats_B_Dsply", # LKAS error
+ "CamraDefog_B_Req", # Windshield heater?
+ "CamraStats_D_Dsply", # Camera status
+ "DasAlrtLvl_D_Dsply", # DAS alert level
+ "DasStats_D_Dsply", # DAS status
+ "DasWarn_D_Dsply", # DAS warning
+ "AhbHiBeam_D_Rq", # AHB status
+ "Passthru_63",
+ "Passthru_48",
+ ]}
+
+ values.update({
+ "LaActvStats_D_Dsply": lines, # LKAS status (lines) [0|31]
+ "LaHandsOff_D_Dsply": hands_on_wheel_dsply, # 0=HandsOn, 1=Level1 (w/o chime), 2=Level2 (w/ chime), 3=Suppressed
+ })
+ return packer.make_can_msg("IPMA_Data", CAN.main, values)
+
+
+def create_button_msg(packer, bus: int, stock_values: dict, cancel=False, resume=False, tja_toggle=False):
+ """
+ Creates a CAN message for the Ford SCCM buttons/switches.
+
+ Includes cruise control buttons, turn lights and more.
+
+ Frequency is 10Hz.
+ """
+
+ values = {s: stock_values[s] for s in [
+ "HeadLghtHiFlash_D_Stat", # SCCM Passthrough the remaining buttons
+ "TurnLghtSwtch_D_Stat", # SCCM Turn signal switch
+ "WiprFront_D_Stat",
+ "LghtAmb_D_Sns",
+ "AccButtnGapDecPress",
+ "AccButtnGapIncPress",
+ "AslButtnOnOffCnclPress",
+ "AslButtnOnOffPress",
+ "LaSwtchPos_D_Stat",
+ "CcAslButtnCnclResPress",
+ "CcAslButtnDeny_B_Actl",
+ "CcAslButtnIndxDecPress",
+ "CcAslButtnIndxIncPress",
+ "CcAslButtnOffCnclPress",
+ "CcAslButtnOnOffCncl",
+ "CcAslButtnOnPress",
+ "CcAslButtnResDecPress",
+ "CcAslButtnResIncPress",
+ "CcAslButtnSetDecPress",
+ "CcAslButtnSetIncPress",
+ "CcAslButtnSetPress",
+ "CcButtnOffPress",
+ "CcButtnOnOffCnclPress",
+ "CcButtnOnOffPress",
+ "CcButtnOnPress",
+ "HeadLghtHiFlash_D_Actl",
+ "HeadLghtHiOn_B_StatAhb",
+ "AhbStat_B_Dsply",
+ "AccButtnGapTogglePress",
+ "WiprFrontSwtch_D_Stat",
+ "HeadLghtHiCtrl_D_RqAhb",
+ ]}
+
+ values.update({
+ "CcAslButtnCnclPress": 1 if cancel else 0, # CC cancel button
+ "CcAsllButtnResPress": 1 if resume else 0, # CC resume button
+ "TjaButtnOnOffPress": 1 if tja_toggle else 0, # LCA/TJA toggle button
+ })
+ return packer.make_can_msg("Steering_Data_FD1", bus, values)
diff --git a/opendbc/car/ford/interface.py b/opendbc/car/ford/interface.py
new file mode 100644
index 0000000000..fbd027a2dc
--- /dev/null
+++ b/opendbc/car/ford/interface.py
@@ -0,0 +1,65 @@
+from panda import Panda
+from opendbc.car import get_safety_config, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.ford.fordcan import CanBus
+from opendbc.car.ford.values import Ecu, FordFlags
+from opendbc.car.interfaces import CarInterfaceBase
+
+TransmissionType = structs.CarParams.TransmissionType
+
+
+class CarInterface(CarInterfaceBase):
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "ford"
+ ret.dashcamOnly = bool(ret.flags & FordFlags.CANFD)
+
+ ret.radarUnavailable = True
+ ret.steerControlType = structs.CarParams.SteerControlType.angle
+ ret.steerActuatorDelay = 0.2
+ ret.steerLimitTimer = 1.0
+
+ CAN = CanBus(fingerprint=fingerprint)
+ cfgs = [get_safety_config(structs.CarParams.SafetyModel.ford)]
+ if CAN.main >= 4:
+ cfgs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput))
+ ret.safetyConfigs = cfgs
+
+ ret.experimentalLongitudinalAvailable = True
+ if experimental_long:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_FORD_LONG_CONTROL
+ ret.openpilotLongitudinalControl = True
+
+ if ret.flags & FordFlags.CANFD:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_FORD_CANFD
+ else:
+ # Lock out if the car does not have needed lateral and longitudinal control APIs.
+ # Note that we also check CAN for adaptive cruise, but no known signal for LCA exists
+ pscm_config = next((fw for fw in car_fw if fw.ecu == Ecu.eps and b'\x22\xDE\x01' in fw.request), None)
+ if pscm_config:
+ if len(pscm_config.fwVersion) != 24:
+ ret.dashcamOnly = True
+ else:
+ config_tja = pscm_config.fwVersion[7] # Traffic Jam Assist
+ config_lca = pscm_config.fwVersion[8] # Lane Centering Assist
+ if config_tja != 0xFF or config_lca != 0xFF:
+ ret.dashcamOnly = True
+
+ # Auto Transmission: 0x732 ECU or Gear_Shift_by_Wire_FD1
+ found_ecus = [fw.ecu for fw in car_fw]
+ if Ecu.shiftByWire in found_ecus or 0x5A in fingerprint[CAN.main] or docs:
+ ret.transmissionType = TransmissionType.automatic
+ else:
+ ret.transmissionType = TransmissionType.manual
+ ret.minEnableSpeed = 20.0 * CV.MPH_TO_MS
+
+ # BSM: Side_Detect_L_Stat, Side_Detect_R_Stat
+ # TODO: detect bsm in car_fw?
+ ret.enableBsm = 0x3A6 in fingerprint[CAN.main] and 0x3A7 in fingerprint[CAN.main]
+
+ # LCA can steer down to zero
+ ret.minSteerSpeed = 0.
+
+ ret.autoResumeSng = ret.minEnableSpeed == -1.
+ ret.centerToFront = ret.wheelbase * 0.44
+ return ret
diff --git a/opendbc/car/ford/radar_interface.py b/opendbc/car/ford/radar_interface.py
new file mode 100644
index 0000000000..efe3bef7d5
--- /dev/null
+++ b/opendbc/car/ford/radar_interface.py
@@ -0,0 +1,143 @@
+from math import cos, sin
+from opendbc.can.parser import CANParser
+from opendbc.car import structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.ford.fordcan import CanBus
+from opendbc.car.ford.values import DBC, RADAR
+from opendbc.car.interfaces import RadarInterfaceBase
+
+DELPHI_ESR_RADAR_MSGS = list(range(0x500, 0x540))
+
+DELPHI_MRR_RADAR_START_ADDR = 0x120
+DELPHI_MRR_RADAR_MSG_COUNT = 64
+
+
+def _create_delphi_esr_radar_can_parser(CP) -> CANParser:
+ msg_n = len(DELPHI_ESR_RADAR_MSGS)
+ messages = list(zip(DELPHI_ESR_RADAR_MSGS, [20] * msg_n, strict=True))
+
+ return CANParser(RADAR.DELPHI_ESR, messages, CanBus(CP).radar)
+
+
+def _create_delphi_mrr_radar_can_parser(CP) -> CANParser:
+ messages = []
+
+ for i in range(1, DELPHI_MRR_RADAR_MSG_COUNT + 1):
+ msg = f"MRR_Detection_{i:03d}"
+ messages += [(msg, 20)]
+
+ return CANParser(RADAR.DELPHI_MRR, messages, CanBus(CP).radar)
+
+
+class RadarInterface(RadarInterfaceBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+
+ self.updated_messages = set()
+ self.track_id = 0
+ self.radar = DBC[CP.carFingerprint]['radar']
+ if self.radar is None or CP.radarUnavailable:
+ self.rcp = None
+ elif self.radar == RADAR.DELPHI_ESR:
+ self.rcp = _create_delphi_esr_radar_can_parser(CP)
+ self.trigger_msg = DELPHI_ESR_RADAR_MSGS[-1]
+ self.valid_cnt = {key: 0 for key in DELPHI_ESR_RADAR_MSGS}
+ elif self.radar == RADAR.DELPHI_MRR:
+ self.rcp = _create_delphi_mrr_radar_can_parser(CP)
+ self.trigger_msg = DELPHI_MRR_RADAR_START_ADDR + DELPHI_MRR_RADAR_MSG_COUNT - 1
+ else:
+ raise ValueError(f"Unsupported radar: {self.radar}")
+
+ def update(self, can_strings):
+ if self.rcp is None:
+ return super().update(None)
+
+ vls = self.rcp.update_strings(can_strings)
+ self.updated_messages.update(vls)
+
+ if self.trigger_msg not in self.updated_messages:
+ return None
+
+ ret = structs.RadarData()
+ errors = []
+ if not self.rcp.can_valid:
+ errors.append("canError")
+ ret.errors = errors
+
+ if self.radar == RADAR.DELPHI_ESR:
+ self._update_delphi_esr()
+ elif self.radar == RADAR.DELPHI_MRR:
+ self._update_delphi_mrr()
+
+ ret.points = list(self.pts.values())
+ self.updated_messages.clear()
+ return ret
+
+ def _update_delphi_esr(self):
+ for ii in sorted(self.updated_messages):
+ cpt = self.rcp.vl[ii]
+
+ if cpt['X_Rel'] > 0.00001:
+ self.valid_cnt[ii] = 0 # reset counter
+
+ if cpt['X_Rel'] > 0.00001:
+ self.valid_cnt[ii] += 1
+ else:
+ self.valid_cnt[ii] = max(self.valid_cnt[ii] - 1, 0)
+ #print ii, self.valid_cnt[ii], cpt['VALID'], cpt['X_Rel'], cpt['Angle']
+
+ # radar point only valid if there have been enough valid measurements
+ if self.valid_cnt[ii] > 0:
+ if ii not in self.pts:
+ self.pts[ii] = structs.RadarData.RadarPoint()
+ self.pts[ii].trackId = self.track_id
+ self.track_id += 1
+ self.pts[ii].dRel = cpt['X_Rel'] # from front of car
+ self.pts[ii].yRel = cpt['X_Rel'] * cpt['Angle'] * CV.DEG_TO_RAD # in car frame's y axis, left is positive
+ self.pts[ii].vRel = cpt['V_Rel']
+ self.pts[ii].aRel = float('nan')
+ self.pts[ii].yvRel = float('nan')
+ self.pts[ii].measured = True
+ else:
+ if ii in self.pts:
+ del self.pts[ii]
+
+ def _update_delphi_mrr(self):
+ for ii in range(1, DELPHI_MRR_RADAR_MSG_COUNT + 1):
+ msg = self.rcp.vl[f"MRR_Detection_{ii:03d}"]
+
+ # SCAN_INDEX rotates through 0..3 on each message
+ # treat these as separate points
+ scanIndex = msg[f"CAN_SCAN_INDEX_2LSB_{ii:02d}"]
+ i = (ii - 1) * 4 + scanIndex
+
+ if i not in self.pts:
+ self.pts[i] = structs.RadarData.RadarPoint()
+ self.pts[i].trackId = self.track_id
+ self.pts[i].aRel = float('nan')
+ self.pts[i].yvRel = float('nan')
+ self.track_id += 1
+
+ valid = bool(msg[f"CAN_DET_VALID_LEVEL_{ii:02d}"])
+
+ if valid:
+ azimuth = msg[f"CAN_DET_AZIMUTH_{ii:02d}"] # rad [-3.1416|3.13964]
+ dist = msg[f"CAN_DET_RANGE_{ii:02d}"] # m [0|255.984]
+ distRate = msg[f"CAN_DET_RANGE_RATE_{ii:02d}"] # m/s [-128|127.984]
+ dRel = cos(azimuth) * dist # m from front of car
+ yRel = -sin(azimuth) * dist # in car frame's y axis, left is positive
+
+ # delphi doesn't notify of track switches, so do it manually
+ # TODO: refactor this to radard if more radars behave this way
+ if abs(self.pts[i].vRel - distRate) > 2 or abs(self.pts[i].dRel - dRel) > 5:
+ self.track_id += 1
+ self.pts[i].trackId = self.track_id
+
+ self.pts[i].dRel = dRel
+ self.pts[i].yRel = yRel
+ self.pts[i].vRel = distRate
+
+ self.pts[i].measured = True
+
+ else:
+ del self.pts[i]
diff --git a/opendbc/car/ford/tests/__init__.py b/opendbc/car/ford/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/ford/tests/print_platform_codes.py b/opendbc/car/ford/tests/print_platform_codes.py
new file mode 100755
index 0000000000..dfb93c06fd
--- /dev/null
+++ b/opendbc/car/ford/tests/print_platform_codes.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+from collections import defaultdict
+
+from opendbc.car.structs import CarParams
+from opendbc.car.ford.values import get_platform_codes
+from opendbc.car.ford.fingerprints import FW_VERSIONS
+
+Ecu = CarParams.Ecu
+
+if __name__ == "__main__":
+ cars_for_code: defaultdict = defaultdict(lambda: defaultdict(set))
+
+ for car_model, ecus in FW_VERSIONS.items():
+ print(car_model)
+ for ecu in sorted(ecus):
+ platform_codes = get_platform_codes(ecus[ecu])
+ for code in platform_codes:
+ cars_for_code[ecu][code].add(car_model)
+
+ print(f' (Ecu.{ecu[0]}, {hex(ecu[1])}, {ecu[2]}):')
+ print(f' Codes: {sorted(platform_codes)}')
+ print()
+
+ print('\nCar models vs. platform codes:')
+ for ecu, codes in cars_for_code.items():
+ print(f' (Ecu.{ecu[0]}, {hex(ecu[1])}, {ecu[2]}):')
+ for code, cars in codes.items():
+ print(f' {code!r}: {sorted(map(str, cars))}')
diff --git a/opendbc/car/ford/tests/test_ford.py b/opendbc/car/ford/tests/test_ford.py
new file mode 100644
index 0000000000..149fa4956f
--- /dev/null
+++ b/opendbc/car/ford/tests/test_ford.py
@@ -0,0 +1,142 @@
+import random
+from collections.abc import Iterable
+
+from hypothesis import settings, given, strategies as st
+from parameterized import parameterized
+
+from opendbc.car.structs import CarParams
+from opendbc.car.fw_versions import build_fw_dict
+from opendbc.car.ford.values import CAR, FW_QUERY_CONFIG, FW_PATTERN, get_platform_codes
+from opendbc.car.ford.fingerprints import FW_VERSIONS
+
+Ecu = CarParams.Ecu
+
+
+ECU_ADDRESSES = {
+ Ecu.eps: 0x730, # Power Steering Control Module (PSCM)
+ Ecu.abs: 0x760, # Anti-Lock Brake System (ABS)
+ Ecu.fwdRadar: 0x764, # Cruise Control Module (CCM)
+ Ecu.fwdCamera: 0x706, # Image Processing Module A (IPMA)
+ Ecu.engine: 0x7E0, # Powertrain Control Module (PCM)
+ Ecu.shiftByWire: 0x732, # Gear Shift Module (GSM)
+ Ecu.debug: 0x7D0, # Accessory Protocol Interface Module (APIM)
+}
+
+
+ECU_PART_NUMBER = {
+ Ecu.eps: [
+ b"14D003",
+ ],
+ Ecu.abs: [
+ b"2D053",
+ ],
+ Ecu.fwdRadar: [
+ b"14D049",
+ ],
+ Ecu.fwdCamera: [
+ b"14F397", # Ford Q3
+ b"14H102", # Ford Q4
+ ],
+}
+
+
+class TestFordFW:
+ def test_fw_query_config(self):
+ for (ecu, addr, subaddr) in FW_QUERY_CONFIG.extra_ecus:
+ assert ecu in ECU_ADDRESSES, "Unknown ECU"
+ assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"
+ assert subaddr is None, "Unexpected ECU subaddress"
+
+ @parameterized.expand(FW_VERSIONS.items())
+ def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[Ecu, int, int | None], Iterable[bytes]]):
+ for (ecu, addr, subaddr), fws in fw_versions.items():
+ assert ecu in ECU_PART_NUMBER, "Unexpected ECU"
+ assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"
+ assert subaddr is None, "Unexpected ECU subaddress"
+
+ for fw in fws:
+ assert len(fw) == 24, "Expected ECU response to be 24 bytes"
+
+ match = FW_PATTERN.match(fw)
+ assert match is not None, f"Unable to parse FW: {fw!r}"
+ if match:
+ part_number = match.group("part_number")
+ assert part_number in ECU_PART_NUMBER[ecu], f"Unexpected part number for {fw!r}"
+
+ codes = get_platform_codes([fw])
+ assert 1 == len(codes), f"Unable to parse FW: {fw!r}"
+
+ @settings(max_examples=100)
+ @given(data=st.data())
+ def test_platform_codes_fuzzy_fw(self, data):
+ """Ensure function doesn't raise an exception"""
+ fw_strategy = st.lists(st.binary())
+ fws = data.draw(fw_strategy)
+ get_platform_codes(fws)
+
+ def test_platform_codes_spot_check(self):
+ # Asserts basic platform code parsing behavior for a few cases
+ results = get_platform_codes([
+ b"JX6A-14C204-BPL\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ b"NZ6T-14F397-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ b"PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ b"LB5A-14C204-EAC\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ ])
+ assert results == {(b"X6A", b"J"), (b"Z6T", b"N"), (b"J6T", b"P"), (b"B5A", b"L")}
+
+ def test_fuzzy_match(self):
+ for platform, fw_by_addr in FW_VERSIONS.items():
+ # Ensure there's no overlaps in platform codes
+ for _ in range(20):
+ car_fw = []
+ for ecu, fw_versions in fw_by_addr.items():
+ ecu_name, addr, sub_addr = ecu
+ fw = random.choice(fw_versions)
+ car_fw.append(CarParams.CarFw(ecu=ecu_name, fwVersion=fw, address=addr,
+ subAddress=0 if sub_addr is None else sub_addr))
+
+ CP = CarParams(carFw=car_fw)
+ matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
+ assert matches == {platform}
+
+ def test_match_fw_fuzzy(self):
+ offline_fw = {
+ (Ecu.eps, 0x730, None): [
+ b"L1MC-14D003-AJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ b"L1MC-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ ],
+ (Ecu.abs, 0x760, None): [
+ b"L1MC-2D053-BA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ b"L1MC-2D053-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b"LB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ b"LB5T-14D049-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ ],
+ # We consider all model year hints for ECU, even with different platform codes
+ (Ecu.fwdCamera, 0x706, None): [
+ b"LB5T-14F397-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ b"NC5T-14F397-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ ],
+ }
+ expected_fingerprint = CAR.FORD_EXPLORER_MK6
+
+ # ensure that we fuzzy match on all non-exact FW with changed revisions
+ live_fw = {
+ (0x730, None): {b"L1MC-14D003-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
+ (0x760, None): {b"L1MC-2D053-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
+ (0x764, None): {b"LB5T-14D049-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
+ (0x706, None): {b"LB5T-14F397-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
+ }
+ candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw})
+ assert candidates == {expected_fingerprint}
+
+ # model year hint in between the range should match
+ live_fw[(0x706, None)] = {b"MB5T-14F397-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"}
+ candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw,})
+ assert candidates == {expected_fingerprint}
+
+ # unseen model year hint should not match
+ live_fw[(0x760, None)] = {b"M1MC-2D053-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"}
+ candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw})
+ assert len(candidates) == 0, "Should not match new model year hint"
diff --git a/opendbc/car/ford/values.py b/opendbc/car/ford/values.py
new file mode 100644
index 0000000000..c3cc9b5641
--- /dev/null
+++ b/opendbc/car/ford/values.py
@@ -0,0 +1,279 @@
+import copy
+import re
+from dataclasses import dataclass, field, replace
+from enum import Enum, IntFlag
+
+from panda import uds
+from opendbc.car import AngleRateLimit, CarSpecs, dbc_dict, DbcDict, PlatformConfig, Platforms
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \
+ Device
+from opendbc.car.fw_query_definitions import FwQueryConfig, LiveFwVersions, OfflineFwVersions, Request, StdQueries, p16
+
+Ecu = CarParams.Ecu
+
+
+class CarControllerParams:
+ STEER_STEP = 5 # LateralMotionControl, 20Hz
+ LKA_STEP = 3 # Lane_Assist_Data1, 33Hz
+ ACC_CONTROL_STEP = 2 # ACCDATA, 50Hz
+ LKAS_UI_STEP = 100 # IPMA_Data, 1Hz
+ ACC_UI_STEP = 20 # ACCDATA_3, 5Hz
+ BUTTONS_STEP = 5 # Steering_Data_FD1, 10Hz, but send twice as fast
+
+ CURVATURE_MAX = 0.02 # Max curvature for steering command, m^-1
+ STEER_DRIVER_ALLOWANCE = 1.0 # Driver intervention threshold, Nm
+
+ # Curvature rate limits
+ # The curvature signal is limited to 0.003 to 0.009 m^-1/sec by the EPS depending on speed and direction
+ # Limit to ~2 m/s^3 up, ~3 m/s^3 down at 75 mph
+ # Worst case, the low speed limits will allow 4.3 m/s^3 up, 4.9 m/s^3 down at 75 mph
+ ANGLE_RATE_LIMIT_UP = AngleRateLimit(speed_bp=[5, 25], angle_v=[0.0002, 0.0001])
+ ANGLE_RATE_LIMIT_DOWN = AngleRateLimit(speed_bp=[5, 25], angle_v=[0.000225, 0.00015])
+ CURVATURE_ERROR = 0.002 # ~6 degrees at 10 m/s, ~10 degrees at 35 m/s
+
+ ACCEL_MAX = 2.0 # m/s^2 max acceleration
+ ACCEL_MIN = -3.5 # m/s^2 max deceleration
+ MIN_GAS = -0.5
+ INACTIVE_GAS = -5.0
+
+ def __init__(self, CP):
+ pass
+
+
+class FordFlags(IntFlag):
+ # Static flags
+ CANFD = 1
+
+
+class RADAR:
+ DELPHI_ESR = 'ford_fusion_2018_adas'
+ DELPHI_MRR = 'FORD_CADS'
+
+
+class Footnote(Enum):
+ FOCUS = CarFootnote(
+ "Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in " +
+ "North and South America/Southeast Asia.",
+ Column.MODEL,
+ )
+
+
+@dataclass
+class FordCarDocs(CarDocs):
+ package: str = "Co-Pilot360 Assist+"
+ hybrid: bool = False
+ plug_in_hybrid: bool = False
+
+ def init_make(self, CP: CarParams):
+ harness = CarHarness.ford_q4 if CP.flags & FordFlags.CANFD else CarHarness.ford_q3
+ if CP.carFingerprint in (CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_MAVERICK_MK1, CAR.FORD_F_150_MK14, CAR.FORD_F_150_LIGHTNING_MK1):
+ self.car_parts = CarParts([Device.threex_angled_mount, harness])
+ else:
+ self.car_parts = CarParts([Device.threex, harness])
+
+
+@dataclass
+class FordPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('ford_lincoln_base_pt', RADAR.DELPHI_MRR))
+
+ def init(self):
+ for car_docs in list(self.car_docs):
+ if car_docs.hybrid:
+ name = f"{car_docs.make} {car_docs.model} Hybrid {car_docs.years}"
+ self.car_docs.append(replace(copy.deepcopy(car_docs), name=name))
+ if car_docs.plug_in_hybrid:
+ name = f"{car_docs.make} {car_docs.model} Plug-in Hybrid {car_docs.years}"
+ self.car_docs.append(replace(copy.deepcopy(car_docs), name=name))
+
+
+@dataclass
+class FordCANFDPlatformConfig(FordPlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('ford_lincoln_base_pt', None))
+
+ def init(self):
+ super().init()
+ self.flags |= FordFlags.CANFD
+
+
+class CAR(Platforms):
+ FORD_BRONCO_SPORT_MK1 = FordPlatformConfig(
+ [FordCarDocs("Ford Bronco Sport 2021-23")],
+ CarSpecs(mass=1625, wheelbase=2.67, steerRatio=17.7),
+ )
+ FORD_ESCAPE_MK4 = FordPlatformConfig(
+ [
+ FordCarDocs("Ford Escape 2020-22", hybrid=True, plug_in_hybrid=True),
+ FordCarDocs("Ford Kuga 2020-22", "Adaptive Cruise Control with Lane Centering", hybrid=True, plug_in_hybrid=True),
+ ],
+ CarSpecs(mass=1750, wheelbase=2.71, steerRatio=16.7),
+ )
+ FORD_EXPLORER_MK6 = FordPlatformConfig(
+ [
+ FordCarDocs("Ford Explorer 2020-23", hybrid=True), # Hybrid: Limited and Platinum only
+ FordCarDocs("Lincoln Aviator 2020-23", "Co-Pilot360 Plus", plug_in_hybrid=True), # Hybrid: Grand Touring only
+ ],
+ CarSpecs(mass=2050, wheelbase=3.025, steerRatio=16.8),
+ )
+ FORD_F_150_MK14 = FordCANFDPlatformConfig(
+ [FordCarDocs("Ford F-150 2022-23", "Co-Pilot360 Active 2.0", hybrid=True)],
+ CarSpecs(mass=2000, wheelbase=3.69, steerRatio=17.0),
+ )
+ FORD_F_150_LIGHTNING_MK1 = FordCANFDPlatformConfig(
+ [FordCarDocs("Ford F-150 Lightning 2021-23", "Co-Pilot360 Active 2.0")],
+ CarSpecs(mass=2948, wheelbase=3.70, steerRatio=16.9),
+ )
+ FORD_FOCUS_MK4 = FordPlatformConfig(
+ [FordCarDocs("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS], hybrid=True)], # mHEV only
+ CarSpecs(mass=1350, wheelbase=2.7, steerRatio=15.0),
+ )
+ FORD_MAVERICK_MK1 = FordPlatformConfig(
+ [
+ FordCarDocs("Ford Maverick 2022", "LARIAT Luxury", hybrid=True),
+ FordCarDocs("Ford Maverick 2023-24", "Co-Pilot360 Assist", hybrid=True),
+ ],
+ CarSpecs(mass=1650, wheelbase=3.076, steerRatio=17.0),
+ )
+ FORD_MUSTANG_MACH_E_MK1 = FordCANFDPlatformConfig(
+ [FordCarDocs("Ford Mustang Mach-E 2021-23", "Co-Pilot360 Active 2.0")],
+ CarSpecs(mass=2200, wheelbase=2.984, steerRatio=17.0), # TODO: check steer ratio
+ )
+ FORD_RANGER_MK2 = FordCANFDPlatformConfig(
+ [FordCarDocs("Ford Ranger 2024", "Adaptive Cruise Control with Lane Centering")],
+ CarSpecs(mass=2000, wheelbase=3.27, steerRatio=17.0),
+ )
+
+
+# FW response contains a combined software and part number
+# A-Z except no I, O or W
+# e.g. NZ6A-14C204-AAA
+# 1222-333333-444
+# 1 = Model year hint (approximates model year/generation)
+# 2 = Platform hint
+# 3 = Part number
+# 4 = Software version
+FW_ALPHABET = b'A-HJ-NP-VX-Z'
+FW_PATTERN = re.compile(b'^(?P[' + FW_ALPHABET + b'])' +
+ b'(?P[0-9' + FW_ALPHABET + b']{3})-' +
+ b'(?P[0-9' + FW_ALPHABET + b']{5,6})-' +
+ b'(?P[' + FW_ALPHABET + b']{2,})\x00*$')
+
+
+def get_platform_codes(fw_versions: list[bytes] | set[bytes]) -> set[tuple[bytes, bytes]]:
+ codes = set()
+ for fw in fw_versions:
+ match = FW_PATTERN.match(fw)
+ if match is not None:
+ codes.add((match.group('platform_hint'), match.group('model_year_hint')))
+
+ return codes
+
+
+def match_fw_to_car_fuzzy(live_fw_versions: LiveFwVersions, vin: str, offline_fw_versions: OfflineFwVersions) -> set[str]:
+ candidates: set[str] = set()
+
+ for candidate, fws in offline_fw_versions.items():
+ # Keep track of ECUs which pass all checks (platform hint, within model year hint range)
+ valid_found_ecus = set()
+ valid_expected_ecus = {ecu[1:] for ecu in fws if ecu[0] in PLATFORM_CODE_ECUS}
+ for ecu, expected_versions in fws.items():
+ addr = ecu[1:]
+ # Only check ECUs expected to have platform codes
+ if ecu[0] not in PLATFORM_CODE_ECUS:
+ continue
+
+ # Expected platform codes & model year hints
+ codes = get_platform_codes(expected_versions)
+ expected_platform_codes = {code for code, _ in codes}
+ expected_model_year_hints = {model_year_hint for _, model_year_hint in codes}
+
+ # Found platform codes & model year hints
+ codes = get_platform_codes(live_fw_versions.get(addr, set()))
+ found_platform_codes = {code for code, _ in codes}
+ found_model_year_hints = {model_year_hint for _, model_year_hint in codes}
+
+ # Check platform code matches for any found versions
+ if not any(found_platform_code in expected_platform_codes for found_platform_code in found_platform_codes):
+ break
+
+ # Check any model year hint within range in the database. Note that some models have more than one
+ # platform code per ECU which we don't consider as separate ranges
+ if not any(min(expected_model_year_hints) <= found_model_year_hint <= max(expected_model_year_hints) for
+ found_model_year_hint in found_model_year_hints):
+ break
+
+ valid_found_ecus.add(addr)
+
+ # If all live ECUs pass all checks for candidate, add it as a match
+ if valid_expected_ecus.issubset(valid_found_ecus):
+ candidates.add(candidate)
+
+ return candidates
+
+
+# All of these ECUs must be present and are expected to have platform codes we can match
+PLATFORM_CODE_ECUS = (Ecu.abs, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.eps)
+
+DATA_IDENTIFIER_FORD_ASBUILT = 0xDE00
+
+ASBUILT_BLOCKS: list[tuple[int, list]] = [
+ (1, [Ecu.debug, Ecu.fwdCamera, Ecu.eps]),
+ (2, [Ecu.abs, Ecu.debug, Ecu.eps]),
+ (3, [Ecu.abs, Ecu.debug, Ecu.eps]),
+ (4, [Ecu.debug, Ecu.fwdCamera]),
+ (5, [Ecu.debug]),
+ (6, [Ecu.debug]),
+ (7, [Ecu.debug]),
+ (8, [Ecu.debug]),
+ (9, [Ecu.debug]),
+ (16, [Ecu.debug, Ecu.fwdCamera]),
+ (18, [Ecu.fwdCamera]),
+ (20, [Ecu.fwdCamera]),
+ (21, [Ecu.fwdCamera]),
+]
+
+
+def ford_asbuilt_block_request(block_id: int):
+ return bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + p16(DATA_IDENTIFIER_FORD_ASBUILT + block_id - 1)
+
+
+def ford_asbuilt_block_response(block_id: int):
+ return bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + p16(DATA_IDENTIFIER_FORD_ASBUILT + block_id - 1)
+
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[
+ # CAN and CAN FD queries are combined.
+ # FIXME: For CAN FD, ECUs respond with frames larger than 8 bytes on the powertrain bus
+ Request(
+ [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
+ [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
+ logging=True,
+ ),
+ Request(
+ [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
+ [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
+ bus=0,
+ auxiliary=True,
+ ),
+ *[Request(
+ [StdQueries.TESTER_PRESENT_REQUEST, ford_asbuilt_block_request(block_id)],
+ [StdQueries.TESTER_PRESENT_RESPONSE, ford_asbuilt_block_response(block_id)],
+ whitelist_ecus=ecus,
+ bus=0,
+ logging=True,
+ ) for block_id, ecus in ASBUILT_BLOCKS],
+ ],
+ extra_ecus=[
+ (Ecu.engine, 0x7e0, None), # Powertrain Control Module
+ # Note: We are unlikely to get a response from behind the gateway
+ (Ecu.shiftByWire, 0x732, None), # Gear Shift Module
+ (Ecu.debug, 0x7d0, None), # Accessory Protocol Interface Module
+ ],
+ # Custom fuzzy fingerprinting function using platform and model year hints
+ match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
+)
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/fw_query_definitions.py b/opendbc/car/fw_query_definitions.py
new file mode 100644
index 0000000000..f32da6dd93
--- /dev/null
+++ b/opendbc/car/fw_query_definitions.py
@@ -0,0 +1,119 @@
+import copy
+from dataclasses import dataclass, field
+import struct
+from collections.abc import Callable
+
+from panda import uds
+
+from opendbc.car.structs import CarParams
+
+AddrType = tuple[int, int | None]
+EcuAddrBusType = tuple[int, int | None, int]
+EcuAddrSubAddr = tuple[CarParams.Ecu, int, int | None]
+
+LiveFwVersions = dict[AddrType, set[bytes]]
+OfflineFwVersions = dict[str, dict[EcuAddrSubAddr, list[bytes]]]
+
+# A global list of addresses we will only ever consider for VIN responses
+# engine, hybrid controller, Ford abs, Hyundai CAN FD cluster, 29-bit engine, PGM-FI
+# TODO: move these to each brand's FW query config
+STANDARD_VIN_ADDRS = [0x7e0, 0x7e2, 0x760, 0x7c6, 0x18da10f1, 0x18da0ef1]
+
+
+def p16(val):
+ return struct.pack("!H", val)
+
+
+class StdQueries:
+ # FW queries
+ TESTER_PRESENT_REQUEST = bytes([uds.SERVICE_TYPE.TESTER_PRESENT, 0x0])
+ TESTER_PRESENT_RESPONSE = bytes([uds.SERVICE_TYPE.TESTER_PRESENT + 0x40, 0x0])
+
+ SHORT_TESTER_PRESENT_REQUEST = bytes([uds.SERVICE_TYPE.TESTER_PRESENT])
+ SHORT_TESTER_PRESENT_RESPONSE = bytes([uds.SERVICE_TYPE.TESTER_PRESENT + 0x40])
+
+ DEFAULT_DIAGNOSTIC_REQUEST = bytes([uds.SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL,
+ uds.SESSION_TYPE.DEFAULT])
+ DEFAULT_DIAGNOSTIC_RESPONSE = bytes([uds.SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL + 0x40,
+ uds.SESSION_TYPE.DEFAULT, 0x0, 0x32, 0x1, 0xf4])
+
+ EXTENDED_DIAGNOSTIC_REQUEST = bytes([uds.SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL,
+ uds.SESSION_TYPE.EXTENDED_DIAGNOSTIC])
+ EXTENDED_DIAGNOSTIC_RESPONSE = bytes([uds.SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL + 0x40,
+ uds.SESSION_TYPE.EXTENDED_DIAGNOSTIC, 0x0, 0x32, 0x1, 0xf4])
+
+ MANUFACTURER_SOFTWARE_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_NUMBER)
+ MANUFACTURER_SOFTWARE_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_NUMBER)
+
+ SUPPLIER_SOFTWARE_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.SYSTEM_SUPPLIER_ECU_SOFTWARE_VERSION_NUMBER)
+ SUPPLIER_SOFTWARE_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.SYSTEM_SUPPLIER_ECU_SOFTWARE_VERSION_NUMBER)
+
+ UDS_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
+ UDS_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
+
+ OBD_VERSION_REQUEST = b'\x09\x04'
+ OBD_VERSION_RESPONSE = b'\x49\x04'
+
+ # VIN queries
+ OBD_VIN_REQUEST = b'\x09\x02'
+ OBD_VIN_RESPONSE = b'\x49\x02\x01'
+
+ UDS_VIN_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + p16(uds.DATA_IDENTIFIER_TYPE.VIN)
+ UDS_VIN_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + p16(uds.DATA_IDENTIFIER_TYPE.VIN)
+
+ GM_VIN_REQUEST = b'\x1a\x90'
+ GM_VIN_RESPONSE = b'\x5a\x90'
+
+ KWP_VIN_REQUEST = b'\x21\x81'
+ KWP_VIN_RESPONSE = b'\x61\x81'
+
+
+@dataclass
+class Request:
+ request: list[bytes]
+ response: list[bytes]
+ whitelist_ecus: list[CarParams.Ecu] = field(default_factory=list)
+ rx_offset: int = 0x8
+ bus: int = 1
+ # Whether this query should be run on the first auxiliary panda (CAN FD cars for example)
+ auxiliary: bool = False
+ # FW responses from these queries will not be used for fingerprinting
+ logging: bool = False
+ # pandad toggles OBD multiplexing on/off as needed
+ obd_multiplexing: bool = True
+
+
+@dataclass
+class FwQueryConfig:
+ requests: list[Request]
+ # TODO: make this automatic and remove hardcoded lists, or do fingerprinting with ecus
+ # Overrides and removes from essential ecus for specific models and ecus (exact matching)
+ non_essential_ecus: dict[CarParams.Ecu, list[str]] = field(default_factory=dict)
+ # Ecus added for data collection, not to be fingerprinted on
+ extra_ecus: list[tuple[CarParams.Ecu, int, int | None]] = field(default_factory=list)
+ # Function a brand can implement to provide better fuzzy matching. Takes in FW versions and VIN,
+ # returns set of candidates. Only will match if one candidate is returned
+ match_fw_to_car_fuzzy: Callable[[LiveFwVersions, str, OfflineFwVersions], set[str]] | None = None
+
+ def __post_init__(self):
+ for i in range(len(self.requests)):
+ if self.requests[i].auxiliary:
+ new_request = copy.deepcopy(self.requests[i])
+ new_request.bus += 4
+ self.requests.append(new_request)
+
+ def get_all_ecus(self, offline_fw_versions: OfflineFwVersions,
+ include_extra_ecus: bool = True) -> set[EcuAddrSubAddr]:
+ # Add ecus in database + extra ecus
+ brand_ecus = {ecu for ecus in offline_fw_versions.values() for ecu in ecus}
+
+ if include_extra_ecus:
+ brand_ecus |= set(self.extra_ecus)
+
+ return brand_ecus
diff --git a/opendbc/car/fw_versions.py b/opendbc/car/fw_versions.py
new file mode 100644
index 0000000000..3962ec6328
--- /dev/null
+++ b/opendbc/car/fw_versions.py
@@ -0,0 +1,327 @@
+from collections import defaultdict
+from collections.abc import Callable, Iterator
+from typing import Protocol, TypeVar
+
+from tqdm import tqdm
+
+from panda import uds
+from opendbc.car import carlog
+from opendbc.car.can_definitions import CanRecvCallable, CanSendCallable
+from opendbc.car.structs import CarParams
+from opendbc.car.ecu_addrs import get_ecu_addrs
+from opendbc.car.fingerprints import FW_VERSIONS
+from opendbc.car.fw_query_definitions import AddrType, EcuAddrBusType, FwQueryConfig, LiveFwVersions, OfflineFwVersions
+from opendbc.car.interfaces import get_interface_attr
+from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
+
+Ecu = CarParams.Ecu
+ESSENTIAL_ECUS = [Ecu.engine, Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.vsa]
+FUZZY_EXCLUDE_ECUS = [Ecu.fwdCamera, Ecu.fwdRadar, Ecu.eps, Ecu.debug]
+
+FW_QUERY_CONFIGS: dict[str, FwQueryConfig] = get_interface_attr('FW_QUERY_CONFIG', ignore_none=True)
+VERSIONS = get_interface_attr('FW_VERSIONS', ignore_none=True)
+
+MODEL_TO_BRAND = {c: b for b, e in VERSIONS.items() for c in e}
+REQUESTS = [(brand, config, r) for brand, config in FW_QUERY_CONFIGS.items() for r in config.requests]
+
+T = TypeVar('T')
+ObdCallback = Callable[[bool], None]
+
+
+def chunks(l: list[T], n: int = 128) -> Iterator[list[T]]:
+ for i in range(0, len(l), n):
+ yield l[i:i + n]
+
+
+def is_brand(brand: str, filter_brand: str | None) -> bool:
+ """Returns if brand matches filter_brand or no brand filter is specified"""
+ return filter_brand is None or brand == filter_brand
+
+
+def build_fw_dict(fw_versions: list[CarParams.CarFw], filter_brand: str = None) -> dict[AddrType, set[bytes]]:
+ fw_versions_dict: defaultdict[AddrType, set[bytes]] = defaultdict(set)
+ for fw in fw_versions:
+ if is_brand(fw.brand, filter_brand) and not fw.logging:
+ sub_addr = fw.subAddress if fw.subAddress != 0 else None
+ fw_versions_dict[(fw.address, sub_addr)].add(fw.fwVersion)
+ return dict(fw_versions_dict)
+
+
+class MatchFwToCar(Protocol):
+ def __call__(self, live_fw_versions: LiveFwVersions, match_brand: str = None, log: bool = True) -> set[str]:
+ ...
+
+
+def match_fw_to_car_fuzzy(live_fw_versions: LiveFwVersions, match_brand: str = None, log: bool = True, exclude: str = None) -> set[str]:
+ """Do a fuzzy FW match. This function will return a match, and the number of firmware version
+ that were matched uniquely to that specific car. If multiple ECUs uniquely match to different cars
+ the match is rejected."""
+
+ # Build lookup table from (addr, sub_addr, fw) to list of candidate cars
+ all_fw_versions = defaultdict(list)
+ for candidate, fw_by_addr in FW_VERSIONS.items():
+ if not is_brand(MODEL_TO_BRAND[candidate], match_brand):
+ continue
+
+ if candidate == exclude:
+ continue
+
+ for addr, fws in fw_by_addr.items():
+ # These ECUs are known to be shared between models (EPS only between hybrid/ICE version)
+ # Getting this exactly right isn't crucial, but excluding camera and radar makes it almost
+ # impossible to get 3 matching versions, even if two models with shared parts are released at the same
+ # time and only one is in our database.
+ if addr[0] in FUZZY_EXCLUDE_ECUS:
+ continue
+ for f in fws:
+ all_fw_versions[(addr[1], addr[2], f)].append(candidate)
+
+ matched_ecus = set()
+ match: str | None = None
+ for addr, versions in live_fw_versions.items():
+ ecu_key = (addr[0], addr[1])
+ for version in versions:
+ # All cars that have this FW response on the specified address
+ candidates = all_fw_versions[(*ecu_key, version)]
+
+ if len(candidates) == 1:
+ matched_ecus.add(ecu_key)
+ if match is None:
+ match = candidates[0]
+ # We uniquely matched two different cars. No fuzzy match possible
+ elif match != candidates[0]:
+ return set()
+
+ # Note that it is possible to match to a candidate without all its ECUs being present
+ # if there are enough matches. FIXME: parameterize this or require all ECUs to exist like exact matching
+ if match and len(matched_ecus) >= 2:
+ if log:
+ carlog.error(f"Fingerprinted {match} using fuzzy match. {len(matched_ecus)} matching ECUs")
+ return {match}
+ else:
+ return set()
+
+
+def match_fw_to_car_exact(live_fw_versions: LiveFwVersions, match_brand: str = None, log: bool = True, extra_fw_versions: dict = None) -> set[str]:
+ """Do an exact FW match. Returns all cars that match the given
+ FW versions for a list of "essential" ECUs. If an ECU is not considered
+ essential the FW version can be missing to get a fingerprint, but if it's present it
+ needs to match the database."""
+ if extra_fw_versions is None:
+ extra_fw_versions = {}
+
+ invalid = set()
+ candidates = {c: f for c, f in FW_VERSIONS.items() if
+ is_brand(MODEL_TO_BRAND[c], match_brand)}
+
+ for candidate, fws in candidates.items():
+ config = FW_QUERY_CONFIGS[MODEL_TO_BRAND[candidate]]
+ for ecu, expected_versions in fws.items():
+ expected_versions = expected_versions + extra_fw_versions.get(candidate, {}).get(ecu, [])
+ ecu_type = ecu[0]
+ addr = ecu[1:]
+
+ found_versions = live_fw_versions.get(addr, set())
+ if not len(found_versions):
+ # Some models can sometimes miss an ecu, or show on two different addresses
+ # FIXME: this logic can be improved to be more specific, should require one of the two addresses
+ if candidate in config.non_essential_ecus.get(ecu_type, []):
+ continue
+
+ # Ignore non essential ecus
+ if ecu_type not in ESSENTIAL_ECUS:
+ continue
+
+ # Virtual debug ecu doesn't need to match the database
+ if ecu_type == Ecu.debug:
+ continue
+
+ if not any(found_version in expected_versions for found_version in found_versions):
+ invalid.add(candidate)
+ break
+
+ return set(candidates.keys()) - invalid
+
+
+def match_fw_to_car(fw_versions: list[CarParams.CarFw], vin: str, allow_exact: bool = True,
+ allow_fuzzy: bool = True, log: bool = True) -> tuple[bool, set[str]]:
+ # Try exact matching first
+ exact_matches: list[tuple[bool, MatchFwToCar]] = []
+ if allow_exact:
+ exact_matches = [(True, match_fw_to_car_exact)]
+ if allow_fuzzy:
+ exact_matches.append((False, match_fw_to_car_fuzzy))
+
+ for exact_match, match_func in exact_matches:
+ # For each brand, attempt to fingerprint using all FW returned from its queries
+ matches: set[str] = set()
+ for brand in VERSIONS.keys():
+ fw_versions_dict = build_fw_dict(fw_versions, filter_brand=brand)
+ matches |= match_func(fw_versions_dict, match_brand=brand, log=log)
+
+ # If specified and no matches so far, fall back to brand's fuzzy fingerprinting function
+ config = FW_QUERY_CONFIGS[brand]
+ if not exact_match and not len(matches) and config.match_fw_to_car_fuzzy is not None:
+ matches |= config.match_fw_to_car_fuzzy(fw_versions_dict, vin, VERSIONS[brand])
+
+ if len(matches):
+ return exact_match, matches
+
+ return True, set()
+
+
+def get_present_ecus(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, num_pandas: int = 1) -> set[EcuAddrBusType]:
+ # queries are split by OBD multiplexing mode
+ queries: dict[bool, list[list[EcuAddrBusType]]] = {True: [], False: []}
+ parallel_queries: dict[bool, list[EcuAddrBusType]] = {True: [], False: []}
+ responses: set[EcuAddrBusType] = set()
+
+ for brand, config, r in REQUESTS:
+ # Skip query if no panda available
+ if r.bus > num_pandas * 4 - 1:
+ continue
+
+ for ecu_type, addr, sub_addr in config.get_all_ecus(VERSIONS[brand]):
+ # Only query ecus in whitelist if whitelist is not empty
+ if len(r.whitelist_ecus) == 0 or ecu_type in r.whitelist_ecus:
+ a = (addr, sub_addr, r.bus)
+ # Build set of queries
+ if sub_addr is None:
+ if a not in parallel_queries[r.obd_multiplexing]:
+ parallel_queries[r.obd_multiplexing].append(a)
+ else: # subaddresses must be queried one by one
+ if [a] not in queries[r.obd_multiplexing]:
+ queries[r.obd_multiplexing].append([a])
+
+ # Build set of expected responses to filter
+ response_addr = uds.get_rx_addr_for_tx_addr(addr, r.rx_offset)
+ responses.add((response_addr, sub_addr, r.bus))
+
+ for obd_multiplexing in queries:
+ queries[obd_multiplexing].insert(0, parallel_queries[obd_multiplexing])
+
+ ecu_responses = set()
+ for obd_multiplexing in queries:
+ set_obd_multiplexing(obd_multiplexing)
+ for query in queries[obd_multiplexing]:
+ ecu_responses.update(get_ecu_addrs(can_recv, can_send, set(query), responses, timeout=0.1))
+ return ecu_responses
+
+
+def get_brand_ecu_matches(ecu_rx_addrs: set[EcuAddrBusType]) -> dict[str, set[AddrType]]:
+ """Returns dictionary of brands and matches with ECUs in their FW versions"""
+
+ brand_addrs = {brand: {(addr, subaddr) for _, addr, subaddr in config.get_all_ecus(VERSIONS[brand])} for
+ brand, config in FW_QUERY_CONFIGS.items()}
+ brand_matches: dict[str, set[AddrType]] = {brand: set() for brand, _, _ in REQUESTS}
+
+ brand_rx_offsets = {(brand, r.rx_offset) for brand, _, r in REQUESTS}
+ for addr, sub_addr, _ in ecu_rx_addrs:
+ # Since we can't know what request an ecu responded to, add matches for all possible rx offsets
+ for brand, rx_offset in brand_rx_offsets:
+ a = (uds.get_rx_addr_for_tx_addr(addr, -rx_offset), sub_addr)
+ if a in brand_addrs[brand]:
+ brand_matches[brand].add(a)
+
+ return brand_matches
+
+
+def get_fw_versions_ordered(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, vin: str,
+ ecu_rx_addrs: set[EcuAddrBusType], timeout: float = 0.1, num_pandas: int = 1, debug: bool = False,
+ progress: bool = False) -> list[CarParams.CarFw]:
+ """Queries for FW versions ordering brands by likelihood, breaks when exact match is found"""
+
+ all_car_fw = []
+ brand_matches = get_brand_ecu_matches(ecu_rx_addrs)
+
+ for brand in sorted(brand_matches, key=lambda b: len(brand_matches[b]), reverse=True):
+ # Skip this brand if there are no matching present ECUs
+ if not len(brand_matches[brand]):
+ continue
+
+ car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing, query_brand=brand, timeout=timeout, num_pandas=num_pandas, debug=debug,
+ progress=progress)
+ all_car_fw.extend(car_fw)
+
+ # If there is a match using this brand's FW alone, finish querying early
+ _, matches = match_fw_to_car(car_fw, vin, log=False)
+ if len(matches) == 1:
+ break
+
+ return all_car_fw
+
+
+def get_fw_versions(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, query_brand: str = None,
+ extra: OfflineFwVersions = None, timeout: float = 0.1, num_pandas: int = 1, debug: bool = False,
+ progress: bool = False) -> list[CarParams.CarFw]:
+ versions = VERSIONS.copy()
+
+ if query_brand is not None:
+ versions = {query_brand: versions[query_brand]}
+
+ if extra is not None:
+ versions.update(extra)
+
+ # Extract ECU addresses to query from fingerprints
+ # ECUs using a subaddress need be queried one by one, the rest can be done in parallel
+ addrs = []
+ parallel_addrs = []
+ ecu_types = {}
+
+ for brand, brand_versions in versions.items():
+ config = FW_QUERY_CONFIGS[brand]
+ for ecu_type, addr, sub_addr in config.get_all_ecus(brand_versions):
+ a = (brand, addr, sub_addr)
+ if a not in ecu_types:
+ ecu_types[a] = ecu_type
+
+ if sub_addr is None:
+ if a not in parallel_addrs:
+ parallel_addrs.append(a)
+ else:
+ if [a] not in addrs:
+ addrs.append([a])
+
+ addrs.insert(0, parallel_addrs)
+
+ # Get versions and build capnp list to put into CarParams
+ car_fw = []
+ requests = [(brand, config, r) for brand, config, r in REQUESTS if is_brand(brand, query_brand)]
+ for addr_group in tqdm(addrs, disable=not progress): # split by subaddr, if any
+ for addr_chunk in chunks(addr_group):
+ for brand, config, r in requests:
+ # Skip query if no panda available
+ if r.bus > num_pandas * 4 - 1:
+ continue
+
+ # Toggle OBD multiplexing for each request
+ if r.bus % 4 == 1:
+ set_obd_multiplexing(r.obd_multiplexing)
+
+ try:
+ query_addrs = [(a, s) for (b, a, s) in addr_chunk if b in (brand, 'any') and
+ (len(r.whitelist_ecus) == 0 or ecu_types[(b, a, s)] in r.whitelist_ecus)]
+
+ if query_addrs:
+ query = IsoTpParallelQuery(can_send, can_recv, r.bus, query_addrs, r.request, r.response, r.rx_offset, debug=debug)
+ for (tx_addr, sub_addr), version in query.get_data(timeout).items():
+ f = CarParams.CarFw()
+
+ f.ecu = ecu_types.get((brand, tx_addr, sub_addr), Ecu.unknown)
+ f.fwVersion = version
+ f.address = tx_addr
+ f.responseAddress = uds.get_rx_addr_for_tx_addr(tx_addr, r.rx_offset)
+ f.request = r.request
+ f.brand = brand
+ f.bus = r.bus
+ f.logging = r.logging or (f.ecu, tx_addr, sub_addr) in config.extra_ecus
+ f.obdMultiplexing = r.obd_multiplexing
+
+ if sub_addr is not None:
+ f.subAddress = sub_addr
+
+ car_fw.append(f)
+ except Exception:
+ carlog.exception("FW query exception")
+
+ return car_fw
diff --git a/opendbc/car/gm/__init__.py b/opendbc/car/gm/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/gm/carcontroller.py b/opendbc/car/gm/carcontroller.py
new file mode 100644
index 0000000000..0d006f8673
--- /dev/null
+++ b/opendbc/car/gm/carcontroller.py
@@ -0,0 +1,163 @@
+import copy
+from opendbc.can.packer import CANPacker
+from opendbc.car import DT_CTRL, apply_driver_steer_torque_limits, structs
+from opendbc.car.gm import gmcan
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.gm.values import DBC, CanBus, CarControllerParams, CruiseButtons
+from opendbc.car.common.numpy_fast import interp
+from opendbc.car.interfaces import CarControllerBase
+
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+NetworkLocation = structs.CarParams.NetworkLocation
+LongCtrlState = structs.CarControl.Actuators.LongControlState
+
+# Camera cancels up to 0.1s after brake is pressed, ECM allows 0.5s
+CAMERA_CANCEL_DELAY_FRAMES = 10
+# Enforce a minimum interval between steering messages to avoid a fault
+MIN_STEER_MSG_INTERVAL_MS = 15
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.start_time = 0.
+ self.apply_steer_last = 0
+ self.apply_gas = 0
+ self.apply_brake = 0
+ self.last_steer_frame = 0
+ self.last_button_frame = 0
+ self.cancel_counter = 0
+
+ self.lka_steering_cmd_counter = 0
+ self.lka_icon_status_last = (False, False)
+
+ self.params = CarControllerParams(self.CP)
+
+ self.packer_pt = CANPacker(DBC[self.CP.carFingerprint]['pt'])
+ self.packer_obj = CANPacker(DBC[self.CP.carFingerprint]['radar'])
+ self.packer_ch = CANPacker(DBC[self.CP.carFingerprint]['chassis'])
+
+ def update(self, CC, CS, now_nanos):
+ actuators = CC.actuators
+ hud_control = CC.hudControl
+ hud_alert = hud_control.visualAlert
+ hud_v_cruise = hud_control.setSpeed
+ if hud_v_cruise > 70:
+ hud_v_cruise = 0
+
+ # Send CAN commands.
+ can_sends = []
+
+ # Steering (Active: 50Hz, inactive: 10Hz)
+ steer_step = self.params.STEER_STEP if CC.latActive else self.params.INACTIVE_STEER_STEP
+
+ if self.CP.networkLocation == NetworkLocation.fwdCamera:
+ # Also send at 50Hz:
+ # - on startup, first few msgs are blocked
+ # - until we're in sync with camera so counters align when relay closes, preventing a fault.
+ # openpilot can subtly drift, so this is activated throughout a drive to stay synced
+ out_of_sync = self.lka_steering_cmd_counter % 4 != (CS.cam_lka_steering_cmd_counter + 1) % 4
+ if CS.loopback_lka_steering_cmd_ts_nanos == 0 or out_of_sync:
+ steer_step = self.params.STEER_STEP
+
+ self.lka_steering_cmd_counter += 1 if CS.loopback_lka_steering_cmd_updated else 0
+
+ # Avoid GM EPS faults when transmitting messages too close together: skip this transmit if we
+ # received the ASCMLKASteeringCmd loopback confirmation too recently
+ last_lka_steer_msg_ms = (now_nanos - CS.loopback_lka_steering_cmd_ts_nanos) * 1e-6
+ if (self.frame - self.last_steer_frame) >= steer_step and last_lka_steer_msg_ms > MIN_STEER_MSG_INTERVAL_MS:
+ # Initialize ASCMLKASteeringCmd counter using the camera until we get a msg on the bus
+ if CS.loopback_lka_steering_cmd_ts_nanos == 0:
+ self.lka_steering_cmd_counter = CS.pt_lka_steering_cmd_counter + 1
+
+ if CC.latActive:
+ new_steer = int(round(actuators.steer * self.params.STEER_MAX))
+ apply_steer = apply_driver_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.params)
+ else:
+ apply_steer = 0
+
+ self.last_steer_frame = self.frame
+ self.apply_steer_last = apply_steer
+ idx = self.lka_steering_cmd_counter % 4
+ can_sends.append(gmcan.create_steering_control(self.packer_pt, CanBus.POWERTRAIN, apply_steer, idx, CC.latActive))
+
+ if self.CP.openpilotLongitudinalControl:
+ # Gas/regen, brakes, and UI commands - all at 25Hz
+ if self.frame % 4 == 0:
+ stopping = actuators.longControlState == LongCtrlState.stopping
+ if not CC.longActive:
+ # ASCM sends max regen when not enabled
+ self.apply_gas = self.params.INACTIVE_REGEN
+ self.apply_brake = 0
+ else:
+ self.apply_gas = int(round(interp(actuators.accel, self.params.GAS_LOOKUP_BP, self.params.GAS_LOOKUP_V)))
+ self.apply_brake = int(round(interp(actuators.accel, self.params.BRAKE_LOOKUP_BP, self.params.BRAKE_LOOKUP_V)))
+ # Don't allow any gas above inactive regen while stopping
+ # FIXME: brakes aren't applied immediately when enabling at a stop
+ if stopping:
+ self.apply_gas = self.params.INACTIVE_REGEN
+
+ idx = (self.frame // 4) % 4
+
+ at_full_stop = CC.longActive and CS.out.standstill
+ near_stop = CC.longActive and (CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE)
+ friction_brake_bus = CanBus.CHASSIS
+ # GM Camera exceptions
+ # TODO: can we always check the longControlState?
+ if self.CP.networkLocation == NetworkLocation.fwdCamera:
+ at_full_stop = at_full_stop and stopping
+ friction_brake_bus = CanBus.POWERTRAIN
+
+ # GasRegenCmdActive needs to be 1 to avoid cruise faults. It describes the ACC state, not actuation
+ can_sends.append(gmcan.create_gas_regen_command(self.packer_pt, CanBus.POWERTRAIN, self.apply_gas, idx, CC.enabled, at_full_stop))
+ can_sends.append(gmcan.create_friction_brake_command(self.packer_ch, friction_brake_bus, self.apply_brake,
+ idx, CC.enabled, near_stop, at_full_stop, self.CP))
+
+ # Send dashboard UI commands (ACC status)
+ send_fcw = hud_alert == VisualAlert.fcw
+ can_sends.append(gmcan.create_acc_dashboard_command(self.packer_pt, CanBus.POWERTRAIN, CC.enabled,
+ hud_v_cruise * CV.MS_TO_KPH, hud_control, send_fcw))
+
+ # Radar needs to know current speed and yaw rate (50hz),
+ # and that ADAS is alive (10hz)
+ if not self.CP.radarUnavailable:
+ tt = self.frame * DT_CTRL
+ time_and_headlights_step = 10
+ if self.frame % time_and_headlights_step == 0:
+ idx = (self.frame // time_and_headlights_step) % 4
+ can_sends.append(gmcan.create_adas_time_status(CanBus.OBSTACLE, int((tt - self.start_time) * 60), idx))
+ can_sends.append(gmcan.create_adas_headlights_status(self.packer_obj, CanBus.OBSTACLE))
+
+ speed_and_accelerometer_step = 2
+ if self.frame % speed_and_accelerometer_step == 0:
+ idx = (self.frame // speed_and_accelerometer_step) % 4
+ can_sends.append(gmcan.create_adas_steering_status(CanBus.OBSTACLE, idx))
+ can_sends.append(gmcan.create_adas_accelerometer_speed_status(CanBus.OBSTACLE, CS.out.vEgo, idx))
+
+ if self.CP.networkLocation == NetworkLocation.gateway and self.frame % self.params.ADAS_KEEPALIVE_STEP == 0:
+ can_sends += gmcan.create_adas_keepalive(CanBus.POWERTRAIN)
+
+ else:
+ # While car is braking, cancel button causes ECM to enter a soft disable state with a fault status.
+ # A delayed cancellation allows camera to cancel and avoids a fault when user depresses brake quickly
+ self.cancel_counter = self.cancel_counter + 1 if CC.cruiseControl.cancel else 0
+
+ # Stock longitudinal, integrated at camera
+ if (self.frame - self.last_button_frame) * DT_CTRL > 0.04:
+ if self.cancel_counter > CAMERA_CANCEL_DELAY_FRAMES:
+ self.last_button_frame = self.frame
+ can_sends.append(gmcan.create_buttons(self.packer_pt, CanBus.CAMERA, CS.buttons_counter, CruiseButtons.CANCEL))
+
+ if self.CP.networkLocation == NetworkLocation.fwdCamera:
+ # Silence "Take Steering" alert sent by camera, forward PSCMStatus with HandsOffSWlDetectionStatus=1
+ if self.frame % 10 == 0:
+ can_sends.append(gmcan.create_pscm_status(self.packer_pt, CanBus.CAMERA, CS.pscm_status))
+
+ new_actuators = copy.copy(actuators)
+ new_actuators.steer = self.apply_steer_last / self.params.STEER_MAX
+ new_actuators.steerOutputCan = self.apply_steer_last
+ new_actuators.gas = self.apply_gas
+ new_actuators.brake = self.apply_brake
+
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/gm/carstate.py b/opendbc/car/gm/carstate.py
new file mode 100644
index 0000000000..318646bbe2
--- /dev/null
+++ b/opendbc/car/gm/carstate.py
@@ -0,0 +1,197 @@
+import copy
+from opendbc.can.can_define import CANDefine
+from opendbc.can.parser import CANParser
+from opendbc.car import create_button_events, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.common.numpy_fast import mean
+from opendbc.car.interfaces import CarStateBase
+from opendbc.car.gm.values import DBC, AccState, CanBus, CruiseButtons, STEER_THRESHOLD, SDGM_CAR
+
+ButtonType = structs.CarState.ButtonEvent.Type
+TransmissionType = structs.CarParams.TransmissionType
+NetworkLocation = structs.CarParams.NetworkLocation
+
+STANDSTILL_THRESHOLD = 10 * 0.0311 * CV.KPH_TO_MS
+
+BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise,
+ CruiseButtons.MAIN: ButtonType.altButton3, CruiseButtons.CANCEL: ButtonType.cancel}
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+ self.shifter_values = can_define.dv["ECMPRDNL2"]["PRNDL2"]
+ self.cluster_speed_hyst_gap = CV.KPH_TO_MS / 2.
+ self.cluster_min_speed = CV.KPH_TO_MS / 2.
+
+ self.loopback_lka_steering_cmd_updated = False
+ self.loopback_lka_steering_cmd_ts_nanos = 0
+ self.pt_lka_steering_cmd_counter = 0
+ self.cam_lka_steering_cmd_counter = 0
+ self.buttons_counter = 0
+
+ self.distance_button = 0
+
+ def update(self, pt_cp, cam_cp, _, __, loopback_cp) -> structs.CarState:
+ ret = structs.CarState()
+
+ prev_cruise_buttons = self.cruise_buttons
+ prev_distance_button = self.distance_button
+ self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtons"]
+ self.distance_button = pt_cp.vl["ASCMSteeringButton"]["DistanceButton"]
+ self.buttons_counter = pt_cp.vl["ASCMSteeringButton"]["RollingCounter"]
+ self.pscm_status = copy.copy(pt_cp.vl["PSCMStatus"])
+ # This is to avoid a fault where you engage while still moving backwards after shifting to D.
+ # An Equinox has been seen with an unsupported status (3), so only check if either wheel is in reverse (2)
+ self.moving_backward = (pt_cp.vl["EBCMWheelSpdRear"]["RLWheelDir"] == 2) or (pt_cp.vl["EBCMWheelSpdRear"]["RRWheelDir"] == 2)
+
+ # Variables used for avoiding LKAS faults
+ self.loopback_lka_steering_cmd_updated = len(loopback_cp.vl_all["ASCMLKASteeringCmd"]["RollingCounter"]) > 0
+ if self.loopback_lka_steering_cmd_updated:
+ self.loopback_lka_steering_cmd_ts_nanos = loopback_cp.ts_nanos["ASCMLKASteeringCmd"]["RollingCounter"]
+ if self.CP.networkLocation == NetworkLocation.fwdCamera:
+ self.pt_lka_steering_cmd_counter = pt_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"]
+ self.cam_lka_steering_cmd_counter = cam_cp.vl["ASCMLKASteeringCmd"]["RollingCounter"]
+
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ pt_cp.vl["EBCMWheelSpdFront"]["FLWheelSpd"],
+ pt_cp.vl["EBCMWheelSpdFront"]["FRWheelSpd"],
+ pt_cp.vl["EBCMWheelSpdRear"]["RLWheelSpd"],
+ pt_cp.vl["EBCMWheelSpdRear"]["RRWheelSpd"],
+ )
+ ret.vEgoRaw = mean([ret.wheelSpeeds.fl, ret.wheelSpeeds.fr, ret.wheelSpeeds.rl, ret.wheelSpeeds.rr])
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ # sample rear wheel speeds, standstill=True if ECM allows engagement with brake
+ ret.standstill = ret.wheelSpeeds.rl <= STANDSTILL_THRESHOLD and ret.wheelSpeeds.rr <= STANDSTILL_THRESHOLD
+
+ if pt_cp.vl["ECMPRDNL2"]["ManualMode"] == 1:
+ ret.gearShifter = self.parse_gear_shifter("T")
+ else:
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(pt_cp.vl["ECMPRDNL2"]["PRNDL2"], None))
+
+ ret.brake = pt_cp.vl["ECMAcceleratorPos"]["BrakePedalPos"]
+ if self.CP.networkLocation == NetworkLocation.fwdCamera:
+ ret.brakePressed = pt_cp.vl["ECMEngineStatus"]["BrakePressed"] != 0
+ else:
+ # Some Volt 2016-17 have loose brake pedal push rod retainers which causes the ECM to believe
+ # that the brake is being intermittently pressed without user interaction.
+ # To avoid a cruise fault we need to use a conservative brake position threshold
+ # https://static.nhtsa.gov/odi/tsbs/2017/MC-10137629-9999.pdf
+ ret.brakePressed = ret.brake >= 8
+
+ # Regen braking is braking
+ if self.CP.transmissionType == TransmissionType.direct:
+ ret.regenBraking = pt_cp.vl["EBCMRegenPaddle"]["RegenPaddle"] != 0
+
+ ret.gas = pt_cp.vl["AcceleratorPedal2"]["AcceleratorPedal2"] / 254.
+ ret.gasPressed = ret.gas > 1e-5
+
+ ret.steeringAngleDeg = pt_cp.vl["PSCMSteeringAngle"]["SteeringWheelAngle"]
+ ret.steeringRateDeg = pt_cp.vl["PSCMSteeringAngle"]["SteeringWheelRate"]
+ ret.steeringTorque = pt_cp.vl["PSCMStatus"]["LKADriverAppldTrq"]
+ ret.steeringTorqueEps = pt_cp.vl["PSCMStatus"]["LKATorqueDelivered"]
+ ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD
+
+ # 0 inactive, 1 active, 2 temporarily limited, 3 failed
+ self.lkas_status = pt_cp.vl["PSCMStatus"]["LKATorqueDeliveredStatus"]
+ ret.steerFaultTemporary = self.lkas_status == 2
+ ret.steerFaultPermanent = self.lkas_status == 3
+
+ # 1 - open, 0 - closed
+ ret.doorOpen = (pt_cp.vl["BCMDoorBeltStatus"]["FrontLeftDoor"] == 1 or
+ pt_cp.vl["BCMDoorBeltStatus"]["FrontRightDoor"] == 1 or
+ pt_cp.vl["BCMDoorBeltStatus"]["RearLeftDoor"] == 1 or
+ pt_cp.vl["BCMDoorBeltStatus"]["RearRightDoor"] == 1)
+
+ # 1 - latched
+ ret.seatbeltUnlatched = pt_cp.vl["BCMDoorBeltStatus"]["LeftSeatBelt"] == 0
+ ret.leftBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 1
+ ret.rightBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 2
+
+ ret.parkingBrake = pt_cp.vl["BCMGeneralPlatformStatus"]["ParkBrakeSwActive"] == 1
+ ret.cruiseState.available = pt_cp.vl["ECMEngineStatus"]["CruiseMainOn"] != 0
+ ret.espDisabled = pt_cp.vl["ESPStatus"]["TractionControlOn"] != 1
+ ret.accFaulted = (pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.FAULTED or
+ pt_cp.vl["EBCMFrictionBrakeStatus"]["FrictionBrakeUnavailable"] == 1)
+
+ ret.cruiseState.enabled = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] != AccState.OFF
+ ret.cruiseState.standstill = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.STANDSTILL
+ if self.CP.networkLocation == NetworkLocation.fwdCamera:
+ ret.cruiseState.speed = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCSpeedSetpoint"] * CV.KPH_TO_MS
+ if self.CP.carFingerprint not in SDGM_CAR:
+ ret.stockAeb = cam_cp.vl["AEBCmd"]["AEBCmdActive"] != 0
+ # openpilot controls nonAdaptive when not pcmCruise
+ if self.CP.pcmCruise:
+ ret.cruiseState.nonAdaptive = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCCruiseState"] not in (2, 3)
+
+ if self.CP.enableBsm:
+ ret.leftBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["LeftBSM"] == 1
+ ret.rightBlindspot = pt_cp.vl["BCMBlindSpotMonitor"]["RightBSM"] == 1
+
+ # Don't add event if transitioning from INIT, unless it's to an actual button
+ if self.cruise_buttons != CruiseButtons.UNPRESS or prev_cruise_buttons != CruiseButtons.INIT:
+ ret.buttonEvents = [
+ *create_button_events(self.cruise_buttons, prev_cruise_buttons, BUTTONS_DICT,
+ unpressed_btn=CruiseButtons.UNPRESS),
+ *create_button_events(self.distance_button, prev_distance_button,
+ {1: ButtonType.gapAdjustCruise})
+ ]
+
+ return ret
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ messages = []
+ if CP.networkLocation == NetworkLocation.fwdCamera:
+ messages += [
+ ("ASCMLKASteeringCmd", 10),
+ ("ASCMActiveCruiseControlStatus", 25),
+ ]
+ if CP.carFingerprint not in SDGM_CAR:
+ messages += [
+ ("AEBCmd", 10),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus.CAMERA)
+
+ @staticmethod
+ def get_can_parser(CP):
+ messages = [
+ ("BCMTurnSignals", 1),
+ ("ECMPRDNL2", 10),
+ ("PSCMStatus", 10),
+ ("ESPStatus", 10),
+ ("BCMDoorBeltStatus", 10),
+ ("BCMGeneralPlatformStatus", 10),
+ ("EBCMWheelSpdFront", 20),
+ ("EBCMWheelSpdRear", 20),
+ ("EBCMFrictionBrakeStatus", 20),
+ ("AcceleratorPedal2", 33),
+ ("ASCMSteeringButton", 33),
+ ("ECMEngineStatus", 100),
+ ("PSCMSteeringAngle", 100),
+ ("ECMAcceleratorPos", 80),
+ ]
+
+ if CP.enableBsm:
+ messages.append(("BCMBlindSpotMonitor", 10))
+
+ # Used to read back last counter sent to PT by camera
+ if CP.networkLocation == NetworkLocation.fwdCamera:
+ messages += [
+ ("ASCMLKASteeringCmd", 0),
+ ]
+
+ if CP.transmissionType == TransmissionType.direct:
+ messages.append(("EBCMRegenPaddle", 50))
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus.POWERTRAIN)
+
+ @staticmethod
+ def get_loopback_can_parser(CP):
+ messages = [
+ ("ASCMLKASteeringCmd", 0),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus.LOOPBACK)
diff --git a/opendbc/car/gm/fingerprints.py b/opendbc/car/gm/fingerprints.py
new file mode 100644
index 0000000000..971e83eb3d
--- /dev/null
+++ b/opendbc/car/gm/fingerprints.py
@@ -0,0 +1,67 @@
+# ruff: noqa: E501
+from opendbc.car.gm.values import CAR
+
+# Trailblazer also matches as a SILVERADO, TODO: split with fw versions
+# FIXME: There are Equinox users with different message lengths, specifically 304 and 320
+
+
+FINGERPRINTS = {
+ CAR.HOLDEN_ASTRA: [{
+ 190: 8, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 8, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 393: 8, 398: 8, 401: 8, 413: 8, 417: 8, 419: 8, 422: 1, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 8, 455: 7, 456: 8, 458: 5, 479: 8, 481: 7, 485: 8, 489: 8, 497: 8, 499: 3, 500: 8, 501: 8, 508: 8, 528: 5, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 647: 5, 707: 8, 715: 8, 723: 8, 753: 5, 761: 7, 806: 1, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 961: 8, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1009: 8, 1011: 6, 1017: 8, 1019: 3, 1020: 8, 1105: 6, 1217: 8, 1221: 5, 1225: 8, 1233: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 8, 1280: 4, 1300: 8, 1328: 4, 1417: 8, 1906: 7, 1907: 7, 1908: 7, 1912: 7, 1919: 7
+ }],
+ CAR.CHEVROLET_VOLT: [{
+ 170: 8, 171: 8, 189: 7, 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 288: 5, 289: 8, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 389: 2, 390: 7, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 528: 4, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 566: 5, 567: 3, 568: 1, 573: 1, 577: 8, 647: 3, 707: 8, 711: 6, 715: 8, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 961: 8, 969: 8, 977: 8, 979: 7, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1017: 8, 1019: 2, 1020: 8, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1273: 3, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1601: 8, 1905: 7, 1906: 7, 1907: 7, 1910: 7, 1912: 7, 1922: 7, 1927: 7, 1928: 7, 2016: 8, 2020: 8, 2024: 8, 2028: 8
+ },
+ {
+ 170: 8, 171: 8, 189: 7, 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 288: 5, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 389: 2, 390: 7, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 528: 4, 532: 6, 546: 7, 550: 8, 554: 3, 558: 8, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 566: 5, 567: 3, 568: 1, 573: 1, 577: 8, 578: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 715: 8, 717: 5, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 988: 6, 989: 8, 995: 7, 1001: 8, 1005: 6, 1009: 8, 1017: 8, 1019: 2, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1273: 3, 1275: 3, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1516: 8, 1601: 8, 1618: 8, 1905: 7, 1906: 7, 1907: 7, 1910: 7, 1912: 7, 1922: 7, 1927: 7, 1930: 7, 2016: 8, 2018: 8, 2020: 8, 2024: 8, 2028: 8
+ },
+ {
+ 170: 8, 171: 8, 189: 7, 190: 6, 192: 5, 193: 8, 197: 8, 199: 4, 201: 6, 209: 7, 211: 2, 241: 6, 288: 5, 289: 1, 290: 1, 298: 2, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 368: 8, 381: 2, 384: 8, 386: 5, 388: 8, 389: 2, 390: 7, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 8, 479: 3, 481: 7, 485: 8, 489: 5, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 3, 508: 8, 512: 3, 528: 4, 530: 8, 532: 6, 537: 5, 539: 8, 542: 7, 546: 7, 550: 8, 554: 3, 558: 8, 560: 6, 562: 4, 563: 5, 564: 5, 565: 5, 566: 5, 567: 3, 568: 1, 573: 1, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 711: 6, 761: 7, 810: 8, 821: 4, 823: 7, 832: 8, 840: 5, 842: 5, 844: 8, 853: 8, 866: 4, 961: 8, 967: 4, 969: 8, 977: 8, 979: 7, 988: 6, 989: 8, 995: 7, 1001: 5, 1003: 5, 1005: 6, 1009: 8, 1017: 8, 1019: 2, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1273: 3, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1905: 7, 1906: 7, 1907: 7, 1910: 7, 1912: 7, 1922: 7, 1927: 7
+ }],
+ CAR.BUICK_LACROSSE: [{
+ 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 353: 3, 381: 6, 386: 8, 388: 8, 393: 7, 398: 8, 407: 7, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 455: 7, 456: 8, 463: 3, 479: 3, 481: 7, 485: 8, 487: 8, 489: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 510: 8, 528: 5, 532: 6, 534: 2, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 573: 1, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 5, 707: 8, 753: 5, 761: 7, 801: 8, 804: 3, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 872: 1, 882: 8, 890: 1, 892: 2, 893: 1, 894: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1011: 6, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1217: 8, 1221: 5, 1223: 2, 1225: 7, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1417: 8, 1609: 8, 1613: 8, 1649: 8, 1792: 8, 1798: 8, 1824: 8, 1825: 8, 1840: 8, 1842: 8, 1858: 8, 1860: 8, 1863: 8, 1872: 8, 1875: 8, 1882: 8, 1888: 8, 1889: 8, 1892: 8, 1904: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1914: 7, 1916: 7, 1918: 7, 1919: 7, 1937: 8, 1953: 8, 1968: 8, 2001: 8, 2017: 8, 2018: 8, 2020: 8, 2026: 8
+ }],
+ CAR.BUICK_REGAL: [{
+ 190: 8, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 8, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 393: 7, 398: 8, 407: 7, 413: 8, 417: 8, 419: 8, 422: 4, 426: 8, 431: 8, 442: 8, 451: 8, 452: 8, 453: 8, 455: 7, 456: 8, 463: 3, 479: 8, 481: 7, 485: 8, 487: 8, 489: 8, 495: 8, 497: 8, 499: 3, 500: 8, 501: 8, 508: 8, 528: 5, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 569: 3, 573: 1, 577: 8, 578: 8, 579: 8, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 882: 8, 884: 8, 890: 1, 892: 2, 893: 2, 894: 1, 961: 8, 967: 8, 969: 8, 977: 8, 979: 8, 985: 8, 1001: 8, 1005: 6, 1009: 8, 1011: 8, 1013: 3, 1017: 8, 1020: 8, 1024: 8, 1025: 8, 1026: 8, 1027: 8, 1028: 8, 1029: 8, 1030: 8, 1031: 8, 1032: 2, 1033: 7, 1034: 7, 1105: 6, 1217: 8, 1221: 5, 1223: 8, 1225: 7, 1233: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 8, 1263: 8, 1265: 8, 1267: 8, 1271: 8, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1328: 4, 1417: 8, 1601: 8, 1602: 8, 1603: 7, 1611: 8, 1618: 8, 1906: 8, 1907: 7, 1912: 7, 1914: 7, 1916: 7, 1919: 7, 1930: 7, 2016: 8, 2018: 8, 2019: 8, 2024: 8, 2026: 8
+ }],
+ CAR.CADILLAC_ATS: [{
+ 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 368: 3, 381: 6, 384: 4, 386: 8, 388: 8, 393: 7, 398: 8, 401: 8, 407: 7, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 455: 7, 456: 8, 462: 4, 479: 3, 481: 7, 485: 8, 487: 8, 489: 8, 491: 2, 493: 8, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 510: 8, 528: 5, 532: 6, 534: 2, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 719: 5, 723: 2, 753: 5, 761: 7, 801: 8, 804: 3, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 882: 8, 890: 1, 892: 2, 893: 2, 894: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1011: 6, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1233: 8, 1241: 3, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1271: 8, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1601: 8, 1904: 7, 1906: 7, 1907: 7, 1912: 7, 1916: 7, 1917: 7, 1918: 7, 1919: 7, 1920: 7, 1930: 7, 2016: 8, 2024: 8
+ }],
+ CAR.CHEVROLET_MALIBU: [{
+ 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 393: 7, 398: 8, 407: 7, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 455: 7, 456: 8, 479: 3, 481: 7, 485: 8, 487: 8, 489: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 510: 8, 528: 5, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 961: 8, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1217: 8, 1221: 5, 1223: 2, 1225: 7, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1601: 8, 1906: 7, 1907: 7, 1912: 7, 1919: 7, 1930: 7, 2016: 8, 2024: 8
+ }],
+ CAR.GMC_ACADIA: [{
+ 190: 6, 192: 5, 193: 8, 197: 8, 199: 4, 201: 6, 208: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 289: 1, 290: 1, 298: 8, 304: 8, 309: 8, 313: 8, 320: 8, 322: 7, 328: 1, 352: 7, 368: 8, 381: 8, 384: 8, 386: 8, 388: 8, 393: 8, 398: 8, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 454: 8, 455: 7, 458: 8, 460: 4, 462: 4, 463: 3, 479: 3, 481: 7, 485: 8, 489: 5, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 510: 8, 512: 3, 530: 8, 532: 6, 534: 2, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 567: 5, 568: 2, 573: 1, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 801: 8, 803: 8, 804: 3, 805: 8, 832: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 961: 8, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1003: 5, 1005: 6, 1009: 8, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1217: 8, 1221: 5, 1225: 8, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1328: 4, 1417: 8, 1906: 7, 1907: 7, 1912: 7, 1914: 7, 1918: 7, 1919: 7, 1920: 7, 1930: 7
+ },
+ {
+ 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 208: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 289: 8, 298: 8, 304: 1, 309: 8, 313: 8, 320: 3, 322: 7, 328: 1, 338: 6, 340: 6, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 393: 8, 398: 8, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 454: 8, 455: 7, 462: 4, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 510: 8, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 567: 5, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 961: 8, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1217: 8, 1221: 5, 1225: 8, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1328: 4, 1417: 8, 1601: 8, 1906: 7, 1907: 7, 1912: 7, 1914: 7, 1919: 7, 1920: 7, 1930: 7, 2016: 8, 2024: 8
+ }],
+ CAR.CADILLAC_ESCALADE: [{
+ 170: 8, 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 208: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 393: 7, 398: 8, 407: 4, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 454: 8, 455: 7, 460: 5, 462: 4, 463: 3, 479: 3, 481: 7, 485: 8, 487: 8, 489: 8, 497: 8, 499: 3, 500: 6, 501: 8, 508: 8, 510: 8, 532: 6, 534: 2, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 573: 1, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 719: 5, 761: 7, 801: 8, 804: 3, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 961: 8, 967: 4, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1017: 8, 1019: 2, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1217: 8, 1221: 5, 1223: 2, 1225: 7, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1609: 8, 1613: 8, 1649: 8, 1792: 8, 1798: 8, 1824: 8, 1825: 8, 1840: 8, 1842: 8, 1858: 8, 1860: 8, 1863: 8, 1872: 8, 1875: 8, 1882: 8, 1888: 8, 1889: 8, 1892: 8, 1906: 7, 1907: 7, 1912: 7, 1914: 7, 1917: 7, 1918: 7, 1919: 7, 1920: 7, 1930: 7, 1937: 8, 1953: 8, 1968: 8, 2001: 8, 2017: 8, 2018: 8, 2020: 8, 2026: 8
+ }],
+ CAR.CADILLAC_ESCALADE_ESV: [{
+ 309: 1, 848: 8, 849: 8, 850: 8, 851: 8, 852: 8, 853: 8, 854: 3, 1056: 6, 1057: 8, 1058: 8, 1059: 8, 1060: 8, 1061: 8, 1062: 8, 1063: 8, 1064: 8, 1065: 8, 1066: 8, 1067: 8, 1068: 8, 1120: 8, 1121: 8, 1122: 8, 1123: 8, 1124: 8, 1125: 8, 1126: 8, 1127: 8, 1128: 8, 1129: 8, 1130: 8, 1131: 8, 1132: 8, 1133: 8, 1134: 8, 1135: 8, 1136: 8, 1137: 8, 1138: 8, 1139: 8, 1140: 8, 1141: 8, 1142: 8, 1143: 8, 1146: 8, 1147: 8, 1148: 8, 1149: 8, 1150: 8, 1151: 8, 1216: 8, 1217: 8, 1218: 8, 1219: 8, 1220: 8, 1221: 8, 1222: 8, 1223: 8, 1224: 8, 1225: 8, 1226: 8, 1232: 8, 1233: 8, 1234: 8, 1235: 8, 1236: 8, 1237: 8, 1238: 8, 1239: 8, 1240: 8, 1241: 8, 1242: 8, 1787: 8, 1788: 8
+ }],
+ CAR.CADILLAC_ESCALADE_ESV_2019: [{
+ 715: 8, 840: 5, 717: 5, 869: 4, 880: 6, 289: 8, 454: 8, 842: 5, 460: 5, 463: 3, 801: 8, 170: 8, 190: 6, 241: 6, 201: 8, 417: 7, 211: 2, 419: 1, 398: 8, 426: 7, 487: 8, 442: 8, 451: 8, 452: 8, 453: 6, 479: 3, 311: 8, 500: 6, 647: 6, 193: 8, 707: 8, 197: 8, 209: 7, 199: 4, 455: 7, 313: 8, 481: 7, 485: 8, 489: 8, 249: 8, 393: 7, 407: 7, 413: 8, 422: 4, 431: 8, 501: 8, 499: 3, 810: 8, 508: 8, 381: 8, 462: 4, 532: 6, 562: 8, 386: 8, 761: 7, 573: 1, 554: 3, 719: 5, 560: 8, 1279: 4, 388: 8, 288: 5, 1005: 6, 497: 8, 844: 8, 961: 8, 967: 4, 977: 8, 979: 8, 985: 5, 1001: 8, 1017: 8, 1019: 2, 1020: 8, 1217: 8, 510: 8, 866: 4, 304: 1, 969: 8, 384: 4, 1033: 7, 1009: 8, 1034: 7, 1296: 4, 1930: 7, 1105: 5, 1013: 5, 1225: 7, 1919: 7, 320: 3, 534: 2, 352: 5, 298: 8, 1223: 2, 1233: 8, 608: 8, 1265: 8, 609: 6, 1267: 1, 1417: 8, 610: 6, 1906: 7, 611: 6, 612: 8, 613: 8, 208: 8, 564: 5, 309: 8, 1221: 5, 1280: 4, 1249: 8, 1907: 7, 1257: 6, 1300: 8, 1920: 7, 563: 5, 1322: 6, 1323: 4, 1328: 4, 1917: 7, 328: 1, 1912: 7, 1914: 7, 804: 3, 1918: 7
+ }],
+ CAR.CHEVROLET_BOLT_EUV: [{
+ 189: 7, 190: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 3, 241: 6, 257: 8, 288: 5, 289: 8, 298: 8, 304: 3, 309: 8, 311: 8, 313: 8, 320: 4, 322: 7, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 451: 8, 452: 8, 453: 6, 458: 5, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 528: 5, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 566: 8, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 869: 4, 880: 6, 977: 8, 1001: 8, 1017: 8, 1020: 8, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1265: 8, 1280: 4, 1296: 4, 1300: 8, 1611: 8, 1930: 7
+ }],
+ CAR.CHEVROLET_SILVERADO: [{
+ 190: 6, 193: 8, 197: 8, 201: 8, 208: 8, 209: 7, 211: 2, 241: 6, 249: 8, 257: 8, 288: 5, 289: 8, 298: 8, 304: 3, 309: 8, 311: 8, 313: 8, 320: 4, 322: 7, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 413: 8, 451: 8, 452: 8, 453: 6, 455: 7, 460: 5, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 528: 5, 532: 6, 534: 2, 560: 8, 562: 8, 563: 5, 565: 5, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 761: 7, 789: 5, 800: 6, 801: 8, 810: 8, 840: 5, 842: 5, 844: 8, 848: 4, 869: 4, 880: 6, 977: 8, 1001: 8, 1011: 6, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1271: 8, 1280: 4, 1296: 4, 1300: 8, 1611: 8, 1930: 7
+ }],
+ CAR.CHEVROLET_EQUINOX: [{
+ 190: 6, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 257: 8, 288: 5, 289: 8, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 8, 384: 4, 386: 8, 388: 8, 413: 8, 451: 8, 452: 8, 453: 6, 455: 7, 463: 3, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 500: 6, 501: 8, 510: 8, 528: 5, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 789: 5, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4, 880: 6, 977: 8, 1001: 8, 1011: 6, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1271: 8, 1280: 4, 1296: 4, 1300: 8, 1611: 8, 1930: 7
+ },
+ {
+ 190: 6, 201: 8, 211: 2, 717: 5, 241: 6, 451: 8, 298: 8, 452: 8, 453: 6, 479: 3, 485: 8, 249: 8, 500: 6, 587: 8, 1611: 8, 289: 8, 481: 7, 193: 8, 197: 8, 209: 7, 455: 7, 489: 8, 309: 8, 413: 8, 501: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 311: 8, 510: 8, 528: 5, 532: 6, 715: 8, 560: 8, 562: 8, 707: 8, 789: 5, 869: 4, 880: 6, 761: 7, 840: 5, 842: 5, 844: 8, 313: 8, 381: 8, 386: 8, 810: 8, 322: 7, 384: 4, 800: 6, 1033: 7, 1034: 7, 1296: 4, 753: 5, 388: 8, 288: 5, 497: 8, 463: 3, 304: 3, 977: 8, 1001: 8, 1280: 4, 320: 4, 352: 5, 563: 5, 565: 5, 1221: 5, 1011: 6, 1017: 8, 1020: 8, 1249: 8, 1300: 8, 328: 1, 1217: 8, 1233: 8, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1930: 7, 1271: 8
+ }],
+ CAR.CADILLAC_XT4: [
+ {
+ 190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 257: 8, 288: 5, 289: 8, 292: 2, 298: 8, 304: 3, 309: 8, 313: 8, 320: 4, 322: 7, 328: 1, 331: 3, 352: 5, 353: 3, 368: 3, 381: 8, 384: 4, 386: 8, 388: 8, 393: 7, 398: 8, 401: 8, 407: 7, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 455: 7, 479: 3, 481: 7, 485: 8, 489: 8, 497: 8, 499: 3, 500: 6, 501: 8, 503: 2, 508: 8, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 573: 1, 577: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 6, 707: 8, 715: 8, 717: 5, 719: 5, 761: 7, 806: 1, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 872: 1, 880: 6, 961: 8, 969: 8, 975: 2, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1011: 6, 1013: 5, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1037: 5, 1105: 5, 1187: 5, 1195: 3, 1217: 8, 1221: 5, 1223: 2, 1225: 7, 1233: 8, 1236: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1268: 2, 1271: 8, 1273: 3, 1276: 2, 1277: 7, 1278: 4, 1279: 4, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1345: 8, 1417: 8, 1512: 8, 1517: 8, 1601: 8, 1609: 8, 1613: 8, 1649: 8, 1792: 8, 1793: 8, 1798: 8, 1824: 8, 1825: 8, 1840: 8, 1842: 8, 1858: 8, 1860: 8, 1863: 8, 1872: 8, 1875: 8, 1882: 8, 1888: 8, 1889: 8, 1892: 8, 1906: 7, 1907: 7, 1912: 7, 1919: 7, 1920: 8, 1924: 8, 1930: 7, 1937: 8, 1953: 8, 1968: 8, 1969: 8, 1971: 8, 1975: 8, 1984: 8, 1988: 8, 2000: 8, 2001: 8, 2002: 8, 2016: 8, 2017: 8, 2018: 8, 2020: 8, 2021: 8, 2024: 8, 2026: 8
+ }],
+}
+
+FW_VERSIONS: dict[str, dict[tuple, list[bytes]]] = {
+}
diff --git a/opendbc/car/gm/gmcan.py b/opendbc/car/gm/gmcan.py
new file mode 100644
index 0000000000..6c583fe336
--- /dev/null
+++ b/opendbc/car/gm/gmcan.py
@@ -0,0 +1,173 @@
+from opendbc.car.can_definitions import CanData
+from opendbc.car.gm.values import CAR
+
+
+def create_buttons(packer, bus, idx, button):
+ values = {
+ "ACCButtons": button,
+ "RollingCounter": idx,
+ "ACCAlwaysOne": 1,
+ "DistanceButton": 0,
+ }
+
+ checksum = 240 + int(values["ACCAlwaysOne"] * 0xf)
+ checksum += values["RollingCounter"] * (0x4ef if values["ACCAlwaysOne"] != 0 else 0x3f0)
+ checksum -= int(values["ACCButtons"] - 1) << 4 # not correct if value is 0
+ checksum -= 2 * values["DistanceButton"]
+
+ values["SteeringButtonChecksum"] = checksum
+ return packer.make_can_msg("ASCMSteeringButton", bus, values)
+
+
+def create_pscm_status(packer, bus, pscm_status):
+ values = {s: pscm_status[s] for s in [
+ "HandsOffSWDetectionMode",
+ "HandsOffSWlDetectionStatus",
+ "LKATorqueDeliveredStatus",
+ "LKADriverAppldTrq",
+ "LKATorqueDelivered",
+ "LKATotalTorqueDelivered",
+ "RollingCounter",
+ "PSCMStatusChecksum",
+ ]}
+ checksum_mod = int(1 - values["HandsOffSWlDetectionStatus"]) << 5
+ values["HandsOffSWlDetectionStatus"] = 1
+ values["PSCMStatusChecksum"] += checksum_mod
+ return packer.make_can_msg("PSCMStatus", bus, values)
+
+
+def create_steering_control(packer, bus, apply_steer, idx, lkas_active):
+ values = {
+ "LKASteeringCmdActive": lkas_active,
+ "LKASteeringCmd": apply_steer,
+ "RollingCounter": idx,
+ "LKASteeringCmdChecksum": 0x1000 - (lkas_active << 11) - (apply_steer & 0x7ff) - idx
+ }
+
+ return packer.make_can_msg("ASCMLKASteeringCmd", bus, values)
+
+
+def create_adas_keepalive(bus):
+ dat = b"\x00\x00\x00\x00\x00\x00\x00"
+ return [CanData(0x409, dat, bus), CanData(0x40a, dat, bus)]
+
+
+def create_gas_regen_command(packer, bus, throttle, idx, enabled, at_full_stop):
+ values = {
+ "GasRegenCmdActive": enabled,
+ "RollingCounter": idx,
+ "GasRegenCmdActiveInv": 1 - enabled,
+ "GasRegenCmd": throttle,
+ "GasRegenFullStopActive": at_full_stop,
+ "GasRegenAlwaysOne": 1,
+ "GasRegenAlwaysOne2": 1,
+ "GasRegenAlwaysOne3": 1,
+ }
+
+ dat = packer.make_can_msg("ASCMGasRegenCmd", bus, values)[1]
+ values["GasRegenChecksum"] = (((0xff - dat[1]) & 0xff) << 16) | \
+ (((0xff - dat[2]) & 0xff) << 8) | \
+ ((0x100 - dat[3] - idx) & 0xff)
+
+ return packer.make_can_msg("ASCMGasRegenCmd", bus, values)
+
+
+def create_friction_brake_command(packer, bus, apply_brake, idx, enabled, near_stop, at_full_stop, CP):
+ mode = 0x1
+
+ # TODO: Understand this better. Volts and ICE Camera ACC cars are 0x1 when enabled with no brake
+ if enabled and CP.carFingerprint in (CAR.CHEVROLET_BOLT_EUV,):
+ mode = 0x9
+
+ if apply_brake > 0:
+ mode = 0xa
+ if at_full_stop:
+ mode = 0xd
+
+ # TODO: this is to have GM bringing the car to complete stop,
+ # but currently it conflicts with OP controls, so turned off. Not set by all cars
+ #elif near_stop:
+ # mode = 0xb
+
+ brake = (0x1000 - apply_brake) & 0xfff
+ checksum = (0x10000 - (mode << 12) - brake - idx) & 0xffff
+
+ values = {
+ "RollingCounter": idx,
+ "FrictionBrakeMode": mode,
+ "FrictionBrakeChecksum": checksum,
+ "FrictionBrakeCmd": -apply_brake
+ }
+
+ return packer.make_can_msg("EBCMFrictionBrakeCmd", bus, values)
+
+
+def create_acc_dashboard_command(packer, bus, enabled, target_speed_kph, hud_control, fcw):
+ target_speed = min(target_speed_kph, 255)
+
+ values = {
+ "ACCAlwaysOne": 1,
+ "ACCResumeButton": 0,
+ "ACCSpeedSetpoint": target_speed,
+ "ACCGapLevel": hud_control.leadDistanceBars * enabled, # 3 "far", 0 "inactive"
+ "ACCCmdActive": enabled,
+ "ACCAlwaysOne2": 1,
+ "ACCLeadCar": hud_control.leadVisible,
+ "FCWAlert": 0x3 if fcw else 0
+ }
+
+ return packer.make_can_msg("ASCMActiveCruiseControlStatus", bus, values)
+
+
+def create_adas_time_status(bus, tt, idx):
+ dat = [(tt >> 20) & 0xff, (tt >> 12) & 0xff, (tt >> 4) & 0xff,
+ ((tt & 0xf) << 4) + (idx << 2)]
+ chksum = 0x1000 - dat[0] - dat[1] - dat[2] - dat[3]
+ chksum = chksum & 0xfff
+ dat += [0x40 + (chksum >> 8), chksum & 0xff, 0x12]
+ return CanData(0xa1, bytes(dat), bus)
+
+
+def create_adas_steering_status(bus, idx):
+ dat = [idx << 6, 0xf0, 0x20, 0, 0, 0]
+ chksum = 0x60 + sum(dat)
+ dat += [chksum >> 8, chksum & 0xff]
+ return CanData(0x306, bytes(dat), bus)
+
+
+def create_adas_accelerometer_speed_status(bus, speed_ms, idx):
+ spd = int(speed_ms * 16) & 0xfff
+ accel = 0 & 0xfff
+ # 0 if in park/neutral, 0x10 if in reverse, 0x08 for D/L
+ #stick = 0x08
+ near_range_cutoff = 0x27
+ near_range_mode = 1 if spd <= near_range_cutoff else 0
+ far_range_mode = 1 - near_range_mode
+ dat = [0x08, spd >> 4, ((spd & 0xf) << 4) | (accel >> 8), accel & 0xff, 0]
+ chksum = 0x62 + far_range_mode + (idx << 2) + dat[0] + dat[1] + dat[2] + dat[3] + dat[4]
+ dat += [(idx << 5) + (far_range_mode << 4) + (near_range_mode << 3) + (chksum >> 8), chksum & 0xff]
+ return CanData(0x308, bytes(dat), bus)
+
+
+def create_adas_headlights_status(packer, bus):
+ values = {
+ "Always42": 0x42,
+ "Always4": 0x4,
+ }
+ return packer.make_can_msg("ASCMHeadlight", bus, values)
+
+
+def create_lka_icon_command(bus, active, critical, steer):
+ if active and steer == 1:
+ if critical:
+ dat = b"\x50\xc0\x14"
+ else:
+ dat = b"\x50\x40\x18"
+ elif active:
+ if critical:
+ dat = b"\x40\xc0\x14"
+ else:
+ dat = b"\x40\x40\x18"
+ else:
+ dat = b"\x00\x00\x00"
+ return CanData(0x104c006c, dat, bus)
diff --git a/opendbc/car/gm/interface.py b/opendbc/car/gm/interface.py
new file mode 100755
index 0000000000..25dc2c85de
--- /dev/null
+++ b/opendbc/car/gm/interface.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3
+import os
+from math import fabs, exp
+from panda import Panda
+
+from opendbc.car import get_safety_config, get_friction, structs
+from opendbc.car.common.basedir import BASEDIR
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.gm.radar_interface import RADAR_HEADER_MSG
+from opendbc.car.gm.values import CAR, CarControllerParams, EV_CAR, CAMERA_ACC_CAR, SDGM_CAR, CanBus
+from opendbc.car.interfaces import CarInterfaceBase, TorqueFromLateralAccelCallbackType, FRICTION_THRESHOLD, LatControlInputs, NanoFFModel
+
+TransmissionType = structs.CarParams.TransmissionType
+NetworkLocation = structs.CarParams.NetworkLocation
+
+NON_LINEAR_TORQUE_PARAMS = {
+ CAR.CHEVROLET_BOLT_EUV: [2.6531724862969748, 1.0, 0.1919764879840985, 0.009054123646805178],
+ CAR.GMC_ACADIA: [4.78003305, 1.0, 0.3122, 0.05591772],
+ CAR.CHEVROLET_SILVERADO: [3.29974374, 1.0, 0.25571356, 0.0465122]
+}
+
+NEURAL_PARAMS_PATH = os.path.join(BASEDIR, 'torque_data/neural_ff_weights.json')
+
+
+class CarInterface(CarInterfaceBase):
+ @staticmethod
+ def get_pid_accel_limits(CP, current_speed, cruise_speed):
+ return CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX
+
+ # Determined by iteratively plotting and minimizing error for f(angle, speed) = steer.
+ @staticmethod
+ def get_steer_feedforward_volt(desired_angle, v_ego):
+ desired_angle *= 0.02904609
+ sigmoid = desired_angle / (1 + fabs(desired_angle))
+ return 0.10006696 * sigmoid * (v_ego + 3.12485927)
+
+ def get_steer_feedforward_function(self):
+ if self.CP.carFingerprint == CAR.CHEVROLET_VOLT:
+ return self.get_steer_feedforward_volt
+ else:
+ return CarInterfaceBase.get_steer_feedforward_default
+
+ def torque_from_lateral_accel_siglin(self, latcontrol_inputs: LatControlInputs, torque_params: structs.CarParams.LateralTorqueTuning,
+ lateral_accel_error: float, lateral_accel_deadzone: float, friction_compensation: bool, gravity_adjusted: bool) -> float:
+ friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation)
+
+ def sig(val):
+ # https://timvieira.github.io/blog/post/2014/02/11/exp-normalize-trick
+ if val >= 0:
+ return 1 / (1 + exp(-val)) - 0.5
+ else:
+ z = exp(val)
+ return z / (1 + z) - 0.5
+
+ # The "lat_accel vs torque" relationship is assumed to be the sum of "sigmoid + linear" curves
+ # An important thing to consider is that the slope at 0 should be > 0 (ideally >1)
+ # This has big effect on the stability about 0 (noise when going straight)
+ # ToDo: To generalize to other GMs, explore tanh function as the nonlinear
+ non_linear_torque_params = NON_LINEAR_TORQUE_PARAMS.get(self.CP.carFingerprint) # type: ignore[call-overload]
+ assert non_linear_torque_params, "The params are not defined"
+ a, b, c, _ = non_linear_torque_params
+ steer_torque = (sig(latcontrol_inputs.lateral_acceleration * a) * b) + (latcontrol_inputs.lateral_acceleration * c)
+ return float(steer_torque) + friction
+
+ def torque_from_lateral_accel_neural(self, latcontrol_inputs: LatControlInputs, torque_params: structs.CarParams.LateralTorqueTuning,
+ lateral_accel_error: float, lateral_accel_deadzone: float, friction_compensation: bool, gravity_adjusted: bool) -> float:
+ friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation)
+ inputs = list(latcontrol_inputs)
+ if gravity_adjusted:
+ inputs[0] += inputs[1]
+ return float(self.neural_ff_model.predict(inputs)) + friction
+
+ def torque_from_lateral_accel(self) -> TorqueFromLateralAccelCallbackType:
+ if self.CP.carFingerprint == CAR.CHEVROLET_BOLT_EUV:
+ self.neural_ff_model = NanoFFModel(NEURAL_PARAMS_PATH, self.CP.carFingerprint)
+ return self.torque_from_lateral_accel_neural
+ elif self.CP.carFingerprint in NON_LINEAR_TORQUE_PARAMS:
+ return self.torque_from_lateral_accel_siglin
+ else:
+ return self.torque_from_lateral_accel_linear
+
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "gm"
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.gm)]
+ ret.autoResumeSng = False
+ ret.enableBsm = 0x142 in fingerprint[CanBus.POWERTRAIN]
+
+ if candidate in EV_CAR:
+ ret.transmissionType = TransmissionType.direct
+ else:
+ ret.transmissionType = TransmissionType.automatic
+
+ ret.longitudinalTuning.kiBP = [5., 35.]
+
+ if candidate in (CAMERA_ACC_CAR | SDGM_CAR):
+ ret.experimentalLongitudinalAvailable = candidate not in SDGM_CAR
+ ret.networkLocation = NetworkLocation.fwdCamera
+ ret.radarUnavailable = True # no radar
+ ret.pcmCruise = True
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_HW_CAM
+ ret.minEnableSpeed = -1 if candidate in SDGM_CAR else 5 * CV.KPH_TO_MS
+ ret.minSteerSpeed = 10 * CV.KPH_TO_MS
+
+ # Tuning for experimental long
+ ret.longitudinalTuning.kiV = [2.0, 1.5]
+ ret.stoppingDecelRate = 2.0 # reach brake quickly after enabling
+ ret.vEgoStopping = 0.25
+ ret.vEgoStarting = 0.25
+
+ if experimental_long:
+ ret.pcmCruise = False
+ ret.openpilotLongitudinalControl = True
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_GM_HW_CAM_LONG
+
+ else: # ASCM, OBD-II harness
+ ret.openpilotLongitudinalControl = True
+ ret.networkLocation = NetworkLocation.gateway
+ ret.radarUnavailable = RADAR_HEADER_MSG not in fingerprint[CanBus.OBSTACLE] and not docs
+ ret.pcmCruise = False # stock non-adaptive cruise control is kept off
+ # supports stop and go, but initial engage must (conservatively) be above 18mph
+ ret.minEnableSpeed = 18 * CV.MPH_TO_MS
+ ret.minSteerSpeed = 7 * CV.MPH_TO_MS
+
+ # Tuning
+ ret.longitudinalTuning.kiV = [2.4, 1.5]
+
+ # These cars have been put into dashcam only due to both a lack of users and test coverage.
+ # These cars likely still work fine. Once a user confirms each car works and a test route is
+ # added to opendbc/car/tests/routes.py, we can remove it from this list.
+ ret.dashcamOnly = candidate in {CAR.CADILLAC_ATS, CAR.HOLDEN_ASTRA, CAR.CHEVROLET_MALIBU, CAR.BUICK_REGAL} or \
+ (ret.networkLocation == NetworkLocation.gateway and ret.radarUnavailable)
+
+ # Start with a baseline tuning for all GM vehicles. Override tuning as needed in each model section below.
+ ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.00]]
+ ret.lateralTuning.pid.kf = 0.00004 # full torque for 20 deg at 80mph means 0.00007818594
+ ret.steerActuatorDelay = 0.1 # Default delay, not measured yet
+
+ ret.steerLimitTimer = 0.4
+ ret.radarTimeStep = 0.0667 # GM radar runs at 15Hz instead of standard 20Hz
+ ret.longitudinalActuatorDelay = 0.5 # large delay to initially start braking
+
+ if candidate == CAR.CHEVROLET_VOLT:
+ ret.lateralTuning.pid.kpBP = [0., 40.]
+ ret.lateralTuning.pid.kpV = [0., 0.17]
+ ret.lateralTuning.pid.kiBP = [0.]
+ ret.lateralTuning.pid.kiV = [0.]
+ ret.lateralTuning.pid.kf = 1. # get_steer_feedforward_volt()
+ ret.steerActuatorDelay = 0.2
+
+ elif candidate == CAR.GMC_ACADIA:
+ ret.minEnableSpeed = -1. # engage speed is decided by pcm
+ ret.steerActuatorDelay = 0.2
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ elif candidate == CAR.BUICK_LACROSSE:
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ elif candidate == CAR.CADILLAC_ESCALADE:
+ ret.minEnableSpeed = -1. # engage speed is decided by pcm
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ elif candidate in (CAR.CADILLAC_ESCALADE_ESV, CAR.CADILLAC_ESCALADE_ESV_2019):
+ ret.minEnableSpeed = -1. # engage speed is decided by pcm
+
+ if candidate == CAR.CADILLAC_ESCALADE_ESV:
+ ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[10., 41.0], [10., 41.0]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.13, 0.24], [0.01, 0.02]]
+ ret.lateralTuning.pid.kf = 0.000045
+ else:
+ ret.steerActuatorDelay = 0.2
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ elif candidate == CAR.CHEVROLET_BOLT_EUV:
+ ret.steerActuatorDelay = 0.2
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ elif candidate == CAR.CHEVROLET_SILVERADO:
+ # On the Bolt, the ECM and camera independently check that you are either above 5 kph or at a stop
+ # with foot on brake to allow engagement, but this platform only has that check in the camera.
+ # TODO: check if this is split by EV/ICE with more platforms in the future
+ if ret.openpilotLongitudinalControl:
+ ret.minEnableSpeed = -1.
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ elif candidate == CAR.CHEVROLET_EQUINOX:
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ elif candidate == CAR.CHEVROLET_TRAILBLAZER:
+ ret.steerActuatorDelay = 0.2
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ elif candidate == CAR.CADILLAC_XT4:
+ ret.steerActuatorDelay = 0.2
+ ret.minSteerSpeed = 30 * CV.MPH_TO_MS
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ return ret
diff --git a/opendbc/car/gm/radar_interface.py b/opendbc/car/gm/radar_interface.py
new file mode 100755
index 0000000000..78f6a89a94
--- /dev/null
+++ b/opendbc/car/gm/radar_interface.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+import math
+from opendbc.can.parser import CANParser
+from opendbc.car import structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.gm.values import DBC, CanBus
+from opendbc.car.interfaces import RadarInterfaceBase
+
+RADAR_HEADER_MSG = 1120
+SLOT_1_MSG = RADAR_HEADER_MSG + 1
+NUM_SLOTS = 20
+
+# Actually it's 0x47f, but can parser only reports
+# messages that are present in DBC
+LAST_RADAR_MSG = RADAR_HEADER_MSG + NUM_SLOTS
+
+
+def create_radar_can_parser(car_fingerprint):
+ # C1A-ARS3-A by Continental
+ radar_targets = list(range(SLOT_1_MSG, SLOT_1_MSG + NUM_SLOTS))
+ signals = list(zip(['FLRRNumValidTargets',
+ 'FLRRSnsrBlckd', 'FLRRYawRtPlsblityFlt',
+ 'FLRRHWFltPrsntInt', 'FLRRAntTngFltPrsnt',
+ 'FLRRAlgnFltPrsnt', 'FLRRSnstvFltPrsntInt'] +
+ ['TrkRange'] * NUM_SLOTS + ['TrkRangeRate'] * NUM_SLOTS +
+ ['TrkRangeAccel'] * NUM_SLOTS + ['TrkAzimuth'] * NUM_SLOTS +
+ ['TrkWidth'] * NUM_SLOTS + ['TrkObjectID'] * NUM_SLOTS,
+ [RADAR_HEADER_MSG] * 7 + radar_targets * 6, strict=True))
+
+ messages = list({(s[1], 14) for s in signals})
+
+ return CANParser(DBC[car_fingerprint]['radar'], messages, CanBus.OBSTACLE)
+
+
+class RadarInterface(RadarInterfaceBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+
+ self.rcp = None if CP.radarUnavailable else create_radar_can_parser(CP.carFingerprint)
+
+ self.trigger_msg = LAST_RADAR_MSG
+ self.updated_messages = set()
+ self.radar_ts = CP.radarTimeStep
+
+ def update(self, can_strings):
+ if self.rcp is None:
+ return super().update(None)
+
+ vls = self.rcp.update_strings(can_strings)
+ self.updated_messages.update(vls)
+
+ if self.trigger_msg not in self.updated_messages:
+ return None
+
+ ret = structs.RadarData()
+ header = self.rcp.vl[RADAR_HEADER_MSG]
+ fault = header['FLRRSnsrBlckd'] or header['FLRRSnstvFltPrsntInt'] or \
+ header['FLRRYawRtPlsblityFlt'] or header['FLRRHWFltPrsntInt'] or \
+ header['FLRRAntTngFltPrsnt'] or header['FLRRAlgnFltPrsnt']
+ errors = []
+ if not self.rcp.can_valid:
+ errors.append("canError")
+ if fault:
+ errors.append("fault")
+ ret.errors = errors
+
+ currentTargets = set()
+ num_targets = header['FLRRNumValidTargets']
+
+ # Not all radar messages describe targets,
+ # no need to monitor all of the self.rcp.msgs_upd
+ for ii in self.updated_messages:
+ if ii == RADAR_HEADER_MSG:
+ continue
+
+ if num_targets == 0:
+ break
+
+ cpt = self.rcp.vl[ii]
+ # Zero distance means it's an empty target slot
+ if cpt['TrkRange'] > 0.0:
+ targetId = cpt['TrkObjectID']
+ currentTargets.add(targetId)
+ if targetId not in self.pts:
+ self.pts[targetId] = structs.RadarData.RadarPoint()
+ self.pts[targetId].trackId = targetId
+ distance = cpt['TrkRange']
+ self.pts[targetId].dRel = distance # from front of car
+ # From driver's pov, left is positive
+ self.pts[targetId].yRel = math.sin(cpt['TrkAzimuth'] * CV.DEG_TO_RAD) * distance
+ self.pts[targetId].vRel = cpt['TrkRangeRate']
+ self.pts[targetId].aRel = float('nan')
+ self.pts[targetId].yvRel = float('nan')
+
+ for oldTarget in list(self.pts.keys()):
+ if oldTarget not in currentTargets:
+ del self.pts[oldTarget]
+
+ ret.points = list(self.pts.values())
+ self.updated_messages.clear()
+ return ret
diff --git a/opendbc/car/gm/tests/__init__.py b/opendbc/car/gm/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/gm/tests/test_gm.py b/opendbc/car/gm/tests/test_gm.py
new file mode 100644
index 0000000000..c14b9bd243
--- /dev/null
+++ b/opendbc/car/gm/tests/test_gm.py
@@ -0,0 +1,20 @@
+from parameterized import parameterized
+
+from opendbc.car.gm.fingerprints import FINGERPRINTS
+from opendbc.car.gm.values import CAMERA_ACC_CAR, GM_RX_OFFSET
+
+CAMERA_DIAGNOSTIC_ADDRESS = 0x24b
+
+
+class TestGMFingerprint:
+ @parameterized.expand(FINGERPRINTS.items())
+ def test_can_fingerprints(self, car_model, fingerprints):
+ assert len(fingerprints) > 0
+
+ assert all(len(finger) for finger in fingerprints)
+
+ # The camera can sometimes be communicating on startup
+ if car_model in CAMERA_ACC_CAR:
+ for finger in fingerprints:
+ for required_addr in (CAMERA_DIAGNOSTIC_ADDRESS, CAMERA_DIAGNOSTIC_ADDRESS + GM_RX_OFFSET):
+ assert finger.get(required_addr) == 8, required_addr
diff --git a/opendbc/car/gm/values.py b/opendbc/car/gm/values.py
new file mode 100644
index 0000000000..bf16e61763
--- /dev/null
+++ b/opendbc/car/gm/values.py
@@ -0,0 +1,250 @@
+from dataclasses import dataclass, field
+
+from opendbc.car import dbc_dict, PlatformConfig, DbcDict, Platforms, CarSpecs
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarHarness, CarDocs, CarParts
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
+
+Ecu = CarParams.Ecu
+
+
+class CarControllerParams:
+ STEER_MAX = 300 # GM limit is 3Nm. Used by carcontroller to generate LKA output
+ STEER_STEP = 3 # Active control frames per command (~33hz)
+ INACTIVE_STEER_STEP = 10 # Inactive control frames per command (10hz)
+ STEER_DELTA_UP = 10 # Delta rates require review due to observed EPS weakness
+ STEER_DELTA_DOWN = 15
+ STEER_DRIVER_ALLOWANCE = 65
+ STEER_DRIVER_MULTIPLIER = 4
+ STEER_DRIVER_FACTOR = 100
+ NEAR_STOP_BRAKE_PHASE = 0.5 # m/s
+
+ # Heartbeat for dash "Service Adaptive Cruise" and "Service Front Camera"
+ ADAS_KEEPALIVE_STEP = 100
+ CAMERA_KEEPALIVE_STEP = 100
+
+ # Allow small margin below -3.5 m/s^2 from ISO 15622:2018 since we
+ # perform the closed loop control, and might need some
+ # to apply some more braking if we're on a downhill slope.
+ # Our controller should still keep the 2 second average above
+ # -3.5 m/s^2 as per planner limits
+ ACCEL_MAX = 2. # m/s^2
+ ACCEL_MIN = -4. # m/s^2
+
+ def __init__(self, CP):
+ # Gas/brake lookups
+ self.ZERO_GAS = 2048 # Coasting
+ self.MAX_BRAKE = 400 # ~ -4.0 m/s^2 with regen
+
+ if CP.carFingerprint in (CAMERA_ACC_CAR | SDGM_CAR):
+ self.MAX_GAS = 3400
+ self.MAX_ACC_REGEN = 1514
+ self.INACTIVE_REGEN = 1554
+ # Camera ACC vehicles have no regen while enabled.
+ # Camera transitions to MAX_ACC_REGEN from ZERO_GAS and uses friction brakes instantly
+ max_regen_acceleration = 0.
+
+ else:
+ self.MAX_GAS = 3072 # Safety limit, not ACC max. Stock ACC >4096 from standstill.
+ self.MAX_ACC_REGEN = 1404 # Max ACC regen is slightly less than max paddle regen
+ self.INACTIVE_REGEN = 1404
+ # ICE has much less engine braking force compared to regen in EVs,
+ # lower threshold removes some braking deadzone
+ max_regen_acceleration = -1. if CP.carFingerprint in EV_CAR else -0.1
+
+ self.GAS_LOOKUP_BP = [max_regen_acceleration, 0., self.ACCEL_MAX]
+ self.GAS_LOOKUP_V = [self.MAX_ACC_REGEN, self.ZERO_GAS, self.MAX_GAS]
+
+ self.BRAKE_LOOKUP_BP = [self.ACCEL_MIN, max_regen_acceleration]
+ self.BRAKE_LOOKUP_V = [self.MAX_BRAKE, 0.]
+
+
+@dataclass
+class GMCarDocs(CarDocs):
+ package: str = "Adaptive Cruise Control (ACC)"
+
+ def init_make(self, CP: CarParams):
+ if CP.networkLocation == CarParams.NetworkLocation.fwdCamera:
+ if CP.carFingerprint in SDGM_CAR:
+ self.car_parts = CarParts.common([CarHarness.gmsdgm])
+ else:
+ self.car_parts = CarParts.common([CarHarness.gm])
+ else:
+ self.car_parts = CarParts.common([CarHarness.obd_ii])
+
+
+@dataclass(frozen=True, kw_only=True)
+class GMCarSpecs(CarSpecs):
+ tireStiffnessFactor: float = 0.444 # not optimized yet
+
+
+@dataclass
+class GMPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'))
+
+
+@dataclass
+class GMASCMPlatformConfig(GMPlatformConfig):
+ def init(self):
+ # ASCM is supported, but due to a janky install and hardware configuration, we are not showing in the car docs
+ self.car_docs = []
+
+@dataclass
+class GMSDGMPlatformConfig(GMPlatformConfig):
+ def init(self):
+ # Don't show in docs until the harness is sold. See https://github.com/commaai/openpilot/issues/32471
+ self.car_docs = []
+
+
+class CAR(Platforms):
+ HOLDEN_ASTRA = GMASCMPlatformConfig(
+ [GMCarDocs("Holden Astra 2017")],
+ GMCarSpecs(mass=1363, wheelbase=2.662, steerRatio=15.7, centerToFrontRatio=0.4),
+ )
+ CHEVROLET_VOLT = GMASCMPlatformConfig(
+ [GMCarDocs("Chevrolet Volt 2017-18", min_enable_speed=0, video_link="https://youtu.be/QeMCN_4TFfQ")],
+ GMCarSpecs(mass=1607, wheelbase=2.69, steerRatio=17.7, centerToFrontRatio=0.45, tireStiffnessFactor=0.469),
+ )
+ CADILLAC_ATS = GMASCMPlatformConfig(
+ [GMCarDocs("Cadillac ATS Premium Performance 2018")],
+ GMCarSpecs(mass=1601, wheelbase=2.78, steerRatio=15.3),
+ )
+ CHEVROLET_MALIBU = GMASCMPlatformConfig(
+ [GMCarDocs("Chevrolet Malibu Premier 2017")],
+ GMCarSpecs(mass=1496, wheelbase=2.83, steerRatio=15.8, centerToFrontRatio=0.4),
+ )
+ GMC_ACADIA = GMASCMPlatformConfig(
+ [GMCarDocs("GMC Acadia 2018", video_link="https://www.youtube.com/watch?v=0ZN6DdsBUZo")],
+ GMCarSpecs(mass=1975, wheelbase=2.86, steerRatio=14.4, centerToFrontRatio=0.4),
+ )
+ BUICK_LACROSSE = GMASCMPlatformConfig(
+ [GMCarDocs("Buick LaCrosse 2017-19", "Driver Confidence Package 2")],
+ GMCarSpecs(mass=1712, wheelbase=2.91, steerRatio=15.8, centerToFrontRatio=0.4),
+ )
+ BUICK_REGAL = GMASCMPlatformConfig(
+ [GMCarDocs("Buick Regal Essence 2018")],
+ GMCarSpecs(mass=1714, wheelbase=2.83, steerRatio=14.4, centerToFrontRatio=0.4),
+ )
+ CADILLAC_ESCALADE = GMASCMPlatformConfig(
+ [GMCarDocs("Cadillac Escalade 2017", "Driver Assist Package")],
+ GMCarSpecs(mass=2564, wheelbase=2.95, steerRatio=17.3),
+ )
+ CADILLAC_ESCALADE_ESV = GMASCMPlatformConfig(
+ [GMCarDocs("Cadillac Escalade ESV 2016", "Adaptive Cruise Control (ACC) & LKAS")],
+ GMCarSpecs(mass=2739, wheelbase=3.302, steerRatio=17.3, tireStiffnessFactor=1.0),
+ )
+ CADILLAC_ESCALADE_ESV_2019 = GMASCMPlatformConfig(
+ [GMCarDocs("Cadillac Escalade ESV 2019", "Adaptive Cruise Control (ACC) & LKAS")],
+ CADILLAC_ESCALADE_ESV.specs,
+ )
+ CHEVROLET_BOLT_EUV = GMPlatformConfig(
+ [
+ GMCarDocs("Chevrolet Bolt EUV 2022-23", "Premier or Premier Redline Trim without Super Cruise Package", video_link="https://youtu.be/xvwzGMUA210"),
+ GMCarDocs("Chevrolet Bolt EV 2022-23", "2LT Trim with Adaptive Cruise Control Package"),
+ ],
+ GMCarSpecs(mass=1669, wheelbase=2.63779, steerRatio=16.8, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
+ )
+ CHEVROLET_SILVERADO = GMPlatformConfig(
+ [
+ GMCarDocs("Chevrolet Silverado 1500 2020-21", "Safety Package II"),
+ GMCarDocs("GMC Sierra 1500 2020-21", "Driver Alert Package II", video_link="https://youtu.be/5HbNoBLzRwE"),
+ ],
+ GMCarSpecs(mass=2450, wheelbase=3.75, steerRatio=16.3, tireStiffnessFactor=1.0),
+ )
+ CHEVROLET_EQUINOX = GMPlatformConfig(
+ [GMCarDocs("Chevrolet Equinox 2019-22")],
+ GMCarSpecs(mass=1588, wheelbase=2.72, steerRatio=14.4, centerToFrontRatio=0.4),
+ )
+ CHEVROLET_TRAILBLAZER = GMPlatformConfig(
+ [GMCarDocs("Chevrolet Trailblazer 2021-22")],
+ GMCarSpecs(mass=1345, wheelbase=2.64, steerRatio=16.8, centerToFrontRatio=0.4, tireStiffnessFactor=1.0),
+ )
+ CADILLAC_XT4 = GMSDGMPlatformConfig(
+ [GMCarDocs("Cadillac XT4 2023", "Driver Assist Package")],
+ CarSpecs(mass=1660, wheelbase=2.78, steerRatio=14.4, centerToFrontRatio=0.4),
+ )
+
+
+class CruiseButtons:
+ INIT = 0
+ UNPRESS = 1
+ RES_ACCEL = 2
+ DECEL_SET = 3
+ MAIN = 5
+ CANCEL = 6
+
+class AccState:
+ OFF = 0
+ ACTIVE = 1
+ FAULTED = 3
+ STANDSTILL = 4
+
+class CanBus:
+ POWERTRAIN = 0
+ OBSTACLE = 1
+ CAMERA = 2
+ CHASSIS = 2
+ LOOPBACK = 128
+ DROPPED = 192
+
+
+# In a Data Module, an identifier is a string used to recognize an object,
+# either by itself or together with the identifiers of parent objects.
+# Each returns a 4 byte hex representation of the decimal part number. `b"\x02\x8c\xf0'"` -> 42790951
+GM_BOOT_SOFTWARE_PART_NUMER_REQUEST = b'\x1a\xc0' # likely does not contain anything useful
+GM_SOFTWARE_MODULE_1_REQUEST = b'\x1a\xc1'
+GM_SOFTWARE_MODULE_2_REQUEST = b'\x1a\xc2'
+GM_SOFTWARE_MODULE_3_REQUEST = b'\x1a\xc3'
+
+# Part number of XML data file that is used to configure ECU
+GM_XML_DATA_FILE_PART_NUMBER = b'\x1a\x9c'
+GM_XML_CONFIG_COMPAT_ID = b'\x1a\x9b' # used to know if XML file is compatible with the ECU software/hardware
+
+# This DID is for identifying the part number that reflects the mix of hardware,
+# software, and calibrations in the ECU when it first arrives at the vehicle assembly plant.
+# If there's an Alpha Code, it's associated with this part number and stored in the DID $DB.
+GM_END_MODEL_PART_NUMBER_REQUEST = b'\x1a\xcb'
+GM_END_MODEL_PART_NUMBER_ALPHA_CODE_REQUEST = b'\x1a\xdb'
+GM_BASE_MODEL_PART_NUMBER_REQUEST = b'\x1a\xcc'
+GM_BASE_MODEL_PART_NUMBER_ALPHA_CODE_REQUEST = b'\x1a\xdc'
+GM_FW_RESPONSE = b'\x5a'
+
+GM_FW_REQUESTS = [
+ GM_BOOT_SOFTWARE_PART_NUMER_REQUEST,
+ GM_SOFTWARE_MODULE_1_REQUEST,
+ GM_SOFTWARE_MODULE_2_REQUEST,
+ GM_SOFTWARE_MODULE_3_REQUEST,
+ GM_XML_DATA_FILE_PART_NUMBER,
+ GM_XML_CONFIG_COMPAT_ID,
+ GM_END_MODEL_PART_NUMBER_REQUEST,
+ GM_END_MODEL_PART_NUMBER_ALPHA_CODE_REQUEST,
+ GM_BASE_MODEL_PART_NUMBER_REQUEST,
+ GM_BASE_MODEL_PART_NUMBER_ALPHA_CODE_REQUEST,
+]
+
+GM_RX_OFFSET = 0x400
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[request for req in GM_FW_REQUESTS for request in [
+ Request(
+ [StdQueries.SHORT_TESTER_PRESENT_REQUEST, req],
+ [StdQueries.SHORT_TESTER_PRESENT_RESPONSE, GM_FW_RESPONSE + bytes([req[-1]])],
+ rx_offset=GM_RX_OFFSET,
+ bus=0,
+ logging=True,
+ ),
+ ]],
+ extra_ecus=[(Ecu.fwdCamera, 0x24b, None)],
+)
+
+EV_CAR = {CAR.CHEVROLET_VOLT, CAR.CHEVROLET_BOLT_EUV}
+
+# We're integrated at the camera with VOACC on these cars (instead of ASCM w/ OBD-II harness)
+CAMERA_ACC_CAR = {CAR.CHEVROLET_BOLT_EUV, CAR.CHEVROLET_SILVERADO, CAR.CHEVROLET_EQUINOX, CAR.CHEVROLET_TRAILBLAZER}
+
+# We're integrated at the Safety Data Gateway Module on these cars
+SDGM_CAR = {CAR.CADILLAC_XT4}
+
+STEER_THRESHOLD = 1.0
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/honda/__init__.py b/opendbc/car/honda/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/honda/carcontroller.py b/opendbc/car/honda/carcontroller.py
new file mode 100644
index 0000000000..20358c67fb
--- /dev/null
+++ b/opendbc/car/honda/carcontroller.py
@@ -0,0 +1,254 @@
+import copy
+from collections import namedtuple
+
+from opendbc.can.packer import CANPacker
+from opendbc.car import DT_CTRL, rate_limit, make_tester_present_msg, structs
+from opendbc.car.common.numpy_fast import clip, interp
+from opendbc.car.honda import hondacan
+from opendbc.car.honda.values import CruiseButtons, VISUAL_HUD, HONDA_BOSCH, HONDA_BOSCH_RADARLESS, HONDA_NIDEC_ALT_PCM_ACCEL, CarControllerParams
+from opendbc.car.interfaces import CarControllerBase
+
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+LongCtrlState = structs.CarControl.Actuators.LongControlState
+
+
+def compute_gb_honda_bosch(accel, speed):
+ # TODO returns 0s, is unused
+ return 0.0, 0.0
+
+
+def compute_gb_honda_nidec(accel, speed):
+ creep_brake = 0.0
+ creep_speed = 2.3
+ creep_brake_value = 0.15
+ if speed < creep_speed:
+ creep_brake = (creep_speed - speed) / creep_speed * creep_brake_value
+ gb = float(accel) / 4.8 - creep_brake
+ return clip(gb, 0.0, 1.0), clip(-gb, 0.0, 1.0)
+
+
+def compute_gas_brake(accel, speed, fingerprint):
+ if fingerprint in HONDA_BOSCH:
+ return compute_gb_honda_bosch(accel, speed)
+ else:
+ return compute_gb_honda_nidec(accel, speed)
+
+
+# TODO not clear this does anything useful
+def actuator_hysteresis(brake, braking, brake_steady, v_ego, car_fingerprint):
+ # hyst params
+ brake_hyst_on = 0.02 # to activate brakes exceed this value
+ brake_hyst_off = 0.005 # to deactivate brakes below this value
+ brake_hyst_gap = 0.01 # don't change brake command for small oscillations within this value
+
+ # *** hysteresis logic to avoid brake blinking. go above 0.1 to trigger
+ if (brake < brake_hyst_on and not braking) or brake < brake_hyst_off:
+ brake = 0.
+ braking = brake > 0.
+
+ # for small brake oscillations within brake_hyst_gap, don't change the brake command
+ if brake == 0.:
+ brake_steady = 0.
+ elif brake > brake_steady + brake_hyst_gap:
+ brake_steady = brake - brake_hyst_gap
+ elif brake < brake_steady - brake_hyst_gap:
+ brake_steady = brake + brake_hyst_gap
+ brake = brake_steady
+
+ return brake, braking, brake_steady
+
+
+def brake_pump_hysteresis(apply_brake, apply_brake_last, last_pump_ts, ts):
+ pump_on = False
+
+ # reset pump timer if:
+ # - there is an increment in brake request
+ # - we are applying steady state brakes and we haven't been running the pump
+ # for more than 20s (to prevent pressure bleeding)
+ if apply_brake > apply_brake_last or (ts - last_pump_ts > 20. and apply_brake > 0):
+ last_pump_ts = ts
+
+ # once the pump is on, run it for at least 0.2s
+ if ts - last_pump_ts < 0.2 and apply_brake > 0:
+ pump_on = True
+
+ return pump_on, last_pump_ts
+
+
+def process_hud_alert(hud_alert):
+ # initialize to no alert
+ fcw_display = 0
+ steer_required = 0
+ acc_alert = 0
+
+ # priority is: FCW, steer required, all others
+ if hud_alert == VisualAlert.fcw:
+ fcw_display = VISUAL_HUD[hud_alert]
+ elif hud_alert in (VisualAlert.steerRequired, VisualAlert.ldw):
+ steer_required = VISUAL_HUD[hud_alert]
+ else:
+ acc_alert = VISUAL_HUD[hud_alert]
+
+ return fcw_display, steer_required, acc_alert
+
+
+HUDData = namedtuple("HUDData",
+ ["pcm_accel", "v_cruise", "lead_visible",
+ "lanes_visible", "fcw", "acc_alert", "steer_required", "lead_distance_bars"])
+
+
+def rate_limit_steer(new_steer, last_steer):
+ # TODO just hardcoded ramp to min/max in 0.33s for all Honda
+ MAX_DELTA = 3 * DT_CTRL
+ return clip(new_steer, last_steer - MAX_DELTA, last_steer + MAX_DELTA)
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.packer = CANPacker(dbc_name)
+ self.params = CarControllerParams(CP)
+ self.CAN = hondacan.CanBus(CP)
+
+ self.braking = False
+ self.brake_steady = 0.
+ self.brake_last = 0.
+ self.apply_brake_last = 0
+ self.last_pump_ts = 0.
+ self.stopping_counter = 0
+
+ self.accel = 0.0
+ self.speed = 0.0
+ self.gas = 0.0
+ self.brake = 0.0
+ self.last_steer = 0.0
+
+ def update(self, CC, CS, now_nanos):
+ actuators = CC.actuators
+ hud_control = CC.hudControl
+ conversion = hondacan.get_cruise_speed_conversion(self.CP.carFingerprint, CS.is_metric)
+ hud_v_cruise = hud_control.setSpeed / conversion if hud_control.speedVisible else 255
+ pcm_cancel_cmd = CC.cruiseControl.cancel
+
+ if CC.longActive:
+ accel = actuators.accel
+ gas, brake = compute_gas_brake(actuators.accel, CS.out.vEgo, self.CP.carFingerprint)
+ else:
+ accel = 0.0
+ gas, brake = 0.0, 0.0
+
+ # *** rate limit steer ***
+ limited_steer = rate_limit_steer(actuators.steer, self.last_steer)
+ self.last_steer = limited_steer
+
+ # *** apply brake hysteresis ***
+ pre_limit_brake, self.braking, self.brake_steady = actuator_hysteresis(brake, self.braking, self.brake_steady,
+ CS.out.vEgo, self.CP.carFingerprint)
+
+ # *** rate limit after the enable check ***
+ self.brake_last = rate_limit(pre_limit_brake, self.brake_last, -2., DT_CTRL)
+
+ # vehicle hud display, wait for one update from 10Hz 0x304 msg
+ fcw_display, steer_required, acc_alert = process_hud_alert(hud_control.visualAlert)
+
+ # **** process the car messages ****
+
+ # steer torque is converted back to CAN reference (positive when steering right)
+ apply_steer = int(interp(-limited_steer * self.params.STEER_MAX,
+ self.params.STEER_LOOKUP_BP, self.params.STEER_LOOKUP_V))
+
+ # Send CAN commands
+ can_sends = []
+
+ # tester present - w/ no response (keeps radar disabled)
+ if self.CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and self.CP.openpilotLongitudinalControl:
+ if self.frame % 10 == 0:
+ can_sends.append(make_tester_present_msg(0x18DAB0F1, 1, suppress_response=True))
+
+ # Send steering command.
+ can_sends.append(hondacan.create_steering_control(self.packer, self.CAN, apply_steer, CC.latActive, self.CP.carFingerprint,
+ CS.CP.openpilotLongitudinalControl))
+
+ # wind brake from air resistance decel at high speed
+ wind_brake = interp(CS.out.vEgo, [0.0, 2.3, 35.0], [0.001, 0.002, 0.15])
+ # all of this is only relevant for HONDA NIDEC
+ max_accel = interp(CS.out.vEgo, self.params.NIDEC_MAX_ACCEL_BP, self.params.NIDEC_MAX_ACCEL_V)
+ # TODO this 1.44 is just to maintain previous behavior
+ pcm_speed_BP = [-wind_brake,
+ -wind_brake * (3 / 4),
+ 0.0,
+ 0.5]
+ # The Honda ODYSSEY seems to have different PCM_ACCEL
+ # msgs, is it other cars too?
+ if not CC.longActive:
+ pcm_speed = 0.0
+ pcm_accel = int(0.0)
+ elif self.CP.carFingerprint in HONDA_NIDEC_ALT_PCM_ACCEL:
+ pcm_speed_V = [0.0,
+ clip(CS.out.vEgo - 3.0, 0.0, 100.0),
+ clip(CS.out.vEgo + 0.0, 0.0, 100.0),
+ clip(CS.out.vEgo + 5.0, 0.0, 100.0)]
+ pcm_speed = interp(gas - brake, pcm_speed_BP, pcm_speed_V)
+ pcm_accel = int(1.0 * self.params.NIDEC_GAS_MAX)
+ else:
+ pcm_speed_V = [0.0,
+ clip(CS.out.vEgo - 2.0, 0.0, 100.0),
+ clip(CS.out.vEgo + 2.0, 0.0, 100.0),
+ clip(CS.out.vEgo + 5.0, 0.0, 100.0)]
+ pcm_speed = interp(gas - brake, pcm_speed_BP, pcm_speed_V)
+ pcm_accel = int(clip((accel / 1.44) / max_accel, 0.0, 1.0) * self.params.NIDEC_GAS_MAX)
+
+ if not self.CP.openpilotLongitudinalControl:
+ if self.frame % 2 == 0 and self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS: # radarless cars don't have supplemental message
+ can_sends.append(hondacan.create_bosch_supplemental_1(self.packer, self.CAN, self.CP.carFingerprint))
+ # If using stock ACC, spam cancel command to kill gas when OP disengages.
+ if pcm_cancel_cmd:
+ can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CruiseButtons.CANCEL, self.CP.carFingerprint))
+ elif CC.cruiseControl.resume:
+ can_sends.append(hondacan.spam_buttons_command(self.packer, self.CAN, CruiseButtons.RES_ACCEL, self.CP.carFingerprint))
+
+ else:
+ # Send gas and brake commands.
+ if self.frame % 2 == 0:
+ ts = self.frame * DT_CTRL
+
+ if self.CP.carFingerprint in HONDA_BOSCH:
+ self.accel = clip(accel, self.params.BOSCH_ACCEL_MIN, self.params.BOSCH_ACCEL_MAX)
+ self.gas = interp(accel, self.params.BOSCH_GAS_LOOKUP_BP, self.params.BOSCH_GAS_LOOKUP_V)
+
+ stopping = actuators.longControlState == LongCtrlState.stopping
+ self.stopping_counter = self.stopping_counter + 1 if stopping else 0
+ can_sends.extend(hondacan.create_acc_commands(self.packer, self.CAN, CC.enabled, CC.longActive, self.accel, self.gas,
+ self.stopping_counter, self.CP.carFingerprint))
+ else:
+ apply_brake = clip(self.brake_last - wind_brake, 0.0, 1.0)
+ apply_brake = int(clip(apply_brake * self.params.NIDEC_BRAKE_MAX, 0, self.params.NIDEC_BRAKE_MAX - 1))
+ pump_on, self.last_pump_ts = brake_pump_hysteresis(apply_brake, self.apply_brake_last, self.last_pump_ts, ts)
+
+ pcm_override = True
+ can_sends.append(hondacan.create_brake_command(self.packer, self.CAN, apply_brake, pump_on,
+ pcm_override, pcm_cancel_cmd, fcw_display,
+ self.CP.carFingerprint, CS.stock_brake))
+ self.apply_brake_last = apply_brake
+ self.brake = apply_brake / self.params.NIDEC_BRAKE_MAX
+
+ # Send dashboard UI commands.
+ if self.frame % 10 == 0:
+ hud = HUDData(int(pcm_accel), int(round(hud_v_cruise)), hud_control.leadVisible,
+ hud_control.lanesVisible, fcw_display, acc_alert, steer_required, hud_control.leadDistanceBars)
+ can_sends.extend(hondacan.create_ui_commands(self.packer, self.CAN, self.CP, CC.enabled, pcm_speed, hud, CS.is_metric, CS.acc_hud, CS.lkas_hud))
+
+ if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint not in HONDA_BOSCH:
+ self.speed = pcm_speed
+ self.gas = pcm_accel / self.params.NIDEC_GAS_MAX
+
+ new_actuators = copy.copy(actuators)
+ new_actuators.speed = self.speed
+ new_actuators.accel = self.accel
+ new_actuators.gas = self.gas
+ new_actuators.brake = self.brake
+ new_actuators.steer = self.last_steer
+ new_actuators.steerOutputCan = apply_steer
+
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/honda/carstate.py b/opendbc/car/honda/carstate.py
new file mode 100644
index 0000000000..5039fb1245
--- /dev/null
+++ b/opendbc/car/honda/carstate.py
@@ -0,0 +1,307 @@
+from collections import defaultdict
+
+from opendbc.can.can_define import CANDefine
+from opendbc.can.parser import CANParser
+from opendbc.car import create_button_events, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.common.numpy_fast import interp
+from opendbc.car.honda.hondacan import CanBus, get_cruise_speed_conversion
+from opendbc.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, \
+ HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, \
+ HondaFlags, CruiseButtons, CruiseSettings
+from opendbc.car.interfaces import CarStateBase
+
+TransmissionType = structs.CarParams.TransmissionType
+ButtonType = structs.CarState.ButtonEvent.Type
+
+BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise,
+ CruiseButtons.MAIN: ButtonType.altButton3, CruiseButtons.CANCEL: ButtonType.cancel}
+SETTINGS_BUTTONS_DICT = {CruiseSettings.DISTANCE: ButtonType.gapAdjustCruise, CruiseSettings.LKAS: ButtonType.altButton1}
+
+
+def get_can_messages(CP, gearbox_msg):
+ messages = [
+ ("ENGINE_DATA", 100),
+ ("WHEEL_SPEEDS", 50),
+ ("STEERING_SENSORS", 100),
+ ("SEATBELT_STATUS", 10),
+ ("CRUISE", 10),
+ ("POWERTRAIN_DATA", 100),
+ ("CAR_SPEED", 10),
+ ("VSA_STATUS", 50),
+ ("STEER_STATUS", 100),
+ ("STEER_MOTOR_TORQUE", 0), # TODO: not on every car
+ ]
+
+ if CP.carFingerprint == CAR.HONDA_ODYSSEY_CHN:
+ messages += [
+ ("SCM_FEEDBACK", 25),
+ ("SCM_BUTTONS", 50),
+ ]
+ else:
+ messages += [
+ ("SCM_FEEDBACK", 10),
+ ("SCM_BUTTONS", 25),
+ ]
+
+ if CP.carFingerprint in (CAR.HONDA_CRV_HYBRID, CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.ACURA_RDX_3G, CAR.HONDA_E):
+ messages.append((gearbox_msg, 50))
+ else:
+ messages.append((gearbox_msg, 100))
+
+ if CP.flags & HondaFlags.BOSCH_ALT_BRAKE:
+ messages.append(("BRAKE_MODULE", 50))
+
+ if CP.carFingerprint in (HONDA_BOSCH | {CAR.HONDA_CIVIC, CAR.HONDA_ODYSSEY, CAR.HONDA_ODYSSEY_CHN}):
+ messages.append(("EPB_STATUS", 50))
+
+ if CP.carFingerprint in HONDA_BOSCH:
+ # these messages are on camera bus on radarless cars
+ if not CP.openpilotLongitudinalControl and CP.carFingerprint not in HONDA_BOSCH_RADARLESS:
+ messages += [
+ ("ACC_HUD", 10),
+ ("ACC_CONTROL", 50),
+ ]
+ else: # Nidec signals
+ if CP.carFingerprint == CAR.HONDA_ODYSSEY_CHN:
+ messages.append(("CRUISE_PARAMS", 10))
+ else:
+ messages.append(("CRUISE_PARAMS", 50))
+
+ # TODO: clean this up
+ if CP.carFingerprint in (CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.HONDA_CRV_HYBRID, CAR.HONDA_INSIGHT,
+ CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.HONDA_CIVIC_2022, CAR.HONDA_HRV_3G):
+ pass
+ elif CP.carFingerprint in (CAR.HONDA_ODYSSEY_CHN, CAR.HONDA_FREED, CAR.HONDA_HRV):
+ pass
+ else:
+ messages.append(("DOORS_STATUS", 3))
+
+ if CP.carFingerprint in HONDA_BOSCH_RADARLESS:
+ messages.append(("CRUISE_FAULT_STATUS", 50))
+ elif CP.openpilotLongitudinalControl:
+ messages.append(("STANDSTILL", 50))
+
+ return messages
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+ self.gearbox_msg = "GEARBOX"
+ if CP.carFingerprint == CAR.HONDA_ACCORD and CP.transmissionType == TransmissionType.cvt:
+ self.gearbox_msg = "GEARBOX_15T"
+
+ self.main_on_sig_msg = "SCM_FEEDBACK"
+ if CP.carFingerprint in HONDA_NIDEC_ALT_SCM_MESSAGES:
+ self.main_on_sig_msg = "SCM_BUTTONS"
+
+ self.shifter_values = can_define.dv[self.gearbox_msg]["GEAR_SHIFTER"]
+ self.steer_status_values = defaultdict(lambda: "UNKNOWN", can_define.dv["STEER_STATUS"]["STEER_STATUS"])
+
+ self.brake_switch_prev = False
+ self.brake_switch_active = False
+ self.cruise_setting = 0
+ self.v_cruise_pcm_prev = 0
+
+ # When available we use cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] to populate vEgoCluster
+ # However, on cars without a digital speedometer this is not always present (HRV, FIT, CRV 2016, ILX and RDX)
+ self.dash_speed_seen = False
+
+ def update(self, cp, cp_cam, _, cp_body, __) -> structs.CarState:
+ ret = structs.CarState()
+
+ # car params
+ v_weight_v = [0., 1.] # don't trust smooth speed at low values to avoid premature zero snapping
+ v_weight_bp = [1., 6.] # smooth blending, below ~0.6m/s the smooth speed snaps to zero
+
+ # update prevs, update must run once per loop
+ prev_cruise_buttons = self.cruise_buttons
+ prev_cruise_setting = self.cruise_setting
+ self.cruise_setting = cp.vl["SCM_BUTTONS"]["CRUISE_SETTING"]
+ self.cruise_buttons = cp.vl["SCM_BUTTONS"]["CRUISE_BUTTONS"]
+
+ # used for car hud message
+ self.is_metric = not cp.vl["CAR_SPEED"]["IMPERIAL_UNIT"]
+
+ # ******************* parse out can *******************
+ # STANDSTILL->WHEELS_MOVING bit can be noisy around zero, so use XMISSION_SPEED
+ # panda checks if the signal is non-zero
+ ret.standstill = cp.vl["ENGINE_DATA"]["XMISSION_SPEED"] < 1e-5
+ # TODO: find a common signal across all cars
+ if self.CP.carFingerprint in (CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.HONDA_CRV_HYBRID, CAR.HONDA_INSIGHT,
+ CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.HONDA_CIVIC_2022, CAR.HONDA_HRV_3G):
+ ret.doorOpen = bool(cp.vl["SCM_FEEDBACK"]["DRIVERS_DOOR_OPEN"])
+ elif self.CP.carFingerprint in (CAR.HONDA_ODYSSEY_CHN, CAR.HONDA_FREED, CAR.HONDA_HRV):
+ ret.doorOpen = bool(cp.vl["SCM_BUTTONS"]["DRIVERS_DOOR_OPEN"])
+ else:
+ ret.doorOpen = any([cp.vl["DOORS_STATUS"]["DOOR_OPEN_FL"], cp.vl["DOORS_STATUS"]["DOOR_OPEN_FR"],
+ cp.vl["DOORS_STATUS"]["DOOR_OPEN_RL"], cp.vl["DOORS_STATUS"]["DOOR_OPEN_RR"]])
+ ret.seatbeltUnlatched = bool(cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_LAMP"] or not cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_LATCHED"])
+
+ steer_status = self.steer_status_values[cp.vl["STEER_STATUS"]["STEER_STATUS"]]
+ ret.steerFaultPermanent = steer_status not in ("NORMAL", "NO_TORQUE_ALERT_1", "NO_TORQUE_ALERT_2", "LOW_SPEED_LOCKOUT", "TMP_FAULT")
+ # LOW_SPEED_LOCKOUT is not worth a warning
+ # NO_TORQUE_ALERT_2 can be caused by bump or steering nudge from driver
+ ret.steerFaultTemporary = steer_status not in ("NORMAL", "LOW_SPEED_LOCKOUT", "NO_TORQUE_ALERT_2")
+
+ if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS:
+ ret.accFaulted = bool(cp.vl["CRUISE_FAULT_STATUS"]["CRUISE_FAULT"])
+ else:
+ # On some cars, these two signals are always 1, this flag is masking a bug in release
+ # FIXME: find and set the ACC faulted signals on more platforms
+ if self.CP.openpilotLongitudinalControl:
+ ret.accFaulted = bool(cp.vl["STANDSTILL"]["BRAKE_ERROR_1"] or cp.vl["STANDSTILL"]["BRAKE_ERROR_2"])
+
+ # Log non-critical stock ACC/LKAS faults if Nidec (camera)
+ if self.CP.carFingerprint not in HONDA_BOSCH:
+ ret.carFaultedNonCritical = bool(cp_cam.vl["ACC_HUD"]["ACC_PROBLEM"] or cp_cam.vl["LKAS_HUD"]["LKAS_PROBLEM"])
+
+ ret.espDisabled = cp.vl["VSA_STATUS"]["ESP_DISABLED"] != 0
+
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_FL"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_FR"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_RL"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_RR"],
+ )
+ v_wheel = (ret.wheelSpeeds.fl + ret.wheelSpeeds.fr + ret.wheelSpeeds.rl + ret.wheelSpeeds.rr) / 4.0
+
+ # blend in transmission speed at low speed, since it has more low speed accuracy
+ v_weight = interp(v_wheel, v_weight_bp, v_weight_v)
+ ret.vEgoRaw = (1. - v_weight) * cp.vl["ENGINE_DATA"]["XMISSION_SPEED"] * CV.KPH_TO_MS * self.CP.wheelSpeedFactor + v_weight * v_wheel
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+
+ self.dash_speed_seen = self.dash_speed_seen or cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] > 1e-3
+ if self.dash_speed_seen:
+ conversion = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
+ ret.vEgoCluster = cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] * conversion
+
+ ret.steeringAngleDeg = cp.vl["STEERING_SENSORS"]["STEER_ANGLE"]
+ ret.steeringRateDeg = cp.vl["STEERING_SENSORS"]["STEER_ANGLE_RATE"]
+
+ ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_stalk(
+ 250, cp.vl["SCM_FEEDBACK"]["LEFT_BLINKER"], cp.vl["SCM_FEEDBACK"]["RIGHT_BLINKER"])
+ ret.brakeHoldActive = cp.vl["VSA_STATUS"]["BRAKE_HOLD_ACTIVE"] == 1
+
+ # TODO: set for all cars
+ if self.CP.carFingerprint in (HONDA_BOSCH | {CAR.HONDA_CIVIC, CAR.HONDA_ODYSSEY, CAR.HONDA_ODYSSEY_CHN}):
+ ret.parkingBrake = cp.vl["EPB_STATUS"]["EPB_STATE"] != 0
+
+ gear = int(cp.vl[self.gearbox_msg]["GEAR_SHIFTER"])
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(gear, None))
+
+ ret.gas = cp.vl["POWERTRAIN_DATA"]["PEDAL_GAS"]
+ ret.gasPressed = ret.gas > 1e-5
+
+ ret.steeringTorque = cp.vl["STEER_STATUS"]["STEER_TORQUE_SENSOR"]
+ ret.steeringTorqueEps = cp.vl["STEER_MOTOR_TORQUE"]["MOTOR_TORQUE"]
+ ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD.get(self.CP.carFingerprint, 1200) # type: ignore[call-overload]
+
+ if self.CP.carFingerprint in HONDA_BOSCH:
+ # The PCM always manages its own cruise control state, but doesn't publish it
+ if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS:
+ ret.cruiseState.nonAdaptive = cp_cam.vl["ACC_HUD"]["CRUISE_CONTROL_LABEL"] != 0
+
+ if not self.CP.openpilotLongitudinalControl:
+ # ACC_HUD is on camera bus on radarless cars
+ acc_hud = cp_cam.vl["ACC_HUD"] if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS else cp.vl["ACC_HUD"]
+ ret.cruiseState.nonAdaptive = acc_hud["CRUISE_CONTROL_LABEL"] != 0
+ ret.cruiseState.standstill = acc_hud["CRUISE_SPEED"] == 252.
+
+ conversion = get_cruise_speed_conversion(self.CP.carFingerprint, self.is_metric)
+ # On set, cruise set speed pulses between 254~255 and the set speed prev is set to avoid this.
+ ret.cruiseState.speed = self.v_cruise_pcm_prev if acc_hud["CRUISE_SPEED"] > 160.0 else acc_hud["CRUISE_SPEED"] * conversion
+ self.v_cruise_pcm_prev = ret.cruiseState.speed
+ else:
+ ret.cruiseState.speed = cp.vl["CRUISE"]["CRUISE_SPEED_PCM"] * CV.KPH_TO_MS
+
+ if self.CP.flags & HondaFlags.BOSCH_ALT_BRAKE:
+ ret.brakePressed = cp.vl["BRAKE_MODULE"]["BRAKE_PRESSED"] != 0
+ else:
+ # brake switch has shown some single time step noise, so only considered when
+ # switch is on for at least 2 consecutive CAN samples
+ # brake switch rises earlier than brake pressed but is never 1 when in park
+ brake_switch_vals = cp.vl_all["POWERTRAIN_DATA"]["BRAKE_SWITCH"]
+ if len(brake_switch_vals):
+ brake_switch = cp.vl["POWERTRAIN_DATA"]["BRAKE_SWITCH"] != 0
+ if len(brake_switch_vals) > 1:
+ self.brake_switch_prev = brake_switch_vals[-2] != 0
+ self.brake_switch_active = brake_switch and self.brake_switch_prev
+ self.brake_switch_prev = brake_switch
+ ret.brakePressed = (cp.vl["POWERTRAIN_DATA"]["BRAKE_PRESSED"] != 0) or self.brake_switch_active
+
+ ret.brake = cp.vl["VSA_STATUS"]["USER_BRAKE"]
+ ret.cruiseState.enabled = cp.vl["POWERTRAIN_DATA"]["ACC_STATUS"] != 0
+ ret.cruiseState.available = bool(cp.vl[self.main_on_sig_msg]["MAIN_ON"])
+
+ # 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:
+ ret.brakePressed = True
+
+ if self.CP.carFingerprint in HONDA_BOSCH:
+ # TODO: find the radarless AEB_STATUS bit and make sure ACCEL_COMMAND is correct to enable AEB alerts
+ if self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS:
+ ret.stockAeb = (not self.CP.openpilotLongitudinalControl) and bool(cp.vl["ACC_CONTROL"]["AEB_STATUS"] and cp.vl["ACC_CONTROL"]["ACCEL_COMMAND"] < -1e-5)
+ else:
+ ret.stockAeb = bool(cp_cam.vl["BRAKE_COMMAND"]["AEB_REQ_1"] and cp_cam.vl["BRAKE_COMMAND"]["COMPUTER_BRAKE"] > 1e-5)
+
+ self.acc_hud = False
+ self.lkas_hud = False
+ if self.CP.carFingerprint not in HONDA_BOSCH:
+ ret.stockFcw = cp_cam.vl["BRAKE_COMMAND"]["FCW"] != 0
+ self.acc_hud = cp_cam.vl["ACC_HUD"]
+ self.stock_brake = cp_cam.vl["BRAKE_COMMAND"]
+ if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS:
+ self.lkas_hud = cp_cam.vl["LKAS_HUD"]
+
+ if self.CP.enableBsm:
+ # BSM messages are on B-CAN, requires a panda forwarding B-CAN messages to CAN 0
+ # more info here: https://github.com/commaai/openpilot/pull/1867
+ ret.leftBlindspot = cp_body.vl["BSM_STATUS_LEFT"]["BSM_ALERT"] == 1
+ ret.rightBlindspot = cp_body.vl["BSM_STATUS_RIGHT"]["BSM_ALERT"] == 1
+
+ ret.buttonEvents = [
+ *create_button_events(self.cruise_buttons, prev_cruise_buttons, BUTTONS_DICT),
+ *create_button_events(self.cruise_setting, prev_cruise_setting, SETTINGS_BUTTONS_DICT),
+ ]
+
+ return ret
+
+ def get_can_parser(self, CP):
+ messages = get_can_messages(CP, self.gearbox_msg)
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus(CP).pt)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ messages = [
+ ("STEERING_CONTROL", 100),
+ ]
+
+ if CP.carFingerprint in HONDA_BOSCH_RADARLESS:
+ messages += [
+ ("ACC_HUD", 10),
+ ("LKAS_HUD", 10),
+ ]
+
+ elif CP.carFingerprint not in HONDA_BOSCH:
+ messages += [
+ ("ACC_HUD", 10),
+ ("LKAS_HUD", 10),
+ ("BRAKE_COMMAND", 50),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus(CP).camera)
+
+ @staticmethod
+ def get_body_can_parser(CP):
+ if CP.enableBsm:
+ messages = [
+ ("BSM_STATUS_LEFT", 3),
+ ("BSM_STATUS_RIGHT", 3),
+ ]
+ bus_body = CanBus(CP).radar # B-CAN is forwarded to ACC-CAN radar side (CAN 0 on fake ethernet port)
+ return CANParser(DBC[CP.carFingerprint]["body"], messages, bus_body)
+ return None
diff --git a/opendbc/car/honda/fingerprints.py b/opendbc/car/honda/fingerprints.py
new file mode 100644
index 0000000000..11472d6373
--- /dev/null
+++ b/opendbc/car/honda/fingerprints.py
@@ -0,0 +1,899 @@
+from opendbc.car.structs import CarParams
+from opendbc.car.honda.values import CAR
+
+Ecu = CarParams.Ecu
+
+# Modified FW can be identified by the second dash being replaced by a comma
+# For example: `b'39990-TVA,A150\x00\x00'`
+#
+# TODO: vsa is "essential" for fpv2 but doesn't appear on some CAR.FREED models
+
+
+FW_VERSIONS = {
+ CAR.HONDA_ACCORD: {
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-TVC-A910\x00\x00',
+ b'54008-TWA-A910\x00\x00',
+ ],
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-6A7-A220\x00\x00',
+ b'28101-6A7-A230\x00\x00',
+ b'28101-6A7-A320\x00\x00',
+ b'28101-6A7-A330\x00\x00',
+ b'28101-6A7-A410\x00\x00',
+ b'28101-6A7-A510\x00\x00',
+ b'28101-6A7-A610\x00\x00',
+ b'28101-6A7-A710\x00\x00',
+ b'28101-6A9-H140\x00\x00',
+ b'28101-6A9-H420\x00\x00',
+ b'28102-6B8-A560\x00\x00',
+ b'28102-6B8-A570\x00\x00',
+ b'28102-6B8-A700\x00\x00',
+ b'28102-6B8-A800\x00\x00',
+ b'28102-6B8-C560\x00\x00',
+ b'28102-6B8-C570\x00\x00',
+ b'28102-6B8-M520\x00\x00',
+ b'28102-6B8-R700\x00\x00',
+ ],
+ (Ecu.electricBrakeBooster, 0x18da2bf1, None): [
+ b'46114-TVA-A050\x00\x00',
+ b'46114-TVA-A060\x00\x00',
+ b'46114-TVA-A080\x00\x00',
+ b'46114-TVA-A120\x00\x00',
+ b'46114-TVA-A320\x00\x00',
+ b'46114-TVE-H550\x00\x00',
+ b'46114-TVE-H560\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TVA-B040\x00\x00',
+ b'57114-TVA-B050\x00\x00',
+ b'57114-TVA-B060\x00\x00',
+ b'57114-TVA-B530\x00\x00',
+ b'57114-TVA-C040\x00\x00',
+ b'57114-TVA-C050\x00\x00',
+ b'57114-TVA-C060\x00\x00',
+ b'57114-TVA-C530\x00\x00',
+ b'57114-TVA-E520\x00\x00',
+ b'57114-TVE-H250\x00\x00',
+ b'57114-TWA-A040\x00\x00',
+ b'57114-TWA-A050\x00\x00',
+ b'57114-TWA-A530\x00\x00',
+ b'57114-TWA-B520\x00\x00',
+ b'57114-TWA-C510\x00\x00',
+ b'57114-TWB-H030\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TBX-H120\x00\x00',
+ b'39990-TVA,A150\x00\x00',
+ b'39990-TVA-A140\x00\x00',
+ b'39990-TVA-A150\x00\x00',
+ b'39990-TVA-A160\x00\x00',
+ b'39990-TVA-A340\x00\x00',
+ b'39990-TVA-X030\x00\x00',
+ b'39990-TVA-X040\x00\x00',
+ b'39990-TVE-H130\x00\x00',
+ b'39990-TWB-H120\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TBX-H230\x00\x00',
+ b'77959-TVA-A460\x00\x00',
+ b'77959-TVA-F330\x00\x00',
+ b'77959-TVA-H230\x00\x00',
+ b'77959-TVA-L420\x00\x00',
+ b'77959-TVA-X330\x00\x00',
+ b'77959-TWA-A440\x00\x00',
+ b'77959-TWA-L420\x00\x00',
+ b'77959-TWB-H220\x00\x00',
+ ],
+ (Ecu.hud, 0x18da61f1, None): [
+ b'78209-TVA-A010\x00\x00',
+ b'78209-TVA-A110\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36802-TBX-H140\x00\x00',
+ b'36802-TVA-A150\x00\x00',
+ b'36802-TVA-A160\x00\x00',
+ b'36802-TVA-A170\x00\x00',
+ b'36802-TVA-A180\x00\x00',
+ b'36802-TVA-A330\x00\x00',
+ b'36802-TVC-A330\x00\x00',
+ b'36802-TVE-H070\x00\x00',
+ b'36802-TWA-A070\x00\x00',
+ b'36802-TWA-A080\x00\x00',
+ b'36802-TWA-A210\x00\x00',
+ b'36802-TWA-A330\x00\x00',
+ b'36802-TWB-H060\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x18dab5f1, None): [
+ b'36161-TBX-H130\x00\x00',
+ b'36161-TVA-A060\x00\x00',
+ b'36161-TVA-A330\x00\x00',
+ b'36161-TVC-A330\x00\x00',
+ b'36161-TVE-H050\x00\x00',
+ b'36161-TWA-A070\x00\x00',
+ b'36161-TWA-A330\x00\x00',
+ b'36161-TWB-H040\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TVA-A010\x00\x00',
+ b'38897-TVA-A020\x00\x00',
+ b'38897-TVA-A230\x00\x00',
+ b'38897-TVA-A240\x00\x00',
+ b'38897-TWA-A120\x00\x00',
+ b'38897-TWD-J020\x00\x00',
+ ],
+ },
+ CAR.HONDA_CIVIC: {
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-5CG-A040\x00\x00',
+ b'28101-5CG-A050\x00\x00',
+ b'28101-5CG-A070\x00\x00',
+ b'28101-5CG-A080\x00\x00',
+ b'28101-5CG-A320\x00\x00',
+ b'28101-5CG-A810\x00\x00',
+ b'28101-5CG-A820\x00\x00',
+ b'28101-5DJ-A040\x00\x00',
+ b'28101-5DJ-A060\x00\x00',
+ b'28101-5DJ-A510\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TBA-A540\x00\x00',
+ b'57114-TBA-A550\x00\x00',
+ b'57114-TBA-A560\x00\x00',
+ b'57114-TBA-A570\x00\x00',
+ b'57114-TEA-Q220\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TBA,A030\x00\x00',
+ b'39990-TBA-A030\x00\x00',
+ b'39990-TBG-A030\x00\x00',
+ b'39990-TEA-T020\x00\x00',
+ b'39990-TEG-A010\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TBA-A030\x00\x00',
+ b'77959-TBA-A040\x00\x00',
+ b'77959-TBG-A020\x00\x00',
+ b'77959-TBG-A030\x00\x00',
+ b'77959-TEA-Q820\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-TBA-A020\x00\x00',
+ b'36161-TBA-A030\x00\x00',
+ b'36161-TBA-A040\x00\x00',
+ b'36161-TBC-A020\x00\x00',
+ b'36161-TBC-A030\x00\x00',
+ b'36161-TED-Q320\x00\x00',
+ b'36161-TEG-A010\x00\x00',
+ b'36161-TEG-A020\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TBA-A010\x00\x00',
+ b'38897-TBA-A020\x00\x00',
+ ],
+ },
+ CAR.HONDA_CIVIC_BOSCH: {
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-5CG-A920\x00\x00',
+ b'28101-5CG-AB10\x00\x00',
+ b'28101-5CG-C110\x00\x00',
+ b'28101-5CG-C220\x00\x00',
+ b'28101-5CG-C320\x00\x00',
+ b'28101-5CG-G020\x00\x00',
+ b'28101-5CG-L020\x00\x00',
+ b'28101-5CK-A130\x00\x00',
+ b'28101-5CK-A140\x00\x00',
+ b'28101-5CK-A150\x00\x00',
+ b'28101-5CK-C130\x00\x00',
+ b'28101-5CK-C140\x00\x00',
+ b'28101-5CK-C150\x00\x00',
+ b'28101-5CK-G210\x00\x00',
+ b'28101-5CK-J710\x00\x00',
+ b'28101-5CK-Q610\x00\x00',
+ b'28101-5DJ-A610\x00\x00',
+ b'28101-5DJ-A710\x00\x00',
+ b'28101-5DV-E330\x00\x00',
+ b'28101-5DV-E610\x00\x00',
+ b'28101-5DV-E820\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TBG-A330\x00\x00',
+ b'57114-TBG-A340\x00\x00',
+ b'57114-TBG-A350\x00\x00',
+ b'57114-TGG-A340\x00\x00',
+ b'57114-TGG-C320\x00\x00',
+ b'57114-TGG-G320\x00\x00',
+ b'57114-TGG-L320\x00\x00',
+ b'57114-TGG-L330\x00\x00',
+ b'57114-TGH-L130\x00\x00',
+ b'57114-TGK-T320\x00\x00',
+ b'57114-TGL-G330\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TBA-C020\x00\x00',
+ b'39990-TBA-C120\x00\x00',
+ b'39990-TEA-T820\x00\x00',
+ b'39990-TEZ-T020\x00\x00',
+ b'39990-TGG,A020\x00\x00',
+ b'39990-TGG,A120\x00\x00',
+ b'39990-TGG-A020\x00\x00',
+ b'39990-TGG-A120\x00\x00',
+ b'39990-TGG-J510\x00\x00',
+ b'39990-TGH-J530\x00\x00',
+ b'39990-TGL-E130\x00\x00',
+ b'39990-TGN-E120\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TBA-A060\x00\x00',
+ b'77959-TBG-A050\x00\x00',
+ b'77959-TEA-G020\x00\x00',
+ b'77959-TGG-A020\x00\x00',
+ b'77959-TGG-A030\x00\x00',
+ b'77959-TGG-E010\x00\x00',
+ b'77959-TGG-G010\x00\x00',
+ b'77959-TGG-G110\x00\x00',
+ b'77959-TGG-J320\x00\x00',
+ b'77959-TGG-Z820\x00\x00',
+ b'77959-TGH-J110\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36802-TBA-A150\x00\x00',
+ b'36802-TBA-A160\x00\x00',
+ b'36802-TFJ-G060\x00\x00',
+ b'36802-TGG-A050\x00\x00',
+ b'36802-TGG-A060\x00\x00',
+ b'36802-TGG-A070\x00\x00',
+ b'36802-TGG-A130\x00\x00',
+ b'36802-TGG-G040\x00\x00',
+ b'36802-TGG-G130\x00\x00',
+ b'36802-TGH-A140\x00\x00',
+ b'36802-TGK-Q120\x00\x00',
+ b'36802-TGL-G040\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x18dab5f1, None): [
+ b'36161-TBA-A130\x00\x00',
+ b'36161-TBA-A140\x00\x00',
+ b'36161-TFJ-G070\x00\x00',
+ b'36161-TGG-A060\x00\x00',
+ b'36161-TGG-A080\x00\x00',
+ b'36161-TGG-A120\x00\x00',
+ b'36161-TGG-G050\x00\x00',
+ b'36161-TGG-G070\x00\x00',
+ b'36161-TGG-G130\x00\x00',
+ b'36161-TGG-G140\x00\x00',
+ b'36161-TGH-A140\x00\x00',
+ b'36161-TGK-Q120\x00\x00',
+ b'36161-TGL-G050\x00\x00',
+ b'36161-TGL-G070\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TBA-A020\x00\x00',
+ b'38897-TBA-A110\x00\x00',
+ b'38897-TGH-A010\x00\x00',
+ ],
+ (Ecu.electricBrakeBooster, 0x18da2bf1, None): [
+ b'39494-TGL-G030\x00\x00',
+ ],
+ },
+ CAR.HONDA_CIVIC_BOSCH_DIESEL: {
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-59Y-G220\x00\x00',
+ b'28101-59Y-G620\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TGN-E320\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TFK-G020\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TFK-G210\x00\x00',
+ b'77959-TGN-G220\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36802-TFK-G130\x00\x00',
+ b'36802-TGN-G130\x00\x00',
+ ],
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-TGN-E010\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x18dab5f1, None): [
+ b'36161-TFK-G130\x00\x00',
+ b'36161-TGN-G130\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TBA-A020\x00\x00',
+ ],
+ },
+ CAR.HONDA_CRV: {
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-T1W-A230\x00\x00',
+ b'57114-T1W-A240\x00\x00',
+ b'57114-TFF-A940\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-T0A-A230\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-T1W-A830\x00\x00',
+ b'36161-T1W-C830\x00\x00',
+ b'36161-T1X-A830\x00\x00',
+ ],
+ },
+ CAR.HONDA_CRV_5G: {
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-5RG-A020\x00\x00',
+ b'28101-5RG-A030\x00\x00',
+ b'28101-5RG-A040\x00\x00',
+ b'28101-5RG-A120\x00\x00',
+ b'28101-5RG-A220\x00\x00',
+ b'28101-5RH-A020\x00\x00',
+ b'28101-5RH-A030\x00\x00',
+ b'28101-5RH-A040\x00\x00',
+ b'28101-5RH-A120\x00\x00',
+ b'28101-5RH-A220\x00\x00',
+ b'28101-5RL-Q010\x00\x00',
+ b'28101-5RM-F010\x00\x00',
+ b'28101-5RM-K010\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TLA-A040\x00\x00',
+ b'57114-TLA-A050\x00\x00',
+ b'57114-TLA-A060\x00\x00',
+ b'57114-TLB-A830\x00\x00',
+ b'57114-TMC-Z040\x00\x00',
+ b'57114-TMC-Z050\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TLA,A040\x00\x00',
+ b'39990-TLA-A040\x00\x00',
+ b'39990-TLA-A110\x00\x00',
+ b'39990-TLA-A220\x00\x00',
+ b'39990-TME-T030\x00\x00',
+ b'39990-TME-T120\x00\x00',
+ b'39990-TMT-T010\x00\x00',
+ ],
+ (Ecu.electricBrakeBooster, 0x18da2bf1, None): [
+ b'46114-TLA-A040\x00\x00',
+ b'46114-TLA-A050\x00\x00',
+ b'46114-TLA-A930\x00\x00',
+ b'46114-TMC-U020\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TLA-A010\x00\x00',
+ b'38897-TLA-A110\x00\x00',
+ b'38897-TNY-G010\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36802-TLA-A040\x00\x00',
+ b'36802-TLA-A050\x00\x00',
+ b'36802-TLA-A060\x00\x00',
+ b'36802-TLA-A070\x00\x00',
+ b'36802-TMC-Q040\x00\x00',
+ b'36802-TMC-Q070\x00\x00',
+ b'36802-TNY-A030\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x18dab5f1, None): [
+ b'36161-TLA-A060\x00\x00',
+ b'36161-TLA-A070\x00\x00',
+ b'36161-TLA-A080\x00\x00',
+ b'36161-TMC-Q020\x00\x00',
+ b'36161-TMC-Q030\x00\x00',
+ b'36161-TMC-Q040\x00\x00',
+ b'36161-TNY-A020\x00\x00',
+ b'36161-TNY-A030\x00\x00',
+ b'36161-TNY-A040\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TLA-A240\x00\x00',
+ b'77959-TLA-A250\x00\x00',
+ b'77959-TLA-A320\x00\x00',
+ b'77959-TLA-A410\x00\x00',
+ b'77959-TLA-A420\x00\x00',
+ b'77959-TLA-Q040\x00\x00',
+ b'77959-TLA-Z040\x00\x00',
+ b'77959-TMM-F040\x00\x00',
+ ],
+ },
+ CAR.HONDA_CRV_EU: {
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-T1V-G920\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-T1V-G520\x00\x00',
+ ],
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-T1V-G010\x00\x00',
+ ],
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-5LH-E120\x00\x00',
+ b'28103-5LH-E100\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-T1G-G940\x00\x00',
+ ],
+ },
+ CAR.HONDA_CRV_HYBRID: {
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TMB-H030\x00\x00',
+ b'57114-TPA-G020\x00\x00',
+ b'57114-TPG-A020\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TMA-H020\x00\x00',
+ b'39990-TPA-G030\x00\x00',
+ b'39990-TPG-A020\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TMA-H110\x00\x00',
+ b'38897-TPG-A110\x00\x00',
+ b'38897-TPG-A210\x00\x00',
+ ],
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-TMB-H510\x00\x00',
+ b'54008-TMB-H610\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x18dab5f1, None): [
+ b'36161-TMB-H040\x00\x00',
+ b'36161-TPA-E050\x00\x00',
+ b'36161-TPG-A030\x00\x00',
+ b'36161-TPG-A040\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36802-TMB-H040\x00\x00',
+ b'36802-TPA-E040\x00\x00',
+ b'36802-TPG-A020\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TLA-C320\x00\x00',
+ b'77959-TLA-C410\x00\x00',
+ b'77959-TLA-C420\x00\x00',
+ b'77959-TLA-G220\x00\x00',
+ b'77959-TLA-H240\x00\x00',
+ ],
+ },
+ CAR.HONDA_FIT: {
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-T5R-L020\x00\x00',
+ b'57114-T5R-L220\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-T5R-C020\x00\x00',
+ b'39990-T5R-C030\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-T5A-J010\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-T5R-A040\x00\x00',
+ b'36161-T5R-A240\x00\x00',
+ b'36161-T5R-A520\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-T5R-A230\x00\x00',
+ ],
+ },
+ CAR.HONDA_FREED: {
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TDK-J010\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TDK-J050\x00\x00',
+ b'39990-TDK-N020\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TDK-J120\x00\x00',
+ b'57114-TDK-J330\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-TDK-J070\x00\x00',
+ b'36161-TDK-J080\x00\x00',
+ b'36161-TDK-J530\x00\x00',
+ ],
+ },
+ CAR.HONDA_ODYSSEY: {
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-THR-A010\x00\x00',
+ b'38897-THR-A020\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-THR-A020\x00\x00',
+ b'39990-THR-A030\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-THR-A010\x00\x00',
+ b'77959-THR-A110\x00\x00',
+ b'77959-THR-X010\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-THR-A020\x00\x00',
+ b'36161-THR-A030\x00\x00',
+ b'36161-THR-A110\x00\x00',
+ b'36161-THR-A720\x00\x00',
+ b'36161-THR-A730\x00\x00',
+ b'36161-THR-A810\x00\x00',
+ b'36161-THR-A910\x00\x00',
+ b'36161-THR-C010\x00\x00',
+ b'36161-THR-D110\x00\x00',
+ b'36161-THR-K020\x00\x00',
+ ],
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-5NZ-A110\x00\x00',
+ b'28101-5NZ-A310\x00\x00',
+ b'28101-5NZ-C310\x00\x00',
+ b'28102-5MX-A001\x00\x00',
+ b'28102-5MX-A600\x00\x00',
+ b'28102-5MX-A610\x00\x00',
+ b'28102-5MX-A700\x00\x00',
+ b'28102-5MX-A710\x00\x00',
+ b'28102-5MX-A900\x00\x00',
+ b'28102-5MX-A910\x00\x00',
+ b'28102-5MX-C001\x00\x00',
+ b'28102-5MX-C610\x00\x00',
+ b'28102-5MX-C910\x00\x00',
+ b'28102-5MX-D001\x00\x00',
+ b'28102-5MX-D710\x00\x00',
+ b'28102-5MX-K610\x00\x00',
+ b'28103-5NZ-A100\x00\x00',
+ b'28103-5NZ-A300\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-THR-A040\x00\x00',
+ b'57114-THR-A110\x00\x00',
+ ],
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-THR-A020\x00\x00',
+ ],
+ },
+ CAR.HONDA_ODYSSEY_CHN: {
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-T6D-H220\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-T6A-J010\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-T6A-P040\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-T6A-P110\x00\x00',
+ ],
+ },
+ CAR.HONDA_PILOT: {
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-TG7-A520\x00\x00',
+ b'54008-TG7-A530\x00\x00',
+ ],
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-5EY-A040\x00\x00',
+ b'28101-5EY-A050\x00\x00',
+ b'28101-5EY-A100\x00\x00',
+ b'28101-5EY-A430\x00\x00',
+ b'28101-5EY-A500\x00\x00',
+ b'28101-5EZ-A050\x00\x00',
+ b'28101-5EZ-A060\x00\x00',
+ b'28101-5EZ-A100\x00\x00',
+ b'28101-5EZ-A210\x00\x00',
+ b'28101-5EZ-A330\x00\x00',
+ b'28101-5EZ-A430\x00\x00',
+ b'28101-5EZ-A500\x00\x00',
+ b'28101-5EZ-A600\x00\x00',
+ b'28101-5EZ-A700\x00\x00',
+ b'28103-5EY-A110\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TG7-A030\x00\x00',
+ b'38897-TG7-A040\x00\x00',
+ b'38897-TG7-A110\x00\x00',
+ b'38897-TG7-A210\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TG7-A030\x00\x00',
+ b'39990-TG7-A040\x00\x00',
+ b'39990-TG7-A060\x00\x00',
+ b'39990-TG7-A070\x00\x00',
+ b'39990-TGS-A230\x00\x00',
+ b'39990-TGS-A320\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-TG7-A310\x00\x00',
+ b'36161-TG7-A520\x00\x00',
+ b'36161-TG7-A630\x00\x00',
+ b'36161-TG7-A720\x00\x00',
+ b'36161-TG7-A820\x00\x00',
+ b'36161-TG7-A930\x00\x00',
+ b'36161-TG7-C520\x00\x00',
+ b'36161-TG7-D520\x00\x00',
+ b'36161-TG7-D630\x00\x00',
+ b'36161-TG7-Y630\x00\x00',
+ b'36161-TG8-A410\x00\x00',
+ b'36161-TG8-A520\x00\x00',
+ b'36161-TG8-A630\x00\x00',
+ b'36161-TG8-A720\x00\x00',
+ b'36161-TG8-A830\x00\x00',
+ b'36161-TGS-A030\x00\x00',
+ b'36161-TGS-A130\x00\x00',
+ b'36161-TGS-A220\x00\x00',
+ b'36161-TGS-A320\x00\x00',
+ b'36161-TGT-A030\x00\x00',
+ b'36161-TGT-A130\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TG7-A020\x00\x00',
+ b'77959-TG7-A110\x00\x00',
+ b'77959-TG7-A210\x00\x00',
+ b'77959-TG7-Y210\x00\x00',
+ b'77959-TGS-A010\x00\x00',
+ b'77959-TGS-A110\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TG7-A130\x00\x00',
+ b'57114-TG7-A140\x00\x00',
+ b'57114-TG7-A230\x00\x00',
+ b'57114-TG7-A240\x00\x00',
+ b'57114-TG7-A630\x00\x00',
+ b'57114-TG7-A730\x00\x00',
+ b'57114-TG8-A140\x00\x00',
+ b'57114-TG8-A230\x00\x00',
+ b'57114-TG8-A240\x00\x00',
+ b'57114-TG8-A630\x00\x00',
+ b'57114-TG8-A730\x00\x00',
+ b'57114-TGS-A530\x00\x00',
+ b'57114-TGT-A530\x00\x00',
+ ],
+ },
+ CAR.ACURA_RDX: {
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TX4-A220\x00\x00',
+ b'57114-TX5-A220\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-TX4-A030\x00\x00',
+ b'36161-TX5-A030\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TX4-B010\x00\x00',
+ b'77959-TX4-C010\x00\x00',
+ b'77959-TX4-C020\x00\x00',
+ ],
+ },
+ CAR.ACURA_RDX_3G: {
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TJB-A030\x00\x00',
+ b'57114-TJB-A040\x00\x00',
+ b'57114-TJB-A120\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36802-TJB-A040\x00\x00',
+ b'36802-TJB-A050\x00\x00',
+ b'36802-TJB-A540\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x18dab5f1, None): [
+ b'36161-TJB-A040\x00\x00',
+ b'36161-TJB-A530\x00\x00',
+ ],
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-TJB-A520\x00\x00',
+ b'54008-TJB-A530\x00\x00',
+ ],
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28102-5YK-A610\x00\x00',
+ b'28102-5YK-A620\x00\x00',
+ b'28102-5YK-A630\x00\x00',
+ b'28102-5YK-A700\x00\x00',
+ b'28102-5YK-A711\x00\x00',
+ b'28102-5YK-A800\x00\x00',
+ b'28102-5YL-A620\x00\x00',
+ b'28102-5YL-A700\x00\x00',
+ b'28102-5YL-A711\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TJB-A040\x00\x00',
+ b'77959-TJB-A120\x00\x00',
+ b'77959-TJB-A210\x00\x00',
+ ],
+ (Ecu.electricBrakeBooster, 0x18da2bf1, None): [
+ b'46114-TJB-A040\x00\x00',
+ b'46114-TJB-A050\x00\x00',
+ b'46114-TJB-A060\x00\x00',
+ b'46114-TJB-A120\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TJB-A040\x00\x00',
+ b'38897-TJB-A110\x00\x00',
+ b'38897-TJB-A120\x00\x00',
+ b'38897-TJB-A220\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TJB-A030\x00\x00',
+ b'39990-TJB-A040\x00\x00',
+ b'39990-TJB-A070\x00\x00',
+ b'39990-TJB-A130\x00\x00',
+ ],
+ },
+ CAR.HONDA_RIDGELINE: {
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-T6Z-A020\x00\x00',
+ b'39990-T6Z-A030\x00\x00',
+ b'39990-T6Z-A050\x00\x00',
+ b'39990-T6Z-A110\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-T6Z-A020\x00\x00',
+ b'36161-T6Z-A310\x00\x00',
+ b'36161-T6Z-A420\x00\x00',
+ b'36161-T6Z-A520\x00\x00',
+ b'36161-T6Z-A620\x00\x00',
+ b'36161-T6Z-A720\x00\x00',
+ b'36161-TJZ-A120\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-T6Z-A010\x00\x00',
+ b'38897-T6Z-A110\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-T6Z-A020\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-T6Z-A120\x00\x00',
+ b'57114-T6Z-A130\x00\x00',
+ b'57114-T6Z-A520\x00\x00',
+ b'57114-T6Z-A610\x00\x00',
+ b'57114-TJZ-A520\x00\x00',
+ ],
+ },
+ CAR.HONDA_INSIGHT: {
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TXM-A040\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36802-TXM-A070\x00\x00',
+ b'36802-TXM-A080\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x18dab5f1, None): [
+ b'36161-TXM-A050\x00\x00',
+ b'36161-TXM-A060\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TXM-A230\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TXM-A030\x00\x00',
+ b'57114-TXM-A040\x00\x00',
+ ],
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-TWA-A910\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TXM-A020\x00\x00',
+ ],
+ },
+ CAR.HONDA_HRV: {
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-T7A-A010\x00\x00',
+ b'38897-T7A-A110\x00\x00',
+ ],
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-THX-A020\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-T7A-A040\x00\x00',
+ b'36161-T7A-A140\x00\x00',
+ b'36161-T7A-A240\x00\x00',
+ b'36161-T7A-C440\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-T7A-A230\x00\x00',
+ ],
+ },
+ CAR.HONDA_HRV_3G: {
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-3M0-G110\x00\x00',
+ b'39990-3W0-A030\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-3M0-M110\x00\x00',
+ b'38897-3W1-A010\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-3M0-K840\x00\x00',
+ b'77959-3V0-A820\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'8S102-3M6-P030\x00\x00',
+ b'8S102-3W0-A060\x00\x00',
+ b'8S102-3W0-AB10\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-3M6-M010\x00\x00',
+ b'57114-3W0-A040\x00\x00',
+ ],
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-6EH-A010\x00\x00',
+ b'28101-6JC-M310\x00\x00',
+ ],
+ (Ecu.electricBrakeBooster, 0x18da2bf1, None): [
+ b'46114-3W0-A020\x00\x00',
+ ],
+ },
+ CAR.ACURA_ILX: {
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TX6-A010\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-TV9-A140\x00\x00',
+ b'36161-TX6-A030\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TX6-A230\x00\x00',
+ b'77959-TX6-C210\x00\x00',
+ ],
+ },
+ CAR.HONDA_E: {
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-TYF-N030\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-TYF-E140\x00\x00',
+ ],
+ (Ecu.shiftByWire, 0x18da0bf1, None): [
+ b'54008-TYF-E010\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-TYF-G430\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36802-TYF-E030\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x18dab5f1, None): [
+ b'36161-TYF-E020\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-TYF-E030\x00\x00',
+ ],
+ },
+ CAR.HONDA_CIVIC_2022: {
+ (Ecu.eps, 0x18da30f1, None): [
+ b'39990-T24-T120\x00\x00',
+ b'39990-T39-A130\x00\x00',
+ b'39990-T43-J020\x00\x00',
+ ],
+ (Ecu.gateway, 0x18daeff1, None): [
+ b'38897-T20-A020\x00\x00',
+ b'38897-T20-A210\x00\x00',
+ b'38897-T20-A310\x00\x00',
+ b'38897-T20-A510\x00\x00',
+ b'38897-T21-A010\x00\x00',
+ b'38897-T24-Z120\x00\x00',
+ ],
+ (Ecu.srs, 0x18da53f1, None): [
+ b'77959-T20-A970\x00\x00',
+ b'77959-T20-A980\x00\x00',
+ b'77959-T20-M820\x00\x00',
+ b'77959-T47-A940\x00\x00',
+ b'77959-T47-A950\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x18dab0f1, None): [
+ b'36161-T20-A060\x00\x00',
+ b'36161-T20-A070\x00\x00',
+ b'36161-T20-A080\x00\x00',
+ b'36161-T24-T070\x00\x00',
+ b'36161-T47-A050\x00\x00',
+ b'36161-T47-A070\x00\x00',
+ b'8S102-T20-AA10\x00\x00',
+ b'8S102-T47-AA10\x00\x00',
+ ],
+ (Ecu.vsa, 0x18da28f1, None): [
+ b'57114-T20-AB40\x00\x00',
+ b'57114-T24-TB30\x00\x00',
+ b'57114-T43-JB30\x00\x00',
+ ],
+ (Ecu.transmission, 0x18da1ef1, None): [
+ b'28101-65D-A020\x00\x00',
+ b'28101-65D-A120\x00\x00',
+ b'28101-65H-A020\x00\x00',
+ b'28101-65H-A120\x00\x00',
+ b'28101-65J-N010\x00\x00',
+ ],
+ },
+}
diff --git a/opendbc/car/honda/hondacan.py b/opendbc/car/honda/hondacan.py
new file mode 100644
index 0000000000..2a9465b65b
--- /dev/null
+++ b/opendbc/car/honda/hondacan.py
@@ -0,0 +1,211 @@
+from opendbc.car import CanBusBase
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.honda.values import HondaFlags, HONDA_BOSCH, HONDA_BOSCH_RADARLESS, CAR, CarControllerParams
+
+# CAN bus layout with relay
+# 0 = ACC-CAN - radar side
+# 1 = F-CAN B - powertrain
+# 2 = ACC-CAN - camera side
+# 3 = F-CAN A - OBDII port
+
+
+class CanBus(CanBusBase):
+ def __init__(self, CP=None, fingerprint=None) -> None:
+ # use fingerprint if specified
+ super().__init__(CP if fingerprint is None else None, fingerprint)
+
+ if CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS):
+ self._pt, self._radar = self.offset + 1, self.offset
+ else:
+ self._pt, self._radar = self.offset, self.offset + 1
+
+ @property
+ def pt(self) -> int:
+ return self._pt
+
+ @property
+ def radar(self) -> int:
+ return self._radar
+
+ @property
+ def camera(self) -> int:
+ return self.offset + 2
+
+
+def get_lkas_cmd_bus(CAN, car_fingerprint, radar_disabled=False):
+ no_radar = car_fingerprint in HONDA_BOSCH_RADARLESS
+ if radar_disabled or no_radar:
+ # when radar is disabled, steering commands are sent directly to powertrain bus
+ return CAN.pt
+ # normally steering commands are sent to radar, which forwards them to powertrain bus
+ return 0
+
+
+def get_cruise_speed_conversion(car_fingerprint: str, is_metric: bool) -> float:
+ # on certain cars, CRUISE_SPEED changes to imperial with car's unit setting
+ return CV.MPH_TO_MS if car_fingerprint in HONDA_BOSCH_RADARLESS and not is_metric else CV.KPH_TO_MS
+
+
+def create_brake_command(packer, CAN, apply_brake, pump_on, pcm_override, pcm_cancel_cmd, fcw, car_fingerprint, stock_brake):
+ # TODO: do we loose pressure if we keep pump off for long?
+ brakelights = apply_brake > 0
+ brake_rq = apply_brake > 0
+ pcm_fault_cmd = False
+
+ values = {
+ "COMPUTER_BRAKE": apply_brake,
+ "BRAKE_PUMP_REQUEST": pump_on,
+ "CRUISE_OVERRIDE": pcm_override,
+ "CRUISE_FAULT_CMD": pcm_fault_cmd,
+ "CRUISE_CANCEL_CMD": pcm_cancel_cmd,
+ "COMPUTER_BRAKE_REQUEST": brake_rq,
+ "SET_ME_1": 1,
+ "BRAKE_LIGHTS": brakelights,
+ "CHIME": stock_brake["CHIME"] if fcw else 0, # send the chime for stock fcw
+ "FCW": fcw << 1, # TODO: Why are there two bits for fcw?
+ "AEB_REQ_1": 0,
+ "AEB_REQ_2": 0,
+ "AEB_STATUS": 0,
+ }
+ return packer.make_can_msg("BRAKE_COMMAND", CAN.pt, values)
+
+
+def create_acc_commands(packer, CAN, enabled, active, accel, gas, stopping_counter, car_fingerprint):
+ commands = []
+ min_gas_accel = CarControllerParams.BOSCH_GAS_LOOKUP_BP[0]
+
+ control_on = 5 if enabled else 0
+ gas_command = gas if active and accel > min_gas_accel else -30000
+ accel_command = accel if active else 0
+ braking = 1 if active and accel < min_gas_accel else 0
+ standstill = 1 if active and stopping_counter > 0 else 0
+ standstill_release = 1 if active and stopping_counter == 0 else 0
+
+ # common ACC_CONTROL values
+ acc_control_values = {
+ 'ACCEL_COMMAND': accel_command,
+ 'STANDSTILL': standstill,
+ }
+
+ if car_fingerprint in HONDA_BOSCH_RADARLESS:
+ acc_control_values.update({
+ "CONTROL_ON": enabled,
+ "IDLESTOP_ALLOW": stopping_counter > 200, # allow idle stop after 4 seconds (50 Hz)
+ })
+ else:
+ acc_control_values.update({
+ # setting CONTROL_ON causes car to set POWERTRAIN_DATA->ACC_STATUS = 1
+ "CONTROL_ON": control_on,
+ "GAS_COMMAND": gas_command, # used for gas
+ "BRAKE_LIGHTS": braking,
+ "BRAKE_REQUEST": braking,
+ "STANDSTILL_RELEASE": standstill_release,
+ })
+ acc_control_on_values = {
+ "SET_TO_3": 0x03,
+ "CONTROL_ON": enabled,
+ "SET_TO_FF": 0xff,
+ "SET_TO_75": 0x75,
+ "SET_TO_30": 0x30,
+ }
+ commands.append(packer.make_can_msg("ACC_CONTROL_ON", CAN.pt, acc_control_on_values))
+
+ commands.append(packer.make_can_msg("ACC_CONTROL", CAN.pt, acc_control_values))
+ return commands
+
+
+def create_steering_control(packer, CAN, apply_steer, lkas_active, car_fingerprint, radar_disabled):
+ values = {
+ "STEER_TORQUE": apply_steer if lkas_active else 0,
+ "STEER_TORQUE_REQUEST": lkas_active,
+ }
+ bus = get_lkas_cmd_bus(CAN, car_fingerprint, radar_disabled)
+ return packer.make_can_msg("STEERING_CONTROL", bus, values)
+
+
+def create_bosch_supplemental_1(packer, CAN, car_fingerprint):
+ # non-active params
+ values = {
+ "SET_ME_X04": 0x04,
+ "SET_ME_X80": 0x80,
+ "SET_ME_X10": 0x10,
+ }
+ bus = get_lkas_cmd_bus(CAN, car_fingerprint)
+ return packer.make_can_msg("BOSCH_SUPPLEMENTAL_1", bus, values)
+
+
+def create_ui_commands(packer, CAN, CP, enabled, pcm_speed, hud, is_metric, acc_hud, lkas_hud):
+ commands = []
+ radar_disabled = CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and CP.openpilotLongitudinalControl
+ bus_lkas = get_lkas_cmd_bus(CAN, CP.carFingerprint, radar_disabled)
+
+ if CP.openpilotLongitudinalControl:
+ acc_hud_values = {
+ 'CRUISE_SPEED': hud.v_cruise,
+ 'ENABLE_MINI_CAR': 1 if enabled else 0,
+ # only moves the lead car without ACC_ON
+ 'HUD_DISTANCE': hud.lead_distance_bars, # wraps to 0 at 4 bars
+ 'IMPERIAL_UNIT': int(not is_metric),
+ 'HUD_LEAD': 2 if enabled and hud.lead_visible else 1 if enabled else 0,
+ 'SET_ME_X01_2': 1,
+ }
+
+ if CP.carFingerprint in HONDA_BOSCH:
+ acc_hud_values['ACC_ON'] = int(enabled)
+ acc_hud_values['FCM_OFF'] = 1
+ acc_hud_values['FCM_OFF_2'] = 1
+ else:
+ # Shows the distance bars, TODO: stock camera shows updates temporarily while disabled
+ acc_hud_values['ACC_ON'] = int(enabled)
+ acc_hud_values['PCM_SPEED'] = pcm_speed * CV.MS_TO_KPH
+ acc_hud_values['PCM_GAS'] = hud.pcm_accel
+ acc_hud_values['SET_ME_X01'] = 1
+ acc_hud_values['FCM_OFF'] = acc_hud['FCM_OFF']
+ acc_hud_values['FCM_OFF_2'] = acc_hud['FCM_OFF_2']
+ acc_hud_values['FCM_PROBLEM'] = acc_hud['FCM_PROBLEM']
+ acc_hud_values['ICONS'] = acc_hud['ICONS']
+ commands.append(packer.make_can_msg("ACC_HUD", CAN.pt, acc_hud_values))
+
+ lkas_hud_values = {
+ 'SET_ME_X41': 0x41,
+ 'STEERING_REQUIRED': hud.steer_required,
+ 'SOLID_LANES': hud.lanes_visible,
+ 'BEEP': 0,
+ }
+
+ if CP.carFingerprint in HONDA_BOSCH_RADARLESS:
+ lkas_hud_values['LANE_LINES'] = 3
+ lkas_hud_values['DASHED_LANES'] = hud.lanes_visible
+ # car likely needs to see LKAS_PROBLEM fall within a specific time frame, so forward from camera
+ lkas_hud_values['LKAS_PROBLEM'] = lkas_hud['LKAS_PROBLEM']
+
+ if not (CP.flags & HondaFlags.BOSCH_EXT_HUD):
+ lkas_hud_values['SET_ME_X48'] = 0x48
+
+ if CP.flags & HondaFlags.BOSCH_EXT_HUD and not CP.openpilotLongitudinalControl:
+ commands.append(packer.make_can_msg('LKAS_HUD_A', bus_lkas, lkas_hud_values))
+ commands.append(packer.make_can_msg('LKAS_HUD_B', bus_lkas, lkas_hud_values))
+ else:
+ commands.append(packer.make_can_msg('LKAS_HUD', bus_lkas, lkas_hud_values))
+
+ if radar_disabled:
+ radar_hud_values = {
+ 'CMBS_OFF': 0x01,
+ 'SET_TO_1': 0x01,
+ }
+ commands.append(packer.make_can_msg('RADAR_HUD', CAN.pt, radar_hud_values))
+
+ if CP.carFingerprint == CAR.HONDA_CIVIC_BOSCH:
+ commands.append(packer.make_can_msg("LEGACY_BRAKE_COMMAND", CAN.pt, {}))
+
+ return commands
+
+
+def spam_buttons_command(packer, CAN, button_val, car_fingerprint):
+ values = {
+ 'CRUISE_BUTTONS': button_val,
+ 'CRUISE_SETTING': 0,
+ }
+ # send buttons to camera on radarless cars
+ bus = CAN.camera if car_fingerprint in HONDA_BOSCH_RADARLESS else CAN.pt
+ return packer.make_can_msg("SCM_BUTTONS", bus, values)
diff --git a/opendbc/car/honda/interface.py b/opendbc/car/honda/interface.py
new file mode 100755
index 0000000000..9f2036e1cb
--- /dev/null
+++ b/opendbc/car/honda/interface.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+from panda import Panda
+from opendbc.car import get_safety_config, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.common.numpy_fast import interp
+from opendbc.car.honda.hondacan import CanBus
+from opendbc.car.honda.values import CarControllerParams, HondaFlags, CAR, HONDA_BOSCH, \
+ HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS
+from opendbc.car.interfaces import CarInterfaceBase
+from opendbc.car.disable_ecu import disable_ecu
+
+TransmissionType = structs.CarParams.TransmissionType
+
+
+class CarInterface(CarInterfaceBase):
+ @staticmethod
+ def get_pid_accel_limits(CP, current_speed, cruise_speed):
+ if CP.carFingerprint in HONDA_BOSCH:
+ return CarControllerParams.BOSCH_ACCEL_MIN, CarControllerParams.BOSCH_ACCEL_MAX
+ else:
+ # NIDECs don't allow acceleration near cruise_speed,
+ # so limit limits of pid to prevent windup
+ ACCEL_MAX_VALS = [CarControllerParams.NIDEC_ACCEL_MAX, 0.2]
+ ACCEL_MAX_BP = [cruise_speed - 2., cruise_speed - .2]
+ return CarControllerParams.NIDEC_ACCEL_MIN, interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS)
+
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "honda"
+
+ CAN = CanBus(ret, fingerprint)
+
+ if candidate in HONDA_BOSCH:
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hondaBosch)]
+ ret.radarUnavailable = True
+ # Disable the radar and let openpilot control longitudinal
+ # WARNING: THIS DISABLES AEB!
+ # If Bosch radarless, this blocks ACC messages from the camera
+ ret.experimentalLongitudinalAvailable = True
+ ret.openpilotLongitudinalControl = experimental_long
+ ret.pcmCruise = not ret.openpilotLongitudinalControl
+ else:
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hondaNidec)]
+ ret.openpilotLongitudinalControl = True
+
+ ret.pcmCruise = True
+
+ if candidate == CAR.HONDA_CRV_5G:
+ ret.enableBsm = 0x12f8bfa7 in fingerprint[CAN.radar]
+
+ # Detect Bosch cars with new HUD msgs
+ if any(0x33DA in f for f in fingerprint.values()):
+ ret.flags |= HondaFlags.BOSCH_EXT_HUD.value
+
+ # Accord ICE 1.5T CVT has different gearbox message
+ if candidate == CAR.HONDA_ACCORD and 0x191 in fingerprint[CAN.pt]:
+ ret.transmissionType = TransmissionType.cvt
+
+ # Certain Hondas have an extra steering sensor at the bottom of the steering rack,
+ # which improves controls quality as it removes the steering column torsion from feedback.
+ # Tire stiffness factor fictitiously lower if it includes the steering column torsion effect.
+ # For modeling details, see p.198-200 in "The Science of Vehicle Dynamics (2014), M. Guiggiani"
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0], [0]]
+ ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]]
+ ret.lateralTuning.pid.kf = 0.00006 # conservative feed-forward
+
+ if candidate in HONDA_BOSCH:
+ ret.longitudinalActuatorDelay = 0.5 # s
+ if candidate in HONDA_BOSCH_RADARLESS:
+ ret.stopAccel = CarControllerParams.BOSCH_ACCEL_MIN # stock uses -4.0 m/s^2 once stopped but limited by safety model
+ else:
+ # default longitudinal tuning for all hondas
+ ret.longitudinalTuning.kiBP = [0., 5., 35.]
+ ret.longitudinalTuning.kiV = [1.2, 0.8, 0.5]
+
+ eps_modified = False
+ for fw in car_fw:
+ if fw.ecu == "eps" and b"," in fw.fwVersion:
+ eps_modified = True
+
+ if candidate == CAR.HONDA_CIVIC:
+ if eps_modified:
+ # stock request input values: 0x0000, 0x00DE, 0x014D, 0x01EF, 0x0290, 0x0377, 0x0454, 0x0610, 0x06EE
+ # stock request output values: 0x0000, 0x0917, 0x0DC5, 0x1017, 0x119F, 0x140B, 0x1680, 0x1680, 0x1680
+ # modified request output values: 0x0000, 0x0917, 0x0DC5, 0x1017, 0x119F, 0x140B, 0x1680, 0x2880, 0x3180
+ # stock filter output values: 0x009F, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108
+ # modified filter output values: 0x009F, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0400, 0x0480
+ # note: max request allowed is 4096, but request is capped at 3840 in firmware, so modifications result in 2x max
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560, 8000], [0, 2560, 3840]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.1]]
+ else:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[1.1], [0.33]]
+
+ elif candidate in (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_BOSCH_DIESEL, CAR.HONDA_CIVIC_2022):
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
+
+ elif candidate == CAR.HONDA_ACCORD:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+
+ if eps_modified:
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.09]]
+ else:
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]]
+
+ elif candidate == CAR.ACURA_ILX:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
+
+ elif candidate in (CAR.HONDA_CRV, CAR.HONDA_CRV_EU):
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 1000], [0, 1000]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
+ ret.wheelSpeedFactor = 1.025
+
+ elif candidate == CAR.HONDA_CRV_5G:
+ if eps_modified:
+ # stock request input values: 0x0000, 0x00DB, 0x01BB, 0x0296, 0x0377, 0x0454, 0x0532, 0x0610, 0x067F
+ # stock request output values: 0x0000, 0x0500, 0x0A15, 0x0E6D, 0x1100, 0x1200, 0x129A, 0x134D, 0x1400
+ # modified request output values: 0x0000, 0x0500, 0x0A15, 0x0E6D, 0x1100, 0x1200, 0x1ACD, 0x239A, 0x2800
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560, 10000], [0, 2560, 3840]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.21], [0.07]]
+ else:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.64], [0.192]]
+ ret.wheelSpeedFactor = 1.025
+
+ elif candidate == CAR.HONDA_CRV_HYBRID:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]]
+ ret.wheelSpeedFactor = 1.025
+
+ elif candidate == CAR.HONDA_FIT:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.05]]
+
+ elif candidate == CAR.HONDA_FREED:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.05]]
+
+ elif candidate in (CAR.HONDA_HRV, CAR.HONDA_HRV_3G):
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]]
+ if candidate == CAR.HONDA_HRV:
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.16], [0.025]]
+ ret.wheelSpeedFactor = 1.025
+ else:
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]] # TODO: can probably use some tuning
+
+ elif candidate == CAR.ACURA_RDX:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 1000], [0, 1000]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]]
+
+ elif candidate == CAR.ACURA_RDX_3G:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.06]]
+
+ elif candidate in (CAR.HONDA_ODYSSEY, CAR.HONDA_ODYSSEY_CHN):
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.28], [0.08]]
+ if candidate == CAR.HONDA_ODYSSEY_CHN:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 32767], [0, 32767]] # TODO: determine if there is a dead zone at the top end
+ else:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+
+ elif candidate == CAR.HONDA_PILOT:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.38], [0.11]]
+
+ elif candidate == CAR.HONDA_RIDGELINE:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.38], [0.11]]
+
+ elif candidate == CAR.HONDA_INSIGHT:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]]
+
+ elif candidate == CAR.HONDA_E:
+ ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]] # TODO: can probably use some tuning
+
+ else:
+ raise ValueError(f"unsupported car {candidate}")
+
+ # These cars use alternate user brake msg (0x1BE)
+ # TODO: Only detect feature for Accord/Accord Hybrid, not all Bosch DBCs have BRAKE_MODULE
+ if 0x1BE in fingerprint[CAN.pt] and candidate in (CAR.HONDA_ACCORD, CAR.HONDA_HRV_3G):
+ ret.flags |= HondaFlags.BOSCH_ALT_BRAKE.value
+
+ if ret.flags & HondaFlags.BOSCH_ALT_BRAKE:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_ALT_BRAKE
+
+ # These cars use alternate SCM messages (SCM_FEEDBACK AND SCM_BUTTON)
+ if candidate in HONDA_NIDEC_ALT_SCM_MESSAGES:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_NIDEC_ALT
+
+ if ret.openpilotLongitudinalControl and candidate in HONDA_BOSCH:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_BOSCH_LONG
+
+ if candidate in HONDA_BOSCH_RADARLESS:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_RADARLESS
+
+ # min speed to enable ACC. if car can do stop and go, then set enabling speed
+ # to a negative value, so it won't matter. Otherwise, add 0.5 mph margin to not
+ # conflict with PCM acc
+ ret.autoResumeSng = candidate in (HONDA_BOSCH | {CAR.HONDA_CIVIC})
+ ret.minEnableSpeed = -1. if ret.autoResumeSng else 25.51 * CV.MPH_TO_MS
+
+ ret.steerActuatorDelay = 0.1
+ ret.steerLimitTimer = 0.8
+
+ return ret
+
+ @staticmethod
+ def init(CP, can_recv, can_send):
+ if CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and CP.openpilotLongitudinalControl:
+ disable_ecu(can_recv, can_send, bus=1, addr=0x18DAB0F1, com_cont_req=b'\x28\x83\x03')
diff --git a/opendbc/car/honda/radar_interface.py b/opendbc/car/honda/radar_interface.py
new file mode 100755
index 0000000000..a47cc70e7f
--- /dev/null
+++ b/opendbc/car/honda/radar_interface.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+from opendbc.can.parser import CANParser
+from opendbc.car import structs
+from opendbc.car.interfaces import RadarInterfaceBase
+from opendbc.car.honda.values import DBC
+
+
+def _create_nidec_can_parser(car_fingerprint):
+ radar_messages = [0x400] + list(range(0x430, 0x43A)) + list(range(0x440, 0x446))
+ messages = [(m, 20) for m in radar_messages]
+ return CANParser(DBC[car_fingerprint]['radar'], messages, 1)
+
+
+class RadarInterface(RadarInterfaceBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ self.track_id = 0
+ self.radar_fault = False
+ self.radar_wrong_config = False
+ self.radar_off_can = CP.radarUnavailable
+ self.radar_ts = CP.radarTimeStep
+
+ self.delay = int(round(0.1 / CP.radarTimeStep)) # 0.1s delay of radar
+
+ # Nidec
+ if self.radar_off_can:
+ self.rcp = None
+ else:
+ self.rcp = _create_nidec_can_parser(CP.carFingerprint)
+ self.trigger_msg = 0x445
+ self.updated_messages = set()
+
+ def update(self, can_strings):
+ # in Bosch radar and we are only steering for now, so sleep 0.05s to keep
+ # radard at 20Hz and return no points
+ if self.radar_off_can:
+ return super().update(None)
+
+ vls = self.rcp.update_strings(can_strings)
+ self.updated_messages.update(vls)
+
+ if self.trigger_msg not in self.updated_messages:
+ return None
+
+ rr = self._update(self.updated_messages)
+ self.updated_messages.clear()
+ return rr
+
+ def _update(self, updated_messages):
+ ret = structs.RadarData()
+
+ for ii in sorted(updated_messages):
+ cpt = self.rcp.vl[ii]
+ if ii == 0x400:
+ # check for radar faults
+ self.radar_fault = cpt['RADAR_STATE'] != 0x79
+ self.radar_wrong_config = cpt['RADAR_STATE'] == 0x69
+ elif cpt['LONG_DIST'] < 255:
+ if ii not in self.pts or cpt['NEW_TRACK']:
+ self.pts[ii] = structs.RadarData.RadarPoint()
+ self.pts[ii].trackId = self.track_id
+ self.track_id += 1
+ self.pts[ii].dRel = cpt['LONG_DIST'] # from front of car
+ self.pts[ii].yRel = -cpt['LAT_DIST'] # in car frame's y axis, left is positive
+ self.pts[ii].vRel = cpt['REL_SPEED']
+ self.pts[ii].aRel = float('nan')
+ self.pts[ii].yvRel = float('nan')
+ self.pts[ii].measured = True
+ else:
+ if ii in self.pts:
+ del self.pts[ii]
+
+ errors = []
+ if not self.rcp.can_valid:
+ errors.append("canError")
+ if self.radar_fault:
+ errors.append("fault")
+ if self.radar_wrong_config:
+ errors.append("wrongConfig")
+ ret.errors = errors
+
+ ret.points = list(self.pts.values())
+
+ return ret
diff --git a/opendbc/car/honda/tests/__init__.py b/opendbc/car/honda/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/honda/tests/test_honda.py b/opendbc/car/honda/tests/test_honda.py
new file mode 100644
index 0000000000..946f496d10
--- /dev/null
+++ b/opendbc/car/honda/tests/test_honda.py
@@ -0,0 +1,14 @@
+import re
+
+from opendbc.car.honda.fingerprints import FW_VERSIONS
+
+HONDA_FW_VERSION_RE = br"[A-Z0-9]{5}-[A-Z0-9]{3}(-|,)[A-Z0-9]{4}(\x00){2}$"
+
+
+class TestHondaFingerprint:
+ def test_fw_version_format(self):
+ # Asserts all FW versions follow an expected format
+ for fw_by_ecu in FW_VERSIONS.values():
+ for fws in fw_by_ecu.values():
+ for fw in fws:
+ assert re.match(HONDA_FW_VERSION_RE, fw) is not None, fw
diff --git a/opendbc/car/honda/values.py b/opendbc/car/honda/values.py
new file mode 100644
index 0000000000..70ccecb0ed
--- /dev/null
+++ b/opendbc/car/honda/values.py
@@ -0,0 +1,330 @@
+from dataclasses import dataclass
+from enum import Enum, IntFlag
+
+from panda import uds
+from opendbc.car import CarSpecs, PlatformConfig, Platforms, dbc_dict, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16
+
+Ecu = structs.CarParams.Ecu
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+
+
+class CarControllerParams:
+ # Allow small margin below -3.5 m/s^2 from ISO 15622:2018 since we
+ # perform the closed loop control, and might need some
+ # to apply some more braking if we're on a downhill slope.
+ # Our controller should still keep the 2 second average above
+ # -3.5 m/s^2 as per planner limits
+ NIDEC_ACCEL_MIN = -4.0 # m/s^2
+ NIDEC_ACCEL_MAX = 1.6 # m/s^2, lower than 2.0 m/s^2 for tuning reasons
+
+ NIDEC_ACCEL_LOOKUP_BP = [-1., 0., .6]
+ NIDEC_ACCEL_LOOKUP_V = [-4.8, 0., 2.0]
+
+ NIDEC_MAX_ACCEL_V = [0.5, 2.4, 1.4, 0.6]
+ NIDEC_MAX_ACCEL_BP = [0.0, 4.0, 10., 20.]
+
+ NIDEC_GAS_MAX = 198 # 0xc6
+ NIDEC_BRAKE_MAX = 1024 // 4
+
+ BOSCH_ACCEL_MIN = -3.5 # m/s^2
+ BOSCH_ACCEL_MAX = 2.0 # m/s^2
+
+ BOSCH_GAS_LOOKUP_BP = [-0.2, 2.0] # 2m/s^2
+ BOSCH_GAS_LOOKUP_V = [0, 1600]
+
+ def __init__(self, CP):
+ self.STEER_MAX = CP.lateralParams.torqueBP[-1]
+ # mirror of list (assuming first item is zero) for interp of signed request values
+ assert(CP.lateralParams.torqueBP[0] == 0)
+ assert(CP.lateralParams.torqueBP[0] == 0)
+ self.STEER_LOOKUP_BP = [v * -1 for v in CP.lateralParams.torqueBP][1:][::-1] + list(CP.lateralParams.torqueBP)
+ self.STEER_LOOKUP_V = [v * -1 for v in CP.lateralParams.torqueV][1:][::-1] + list(CP.lateralParams.torqueV)
+
+
+class HondaFlags(IntFlag):
+ # Detected flags
+ # Bosch models with alternate set of LKAS_HUD messages
+ BOSCH_EXT_HUD = 1
+ BOSCH_ALT_BRAKE = 2
+
+ # Static flags
+ BOSCH = 4
+ BOSCH_RADARLESS = 8
+
+ NIDEC = 16
+ NIDEC_ALT_PCM_ACCEL = 32
+ NIDEC_ALT_SCM_MESSAGES = 64
+
+
+# Car button codes
+class CruiseButtons:
+ RES_ACCEL = 4
+ DECEL_SET = 3
+ CANCEL = 2
+ MAIN = 1
+
+
+class CruiseSettings:
+ DISTANCE = 3
+ LKAS = 1
+
+
+# See dbc files for info on values
+VISUAL_HUD = {
+ VisualAlert.none: 0,
+ VisualAlert.fcw: 1,
+ VisualAlert.steerRequired: 1,
+ VisualAlert.ldw: 1,
+ VisualAlert.brakePressed: 10,
+ VisualAlert.wrongGear: 6,
+ VisualAlert.seatbeltUnbuckled: 5,
+ VisualAlert.speedTooHigh: 8
+}
+
+
+@dataclass
+class HondaCarDocs(CarDocs):
+ package: str = "Honda Sensing"
+
+ def init_make(self, CP: structs.CarParams):
+ if CP.flags & HondaFlags.BOSCH:
+ self.car_parts = CarParts.common([CarHarness.bosch_b]) if CP.flags & HondaFlags.BOSCH_RADARLESS else CarParts.common([CarHarness.bosch_a])
+ else:
+ self.car_parts = CarParts.common([CarHarness.nidec])
+
+
+class Footnote(Enum):
+ CIVIC_DIESEL = CarFootnote(
+ "2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.",
+ Column.FSR_STEERING)
+
+
+class HondaBoschPlatformConfig(PlatformConfig):
+ def init(self):
+ self.flags |= HondaFlags.BOSCH
+
+
+class HondaNidecPlatformConfig(PlatformConfig):
+ def init(self):
+ self.flags |= HondaFlags.NIDEC
+
+
+class CAR(Platforms):
+ # Bosch Cars
+ HONDA_ACCORD = HondaBoschPlatformConfig(
+ [
+ HondaCarDocs("Honda Accord 2018-22", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS),
+ HondaCarDocs("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS),
+ HondaCarDocs("Honda Accord Hybrid 2018-22", "All", min_steer_speed=3. * CV.MPH_TO_MS),
+ ],
+ # steerRatio: 11.82 is spec end-to-end
+ CarSpecs(mass=3279 * CV.LB_TO_KG, wheelbase=2.83, steerRatio=16.33, centerToFrontRatio=0.39, tireStiffnessFactor=0.8467),
+ dbc_dict('honda_accord_2018_can_generated', None),
+ )
+ HONDA_CIVIC_BOSCH = HondaBoschPlatformConfig(
+ [
+ HondaCarDocs("Honda Civic 2019-21", "All", video_link="https://www.youtube.com/watch?v=4Iz1Mz5LGF8",
+ footnotes=[Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS),
+ HondaCarDocs("Honda Civic Hatchback 2017-21", min_steer_speed=12. * CV.MPH_TO_MS),
+ ],
+ CarSpecs(mass=1326, wheelbase=2.7, steerRatio=15.38, centerToFrontRatio=0.4), # steerRatio: 10.93 is end-to-end spec
+ dbc_dict('honda_civic_hatchback_ex_2017_can_generated', None),
+ )
+ HONDA_CIVIC_BOSCH_DIESEL = HondaBoschPlatformConfig(
+ [], # don't show in docs
+ HONDA_CIVIC_BOSCH.specs,
+ dbc_dict('honda_accord_2018_can_generated', None),
+ )
+ HONDA_CIVIC_2022 = HondaBoschPlatformConfig(
+ [
+ HondaCarDocs("Honda Civic 2022-24", "All", video_link="https://youtu.be/ytiOT5lcp6Q"),
+ HondaCarDocs("Honda Civic Hatchback 2022-24", "All", video_link="https://youtu.be/ytiOT5lcp6Q"),
+ ],
+ HONDA_CIVIC_BOSCH.specs,
+ dbc_dict('honda_civic_ex_2022_can_generated', None),
+ flags=HondaFlags.BOSCH_RADARLESS,
+ )
+ HONDA_CRV_5G = HondaBoschPlatformConfig(
+ [HondaCarDocs("Honda CR-V 2017-22", min_steer_speed=12. * CV.MPH_TO_MS)],
+ # steerRatio: 12.3 is spec end-to-end
+ CarSpecs(mass=3410 * CV.LB_TO_KG, wheelbase=2.66, steerRatio=16.0, centerToFrontRatio=0.41, tireStiffnessFactor=0.677),
+ dbc_dict('honda_crv_ex_2017_can_generated', None, body_dbc='honda_crv_ex_2017_body_generated'),
+ flags=HondaFlags.BOSCH_ALT_BRAKE,
+ )
+ HONDA_CRV_HYBRID = HondaBoschPlatformConfig(
+ [HondaCarDocs("Honda CR-V Hybrid 2017-21", min_steer_speed=12. * CV.MPH_TO_MS)],
+ # mass: mean of 4 models in kg, steerRatio: 12.3 is spec end-to-end
+ CarSpecs(mass=1667, wheelbase=2.66, steerRatio=16, centerToFrontRatio=0.41, tireStiffnessFactor=0.677),
+ dbc_dict('honda_accord_2018_can_generated', None),
+ )
+ HONDA_HRV_3G = HondaBoschPlatformConfig(
+ [HondaCarDocs("Honda HR-V 2023", "All")],
+ CarSpecs(mass=3125 * CV.LB_TO_KG, wheelbase=2.61, steerRatio=15.2, centerToFrontRatio=0.41, tireStiffnessFactor=0.5),
+ dbc_dict('honda_civic_ex_2022_can_generated', None),
+ flags=HondaFlags.BOSCH_RADARLESS,
+ )
+ ACURA_RDX_3G = HondaBoschPlatformConfig(
+ [HondaCarDocs("Acura RDX 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS)],
+ CarSpecs(mass=4068 * CV.LB_TO_KG, wheelbase=2.75, steerRatio=11.95, centerToFrontRatio=0.41, tireStiffnessFactor=0.677), # as spec
+ dbc_dict('acura_rdx_2020_can_generated', None),
+ flags=HondaFlags.BOSCH_ALT_BRAKE,
+ )
+ HONDA_INSIGHT = HondaBoschPlatformConfig(
+ [HondaCarDocs("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS)],
+ CarSpecs(mass=2987 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=15.0, centerToFrontRatio=0.39, tireStiffnessFactor=0.82), # as spec
+ dbc_dict('honda_insight_ex_2019_can_generated', None),
+ )
+ HONDA_E = HondaBoschPlatformConfig(
+ [HondaCarDocs("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS)],
+ CarSpecs(mass=3338.8 * CV.LB_TO_KG, wheelbase=2.5, centerToFrontRatio=0.5, steerRatio=16.71, tireStiffnessFactor=0.82),
+ dbc_dict('acura_rdx_2020_can_generated', None),
+ )
+
+ # Nidec Cars
+ ACURA_ILX = HondaNidecPlatformConfig(
+ [HondaCarDocs("Acura ILX 2016-19", "AcuraWatch Plus", min_steer_speed=25. * CV.MPH_TO_MS)],
+ CarSpecs(mass=3095 * CV.LB_TO_KG, wheelbase=2.67, steerRatio=18.61, centerToFrontRatio=0.37, tireStiffnessFactor=0.72), # 15.3 is spec end-to-end
+ dbc_dict('acura_ilx_2016_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_CRV = HondaNidecPlatformConfig(
+ [HondaCarDocs("Honda CR-V 2015-16", "Touring Trim", min_steer_speed=12. * CV.MPH_TO_MS)],
+ CarSpecs(mass=3572 * CV.LB_TO_KG, wheelbase=2.62, steerRatio=16.89, centerToFrontRatio=0.41, tireStiffnessFactor=0.444), # as spec
+ dbc_dict('honda_crv_touring_2016_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_CRV_EU = HondaNidecPlatformConfig(
+ [], # Euro version of CRV Touring, don't show in docs
+ HONDA_CRV.specs,
+ dbc_dict('honda_crv_executive_2016_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_FIT = HondaNidecPlatformConfig(
+ [HondaCarDocs("Honda Fit 2018-20", min_steer_speed=12. * CV.MPH_TO_MS)],
+ CarSpecs(mass=2644 * CV.LB_TO_KG, wheelbase=2.53, steerRatio=13.06, centerToFrontRatio=0.39, tireStiffnessFactor=0.75),
+ dbc_dict('honda_fit_ex_2018_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_FREED = HondaNidecPlatformConfig(
+ [HondaCarDocs("Honda Freed 2020", min_steer_speed=12. * CV.MPH_TO_MS)],
+ CarSpecs(mass=3086. * CV.LB_TO_KG, wheelbase=2.74, steerRatio=13.06, centerToFrontRatio=0.39, tireStiffnessFactor=0.75), # mostly copied from FIT
+ dbc_dict('honda_fit_ex_2018_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_HRV = HondaNidecPlatformConfig(
+ [HondaCarDocs("Honda HR-V 2019-22", min_steer_speed=12. * CV.MPH_TO_MS)],
+ HONDA_HRV_3G.specs,
+ dbc_dict('honda_fit_ex_2018_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_ODYSSEY = HondaNidecPlatformConfig(
+ [HondaCarDocs("Honda Odyssey 2018-20")],
+ CarSpecs(mass=1900, wheelbase=3.0, steerRatio=14.35, centerToFrontRatio=0.41, tireStiffnessFactor=0.82),
+ dbc_dict('honda_odyssey_exl_2018_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_PCM_ACCEL,
+ )
+ HONDA_ODYSSEY_CHN = HondaNidecPlatformConfig(
+ [], # Chinese version of Odyssey, don't show in docs
+ HONDA_ODYSSEY.specs,
+ dbc_dict('honda_odyssey_extreme_edition_2018_china_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ ACURA_RDX = HondaNidecPlatformConfig(
+ [HondaCarDocs("Acura RDX 2016-18", "AcuraWatch Plus", min_steer_speed=12. * CV.MPH_TO_MS)],
+ CarSpecs(mass=3925 * CV.LB_TO_KG, wheelbase=2.68, steerRatio=15.0, centerToFrontRatio=0.38, tireStiffnessFactor=0.444), # as spec
+ dbc_dict('acura_rdx_2018_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_PILOT = HondaNidecPlatformConfig(
+ [
+ HondaCarDocs("Honda Pilot 2016-22", min_steer_speed=12. * CV.MPH_TO_MS),
+ HondaCarDocs("Honda Passport 2019-23", "All", min_steer_speed=12. * CV.MPH_TO_MS),
+ ],
+ CarSpecs(mass=4278 * CV.LB_TO_KG, wheelbase=2.86, centerToFrontRatio=0.428, steerRatio=16.0, tireStiffnessFactor=0.444), # as spec
+ dbc_dict('acura_ilx_2016_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_RIDGELINE = HondaNidecPlatformConfig(
+ [HondaCarDocs("Honda Ridgeline 2017-24", min_steer_speed=12. * CV.MPH_TO_MS)],
+ CarSpecs(mass=4515 * CV.LB_TO_KG, wheelbase=3.18, centerToFrontRatio=0.41, steerRatio=15.59, tireStiffnessFactor=0.444), # as spec
+ dbc_dict('acura_ilx_2016_can_generated', 'acura_ilx_2016_nidec'),
+ flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
+ )
+ HONDA_CIVIC = HondaNidecPlatformConfig(
+ [HondaCarDocs("Honda Civic 2016-18", min_steer_speed=12. * CV.MPH_TO_MS, video_link="https://youtu.be/-IkImTe1NYE")],
+ CarSpecs(mass=1326, wheelbase=2.70, centerToFrontRatio=0.4, steerRatio=15.38), # 10.93 is end-to-end spec
+ dbc_dict('honda_civic_touring_2016_can_generated', 'acura_ilx_2016_nidec'),
+ )
+
+
+HONDA_ALT_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(0xF112)
+HONDA_ALT_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+ p16(0xF112)
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[
+ # Currently used to fingerprint
+ Request(
+ [StdQueries.UDS_VERSION_REQUEST],
+ [StdQueries.UDS_VERSION_RESPONSE],
+ bus=1,
+ ),
+
+ # Data collection requests:
+ # Log manufacturer-specific identifier for current ECUs
+ Request(
+ [HONDA_ALT_VERSION_REQUEST],
+ [HONDA_ALT_VERSION_RESPONSE],
+ bus=1,
+ logging=True,
+ ),
+ # Nidec PT bus
+ Request(
+ [StdQueries.UDS_VERSION_REQUEST],
+ [StdQueries.UDS_VERSION_RESPONSE],
+ bus=0,
+ ),
+ # Bosch PT bus
+ Request(
+ [StdQueries.UDS_VERSION_REQUEST],
+ [StdQueries.UDS_VERSION_RESPONSE],
+ bus=1,
+ obd_multiplexing=False,
+ ),
+ ],
+ # We lose these ECUs without the comma power on these cars.
+ # Note that we still attempt to match with them when they are present
+ # This is or'd with (ALL_ECUS - ESSENTIAL_ECUS) from fw_versions.py
+ non_essential_ecus={
+ Ecu.eps: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC_2022, CAR.HONDA_E, CAR.HONDA_HRV_3G],
+ Ecu.vsa: [CAR.ACURA_RDX_3G, CAR.HONDA_ACCORD, CAR.HONDA_CIVIC, CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_2022, CAR.HONDA_CRV_5G, CAR.HONDA_CRV_HYBRID,
+ CAR.HONDA_E, CAR.HONDA_HRV_3G, CAR.HONDA_INSIGHT],
+ },
+ extra_ecus=[
+ (Ecu.combinationMeter, 0x18da60f1, None),
+ (Ecu.programmedFuelInjection, 0x18da10f1, None),
+ # The only other ECU on PT bus accessible by camera on radarless Civic
+ # This is likely a manufacturer-specific sub-address implementation: the camera responds to this and 0x18dab0f1
+ # Unclear what the part number refers to: 8S103 is 'Camera Set Mono', while 36160 is 'Camera Monocular - Honda'
+ # TODO: add query back, camera does not support querying both in parallel and 0x18dab0f1 often fails to respond
+ # (Ecu.unknown, 0x18DAB3F1, None),
+ ],
+)
+
+STEER_THRESHOLD = {
+ # default is 1200, overrides go here
+ CAR.ACURA_RDX: 400,
+ CAR.HONDA_CRV_EU: 400,
+}
+
+HONDA_NIDEC_ALT_PCM_ACCEL = CAR.with_flags(HondaFlags.NIDEC_ALT_PCM_ACCEL)
+HONDA_NIDEC_ALT_SCM_MESSAGES = CAR.with_flags(HondaFlags.NIDEC_ALT_SCM_MESSAGES)
+HONDA_BOSCH = CAR.with_flags(HondaFlags.BOSCH)
+HONDA_BOSCH_RADARLESS = CAR.with_flags(HondaFlags.BOSCH_RADARLESS)
+
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/hyundai/__init__.py b/opendbc/car/hyundai/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/hyundai/carcontroller.py b/opendbc/car/hyundai/carcontroller.py
new file mode 100644
index 0000000000..7533a401ef
--- /dev/null
+++ b/opendbc/car/hyundai/carcontroller.py
@@ -0,0 +1,207 @@
+import copy
+from opendbc.can.packer import CANPacker
+from opendbc.car import DT_CTRL, apply_driver_steer_torque_limits, common_fault_avoidance, make_tester_present_msg, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.common.numpy_fast import clip
+from opendbc.car.hyundai import hyundaicanfd, hyundaican
+from opendbc.car.hyundai.carstate import CarState
+from opendbc.car.hyundai.hyundaicanfd import CanBus
+from opendbc.car.hyundai.values import HyundaiFlags, Buttons, CarControllerParams, CANFD_CAR, CAR
+from opendbc.car.interfaces import CarControllerBase
+
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+LongCtrlState = structs.CarControl.Actuators.LongControlState
+
+# EPS faults if you apply torque while the steering angle is above 90 degrees for more than 1 second
+# All slightly below EPS thresholds to avoid fault
+MAX_ANGLE = 85
+MAX_ANGLE_FRAMES = 89
+MAX_ANGLE_CONSECUTIVE_FRAMES = 2
+
+
+def process_hud_alert(enabled, fingerprint, hud_control):
+ sys_warning = (hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw))
+
+ # initialize to no line visible
+ # TODO: this is not accurate for all cars
+ sys_state = 1
+ if hud_control.leftLaneVisible and hud_control.rightLaneVisible or sys_warning: # HUD alert only display when LKAS status is active
+ sys_state = 3 if enabled or sys_warning else 4
+ elif hud_control.leftLaneVisible:
+ sys_state = 5
+ elif hud_control.rightLaneVisible:
+ sys_state = 6
+
+ # initialize to no warnings
+ left_lane_warning = 0
+ right_lane_warning = 0
+ if hud_control.leftLaneDepart:
+ left_lane_warning = 1 if fingerprint in (CAR.GENESIS_G90, CAR.GENESIS_G80) else 2
+ if hud_control.rightLaneDepart:
+ right_lane_warning = 1 if fingerprint in (CAR.GENESIS_G90, CAR.GENESIS_G80) else 2
+
+ return sys_warning, sys_state, left_lane_warning, right_lane_warning
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.CAN = CanBus(CP)
+ self.params = CarControllerParams(CP)
+ self.packer = CANPacker(dbc_name)
+ self.angle_limit_counter = 0
+
+ self.accel_last = 0
+ self.apply_steer_last = 0
+ self.car_fingerprint = CP.carFingerprint
+ self.last_button_frame = 0
+
+ def update(self, CC, CS, now_nanos):
+ actuators = CC.actuators
+ hud_control = CC.hudControl
+
+ # steering torque
+ new_steer = int(round(actuators.steer * self.params.STEER_MAX))
+ apply_steer = apply_driver_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.params)
+
+ # >90 degree steering fault prevention
+ self.angle_limit_counter, apply_steer_req = common_fault_avoidance(abs(CS.out.steeringAngleDeg) >= MAX_ANGLE, CC.latActive,
+ self.angle_limit_counter, MAX_ANGLE_FRAMES,
+ MAX_ANGLE_CONSECUTIVE_FRAMES)
+
+ if not CC.latActive:
+ apply_steer = 0
+
+ # Hold torque with induced temporary fault when cutting the actuation bit
+ torque_fault = CC.latActive and not apply_steer_req
+
+ self.apply_steer_last = apply_steer
+
+ # accel + longitudinal
+ accel = clip(actuators.accel, CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX)
+ stopping = actuators.longControlState == LongCtrlState.stopping
+ set_speed_in_units = hud_control.setSpeed * (CV.MS_TO_KPH if CS.is_metric else CV.MS_TO_MPH)
+
+ # HUD messages
+ sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.enabled, self.car_fingerprint,
+ hud_control)
+
+ can_sends = []
+
+ # *** common hyundai stuff ***
+
+ # tester present - w/ no response (keeps relevant ECU disabled)
+ if self.frame % 100 == 0 and not (self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value) and self.CP.openpilotLongitudinalControl:
+ # for longitudinal control, either radar or ADAS driving ECU
+ addr, bus = 0x7d0, 0
+ if self.CP.flags & HyundaiFlags.CANFD_HDA2.value:
+ addr, bus = 0x730, self.CAN.ECAN
+ can_sends.append(make_tester_present_msg(addr, bus, suppress_response=True))
+
+ # for blinkers
+ if self.CP.flags & HyundaiFlags.ENABLE_BLINKERS:
+ can_sends.append(make_tester_present_msg(0x7b1, self.CAN.ECAN, suppress_response=True))
+
+ # CAN-FD platforms
+ if self.CP.carFingerprint in CANFD_CAR:
+ hda2 = self.CP.flags & HyundaiFlags.CANFD_HDA2
+ hda2_long = hda2 and self.CP.openpilotLongitudinalControl
+
+ # steering control
+ can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, apply_steer_req, apply_steer))
+
+ # prevent LFA from activating on HDA2 by sending "no lane lines detected" to ADAS ECU
+ if self.frame % 5 == 0 and hda2:
+ can_sends.append(hyundaicanfd.create_suppress_lfa(self.packer, self.CAN, CS.hda2_lfa_block_msg,
+ self.CP.flags & HyundaiFlags.CANFD_HDA2_ALT_STEERING))
+
+ # LFA and HDA icons
+ if self.frame % 5 == 0 and (not hda2 or hda2_long):
+ can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CAN, CC.enabled))
+
+ # blinkers
+ if hda2 and self.CP.flags & HyundaiFlags.ENABLE_BLINKERS:
+ can_sends.extend(hyundaicanfd.create_spas_messages(self.packer, self.CAN, self.frame, CC.leftBlinker, CC.rightBlinker))
+
+ if self.CP.openpilotLongitudinalControl:
+ if hda2:
+ can_sends.extend(hyundaicanfd.create_adrv_messages(self.packer, self.CAN, self.frame))
+ if self.frame % 2 == 0:
+ can_sends.append(hyundaicanfd.create_acc_control(self.packer, self.CAN, CC.enabled, self.accel_last, accel, stopping, CC.cruiseControl.override,
+ set_speed_in_units, hud_control))
+ self.accel_last = accel
+ else:
+ # button presses
+ can_sends.extend(self.create_button_messages(CC, CS, use_clu11=False))
+ else:
+ can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.CP, apply_steer, apply_steer_req,
+ torque_fault, CS.lkas11, sys_warning, sys_state, CC.enabled,
+ hud_control.leftLaneVisible, hud_control.rightLaneVisible,
+ left_lane_warning, right_lane_warning))
+
+ if not self.CP.openpilotLongitudinalControl:
+ can_sends.extend(self.create_button_messages(CC, CS, use_clu11=True))
+
+ if self.frame % 2 == 0 and self.CP.openpilotLongitudinalControl:
+ # TODO: unclear if this is needed
+ jerk = 3.0 if actuators.longControlState == LongCtrlState.pid else 1.0
+ use_fca = self.CP.flags & HyundaiFlags.USE_FCA.value
+ can_sends.extend(hyundaican.create_acc_commands(self.packer, CC.enabled, accel, jerk, int(self.frame / 2),
+ hud_control, set_speed_in_units, stopping,
+ CC.cruiseControl.override, use_fca))
+
+ # 20 Hz LFA MFA message
+ if self.frame % 5 == 0 and self.CP.flags & HyundaiFlags.SEND_LFA.value:
+ can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled))
+
+ # 5 Hz ACC options
+ if self.frame % 20 == 0 and self.CP.openpilotLongitudinalControl:
+ can_sends.extend(hyundaican.create_acc_opt(self.packer))
+
+ # 2 Hz front radar options
+ if self.frame % 50 == 0 and self.CP.openpilotLongitudinalControl:
+ can_sends.append(hyundaican.create_frt_radar_opt(self.packer))
+
+ new_actuators = copy.copy(actuators)
+ new_actuators.steer = apply_steer / self.params.STEER_MAX
+ new_actuators.steerOutputCan = apply_steer
+ new_actuators.accel = accel
+
+ self.frame += 1
+ return new_actuators, can_sends
+
+ def create_button_messages(self, CC: structs.CarControl, CS: CarState, use_clu11: bool):
+ can_sends = []
+ if use_clu11:
+ if CC.cruiseControl.cancel:
+ 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
+ if (self.frame - self.last_button_frame) * DT_CTRL > 0.1:
+ # send 25 messages at a time to increases the likelihood of resume being accepted
+ can_sends.extend([hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.RES_ACCEL, self.CP)] * 25)
+ if (self.frame - self.last_button_frame) * DT_CTRL >= 0.15:
+ self.last_button_frame = self.frame
+ else:
+ if (self.frame - self.last_button_frame) * DT_CTRL > 0.25:
+ # cruise cancel
+ if CC.cruiseControl.cancel:
+ 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:
+ 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
+
+ # cruise standstill resume
+ elif CC.cruiseControl.resume:
+ if self.CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS:
+ # TODO: resume for alt button cars
+ pass
+ else:
+ for _ in range(20):
+ can_sends.append(hyundaicanfd.create_buttons(self.packer, self.CP, self.CAN, CS.buttons_counter+1, Buttons.RES_ACCEL))
+ self.last_button_frame = self.frame
+
+ return can_sends
diff --git a/opendbc/car/hyundai/carstate.py b/opendbc/car/hyundai/carstate.py
new file mode 100644
index 0000000000..a8f4bd9c39
--- /dev/null
+++ b/opendbc/car/hyundai/carstate.py
@@ -0,0 +1,380 @@
+from collections import deque
+import copy
+import math
+
+from opendbc.can.parser import CANParser
+from opendbc.can.can_define import CANDefine
+from opendbc.car import create_button_events, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.hyundai.hyundaicanfd import CanBus
+from opendbc.car.hyundai.values import HyundaiFlags, CAR, DBC, CAN_GEARS, CAMERA_SCC_CAR, \
+ CANFD_CAR, Buttons, CarControllerParams
+from opendbc.car.interfaces import CarStateBase
+
+ButtonType = structs.CarState.ButtonEvent.Type
+
+PREV_BUTTON_SAMPLES = 8
+CLUSTER_SAMPLE_RATE = 20 # frames
+STANDSTILL_THRESHOLD = 12 * 0.03125 * CV.KPH_TO_MS
+
+BUTTONS_DICT = {Buttons.RES_ACCEL: ButtonType.accelCruise, Buttons.SET_DECEL: ButtonType.decelCruise,
+ Buttons.GAP_DIST: ButtonType.gapAdjustCruise, Buttons.CANCEL: ButtonType.cancel}
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+
+ self.cruise_buttons: deque = deque([Buttons.NONE] * PREV_BUTTON_SAMPLES, maxlen=PREV_BUTTON_SAMPLES)
+ self.main_buttons: deque = deque([Buttons.NONE] * PREV_BUTTON_SAMPLES, maxlen=PREV_BUTTON_SAMPLES)
+
+ self.gear_msg_canfd = "GEAR_ALT" if CP.flags & HyundaiFlags.CANFD_ALT_GEARS else \
+ "GEAR_ALT_2" if CP.flags & HyundaiFlags.CANFD_ALT_GEARS_2 else \
+ "GEAR_SHIFTER"
+ if CP.carFingerprint in CANFD_CAR:
+ self.shifter_values = can_define.dv[self.gear_msg_canfd]["GEAR"]
+ elif self.CP.carFingerprint in CAN_GEARS["use_cluster_gears"]:
+ self.shifter_values = can_define.dv["CLU15"]["CF_Clu_Gear"]
+ elif self.CP.carFingerprint in CAN_GEARS["use_tcu_gears"]:
+ self.shifter_values = can_define.dv["TCU12"]["CUR_GR"]
+ else: # preferred and elect gear methods use same definition
+ self.shifter_values = can_define.dv["LVR12"]["CF_Lvr_Gear"]
+
+ self.accelerator_msg_canfd = "ACCELERATOR" if CP.flags & HyundaiFlags.EV else \
+ "ACCELERATOR_ALT" if CP.flags & HyundaiFlags.HYBRID else \
+ "ACCELERATOR_BRAKE_ALT"
+ self.cruise_btns_msg_canfd = "CRUISE_BUTTONS_ALT" if CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS else \
+ "CRUISE_BUTTONS"
+ self.is_metric = False
+ self.buttons_counter = 0
+
+ self.cruise_info = {}
+
+ # On some cars, CLU15->CF_Clu_VehicleSpeed can oscillate faster than the dash updates. Sample at 5 Hz
+ self.cluster_speed = 0
+ self.cluster_speed_counter = CLUSTER_SAMPLE_RATE
+
+ self.params = CarControllerParams(CP)
+
+ def update(self, cp, cp_cam, *_) -> structs.CarState:
+ if self.CP.carFingerprint in CANFD_CAR:
+ return self.update_canfd(cp, cp_cam)
+
+ ret = structs.CarState()
+ cp_cruise = cp_cam if self.CP.carFingerprint in CAMERA_SCC_CAR else cp
+ self.is_metric = cp.vl["CLU11"]["CF_Clu_SPEED_UNIT"] == 0
+ speed_conv = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
+
+ ret.doorOpen = any([cp.vl["CGW1"]["CF_Gway_DrvDrSw"], cp.vl["CGW1"]["CF_Gway_AstDrSw"],
+ cp.vl["CGW2"]["CF_Gway_RLDrSw"], cp.vl["CGW2"]["CF_Gway_RRDrSw"]])
+
+ ret.seatbeltUnlatched = cp.vl["CGW1"]["CF_Gway_DrvSeatBeltSw"] == 0
+
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ cp.vl["WHL_SPD11"]["WHL_SPD_FL"],
+ cp.vl["WHL_SPD11"]["WHL_SPD_FR"],
+ cp.vl["WHL_SPD11"]["WHL_SPD_RL"],
+ cp.vl["WHL_SPD11"]["WHL_SPD_RR"],
+ )
+ ret.vEgoRaw = (ret.wheelSpeeds.fl + ret.wheelSpeeds.fr + ret.wheelSpeeds.rl + ret.wheelSpeeds.rr) / 4.
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.standstill = ret.wheelSpeeds.fl <= STANDSTILL_THRESHOLD and ret.wheelSpeeds.rr <= STANDSTILL_THRESHOLD
+
+ self.cluster_speed_counter += 1
+ if self.cluster_speed_counter > CLUSTER_SAMPLE_RATE:
+ self.cluster_speed = cp.vl["CLU15"]["CF_Clu_VehicleSpeed"]
+ self.cluster_speed_counter = 0
+
+ # Mimic how dash converts to imperial.
+ # Sorento is the only platform where CF_Clu_VehicleSpeed is already imperial when not is_metric
+ # TODO: CGW_USM1->CF_Gway_DrLockSoundRValue may describe this
+ if not self.is_metric and self.CP.carFingerprint not in (CAR.KIA_SORENTO,):
+ self.cluster_speed = math.floor(self.cluster_speed * CV.KPH_TO_MPH + CV.KPH_TO_MPH)
+
+ ret.vEgoCluster = self.cluster_speed * speed_conv
+
+ ret.steeringAngleDeg = cp.vl["SAS11"]["SAS_Angle"]
+ ret.steeringRateDeg = cp.vl["SAS11"]["SAS_Speed"]
+ ret.yawRate = cp.vl["ESP12"]["YAW_RATE"]
+ ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(
+ 50, cp.vl["CGW1"]["CF_Gway_TurnSigLh"], cp.vl["CGW1"]["CF_Gway_TurnSigRh"])
+ ret.steeringTorque = cp.vl["MDPS12"]["CR_Mdps_StrColTq"]
+ ret.steeringTorqueEps = cp.vl["MDPS12"]["CR_Mdps_OutTq"]
+ ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.params.STEER_THRESHOLD, 5)
+ ret.steerFaultTemporary = cp.vl["MDPS12"]["CF_Mdps_ToiUnavail"] != 0 or cp.vl["MDPS12"]["CF_Mdps_ToiFlt"] != 0
+
+ # cruise state
+ if self.CP.openpilotLongitudinalControl:
+ # These are not used for engage/disengage since openpilot keeps track of state using the buttons
+ ret.cruiseState.available = cp.vl["TCS13"]["ACCEnable"] == 0
+ ret.cruiseState.enabled = cp.vl["TCS13"]["ACC_REQ"] == 1
+ ret.cruiseState.standstill = False
+ ret.cruiseState.nonAdaptive = False
+ else:
+ ret.cruiseState.available = cp_cruise.vl["SCC11"]["MainMode_ACC"] == 1
+ ret.cruiseState.enabled = cp_cruise.vl["SCC12"]["ACCMode"] != 0
+ ret.cruiseState.standstill = cp_cruise.vl["SCC11"]["SCCInfoDisplay"] == 4.
+ ret.cruiseState.nonAdaptive = cp_cruise.vl["SCC11"]["SCCInfoDisplay"] == 2. # Shows 'Cruise Control' on dash
+ ret.cruiseState.speed = cp_cruise.vl["SCC11"]["VSetDis"] * speed_conv
+
+ # TODO: Find brake pressure
+ ret.brake = 0
+ ret.brakePressed = cp.vl["TCS13"]["DriverOverride"] == 2 # 2 includes regen braking by user on HEV/EV
+ ret.brakeHoldActive = cp.vl["TCS15"]["AVH_LAMP"] == 2 # 0 OFF, 1 ERROR, 2 ACTIVE, 3 READY
+ ret.parkingBrake = cp.vl["TCS13"]["PBRAKE_ACT"] == 1
+ ret.espDisabled = cp.vl["TCS11"]["TCS_PAS"] == 1
+ ret.espActive = cp.vl["TCS11"]["ABS_ACT"] == 1
+ ret.accFaulted = cp.vl["TCS13"]["ACCEnable"] != 0 # 0 ACC CONTROL ENABLED, 1-3 ACC CONTROL DISABLED
+
+ if self.CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV):
+ if self.CP.flags & HyundaiFlags.HYBRID:
+ ret.gas = cp.vl["E_EMS11"]["CR_Vcu_AccPedDep_Pos"] / 254.
+ else:
+ ret.gas = cp.vl["E_EMS11"]["Accel_Pedal_Pos"] / 254.
+ ret.gasPressed = ret.gas > 0
+ else:
+ ret.gas = cp.vl["EMS12"]["PV_AV_CAN"] / 100.
+ ret.gasPressed = bool(cp.vl["EMS16"]["CF_Ems_AclAct"])
+
+ # Gear Selection via Cluster - For those Kia/Hyundai which are not fully discovered, we can use the Cluster Indicator for Gear Selection,
+ # as this seems to be standard over all cars, but is not the preferred method.
+ if self.CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV):
+ gear = cp.vl["ELECT_GEAR"]["Elect_Gear_Shifter"]
+ elif self.CP.carFingerprint in CAN_GEARS["use_cluster_gears"]:
+ gear = cp.vl["CLU15"]["CF_Clu_Gear"]
+ elif self.CP.carFingerprint in CAN_GEARS["use_tcu_gears"]:
+ gear = cp.vl["TCU12"]["CUR_GR"]
+ else:
+ gear = cp.vl["LVR12"]["CF_Lvr_Gear"]
+
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(gear))
+
+ if not self.CP.openpilotLongitudinalControl:
+ aeb_src = "FCA11" if self.CP.flags & HyundaiFlags.USE_FCA.value else "SCC12"
+ aeb_sig = "FCA_CmdAct" if self.CP.flags & HyundaiFlags.USE_FCA.value else "AEB_CmdAct"
+ aeb_warning = cp_cruise.vl[aeb_src]["CF_VSM_Warn"] != 0
+ scc_warning = cp_cruise.vl["SCC12"]["TakeOverReq"] == 1 # sometimes only SCC system shows an FCW
+ aeb_braking = cp_cruise.vl[aeb_src]["CF_VSM_DecCmdAct"] != 0 or cp_cruise.vl[aeb_src][aeb_sig] != 0
+ ret.stockFcw = (aeb_warning or scc_warning) and not aeb_braking
+ ret.stockAeb = aeb_warning and aeb_braking
+
+ if self.CP.enableBsm:
+ ret.leftBlindspot = cp.vl["LCA11"]["CF_Lca_IndLeft"] != 0
+ ret.rightBlindspot = cp.vl["LCA11"]["CF_Lca_IndRight"] != 0
+
+ # save the entire LKAS11 and CLU11
+ self.lkas11 = copy.copy(cp_cam.vl["LKAS11"])
+ self.clu11 = copy.copy(cp.vl["CLU11"])
+ self.steer_state = cp.vl["MDPS12"]["CF_Mdps_ToiActive"] # 0 NOT ACTIVE, 1 ACTIVE
+ prev_cruise_buttons = self.cruise_buttons[-1]
+ self.cruise_buttons.extend(cp.vl_all["CLU11"]["CF_Clu_CruiseSwState"])
+ self.main_buttons.extend(cp.vl_all["CLU11"]["CF_Clu_CruiseSwMain"])
+
+ if self.CP.openpilotLongitudinalControl:
+ ret.buttonEvents = create_button_events(self.cruise_buttons[-1], prev_cruise_buttons, BUTTONS_DICT)
+
+ return ret
+
+ def update_canfd(self, cp, cp_cam) -> structs.CarState:
+ ret = structs.CarState()
+
+ self.is_metric = cp.vl["CRUISE_BUTTONS_ALT"]["DISTANCE_UNIT"] != 1
+ speed_factor = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
+
+ if self.CP.flags & (HyundaiFlags.EV | HyundaiFlags.HYBRID):
+ offset = 255. if self.CP.flags & HyundaiFlags.EV else 1023.
+ ret.gas = cp.vl[self.accelerator_msg_canfd]["ACCELERATOR_PEDAL"] / offset
+ ret.gasPressed = ret.gas > 1e-5
+ else:
+ ret.gasPressed = bool(cp.vl[self.accelerator_msg_canfd]["ACCELERATOR_PEDAL_PRESSED"])
+
+ ret.brakePressed = cp.vl["TCS"]["DriverBraking"] == 1
+
+ ret.doorOpen = cp.vl["DOORS_SEATBELTS"]["DRIVER_DOOR"] == 1
+ ret.seatbeltUnlatched = cp.vl["DOORS_SEATBELTS"]["DRIVER_SEATBELT"] == 0
+
+ gear = cp.vl[self.gear_msg_canfd]["GEAR"]
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(gear))
+
+ # TODO: figure out positions
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_1"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_2"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_3"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_4"],
+ )
+ ret.vEgoRaw = (ret.wheelSpeeds.fl + ret.wheelSpeeds.fr + ret.wheelSpeeds.rl + ret.wheelSpeeds.rr) / 4.
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.standstill = ret.wheelSpeeds.fl <= STANDSTILL_THRESHOLD and ret.wheelSpeeds.rr <= STANDSTILL_THRESHOLD
+
+ ret.steeringRateDeg = cp.vl["STEERING_SENSORS"]["STEERING_RATE"]
+ ret.steeringAngleDeg = cp.vl["STEERING_SENSORS"]["STEERING_ANGLE"] * -1
+ ret.steeringTorque = cp.vl["MDPS"]["STEERING_COL_TORQUE"]
+ ret.steeringTorqueEps = cp.vl["MDPS"]["STEERING_OUT_TORQUE"]
+ ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > self.params.STEER_THRESHOLD, 5)
+ ret.steerFaultTemporary = cp.vl["MDPS"]["LKA_FAULT"] != 0
+
+ # TODO: alt signal usage may be described by cp.vl['BLINKERS']['USE_ALT_LAMP']
+ left_blinker_sig, right_blinker_sig = "LEFT_LAMP", "RIGHT_LAMP"
+ if self.CP.carFingerprint == CAR.HYUNDAI_KONA_EV_2ND_GEN:
+ left_blinker_sig, right_blinker_sig = "LEFT_LAMP_ALT", "RIGHT_LAMP_ALT"
+ ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(50, cp.vl["BLINKERS"][left_blinker_sig],
+ cp.vl["BLINKERS"][right_blinker_sig])
+ if self.CP.enableBsm:
+ ret.leftBlindspot = cp.vl["BLINDSPOTS_REAR_CORNERS"]["FL_INDICATOR"] != 0
+ ret.rightBlindspot = cp.vl["BLINDSPOTS_REAR_CORNERS"]["FR_INDICATOR"] != 0
+
+ # cruise state
+ # CAN FD cars enable on main button press, set available if no TCS faults preventing engagement
+ ret.cruiseState.available = cp.vl["TCS"]["ACCEnable"] == 0
+ if self.CP.openpilotLongitudinalControl:
+ # These are not used for engage/disengage since openpilot keeps track of state using the buttons
+ ret.cruiseState.enabled = cp.vl["TCS"]["ACC_REQ"] == 1
+ ret.cruiseState.standstill = False
+ else:
+ cp_cruise_info = cp_cam if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else cp
+ ret.cruiseState.enabled = cp_cruise_info.vl["SCC_CONTROL"]["ACCMode"] in (1, 2)
+ ret.cruiseState.standstill = cp_cruise_info.vl["SCC_CONTROL"]["CRUISE_STANDSTILL"] == 1
+ ret.cruiseState.speed = cp_cruise_info.vl["SCC_CONTROL"]["VSetDis"] * speed_factor
+ self.cruise_info = copy.copy(cp_cruise_info.vl["SCC_CONTROL"])
+
+ # Manual Speed Limit Assist is a feature that replaces non-adaptive cruise control on EV CAN FD platforms.
+ # It limits the vehicle speed, overridable by pressing the accelerator past a certain point.
+ # The car will brake, but does not respect positive acceleration commands in this mode
+ # TODO: find this message on ICE & HYBRID cars + cruise control signals (if exists)
+ if self.CP.flags & HyundaiFlags.EV:
+ ret.cruiseState.nonAdaptive = cp.vl["MANUAL_SPEED_LIMIT_ASSIST"]["MSLA_ENABLED"] == 1
+
+ prev_cruise_buttons = self.cruise_buttons[-1]
+ self.cruise_buttons.extend(cp.vl_all[self.cruise_btns_msg_canfd]["CRUISE_BUTTONS"])
+ self.main_buttons.extend(cp.vl_all[self.cruise_btns_msg_canfd]["ADAPTIVE_CRUISE_MAIN_BTN"])
+ self.buttons_counter = cp.vl[self.cruise_btns_msg_canfd]["COUNTER"]
+ ret.accFaulted = cp.vl["TCS"]["ACCEnable"] != 0 # 0 ACC CONTROL ENABLED, 1-3 ACC CONTROL DISABLED
+
+ if self.CP.flags & HyundaiFlags.CANFD_HDA2:
+ self.hda2_lfa_block_msg = copy.copy(cp_cam.vl["CAM_0x362"] if self.CP.flags & HyundaiFlags.CANFD_HDA2_ALT_STEERING
+ else cp_cam.vl["CAM_0x2a4"])
+
+ if self.CP.openpilotLongitudinalControl:
+ ret.buttonEvents = create_button_events(self.cruise_buttons[-1], prev_cruise_buttons, BUTTONS_DICT)
+
+ return ret
+
+ def get_can_parser(self, CP):
+ if CP.carFingerprint in CANFD_CAR:
+ return self.get_can_parser_canfd(CP)
+
+ messages = [
+ # address, frequency
+ ("MDPS12", 50),
+ ("TCS11", 100),
+ ("TCS13", 50),
+ ("TCS15", 10),
+ ("CLU11", 50),
+ ("CLU15", 5),
+ ("ESP12", 100),
+ ("CGW1", 10),
+ ("CGW2", 5),
+ ("CGW4", 5),
+ ("WHL_SPD11", 50),
+ ("SAS11", 100),
+ ]
+
+ if not CP.openpilotLongitudinalControl and CP.carFingerprint not in CAMERA_SCC_CAR:
+ messages += [
+ ("SCC11", 50),
+ ("SCC12", 50),
+ ]
+ if CP.flags & HyundaiFlags.USE_FCA.value:
+ messages.append(("FCA11", 50))
+
+ if CP.enableBsm:
+ messages.append(("LCA11", 50))
+
+ if CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV):
+ messages.append(("E_EMS11", 50))
+ else:
+ messages += [
+ ("EMS12", 100),
+ ("EMS16", 100),
+ ]
+
+ if CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV):
+ messages.append(("ELECT_GEAR", 20))
+ elif CP.carFingerprint in CAN_GEARS["use_cluster_gears"]:
+ pass
+ elif CP.carFingerprint in CAN_GEARS["use_tcu_gears"]:
+ messages.append(("TCU12", 100))
+ else:
+ messages.append(("LVR12", 100))
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ if CP.carFingerprint in CANFD_CAR:
+ return CarState.get_cam_can_parser_canfd(CP)
+
+ messages = [
+ ("LKAS11", 100)
+ ]
+
+ if not CP.openpilotLongitudinalControl and CP.carFingerprint in CAMERA_SCC_CAR:
+ messages += [
+ ("SCC11", 50),
+ ("SCC12", 50),
+ ]
+
+ if CP.flags & HyundaiFlags.USE_FCA.value:
+ messages.append(("FCA11", 50))
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 2)
+
+ def get_can_parser_canfd(self, CP):
+ messages = [
+ (self.gear_msg_canfd, 100),
+ (self.accelerator_msg_canfd, 100),
+ ("WHEEL_SPEEDS", 100),
+ ("STEERING_SENSORS", 100),
+ ("MDPS", 100),
+ ("TCS", 50),
+ ("CRUISE_BUTTONS_ALT", 50),
+ ("BLINKERS", 4),
+ ("DOORS_SEATBELTS", 4),
+ ]
+
+ if CP.flags & HyundaiFlags.EV:
+ messages += [
+ ("MANUAL_SPEED_LIMIT_ASSIST", 10),
+ ]
+
+ if not (CP.flags & HyundaiFlags.CANFD_ALT_BUTTONS):
+ messages += [
+ ("CRUISE_BUTTONS", 50)
+ ]
+
+ if CP.enableBsm:
+ messages += [
+ ("BLINDSPOTS_REAR_CORNERS", 20),
+ ]
+
+ if not (CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value) and not CP.openpilotLongitudinalControl:
+ messages += [
+ ("SCC_CONTROL", 50),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus(CP).ECAN)
+
+ @staticmethod
+ def get_cam_can_parser_canfd(CP):
+ messages = []
+ if CP.flags & HyundaiFlags.CANFD_HDA2:
+ block_lfa_msg = "CAM_0x362" if CP.flags & HyundaiFlags.CANFD_HDA2_ALT_STEERING else "CAM_0x2a4"
+ messages += [(block_lfa_msg, 20)]
+ elif CP.flags & HyundaiFlags.CANFD_CAMERA_SCC:
+ messages += [
+ ("SCC_CONTROL", 50),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus(CP).CAM)
diff --git a/opendbc/car/hyundai/fingerprints.py b/opendbc/car/hyundai/fingerprints.py
new file mode 100644
index 0000000000..d243ec566d
--- /dev/null
+++ b/opendbc/car/hyundai/fingerprints.py
@@ -0,0 +1,1142 @@
+from opendbc.car.structs import CarParams
+from opendbc.car.hyundai.values import CAR
+
+Ecu = CarParams.Ecu
+
+# The existence of SCC or RDR in the fwdRadar FW usually determines the radar's function,
+# i.e. if it sends the SCC messages or if another ECU like the camera or ADAS Driving ECU does
+
+
+FW_VERSIONS = {
+ CAR.HYUNDAI_AZERA_6TH_GEN: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00IG__ SCC F-CU- 1.00 1.00 99110-G8100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00IG MDPS C 1.00 1.02 56310G8510\x00 4IGSC103',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00IG MFC AT MES LHD 1.00 1.04 99211-G8100 200511',
+ ],
+ },
+ CAR.HYUNDAI_AZERA_HEV_6TH_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00IGH MFC AT KOR LHD 1.00 1.00 99211-G8000 180903',
+ b'\xf1\x00IGH MFC AT KOR LHD 1.00 1.01 99211-G8000 181109',
+ b'\xf1\x00IGH MFC AT KOR LHD 1.00 1.02 99211-G8100 191029',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00IG MDPS C 1.00 1.00 56310M9600\x00 4IHSC100',
+ b'\xf1\x00IG MDPS C 1.00 1.01 56310M9350\x00 4IH8C101',
+ b'\xf1\x00IG MDPS C 1.00 1.02 56310M9350\x00 4IH8C102',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00IGhe SCC FHCUP 1.00 1.00 99110-M9100 ',
+ b'\xf1\x00IGhe SCC FHCUP 1.00 1.01 99110-M9000 ',
+ b'\xf1\x00IGhe SCC FHCUP 1.00 1.02 99110-M9000 ',
+ ],
+ },
+ CAR.HYUNDAI_GENESIS: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DH LKAS 1.1 -150210',
+ b'\xf1\x00DH LKAS 1.4 -140110',
+ b'\xf1\x00DH LKAS 1.5 -140425',
+ ],
+ },
+ CAR.HYUNDAI_IONIQ: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 ',
+ b'\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00AE MDPS C 1.00 1.05 56310/G2501 4AEHC105',
+ b'\xf1\x00AE MDPS C 1.00 1.07 56310/G2301 4AEHC107',
+ b'\xf1\x00AE MDPS C 1.00 1.07 56310/G2501 4AEHC107',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00AEH MFC AT EUR LHD 1.00 1.00 95740-G2400 180222',
+ b'\xf1\x00AEH MFC AT USA LHD 1.00 1.00 95740-G2400 180222',
+ ],
+ },
+ CAR.HYUNDAI_IONIQ_PHEV_2019: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 ',
+ b'\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00AE MDPS C 1.00 1.07 56310/G2501 4AEHC107',
+ b'\xf1\x00AE MDPS C 1.00 1.07 56310/G2551 4AEHC107',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00AEP MFC AT AUS RHD 1.00 1.00 95740-G2400 180222',
+ b'\xf1\x00AEP MFC AT USA LHD 1.00 1.00 95740-G2400 180222',
+ ],
+ },
+ CAR.HYUNDAI_IONIQ_PHEV: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00AEhe SCC F-CUP 1.00 1.00 99110-G2200 ',
+ b'\xf1\x00AEhe SCC F-CUP 1.00 1.00 99110-G2600 ',
+ b'\xf1\x00AEhe SCC F-CUP 1.00 1.02 99110-G2100 ',
+ b'\xf1\x00AEhe SCC FHCUP 1.00 1.00 99110-G2600 ',
+ b'\xf1\x00AEhe SCC FHCUP 1.00 1.02 99110-G2100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2210 4APHC101',
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2310 4APHC101',
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2510 4APHC101',
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G2560 4APHC101',
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310G2510\x00 4APHC101',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00AEP MFC AT EUR LHD 1.00 1.01 95740-G2600 190819',
+ b'\xf1\x00AEP MFC AT EUR RHD 1.00 1.01 95740-G2600 190819',
+ b'\xf1\x00AEP MFC AT USA LHD 1.00 1.00 95740-G2700 201027',
+ b'\xf1\x00AEP MFC AT USA LHD 1.00 1.01 95740-G2600 190819',
+ ],
+ },
+ CAR.HYUNDAI_IONIQ_EV_2020: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00AEev SCC F-CUP 1.00 1.00 99110-G7200 ',
+ b'\xf1\x00AEev SCC F-CUP 1.00 1.00 99110-G7500 ',
+ b'\xf1\x00AEev SCC F-CUP 1.00 1.01 99110-G7000 ',
+ b'\xf1\x00AEev SCC F-CUP 1.00 1.01 99110-G7100 ',
+ b'\xf1\x00AEev SCC FHCUP 1.00 1.01 99110-G7100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G7310 4APEC101',
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G7510 4APEC101',
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310/G7560 4APEC101',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00AEE MFC AT EUR LHD 1.00 1.00 95740-G2600 190730',
+ b'\xf1\x00AEE MFC AT EUR LHD 1.00 1.00 95740-G2700 201027',
+ b'\xf1\x00AEE MFC AT EUR LHD 1.00 1.01 95740-G2600 190819',
+ b'\xf1\x00AEE MFC AT EUR LHD 1.00 1.03 95740-G2500 190516',
+ b'\xf1\x00AEE MFC AT EUR RHD 1.00 1.01 95740-G2600 190819',
+ b'\xf1\x00AEE MFC AT USA LHD 1.00 1.01 95740-G2600 190819',
+ ],
+ },
+ CAR.HYUNDAI_IONIQ_EV_LTD: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00AEev SCC F-CUP 1.00 1.00 96400-G7000 ',
+ b'\xf1\x00AEev SCC F-CUP 1.00 1.00 96400-G7100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00AE MDPS C 1.00 1.02 56310G7300\x00 4AEEC102',
+ b'\xf1\x00AE MDPS C 1.00 1.03 56310/G7300 4AEEC103',
+ b'\xf1\x00AE MDPS C 1.00 1.03 56310G7300\x00 4AEEC103',
+ b'\xf1\x00AE MDPS C 1.00 1.04 56310/G7301 4AEEC104',
+ b'\xf1\x00AE MDPS C 1.00 1.04 56310/G7501 4AEEC104',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00AEE MFC AT EUR LHD 1.00 1.00 95740-G2300 170703',
+ b'\xf1\x00AEE MFC AT EUR LHD 1.00 1.00 95740-G2400 180222',
+ b'\xf1\x00AEE MFC AT EUR LHD 1.00 1.00 95740-G7200 160418',
+ b'\xf1\x00AEE MFC AT USA LHD 1.00 1.00 95740-G2400 180222',
+ ],
+ },
+ CAR.HYUNDAI_IONIQ_HEV_2022: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00AEhe SCC F-CUP 1.00 1.00 99110-G2600 ',
+ b'\xf1\x00AEhe SCC FHCUP 1.00 1.00 99110-G2600 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00AE MDPS C 1.00 1.01 56310G2510\x00 4APHC101',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00AEH MFC AT USA LHD 1.00 1.00 95740-G2700 201027',
+ ],
+ },
+ CAR.HYUNDAI_SONATA: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DN8_ SCC F-CU- 1.00 1.00 99110-L0000 ',
+ b'\xf1\x00DN8_ SCC F-CUP 1.00 1.00 99110-L0000 ',
+ b'\xf1\x00DN8_ SCC F-CUP 1.00 1.02 99110-L1000 ',
+ b'\xf1\x00DN8_ SCC FHCUP 1.00 1.00 99110-L0000 ',
+ b'\xf1\x00DN8_ SCC FHCUP 1.00 1.01 99110-L1000 ',
+ b'\xf1\x00DN8_ SCC FHCUP 1.00 1.02 99110-L1000 ',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00DN ESC \x01 102\x19\x04\x13 58910-L1300',
+ b'\xf1\x00DN ESC \x03 100 \x08\x01 58910-L0300',
+ b'\xf1\x00DN ESC \x06 104\x19\x08\x01 58910-L0100',
+ b'\xf1\x00DN ESC \x06 106 \x07\x01 58910-L0100',
+ b'\xf1\x00DN ESC \x06 107 \x07\x03 58910-L1300',
+ b'\xf1\x00DN ESC \x06 107"\x08\x07 58910-L0100',
+ b'\xf1\x00DN ESC \x07 104\x19\x08\x01 58910-L0100',
+ b'\xf1\x00DN ESC \x07 106 \x07\x01 58910-L0100',
+ b'\xf1\x00DN ESC \x07 107"\x08\x07 58910-L0100',
+ b'\xf1\x00DN ESC \x08 103\x19\x06\x01 58910-L1300',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00DN8 MDPS C 1,00 1,01 56310L0010\x00 4DNAC101',
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0010 4DNAC101',
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0200 4DNAC102',
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0210 4DNAC101',
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0210 4DNAC102',
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0010\x00 4DNAC101',
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0200\x00 4DNAC102',
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0210\x00 4DNAC101',
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0210\x00 4DNAC102',
+ b'\xf1\x00DN8 MDPS C 1.00 1.03 56310-L1010 4DNDC103',
+ b'\xf1\x00DN8 MDPS C 1.00 1.03 56310-L1030 4DNDC103',
+ b'\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP100',
+ b'\xf1\x00DN8 MDPS R 1.00 1.00 57700-L0000 4DNAP101',
+ b'\xf1\x00DN8 MDPS R 1.00 1.02 57700-L1000 4DNDP105',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DN8 MFC AT KOR LHD 1.00 1.02 99211-L1000 190422',
+ b'\xf1\x00DN8 MFC AT KOR LHD 1.00 1.04 99211-L1000 191016',
+ b'\xf1\x00DN8 MFC AT RUS LHD 1.00 1.03 99211-L1000 190705',
+ b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.00 99211-L0000 190716',
+ b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.01 99211-L0000 191016',
+ b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.03 99211-L0000 210603',
+ b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.05 99211-L1000 201109',
+ b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.06 99211-L1000 210325',
+ b'\xf1\x00DN8 MFC AT USA LHD 1.00 1.07 99211-L1000 211223',
+ ],
+ },
+ CAR.HYUNDAI_SONATA_LF: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00LF__ SCC F-CUP 1.00 1.00 96401-C2200 ',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00LF ESC \t 11 \x17\x01\x13 58920-C2610',
+ b'\xf1\x00LF ESC \x0c 11 \x17\x01\x13 58920-C2610',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00LFF LKAS AT USA LHD 1.00 1.01 95740-C1000 E51',
+ b'\xf1\x00LFF LKAS AT USA LHD 1.01 1.02 95740-C1000 E52',
+ ],
+ },
+ CAR.HYUNDAI_TUCSON: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00TL__ FCA F-CUP 1.00 1.01 99110-D3500 ',
+ b'\xf1\x00TL__ FCA F-CUP 1.00 1.02 99110-D3510 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00TL MFC AT KOR LHD 1.00 1.02 95895-D3800 180719',
+ b'\xf1\x00TL MFC AT USA LHD 1.00 1.06 95895-D3800 190107',
+ ],
+ },
+ CAR.HYUNDAI_SANTA_FE: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00TM__ SCC F-CUP 1.00 1.00 99110-S1210 ',
+ b'\xf1\x00TM__ SCC F-CUP 1.00 1.01 99110-S2000 ',
+ b'\xf1\x00TM__ SCC F-CUP 1.00 1.02 99110-S2000 ',
+ b'\xf1\x00TM__ SCC F-CUP 1.00 1.03 99110-S2000 ',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00TM ESC \x02 100\x18\x030 58910-S2600',
+ b'\xf1\x00TM ESC \x02 102\x18\x07\x01 58910-S2600',
+ b'\xf1\x00TM ESC \x02 103\x18\x11\x07 58910-S2600',
+ b'\xf1\x00TM ESC \x02 104\x19\x07\x07 58910-S2600',
+ b'\xf1\x00TM ESC \x03 103\x18\x11\x07 58910-S2600',
+ b'\xf1\x00TM ESC \x0c 103\x18\x11\x08 58910-S2650',
+ b'\xf1\x00TM ESC \r 100\x18\x031 58910-S2650',
+ b'\xf1\x00TM ESC \r 103\x18\x11\x08 58910-S2650',
+ b'\xf1\x00TM ESC \r 104\x19\x07\x08 58910-S2650',
+ b'\xf1\x00TM ESC \r 105\x19\x05# 58910-S1500',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00TM MDPS C 1.00 1.00 56340-S2000 8409',
+ b'\xf1\x00TM MDPS C 1.00 1.00 56340-S2000 8A12',
+ b'\xf1\x00TM MDPS C 1.00 1.01 56340-S2000 9129',
+ b'\xf1\x00TM MDPS R 1.00 1.02 57700-S1100 4TMDP102',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00TM MFC AT EUR LHD 1.00 1.01 99211-S1010 181207',
+ b'\xf1\x00TM MFC AT USA LHD 1.00 1.00 99211-S2000 180409',
+ ],
+ },
+ CAR.HYUNDAI_SANTA_FE_2022: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00TM__ SCC F-CUP 1.00 1.00 99110-S1500 ',
+ b'\xf1\x00TM__ SCC F-CUP 1.00 1.01 99110-S1500 ',
+ b'\xf1\x00TM__ SCC FHCUP 1.00 1.00 99110-S1500 ',
+ b'\xf1\x00TM__ SCC FHCUP 1.00 1.01 99110-S1500 ',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00TM ESC \x01 102!\x04\x03 58910-S2DA0',
+ b'\xf1\x00TM ESC \x01 104"\x10\x07 58910-S2DA0',
+ b'\xf1\x00TM ESC \x02 101 \x08\x04 58910-S2GA0',
+ b'\xf1\x00TM ESC \x02 103"\x07\x08 58910-S2GA0',
+ b'\xf1\x00TM ESC \x03 101 \x08\x02 58910-S2DA0',
+ b'\xf1\x00TM ESC \x03 102!\x04\x03 58910-S2DA0',
+ b'\xf1\x00TM ESC \x04 101 \x08\x04 58910-S2GA0',
+ b'\xf1\x00TM ESC \x04 102!\x04\x05 58910-S2GA0',
+ b'\xf1\x00TM ESC \x04 103"\x07\x08 58910-S2GA0',
+ b'\xf1\x00TM ESC \x1e 102 \x08\x08 58910-S1DA0',
+ b'\xf1\x00TM ESC 103!\x030 58910-S1MA0',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00TM MDPS C 1.00 1.01 56310-S1AB0 4TSDC101',
+ b'\xf1\x00TM MDPS C 1.00 1.01 56310-S1EB0 4TSDC101',
+ b'\xf1\x00TM MDPS C 1.00 1.02 56370-S2AA0 0B19',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00TM MFC AT EUR LHD 1.00 1.03 99211-S1500 210224',
+ b'\xf1\x00TM MFC AT MES LHD 1.00 1.05 99211-S1500 220126',
+ b'\xf1\x00TMA MFC AT MEX LHD 1.00 1.01 99211-S2500 210205',
+ b'\xf1\x00TMA MFC AT USA LHD 1.00 1.00 99211-S2500 200720',
+ b'\xf1\x00TMA MFC AT USA LHD 1.00 1.01 99211-S2500 210205',
+ b'\xf1\x00TMA MFC AT USA LHD 1.00 1.03 99211-S2500 220414',
+ ],
+ },
+ CAR.HYUNDAI_SANTA_FE_HEV_2022: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00TMhe SCC FHCUP 1.00 1.00 99110-CL500 ',
+ b'\xf1\x00TMhe SCC FHCUP 1.00 1.01 99110-CL500 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00TM MDPS C 1.00 1.02 56310-CLAC0 4TSHC102',
+ b'\xf1\x00TM MDPS C 1.00 1.02 56310-CLEC0 4TSHC102',
+ b'\xf1\x00TM MDPS C 1.00 1.02 56310-GA000 4TSHA100',
+ b'\xf1\x00TM MDPS C 1.00 1.02 56310GA000\x00 4TSHA100',
+ b'\xf1\x00TM MDPS R 1.00 1.05 57700-CL000 4TSHP105',
+ b'\xf1\x00TM MDPS R 1.00 1.06 57700-CL000 4TSHP106',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00TMA MFC AT USA LHD 1.00 1.03 99211-S2500 220414',
+ b'\xf1\x00TMH MFC AT EUR LHD 1.00 1.06 99211-S1500 220727',
+ b'\xf1\x00TMH MFC AT KOR LHD 1.00 1.06 99211-S1500 220727',
+ b'\xf1\x00TMH MFC AT USA LHD 1.00 1.03 99211-S1500 210224',
+ b'\xf1\x00TMH MFC AT USA LHD 1.00 1.05 99211-S1500 220126',
+ b'\xf1\x00TMH MFC AT USA LHD 1.00 1.06 99211-S1500 220727',
+ ],
+ },
+ CAR.HYUNDAI_SANTA_FE_PHEV_2022: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00TMhe SCC F-CUP 1.00 1.00 99110-CL500 ',
+ b'\xf1\x00TMhe SCC FHCUP 1.00 1.00 99110-CL500 ',
+ b'\xf1\x00TMhe SCC FHCUP 1.00 1.01 99110-CL500 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00TM MDPS C 1.00 1.02 56310-CLAC0 4TSHC102',
+ b'\xf1\x00TM MDPS C 1.00 1.02 56310-CLEC0 4TSHC102',
+ b'\xf1\x00TM MDPS C 1.00 1.02 56310CLEC0\x00 4TSHC102',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00TMP MFC AT USA LHD 1.00 1.03 99211-S1500 210224',
+ b'\xf1\x00TMP MFC AT USA LHD 1.00 1.05 99211-S1500 220126',
+ b'\xf1\x00TMP MFC AT USA LHD 1.00 1.06 99211-S1500 220727',
+ ],
+ },
+ CAR.HYUNDAI_CUSTIN_1ST_GEN: {
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00KU ESC \x01 101!\x02\x03 58910-O3200',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00KU__ SCC F-CUP 1.00 1.01 99110-O3000 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00KU MDPS C 1.00 1.01 56310/O3100 4KUCC101',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00KU2 MFC AT CHN LHD 1.00 1.02 99211-O3000 220923',
+ ],
+ },
+ CAR.KIA_STINGER: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00CK__ SCC F_CUP 1.00 1.01 96400-J5000 ',
+ b'\xf1\x00CK__ SCC F_CUP 1.00 1.01 96400-J5100 ',
+ b'\xf1\x00CK__ SCC F_CUP 1.00 1.02 96400-J5100 ',
+ b'\xf1\x00CK__ SCC F_CUP 1.00 1.03 96400-J5100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00CK MDPS R 1.00 1.04 57700-J5200 4C2CL104',
+ b'\xf1\x00CK MDPS R 1.00 1.04 57700-J5220 4C2VL104',
+ b'\xf1\x00CK MDPS R 1.00 1.04 57700-J5420 4C4VL104',
+ b'\xf1\x00CK MDPS R 1.00 1.06 57700-J5220 4C2VL106',
+ b'\xf1\x00CK MDPS R 1.00 1.06 57700-J5420 4C4VL106',
+ b'\xf1\x00CK MDPS R 1.00 1.07 57700-J5220 4C2VL107',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00CK MFC AT EUR LHD 1.00 1.03 95740-J5000 170822',
+ b'\xf1\x00CK MFC AT USA LHD 1.00 1.03 95740-J5000 170822',
+ b'\xf1\x00CK MFC AT USA LHD 1.00 1.04 95740-J5000 180504',
+ ],
+ },
+ CAR.KIA_STINGER_2022: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00CK__ SCC F-CUP 1.00 1.00 99110-J5500 ',
+ b'\xf1\x00CK__ SCC FHCUP 1.00 1.00 99110-J5500 ',
+ b'\xf1\x00CK__ SCC FHCUP 1.00 1.00 99110-J5600 ',
+ b'\xf1\x00CK__ SCC FHCUP 1.00 1.01 99110-J5100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00CK MDPS R 1.00 5.03 57700-J5300 4C2CL503',
+ b'\xf1\x00CK MDPS R 1.00 5.03 57700-J5320 4C2VL503',
+ b'\xf1\x00CK MDPS R 1.00 5.03 57700-J5380 4C2VR503',
+ b'\xf1\x00CK MDPS R 1.00 5.03 57700-J5520 4C4VL503',
+ b'\xf1\x00CK MDPS R 1.00 5.04 57700-J5520 4C4VL504',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00CK MFC AT AUS RHD 1.00 1.00 99211-J5500 210622',
+ b'\xf1\x00CK MFC AT KOR LHD 1.00 1.00 99211-J5500 210622',
+ b'\xf1\x00CK MFC AT USA LHD 1.00 1.00 99211-J5500 210622',
+ b'\xf1\x00CK MFC AT USA LHD 1.00 1.03 99211-J5000 201209',
+ ],
+ },
+ CAR.HYUNDAI_PALISADE: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00LX2 SCC FHCUP 1.00 1.04 99110-S8100 ',
+ b'\xf1\x00LX2_ SCC F-CUP 1.00 1.04 99110-S8100 ',
+ b'\xf1\x00LX2_ SCC F-CUP 1.00 1.05 99110-S8100 ',
+ b'\xf1\x00LX2_ SCC FHCU- 1.00 1.05 99110-S8100 ',
+ b'\xf1\x00LX2_ SCC FHCUP 1.00 1.00 99110-S8110 ',
+ b'\xf1\x00LX2_ SCC FHCUP 1.00 1.03 99110-S8100 ',
+ b'\xf1\x00LX2_ SCC FHCUP 1.00 1.04 99110-S8100 ',
+ b'\xf1\x00LX2_ SCC FHCUP 1.00 1.05 99110-S8100 ',
+ b'\xf1\x00ON__ FCA FHCUP 1.00 1.00 99110-S9110 ',
+ b'\xf1\x00ON__ FCA FHCUP 1.00 1.01 99110-S9110 ',
+ b'\xf1\x00ON__ FCA FHCUP 1.00 1.02 99110-S9100 ',
+ b'\xf1\x00ON__ FCA FHCUP 1.00 1.03 99110-S9100 ',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00LX ESC \x01 103\x19\t\x10 58910-S8360',
+ b'\xf1\x00LX ESC \x01 1031\t\x10 58910-S8360',
+ b'\xf1\x00LX ESC \x01 104 \x10\x15 58910-S8350',
+ b'\xf1\x00LX ESC \x01 104 \x10\x16 58910-S8360',
+ b'\xf1\x00LX ESC \x0b 101\x19\x03\x17 58910-S8330',
+ b'\xf1\x00LX ESC \x0b 101\x19\x03 58910-S8360',
+ b'\xf1\x00LX ESC \x0b 102\x19\x05\x07 58910-S8330',
+ b'\xf1\x00LX ESC \x0b 103\x19\t\x07 58910-S8330',
+ b'\xf1\x00LX ESC \x0b 103\x19\t\t 58910-S8350',
+ b'\xf1\x00LX ESC \x0b 103\x19\t\x10 58910-S8360',
+ b'\xf1\x00LX ESC \x0b 104 \x10\x16 58910-S8360',
+ b'\xf1\x00ON ESC \x01 101\x19\t\x08 58910-S9360',
+ b'\xf1\x00ON ESC \x0b 100\x18\x12\x18 58910-S9360',
+ b'\xf1\x00ON ESC \x0b 101\x19\t\x05 58910-S9320',
+ b'\xf1\x00ON ESC \x0b 101\x19\t\x08 58910-S9360',
+ b'\xf1\x00ON ESC \x0b 103$\x04\x08 58910-S9360',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00LX2 MDPS C 1,00 1,03 56310-S8020 4LXDC103',
+ b'\xf1\x00LX2 MDPS C 1.00 1.03 56310-S8000 4LXDC103',
+ b'\xf1\x00LX2 MDPS C 1.00 1.03 56310-S8020 4LXDC103',
+ b'\xf1\x00LX2 MDPS C 1.00 1.03 56310-XX000 4LXDC103',
+ b'\xf1\x00LX2 MDPS C 1.00 1.04 56310-S8020 4LXDC104',
+ b'\xf1\x00LX2 MDPS C 1.00 1.04 56310-S8420 4LXDC104',
+ b'\xf1\x00LX2 MDPS R 1.00 1.02 56370-S8300 9318',
+ b'\xf1\x00ON MDPS C 1.00 1.00 56340-S9000 8B13',
+ b'\xf1\x00ON MDPS C 1.00 1.01 56340-S9000 9201',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00LX2 MFC AT KOR LHD 1.00 1.08 99211-S8100 200903',
+ b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.00 99211-S8110 210226',
+ b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.03 99211-S8100 190125',
+ b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.05 99211-S8100 190909',
+ b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.07 99211-S8100 200422',
+ b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.08 99211-S8100 200903',
+ b'\xf1\x00ON MFC AT USA LHD 1.00 1.01 99211-S9100 181105',
+ b'\xf1\x00ON MFC AT USA LHD 1.00 1.03 99211-S9100 200720',
+ b'\xf1\x00ON MFC AT USA LHD 1.00 1.04 99211-S9100 211227',
+ ],
+ },
+ CAR.HYUNDAI_VELOSTER: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00JS__ SCC H-CUP 1.00 1.02 95650-J3200 ',
+ b'\xf1\x00JS__ SCC HNCUP 1.00 1.02 95650-J3100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00JSL MDPS C 1.00 1.03 56340-J3000 8308',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00JS LKAS AT KOR LHD 1.00 1.03 95740-J3000 K33',
+ b'\xf1\x00JS LKAS AT USA LHD 1.00 1.02 95740-J3000 K32',
+ ],
+ },
+ CAR.GENESIS_G70: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00IK__ SCC F-CUP 1.00 1.01 96400-G9100 ',
+ b'\xf1\x00IK__ SCC F-CUP 1.00 1.02 96400-G9100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00IK MDPS R 1.00 1.06 57700-G9420 4I4VL106',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00IK MFC AT USA LHD 1.00 1.01 95740-G9000 170920',
+ ],
+ },
+ CAR.GENESIS_G70_2020: {
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00IK MDPS R 1.00 1.06 57700-G9220 4I2VL106',
+ b'\xf1\x00IK MDPS R 1.00 1.07 57700-G9220 4I2VL107',
+ b'\xf1\x00IK MDPS R 1.00 1.07 57700-G9420 4I4VL107',
+ b'\xf1\x00IK MDPS R 1.00 1.08 57700-G9200 4I2CL108',
+ b'\xf1\x00IK MDPS R 1.00 1.08 57700-G9420 4I4VL108',
+ b'\xf1\x00IK MDPS R 1.00 5.09 57700-G9520 4I4VL509',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00IK__ SCC F-CUP 1.00 1.01 96400-G9100 ',
+ b'\xf1\x00IK__ SCC F-CUP 1.00 1.02 96400-G9100 ',
+ b'\xf1\x00IK__ SCC F-CUP 1.00 1.02 96400-G9100 \xf1\xa01.02',
+ b'\xf1\x00IK__ SCC FHCUP 1.00 1.00 99110-G9300 ',
+ b'\xf1\x00IK__ SCC FHCUP 1.00 1.02 96400-G9000 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00IK MFC AT KOR LHD 1.00 1.01 95740-G9000 170920',
+ b'\xf1\x00IK MFC AT USA LHD 1.00 1.01 95740-G9000 170920',
+ b'\xf1\x00IK MFC AT USA LHD 1.00 1.04 99211-G9000 220401',
+ ],
+ },
+ CAR.GENESIS_G80: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DH__ SCC F-CU- 1.00 1.01 96400-B1110 ',
+ b'\xf1\x00DH__ SCC F-CUP 1.00 1.01 96400-B1120 ',
+ b'\xf1\x00DH__ SCC F-CUP 1.00 1.02 96400-B1120 ',
+ b'\xf1\x00DH__ SCC FHCUP 1.00 1.01 96400-B1110 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DH LKAS AT EUR LHD 1.01 1.01 95895-B1500 161014',
+ b'\xf1\x00DH LKAS AT KOR LHD 1.01 1.01 95895-B1500 161014',
+ b'\xf1\x00DH LKAS AT KOR LHD 1.01 1.02 95895-B1500 170810',
+ b'\xf1\x00DH LKAS AT USA LHD 1.01 1.01 95895-B1500 161014',
+ b'\xf1\x00DH LKAS AT USA LHD 1.01 1.02 95895-B1500 170810',
+ b'\xf1\x00DH LKAS AT USA LHD 1.01 1.03 95895-B1500 180713',
+ b'\xf1\x00DH LKAS AT USA LHD 1.01 1.04 95895-B1500 181213',
+ ],
+ },
+ CAR.GENESIS_G90: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00HI__ SCC F-CUP 1.00 1.01 96400-D2100 ',
+ b'\xf1\x00HI__ SCC FHCUP 1.00 1.02 99110-D2100 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00HI LKAS AT USA LHD 1.00 1.00 95895-D2020 160302',
+ b'\xf1\x00HI LKAS AT USA LHD 1.00 1.00 95895-D2030 170208',
+ b'\xf1\x00HI MFC AT USA LHD 1.00 1.03 99211-D2000 190831',
+ ],
+ },
+ CAR.HYUNDAI_KONA: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00OS__ SCC F-CUP 1.00 1.00 95655-J9200 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00OS MDPS C 1.00 1.05 56310J9030\x00 4OSDC105',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00OS9 LKAS AT USA LHD 1.00 1.00 95740-J9300 g21',
+ ],
+ },
+ CAR.KIA_CEED: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00CD__ SCC F-CUP 1.00 1.02 99110-J7000 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00CD MDPS C 1.00 1.06 56310-XX000 4CDEC106',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00CD LKAS AT EUR LHD 1.00 1.01 99211-J7000 B40',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00CD ESC \x03 102\x18\x08\x05 58920-J7350',
+ ],
+ },
+ CAR.KIA_FORTE: {
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00BD MDPS C 1.00 1.02 56310-XX000 4BD2C102',
+ b'\xf1\x00BD MDPS C 1.00 1.08 56310/M6300 4BDDC108',
+ b'\xf1\x00BD MDPS C 1.00 1.08 56310M6300\x00 4BDDC108',
+ b'\xf1\x00BDm MDPS C A.01 1.01 56310M7800\x00 4BPMC101',
+ b'\xf1\x00BDm MDPS C A.01 1.03 56310M7800\x00 4BPMC103',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00BD LKAS AT USA LHD 1.00 1.04 95740-M6000 J33',
+ b'\xf1\x00BDP LKAS AT USA LHD 1.00 1.05 99211-M6500 744',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00BDPE_SCC FHCUPC 1.00 1.04 99110-M6500\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\xf1\x00BD__ SCC H-CUP 1.00 1.02 99110-M6000 ',
+ ],
+ },
+ CAR.KIA_K5_2021: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DL3_ SCC F-CUP 1.00 1.03 99110-L2100 ',
+ b'\xf1\x00DL3_ SCC FHCUP 1.00 1.03 99110-L2000 ',
+ b'\xf1\x00DL3_ SCC FHCUP 1.00 1.03 99110-L2100 ',
+ b'\xf1\x00DL3_ SCC FHCUP 1.00 1.04 99110-L2100 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00DL3 MDPS C 1.00 1.01 56310-L3110 4DLAC101',
+ b'\xf1\x00DL3 MDPS C 1.00 1.01 56310-L3220 4DLAC101',
+ b'\xf1\x00DL3 MDPS C 1.00 1.02 56310-L2220 4DLDC102',
+ b'\xf1\x00DL3 MDPS C 1.00 1.02 56310L3220\x00 4DLAC102',
+ b'\xf1\x00DL3 MDPS R 1.00 1.02 57700-L3000 4DLAP102',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DL3 MFC AT KOR LHD 1.00 1.04 99210-L2000 210527',
+ b'\xf1\x00DL3 MFC AT USA LHD 1.00 1.03 99210-L3000 200915',
+ b'\xf1\x00DL3 MFC AT USA LHD 1.00 1.04 99210-L3000 210208',
+ b'\xf1\x00DL3 MFC AT USA LHD 1.00 1.05 99210-L3000 211222',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00DL ESC \x01 104 \x07\x12 58910-L2200',
+ b'\xf1\x00DL ESC \x03 100 \x08\x02 58910-L3600',
+ b'\xf1\x00DL ESC \x06 101 \x04\x02 58910-L3200',
+ b'\xf1\x00DL ESC \x06 103"\x08\x06 58910-L3200',
+ b'\xf1\x00DL ESC \t 100 \x06\x02 58910-L3800',
+ b'\xf1\x00DL ESC \t 101 \x07\x02 58910-L3800',
+ b'\xf1\x00DL ESC \t 102"\x08\x10 58910-L3800',
+ ],
+ },
+ CAR.KIA_K5_HEV_2020: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DLhe SCC FHCUP 1.00 1.02 99110-L7000 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00DL3 MDPS C 1.00 1.02 56310-L7000 4DLHC102',
+ b'\xf1\x00DL3 MDPS C 1.00 1.02 56310-L7220 4DLHC102',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DL3HMFC AT KOR LHD 1.00 1.01 99210-L2000 191022',
+ b'\xf1\x00DL3HMFC AT KOR LHD 1.00 1.02 99210-L2000 200309',
+ b'\xf1\x00DL3HMFC AT KOR LHD 1.00 1.04 99210-L2000 210527',
+ ],
+ },
+ CAR.HYUNDAI_KONA_EV: {
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00OS IEB \x01 212 \x11\x13 58520-K4000',
+ b'\xf1\x00OS IEB \x02 210 \x02\x14 58520-K4000',
+ b'\xf1\x00OS IEB \x02 212 \x11\x13 58520-K4000',
+ b'\xf1\x00OS IEB \x03 210 \x02\x14 58520-K4000',
+ b'\xf1\x00OS IEB \x03 211 \x04\x02 58520-K4000',
+ b'\xf1\x00OS IEB \x03 212 \x11\x13 58520-K4000',
+ b'\xf1\x00OS IEB \r 105\x18\t\x18 58520-K4000',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00OE2 LKAS AT EUR LHD 1.00 1.00 95740-K4200 200',
+ b'\xf1\x00OSE LKAS AT EUR LHD 1.00 1.00 95740-K4100 W40',
+ b'\xf1\x00OSE LKAS AT EUR RHD 1.00 1.00 95740-K4100 W40',
+ b'\xf1\x00OSE LKAS AT KOR LHD 1.00 1.00 95740-K4100 W40',
+ b'\xf1\x00OSE LKAS AT USA LHD 1.00 1.00 95740-K4100 W40',
+ b'\xf1\x00OSE LKAS AT USA LHD 1.00 1.00 95740-K4300 W50',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00OS MDPS C 1.00 1.03 56310/K4550 4OEDC103',
+ b'\xf1\x00OS MDPS C 1.00 1.04 56310-XX000 4OEDC104',
+ b'\xf1\x00OS MDPS C 1.00 1.04 56310K4000\x00 4OEDC104',
+ b'\xf1\x00OS MDPS C 1.00 1.04 56310K4050\x00 4OEDC104',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00OSev SCC F-CUP 1.00 1.00 99110-K4000 ',
+ b'\xf1\x00OSev SCC F-CUP 1.00 1.00 99110-K4100 ',
+ b'\xf1\x00OSev SCC F-CUP 1.00 1.01 99110-K4000 ',
+ b'\xf1\x00OSev SCC FNCUP 1.00 1.01 99110-K4000 ',
+ ],
+ },
+ CAR.HYUNDAI_KONA_EV_2022: {
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00OS IEB \x02 102"\x05\x16 58520-K4010',
+ b'\xf1\x00OS IEB \x03 101 \x11\x13 58520-K4010',
+ b'\xf1\x00OS IEB \x03 102"\x05\x16 58520-K4010',
+ b'\xf1\x00OS IEB \r 102"\x05\x16 58520-K4010',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00OSP LKA AT AUS RHD 1.00 1.04 99211-J9200 904',
+ b'\xf1\x00OSP LKA AT CND LHD 1.00 1.02 99211-J9110 802',
+ b'\xf1\x00OSP LKA AT EUR LHD 1.00 1.04 99211-J9200 904',
+ b'\xf1\x00OSP LKA AT EUR RHD 1.00 1.02 99211-J9110 802',
+ b'\xf1\x00OSP LKA AT EUR RHD 1.00 1.04 99211-J9200 904',
+ b'\xf1\x00OSP LKA AT USA LHD 1.00 1.04 99211-J9200 904',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00OSP MDPS C 1.00 1.02 56310-K4271 4OEPC102',
+ b'\xf1\x00OSP MDPS C 1.00 1.02 56310/K4271 4OEPC102',
+ b'\xf1\x00OSP MDPS C 1.00 1.02 56310/K4970 4OEPC102',
+ b'\xf1\x00OSP MDPS C 1.00 1.02 56310K4260\x00 4OEPC102',
+ b'\xf1\x00OSP MDPS C 1.00 1.02 56310K4261\x00 4OEPC102',
+ b'\xf1\x00OSP MDPS C 1.00 1.02 56310K4971\x00 4OEPC102',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00YB__ FCA ----- 1.00 1.01 99110-K4500 \x00\x00\x00',
+ ],
+ },
+ CAR.HYUNDAI_KONA_EV_2ND_GEN: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00SXev RDR ----- 1.00 1.00 99110-BF000 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00SX2EMFC AT KOR LHD 1.00 1.00 99211-BF000 230410',
+ ],
+ },
+ CAR.KIA_NIRO_EV: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DEev SCC F-CUP 1.00 1.00 99110-Q4000 ',
+ b'\xf1\x00DEev SCC F-CUP 1.00 1.00 99110-Q4100 ',
+ b'\xf1\x00DEev SCC F-CUP 1.00 1.00 99110-Q4500 ',
+ b'\xf1\x00DEev SCC F-CUP 1.00 1.00 99110-Q4600 ',
+ b'\xf1\x00DEev SCC F-CUP 1.00 1.02 96400-Q4000 ',
+ b'\xf1\x00DEev SCC F-CUP 1.00 1.02 96400-Q4100 ',
+ b'\xf1\x00DEev SCC F-CUP 1.00 1.03 96400-Q4100 ',
+ b'\xf1\x00DEev SCC FHCUP 1.00 1.00 99110-Q4600 ',
+ b'\xf1\x00DEev SCC FHCUP 1.00 1.03 96400-Q4000 ',
+ b'\xf1\x00DEev SCC FNCUP 1.00 1.00 99110-Q4600 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00DE MDPS C 1.00 1.04 56310Q4100\x00 4DEEC104',
+ b'\xf1\x00DE MDPS C 1.00 1.05 56310Q4000\x00 4DEEC105',
+ b'\xf1\x00DE MDPS C 1.00 1.05 56310Q4100\x00 4DEEC105',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DEE MFC AT EUR LHD 1.00 1.00 99211-Q4000 191211',
+ b'\xf1\x00DEE MFC AT EUR LHD 1.00 1.00 99211-Q4100 200706',
+ b'\xf1\x00DEE MFC AT EUR LHD 1.00 1.03 95740-Q4000 180821',
+ b'\xf1\x00DEE MFC AT KOR LHD 1.00 1.02 95740-Q4000 180705',
+ b'\xf1\x00DEE MFC AT KOR LHD 1.00 1.03 95740-Q4000 180821',
+ b'\xf1\x00DEE MFC AT USA LHD 1.00 1.00 99211-Q4000 191211',
+ b'\xf1\x00DEE MFC AT USA LHD 1.00 1.01 99211-Q4500 210428',
+ b'\xf1\x00DEE MFC AT USA LHD 1.00 1.03 95740-Q4000 180821',
+ ],
+ },
+ CAR.KIA_NIRO_EV_2ND_GEN: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00SG2_ RDR ----- 1.00 1.01 99110-AT000 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00SG2EMFC AT EUR LHD 1.01 1.09 99211-AT000 220801',
+ b'\xf1\x00SG2EMFC AT USA LHD 1.01 1.09 99211-AT000 220801',
+ ],
+ },
+ CAR.KIA_NIRO_PHEV: {
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00DE MDPS C 1.00 1.01 56310G5520\x00 4DEPC101',
+ b'\xf1\x00DE MDPS C 1.00 1.09 56310G5301\x00 4DEHC109',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DEH MFC AT USA LHD 1.00 1.00 95740-G5010 170117',
+ b'\xf1\x00DEP MFC AT USA LHD 1.00 1.00 95740-G5010 170117',
+ b'\xf1\x00DEP MFC AT USA LHD 1.00 1.01 95740-G5010 170424',
+ b'\xf1\x00DEP MFC AT USA LHD 1.00 1.05 99211-G5000 190826',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DEhe SCC F-CUP 1.00 1.02 99110-G5100 ',
+ b'\xf1\x00DEhe SCC FHCUP 1.00 1.02 99110-G5100 ',
+ b'\xf1\x00DEhe SCC H-CUP 1.01 1.02 96400-G5100 ',
+ ],
+ },
+ CAR.KIA_NIRO_PHEV_2022: {
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00DE MDPS C 1.00 1.01 56310G5520\x00 4DEPC101',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DEP MFC AT USA LHD 1.00 1.00 99211-G5500 210428',
+ b'\xf1\x00DEP MFC AT USA LHD 1.00 1.06 99211-G5000 201028',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DEhe SCC F-CUP 1.00 1.00 99110-G5600 ',
+ b'\xf1\x00DEhe SCC FHCUP 1.00 1.00 99110-G5600 ',
+ ],
+ },
+ CAR.KIA_NIRO_HEV_2021: {
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00DE MDPS C 1.00 1.01 56310G5520\x00 4DEPC101',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DEH MFC AT KOR LHD 1.00 1.04 99211-G5000 190516',
+ b'\xf1\x00DEH MFC AT USA LHD 1.00 1.00 99211-G5500 210428',
+ b'\xf1\x00DEH MFC AT USA LHD 1.00 1.07 99211-G5000 201221',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DEhe SCC FHCUP 1.00 1.00 99110-G5600 ',
+ b'\xf1\x00DEhe SCC FHCUP 1.00 1.01 99110-G5000 ',
+ ],
+ },
+ CAR.KIA_SELTOS: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00SP2_ SCC FHCUP 1.01 1.05 99110-Q5100 ',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00SP ESC \x07 101\x19\t\x05 58910-Q5450',
+ b'\xf1\x00SP ESC \t 101\x19\t\x05 58910-Q5450',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00SP2 MDPS C 1.00 1.04 56300Q5200 ',
+ b'\xf1\x00SP2 MDPS C 1.01 1.05 56300Q5200 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00SP2 MFC AT USA LHD 1.00 1.04 99210-Q5000 191114',
+ b'\xf1\x00SP2 MFC AT USA LHD 1.00 1.05 99210-Q5000 201012',
+ ],
+ },
+ CAR.KIA_OPTIMA_G4: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00JF__ SCC F-CUP 1.00 1.00 96400-D4100 ',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00JF ESC \t 17 \x16\x06# 58920-D4180',
+ b'\xf1\x00JF ESC \x0f 16 \x16\x06\x17 58920-D5080',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00JFWGN LDWS AT USA LHD 1.00 1.02 95895-D4100 G21',
+ b'\xf1\x00JFWGN LKAS AT EUR LHD 1.00 1.01 95895-D4100 G20',
+ ],
+ },
+ CAR.KIA_OPTIMA_G4_FL: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00JF__ SCC F-CUP 1.00 1.00 96400-D4110 ',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b"\xf1\x00JF ESC \t 11 \x18\x03' 58920-D5260",
+ b'\xf1\x00JF ESC \x0b 11 \x18\x030 58920-D5180',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00JFA LKAS AT USA LHD 1.00 1.00 95895-D5001 h32',
+ b'\xf1\x00JFA LKAS AT USA LHD 1.00 1.00 95895-D5100 h32',
+ ],
+ },
+ CAR.KIA_OPTIMA_H: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00JFhe SCC FNCUP 1.00 1.00 96400-A8000 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00JFP LKAS AT EUR LHD 1.00 1.03 95895-A8100 160711',
+ ],
+ },
+ CAR.KIA_OPTIMA_H_G4_FL: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00JFhe SCC FHCUP 1.00 1.01 99110-A8500 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00JFH MFC AT KOR LHD 1.00 1.01 95895-A8200 180323',
+ ],
+ },
+ CAR.HYUNDAI_ELANTRA: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00AD LKAS AT USA LHD 1.01 1.01 95895-F2000 251',
+ b'\xf1\x00ADP LKAS AT USA LHD 1.00 1.03 99211-F2000 X31',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00AD ESC \x11 11 \x18\x05\x06 58910-F2840',
+ b'\xf1\x00AD ESC \x11 12 \x15\t\t 58920-F2810',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00AD__ SCC H-CUP 1.00 1.00 99110-F2100 ',
+ b'\xf1\x00AD__ SCC H-CUP 1.00 1.01 96400-F2100 ',
+ ],
+ },
+ CAR.HYUNDAI_ELANTRA_GT_I30: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00PD LKAS AT KOR LHD 1.00 1.02 95740-G3000 A51',
+ b'\xf1\x00PD LKAS AT USA LHD 1.00 1.02 95740-G3000 A51',
+ b'\xf1\x00PD LKAS AT USA LHD 1.01 1.01 95740-G3100 A54',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00PD MDPS C 1.00 1.00 56310G3300\x00 4PDDC100',
+ b'\xf1\x00PD MDPS C 1.00 1.03 56310/G3300 4PDDC103',
+ b'\xf1\x00PD MDPS C 1.00 1.04 56310/G3300 4PDDC104',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00PD ESC \t 104\x18\t\x03 58920-G3350',
+ b'\xf1\x00PD ESC \x0b 103\x17\x110 58920-G3350',
+ b'\xf1\x00PD ESC \x0b 104\x18\t\x03 58920-G3350',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00PD__ SCC F-CUP 1.00 1.00 96400-G3300 ',
+ b'\xf1\x00PD__ SCC F-CUP 1.01 1.00 96400-G3100 ',
+ b'\xf1\x00PD__ SCC FNCUP 1.01 1.00 96400-G3000 ',
+ ],
+ },
+ CAR.HYUNDAI_ELANTRA_2021: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00CN7_ SCC F-CUP 1.00 1.01 99110-AA000 ',
+ b'\xf1\x00CN7_ SCC FHCUP 1.00 1.01 99110-AA000 ',
+ b'\xf1\x00CN7_ SCC FNCUP 1.00 1.01 99110-AA000 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00CN7 MDPS C 1.00 1.06 56310/AA050 4CNDC106',
+ b'\xf1\x00CN7 MDPS C 1.00 1.06 56310/AA070 4CNDC106',
+ b'\xf1\x00CN7 MDPS C 1.00 1.06 56310AA050\x00 4CNDC106',
+ b'\xf1\x00CN7 MDPS C 1.00 1.07 56310AA050\x00 4CNDC107',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.00 99210-AB000 200819',
+ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.01 99210-AB000 210205',
+ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.02 99210-AB000 220111',
+ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.03 99210-AA000 200819',
+ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.03 99210-AB000 220426',
+ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.06 99210-AA000 220111',
+ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.07 99210-AA000 220426',
+ b'\xf1\x00CN7 MFC AT USA LHD 1.00 1.08 99210-AA000 220728',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00CN ESC \t 101 \x10\x03 58910-AB800',
+ b'\xf1\x00CN ESC \t 104 \x08\x03 58910-AA800',
+ b'\xf1\x00CN ESC \t 105 \x10\x03 58910-AA800',
+ ],
+ },
+ CAR.HYUNDAI_ELANTRA_HEV_2021: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.03 99210-AA000 200819',
+ b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.05 99210-AA000 210930',
+ b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.07 99210-AA000 220426',
+ b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.08 99210-AA000 220728',
+ b'\xf1\x00CN7HMFC AT USA LHD 1.00 1.09 99210-AA000 221108',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00CNhe SCC FHCUP 1.00 1.01 99110-BY000 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00CN7 MDPS C 1.00 1.02 56310/BY050 4CNHC102',
+ b'\xf1\x00CN7 MDPS C 1.00 1.03 56310/BY050 4CNHC103',
+ b'\xf1\x00CN7 MDPS C 1.00 1.03 56310BY050\x00 4CNHC103',
+ b'\xf1\x00CN7 MDPS C 1.00 1.03 56310BY0500 4CNHC103',
+ b'\xf1\x00CN7 MDPS C 1.00 1.04 56310BY050\x00 4CNHC104',
+ ],
+ },
+ CAR.HYUNDAI_KONA_HEV: {
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00OS IEB \x01 104 \x11 58520-CM000',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00OShe SCC FNCUP 1.00 1.01 99110-CM000 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00OS MDPS C 1.00 1.00 56310CM030\x00 4OHDC100',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00OSH LKAS AT KOR LHD 1.00 1.01 95740-CM000 l31',
+ ],
+ },
+ CAR.HYUNDAI_SONATA_HYBRID: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00DNhe SCC F-CUP 1.00 1.02 99110-L5000 ',
+ b'\xf1\x00DNhe SCC FHCUP 1.00 1.00 99110-L5000 ',
+ b'\xf1\x00DNhe SCC FHCUP 1.00 1.02 99110-L5000 ',
+ ],
+ (Ecu.eps, 0x7d4, None): [
+ b'\xf1\x00DN8 MDPS C 1.00 1.01 56310-L5000 4DNHC101',
+ b'\xf1\x00DN8 MDPS C 1.00 1.02 56310-L5450 4DNHC102',
+ b'\xf1\x00DN8 MDPS C 1.00 1.02 56310-L5500 4DNHC102',
+ b'\xf1\x00DN8 MDPS C 1.00 1.03 56310-L5450 4DNHC103',
+ b'\xf1\x00DN8 MDPS C 1.00 1.03 56310L5450\x00 4DNHC104',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00DN8HMFC AT KOR LHD 1.00 1.03 99211-L1000 190705',
+ b'\xf1\x00DN8HMFC AT USA LHD 1.00 1.04 99211-L1000 191016',
+ b'\xf1\x00DN8HMFC AT USA LHD 1.00 1.05 99211-L1000 201109',
+ b'\xf1\x00DN8HMFC AT USA LHD 1.00 1.06 99211-L1000 210325',
+ b'\xf1\x00DN8HMFC AT USA LHD 1.00 1.07 99211-L1000 211223',
+ ],
+ },
+ CAR.KIA_SORENTO: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00UMP LKAS AT KOR LHD 1.00 1.00 95740-C5550 S30',
+ b'\xf1\x00UMP LKAS AT USA LHD 1.00 1.00 95740-C6550 d00',
+ b'\xf1\x00UMP LKAS AT USA LHD 1.01 1.01 95740-C6550 d01',
+ ],
+ (Ecu.abs, 0x7d1, None): [
+ b'\xf1\x00UM ESC \x02 12 \x18\x05\x05 58910-C6300',
+ b'\xf1\x00UM ESC \x0c 12 \x18\x05\x06 58910-C6330',
+ b'\xf1\x00UM ESC \x13 12 \x17\x07\x05 58910-C5320',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00UM__ SCC F-CUP 1.00 1.00 96400-C5500 ',
+ b'\xf1\x00UM__ SCC F-CUP 1.00 1.00 96400-C6500 ',
+ ],
+ },
+ CAR.KIA_EV6: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00CV1 MFC AT EUR LHD 1.00 1.05 99210-CV000 211027',
+ b'\xf1\x00CV1 MFC AT EUR LHD 1.00 1.06 99210-CV000 220328',
+ b'\xf1\x00CV1 MFC AT EUR RHD 1.00 1.00 99210-CV100 220630',
+ b'\xf1\x00CV1 MFC AT KOR LHD 1.00 1.00 99210-CV100 220630',
+ b'\xf1\x00CV1 MFC AT KOR LHD 1.00 1.04 99210-CV000 210823',
+ b'\xf1\x00CV1 MFC AT KOR LHD 1.00 1.05 99210-CV000 211027',
+ b'\xf1\x00CV1 MFC AT KOR LHD 1.00 1.06 99210-CV000 220328',
+ b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.00 99210-CV100 220630',
+ b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.00 99210-CV200 230510',
+ b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.05 99210-CV000 211027',
+ b'\xf1\x00CV1 MFC AT USA LHD 1.00 1.06 99210-CV000 220328',
+ ],
+ },
+ CAR.HYUNDAI_IONIQ_5: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00NE1_ RDR ----- 1.00 1.00 99110-GI000 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00NE1 MFC AT CAN LHD 1.00 1.01 99211-GI010 211007',
+ b'\xf1\x00NE1 MFC AT CAN LHD 1.00 1.05 99211-GI010 220614',
+ b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.00 99211-GI100 230915',
+ b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.01 99211-GI010 211007',
+ b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.01 99211-GI100 240110',
+ b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.06 99211-GI000 210813',
+ b'\xf1\x00NE1 MFC AT EUR LHD 1.00 1.06 99211-GI010 230110',
+ b'\xf1\x00NE1 MFC AT EUR RHD 1.00 1.01 99211-GI010 211007',
+ b'\xf1\x00NE1 MFC AT EUR RHD 1.00 1.02 99211-GI010 211206',
+ b'\xf1\x00NE1 MFC AT KOR LHD 1.00 1.00 99211-GI020 230719',
+ b'\xf1\x00NE1 MFC AT KOR LHD 1.00 1.05 99211-GI010 220614',
+ b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.00 99211-GI020 230719',
+ b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.00 99211-GI100 230915',
+ b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.01 99211-GI010 211007',
+ b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.02 99211-GI010 211206',
+ b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.03 99211-GI010 220401',
+ b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.05 99211-GI010 220614',
+ b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.06 99211-GI010 230110',
+ ],
+ },
+ CAR.HYUNDAI_IONIQ_6: {
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00CE__ RDR ----- 1.00 1.01 99110-KL000 ',
+ ],
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00CE MFC AT CAN LHD 1.00 1.04 99211-KL000 221213',
+ b'\xf1\x00CE MFC AT EUR LHD 1.00 1.03 99211-KL000 221011',
+ b'\xf1\x00CE MFC AT EUR LHD 1.00 1.04 99211-KL000 221213',
+ b'\xf1\x00CE MFC AT USA LHD 1.00 1.04 99211-KL000 221213',
+ ],
+ },
+ CAR.HYUNDAI_TUCSON_4TH_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00NX4 FR_CMR AT CAN LHD 1.00 1.00 99211-N9260 14Y',
+ b'\xf1\x00NX4 FR_CMR AT CAN LHD 1.00 1.01 99211-N9100 14A',
+ b'\xf1\x00NX4 FR_CMR AT EUR LHD 1.00 1.00 99211-N9220 14K',
+ b'\xf1\x00NX4 FR_CMR AT EUR LHD 1.00 2.02 99211-N9000 14E',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9210 14G',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9220 14K',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9240 14Q',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9250 14W',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-N9260 14Y',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.01 99211-N9100 14A',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.01 99211-N9240 14T',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00NX4__ 1.00 1.00 99110-N9100 ',
+ b'\xf1\x00NX4__ 1.00 1.01 99110-N9000 ',
+ b'\xf1\x00NX4__ 1.00 1.02 99110-N9000 ',
+ b'\xf1\x00NX4__ 1.01 1.00 99110-N9100 ',
+ ],
+ },
+ CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-CW000 14M',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-CW010 14X',
+ b'\xf1\x00NX4 FR_CMR AT USA LHD 1.00 1.00 99211-CW020 14Z',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00NX4__ 1.00 1.00 99110-K5000 ',
+ b'\xf1\x00NX4__ 1.01 1.00 99110-K5000 ',
+ ],
+ },
+ CAR.KIA_SPORTAGE_5TH_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00NQ5 FR_CMR AT AUS RHD 1.00 1.00 99211-P1040 663',
+ b'\xf1\x00NQ5 FR_CMR AT EUR LHD 1.00 1.00 99211-P1040 663',
+ b'\xf1\x00NQ5 FR_CMR AT GEN LHD 1.00 1.00 99211-P1060 665',
+ b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1030 662',
+ b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1040 663',
+ b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1060 665',
+ b'\xf1\x00NQ5 FR_CMR AT USA LHD 1.00 1.00 99211-P1070 690',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00NQ5__ 1.00 1.02 99110-P1000 ',
+ b'\xf1\x00NQ5__ 1.00 1.03 99110-CH000 ',
+ b'\xf1\x00NQ5__ 1.00 1.03 99110-P1000 ',
+ b'\xf1\x00NQ5__ 1.01 1.03 99110-CH000 ',
+ b'\xf1\x00NQ5__ 1.01 1.03 99110-P1000 ',
+ ],
+ },
+ CAR.GENESIS_GV70_1ST_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00JK1 MFC AT USA LHD 1.00 1.01 99211-AR200 220125',
+ b'\xf1\x00JK1 MFC AT USA LHD 1.00 1.01 99211-AR300 220125',
+ b'\xf1\x00JK1 MFC AT USA LHD 1.00 1.04 99211-AR000 210204',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00JK1_ SCC FHCUP 1.00 1.00 99110-AR200 ',
+ b'\xf1\x00JK1_ SCC FHCUP 1.00 1.00 99110-AR300 ',
+ b'\xf1\x00JK1_ SCC FHCUP 1.00 1.02 99110-AR000 ',
+ ],
+ },
+ CAR.GENESIS_GV60_EV_1ST_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00JW1 MFC AT USA LHD 1.00 1.02 99211-CU000 211215',
+ b'\xf1\x00JW1 MFC AT USA LHD 1.00 1.02 99211-CU100 211215',
+ b'\xf1\x00JW1 MFC AT USA LHD 1.00 1.03 99211-CU000 221118',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00JW1_ RDR ----- 1.00 1.00 99110-CU000 ',
+ ],
+ },
+ CAR.KIA_SORENTO_4TH_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00MQ4 MFC AT USA LHD 1.00 1.00 99210-R5100 221019',
+ b'\xf1\x00MQ4 MFC AT USA LHD 1.00 1.03 99210-R5000 200903',
+ b'\xf1\x00MQ4 MFC AT USA LHD 1.00 1.05 99210-R5000 210623',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00MQ4_ SCC F-CUP 1.00 1.06 99110-P2000 ',
+ b'\xf1\x00MQ4_ SCC FHCUP 1.00 1.00 99110-R5000 ',
+ b'\xf1\x00MQ4_ SCC FHCUP 1.00 1.06 99110-P2000 ',
+ b'\xf1\x00MQ4_ SCC FHCUP 1.00 1.08 99110-P2000 ',
+ ],
+ },
+ CAR.KIA_SORENTO_HEV_4TH_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00MQ4HMFC AT KOR LHD 1.00 1.04 99210-P2000 200330',
+ b'\xf1\x00MQ4HMFC AT KOR LHD 1.00 1.12 99210-P2000 230331',
+ b'\xf1\x00MQ4HMFC AT USA LHD 1.00 1.10 99210-P2000 210406',
+ b'\xf1\x00MQ4HMFC AT USA LHD 1.00 1.11 99210-P2000 211217',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00MQhe SCC FHCUP 1.00 1.04 99110-P4000 ',
+ b'\xf1\x00MQhe SCC FHCUP 1.00 1.06 99110-P4000 ',
+ b'\xf1\x00MQhe SCC FHCUP 1.00 1.07 99110-P4000 ',
+ ],
+ },
+ CAR.KIA_NIRO_HEV_2ND_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00SG2HMFC AT USA LHD 1.01 1.08 99211-AT000 220531',
+ b'\xf1\x00SG2HMFC AT USA LHD 1.01 1.09 99211-AT000 220801',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00SG2_ RDR ----- 1.00 1.01 99110-AT000 ',
+ ],
+ },
+ CAR.GENESIS_GV80: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00JX1 MFC AT USA LHD 1.00 1.02 99211-T6110 220513',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00JX1_ SCC FHCUP 1.00 1.01 99110-T6100 ',
+ ],
+ },
+ CAR.KIA_CARNIVAL_4TH_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00KA4 MFC AT EUR LHD 1.00 1.06 99210-R0000 220221',
+ b'\xf1\x00KA4 MFC AT KOR LHD 1.00 1.06 99210-R0000 220221',
+ b'\xf1\x00KA4 MFC AT USA LHD 1.00 1.00 99210-R0100 230105',
+ b'\xf1\x00KA4 MFC AT USA LHD 1.00 1.01 99210-R0100 230710',
+ b'\xf1\x00KA4 MFC AT USA LHD 1.00 1.05 99210-R0000 201221',
+ b'\xf1\x00KA4 MFC AT USA LHD 1.00 1.06 99210-R0000 220221',
+ b'\xf1\x00KA4CMFC AT CHN LHD 1.00 1.01 99211-I4000 210525',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00KA4_ SCC F-CUP 1.00 1.03 99110-R0000 ',
+ b'\xf1\x00KA4_ SCC FHCUP 1.00 1.00 99110-R0100 ',
+ b'\xf1\x00KA4_ SCC FHCUP 1.00 1.03 99110-R0000 ',
+ b'\xf1\x00KA4c SCC FHCUP 1.00 1.01 99110-I4000 ',
+ ],
+ },
+ CAR.KIA_K8_HEV_1ST_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00GL3HMFC AT KOR LHD 1.00 1.03 99211-L8000 210907',
+ b'\xf1\x00GL3HMFC AT KOR LHD 1.00 1.04 99211-L8000 230207',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00GL3_ RDR ----- 1.00 1.02 99110-L8000 ',
+ ],
+ },
+ CAR.HYUNDAI_STARIA_4TH_GEN: {
+ (Ecu.fwdCamera, 0x7c4, None): [
+ b'\xf1\x00US4 MFC AT KOR LHD 1.00 1.06 99211-CG000 230524',
+ ],
+ (Ecu.fwdRadar, 0x7d0, None): [
+ b'\xf1\x00US4_ RDR ----- 1.00 1.00 99110-CG000 ',
+ ],
+ },
+}
diff --git a/opendbc/car/hyundai/hyundaican.py b/opendbc/car/hyundai/hyundaican.py
new file mode 100644
index 0000000000..0b65a8aed8
--- /dev/null
+++ b/opendbc/car/hyundai/hyundaican.py
@@ -0,0 +1,213 @@
+import crcmod
+from opendbc.car.hyundai.values import CAR, HyundaiFlags
+
+hyundai_checksum = crcmod.mkCrcFun(0x11D, initCrc=0xFD, rev=False, xorOut=0xdf)
+
+def create_lkas11(packer, frame, CP, apply_steer, steer_req,
+ torque_fault, lkas11, sys_warning, sys_state, enabled,
+ left_lane, right_lane,
+ left_lane_depart, right_lane_depart):
+ values = {s: lkas11[s] for s in [
+ "CF_Lkas_LdwsActivemode",
+ "CF_Lkas_LdwsSysState",
+ "CF_Lkas_SysWarning",
+ "CF_Lkas_LdwsLHWarning",
+ "CF_Lkas_LdwsRHWarning",
+ "CF_Lkas_HbaLamp",
+ "CF_Lkas_FcwBasReq",
+ "CF_Lkas_HbaSysState",
+ "CF_Lkas_FcwOpt",
+ "CF_Lkas_HbaOpt",
+ "CF_Lkas_FcwSysState",
+ "CF_Lkas_FcwCollisionWarning",
+ "CF_Lkas_FusionState",
+ "CF_Lkas_FcwOpt_USM",
+ "CF_Lkas_LdwsOpt_USM",
+ ]}
+ values["CF_Lkas_LdwsSysState"] = sys_state
+ values["CF_Lkas_SysWarning"] = 3 if sys_warning else 0
+ values["CF_Lkas_LdwsLHWarning"] = left_lane_depart
+ values["CF_Lkas_LdwsRHWarning"] = right_lane_depart
+ values["CR_Lkas_StrToqReq"] = apply_steer
+ values["CF_Lkas_ActToi"] = steer_req
+ values["CF_Lkas_ToiFlt"] = torque_fault # seems to allow actuation on CR_Lkas_StrToqReq
+ values["CF_Lkas_MsgCount"] = frame % 0x10
+
+ if CP.carFingerprint in (CAR.HYUNDAI_SONATA, CAR.HYUNDAI_PALISADE, CAR.KIA_NIRO_EV, CAR.KIA_NIRO_HEV_2021, CAR.HYUNDAI_SANTA_FE,
+ CAR.HYUNDAI_IONIQ_EV_2020, CAR.HYUNDAI_IONIQ_PHEV, CAR.KIA_SELTOS, CAR.HYUNDAI_ELANTRA_2021, CAR.GENESIS_G70_2020,
+ CAR.HYUNDAI_ELANTRA_HEV_2021, CAR.HYUNDAI_SONATA_HYBRID, CAR.HYUNDAI_KONA_EV, CAR.HYUNDAI_KONA_HEV, CAR.HYUNDAI_KONA_EV_2022,
+ CAR.HYUNDAI_SANTA_FE_2022, CAR.KIA_K5_2021, CAR.HYUNDAI_IONIQ_HEV_2022, CAR.HYUNDAI_SANTA_FE_HEV_2022,
+ CAR.HYUNDAI_SANTA_FE_PHEV_2022, CAR.KIA_STINGER_2022, CAR.KIA_K5_HEV_2020, CAR.KIA_CEED,
+ CAR.HYUNDAI_AZERA_6TH_GEN, CAR.HYUNDAI_AZERA_HEV_6TH_GEN, CAR.HYUNDAI_CUSTIN_1ST_GEN):
+ values["CF_Lkas_LdwsActivemode"] = int(left_lane) + (int(right_lane) << 1)
+ values["CF_Lkas_LdwsOpt_USM"] = 2
+
+ # FcwOpt_USM 5 = Orange blinking car + lanes
+ # FcwOpt_USM 4 = Orange car + lanes
+ # FcwOpt_USM 3 = Green blinking car + lanes
+ # FcwOpt_USM 2 = Green car + lanes
+ # FcwOpt_USM 1 = White car + lanes
+ # FcwOpt_USM 0 = No car + lanes
+ values["CF_Lkas_FcwOpt_USM"] = 2 if enabled else 1
+
+ # SysWarning 4 = keep hands on wheel
+ # SysWarning 5 = keep hands on wheel (red)
+ # SysWarning 6 = keep hands on wheel (red) + beep
+ # Note: the warning is hidden while the blinkers are on
+ values["CF_Lkas_SysWarning"] = 4 if sys_warning else 0
+
+ # Likely cars lacking the ability to show individual lane lines in the dash
+ elif CP.carFingerprint in (CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL):
+ # SysWarning 4 = keep hands on wheel + beep
+ values["CF_Lkas_SysWarning"] = 4 if sys_warning else 0
+
+ # SysState 0 = no icons
+ # SysState 1-2 = white car + lanes
+ # SysState 3 = green car + lanes, green steering wheel
+ # SysState 4 = green car + lanes
+ values["CF_Lkas_LdwsSysState"] = 3 if enabled else 1
+ values["CF_Lkas_LdwsOpt_USM"] = 2 # non-2 changes above SysState definition
+
+ # these have no effect
+ values["CF_Lkas_LdwsActivemode"] = 0
+ values["CF_Lkas_FcwOpt_USM"] = 0
+
+ elif CP.carFingerprint == CAR.HYUNDAI_GENESIS:
+ # This field is actually LdwsActivemode
+ # Genesis and Optima fault when forwarding while engaged
+ values["CF_Lkas_LdwsActivemode"] = 2
+
+ dat = packer.make_can_msg("LKAS11", 0, values)[1]
+
+ if CP.flags & HyundaiFlags.CHECKSUM_CRC8:
+ # CRC Checksum as seen on 2019 Hyundai Santa Fe
+ dat = dat[:6] + dat[7:8]
+ checksum = hyundai_checksum(dat)
+ elif CP.flags & HyundaiFlags.CHECKSUM_6B:
+ # Checksum of first 6 Bytes, as seen on 2018 Kia Sorento
+ checksum = sum(dat[:6]) % 256
+ else:
+ # Checksum of first 6 Bytes and last Byte as seen on 2018 Kia Stinger
+ checksum = (sum(dat[:6]) + dat[7]) % 256
+
+ values["CF_Lkas_Chksum"] = checksum
+
+ return packer.make_can_msg("LKAS11", 0, values)
+
+
+def create_clu11(packer, frame, clu11, button, CP):
+ values = {s: clu11[s] for s in [
+ "CF_Clu_CruiseSwState",
+ "CF_Clu_CruiseSwMain",
+ "CF_Clu_SldMainSW",
+ "CF_Clu_ParityBit1",
+ "CF_Clu_VanzDecimal",
+ "CF_Clu_Vanz",
+ "CF_Clu_SPEED_UNIT",
+ "CF_Clu_DetentOut",
+ "CF_Clu_RheostatLevel",
+ "CF_Clu_CluInfo",
+ "CF_Clu_AmpInfo",
+ "CF_Clu_AliveCnt1",
+ ]}
+ values["CF_Clu_CruiseSwState"] = button
+ values["CF_Clu_AliveCnt1"] = frame % 0x10
+ # send buttons to camera on camera-scc based cars
+ bus = 2 if CP.flags & HyundaiFlags.CAMERA_SCC else 0
+ return packer.make_can_msg("CLU11", bus, values)
+
+
+def create_lfahda_mfc(packer, enabled, hda_set_speed=0):
+ values = {
+ "LFA_Icon_State": 2 if enabled else 0,
+ "HDA_Active": 1 if hda_set_speed else 0,
+ "HDA_Icon_State": 2 if hda_set_speed else 0,
+ "HDA_VSetReq": hda_set_speed,
+ }
+ return packer.make_can_msg("LFAHDA_MFC", 0, values)
+
+def create_acc_commands(packer, enabled, accel, upper_jerk, idx, hud_control, set_speed, stopping, long_override, use_fca):
+ commands = []
+
+ scc11_values = {
+ "MainMode_ACC": 1,
+ "TauGapSet": hud_control.leadDistanceBars,
+ "VSetDis": set_speed if enabled else 0,
+ "AliveCounterACC": idx % 0x10,
+ "ObjValid": 1, # close lead makes controls tighter
+ "ACC_ObjStatus": 1, # close lead makes controls tighter
+ "ACC_ObjLatPos": 0,
+ "ACC_ObjRelSpd": 0,
+ "ACC_ObjDist": 1, # close lead makes controls tighter
+ }
+ commands.append(packer.make_can_msg("SCC11", 0, scc11_values))
+
+ scc12_values = {
+ "ACCMode": 2 if enabled and long_override else 1 if enabled else 0,
+ "StopReq": 1 if stopping else 0,
+ "aReqRaw": accel,
+ "aReqValue": accel, # stock ramps up and down respecting jerk limit until it reaches aReqRaw
+ "CR_VSM_Alive": idx % 0xF,
+ }
+
+ # show AEB disabled indicator on dash with SCC12 if not sending FCA messages.
+ # these signals also prevent a TCS fault on non-FCA cars with alpha longitudinal
+ if not use_fca:
+ scc12_values["CF_VSM_ConfMode"] = 1
+ scc12_values["AEB_Status"] = 1 # AEB disabled
+
+ scc12_dat = packer.make_can_msg("SCC12", 0, scc12_values)[1]
+ scc12_values["CR_VSM_ChkSum"] = 0x10 - sum(sum(divmod(i, 16)) for i in scc12_dat) % 0x10
+
+ commands.append(packer.make_can_msg("SCC12", 0, scc12_values))
+
+ scc14_values = {
+ "ComfortBandUpper": 0.0, # stock usually is 0 but sometimes uses higher values
+ "ComfortBandLower": 0.0, # stock usually is 0 but sometimes uses higher values
+ "JerkUpperLimit": upper_jerk, # stock usually is 1.0 but sometimes uses higher values
+ "JerkLowerLimit": 5.0, # stock usually is 0.5 but sometimes uses higher values
+ "ACCMode": 2 if enabled and long_override else 1 if enabled else 4, # stock will always be 4 instead of 0 after first disengage
+ "ObjGap": 2 if hud_control.leadVisible else 0, # 5: >30, m, 4: 25-30 m, 3: 20-25 m, 2: < 20 m, 0: no lead
+ }
+ commands.append(packer.make_can_msg("SCC14", 0, scc14_values))
+
+ # Only send FCA11 on cars where it exists on the bus
+ if use_fca:
+ # note that some vehicles most likely have an alternate checksum/counter definition
+ # https://github.com/commaai/opendbc/commit/9ddcdb22c4929baf310295e832668e6e7fcfa602
+ fca11_values = {
+ "CR_FCA_Alive": idx % 0xF,
+ "PAINT1_Status": 1,
+ "FCA_DrvSetStatus": 1,
+ "FCA_Status": 1, # AEB disabled
+ }
+ fca11_dat = packer.make_can_msg("FCA11", 0, fca11_values)[1]
+ fca11_values["CR_FCA_ChkSum"] = hyundai_checksum(fca11_dat[:7])
+ commands.append(packer.make_can_msg("FCA11", 0, fca11_values))
+
+ return commands
+
+def create_acc_opt(packer):
+ commands = []
+
+ scc13_values = {
+ "SCCDrvModeRValue": 2,
+ "SCC_Equip": 1,
+ "Lead_Veh_Dep_Alert_USM": 2,
+ }
+ commands.append(packer.make_can_msg("SCC13", 0, scc13_values))
+
+ # TODO: this needs to be detected and conditionally sent on unsupported long cars
+ fca12_values = {
+ "FCA_DrvSetState": 2,
+ "FCA_USM": 1, # AEB disabled
+ }
+ commands.append(packer.make_can_msg("FCA12", 0, fca12_values))
+
+ return commands
+
+def create_frt_radar_opt(packer):
+ frt_radar11_values = {
+ "CF_FCA_Equip_Front_Radar": 1,
+ }
+ return packer.make_can_msg("FRT_RADAR11", 0, frt_radar11_values)
diff --git a/opendbc/car/hyundai/hyundaicanfd.py b/opendbc/car/hyundai/hyundaicanfd.py
new file mode 100644
index 0000000000..82ed0828b7
--- /dev/null
+++ b/opendbc/car/hyundai/hyundaicanfd.py
@@ -0,0 +1,223 @@
+from opendbc.car import CanBusBase
+from opendbc.car.common.numpy_fast import clip
+from opendbc.car.hyundai.values import HyundaiFlags
+
+
+class CanBus(CanBusBase):
+ def __init__(self, CP, hda2=None, fingerprint=None) -> None:
+ super().__init__(CP, fingerprint)
+
+ if hda2 is None:
+ assert CP is not None
+ hda2 = CP.flags & HyundaiFlags.CANFD_HDA2.value
+
+ # On the CAN-FD platforms, the LKAS camera is on both A-CAN and E-CAN. HDA2 cars
+ # have a different harness than the HDA1 and non-HDA variants in order to split
+ # a different bus, since the steering is done by different ECUs.
+ self._a, self._e = 1, 0
+ if hda2:
+ self._a, self._e = 0, 1
+
+ self._a += self.offset
+ self._e += self.offset
+ self._cam = 2 + self.offset
+
+ @property
+ def ECAN(self):
+ return self._e
+
+ @property
+ def ACAN(self):
+ return self._a
+
+ @property
+ def CAM(self):
+ return self._cam
+
+
+def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_steer):
+
+ ret = []
+
+ values = {
+ "LKA_MODE": 2,
+ "LKA_ICON": 2 if enabled else 1,
+ "TORQUE_REQUEST": apply_steer,
+ "LKA_ASSIST": 0,
+ "STEER_REQ": 1 if lat_active else 0,
+ "STEER_MODE": 0,
+ "HAS_LANE_SAFETY": 0, # hide LKAS settings
+ "NEW_SIGNAL_1": 0,
+ "NEW_SIGNAL_2": 0,
+ }
+
+ if CP.flags & HyundaiFlags.CANFD_HDA2:
+ hda2_lkas_msg = "LKAS_ALT" if CP.flags & HyundaiFlags.CANFD_HDA2_ALT_STEERING else "LKAS"
+ if CP.openpilotLongitudinalControl:
+ ret.append(packer.make_can_msg("LFA", CAN.ECAN, values))
+ ret.append(packer.make_can_msg(hda2_lkas_msg, CAN.ACAN, values))
+ else:
+ ret.append(packer.make_can_msg("LFA", CAN.ECAN, values))
+
+ return ret
+
+def create_suppress_lfa(packer, CAN, hda2_lfa_block_msg, hda2_alt_steering):
+ suppress_msg = "CAM_0x362" if hda2_alt_steering else "CAM_0x2a4"
+ msg_bytes = 32 if hda2_alt_steering else 24
+
+ values = {f"BYTE{i}": hda2_lfa_block_msg[f"BYTE{i}"] for i in range(3, msg_bytes) if i != 7}
+ values["COUNTER"] = hda2_lfa_block_msg["COUNTER"]
+ values["SET_ME_0"] = 0
+ values["SET_ME_0_2"] = 0
+ values["LEFT_LANE_LINE"] = 0
+ values["RIGHT_LANE_LINE"] = 0
+ return packer.make_can_msg(suppress_msg, CAN.ACAN, values)
+
+def create_buttons(packer, CP, CAN, cnt, btn):
+ values = {
+ "COUNTER": cnt,
+ "SET_ME_1": 1,
+ "CRUISE_BUTTONS": btn,
+ }
+
+ bus = CAN.ECAN if CP.flags & HyundaiFlags.CANFD_HDA2 else CAN.CAM
+ return packer.make_can_msg("CRUISE_BUTTONS", bus, values)
+
+def create_acc_cancel(packer, CP, CAN, cruise_info_copy):
+ # TODO: why do we copy different values here?
+ if CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value:
+ values = {s: cruise_info_copy[s] for s in [
+ "COUNTER",
+ "CHECKSUM",
+ "NEW_SIGNAL_1",
+ "MainMode_ACC",
+ "ACCMode",
+ "ZEROS_9",
+ "CRUISE_STANDSTILL",
+ "ZEROS_5",
+ "DISTANCE_SETTING",
+ "VSetDis",
+ ]}
+ else:
+ values = {s: cruise_info_copy[s] for s in [
+ "COUNTER",
+ "CHECKSUM",
+ "ACCMode",
+ "VSetDis",
+ "CRUISE_STANDSTILL",
+ ]}
+ values.update({
+ "ACCMode": 4,
+ "aReqRaw": 0.0,
+ "aReqValue": 0.0,
+ })
+ return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values)
+
+def create_lfahda_cluster(packer, CAN, enabled):
+ values = {
+ "HDA_ICON": 1 if enabled else 0,
+ "LFA_ICON": 2 if enabled else 0,
+ }
+ return packer.make_can_msg("LFAHDA_CLUSTER", CAN.ECAN, values)
+
+
+def create_acc_control(packer, CAN, enabled, accel_last, accel, stopping, gas_override, set_speed, hud_control):
+ jerk = 5
+ jn = jerk / 50
+ if not enabled or gas_override:
+ a_val, a_raw = 0, 0
+ else:
+ a_raw = accel
+ a_val = clip(accel, accel_last - jn, accel_last + jn)
+
+ values = {
+ "ACCMode": 0 if not enabled else (2 if gas_override else 1),
+ "MainMode_ACC": 1,
+ "StopReq": 1 if stopping else 0,
+ "aReqValue": a_val,
+ "aReqRaw": a_raw,
+ "VSetDis": set_speed,
+ "JerkLowerLimit": jerk if enabled else 1,
+ "JerkUpperLimit": 3.0,
+
+ "ACC_ObjDist": 1,
+ "ObjValid": 0,
+ "OBJ_STATUS": 2,
+ "SET_ME_2": 0x4,
+ "SET_ME_3": 0x3,
+ "SET_ME_TMP_64": 0x64,
+ "DISTANCE_SETTING": hud_control.leadDistanceBars,
+ }
+
+ return packer.make_can_msg("SCC_CONTROL", CAN.ECAN, values)
+
+
+def create_spas_messages(packer, CAN, frame, left_blink, right_blink):
+ ret = []
+
+ values = {
+ }
+ ret.append(packer.make_can_msg("SPAS1", CAN.ECAN, values))
+
+ blink = 0
+ if left_blink:
+ blink = 3
+ elif right_blink:
+ blink = 4
+ values = {
+ "BLINKER_CONTROL": blink,
+ }
+ ret.append(packer.make_can_msg("SPAS2", CAN.ECAN, values))
+
+ return ret
+
+
+def create_adrv_messages(packer, CAN, frame):
+ # messages needed to car happy after disabling
+ # the ADAS Driving ECU to do longitudinal control
+
+ ret = []
+
+ values = {
+ }
+ ret.append(packer.make_can_msg("ADRV_0x51", CAN.ACAN, values))
+
+ if frame % 2 == 0:
+ values = {
+ 'AEB_SETTING': 0x1, # show AEB disabled icon
+ 'SET_ME_2': 0x2,
+ 'SET_ME_FF': 0xff,
+ 'SET_ME_FC': 0xfc,
+ 'SET_ME_9': 0x9,
+ }
+ ret.append(packer.make_can_msg("ADRV_0x160", CAN.ECAN, values))
+
+ if frame % 5 == 0:
+ values = {
+ 'SET_ME_1C': 0x1c,
+ 'SET_ME_FF': 0xff,
+ 'SET_ME_TMP_F': 0xf,
+ 'SET_ME_TMP_F_2': 0xf,
+ }
+ ret.append(packer.make_can_msg("ADRV_0x1ea", CAN.ECAN, values))
+
+ values = {
+ 'SET_ME_E1': 0xe1,
+ 'SET_ME_3A': 0x3a,
+ }
+ ret.append(packer.make_can_msg("ADRV_0x200", CAN.ECAN, values))
+
+ if frame % 20 == 0:
+ values = {
+ 'SET_ME_15': 0x15,
+ }
+ ret.append(packer.make_can_msg("ADRV_0x345", CAN.ECAN, values))
+
+ if frame % 100 == 0:
+ values = {
+ 'SET_ME_22': 0x22,
+ 'SET_ME_41': 0x41,
+ }
+ ret.append(packer.make_can_msg("ADRV_0x1da", CAN.ECAN, values))
+
+ return ret
diff --git a/opendbc/car/hyundai/interface.py b/opendbc/car/hyundai/interface.py
new file mode 100644
index 0000000000..27df5b556d
--- /dev/null
+++ b/opendbc/car/hyundai/interface.py
@@ -0,0 +1,147 @@
+from panda import Panda
+from opendbc.car import get_safety_config, structs
+from opendbc.car.hyundai.hyundaicanfd import CanBus
+from opendbc.car.hyundai.values import HyundaiFlags, CAR, DBC, CANFD_CAR, CAMERA_SCC_CAR, CANFD_RADAR_SCC_CAR, \
+ CANFD_UNSUPPORTED_LONGITUDINAL_CAR, EV_CAR, HYBRID_CAR, LEGACY_SAFETY_MODE_CAR, \
+ UNSUPPORTED_LONGITUDINAL_CAR, Buttons
+from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR
+from opendbc.car.interfaces import CarInterfaceBase
+from opendbc.car.disable_ecu import disable_ecu
+
+Ecu = structs.CarParams.Ecu
+
+ENABLE_BUTTONS = (Buttons.RES_ACCEL, Buttons.SET_DECEL, Buttons.CANCEL)
+
+
+class CarInterface(CarInterfaceBase):
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "hyundai"
+ ret.radarUnavailable = RADAR_START_ADDR not in fingerprint[1] or DBC[ret.carFingerprint]["radar"] is None
+
+ # These cars have been put into dashcam only due to both a lack of users and test coverage.
+ # These cars likely still work fine. Once a user confirms each car works and a test route is
+ # added to opendbc/car/tests/routes.py, we can remove it from this list.
+ # FIXME: the Optima Hybrid 2017 uses a different SCC12 checksum
+ ret.dashcamOnly = candidate in {CAR.KIA_OPTIMA_H, }
+
+ hda2 = Ecu.adas in [fw.ecu for fw in car_fw]
+ CAN = CanBus(None, hda2, fingerprint)
+
+ if candidate in CANFD_CAR:
+ # detect if car is hybrid
+ if 0x105 in fingerprint[CAN.ECAN]:
+ ret.flags |= HyundaiFlags.HYBRID.value
+ elif candidate in EV_CAR:
+ ret.flags |= HyundaiFlags.EV.value
+
+ # detect HDA2 with ADAS Driving ECU
+ if hda2:
+ ret.flags |= HyundaiFlags.CANFD_HDA2.value
+ if 0x110 in fingerprint[CAN.CAM]:
+ ret.flags |= HyundaiFlags.CANFD_HDA2_ALT_STEERING.value
+ else:
+ # non-HDA2
+ if 0x1cf not in fingerprint[CAN.ECAN]:
+ ret.flags |= HyundaiFlags.CANFD_ALT_BUTTONS.value
+ # ICE cars do not have 0x130; GEARS message on 0x40 or 0x70 instead
+ if 0x130 not in fingerprint[CAN.ECAN]:
+ if 0x40 not in fingerprint[CAN.ECAN]:
+ ret.flags |= HyundaiFlags.CANFD_ALT_GEARS_2.value
+ else:
+ ret.flags |= HyundaiFlags.CANFD_ALT_GEARS.value
+ if candidate not in CANFD_RADAR_SCC_CAR:
+ ret.flags |= HyundaiFlags.CANFD_CAMERA_SCC.value
+ else:
+ # TODO: detect EV and hybrid
+ if candidate in HYBRID_CAR:
+ ret.flags |= HyundaiFlags.HYBRID.value
+ elif candidate in EV_CAR:
+ ret.flags |= HyundaiFlags.EV.value
+
+ # Send LFA message on cars with HDA
+ if 0x485 in fingerprint[2]:
+ ret.flags |= HyundaiFlags.SEND_LFA.value
+
+ # These cars use the FCA11 message for the AEB and FCW signals, all others use SCC12
+ if 0x38d in fingerprint[0] or 0x38d in fingerprint[2]:
+ ret.flags |= HyundaiFlags.USE_FCA.value
+
+ ret.steerActuatorDelay = 0.1 # Default delay
+ ret.steerLimitTimer = 0.4
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ if candidate == CAR.KIA_OPTIMA_G4_FL:
+ ret.steerActuatorDelay = 0.2
+
+ # *** longitudinal control ***
+ if candidate in CANFD_CAR:
+ ret.experimentalLongitudinalAvailable = candidate not in (CANFD_UNSUPPORTED_LONGITUDINAL_CAR | CANFD_RADAR_SCC_CAR)
+ else:
+ ret.experimentalLongitudinalAvailable = candidate not in (UNSUPPORTED_LONGITUDINAL_CAR | CAMERA_SCC_CAR)
+ ret.openpilotLongitudinalControl = experimental_long and ret.experimentalLongitudinalAvailable
+ ret.pcmCruise = not ret.openpilotLongitudinalControl
+
+ ret.stoppingControl = True
+ ret.startingState = True
+ ret.vEgoStarting = 0.1
+ ret.startAccel = 1.0
+ ret.longitudinalActuatorDelay = 0.5
+
+ # *** feature detection ***
+ if candidate in CANFD_CAR:
+ ret.enableBsm = 0x1e5 in fingerprint[CAN.ECAN]
+ else:
+ ret.enableBsm = 0x58b in fingerprint[0]
+
+ # *** panda safety config ***
+ if candidate in CANFD_CAR:
+ cfgs = [get_safety_config(structs.CarParams.SafetyModel.hyundaiCanfd), ]
+ if CAN.ECAN >= 4:
+ cfgs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput))
+ ret.safetyConfigs = cfgs
+
+ if ret.flags & HyundaiFlags.CANFD_HDA2:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2
+ if ret.flags & HyundaiFlags.CANFD_HDA2_ALT_STEERING:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2_ALT_STEERING
+ if ret.flags & HyundaiFlags.CANFD_ALT_BUTTONS:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_ALT_BUTTONS
+ if ret.flags & HyundaiFlags.CANFD_CAMERA_SCC:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC
+ else:
+ if candidate in LEGACY_SAFETY_MODE_CAR:
+ # these cars require a special panda safety mode due to missing counters and checksums in the messages
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hyundaiLegacy)]
+ else:
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.hyundai, 0)]
+
+ if candidate in CAMERA_SCC_CAR:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC
+
+ if ret.openpilotLongitudinalControl:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_LONG
+ if ret.flags & HyundaiFlags.HYBRID:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_HYBRID_GAS
+ elif ret.flags & HyundaiFlags.EV:
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_EV_GAS
+
+ if candidate in (CAR.HYUNDAI_KONA, CAR.HYUNDAI_KONA_EV, CAR.HYUNDAI_KONA_HEV, CAR.HYUNDAI_KONA_EV_2022):
+ ret.flags |= HyundaiFlags.ALT_LIMITS.value
+ ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_ALT_LIMITS
+
+ ret.centerToFront = ret.wheelbase * 0.4
+
+ return ret
+
+ @staticmethod
+ def init(CP, can_recv, can_send):
+ if CP.openpilotLongitudinalControl and not (CP.flags & HyundaiFlags.CANFD_CAMERA_SCC.value):
+ addr, bus = 0x7d0, 0
+ if CP.flags & HyundaiFlags.CANFD_HDA2.value:
+ addr, bus = 0x730, CanBus(CP).ECAN
+ disable_ecu(can_recv, can_send, bus=bus, addr=addr, com_cont_req=b'\x28\x83\x01')
+
+ # for blinkers
+ if CP.flags & HyundaiFlags.ENABLE_BLINKERS:
+ disable_ecu(can_recv, can_send, bus=CanBus(CP).ECAN, addr=0x7B1, com_cont_req=b'\x28\x83\x01')
diff --git a/opendbc/car/hyundai/radar_interface.py b/opendbc/car/hyundai/radar_interface.py
new file mode 100644
index 0000000000..a9078749d6
--- /dev/null
+++ b/opendbc/car/hyundai/radar_interface.py
@@ -0,0 +1,79 @@
+import math
+
+from opendbc.can.parser import CANParser
+from opendbc.car import structs
+from opendbc.car.interfaces import RadarInterfaceBase
+from opendbc.car.hyundai.values import DBC
+
+RADAR_START_ADDR = 0x500
+RADAR_MSG_COUNT = 32
+
+# POC for parsing corner radars: https://github.com/commaai/openpilot/pull/24221/
+
+def get_radar_can_parser(CP):
+ if DBC[CP.carFingerprint]['radar'] is None:
+ return None
+
+ messages = [(f"RADAR_TRACK_{addr:x}", 50) for addr in range(RADAR_START_ADDR, RADAR_START_ADDR + RADAR_MSG_COUNT)]
+ return CANParser(DBC[CP.carFingerprint]['radar'], messages, 1)
+
+
+class RadarInterface(RadarInterfaceBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ self.updated_messages = set()
+ self.trigger_msg = RADAR_START_ADDR + RADAR_MSG_COUNT - 1
+ self.track_id = 0
+
+ self.radar_off_can = CP.radarUnavailable
+ self.rcp = get_radar_can_parser(CP)
+
+ def update(self, can_strings):
+ if self.radar_off_can or (self.rcp is None):
+ return super().update(None)
+
+ vls = self.rcp.update_strings(can_strings)
+ self.updated_messages.update(vls)
+
+ if self.trigger_msg not in self.updated_messages:
+ return None
+
+ rr = self._update(self.updated_messages)
+ self.updated_messages.clear()
+
+ return rr
+
+ def _update(self, updated_messages):
+ ret = structs.RadarData()
+ if self.rcp is None:
+ return ret
+
+ errors = []
+
+ if not self.rcp.can_valid:
+ errors.append("canError")
+ ret.errors = errors
+
+ for addr in range(RADAR_START_ADDR, RADAR_START_ADDR + RADAR_MSG_COUNT):
+ msg = self.rcp.vl[f"RADAR_TRACK_{addr:x}"]
+
+ if addr not in self.pts:
+ self.pts[addr] = structs.RadarData.RadarPoint()
+ self.pts[addr].trackId = self.track_id
+ self.track_id += 1
+
+ valid = msg['STATE'] in (3, 4)
+ if valid:
+ azimuth = math.radians(msg['AZIMUTH'])
+ self.pts[addr].measured = True
+ self.pts[addr].dRel = math.cos(azimuth) * msg['LONG_DIST']
+ self.pts[addr].yRel = 0.5 * -math.sin(azimuth) * msg['LONG_DIST']
+ self.pts[addr].vRel = msg['REL_SPEED']
+ self.pts[addr].aRel = msg['REL_ACCEL']
+ self.pts[addr].yvRel = float('nan')
+
+ else:
+ del self.pts[addr]
+
+ ret.points = list(self.pts.values())
+ return ret
diff --git a/opendbc/car/hyundai/tests/__init__.py b/opendbc/car/hyundai/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/hyundai/tests/print_platform_codes.py b/opendbc/car/hyundai/tests/print_platform_codes.py
new file mode 100755
index 0000000000..325b020f9b
--- /dev/null
+++ b/opendbc/car/hyundai/tests/print_platform_codes.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+from opendbc.car.structs import CarParams
+from opendbc.car.hyundai.values import PLATFORM_CODE_ECUS, get_platform_codes
+from opendbc.car.hyundai.fingerprints import FW_VERSIONS
+
+Ecu = CarParams.Ecu
+
+if __name__ == "__main__":
+ for car_model, ecus in FW_VERSIONS.items():
+ print()
+ print(car_model)
+ for ecu in sorted(ecus):
+ if ecu[0] not in PLATFORM_CODE_ECUS:
+ continue
+
+ platform_codes = get_platform_codes(ecus[ecu])
+ codes = {code for code, _ in platform_codes}
+ dates = {date for _, date in platform_codes if date is not None}
+ print(f' (Ecu.{ecu[0]}, {hex(ecu[1])}, {ecu[2]}):')
+ print(f' Codes: {codes}')
+ print(f' Dates: {dates}')
diff --git a/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc/car/hyundai/tests/test_hyundai.py
new file mode 100644
index 0000000000..9fc28827e1
--- /dev/null
+++ b/opendbc/car/hyundai/tests/test_hyundai.py
@@ -0,0 +1,235 @@
+from hypothesis import settings, given, strategies as st
+
+import pytest
+
+from opendbc.car import gen_empty_fingerprint
+from opendbc.car.structs import CarParams
+from opendbc.car.fw_versions import build_fw_dict
+from opendbc.car.hyundai.interface import CarInterface
+from opendbc.car.hyundai.radar_interface import RADAR_START_ADDR
+from opendbc.car.hyundai.values import CAMERA_SCC_CAR, CANFD_CAR, CAN_GEARS, CAR, CHECKSUM, DATE_FW_ECUS, \
+ HYBRID_CAR, EV_CAR, FW_QUERY_CONFIG, LEGACY_SAFETY_MODE_CAR, CANFD_FUZZY_WHITELIST, \
+ UNSUPPORTED_LONGITUDINAL_CAR, PLATFORM_CODE_ECUS, HYUNDAI_VERSION_REQUEST_LONG, \
+ HyundaiFlags, get_platform_codes
+from opendbc.car.hyundai.fingerprints import FW_VERSIONS
+
+Ecu = CarParams.Ecu
+
+# Some platforms have date codes in a different format we don't yet parse (or are missing).
+# For now, assert list of expected missing date cars
+NO_DATES_PLATFORMS = {
+ # CAN FD
+ CAR.KIA_SPORTAGE_5TH_GEN,
+ CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN,
+ CAR.HYUNDAI_TUCSON_4TH_GEN,
+ # CAN
+ CAR.HYUNDAI_ELANTRA,
+ CAR.HYUNDAI_ELANTRA_GT_I30,
+ CAR.KIA_CEED,
+ CAR.KIA_FORTE,
+ CAR.KIA_OPTIMA_G4,
+ CAR.KIA_OPTIMA_G4_FL,
+ CAR.KIA_SORENTO,
+ CAR.HYUNDAI_KONA,
+ CAR.HYUNDAI_KONA_EV,
+ CAR.HYUNDAI_KONA_EV_2022,
+ CAR.HYUNDAI_KONA_HEV,
+ CAR.HYUNDAI_SONATA_LF,
+ CAR.HYUNDAI_VELOSTER,
+}
+
+CANFD_EXPECTED_ECUS = {Ecu.fwdCamera, Ecu.fwdRadar}
+
+
+class TestHyundaiFingerprint:
+ def test_feature_detection(self):
+ # HDA2
+ for has_adas in (True, False):
+ car_fw = [CarParams.CarFw(ecu=Ecu.adas if has_adas else Ecu.fwdCamera)]
+ CP = CarInterface.get_params(CAR.KIA_EV6, gen_empty_fingerprint(), car_fw, False, False)
+ assert bool(CP.flags & HyundaiFlags.CANFD_HDA2) == has_adas
+
+ # radar available
+ for radar in (True, False):
+ fingerprint = gen_empty_fingerprint()
+ if radar:
+ fingerprint[1][RADAR_START_ADDR] = 8
+ CP = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, car_fw, False, False)
+ assert CP.radarUnavailable != radar
+
+ def test_can_features(self):
+ # Test no EV/HEV in any gear lists (should all use ELECT_GEAR)
+ assert set.union(*CAN_GEARS.values()) & (HYBRID_CAR | EV_CAR) == set()
+
+ # Test CAN FD car not in CAN feature lists
+ can_specific_feature_list = set.union(*CAN_GEARS.values(), *CHECKSUM.values(), LEGACY_SAFETY_MODE_CAR, UNSUPPORTED_LONGITUDINAL_CAR, CAMERA_SCC_CAR)
+ for car_model in CANFD_CAR:
+ assert car_model not in can_specific_feature_list, "CAN FD car unexpectedly found in a CAN feature list"
+
+ def test_hybrid_ev_sets(self):
+ assert HYBRID_CAR & EV_CAR == set(), "Shared cars between hybrid and EV"
+ assert CANFD_CAR & HYBRID_CAR == set(), "Hard coding CAN FD cars as hybrid is no longer supported"
+
+ def test_canfd_ecu_whitelist(self):
+ # Asserts only expected Ecus can exist in database for CAN-FD cars
+ for car_model in CANFD_CAR:
+ ecus = {fw[0] for fw in FW_VERSIONS[car_model].keys()}
+ ecus_not_in_whitelist = ecus - CANFD_EXPECTED_ECUS
+ ecu_strings = ", ".join([f"Ecu.{ecu}" for ecu in ecus_not_in_whitelist])
+ assert len(ecus_not_in_whitelist) == 0, \
+ f"{car_model}: Car model has unexpected ECUs: {ecu_strings}"
+
+ def test_blacklisted_parts(self, subtests):
+ # Asserts no ECUs known to be shared across platforms exist in the database.
+ # Tucson having Santa Cruz camera and EPS for example
+ for car_model, ecus in FW_VERSIONS.items():
+ with subtests.test(car_model=car_model.value):
+ if car_model == CAR.HYUNDAI_SANTA_CRUZ_1ST_GEN:
+ pytest.skip("Skip checking Santa Cruz for its parts")
+
+ for code, _ in get_platform_codes(ecus[(Ecu.fwdCamera, 0x7c4, None)]):
+ if b"-" not in code:
+ continue
+ part = code.split(b"-")[1]
+ assert not part.startswith(b'CW'), "Car has bad part number"
+
+ def test_correct_ecu_response_database(self, subtests):
+ """
+ Assert standard responses for certain ECUs, since they can
+ respond to multiple queries with different data
+ """
+ expected_fw_prefix = HYUNDAI_VERSION_REQUEST_LONG[1:]
+ for car_model, ecus in FW_VERSIONS.items():
+ with subtests.test(car_model=car_model.value):
+ for ecu, fws in ecus.items():
+ assert all(fw.startswith(expected_fw_prefix) for fw in fws), \
+ f"FW from unexpected request in database: {(ecu, fws)}"
+
+ @settings(max_examples=100)
+ @given(data=st.data())
+ def test_platform_codes_fuzzy_fw(self, data):
+ """Ensure function doesn't raise an exception"""
+ fw_strategy = st.lists(st.binary())
+ fws = data.draw(fw_strategy)
+ get_platform_codes(fws)
+
+ def test_expected_platform_codes(self, subtests):
+ # Ensures we don't accidentally add multiple platform codes for a car unless it is intentional
+ for car_model, ecus in FW_VERSIONS.items():
+ with subtests.test(car_model=car_model.value):
+ for ecu, fws in ecus.items():
+ if ecu[0] not in PLATFORM_CODE_ECUS:
+ continue
+
+ # Third and fourth character are usually EV/hybrid identifiers
+ codes = {code.split(b"-")[0][:2] for code, _ in get_platform_codes(fws)}
+ if car_model == CAR.HYUNDAI_PALISADE:
+ assert codes == {b"LX", b"ON"}, f"Car has unexpected platform codes: {car_model} {codes}"
+ elif car_model == CAR.HYUNDAI_KONA_EV and ecu[0] == Ecu.fwdCamera:
+ assert codes == {b"OE", b"OS"}, f"Car has unexpected platform codes: {car_model} {codes}"
+ else:
+ assert len(codes) == 1, f"Car has multiple platform codes: {car_model} {codes}"
+
+ # Tests for platform codes, part numbers, and FW dates which Hyundai will use to fuzzy
+ # fingerprint in the absence of full FW matches:
+ def test_platform_code_ecus_available(self, subtests):
+ # TODO: add queries for these non-CAN FD cars to get EPS
+ no_eps_platforms = CANFD_CAR | {CAR.KIA_SORENTO, CAR.KIA_OPTIMA_G4, CAR.KIA_OPTIMA_G4_FL, CAR.KIA_OPTIMA_H,
+ CAR.KIA_OPTIMA_H_G4_FL, CAR.HYUNDAI_SONATA_LF, CAR.HYUNDAI_TUCSON, CAR.GENESIS_G90, CAR.GENESIS_G80, CAR.HYUNDAI_ELANTRA}
+
+ # 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):
+ for platform_code_ecu in PLATFORM_CODE_ECUS:
+ if platform_code_ecu in (Ecu.fwdRadar, Ecu.eps) and car_model == CAR.HYUNDAI_GENESIS:
+ continue
+ if platform_code_ecu == Ecu.eps and car_model in no_eps_platforms:
+ continue
+ assert platform_code_ecu in [e[0] for e in ecus]
+
+ def test_fw_format(self, subtests):
+ # Asserts:
+ # - every supported ECU FW version returns one platform code
+ # - every supported ECU FW version has a part number
+ # - expected parsing of ECU FW dates
+
+ for car_model, ecus in FW_VERSIONS.items():
+ with subtests.test(car_model=car_model.value):
+ for ecu, fws in ecus.items():
+ if ecu[0] not in PLATFORM_CODE_ECUS:
+ continue
+
+ codes = set()
+ for fw in fws:
+ result = get_platform_codes([fw])
+ assert 1 == len(result), f"Unable to parse FW: {fw}"
+ codes |= result
+
+ if ecu[0] not in DATE_FW_ECUS or car_model in NO_DATES_PLATFORMS:
+ assert all(date is None for _, date in codes)
+ else:
+ assert all(date is not None for _, date in codes)
+
+ if car_model == CAR.HYUNDAI_GENESIS:
+ pytest.skip("No part numbers for car model")
+
+ # Hyundai places the ECU part number in their FW versions, assert all parsable
+ # Some examples of valid formats: b"56310-L0010", b"56310L0010", b"56310/M6300"
+ assert all(b"-" in code for code, _ in codes), \
+ f"FW does not have part number: {fw}"
+
+ def test_platform_codes_spot_check(self):
+ # Asserts basic platform code parsing behavior for a few cases
+ results = get_platform_codes([b"\xf1\x00DH LKAS 1.1 -150210"])
+ assert results == {(b"DH", b"150210")}
+
+ # Some cameras and all radars do not have dates
+ results = get_platform_codes([b"\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 "])
+ assert results == {(b"AEhe-G2000", None)}
+
+ results = get_platform_codes([b"\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 "])
+ assert results == {(b"CV1-CV000", None)}
+
+ results = get_platform_codes([
+ b"\xf1\x00DH LKAS 1.1 -150210",
+ b"\xf1\x00AEhe SCC H-CUP 1.01 1.01 96400-G2000 ",
+ b"\xf1\x00CV1_ RDR ----- 1.00 1.01 99110-CV000 ",
+ ])
+ assert results == {(b"DH", b"150210"), (b"AEhe-G2000", None), (b"CV1-CV000", None)}
+
+ results = get_platform_codes([
+ b"\xf1\x00LX2 MFC AT USA LHD 1.00 1.07 99211-S8100 220222",
+ b"\xf1\x00LX2 MFC AT USA LHD 1.00 1.08 99211-S8100 211103",
+ b"\xf1\x00ON MFC AT USA LHD 1.00 1.01 99211-S9100 190405",
+ b"\xf1\x00ON MFC AT USA LHD 1.00 1.03 99211-S9100 190720",
+ ])
+ assert results == {(b"LX2-S8100", b"220222"), (b"LX2-S8100", b"211103"),
+ (b"ON-S9100", b"190405"), (b"ON-S9100", b"190720")}
+
+ def test_fuzzy_excluded_platforms(self):
+ # Asserts a list of platforms that will not fuzzy fingerprint with platform codes due to them being shared.
+ # This list can be shrunk as we combine platforms and detect features
+ excluded_platforms = {
+ CAR.GENESIS_G70, # shared platform code, part number, and date
+ CAR.GENESIS_G70_2020,
+ }
+ excluded_platforms |= CANFD_CAR - EV_CAR - CANFD_FUZZY_WHITELIST # shared platform codes
+ excluded_platforms |= NO_DATES_PLATFORMS # date codes are required to match
+
+ platforms_with_shared_codes = set()
+ for platform, fw_by_addr in FW_VERSIONS.items():
+ car_fw = []
+ for ecu, fw_versions in fw_by_addr.items():
+ ecu_name, addr, sub_addr = ecu
+ for fw in fw_versions:
+ car_fw.append(CarParams.CarFw(ecu=ecu_name, fwVersion=fw, address=addr,
+ subAddress=0 if sub_addr is None else sub_addr))
+
+ CP = CarParams(carFw=car_fw)
+ matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
+ if len(matches) == 1:
+ assert list(matches)[0] == platform
+ else:
+ platforms_with_shared_codes.add(platform)
+
+ assert platforms_with_shared_codes == excluded_platforms
diff --git a/opendbc/car/hyundai/values.py b/opendbc/car/hyundai/values.py
new file mode 100644
index 0000000000..2394708de2
--- /dev/null
+++ b/opendbc/car/hyundai/values.py
@@ -0,0 +1,752 @@
+import re
+from dataclasses import dataclass, field
+from enum import Enum, IntFlag
+
+from panda import uds
+from opendbc.car import CarSpecs, DbcDict, PlatformConfig, Platforms, dbc_dict
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, p16
+
+Ecu = CarParams.Ecu
+
+
+class CarControllerParams:
+ ACCEL_MIN = -3.5 # m/s
+ ACCEL_MAX = 2.0 # m/s
+
+ def __init__(self, CP):
+ self.STEER_DELTA_UP = 3
+ self.STEER_DELTA_DOWN = 7
+ self.STEER_DRIVER_ALLOWANCE = 50
+ self.STEER_DRIVER_MULTIPLIER = 2
+ self.STEER_DRIVER_FACTOR = 1
+ self.STEER_THRESHOLD = 150
+ self.STEER_STEP = 1 # 100 Hz
+
+ if CP.carFingerprint in CANFD_CAR:
+ self.STEER_MAX = 270
+ self.STEER_DRIVER_ALLOWANCE = 250
+ self.STEER_DRIVER_MULTIPLIER = 2
+ self.STEER_THRESHOLD = 250
+ self.STEER_DELTA_UP = 2
+ self.STEER_DELTA_DOWN = 3
+
+ # To determine the limit for your car, find the maximum value that the stock LKAS will request.
+ # If the max stock LKAS request is <384, add your car to this list.
+ elif CP.carFingerprint in (CAR.GENESIS_G80, CAR.GENESIS_G90, CAR.HYUNDAI_ELANTRA, CAR.HYUNDAI_ELANTRA_GT_I30, CAR.HYUNDAI_IONIQ,
+ CAR.HYUNDAI_IONIQ_EV_LTD, CAR.HYUNDAI_SANTA_FE_PHEV_2022, CAR.HYUNDAI_SONATA_LF, CAR.KIA_FORTE, CAR.KIA_NIRO_PHEV,
+ CAR.KIA_OPTIMA_H, CAR.KIA_OPTIMA_H_G4_FL, CAR.KIA_SORENTO):
+ self.STEER_MAX = 255
+
+ # these cars have significantly more torque than most HKG; limit to 70% of max
+ elif CP.flags & HyundaiFlags.ALT_LIMITS:
+ self.STEER_MAX = 270
+ self.STEER_DELTA_UP = 2
+ self.STEER_DELTA_DOWN = 3
+
+ # Default for most HKG
+ else:
+ self.STEER_MAX = 384
+
+
+class HyundaiFlags(IntFlag):
+ # Dynamic Flags
+ CANFD_HDA2 = 1
+ CANFD_ALT_BUTTONS = 2
+ CANFD_ALT_GEARS = 2 ** 2
+ CANFD_CAMERA_SCC = 2 ** 3
+
+ ALT_LIMITS = 2 ** 4
+ ENABLE_BLINKERS = 2 ** 5
+ CANFD_ALT_GEARS_2 = 2 ** 6
+ SEND_LFA = 2 ** 7
+ USE_FCA = 2 ** 8
+ CANFD_HDA2_ALT_STEERING = 2 ** 9
+
+ # these cars use a different gas signal
+ HYBRID = 2 ** 10
+ EV = 2 ** 11
+
+ # Static flags
+
+ # If 0x500 is present on bus 1 it probably has a Mando radar outputting radar points.
+ # If no points are outputted by default it might be possible to turn it on using selfdrive/debug/hyundai_enable_radar_points.py
+ MANDO_RADAR = 2 ** 12
+ CANFD = 2 ** 13
+
+ # The radar does SCC on these cars when HDA I, rather than the camera
+ RADAR_SCC = 2 ** 14
+ CAMERA_SCC = 2 ** 15
+ CHECKSUM_CRC8 = 2 ** 16
+ CHECKSUM_6B = 2 ** 17
+
+ # these cars require a special panda safety mode due to missing counters and checksums in the messages
+ LEGACY = 2 ** 18
+
+ # these cars have not been verified to work with longitudinal yet - radar disable, sending correct messages, etc.
+ UNSUPPORTED_LONGITUDINAL = 2 ** 19
+
+ CANFD_NO_RADAR_DISABLE = 2 ** 20
+
+ CLUSTER_GEARS = 2 ** 21
+ TCU_GEARS = 2 ** 22
+
+ MIN_STEER_32_MPH = 2 ** 23
+
+
+class Footnote(Enum):
+ CANFD = CarFootnote(
+ "Requires a CAN FD panda kit if not using " +
+ "comma 3X for this CAN FD car.",
+ Column.MODEL, shop_footnote=False)
+
+
+@dataclass
+class HyundaiCarDocs(CarDocs):
+ package: str = "Smart Cruise Control (SCC)"
+
+ def init_make(self, CP: CarParams):
+ if CP.flags & HyundaiFlags.CANFD:
+ self.footnotes.insert(0, Footnote.CANFD)
+
+
+@dataclass
+class HyundaiPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict("hyundai_kia_generic", None))
+
+ def init(self):
+ if self.flags & HyundaiFlags.MANDO_RADAR:
+ self.dbc_dict = dbc_dict('hyundai_kia_generic', 'hyundai_kia_mando_front_radar_generated')
+
+ if self.flags & HyundaiFlags.MIN_STEER_32_MPH:
+ self.specs = self.specs.override(minSteerSpeed=32 * CV.MPH_TO_MS)
+
+
+@dataclass
+class HyundaiCanFDPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict("hyundai_canfd", None))
+
+ def init(self):
+ self.flags |= HyundaiFlags.CANFD
+
+
+class CAR(Platforms):
+ # Hyundai
+ HYUNDAI_AZERA_6TH_GEN = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Azera 2022", "All", car_parts=CarParts.common([CarHarness.hyundai_k]))],
+ CarSpecs(mass=1600, wheelbase=2.885, steerRatio=14.5),
+ )
+ HYUNDAI_AZERA_HEV_6TH_GEN = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Hyundai Azera Hybrid 2019", "All", car_parts=CarParts.common([CarHarness.hyundai_c])),
+ HyundaiCarDocs("Hyundai Azera Hybrid 2020", "All", car_parts=CarParts.common([CarHarness.hyundai_k])),
+ ],
+ CarSpecs(mass=1675, wheelbase=2.885, steerRatio=14.5),
+ flags=HyundaiFlags.HYBRID,
+ )
+ HYUNDAI_ELANTRA = HyundaiPlatformConfig(
+ [
+ # TODO: 2017-18 could be Hyundai G
+ HyundaiCarDocs("Hyundai Elantra 2017-18", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_b])),
+ HyundaiCarDocs("Hyundai Elantra 2019", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_g])),
+ ],
+ # steerRatio: 14 is Stock | Settled Params Learner values are steerRatio: 15.401566348670535, stiffnessFactor settled on 1.0081302973865127
+ CarSpecs(mass=1275, wheelbase=2.7, steerRatio=15.4, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.LEGACY | HyundaiFlags.CLUSTER_GEARS | HyundaiFlags.MIN_STEER_32_MPH,
+ )
+ HYUNDAI_ELANTRA_GT_I30 = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Hyundai Elantra GT 2017-19", car_parts=CarParts.common([CarHarness.hyundai_e])),
+ HyundaiCarDocs("Hyundai i30 2017-19", car_parts=CarParts.common([CarHarness.hyundai_e])),
+ ],
+ HYUNDAI_ELANTRA.specs,
+ flags=HyundaiFlags.LEGACY | HyundaiFlags.CLUSTER_GEARS | HyundaiFlags.MIN_STEER_32_MPH,
+ )
+ HYUNDAI_ELANTRA_2021 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Elantra 2021-23", video_link="https://youtu.be/_EdYQtV52-c", car_parts=CarParts.common([CarHarness.hyundai_k]))],
+ CarSpecs(mass=2800 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=12.9, tireStiffnessFactor=0.65),
+ flags=HyundaiFlags.CHECKSUM_CRC8,
+ )
+ HYUNDAI_ELANTRA_HEV_2021 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Elantra Hybrid 2021-23", video_link="https://youtu.be/_EdYQtV52-c",
+ car_parts=CarParts.common([CarHarness.hyundai_k]))],
+ CarSpecs(mass=3017 * CV.LB_TO_KG, wheelbase=2.72, steerRatio=12.9, tireStiffnessFactor=0.65),
+ flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID,
+ )
+ HYUNDAI_GENESIS = HyundaiPlatformConfig(
+ [
+ # TODO: check 2015 packages
+ HyundaiCarDocs("Hyundai Genesis 2015-16", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_j])),
+ HyundaiCarDocs("Genesis G80 2017", "All", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_j])),
+ ],
+ CarSpecs(mass=2060, wheelbase=3.01, steerRatio=16.5, minSteerSpeed=60 * CV.KPH_TO_MS),
+ flags=HyundaiFlags.CHECKSUM_6B | HyundaiFlags.LEGACY,
+ )
+ HYUNDAI_IONIQ = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Ioniq Hybrid 2017-19", car_parts=CarParts.common([CarHarness.hyundai_c]))],
+ CarSpecs(mass=1490, wheelbase=2.7, steerRatio=13.73, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.HYBRID | HyundaiFlags.MIN_STEER_32_MPH,
+ )
+ HYUNDAI_IONIQ_HEV_2022 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Ioniq Hybrid 2020-22", car_parts=CarParts.common([CarHarness.hyundai_h]))], # TODO: confirm 2020-21 harness,
+ CarSpecs(mass=1490, wheelbase=2.7, steerRatio=13.73, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.HYBRID | HyundaiFlags.LEGACY,
+ )
+ HYUNDAI_IONIQ_EV_LTD = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Ioniq Electric 2019", car_parts=CarParts.common([CarHarness.hyundai_c]))],
+ CarSpecs(mass=1490, wheelbase=2.7, steerRatio=13.73, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.EV | HyundaiFlags.LEGACY | HyundaiFlags.MIN_STEER_32_MPH,
+ )
+ HYUNDAI_IONIQ_EV_2020 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Ioniq Electric 2020", "All", car_parts=CarParts.common([CarHarness.hyundai_h]))],
+ CarSpecs(mass=1490, wheelbase=2.7, steerRatio=13.73, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.EV,
+ )
+ HYUNDAI_IONIQ_PHEV_2019 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Ioniq Plug-in Hybrid 2019", car_parts=CarParts.common([CarHarness.hyundai_c]))],
+ CarSpecs(mass=1490, wheelbase=2.7, steerRatio=13.73, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.HYBRID | HyundaiFlags.MIN_STEER_32_MPH,
+ )
+ HYUNDAI_IONIQ_PHEV = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Ioniq Plug-in Hybrid 2020-22", "All", car_parts=CarParts.common([CarHarness.hyundai_h]))],
+ CarSpecs(mass=1490, wheelbase=2.7, steerRatio=13.73, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.HYBRID,
+ )
+ HYUNDAI_KONA = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Kona 2020", min_enable_speed=6 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_b]))],
+ CarSpecs(mass=1275, wheelbase=2.6, steerRatio=13.42, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.CLUSTER_GEARS,
+ )
+ HYUNDAI_KONA_EV = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Kona Electric 2018-21", car_parts=CarParts.common([CarHarness.hyundai_g]))],
+ CarSpecs(mass=1685, wheelbase=2.6, steerRatio=13.42, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.EV,
+ )
+ HYUNDAI_KONA_EV_2022 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Kona Electric 2022-23", car_parts=CarParts.common([CarHarness.hyundai_o]))],
+ CarSpecs(mass=1743, wheelbase=2.6, steerRatio=13.42, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.CAMERA_SCC | HyundaiFlags.EV,
+ )
+ HYUNDAI_KONA_EV_2ND_GEN = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Hyundai Kona Electric (with HDA II, Korea only) 2023", video_link="https://www.youtube.com/watch?v=U2fOCmcQ8hw",
+ car_parts=CarParts.common([CarHarness.hyundai_r]))],
+ CarSpecs(mass=1740, wheelbase=2.66, steerRatio=13.6, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.EV | HyundaiFlags.CANFD_NO_RADAR_DISABLE,
+ )
+ HYUNDAI_KONA_HEV = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Kona Hybrid 2020", car_parts=CarParts.common([CarHarness.hyundai_i]))], # TODO: check packages,
+ CarSpecs(mass=1425, wheelbase=2.6, steerRatio=13.42, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.HYBRID,
+ )
+ HYUNDAI_SANTA_FE = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Santa Fe 2019-20", "All", video_link="https://youtu.be/bjDR0YjM__s",
+ car_parts=CarParts.common([CarHarness.hyundai_d]))],
+ CarSpecs(mass=3982 * CV.LB_TO_KG, wheelbase=2.766, steerRatio=16.55, tireStiffnessFactor=0.82),
+ flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.CHECKSUM_CRC8,
+ )
+ HYUNDAI_SANTA_FE_2022 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Santa Fe 2021-23", "All", video_link="https://youtu.be/VnHzSTygTS4",
+ car_parts=CarParts.common([CarHarness.hyundai_l]))],
+ HYUNDAI_SANTA_FE.specs,
+ flags=HyundaiFlags.CHECKSUM_CRC8,
+ )
+ HYUNDAI_SANTA_FE_HEV_2022 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Santa Fe Hybrid 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l]))],
+ HYUNDAI_SANTA_FE.specs,
+ flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID,
+ )
+ HYUNDAI_SANTA_FE_PHEV_2022 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Santa Fe Plug-in Hybrid 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l]))],
+ HYUNDAI_SANTA_FE.specs,
+ flags=HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID,
+ )
+ HYUNDAI_SONATA = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Sonata 2020-23", "All", video_link="https://www.youtube.com/watch?v=ix63r9kE3Fw",
+ car_parts=CarParts.common([CarHarness.hyundai_a]))],
+ CarSpecs(mass=1513, wheelbase=2.84, steerRatio=13.27 * 1.15, tireStiffnessFactor=0.65), # 15% higher at the center seems reasonable
+ flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.CHECKSUM_CRC8,
+ )
+ HYUNDAI_SONATA_LF = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Sonata 2018-19", car_parts=CarParts.common([CarHarness.hyundai_e]))],
+ CarSpecs(mass=1536, wheelbase=2.804, steerRatio=13.27 * 1.15), # 15% higher at the center seems reasonable
+
+ flags=HyundaiFlags.UNSUPPORTED_LONGITUDINAL | HyundaiFlags.TCU_GEARS,
+ )
+ HYUNDAI_STARIA_4TH_GEN = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Hyundai Staria 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_k]))],
+ CarSpecs(mass=2205, wheelbase=3.273, steerRatio=11.94), # https://www.hyundai.com/content/dam/hyundai/au/en/models/staria-load/premium-pip-update-2023/spec-sheet/STARIA_Load_Spec-Table_March_2023_v3.1.pdf
+ )
+ HYUNDAI_TUCSON = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Hyundai Tucson 2021", min_enable_speed=19 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_l])),
+ HyundaiCarDocs("Hyundai Tucson Diesel 2019", car_parts=CarParts.common([CarHarness.hyundai_l])),
+ ],
+ CarSpecs(mass=3520 * CV.LB_TO_KG, wheelbase=2.67, steerRatio=16.1, tireStiffnessFactor=0.385),
+ flags=HyundaiFlags.TCU_GEARS,
+ )
+ HYUNDAI_PALISADE = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Hyundai Palisade 2020-22", "All", video_link="https://youtu.be/TAnDqjF4fDY?t=456", car_parts=CarParts.common([CarHarness.hyundai_h])),
+ HyundaiCarDocs("Kia Telluride 2020-22", "All", car_parts=CarParts.common([CarHarness.hyundai_h])),
+ ],
+ CarSpecs(mass=1999, wheelbase=2.9, steerRatio=15.6 * 1.15, tireStiffnessFactor=0.63),
+ flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.CHECKSUM_CRC8,
+ )
+ HYUNDAI_VELOSTER = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_e]))],
+ CarSpecs(mass=2917 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75 * 1.15, tireStiffnessFactor=0.5),
+ flags=HyundaiFlags.LEGACY | HyundaiFlags.TCU_GEARS,
+ )
+ HYUNDAI_SONATA_HYBRID = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Sonata Hybrid 2020-23", "All", car_parts=CarParts.common([CarHarness.hyundai_a]))],
+ HYUNDAI_SONATA.specs,
+ flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID,
+ )
+ HYUNDAI_IONIQ_5 = HyundaiCanFDPlatformConfig(
+ [
+ HyundaiCarDocs("Hyundai Ioniq 5 (Non-US only) 2022-24", "All", car_parts=CarParts.common([CarHarness.hyundai_q])),
+ HyundaiCarDocs("Hyundai Ioniq 5 (without HDA II) 2022-24", "Highway Driving Assist", car_parts=CarParts.common([CarHarness.hyundai_k])),
+ HyundaiCarDocs("Hyundai Ioniq 5 (with HDA II) 2022-24", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_q])),
+ ],
+ CarSpecs(mass=1948, wheelbase=2.97, steerRatio=14.26, tireStiffnessFactor=0.65),
+ flags=HyundaiFlags.EV,
+ )
+ HYUNDAI_IONIQ_6 = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Hyundai Ioniq 6 (with HDA II) 2023-24", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_p]))],
+ HYUNDAI_IONIQ_5.specs,
+ flags=HyundaiFlags.EV | HyundaiFlags.CANFD_NO_RADAR_DISABLE,
+ )
+ HYUNDAI_TUCSON_4TH_GEN = HyundaiCanFDPlatformConfig(
+ [
+ HyundaiCarDocs("Hyundai Tucson 2022", car_parts=CarParts.common([CarHarness.hyundai_n])),
+ HyundaiCarDocs("Hyundai Tucson 2023-24", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
+ HyundaiCarDocs("Hyundai Tucson Hybrid 2022-24", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
+ HyundaiCarDocs("Hyundai Tucson Plug-in Hybrid 2024", "All", car_parts=CarParts.common([CarHarness.hyundai_n])),
+ ],
+ CarSpecs(mass=1630, wheelbase=2.756, steerRatio=13.7, tireStiffnessFactor=0.385),
+ )
+ HYUNDAI_SANTA_CRUZ_1ST_GEN = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Hyundai Santa Cruz 2022-24", car_parts=CarParts.common([CarHarness.hyundai_n]))],
+ # weight from Limited trim - the only supported trim, steering ratio according to Hyundai News https://www.hyundainews.com/assets/documents/original/48035-2022SantaCruzProductGuideSpecsv2081521.pdf
+ CarSpecs(mass=1870, wheelbase=3, steerRatio=14.2),
+ )
+ HYUNDAI_CUSTIN_1ST_GEN = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Hyundai Custin 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_k]))],
+ CarSpecs(mass=1690, wheelbase=3.055, steerRatio=17), # mass: from https://www.hyundai-motor.com.tw/clicktobuy/custin#spec_0, steerRatio: from learner
+ flags=HyundaiFlags.CHECKSUM_CRC8,
+ )
+
+ # Kia
+ KIA_FORTE = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Forte 2019-21", min_enable_speed=6 * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_g])),
+ HyundaiCarDocs("Kia Forte 2022-23", car_parts=CarParts.common([CarHarness.hyundai_e])),
+ ],
+ CarSpecs(mass=2878 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75, tireStiffnessFactor=0.5)
+ )
+ KIA_K5_2021 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia K5 2021-24", car_parts=CarParts.common([CarHarness.hyundai_a]))],
+ CarSpecs(mass=3381 * CV.LB_TO_KG, wheelbase=2.85, steerRatio=13.27, tireStiffnessFactor=0.5), # 2021 Kia K5 Steering Ratio (all trims)
+ flags=HyundaiFlags.CHECKSUM_CRC8,
+ )
+ KIA_K5_HEV_2020 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia K5 Hybrid 2020-22", car_parts=CarParts.common([CarHarness.hyundai_a]))],
+ KIA_K5_2021.specs,
+ flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.CHECKSUM_CRC8 | HyundaiFlags.HYBRID,
+ )
+ KIA_K8_HEV_1ST_GEN = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Kia K8 Hybrid (with HDA II) 2023", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_q]))],
+ # mass: https://carprices.ae/brands/kia/2023/k8/1.6-turbo-hybrid, steerRatio: guesstimate from K5 platform
+ CarSpecs(mass=1630, wheelbase=2.895, steerRatio=13.27)
+ )
+ KIA_NIRO_EV = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Niro EV 2019", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", car_parts=CarParts.common([CarHarness.hyundai_h])),
+ HyundaiCarDocs("Kia Niro EV 2020", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", car_parts=CarParts.common([CarHarness.hyundai_f])),
+ HyundaiCarDocs("Kia Niro EV 2021", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", car_parts=CarParts.common([CarHarness.hyundai_c])),
+ HyundaiCarDocs("Kia Niro EV 2022", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", car_parts=CarParts.common([CarHarness.hyundai_h])),
+ ],
+ CarSpecs(mass=3543 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=13.6, tireStiffnessFactor=0.385), # average of all the cars
+ flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.EV,
+ )
+ KIA_NIRO_EV_2ND_GEN = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Kia Niro EV 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_a]))],
+ KIA_NIRO_EV.specs,
+ flags=HyundaiFlags.EV,
+ )
+ KIA_NIRO_PHEV = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Niro Hybrid 2018", "All", min_enable_speed=10. * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_c])),
+ HyundaiCarDocs("Kia Niro Plug-in Hybrid 2018-19", "All", min_enable_speed=10. * CV.MPH_TO_MS, car_parts=CarParts.common([CarHarness.hyundai_c])),
+ HyundaiCarDocs("Kia Niro Plug-in Hybrid 2020", car_parts=CarParts.common([CarHarness.hyundai_d])),
+ ],
+ KIA_NIRO_EV.specs,
+ flags=HyundaiFlags.MANDO_RADAR | HyundaiFlags.HYBRID | HyundaiFlags.UNSUPPORTED_LONGITUDINAL | HyundaiFlags.MIN_STEER_32_MPH,
+ )
+ KIA_NIRO_PHEV_2022 = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Niro Plug-in Hybrid 2021", car_parts=CarParts.common([CarHarness.hyundai_d])),
+ HyundaiCarDocs("Kia Niro Plug-in Hybrid 2022", car_parts=CarParts.common([CarHarness.hyundai_f])),
+ ],
+ KIA_NIRO_EV.specs,
+ flags=HyundaiFlags.HYBRID | HyundaiFlags.MANDO_RADAR,
+ )
+ KIA_NIRO_HEV_2021 = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Niro Hybrid 2021", car_parts=CarParts.common([CarHarness.hyundai_d])),
+ HyundaiCarDocs("Kia Niro Hybrid 2022", car_parts=CarParts.common([CarHarness.hyundai_f])),
+ ],
+ KIA_NIRO_EV.specs,
+ flags=HyundaiFlags.HYBRID,
+ )
+ KIA_NIRO_HEV_2ND_GEN = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Kia Niro Hybrid 2023", car_parts=CarParts.common([CarHarness.hyundai_a]))],
+ KIA_NIRO_EV.specs,
+ )
+ KIA_OPTIMA_G4 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia Optima 2017", "Advanced Smart Cruise Control",
+ car_parts=CarParts.common([CarHarness.hyundai_b]))], # TODO: may support 2016, 2018
+ CarSpecs(mass=3558 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75, tireStiffnessFactor=0.5),
+ flags=HyundaiFlags.LEGACY | HyundaiFlags.TCU_GEARS | HyundaiFlags.MIN_STEER_32_MPH,
+ )
+ KIA_OPTIMA_G4_FL = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia Optima 2019-20", car_parts=CarParts.common([CarHarness.hyundai_g]))],
+ CarSpecs(mass=3558 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75, tireStiffnessFactor=0.5),
+ flags=HyundaiFlags.UNSUPPORTED_LONGITUDINAL | HyundaiFlags.TCU_GEARS,
+ )
+ # TODO: may support adjacent years. may have a non-zero minimum steering speed
+ KIA_OPTIMA_H = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia Optima Hybrid 2017", "Advanced Smart Cruise Control", car_parts=CarParts.common([CarHarness.hyundai_c]))],
+ CarSpecs(mass=3558 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75, tireStiffnessFactor=0.5),
+ flags=HyundaiFlags.HYBRID | HyundaiFlags.LEGACY,
+ )
+ KIA_OPTIMA_H_G4_FL = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia Optima Hybrid 2019", car_parts=CarParts.common([CarHarness.hyundai_h]))],
+ CarSpecs(mass=3558 * CV.LB_TO_KG, wheelbase=2.8, steerRatio=13.75, tireStiffnessFactor=0.5),
+ flags=HyundaiFlags.HYBRID | HyundaiFlags.UNSUPPORTED_LONGITUDINAL,
+ )
+ KIA_SELTOS = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia Seltos 2021", car_parts=CarParts.common([CarHarness.hyundai_a]))],
+ CarSpecs(mass=1337, wheelbase=2.63, steerRatio=14.56),
+ flags=HyundaiFlags.CHECKSUM_CRC8,
+ )
+ KIA_SPORTAGE_5TH_GEN = HyundaiCanFDPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Sportage 2023-24", car_parts=CarParts.common([CarHarness.hyundai_n])),
+ HyundaiCarDocs("Kia Sportage Hybrid 2023", car_parts=CarParts.common([CarHarness.hyundai_n])),
+ ],
+ # weight from SX and above trims, average of FWD and AWD version, steering ratio according to Kia News https://www.kiamedia.com/us/en/models/sportage/2023/specifications
+ CarSpecs(mass=1725, wheelbase=2.756, steerRatio=13.6),
+ )
+ KIA_SORENTO = HyundaiPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Sorento 2018", "Advanced Smart Cruise Control & LKAS", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8",
+ car_parts=CarParts.common([CarHarness.hyundai_e])),
+ HyundaiCarDocs("Kia Sorento 2019", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", car_parts=CarParts.common([CarHarness.hyundai_e])),
+ ],
+ CarSpecs(mass=1985, wheelbase=2.78, steerRatio=14.4 * 1.1), # 10% higher at the center seems reasonable
+ flags=HyundaiFlags.CHECKSUM_6B | HyundaiFlags.UNSUPPORTED_LONGITUDINAL,
+ )
+ KIA_SORENTO_4TH_GEN = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Kia Sorento 2021-23", car_parts=CarParts.common([CarHarness.hyundai_k]))],
+ CarSpecs(mass=3957 * CV.LB_TO_KG, wheelbase=2.81, steerRatio=13.5), # average of the platforms
+ flags=HyundaiFlags.RADAR_SCC,
+ )
+ KIA_SORENTO_HEV_4TH_GEN = HyundaiCanFDPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Sorento Hybrid 2021-23", "All", car_parts=CarParts.common([CarHarness.hyundai_a])),
+ HyundaiCarDocs("Kia Sorento Plug-in Hybrid 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_a])),
+ ],
+ CarSpecs(mass=4395 * CV.LB_TO_KG, wheelbase=2.81, steerRatio=13.5), # average of the platforms
+ flags=HyundaiFlags.RADAR_SCC,
+ )
+ KIA_STINGER = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia Stinger 2018-20", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0",
+ car_parts=CarParts.common([CarHarness.hyundai_c]))],
+ CarSpecs(mass=1825, wheelbase=2.78, steerRatio=14.4 * 1.15) # 15% higher at the center seems reasonable
+ )
+ KIA_STINGER_2022 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia Stinger 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_k]))],
+ KIA_STINGER.specs,
+ )
+ KIA_CEED = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Kia Ceed 2019", car_parts=CarParts.common([CarHarness.hyundai_e]))],
+ CarSpecs(mass=1450, wheelbase=2.65, steerRatio=13.75, tireStiffnessFactor=0.5),
+ flags=HyundaiFlags.LEGACY,
+ )
+ KIA_EV6 = HyundaiCanFDPlatformConfig(
+ [
+ HyundaiCarDocs("Kia EV6 (Non-US only) 2022-24", "All", car_parts=CarParts.common([CarHarness.hyundai_p])),
+ HyundaiCarDocs("Kia EV6 (without HDA II) 2022-24", "Highway Driving Assist", car_parts=CarParts.common([CarHarness.hyundai_l])),
+ HyundaiCarDocs("Kia EV6 (with HDA II) 2022-24", "Highway Driving Assist II", car_parts=CarParts.common([CarHarness.hyundai_p]))
+ ],
+ CarSpecs(mass=2055, wheelbase=2.9, steerRatio=16, tireStiffnessFactor=0.65),
+ flags=HyundaiFlags.EV,
+ )
+ KIA_CARNIVAL_4TH_GEN = HyundaiCanFDPlatformConfig(
+ [
+ HyundaiCarDocs("Kia Carnival 2022-24", car_parts=CarParts.common([CarHarness.hyundai_a])),
+ HyundaiCarDocs("Kia Carnival (China only) 2023", car_parts=CarParts.common([CarHarness.hyundai_k]))
+ ],
+ CarSpecs(mass=2087, wheelbase=3.09, steerRatio=14.23),
+ flags=HyundaiFlags.RADAR_SCC,
+ )
+
+ # Genesis
+ GENESIS_GV60_EV_1ST_GEN = HyundaiCanFDPlatformConfig(
+ [
+ HyundaiCarDocs("Genesis GV60 (Advanced Trim) 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_a])),
+ HyundaiCarDocs("Genesis GV60 (Performance Trim) 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_k])),
+ ],
+ CarSpecs(mass=2205, wheelbase=2.9, steerRatio=12.6), # steerRatio: https://www.motor1.com/reviews/586376/2023-genesis-gv60-first-drive/#:~:text=Relative%20to%20the%20related%20Ioniq,5%2FEV6%27s%2014.3%3A1.
+ flags=HyundaiFlags.EV,
+ )
+ GENESIS_G70 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Genesis G70 2018", "All", car_parts=CarParts.common([CarHarness.hyundai_f]))],
+ CarSpecs(mass=1640, wheelbase=2.84, steerRatio=13.56),
+ flags=HyundaiFlags.LEGACY,
+ )
+ GENESIS_G70_2020 = HyundaiPlatformConfig(
+ [
+ # TODO: 2021 MY harness is unknown
+ HyundaiCarDocs("Genesis G70 2019-21", "All", car_parts=CarParts.common([CarHarness.hyundai_f])),
+ # TODO: From 3.3T Sport Advanced 2022 & Prestige 2023 Trim, 2.0T is unknown
+ HyundaiCarDocs("Genesis G70 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l])),
+ ],
+ GENESIS_G70.specs,
+ flags=HyundaiFlags.MANDO_RADAR,
+ )
+ GENESIS_GV70_1ST_GEN = HyundaiCanFDPlatformConfig(
+ [
+ # TODO: Hyundai P is likely the correct harness for HDA II for 2.5T (unsupported due to missing ADAS ECU, is that the radar?)
+ HyundaiCarDocs("Genesis GV70 (2.5T Trim, without HDA II) 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_l])),
+ HyundaiCarDocs("Genesis GV70 (3.5T Trim, without HDA II) 2022-23", "All", car_parts=CarParts.common([CarHarness.hyundai_m])),
+ ],
+ CarSpecs(mass=1950, wheelbase=2.87, steerRatio=14.6),
+ flags=HyundaiFlags.RADAR_SCC,
+ )
+ GENESIS_G80 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Genesis G80 2018-19", "All", car_parts=CarParts.common([CarHarness.hyundai_h]))],
+ CarSpecs(mass=2060, wheelbase=3.01, steerRatio=16.5),
+ flags=HyundaiFlags.LEGACY,
+ )
+ GENESIS_G90 = HyundaiPlatformConfig(
+ [HyundaiCarDocs("Genesis G90 2017-20", "All", car_parts=CarParts.common([CarHarness.hyundai_c]))],
+ CarSpecs(mass=2200, wheelbase=3.15, steerRatio=12.069),
+ )
+ GENESIS_GV80 = HyundaiCanFDPlatformConfig(
+ [HyundaiCarDocs("Genesis GV80 2023", "All", car_parts=CarParts.common([CarHarness.hyundai_m]))],
+ CarSpecs(mass=2258, wheelbase=2.95, steerRatio=14.14),
+ flags=HyundaiFlags.RADAR_SCC,
+ )
+
+
+class Buttons:
+ NONE = 0
+ RES_ACCEL = 1
+ SET_DECEL = 2
+ GAP_DIST = 3
+ CANCEL = 4 # on newer models, this is a pause/resume button
+
+
+def get_platform_codes(fw_versions: list[bytes]) -> set[tuple[bytes, bytes | None]]:
+ # Returns unique, platform-specific identification codes for a set of versions
+ codes = set() # (code-Optional[part], date)
+ for fw in fw_versions:
+ code_match = PLATFORM_CODE_FW_PATTERN.search(fw)
+ part_match = PART_NUMBER_FW_PATTERN.search(fw)
+ date_match = DATE_FW_PATTERN.search(fw)
+ if code_match is not None:
+ code: bytes = code_match.group()
+ part = part_match.group() if part_match else None
+ date = date_match.group() if date_match else None
+ if part is not None:
+ # part number starts with generic ECU part type, add what is specific to platform
+ code += b"-" + part[-5:]
+
+ codes.add((code, date))
+ return codes
+
+
+def match_fw_to_car_fuzzy(live_fw_versions, vin, offline_fw_versions) -> set[str]:
+ # Non-electric CAN FD platforms often do not have platform code specifiers needed
+ # to distinguish between hybrid and ICE. All EVs so far are either exclusively
+ # electric or specify electric in the platform code.
+ fuzzy_platform_blacklist = {str(c) for c in (CANFD_CAR - EV_CAR - CANFD_FUZZY_WHITELIST)}
+ candidates: set[str] = set()
+
+ for candidate, fws in offline_fw_versions.items():
+ # Keep track of ECUs which pass all checks (platform codes, within date range)
+ valid_found_ecus = set()
+ valid_expected_ecus = {ecu[1:] for ecu in fws if ecu[0] in PLATFORM_CODE_ECUS}
+ for ecu, expected_versions in fws.items():
+ addr = ecu[1:]
+ # Only check ECUs expected to have platform codes
+ if ecu[0] not in PLATFORM_CODE_ECUS:
+ continue
+
+ # Expected platform codes & dates
+ codes = get_platform_codes(expected_versions)
+ expected_platform_codes = {code for code, _ in codes}
+ expected_dates = {date for _, date in codes if date is not None}
+
+ # Found platform codes & dates
+ codes = get_platform_codes(live_fw_versions.get(addr, set()))
+ found_platform_codes = {code for code, _ in codes}
+ found_dates = {date for _, date in codes if date is not None}
+
+ # Check platform code + part number matches for any found versions
+ if not any(found_platform_code in expected_platform_codes for found_platform_code in found_platform_codes):
+ break
+
+ if ecu[0] in DATE_FW_ECUS:
+ # If ECU can have a FW date, require it to exist
+ # (this excludes candidates in the database without dates)
+ if not len(expected_dates) or not len(found_dates):
+ break
+
+ # Check any date within range in the database, format is %y%m%d
+ if not any(min(expected_dates) <= found_date <= max(expected_dates) for found_date in found_dates):
+ break
+
+ valid_found_ecus.add(addr)
+
+ # If all live ECUs pass all checks for candidate, add it as a match
+ if valid_expected_ecus.issubset(valid_found_ecus):
+ candidates.add(candidate)
+
+ return candidates - fuzzy_platform_blacklist
+
+
+HYUNDAI_VERSION_REQUEST_LONG = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(0xf100) # Long description
+
+HYUNDAI_VERSION_REQUEST_ALT = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(0xf110) # Alt long description
+
+HYUNDAI_ECU_MANUFACTURING_DATE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.ECU_MANUFACTURING_DATE)
+
+HYUNDAI_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40])
+
+# Regex patterns for parsing platform code, FW date, and part number from FW versions
+PLATFORM_CODE_FW_PATTERN = re.compile(b'((?<=' + HYUNDAI_VERSION_REQUEST_LONG[1:] +
+ b')[A-Z]{2}[A-Za-z0-9]{0,2})')
+DATE_FW_PATTERN = re.compile(b'(?<=[ -])([0-9]{6}$)')
+PART_NUMBER_FW_PATTERN = re.compile(b'(?<=[0-9][.,][0-9]{2} )([0-9]{5}[-/]?[A-Z][A-Z0-9]{3}[0-9])')
+
+# We've seen both ICE and hybrid for these platforms, and they have hybrid descriptors (e.g. MQ4 vs MQ4H)
+CANFD_FUZZY_WHITELIST = {CAR.KIA_SORENTO_4TH_GEN, CAR.KIA_SORENTO_HEV_4TH_GEN, CAR.KIA_K8_HEV_1ST_GEN,
+ # TODO: the hybrid variant is not out yet
+ CAR.KIA_CARNIVAL_4TH_GEN}
+
+# List of ECUs expected to have platform codes, camera and radar should exist on all cars
+# TODO: use abs, it has the platform code and part number on many platforms
+PLATFORM_CODE_ECUS = [Ecu.fwdRadar, Ecu.fwdCamera, Ecu.eps]
+# So far we've only seen dates in fwdCamera
+# TODO: there are date codes in the ABS firmware versions in hex
+DATE_FW_ECUS = [Ecu.fwdCamera]
+
+# Note: an ECU on CAN FD cars may sometimes send 0x30080aaaaaaaaaaa (flow control continue) while we
+# are attempting to query ECUs. This currently does not seem to affect fingerprinting from the camera
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[
+ # TODO: add back whitelists
+ # CAN queries (OBD-II port)
+ Request(
+ [HYUNDAI_VERSION_REQUEST_LONG],
+ [HYUNDAI_VERSION_RESPONSE],
+ ),
+
+ # CAN & CAN-FD queries (from camera)
+ Request(
+ [HYUNDAI_VERSION_REQUEST_LONG],
+ [HYUNDAI_VERSION_RESPONSE],
+ bus=0,
+ auxiliary=True,
+ ),
+ Request(
+ [HYUNDAI_VERSION_REQUEST_LONG],
+ [HYUNDAI_VERSION_RESPONSE],
+ bus=1,
+ auxiliary=True,
+ obd_multiplexing=False,
+ ),
+
+ # CAN & CAN FD query to understand the three digit date code
+ # HDA2 cars usually use 6 digit date codes, so skip bus 1
+ Request(
+ [HYUNDAI_ECU_MANUFACTURING_DATE],
+ [HYUNDAI_VERSION_RESPONSE],
+ bus=0,
+ auxiliary=True,
+ logging=True,
+ ),
+
+ # CAN-FD alt request logging queries for hvac and parkingAdas
+ Request(
+ [HYUNDAI_VERSION_REQUEST_ALT],
+ [HYUNDAI_VERSION_RESPONSE],
+ bus=0,
+ auxiliary=True,
+ logging=True,
+ ),
+ Request(
+ [HYUNDAI_VERSION_REQUEST_ALT],
+ [HYUNDAI_VERSION_RESPONSE],
+ bus=1,
+ auxiliary=True,
+ logging=True,
+ obd_multiplexing=False,
+ ),
+ ],
+ # We lose these ECUs without the comma power on these cars.
+ # Note that we still attempt to match with them when they are present
+ non_essential_ecus={
+ Ecu.abs: [CAR.HYUNDAI_PALISADE, CAR.HYUNDAI_SONATA, CAR.HYUNDAI_SANTA_FE_2022, CAR.KIA_K5_2021, CAR.HYUNDAI_ELANTRA_2021,
+ CAR.HYUNDAI_SANTA_FE, CAR.HYUNDAI_KONA_EV_2022, CAR.HYUNDAI_KONA_EV, CAR.HYUNDAI_CUSTIN_1ST_GEN, CAR.KIA_SORENTO,
+ CAR.KIA_CEED, CAR.KIA_SELTOS],
+ },
+ extra_ecus=[
+ (Ecu.adas, 0x730, None), # ADAS Driving ECU on HDA2 platforms
+ (Ecu.parkingAdas, 0x7b1, None), # ADAS Parking ECU (may exist on all platforms)
+ (Ecu.hvac, 0x7b3, None), # HVAC Control Assembly
+ (Ecu.cornerRadar, 0x7b7, None),
+ (Ecu.combinationMeter, 0x7c6, None), # CAN FD Instrument cluster
+ ],
+ # Custom fuzzy fingerprinting function using platform codes, part numbers + FW dates:
+ match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
+)
+
+CHECKSUM = {
+ "crc8": CAR.with_flags(HyundaiFlags.CHECKSUM_CRC8),
+ "6B": CAR.with_flags(HyundaiFlags.CHECKSUM_6B),
+}
+
+CAN_GEARS = {
+ # which message has the gear. hybrid and EV use ELECT_GEAR
+ "use_cluster_gears": CAR.with_flags(HyundaiFlags.CLUSTER_GEARS),
+ "use_tcu_gears": CAR.with_flags(HyundaiFlags.TCU_GEARS),
+}
+
+CANFD_CAR = CAR.with_flags(HyundaiFlags.CANFD)
+CANFD_RADAR_SCC_CAR = CAR.with_flags(HyundaiFlags.RADAR_SCC)
+
+# These CAN FD cars do not accept communication control to disable the ADAS ECU,
+# responds with 0x7F2822 - 'conditions not correct'
+CANFD_UNSUPPORTED_LONGITUDINAL_CAR = CAR.with_flags(HyundaiFlags.CANFD_NO_RADAR_DISABLE)
+
+# The camera does SCC on these cars, rather than the radar
+CAMERA_SCC_CAR = CAR.with_flags(HyundaiFlags.CAMERA_SCC)
+
+HYBRID_CAR = CAR.with_flags(HyundaiFlags.HYBRID)
+
+EV_CAR = CAR.with_flags(HyundaiFlags.EV)
+
+LEGACY_SAFETY_MODE_CAR = CAR.with_flags(HyundaiFlags.LEGACY)
+
+UNSUPPORTED_LONGITUDINAL_CAR = CAR.with_flags(HyundaiFlags.LEGACY) | CAR.with_flags(HyundaiFlags.UNSUPPORTED_LONGITUDINAL)
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/interfaces.py b/opendbc/car/interfaces.py
new file mode 100644
index 0000000000..64b648f1cf
--- /dev/null
+++ b/opendbc/car/interfaces.py
@@ -0,0 +1,454 @@
+import json
+import os
+import numpy as np
+import time
+import tomllib
+from abc import abstractmethod, ABC
+from enum import StrEnum
+from typing import Any, NamedTuple
+from collections.abc import Callable
+from functools import cache
+
+from opendbc.car import DT_CTRL, apply_hysteresis, gen_empty_fingerprint, scale_rot_inertia, scale_tire_stiffness, get_friction, STD_CARGO_KG
+from opendbc.car import structs
+from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
+from opendbc.car.common.basedir import BASEDIR
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.common.simple_kalman import KF1D, get_kalman_gain
+from opendbc.car.common.numpy_fast import clip
+from opendbc.car.values import PLATFORMS
+
+GearShifter = structs.CarState.GearShifter
+
+V_CRUISE_MAX = 145
+MAX_CTRL_SPEED = (V_CRUISE_MAX + 4) * CV.KPH_TO_MS
+ACCEL_MAX = 2.0
+ACCEL_MIN = -3.5
+FRICTION_THRESHOLD = 0.3
+
+TORQUE_PARAMS_PATH = os.path.join(BASEDIR, 'torque_data/params.toml')
+TORQUE_OVERRIDE_PATH = os.path.join(BASEDIR, 'torque_data/override.toml')
+TORQUE_SUBSTITUTE_PATH = os.path.join(BASEDIR, 'torque_data/substitute.toml')
+
+GEAR_SHIFTER_MAP: dict[str, GearShifter] = {
+ 'P': GearShifter.park, 'PARK': GearShifter.park,
+ 'R': GearShifter.reverse, 'REVERSE': GearShifter.reverse,
+ 'N': GearShifter.neutral, 'NEUTRAL': GearShifter.neutral,
+ 'E': GearShifter.eco, 'ECO': GearShifter.eco,
+ 'T': GearShifter.manumatic, 'MANUAL': GearShifter.manumatic,
+ 'D': GearShifter.drive, 'DRIVE': GearShifter.drive,
+ 'S': GearShifter.sport, 'SPORT': GearShifter.sport,
+ 'L': GearShifter.low, 'LOW': GearShifter.low,
+ 'B': GearShifter.brake, 'BRAKE': GearShifter.brake,
+}
+
+
+class LatControlInputs(NamedTuple):
+ lateral_acceleration: float
+ roll_compensation: float
+ vego: float
+ aego: float
+
+
+TorqueFromLateralAccelCallbackType = Callable[[LatControlInputs, structs.CarParams.LateralTorqueTuning, float, float, bool, bool], float]
+
+
+@cache
+def get_torque_params():
+ with open(TORQUE_SUBSTITUTE_PATH, 'rb') as f:
+ sub = tomllib.load(f)
+ with open(TORQUE_PARAMS_PATH, 'rb') as f:
+ params = tomllib.load(f)
+ with open(TORQUE_OVERRIDE_PATH, 'rb') as f:
+ override = tomllib.load(f)
+
+ torque_params = {}
+ for candidate in (sub.keys() | params.keys() | override.keys()) - {'legend'}:
+ if sum([candidate in x for x in [sub, params, override]]) > 1:
+ raise RuntimeError(f'{candidate} is defined twice in torque config')
+
+ sub_candidate = sub.get(candidate, candidate)
+
+ if sub_candidate in override:
+ out = override[sub_candidate]
+ elif sub_candidate in params:
+ out = params[sub_candidate]
+ else:
+ raise NotImplementedError(f"Did not find torque params for {sub_candidate}")
+
+ torque_params[sub_candidate] = {key: out[i] for i, key in enumerate(params['legend'])}
+ if candidate in sub:
+ torque_params[candidate] = torque_params[sub_candidate]
+
+ return torque_params
+
+# generic car and radar interfaces
+
+class CarInterfaceBase(ABC):
+ def __init__(self, CP: structs.CarParams, CarController, CarState):
+ self.CP = CP
+
+ self.frame = 0
+ self.v_ego_cluster_seen = False
+
+ self.CS: CarStateBase = CarState(CP)
+ self.cp = self.CS.get_can_parser(CP)
+ self.cp_cam = self.CS.get_cam_can_parser(CP)
+ self.cp_adas = self.CS.get_adas_can_parser(CP)
+ self.cp_body = self.CS.get_body_can_parser(CP)
+ self.cp_loopback = self.CS.get_loopback_can_parser(CP)
+ self.can_parsers = (self.cp, self.cp_cam, self.cp_adas, self.cp_body, self.cp_loopback)
+
+ dbc_name = "" if self.cp is None else self.cp.dbc_name
+ self.CC: CarControllerBase = CarController(dbc_name, CP)
+
+ def apply(self, c: structs.CarControl, now_nanos: int | None = None) -> tuple[structs.CarControl.Actuators, list[CanData]]:
+ if now_nanos is None:
+ now_nanos = int(time.monotonic() * 1e9)
+ return self.CC.update(c, self.CS, now_nanos)
+
+ @staticmethod
+ def get_pid_accel_limits(CP, current_speed, cruise_speed):
+ return ACCEL_MIN, ACCEL_MAX
+
+ @classmethod
+ def get_non_essential_params(cls, candidate: str) -> structs.CarParams:
+ """
+ Parameters essential to controlling the car may be incomplete or wrong without FW versions or fingerprints.
+ """
+ return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False)
+
+ @classmethod
+ def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[structs.CarParams.CarFw],
+ experimental_long: bool, docs: bool) -> structs.CarParams:
+ ret = CarInterfaceBase.get_std_params(candidate)
+
+ platform = PLATFORMS[candidate]
+ ret.mass = platform.config.specs.mass
+ ret.wheelbase = platform.config.specs.wheelbase
+ ret.steerRatio = platform.config.specs.steerRatio
+ ret.centerToFront = ret.wheelbase * platform.config.specs.centerToFrontRatio
+ ret.minEnableSpeed = platform.config.specs.minEnableSpeed
+ ret.minSteerSpeed = platform.config.specs.minSteerSpeed
+ ret.tireStiffnessFactor = platform.config.specs.tireStiffnessFactor
+ ret.flags |= int(platform.config.flags)
+
+ ret = cls._get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs)
+
+ # Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload
+ if not ret.notCar:
+ ret.mass = ret.mass + STD_CARGO_KG
+
+ # Set params dependent on values set by the car interface
+ ret.rotationalInertia = scale_rot_inertia(ret.mass, ret.wheelbase)
+ ret.tireStiffnessFront, ret.tireStiffnessRear = scale_tire_stiffness(ret.mass, ret.wheelbase, ret.centerToFront, ret.tireStiffnessFactor)
+
+ return ret
+
+ @staticmethod
+ @abstractmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint: dict[int, dict[int, int]],
+ car_fw: list[structs.CarParams.CarFw], experimental_long: bool, docs: bool) -> structs.CarParams:
+ raise NotImplementedError
+
+ @staticmethod
+ def init(CP: structs.CarParams, can_recv: CanRecvCallable, can_send: CanSendCallable):
+ pass
+
+ @staticmethod
+ def get_steer_feedforward_default(desired_angle, v_ego):
+ # Proportional to realigning tire momentum: lateral acceleration.
+ return desired_angle * (v_ego**2)
+
+ def get_steer_feedforward_function(self):
+ return self.get_steer_feedforward_default
+
+ def torque_from_lateral_accel_linear(self, latcontrol_inputs: LatControlInputs, torque_params: structs.CarParams.LateralTorqueTuning,
+ lateral_accel_error: float, lateral_accel_deadzone: float, friction_compensation: bool, gravity_adjusted: bool) -> float:
+ # The default is a linear relationship between torque and lateral acceleration (accounting for road roll and steering friction)
+ friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation)
+ return (latcontrol_inputs.lateral_acceleration / float(torque_params.latAccelFactor)) + friction
+
+ def torque_from_lateral_accel(self) -> TorqueFromLateralAccelCallbackType:
+ return self.torque_from_lateral_accel_linear
+
+ # returns a set of default params to avoid repetition in car specific params
+ @staticmethod
+ def get_std_params(candidate: str) -> structs.CarParams:
+ ret = structs.CarParams()
+ ret.carFingerprint = candidate
+
+ # Car docs fields
+ ret.maxLateralAccel = get_torque_params()[candidate]['MAX_LAT_ACCEL_MEASURED']
+ ret.autoResumeSng = True # describes whether car can resume from a stop automatically
+
+ # standard ALC params
+ ret.tireStiffnessFactor = 1.0
+ ret.steerControlType = structs.CarParams.SteerControlType.torque
+ ret.minSteerSpeed = 0.
+ ret.wheelSpeedFactor = 1.0
+
+ ret.pcmCruise = True # openpilot's state is tied to the PCM's cruise state on most cars
+ ret.minEnableSpeed = -1. # enable is done by stock ACC, so ignore this
+ ret.steerRatioRear = 0. # no rear steering, at least on the listed cars aboveA
+ ret.openpilotLongitudinalControl = False
+ ret.stopAccel = -2.0
+ ret.stoppingDecelRate = 0.8 # brake_travel/s while trying to stop
+ ret.vEgoStopping = 0.5
+ ret.vEgoStarting = 0.5
+ ret.stoppingControl = True
+ ret.longitudinalTuning.kf = 1.
+ ret.longitudinalTuning.kpBP = [0.]
+ ret.longitudinalTuning.kpV = [0.]
+ ret.longitudinalTuning.kiBP = [0.]
+ ret.longitudinalTuning.kiV = [0.]
+ # TODO estimate car specific lag, use .15s for now
+ ret.longitudinalActuatorDelay = 0.15
+ ret.steerLimitTimer = 1.0
+ return ret
+
+ @staticmethod
+ def configure_torque_tune(candidate: str, tune: structs.CarParams.LateralTuning, steering_angle_deadzone_deg: float = 0.0, use_steering_angle: bool = True):
+ params = get_torque_params()[candidate]
+
+ tune.init('torque')
+ tune.torque.useSteeringAngle = use_steering_angle
+ tune.torque.kp = 1.0
+ tune.torque.kf = 1.0
+ tune.torque.ki = 0.1
+ tune.torque.friction = params['FRICTION']
+ tune.torque.latAccelFactor = params['LAT_ACCEL_FACTOR']
+ tune.torque.latAccelOffset = 0.0
+ tune.torque.steeringAngleDeadzoneDeg = steering_angle_deadzone_deg
+
+ def _update(self) -> structs.CarState:
+ return self.CS.update(*self.can_parsers)
+
+ def update(self, can_packets: list[tuple[int, list[CanData]]]) -> structs.CarState:
+ # parse can
+ for cp in self.can_parsers:
+ if cp is not None:
+ cp.update_strings(can_packets)
+
+ # get CarState
+ ret = self._update()
+
+ ret.canValid = all(cp.can_valid for cp in self.can_parsers if cp is not None)
+ ret.canTimeout = any(cp.bus_timeout for cp in self.can_parsers if cp is not None)
+
+ if ret.vEgoCluster == 0.0 and not self.v_ego_cluster_seen:
+ ret.vEgoCluster = ret.vEgo
+ else:
+ self.v_ego_cluster_seen = True
+
+ # Many cars apply hysteresis to the ego dash speed
+ ret.vEgoCluster = apply_hysteresis(ret.vEgoCluster, self.CS.out.vEgoCluster, self.CS.cluster_speed_hyst_gap)
+ if abs(ret.vEgo) < self.CS.cluster_min_speed:
+ ret.vEgoCluster = 0.0
+
+ if ret.cruiseState.speedCluster == 0:
+ ret.cruiseState.speedCluster = ret.cruiseState.speed
+
+ # save for next iteration
+ self.CS.out = ret
+
+ return ret
+
+
+class RadarInterfaceBase(ABC):
+ def __init__(self, CP: structs.CarParams):
+ self.CP = CP
+ self.rcp = None
+ self.pts: dict[int, structs.RadarData.RadarPoint] = {}
+ self.delay = 0
+ self.radar_ts = CP.radarTimeStep
+ self.frame = 0
+
+ def update(self, can_packets: list[tuple[int, list[CanData]]]) -> structs.RadarData | None:
+ self.frame += 1
+ if (self.frame % int(100 * self.radar_ts)) == 0:
+ return structs.RadarData()
+ return None
+
+
+class CarStateBase(ABC):
+ def __init__(self, CP: structs.CarParams):
+ self.CP = CP
+ self.car_fingerprint = CP.carFingerprint
+ self.out = structs.CarState()
+
+ self.cruise_buttons = 0
+ self.left_blinker_cnt = 0
+ self.right_blinker_cnt = 0
+ self.steering_pressed_cnt = 0
+ self.left_blinker_prev = False
+ self.right_blinker_prev = False
+ self.cluster_speed_hyst_gap = 0.0
+ self.cluster_min_speed = 0.0 # min speed before dropping to 0
+
+ Q = [[0.0, 0.0], [0.0, 100.0]]
+ R = 0.3
+ A = [[1.0, DT_CTRL], [0.0, 1.0]]
+ C = [[1.0, 0.0]]
+ x0=[[0.0], [0.0]]
+ K = get_kalman_gain(DT_CTRL, np.array(A), np.array(C), np.array(Q), R)
+ self.v_ego_kf = KF1D(x0=x0, A=A, C=C[0], K=K)
+
+ @abstractmethod
+ def update(self, cp, cp_cam, cp_adas, cp_body, cp_loopback) -> structs.CarState:
+ pass
+
+ def update_speed_kf(self, v_ego_raw):
+ if abs(v_ego_raw - self.v_ego_kf.x[0][0]) > 2.0: # Prevent large accelerations when car starts at non zero speed
+ self.v_ego_kf.set_x([[v_ego_raw], [0.0]])
+
+ v_ego_x = self.v_ego_kf.update(v_ego_raw)
+ return float(v_ego_x[0]), float(v_ego_x[1])
+
+ def get_wheel_speeds(self, fl, fr, rl, rr, unit=CV.KPH_TO_MS):
+ factor = unit * self.CP.wheelSpeedFactor
+
+ wheelSpeeds = structs.CarState.WheelSpeeds()
+ wheelSpeeds.fl = fl * factor
+ wheelSpeeds.fr = fr * factor
+ wheelSpeeds.rl = rl * factor
+ wheelSpeeds.rr = rr * factor
+ return wheelSpeeds
+
+ def update_blinker_from_lamp(self, blinker_time: int, left_blinker_lamp: bool, right_blinker_lamp: bool):
+ """Update blinkers from lights. Enable output when light was seen within the last `blinker_time`
+ iterations"""
+ # TODO: Handle case when switching direction. Now both blinkers can be on at the same time
+ self.left_blinker_cnt = blinker_time if left_blinker_lamp else max(self.left_blinker_cnt - 1, 0)
+ self.right_blinker_cnt = blinker_time if right_blinker_lamp else max(self.right_blinker_cnt - 1, 0)
+ return self.left_blinker_cnt > 0, self.right_blinker_cnt > 0
+
+ def update_steering_pressed(self, steering_pressed, steering_pressed_min_count):
+ """Applies filtering on steering pressed for noisy driver torque signals."""
+ self.steering_pressed_cnt += 1 if steering_pressed else -1
+ self.steering_pressed_cnt = clip(self.steering_pressed_cnt, 0, steering_pressed_min_count * 2)
+ return self.steering_pressed_cnt > steering_pressed_min_count
+
+ def update_blinker_from_stalk(self, blinker_time: int, left_blinker_stalk: bool, right_blinker_stalk: bool):
+ """Update blinkers from stalk position. When stalk is seen the blinker will be on for at least blinker_time,
+ or until the stalk is turned off, whichever is longer. If the opposite stalk direction is seen the blinker
+ is forced to the other side. On a rising edge of the stalk the timeout is reset."""
+
+ if left_blinker_stalk:
+ self.right_blinker_cnt = 0
+ if not self.left_blinker_prev:
+ self.left_blinker_cnt = blinker_time
+
+ if right_blinker_stalk:
+ self.left_blinker_cnt = 0
+ if not self.right_blinker_prev:
+ self.right_blinker_cnt = blinker_time
+
+ self.left_blinker_cnt = max(self.left_blinker_cnt - 1, 0)
+ self.right_blinker_cnt = max(self.right_blinker_cnt - 1, 0)
+
+ self.left_blinker_prev = left_blinker_stalk
+ self.right_blinker_prev = right_blinker_stalk
+
+ return bool(left_blinker_stalk or self.left_blinker_cnt > 0), bool(right_blinker_stalk or self.right_blinker_cnt > 0)
+
+ @staticmethod
+ def parse_gear_shifter(gear: str | None) -> GearShifter:
+ if gear is None:
+ return GearShifter.unknown
+ return GEAR_SHIFTER_MAP.get(gear.upper(), GearShifter.unknown)
+
+ @staticmethod
+ def get_can_parser(CP):
+ return None
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ return None
+
+ @staticmethod
+ def get_adas_can_parser(CP):
+ return None
+
+ @staticmethod
+ def get_body_can_parser(CP):
+ return None
+
+ @staticmethod
+ def get_loopback_can_parser(CP):
+ return None
+
+
+class CarControllerBase(ABC):
+ def __init__(self, dbc_name: str, CP: structs.CarParams):
+ self.CP = CP
+ self.frame = 0
+
+ @abstractmethod
+ def update(self, CC: structs.CarControl, CS: CarStateBase, now_nanos: int) -> tuple[structs.CarControl.Actuators, list[CanData]]:
+ pass
+
+
+INTERFACE_ATTR_FILE = {
+ "FINGERPRINTS": "fingerprints",
+ "FW_VERSIONS": "fingerprints",
+}
+
+# interface-specific helpers
+
+def get_interface_attr(attr: str, combine_brands: bool = False, ignore_none: bool = False) -> dict[str | StrEnum, Any]:
+ # read all the folders in opendbc/car and return a dict where:
+ # - keys are all the car models or brand names
+ # - values are attr values from all car folders
+ result = {}
+ for car_folder in sorted([x[0] for x in os.walk(BASEDIR)]):
+ try:
+ brand_name = car_folder.split('/')[-1]
+ brand_values = __import__(f'opendbc.car.{brand_name}.{INTERFACE_ATTR_FILE.get(attr, "values")}', fromlist=[attr])
+ if hasattr(brand_values, attr) or not ignore_none:
+ attr_data = getattr(brand_values, attr, None)
+ else:
+ continue
+
+ if combine_brands:
+ if isinstance(attr_data, dict):
+ for f, v in attr_data.items():
+ result[f] = v
+ else:
+ result[brand_name] = attr_data
+ except (ImportError, OSError):
+ pass
+
+ return result
+
+
+class NanoFFModel:
+ def __init__(self, weights_loc: str, platform: str):
+ self.weights_loc = weights_loc
+ self.platform = platform
+ self.load_weights(platform)
+
+ def load_weights(self, platform: str):
+ with open(self.weights_loc) as fob:
+ self.weights = {k: np.array(v) for k, v in json.load(fob)[platform].items()}
+
+ def relu(self, x: np.ndarray):
+ return np.maximum(0.0, x)
+
+ def forward(self, x: np.ndarray):
+ assert x.ndim == 1
+ x = (x - self.weights['input_norm_mat'][:, 0]) / (self.weights['input_norm_mat'][:, 1] - self.weights['input_norm_mat'][:, 0])
+ x = self.relu(np.dot(x, self.weights['w_1']) + self.weights['b_1'])
+ x = self.relu(np.dot(x, self.weights['w_2']) + self.weights['b_2'])
+ x = self.relu(np.dot(x, self.weights['w_3']) + self.weights['b_3'])
+ x = np.dot(x, self.weights['w_4']) + self.weights['b_4']
+ return x
+
+ def predict(self, x: list[float], do_sample: bool = False):
+ x = self.forward(np.array(x))
+ if do_sample:
+ pred = np.random.laplace(x[0], np.exp(x[1]) / self.weights['temperature'])
+ else:
+ pred = x[0]
+ pred = pred * (self.weights['output_norm_mat'][1] - self.weights['output_norm_mat'][0]) + self.weights['output_norm_mat'][0]
+ return pred
diff --git a/opendbc/car/isotp_parallel_query.py b/opendbc/car/isotp_parallel_query.py
new file mode 100644
index 0000000000..68f5b0ac27
--- /dev/null
+++ b/opendbc/car/isotp_parallel_query.py
@@ -0,0 +1,172 @@
+import time
+from collections import defaultdict
+from functools import partial
+
+from opendbc.car import carlog
+from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
+from opendbc.car.fw_query_definitions import AddrType
+from panda import uds
+
+
+class IsoTpParallelQuery:
+ def __init__(self, can_send: CanSendCallable, can_recv: CanRecvCallable, bus: int, addrs: list[int] | list[AddrType],
+ request: list[bytes], response: list[bytes], response_offset: int = 0x8,
+ functional_addrs: list[int] = None, debug: bool = False, response_pending_timeout: float = 10) -> None:
+ self.can_send = can_send
+ self.can_recv = can_recv
+ self.bus = bus
+ self.request = request
+ self.response = response
+ self.functional_addrs = functional_addrs or []
+ self.debug = debug
+ self.response_pending_timeout = response_pending_timeout
+
+ real_addrs = [a if isinstance(a, tuple) else (a, None) for a in addrs]
+ for tx_addr, _ in real_addrs:
+ assert tx_addr not in uds.FUNCTIONAL_ADDRS, f"Functional address should be defined in functional_addrs: {hex(tx_addr)}"
+
+ self.msg_addrs = {tx_addr: uds.get_rx_addr_for_tx_addr(tx_addr[0], rx_offset=response_offset) for tx_addr in real_addrs}
+ self.msg_buffer: dict[int, list[CanData]] = defaultdict(list)
+
+ def rx(self) -> None:
+ """Drain can socket and sort messages into buffers based on address"""
+ can_packets = self.can_recv(wait_for_one=True)
+
+ for packet in can_packets:
+ for msg in packet:
+ if msg.src == self.bus and msg.address in self.msg_addrs.values():
+ self.msg_buffer[msg.address].append(CanData(msg.address, msg.dat, msg.src))
+
+ def _can_tx(self, tx_addr: int, dat: bytes, bus: int):
+ """Helper function to send single message"""
+ msg = CanData(tx_addr, dat, bus)
+ self.can_send([msg])
+
+ def _can_rx(self, addr, sub_addr=None):
+ """Helper function to retrieve message with specified address and subadress from buffer"""
+ keep_msgs = []
+
+ if sub_addr is None:
+ msgs = self.msg_buffer[addr]
+ else:
+ # Filter based on subadress
+ msgs = []
+ for m in self.msg_buffer[addr]:
+ first_byte = m[1][0]
+ if first_byte == sub_addr:
+ msgs.append(m)
+ else:
+ keep_msgs.append(m)
+
+ self.msg_buffer[addr] = keep_msgs
+ return msgs
+
+ def _drain_rx(self) -> None:
+ self.can_recv()
+ self.msg_buffer = defaultdict(list)
+
+ def _create_isotp_msg(self, tx_addr: int, sub_addr: int | None, rx_addr: int):
+ can_client = uds.CanClient(self._can_tx, partial(self._can_rx, rx_addr, sub_addr=sub_addr), tx_addr, rx_addr,
+ self.bus, sub_addr=sub_addr, debug=self.debug)
+
+ max_len = 8 if sub_addr is None else 7
+ # uses iso-tp frame separation time of 10 ms
+ # TODO: use single_frame_mode so ECUs can send as fast as they want,
+ # as well as reduces chances we process messages from previous queries
+ return uds.IsoTpMessage(can_client, timeout=0, separation_time=0.01, debug=self.debug, max_len=max_len)
+
+ def get_data(self, timeout: float, total_timeout: float = 60.) -> dict[AddrType, bytes]:
+ self._drain_rx()
+
+ # Create message objects
+ msgs = {}
+ request_counter = {}
+ request_done = {}
+ for tx_addr, rx_addr in self.msg_addrs.items():
+ msgs[tx_addr] = self._create_isotp_msg(*tx_addr, rx_addr)
+ request_counter[tx_addr] = 0
+ request_done[tx_addr] = False
+
+ # Send first request to functional addrs, subsequent responses are handled on physical addrs
+ if len(self.functional_addrs):
+ for addr in self.functional_addrs:
+ self._create_isotp_msg(addr, None, -1).send(self.request[0])
+
+ # Send first frame (single or first) to all addresses and receive asynchronously in the loop below.
+ # If querying functional addrs, only set up physical IsoTpMessages to send consecutive frames
+ for msg in msgs.values():
+ msg.send(self.request[0], setup_only=len(self.functional_addrs) > 0)
+
+ results = {}
+ start_time = time.monotonic()
+ addrs_responded = set() # track addresses that have ever sent a valid iso-tp frame for timeout logging
+ response_timeouts = {tx_addr: start_time + timeout for tx_addr in self.msg_addrs}
+ while True:
+ self.rx()
+
+ for tx_addr, msg in msgs.items():
+ try:
+ dat, rx_in_progress = msg.recv()
+ except Exception:
+ carlog.exception(f"Error processing UDS response: {tx_addr}")
+ request_done[tx_addr] = True
+ continue
+
+ # Extend timeout for each consecutive ISO-TP frame to avoid timing out on long responses
+ if rx_in_progress:
+ addrs_responded.add(tx_addr)
+ response_timeouts[tx_addr] = time.monotonic() + timeout
+
+ if dat is None:
+ continue
+
+ # Log unexpected empty responses
+ if len(dat) == 0:
+ carlog.error(f"iso-tp query empty response: {tx_addr}")
+ request_done[tx_addr] = True
+ continue
+
+ counter = request_counter[tx_addr]
+ expected_response = self.response[counter]
+ response_valid = dat.startswith(expected_response)
+
+ if response_valid:
+ if counter + 1 < len(self.request):
+ response_timeouts[tx_addr] = time.monotonic() + timeout
+ msg.send(self.request[counter + 1])
+ request_counter[tx_addr] += 1
+ else:
+ results[tx_addr] = dat[len(expected_response):]
+ request_done[tx_addr] = True
+ else:
+ error_code = dat[2] if len(dat) > 2 else -1
+ if error_code == 0x78:
+ response_timeouts[tx_addr] = time.monotonic() + self.response_pending_timeout
+ carlog.error(f"iso-tp query response pending: {tx_addr}")
+ else:
+ request_done[tx_addr] = True
+ carlog.error(f"iso-tp query bad response: {tx_addr} - 0x{dat.hex()}")
+
+ # Mark request done if address timed out
+ cur_time = time.monotonic()
+ for tx_addr in response_timeouts:
+ if cur_time - response_timeouts[tx_addr] > 0:
+ if not request_done[tx_addr]:
+ if request_counter[tx_addr] > 0:
+ carlog.error(f"iso-tp query timeout after receiving partial response: {tx_addr}")
+ elif tx_addr in addrs_responded:
+ carlog.error(f"iso-tp query timeout while receiving response: {tx_addr}")
+ # TODO: handle functional addresses
+ # else:
+ # carlog.error(f"iso-tp query timeout with no response: {tx_addr}")
+ request_done[tx_addr] = True
+
+ # Break if all requests are done (finished or timed out)
+ if all(request_done.values()):
+ break
+
+ if cur_time - start_time > total_timeout:
+ carlog.error("iso-tp query timeout while receiving data")
+ break
+
+ return results
diff --git a/opendbc/car/mazda/__init__.py b/opendbc/car/mazda/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/mazda/carcontroller.py b/opendbc/car/mazda/carcontroller.py
new file mode 100644
index 0000000000..08b829004f
--- /dev/null
+++ b/opendbc/car/mazda/carcontroller.py
@@ -0,0 +1,65 @@
+import copy
+from opendbc.can.packer import CANPacker
+from opendbc.car import apply_driver_steer_torque_limits, structs
+from opendbc.car.interfaces import CarControllerBase
+from opendbc.car.mazda import mazdacan
+from opendbc.car.mazda.values import CarControllerParams, Buttons
+
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.apply_steer_last = 0
+ self.packer = CANPacker(dbc_name)
+ self.brake_counter = 0
+
+ def update(self, CC, CS, now_nanos):
+ can_sends = []
+
+ apply_steer = 0
+
+ if CC.latActive:
+ # calculate steer and also set limits due to driver torque
+ new_steer = int(round(CC.actuators.steer * CarControllerParams.STEER_MAX))
+ apply_steer = apply_driver_steer_torque_limits(new_steer, self.apply_steer_last,
+ CS.out.steeringTorque, CarControllerParams)
+
+ if CC.cruiseControl.cancel:
+ # If brake is pressed, let us wait >70ms before trying to disable crz to avoid
+ # a race condition with the stock system, where the second cancel from openpilot
+ # will disable the crz 'main on'. crz ctrl msg runs at 50hz. 70ms allows us to
+ # read 3 messages and most likely sync state before we attempt cancel.
+ self.brake_counter = self.brake_counter + 1
+ if self.frame % 10 == 0 and not (CS.out.brakePressed and self.brake_counter < 7):
+ # Cancel Stock ACC if it's enabled while OP is disengaged
+ # Send at a rate of 10hz until we sync with stock ACC state
+ can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP, CS.crz_btns_counter, Buttons.CANCEL))
+ else:
+ self.brake_counter = 0
+ if CC.cruiseControl.resume and self.frame % 5 == 0:
+ # Mazda Stop and Go requires a RES button (or gas) press if the car stops more than 3 seconds
+ # Send Resume button when planner wants car to move
+ can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP, CS.crz_btns_counter, Buttons.RESUME))
+
+ self.apply_steer_last = apply_steer
+
+ # send HUD alerts
+ if self.frame % 50 == 0:
+ ldw = CC.hudControl.visualAlert == VisualAlert.ldw
+ steer_required = CC.hudControl.visualAlert == VisualAlert.steerRequired
+ # TODO: find a way to silence audible warnings so we can add more hud alerts
+ steer_required = steer_required and CS.lkas_allowed_speed
+ can_sends.append(mazdacan.create_alert_command(self.packer, CS.cam_laneinfo, ldw, steer_required))
+
+ # send steering command
+ can_sends.append(mazdacan.create_steering_control(self.packer, self.CP,
+ self.frame, apply_steer, CS.cam_lkas))
+
+ new_actuators = copy.copy(CC.actuators)
+ new_actuators.steer = apply_steer / CarControllerParams.STEER_MAX
+ new_actuators.steerOutputCan = apply_steer
+
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/mazda/carstate.py b/opendbc/car/mazda/carstate.py
new file mode 100644
index 0000000000..7e69beb132
--- /dev/null
+++ b/opendbc/car/mazda/carstate.py
@@ -0,0 +1,158 @@
+from opendbc.can.can_define import CANDefine
+from opendbc.can.parser import CANParser
+from opendbc.car import create_button_events, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.interfaces import CarStateBase
+from opendbc.car.mazda.values import DBC, LKAS_LIMITS, MazdaFlags
+
+ButtonType = structs.CarState.ButtonEvent.Type
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+ self.shifter_values = can_define.dv["GEAR"]["GEAR"]
+
+ self.crz_btns_counter = 0
+ self.acc_active_last = False
+ self.low_speed_alert = False
+ self.lkas_allowed_speed = False
+ self.lkas_disabled = False
+
+ self.distance_button = 0
+
+ def update(self, cp, cp_cam, *_) -> structs.CarState:
+
+ ret = structs.CarState()
+
+ prev_distance_button = self.distance_button
+ self.distance_button = cp.vl["CRZ_BTNS"]["DISTANCE_LESS"]
+
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ cp.vl["WHEEL_SPEEDS"]["FL"],
+ cp.vl["WHEEL_SPEEDS"]["FR"],
+ cp.vl["WHEEL_SPEEDS"]["RL"],
+ cp.vl["WHEEL_SPEEDS"]["RR"],
+ )
+ ret.vEgoRaw = (ret.wheelSpeeds.fl + ret.wheelSpeeds.fr + ret.wheelSpeeds.rl + ret.wheelSpeeds.rr) / 4.
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+
+ # Match panda speed reading
+ speed_kph = cp.vl["ENGINE_DATA"]["SPEED"]
+ ret.standstill = speed_kph <= .1
+
+ can_gear = int(cp.vl["GEAR"]["GEAR"])
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None))
+
+ ret.genericToggle = bool(cp.vl["BLINK_INFO"]["HIGH_BEAMS"])
+ ret.leftBlindspot = cp.vl["BSM"]["LEFT_BS_STATUS"] != 0
+ ret.rightBlindspot = cp.vl["BSM"]["RIGHT_BS_STATUS"] != 0
+ ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(40, cp.vl["BLINK_INFO"]["LEFT_BLINK"] == 1,
+ cp.vl["BLINK_INFO"]["RIGHT_BLINK"] == 1)
+
+ ret.steeringAngleDeg = cp.vl["STEER"]["STEER_ANGLE"]
+ ret.steeringTorque = cp.vl["STEER_TORQUE"]["STEER_TORQUE_SENSOR"]
+ ret.steeringPressed = abs(ret.steeringTorque) > LKAS_LIMITS.STEER_THRESHOLD
+
+ ret.steeringTorqueEps = cp.vl["STEER_TORQUE"]["STEER_TORQUE_MOTOR"]
+ ret.steeringRateDeg = cp.vl["STEER_RATE"]["STEER_ANGLE_RATE"]
+
+ # TODO: this should be from 0 - 1.
+ ret.brakePressed = cp.vl["PEDALS"]["BRAKE_ON"] == 1
+ ret.brake = cp.vl["BRAKE"]["BRAKE_PRESSURE"]
+
+ ret.seatbeltUnlatched = cp.vl["SEATBELT"]["DRIVER_SEATBELT"] == 0
+ ret.doorOpen = any([cp.vl["DOORS"]["FL"], cp.vl["DOORS"]["FR"],
+ cp.vl["DOORS"]["BL"], cp.vl["DOORS"]["BR"]])
+
+ # TODO: this should be from 0 - 1.
+ ret.gas = cp.vl["ENGINE_DATA"]["PEDAL_GAS"]
+ ret.gasPressed = ret.gas > 0
+
+ # Either due to low speed or hands off
+ lkas_blocked = cp.vl["STEER_RATE"]["LKAS_BLOCK"] == 1
+
+ if self.CP.minSteerSpeed > 0:
+ # LKAS is enabled at 52kph going up and disabled at 45kph going down
+ # wait for LKAS_BLOCK signal to clear when going up since it lags behind the speed sometimes
+ if speed_kph > LKAS_LIMITS.ENABLE_SPEED and not lkas_blocked:
+ self.lkas_allowed_speed = True
+ elif speed_kph < LKAS_LIMITS.DISABLE_SPEED:
+ self.lkas_allowed_speed = False
+ else:
+ self.lkas_allowed_speed = True
+
+ # TODO: the signal used for available seems to be the adaptive cruise signal, instead of the main on
+ # it should be used for carState.cruiseState.nonAdaptive instead
+ ret.cruiseState.available = cp.vl["CRZ_CTRL"]["CRZ_AVAILABLE"] == 1
+ ret.cruiseState.enabled = cp.vl["CRZ_CTRL"]["CRZ_ACTIVE"] == 1
+ ret.cruiseState.standstill = cp.vl["PEDALS"]["STANDSTILL"] == 1
+ ret.cruiseState.speed = cp.vl["CRZ_EVENTS"]["CRZ_SPEED"] * CV.KPH_TO_MS
+
+ if ret.cruiseState.enabled:
+ if not self.lkas_allowed_speed and self.acc_active_last:
+ self.low_speed_alert = True
+ else:
+ self.low_speed_alert = False
+
+ # Check if LKAS is disabled due to lack of driver torque when all other states indicate
+ # it should be enabled (steer lockout). Don't warn until we actually get lkas active
+ # and lose it again, i.e, after initial lkas activation
+ ret.steerFaultTemporary = self.lkas_allowed_speed and lkas_blocked
+
+ self.acc_active_last = ret.cruiseState.enabled
+
+ self.crz_btns_counter = cp.vl["CRZ_BTNS"]["CTR"]
+
+ # camera signals
+ self.lkas_disabled = cp_cam.vl["CAM_LANEINFO"]["LANE_LINES"] == 0
+ self.cam_lkas = cp_cam.vl["CAM_LKAS"]
+ self.cam_laneinfo = cp_cam.vl["CAM_LANEINFO"]
+ ret.steerFaultPermanent = cp_cam.vl["CAM_LKAS"]["ERR_BIT_1"] == 1
+
+ # TODO: add button types for inc and dec
+ ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
+
+ return ret
+
+ @staticmethod
+ def get_can_parser(CP):
+ messages = [
+ # sig_address, frequency
+ ("BLINK_INFO", 10),
+ ("STEER", 67),
+ ("STEER_RATE", 83),
+ ("STEER_TORQUE", 83),
+ ("WHEEL_SPEEDS", 100),
+ ]
+
+ if CP.flags & MazdaFlags.GEN1:
+ messages += [
+ ("ENGINE_DATA", 100),
+ ("CRZ_CTRL", 50),
+ ("CRZ_EVENTS", 50),
+ ("CRZ_BTNS", 10),
+ ("PEDALS", 50),
+ ("BRAKE", 50),
+ ("SEATBELT", 10),
+ ("DOORS", 10),
+ ("GEAR", 20),
+ ("BSM", 10),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ messages = []
+
+ if CP.flags & MazdaFlags.GEN1:
+ messages += [
+ # sig_address, frequency
+ ("CAM_LANEINFO", 2),
+ ("CAM_LKAS", 16),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 2)
diff --git a/opendbc/car/mazda/fingerprints.py b/opendbc/car/mazda/fingerprints.py
new file mode 100644
index 0000000000..d7c430bc78
--- /dev/null
+++ b/opendbc/car/mazda/fingerprints.py
@@ -0,0 +1,266 @@
+from opendbc.car.structs import CarParams
+from opendbc.car.mazda.values import CAR
+
+Ecu = CarParams.Ecu
+
+FW_VERSIONS = {
+ CAR.MAZDA_CX5_2022: {
+ (Ecu.eps, 0x730, None): [
+ b'KSD5-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'PEW5-188K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PW67-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2D-188K2-G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2G-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2H-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2H-188K2-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX85-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXFG-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXFG-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'SH54-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'K131-67XK2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'KGWD-437K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KSD5-437K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'GSH7-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-U\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'PG69-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PW66-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXDL-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXFG-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXFG-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB2-21PS1-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB2-21PS1-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYJ3-21PS1-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'SH51-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.MAZDA_CX5: {
+ (Ecu.eps, 0x730, None): [
+ b'K319-3210X-A-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KCB8-3210X-B-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KJ01-3210X-G-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KJ01-3210X-J-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KJ01-3210X-M-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'PA53-188K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PAR4-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2E-188K2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2F-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2G-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2H-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2H-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2H-188K2-G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX2K-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX38-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX42-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX68-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYFA-188K2-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYFC-188K2-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYFD-188K2-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYNF-188K2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'SHKT-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'K123-67XK2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'K123-437K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KBJ5-437K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KL2K-437K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KN0W-437K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'B61L-67XK2-R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'B61L-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'B61L-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'B61L-67XK2-V\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-M\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-N\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'PA66-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PA66-21PS1-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX39-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX39-21PS1-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX68-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB1-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB1-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB1-21PS1-G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB2-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB2-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB2-21PS1-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB2-21PS1-G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYB2-21PS1-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYNC-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'SH9T-21PS1-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.MAZDA_CX9: {
+ (Ecu.eps, 0x730, None): [
+ b'K070-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KJ01-3210X-G-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KJ01-3210X-L-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'PX23-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX24-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM4-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXN8-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXN8-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYD7-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYD8-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYFM-188K2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYFM-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'K123-67XK2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'TK80-67XK2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'TK80-67XK2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'TA0B-437K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'TK79-437K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'TK79-437K2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'TM53-437K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'TN40-437K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'B61L-67XK2-P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'B61L-67XK2-V\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-K\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'TK80-67XK2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'PXM4-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM7-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM7-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYD5-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYD5-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYD6-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYD6-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYFM-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYFM-21PS1-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.MAZDA_3: {
+ (Ecu.eps, 0x730, None): [
+ b'BHN1-3210X-J-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K070-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'KR11-3210X-K-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'P5JD-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PY2P-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYJW-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYKC-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYKE-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'B63C-67XK2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GHP9-67Y10---41\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'B45A-437AS-0-08\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'B61L-67XK2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'B61L-67XK2-P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'B61L-67XK2-Q\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'B61L-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'P52G-21PS1-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PY2S-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYKA-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYKE-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYKE-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.MAZDA_6: {
+ (Ecu.eps, 0x730, None): [
+ b'GBEF-3210X-B-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GBEF-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GFBC-3210X-A-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'PA34-188K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PX4F-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYH7-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYH7-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'K131-67XK2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'GBVH-437K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GBVH-437K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GDDM-437K2-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'B61L-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'B61L-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'PA28-21PS1-A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYH3-21PS1-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PYH7-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+ CAR.MAZDA_CX9_2021: {
+ (Ecu.eps, 0x730, None): [
+ b'TC3M-3210X-A-00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'PXGW-188K2-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXGW-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM4-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM4-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM6-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM7-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x764, None): [
+ b'K131-67XK2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'K131-67XK2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x760, None): [
+ b'TA0B-437K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x706, None): [
+ b'GSH7-67XK2-M\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-N\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-T\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'GSH7-67XK2-U\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'PXM4-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM6-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'PXM7-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ },
+}
diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py
new file mode 100755
index 0000000000..b9a6932bb7
--- /dev/null
+++ b/opendbc/car/mazda/interface.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+from opendbc.car import get_safety_config, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.mazda.values import CAR, LKAS_LIMITS
+from opendbc.car.interfaces import CarInterfaceBase
+
+
+
+class CarInterface(CarInterfaceBase):
+
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "mazda"
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.mazda)]
+ ret.radarUnavailable = True
+
+ ret.dashcamOnly = candidate not in (CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9_2021)
+
+ ret.steerActuatorDelay = 0.1
+ ret.steerLimitTimer = 0.8
+
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ if candidate not in (CAR.MAZDA_CX5_2022,):
+ ret.minSteerSpeed = LKAS_LIMITS.DISABLE_SPEED * CV.KPH_TO_MS
+
+ ret.centerToFront = ret.wheelbase * 0.41
+
+ return ret
diff --git a/opendbc/car/mazda/mazdacan.py b/opendbc/car/mazda/mazdacan.py
new file mode 100644
index 0000000000..0605d980f8
--- /dev/null
+++ b/opendbc/car/mazda/mazdacan.py
@@ -0,0 +1,128 @@
+from opendbc.car.mazda.values import Buttons, MazdaFlags
+
+
+def create_steering_control(packer, CP, frame, apply_steer, lkas):
+
+ tmp = apply_steer + 2048
+
+ lo = tmp & 0xFF
+ hi = tmp >> 8
+
+ # copy values from camera
+ b1 = int(lkas["BIT_1"])
+ er1 = int(lkas["ERR_BIT_1"])
+ lnv = 0
+ ldw = 0
+ er2 = int(lkas["ERR_BIT_2"])
+
+ # Some older models do have these, newer models don't.
+ # Either way, they all work just fine if set to zero.
+ steering_angle = 0
+ b2 = 0
+
+ tmp = steering_angle + 2048
+ ahi = tmp >> 10
+ amd = (tmp & 0x3FF) >> 2
+ amd = (amd >> 4) | (( amd & 0xF) << 4)
+ alo = (tmp & 0x3) << 2
+
+ ctr = frame % 16
+ # bytes: [ 1 ] [ 2 ] [ 3 ] [ 4 ]
+ csum = 249 - ctr - hi - lo - (lnv << 3) - er1 - (ldw << 7) - ( er2 << 4) - (b1 << 5)
+
+ # bytes [ 5 ] [ 6 ] [ 7 ]
+ csum = csum - ahi - amd - alo - b2
+
+ if ahi == 1:
+ csum = csum + 15
+
+ if csum < 0:
+ if csum < -256:
+ csum = csum + 512
+ else:
+ csum = csum + 256
+
+ csum = csum % 256
+
+ values = {}
+ if CP.flags & MazdaFlags.GEN1:
+ values = {
+ "LKAS_REQUEST": apply_steer,
+ "CTR": ctr,
+ "ERR_BIT_1": er1,
+ "LINE_NOT_VISIBLE" : lnv,
+ "LDW": ldw,
+ "BIT_1": b1,
+ "ERR_BIT_2": er2,
+ "STEERING_ANGLE": steering_angle,
+ "ANGLE_ENABLED": b2,
+ "CHKSUM": csum
+ }
+
+ return packer.make_can_msg("CAM_LKAS", 0, values)
+
+
+def create_alert_command(packer, cam_msg: dict, ldw: bool, steer_required: bool):
+ values = {s: cam_msg[s] for s in [
+ "LINE_VISIBLE",
+ "LINE_NOT_VISIBLE",
+ "LANE_LINES",
+ "BIT1",
+ "BIT2",
+ "BIT3",
+ "NO_ERR_BIT",
+ "S1",
+ "S1_HBEAM",
+ ]}
+ values.update({
+ # TODO: what's the difference between all these? do we need to send all?
+ "HANDS_WARN_3_BITS": 0b111 if steer_required else 0,
+ "HANDS_ON_STEER_WARN": steer_required,
+ "HANDS_ON_STEER_WARN_2": steer_required,
+
+ # TODO: right lane works, left doesn't
+ # TODO: need to do something about L/R
+ "LDW_WARN_LL": 0,
+ "LDW_WARN_RL": 0,
+ })
+ return packer.make_can_msg("CAM_LANEINFO", 0, values)
+
+
+def create_button_cmd(packer, CP, counter, button):
+
+ can = int(button == Buttons.CANCEL)
+ res = int(button == Buttons.RESUME)
+
+ if CP.flags & MazdaFlags.GEN1:
+ values = {
+ "CAN_OFF": can,
+ "CAN_OFF_INV": (can + 1) % 2,
+
+ "SET_P": 0,
+ "SET_P_INV": 1,
+
+ "RES": res,
+ "RES_INV": (res + 1) % 2,
+
+ "SET_M": 0,
+ "SET_M_INV": 1,
+
+ "DISTANCE_LESS": 0,
+ "DISTANCE_LESS_INV": 1,
+
+ "DISTANCE_MORE": 0,
+ "DISTANCE_MORE_INV": 1,
+
+ "MODE_X": 0,
+ "MODE_X_INV": 1,
+
+ "MODE_Y": 0,
+ "MODE_Y_INV": 1,
+
+ "BIT1": 1,
+ "BIT2": 1,
+ "BIT3": 1,
+ "CTR": (counter + 1) % 16,
+ }
+
+ return packer.make_can_msg("CRZ_BTNS", 0, values)
diff --git a/opendbc/car/mazda/radar_interface.py b/opendbc/car/mazda/radar_interface.py
new file mode 100755
index 0000000000..56bda583e2
--- /dev/null
+++ b/opendbc/car/mazda/radar_interface.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+from opendbc.car.interfaces import RadarInterfaceBase
+
+class RadarInterface(RadarInterfaceBase):
+ pass
diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py
new file mode 100644
index 0000000000..47cf43ca06
--- /dev/null
+++ b/opendbc/car/mazda/values.py
@@ -0,0 +1,104 @@
+from dataclasses import dataclass, field
+from enum import IntFlag
+
+from opendbc.car import CarSpecs, DbcDict, PlatformConfig, Platforms, dbc_dict
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarHarness, CarDocs, CarParts
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
+
+Ecu = CarParams.Ecu
+
+
+# Steer torque limits
+
+class CarControllerParams:
+ STEER_MAX = 800 # theoretical max_steer 2047
+ STEER_DELTA_UP = 10 # torque increase per refresh
+ STEER_DELTA_DOWN = 25 # torque decrease per refresh
+ STEER_DRIVER_ALLOWANCE = 15 # allowed driver torque before start limiting
+ STEER_DRIVER_MULTIPLIER = 1 # weight driver torque
+ STEER_DRIVER_FACTOR = 1 # from dbc
+ STEER_ERROR_MAX = 350 # max delta between torque cmd and torque motor
+ STEER_STEP = 1 # 100 Hz
+
+ def __init__(self, CP):
+ pass
+
+
+@dataclass
+class MazdaCarDocs(CarDocs):
+ package: str = "All"
+ car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.mazda]))
+
+
+@dataclass(frozen=True, kw_only=True)
+class MazdaCarSpecs(CarSpecs):
+ tireStiffnessFactor: float = 0.7 # not optimized yet
+
+
+class MazdaFlags(IntFlag):
+ # Static flags
+ # Gen 1 hardware: same CAN messages and same camera
+ GEN1 = 1
+
+
+@dataclass
+class MazdaPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('mazda_2017', None))
+ flags: int = MazdaFlags.GEN1
+
+
+class CAR(Platforms):
+ MAZDA_CX5 = MazdaPlatformConfig(
+ [MazdaCarDocs("Mazda CX-5 2017-21")],
+ MazdaCarSpecs(mass=3655 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=15.5)
+ )
+ MAZDA_CX9 = MazdaPlatformConfig(
+ [MazdaCarDocs("Mazda CX-9 2016-20")],
+ MazdaCarSpecs(mass=4217 * CV.LB_TO_KG, wheelbase=3.1, steerRatio=17.6)
+ )
+ MAZDA_3 = MazdaPlatformConfig(
+ [MazdaCarDocs("Mazda 3 2017-18")],
+ MazdaCarSpecs(mass=2875 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=14.0)
+ )
+ MAZDA_6 = MazdaPlatformConfig(
+ [MazdaCarDocs("Mazda 6 2017-20")],
+ MazdaCarSpecs(mass=3443 * CV.LB_TO_KG, wheelbase=2.83, steerRatio=15.5)
+ )
+ MAZDA_CX9_2021 = MazdaPlatformConfig(
+ [MazdaCarDocs("Mazda CX-9 2021-23", video_link="https://youtu.be/dA3duO4a0O4")],
+ MAZDA_CX9.specs
+ )
+ MAZDA_CX5_2022 = MazdaPlatformConfig(
+ [MazdaCarDocs("Mazda CX-5 2022-24")],
+ MAZDA_CX5.specs,
+ )
+
+
+class LKAS_LIMITS:
+ STEER_THRESHOLD = 15
+ DISABLE_SPEED = 45 # kph
+ ENABLE_SPEED = 52 # kph
+
+
+class Buttons:
+ NONE = 0
+ SET_PLUS = 1
+ SET_MINUS = 2
+ RESUME = 3
+ CANCEL = 4
+
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[
+ # TODO: check data to ensure ABS does not skip ISO-TP frames on bus 0
+ Request(
+ [StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
+ [StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
+ bus=0,
+ ),
+ ],
+)
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/mock/__init__.py b/opendbc/car/mock/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/mock/carcontroller.py b/opendbc/car/mock/carcontroller.py
new file mode 100644
index 0000000000..6318cb7c34
--- /dev/null
+++ b/opendbc/car/mock/carcontroller.py
@@ -0,0 +1,6 @@
+from opendbc.car.interfaces import CarControllerBase
+
+
+class CarController(CarControllerBase):
+ def update(self, CC, CS, now_nanos):
+ return CC.actuators, []
diff --git a/opendbc/car/mock/carstate.py b/opendbc/car/mock/carstate.py
new file mode 100644
index 0000000000..9cbdf99e91
--- /dev/null
+++ b/opendbc/car/mock/carstate.py
@@ -0,0 +1,7 @@
+from opendbc.car import structs
+from opendbc.car.interfaces import CarStateBase
+
+
+class CarState(CarStateBase):
+ def update(self, *_) -> structs.CarState:
+ return structs.CarState()
diff --git a/opendbc/car/mock/interface.py b/opendbc/car/mock/interface.py
new file mode 100755
index 0000000000..f7c755c22b
--- /dev/null
+++ b/opendbc/car/mock/interface.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+from opendbc.car import structs
+from opendbc.car.interfaces import CarInterfaceBase
+
+
+# mocked car interface for dashcam mode
+class CarInterface(CarInterfaceBase):
+
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "mock"
+ ret.mass = 1700.
+ ret.wheelbase = 2.70
+ ret.centerToFront = ret.wheelbase * 0.5
+ ret.steerRatio = 13.
+ ret.dashcamOnly = True
+ return ret
diff --git a/opendbc/car/mock/radar_interface.py b/opendbc/car/mock/radar_interface.py
new file mode 100644
index 0000000000..6e552bf618
--- /dev/null
+++ b/opendbc/car/mock/radar_interface.py
@@ -0,0 +1,4 @@
+from opendbc.car.interfaces import RadarInterfaceBase
+
+class RadarInterface(RadarInterfaceBase):
+ pass
diff --git a/opendbc/car/mock/values.py b/opendbc/car/mock/values.py
new file mode 100644
index 0000000000..aa844de4c5
--- /dev/null
+++ b/opendbc/car/mock/values.py
@@ -0,0 +1,9 @@
+from opendbc.car import CarSpecs, PlatformConfig, Platforms
+
+
+class CAR(Platforms):
+ MOCK = PlatformConfig(
+ [],
+ CarSpecs(mass=1700, wheelbase=2.7, steerRatio=13),
+ {}
+ )
diff --git a/opendbc/car/nissan/__init__.py b/opendbc/car/nissan/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/nissan/carcontroller.py b/opendbc/car/nissan/carcontroller.py
new file mode 100644
index 0000000000..9e38312550
--- /dev/null
+++ b/opendbc/car/nissan/carcontroller.py
@@ -0,0 +1,81 @@
+import copy
+from opendbc.can.packer import CANPacker
+from opendbc.car import apply_std_steer_angle_limits, structs
+from opendbc.car.interfaces import CarControllerBase
+from opendbc.car.nissan import nissancan
+from opendbc.car.nissan.values import CAR, CarControllerParams
+
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.car_fingerprint = CP.carFingerprint
+
+ self.lkas_max_torque = 0
+ self.apply_angle_last = 0
+
+ self.packer = CANPacker(dbc_name)
+
+ def update(self, CC, CS, now_nanos):
+ actuators = CC.actuators
+ hud_control = CC.hudControl
+ pcm_cancel_cmd = CC.cruiseControl.cancel
+
+ can_sends = []
+
+ ### STEER ###
+ steer_hud_alert = 1 if hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw) else 0
+
+ if CC.latActive:
+ # windup slower
+ apply_angle = apply_std_steer_angle_limits(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw, CarControllerParams)
+
+ # Max torque from driver before EPS will give up and not apply torque
+ if not bool(CS.out.steeringPressed):
+ self.lkas_max_torque = CarControllerParams.LKAS_MAX_TORQUE
+ else:
+ # Scale max torque based on how much torque the driver is applying to the wheel
+ self.lkas_max_torque = max(
+ # Scale max torque down to half LKAX_MAX_TORQUE as a minimum
+ CarControllerParams.LKAS_MAX_TORQUE * 0.5,
+ # Start scaling torque at STEER_THRESHOLD
+ CarControllerParams.LKAS_MAX_TORQUE - 0.6 * max(0, abs(CS.out.steeringTorque) - CarControllerParams.STEER_THRESHOLD)
+ )
+
+ else:
+ apply_angle = CS.out.steeringAngleDeg
+ self.lkas_max_torque = 0
+
+ self.apply_angle_last = apply_angle
+
+ if self.CP.carFingerprint in (CAR.NISSAN_ROGUE, CAR.NISSAN_XTRAIL, CAR.NISSAN_ALTIMA) and pcm_cancel_cmd:
+ can_sends.append(nissancan.create_acc_cancel_cmd(self.packer, self.car_fingerprint, CS.cruise_throttle_msg))
+
+ # TODO: Find better way to cancel!
+ # For some reason spamming the cancel button is unreliable on the Leaf
+ # We now cancel by making propilot think the seatbelt is unlatched,
+ # this generates a beep and a warning message every time you disengage
+ if self.CP.carFingerprint in (CAR.NISSAN_LEAF, CAR.NISSAN_LEAF_IC) and self.frame % 2 == 0:
+ can_sends.append(nissancan.create_cancel_msg(self.packer, CS.cancel_msg, pcm_cancel_cmd))
+
+ can_sends.append(nissancan.create_steering_control(
+ self.packer, apply_angle, self.frame, CC.latActive, self.lkas_max_torque))
+
+ # Below are the HUD messages. We copy the stock message and modify
+ if self.CP.carFingerprint != CAR.NISSAN_ALTIMA:
+ if self.frame % 2 == 0:
+ can_sends.append(nissancan.create_lkas_hud_msg(self.packer, CS.lkas_hud_msg, CC.enabled, hud_control.leftLaneVisible, hud_control.rightLaneVisible,
+ hud_control.leftLaneDepart, hud_control.rightLaneDepart))
+
+ if self.frame % 50 == 0:
+ can_sends.append(nissancan.create_lkas_hud_info_msg(
+ self.packer, CS.lkas_hud_info_msg, steer_hud_alert
+ ))
+
+ new_actuators = copy.copy(actuators)
+ new_actuators.steeringAngleDeg = apply_angle
+
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/nissan/carstate.py b/opendbc/car/nissan/carstate.py
new file mode 100644
index 0000000000..040fb1934f
--- /dev/null
+++ b/opendbc/car/nissan/carstate.py
@@ -0,0 +1,201 @@
+import copy
+from collections import deque
+from opendbc.can.can_define import CANDefine
+from opendbc.can.parser import CANParser
+from opendbc.car import create_button_events, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.interfaces import CarStateBase
+from opendbc.car.nissan.values import CAR, DBC, CarControllerParams
+
+ButtonType = structs.CarState.ButtonEvent.Type
+
+TORQUE_SAMPLES = 12
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+
+ self.lkas_hud_msg = {}
+ self.lkas_hud_info_msg = {}
+
+ self.steeringTorqueSamples = deque(TORQUE_SAMPLES*[0], TORQUE_SAMPLES)
+ self.shifter_values = can_define.dv["GEARBOX"]["GEAR_SHIFTER"]
+
+ self.distance_button = 0
+
+ def update(self, cp, cp_cam, cp_adas, *_) -> structs.CarState:
+ ret = structs.CarState()
+
+ prev_distance_button = self.distance_button
+ self.distance_button = cp.vl["CRUISE_THROTTLE"]["FOLLOW_DISTANCE_BUTTON"]
+
+ if self.CP.carFingerprint in (CAR.NISSAN_ROGUE, CAR.NISSAN_XTRAIL, CAR.NISSAN_ALTIMA):
+ ret.gas = cp.vl["GAS_PEDAL"]["GAS_PEDAL"]
+ elif self.CP.carFingerprint in (CAR.NISSAN_LEAF, CAR.NISSAN_LEAF_IC):
+ ret.gas = cp.vl["CRUISE_THROTTLE"]["GAS_PEDAL"]
+
+ ret.gasPressed = bool(ret.gas > 3)
+
+ if self.CP.carFingerprint in (CAR.NISSAN_ROGUE, CAR.NISSAN_XTRAIL, CAR.NISSAN_ALTIMA):
+ ret.brakePressed = bool(cp.vl["DOORS_LIGHTS"]["USER_BRAKE_PRESSED"])
+ elif self.CP.carFingerprint in (CAR.NISSAN_LEAF, CAR.NISSAN_LEAF_IC):
+ ret.brakePressed = bool(cp.vl["CRUISE_THROTTLE"]["USER_BRAKE_PRESSED"])
+
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ cp.vl["WHEEL_SPEEDS_FRONT"]["WHEEL_SPEED_FL"],
+ cp.vl["WHEEL_SPEEDS_FRONT"]["WHEEL_SPEED_FR"],
+ cp.vl["WHEEL_SPEEDS_REAR"]["WHEEL_SPEED_RL"],
+ cp.vl["WHEEL_SPEEDS_REAR"]["WHEEL_SPEED_RR"],
+ )
+ ret.vEgoRaw = (ret.wheelSpeeds.fl + ret.wheelSpeeds.fr + ret.wheelSpeeds.rl + ret.wheelSpeeds.rr) / 4.
+
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.standstill = cp.vl["WHEEL_SPEEDS_REAR"]["WHEEL_SPEED_RL"] == 0.0 and cp.vl["WHEEL_SPEEDS_REAR"]["WHEEL_SPEED_RR"] == 0.0
+
+ if self.CP.carFingerprint == CAR.NISSAN_ALTIMA:
+ ret.cruiseState.enabled = bool(cp.vl["CRUISE_STATE"]["CRUISE_ENABLED"])
+ else:
+ ret.cruiseState.enabled = bool(cp_adas.vl["CRUISE_STATE"]["CRUISE_ENABLED"])
+
+ if self.CP.carFingerprint in (CAR.NISSAN_ROGUE, CAR.NISSAN_XTRAIL):
+ ret.seatbeltUnlatched = cp.vl["HUD"]["SEATBELT_DRIVER_LATCHED"] == 0
+ ret.cruiseState.available = bool(cp_cam.vl["PRO_PILOT"]["CRUISE_ON"])
+ elif self.CP.carFingerprint in (CAR.NISSAN_LEAF, CAR.NISSAN_LEAF_IC):
+ if self.CP.carFingerprint == CAR.NISSAN_LEAF:
+ ret.seatbeltUnlatched = cp.vl["SEATBELT"]["SEATBELT_DRIVER_LATCHED"] == 0
+ elif self.CP.carFingerprint == CAR.NISSAN_LEAF_IC:
+ ret.seatbeltUnlatched = cp.vl["CANCEL_MSG"]["CANCEL_SEATBELT"] == 1
+ ret.cruiseState.available = bool(cp.vl["CRUISE_THROTTLE"]["CRUISE_AVAILABLE"])
+ elif self.CP.carFingerprint == CAR.NISSAN_ALTIMA:
+ ret.seatbeltUnlatched = cp.vl["HUD"]["SEATBELT_DRIVER_LATCHED"] == 0
+ ret.cruiseState.available = bool(cp_adas.vl["PRO_PILOT"]["CRUISE_ON"])
+
+ if self.CP.carFingerprint == CAR.NISSAN_ALTIMA:
+ speed = cp.vl["PROPILOT_HUD"]["SET_SPEED"]
+ else:
+ speed = cp_adas.vl["PROPILOT_HUD"]["SET_SPEED"]
+
+ if speed != 255:
+ if self.CP.carFingerprint in (CAR.NISSAN_LEAF, CAR.NISSAN_LEAF_IC):
+ conversion = CV.MPH_TO_MS if cp.vl["HUD_SETTINGS"]["SPEED_MPH"] else CV.KPH_TO_MS
+ else:
+ conversion = CV.MPH_TO_MS if cp.vl["HUD"]["SPEED_MPH"] else CV.KPH_TO_MS
+ ret.cruiseState.speed = speed * conversion
+ ret.cruiseState.speedCluster = (speed - 1) * conversion # Speed on HUD is always 1 lower than actually sent on can bus
+
+ if self.CP.carFingerprint == CAR.NISSAN_ALTIMA:
+ ret.steeringTorque = cp_cam.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_DRIVER"]
+ else:
+ ret.steeringTorque = cp.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_DRIVER"]
+
+ self.steeringTorqueSamples.append(ret.steeringTorque)
+ # Filtering driver torque to prevent steeringPressed false positives
+ ret.steeringPressed = bool(abs(sum(self.steeringTorqueSamples) / TORQUE_SAMPLES) > CarControllerParams.STEER_THRESHOLD)
+
+ ret.steeringAngleDeg = cp.vl["STEER_ANGLE_SENSOR"]["STEER_ANGLE"]
+
+ ret.leftBlinker = bool(cp.vl["LIGHTS"]["LEFT_BLINKER"])
+ ret.rightBlinker = bool(cp.vl["LIGHTS"]["RIGHT_BLINKER"])
+
+ ret.doorOpen = any([cp.vl["DOORS_LIGHTS"]["DOOR_OPEN_RR"],
+ cp.vl["DOORS_LIGHTS"]["DOOR_OPEN_RL"],
+ cp.vl["DOORS_LIGHTS"]["DOOR_OPEN_FR"],
+ cp.vl["DOORS_LIGHTS"]["DOOR_OPEN_FL"]])
+
+ ret.espDisabled = bool(cp.vl["ESP"]["ESP_DISABLED"])
+
+ can_gear = int(cp.vl["GEARBOX"]["GEAR_SHIFTER"])
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None))
+
+ if self.CP.carFingerprint == CAR.NISSAN_ALTIMA:
+ self.lkas_enabled = bool(cp.vl["LKAS_SETTINGS"]["LKAS_ENABLED"])
+ else:
+ self.lkas_enabled = bool(cp_adas.vl["LKAS_SETTINGS"]["LKAS_ENABLED"])
+
+ self.cruise_throttle_msg = copy.copy(cp.vl["CRUISE_THROTTLE"])
+
+ if self.CP.carFingerprint in (CAR.NISSAN_LEAF, CAR.NISSAN_LEAF_IC):
+ self.cancel_msg = copy.copy(cp.vl["CANCEL_MSG"])
+
+ if self.CP.carFingerprint != CAR.NISSAN_ALTIMA:
+ self.lkas_hud_msg = copy.copy(cp_adas.vl["PROPILOT_HUD"])
+ self.lkas_hud_info_msg = copy.copy(cp_adas.vl["PROPILOT_HUD_INFO_MSG"])
+
+ ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
+
+ return ret
+
+ @staticmethod
+ def get_can_parser(CP):
+ messages = [
+ # sig_address, frequency
+ ("STEER_ANGLE_SENSOR", 100),
+ ("WHEEL_SPEEDS_REAR", 50),
+ ("WHEEL_SPEEDS_FRONT", 50),
+ ("ESP", 25),
+ ("GEARBOX", 25),
+ ("DOORS_LIGHTS", 10),
+ ("LIGHTS", 10),
+ ]
+
+ if CP.carFingerprint in (CAR.NISSAN_ROGUE, CAR.NISSAN_XTRAIL, CAR.NISSAN_ALTIMA):
+ messages += [
+ ("GAS_PEDAL", 100),
+ ("CRUISE_THROTTLE", 50),
+ ("HUD", 25),
+ ]
+
+ elif CP.carFingerprint in (CAR.NISSAN_LEAF, CAR.NISSAN_LEAF_IC):
+ messages += [
+ ("BRAKE_PEDAL", 100),
+ ("CRUISE_THROTTLE", 50),
+ ("CANCEL_MSG", 50),
+ ("HUD_SETTINGS", 25),
+ ("SEATBELT", 10),
+ ]
+
+ if CP.carFingerprint == CAR.NISSAN_ALTIMA:
+ messages += [
+ ("CRUISE_STATE", 10),
+ ("LKAS_SETTINGS", 10),
+ ("PROPILOT_HUD", 50),
+ ]
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 1)
+
+ messages.append(("STEER_TORQUE_SENSOR", 100))
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0)
+
+ @staticmethod
+ def get_adas_can_parser(CP):
+ # this function generates lists for signal, messages and initial values
+
+ if CP.carFingerprint == CAR.NISSAN_ALTIMA:
+ messages = [
+ ("LKAS", 100),
+ ("PRO_PILOT", 100),
+ ]
+ else:
+ messages = [
+ ("PROPILOT_HUD_INFO_MSG", 2),
+ ("LKAS_SETTINGS", 10),
+ ("CRUISE_STATE", 50),
+ ("PROPILOT_HUD", 50),
+ ("LKAS", 100),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 2)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ messages = []
+
+ if CP.carFingerprint in (CAR.NISSAN_ROGUE, CAR.NISSAN_XTRAIL):
+ messages.append(("PRO_PILOT", 100))
+ elif CP.carFingerprint == CAR.NISSAN_ALTIMA:
+ messages.append(("STEER_TORQUE_SENSOR", 100))
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0)
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 1)
diff --git a/opendbc/car/nissan/fingerprints.py b/opendbc/car/nissan/fingerprints.py
new file mode 100644
index 0000000000..9cef01cec4
--- /dev/null
+++ b/opendbc/car/nissan/fingerprints.py
@@ -0,0 +1,119 @@
+# ruff: noqa: E501
+from opendbc.car.structs import CarParams
+from opendbc.car.nissan.values import CAR
+
+Ecu = CarParams.Ecu
+
+FINGERPRINTS = {
+ CAR.NISSAN_XTRAIL: [{
+ 2: 5, 42: 6, 346: 6, 347: 5, 348: 8, 349: 7, 361: 8, 386: 8, 389: 8, 397: 8, 398: 8, 403: 8, 520: 2, 523: 6, 548: 8, 645: 8, 658: 8, 665: 8, 666: 8, 674: 2, 682: 8, 683: 8, 689: 8, 723: 8, 758: 3, 768: 2, 783: 3, 851: 8, 855: 8, 1041: 8, 1055: 2, 1104: 4, 1105: 6, 1107: 4, 1108: 8, 1111: 4, 1227: 8, 1228: 8, 1247: 4, 1266: 8, 1273: 7, 1342: 1, 1376: 6, 1401: 8, 1474: 2, 1497: 3, 1821: 8, 1823: 8, 1837: 8, 2015: 8, 2016: 8, 2024: 8
+ },
+ {
+ 2: 5, 42: 6, 346: 6, 347: 5, 348: 8, 349: 7, 361: 8, 386: 8, 389: 8, 397: 8, 398: 8, 403: 8, 520: 2, 523: 6, 527: 1, 548: 8, 637: 4, 645: 8, 658: 8, 665: 8, 666: 8, 674: 2, 682: 8, 683: 8, 689: 8, 723: 8, 758: 3, 768: 6, 783: 3, 851: 8, 855: 8, 1041: 8, 1055: 2, 1104: 4, 1105: 6, 1107: 4, 1108: 8, 1111: 4, 1227: 8, 1228: 8, 1247: 4, 1266: 8, 1273: 7, 1342: 1, 1376: 6, 1401: 8, 1474: 8, 1497: 3, 1534: 6, 1792: 8, 1821: 8, 1823: 8, 1837: 8, 1872: 8, 1937: 8, 1953: 8, 1968: 8, 2015: 8, 2016: 8, 2024: 8
+ }],
+ CAR.NISSAN_LEAF: [{
+ 2: 5, 42: 6, 264: 3, 361: 8, 372: 8, 384: 8, 389: 8, 403: 8, 459: 7, 460: 4, 470: 8, 520: 1, 569: 8, 581: 8, 634: 7, 640: 8, 644: 8, 645: 8, 646: 5, 658: 8, 682: 8, 683: 8, 689: 8, 724: 6, 758: 3, 761: 2, 783: 3, 852: 8, 853: 8, 856: 8, 861: 8, 944: 1, 976: 6, 1008: 7, 1011: 7, 1057: 3, 1227: 8, 1228: 8, 1261: 5, 1342: 1, 1354: 8, 1361: 8, 1459: 8, 1477: 8, 1497: 3, 1549: 8, 1573: 6, 1821: 8, 1837: 8, 1856: 8, 1859: 8, 1861: 8, 1864: 8, 1874: 8, 1888: 8, 1891: 8, 1893: 8, 1906: 8, 1947: 8, 1949: 8, 1979: 8, 1981: 8, 2016: 8, 2017: 8, 2021: 8, 643: 5, 1792: 8, 1872: 8, 1937: 8, 1953: 8, 1968: 8, 1988: 8, 2000: 8, 2001: 8, 2004: 8, 2005: 8, 2015: 8
+ },
+ {
+ 2: 5, 42: 8, 264: 3, 361: 8, 372: 8, 384: 8, 389: 8, 403: 8, 459: 7, 460: 4, 470: 8, 520: 1, 569: 8, 581: 8, 634: 7, 640: 8, 643: 5, 644: 8, 645: 8, 646: 5, 658: 8, 682: 8, 683: 8, 689: 8, 724: 6, 758: 3, 761: 2, 772: 8, 773: 6, 774: 7, 775: 8, 776: 6, 777: 7, 778: 6, 783: 3, 852: 8, 853: 8, 856: 8, 861: 8, 943: 8, 944: 1, 976: 6, 1008: 7, 1009: 8, 1010: 8, 1011: 7, 1012: 8, 1013: 8, 1019: 8, 1020: 8, 1021: 8, 1022: 8, 1057: 3, 1227: 8, 1228: 8, 1261: 5, 1342: 1, 1354: 8, 1361: 8, 1402: 8, 1459: 8, 1477: 8, 1497: 3, 1549: 8, 1573: 6, 1821: 8, 1837: 8
+ }],
+ CAR.NISSAN_LEAF_IC: [{
+ 2: 5, 42: 6, 264: 3, 282: 8, 361: 8, 372: 8, 384: 8, 389: 8, 403: 8, 459: 7, 460: 4, 470: 8, 520: 1, 569: 8, 581: 8, 634: 7, 640: 8, 643: 5, 644: 8, 645: 8, 646: 5, 658: 8, 682: 8, 683: 8, 689: 8, 756: 5, 758: 3, 761: 2, 783: 3, 830: 2, 852: 8, 853: 8, 856: 8, 861: 8, 943: 8, 944: 1, 1001: 6, 1057: 3, 1227: 8, 1228: 8, 1229: 8, 1342: 1, 1354: 8, 1361: 8, 1459: 8, 1477: 8, 1497: 3, 1514: 6, 1549: 8, 1573: 6, 1792: 8, 1821: 8, 1822: 8, 1837: 8, 1838: 8, 1872: 8, 1937: 8, 1953: 8, 1968: 8, 1988: 8, 2000: 8, 2001: 8, 2004: 8, 2005: 8, 2015: 8, 2016: 8, 2017: 8
+ }],
+ CAR.NISSAN_ROGUE: [{
+ 2: 5, 42: 6, 346: 6, 347: 5, 348: 8, 349: 7, 361: 8, 386: 8, 389: 8, 397: 8, 398: 8, 403: 8, 520: 2, 523: 6, 548: 8, 634: 7, 643: 5, 645: 8, 658: 8, 665: 8, 666: 8, 674: 2, 682: 8, 683: 8, 689: 8, 723: 8, 758: 3, 772: 8, 773: 6, 774: 7, 775: 8, 776: 6, 777: 7, 778: 6, 783: 3, 851: 8, 855: 8, 1041: 8, 1042: 8, 1055: 2, 1104: 4, 1105: 6, 1107: 4, 1108: 8, 1110: 7, 1111: 7, 1227: 8, 1228: 8, 1247: 4, 1266: 8, 1273: 7, 1342: 1, 1376: 6, 1401: 8, 1474: 2, 1497: 3, 1534: 7, 1792: 8, 1821: 8, 1823: 8, 1837: 8, 1839: 8, 1872: 8, 1937: 8, 1953: 8, 1968: 8, 1988: 8, 2000: 8, 2001: 8, 2004: 8, 2005: 8, 2015: 8, 2016: 8, 2017: 8, 2024: 8, 2025: 8
+ }],
+ CAR.NISSAN_ALTIMA: [{
+ 2: 5, 42: 6, 346: 6, 347: 5, 348: 8, 349: 7, 361: 8, 386: 8, 389: 8, 397: 8, 398: 8, 403: 8, 438: 8, 451: 8, 517: 8, 520: 2, 522: 8, 523: 6, 539: 8, 541: 7, 542: 8, 543: 8, 544: 8, 545: 8, 546: 8, 547: 8, 548: 8, 570: 8, 576: 8, 577: 8, 582: 8, 583: 8, 584: 8, 586: 8, 587: 8, 588: 8, 589: 8, 590: 8, 591: 8, 592: 8, 600: 8, 601: 8, 610: 8, 611: 8, 612: 8, 614: 8, 615: 8, 616: 8, 617: 8, 622: 8, 623: 8, 634: 7, 638: 8, 645: 8, 648: 5, 654: 6, 658: 8, 659: 8, 660: 8, 661: 8, 665: 8, 666: 8, 674: 2, 675: 8, 676: 8, 682: 8, 683: 8, 684: 8, 685: 8, 686: 8, 687: 8, 689: 8, 690: 8, 703: 8, 708: 7, 709: 7, 711: 7, 712: 7, 713: 7, 714: 8, 715: 8, 716: 8, 717: 7, 718: 7, 719: 7, 720: 7, 723: 8, 726: 7, 727: 7, 728: 7, 735: 8, 746: 8, 748: 6, 749: 6, 750: 8, 758: 3, 772: 8, 773: 6, 774: 7, 775: 8, 776: 6, 777: 7, 778: 6, 779: 7, 781: 7, 782: 7, 783: 3, 851: 8, 855: 5, 1001: 6, 1041: 8, 1042: 8, 1055: 3, 1100: 7, 1104: 4, 1105: 6, 1107: 4, 1108: 8, 1110: 7, 1111: 7, 1144: 7, 1145: 7, 1227: 8, 1228: 8, 1229: 8, 1232: 8, 1247: 4, 1258: 8, 1259: 8, 1266: 8, 1273: 7, 1306: 1, 1314: 8, 1323: 8, 1324: 8, 1342: 1, 1376: 8, 1401: 8, 1454: 8, 1497: 3, 1514: 6, 1526: 8, 1527: 5, 1792: 8, 1821: 8, 1823: 8, 1837: 8, 1872: 8, 1937: 8, 1953: 8, 1968: 8, 1988: 8, 2000: 8, 2001: 8, 2004: 8, 2005: 8, 2015: 8, 2016: 8, 2017: 8, 2024: 8, 2025: 8
+ }],
+}
+
+FW_VERSIONS = {
+ CAR.NISSAN_ALTIMA: {
+ (Ecu.fwdCamera, 0x707, None): [
+ b'284N86CA1D',
+ ],
+ (Ecu.eps, 0x742, None): [
+ b'6CA2B\xa9A\x02\x02G8A89P90D6A\x00\x00\x01\x80',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'237109HE2B',
+ ],
+ (Ecu.gateway, 0x18dad0f1, None): [
+ b'284U29HE0A',
+ ],
+ },
+ CAR.NISSAN_LEAF: {
+ (Ecu.abs, 0x740, None): [
+ b'476605SA1C',
+ b'476605SA7D',
+ b'476605SC2D',
+ b'476606WK7B',
+ b'476606WK9B',
+ ],
+ (Ecu.eps, 0x742, None): [
+ b'5SA2A\x99A\x05\x02N123F\x15b\x00\x00\x00\x00\x00\x00\x00\x80',
+ b'5SA2A\xb7A\x05\x02N123F\x15\xa2\x00\x00\x00\x00\x00\x00\x00\x80',
+ b'5SN2A\xb7A\x05\x02N123F\x15\xa2\x00\x00\x00\x00\x00\x00\x00\x80',
+ b'5SN2A\xb7A\x05\x02N126F\x15\xb2\x00\x00\x00\x00\x00\x00\x00\x80',
+ ],
+ (Ecu.fwdCamera, 0x707, None): [
+ b'5SA0ADB\x04\x18\x00\x00\x00\x00\x00_*6\x04\x94a\x00\x00\x00\x80',
+ b'5SA2ADB\x04\x18\x00\x00\x00\x00\x00_*6\x04\x94a\x00\x00\x00\x80',
+ b'6WK2ADB\x04\x18\x00\x00\x00\x00\x00R;1\x18\x99\x10\x00\x00\x00\x80',
+ b'6WK2BDB\x04\x18\x00\x00\x00\x00\x00R;1\x18\x99\x10\x00\x00\x00\x80',
+ b'6WK2CDB\x04\x18\x00\x00\x00\x00\x00R=1\x18\x99\x10\x00\x00\x00\x80',
+ ],
+ (Ecu.gateway, 0x18dad0f1, None): [
+ b'284U25SA3C',
+ b'284U25SP0C',
+ b'284U25SP1C',
+ b'284U26WK0A',
+ b'284U26WK0C',
+ ],
+ },
+ CAR.NISSAN_LEAF_IC: {
+ (Ecu.fwdCamera, 0x707, None): [
+ b'5SH1BDB\x04\x18\x00\x00\x00\x00\x00_-?\x04\x91\xf2\x00\x00\x00\x80',
+ b'5SH4BDB\x04\x18\x00\x00\x00\x00\x00_-?\x04\x91\xf2\x00\x00\x00\x80',
+ b'5SK0ADB\x04\x18\x00\x00\x00\x00\x00_(5\x07\x9aQ\x00\x00\x00\x80',
+ ],
+ (Ecu.abs, 0x740, None): [
+ b'476605SD2E',
+ b'476605SH1D',
+ b'476605SK2A',
+ ],
+ (Ecu.eps, 0x742, None): [
+ b'5SH2A\x99A\x05\x02N123F\x15\x81\x00\x00\x00\x00\x00\x00\x00\x80',
+ b'5SH2C\xb7A\x05\x02N123F\x15\xa3\x00\x00\x00\x00\x00\x00\x00\x80',
+ b'5SK3A\x99A\x05\x02N123F\x15u\x00\x00\x00\x00\x00\x00\x00\x80',
+ ],
+ (Ecu.gateway, 0x18dad0f1, None): [
+ b'284U25SF0C',
+ b'284U25SH3A',
+ b'284U25SK2D',
+ ],
+ },
+ CAR.NISSAN_XTRAIL: {
+ (Ecu.fwdCamera, 0x707, None): [
+ b'284N86FR2A',
+ ],
+ (Ecu.abs, 0x740, None): [
+ b'6FU0AD\x11\x02\x00\x02e\x95e\x80iQ#\x01\x00\x00\x00\x00\x00\x80',
+ b'6FU1BD\x11\x02\x00\x02e\x95e\x80iX#\x01\x00\x00\x00\x00\x00\x80',
+ ],
+ (Ecu.eps, 0x742, None): [
+ b'6FP2A\x99A\x05\x02N123F\x18\x02\x00\x00\x00\x00\x00\x00\x00\x80',
+ ],
+ (Ecu.combinationMeter, 0x743, None): [
+ b'6FR2A\x18B\x05\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'6FR9A\xa0A\x06\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80',
+ b'6FU9B\xa0A\x06\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80',
+ ],
+ (Ecu.gateway, 0x18dad0f1, None): [
+ b'284U26FR0E',
+ ],
+ },
+}
diff --git a/opendbc/car/nissan/interface.py b/opendbc/car/nissan/interface.py
new file mode 100644
index 0000000000..14255338b9
--- /dev/null
+++ b/opendbc/car/nissan/interface.py
@@ -0,0 +1,26 @@
+from panda import Panda
+from opendbc.car import get_safety_config, structs
+from opendbc.car.interfaces import CarInterfaceBase
+from opendbc.car.nissan.values import CAR
+
+
+class CarInterface(CarInterfaceBase):
+
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "nissan"
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.nissan)]
+ ret.autoResumeSng = False
+
+ ret.steerLimitTimer = 1.0
+
+ ret.steerActuatorDelay = 0.1
+
+ ret.steerControlType = structs.CarParams.SteerControlType.angle
+ ret.radarUnavailable = True
+
+ if candidate == CAR.NISSAN_ALTIMA:
+ # Altima has EPS on C-CAN unlike the others that have it on V-CAN
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_NISSAN_ALT_EPS_BUS
+
+ return ret
diff --git a/opendbc/car/nissan/nissancan.py b/opendbc/car/nissan/nissancan.py
new file mode 100644
index 0000000000..555020d858
--- /dev/null
+++ b/opendbc/car/nissan/nissancan.py
@@ -0,0 +1,154 @@
+import crcmod
+from opendbc.car.nissan.values import CAR
+
+# TODO: add this checksum to the CANPacker
+nissan_checksum = crcmod.mkCrcFun(0x11d, initCrc=0x00, rev=False, xorOut=0xff)
+
+
+def create_steering_control(packer, apply_steer, frame, steer_on, lkas_max_torque):
+ values = {
+ "COUNTER": frame % 0x10,
+ "DESIRED_ANGLE": apply_steer,
+ "SET_0x80_2": 0x80,
+ "SET_0x80": 0x80,
+ "MAX_TORQUE": lkas_max_torque if steer_on else 0,
+ "LKA_ACTIVE": steer_on,
+ }
+
+ dat = packer.make_can_msg("LKAS", 0, values)[1]
+
+ values["CHECKSUM"] = nissan_checksum(dat[:7])
+ return packer.make_can_msg("LKAS", 0, values)
+
+
+def create_acc_cancel_cmd(packer, car_fingerprint, cruise_throttle_msg):
+ values = {s: cruise_throttle_msg[s] for s in [
+ "COUNTER",
+ "PROPILOT_BUTTON",
+ "CANCEL_BUTTON",
+ "GAS_PEDAL_INVERTED",
+ "SET_BUTTON",
+ "RES_BUTTON",
+ "FOLLOW_DISTANCE_BUTTON",
+ "NO_BUTTON_PRESSED",
+ "GAS_PEDAL",
+ "USER_BRAKE_PRESSED",
+ "NEW_SIGNAL_2",
+ "GAS_PRESSED_INVERTED",
+ "unsure1",
+ "unsure2",
+ "unsure3",
+ ]}
+ can_bus = 1 if car_fingerprint == CAR.NISSAN_ALTIMA else 2
+
+ values["CANCEL_BUTTON"] = 1
+ values["NO_BUTTON_PRESSED"] = 0
+ values["PROPILOT_BUTTON"] = 0
+ values["SET_BUTTON"] = 0
+ values["RES_BUTTON"] = 0
+ values["FOLLOW_DISTANCE_BUTTON"] = 0
+
+ return packer.make_can_msg("CRUISE_THROTTLE", can_bus, values)
+
+
+def create_cancel_msg(packer, cancel_msg, cruise_cancel):
+ values = {s: cancel_msg[s] for s in [
+ "CANCEL_SEATBELT",
+ "NEW_SIGNAL_1",
+ "NEW_SIGNAL_2",
+ "NEW_SIGNAL_3",
+ ]}
+
+ if cruise_cancel:
+ values["CANCEL_SEATBELT"] = 1
+
+ return packer.make_can_msg("CANCEL_MSG", 2, values)
+
+
+def create_lkas_hud_msg(packer, lkas_hud_msg, enabled, left_line, right_line, left_lane_depart, right_lane_depart):
+ values = {s: lkas_hud_msg[s] for s in [
+ "LARGE_WARNING_FLASHING",
+ "SIDE_RADAR_ERROR_FLASHING1",
+ "SIDE_RADAR_ERROR_FLASHING2",
+ "LEAD_CAR",
+ "LEAD_CAR_ERROR",
+ "FRONT_RADAR_ERROR",
+ "FRONT_RADAR_ERROR_FLASHING",
+ "SIDE_RADAR_ERROR_FLASHING3",
+ "LKAS_ERROR_FLASHING",
+ "SAFETY_SHIELD_ACTIVE",
+ "RIGHT_LANE_GREEN_FLASH",
+ "LEFT_LANE_GREEN_FLASH",
+ "FOLLOW_DISTANCE",
+ "AUDIBLE_TONE",
+ "SPEED_SET_ICON",
+ "SMALL_STEERING_WHEEL_ICON",
+ "unknown59",
+ "unknown55",
+ "unknown26",
+ "unknown28",
+ "unknown31",
+ "SET_SPEED",
+ "unknown43",
+ "unknown08",
+ "unknown05",
+ "unknown02",
+ ]}
+
+ values["RIGHT_LANE_YELLOW_FLASH"] = 1 if right_lane_depart else 0
+ values["LEFT_LANE_YELLOW_FLASH"] = 1 if left_lane_depart else 0
+
+ values["LARGE_STEERING_WHEEL_ICON"] = 2 if enabled else 0
+ values["RIGHT_LANE_GREEN"] = 1 if right_line and enabled else 0
+ values["LEFT_LANE_GREEN"] = 1 if left_line and enabled else 0
+
+ return packer.make_can_msg("PROPILOT_HUD", 0, values)
+
+
+def create_lkas_hud_info_msg(packer, lkas_hud_info_msg, steer_hud_alert):
+ values = {s: lkas_hud_info_msg[s] for s in [
+ "NA_HIGH_ACCEL_TEMP",
+ "SIDE_RADAR_NA_HIGH_CABIN_TEMP",
+ "SIDE_RADAR_MALFUNCTION",
+ "LKAS_MALFUNCTION",
+ "FRONT_RADAR_MALFUNCTION",
+ "SIDE_RADAR_NA_CLEAN_REAR_CAMERA",
+ "NA_POOR_ROAD_CONDITIONS",
+ "CURRENTLY_UNAVAILABLE",
+ "SAFETY_SHIELD_OFF",
+ "FRONT_COLLISION_NA_FRONT_RADAR_OBSTRUCTION",
+ "PEDAL_MISSAPPLICATION_SYSTEM_ACTIVATED",
+ "SIDE_IMPACT_NA_RADAR_OBSTRUCTION",
+ "WARNING_DO_NOT_ENTER",
+ "SIDE_IMPACT_SYSTEM_OFF",
+ "SIDE_IMPACT_MALFUNCTION",
+ "FRONT_COLLISION_MALFUNCTION",
+ "SIDE_RADAR_MALFUNCTION2",
+ "LKAS_MALFUNCTION2",
+ "FRONT_RADAR_MALFUNCTION2",
+ "PROPILOT_NA_MSGS",
+ "BOTTOM_MSG",
+ "HANDS_ON_WHEEL_WARNING",
+ "WARNING_STEP_ON_BRAKE_NOW",
+ "PROPILOT_NA_FRONT_CAMERA_OBSTRUCTED",
+ "PROPILOT_NA_HIGH_CABIN_TEMP",
+ "WARNING_PROPILOT_MALFUNCTION",
+ "ACC_UNAVAILABLE_HIGH_CABIN_TEMP",
+ "ACC_NA_FRONT_CAMERA_IMPARED",
+ "unknown07",
+ "unknown10",
+ "unknown15",
+ "unknown23",
+ "unknown19",
+ "unknown31",
+ "unknown32",
+ "unknown46",
+ "unknown61",
+ "unknown55",
+ "unknown50",
+ ]}
+
+ if steer_hud_alert:
+ values["HANDS_ON_WHEEL_WARNING"] = 1
+
+ return packer.make_can_msg("PROPILOT_HUD_INFO_MSG", 0, values)
diff --git a/opendbc/car/nissan/radar_interface.py b/opendbc/car/nissan/radar_interface.py
new file mode 100644
index 0000000000..6e552bf618
--- /dev/null
+++ b/opendbc/car/nissan/radar_interface.py
@@ -0,0 +1,4 @@
+from opendbc.car.interfaces import RadarInterfaceBase
+
+class RadarInterface(RadarInterfaceBase):
+ pass
diff --git a/opendbc/car/nissan/values.py b/opendbc/car/nissan/values.py
new file mode 100644
index 0000000000..da396e655e
--- /dev/null
+++ b/opendbc/car/nissan/values.py
@@ -0,0 +1,107 @@
+from dataclasses import dataclass, field
+
+from panda import uds
+from opendbc.car import AngleRateLimit, CarSpecs, DbcDict, PlatformConfig, Platforms, dbc_dict
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarDocs, CarHarness, CarParts
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
+
+Ecu = CarParams.Ecu
+
+
+class CarControllerParams:
+ ANGLE_RATE_LIMIT_UP = AngleRateLimit(speed_bp=[0., 5., 15.], angle_v=[5., .8, .15])
+ ANGLE_RATE_LIMIT_DOWN = AngleRateLimit(speed_bp=[0., 5., 15.], angle_v=[5., 3.5, 0.4])
+ LKAS_MAX_TORQUE = 1 # A value of 1 is easy to overpower
+ STEER_THRESHOLD = 1.0
+
+ def __init__(self, CP):
+ pass
+
+
+@dataclass
+class NissanCarDocs(CarDocs):
+ package: str = "ProPILOT Assist"
+ car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.nissan_a]))
+
+
+@dataclass(frozen=True)
+class NissanCarSpecs(CarSpecs):
+ centerToFrontRatio: float = 0.44
+ steerRatio: float = 17.
+
+
+@dataclass
+class NissanPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('nissan_x_trail_2017_generated', None))
+
+
+class CAR(Platforms):
+ NISSAN_XTRAIL = NissanPlatformConfig(
+ [NissanCarDocs("Nissan X-Trail 2017")],
+ NissanCarSpecs(mass=1610, wheelbase=2.705)
+ )
+ NISSAN_LEAF = NissanPlatformConfig(
+ [NissanCarDocs("Nissan Leaf 2018-23", video_link="https://youtu.be/vaMbtAh_0cY")],
+ NissanCarSpecs(mass=1610, wheelbase=2.705),
+ dbc_dict('nissan_leaf_2018_generated', None),
+ )
+ # Leaf with ADAS ECU found behind instrument cluster instead of glovebox
+ # Currently the only known difference between them is the inverted seatbelt signal.
+ NISSAN_LEAF_IC = NISSAN_LEAF.override(car_docs=[])
+ NISSAN_ROGUE = NissanPlatformConfig(
+ [NissanCarDocs("Nissan Rogue 2018-20")],
+ NissanCarSpecs(mass=1610, wheelbase=2.705)
+ )
+ NISSAN_ALTIMA = NissanPlatformConfig(
+ [NissanCarDocs("Nissan Altima 2019-20", car_parts=CarParts.common([CarHarness.nissan_b]))],
+ NissanCarSpecs(mass=1492, wheelbase=2.824)
+ )
+
+
+DBC = CAR.create_dbc_map()
+
+# Default diagnostic session
+NISSAN_DIAGNOSTIC_REQUEST_KWP = bytes([uds.SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL, 0x81])
+NISSAN_DIAGNOSTIC_RESPONSE_KWP = bytes([uds.SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL + 0x40, 0x81])
+
+# Manufacturer specific
+NISSAN_DIAGNOSTIC_REQUEST_KWP_2 = bytes([uds.SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL, 0xda])
+NISSAN_DIAGNOSTIC_RESPONSE_KWP_2 = bytes([uds.SERVICE_TYPE.DIAGNOSTIC_SESSION_CONTROL + 0x40, 0xda])
+
+NISSAN_VERSION_REQUEST_KWP = b'\x21\x83'
+NISSAN_VERSION_RESPONSE_KWP = b'\x61\x83'
+
+NISSAN_RX_OFFSET = 0x20
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[request for bus, logging in ((0, False), (1, True)) for request in [
+ Request(
+ [NISSAN_DIAGNOSTIC_REQUEST_KWP, NISSAN_VERSION_REQUEST_KWP],
+ [NISSAN_DIAGNOSTIC_RESPONSE_KWP, NISSAN_VERSION_RESPONSE_KWP],
+ bus=bus,
+ logging=logging,
+ ),
+ Request(
+ [NISSAN_DIAGNOSTIC_REQUEST_KWP, NISSAN_VERSION_REQUEST_KWP],
+ [NISSAN_DIAGNOSTIC_RESPONSE_KWP, NISSAN_VERSION_RESPONSE_KWP],
+ rx_offset=NISSAN_RX_OFFSET,
+ bus=bus,
+ logging=logging,
+ ),
+ # Rogue's engine solely responds to this
+ Request(
+ [NISSAN_DIAGNOSTIC_REQUEST_KWP_2, NISSAN_VERSION_REQUEST_KWP],
+ [NISSAN_DIAGNOSTIC_RESPONSE_KWP_2, NISSAN_VERSION_RESPONSE_KWP],
+ bus=bus,
+ logging=logging,
+ ),
+ Request(
+ [StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
+ [StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
+ rx_offset=NISSAN_RX_OFFSET,
+ bus=bus,
+ logging=logging,
+ ),
+ ]],
+)
diff --git a/opendbc/car/panda_runner.py b/opendbc/car/panda_runner.py
new file mode 100644
index 0000000000..2e6221decb
--- /dev/null
+++ b/opendbc/car/panda_runner.py
@@ -0,0 +1,57 @@
+import time
+from contextlib import AbstractContextManager
+
+from panda import Panda
+from opendbc.car.car_helpers import get_car
+from opendbc.car.can_definitions import CanData
+from opendbc.car.structs import CarControl
+
+class PandaRunner(AbstractContextManager):
+ def __enter__(self):
+ self.p = Panda()
+ self.p.reset()
+
+ # setup + fingerprinting
+ self.p.set_safety_mode(Panda.SAFETY_ELM327, 1)
+ self.CI = get_car(self._can_recv, self.p.can_send_many, self.p.set_obd, True)
+ print("fingerprinted", self.CI.CP.carName)
+ assert self.CI.CP.carFingerprint != "mock", "Unable to identify car. Check connections and ensure car is supported."
+
+ self.p.set_safety_mode(Panda.SAFETY_ELM327, 1)
+ self.CI.init(self.CI.CP, self._can_recv, self.p.can_send_many)
+ self.p.set_safety_mode(Panda.SAFETY_TOYOTA, self.CI.CP.safetyConfigs[0].safetyParam)
+
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.p.set_safety_mode(Panda.SAFETY_NOOUTPUT)
+ self.p.reset() # avoid siren
+ return super().__exit__(exc_type, exc_value, traceback)
+
+ @property
+ def panda(self) -> Panda:
+ return self.p
+
+ def _can_recv(self, wait_for_one: bool = False) -> list[list[CanData]]:
+ recv = self.p.can_recv()
+ while len(recv) == 0 and wait_for_one:
+ recv = self.p.can_recv()
+ return [[CanData(addr, dat, bus) for addr, dat, bus in recv], ]
+
+ def read(self, strict: bool = True):
+ cs = self.CI.update([int(time.monotonic()*1e9), self._can_recv()[0]])
+ if strict:
+ assert cs.canValid, "CAN went invalid, check connections"
+ return cs
+
+ def write(self, cc: CarControl) -> None:
+ if cc.enabled and not self.p.health()['controls_allowed']:
+ # prevent the car from faulting. print a warning?
+ cc = CarControl(enabled=False)
+ _, can_sends = self.CI.apply(cc)
+ self.p.can_send_many(can_sends, timeout=25)
+ self.p.send_heartbeat()
+
+if __name__ == "__main__":
+ with PandaRunner() as p:
+ print(p.read())
diff --git a/opendbc/car/structs.py b/opendbc/car/structs.py
new file mode 100644
index 0000000000..c375468353
--- /dev/null
+++ b/opendbc/car/structs.py
@@ -0,0 +1,499 @@
+from dataclasses import dataclass as _dataclass, field, is_dataclass
+from enum import Enum, StrEnum as _StrEnum, auto
+from typing import dataclass_transform, get_origin
+
+AUTO_OBJ = object()
+
+
+def auto_field():
+ return AUTO_OBJ
+
+
+@dataclass_transform()
+def auto_dataclass(cls=None, /, **kwargs):
+ cls_annotations = cls.__dict__.get('__annotations__', {})
+ for name, typ in cls_annotations.items():
+ current_value = getattr(cls, name)
+ if current_value is AUTO_OBJ:
+ origin_typ = get_origin(typ) or typ
+ if isinstance(origin_typ, str):
+ raise TypeError(f"Forward references are not supported for auto_field: '{origin_typ}'. Use a default_factory with lambda instead.")
+ elif origin_typ in (int, float, str, bytes, list, tuple, bool) or is_dataclass(origin_typ):
+ setattr(cls, name, field(default_factory=origin_typ))
+ elif issubclass(origin_typ, Enum): # first enum is the default
+ setattr(cls, name, field(default=next(iter(origin_typ))))
+ else:
+ raise TypeError(f"Unsupported type for auto_field: {origin_typ}")
+
+ # TODO: use slots, this prevents accidentally setting attributes that don't exist
+ return _dataclass(cls, **kwargs)
+
+
+class StrEnum(_StrEnum):
+ @staticmethod
+ def _generate_next_value_(name, *args):
+ # auto() defaults to name.lower()
+ return name
+
+
+@auto_dataclass
+class CarState:
+ # CAN health
+ canValid: bool = auto_field() # invalid counter/checksums
+ canTimeout: bool = auto_field() # CAN bus dropped out
+ canErrorCounter: int = auto_field()
+
+ # car speed
+ vEgo: float = auto_field() # best estimate of speed
+ aEgo: float = auto_field() # best estimate of acceleration
+ vEgoRaw: float = auto_field() # unfiltered speed from CAN sensors
+ vEgoCluster: float = auto_field() # best estimate of speed shown on car's instrument cluster, used for UI
+
+ yawRate: float = auto_field() # best estimate of yaw rate
+ standstill: bool = auto_field()
+ wheelSpeeds: 'CarState.WheelSpeeds' = field(default_factory=lambda: CarState.WheelSpeeds())
+
+ # gas pedal, 0.0-1.0
+ gas: float = auto_field() # this is user pedal only
+ gasPressed: bool = auto_field() # this is user pedal only
+
+ engineRpm: float = auto_field()
+
+ # brake pedal, 0.0-1.0
+ brake: float = auto_field() # this is user pedal only
+ brakePressed: bool = auto_field() # this is user pedal only
+ regenBraking: bool = auto_field() # this is user pedal only
+ parkingBrake: bool = auto_field()
+ brakeHoldActive: bool = auto_field()
+
+ # steering wheel
+ steeringAngleDeg: float = auto_field()
+ steeringAngleOffsetDeg: float = auto_field() # Offset betweens sensors in case there multiple
+ steeringRateDeg: float = auto_field()
+ steeringTorque: float = auto_field() # TODO: standardize units
+ steeringTorqueEps: float = auto_field() # TODO: standardize units
+ steeringPressed: bool = auto_field() # if the user is using the steering wheel
+ steerFaultTemporary: bool = auto_field() # temporary EPS fault
+ steerFaultPermanent: bool = auto_field() # permanent EPS fault
+ stockAeb: bool = auto_field()
+ stockFcw: bool = auto_field()
+ espDisabled: bool = auto_field()
+ accFaulted: bool = auto_field()
+ carFaultedNonCritical: bool = auto_field() # some ECU is faulted, but car remains controllable
+ espActive: bool = auto_field()
+ vehicleSensorsInvalid: bool = auto_field() # invalid steering angle readings, etc.
+
+ # cruise state
+ cruiseState: 'CarState.CruiseState' = field(default_factory=lambda: CarState.CruiseState())
+
+ # gear
+ gearShifter: 'CarState.GearShifter' = field(default_factory=lambda: CarState.GearShifter.unknown)
+
+ # button presses
+ buttonEvents: list['CarState.ButtonEvent'] = auto_field()
+ leftBlinker: bool = auto_field()
+ rightBlinker: bool = auto_field()
+ genericToggle: bool = auto_field()
+
+ # lock info
+ doorOpen: bool = auto_field()
+ seatbeltUnlatched: bool = auto_field()
+
+ # clutch (manual transmission only)
+ clutchPressed: bool = auto_field()
+
+ # blindspot sensors
+ leftBlindspot: bool = auto_field() # Is there something blocking the left lane change
+ rightBlindspot: bool = auto_field() # Is there something blocking the right lane change
+
+ fuelGauge: float = auto_field() # battery or fuel tank level from 0.0 to 1.0
+ charging: bool = auto_field()
+
+ # process meta
+ cumLagMs: float = auto_field()
+
+ @auto_dataclass
+ class WheelSpeeds:
+ # optional wheel speeds
+ fl: float = auto_field()
+ fr: float = auto_field()
+ rl: float = auto_field()
+ rr: float = auto_field()
+
+ @auto_dataclass
+ class CruiseState:
+ enabled: bool = auto_field()
+ speed: float = auto_field()
+ speedCluster: float = auto_field() # Set speed as shown on instrument cluster
+ available: bool = auto_field()
+ speedOffset: float = auto_field()
+ standstill: bool = auto_field()
+ nonAdaptive: bool = auto_field()
+
+ class GearShifter(StrEnum):
+ unknown = auto()
+ park = auto()
+ drive = auto()
+ neutral = auto()
+ reverse = auto()
+ sport = auto()
+ low = auto()
+ brake = auto()
+ eco = auto()
+ manumatic = auto()
+
+ # send on change
+ @auto_dataclass
+ class ButtonEvent:
+ pressed: bool = auto_field()
+ type: 'CarState.ButtonEvent.Type' = field(default_factory=lambda: CarState.ButtonEvent.Type.unknown)
+
+ class Type(StrEnum):
+ unknown = auto()
+ leftBlinker = auto()
+ rightBlinker = auto()
+ accelCruise = auto()
+ decelCruise = auto()
+ cancel = auto()
+ altButton1 = auto()
+ altButton2 = auto()
+ altButton3 = auto()
+ setCruise = auto()
+ resumeCruise = auto()
+ gapAdjustCruise = auto()
+
+
+@auto_dataclass
+class RadarData:
+ errors: list['Error'] = auto_field()
+ points: list['RadarPoint'] = auto_field()
+
+ class Error(StrEnum):
+ canError = auto()
+ fault = auto()
+ wrongConfig = auto()
+
+ @auto_dataclass
+ class RadarPoint:
+ trackId: int = auto_field() # no trackId reuse
+
+ # these 3 are the minimum required
+ dRel: float = auto_field() # m from the front bumper of the car
+ yRel: float = auto_field() # m
+ vRel: float = auto_field() # m/s
+
+ # these are optional and valid if they are not NaN
+ aRel: float = auto_field() # m/s^2
+ yvRel: float = auto_field() # m/s
+
+ # some radars flag measurements VS estimates
+ measured: bool = auto_field()
+
+
+@auto_dataclass
+class CarControl:
+ # must be true for any actuator commands to work
+ enabled: bool = auto_field()
+ latActive: bool = auto_field()
+ longActive: bool = auto_field()
+
+ # Actuator commands as computed by controlsd
+ actuators: 'CarControl.Actuators' = field(default_factory=lambda: CarControl.Actuators())
+
+ leftBlinker: bool = auto_field()
+ rightBlinker: bool = auto_field()
+
+ orientationNED: list[float] = auto_field()
+ angularVelocity: list[float] = auto_field()
+
+ cruiseControl: 'CarControl.CruiseControl' = field(default_factory=lambda: CarControl.CruiseControl())
+ hudControl: 'CarControl.HUDControl' = field(default_factory=lambda: CarControl.HUDControl())
+
+ @auto_dataclass
+ class Actuators:
+ # range from 0.0 - 1.0
+ gas: float = auto_field()
+ brake: float = auto_field()
+ # range from -1.0 - 1.0
+ steer: float = auto_field()
+ # value sent over can to the car
+ steerOutputCan: float = auto_field()
+ steeringAngleDeg: float = auto_field()
+
+ curvature: float = auto_field()
+
+ speed: float = auto_field() # m/s
+ accel: float = auto_field() # m/s^2
+ longControlState: 'CarControl.Actuators.LongControlState' = field(default_factory=lambda: CarControl.Actuators.LongControlState.off)
+
+ class LongControlState(StrEnum):
+ off = auto()
+ pid = auto()
+ stopping = auto()
+ starting = auto()
+
+ @auto_dataclass
+ class CruiseControl:
+ cancel: bool = auto_field()
+ resume: bool = auto_field()
+ override: bool = auto_field()
+
+ @auto_dataclass
+ class HUDControl:
+ speedVisible: bool = auto_field()
+ setSpeed: float = auto_field()
+ lanesVisible: bool = auto_field()
+ leadVisible: bool = auto_field()
+ visualAlert: 'CarControl.HUDControl.VisualAlert' = field(default_factory=lambda: CarControl.HUDControl.VisualAlert.none)
+ audibleAlert: 'CarControl.HUDControl.AudibleAlert' = field(default_factory=lambda: CarControl.HUDControl.AudibleAlert.none)
+ rightLaneVisible: bool = auto_field()
+ leftLaneVisible: bool = auto_field()
+ rightLaneDepart: bool = auto_field()
+ leftLaneDepart: bool = auto_field()
+ leadDistanceBars: int = auto_field() # 1-3: 1 is closest, 3 is farthest. some ports may utilize 2-4 bars instead
+
+ class VisualAlert(StrEnum):
+ # these are the choices from the Honda
+ # map as good as you can for your car
+ none = auto()
+ fcw = auto()
+ steerRequired = auto()
+ brakePressed = auto()
+ wrongGear = auto()
+ seatbeltUnbuckled = auto()
+ speedTooHigh = auto()
+ ldw = auto()
+
+ class AudibleAlert(StrEnum):
+ none = auto()
+
+ engage = auto()
+ disengage = auto()
+ refuse = auto()
+
+ warningSoft = auto()
+ warningImmediate = auto()
+
+ prompt = auto()
+ promptRepeat = auto()
+ promptDistracted = auto()
+
+
+@auto_dataclass
+class CarParams:
+ carName: str = auto_field()
+ carFingerprint: str = auto_field()
+ fuzzyFingerprint: bool = auto_field()
+
+ notCar: bool = auto_field() # flag for non-car robotics platforms
+
+ pcmCruise: bool = auto_field() # is openpilot's state tied to the PCM's cruise state?
+ enableDsu: bool = auto_field() # driving support unit
+ enableBsm: bool = auto_field() # blind spot monitoring
+ flags: int = auto_field() # flags for car specific quirks
+ experimentalLongitudinalAvailable: bool = auto_field()
+
+ minEnableSpeed: float = auto_field()
+ minSteerSpeed: float = auto_field()
+ safetyConfigs: list['CarParams.SafetyConfig'] = auto_field()
+ alternativeExperience: int = auto_field() # panda flag for features like no disengage on gas
+
+ maxLateralAccel: float = auto_field()
+ autoResumeSng: bool = auto_field() # describes whether car can resume from a stop automatically
+
+ mass: float = auto_field() # [kg] curb weight: all fluids no cargo
+ wheelbase: float = auto_field() # [m] distance from rear axle to front axle
+ centerToFront: float = auto_field() # [m] distance from center of mass to front axle
+ steerRatio: float = auto_field() # [] ratio of steering wheel angle to front wheel angle
+ steerRatioRear: float = auto_field() # [] ratio of steering wheel angle to rear wheel angle (usually 0)
+
+ rotationalInertia: float = auto_field() # [kg*m2] body rotational inertia
+ tireStiffnessFactor: float = auto_field() # scaling factor used in calculating tireStiffness[Front,Rear]
+ tireStiffnessFront: float = auto_field() # [N/rad] front tire coeff of stiff
+ tireStiffnessRear: float = auto_field() # [N/rad] rear tire coeff of stiff
+
+ longitudinalTuning: 'CarParams.LongitudinalPIDTuning' = field(default_factory=lambda: CarParams.LongitudinalPIDTuning())
+ lateralParams: 'CarParams.LateralParams' = field(default_factory=lambda: CarParams.LateralParams())
+ lateralTuning: 'CarParams.LateralTuning' = field(default_factory=lambda: CarParams.LateralTuning())
+
+ @auto_dataclass
+ class LateralTuning:
+ def init(self, which: str):
+ self.which = CarParams.LateralTuning.Which(which)
+
+ class Which(StrEnum):
+ pid = auto()
+ torque = auto()
+
+ def __call__(self):
+ return self.value
+
+ which: 'CarParams.LateralTuning.Which' = field(default_factory=lambda: CarParams.LateralTuning.Which.pid)
+
+ pid: 'CarParams.LateralPIDTuning' = field(default_factory=lambda: CarParams.LateralPIDTuning())
+ torque: 'CarParams.LateralTorqueTuning' = field(default_factory=lambda: CarParams.LateralTorqueTuning())
+
+ @auto_dataclass
+ class SafetyConfig:
+ safetyModel: 'CarParams.SafetyModel' = field(default_factory=lambda: CarParams.SafetyModel.silent)
+ safetyParam: int = auto_field()
+
+ @auto_dataclass
+ class LateralParams:
+ torqueBP: list[int] = auto_field()
+ torqueV: list[int] = auto_field()
+
+ @auto_dataclass
+ class LateralPIDTuning:
+ kpBP: list[float] = auto_field()
+ kpV: list[float] = auto_field()
+ kiBP: list[float] = auto_field()
+ kiV: list[float] = auto_field()
+ kf: float = auto_field()
+
+ @auto_dataclass
+ class LateralTorqueTuning:
+ useSteeringAngle: bool = auto_field()
+ kp: float = auto_field()
+ ki: float = auto_field()
+ friction: float = auto_field()
+ kf: float = auto_field()
+ steeringAngleDeadzoneDeg: float = auto_field()
+ latAccelFactor: float = auto_field()
+ latAccelOffset: float = auto_field()
+
+ steerLimitAlert: bool = auto_field()
+ steerLimitTimer: float = auto_field() # time before steerLimitAlert is issued
+
+ vEgoStopping: float = auto_field() # Speed at which the car goes into stopping state
+ vEgoStarting: float = auto_field() # Speed at which the car goes into starting state
+ stoppingControl: bool = auto_field() # Does the car allow full control even at lows speeds when stopping
+ steerControlType: 'CarParams.SteerControlType' = field(default_factory=lambda: CarParams.SteerControlType.torque)
+ radarUnavailable: bool = auto_field() # True when radar objects aren't visible on CAN or aren't parsed out
+ stopAccel: float = auto_field() # Required acceleration to keep vehicle stationary
+ stoppingDecelRate: float = auto_field() # m/s^2/s while trying to stop
+ startAccel: float = auto_field() # Required acceleration to get car moving
+ startingState: bool = auto_field() # Does this car make use of special starting state
+
+ steerActuatorDelay: float = auto_field() # Steering wheel actuator delay in seconds
+ longitudinalActuatorDelay: float = auto_field() # Gas/Brake actuator delay in seconds
+ openpilotLongitudinalControl: bool = auto_field() # is openpilot doing the longitudinal control?
+ carVin: str = auto_field() # VIN number queried during fingerprinting
+ dashcamOnly: bool = auto_field()
+ passive: bool = auto_field() # is openpilot in control?
+ transmissionType: 'CarParams.TransmissionType' = field(default_factory=lambda: CarParams.TransmissionType.unknown)
+ carFw: list['CarParams.CarFw'] = auto_field()
+
+ radarTimeStep: float = 0.05 # time delta between radar updates, 20Hz is very standard
+ fingerprintSource: 'CarParams.FingerprintSource' = field(default_factory=lambda: CarParams.FingerprintSource.can)
+ # Where Panda/C2 is integrated into the car's CAN network
+ networkLocation: 'CarParams.NetworkLocation' = field(default_factory=lambda: CarParams.NetworkLocation.fwdCamera)
+
+ wheelSpeedFactor: float = auto_field() # Multiplier on wheels speeds to computer actual speeds
+
+ @auto_dataclass
+ class LongitudinalPIDTuning:
+ kpBP: list[float] = auto_field()
+ kpV: list[float] = auto_field()
+ kiBP: list[float] = auto_field()
+ kiV: list[float] = auto_field()
+ kf: float = auto_field()
+
+ class SafetyModel(StrEnum):
+ silent = auto()
+ hondaNidec = auto()
+ toyota = auto()
+ elm327 = auto()
+ gm = auto()
+ hondaBoschGiraffe = auto()
+ ford = auto()
+ cadillac = auto()
+ hyundai = auto()
+ chrysler = auto()
+ tesla = auto()
+ subaru = auto()
+ gmPassive = auto()
+ mazda = auto()
+ nissan = auto()
+ volkswagen = auto()
+ toyotaIpas = auto()
+ allOutput = auto()
+ gmAscm = auto()
+ noOutput = auto() # like silent but without silent CAN TXs
+ hondaBosch = auto()
+ volkswagenPq = auto()
+ subaruPreglobal = auto() # pre-Global platform
+ hyundaiLegacy = auto()
+ hyundaiCommunity = auto()
+ volkswagenMlb = auto()
+ hongqi = auto()
+ body = auto()
+ hyundaiCanfd = auto()
+ volkswagenMqbEvo = auto()
+ chryslerCusw = auto()
+ psa = auto()
+
+ class SteerControlType(StrEnum):
+ torque = auto()
+ angle = auto()
+
+ class TransmissionType(StrEnum):
+ unknown = auto()
+ automatic = auto() # Traditional auto, including DSG
+ manual = auto() # True "stick shift" only
+ direct = auto() # Electric vehicle or other direct drive
+ cvt = auto()
+
+ @auto_dataclass
+ class CarFw:
+ ecu: 'CarParams.Ecu' = field(default_factory=lambda: CarParams.Ecu.unknown)
+ fwVersion: bytes = auto_field()
+ address: int = auto_field()
+ subAddress: int = auto_field()
+ responseAddress: int = auto_field()
+ request: list[bytes] = auto_field()
+ brand: str = auto_field()
+ bus: int = auto_field()
+ logging: bool = auto_field()
+ obdMultiplexing: bool = auto_field()
+
+ class Ecu(StrEnum):
+ eps = auto()
+ abs = auto()
+ fwdRadar = auto()
+ fwdCamera = auto()
+ engine = auto()
+ unknown = auto()
+ transmission = auto() # Transmission Control Module
+ hybrid = auto() # hybrid control unit, e.g. Chrysler's HCP, Honda's IMA Control Unit, Toyota's hybrid control computer
+ srs = auto() # airbag
+ gateway = auto() # can gateway
+ hud = auto() # heads up display
+ combinationMeter = auto() # instrument cluster
+ electricBrakeBooster = auto()
+ shiftByWire = auto()
+ adas = auto()
+ cornerRadar = auto()
+ hvac = auto()
+ parkingAdas = auto() # parking assist system ECU, e.g. Toyota's IPAS, Hyundai's RSPA, etc.
+ epb = auto() # electronic parking brake
+ telematics = auto()
+ body = auto() # body control module
+
+ # Toyota only
+ dsu = auto()
+
+ # Honda only
+ vsa = auto() # Vehicle Stability Assist
+ programmedFuelInjection = auto()
+
+ debug = auto()
+
+ class FingerprintSource(StrEnum):
+ can = auto()
+ fw = auto()
+ fixed = auto()
+
+ class NetworkLocation(StrEnum):
+ fwdCamera = auto() # Standard/default integration at LKAS camera
+ gateway = auto() # Integration at vehicle's CAN gateway
diff --git a/opendbc/car/subaru/__init__.py b/opendbc/car/subaru/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/subaru/carcontroller.py b/opendbc/car/subaru/carcontroller.py
new file mode 100644
index 0000000000..4ee3655268
--- /dev/null
+++ b/opendbc/car/subaru/carcontroller.py
@@ -0,0 +1,144 @@
+import copy
+from opendbc.can.packer import CANPacker
+from opendbc.car import apply_driver_steer_torque_limits, common_fault_avoidance, make_tester_present_msg
+from opendbc.car.common.numpy_fast import clip, interp
+from opendbc.car.interfaces import CarControllerBase
+from opendbc.car.subaru import subarucan
+from opendbc.car.subaru.values import DBC, GLOBAL_ES_ADDR, CanBus, CarControllerParams, SubaruFlags
+
+# FIXME: These limits aren't exact. The real limit is more than likely over a larger time period and
+# involves the total steering angle change rather than rate, but these limits work well for now
+MAX_STEER_RATE = 25 # deg/s
+MAX_STEER_RATE_FRAMES = 7 # tx control frames needed before torque can be cut
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.apply_steer_last = 0
+
+ self.cruise_button_prev = 0
+ self.steer_rate_counter = 0
+
+ self.p = CarControllerParams(CP)
+ self.packer = CANPacker(DBC[CP.carFingerprint]['pt'])
+
+ def update(self, CC, CS, now_nanos):
+ actuators = CC.actuators
+ hud_control = CC.hudControl
+ pcm_cancel_cmd = CC.cruiseControl.cancel
+
+ can_sends = []
+
+ # *** steering ***
+ if (self.frame % self.p.STEER_STEP) == 0:
+ apply_steer = int(round(actuators.steer * self.p.STEER_MAX))
+
+ # limits due to driver torque
+
+ new_steer = int(round(apply_steer))
+ apply_steer = apply_driver_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.p)
+
+ if not CC.latActive:
+ apply_steer = 0
+
+ if self.CP.flags & SubaruFlags.PREGLOBAL:
+ can_sends.append(subarucan.create_preglobal_steering_control(self.packer, self.frame // self.p.STEER_STEP, apply_steer, CC.latActive))
+ else:
+ apply_steer_req = CC.latActive
+
+ if self.CP.flags & SubaruFlags.STEER_RATE_LIMITED:
+ # Steering rate fault prevention
+ self.steer_rate_counter, apply_steer_req = \
+ common_fault_avoidance(abs(CS.out.steeringRateDeg) > MAX_STEER_RATE, apply_steer_req,
+ self.steer_rate_counter, MAX_STEER_RATE_FRAMES)
+
+ can_sends.append(subarucan.create_steering_control(self.packer, apply_steer, apply_steer_req))
+
+ self.apply_steer_last = apply_steer
+
+ # *** longitudinal ***
+
+ if CC.longActive:
+ apply_throttle = int(round(interp(actuators.accel, CarControllerParams.THROTTLE_LOOKUP_BP, CarControllerParams.THROTTLE_LOOKUP_V)))
+ apply_rpm = int(round(interp(actuators.accel, CarControllerParams.RPM_LOOKUP_BP, CarControllerParams.RPM_LOOKUP_V)))
+ apply_brake = int(round(interp(actuators.accel, CarControllerParams.BRAKE_LOOKUP_BP, CarControllerParams.BRAKE_LOOKUP_V)))
+
+ # limit min and max values
+ cruise_throttle = clip(apply_throttle, CarControllerParams.THROTTLE_MIN, CarControllerParams.THROTTLE_MAX)
+ cruise_rpm = clip(apply_rpm, CarControllerParams.RPM_MIN, CarControllerParams.RPM_MAX)
+ cruise_brake = clip(apply_brake, CarControllerParams.BRAKE_MIN, CarControllerParams.BRAKE_MAX)
+ else:
+ cruise_throttle = CarControllerParams.THROTTLE_INACTIVE
+ cruise_rpm = CarControllerParams.RPM_MIN
+ cruise_brake = CarControllerParams.BRAKE_MIN
+
+ # *** alerts and pcm cancel ***
+ if self.CP.flags & SubaruFlags.PREGLOBAL:
+ if self.frame % 5 == 0:
+ # 1 = main, 2 = set shallow, 3 = set deep, 4 = resume shallow, 5 = resume deep
+ # disengage ACC when OP is disengaged
+ if pcm_cancel_cmd:
+ cruise_button = 1
+ # turn main on if off and past start-up state
+ elif not CS.out.cruiseState.available and CS.ready:
+ cruise_button = 1
+ else:
+ cruise_button = CS.cruise_button
+
+ # unstick previous mocked button press
+ if cruise_button == 1 and self.cruise_button_prev == 1:
+ cruise_button = 0
+ self.cruise_button_prev = cruise_button
+
+ can_sends.append(subarucan.create_preglobal_es_distance(self.packer, cruise_button, CS.es_distance_msg))
+
+ else:
+ if self.frame % 10 == 0:
+ can_sends.append(subarucan.create_es_dashstatus(self.packer, self.frame // 10, CS.es_dashstatus_msg, CC.enabled,
+ self.CP.openpilotLongitudinalControl, CC.longActive, hud_control.leadVisible))
+
+ can_sends.append(subarucan.create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.enabled, hud_control.visualAlert,
+ hud_control.leftLaneVisible, hud_control.rightLaneVisible,
+ hud_control.leftLaneDepart, hud_control.rightLaneDepart))
+
+ if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT:
+ can_sends.append(subarucan.create_es_infotainment(self.packer, self.frame // 10, CS.es_infotainment_msg, hud_control.visualAlert))
+
+ if self.CP.openpilotLongitudinalControl:
+ if self.frame % 5 == 0:
+ can_sends.append(subarucan.create_es_status(self.packer, self.frame // 5, CS.es_status_msg,
+ self.CP.openpilotLongitudinalControl, CC.longActive, cruise_rpm))
+
+ can_sends.append(subarucan.create_es_brake(self.packer, self.frame // 5, CS.es_brake_msg,
+ self.CP.openpilotLongitudinalControl, CC.longActive, cruise_brake))
+
+ can_sends.append(subarucan.create_es_distance(self.packer, self.frame // 5, CS.es_distance_msg, 0, pcm_cancel_cmd,
+ self.CP.openpilotLongitudinalControl, cruise_brake > 0, cruise_throttle))
+ else:
+ if pcm_cancel_cmd:
+ if not (self.CP.flags & SubaruFlags.HYBRID):
+ bus = CanBus.alt if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else CanBus.main
+ can_sends.append(subarucan.create_es_distance(self.packer, CS.es_distance_msg["COUNTER"] + 1, CS.es_distance_msg, bus, pcm_cancel_cmd))
+
+ if self.CP.flags & SubaruFlags.DISABLE_EYESIGHT:
+ # Tester present (keeps eyesight disabled)
+ if self.frame % 100 == 0:
+ can_sends.append(make_tester_present_msg(GLOBAL_ES_ADDR, CanBus.camera, suppress_response=True))
+
+ # Create all of the other eyesight messages to keep the rest of the car happy when eyesight is disabled
+ if self.frame % 5 == 0:
+ can_sends.append(subarucan.create_es_highbeamassist(self.packer))
+
+ if self.frame % 10 == 0:
+ can_sends.append(subarucan.create_es_static_1(self.packer))
+
+ if self.frame % 2 == 0:
+ can_sends.append(subarucan.create_es_static_2(self.packer))
+
+ new_actuators = copy.copy(actuators)
+ new_actuators.steer = self.apply_steer_last / self.p.STEER_MAX
+ new_actuators.steerOutputCan = self.apply_steer_last
+
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/subaru/carstate.py b/opendbc/car/subaru/carstate.py
new file mode 100644
index 0000000000..84fa215815
--- /dev/null
+++ b/opendbc/car/subaru/carstate.py
@@ -0,0 +1,228 @@
+import copy
+from opendbc.can.can_define import CANDefine
+from opendbc.can.parser import CANParser
+from opendbc.car import structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.interfaces import CarStateBase
+from opendbc.car.subaru.values import DBC, CanBus, SubaruFlags
+from opendbc.car import CanSignalRateCalculator
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+ self.shifter_values = can_define.dv["Transmission"]["Gear"]
+
+ self.angle_rate_calulator = CanSignalRateCalculator(50)
+
+ def update(self, cp, cp_cam, _, cp_body, __) -> structs.CarState:
+ ret = structs.CarState()
+
+ throttle_msg = cp.vl["Throttle"] if not (self.CP.flags & SubaruFlags.HYBRID) else cp_body.vl["Throttle_Hybrid"]
+ ret.gas = throttle_msg["Throttle_Pedal"] / 255.
+
+ ret.gasPressed = ret.gas > 1e-5
+ if self.CP.flags & SubaruFlags.PREGLOBAL:
+ ret.brakePressed = cp.vl["Brake_Pedal"]["Brake_Pedal"] > 0
+ else:
+ cp_brakes = cp_body if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp
+ ret.brakePressed = cp_brakes.vl["Brake_Status"]["Brake"] == 1
+
+ cp_es_distance = cp_body if self.CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID) else cp_cam
+ if not (self.CP.flags & SubaruFlags.HYBRID):
+ eyesight_fault = bool(cp_es_distance.vl["ES_Distance"]["Cruise_Fault"])
+
+ # if openpilot is controlling long, an eyesight fault is a non-critical fault. otherwise it's an ACC fault
+ if self.CP.openpilotLongitudinalControl:
+ ret.carFaultedNonCritical = eyesight_fault
+ else:
+ ret.accFaulted = eyesight_fault
+
+ cp_wheels = cp_body if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ cp_wheels.vl["Wheel_Speeds"]["FL"],
+ cp_wheels.vl["Wheel_Speeds"]["FR"],
+ cp_wheels.vl["Wheel_Speeds"]["RL"],
+ cp_wheels.vl["Wheel_Speeds"]["RR"],
+ )
+ ret.vEgoRaw = (ret.wheelSpeeds.fl + ret.wheelSpeeds.fr + ret.wheelSpeeds.rl + ret.wheelSpeeds.rr) / 4.
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.standstill = ret.vEgoRaw == 0
+
+ # continuous blinker signals for assisted lane change
+ ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(50, cp.vl["Dashlights"]["LEFT_BLINKER"],
+ cp.vl["Dashlights"]["RIGHT_BLINKER"])
+
+ if self.CP.enableBsm:
+ ret.leftBlindspot = (cp.vl["BSD_RCTA"]["L_ADJACENT"] == 1) or (cp.vl["BSD_RCTA"]["L_APPROACHING"] == 1)
+ ret.rightBlindspot = (cp.vl["BSD_RCTA"]["R_ADJACENT"] == 1) or (cp.vl["BSD_RCTA"]["R_APPROACHING"] == 1)
+
+ cp_transmission = cp_body if self.CP.flags & SubaruFlags.HYBRID else cp
+ can_gear = int(cp_transmission.vl["Transmission"]["Gear"])
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None))
+
+ ret.steeringAngleDeg = cp.vl["Steering_Torque"]["Steering_Angle"]
+
+ if not (self.CP.flags & SubaruFlags.PREGLOBAL):
+ # ideally we get this from the car, but unclear if it exists. diagnostic software doesn't even have it
+ ret.steeringRateDeg = self.angle_rate_calulator.update(ret.steeringAngleDeg, cp.vl["Steering_Torque"]["COUNTER"])
+
+ ret.steeringTorque = cp.vl["Steering_Torque"]["Steer_Torque_Sensor"]
+ ret.steeringTorqueEps = cp.vl["Steering_Torque"]["Steer_Torque_Output"]
+
+ steer_threshold = 75 if self.CP.flags & SubaruFlags.PREGLOBAL else 80
+ ret.steeringPressed = abs(ret.steeringTorque) > steer_threshold
+
+ cp_cruise = cp_body if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp
+ if self.CP.flags & SubaruFlags.HYBRID:
+ ret.cruiseState.enabled = cp_cam.vl["ES_DashStatus"]['Cruise_Activated'] != 0
+ ret.cruiseState.available = cp_cam.vl["ES_DashStatus"]['Cruise_On'] != 0
+ else:
+ ret.cruiseState.enabled = cp_cruise.vl["CruiseControl"]["Cruise_Activated"] != 0
+ ret.cruiseState.available = cp_cruise.vl["CruiseControl"]["Cruise_On"] != 0
+ ret.cruiseState.speed = cp_cam.vl["ES_DashStatus"]["Cruise_Set_Speed"] * CV.KPH_TO_MS
+
+ if (self.CP.flags & SubaruFlags.PREGLOBAL and cp.vl["Dash_State2"]["UNITS"] == 1) or \
+ (not (self.CP.flags & SubaruFlags.PREGLOBAL) and cp.vl["Dashlights"]["UNITS"] == 1):
+ ret.cruiseState.speed *= CV.MPH_TO_KPH
+
+ ret.seatbeltUnlatched = cp.vl["Dashlights"]["SEATBELT_FL"] == 1
+ ret.doorOpen = any([cp.vl["BodyInfo"]["DOOR_OPEN_RR"],
+ cp.vl["BodyInfo"]["DOOR_OPEN_RL"],
+ cp.vl["BodyInfo"]["DOOR_OPEN_FR"],
+ cp.vl["BodyInfo"]["DOOR_OPEN_FL"]])
+ ret.steerFaultPermanent = cp.vl["Steering_Torque"]["Steer_Error_1"] == 1
+
+ if self.CP.flags & SubaruFlags.PREGLOBAL:
+ self.cruise_button = cp_cam.vl["ES_Distance"]["Cruise_Button"]
+ self.ready = not cp_cam.vl["ES_DashStatus"]["Not_Ready_Startup"]
+ else:
+ ret.steerFaultTemporary = cp.vl["Steering_Torque"]["Steer_Warning"] == 1
+ ret.cruiseState.nonAdaptive = cp_cam.vl["ES_DashStatus"]["Conventional_Cruise"] == 1
+ ret.cruiseState.standstill = cp_cam.vl["ES_DashStatus"]["Cruise_State"] == 3
+ ret.stockFcw = (cp_cam.vl["ES_LKAS_State"]["LKAS_Alert"] == 1) or \
+ (cp_cam.vl["ES_LKAS_State"]["LKAS_Alert"] == 2)
+
+ self.es_lkas_state_msg = copy.copy(cp_cam.vl["ES_LKAS_State"])
+ cp_es_brake = cp_body if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp_cam
+ self.es_brake_msg = copy.copy(cp_es_brake.vl["ES_Brake"])
+ cp_es_status = cp_body if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp_cam
+
+ # TODO: Hybrid cars don't have ES_Distance, need a replacement
+ if not (self.CP.flags & SubaruFlags.HYBRID):
+ # 8 is known AEB, there are a few other values related to AEB we ignore
+ ret.stockAeb = (cp_es_distance.vl["ES_Brake"]["AEB_Status"] == 8) and \
+ (cp_es_distance.vl["ES_Brake"]["Brake_Pressure"] != 0)
+
+ self.es_status_msg = copy.copy(cp_es_status.vl["ES_Status"])
+ self.cruise_control_msg = copy.copy(cp_cruise.vl["CruiseControl"])
+
+ if not (self.CP.flags & SubaruFlags.HYBRID):
+ self.es_distance_msg = copy.copy(cp_es_distance.vl["ES_Distance"])
+
+ self.es_dashstatus_msg = copy.copy(cp_cam.vl["ES_DashStatus"])
+ if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT:
+ self.es_infotainment_msg = copy.copy(cp_cam.vl["ES_Infotainment"])
+
+ return ret
+
+ @staticmethod
+ def get_common_global_body_messages(CP):
+ messages = [
+ ("Wheel_Speeds", 50),
+ ("Brake_Status", 50),
+ ]
+
+ if not (CP.flags & SubaruFlags.HYBRID):
+ messages.append(("CruiseControl", 20))
+
+ return messages
+
+ @staticmethod
+ def get_common_global_es_messages(CP):
+ messages = [
+ ("ES_Brake", 20),
+ ]
+
+ if not (CP.flags & SubaruFlags.HYBRID):
+ messages += [
+ ("ES_Distance", 20),
+ ("ES_Status", 20)
+ ]
+
+ return messages
+
+ @staticmethod
+ def get_common_preglobal_body_messages():
+ messages = [
+ ("CruiseControl", 50),
+ ("Wheel_Speeds", 50),
+ ("Dash_State2", 1),
+ ]
+
+ return messages
+
+ @staticmethod
+ def get_can_parser(CP):
+ messages = [
+ # sig_address, frequency
+ ("Dashlights", 10),
+ ("Steering_Torque", 50),
+ ("BodyInfo", 1),
+ ("Brake_Pedal", 50),
+ ]
+
+ if not (CP.flags & SubaruFlags.HYBRID):
+ messages += [
+ ("Throttle", 100),
+ ("Transmission", 100)
+ ]
+
+ if CP.enableBsm:
+ messages.append(("BSD_RCTA", 17))
+
+ if not (CP.flags & SubaruFlags.PREGLOBAL):
+ if not (CP.flags & SubaruFlags.GLOBAL_GEN2):
+ messages += CarState.get_common_global_body_messages(CP)
+ else:
+ messages += CarState.get_common_preglobal_body_messages()
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus.main)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ if CP.flags & SubaruFlags.PREGLOBAL:
+ messages = [
+ ("ES_DashStatus", 20),
+ ("ES_Distance", 20),
+ ]
+ else:
+ messages = [
+ ("ES_DashStatus", 10),
+ ("ES_LKAS_State", 10),
+ ]
+
+ if not (CP.flags & SubaruFlags.GLOBAL_GEN2):
+ messages += CarState.get_common_global_es_messages(CP)
+
+ if CP.flags & SubaruFlags.SEND_INFOTAINMENT:
+ messages.append(("ES_Infotainment", 10))
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus.camera)
+
+ @staticmethod
+ def get_body_can_parser(CP):
+ messages = []
+
+ if CP.flags & SubaruFlags.GLOBAL_GEN2:
+ messages += CarState.get_common_global_body_messages(CP)
+ messages += CarState.get_common_global_es_messages(CP)
+
+ if CP.flags & SubaruFlags.HYBRID:
+ messages += [
+ ("Throttle_Hybrid", 40),
+ ("Transmission", 100)
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CanBus.alt)
diff --git a/opendbc/car/subaru/fingerprints.py b/opendbc/car/subaru/fingerprints.py
new file mode 100644
index 0000000000..3c61915fc5
--- /dev/null
+++ b/opendbc/car/subaru/fingerprints.py
@@ -0,0 +1,563 @@
+from opendbc.car.structs import CarParams
+from opendbc.car.subaru.values import CAR
+
+Ecu = CarParams.Ecu
+
+FW_VERSIONS = {
+ CAR.SUBARU_ASCENT: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa5 \x19\x02\x00',
+ b'\xa5 !\x02\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'\x05\xc0\xd0\x00',
+ b'\x85\xc0\xd0\x00',
+ b'\x95\xc0\xd0\x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00d\xb9\x00\x00\x00\x00',
+ b'\x00\x00d\xb9\x1f@ \x10',
+ b'\x00\x00e@\x00\x00\x00\x00',
+ b'\x00\x00e@\x1f@ $',
+ b"\x00\x00e~\x1f@ '",
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xbb,\xa0t\x07',
+ b'\xd1,\xa0q\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\x00>\xf0\x00\x00',
+ b'\x00\xfe\xf7\x00\x00',
+ b'\x01\xfe\xf7\x00\x00',
+ b'\x01\xfe\xf9\x00\x00',
+ b'\x01\xfe\xfa\x00\x00',
+ ],
+ },
+ CAR.SUBARU_ASCENT_2023: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa5 #\x03\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'%\xc0\xd0\x11',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x05!\x08\x1dK\x05!\x08\x01/',
+ ],
+ (Ecu.engine, 0x7a2, None): [
+ b'\xe5,\xa0P\x07',
+ ],
+ (Ecu.transmission, 0x7a3, None): [
+ b'\x04\xfe\xf3\x00\x00',
+ ],
+ },
+ CAR.SUBARU_LEGACY: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa1 \x02\x01',
+ b'\xa1 \x02\x02',
+ b'\xa1 \x03\x03',
+ b'\xa1 \x04\x01',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'\x9b\xc0\x11\x00',
+ b'\x9b\xc0\x11\x02',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00e\x80\x00\x1f@ \x19\x00',
+ b'\x00\x00e\x9a\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xde"a0\x07',
+ b'\xde,\xa0@\x07',
+ b'\xe2"aq\x07',
+ b'\xe2,\xa0@\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xa5\xf6\x05@\x00',
+ b'\xa5\xfe\xc7@\x00',
+ b'\xa7\xf6\x04@\x00',
+ b'\xa7\xfe\xc4@\x00',
+ ],
+ },
+ CAR.SUBARU_IMPREZA: {
+ (Ecu.abs, 0x7b0, None): [
+ b'z\x84\x19\x90\x00',
+ b'z\x94\x08\x90\x00',
+ b'z\x94\x08\x90\x01',
+ b'z\x94\x0c\x90\x00',
+ b'z\x94\x0c\x90\x01',
+ b'z\x94.\x90\x00',
+ b'z\x94?\x90\x00',
+ b'z\x9c\x19\x80\x01',
+ b'\xa2 \x185\x00',
+ b'\xa2 \x193\x00',
+ b'\xa2 \x194\x00',
+ b'\xa2 \x19`\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'z\xc0\x00\x00',
+ b'z\xc0\x04\x00',
+ b'z\xc0\x08\x00',
+ b'z\xc0\n\x00',
+ b'z\xc0\x0c\x00',
+ b'\x8a\xc0\x00\x00',
+ b'\x8a\xc0\x10\x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00c\xf4\x00\x00\x00\x00',
+ b'\x00\x00c\xf4\x1f@ \x07',
+ b'\x00\x00d)\x00\x00\x00\x00',
+ b'\x00\x00d)\x1f@ \x07',
+ b'\x00\x00dd\x00\x00\x00\x00',
+ b'\x00\x00dd\x1f@ \x0e',
+ b'\x00\x00d\xb5\x1f@ \x0e',
+ b'\x00\x00d\xdc\x00\x00\x00\x00',
+ b'\x00\x00d\xdc\x1f@ \x0e',
+ b'\x00\x00e\x02\x1f@ \x14',
+ b'\x00\x00e\x1c\x00\x00\x00\x00',
+ b'\x00\x00e\x1c\x1f@ \x14',
+ b'\x00\x00e+\x00\x00\x00\x00',
+ b'\x00\x00e+\x1f@ \x14',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xaa\x00Bu\x07',
+ b'\xaa\x01bt\x07',
+ b'\xaa!`u\x07',
+ b'\xaa!au\x07',
+ b'\xaa!av\x07',
+ b'\xaa!aw\x07',
+ b'\xaa!dq\x07',
+ b'\xaa!ds\x07',
+ b'\xaa!dt\x07',
+ b'\xaaafs\x07',
+ b'\xbe!as\x07',
+ b'\xbe!at\x07',
+ b'\xbeacr\x07',
+ b'\xc5!`r\x07',
+ b'\xc5!`s\x07',
+ b'\xc5!ap\x07',
+ b'\xc5!ar\x07',
+ b'\xc5!as\x07',
+ b'\xc5!dr\x07',
+ b'\xc5!ds\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xe3\xd0\x081\x00',
+ b'\xe3\xd5\x161\x00',
+ b'\xe3\xe5F1\x00',
+ b'\xe3\xf5\x06\x00\x00',
+ b'\xe3\xf5\x07\x00\x00',
+ b'\xe3\xf5C\x00\x00',
+ b'\xe3\xf5F\x00\x00',
+ b'\xe3\xf5G\x00\x00',
+ b'\xe4\xe5\x021\x00',
+ b'\xe4\xe5\x061\x00',
+ b'\xe4\xf5\x02\x00\x00',
+ b'\xe4\xf5\x07\x00\x00',
+ b'\xe5\xf5\x04\x00\x00',
+ b'\xe5\xf5$\x00\x00',
+ b'\xe5\xf5B\x00\x00',
+ ],
+ },
+ CAR.SUBARU_IMPREZA_2020: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa2 \x193\x00',
+ b'\xa2 \x194\x00',
+ b'\xa2 `\x00',
+ b'\xa2 !3\x00',
+ b'\xa2 !6\x00',
+ b'\xa2 !`\x00',
+ b'\xa2 !i\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'\n\xc0\x04\x00',
+ b'\n\xc0\x04\x01',
+ b'\x9a\xc0\x00\x00',
+ b'\x9a\xc0\x04\x00',
+ b'\x9a\xc0\n\x01',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00eb\x1f@ "',
+ b'\x00\x00eq\x00\x00\x00\x00',
+ b'\x00\x00eq\x1f@ "',
+ b'\x00\x00e\x8f\x00\x00\x00\x00',
+ b'\x00\x00e\x8f\x1f@ )',
+ b'\x00\x00e\x92\x00\x00\x00\x00',
+ b'\x00\x00e\xa4\x00\x00\x00\x00',
+ b'\x00\x00e\xa4\x1f@ (',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xca!`0\x07',
+ b'\xca!`p\x07',
+ b'\xca!ap\x07',
+ b'\xca!f@\x07',
+ b'\xca!fp\x07',
+ b'\xcaacp\x07',
+ b'\xcc!`p\x07',
+ b'\xcc!fp\x07',
+ b'\xcc"f0\x07',
+ b'\xe6!`@\x07',
+ b'\xe6!fp\x07',
+ b'\xe6"f0\x07',
+ b'\xe6"fp\x07',
+ b'\xf3"f@\x07',
+ b'\xf3"fp\x07',
+ b'\xf3"fr\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xe6\x15\x042\x00',
+ b'\xe6\xf5\x04\x00\x00',
+ b'\xe6\xf5$\x00\x00',
+ b'\xe6\xf5D0\x00',
+ b'\xe7\xf5\x04\x00\x00',
+ b'\xe7\xf5D0\x00',
+ b'\xe7\xf6B0\x00',
+ b'\xe9\xf5"\x00\x00',
+ b'\xe9\xf5B0\x00',
+ b'\xe9\xf6B0\x00',
+ b'\xe9\xf6F0\x00',
+ ],
+ },
+ CAR.SUBARU_CROSSTREK_HYBRID: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa2 \x19e\x01',
+ b'\xa2 !e\x01',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'\n\xc2\x01\x00',
+ b'\x9a\xc2\x01\x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00el\x1f@ #',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xd7!`@\x07',
+ b'\xd7!`p\x07',
+ b'\xf4!`0\x07',
+ ],
+ },
+ CAR.SUBARU_FORESTER: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa3 \x18\x14\x00',
+ b'\xa3 \x18&\x00',
+ b'\xa3 \x19\x14\x00',
+ b'\xa3 \x19&\x00',
+ b'\xa3 \x19h\x00',
+ b'\xa3 \x14\x00',
+ b'\xa3 \x14\x01',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'\x8d\xc0\x00\x00',
+ b'\x8d\xc0\x04\x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00e!\x00\x00\x00\x00',
+ b'\x00\x00e!\x1f@ \x11',
+ b'\x00\x00e^\x00\x00\x00\x00',
+ b'\x00\x00e^\x1f@ !',
+ b'\x00\x00e`\x00\x00\x00\x00',
+ b'\x00\x00e`\x1f@ ',
+ b'\x00\x00e\x97\x00\x00\x00\x00',
+ b'\x00\x00e\x97\x1f@ 0',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xb6"`A\x07',
+ b'\xb6\xa2`A\x07',
+ b'\xcb"`@\x07',
+ b'\xcb"`p\x07',
+ b'\xcf"`0\x07',
+ b'\xcf"`p\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\x1a\xe6B1\x00',
+ b'\x1a\xe6F1\x00',
+ b'\x1a\xf6B0\x00',
+ b'\x1a\xf6B`\x00',
+ b'\x1a\xf6F`\x00',
+ b'\x1a\xf6b0\x00',
+ b'\x1a\xf6b`\x00',
+ b'\x1a\xf6f`\x00',
+ ],
+ },
+ CAR.SUBARU_FORESTER_HYBRID: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa3 \x19T\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'\x8d\xc2\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00eY\x1f@ !',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xd2\xa1`r\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\x1b\xa7@a\x00',
+ ],
+ },
+ CAR.SUBARU_FORESTER_PREGLOBAL: {
+ (Ecu.abs, 0x7b0, None): [
+ b'm\x97\x14@',
+ b'}\x97\x14@',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'm\xc0\x10\x00',
+ b'}\xc0\x10\x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00c\xe9\x00\x00\x00\x00',
+ b'\x00\x00c\xe9\x1f@ \x03',
+ b'\x00\x00d5\x1f@ \t',
+ b'\x00\x00d\xd3\x1f@ \t',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xa7"@0\x07',
+ b'\xa7"@p\x07',
+ b'\xa7)\xa0q\x07',
+ b'\xba"@@\x07',
+ b'\xba"@p\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\x1a\xf6F`\x00',
+ b'\xda\xf2`p\x00',
+ b'\xda\xf2`\x80\x00',
+ b'\xda\xfd\xe0\x80\x00',
+ b'\xdc\xf2@`\x00',
+ b'\xdc\xf2``\x00',
+ b'\xdc\xf2`\x80\x00',
+ b'\xdc\xf2`\x81\x00',
+ ],
+ },
+ CAR.SUBARU_LEGACY_PREGLOBAL: {
+ (Ecu.abs, 0x7b0, None): [
+ b'[\x97D\x00',
+ b'[\xba\xc4\x03',
+ b'k\x97D\x00',
+ b'k\x9aD\x00',
+ b'{\x97D\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'K\xb0\x00\x01',
+ b'[\xb0\x00\x01',
+ b'k\xb0\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00c\x94\x1f@\x10\x08',
+ b'\x00\x00c\xb7\x1f@\x10\x16',
+ b'\x00\x00c\xec\x1f@ \x04',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xa0"@q\x07',
+ b'\xa0+@p\x07',
+ b'\xab*@r\x07',
+ b'\xab+@p\x07',
+ b'\xb4"@0\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xbd\xf2\x00`\x00',
+ b'\xbe\xf2\x00p\x00',
+ b'\xbe\xfb\xc0p\x00',
+ b'\xbf\xf2\x00\x80\x00',
+ b'\xbf\xfb\xc0\x80\x00',
+ ],
+ },
+ CAR.SUBARU_OUTBACK_PREGLOBAL: {
+ (Ecu.abs, 0x7b0, None): [
+ b'[\xba\xac\x03',
+ b'[\xf7\xac\x00',
+ b'[\xf7\xac\x03',
+ b'[\xf7\xbc\x03',
+ b'k\x97\xac\x00',
+ b'k\x9a\xac\x00',
+ b'{\x97\xac\x00',
+ b'{\x9a\xac\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'K\xb0\x00\x00',
+ b'K\xb0\x00\x02',
+ b'[\xb0\x00\x00',
+ b'k\xb0\x00\x00',
+ b'{\xb0\x00\x01',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00c\x90\x1f@\x10\x0e',
+ b'\x00\x00c\x94\x00\x00\x00\x00',
+ b'\x00\x00c\x94\x1f@\x10\x08',
+ b'\x00\x00c\xb7\x1f@\x10\x16',
+ b'\x00\x00c\xd1\x1f@\x10\x17',
+ b'\x00\x00c\xec\x1f@ \x04',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xa0"@\x80\x07',
+ b'\xa0*@q\x07',
+ b'\xa0*@u\x07',
+ b'\xa0+@@\x07',
+ b'\xa0bAq\x07',
+ b'\xab"@@\x07',
+ b'\xab"@s\x07',
+ b'\xab*@@\x07',
+ b'\xab+@@\x07',
+ b'\xb4"@0\x07',
+ b'\xb4"@p\x07',
+ b'\xb4"@r\x07',
+ b'\xb4+@p\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xbd\xf2@`\x00',
+ b'\xbd\xf2@\x81\x00',
+ b'\xbd\xfb\xe0\x80\x00',
+ b'\xbe\xf2@p\x00',
+ b'\xbe\xf2@\x80\x00',
+ b'\xbe\xfb\xe0p\x00',
+ b'\xbf\xe2@\x80\x00',
+ b'\xbf\xf2@\x80\x00',
+ b'\xbf\xfb\xe0b\x00',
+ ],
+ },
+ CAR.SUBARU_OUTBACK_PREGLOBAL_2018: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\x8b\x97\xac\x00',
+ b'\x8b\x97\xbc\x00',
+ b'\x8b\x99\xac\x00',
+ b'\x8b\x9a\xac\x00',
+ b'\x9b\x97\xac\x00',
+ b'\x9b\x97\xbe\x10',
+ b'\x9b\x9a\xac\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'{\xb0\x00\x00',
+ b'{\xb0\x00\x01',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00df\x1f@ \n',
+ b'\x00\x00d\x95\x00\x00\x00\x00',
+ b'\x00\x00d\x95\x1f@ \x0f',
+ b'\x00\x00d\xfe\x00\x00\x00\x00',
+ b'\x00\x00d\xfe\x1f@ \x15',
+ b'\x00\x00e\x19\x1f@ \x15',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xb5"@P\x07',
+ b'\xb5"@p\x07',
+ b'\xb5+@@\x07',
+ b'\xb5b@1\x07',
+ b'\xb5q\xe0@\x07',
+ b'\xc4"@0\x07',
+ b'\xc4+@0\x07',
+ b'\xc4b@p\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xbb\xf2@`\x00',
+ b'\xbb\xfb\xe0`\x00',
+ b'\xbc\xaf\xe0`\x00',
+ b'\xbc\xe2@\x80\x00',
+ b'\xbc\xf2@\x80\x00',
+ b'\xbc\xf2@\x81\x00',
+ b'\xbc\xfb\xe0`\x00',
+ b'\xbc\xfb\xe0\x80\x00',
+ ],
+ },
+ CAR.SUBARU_OUTBACK: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa1 \x06\x00',
+ b'\xa1 \x06\x01',
+ b'\xa1 \x06\x02',
+ b'\xa1 \x07\x00',
+ b'\xa1 \x07\x02',
+ b'\xa1 \x07\x03',
+ b'\xa1 \x08\x00',
+ b'\xa1 \x08\x01',
+ b'\xa1 \x08\x02',
+ b'\xa1 "\t\x00',
+ b'\xa1 "\t\x01',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'\x1b\xc0\x10\x00',
+ b'\x9b\xc0\x10\x00',
+ b'\x9b\xc0\x10\x02',
+ b'\x9b\xc0 \x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x00\x00eJ\x00\x00\x00\x00\x00\x00',
+ b'\x00\x00eJ\x00\x1f@ \x19\x00',
+ b'\x00\x00e\x80\x00\x1f@ \x19\x00',
+ b'\x00\x00e\x9a\x00\x00\x00\x00\x00\x00',
+ b'\x00\x00e\x9a\x00\x1f@ 1\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xbc"`@\x07',
+ b'\xbc"`q\x07',
+ b'\xbc,\xa0q\x07',
+ b'\xbc,\xa0u\x07',
+ b'\xde"`0\x07',
+ b'\xde,\xa0@\x07',
+ b'\xe2"`0\x07',
+ b'\xe2"`p\x07',
+ b'\xe2"`q\x07',
+ b'\xe3,\xa0@\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xa5\xf6D@\x00',
+ b'\xa5\xfe\xf6@\x00',
+ b'\xa5\xfe\xf7@\x00',
+ b'\xa5\xfe\xf8@\x00',
+ b'\xa7\x8e\xf40\x00',
+ b'\xa7\xf6D@\x00',
+ b'\xa7\xfe\xf4@\x00',
+ ],
+ },
+ CAR.SUBARU_FORESTER_2022: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa3 !v\x00',
+ b'\xa3 !x\x00',
+ b'\xa3 "v\x00',
+ b'\xa3 "x\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'-\xc0\x040',
+ b'-\xc0%0',
+ b'=\xc0%\x02',
+ b'=\xc04\x02',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\x04!\x01\x1eD\x07!\x00\x04,',
+ b'\x04!\x08\x01.\x07!\x08\x022',
+ b'\r!\x08\x017\n!\x08\x003',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\xd5"`0\x07',
+ b'\xd5"a0\x07',
+ b'\xf1"`q\x07',
+ b'\xf1"aq\x07',
+ b'\xfa"ap\x07',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\x1d\x86B0\x00',
+ b'\x1d\xf6B0\x00',
+ b'\x1e\x86B0\x00',
+ b'\x1e\x86F0\x00',
+ b'\x1e\xf6D0\x00',
+ ],
+ },
+ CAR.SUBARU_OUTBACK_2023: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\xa1 #\x14\x00',
+ b'\xa1 #\x17\x00',
+ ],
+ (Ecu.eps, 0x746, None): [
+ b'+\xc0\x10\x11\x00',
+ b'+\xc0\x12\x11\x00',
+ ],
+ (Ecu.fwdCamera, 0x787, None): [
+ b'\t!\x08\x046\x05!\x08\x01/',
+ ],
+ (Ecu.engine, 0x7a2, None): [
+ b'\xed,\xa0q\x07',
+ b'\xed,\xa2q\x07',
+ ],
+ (Ecu.transmission, 0x7a3, None): [
+ b'\xa8\x8e\xf41\x00',
+ b'\xa8\xfe\xf41\x00',
+ ],
+ },
+}
diff --git a/opendbc/car/subaru/interface.py b/opendbc/car/subaru/interface.py
new file mode 100644
index 0000000000..782f73e8c6
--- /dev/null
+++ b/opendbc/car/subaru/interface.py
@@ -0,0 +1,101 @@
+from panda import Panda
+from opendbc.car import get_safety_config, structs
+from opendbc.car.disable_ecu import disable_ecu
+from opendbc.car.interfaces import CarInterfaceBase
+from opendbc.car.subaru.values import CAR, GLOBAL_ES_ADDR, SubaruFlags
+
+
+class CarInterface(CarInterfaceBase):
+
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "subaru"
+ ret.radarUnavailable = True
+ # for HYBRID CARS to be upstreamed, we need:
+ # - replacement for ES_Distance so we can cancel the cruise control
+ # - to find the Cruise_Activated bit from the car
+ # - proper panda safety setup (use the correct cruise_activated bit, throttle from Throttle_Hybrid, etc)
+ ret.dashcamOnly = bool(ret.flags & (SubaruFlags.PREGLOBAL | SubaruFlags.LKAS_ANGLE | SubaruFlags.HYBRID))
+ ret.autoResumeSng = False
+
+ # Detect infotainment message sent from the camera
+ if not (ret.flags & SubaruFlags.PREGLOBAL) and 0x323 in fingerprint[2]:
+ ret.flags |= SubaruFlags.SEND_INFOTAINMENT.value
+
+ if ret.flags & SubaruFlags.PREGLOBAL:
+ ret.enableBsm = 0x25c in fingerprint[0]
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.subaruPreglobal)]
+ else:
+ ret.enableBsm = 0x228 in fingerprint[0]
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.subaru)]
+ if ret.flags & SubaruFlags.GLOBAL_GEN2:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_SUBARU_GEN2
+
+ ret.steerLimitTimer = 0.4
+ ret.steerActuatorDelay = 0.1
+
+ if ret.flags & SubaruFlags.LKAS_ANGLE:
+ ret.steerControlType = structs.CarParams.SteerControlType.angle
+ else:
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ if candidate in (CAR.SUBARU_ASCENT, CAR.SUBARU_ASCENT_2023):
+ ret.steerActuatorDelay = 0.3 # end-to-end angle controller
+ ret.lateralTuning.init('pid')
+ ret.lateralTuning.pid.kf = 0.00003
+ ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 20.], [0., 20.]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.0025, 0.1], [0.00025, 0.01]]
+
+ elif candidate == CAR.SUBARU_IMPREZA:
+ ret.steerActuatorDelay = 0.4 # end-to-end angle controller
+ ret.lateralTuning.init('pid')
+ ret.lateralTuning.pid.kf = 0.00005
+ ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 20.], [0., 20.]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2, 0.3], [0.02, 0.03]]
+
+ elif candidate == CAR.SUBARU_IMPREZA_2020:
+ ret.lateralTuning.init('pid')
+ ret.lateralTuning.pid.kf = 0.00005
+ ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.045, 0.042, 0.20], [0.04, 0.035, 0.045]]
+
+ elif candidate == CAR.SUBARU_CROSSTREK_HYBRID:
+ ret.steerActuatorDelay = 0.1
+
+ elif candidate in (CAR.SUBARU_FORESTER, CAR.SUBARU_FORESTER_2022, CAR.SUBARU_FORESTER_HYBRID):
+ ret.lateralTuning.init('pid')
+ ret.lateralTuning.pid.kf = 0.000038
+ ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]]
+ ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.01, 0.065, 0.2], [0.001, 0.015, 0.025]]
+
+ elif candidate in (CAR.SUBARU_OUTBACK, CAR.SUBARU_LEGACY, CAR.SUBARU_OUTBACK_2023):
+ ret.steerActuatorDelay = 0.1
+
+ elif candidate in (CAR.SUBARU_FORESTER_PREGLOBAL, CAR.SUBARU_OUTBACK_PREGLOBAL_2018):
+ ret.safetyConfigs[0].safetyParam = Panda.FLAG_SUBARU_PREGLOBAL_REVERSED_DRIVER_TORQUE # Outback 2018-2019 and Forester have reversed driver torque signal
+
+ elif candidate == CAR.SUBARU_LEGACY_PREGLOBAL:
+ ret.steerActuatorDelay = 0.15
+
+ elif candidate == CAR.SUBARU_OUTBACK_PREGLOBAL:
+ pass
+ else:
+ raise ValueError(f"unknown car: {candidate}")
+
+ ret.experimentalLongitudinalAvailable = not (ret.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.PREGLOBAL |
+ SubaruFlags.LKAS_ANGLE | SubaruFlags.HYBRID))
+ ret.openpilotLongitudinalControl = experimental_long and ret.experimentalLongitudinalAvailable
+
+ if ret.flags & SubaruFlags.GLOBAL_GEN2 and ret.openpilotLongitudinalControl:
+ ret.flags |= SubaruFlags.DISABLE_EYESIGHT.value
+
+ if ret.openpilotLongitudinalControl:
+ ret.stoppingControl = True
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_SUBARU_LONG
+
+ return ret
+
+ @staticmethod
+ def init(CP, can_recv, can_send):
+ if CP.flags & SubaruFlags.DISABLE_EYESIGHT:
+ disable_ecu(can_recv, can_send, bus=2, addr=GLOBAL_ES_ADDR, com_cont_req=b'\x28\x03\x01')
diff --git a/opendbc/car/subaru/radar_interface.py b/opendbc/car/subaru/radar_interface.py
new file mode 100644
index 0000000000..6e552bf618
--- /dev/null
+++ b/opendbc/car/subaru/radar_interface.py
@@ -0,0 +1,4 @@
+from opendbc.car.interfaces import RadarInterfaceBase
+
+class RadarInterface(RadarInterfaceBase):
+ pass
diff --git a/opendbc/car/subaru/subarucan.py b/opendbc/car/subaru/subarucan.py
new file mode 100644
index 0000000000..83340dc1f8
--- /dev/null
+++ b/opendbc/car/subaru/subarucan.py
@@ -0,0 +1,321 @@
+from opendbc.car import structs
+from opendbc.car.subaru.values import CanBus
+
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+
+
+def create_steering_control(packer, apply_steer, steer_req):
+ values = {
+ "LKAS_Output": apply_steer,
+ "LKAS_Request": steer_req,
+ "SET_1": 1
+ }
+ return packer.make_can_msg("ES_LKAS", 0, values)
+
+
+def create_steering_control_angle(packer, apply_steer, steer_req):
+ values = {
+ "LKAS_Output": apply_steer,
+ "LKAS_Request": steer_req,
+ "SET_3": 3
+ }
+ return packer.make_can_msg("ES_LKAS_ANGLE", 0, values)
+
+
+def create_steering_status(packer):
+ return packer.make_can_msg("ES_LKAS_State", 0, {})
+
+def create_es_distance(packer, frame, es_distance_msg, bus, pcm_cancel_cmd, long_enabled = False, brake_cmd = False, cruise_throttle = 0):
+ values = {s: es_distance_msg[s] for s in [
+ "CHECKSUM",
+ "Signal1",
+ "Cruise_Fault",
+ "Cruise_Throttle",
+ "Signal2",
+ "Car_Follow",
+ "Low_Speed_Follow",
+ "Cruise_Soft_Disable",
+ "Signal7",
+ "Cruise_Brake_Active",
+ "Distance_Swap",
+ "Cruise_EPB",
+ "Signal4",
+ "Close_Distance",
+ "Signal5",
+ "Cruise_Cancel",
+ "Cruise_Set",
+ "Cruise_Resume",
+ "Signal6",
+ ]}
+
+ values["COUNTER"] = frame % 0x10
+
+ if long_enabled:
+ values["Cruise_Throttle"] = cruise_throttle
+
+ # Do not disable openpilot on Eyesight Soft Disable, if openpilot is controlling long
+ values["Cruise_Soft_Disable"] = 0
+ values["Cruise_Fault"] = 0
+
+ values["Cruise_Brake_Active"] = brake_cmd
+
+ if pcm_cancel_cmd:
+ values["Cruise_Cancel"] = 1
+ values["Cruise_Throttle"] = 1818 # inactive throttle
+
+ return packer.make_can_msg("ES_Distance", bus, values)
+
+
+def create_es_lkas_state(packer, frame, es_lkas_state_msg, enabled, visual_alert, left_line, right_line, left_lane_depart, right_lane_depart):
+ values = {s: es_lkas_state_msg[s] for s in [
+ "CHECKSUM",
+ "LKAS_Alert_Msg",
+ "Signal1",
+ "LKAS_ACTIVE",
+ "LKAS_Dash_State",
+ "Signal2",
+ "Backward_Speed_Limit_Menu",
+ "LKAS_Left_Line_Enable",
+ "LKAS_Left_Line_Light_Blink",
+ "LKAS_Right_Line_Enable",
+ "LKAS_Right_Line_Light_Blink",
+ "LKAS_Left_Line_Visible",
+ "LKAS_Right_Line_Visible",
+ "LKAS_Alert",
+ "Signal3",
+ ]}
+
+ values["COUNTER"] = frame % 0x10
+
+ # Filter the stock LKAS "Keep hands on wheel" alert
+ if values["LKAS_Alert_Msg"] == 1:
+ values["LKAS_Alert_Msg"] = 0
+
+ # Filter the stock LKAS sending an audible alert when it turns off LKAS
+ if values["LKAS_Alert"] == 27:
+ values["LKAS_Alert"] = 0
+
+ # Filter the stock LKAS sending an audible alert when "Keep hands on wheel" alert is active (2020+ models)
+ if values["LKAS_Alert"] == 28 and values["LKAS_Alert_Msg"] == 7:
+ values["LKAS_Alert"] = 0
+
+ # Filter the stock LKAS sending an audible alert when "Keep hands on wheel OFF" alert is active (2020+ models)
+ if values["LKAS_Alert"] == 30:
+ values["LKAS_Alert"] = 0
+
+ # Filter the stock LKAS sending "Keep hands on wheel OFF" alert (2020+ models)
+ if values["LKAS_Alert_Msg"] == 7:
+ values["LKAS_Alert_Msg"] = 0
+
+ # Show Keep hands on wheel alert for openpilot steerRequired alert
+ if visual_alert == VisualAlert.steerRequired:
+ values["LKAS_Alert_Msg"] = 1
+
+ # Ensure we don't overwrite potentially more important alerts from stock (e.g. FCW)
+ if visual_alert == VisualAlert.ldw and values["LKAS_Alert"] == 0:
+ if left_lane_depart:
+ values["LKAS_Alert"] = 12 # Left lane departure dash alert
+ elif right_lane_depart:
+ values["LKAS_Alert"] = 11 # Right lane departure dash alert
+
+ if enabled:
+ values["LKAS_ACTIVE"] = 1 # Show LKAS lane lines
+ values["LKAS_Dash_State"] = 2 # Green enabled indicator
+ else:
+ values["LKAS_Dash_State"] = 0 # LKAS Not enabled
+
+ values["LKAS_Left_Line_Visible"] = int(left_line)
+ values["LKAS_Right_Line_Visible"] = int(right_line)
+
+ return packer.make_can_msg("ES_LKAS_State", CanBus.main, values)
+
+def create_es_dashstatus(packer, frame, dashstatus_msg, enabled, long_enabled, long_active, lead_visible):
+ values = {s: dashstatus_msg[s] for s in [
+ "CHECKSUM",
+ "PCB_Off",
+ "LDW_Off",
+ "Signal1",
+ "Cruise_State_Msg",
+ "LKAS_State_Msg",
+ "Signal2",
+ "Cruise_Soft_Disable",
+ "Cruise_Status_Msg",
+ "Signal3",
+ "Cruise_Distance",
+ "Signal4",
+ "Conventional_Cruise",
+ "Signal5",
+ "Cruise_Disengaged",
+ "Cruise_Activated",
+ "Signal6",
+ "Cruise_Set_Speed",
+ "Cruise_Fault",
+ "Cruise_On",
+ "Display_Own_Car",
+ "Brake_Lights",
+ "Car_Follow",
+ "Signal7",
+ "Far_Distance",
+ "Cruise_State",
+ ]}
+
+ values["COUNTER"] = frame % 0x10
+
+ if long_enabled:
+ values["Cruise_State"] = 0
+ values["Cruise_Activated"] = enabled
+ values["Cruise_Disengaged"] = 0
+ values["Car_Follow"] = int(lead_visible)
+
+ values["PCB_Off"] = 1 # AEB is not presevered, so show the PCB_Off on dash
+ values["LDW_Off"] = 0
+ values["Cruise_Fault"] = 0
+
+ # Filter stock LKAS disabled and Keep hands on steering wheel OFF alerts
+ if values["LKAS_State_Msg"] in (2, 3):
+ values["LKAS_State_Msg"] = 0
+
+ return packer.make_can_msg("ES_DashStatus", CanBus.main, values)
+
+def create_es_brake(packer, frame, es_brake_msg, long_enabled, long_active, brake_value):
+ values = {s: es_brake_msg[s] for s in [
+ "CHECKSUM",
+ "Signal1",
+ "Brake_Pressure",
+ "AEB_Status",
+ "Cruise_Brake_Lights",
+ "Cruise_Brake_Fault",
+ "Cruise_Brake_Active",
+ "Cruise_Activated",
+ "Signal3",
+ ]}
+
+ values["COUNTER"] = frame % 0x10
+
+ if long_enabled:
+ values["Cruise_Brake_Fault"] = 0
+ values["Cruise_Activated"] = long_active
+
+ values["Brake_Pressure"] = brake_value
+
+ values["Cruise_Brake_Active"] = brake_value > 0
+ values["Cruise_Brake_Lights"] = brake_value >= 70
+
+ return packer.make_can_msg("ES_Brake", CanBus.main, values)
+
+def create_es_status(packer, frame, es_status_msg, long_enabled, long_active, cruise_rpm):
+ values = {s: es_status_msg[s] for s in [
+ "CHECKSUM",
+ "Signal1",
+ "Cruise_Fault",
+ "Cruise_RPM",
+ "Cruise_Activated",
+ "Brake_Lights",
+ "Cruise_Hold",
+ "Signal3",
+ ]}
+
+ values["COUNTER"] = frame % 0x10
+
+ if long_enabled:
+ values["Cruise_RPM"] = cruise_rpm
+ values["Cruise_Fault"] = 0
+
+ values["Cruise_Activated"] = long_active
+
+ return packer.make_can_msg("ES_Status", CanBus.main, values)
+
+
+def create_es_infotainment(packer, frame, es_infotainment_msg, visual_alert):
+ # Filter stock LKAS disabled and Keep hands on steering wheel OFF alerts
+ values = {s: es_infotainment_msg[s] for s in [
+ "CHECKSUM",
+ "LKAS_State_Infotainment",
+ "LKAS_Blue_Lines",
+ "Signal1",
+ "Signal2",
+ ]}
+
+ values["COUNTER"] = frame % 0x10
+
+ if values["LKAS_State_Infotainment"] in (3, 4):
+ values["LKAS_State_Infotainment"] = 0
+
+ # Show Keep hands on wheel alert for openpilot steerRequired alert
+ if visual_alert == VisualAlert.steerRequired:
+ values["LKAS_State_Infotainment"] = 3
+
+ # Show Obstacle Detected for fcw
+ if visual_alert == VisualAlert.fcw:
+ values["LKAS_State_Infotainment"] = 2
+
+ return packer.make_can_msg("ES_Infotainment", CanBus.main, values)
+
+
+def create_es_highbeamassist(packer):
+ values = {
+ "HBA_Available": False,
+ }
+
+ return packer.make_can_msg("ES_HighBeamAssist", CanBus.main, values)
+
+
+def create_es_static_1(packer):
+ values = {
+ "SET_3": 3,
+ }
+
+ return packer.make_can_msg("ES_STATIC_1", CanBus.main, values)
+
+
+def create_es_static_2(packer):
+ values = {
+ "SET_3": 3,
+ }
+
+ return packer.make_can_msg("ES_STATIC_2", CanBus.main, values)
+
+
+# *** Subaru Pre-global ***
+
+def subaru_preglobal_checksum(packer, values, addr, checksum_byte=7):
+ dat = packer.make_can_msg(addr, 0, values)[1]
+ return (sum(dat[:checksum_byte]) + sum(dat[checksum_byte+1:])) % 256
+
+
+def create_preglobal_steering_control(packer, frame, apply_steer, steer_req):
+ values = {
+ "COUNTER": frame % 0x08,
+ "LKAS_Command": apply_steer,
+ "LKAS_Active": steer_req,
+ }
+ values["Checksum"] = subaru_preglobal_checksum(packer, values, "ES_LKAS")
+
+ return packer.make_can_msg("ES_LKAS", CanBus.main, values)
+
+
+def create_preglobal_es_distance(packer, cruise_button, es_distance_msg):
+ values = {s: es_distance_msg[s] for s in [
+ "Cruise_Throttle",
+ "Signal1",
+ "Car_Follow",
+ "Signal2",
+ "Cruise_Brake_Active",
+ "Distance_Swap",
+ "Standstill",
+ "Signal3",
+ "Close_Distance",
+ "Signal4",
+ "Standstill_2",
+ "Cruise_Fault",
+ "Signal5",
+ "COUNTER",
+ "Signal6",
+ "Cruise_Button",
+ "Signal7",
+ ]}
+
+ values["Cruise_Button"] = cruise_button
+ values["Checksum"] = subaru_preglobal_checksum(packer, values, "ES_Distance")
+
+ return packer.make_can_msg("ES_Distance", CanBus.main, values)
diff --git a/opendbc/car/subaru/tests/test_subaru.py b/opendbc/car/subaru/tests/test_subaru.py
new file mode 100644
index 0000000000..7f3d9bb3c5
--- /dev/null
+++ b/opendbc/car/subaru/tests/test_subaru.py
@@ -0,0 +1,10 @@
+from opendbc.car.subaru.fingerprints import FW_VERSIONS
+
+
+class TestSubaruFingerprint:
+ def test_fw_version_format(self):
+ for platform, fws_per_ecu in FW_VERSIONS.items():
+ for (ecu, _, _), fws in fws_per_ecu.items():
+ fw_size = len(fws[0])
+ for fw in fws:
+ assert len(fw) == fw_size, f"{platform} {ecu}: {len(fw)} {fw_size}"
diff --git a/opendbc/car/subaru/values.py b/opendbc/car/subaru/values.py
new file mode 100644
index 0000000000..15b8a8b8ec
--- /dev/null
+++ b/opendbc/car/subaru/values.py
@@ -0,0 +1,275 @@
+from dataclasses import dataclass, field
+from enum import Enum, IntFlag
+
+from panda import uds
+from opendbc.car import CarSpecs, DbcDict, PlatformConfig, Platforms, dbc_dict
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Tool, Column
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16
+
+Ecu = CarParams.Ecu
+
+
+class CarControllerParams:
+ def __init__(self, CP):
+ self.STEER_STEP = 2 # how often we update the steer cmd
+ self.STEER_DELTA_UP = 50 # torque increase per refresh, 0.8s to max
+ self.STEER_DELTA_DOWN = 70 # torque decrease per refresh
+ self.STEER_DRIVER_ALLOWANCE = 60 # allowed driver torque before start limiting
+ self.STEER_DRIVER_MULTIPLIER = 50 # weight driver torque heavily
+ self.STEER_DRIVER_FACTOR = 1 # from dbc
+
+ if CP.flags & SubaruFlags.GLOBAL_GEN2:
+ self.STEER_MAX = 1000
+ self.STEER_DELTA_UP = 40
+ self.STEER_DELTA_DOWN = 40
+ elif CP.carFingerprint == CAR.SUBARU_IMPREZA_2020:
+ self.STEER_MAX = 1439
+ else:
+ self.STEER_MAX = 2047
+
+ THROTTLE_MIN = 808
+ THROTTLE_MAX = 3400
+
+ THROTTLE_INACTIVE = 1818 # corresponds to zero acceleration
+ THROTTLE_ENGINE_BRAKE = 808 # while braking, eyesight sets throttle to this, probably for engine braking
+
+ BRAKE_MIN = 0
+ BRAKE_MAX = 600 # about -3.5m/s2 from testing
+
+ RPM_MIN = 0
+ RPM_MAX = 3600
+
+ RPM_INACTIVE = 600 # a good base rpm for zero acceleration
+
+ THROTTLE_LOOKUP_BP = [0, 2]
+ THROTTLE_LOOKUP_V = [THROTTLE_INACTIVE, THROTTLE_MAX]
+
+ RPM_LOOKUP_BP = [0, 2]
+ RPM_LOOKUP_V = [RPM_INACTIVE, RPM_MAX]
+
+ BRAKE_LOOKUP_BP = [-3.5, 0]
+ BRAKE_LOOKUP_V = [BRAKE_MAX, BRAKE_MIN]
+
+
+class SubaruFlags(IntFlag):
+ # Detected flags
+ SEND_INFOTAINMENT = 1
+ DISABLE_EYESIGHT = 2
+
+ # Static flags
+ GLOBAL_GEN2 = 4
+
+ # Cars that temporarily fault when steering angle rate is greater than some threshold.
+ # Appears to be all torque-based cars produced around 2019 - present
+ STEER_RATE_LIMITED = 8
+ PREGLOBAL = 16
+ HYBRID = 32
+ LKAS_ANGLE = 64
+
+
+GLOBAL_ES_ADDR = 0x787
+GEN2_ES_BUTTONS_DID = b'\x11\x30'
+
+
+class CanBus:
+ main = 0
+ alt = 1
+ camera = 2
+
+
+class Footnote(Enum):
+ GLOBAL = CarFootnote(
+ "In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.",
+ Column.PACKAGE)
+ EXP_LONG = CarFootnote(
+ "Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB.",
+ Column.LONGITUDINAL)
+
+
+@dataclass
+class SubaruCarDocs(CarDocs):
+ package: str = "EyeSight Driver Assistance"
+ car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.subaru_a]))
+ footnotes: list[Enum] = field(default_factory=lambda: [Footnote.GLOBAL])
+
+ def init_make(self, CP: CarParams):
+ self.car_parts.parts.extend([Tool.socket_8mm_deep, Tool.pry_tool])
+
+ if CP.experimentalLongitudinalAvailable:
+ self.footnotes.append(Footnote.EXP_LONG)
+
+
+@dataclass
+class SubaruPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('subaru_global_2017_generated', None))
+
+ def init(self):
+ if self.flags & SubaruFlags.HYBRID:
+ self.dbc_dict = dbc_dict('subaru_global_2020_hybrid_generated', None)
+
+
+@dataclass
+class SubaruGen2PlatformConfig(SubaruPlatformConfig):
+ def init(self):
+ super().init()
+ self.flags |= SubaruFlags.GLOBAL_GEN2
+ if not (self.flags & SubaruFlags.LKAS_ANGLE):
+ self.flags |= SubaruFlags.STEER_RATE_LIMITED
+
+
+class CAR(Platforms):
+ # Global platform
+ SUBARU_ASCENT = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Ascent 2019-21", "All")],
+ CarSpecs(mass=2031, wheelbase=2.89, steerRatio=13.5),
+ )
+ SUBARU_OUTBACK = SubaruGen2PlatformConfig(
+ [SubaruCarDocs("Subaru Outback 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b]))],
+ CarSpecs(mass=1568, wheelbase=2.67, steerRatio=17),
+ )
+ SUBARU_LEGACY = SubaruGen2PlatformConfig(
+ [SubaruCarDocs("Subaru Legacy 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b]))],
+ SUBARU_OUTBACK.specs,
+ )
+ SUBARU_IMPREZA = SubaruPlatformConfig(
+ [
+ SubaruCarDocs("Subaru Impreza 2017-19"),
+ SubaruCarDocs("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"),
+ SubaruCarDocs("Subaru XV 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"),
+ ],
+ CarSpecs(mass=1568, wheelbase=2.67, steerRatio=15),
+ )
+ SUBARU_IMPREZA_2020 = SubaruPlatformConfig(
+ [
+ SubaruCarDocs("Subaru Impreza 2020-22"),
+ SubaruCarDocs("Subaru Crosstrek 2020-23"),
+ SubaruCarDocs("Subaru XV 2020-21"),
+ ],
+ CarSpecs(mass=1480, wheelbase=2.67, steerRatio=17),
+ flags=SubaruFlags.STEER_RATE_LIMITED,
+ )
+ # TODO: is there an XV and Impreza too?
+ SUBARU_CROSSTREK_HYBRID = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Crosstrek Hybrid 2020", car_parts=CarParts.common([CarHarness.subaru_b]))],
+ CarSpecs(mass=1668, wheelbase=2.67, steerRatio=17),
+ flags=SubaruFlags.HYBRID,
+ )
+ SUBARU_FORESTER = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Forester 2019-21", "All")],
+ CarSpecs(mass=1568, wheelbase=2.67, steerRatio=17),
+ flags=SubaruFlags.STEER_RATE_LIMITED,
+ )
+ SUBARU_FORESTER_HYBRID = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Forester Hybrid 2020")],
+ SUBARU_FORESTER.specs,
+ flags=SubaruFlags.HYBRID,
+ )
+ # Pre-global
+ SUBARU_FORESTER_PREGLOBAL = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Forester 2017-18")],
+ CarSpecs(mass=1568, wheelbase=2.67, steerRatio=20),
+ dbc_dict('subaru_forester_2017_generated', None),
+ flags=SubaruFlags.PREGLOBAL,
+ )
+ SUBARU_LEGACY_PREGLOBAL = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Legacy 2015-18")],
+ CarSpecs(mass=1568, wheelbase=2.67, steerRatio=12.5),
+ dbc_dict('subaru_outback_2015_generated', None),
+ flags=SubaruFlags.PREGLOBAL,
+ )
+ SUBARU_OUTBACK_PREGLOBAL = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Outback 2015-17")],
+ SUBARU_FORESTER_PREGLOBAL.specs,
+ dbc_dict('subaru_outback_2015_generated', None),
+ flags=SubaruFlags.PREGLOBAL,
+ )
+ SUBARU_OUTBACK_PREGLOBAL_2018 = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Outback 2018-19")],
+ SUBARU_FORESTER_PREGLOBAL.specs,
+ dbc_dict('subaru_outback_2019_generated', None),
+ flags=SubaruFlags.PREGLOBAL,
+ )
+ # Angle LKAS
+ SUBARU_FORESTER_2022 = SubaruPlatformConfig(
+ [SubaruCarDocs("Subaru Forester 2022-24", "All", car_parts=CarParts.common([CarHarness.subaru_c]))],
+ SUBARU_FORESTER.specs,
+ flags=SubaruFlags.LKAS_ANGLE,
+ )
+ SUBARU_OUTBACK_2023 = SubaruGen2PlatformConfig(
+ [SubaruCarDocs("Subaru Outback 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d]))],
+ SUBARU_OUTBACK.specs,
+ flags=SubaruFlags.LKAS_ANGLE,
+ )
+ SUBARU_ASCENT_2023 = SubaruGen2PlatformConfig(
+ [SubaruCarDocs("Subaru Ascent 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d]))],
+ SUBARU_ASCENT.specs,
+ flags=SubaruFlags.LKAS_ANGLE,
+ )
+
+
+SUBARU_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_DATA_IDENTIFICATION)
+SUBARU_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_DATA_IDENTIFICATION)
+
+# The EyeSight ECU takes 10s to respond to SUBARU_VERSION_REQUEST properly,
+# log this alternate manufacturer-specific query
+SUBARU_ALT_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(0xf100)
+SUBARU_ALT_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+ p16(0xf100)
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[
+ Request(
+ [StdQueries.TESTER_PRESENT_REQUEST, SUBARU_VERSION_REQUEST],
+ [StdQueries.TESTER_PRESENT_RESPONSE, SUBARU_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.abs, Ecu.eps, Ecu.fwdCamera, Ecu.engine, Ecu.transmission],
+ logging=True,
+ ),
+ # Non-OBD requests
+ # Some Eyesight modules fail on TESTER_PRESENT_REQUEST
+ # TODO: check if this resolves the fingerprinting issue for the 2023 Ascent and other new Subaru cars
+ Request(
+ [SUBARU_VERSION_REQUEST],
+ [SUBARU_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.fwdCamera],
+ bus=0,
+ ),
+ Request(
+ [SUBARU_ALT_VERSION_REQUEST],
+ [SUBARU_ALT_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.fwdCamera],
+ bus=0,
+ logging=True,
+ ),
+ Request(
+ [StdQueries.DEFAULT_DIAGNOSTIC_REQUEST, StdQueries.TESTER_PRESENT_REQUEST, SUBARU_VERSION_REQUEST],
+ [StdQueries.DEFAULT_DIAGNOSTIC_RESPONSE, StdQueries.TESTER_PRESENT_RESPONSE, SUBARU_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.fwdCamera],
+ bus=0,
+ logging=True,
+ ),
+ Request(
+ [StdQueries.TESTER_PRESENT_REQUEST, SUBARU_VERSION_REQUEST],
+ [StdQueries.TESTER_PRESENT_RESPONSE, SUBARU_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.abs, Ecu.eps, Ecu.fwdCamera, Ecu.engine, Ecu.transmission],
+ bus=0,
+ ),
+ # GEN2 powertrain bus query
+ Request(
+ [StdQueries.TESTER_PRESENT_REQUEST, SUBARU_VERSION_REQUEST],
+ [StdQueries.TESTER_PRESENT_RESPONSE, SUBARU_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.abs, Ecu.eps, Ecu.fwdCamera, Ecu.engine, Ecu.transmission],
+ bus=1,
+ obd_multiplexing=False,
+ ),
+ ],
+ # We don't get the EPS from non-OBD queries on GEN2 cars. Note that we still attempt to match when it exists
+ non_essential_ecus={
+ Ecu.eps: list(CAR.with_flags(SubaruFlags.GLOBAL_GEN2)),
+ }
+)
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/tests/__init__.py b/opendbc/car/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/tests/routes.py b/opendbc/car/tests/routes.py
new file mode 100644
index 0000000000..6695b4cbc2
--- /dev/null
+++ b/opendbc/car/tests/routes.py
@@ -0,0 +1,296 @@
+from typing import NamedTuple
+
+from opendbc.car.chrysler.values import CAR as CHRYSLER
+from opendbc.car.gm.values import CAR as GM
+from opendbc.car.ford.values import CAR as FORD
+from opendbc.car.honda.values import CAR as HONDA
+from opendbc.car.hyundai.values import CAR as HYUNDAI
+from opendbc.car.nissan.values import CAR as NISSAN
+from opendbc.car.mazda.values import CAR as MAZDA
+from opendbc.car.mock.values import CAR as MOCK
+from opendbc.car.subaru.values import CAR as SUBARU
+from opendbc.car.toyota.values import CAR as TOYOTA
+from opendbc.car.values import Platform
+from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN
+from opendbc.car.body.values import CAR as COMMA
+
+# FIXME: add routes for these cars
+non_tested_cars = [
+ MOCK.MOCK,
+ FORD.FORD_F_150_MK14,
+ GM.CADILLAC_ATS,
+ GM.HOLDEN_ASTRA,
+ GM.CHEVROLET_MALIBU,
+ HYUNDAI.GENESIS_G90,
+ HONDA.HONDA_ODYSSEY_CHN,
+ VOLKSWAGEN.VOLKSWAGEN_CRAFTER_MK2, # need a route from an ACC-equipped Crafter
+ SUBARU.SUBARU_FORESTER_HYBRID,
+]
+
+
+class CarTestRoute(NamedTuple):
+ route: str
+ car_model: Platform | None
+ segment: int | None = None
+
+
+routes = [
+ CarTestRoute("efdf9af95e71cd84|2022-05-13--19-03-31", COMMA.COMMA_BODY),
+
+ CarTestRoute("0c94aa1e1296d7c6|2021-05-05--19-48-37", CHRYSLER.JEEP_GRAND_CHEROKEE),
+ CarTestRoute("91dfedae61d7bd75|2021-05-22--20-07-52", CHRYSLER.JEEP_GRAND_CHEROKEE_2019),
+ CarTestRoute("420a8e183f1aed48|2020-03-05--07-15-29", CHRYSLER.CHRYSLER_PACIFICA_2018_HYBRID), # 2017
+ CarTestRoute("43a685a66291579b|2021-05-27--19-47-29", CHRYSLER.CHRYSLER_PACIFICA_2018),
+ CarTestRoute("378472f830ee7395|2021-05-28--07-38-43", CHRYSLER.CHRYSLER_PACIFICA_2018_HYBRID),
+ CarTestRoute("8190c7275a24557b|2020-01-29--08-33-58", CHRYSLER.CHRYSLER_PACIFICA_2019_HYBRID),
+ CarTestRoute("3d84727705fecd04|2021-05-25--08-38-56", CHRYSLER.CHRYSLER_PACIFICA_2020),
+ CarTestRoute("221c253375af4ee9|2022-06-15--18-38-24", CHRYSLER.RAM_1500_5TH_GEN),
+ CarTestRoute("8fb5eabf914632ae|2022-08-04--17-28-53", CHRYSLER.RAM_HD_5TH_GEN, segment=6),
+ CarTestRoute("3379c85aeedc8285|2023-12-07--17-49-39", CHRYSLER.DODGE_DURANGO),
+
+ CarTestRoute("54827bf84c38b14f|2023-01-25--14-14-11", FORD.FORD_BRONCO_SPORT_MK1),
+ CarTestRoute("f8eaaccd2a90aef8|2023-05-04--15-10-09", FORD.FORD_ESCAPE_MK4),
+ CarTestRoute("62241b0c7fea4589|2022-09-01--15-32-49", FORD.FORD_EXPLORER_MK6),
+ CarTestRoute("e886087f430e7fe7|2023-06-16--23-06-36", FORD.FORD_FOCUS_MK4),
+ CarTestRoute("bd37e43731e5964b|2023-04-30--10-42-26", FORD.FORD_MAVERICK_MK1),
+ CarTestRoute("112e4d6e0cad05e1|2023-11-14--08-21-43", FORD.FORD_F_150_LIGHTNING_MK1),
+ CarTestRoute("83a4e056c7072678|2023-11-13--16-51-33", FORD.FORD_MUSTANG_MACH_E_MK1),
+ CarTestRoute("37998aa0fade36ab/00000000--48f927c4f5", FORD.FORD_RANGER_MK2),
+ #TestRoute("f1b4c567731f4a1b|2018-04-30--10-15-35", FORD.FUSION),
+
+ CarTestRoute("7cc2a8365b4dd8a9|2018-12-02--12-10-44", GM.GMC_ACADIA),
+ CarTestRoute("aa20e335f61ba898|2019-02-05--16-59-04", GM.BUICK_REGAL),
+ CarTestRoute("75a6bcb9b8b40373|2023-03-11--22-47-33", GM.BUICK_LACROSSE),
+ CarTestRoute("e746f59bc96fd789|2024-01-31--22-25-58", GM.CHEVROLET_EQUINOX),
+ CarTestRoute("ef8f2185104d862e|2023-02-09--18-37-13", GM.CADILLAC_ESCALADE),
+ CarTestRoute("46460f0da08e621e|2021-10-26--07-21-46", GM.CADILLAC_ESCALADE_ESV),
+ CarTestRoute("168f8b3be57f66ae|2023-09-12--21-44-42", GM.CADILLAC_ESCALADE_ESV_2019),
+ CarTestRoute("c950e28c26b5b168|2018-05-30--22-03-41", GM.CHEVROLET_VOLT),
+ CarTestRoute("f08912a233c1584f|2022-08-11--18-02-41", GM.CHEVROLET_BOLT_EUV, segment=1),
+ CarTestRoute("555d4087cf86aa91|2022-12-02--12-15-07", GM.CHEVROLET_BOLT_EUV, segment=14), # Bolt EV
+ CarTestRoute("38aa7da107d5d252|2022-08-15--16-01-12", GM.CHEVROLET_SILVERADO),
+ CarTestRoute("5085c761395d1fe6|2023-04-07--18-20-06", GM.CHEVROLET_TRAILBLAZER),
+ CarTestRoute("162796f1469f2f1b/00000005--6f334eda14", GM.CADILLAC_XT4),
+
+ CarTestRoute("0e7a2ba168465df5|2020-10-18--14-14-22", HONDA.ACURA_RDX_3G),
+ CarTestRoute("a74b011b32b51b56|2020-07-26--17-09-36", HONDA.HONDA_CIVIC),
+ CarTestRoute("a859a044a447c2b0|2020-03-03--18-42-45", HONDA.HONDA_CRV_EU),
+ CarTestRoute("68aac44ad69f838e|2021-05-18--20-40-52", HONDA.HONDA_CRV),
+ CarTestRoute("14fed2e5fa0aa1a5|2021-05-25--14-59-42", HONDA.HONDA_CRV_HYBRID),
+ CarTestRoute("52f3e9ae60c0d886|2021-05-23--15-59-43", HONDA.HONDA_FIT),
+ CarTestRoute("2c4292a5cd10536c|2021-08-19--21-32-15", HONDA.HONDA_FREED),
+ CarTestRoute("03be5f2fd5c508d1|2020-04-19--18-44-15", HONDA.HONDA_HRV),
+ CarTestRoute("320098ff6c5e4730|2023-04-13--17-47-46", HONDA.HONDA_HRV_3G),
+ CarTestRoute("147613502316e718/00000001--dd141a3140", HONDA.HONDA_HRV_3G), # Brazilian model
+ CarTestRoute("917b074700869333|2021-05-24--20-40-20", HONDA.ACURA_ILX),
+ CarTestRoute("08a3deb07573f157|2020-03-06--16-11-19", HONDA.HONDA_ACCORD), # 1.5T
+ CarTestRoute("1da5847ac2488106|2021-05-24--19-31-50", HONDA.HONDA_ACCORD), # 2.0T
+ CarTestRoute("085ac1d942c35910|2021-03-25--20-11-15", HONDA.HONDA_ACCORD), # 2021 with new style HUD msgs
+ CarTestRoute("07585b0da3c88459|2021-05-26--18-52-04", HONDA.HONDA_ACCORD), # hybrid
+ CarTestRoute("f29e2b57a55e7ad5|2021-03-24--20-52-38", HONDA.HONDA_ACCORD), # hybrid, 2021 with new style HUD msgs
+ CarTestRoute("1ad763dd22ef1a0e|2020-02-29--18-37-03", HONDA.HONDA_CRV_5G),
+ CarTestRoute("0a96f86fcfe35964|2020-02-05--07-25-51", HONDA.HONDA_ODYSSEY),
+ CarTestRoute("d83f36766f8012a5|2020-02-05--18-42-21", HONDA.HONDA_CIVIC_BOSCH_DIESEL),
+ CarTestRoute("f0890d16a07a236b|2021-05-25--17-27-22", HONDA.HONDA_INSIGHT),
+ CarTestRoute("07d37d27996096b6|2020-03-04--21-57-27", HONDA.HONDA_PILOT),
+ CarTestRoute("684e8f96bd491a0e|2021-11-03--11-08-42", HONDA.HONDA_PILOT), # Passport
+ CarTestRoute("0a78dfbacc8504ef|2020-03-04--13-29-55", HONDA.HONDA_CIVIC_BOSCH),
+ CarTestRoute("f34a60d68d83b1e5|2020-10-06--14-35-55", HONDA.ACURA_RDX),
+ CarTestRoute("54fd8451b3974762|2021-04-01--14-50-10", HONDA.HONDA_RIDGELINE),
+ CarTestRoute("2d5808fae0b38ac6|2021-09-01--17-14-11", HONDA.HONDA_E),
+ CarTestRoute("f44aa96ace22f34a|2021-12-22--06-22-31", HONDA.HONDA_CIVIC_2022),
+
+ CarTestRoute("87d7f06ade479c2e|2023-09-11--23-30-11", HYUNDAI.HYUNDAI_AZERA_6TH_GEN),
+ CarTestRoute("66189dd8ec7b50e6|2023-09-20--07-02-12", HYUNDAI.HYUNDAI_AZERA_HEV_6TH_GEN),
+ CarTestRoute("6fe86b4e410e4c37|2020-07-22--16-27-13", HYUNDAI.HYUNDAI_GENESIS),
+ CarTestRoute("b5d6dc830ad63071|2022-12-12--21-28-25", HYUNDAI.GENESIS_GV60_EV_1ST_GEN, segment=12),
+ CarTestRoute("70c5bec28ec8e345|2020-08-08--12-22-23", HYUNDAI.GENESIS_G70),
+ CarTestRoute("ca4de5b12321bd98|2022-10-18--21-15-59", HYUNDAI.GENESIS_GV70_1ST_GEN),
+ CarTestRoute("6b301bf83f10aa90|2020-11-22--16-45-07", HYUNDAI.GENESIS_G80),
+ CarTestRoute("0bbe367c98fa1538|2023-09-16--00-16-49", HYUNDAI.HYUNDAI_CUSTIN_1ST_GEN),
+ CarTestRoute("f0709d2bc6ca451f|2022-10-15--08-13-54", HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN),
+ CarTestRoute("4dbd55df87507948|2022-03-01--09-45-38", HYUNDAI.HYUNDAI_SANTA_FE),
+ CarTestRoute("bf43d9df2b660eb0|2021-09-23--14-16-37", HYUNDAI.HYUNDAI_SANTA_FE_2022),
+ CarTestRoute("37398f32561a23ad|2021-11-18--00-11-35", HYUNDAI.HYUNDAI_SANTA_FE_HEV_2022),
+ CarTestRoute("656ac0d830792fcc|2021-12-28--14-45-56", HYUNDAI.HYUNDAI_SANTA_FE_PHEV_2022, segment=1),
+ CarTestRoute("de59124955b921d8|2023-06-24--00-12-50", HYUNDAI.KIA_CARNIVAL_4TH_GEN),
+ CarTestRoute("409c9409979a8abc|2023-07-11--09-06-44", HYUNDAI.KIA_CARNIVAL_4TH_GEN), # Chinese model
+ CarTestRoute("e0e98335f3ebc58f|2021-03-07--16-38-29", HYUNDAI.KIA_CEED),
+ CarTestRoute("7653b2bce7bcfdaa|2020-03-04--15-34-32", HYUNDAI.KIA_OPTIMA_G4),
+ CarTestRoute("018654717bc93d7d|2022-09-19--23-11-10", HYUNDAI.KIA_OPTIMA_G4_FL, segment=0),
+ CarTestRoute("f9716670b2481438|2023-08-23--14-49-50", HYUNDAI.KIA_OPTIMA_H),
+ CarTestRoute("6a42c1197b2a8179|2023-09-21--10-23-44", HYUNDAI.KIA_OPTIMA_H_G4_FL),
+ CarTestRoute("c75a59efa0ecd502|2021-03-11--20-52-55", HYUNDAI.KIA_SELTOS),
+ CarTestRoute("5b7c365c50084530|2020-04-15--16-13-24", HYUNDAI.HYUNDAI_SONATA),
+ CarTestRoute("b2a38c712dcf90bd|2020-05-18--18-12-48", HYUNDAI.HYUNDAI_SONATA_LF),
+ CarTestRoute("c344fd2492c7a9d2|2023-12-11--09-03-23", HYUNDAI.HYUNDAI_STARIA_4TH_GEN),
+ CarTestRoute("fb3fd42f0baaa2f8|2022-03-30--15-25-05", HYUNDAI.HYUNDAI_TUCSON),
+ CarTestRoute("db68bbe12250812c|2022-12-05--00-54-12", HYUNDAI.HYUNDAI_TUCSON_4TH_GEN), # 2023
+ CarTestRoute("36e10531feea61a4|2022-07-25--13-37-42", HYUNDAI.HYUNDAI_TUCSON_4TH_GEN), # hybrid
+ CarTestRoute("5875672fc1d4bf57|2020-07-23--21-33-28", HYUNDAI.KIA_SORENTO),
+ CarTestRoute("1d0d000db3370fd0|2023-01-04--22-28-42", HYUNDAI.KIA_SORENTO_4TH_GEN, segment=5),
+ CarTestRoute("fc19648042eb6896|2023-08-16--11-43-27", HYUNDAI.KIA_SORENTO_HEV_4TH_GEN, segment=14),
+ CarTestRoute("628935d7d3e5f4f7|2022-11-30--01-12-46", HYUNDAI.KIA_SORENTO_HEV_4TH_GEN), # plug-in hybrid
+ CarTestRoute("9c917ba0d42ffe78|2020-04-17--12-43-19", HYUNDAI.HYUNDAI_PALISADE),
+ CarTestRoute("05a8f0197fdac372|2022-10-19--14-14-09", HYUNDAI.HYUNDAI_IONIQ_5), # HDA2
+ CarTestRoute("eb4eae1476647463|2023-08-26--18-07-04", HYUNDAI.HYUNDAI_IONIQ_6, segment=6), # HDA2
+ CarTestRoute("3f29334d6134fcd4|2022-03-30--22-00-50", HYUNDAI.HYUNDAI_IONIQ_PHEV_2019),
+ CarTestRoute("fa8db5869167f821|2021-06-10--22-50-10", HYUNDAI.HYUNDAI_IONIQ_PHEV),
+ CarTestRoute("e1107f9d04dfb1e2|2023-09-05--22-32-12", HYUNDAI.HYUNDAI_IONIQ_PHEV), # openpilot longitudinal enabled
+ CarTestRoute("2c5cf2dd6102e5da|2020-12-17--16-06-44", HYUNDAI.HYUNDAI_IONIQ_EV_2020),
+ CarTestRoute("610ebb9faaad6b43|2020-06-13--15-28-36", HYUNDAI.HYUNDAI_IONIQ_EV_LTD),
+ CarTestRoute("2c5cf2dd6102e5da|2020-06-26--16-00-08", HYUNDAI.HYUNDAI_IONIQ),
+ CarTestRoute("012c95f06918eca4|2023-01-15--11-19-36", HYUNDAI.HYUNDAI_IONIQ), # openpilot longitudinal enabled
+ CarTestRoute("ab59fe909f626921|2021-10-18--18-34-28", HYUNDAI.HYUNDAI_IONIQ_HEV_2022),
+ CarTestRoute("22d955b2cd499c22|2020-08-10--19-58-21", HYUNDAI.HYUNDAI_KONA),
+ CarTestRoute("efc48acf44b1e64d|2021-05-28--21-05-04", HYUNDAI.HYUNDAI_KONA_EV),
+ CarTestRoute("f90d3cd06caeb6fa|2023-09-06--17-15-47", HYUNDAI.HYUNDAI_KONA_EV), # openpilot longitudinal enabled
+ CarTestRoute("ff973b941a69366f|2022-07-28--22-01-19", HYUNDAI.HYUNDAI_KONA_EV_2022, segment=11),
+ CarTestRoute("1618132d68afc876|2023-08-27--09-32-14", HYUNDAI.HYUNDAI_KONA_EV_2ND_GEN, segment=13),
+ CarTestRoute("49f3c13141b6bc87|2021-07-28--08-05-13", HYUNDAI.HYUNDAI_KONA_HEV),
+ CarTestRoute("5dddcbca6eb66c62|2020-07-26--13-24-19", HYUNDAI.KIA_STINGER),
+ CarTestRoute("5b50b883a4259afb|2022-11-09--15-00-42", HYUNDAI.KIA_STINGER_2022),
+ CarTestRoute("d624b3d19adce635|2020-08-01--14-59-12", HYUNDAI.HYUNDAI_VELOSTER),
+ CarTestRoute("d545129f3ca90f28|2022-10-19--09-22-54", HYUNDAI.KIA_EV6), # HDA2
+ CarTestRoute("68d6a96e703c00c9|2022-09-10--16-09-39", HYUNDAI.KIA_EV6), # HDA1
+ CarTestRoute("9b25e8c1484a1b67|2023-04-13--10-41-45", HYUNDAI.KIA_EV6),
+ CarTestRoute("007d5e4ad9f86d13|2021-09-30--15-09-23", HYUNDAI.KIA_K5_2021),
+ CarTestRoute("c58dfc9fc16590e0|2023-01-14--13-51-48", HYUNDAI.KIA_K5_HEV_2020),
+ CarTestRoute("78ad5150de133637|2023-09-13--16-15-57", HYUNDAI.KIA_K8_HEV_1ST_GEN),
+ CarTestRoute("50c6c9b85fd1ff03|2020-10-26--17-56-06", HYUNDAI.KIA_NIRO_EV),
+ CarTestRoute("b153671049a867b3|2023-04-05--10-00-30", HYUNDAI.KIA_NIRO_EV_2ND_GEN),
+ CarTestRoute("173219cf50acdd7b|2021-07-05--10-27-41", HYUNDAI.KIA_NIRO_PHEV),
+ CarTestRoute("23349923ba5c4e3b|2023-12-02--08-51-54", HYUNDAI.KIA_NIRO_PHEV_2022),
+ CarTestRoute("34a875f29f69841a|2021-07-29--13-02-09", HYUNDAI.KIA_NIRO_HEV_2021),
+ CarTestRoute("db04d2c63990e3ba|2023-02-08--16-52-39", HYUNDAI.KIA_NIRO_HEV_2ND_GEN),
+ CarTestRoute("50a2212c41f65c7b|2021-05-24--16-22-06", HYUNDAI.KIA_FORTE),
+ CarTestRoute("192283cdbb7a58c2|2022-10-15--01-43-18", HYUNDAI.KIA_SPORTAGE_5TH_GEN),
+ CarTestRoute("09559f1fcaed4704|2023-11-16--02-24-57", HYUNDAI.KIA_SPORTAGE_5TH_GEN), # openpilot longitudinal
+ CarTestRoute("b3537035ffe6a7d6|2022-10-17--15-23-49", HYUNDAI.KIA_SPORTAGE_5TH_GEN), # hybrid
+ CarTestRoute("c5ac319aa9583f83|2021-06-01--18-18-31", HYUNDAI.HYUNDAI_ELANTRA),
+ CarTestRoute("734ef96182ddf940|2022-10-02--16-41-44", HYUNDAI.HYUNDAI_ELANTRA_GT_I30),
+ CarTestRoute("82e9cdd3f43bf83e|2021-05-15--02-42-51", HYUNDAI.HYUNDAI_ELANTRA_2021),
+ CarTestRoute("715ac05b594e9c59|2021-06-20--16-21-07", HYUNDAI.HYUNDAI_ELANTRA_HEV_2021),
+ CarTestRoute("7120aa90bbc3add7|2021-08-02--07-12-31", HYUNDAI.HYUNDAI_SONATA_HYBRID),
+ CarTestRoute("715ac05b594e9c59|2021-10-27--23-24-56", HYUNDAI.GENESIS_G70_2020),
+ CarTestRoute("6b0d44d22df18134|2023-05-06--10-36-55", HYUNDAI.GENESIS_GV80),
+
+ CarTestRoute("00c829b1b7613dea|2021-06-24--09-10-10", TOYOTA.TOYOTA_ALPHARD_TSS2),
+ CarTestRoute("912119ebd02c7a42|2022-03-19--07-24-50", TOYOTA.TOYOTA_ALPHARD_TSS2), # hybrid
+ CarTestRoute("000cf3730200c71c|2021-05-24--10-42-05", TOYOTA.TOYOTA_AVALON),
+ CarTestRoute("0bb588106852abb7|2021-05-26--12-22-01", TOYOTA.TOYOTA_AVALON_2019),
+ CarTestRoute("87bef2930af86592|2021-05-30--09-40-54", TOYOTA.TOYOTA_AVALON_2019), # hybrid
+ CarTestRoute("e9966711cfb04ce3|2022-01-11--07-59-43", TOYOTA.TOYOTA_AVALON_TSS2),
+ CarTestRoute("eca1080a91720a54|2022-03-17--13-32-29", TOYOTA.TOYOTA_AVALON_TSS2), # hybrid
+ CarTestRoute("6cdecc4728d4af37|2020-02-23--15-44-18", TOYOTA.TOYOTA_CAMRY),
+ CarTestRoute("2f37c007683e85ba|2023-09-02--14-39-44", TOYOTA.TOYOTA_CAMRY), # openpilot longitudinal, with radar CAN filter
+ CarTestRoute("54034823d30962f5|2021-05-24--06-37-34", TOYOTA.TOYOTA_CAMRY), # hybrid
+ CarTestRoute("3456ad0cd7281b24|2020-12-13--17-45-56", TOYOTA.TOYOTA_CAMRY_TSS2),
+ CarTestRoute("ffccc77938ddbc44|2021-01-04--16-55-41", TOYOTA.TOYOTA_CAMRY_TSS2), # hybrid
+ CarTestRoute("4e45c89c38e8ec4d|2021-05-02--02-49-28", TOYOTA.TOYOTA_COROLLA),
+ CarTestRoute("5f5afb36036506e4|2019-05-14--02-09-54", TOYOTA.TOYOTA_COROLLA_TSS2),
+ CarTestRoute("5ceff72287a5c86c|2019-10-19--10-59-02", TOYOTA.TOYOTA_COROLLA_TSS2), # hybrid
+ CarTestRoute("d2525c22173da58b|2021-04-25--16-47-04", TOYOTA.TOYOTA_PRIUS),
+ CarTestRoute("b14c5b4742e6fc85|2020-07-28--19-50-11", TOYOTA.TOYOTA_RAV4),
+ CarTestRoute("32a7df20486b0f70|2020-02-06--16-06-50", TOYOTA.TOYOTA_RAV4H),
+ CarTestRoute("cdf2f7de565d40ae|2019-04-25--03-53-41", TOYOTA.TOYOTA_RAV4_TSS2),
+ CarTestRoute("a5c341bb250ca2f0|2022-05-18--16-05-17", TOYOTA.TOYOTA_RAV4_TSS2_2022),
+ CarTestRoute("ad5a3fa719bc2f83|2023-10-17--19-48-42", TOYOTA.TOYOTA_RAV4_TSS2_2023),
+ CarTestRoute("7e34a988419b5307|2019-12-18--19-13-30", TOYOTA.TOYOTA_RAV4_TSS2), # hybrid
+ CarTestRoute("2475fb3eb2ffcc2e|2022-04-29--12-46-23", TOYOTA.TOYOTA_RAV4_TSS2_2022), # hybrid
+ CarTestRoute("7a31f030957b9c85|2023-04-01--14-12-51", TOYOTA.LEXUS_ES),
+ CarTestRoute("37041c500fd30100|2020-12-30--12-17-24", TOYOTA.LEXUS_ES), # hybrid
+ CarTestRoute("e6a24be49a6cd46e|2019-10-29--10-52-42", TOYOTA.LEXUS_ES_TSS2),
+ CarTestRoute("f49e8041283f2939|2019-05-30--11-51-51", TOYOTA.LEXUS_ES_TSS2), # hybrid
+ CarTestRoute("da23c367491f53e2|2021-05-21--09-09-11", TOYOTA.LEXUS_CTH, segment=3),
+ CarTestRoute("32696cea52831b02|2021-11-19--18-13-30", TOYOTA.LEXUS_RC),
+ CarTestRoute("ab9b64a5e5960cba|2023-10-24--17-32-08", TOYOTA.LEXUS_GS_F),
+ CarTestRoute("886fcd8408d570e9|2020-01-29--02-18-55", TOYOTA.LEXUS_RX),
+ CarTestRoute("d27ad752e9b08d4f|2021-05-26--19-39-51", TOYOTA.LEXUS_RX), # hybrid
+ CarTestRoute("01b22eb2ed121565|2020-02-02--11-25-51", TOYOTA.LEXUS_RX_TSS2),
+ CarTestRoute("b74758c690a49668|2020-05-20--15-58-57", TOYOTA.LEXUS_RX_TSS2), # hybrid
+ CarTestRoute("964c09eb11ca8089|2020-11-03--22-04-00", TOYOTA.LEXUS_NX),
+ CarTestRoute("ec429c0f37564e3c|2020-02-01--17-28-12", TOYOTA.LEXUS_NX), # hybrid
+ CarTestRoute("3fd5305f8b6ca765|2021-04-28--19-26-49", TOYOTA.LEXUS_NX_TSS2),
+ CarTestRoute("09ae96064ed85a14|2022-06-09--12-22-31", TOYOTA.LEXUS_NX_TSS2), # hybrid
+ CarTestRoute("4765fbbf59e3cd88|2024-02-06--17-45-32", TOYOTA.LEXUS_LC_TSS2),
+ CarTestRoute("0a302ffddbb3e3d3|2020-02-08--16-19-08", TOYOTA.TOYOTA_HIGHLANDER_TSS2),
+ CarTestRoute("437e4d2402abf524|2021-05-25--07-58-50", TOYOTA.TOYOTA_HIGHLANDER_TSS2), # hybrid
+ CarTestRoute("3183cd9b021e89ce|2021-05-25--10-34-44", TOYOTA.TOYOTA_HIGHLANDER),
+ CarTestRoute("80d16a262e33d57f|2021-05-23--20-01-43", TOYOTA.TOYOTA_HIGHLANDER), # hybrid
+ CarTestRoute("eb6acd681135480d|2019-06-20--20-00-00", TOYOTA.TOYOTA_SIENNA),
+ CarTestRoute("2e07163a1ba9a780|2019-08-25--13-15-13", TOYOTA.LEXUS_IS),
+ CarTestRoute("649bf2997ada6e3a|2023-08-08--18-04-22", TOYOTA.LEXUS_IS_TSS2),
+ CarTestRoute("0a0de17a1e6a2d15|2020-09-21--21-24-41", TOYOTA.TOYOTA_PRIUS_TSS2),
+ CarTestRoute("9b36accae406390e|2021-03-30--10-41-38", TOYOTA.TOYOTA_MIRAI),
+ CarTestRoute("cd9cff4b0b26c435|2021-05-13--15-12-39", TOYOTA.TOYOTA_CHR),
+ CarTestRoute("57858ede0369a261|2021-05-18--20-34-20", TOYOTA.TOYOTA_CHR), # hybrid
+ CarTestRoute("ea8fbe72b96a185c|2023-02-08--15-11-46", TOYOTA.TOYOTA_CHR_TSS2),
+ CarTestRoute("6719965b0e1d1737|2023-02-09--22-44-05", TOYOTA.TOYOTA_CHR_TSS2), # hybrid
+ CarTestRoute("6719965b0e1d1737|2023-08-29--06-40-05", TOYOTA.TOYOTA_CHR_TSS2), # hybrid, openpilot longitudinal, radar disabled
+ CarTestRoute("14623aae37e549f3|2021-10-24--01-20-49", TOYOTA.TOYOTA_PRIUS_V),
+
+ CarTestRoute("202c40641158a6e5|2021-09-21--09-43-24", VOLKSWAGEN.VOLKSWAGEN_ARTEON_MK1),
+ CarTestRoute("2c68dda277d887ac|2021-05-11--15-22-20", VOLKSWAGEN.VOLKSWAGEN_ATLAS_MK1),
+ CarTestRoute("ffcd23abbbd02219|2024-02-28--14-59-38", VOLKSWAGEN.VOLKSWAGEN_CADDY_MK3),
+ CarTestRoute("cae14e88932eb364|2021-03-26--14-43-28", VOLKSWAGEN.VOLKSWAGEN_GOLF_MK7), # Stock ACC
+ CarTestRoute("3cfdec54aa035f3f|2022-10-13--14-58-58", VOLKSWAGEN.VOLKSWAGEN_GOLF_MK7), # openpilot longitudinal
+ CarTestRoute("58a7d3b707987d65|2021-03-25--17-26-37", VOLKSWAGEN.VOLKSWAGEN_JETTA_MK7),
+ CarTestRoute("4d134e099430fba2|2021-03-26--00-26-06", VOLKSWAGEN.VOLKSWAGEN_PASSAT_MK8),
+ CarTestRoute("3cfdec54aa035f3f|2022-07-19--23-45-10", VOLKSWAGEN.VOLKSWAGEN_PASSAT_NMS),
+ CarTestRoute("0cd0b7f7e31a3853|2021-11-03--19-30-22", VOLKSWAGEN.VOLKSWAGEN_POLO_MK6),
+ CarTestRoute("064d1816e448f8eb|2022-09-29--15-32-34", VOLKSWAGEN.VOLKSWAGEN_SHARAN_MK2),
+ CarTestRoute("7d82b2f3a9115f1f|2021-10-21--15-39-42", VOLKSWAGEN.VOLKSWAGEN_TAOS_MK1),
+ CarTestRoute("2744c89a8dda9a51|2021-07-24--21-28-06", VOLKSWAGEN.VOLKSWAGEN_TCROSS_MK1),
+ CarTestRoute("2cef8a0b898f331a|2021-03-25--20-13-57", VOLKSWAGEN.VOLKSWAGEN_TIGUAN_MK2),
+ CarTestRoute("a589dcc642fdb10a|2021-06-14--20-54-26", VOLKSWAGEN.VOLKSWAGEN_TOURAN_MK2),
+ CarTestRoute("a459f4556782eba1|2021-09-19--09-48-00", VOLKSWAGEN.VOLKSWAGEN_TRANSPORTER_T61),
+ CarTestRoute("0cd0b7f7e31a3853|2021-11-18--00-38-32", VOLKSWAGEN.VOLKSWAGEN_TROC_MK1),
+ CarTestRoute("07667b885add75fd|2021-01-23--19-48-42", VOLKSWAGEN.AUDI_A3_MK3),
+ CarTestRoute("6c6b466346192818|2021-06-06--14-17-47", VOLKSWAGEN.AUDI_Q2_MK1),
+ CarTestRoute("0cd0b7f7e31a3853|2021-12-03--03-12-05", VOLKSWAGEN.AUDI_Q3_MK2),
+ CarTestRoute("8f205bdd11bcbb65|2021-03-26--01-00-17", VOLKSWAGEN.SEAT_ATECA_MK1),
+ CarTestRoute("fc6b6c9a3471c846|2021-05-27--13-39-56", VOLKSWAGEN.SEAT_ATECA_MK1), # Leon
+ CarTestRoute("0bbe367c98fa1538|2023-03-04--17-46-11", VOLKSWAGEN.SKODA_FABIA_MK4),
+ CarTestRoute("12d6ae3057c04b0d|2021-09-15--00-04-07", VOLKSWAGEN.SKODA_KAMIQ_MK1),
+ CarTestRoute("12d6ae3057c04b0d|2021-09-04--21-21-21", VOLKSWAGEN.SKODA_KAROQ_MK1),
+ CarTestRoute("90434ff5d7c8d603|2021-03-15--12-07-31", VOLKSWAGEN.SKODA_KODIAQ_MK1),
+ CarTestRoute("66e5edc3a16459c5|2021-05-25--19-00-29", VOLKSWAGEN.SKODA_OCTAVIA_MK3),
+ CarTestRoute("026b6d18fba6417f|2021-03-26--09-17-04", VOLKSWAGEN.SKODA_KAMIQ_MK1), # Scala
+ CarTestRoute("b2e9858e29db492b|2021-03-26--16-58-42", VOLKSWAGEN.SKODA_SUPERB_MK3),
+
+ CarTestRoute("3c8f0c502e119c1c|2020-06-30--12-58-02", SUBARU.SUBARU_ASCENT),
+ CarTestRoute("c321c6b697c5a5ff|2020-06-23--11-04-33", SUBARU.SUBARU_FORESTER),
+ CarTestRoute("791340bc01ed993d|2019-03-10--16-28-08", SUBARU.SUBARU_IMPREZA),
+ CarTestRoute("8bf7e79a3ce64055|2021-05-24--09-36-27", SUBARU.SUBARU_IMPREZA_2020),
+ CarTestRoute("8de015561e1ea4a0|2023-08-29--17-08-31", SUBARU.SUBARU_IMPREZA), # openpilot longitudinal
+ # CarTestRoute("c3d1ccb52f5f9d65|2023-07-22--01-23-20", SUBARU.OUTBACK, segment=9), # gen2 longitudinal, eyesight disabled
+ CarTestRoute("1bbe6bf2d62f58a8|2022-07-14--17-11-43", SUBARU.SUBARU_OUTBACK, segment=10),
+ CarTestRoute("c56e69bbc74b8fad|2022-08-18--09-43-51", SUBARU.SUBARU_LEGACY, segment=3),
+ CarTestRoute("f4e3a0c511a076f4|2022-08-04--16-16-48", SUBARU.SUBARU_CROSSTREK_HYBRID, segment=2),
+ CarTestRoute("7fd1e4f3a33c1673|2022-12-04--15-09-53", SUBARU.SUBARU_FORESTER_2022, segment=4),
+ CarTestRoute("f3b34c0d2632aa83|2023-07-23--20-43-25", SUBARU.SUBARU_OUTBACK_2023, segment=7),
+ CarTestRoute("99437cef6d5ff2ee|2023-03-13--21-21-38", SUBARU.SUBARU_ASCENT_2023, segment=7),
+ # Pre-global, dashcam
+ CarTestRoute("95441c38ae8c130e|2020-06-08--12-10-17", SUBARU.SUBARU_FORESTER_PREGLOBAL),
+ CarTestRoute("df5ca7660000fba8|2020-06-16--17-37-19", SUBARU.SUBARU_LEGACY_PREGLOBAL),
+ CarTestRoute("5ab784f361e19b78|2020-06-08--16-30-41", SUBARU.SUBARU_OUTBACK_PREGLOBAL),
+ CarTestRoute("e19eb5d5353b1ac1|2020-08-09--14-37-56", SUBARU.SUBARU_OUTBACK_PREGLOBAL_2018),
+
+ CarTestRoute("fbbfa6af821552b9|2020-03-03--08-09-43", NISSAN.NISSAN_XTRAIL),
+ CarTestRoute("5b7c365c50084530|2020-03-25--22-10-13", NISSAN.NISSAN_LEAF),
+ CarTestRoute("22c3dcce2dd627eb|2020-12-30--16-38-48", NISSAN.NISSAN_LEAF_IC),
+ CarTestRoute("059ab9162e23198e|2020-05-30--09-41-01", NISSAN.NISSAN_ROGUE),
+ CarTestRoute("b72d3ec617c0a90f|2020-12-11--15-38-17", NISSAN.NISSAN_ALTIMA),
+
+ CarTestRoute("32a319f057902bb3|2020-04-27--15-18-58", MAZDA.MAZDA_CX5),
+ CarTestRoute("10b5a4b380434151|2020-08-26--17-11-45", MAZDA.MAZDA_CX9),
+ CarTestRoute("74f1038827005090|2020-08-26--20-05-50", MAZDA.MAZDA_3),
+ CarTestRoute("fb53c640f499b73d|2021-06-01--04-17-56", MAZDA.MAZDA_6),
+ CarTestRoute("f6d5b1a9d7a1c92e|2021-07-08--06-56-59", MAZDA.MAZDA_CX9_2021),
+ CarTestRoute("a4af1602d8e668ac|2022-02-03--12-17-07", MAZDA.MAZDA_CX5_2022),
+
+ # Segments that test specific issues
+ # Controls mismatch due to standstill threshold
+ CarTestRoute("bec2dcfde6a64235|2022-04-08--14-21-32", HONDA.HONDA_CRV_HYBRID, segment=22),
+]
diff --git a/opendbc/car/tests/test_can_fingerprint.py b/opendbc/car/tests/test_can_fingerprint.py
new file mode 100644
index 0000000000..79863378f5
--- /dev/null
+++ b/opendbc/car/tests/test_can_fingerprint.py
@@ -0,0 +1,56 @@
+from parameterized import parameterized
+
+from opendbc.car.can_definitions import CanData
+from opendbc.car.car_helpers import FRAME_FINGERPRINT, can_fingerprint
+from opendbc.car.fingerprints import _FINGERPRINTS as FINGERPRINTS
+
+
+class TestCanFingerprint:
+ @parameterized.expand(list(FINGERPRINTS.items()))
+ def test_can_fingerprint(self, car_model, fingerprints):
+ """Tests online fingerprinting function on offline fingerprints"""
+
+ for fingerprint in fingerprints: # can have multiple fingerprints for each platform
+ can = [CanData(address=address, dat=b'\x00' * length, src=src)
+ for address, length in fingerprint.items() for src in (0, 1)]
+
+ fingerprint_iter = iter([can])
+ car_fingerprint, finger = can_fingerprint(lambda **kwargs: [next(fingerprint_iter, [])]) # noqa: B023
+
+ assert car_fingerprint == car_model
+ assert finger[0] == fingerprint
+ assert finger[1] == fingerprint
+ assert finger[2] == {}
+
+ def test_timing(self, subtests):
+ # just pick any CAN fingerprinting car
+ car_model = "CHEVROLET_BOLT_EUV"
+ fingerprint = FINGERPRINTS[car_model][0]
+
+ cases = []
+
+ # case 1 - one match, make sure we keep going for 100 frames
+ can = [CanData(address=address, dat=b'\x00' * length, src=src)
+ for address, length in fingerprint.items() for src in (0, 1)]
+ cases.append((FRAME_FINGERPRINT, car_model, can))
+
+ # case 2 - no matches, make sure we keep going for 100 frames
+ can = [CanData(address=1, dat=b'\x00' * 1, src=src) for src in (0, 1)] # uncommon address
+ cases.append((FRAME_FINGERPRINT, None, can))
+
+ # case 3 - multiple matches, make sure we keep going for 200 frames to try to eliminate some
+ can = [CanData(address=2016, dat=b'\x00' * 8, src=src) for src in (0, 1)] # common address
+ cases.append((FRAME_FINGERPRINT * 2, None, can))
+
+ for expected_frames, car_model, can in cases:
+ with subtests.test(expected_frames=expected_frames, car_model=car_model):
+ frames = 0
+
+ def can_recv(**kwargs):
+ nonlocal frames
+ frames += 1
+ return [can] # noqa: B023
+
+ car_fingerprint, _ = can_fingerprint(can_recv)
+ assert car_fingerprint == car_model
+ assert frames == expected_frames + 2 # TODO: fix extra frames
diff --git a/opendbc/car/tests/test_car_interfaces.py b/opendbc/car/tests/test_car_interfaces.py
new file mode 100644
index 0000000000..f54bb3edae
--- /dev/null
+++ b/opendbc/car/tests/test_car_interfaces.py
@@ -0,0 +1,146 @@
+import os
+import math
+import hypothesis.strategies as st
+from hypothesis import Phase, given, settings
+import importlib
+from parameterized import parameterized
+from collections.abc import Callable
+from typing import Any
+
+from opendbc.car import DT_CTRL, CanData, gen_empty_fingerprint, structs
+from opendbc.car.car_helpers import interfaces
+from opendbc.car.fingerprints import all_known_cars
+from opendbc.car.fw_versions import FW_VERSIONS, FW_QUERY_CONFIGS
+from opendbc.car.interfaces import get_interface_attr
+from opendbc.car.mock.values import CAR as MOCK
+
+DrawType = Callable[[st.SearchStrategy], Any]
+
+ALL_ECUS = {ecu for ecus in FW_VERSIONS.values() for ecu in ecus.keys()}
+ALL_ECUS |= {ecu for config in FW_QUERY_CONFIGS.values() for ecu in config.extra_ecus}
+
+ALL_REQUESTS = {tuple(r.request) for config in FW_QUERY_CONFIGS.values() for r in config.requests}
+
+MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '5'))
+
+
+def get_fuzzy_car_interface_args(draw: DrawType) -> dict:
+ # Fuzzy CAN fingerprints and FW versions to test more states of the CarInterface
+ fingerprint_strategy = st.fixed_dictionaries({key: st.dictionaries(st.integers(min_value=0, max_value=0x800),
+ st.integers(min_value=0, max_value=64)) for key in
+ gen_empty_fingerprint()})
+
+ # only pick from possible ecus to reduce search space
+ car_fw_strategy = st.lists(st.sampled_from(sorted(ALL_ECUS)))
+
+ params_strategy = st.fixed_dictionaries({
+ 'fingerprints': fingerprint_strategy,
+ 'car_fw': car_fw_strategy,
+ 'experimental_long': st.booleans(),
+ })
+
+ params: dict = draw(params_strategy)
+ params['car_fw'] = [structs.CarParams.CarFw(ecu=fw[0], address=fw[1], subAddress=fw[2] or 0,
+ request=draw(st.sampled_from(sorted(ALL_REQUESTS))))
+ for fw in params['car_fw']]
+ return params
+
+
+class TestCarInterfaces:
+ # FIXME: Due to the lists used in carParams, Phase.target is very slow and will cause
+ # many generated examples to overrun when max_examples > ~20, don't use it
+ @parameterized.expand([(car,) for car in sorted(all_known_cars())] + [MOCK.MOCK])
+ @settings(max_examples=MAX_EXAMPLES, deadline=None,
+ phases=(Phase.reuse, Phase.generate, Phase.shrink))
+ @given(data=st.data())
+ def test_car_interfaces(self, car_name, data):
+ CarInterface, CarController, CarState = interfaces[car_name]
+
+ args = get_fuzzy_car_interface_args(data.draw)
+
+ car_params = CarInterface.get_params(car_name, args['fingerprints'], args['car_fw'],
+ experimental_long=args['experimental_long'], docs=False)
+ car_interface = CarInterface(car_params, CarController, CarState)
+ assert car_params
+ assert car_interface
+
+ assert car_params.mass > 1
+ assert car_params.wheelbase > 0
+ # centerToFront is center of gravity to front wheels, assert a reasonable range
+ assert car_params.wheelbase * 0.3 < car_params.centerToFront < car_params.wheelbase * 0.7
+ assert car_params.maxLateralAccel > 0
+
+ # Longitudinal sanity checks
+ assert len(car_params.longitudinalTuning.kpV) == len(car_params.longitudinalTuning.kpBP)
+ assert len(car_params.longitudinalTuning.kiV) == len(car_params.longitudinalTuning.kiBP)
+
+ # Lateral sanity checks
+ if car_params.steerControlType != structs.CarParams.SteerControlType.angle:
+ tune = car_params.lateralTuning
+ if tune.which() == 'pid':
+ if car_name != MOCK.MOCK:
+ assert not math.isnan(tune.pid.kf) and tune.pid.kf > 0
+ assert len(tune.pid.kpV) > 0 and len(tune.pid.kpV) == len(tune.pid.kpBP)
+ assert len(tune.pid.kiV) > 0 and len(tune.pid.kiV) == len(tune.pid.kiBP)
+
+ elif tune.which() == 'torque':
+ assert not math.isnan(tune.torque.kf) and tune.torque.kf > 0
+ assert not math.isnan(tune.torque.friction) and tune.torque.friction > 0
+
+ # Run car interface
+ # TODO: use hypothesis to generate random messages
+ now_nanos = 0
+ CC = structs.CarControl()
+ for _ in range(10):
+ car_interface.update([])
+ car_interface.apply(CC, now_nanos)
+ now_nanos += DT_CTRL * 1e9 # 10 ms
+
+ CC = structs.CarControl()
+ CC.enabled = True
+ for _ in range(10):
+ car_interface.update([])
+ car_interface.apply(CC, now_nanos)
+ now_nanos += DT_CTRL * 1e9 # 10ms
+
+ # Test radar interface
+ RadarInterface = importlib.import_module(f'opendbc.car.{car_params.carName}.radar_interface').RadarInterface
+ radar_interface = RadarInterface(car_params)
+ assert radar_interface
+
+ # Run radar interface once
+ radar_interface.update([])
+ if not car_params.radarUnavailable and radar_interface.rcp is not None and \
+ hasattr(radar_interface, '_update') and hasattr(radar_interface, 'trigger_msg'):
+ radar_interface._update([radar_interface.trigger_msg])
+
+ # Test radar fault
+ if not car_params.radarUnavailable and radar_interface.rcp is not None:
+ cans = [(0, [CanData(0, b'', 0) for _ in range(5)])]
+ rr = radar_interface.update(cans)
+ assert rr is None or len(rr.errors) > 0
+
+ def test_interface_attrs(self):
+ """Asserts basic behavior of interface attribute getter"""
+ num_brands = len(get_interface_attr('CAR'))
+ assert num_brands >= 12
+
+ # Should return value for all brands when not combining, even if attribute doesn't exist
+ ret = get_interface_attr('FAKE_ATTR')
+ assert len(ret) == num_brands
+
+ # Make sure we can combine dicts
+ ret = get_interface_attr('DBC', combine_brands=True)
+ assert len(ret) >= 160
+
+ # We don't support combining non-dicts
+ ret = get_interface_attr('CAR', combine_brands=True)
+ assert len(ret) == 0
+
+ # If brand has None value, it shouldn't return when ignore_none=True is specified
+ none_brands = {b for b, v in get_interface_attr('FINGERPRINTS').items() if v is None}
+ assert len(none_brands) >= 1
+
+ ret = get_interface_attr('FINGERPRINTS', ignore_none=True)
+ none_brands_in_ret = none_brands.intersection(ret)
+ assert len(none_brands_in_ret) == 0, f'Brands with None values in ignore_none=True result: {none_brands_in_ret}'
diff --git a/opendbc/car/tests/test_docs.py b/opendbc/car/tests/test_docs.py
new file mode 100644
index 0000000000..9e6e7b632d
--- /dev/null
+++ b/opendbc/car/tests/test_docs.py
@@ -0,0 +1,74 @@
+from collections import defaultdict
+import pytest
+import re
+
+from opendbc.car.car_helpers import interfaces
+from opendbc.car.docs import get_all_car_docs
+from opendbc.car.docs_definitions import Cable, Column, PartType, Star
+from opendbc.car.honda.values import CAR as HONDA
+from opendbc.car.values import PLATFORMS
+
+
+class TestCarDocs:
+ @classmethod
+ def setup_class(cls):
+ cls.all_cars = get_all_car_docs()
+
+ def test_duplicate_years(self, subtests):
+ make_model_years = defaultdict(list)
+ for car in self.all_cars:
+ with subtests.test(car_docs_name=car.name):
+ make_model = (car.make, car.model)
+ for year in car.year_list:
+ assert year not in make_model_years[make_model], f"{car.name}: Duplicate model year"
+ make_model_years[make_model].append(year)
+
+ def test_missing_car_docs(self, subtests):
+ all_car_docs_platforms = [name for name, config in PLATFORMS.items()]
+ for platform in sorted(interfaces.keys()):
+ with subtests.test(platform=platform):
+ assert platform in all_car_docs_platforms, f"Platform: {platform} doesn't have a CarDocs entry"
+
+ def test_naming_conventions(self, subtests):
+ # Asserts market-standard car naming conventions by brand
+ for car in self.all_cars:
+ with subtests.test(car=car.name):
+ tokens = car.model.lower().split(" ")
+ if car.car_name == "hyundai":
+ assert "phev" not in tokens, "Use `Plug-in Hybrid`"
+ assert "hev" not in tokens, "Use `Hybrid`"
+ if "plug-in hybrid" in car.model.lower():
+ assert "Plug-in Hybrid" in car.model, "Use correct capitalization"
+ if car.make != "Kia":
+ assert "ev" not in tokens, "Use `Electric`"
+ elif car.car_name == "toyota":
+ if "rav4" in tokens:
+ assert "RAV4" in car.model, "Use correct capitalization"
+
+ def test_torque_star(self, subtests):
+ # Asserts brand-specific assumptions around steering torque star
+ for car in self.all_cars:
+ with subtests.test(car=car.name):
+ # honda sanity check, it's the definition of a no torque star
+ if car.car_fingerprint in (HONDA.HONDA_ACCORD, HONDA.HONDA_CIVIC, HONDA.HONDA_CRV, HONDA.HONDA_ODYSSEY, HONDA.HONDA_PILOT):
+ assert car.row[Column.STEERING_TORQUE] == Star.EMPTY, f"{car.name} has full torque star"
+ elif car.car_name in ("toyota", "hyundai"):
+ assert car.row[Column.STEERING_TORQUE] != Star.EMPTY, f"{car.name} has no torque star"
+
+ def test_year_format(self, subtests):
+ for car in self.all_cars:
+ with subtests.test(car=car.name):
+ assert re.search(r"\d{4}-\d{4}", car.name) is None, f"Format years correctly: {car.name}"
+
+ def test_harnesses(self, subtests):
+ for car in self.all_cars:
+ with subtests.test(car=car.name):
+ if car.name == "comma body":
+ pytest.skip()
+
+ car_part_type = [p.part_type for p in car.car_parts.all_parts()]
+ car_parts = list(car.car_parts.all_parts())
+ assert len(car_parts) > 0, f"Need to specify car parts: {car.name}"
+ assert car_part_type.count(PartType.connector) == 1, f"Need to specify one harness connector: {car.name}"
+ assert car_part_type.count(PartType.mount) == 1, f"Need to specify one mount: {car.name}"
+ assert Cable.right_angle_obd_c_cable_1_5ft in car_parts, f"Need to specify a right angle OBD-C cable (1.5ft): {car.name}"
diff --git a/opendbc/car/tests/test_fw_fingerprint.py b/opendbc/car/tests/test_fw_fingerprint.py
new file mode 100644
index 0000000000..aabdaa4ce6
--- /dev/null
+++ b/opendbc/car/tests/test_fw_fingerprint.py
@@ -0,0 +1,330 @@
+import pytest
+import random
+import time
+from collections import defaultdict
+from parameterized import parameterized
+
+from opendbc.car.can_definitions import CanData
+from opendbc.car.car_helpers import interfaces
+from opendbc.car.structs import CarParams
+from opendbc.car.fingerprints import FW_VERSIONS
+from opendbc.car.fw_versions import ESSENTIAL_ECUS, FW_QUERY_CONFIGS, FUZZY_EXCLUDE_ECUS, VERSIONS, build_fw_dict, \
+ match_fw_to_car, get_brand_ecu_matches, get_fw_versions, get_present_ecus
+from opendbc.car.vin import get_vin
+
+CarFw = CarParams.CarFw
+Ecu = CarParams.Ecu
+
+
+class TestFwFingerprint:
+ def assertFingerprints(self, candidates, expected):
+ candidates = list(candidates)
+ assert len(candidates) == 1, f"got more than one candidate: {candidates}"
+ assert candidates[0] == expected
+
+ @parameterized.expand([(b, c, e[c], n) for b, e in VERSIONS.items() for c in e for n in (True, False)])
+ def test_exact_match(self, brand, car_model, ecus, test_non_essential):
+ config = FW_QUERY_CONFIGS[brand]
+ CP = CarParams()
+ for _ in range(20):
+ fw = []
+ for ecu, fw_versions in ecus.items():
+ # Assume non-essential ECUs apply to all cars, so we catch cases where Car A with
+ # missing ECUs won't match to Car B where only Car B has labeled non-essential ECUs
+ if ecu[0] in config.non_essential_ecus and test_non_essential:
+ continue
+
+ ecu_name, addr, sub_addr = ecu
+ fw.append(CarFw(ecu=ecu_name, fwVersion=random.choice(fw_versions), brand=brand,
+ address=addr, subAddress=0 if sub_addr is None else sub_addr))
+ CP.carFw = fw
+ _, matches = match_fw_to_car(CP.carFw, CP.carVin, allow_fuzzy=False)
+ if not test_non_essential:
+ self.assertFingerprints(matches, car_model)
+ else:
+ # if we're removing ECUs we expect some match loss, but it shouldn't mismatch
+ if len(matches) != 0:
+ self.assertFingerprints(matches, car_model)
+
+ @parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e])
+ def test_custom_fuzzy_match(self, brand, car_model, ecus):
+ # Assert brand-specific fuzzy fingerprinting function doesn't disagree with standard fuzzy function
+ config = FW_QUERY_CONFIGS[brand]
+ if config.match_fw_to_car_fuzzy is None:
+ pytest.skip("Brand does not implement custom fuzzy fingerprinting function")
+
+ CP = CarParams()
+ for _ in range(5):
+ fw = []
+ for ecu, fw_versions in ecus.items():
+ ecu_name, addr, sub_addr = ecu
+ fw.append(CarFw(ecu=ecu_name, fwVersion=random.choice(fw_versions), brand=brand,
+ address=addr, subAddress=0 if sub_addr is None else sub_addr))
+ CP.carFw = fw
+ _, matches = match_fw_to_car(CP.carFw, CP.carVin, allow_exact=False, log=False)
+ brand_matches = config.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, VERSIONS[brand])
+
+ # If both have matches, they must agree
+ if len(matches) == 1 and len(brand_matches) == 1:
+ assert matches == brand_matches
+
+ @parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e])
+ def test_fuzzy_match_ecu_count(self, brand, car_model, ecus):
+ # Asserts that fuzzy matching does not count matching FW, but ECU address keys
+ valid_ecus = [e for e in ecus if e[0] not in FUZZY_EXCLUDE_ECUS]
+ if not len(valid_ecus):
+ pytest.skip("Car model has no compatible ECUs for fuzzy matching")
+
+ fw = []
+ for ecu in valid_ecus:
+ ecu_name, addr, sub_addr = ecu
+ for _ in range(5):
+ # Add multiple FW versions to simulate ECU returning to multiple queries in a brand
+ fw.append(CarFw(ecu=ecu_name, fwVersion=random.choice(ecus[ecu]), brand=brand,
+ address=addr, subAddress=0 if sub_addr is None else sub_addr))
+ CP = CarParams(carFw=fw)
+ _, matches = match_fw_to_car(CP.carFw, CP.carVin, allow_exact=False, log=False)
+
+ # Assert no match if there are not enough unique ECUs
+ unique_ecus = {(f.address, f.subAddress) for f in fw}
+ if len(unique_ecus) < 2:
+ assert len(matches) == 0, car_model
+ # There won't always be a match due to shared FW, but if there is it should be correct
+ elif len(matches):
+ self.assertFingerprints(matches, car_model)
+
+ def test_fw_version_lists(self, subtests):
+ for car_model, ecus in FW_VERSIONS.items():
+ with subtests.test(car_model=car_model.value):
+ for ecu, ecu_fw in ecus.items():
+ with subtests.test((ecu[0].value, ecu[1], ecu[2])):
+ duplicates = {fw for fw in ecu_fw if ecu_fw.count(fw) > 1}
+ assert not len(duplicates), f'{car_model}: Duplicate FW versions: Ecu.{ecu[0]}, {duplicates}'
+ assert len(ecu_fw) > 0, f'{car_model}: No FW versions: Ecu.{ecu[0]}'
+
+ def test_all_addrs_map_to_one_ecu(self):
+ for brand, cars in VERSIONS.items():
+ addr_to_ecu = defaultdict(set)
+ for ecus in cars.values():
+ for ecu_type, addr, sub_addr in ecus.keys():
+ addr_to_ecu[(addr, sub_addr)].add(ecu_type)
+ ecus_for_addr = addr_to_ecu[(addr, sub_addr)]
+ ecu_strings = ", ".join([f'Ecu.{ecu}' for ecu in ecus_for_addr])
+ assert len(ecus_for_addr) <= 1, f"{brand} has multiple ECUs that map to one address: {ecu_strings} -> ({hex(addr)}, {sub_addr})"
+
+ def test_data_collection_ecus(self, subtests):
+ # Asserts no extra ECUs are in the fingerprinting database
+ for brand, config in FW_QUERY_CONFIGS.items():
+ for car_model, ecus in VERSIONS[brand].items():
+ bad_ecus = set(ecus).intersection(config.extra_ecus)
+ with subtests.test(car_model=car_model.value):
+ assert not len(bad_ecus), f'{car_model}: Fingerprints contain ECUs added for data collection: {bad_ecus}'
+
+ def test_blacklisted_ecus(self, subtests):
+ blacklisted_addrs = (0x7c4, 0x7d0) # includes A/C ecu and an unknown ecu
+ for car_model, ecus in FW_VERSIONS.items():
+ with subtests.test(car_model=car_model.value):
+ CP = interfaces[car_model][0].get_non_essential_params(car_model)
+ if CP.carName == 'subaru':
+ for ecu in ecus.keys():
+ assert ecu[1] not in blacklisted_addrs, f'{car_model}: Blacklisted ecu: (Ecu.{ecu[0]}, {hex(ecu[1])})'
+
+ elif CP.carName == "chrysler":
+ # Some HD trucks have a combined TCM and ECM
+ if CP.carFingerprint.startswith("RAM_HD"):
+ for ecu in ecus.keys():
+ assert ecu[0] != Ecu.transmission, f"{car_model}: Blacklisted ecu: (Ecu.{ecu[0]}, {hex(ecu[1])})"
+
+ def test_non_essential_ecus(self, subtests):
+ for brand, config in FW_QUERY_CONFIGS.items():
+ with subtests.test(brand):
+ # These ECUs are already not in ESSENTIAL_ECUS which the fingerprint functions give a pass if missing
+ unnecessary_non_essential_ecus = set(config.non_essential_ecus) - set(ESSENTIAL_ECUS)
+ assert unnecessary_non_essential_ecus == set(), "Declaring non-essential ECUs non-essential is not required: " + \
+ f"{', '.join([f'Ecu.{ecu}' for ecu in unnecessary_non_essential_ecus])}"
+
+ def test_missing_versions_and_configs(self, subtests):
+ brand_versions = set(VERSIONS.keys())
+ brand_configs = set(FW_QUERY_CONFIGS.keys())
+ if len(brand_configs - brand_versions):
+ with subtests.test():
+ pytest.fail(f"Brands do not implement FW_VERSIONS: {brand_configs - brand_versions}")
+
+ if len(brand_versions - brand_configs):
+ with subtests.test():
+ pytest.fail(f"Brands do not implement FW_QUERY_CONFIG: {brand_versions - brand_configs}")
+
+ # Ensure each brand has at least 1 ECU to query, and extra ECU retrieval
+ for brand, config in FW_QUERY_CONFIGS.items():
+ assert len(config.get_all_ecus({}, include_extra_ecus=False)) == 0
+ assert config.get_all_ecus({}) == set(config.extra_ecus)
+ assert len(config.get_all_ecus(VERSIONS[brand])) > 0
+
+ def test_fw_request_ecu_whitelist(self, subtests):
+ for brand, config in FW_QUERY_CONFIGS.items():
+ with subtests.test(brand=brand):
+ whitelisted_ecus = {ecu for r in config.requests for ecu in r.whitelist_ecus}
+ brand_ecus = {fw[0] for car_fw in VERSIONS[brand].values() for fw in car_fw}
+ brand_ecus |= {ecu[0] for ecu in config.extra_ecus}
+
+ # each ecu in brand's fw versions + extra ecus needs to be whitelisted at least once
+ ecus_not_whitelisted = brand_ecus - whitelisted_ecus
+
+ ecu_strings = ", ".join([f'Ecu.{ecu}' for ecu in ecus_not_whitelisted])
+ assert not (len(whitelisted_ecus) and len(ecus_not_whitelisted)), \
+ f'{brand.title()}: ECUs not in any FW query whitelists: {ecu_strings}'
+
+ def test_fw_requests(self, subtests):
+ # Asserts equal length request and response lists
+ for brand, config in FW_QUERY_CONFIGS.items():
+ with subtests.test(brand=brand):
+ for request_obj in config.requests:
+ assert len(request_obj.request) == len(request_obj.response)
+
+ # No request on the OBD port (bus 1, multiplexed) should be run on an aux panda
+ assert not (request_obj.auxiliary and request_obj.bus == 1 and request_obj.obd_multiplexing), \
+ f"{brand.title()}: OBD multiplexed request is marked auxiliary: {request_obj}"
+
+ def test_brand_ecu_matches(self):
+ empty_response = {brand: set() for brand in FW_QUERY_CONFIGS}
+ assert get_brand_ecu_matches(set()) == empty_response
+
+ # we ignore bus
+ expected_response = empty_response | {'toyota': {(0x750, 0xf)}}
+ assert get_brand_ecu_matches({(0x758, 0xf, 99)}) == expected_response
+
+
+class TestFwFingerprintTiming:
+ N: int = 5
+ TOL: float = 0.05
+
+ # for patched functions
+ current_obd_multiplexing: bool
+ total_time: float
+
+ @staticmethod
+ def fake_can_send(msgs):
+ pass
+
+ @staticmethod
+ def fake_can_recv(wait_for_one: bool = False) -> list[list[CanData]]:
+ return ([[CanData(random.randint(0x600, 0x800), b'\x00' * 8, 0)]]
+ if random.uniform(0, 1) > 0.5 else [])
+
+ def fake_set_obd_multiplexing(self, obd_multiplexing):
+ """The 10Hz blocking params loop adds on average 50ms to the query time for each OBD multiplexing change"""
+ if obd_multiplexing != self.current_obd_multiplexing:
+ self.current_obd_multiplexing = obd_multiplexing
+ self.total_time += 0.1 / 2
+
+ def fake_get_data(self, timeout):
+ self.total_time += timeout
+ return {}
+
+ def _benchmark_brand(self, brand, num_pandas, mocker):
+ self.total_time = 0
+ mocker.patch("opendbc.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)
+ for _ in range(self.N):
+ # Treat each brand as the most likely (aka, the first) brand with OBD multiplexing initially on
+ self.current_obd_multiplexing = True
+
+ t = time.perf_counter()
+ get_fw_versions(self.fake_can_recv, self.fake_can_send, self.fake_set_obd_multiplexing, brand, num_pandas=num_pandas)
+ self.total_time += time.perf_counter() - t
+
+ return self.total_time / self.N
+
+ def _assert_timing(self, avg_time, ref_time):
+ assert avg_time < ref_time + self.TOL
+ assert avg_time > ref_time - self.TOL, "Performance seems to have improved, update test refs."
+
+ def test_startup_timing(self, subtests, mocker):
+ # Tests worse-case VIN query time and typical present ECU query time
+ vin_ref_times = {'worst': 1.4, 'best': 0.7} # best assumes we go through all queries to get a match
+ present_ecu_ref_time = 0.45
+
+ def fake_get_ecu_addrs(*_, timeout):
+ self.total_time += timeout
+ return set()
+
+ self.total_time = 0.0
+ mocker.patch("opendbc.car.fw_versions.get_ecu_addrs", fake_get_ecu_addrs)
+ for _ in range(self.N):
+ self.current_obd_multiplexing = True
+ get_present_ecus(self.fake_can_recv, self.fake_can_send, self.fake_set_obd_multiplexing, num_pandas=2)
+ self._assert_timing(self.total_time / self.N, present_ecu_ref_time)
+ print(f'get_present_ecus, query time={self.total_time / self.N} seconds')
+
+ for name, args in (('worst', {}), ('best', {'retry': 1})):
+ with subtests.test(name=name):
+ self.total_time = 0.0
+ mocker.patch("opendbc.car.isotp_parallel_query.IsoTpParallelQuery.get_data", self.fake_get_data)
+ for _ in range(self.N):
+ get_vin(self.fake_can_recv, self.fake_can_send, (0, 1), **args)
+ self._assert_timing(self.total_time / self.N, vin_ref_times[name])
+ print(f'get_vin {name} case, query time={self.total_time / self.N} seconds')
+
+ def test_fw_query_timing(self, subtests, mocker):
+ total_ref_time = {1: 6.9, 2: 7.5}
+ brand_ref_times = {
+ 1: {
+ 'gm': 1.0,
+ 'body': 0.1,
+ 'chrysler': 0.3,
+ 'ford': 1.5,
+ 'honda': 0.45,
+ 'hyundai': 0.65,
+ 'mazda': 0.1,
+ 'nissan': 0.8,
+ 'subaru': 0.65,
+ 'toyota': 0.7,
+ 'volkswagen': 0.65,
+ },
+ 2: {
+ 'ford': 1.6,
+ 'hyundai': 1.15,
+ }
+ }
+
+ total_times = {1: 0.0, 2: 0.0}
+ for num_pandas in (1, 2):
+ for brand, config in FW_QUERY_CONFIGS.items():
+ with subtests.test(brand=brand, num_pandas=num_pandas):
+ avg_time = self._benchmark_brand(brand, num_pandas, mocker)
+ total_times[num_pandas] += avg_time
+ avg_time = round(avg_time, 2)
+
+ ref_time = brand_ref_times[num_pandas].get(brand)
+ if ref_time is None:
+ # ref time should be same as 1 panda if no aux queries
+ ref_time = brand_ref_times[num_pandas - 1][brand]
+
+ self._assert_timing(avg_time, ref_time)
+ print(f'{brand=}, {num_pandas=}, {len(config.requests)=}, avg FW query time={avg_time} seconds')
+
+ for num_pandas in (1, 2):
+ with subtests.test(brand='all_brands', num_pandas=num_pandas):
+ total_time = round(total_times[num_pandas], 2)
+ self._assert_timing(total_time, total_ref_time[num_pandas])
+ print(f'all brands, total FW query time={total_time} seconds')
+
+ def test_get_fw_versions(self, subtests, mocker):
+ # some coverage on IsoTpParallelQuery and panda UDS library
+ # TODO: replace this with full fingerprint simulation testing
+ # https://github.com/commaai/panda/pull/1329
+
+ def fake_carlog_exception(*args, **kwargs):
+ raise
+
+ t = 0
+
+ def fake_monotonic():
+ nonlocal t
+ t += 0.0001
+ return t
+
+ mocker.patch("opendbc.car.carlog.exception", fake_carlog_exception)
+ mocker.patch("time.monotonic", fake_monotonic)
+ for brand in FW_QUERY_CONFIGS.keys():
+ with subtests.test(brand=brand):
+ get_fw_versions(self.fake_can_recv, self.fake_can_send, lambda obd: None, brand)
diff --git a/opendbc/car/tests/test_lateral_limits.py b/opendbc/car/tests/test_lateral_limits.py
new file mode 100755
index 0000000000..87c7a5e256
--- /dev/null
+++ b/opendbc/car/tests/test_lateral_limits.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+from collections import defaultdict
+import importlib
+from parameterized import parameterized_class
+import pytest
+import sys
+
+from opendbc.car import DT_CTRL
+from opendbc.car.car_helpers import interfaces
+from opendbc.car.fingerprints import all_known_cars
+from opendbc.car.interfaces import get_torque_params
+
+CAR_MODELS = all_known_cars()
+
+# ISO 11270 - allowed up jerk is strictly lower than recommended limits
+MAX_LAT_ACCEL = 3.0 # m/s^2
+MAX_LAT_JERK_UP = 2.5 # m/s^3
+MAX_LAT_JERK_DOWN = 5.0 # m/s^3
+MAX_LAT_JERK_UP_TOLERANCE = 0.5 # m/s^3
+
+# jerk is measured over half a second
+JERK_MEAS_T = 0.5
+
+
+@parameterized_class('car_model', [(c,) for c in sorted(CAR_MODELS)])
+class TestLateralLimits:
+ car_model: str
+
+ @classmethod
+ def setup_class(cls):
+ CarInterface, _, _ = interfaces[cls.car_model]
+ CP = CarInterface.get_non_essential_params(cls.car_model)
+
+ if CP.dashcamOnly:
+ pytest.skip("Platform is behind dashcamOnly")
+
+ # TODO: test all platforms
+ if CP.lateralTuning.which() != 'torque':
+ pytest.skip()
+
+ if CP.notCar:
+ pytest.skip()
+
+ CarControllerParams = importlib.import_module(f'opendbc.car.{CP.carName}.values').CarControllerParams
+ cls.control_params = CarControllerParams(CP)
+ cls.torque_params = get_torque_params()[cls.car_model]
+
+ @staticmethod
+ def calculate_0_5s_jerk(control_params, torque_params):
+ steer_step = control_params.STEER_STEP
+ max_lat_accel = torque_params['MAX_LAT_ACCEL_MEASURED']
+
+ # Steer up/down delta per 10ms frame, in percentage of max torque
+ steer_up_per_frame = control_params.STEER_DELTA_UP / control_params.STEER_MAX / steer_step
+ steer_down_per_frame = control_params.STEER_DELTA_DOWN / control_params.STEER_MAX / steer_step
+
+ # Lateral acceleration reached in 0.5 seconds, clipping to max torque
+ accel_up_0_5_sec = min(steer_up_per_frame * JERK_MEAS_T / DT_CTRL, 1.0) * max_lat_accel
+ accel_down_0_5_sec = min(steer_down_per_frame * JERK_MEAS_T / DT_CTRL, 1.0) * max_lat_accel
+
+ # Convert to m/s^3
+ return accel_up_0_5_sec / JERK_MEAS_T, accel_down_0_5_sec / JERK_MEAS_T
+
+ def test_jerk_limits(self):
+ up_jerk, down_jerk = self.calculate_0_5s_jerk(self.control_params, self.torque_params)
+ assert up_jerk <= MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE
+ assert down_jerk <= MAX_LAT_JERK_DOWN
+
+ def test_max_lateral_accel(self):
+ assert self.torque_params["MAX_LAT_ACCEL_MEASURED"] <= MAX_LAT_ACCEL
+
+
+class LatAccelReport:
+ car_model_jerks: defaultdict[str, dict[str, float]] = defaultdict(dict)
+
+ def pytest_sessionfinish(self):
+ print(f"\n\n---- Lateral limit report ({len(CAR_MODELS)} cars) ----\n")
+
+ max_car_model_len = max([len(car_model) for car_model in self.car_model_jerks])
+ for car_model, _jerks in sorted(self.car_model_jerks.items(), key=lambda i: i[1]['up_jerk'], reverse=True):
+ violation = _jerks["up_jerk"] > MAX_LAT_JERK_UP + MAX_LAT_JERK_UP_TOLERANCE or \
+ _jerks["down_jerk"] > MAX_LAT_JERK_DOWN
+ violation_str = " - VIOLATION" if violation else ""
+
+ print(f"{car_model:{max_car_model_len}} - up jerk: {round(_jerks['up_jerk'], 2):5} " +
+ f"m/s^3, down jerk: {round(_jerks['down_jerk'], 2):5} m/s^3{violation_str}")
+
+ @pytest.fixture(scope="class", autouse=True)
+ def class_setup(self, request):
+ yield
+ cls = request.cls
+ if hasattr(cls, "control_params"):
+ up_jerk, down_jerk = TestLateralLimits.calculate_0_5s_jerk(cls.control_params, cls.torque_params)
+ self.car_model_jerks[cls.car_model] = {"up_jerk": up_jerk, "down_jerk": down_jerk}
+
+
+if __name__ == '__main__':
+ sys.exit(pytest.main([__file__, '-n0', '--no-summary'], plugins=[LatAccelReport()])) # noqa: TID251
diff --git a/opendbc/car/tests/test_platform_configs.py b/opendbc/car/tests/test_platform_configs.py
new file mode 100644
index 0000000000..1be10a50f9
--- /dev/null
+++ b/opendbc/car/tests/test_platform_configs.py
@@ -0,0 +1,17 @@
+from opendbc.car.values import PLATFORMS
+
+
+class TestPlatformConfigs:
+ def test_configs(self, subtests):
+
+ for name, platform in PLATFORMS.items():
+ with subtests.test(platform=str(platform)):
+ assert platform.config._frozen
+
+ if platform != "MOCK":
+ assert "pt" in platform.config.dbc_dict
+ assert len(platform.config.platform_str) > 0
+
+ assert name == platform.config.platform_str
+
+ assert platform.config.specs is not None
diff --git a/opendbc/car/tests/test_routes.py b/opendbc/car/tests/test_routes.py
new file mode 100644
index 0000000000..5c7ab12e83
--- /dev/null
+++ b/opendbc/car/tests/test_routes.py
@@ -0,0 +1,11 @@
+from parameterized import parameterized
+
+from opendbc.car.values import PLATFORMS
+from opendbc.car.tests.routes import non_tested_cars, routes
+
+
+@parameterized.expand(PLATFORMS.keys())
+def test_test_route_present(platform):
+ tested_platforms = [r.car_model for r in routes]
+ assert platform in set(tested_platforms) | set(non_tested_cars), \
+ f"Missing test route for {platform}. Add a route to opendbc/car/tests/routes.py"
diff --git a/opendbc/car/torque_data/neural_ff_weights.json b/opendbc/car/torque_data/neural_ff_weights.json
new file mode 100644
index 0000000000..251b66efb0
--- /dev/null
+++ b/opendbc/car/torque_data/neural_ff_weights.json
@@ -0,0 +1 @@
+{"CHEVROLET_BOLT_EUV": {"w_1": [[0.3452189564704895, -0.15614677965641022, -0.04062516987323761, -0.5960758328437805, 0.3211185932159424, 0.31732726097106934, -0.04430829733610153, -0.37327295541763306, -0.14118380844593048, 0.12712529301643372, 0.2641555070877075, -0.3451094627380371, -0.005127656273543835, 0.6185108423233032, 0.03725295141339302, 0.3763789236545563], [-0.0708412230014801, 0.3667356073856354, 0.031383827328681946, 0.1740853488445282, -0.04695861041545868, 0.018055908381938934, 0.009072160348296165, -0.23640218377113342, -0.10362917929887772, 0.022628149017691612, -0.224413201212883, 0.20718418061733246, -0.016947750002145767, -0.3872031271457672, -0.15500062704086304, -0.06375953555107117], [-0.0838046595454216, -0.0242826659232378, -0.07765661180019379, 0.028858814388513565, -0.09516210108995438, 0.008368706330657005, 0.1689300835132599, 0.015036891214549541, -0.15121428668498993, 0.1388195902109146, 0.11486363410949707, 0.0651545450091362, 0.13559958338737488, 0.04300367832183838, -0.13856294751167297, -0.058136988431215286], [-0.006249868310987949, 0.08809533715248108, -0.040690965950489044, 0.02359287068247795, -0.00766348373144865, 0.24816390872001648, -0.17360293865203857, -0.03676899895071983, -0.17564819753170013, 0.18998438119888306, -0.050583917647600174, -0.006488069426268339, 0.10649101436138153, -0.024557121098041534, -0.103276826441288, 0.18448011577129364]], "b_1": [0.2935388386249542, 0.10967712104320526, -0.014007942751049995, 0.211833655834198, 0.33605605363845825, 0.37722209095954895, -0.16615016758441925, 0.3134673535823822, 0.06695777177810669, 0.3425212800502777, 0.3769673705101013, 0.23186539113521576, 0.5770409107208252, -0.05929069593548775, 0.01839117519557476, 0.03828774020075798], "w_2": [[-0.06261160969734192, 0.010185074992477894, -0.06083013117313385, -0.04531499370932579, -0.08979734033346176, 0.3432150185108185, -0.019801849499344826, 0.3010321259498596], [0.19698476791381836, -0.009238275699317455, 0.08842222392559052, -0.09516377002000809, -0.05022778362035751, 0.13626104593276978, -0.052890390157699585, 0.15569131076335907], [0.0724768117070198, -0.09018408507108688, 0.06850195676088333, -0.025572121143341064, 0.0680626779794693, -0.07648195326328278, 0.07993496209383011, -0.059752143919467926], [1.267876386642456, -0.05755887180566788, -0.08429178595542908, 0.021366603672504425, -0.0006479775765910745, -1.4292563199996948, -0.08077696710824966, -1.414825439453125], [0.04535430669784546, 0.06555880606174469, -0.027145234867930412, -0.07661093026399612, -0.05702832341194153, 0.23650476336479187, 0.0024587824009358883, 0.20126521587371826], [0.006042032968252897, 0.042880818247795105, 0.002187949838116765, -0.017126334831118584, -0.08352015167474747, 0.19801731407642365, -0.029196614399552345, 0.23713473975658417], [-0.01644900068640709, -0.04358499124646187, 0.014584392309188843, 0.07155826687812805, -0.09354910999536514, -0.033351872116327286, 0.07138452678918839, -0.04755295440554619], [-1.1012420654296875, -0.03534531593322754, 0.02167935110628605, -0.01116552110761404, -0.08436500281095505, 1.1038788557052612, 0.027903547510504723, 1.0676132440567017], [0.03843916580080986, -0.0952216386795044, 0.039226632565259933, 0.002778085647150874, -0.020275786519050598, -0.07848760485649109, 0.04803166165947914, 0.015538203530013561], [0.018385495990514755, -0.025189843028783798, 0.0036680365446954966, -0.02105865254998207, 0.04808586835861206, 0.1575016975402832, 0.02703506126999855, 0.23039312660694122], [-0.0033881019335240126, -0.10210853815078735, -0.04877309128642082, 0.006989633198827505, 0.046798162162303925, 0.38676899671554565, -0.032304272055625916, 0.2345031052827835], [0.22092825174331665, -0.09642873704433441, 0.04499409720301628, 0.05108088254928589, -0.10191166400909424, 0.12818090617656708, -0.021021494641900063, 0.09440375864505768], [0.1212429478764534, -0.028194155544042587, -0.0981956496834755, 0.08226924389600754, 0.055346839129924774, 0.27067816257476807, -0.09064067900180817, 0.12580905854701996], [-1.6740131378173828, -0.02066155895590782, -0.05924689769744873, 0.06347910314798355, -0.07821853458881378, 1.2807466983795166, 0.04589352011680603, 1.310766577720642], [-0.09893272817134857, -0.04093599319458008, -0.02502273954451084, 0.09490344673395157, -0.0211324505507946, -0.09021010994911194, 0.07936318963766098, -0.03593116253614426], [-0.08490308374166489, -0.015558987855911255, -0.048692114651203156, -0.007421435788273811, -0.040531404316425323, 0.25889304280281067, 0.06012800335884094, 0.27946868538856506]], "b_2": [0.07973937690258026, -0.010446485131978989, -0.003066520905122161, -0.031895797699689865, 0.006032303906977177, 0.24106740951538086, -0.008969511836767197, 0.2872662842273712], "w_3": [[-1.364486813545227, -0.11682678014039993, 0.01764785870909691, 0.03926877677440643], [-0.05695437639951706, 0.05472218990325928, 0.1266128271818161, 0.09950875490903854], [0.11415273696184158, -0.10069356113672256, 0.0864749327301979, -0.043946366757154465], [-0.10138195008039474, -0.040128443390131, -0.08937158435583115, -0.0048376512713730335], [-0.0028251828625798225, -0.04743027314543724, 0.06340016424655914, 0.07277824729681015], [0.49482327699661255, -0.06410001963376999, -0.0999293103814125, -0.14250673353672028], [0.042802367359399796, 0.0015462725423276424, -0.05991362780332565, 0.1022040992975235], [0.3523194193840027, 0.07343732565641403, 0.04157765582203865, -0.12358107417821884]], "b_3": [0.2653026282787323, -0.058485131710767746, -0.0744510293006897, 0.012550175189971924], "w_4": [[0.5988775491714478, 0.09668736904859543], [-0.04360569268465042, 0.06491032242774963], [-0.11868984252214432, -0.09601487964391708], [-0.06554870307445526, -0.14189276099205017]], "b_4": [-0.08148707449436188, -2.8251802921295166], "input_norm_mat": [[-3.0, 3.0], [-3.0, 3.0], [0.0, 40.0], [-3.0, 3.0]], "output_norm_mat": [-1.0, 1.0], "temperature": 100.0}}
\ No newline at end of file
diff --git a/opendbc/car/torque_data/override.toml b/opendbc/car/torque_data/override.toml
new file mode 100644
index 0000000000..f4142ba1df
--- /dev/null
+++ b/opendbc/car/torque_data/override.toml
@@ -0,0 +1,73 @@
+legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
+### angle control
+# Nissan appears to have torque
+"NISSAN_XTRAIL" = [nan, 1.5, nan]
+"NISSAN_ALTIMA" = [nan, 1.5, nan]
+"NISSAN_LEAF_IC" = [nan, 1.5, nan]
+"NISSAN_LEAF" = [nan, 1.5, nan]
+"NISSAN_ROGUE" = [nan, 1.5, nan]
+
+# New subarus angle based controllers
+"SUBARU_FORESTER_2022" = [nan, 3.0, nan]
+"SUBARU_OUTBACK_2023" = [nan, 3.0, nan]
+"SUBARU_ASCENT_2023" = [nan, 3.0, nan]
+
+# Toyota LTA also has torque
+"TOYOTA_RAV4_TSS2_2023" = [nan, 3.0, nan]
+
+# Guess
+"FORD_BRONCO_SPORT_MK1" = [nan, 1.5, nan]
+"FORD_ESCAPE_MK4" = [nan, 1.5, nan]
+"FORD_EXPLORER_MK6" = [nan, 1.5, nan]
+"FORD_F_150_MK14" = [nan, 1.5, nan]
+"FORD_FOCUS_MK4" = [nan, 1.5, nan]
+"FORD_MAVERICK_MK1" = [nan, 1.5, nan]
+"FORD_F_150_LIGHTNING_MK1" = [nan, 1.5, nan]
+"FORD_MUSTANG_MACH_E_MK1" = [nan, 1.5, nan]
+"FORD_RANGER_MK2" = [nan, 1.5, nan]
+###
+
+# No steering wheel
+"COMMA_BODY" = [nan, 1000, nan]
+
+# Totally new cars
+"RAM_1500_5TH_GEN" = [2.0, 2.0, 0.05]
+"RAM_HD_5TH_GEN" = [1.4, 1.4, 0.05]
+"SUBARU_OUTBACK" = [2.0, 1.5, 0.2]
+"CADILLAC_ESCALADE" = [1.899999976158142, 1.842270016670227, 0.1120000034570694]
+"CADILLAC_ESCALADE_ESV_2019" = [1.15, 1.3, 0.2]
+"CADILLAC_XT4" = [1.45, 1.6, 0.2]
+"CHEVROLET_BOLT_EUV" = [2.0, 2.0, 0.05]
+"CHEVROLET_SILVERADO" = [1.9, 1.9, 0.112]
+"CHEVROLET_TRAILBLAZER" = [1.33, 1.9, 0.16]
+"CHEVROLET_EQUINOX" = [2.5, 2.5, 0.05]
+"VOLKSWAGEN_CADDY_MK3" = [1.2, 1.2, 0.1]
+"VOLKSWAGEN_PASSAT_NMS" = [2.5, 2.5, 0.1]
+"VOLKSWAGEN_SHARAN_MK2" = [2.5, 2.5, 0.1]
+"HYUNDAI_SANTA_CRUZ_1ST_GEN" = [2.7, 2.7, 0.1]
+"KIA_SPORTAGE_5TH_GEN" = [2.6, 2.6, 0.1]
+"GENESIS_GV70_1ST_GEN" = [2.42, 2.42, 0.1]
+"GENESIS_GV60_EV_1ST_GEN" = [2.5, 2.5, 0.1]
+"KIA_SORENTO_4TH_GEN" = [2.5, 2.5, 0.1]
+"KIA_SORENTO_HEV_4TH_GEN" = [2.5, 2.5, 0.1]
+"KIA_NIRO_HEV_2ND_GEN" = [2.42, 2.5, 0.12]
+"KIA_NIRO_EV_2ND_GEN" = [2.05, 2.5, 0.14]
+"GENESIS_GV80" = [2.5, 2.5, 0.1]
+"KIA_CARNIVAL_4TH_GEN" = [1.75, 1.75, 0.15]
+"GMC_ACADIA" = [1.6, 1.6, 0.2]
+"LEXUS_IS_TSS2" = [2.0, 2.0, 0.1]
+"HYUNDAI_KONA_EV_2ND_GEN" = [2.5, 2.5, 0.1]
+"HYUNDAI_IONIQ_6" = [2.5, 2.5, 0.005]
+"HYUNDAI_AZERA_6TH_GEN" = [1.8, 1.8, 0.1]
+"HYUNDAI_AZERA_HEV_6TH_GEN" = [1.8, 1.8, 0.1]
+"KIA_K8_HEV_1ST_GEN" = [2.5, 2.5, 0.1]
+"HYUNDAI_CUSTIN_1ST_GEN" = [2.5, 2.5, 0.1]
+"LEXUS_GS_F" = [2.5, 2.5, 0.08]
+"HYUNDAI_STARIA_4TH_GEN" = [1.8, 2.0, 0.15]
+
+# Dashcam or fallback configured as ideal car
+"MOCK" = [10.0, 10, 0.0]
+
+# Manually checked
+"HONDA_CIVIC_2022" = [2.5, 1.2, 0.15]
+"HONDA_HRV_3G" = [2.5, 1.2, 0.2]
diff --git a/opendbc/car/torque_data/params.toml b/opendbc/car/torque_data/params.toml
new file mode 100644
index 0000000000..4bb8d45c53
--- /dev/null
+++ b/opendbc/car/torque_data/params.toml
@@ -0,0 +1,81 @@
+legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
+"ACURA_ILX" = [1.524988973896102, 0.519011053086259, 0.34236219253028]
+"ACURA_RDX" = [0.9987728568686902, 0.5323765166196301, 0.303218805715844]
+"ACURA_RDX_3G" = [1.4314459806646749, 0.33874701282109954, 0.18048847083897598]
+"AUDI_A3_MK3" = [1.5122414863077502, 1.7443517531719404, 0.15194151892450905]
+"AUDI_Q3_MK2" = [1.4439223359448605, 1.2254955789112076, 0.1413798895978097]
+"CHEVROLET_VOLT" = [1.5961527626411784, 1.8422651988094612, 0.1572393918005158]
+"CHRYSLER_PACIFICA_2018" = [2.07140, 1.3366521181047952, 0.13776367250652022]
+"CHRYSLER_PACIFICA_2020" = [1.86206, 1.509076559398423, 0.14328246159386085]
+"CHRYSLER_PACIFICA_2018_HYBRID" = [2.08887, 1.2943025830995154, 0.114818]
+"CHRYSLER_PACIFICA_2019_HYBRID" = [1.90120, 1.1958788168371808, 0.131520]
+"GENESIS_G70" = [3.8520195946707947, 2.354697063349854, 0.06830285485626221]
+"HONDA_ACCORD" = [1.6893333799149202, 0.3246749081720698, 0.2120497022936265]
+"HONDA_CIVIC_BOSCH" = [1.691708637466905, 0.40132900729454185, 0.25460295304024094]
+"HONDA_CIVIC" = [1.6528895627785531, 0.4018518740819229, 0.25458812851328544]
+"HONDA_CRV" = [0.7667141440182675, 0.5927571534745969, 0.40909087636157127]
+"HONDA_CRV_5G" = [2.01323205142022, 0.2700612209345081, 0.2238412881331528]
+"HONDA_CRV_HYBRID" = [2.072034634644233, 0.7152085160516978, 0.20237105008376083]
+"HONDA_FIT" = [1.5719981427109775, 0.5712761407108976, 0.110773383324281]
+"HONDA_HRV" = [2.0661212805710205, 0.7521343418694775, 0.17760375789242094]
+"HONDA_INSIGHT" = [1.5201671214069354, 0.5660229120683284, 0.25808042580281876]
+"HONDA_ODYSSEY" = [1.8774809275211801, 0.8394431662987996, 0.2096978613792822]
+"HONDA_PILOT" = [1.7262026201812795, 0.9470005614967523, 0.21351430733218763]
+"HONDA_RIDGELINE" = [1.4146525028237624, 0.7356572861629564, 0.23307177552211328]
+"HYUNDAI_ELANTRA_2021" = [3.169, 2.1259108157250735, 0.0819]
+"HYUNDAI_GENESIS" = [2.7807965280270794, 2.325, 0.0984484465421171]
+"HYUNDAI_IONIQ_5" = [3.172929, 2.713050, 0.096019]
+"HYUNDAI_IONIQ_EV_LTD" = [1.7662975472852054, 1.613755614526594, 0.17087579756306276]
+"HYUNDAI_IONIQ_PHEV" = [3.2928700076638537, 2.1193482926455656, 0.12463700961468778]
+"HYUNDAI_IONIQ_PHEV_2019" = [2.970807902012267, 1.6312321830002083, 0.1088964990357482]
+"HYUNDAI_KONA_EV" = [3.078814714619148, 2.307336938253934, 0.12359762054065548]
+"HYUNDAI_PALISADE" = [2.544642494803999, 1.8721703683337008, 0.1301424599248651]
+"HYUNDAI_SANTA_FE" = [3.0787027729757632, 2.6173437483495565, 0.1207019341823945]
+"HYUNDAI_SANTA_FE_HEV_2022" = [3.501877602644835, 2.729064118456137, 0.10384068104538963]
+"HYUNDAI_SANTA_FE_PHEV_2022" = [1.6953050513611045, 1.5837614296206861, 0.12672855941458458]
+"HYUNDAI_SONATA_LF" = [2.2200457811703953, 1.2967330275895228, 0.14039920986586393]
+"HYUNDAI_SONATA" = [2.9638737459977467, 2.1259108157250735, 0.07813665616927593]
+"HYUNDAI_SONATA_HYBRID" = [2.8990264092395734, 2.061410192222139, 0.0899805488717382]
+"HYUNDAI_TUCSON_4TH_GEN" = [2.960174, 2.860284, 0.108745]
+"JEEP_GRAND_CHEROKEE_2019" = [2.30972, 1.289689569171081, 0.117048]
+"JEEP_GRAND_CHEROKEE" = [2.27116, 1.4057367824262523, 0.11725947414922003]
+"KIA_EV6" = [3.2, 2.093457, 0.005]
+"KIA_K5_2021" = [2.405339728085138, 1.460032270828705, 0.11650989850813716]
+"KIA_NIRO_EV" = [2.9215954981365337, 2.1500583840260044, 0.09236802474810267]
+"KIA_SORENTO" = [2.464854685101844, 1.5335274218367956, 0.12056170567599558]
+"KIA_STINGER" = [2.7499043387418967, 1.849652021986449, 0.12048334239559202]
+"LEXUS_ES_TSS2" = [2.0357564999999997, 1.999082295195227, 0.101533]
+"LEXUS_NX" = [2.3525924753753613, 1.9731412277641067, 0.15168101064205927]
+"LEXUS_NX_TSS2" = [2.4331999786982936, 2.1045680431705414, 0.14099899317761067]
+"LEXUS_RX" = [1.6430539050086406, 1.181960058934143, 0.19768806040843034]
+"LEXUS_RX_TSS2" = [1.5375561442049257, 1.343166476215164, 0.1931062001527557]
+"MAZDA_CX9_2021" = [1.7601682915983443, 1.0889677335154337, 0.17713792194297195]
+"SKODA_SUPERB_MK3" = [1.166437404652981, 1.1686163012668165, 0.12194533036948708]
+"SUBARU_FORESTER" = [3.6617001649776793, 2.342197172531713, 0.11075960785398745]
+"SUBARU_IMPREZA" = [1.0670704910352047, 0.8234374840709592, 0.20986563268614938]
+"SUBARU_IMPREZA_2020" = [2.6068223389108303, 2.134872342760203, 0.15261513193561627]
+"TOYOTA_AVALON" = [2.5185770183845646, 1.7153346784214922, 0.10603968787111022]
+"TOYOTA_AVALON_2019" = [1.7036141952825095, 1.239619084240008, 0.08459830394899492]
+"TOYOTA_AVALON_TSS2" = [2.3154403649717357, 2.7777922854327124, 0.11453999639164605]
+"TOYOTA_CHR" = [1.5591084333664578, 1.271271459066948, 0.20259087058453193]
+"TOYOTA_CHR_TSS2" = [1.7678810166088303, 1.3742176337919942, 0.2319674583741509]
+"TOYOTA_CAMRY" = [2.0568162685952505, 1.7576185169559122, 0.108878753]
+"TOYOTA_CAMRY_TSS2" = [2.3548324999999997, 2.368900128946771, 0.118436]
+"TOYOTA_COROLLA" = [3.117154369115421, 1.8438132575043773, 0.12289685869250652]
+"TOYOTA_COROLLA_TSS2" = [1.991132339206426, 1.868866242720403, 0.19570063298031432]
+"TOYOTA_HIGHLANDER" = [1.8108348718624456, 1.6348421600679828, 0.15972686105120398]
+"TOYOTA_HIGHLANDER_TSS2" = [1.9617570834136164, 1.8611643317268927, 0.14519673256119725]
+"TOYOTA_MIRAI" = [2.506899832157829, 1.7417213930750164, 0.20182618449440565]
+"TOYOTA_PRIUS" = [1.60, 1.5023147650693636, 0.151515]
+"TOYOTA_PRIUS_TSS2" = [1.972600, 1.9104337425537743, 0.170968]
+"TOYOTA_RAV4" = [2.085695074355425, 2.2142832316984733, 0.13339165270103975]
+"TOYOTA_RAV4_TSS2" = [2.279239424615458, 2.087101966779332, 0.13682208413446817]
+"TOYOTA_RAV4H" = [1.9796257271652042, 1.7503987331707576, 0.14628860048885406]
+"TOYOTA_RAV4_TSS2_2022" = [2.241883248393209, 1.9304407208090029, 0.112174]
+"TOYOTA_SIENNA" = [1.689726, 1.3208264576110418, 0.140456]
+"VOLKSWAGEN_ARTEON_MK1" = [1.45136518053819, 1.3639364049316804, 0.23806361745695032]
+"VOLKSWAGEN_ATLAS_MK1" = [1.4677006726964945, 1.6733266634075656, 0.12959584092073367]
+"VOLKSWAGEN_GOLF_MK7" = [1.3750394140491293, 1.5814743077200641, 0.2018321939386586]
+"VOLKSWAGEN_JETTA_MK7" = [1.2271623034089392, 1.216955117387, 0.19437384688370712]
+"VOLKSWAGEN_PASSAT_MK8" = [1.3432120736752917, 1.7087275587362314, 0.19444383787326647]
+"VOLKSWAGEN_TIGUAN_MK2" = [0.9711965500094828, 1.0001565939459098, 0.1465626137072916]
diff --git a/opendbc/car/torque_data/substitute.toml b/opendbc/car/torque_data/substitute.toml
new file mode 100644
index 0000000000..8724a08010
--- /dev/null
+++ b/opendbc/car/torque_data/substitute.toml
@@ -0,0 +1,83 @@
+legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
+"MAZDA_3" = "MAZDA_CX9_2021"
+"MAZDA_6" = "MAZDA_CX9_2021"
+"MAZDA_CX5" = "MAZDA_CX9_2021"
+"MAZDA_CX5_2022" = "MAZDA_CX9_2021"
+"MAZDA_CX9" = "MAZDA_CX9_2021"
+
+"DODGE_DURANGO" = "CHRYSLER_PACIFICA_2020"
+
+"TOYOTA_ALPHARD_TSS2" = "TOYOTA_SIENNA"
+"TOYOTA_PRIUS_V" = "TOYOTA_PRIUS"
+"LEXUS_IS" = "LEXUS_NX"
+"LEXUS_CTH" = "LEXUS_NX"
+"LEXUS_ES" = "TOYOTA_CAMRY"
+"LEXUS_RC" = "LEXUS_NX_TSS2"
+"LEXUS_LC_TSS2" = "LEXUS_NX_TSS2"
+
+"KIA_OPTIMA_G4" = "HYUNDAI_SONATA"
+"KIA_OPTIMA_G4_FL" = "HYUNDAI_SONATA"
+"KIA_OPTIMA_H" = "HYUNDAI_SONATA"
+"KIA_OPTIMA_H_G4_FL" = "HYUNDAI_SONATA"
+"KIA_FORTE" = "HYUNDAI_SONATA"
+"KIA_CEED" = "HYUNDAI_SONATA"
+"KIA_SELTOS" = "HYUNDAI_SONATA"
+"KIA_NIRO_PHEV" = "KIA_NIRO_EV"
+"KIA_NIRO_PHEV_2022" = "KIA_NIRO_EV"
+"KIA_NIRO_HEV_2021" = "KIA_NIRO_EV"
+"HYUNDAI_VELOSTER" = "HYUNDAI_SONATA_LF"
+"HYUNDAI_KONA" = "HYUNDAI_KONA_EV"
+"HYUNDAI_KONA_HEV" = "HYUNDAI_KONA_EV"
+"HYUNDAI_KONA_EV_2022" = "HYUNDAI_KONA_EV"
+"HYUNDAI_IONIQ" = "HYUNDAI_IONIQ_PHEV_2019"
+"HYUNDAI_IONIQ_HEV_2022" = "HYUNDAI_IONIQ_PHEV_2019"
+"HYUNDAI_IONIQ_EV_2020" = "HYUNDAI_IONIQ_PHEV_2019"
+"HYUNDAI_ELANTRA" = "HYUNDAI_SONATA_LF"
+"HYUNDAI_ELANTRA_GT_I30" = "HYUNDAI_SONATA_LF"
+"HYUNDAI_ELANTRA_HEV_2021" = "HYUNDAI_SONATA"
+"HYUNDAI_TUCSON" = "HYUNDAI_SANTA_FE"
+"HYUNDAI_SANTA_FE_2022" = "HYUNDAI_SANTA_FE_HEV_2022"
+"KIA_K5_HEV_2020" = "KIA_K5_2021"
+"KIA_STINGER_2022" = "KIA_STINGER"
+"GENESIS_G90" = "GENESIS_G70"
+"GENESIS_G80" = "GENESIS_G70"
+"GENESIS_G70_2020" = "HYUNDAI_SONATA"
+
+"HONDA_FREED" = "HONDA_ODYSSEY"
+"HONDA_CRV_EU" = "HONDA_CRV"
+"HONDA_CIVIC_BOSCH_DIESEL" = "HONDA_CIVIC_BOSCH"
+"HONDA_E" = "HONDA_CIVIC_BOSCH"
+"HONDA_ODYSSEY_CHN" = "HONDA_ODYSSEY"
+
+"BUICK_LACROSSE" = "CHEVROLET_VOLT"
+"BUICK_REGAL" = "CHEVROLET_VOLT"
+"CADILLAC_ESCALADE_ESV" = "CHEVROLET_VOLT"
+"CADILLAC_ATS" = "CHEVROLET_VOLT"
+"CHEVROLET_MALIBU" = "CHEVROLET_VOLT"
+"HOLDEN_ASTRA" = "CHEVROLET_VOLT"
+
+"SKODA_FABIA_MK4" = "VOLKSWAGEN_GOLF_MK7"
+"SKODA_OCTAVIA_MK3" = "SKODA_SUPERB_MK3"
+"SKODA_KODIAQ_MK1" = "SKODA_SUPERB_MK3"
+"SKODA_KAROQ_MK1" = "SKODA_SUPERB_MK3"
+"SKODA_KAMIQ_MK1" = "SKODA_SUPERB_MK3"
+"VOLKSWAGEN_CRAFTER_MK2" = "VOLKSWAGEN_TIGUAN_MK2"
+"VOLKSWAGEN_TROC_MK1" = "VOLKSWAGEN_TIGUAN_MK2"
+"VOLKSWAGEN_TCROSS_MK1" = "VOLKSWAGEN_TIGUAN_MK2"
+"VOLKSWAGEN_TOURAN_MK2" = "VOLKSWAGEN_TIGUAN_MK2"
+"VOLKSWAGEN_TRANSPORTER_T61" = "VOLKSWAGEN_TIGUAN_MK2"
+"AUDI_Q2_MK1" = "VOLKSWAGEN_TIGUAN_MK2"
+"VOLKSWAGEN_TAOS_MK1" = "VOLKSWAGEN_TIGUAN_MK2"
+"VOLKSWAGEN_POLO_MK6" = "VOLKSWAGEN_GOLF_MK7"
+"SEAT_ATECA_MK1" = "VOLKSWAGEN_GOLF_MK7"
+
+"SUBARU_CROSSTREK_HYBRID" = "SUBARU_IMPREZA_2020"
+"SUBARU_FORESTER_HYBRID" = "SUBARU_IMPREZA_2020"
+"SUBARU_LEGACY" = "SUBARU_OUTBACK"
+
+# Old subarus don't have much data guessing it's like low torque impreza"
+"SUBARU_OUTBACK_PREGLOBAL_2018" = "SUBARU_IMPREZA"
+"SUBARU_OUTBACK_PREGLOBAL" = "SUBARU_IMPREZA"
+"SUBARU_FORESTER_PREGLOBAL" = "SUBARU_IMPREZA"
+"SUBARU_LEGACY_PREGLOBAL" = "SUBARU_IMPREZA"
+"SUBARU_ASCENT" = "SUBARU_FORESTER"
diff --git a/opendbc/car/toyota/__init__.py b/opendbc/car/toyota/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/toyota/carcontroller.py b/opendbc/car/toyota/carcontroller.py
new file mode 100644
index 0000000000..bb6dc4cfc4
--- /dev/null
+++ b/opendbc/car/toyota/carcontroller.py
@@ -0,0 +1,179 @@
+import copy
+from opendbc.car import apply_meas_steer_torque_limits, apply_std_steer_angle_limits, common_fault_avoidance, make_tester_present_msg, structs
+from opendbc.car.can_definitions import CanData
+from opendbc.car.common.numpy_fast import clip
+from opendbc.car.interfaces import CarControllerBase
+from opendbc.car.toyota import toyotacan
+from opendbc.car.toyota.values import CAR, STATIC_DSU_MSGS, NO_STOP_TIMER_CAR, TSS2_CAR, \
+ CarControllerParams, ToyotaFlags, \
+ UNSUPPORTED_DSU_CAR
+from opendbc.can.packer import CANPacker
+
+SteerControlType = structs.CarParams.SteerControlType
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+
+# LKA limits
+# EPS faults if you apply torque while the steering rate is above 100 deg/s for too long
+MAX_STEER_RATE = 100 # deg/s
+MAX_STEER_RATE_FRAMES = 18 # tx control frames needed before torque can be cut
+
+# EPS allows user torque above threshold for 50 frames before permanently faulting
+MAX_USER_TORQUE = 500
+
+# LTA limits
+# EPS ignores commands above this angle and causes PCS to fault
+MAX_LTA_ANGLE = 94.9461 # deg
+MAX_LTA_DRIVER_TORQUE_ALLOWANCE = 150 # slightly above steering pressed allows some resistance when changing lanes
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.params = CarControllerParams(self.CP)
+ self.last_steer = 0
+ self.last_angle = 0
+ self.alert_active = False
+ self.last_standstill = False
+ self.standstill_req = False
+ self.steer_rate_counter = 0
+ self.distance_button = 0
+
+ self.packer = CANPacker(dbc_name)
+ self.gas = 0
+ self.accel = 0
+
+ def update(self, CC, CS, now_nanos):
+ actuators = CC.actuators
+ hud_control = CC.hudControl
+ pcm_cancel_cmd = CC.cruiseControl.cancel
+ lat_active = CC.latActive and abs(CS.out.steeringTorque) < MAX_USER_TORQUE
+
+ # *** control msgs ***
+ can_sends = []
+
+ # *** steer torque ***
+ new_steer = int(round(actuators.steer * self.params.STEER_MAX))
+ apply_steer = apply_meas_steer_torque_limits(new_steer, self.last_steer, CS.out.steeringTorqueEps, self.params)
+
+ # >100 degree/sec steering fault prevention
+ self.steer_rate_counter, apply_steer_req = common_fault_avoidance(abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, lat_active,
+ self.steer_rate_counter, MAX_STEER_RATE_FRAMES)
+
+ if not lat_active:
+ apply_steer = 0
+
+ # *** steer angle ***
+ if self.CP.steerControlType == SteerControlType.angle:
+ # If using LTA control, disable LKA and set steering angle command
+ apply_steer = 0
+ apply_steer_req = False
+ if self.frame % 2 == 0:
+ # EPS uses the torque sensor angle to control with, offset to compensate
+ apply_angle = actuators.steeringAngleDeg + CS.out.steeringAngleOffsetDeg
+
+ # Angular rate limit based on speed
+ apply_angle = apply_std_steer_angle_limits(apply_angle, self.last_angle, CS.out.vEgoRaw, self.params)
+
+ if not lat_active:
+ apply_angle = CS.out.steeringAngleDeg + CS.out.steeringAngleOffsetDeg
+
+ self.last_angle = clip(apply_angle, -MAX_LTA_ANGLE, MAX_LTA_ANGLE)
+
+ self.last_steer = apply_steer
+
+ # toyota can trace shows STEERING_LKA at 42Hz, with counter adding alternatively 1 and 2;
+ # sending it at 100Hz seem to allow a higher rate limit, as the rate limit seems imposed
+ # on consecutive messages
+ can_sends.append(toyotacan.create_steer_command(self.packer, apply_steer, apply_steer_req))
+
+ # STEERING_LTA does not seem to allow more rate by sending faster, and may wind up easier
+ if self.frame % 2 == 0 and self.CP.carFingerprint in TSS2_CAR:
+ lta_active = lat_active and self.CP.steerControlType == SteerControlType.angle
+ # cut steering torque with TORQUE_WIND_DOWN when either EPS torque or driver torque is above
+ # the threshold, to limit max lateral acceleration and for driver torque blending respectively.
+ full_torque_condition = (abs(CS.out.steeringTorqueEps) < self.params.STEER_MAX and
+ abs(CS.out.steeringTorque) < MAX_LTA_DRIVER_TORQUE_ALLOWANCE)
+
+ # TORQUE_WIND_DOWN at 0 ramps down torque at roughly the max down rate of 1500 units/sec
+ torque_wind_down = 100 if lta_active and full_torque_condition else 0
+ can_sends.append(toyotacan.create_lta_steer_command(self.packer, self.CP.steerControlType, self.last_angle,
+ lta_active, self.frame // 2, torque_wind_down))
+
+ # *** gas and brake ***
+ pcm_accel_cmd = clip(actuators.accel, self.params.ACCEL_MIN, self.params.ACCEL_MAX)
+
+ # on entering standstill, send standstill request
+ if CS.out.standstill and not self.last_standstill and (self.CP.carFingerprint not in NO_STOP_TIMER_CAR):
+ self.standstill_req = True
+ if CS.pcm_acc_status != 8:
+ # pcm entered standstill or it's disabled
+ self.standstill_req = False
+
+ self.last_standstill = CS.out.standstill
+
+ # handle UI messages
+ fcw_alert = hud_control.visualAlert == VisualAlert.fcw
+ steer_alert = hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw)
+
+ # we can spam can to cancel the system even if we are using lat only control
+ if (self.frame % 3 == 0 and self.CP.openpilotLongitudinalControl) or pcm_cancel_cmd:
+ lead = hud_control.leadVisible or CS.out.vEgo < 12. # at low speed we always assume the lead is present so ACC can be engaged
+
+ # Press distance button until we are at the correct bar length. Only change while enabled to avoid skipping startup popup
+ if self.frame % 6 == 0 and self.CP.openpilotLongitudinalControl:
+ desired_distance = 4 - hud_control.leadDistanceBars
+ if CS.out.cruiseState.enabled and CS.pcm_follow_distance != desired_distance:
+ self.distance_button = not self.distance_button
+ else:
+ self.distance_button = 0
+
+ # Lexus IS uses a different cancellation message
+ if pcm_cancel_cmd and self.CP.carFingerprint in UNSUPPORTED_DSU_CAR:
+ can_sends.append(toyotacan.create_acc_cancel_command(self.packer))
+ elif self.CP.openpilotLongitudinalControl:
+ can_sends.append(toyotacan.create_accel_command(self.packer, pcm_accel_cmd, pcm_cancel_cmd, self.standstill_req, lead, CS.acc_type, fcw_alert,
+ self.distance_button))
+ self.accel = pcm_accel_cmd
+ else:
+ can_sends.append(toyotacan.create_accel_command(self.packer, 0, pcm_cancel_cmd, False, lead, CS.acc_type, False, self.distance_button))
+
+ # *** hud ui ***
+ if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
+ # ui mesg is at 1Hz but we send asap if:
+ # - there is something to display
+ # - there is something to stop displaying
+ send_ui = False
+ if ((fcw_alert or steer_alert) and not self.alert_active) or \
+ (not (fcw_alert or steer_alert) and self.alert_active):
+ send_ui = True
+ self.alert_active = not self.alert_active
+ elif pcm_cancel_cmd:
+ # forcing the pcm to disengage causes a bad fault sound so play a good sound instead
+ send_ui = True
+
+ if self.frame % 20 == 0 or send_ui:
+ can_sends.append(toyotacan.create_ui_command(self.packer, steer_alert, pcm_cancel_cmd, hud_control.leftLaneVisible,
+ hud_control.rightLaneVisible, hud_control.leftLaneDepart,
+ hud_control.rightLaneDepart, CC.enabled, CS.lkas_hud))
+
+ if (self.frame % 100 == 0 or send_ui) and (self.CP.enableDsu or self.CP.flags & ToyotaFlags.DISABLE_RADAR.value):
+ can_sends.append(toyotacan.create_fcw_command(self.packer, fcw_alert))
+
+ # *** static msgs ***
+ for addr, cars, bus, fr_step, vl in STATIC_DSU_MSGS:
+ if self.frame % fr_step == 0 and self.CP.enableDsu and self.CP.carFingerprint in cars:
+ can_sends.append(CanData(addr, vl, bus))
+
+ # keep radar disabled
+ if self.frame % 20 == 0 and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
+ can_sends.append(make_tester_present_msg(0x750, 0, 0xF))
+
+ new_actuators = copy.copy(actuators)
+ new_actuators.steer = apply_steer / self.params.STEER_MAX
+ new_actuators.steerOutputCan = apply_steer
+ new_actuators.steeringAngleDeg = self.last_angle
+ new_actuators.accel = self.accel
+ new_actuators.gas = self.gas
+
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/toyota/carstate.py b/opendbc/car/toyota/carstate.py
new file mode 100644
index 0000000000..b6d03f9291
--- /dev/null
+++ b/opendbc/car/toyota/carstate.py
@@ -0,0 +1,240 @@
+import copy
+
+from opendbc.can.can_define import CANDefine
+from opendbc.can.parser import CANParser
+from opendbc.car import DT_CTRL, create_button_events, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.common.filter_simple import FirstOrderFilter
+from opendbc.car.common.numpy_fast import mean
+from opendbc.car.interfaces import CarStateBase
+from opendbc.car.toyota.values import ToyotaFlags, CAR, DBC, STEER_THRESHOLD, NO_STOP_TIMER_CAR, \
+ TSS2_CAR, RADAR_ACC_CAR, EPS_SCALE, UNSUPPORTED_DSU_CAR
+
+ButtonType = structs.CarState.ButtonEvent.Type
+SteerControlType = structs.CarParams.SteerControlType
+
+# These steering fault definitions seem to be common across LKA (torque) and LTA (angle):
+# - high steer rate fault: goes to 21 or 25 for 1 frame, then 9 for 2 seconds
+# - lka/lta msg drop out: goes to 9 then 11 for a combined total of 2 seconds, then 3.
+# if using the other control command, goes directly to 3 after 1.5 seconds
+# - initializing: LTA can report 0 as long as STEER_TORQUE_SENSOR->STEER_ANGLE_INITIALIZING is 1,
+# and is a catch-all for LKA
+TEMP_STEER_FAULTS = (0, 9, 11, 21, 25)
+# - lka/lta msg drop out: 3 (recoverable)
+# - prolonged high driver torque: 17 (permanent)
+PERM_STEER_FAULTS = (3, 17)
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+ self.shifter_values = can_define.dv["GEAR_PACKET"]["GEAR"]
+ self.eps_torque_scale = EPS_SCALE[CP.carFingerprint] / 100.
+ self.cluster_speed_hyst_gap = CV.KPH_TO_MS / 2.
+ self.cluster_min_speed = CV.KPH_TO_MS / 2.
+
+ # On cars with cp.vl["STEER_TORQUE_SENSOR"]["STEER_ANGLE"]
+ # the signal is zeroed to where the steering angle is at start.
+ # Need to apply an offset as soon as the steering angle measurements are both received
+ self.accurate_steer_angle_seen = False
+ self.angle_offset = FirstOrderFilter(None, 60.0, DT_CTRL, initialized=False)
+
+ self.distance_button = 0
+
+ self.pcm_follow_distance = 0
+
+ self.low_speed_lockout = False
+ self.acc_type = 1
+ self.lkas_hud = {}
+
+ def update(self, cp, cp_cam, *_) -> structs.CarState:
+ ret = structs.CarState()
+
+ ret.doorOpen = any([cp.vl["BODY_CONTROL_STATE"]["DOOR_OPEN_FL"], cp.vl["BODY_CONTROL_STATE"]["DOOR_OPEN_FR"],
+ cp.vl["BODY_CONTROL_STATE"]["DOOR_OPEN_RL"], cp.vl["BODY_CONTROL_STATE"]["DOOR_OPEN_RR"]])
+ ret.seatbeltUnlatched = cp.vl["BODY_CONTROL_STATE"]["SEATBELT_DRIVER_UNLATCHED"] != 0
+ ret.parkingBrake = cp.vl["BODY_CONTROL_STATE"]["PARKING_BRAKE"] == 1
+
+ ret.brakePressed = cp.vl["BRAKE_MODULE"]["BRAKE_PRESSED"] != 0
+ ret.brakeHoldActive = cp.vl["ESP_CONTROL"]["BRAKE_HOLD_ACTIVE"] == 1
+
+ ret.gasPressed = cp.vl["PCM_CRUISE"]["GAS_RELEASED"] == 0
+
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_FL"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_FR"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_RL"],
+ cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_RR"],
+ )
+ ret.vEgoRaw = mean([ret.wheelSpeeds.fl, ret.wheelSpeeds.fr, ret.wheelSpeeds.rl, ret.wheelSpeeds.rr])
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.vEgoCluster = ret.vEgo * 1.015 # minimum of all the cars
+
+ ret.standstill = abs(ret.vEgoRaw) < 1e-3
+
+ ret.steeringAngleDeg = cp.vl["STEER_ANGLE_SENSOR"]["STEER_ANGLE"] + cp.vl["STEER_ANGLE_SENSOR"]["STEER_FRACTION"]
+ ret.steeringRateDeg = cp.vl["STEER_ANGLE_SENSOR"]["STEER_RATE"]
+ torque_sensor_angle_deg = cp.vl["STEER_TORQUE_SENSOR"]["STEER_ANGLE"]
+
+ # On some cars, the angle measurement is non-zero while initializing
+ if abs(torque_sensor_angle_deg) > 1e-3 and not bool(cp.vl["STEER_TORQUE_SENSOR"]["STEER_ANGLE_INITIALIZING"]):
+ self.accurate_steer_angle_seen = True
+
+ if self.accurate_steer_angle_seen:
+ # Offset seems to be invalid for large steering angles and high angle rates
+ if abs(ret.steeringAngleDeg) < 90 and abs(ret.steeringRateDeg) < 100 and cp.can_valid:
+ self.angle_offset.update(torque_sensor_angle_deg - ret.steeringAngleDeg)
+
+ if self.angle_offset.initialized:
+ ret.steeringAngleOffsetDeg = self.angle_offset.x
+ ret.steeringAngleDeg = torque_sensor_angle_deg - self.angle_offset.x
+
+ can_gear = int(cp.vl["GEAR_PACKET"]["GEAR"])
+ ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None))
+ ret.leftBlinker = cp.vl["BLINKERS_STATE"]["TURN_SIGNALS"] == 1
+ ret.rightBlinker = cp.vl["BLINKERS_STATE"]["TURN_SIGNALS"] == 2
+
+ if self.CP.carFingerprint != CAR.TOYOTA_MIRAI:
+ ret.engineRpm = cp.vl["ENGINE_RPM"]["RPM"]
+
+ ret.steeringTorque = cp.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_DRIVER"]
+ ret.steeringTorqueEps = cp.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_EPS"] * self.eps_torque_scale
+ # we could use the override bit from dbc, but it's triggered at too high torque values
+ ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD
+
+ # Check EPS LKA/LTA fault status
+ ret.steerFaultTemporary = cp.vl["EPS_STATUS"]["LKA_STATE"] in TEMP_STEER_FAULTS
+ ret.steerFaultPermanent = cp.vl["EPS_STATUS"]["LKA_STATE"] in PERM_STEER_FAULTS
+
+ if self.CP.steerControlType == SteerControlType.angle:
+ ret.steerFaultTemporary = ret.steerFaultTemporary or cp.vl["EPS_STATUS"]["LTA_STATE"] in TEMP_STEER_FAULTS
+ ret.steerFaultPermanent = ret.steerFaultPermanent or cp.vl["EPS_STATUS"]["LTA_STATE"] in PERM_STEER_FAULTS
+
+ # Lane Tracing Assist control is unavailable (EPS_STATUS->LTA_STATE=0) until
+ # the more accurate angle sensor signal is initialized
+ ret.vehicleSensorsInvalid = not self.accurate_steer_angle_seen
+
+ if self.CP.carFingerprint in UNSUPPORTED_DSU_CAR:
+ # TODO: find the bit likely in DSU_CRUISE that describes an ACC fault. one may also exist in CLUTCH
+ ret.cruiseState.available = cp.vl["DSU_CRUISE"]["MAIN_ON"] != 0
+ ret.cruiseState.speed = cp.vl["DSU_CRUISE"]["SET_SPEED"] * CV.KPH_TO_MS
+ cluster_set_speed = cp.vl["PCM_CRUISE_ALT"]["UI_SET_SPEED"]
+ else:
+ ret.accFaulted = cp.vl["PCM_CRUISE_2"]["ACC_FAULTED"] != 0
+ ret.cruiseState.available = cp.vl["PCM_CRUISE_2"]["MAIN_ON"] != 0
+ ret.cruiseState.speed = cp.vl["PCM_CRUISE_2"]["SET_SPEED"] * CV.KPH_TO_MS
+ cluster_set_speed = cp.vl["PCM_CRUISE_SM"]["UI_SET_SPEED"]
+
+ # UI_SET_SPEED is always non-zero when main is on, hide until first enable
+ if ret.cruiseState.speed != 0:
+ is_metric = cp.vl["BODY_CONTROL_STATE_2"]["UNITS"] in (1, 2)
+ conversion_factor = CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS
+ ret.cruiseState.speedCluster = cluster_set_speed * conversion_factor
+
+ cp_acc = cp_cam if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR) else cp
+
+ if self.CP.carFingerprint in TSS2_CAR and not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
+ self.acc_type = cp_acc.vl["ACC_CONTROL"]["ACC_TYPE"]
+ ret.stockFcw = bool(cp_acc.vl["PCS_HUD"]["FCW"])
+
+ # some TSS2 cars have low speed lockout permanently set, so ignore on those cars
+ # these cars are identified by an ACC_TYPE value of 2.
+ # TODO: it is possible to avoid the lockout and gain stop and go if you
+ # send your own ACC_CONTROL msg on startup with ACC_TYPE set to 1
+ if (self.CP.carFingerprint not in TSS2_CAR and self.CP.carFingerprint not in UNSUPPORTED_DSU_CAR) or \
+ (self.CP.carFingerprint in TSS2_CAR and self.acc_type == 1):
+ self.low_speed_lockout = cp.vl["PCM_CRUISE_2"]["LOW_SPEED_LOCKOUT"] == 2
+
+ self.pcm_acc_status = cp.vl["PCM_CRUISE"]["CRUISE_STATE"]
+ if self.CP.carFingerprint not in (NO_STOP_TIMER_CAR - TSS2_CAR):
+ # ignore standstill state in certain vehicles, since pcm allows to restart with just an acceleration request
+ ret.cruiseState.standstill = self.pcm_acc_status == 7
+ ret.cruiseState.enabled = bool(cp.vl["PCM_CRUISE"]["CRUISE_ACTIVE"])
+ ret.cruiseState.nonAdaptive = self.pcm_acc_status in (1, 2, 3, 4, 5, 6)
+
+ ret.genericToggle = bool(cp.vl["LIGHT_STALK"]["AUTO_HIGH_BEAM"])
+ ret.espDisabled = cp.vl["ESP_CONTROL"]["TC_DISABLED"] != 0
+
+ if not self.CP.enableDsu and not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
+ ret.stockAeb = bool(cp_acc.vl["PRE_COLLISION"]["PRECOLLISION_ACTIVE"] and cp_acc.vl["PRE_COLLISION"]["FORCE"] < -1e-5)
+
+ if self.CP.enableBsm:
+ ret.leftBlindspot = (cp.vl["BSM"]["L_ADJACENT"] == 1) or (cp.vl["BSM"]["L_APPROACHING"] == 1)
+ ret.rightBlindspot = (cp.vl["BSM"]["R_ADJACENT"] == 1) or (cp.vl["BSM"]["R_APPROACHING"] == 1)
+
+ if self.CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
+ self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD"])
+
+ if self.CP.carFingerprint not in UNSUPPORTED_DSU_CAR:
+ self.pcm_follow_distance = cp.vl["PCM_CRUISE_2"]["PCM_FOLLOW_DISTANCE"]
+
+ if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR):
+ # distance button is wired to the ACC module (camera or radar)
+ prev_distance_button = self.distance_button
+ self.distance_button = cp_acc.vl["ACC_CONTROL"]["DISTANCE"]
+
+ ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
+
+ return ret
+
+ @staticmethod
+ def get_can_parser(CP):
+ messages = [
+ ("GEAR_PACKET", 1),
+ ("LIGHT_STALK", 1),
+ ("BLINKERS_STATE", 0.15),
+ ("BODY_CONTROL_STATE", 3),
+ ("BODY_CONTROL_STATE_2", 2),
+ ("ESP_CONTROL", 3),
+ ("EPS_STATUS", 25),
+ ("BRAKE_MODULE", 40),
+ ("WHEEL_SPEEDS", 80),
+ ("STEER_ANGLE_SENSOR", 80),
+ ("PCM_CRUISE", 33),
+ ("PCM_CRUISE_SM", 1),
+ ("STEER_TORQUE_SENSOR", 50),
+ ]
+
+ if CP.carFingerprint != CAR.TOYOTA_MIRAI:
+ messages.append(("ENGINE_RPM", 42))
+
+ if CP.carFingerprint in UNSUPPORTED_DSU_CAR:
+ messages.append(("DSU_CRUISE", 5))
+ messages.append(("PCM_CRUISE_ALT", 1))
+ else:
+ messages.append(("PCM_CRUISE_2", 33))
+
+ if CP.enableBsm:
+ messages.append(("BSM", 1))
+
+ if CP.carFingerprint in RADAR_ACC_CAR and not CP.flags & ToyotaFlags.DISABLE_RADAR.value:
+ messages += [
+ ("PCS_HUD", 1),
+ ("ACC_CONTROL", 33),
+ ]
+
+ if CP.carFingerprint not in (TSS2_CAR - RADAR_ACC_CAR) and not CP.enableDsu and not CP.flags & ToyotaFlags.DISABLE_RADAR.value:
+ messages += [
+ ("PRE_COLLISION", 33),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 0)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ messages = []
+
+ if CP.carFingerprint != CAR.TOYOTA_PRIUS_V:
+ messages += [
+ ("LKAS_HUD", 1),
+ ]
+
+ if CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR):
+ messages += [
+ ("PRE_COLLISION", 33),
+ ("ACC_CONTROL", 33),
+ ("PCS_HUD", 1),
+ ]
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, 2)
diff --git a/opendbc/car/toyota/fingerprints.py b/opendbc/car/toyota/fingerprints.py
new file mode 100644
index 0000000000..03c4958e0e
--- /dev/null
+++ b/opendbc/car/toyota/fingerprints.py
@@ -0,0 +1,1697 @@
+from opendbc.car.structs import CarParams
+from opendbc.car.toyota.values import CAR
+
+Ecu = CarParams.Ecu
+
+FW_VERSIONS = {
+ CAR.TOYOTA_AVALON: {
+ (Ecu.abs, 0x7b0, None): [
+ b'F152607060\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881510701300\x00\x00\x00\x00',
+ b'881510705100\x00\x00\x00\x00',
+ b'881510705200\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B41051\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0230721100\x00\x00\x00\x00\x00\x00\x00\x00A0C01000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230721200\x00\x00\x00\x00\x00\x00\x00\x00A0C01000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702000\x00\x00\x00\x00',
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F0701100\x00\x00\x00\x00',
+ b'8646F0703000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_AVALON_2019: {
+ (Ecu.abs, 0x7b0, None): [
+ b'F152607110\x00\x00\x00\x00\x00\x00',
+ b'F152607140\x00\x00\x00\x00\x00\x00',
+ b'F152607171\x00\x00\x00\x00\x00\x00',
+ b'F152607180\x00\x00\x00\x00\x00\x00',
+ b'F152641040\x00\x00\x00\x00\x00\x00',
+ b'F152641050\x00\x00\x00\x00\x00\x00',
+ b'F152641060\x00\x00\x00\x00\x00\x00',
+ b'F152641061\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881510703200\x00\x00\x00\x00',
+ b'881510704200\x00\x00\x00\x00',
+ b'881514107100\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B07010\x00\x00\x00\x00\x00\x00',
+ b'8965B41070\x00\x00\x00\x00\x00\x00',
+ b'8965B41080\x00\x00\x00\x00\x00\x00',
+ b'8965B41090\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x700, None): [
+ b'\x01896630725100\x00\x00\x00\x00',
+ b'\x01896630725200\x00\x00\x00\x00',
+ b'\x01896630725300\x00\x00\x00\x00',
+ b'\x01896630725400\x00\x00\x00\x00',
+ b'\x01896630735100\x00\x00\x00\x00',
+ b'\x01896630738000\x00\x00\x00\x00',
+ b'\x02896630724000\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x02896630728000\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x02896630734000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x02896630737000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F0702100\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_AVALON_TSS2: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F152607240\x00\x00\x00\x00\x00\x00',
+ b'\x01F152607250\x00\x00\x00\x00\x00\x00',
+ b'\x01F152607280\x00\x00\x00\x00\x00\x00',
+ b'F152641080\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B41110\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x700, None): [
+ b'\x018966306Q6000\x00\x00\x00\x00',
+ b'\x01896630742000\x00\x00\x00\x00',
+ b'\x01896630743000\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F6201200\x00\x00\x00\x00',
+ b'\x018821F6201300\x00\x00\x00\x00',
+ b'\x018821F6201400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F4104100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F4104100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_CAMRY: {
+ (Ecu.engine, 0x700, None): [
+ b'\x018966306L3100\x00\x00\x00\x00',
+ b'\x018966306L4200\x00\x00\x00\x00',
+ b'\x018966306L5200\x00\x00\x00\x00',
+ b'\x018966306L9000\x00\x00\x00\x00',
+ b'\x018966306P8000\x00\x00\x00\x00',
+ b'\x018966306Q3100\x00\x00\x00\x00',
+ b'\x018966306Q4000\x00\x00\x00\x00',
+ b'\x018966306Q4100\x00\x00\x00\x00',
+ b'\x018966306Q4200\x00\x00\x00\x00',
+ b'\x018966306Q6000\x00\x00\x00\x00',
+ b'\x018966333N1100\x00\x00\x00\x00',
+ b'\x018966333N4300\x00\x00\x00\x00',
+ b'\x018966333P3100\x00\x00\x00\x00',
+ b'\x018966333P3200\x00\x00\x00\x00',
+ b'\x018966333P4200\x00\x00\x00\x00',
+ b'\x018966333P4300\x00\x00\x00\x00',
+ b'\x018966333P4400\x00\x00\x00\x00',
+ b'\x018966333P4500\x00\x00\x00\x00',
+ b'\x018966333P4700\x00\x00\x00\x00',
+ b'\x018966333P4900\x00\x00\x00\x00',
+ b'\x018966333Q6000\x00\x00\x00\x00',
+ b'\x018966333Q6200\x00\x00\x00\x00',
+ b'\x018966333Q6300\x00\x00\x00\x00',
+ b'\x018966333Q6500\x00\x00\x00\x00',
+ b'\x018966333Q9200\x00\x00\x00\x00',
+ b'\x018966333W6000\x00\x00\x00\x00',
+ b'\x018966333X0000\x00\x00\x00\x00',
+ b'\x018966333X4000\x00\x00\x00\x00',
+ b'\x01896633T16000\x00\x00\x00\x00',
+ b'\x028966306B2100\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306B2300\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306B2500\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306N8100\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306N8200\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306N8300\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306N8400\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306R5000\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306R5000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x028966306R6000\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966306R6000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x028966306S0000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x028966306S0100\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x028966306S1100\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x02333P1100\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'8821F0601200 ',
+ b'8821F0601300 ',
+ b'8821F0601400 ',
+ b'8821F0601500 ',
+ b'8821F0602000 ',
+ b'8821F0603300 ',
+ b'8821F0603400 ',
+ b'8821F0604000 ',
+ b'8821F0604100 ',
+ b'8821F0604200 ',
+ b'8821F0605200 ',
+ b'8821F0606200 ',
+ b'8821F0607200 ',
+ b'8821F0608000 ',
+ b'8821F0608200 ',
+ b'8821F0608300 ',
+ b'8821F0609000 ',
+ b'8821F0609100 ',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152606210\x00\x00\x00\x00\x00\x00',
+ b'F152606230\x00\x00\x00\x00\x00\x00',
+ b'F152606260\x00\x00\x00\x00\x00\x00',
+ b'F152606270\x00\x00\x00\x00\x00\x00',
+ b'F152606290\x00\x00\x00\x00\x00\x00',
+ b'F152606410\x00\x00\x00\x00\x00\x00',
+ b'F152633214\x00\x00\x00\x00\x00\x00',
+ b'F152633540\x00\x00\x00\x00\x00\x00',
+ b'F152633660\x00\x00\x00\x00\x00\x00',
+ b'F152633712\x00\x00\x00\x00\x00\x00',
+ b'F152633713\x00\x00\x00\x00\x00\x00',
+ b'F152633A10\x00\x00\x00\x00\x00\x00',
+ b'F152633A20\x00\x00\x00\x00\x00\x00',
+ b'F152633B51\x00\x00\x00\x00\x00\x00',
+ b'F152633B60\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B33540\x00\x00\x00\x00\x00\x00',
+ b'8965B33542\x00\x00\x00\x00\x00\x00',
+ b'8965B33550\x00\x00\x00\x00\x00\x00',
+ b'8965B33551\x00\x00\x00\x00\x00\x00',
+ b'8965B33580\x00\x00\x00\x00\x00\x00',
+ b'8965B33581\x00\x00\x00\x00\x00\x00',
+ b'8965B33611\x00\x00\x00\x00\x00\x00',
+ b'8965B33621\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F0601200 ',
+ b'8821F0601300 ',
+ b'8821F0601400 ',
+ b'8821F0601500 ',
+ b'8821F0602000 ',
+ b'8821F0603300 ',
+ b'8821F0603400 ',
+ b'8821F0604000 ',
+ b'8821F0604100 ',
+ b'8821F0604200 ',
+ b'8821F0605200 ',
+ b'8821F0606200 ',
+ b'8821F0607200 ',
+ b'8821F0608000 ',
+ b'8821F0608200 ',
+ b'8821F0608300 ',
+ b'8821F0609000 ',
+ b'8821F0609100 ',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F0601200 ',
+ b'8646F0601300 ',
+ b'8646F0601400 ',
+ b'8646F0603400 ',
+ b'8646F0603500 ',
+ b'8646F0604000 ',
+ b'8646F0604100 ',
+ b'8646F0605000 ',
+ b'8646F0606000 ',
+ b'8646F0606100 ',
+ b'8646F0607000 ',
+ b'8646F0607100 ',
+ ],
+ },
+ CAR.TOYOTA_CAMRY_TSS2: {
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B33630\x00\x00\x00\x00\x00\x00',
+ b'8965B33640\x00\x00\x00\x00\x00\x00',
+ b'8965B33650\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F152606370\x00\x00\x00\x00\x00\x00',
+ b'\x01F152606390\x00\x00\x00\x00\x00\x00',
+ b'\x01F152606400\x00\x00\x00\x00\x00\x00',
+ b'\x01F152606431\x00\x00\x00\x00\x00\x00',
+ b'\x01F152633E11\x00\x00\x00\x00\x00\x00',
+ b'F152633310\x00\x00\x00\x00\x00\x00',
+ b'F152633D00\x00\x00\x00\x00\x00\x00',
+ b'F152633D60\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x700, None): [
+ b'\x018966306Q5000\x00\x00\x00\x00',
+ b'\x018966306Q6000\x00\x00\x00\x00',
+ b'\x018966306Q7000\x00\x00\x00\x00',
+ b'\x018966306Q9000\x00\x00\x00\x00',
+ b'\x018966306R3000\x00\x00\x00\x00',
+ b'\x018966306R8000\x00\x00\x00\x00',
+ b'\x018966306T0000\x00\x00\x00\x00',
+ b'\x018966306T3100\x00\x00\x00\x00',
+ b'\x018966306T3200\x00\x00\x00\x00',
+ b'\x018966306T4000\x00\x00\x00\x00',
+ b'\x018966306T4100\x00\x00\x00\x00',
+ b'\x018966306V1000\x00\x00\x00\x00',
+ b'\x018966333Z1000\x00\x00\x00\x00',
+ b'\x01896633T20000\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F6201200\x00\x00\x00\x00',
+ b'\x018821F6201300\x00\x00\x00\x00',
+ b'\x018821F6201400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F0602100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ b'\x028646F0602200\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ b'\x028646F0602300\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F3305200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F3305200\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ b'\x028646F3305300\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F3305300\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ b'\x028646F3305400\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F3305500\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_CHR: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896631017100\x00\x00\x00\x00',
+ b'\x01896631017200\x00\x00\x00\x00',
+ b'\x01896631021100\x00\x00\x00\x00',
+ b'\x0189663F413100\x00\x00\x00\x00',
+ b'\x0189663F414100\x00\x00\x00\x00',
+ b'\x0189663F438000\x00\x00\x00\x00',
+ b'\x02896631013200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x0289663F405000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x0289663F405100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x0289663F418000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x0289663F423000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x0289663F431000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'8821F0W01000 ',
+ b'8821F0W01100 ',
+ b'8821FF401600 ',
+ b'8821FF402300 ',
+ b'8821FF402400 ',
+ b'8821FF404000 ',
+ b'8821FF404100 ',
+ b'8821FF405000 ',
+ b'8821FF405100 ',
+ b'8821FF406000 ',
+ b'8821FF407100 ',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152610012\x00\x00\x00\x00\x00\x00',
+ b'F152610013\x00\x00\x00\x00\x00\x00',
+ b'F152610014\x00\x00\x00\x00\x00\x00',
+ b'F152610020\x00\x00\x00\x00\x00\x00',
+ b'F152610040\x00\x00\x00\x00\x00\x00',
+ b'F152610153\x00\x00\x00\x00\x00\x00',
+ b'F152610190\x00\x00\x00\x00\x00\x00',
+ b'F152610200\x00\x00\x00\x00\x00\x00',
+ b'F152610210\x00\x00\x00\x00\x00\x00',
+ b'F152610220\x00\x00\x00\x00\x00\x00',
+ b'F152610230\x00\x00\x00\x00\x00\x00',
+ b'F1526F4034\x00\x00\x00\x00\x00\x00',
+ b'F1526F4044\x00\x00\x00\x00\x00\x00',
+ b'F1526F4073\x00\x00\x00\x00\x00\x00',
+ b'F1526F4121\x00\x00\x00\x00\x00\x00',
+ b'F1526F4122\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B10011\x00\x00\x00\x00\x00\x00',
+ b'8965B10020\x00\x00\x00\x00\x00\x00',
+ b'8965B10040\x00\x00\x00\x00\x00\x00',
+ b'8965B10050\x00\x00\x00\x00\x00\x00',
+ b'8965B10070\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0331024000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00',
+ b'\x0331024000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00',
+ b'\x0331036000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00',
+ b'\x033F401100\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203102\x00\x00\x00\x00',
+ b'\x033F401200\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00',
+ b'\x033F424000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00',
+ b'\x033F424000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F0W01000 ',
+ b'8821F0W01100 ',
+ b'8821FF401600 ',
+ b'8821FF402300 ',
+ b'8821FF402400 ',
+ b'8821FF404000 ',
+ b'8821FF404100 ',
+ b'8821FF405000 ',
+ b'8821FF405100 ',
+ b'8821FF406000 ',
+ b'8821FF407100 ',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646FF401700 ',
+ b'8646FF401800 ',
+ b'8646FF402100 ',
+ b'8646FF404000 ',
+ b'8646FF406000 ',
+ b'8646FF407000 ',
+ b'8646FF407100 ',
+ ],
+ },
+ CAR.TOYOTA_CHR_TSS2: {
+ (Ecu.abs, 0x7b0, None): [
+ b'F152610041\x00\x00\x00\x00\x00\x00',
+ b'F152610260\x00\x00\x00\x00\x00\x00',
+ b'F1526F4270\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B10091\x00\x00\x00\x00\x00\x00',
+ b'8965B10092\x00\x00\x00\x00\x00\x00',
+ b'8965B10110\x00\x00\x00\x00\x00\x00',
+ b'8965B10111\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x700, None): [
+ b'\x0189663F438000\x00\x00\x00\x00',
+ b'\x0189663F459000\x00\x00\x00\x00',
+ b'\x02896631025000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x0289663F453000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0331014000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203402\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821FF410200\x00\x00\x00\x00',
+ b'\x018821FF410300\x00\x00\x00\x00',
+ b'\x018821FF410400\x00\x00\x00\x00',
+ b'\x018821FF410500\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646FF410200\x00\x00\x00\x008646GF408200\x00\x00\x00\x00',
+ b'\x028646FF411100\x00\x00\x00\x008646GF409000\x00\x00\x00\x00',
+ b'\x028646FF413100\x00\x00\x00\x008646GF411100\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_COROLLA: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0230ZC2000\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZC2100\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZC2200\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZC2300\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZC3000\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZC3100\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZC3200\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZC3300\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0330ZC1200\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881510201100\x00\x00\x00\x00',
+ b'881510201200\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152602190\x00\x00\x00\x00\x00\x00',
+ b'F152602191\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B02181\x00\x00\x00\x00\x00\x00',
+ b'8965B02191\x00\x00\x00\x00\x00\x00',
+ b'8965B48150\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F0201101\x00\x00\x00\x00',
+ b'8646F0201200\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_COROLLA_TSS2: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896630A22000\x00\x00\x00\x00',
+ b'\x01896630ZG2000\x00\x00\x00\x00',
+ b'\x01896630ZG5000\x00\x00\x00\x00',
+ b'\x01896630ZG5100\x00\x00\x00\x00',
+ b'\x01896630ZG5200\x00\x00\x00\x00',
+ b'\x01896630ZG5300\x00\x00\x00\x00',
+ b'\x01896630ZJ1000\x00\x00\x00\x00',
+ b'\x01896630ZP1000\x00\x00\x00\x00',
+ b'\x01896630ZP2000\x00\x00\x00\x00',
+ b'\x01896630ZQ5000\x00\x00\x00\x00',
+ b'\x01896630ZU8000\x00\x00\x00\x00',
+ b'\x01896630ZU9000\x00\x00\x00\x00',
+ b'\x01896630ZX4000\x00\x00\x00\x00',
+ b'\x01896630ZX7100\x00\x00\x00\x00',
+ b'\x018966312L8000\x00\x00\x00\x00',
+ b'\x018966312M0000\x00\x00\x00\x00',
+ b'\x018966312M9000\x00\x00\x00\x00',
+ b'\x018966312P9000\x00\x00\x00\x00',
+ b'\x018966312P9100\x00\x00\x00\x00',
+ b'\x018966312P9200\x00\x00\x00\x00',
+ b'\x018966312P9300\x00\x00\x00\x00',
+ b'\x018966312Q2300\x00\x00\x00\x00',
+ b'\x018966312Q8000\x00\x00\x00\x00',
+ b'\x018966312R0000\x00\x00\x00\x00',
+ b'\x018966312R0100\x00\x00\x00\x00',
+ b'\x018966312R0200\x00\x00\x00\x00',
+ b'\x018966312R1000\x00\x00\x00\x00',
+ b'\x018966312R1100\x00\x00\x00\x00',
+ b'\x018966312R3100\x00\x00\x00\x00',
+ b'\x018966312S5000\x00\x00\x00\x00',
+ b'\x018966312S7000\x00\x00\x00\x00',
+ b'\x018966312W3000\x00\x00\x00\x00',
+ b'\x018966312W9000\x00\x00\x00\x00',
+ b'\x01896637621000\x00\x00\x00\x00',
+ b'\x01896637623000\x00\x00\x00\x00',
+ b'\x01896637624000\x00\x00\x00\x00',
+ b'\x01896637626000\x00\x00\x00\x00',
+ b'\x01896637639000\x00\x00\x00\x00',
+ b'\x01896637643000\x00\x00\x00\x00',
+ b'\x01896637644000\x00\x00\x00\x00',
+ b'\x01896637648000\x00\x00\x00\x00',
+ b'\x02896630A07000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630A21000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630ZJ5000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630ZK8000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630ZN8000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630ZQ3000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630ZR2000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630ZT8000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630ZT9000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896630ZZ0000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966312K6000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966312L0000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966312Q3000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966312Q3100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966312Q4000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x038966312L7000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF1205001\x00\x00\x00\x00',
+ b'\x038966312N1000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00',
+ b'\x038966312T3000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF1205001\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0230A10000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230A11000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZN4000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230ZN5000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02312K4000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02312U5000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x03312K7000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203402\x00\x00\x00\x00',
+ b'\x03312M3000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203402\x00\x00\x00\x00',
+ b'\x03312N6000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00',
+ b'\x03312N6000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00',
+ b'\x03312N6000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203402\x00\x00\x00\x00',
+ b'\x03312N6100\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00',
+ b'\x03312N6100\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203402\x00\x00\x00\x00',
+ b'\x03312N6200\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00',
+ b'\x03312N6200\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00',
+ b'\x03312N6200\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203402\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'\x018965B12350\x00\x00\x00\x00\x00\x00',
+ b'\x018965B12470\x00\x00\x00\x00\x00\x00',
+ b'\x018965B12490\x00\x00\x00\x00\x00\x00',
+ b'\x018965B12500\x00\x00\x00\x00\x00\x00',
+ b'\x018965B12510\x00\x00\x00\x00\x00\x00',
+ b'\x018965B12520\x00\x00\x00\x00\x00\x00',
+ b'\x018965B12530\x00\x00\x00\x00\x00\x00',
+ b'\x018965B1254000\x00\x00\x00\x00',
+ b'\x018965B1255000\x00\x00\x00\x00',
+ b'\x018965B1256000\x00\x00\x00\x00',
+ b'\x018965B1270000\x00\x00\x00\x00',
+ b'8965B12361\x00\x00\x00\x00\x00\x00',
+ b'8965B12451\x00\x00\x00\x00\x00\x00',
+ b'8965B16011\x00\x00\x00\x00\x00\x00',
+ b'8965B16101\x00\x00\x00\x00\x00\x00',
+ b'8965B16170\x00\x00\x00\x00\x00\x00',
+ b'8965B76012\x00\x00\x00\x00\x00\x00',
+ b'8965B76050\x00\x00\x00\x00\x00\x00',
+ b'8965B76091\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F152602280\x00\x00\x00\x00\x00\x00',
+ b'\x01F152602281\x00\x00\x00\x00\x00\x00',
+ b'\x01F152602470\x00\x00\x00\x00\x00\x00',
+ b'\x01F152602560\x00\x00\x00\x00\x00\x00',
+ b'\x01F152602590\x00\x00\x00\x00\x00\x00',
+ b'\x01F152602650\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260A010\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260A050\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260A070\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612641\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612651\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612862\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B10\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B51\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B60\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B61\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B62\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B70\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B71\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B81\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B90\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612B91\x00\x00\x00\x00\x00\x00',
+ b'\x01F152612C00\x00\x00\x00\x00\x00\x00',
+ b'\x01F152676250\x00\x00\x00\x00\x00\x00',
+ b'F152612590\x00\x00\x00\x00\x00\x00',
+ b'F152612691\x00\x00\x00\x00\x00\x00',
+ b'F152612692\x00\x00\x00\x00\x00\x00',
+ b'F152612700\x00\x00\x00\x00\x00\x00',
+ b'F152612710\x00\x00\x00\x00\x00\x00',
+ b'F152612790\x00\x00\x00\x00\x00\x00',
+ b'F152612800\x00\x00\x00\x00\x00\x00',
+ b'F152612820\x00\x00\x00\x00\x00\x00',
+ b'F152612840\x00\x00\x00\x00\x00\x00',
+ b'F152612842\x00\x00\x00\x00\x00\x00',
+ b'F152612890\x00\x00\x00\x00\x00\x00',
+ b'F152612A00\x00\x00\x00\x00\x00\x00',
+ b'F152612A10\x00\x00\x00\x00\x00\x00',
+ b'F152612D00\x00\x00\x00\x00\x00\x00',
+ b'F152616011\x00\x00\x00\x00\x00\x00',
+ b'F152616030\x00\x00\x00\x00\x00\x00',
+ b'F152616060\x00\x00\x00\x00\x00\x00',
+ b'F152642540\x00\x00\x00\x00\x00\x00',
+ b'F152676293\x00\x00\x00\x00\x00\x00',
+ b'F152676303\x00\x00\x00\x00\x00\x00',
+ b'F152676304\x00\x00\x00\x00\x00\x00',
+ b'F152676371\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F3301100\x00\x00\x00\x00',
+ b'\x018821F3301200\x00\x00\x00\x00',
+ b'\x018821F3301300\x00\x00\x00\x00',
+ b'\x018821F3301400\x00\x00\x00\x00',
+ b'\x018821F6201400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F12010C0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F12010D0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F1201100\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F1201200\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F1201300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F1201400\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
+ b'\x028646F1202000\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F1202100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F1202200\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
+ b'\x028646F1206000\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
+ b'\x028646F1601100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F1601200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F1601300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F4203400\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F76020C0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F7603100\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F7603200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F7603300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F7605100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_HIGHLANDER: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896630E09000\x00\x00\x00\x00',
+ b'\x01896630E43000\x00\x00\x00\x00',
+ b'\x01896630E43100\x00\x00\x00\x00',
+ b'\x01896630E43200\x00\x00\x00\x00',
+ b'\x01896630E44200\x00\x00\x00\x00',
+ b'\x01896630E44400\x00\x00\x00\x00',
+ b'\x01896630E45000\x00\x00\x00\x00',
+ b'\x01896630E45100\x00\x00\x00\x00',
+ b'\x01896630E45200\x00\x00\x00\x00',
+ b'\x01896630E46000\x00\x00\x00\x00',
+ b'\x01896630E46200\x00\x00\x00\x00',
+ b'\x01896630E74000\x00\x00\x00\x00',
+ b'\x01896630E75000\x00\x00\x00\x00',
+ b'\x01896630E76000\x00\x00\x00\x00',
+ b'\x01896630E77000\x00\x00\x00\x00',
+ b'\x01896630E83000\x00\x00\x00\x00',
+ b'\x01896630E84000\x00\x00\x00\x00',
+ b'\x01896630E85000\x00\x00\x00\x00',
+ b'\x01896630E86000\x00\x00\x00\x00',
+ b'\x01896630E88000\x00\x00\x00\x00',
+ b'\x01896630EA0000\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0230E40000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230E40100\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230E51000\x00\x00\x00\x00\x00\x00\x00\x0050E17000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230EA2000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0230EA2100\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B48140\x00\x00\x00\x00\x00\x00',
+ b'8965B48150\x00\x00\x00\x00\x00\x00',
+ b'8965B48160\x00\x00\x00\x00\x00\x00',
+ b'8965B48210\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F15260E011\x00\x00\x00\x00\x00\x00',
+ b'F152648541\x00\x00\x00\x00\x00\x00',
+ b'F152648542\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881510E01100\x00\x00\x00\x00',
+ b'881510E01200\x00\x00\x00\x00',
+ b'881510E02100\x00\x00\x00\x00',
+ b'881510E02200\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F0E01200\x00\x00\x00\x00',
+ b'8646F0E01300\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_HIGHLANDER_TSS2: {
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B48241\x00\x00\x00\x00\x00\x00',
+ b'8965B48310\x00\x00\x00\x00\x00\x00',
+ b'8965B48320\x00\x00\x00\x00\x00\x00',
+ b'8965B48400\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F15260E051\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260E05300\x00\x00\x00\x00',
+ b'\x01F15260E061\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260E110\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260E170\x00\x00\x00\x00\x00\x00',
+ b'\x01F15264872300\x00\x00\x00\x00',
+ b'\x01F15264872400\x00\x00\x00\x00',
+ b'\x01F15264872500\x00\x00\x00\x00',
+ b'\x01F15264872600\x00\x00\x00\x00',
+ b'\x01F15264872700\x00\x00\x00\x00',
+ b'\x01F15264873500\x00\x00\x00\x00',
+ b'\x01F152648C6300\x00\x00\x00\x00',
+ b'\x01F152648J4000\x00\x00\x00\x00',
+ b'\x01F152648J5000\x00\x00\x00\x00',
+ b'\x01F152648J6000\x00\x00\x00\x00',
+ b'\x01F152648J7000\x00\x00\x00\x00',
+ b'\x01F152648L5000\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x700, None): [
+ b'\x01896630E62100\x00\x00\x00\x00',
+ b'\x01896630E62200\x00\x00\x00\x00',
+ b'\x01896630E64100\x00\x00\x00\x00',
+ b'\x01896630E64200\x00\x00\x00\x00',
+ b'\x01896630E64400\x00\x00\x00\x00',
+ b'\x01896630E67000\x00\x00\x00\x00',
+ b'\x01896630EA1000\x00\x00\x00\x00',
+ b'\x01896630EB1000\x00\x00\x00\x00',
+ b'\x01896630EB1100\x00\x00\x00\x00',
+ b'\x01896630EB1200\x00\x00\x00\x00',
+ b'\x01896630EB1300\x00\x00\x00\x00',
+ b'\x01896630EB2000\x00\x00\x00\x00',
+ b'\x01896630EB2100\x00\x00\x00\x00',
+ b'\x01896630EB2200\x00\x00\x00\x00',
+ b'\x01896630EC4000\x00\x00\x00\x00',
+ b'\x01896630ED9000\x00\x00\x00\x00',
+ b'\x01896630ED9100\x00\x00\x00\x00',
+ b'\x01896630EE1000\x00\x00\x00\x00',
+ b'\x01896630EE1100\x00\x00\x00\x00',
+ b'\x01896630EE4000\x00\x00\x00\x00',
+ b'\x01896630EE4100\x00\x00\x00\x00',
+ b'\x01896630EE5000\x00\x00\x00\x00',
+ b'\x01896630EE6000\x00\x00\x00\x00',
+ b'\x01896630EE7000\x00\x00\x00\x00',
+ b'\x01896630EF8000\x00\x00\x00\x00',
+ b'\x01896630EG3000\x00\x00\x00\x00',
+ b'\x01896630EG5000\x00\x00\x00\x00',
+ b'\x02896630E66000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ b'\x02896630E66100\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ b'\x02896630EB3000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ b'\x02896630EB3100\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F3301400\x00\x00\x00\x00',
+ b'\x018821F6201200\x00\x00\x00\x00',
+ b'\x018821F6201300\x00\x00\x00\x00',
+ b'\x018821F6201400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F0E02100\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F4803000\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F4803000\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ b'\x028646F4803200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_IS: {
+ (Ecu.engine, 0x700, None): [
+ b'\x018966353M7000\x00\x00\x00\x00',
+ b'\x018966353M7100\x00\x00\x00\x00',
+ b'\x018966353Q2000\x00\x00\x00\x00',
+ b'\x018966353Q2100\x00\x00\x00\x00',
+ b'\x018966353Q2300\x00\x00\x00\x00',
+ b'\x018966353Q4000\x00\x00\x00\x00',
+ b'\x018966353R1100\x00\x00\x00\x00',
+ b'\x018966353R7100\x00\x00\x00\x00',
+ b'\x018966353R8000\x00\x00\x00\x00',
+ b'\x018966353R8100\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0232480000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02353P7000\x00\x00\x00\x00\x00\x00\x00\x00530J5000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02353P9000\x00\x00\x00\x00\x00\x00\x00\x00553C1000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152653300\x00\x00\x00\x00\x00\x00',
+ b'F152653301\x00\x00\x00\x00\x00\x00',
+ b'F152653310\x00\x00\x00\x00\x00\x00',
+ b'F152653330\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881515306200\x00\x00\x00\x00',
+ b'881515306400\x00\x00\x00\x00',
+ b'881515306500\x00\x00\x00\x00',
+ b'881515307400\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B53270\x00\x00\x00\x00\x00\x00',
+ b'8965B53271\x00\x00\x00\x00\x00\x00',
+ b'8965B53280\x00\x00\x00\x00\x00\x00',
+ b'8965B53281\x00\x00\x00\x00\x00\x00',
+ b'8965B53311\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F5301101\x00\x00\x00\x00',
+ b'8646F5301200\x00\x00\x00\x00',
+ b'8646F5301300\x00\x00\x00\x00',
+ b'8646F5301400\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_IS_TSS2: {
+ (Ecu.engine, 0x700, None): [
+ b'\x018966353S1000\x00\x00\x00\x00',
+ b'\x018966353S2000\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x02353U0000\x00\x00\x00\x00\x00\x00\x00\x0052422000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F15265337200\x00\x00\x00\x00',
+ b'\x01F15265342000\x00\x00\x00\x00',
+ b'\x01F15265343000\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B53450\x00\x00\x00\x00\x00\x00',
+ b'8965B53800\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F6201200\x00\x00\x00\x00',
+ b'\x018821F6201300\x00\x00\x00\x00',
+ b'\x018821F6201400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F5303300\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F5303300\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ b'\x028646F5303400\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_PRIUS: {
+ (Ecu.engine, 0x700, None): [
+ b'\x02896634761000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634761100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634761200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634762000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634762100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634763000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634763100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634765000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634765100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634769000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634769100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634769200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634770000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634770100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634774000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634774100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634774200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634782000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x02896634784000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966347A0000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966347A5000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966347A8000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966347B0000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x03896634759100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
+ b'\x03896634759200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
+ b'\x03896634759200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00',
+ b'\x03896634759300\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
+ b'\x03896634759300\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00',
+ b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701002\x00\x00\x00\x00',
+ b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
+ b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00',
+ b'\x03896634760100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
+ b'\x03896634760200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
+ b'\x03896634760200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00',
+ b'\x03896634760300\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00',
+ b'\x03896634768000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4703001\x00\x00\x00\x00',
+ b'\x03896634768000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4703002\x00\x00\x00\x00',
+ b'\x03896634768100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4703002\x00\x00\x00\x00',
+ b'\x03896634785000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4705001\x00\x00\x00\x00',
+ b'\x03896634785000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4710001\x00\x00\x00\x00',
+ b'\x03896634786000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4705001\x00\x00\x00\x00',
+ b'\x03896634786000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4710001\x00\x00\x00\x00',
+ b'\x03896634789000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4703002\x00\x00\x00\x00',
+ b'\x038966347A3000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
+ b'\x038966347A3000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4707001\x00\x00\x00\x00',
+ b'\x038966347B6000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4710001\x00\x00\x00\x00',
+ b'\x038966347B7000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00',
+ b'\x038966347B7000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4710001\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B47021\x00\x00\x00\x00\x00\x00',
+ b'8965B47022\x00\x00\x00\x00\x00\x00',
+ b'8965B47023\x00\x00\x00\x00\x00\x00',
+ b'8965B47050\x00\x00\x00\x00\x00\x00',
+ b'8965B47060\x00\x00\x00\x00\x00\x00',
+ b'8965B47070\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152647290\x00\x00\x00\x00\x00\x00',
+ b'F152647300\x00\x00\x00\x00\x00\x00',
+ b'F152647310\x00\x00\x00\x00\x00\x00',
+ b'F152647414\x00\x00\x00\x00\x00\x00',
+ b'F152647415\x00\x00\x00\x00\x00\x00',
+ b'F152647416\x00\x00\x00\x00\x00\x00',
+ b'F152647417\x00\x00\x00\x00\x00\x00',
+ b'F152647470\x00\x00\x00\x00\x00\x00',
+ b'F152647490\x00\x00\x00\x00\x00\x00',
+ b'F152647682\x00\x00\x00\x00\x00\x00',
+ b'F152647683\x00\x00\x00\x00\x00\x00',
+ b'F152647684\x00\x00\x00\x00\x00\x00',
+ b'F152647862\x00\x00\x00\x00\x00\x00',
+ b'F152647863\x00\x00\x00\x00\x00\x00',
+ b'F152647864\x00\x00\x00\x00\x00\x00',
+ b'F152647865\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881514702300\x00\x00\x00\x00',
+ b'881514702400\x00\x00\x00\x00',
+ b'881514703100\x00\x00\x00\x00',
+ b'881514704100\x00\x00\x00\x00',
+ b'881514706000\x00\x00\x00\x00',
+ b'881514706100\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702000\x00\x00\x00\x00',
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F4701300\x00\x00\x00\x00',
+ b'8646F4702001\x00\x00\x00\x00',
+ b'8646F4702100\x00\x00\x00\x00',
+ b'8646F4702200\x00\x00\x00\x00',
+ b'8646F4705000\x00\x00\x00\x00',
+ b'8646F4705200\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_PRIUS_V: {
+ (Ecu.abs, 0x7b0, None): [
+ b'F152647280\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0234781000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881514705100\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F4703300\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_RAV4: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\x02342Q1000\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342Q1100\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342Q1200\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342Q1300\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342Q2000\x00\x00\x00\x00\x00\x00\x00\x0054213000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342Q2100\x00\x00\x00\x00\x00\x00\x00\x0054213000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342Q2200\x00\x00\x00\x00\x00\x00\x00\x0054213000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342Q4000\x00\x00\x00\x00\x00\x00\x00\x0054215000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B42063\x00\x00\x00\x00\x00\x00',
+ b'8965B42073\x00\x00\x00\x00\x00\x00',
+ b'8965B42082\x00\x00\x00\x00\x00\x00',
+ b'8965B42083\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F15260R102\x00\x00\x00\x00\x00\x00',
+ b'F15260R103\x00\x00\x00\x00\x00\x00',
+ b'F152642492\x00\x00\x00\x00\x00\x00',
+ b'F152642493\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881514201200\x00\x00\x00\x00',
+ b'881514201300\x00\x00\x00\x00',
+ b'881514201400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702000\x00\x00\x00\x00',
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F4201100\x00\x00\x00\x00',
+ b'8646F4201200\x00\x00\x00\x00',
+ b'8646F4202001\x00\x00\x00\x00',
+ b'8646F4202100\x00\x00\x00\x00',
+ b'8646F4204000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_RAV4H: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\x02342N9000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342N9100\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02342P0000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B42102\x00\x00\x00\x00\x00\x00',
+ b'8965B42103\x00\x00\x00\x00\x00\x00',
+ b'8965B42112\x00\x00\x00\x00\x00\x00',
+ b'8965B42162\x00\x00\x00\x00\x00\x00',
+ b'8965B42163\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152642090\x00\x00\x00\x00\x00\x00',
+ b'F152642110\x00\x00\x00\x00\x00\x00',
+ b'F152642120\x00\x00\x00\x00\x00\x00',
+ b'F152642400\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881514202200\x00\x00\x00\x00',
+ b'881514202300\x00\x00\x00\x00',
+ b'881514202400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702000\x00\x00\x00\x00',
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F4201100\x00\x00\x00\x00',
+ b'8646F4201200\x00\x00\x00\x00',
+ b'8646F4202001\x00\x00\x00\x00',
+ b'8646F4202100\x00\x00\x00\x00',
+ b'8646F4204000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_RAV4_TSS2: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896630R58000\x00\x00\x00\x00',
+ b'\x01896630R58100\x00\x00\x00\x00',
+ b'\x018966342E2000\x00\x00\x00\x00',
+ b'\x018966342M5000\x00\x00\x00\x00',
+ b'\x018966342M8000\x00\x00\x00\x00',
+ b'\x018966342S9000\x00\x00\x00\x00',
+ b'\x018966342T1000\x00\x00\x00\x00',
+ b'\x018966342T6000\x00\x00\x00\x00',
+ b'\x018966342T9000\x00\x00\x00\x00',
+ b'\x018966342U4000\x00\x00\x00\x00',
+ b'\x018966342U4100\x00\x00\x00\x00',
+ b'\x018966342U5100\x00\x00\x00\x00',
+ b'\x018966342V0000\x00\x00\x00\x00',
+ b'\x018966342V3000\x00\x00\x00\x00',
+ b'\x018966342V3100\x00\x00\x00\x00',
+ b'\x018966342V3200\x00\x00\x00\x00',
+ b'\x018966342W5000\x00\x00\x00\x00',
+ b'\x018966342W7000\x00\x00\x00\x00',
+ b'\x018966342W8000\x00\x00\x00\x00',
+ b'\x018966342X5000\x00\x00\x00\x00',
+ b'\x018966342X6000\x00\x00\x00\x00',
+ b'\x01896634A05000\x00\x00\x00\x00',
+ b'\x01896634A15000\x00\x00\x00\x00',
+ b'\x01896634A19000\x00\x00\x00\x00',
+ b'\x01896634A19100\x00\x00\x00\x00',
+ b'\x01896634A20000\x00\x00\x00\x00',
+ b'\x01896634A20100\x00\x00\x00\x00',
+ b'\x01896634A22000\x00\x00\x00\x00',
+ b'\x01896634A22100\x00\x00\x00\x00',
+ b'\x01896634A25000\x00\x00\x00\x00',
+ b'\x01896634A30000\x00\x00\x00\x00',
+ b'\x01896634A44000\x00\x00\x00\x00',
+ b'\x01896634A45000\x00\x00\x00\x00',
+ b'\x01896634A46000\x00\x00\x00\x00',
+ b'\x028966342M7000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00',
+ b'\x028966342T0000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00',
+ b'\x028966342V1000\x00\x00\x00\x00897CF1202001\x00\x00\x00\x00',
+ b'\x028966342W4001\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00',
+ b'\x028966342Y8000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00',
+ b'\x028966342Z8000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00',
+ b'\x02896634A13000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02896634A13001\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ b'\x02896634A13101\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ b'\x02896634A13201\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ b'\x02896634A14001\x00\x00\x00\x00897CF0R01000\x00\x00\x00\x00',
+ b'\x02896634A14001\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00',
+ b'\x02896634A14001\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ b'\x02896634A14101\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00',
+ b'\x02896634A18000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00',
+ b'\x02896634A18100\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00',
+ b'\x02896634A23000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02896634A23001\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00',
+ b'\x02896634A23101\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00',
+ b'\x02896634A43000\x00\x00\x00\x00897CF4201001\x00\x00\x00\x00',
+ b'\x02896634A47000\x00\x00\x00\x00897CF4201001\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F15260R210\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R220\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R290\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R292\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R300\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R302\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642551\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642561\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642601\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642700\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642701\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642710\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642711\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642750\x00\x00\x00\x00\x00\x00',
+ b'\x01F152642751\x00\x00\x00\x00\x00\x00',
+ b'F152642290\x00\x00\x00\x00\x00\x00',
+ b'F152642291\x00\x00\x00\x00\x00\x00',
+ b'F152642322\x00\x00\x00\x00\x00\x00',
+ b'F152642330\x00\x00\x00\x00\x00\x00',
+ b'F152642331\x00\x00\x00\x00\x00\x00',
+ b'F152642520\x00\x00\x00\x00\x00\x00',
+ b'F152642521\x00\x00\x00\x00\x00\x00',
+ b'F152642531\x00\x00\x00\x00\x00\x00',
+ b'F152642532\x00\x00\x00\x00\x00\x00',
+ b'F152642540\x00\x00\x00\x00\x00\x00',
+ b'F152642541\x00\x00\x00\x00\x00\x00',
+ b'F152642542\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'\x028965B0R01200\x00\x00\x00\x008965B0R02200\x00\x00\x00\x00',
+ b'\x028965B0R01300\x00\x00\x00\x008965B0R02300\x00\x00\x00\x00',
+ b'\x028965B0R01400\x00\x00\x00\x008965B0R02400\x00\x00\x00\x00',
+ b'8965B42170\x00\x00\x00\x00\x00\x00',
+ b'8965B42171\x00\x00\x00\x00\x00\x00',
+ b'8965B42180\x00\x00\x00\x00\x00\x00',
+ b'8965B42181\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F3301100\x00\x00\x00\x00',
+ b'\x018821F3301200\x00\x00\x00\x00',
+ b'\x018821F3301300\x00\x00\x00\x00',
+ b'\x018821F3301400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F4203200\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F4203300\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F4203400\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F4203500\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F4203700\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F4203800\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_RAV4_TSS2_2022: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F15260R350\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R361\x00\x00\x00\x00\x00\x00',
+ b'\x01F15264283100\x00\x00\x00\x00',
+ b'\x01F15264283200\x00\x00\x00\x00',
+ b'\x01F15264286100\x00\x00\x00\x00',
+ b'\x01F15264286200\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'\x028965B0R01500\x00\x00\x00\x008965B0R02500\x00\x00\x00\x00',
+ b'8965B42172\x00\x00\x00\x00\x00\x00',
+ b'8965B42182\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x700, None): [
+ b'\x01896634A02001\x00\x00\x00\x00',
+ b'\x01896634A03000\x00\x00\x00\x00',
+ b'\x01896634A08000\x00\x00\x00\x00',
+ b'\x01896634A61000\x00\x00\x00\x00',
+ b'\x01896634A62000\x00\x00\x00\x00',
+ b'\x01896634A62100\x00\x00\x00\x00',
+ b'\x01896634A63000\x00\x00\x00\x00',
+ b'\x01896634A88000\x00\x00\x00\x00',
+ b'\x01896634A89000\x00\x00\x00\x00',
+ b'\x01896634A89100\x00\x00\x00\x00',
+ b'\x01896634AA0000\x00\x00\x00\x00',
+ b'\x01896634AA0100\x00\x00\x00\x00',
+ b'\x01896634AA1000\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F0R01100\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F0R02100\x00\x00\x00\x008646G0R01100\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_RAV4_TSS2_2023: {
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F15260R440\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R450\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260R50000\x00\x00\x00\x00',
+ b'\x01F15260R51000\x00\x00\x00\x00',
+ b'\x01F15264283200\x00\x00\x00\x00',
+ b'\x01F15264283300\x00\x00\x00\x00',
+ b'\x01F152642F1000\x00\x00\x00\x00',
+ b'\x01F152642F8000\x00\x00\x00\x00',
+ b'\x01F152642F8100\x00\x00\x00\x00',
+ b'\x01F152642F9000\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'\x028965B0R11000\x00\x00\x00\x008965B0R12000\x00\x00\x00\x00',
+ b'8965B42371\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x700, None): [
+ b'\x01896634A61000\x00\x00\x00\x00',
+ b'\x01896634A88100\x00\x00\x00\x00',
+ b'\x01896634A89100\x00\x00\x00\x00',
+ b'\x01896634AE1001\x00\x00\x00\x00',
+ b'\x01896634AF0000\x00\x00\x00\x00',
+ b'\x01896634AJ2000\x00\x00\x00\x00',
+ b'\x01896634AJ3000\x00\x00\x00\x00',
+ b'\x01896634AL5000\x00\x00\x00\x00',
+ b'\x01896634AL6000\x00\x00\x00\x00',
+ b'\x01896634AL8000\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F0R03100\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F0R05100\x00\x00\x00\x008646G0R02100\x00\x00\x00\x00',
+ b'\x028646F0R05200\x00\x00\x00\x008646G0R02200\x00\x00\x00\x00',
+ b'\x028646F0R11000\x00\x00\x00\x008646G0R04000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_SIENNA: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896630832100\x00\x00\x00\x00',
+ b'\x01896630832200\x00\x00\x00\x00',
+ b'\x01896630838000\x00\x00\x00\x00',
+ b'\x01896630838100\x00\x00\x00\x00',
+ b'\x01896630842000\x00\x00\x00\x00',
+ b'\x01896630843000\x00\x00\x00\x00',
+ b'\x01896630851000\x00\x00\x00\x00',
+ b'\x01896630851100\x00\x00\x00\x00',
+ b'\x01896630851200\x00\x00\x00\x00',
+ b'\x01896630852000\x00\x00\x00\x00',
+ b'\x01896630852100\x00\x00\x00\x00',
+ b'\x01896630859000\x00\x00\x00\x00',
+ b'\x01896630860000\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B45070\x00\x00\x00\x00\x00\x00',
+ b'8965B45080\x00\x00\x00\x00\x00\x00',
+ b'8965B45082\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152608130\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881510801100\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702200\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F0801100\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_CTH: {
+ (Ecu.dsu, 0x791, None): [
+ b'881517601100\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152676144\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0237635000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F7601100\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_ES_TSS2: {
+ (Ecu.engine, 0x700, None): [
+ b'\x018966306U6000\x00\x00\x00\x00',
+ b'\x018966333T5000\x00\x00\x00\x00',
+ b'\x018966333T5100\x00\x00\x00\x00',
+ b'\x018966333X6000\x00\x00\x00\x00',
+ b'\x01896633T07000\x00\x00\x00\x00',
+ b'\x01896633T38000\x00\x00\x00\x00',
+ b'\x01896633T58000\x00\x00\x00\x00',
+ b'\x028966333S8000\x00\x00\x00\x00897CF3302002\x00\x00\x00\x00',
+ b'\x028966333S8000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x028966333T0100\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x028966333V4000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x028966333W1000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
+ b'\x02896633T09000\x00\x00\x00\x00897CF3307001\x00\x00\x00\x00',
+ b'\x02896633T10000\x00\x00\x00\x00897CF3307001\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F152606281\x00\x00\x00\x00\x00\x00',
+ b'\x01F152606340\x00\x00\x00\x00\x00\x00',
+ b'\x01F152606461\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260646200\x00\x00\x00\x00',
+ b'F152633423\x00\x00\x00\x00\x00\x00',
+ b'F152633680\x00\x00\x00\x00\x00\x00',
+ b'F152633681\x00\x00\x00\x00\x00\x00',
+ b'F152633F50\x00\x00\x00\x00\x00\x00',
+ b'F152633F51\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B33252\x00\x00\x00\x00\x00\x00',
+ b'8965B33590\x00\x00\x00\x00\x00\x00',
+ b'8965B33690\x00\x00\x00\x00\x00\x00',
+ b'8965B33721\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F3301100\x00\x00\x00\x00',
+ b'\x018821F3301200\x00\x00\x00\x00',
+ b'\x018821F3301300\x00\x00\x00\x00',
+ b'\x018821F3301400\x00\x00\x00\x00',
+ b'\x018821F6201200\x00\x00\x00\x00',
+ b'\x018821F6201300\x00\x00\x00\x00',
+ b'\x018821F6201400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F0610000\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F33030D0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F3303100\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F3303200\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F3304100\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F3304200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F3304300\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
+ b'\x028646F3309100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F3309100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_ES: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\x02333M4100\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02333M4200\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02333R0000\x00\x00\x00\x00\x00\x00\x00\x00A0C01000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152606202\x00\x00\x00\x00\x00\x00',
+ b'F152633171\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881513309400\x00\x00\x00\x00',
+ b'881513309500\x00\x00\x00\x00',
+ b'881513310400\x00\x00\x00\x00',
+ b'881513310500\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B33502\x00\x00\x00\x00\x00\x00',
+ b'8965B33512\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4701100\x00\x00\x00\x00',
+ b'8821F4701200\x00\x00\x00\x00',
+ b'8821F4701300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F3302001\x00\x00\x00\x00',
+ b'8646F3302100\x00\x00\x00\x00',
+ b'8646F3302200\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_GS_F: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0233075200\x00\x00\x00\x00\x00\x00\x00\x00530B9000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152630700\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881513016200\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B30551\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702000\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F3002100\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_NX: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896637850000\x00\x00\x00\x00',
+ b'\x01896637851000\x00\x00\x00\x00',
+ b'\x01896637852000\x00\x00\x00\x00',
+ b'\x01896637854000\x00\x00\x00\x00',
+ b'\x01896637873000\x00\x00\x00\x00',
+ b'\x01896637878000\x00\x00\x00\x00',
+ b'\x01896637878100\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0237841000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0237842000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0237880000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0237882000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0237886000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152678130\x00\x00\x00\x00\x00\x00',
+ b'F152678140\x00\x00\x00\x00\x00\x00',
+ b'F152678160\x00\x00\x00\x00\x00\x00',
+ b'F152678170\x00\x00\x00\x00\x00\x00',
+ b'F152678171\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881517803100\x00\x00\x00\x00',
+ b'881517803300\x00\x00\x00\x00',
+ b'881517804100\x00\x00\x00\x00',
+ b'881517804300\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B78060\x00\x00\x00\x00\x00\x00',
+ b'8965B78080\x00\x00\x00\x00\x00\x00',
+ b'8965B78100\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702100\x00\x00\x00\x00',
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F7801100\x00\x00\x00\x00',
+ b'8646F7801300\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_NX_TSS2: {
+ (Ecu.engine, 0x700, None): [
+ b'\x018966378B2000\x00\x00\x00\x00',
+ b'\x018966378B2100\x00\x00\x00\x00',
+ b'\x018966378B3000\x00\x00\x00\x00',
+ b'\x018966378B3100\x00\x00\x00\x00',
+ b'\x018966378B4100\x00\x00\x00\x00',
+ b'\x018966378G2000\x00\x00\x00\x00',
+ b'\x018966378G3000\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0237881000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0237887000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02378A0000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02378F4000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F152678221\x00\x00\x00\x00\x00\x00',
+ b'F152678200\x00\x00\x00\x00\x00\x00',
+ b'F152678210\x00\x00\x00\x00\x00\x00',
+ b'F152678211\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B78110\x00\x00\x00\x00\x00\x00',
+ b'8965B78120\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F3301200\x00\x00\x00\x00',
+ b'\x018821F3301300\x00\x00\x00\x00',
+ b'\x018821F3301400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F78030A0\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F7803100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_LC_TSS2: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0131130000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152611390\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B11091\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F6201400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F1104200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ b'\x028646F1105200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_RC: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896632461100\x00\x00\x00\x00',
+ b'\x01896632478100\x00\x00\x00\x00',
+ b'\x01896632478200\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0232484000\x00\x00\x00\x00\x00\x00\x00\x0052422000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152624150\x00\x00\x00\x00\x00\x00',
+ b'F152624221\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881512404100\x00\x00\x00\x00',
+ b'881512407000\x00\x00\x00\x00',
+ b'881512409100\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B24081\x00\x00\x00\x00\x00\x00',
+ b'8965B24240\x00\x00\x00\x00\x00\x00',
+ b'8965B24320\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4702300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F2401100\x00\x00\x00\x00',
+ b'8646F2401200\x00\x00\x00\x00',
+ b'8646F2402200\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_RX: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896630E36100\x00\x00\x00\x00',
+ b'\x01896630E36200\x00\x00\x00\x00',
+ b'\x01896630E36300\x00\x00\x00\x00',
+ b'\x01896630E37100\x00\x00\x00\x00',
+ b'\x01896630E37200\x00\x00\x00\x00',
+ b'\x01896630E37300\x00\x00\x00\x00',
+ b'\x01896630E41000\x00\x00\x00\x00',
+ b'\x01896630E41100\x00\x00\x00\x00',
+ b'\x01896630E41200\x00\x00\x00\x00',
+ b'\x01896630E41500\x00\x00\x00\x00',
+ b'\x01896630EA3100\x00\x00\x00\x00',
+ b'\x01896630EA3300\x00\x00\x00\x00',
+ b'\x01896630EA3400\x00\x00\x00\x00',
+ b'\x01896630EA4100\x00\x00\x00\x00',
+ b'\x01896630EA4200\x00\x00\x00\x00',
+ b'\x01896630EA4300\x00\x00\x00\x00',
+ b'\x01896630EA4400\x00\x00\x00\x00',
+ b'\x01896630EA6300\x00\x00\x00\x00',
+ b'\x018966348R1300\x00\x00\x00\x00',
+ b'\x018966348R8500\x00\x00\x00\x00',
+ b'\x018966348R9300\x00\x00\x00\x00',
+ b'\x018966348W1300\x00\x00\x00\x00',
+ b'\x018966348W2300\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x02348J7000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348N0000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348Q4000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348Q4100\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348T1000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348T1100\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348T1200\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348T3000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348V6000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348Z3000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152648361\x00\x00\x00\x00\x00\x00',
+ b'F152648472\x00\x00\x00\x00\x00\x00',
+ b'F152648473\x00\x00\x00\x00\x00\x00',
+ b'F152648474\x00\x00\x00\x00\x00\x00',
+ b'F152648492\x00\x00\x00\x00\x00\x00',
+ b'F152648493\x00\x00\x00\x00\x00\x00',
+ b'F152648494\x00\x00\x00\x00\x00\x00',
+ b'F152648501\x00\x00\x00\x00\x00\x00',
+ b'F152648502\x00\x00\x00\x00\x00\x00',
+ b'F152648504\x00\x00\x00\x00\x00\x00',
+ b'F152648630\x00\x00\x00\x00\x00\x00',
+ b'F152648740\x00\x00\x00\x00\x00\x00',
+ b'F152648A30\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.dsu, 0x791, None): [
+ b'881514810300\x00\x00\x00\x00',
+ b'881514810500\x00\x00\x00\x00',
+ b'881514810700\x00\x00\x00\x00',
+ b'881514811300\x00\x00\x00\x00',
+ b'881514811500\x00\x00\x00\x00',
+ b'881514811700\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B0E011\x00\x00\x00\x00\x00\x00',
+ b'8965B0E012\x00\x00\x00\x00\x00\x00',
+ b'8965B48102\x00\x00\x00\x00\x00\x00',
+ b'8965B48111\x00\x00\x00\x00\x00\x00',
+ b'8965B48112\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'8821F4701000\x00\x00\x00\x00',
+ b'8821F4701100\x00\x00\x00\x00',
+ b'8821F4701200\x00\x00\x00\x00',
+ b'8821F4701300\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'8646F4801100\x00\x00\x00\x00',
+ b'8646F4801200\x00\x00\x00\x00',
+ b'8646F4802001\x00\x00\x00\x00',
+ b'8646F4802100\x00\x00\x00\x00',
+ b'8646F4802200\x00\x00\x00\x00',
+ b'8646F4809000\x00\x00\x00\x00',
+ ],
+ },
+ CAR.LEXUS_RX_TSS2: {
+ (Ecu.engine, 0x700, None): [
+ b'\x01896630EA9000\x00\x00\x00\x00',
+ b'\x01896630EB0000\x00\x00\x00\x00',
+ b'\x01896630EC9000\x00\x00\x00\x00',
+ b'\x01896630EC9100\x00\x00\x00\x00',
+ b'\x01896630ED0000\x00\x00\x00\x00',
+ b'\x01896630ED0100\x00\x00\x00\x00',
+ b'\x01896630ED5000\x00\x00\x00\x00',
+ b'\x01896630ED6000\x00\x00\x00\x00',
+ b'\x018966348R9200\x00\x00\x00\x00',
+ b'\x018966348T8000\x00\x00\x00\x00',
+ b'\x018966348W5100\x00\x00\x00\x00',
+ b'\x018966348W9000\x00\x00\x00\x00',
+ b'\x018966348X0000\x00\x00\x00\x00',
+ b'\x01896634D11000\x00\x00\x00\x00',
+ b'\x01896634D12000\x00\x00\x00\x00',
+ b'\x01896634D12100\x00\x00\x00\x00',
+ b'\x01896634D43000\x00\x00\x00\x00',
+ b'\x01896634D44000\x00\x00\x00\x00',
+ ],
+ (Ecu.engine, 0x7e0, None): [
+ b'\x02348U2000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348X4000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348X5000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348X8000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x02348Y3000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0234D14000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0234D16000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F15260E031\x00\x00\x00\x00\x00\x00',
+ b'\x01F15260E041\x00\x00\x00\x00\x00\x00',
+ b'\x01F152648781\x00\x00\x00\x00\x00\x00',
+ b'\x01F152648801\x00\x00\x00\x00\x00\x00',
+ b'F152648493\x00\x00\x00\x00\x00\x00',
+ b'F152648811\x00\x00\x00\x00\x00\x00',
+ b'F152648831\x00\x00\x00\x00\x00\x00',
+ b'F152648891\x00\x00\x00\x00\x00\x00',
+ b'F152648C80\x00\x00\x00\x00\x00\x00',
+ b'F152648D00\x00\x00\x00\x00\x00\x00',
+ b'F152648D60\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B48261\x00\x00\x00\x00\x00\x00',
+ b'8965B48271\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F3301100\x00\x00\x00\x00',
+ b'\x018821F3301300\x00\x00\x00\x00',
+ b'\x018821F3301400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F4810100\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
+ b'\x028646F4810200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F4810300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F4810400\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_PRIUS_TSS2: {
+ (Ecu.engine, 0x700, None): [
+ b'\x028966347B1000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966347C4000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966347C6000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966347C7000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x028966347C8000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
+ b'\x038966347C0000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4710101\x00\x00\x00\x00',
+ b'\x038966347C1000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4710101\x00\x00\x00\x00',
+ b'\x038966347C5000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4707101\x00\x00\x00\x00',
+ b'\x038966347C5100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4707101\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152647500\x00\x00\x00\x00\x00\x00',
+ b'F152647510\x00\x00\x00\x00\x00\x00',
+ b'F152647520\x00\x00\x00\x00\x00\x00',
+ b'F152647521\x00\x00\x00\x00\x00\x00',
+ b'F152647531\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B47070\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F3301400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F4707000\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646F4710000\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
+ b'\x028646F4712000\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_MIRAI: {
+ (Ecu.abs, 0x7d1, None): [
+ b'\x01898A36203000\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'\x01F15266203200\x00\x00\x00\x00',
+ b'\x01F15266203500\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'\x028965B6204100\x00\x00\x00\x008965B6203100\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F6201200\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F6201400\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
+ ],
+ },
+ CAR.TOYOTA_ALPHARD_TSS2: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\x0235870000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0235879000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
+ b'\x0235883000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.eps, 0x7a1, None): [
+ b'8965B58040\x00\x00\x00\x00\x00\x00',
+ b'8965B58052\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.abs, 0x7b0, None): [
+ b'F152658320\x00\x00\x00\x00\x00\x00',
+ b'F152658341\x00\x00\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdRadar, 0x750, 0xf): [
+ b'\x018821F3301200\x00\x00\x00\x00',
+ b'\x018821F3301400\x00\x00\x00\x00',
+ ],
+ (Ecu.fwdCamera, 0x750, 0x6d): [
+ b'\x028646F58010C0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
+ b'\x028646F5803200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ b'\x028646FV201000\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
+ ],
+ },
+}
diff --git a/opendbc/car/toyota/interface.py b/opendbc/car/toyota/interface.py
new file mode 100644
index 0000000000..6a09301ac6
--- /dev/null
+++ b/opendbc/car/toyota/interface.py
@@ -0,0 +1,142 @@
+from panda import Panda
+from panda import uds
+from opendbc.car import structs, get_safety_config
+from opendbc.car.toyota.values import Ecu, CAR, DBC, ToyotaFlags, CarControllerParams, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, \
+ MIN_ACC_SPEED, EPS_SCALE, UNSUPPORTED_DSU_CAR, NO_STOP_TIMER_CAR, ANGLE_CONTROL_CAR
+from opendbc.car.disable_ecu import disable_ecu
+from opendbc.car.interfaces import CarInterfaceBase
+
+SteerControlType = structs.CarParams.SteerControlType
+
+
+class CarInterface(CarInterfaceBase):
+ @staticmethod
+ def get_pid_accel_limits(CP, current_speed, cruise_speed):
+ return CarControllerParams.ACCEL_MIN, CarControllerParams.ACCEL_MAX
+
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "toyota"
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.toyota)]
+ ret.safetyConfigs[0].safetyParam = EPS_SCALE[candidate]
+
+ # BRAKE_MODULE is on a different address for these cars
+ if DBC[candidate]["pt"] == "toyota_new_mc_pt_generated":
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_ALT_BRAKE
+
+ if candidate in ANGLE_CONTROL_CAR:
+ ret.steerControlType = SteerControlType.angle
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_LTA
+
+ # LTA control can be more delayed and winds up more often
+ ret.steerActuatorDelay = 0.18
+ ret.steerLimitTimer = 0.8
+ else:
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+
+ ret.steerActuatorDelay = 0.12 # Default delay, Prius has larger delay
+ ret.steerLimitTimer = 0.4
+
+ ret.stoppingControl = False # Toyota starts braking more when it thinks you want to stop
+
+ stop_and_go = candidate in TSS2_CAR
+
+ # In TSS2 cars, the camera does long control
+ found_ecus = [fw.ecu for fw in car_fw]
+ ret.enableDsu = len(found_ecus) > 0 and Ecu.dsu not in found_ecus and candidate not in (NO_DSU_CAR | UNSUPPORTED_DSU_CAR)
+
+ if candidate == CAR.TOYOTA_PRIUS:
+ stop_and_go = True
+ # Only give steer angle deadzone to for bad angle sensor prius
+ for fw in car_fw:
+ if fw.ecu == "eps" and not fw.fwVersion == b'8965B47060\x00\x00\x00\x00\x00\x00':
+ ret.steerActuatorDelay = 0.25
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, steering_angle_deadzone_deg=0.2)
+
+ elif candidate in (CAR.LEXUS_RX, CAR.LEXUS_RX_TSS2):
+ stop_and_go = True
+ ret.wheelSpeedFactor = 1.035
+
+ elif candidate in (CAR.TOYOTA_AVALON, CAR.TOYOTA_AVALON_2019, CAR.TOYOTA_AVALON_TSS2):
+ # starting from 2019, all Avalon variants have stop and go
+ # https://engage.toyota.com/static/images/toyota_safety_sense/TSS_Applicability_Chart.pdf
+ stop_and_go = candidate != CAR.TOYOTA_AVALON
+
+ elif candidate in (CAR.TOYOTA_RAV4_TSS2, CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_TSS2_2023):
+ ret.lateralTuning.init('pid')
+ ret.lateralTuning.pid.kiBP = [0.0]
+ ret.lateralTuning.pid.kpBP = [0.0]
+ ret.lateralTuning.pid.kpV = [0.6]
+ ret.lateralTuning.pid.kiV = [0.1]
+ ret.lateralTuning.pid.kf = 0.00007818594
+
+ # 2019+ RAV4 TSS2 uses two different steering racks and specific tuning seems to be necessary.
+ # See https://github.com/commaai/openpilot/pull/21429#issuecomment-873652891
+ for fw in car_fw:
+ if fw.ecu == "eps" and (fw.fwVersion.startswith(b'\x02') or fw.fwVersion in [b'8965B42181\x00\x00\x00\x00\x00\x00']):
+ ret.lateralTuning.pid.kpV = [0.15]
+ ret.lateralTuning.pid.kiV = [0.05]
+ ret.lateralTuning.pid.kf = 0.00004
+ break
+
+ elif candidate in (CAR.TOYOTA_CHR, CAR.TOYOTA_CAMRY, CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_NX):
+ # TODO: Some of these platforms are not advertised to have full range ACC, are they similar to SNG_WITHOUT_DSU cars?
+ stop_and_go = True
+
+ # TODO: these models can do stop and go, but unclear if it requires sDSU or unplugging DSU.
+ # For now, don't list stop and go functionality in the docs
+ if ret.flags & ToyotaFlags.SNG_WITHOUT_DSU:
+ stop_and_go = stop_and_go or (ret.enableDsu and not docs)
+
+ ret.centerToFront = ret.wheelbase * 0.44
+
+ # TODO: Some TSS-P platforms have BSM, but are flipped based on region or driving direction.
+ # Detect flipped signals and enable for C-HR and others
+ ret.enableBsm = 0x3F6 in fingerprint[0] and candidate in TSS2_CAR
+
+ # No radar dbc for cars without DSU which are not TSS 2.0
+ # TODO: make an adas dbc file for dsu-less models
+ ret.radarUnavailable = DBC[candidate]['radar'] is None or candidate in (NO_DSU_CAR - TSS2_CAR)
+
+ # since we don't yet parse radar on TSS2/TSS-P radar-based ACC cars, gate longitudinal behind experimental toggle
+ if candidate in (RADAR_ACC_CAR | NO_DSU_CAR):
+ ret.experimentalLongitudinalAvailable = candidate in RADAR_ACC_CAR
+
+ # Disabling radar is only supported on TSS2 radar-ACC cars
+ if experimental_long and candidate in RADAR_ACC_CAR:
+ ret.flags |= ToyotaFlags.DISABLE_RADAR.value
+
+ # openpilot longitudinal enabled by default:
+ # - cars w/ DSU disconnected
+ # - TSS2 cars with camera sending ACC_CONTROL where we can block it
+ # openpilot longitudinal behind experimental long toggle:
+ # - TSS2 radar ACC cars (disables radar)
+ ret.openpilotLongitudinalControl = ret.enableDsu or candidate in (TSS2_CAR - RADAR_ACC_CAR) or bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value)
+ ret.autoResumeSng = ret.openpilotLongitudinalControl and candidate in NO_STOP_TIMER_CAR
+
+ if not ret.openpilotLongitudinalControl:
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL
+
+ # min speed to enable ACC. if car can do stop and go, then set enabling speed
+ # to a negative value, so it won't matter.
+ ret.minEnableSpeed = -1. if stop_and_go else MIN_ACC_SPEED
+
+ tune = ret.longitudinalTuning
+ if candidate in TSS2_CAR:
+ tune.kpV = [0.0]
+ tune.kiV = [0.5]
+ ret.vEgoStopping = 0.25
+ ret.vEgoStarting = 0.25
+ ret.stoppingDecelRate = 0.3 # reach stopping target smoothly
+ else:
+ tune.kiBP = [0., 5., 35.]
+ tune.kiV = [3.6, 2.4, 1.5]
+
+ return ret
+
+ @staticmethod
+ def init(CP, can_recv, can_send):
+ # disable radar if alpha longitudinal toggled on radar-ACC car
+ if CP.flags & ToyotaFlags.DISABLE_RADAR.value:
+ communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL])
+ disable_ecu(can_recv, can_send, bus=0, addr=0x750, sub_addr=0xf, com_cont_req=communication_control)
diff --git a/opendbc/car/toyota/radar_interface.py b/opendbc/car/toyota/radar_interface.py
new file mode 100755
index 0000000000..922dbac278
--- /dev/null
+++ b/opendbc/car/toyota/radar_interface.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+from opendbc.can.parser import CANParser
+from opendbc.car.structs import RadarData
+from opendbc.car.toyota.values import DBC, TSS2_CAR
+from opendbc.car.interfaces import RadarInterfaceBase
+
+
+def _create_radar_can_parser(car_fingerprint):
+ if car_fingerprint in TSS2_CAR:
+ RADAR_A_MSGS = list(range(0x180, 0x190))
+ RADAR_B_MSGS = list(range(0x190, 0x1a0))
+ else:
+ RADAR_A_MSGS = list(range(0x210, 0x220))
+ RADAR_B_MSGS = list(range(0x220, 0x230))
+
+ msg_a_n = len(RADAR_A_MSGS)
+ msg_b_n = len(RADAR_B_MSGS)
+ messages = list(zip(RADAR_A_MSGS + RADAR_B_MSGS, [20] * (msg_a_n + msg_b_n), strict=True))
+
+ return CANParser(DBC[car_fingerprint]['radar'], messages, 1)
+
+class RadarInterface(RadarInterfaceBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ self.track_id = 0
+ self.radar_ts = CP.radarTimeStep
+
+ if CP.carFingerprint in TSS2_CAR:
+ self.RADAR_A_MSGS = list(range(0x180, 0x190))
+ self.RADAR_B_MSGS = list(range(0x190, 0x1a0))
+ else:
+ self.RADAR_A_MSGS = list(range(0x210, 0x220))
+ self.RADAR_B_MSGS = list(range(0x220, 0x230))
+
+ self.valid_cnt = {key: 0 for key in self.RADAR_A_MSGS}
+
+ self.rcp = None if CP.radarUnavailable else _create_radar_can_parser(CP.carFingerprint)
+ self.trigger_msg = self.RADAR_B_MSGS[-1]
+ self.updated_messages = set()
+
+ def update(self, can_strings):
+ if self.rcp is None:
+ return super().update(None)
+
+ vls = self.rcp.update_strings(can_strings)
+ self.updated_messages.update(vls)
+
+ if self.trigger_msg not in self.updated_messages:
+ return None
+
+ rr = self._update(self.updated_messages)
+ self.updated_messages.clear()
+
+ return rr
+
+ def _update(self, updated_messages):
+ ret = RadarData()
+ errors = []
+ if not self.rcp.can_valid:
+ errors.append("canError")
+ ret.errors = errors
+
+ for ii in sorted(updated_messages):
+ if ii in self.RADAR_A_MSGS:
+ cpt = self.rcp.vl[ii]
+
+ if cpt['LONG_DIST'] >= 255 or cpt['NEW_TRACK']:
+ self.valid_cnt[ii] = 0 # reset counter
+ if cpt['VALID'] and cpt['LONG_DIST'] < 255:
+ self.valid_cnt[ii] += 1
+ else:
+ self.valid_cnt[ii] = max(self.valid_cnt[ii] - 1, 0)
+
+ score = self.rcp.vl[ii+16]['SCORE']
+ # print ii, self.valid_cnt[ii], score, cpt['VALID'], cpt['LONG_DIST'], cpt['LAT_DIST']
+
+ # radar point only valid if it's a valid measurement and score is above 50
+ if cpt['VALID'] or (score > 50 and cpt['LONG_DIST'] < 255 and self.valid_cnt[ii] > 0):
+ if ii not in self.pts or cpt['NEW_TRACK']:
+ self.pts[ii] = RadarData.RadarPoint()
+ self.pts[ii].trackId = self.track_id
+ self.track_id += 1
+ self.pts[ii].dRel = cpt['LONG_DIST'] # from front of car
+ self.pts[ii].yRel = -cpt['LAT_DIST'] # in car frame's y axis, left is positive
+ self.pts[ii].vRel = cpt['REL_SPEED']
+ self.pts[ii].aRel = float('nan')
+ self.pts[ii].yvRel = float('nan')
+ self.pts[ii].measured = bool(cpt['VALID'])
+ else:
+ if ii in self.pts:
+ del self.pts[ii]
+
+ ret.points = list(self.pts.values())
+ return ret
diff --git a/opendbc/car/toyota/tests/__init__.py b/opendbc/car/toyota/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/toyota/tests/print_platform_codes.py b/opendbc/car/toyota/tests/print_platform_codes.py
new file mode 100755
index 0000000000..333e7dca26
--- /dev/null
+++ b/opendbc/car/toyota/tests/print_platform_codes.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+from collections import defaultdict
+from opendbc.car.toyota.values import PLATFORM_CODE_ECUS, get_platform_codes
+from opendbc.car.toyota.fingerprints import FW_VERSIONS
+
+if __name__ == "__main__":
+ parts_for_ecu: dict = defaultdict(set)
+ cars_for_code: dict = defaultdict(lambda: defaultdict(set))
+ for car_model, ecus in FW_VERSIONS.items():
+ print()
+ print(car_model)
+ for ecu in sorted(ecus):
+ if ecu[0] not in PLATFORM_CODE_ECUS:
+ continue
+
+ platform_codes = get_platform_codes(ecus[ecu])
+ parts_for_ecu[ecu] |= {code.split(b'-')[0] for code in platform_codes if code.count(b'-') > 1}
+ for code in platform_codes:
+ cars_for_code[ecu][b'-'.join(code.split(b'-')[:2])] |= {car_model}
+ print(f' (Ecu.{ecu[0]}, {hex(ecu[1])}, {ecu[2]}):')
+ print(f' Codes: {platform_codes}')
+
+ print('\nECU parts:')
+ for ecu, parts in parts_for_ecu.items():
+ print(f' (Ecu.{ecu[0]}, {hex(ecu[1])}, {ecu[2]}): {parts}')
+
+ print('\nCar models vs. platform codes (no major versions):')
+ for ecu, codes in cars_for_code.items():
+ print(f' (Ecu.{ecu[0]}, {hex(ecu[1])}, {ecu[2]}):')
+ for code, cars in codes.items():
+ print(f' {code!r}: {sorted(cars)}')
diff --git a/opendbc/car/toyota/tests/test_toyota.py b/opendbc/car/toyota/tests/test_toyota.py
new file mode 100644
index 0000000000..94c6b7df3a
--- /dev/null
+++ b/opendbc/car/toyota/tests/test_toyota.py
@@ -0,0 +1,166 @@
+from hypothesis import given, settings, strategies as st
+
+from opendbc.car.structs import CarParams
+from opendbc.car.fw_versions import build_fw_dict
+from opendbc.car.toyota.fingerprints import FW_VERSIONS
+from opendbc.car.toyota.values import CAR, DBC, TSS2_CAR, ANGLE_CONTROL_CAR, RADAR_ACC_CAR, \
+ FW_QUERY_CONFIG, PLATFORM_CODE_ECUS, FUZZY_EXCLUDED_PLATFORMS, \
+ get_platform_codes
+
+Ecu = CarParams.Ecu
+
+
+def check_fw_version(fw_version: bytes) -> bool:
+ # TODO: just use the FW patterns, need to support all chunks
+ return b'?' not in fw_version and b'!' not in fw_version
+
+
+class TestToyotaInterfaces:
+ def test_car_sets(self):
+ assert len(ANGLE_CONTROL_CAR - TSS2_CAR) == 0
+ assert len(RADAR_ACC_CAR - TSS2_CAR) == 0
+
+ def test_lta_platforms(self):
+ # At this time, only RAV4 2023 is expected to use LTA/angle control
+ assert ANGLE_CONTROL_CAR == {CAR.TOYOTA_RAV4_TSS2_2023}
+
+ def test_tss2_dbc(self):
+ # We make some assumptions about TSS2 platforms,
+ # like looking up certain signals only in this DBC
+ for car_model, dbc in DBC.items():
+ if car_model in TSS2_CAR:
+ assert dbc["pt"] == "toyota_nodsu_pt_generated"
+
+ def test_essential_ecus(self, subtests):
+ # 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):
+ present_ecus = {ecu[0] for ecu in ecus}
+ missing_ecus = common_ecus - present_ecus
+ assert len(missing_ecus) == 0
+
+ # Some exceptions for other common ECUs
+ if car_model not in (CAR.TOYOTA_ALPHARD_TSS2,):
+ assert Ecu.abs in present_ecus
+
+ if car_model not in (CAR.TOYOTA_MIRAI,):
+ assert Ecu.engine in present_ecus
+
+ if car_model not in (CAR.TOYOTA_PRIUS_V, CAR.LEXUS_CTH):
+ assert Ecu.eps in present_ecus
+
+
+class TestToyotaFingerprint:
+ def test_non_essential_ecus(self, subtests):
+ # 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):
+ 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):
+ # Asserts all FW versions are valid
+ for car_model, ecus in FW_VERSIONS.items():
+ with subtests.test(car_model=car_model.value):
+ for fws in ecus.values():
+ for fw in fws:
+ assert check_fw_version(fw), fw
+
+ # Tests for part numbers, platform codes, and sub-versions which Toyota will use to fuzzy
+ # fingerprint in the absence of full FW matches:
+ @settings(max_examples=100)
+ @given(data=st.data())
+ def test_platform_codes_fuzzy_fw(self, data):
+ fw_strategy = st.lists(st.binary())
+ fws = data.draw(fw_strategy)
+ get_platform_codes(fws)
+
+ def test_platform_code_ecus_available(self, subtests):
+ # 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):
+ 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
+ if platform_code_ecu == Ecu.abs and car_model in (CAR.TOYOTA_ALPHARD_TSS2,):
+ continue
+ assert platform_code_ecu in [e[0] for e in ecus]
+
+ def test_fw_format(self, subtests):
+ # 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):
+ for ecu, fws in ecus.items():
+ if ecu[0] not in PLATFORM_CODE_ECUS:
+ continue
+
+ codes = dict()
+ for fw in fws:
+ result = get_platform_codes([fw])
+ # Check only one platform code and sub-version
+ assert 1 == len(result), f"Unable to parse FW: {fw}"
+ assert 1 == len(list(result.values())[0]), f"Unable to parse FW: {fw}"
+ codes |= result
+
+ # Toyota places the ECU part number in their FW versions, assert all parsable
+ # Note that there is only one unique part number per ECU across the fleet, so this
+ # is not important for identification, just a sanity check.
+ assert all(code.count(b"-") > 1 for code in codes), f"FW does not have part number: {fw} {codes}"
+
+ def test_platform_codes_spot_check(self):
+ # Asserts basic platform code parsing behavior for a few cases
+ results = get_platform_codes([
+ b"F152607140\x00\x00\x00\x00\x00\x00",
+ b"F152607171\x00\x00\x00\x00\x00\x00",
+ b"F152607110\x00\x00\x00\x00\x00\x00",
+ b"F152607180\x00\x00\x00\x00\x00\x00",
+ ])
+ assert results == {b"F1526-07-1": {b"10", b"40", b"71", b"80"}}
+
+ results = get_platform_codes([
+ b"\x028646F4104100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00",
+ b"\x028646F4104100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00",
+ ])
+ assert results == {b"8646F-41-04": {b"100"}}
+
+ # Short version has no part number
+ results = get_platform_codes([
+ b"\x0235870000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00",
+ b"\x0235883000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00",
+ ])
+ assert results == {b"58-70": {b"000"}, b"58-83": {b"000"}}
+
+ results = get_platform_codes([
+ b"F152607110\x00\x00\x00\x00\x00\x00",
+ b"F152607140\x00\x00\x00\x00\x00\x00",
+ b"\x028646F4104100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00",
+ b"\x0235879000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00",
+ ])
+ assert results == {b"F1526-07-1": {b"10", b"40"}, b"8646F-41-04": {b"100"}, b"58-79": {b"000"}}
+
+ def test_fuzzy_excluded_platforms(self):
+ # Asserts a list of platforms that will not fuzzy fingerprint with platform codes due to them being shared.
+ platforms_with_shared_codes = set()
+ for platform, fw_by_addr in FW_VERSIONS.items():
+ car_fw = []
+ for ecu, fw_versions in fw_by_addr.items():
+ ecu_name, addr, sub_addr = ecu
+ for fw in fw_versions:
+ car_fw.append(CarParams.CarFw(ecu=ecu_name, fwVersion=fw, address=addr,
+ subAddress=0 if sub_addr is None else sub_addr))
+
+ CP = CarParams(carFw=car_fw)
+ matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
+ if len(matches) == 1:
+ assert list(matches)[0] == platform
+ else:
+ # If a platform has multiple matches, add it and its matches
+ platforms_with_shared_codes |= {str(platform), *matches}
+
+ assert platforms_with_shared_codes == FUZZY_EXCLUDED_PLATFORMS, (len(platforms_with_shared_codes), len(FW_VERSIONS))
diff --git a/opendbc/car/toyota/toyotacan.py b/opendbc/car/toyota/toyotacan.py
new file mode 100644
index 0000000000..3d97a00db7
--- /dev/null
+++ b/opendbc/car/toyota/toyotacan.py
@@ -0,0 +1,118 @@
+from opendbc.car.structs import CarParams
+
+SteerControlType = CarParams.SteerControlType
+
+
+def create_steer_command(packer, steer, steer_req):
+ """Creates a CAN message for the Toyota Steer Command."""
+
+ values = {
+ "STEER_REQUEST": steer_req,
+ "STEER_TORQUE_CMD": steer,
+ "SET_ME_1": 1,
+ }
+ return packer.make_can_msg("STEERING_LKA", 0, values)
+
+
+def create_lta_steer_command(packer, steer_control_type, steer_angle, steer_req, frame, torque_wind_down):
+ """Creates a CAN message for the Toyota LTA Steer Command."""
+
+ values = {
+ "COUNTER": frame + 128,
+ "SETME_X1": 1, # suspected LTA feature availability
+ # 1 for TSS 2.5 cars, 3 for TSS 2.0. Send based on whether we're using LTA for lateral control
+ "SETME_X3": 1 if steer_control_type == SteerControlType.angle else 3,
+ "PERCENTAGE": 100,
+ "TORQUE_WIND_DOWN": torque_wind_down,
+ "ANGLE": 0,
+ "STEER_ANGLE_CMD": steer_angle,
+ "STEER_REQUEST": steer_req,
+ "STEER_REQUEST_2": steer_req,
+ "CLEAR_HOLD_STEERING_ALERT": 0,
+ }
+ return packer.make_can_msg("STEERING_LTA", 0, values)
+
+
+def create_accel_command(packer, accel, pcm_cancel, standstill_req, lead, acc_type, fcw_alert, distance):
+ # TODO: find the exact canceling bit that does not create a chime
+ values = {
+ "ACCEL_CMD": accel,
+ "ACC_TYPE": acc_type,
+ "DISTANCE": distance,
+ "MINI_CAR": lead,
+ "PERMIT_BRAKING": 1,
+ "RELEASE_STANDSTILL": not standstill_req,
+ "CANCEL_REQ": pcm_cancel,
+ "ALLOW_LONG_PRESS": 1,
+ "ACC_CUT_IN": fcw_alert, # only shown when ACC enabled
+ }
+ return packer.make_can_msg("ACC_CONTROL", 0, values)
+
+
+def create_acc_cancel_command(packer):
+ values = {
+ "GAS_RELEASED": 0,
+ "CRUISE_ACTIVE": 0,
+ "ACC_BRAKING": 0,
+ "ACCEL_NET": 0,
+ "CRUISE_STATE": 0,
+ "CANCEL_REQ": 1,
+ }
+ return packer.make_can_msg("PCM_CRUISE", 0, values)
+
+
+def create_fcw_command(packer, fcw):
+ values = {
+ "PCS_INDICATOR": 1, # PCS turned off
+ "FCW": fcw,
+ "SET_ME_X20": 0x20,
+ "SET_ME_X10": 0x10,
+ "PCS_OFF": 1,
+ "PCS_SENSITIVITY": 0,
+ }
+ return packer.make_can_msg("PCS_HUD", 0, values)
+
+
+def create_ui_command(packer, steer, chime, left_line, right_line, left_lane_depart, right_lane_depart, enabled, stock_lkas_hud):
+ values = {
+ "TWO_BEEPS": chime,
+ "LDA_ALERT": steer,
+ "RIGHT_LINE": 3 if right_lane_depart else 1 if right_line else 2,
+ "LEFT_LINE": 3 if left_lane_depart else 1 if left_line else 2,
+ "BARRIERS": 1 if enabled else 0,
+
+ # static signals
+ "SET_ME_X02": 2,
+ "SET_ME_X01": 1,
+ "LKAS_STATUS": 1,
+ "REPEATED_BEEPS": 0,
+ "LANE_SWAY_FLD": 7,
+ "LANE_SWAY_BUZZER": 0,
+ "LANE_SWAY_WARNING": 0,
+ "LDA_FRONT_CAMERA_BLOCKED": 0,
+ "TAKE_CONTROL": 0,
+ "LANE_SWAY_SENSITIVITY": 2,
+ "LANE_SWAY_TOGGLE": 1,
+ "LDA_ON_MESSAGE": 0,
+ "LDA_MESSAGES": 0,
+ "LDA_SA_TOGGLE": 1,
+ "LDA_SENSITIVITY": 2,
+ "LDA_UNAVAILABLE": 0,
+ "LDA_MALFUNCTION": 0,
+ "LDA_UNAVAILABLE_QUIET": 0,
+ "ADJUSTING_CAMERA": 0,
+ "LDW_EXIST": 1,
+ }
+
+ # lane sway functionality
+ # not all cars have LKAS_HUD — update with camera values if available
+ if len(stock_lkas_hud):
+ values.update({s: stock_lkas_hud[s] for s in [
+ "LANE_SWAY_FLD",
+ "LANE_SWAY_BUZZER",
+ "LANE_SWAY_WARNING",
+ "LANE_SWAY_SENSITIVITY",
+ "LANE_SWAY_TOGGLE",
+ ]})
+
+ return packer.make_can_msg("LKAS_HUD", 0, values)
diff --git a/opendbc/car/toyota/values.py b/opendbc/car/toyota/values.py
new file mode 100644
index 0000000000..89acaf29ed
--- /dev/null
+++ b/opendbc/car/toyota/values.py
@@ -0,0 +1,572 @@
+import re
+from collections import defaultdict
+from dataclasses import dataclass, field
+from enum import Enum, IntFlag
+
+from opendbc.car import CarSpecs, PlatformConfig, Platforms, AngleRateLimit, dbc_dict
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.structs import CarParams
+from opendbc.car.docs_definitions import CarFootnote, CarDocs, Column, CarParts, CarHarness
+from opendbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
+
+Ecu = CarParams.Ecu
+MIN_ACC_SPEED = 19. * CV.MPH_TO_MS
+PEDAL_TRANSITION = 10. * CV.MPH_TO_MS
+
+
+class CarControllerParams:
+ ACCEL_MAX = 1.5 # m/s2, lower than allowed 2.0 m/s2 for tuning reasons
+ ACCEL_MIN = -3.5 # m/s2
+
+ STEER_STEP = 1
+ STEER_MAX = 1500
+ STEER_ERROR_MAX = 350 # max delta between torque cmd and torque motor
+
+ # Lane Tracing Assist (LTA) control limits
+ # Assuming a steering ratio of 13.7:
+ # Limit to ~2.0 m/s^3 up (7.5 deg/s), ~3.5 m/s^3 down (13 deg/s) at 75 mph
+ # Worst case, the low speed limits will allow ~4.0 m/s^3 up (15 deg/s) and ~4.9 m/s^3 down (18 deg/s) at 75 mph,
+ # however the EPS has its own internal limits at all speeds which are less than that:
+ # Observed internal torque rate limit on TSS 2.5 Camry and RAV4 is ~1500 units/sec up and down when using LTA
+ ANGLE_RATE_LIMIT_UP = AngleRateLimit(speed_bp=[5, 25], angle_v=[0.3, 0.15])
+ ANGLE_RATE_LIMIT_DOWN = AngleRateLimit(speed_bp=[5, 25], angle_v=[0.36, 0.26])
+
+ def __init__(self, CP):
+ if CP.lateralTuning.which() == 'torque':
+ self.STEER_DELTA_UP = 15 # 1.0s time to peak torque
+ self.STEER_DELTA_DOWN = 25 # always lower than 45 otherwise the Rav4 faults (Prius seems ok with 50)
+ else:
+ self.STEER_DELTA_UP = 10 # 1.5s time to peak torque
+ self.STEER_DELTA_DOWN = 25 # always lower than 45 otherwise the Rav4 faults (Prius seems ok with 50)
+
+
+class ToyotaFlags(IntFlag):
+ # Detected flags
+ HYBRID = 1
+ DISABLE_RADAR = 4
+
+ # Static flags
+ TSS2 = 8
+ NO_DSU = 16
+ UNSUPPORTED_DSU = 32
+ RADAR_ACC = 64
+ # these cars use the Lane Tracing Assist (LTA) message for lateral control
+ ANGLE_CONTROL = 128
+ NO_STOP_TIMER = 256
+ # these cars are speculated to allow stop and go when the DSU is unplugged
+ SNG_WITHOUT_DSU = 512
+
+
+class Footnote(Enum):
+ CAMRY = CarFootnote(
+ "openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.",
+ Column.FSR_LONGITUDINAL)
+
+
+@dataclass
+class ToyotaCarDocs(CarDocs):
+ package: str = "All"
+ car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.toyota_a]))
+
+
+@dataclass
+class ToyotaTSS2PlatformConfig(PlatformConfig):
+ dbc_dict: dict = field(default_factory=lambda: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'))
+
+ def init(self):
+ self.flags |= ToyotaFlags.TSS2 | ToyotaFlags.NO_STOP_TIMER | ToyotaFlags.NO_DSU
+
+ if self.flags & ToyotaFlags.RADAR_ACC:
+ self.dbc_dict = dbc_dict('toyota_nodsu_pt_generated', None)
+
+
+class CAR(Platforms):
+ # Toyota
+ TOYOTA_ALPHARD_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Alphard 2019-20"),
+ ToyotaCarDocs("Toyota Alphard Hybrid 2021"),
+ ],
+ CarSpecs(mass=4305. * CV.LB_TO_KG, wheelbase=3.0, steerRatio=14.2, tireStiffnessFactor=0.444),
+ )
+ TOYOTA_AVALON = PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Avalon 2016", "Toyota Safety Sense P"),
+ ToyotaCarDocs("Toyota Avalon 2017-18"),
+ ],
+ CarSpecs(mass=3505. * CV.LB_TO_KG, wheelbase=2.82, steerRatio=14.8, tireStiffnessFactor=0.7983),
+ dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
+ )
+ TOYOTA_AVALON_2019 = PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Avalon 2019-21"),
+ ToyotaCarDocs("Toyota Avalon Hybrid 2019-21"),
+ ],
+ TOYOTA_AVALON.specs,
+ dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'),
+ )
+ TOYOTA_AVALON_TSS2 = ToyotaTSS2PlatformConfig( # TSS 2.5
+ [
+ ToyotaCarDocs("Toyota Avalon 2022"),
+ ToyotaCarDocs("Toyota Avalon Hybrid 2022"),
+ ],
+ TOYOTA_AVALON.specs,
+ )
+ TOYOTA_CAMRY = PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Camry 2018-20", video_link="https://www.youtube.com/watch?v=fkcjviZY9CM", footnotes=[Footnote.CAMRY]),
+ ToyotaCarDocs("Toyota Camry Hybrid 2018-20", video_link="https://www.youtube.com/watch?v=Q2DYY0AWKgk"),
+ ],
+ CarSpecs(mass=3400. * CV.LB_TO_KG, wheelbase=2.82448, steerRatio=13.7, tireStiffnessFactor=0.7933),
+ dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'),
+ flags=ToyotaFlags.NO_DSU,
+ )
+ TOYOTA_CAMRY_TSS2 = ToyotaTSS2PlatformConfig( # TSS 2.5
+ [
+ ToyotaCarDocs("Toyota Camry 2021-24", footnotes=[Footnote.CAMRY]),
+ ToyotaCarDocs("Toyota Camry Hybrid 2021-24"),
+ ],
+ TOYOTA_CAMRY.specs,
+ )
+ TOYOTA_CHR = PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota C-HR 2017-20"),
+ ToyotaCarDocs("Toyota C-HR Hybrid 2017-20"),
+ ],
+ CarSpecs(mass=3300. * CV.LB_TO_KG, wheelbase=2.63906, steerRatio=13.6, tireStiffnessFactor=0.7933),
+ dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'),
+ flags=ToyotaFlags.NO_DSU,
+ )
+ TOYOTA_CHR_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota C-HR 2021"),
+ ToyotaCarDocs("Toyota C-HR Hybrid 2021-22"),
+ ],
+ TOYOTA_CHR.specs,
+ flags=ToyotaFlags.RADAR_ACC,
+ )
+ TOYOTA_COROLLA = PlatformConfig(
+ [ToyotaCarDocs("Toyota Corolla 2017-19")],
+ CarSpecs(mass=2860. * CV.LB_TO_KG, wheelbase=2.7, steerRatio=18.27, tireStiffnessFactor=0.444),
+ dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'),
+ )
+ # LSS2 Lexus UX Hybrid is same as a TSS2 Corolla Hybrid
+ TOYOTA_COROLLA_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Corolla 2020-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"),
+ ToyotaCarDocs("Toyota Corolla Cross (Non-US only) 2020-23", min_enable_speed=7.5),
+ ToyotaCarDocs("Toyota Corolla Hatchback 2019-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"),
+ # Hybrid platforms
+ ToyotaCarDocs("Toyota Corolla Hybrid 2020-22"),
+ ToyotaCarDocs("Toyota Corolla Hybrid (South America only) 2020-23", min_enable_speed=7.5),
+ ToyotaCarDocs("Toyota Corolla Cross Hybrid (Non-US only) 2020-22", min_enable_speed=7.5),
+ ToyotaCarDocs("Lexus UX Hybrid 2019-23"),
+ ],
+ CarSpecs(mass=3060. * CV.LB_TO_KG, wheelbase=2.67, steerRatio=13.9, tireStiffnessFactor=0.444),
+ )
+ TOYOTA_HIGHLANDER = PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Highlander 2017-19", video_link="https://www.youtube.com/watch?v=0wS0wXSLzoo"),
+ ToyotaCarDocs("Toyota Highlander Hybrid 2017-19"),
+ ],
+ CarSpecs(mass=4516. * CV.LB_TO_KG, wheelbase=2.8194, steerRatio=16.0, tireStiffnessFactor=0.8),
+ dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
+ flags=ToyotaFlags.NO_STOP_TIMER | ToyotaFlags.SNG_WITHOUT_DSU,
+ )
+ TOYOTA_HIGHLANDER_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Highlander 2020-23"),
+ ToyotaCarDocs("Toyota Highlander Hybrid 2020-23"),
+ ],
+ TOYOTA_HIGHLANDER.specs,
+ )
+ TOYOTA_PRIUS = PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Prius 2016", "Toyota Safety Sense P", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0"),
+ ToyotaCarDocs("Toyota Prius 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0"),
+ ToyotaCarDocs("Toyota Prius Prime 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0"),
+ ],
+ CarSpecs(mass=3045. * CV.LB_TO_KG, wheelbase=2.7, steerRatio=15.74, tireStiffnessFactor=0.6371),
+ dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'),
+ )
+ TOYOTA_PRIUS_V = PlatformConfig(
+ [ToyotaCarDocs("Toyota Prius v 2017", "Toyota Safety Sense P", min_enable_speed=MIN_ACC_SPEED)],
+ CarSpecs(mass=3340. * CV.LB_TO_KG, wheelbase=2.78, steerRatio=17.4, tireStiffnessFactor=0.5533),
+ dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'),
+ flags=ToyotaFlags.NO_STOP_TIMER | ToyotaFlags.SNG_WITHOUT_DSU,
+ )
+ TOYOTA_PRIUS_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota Prius 2021-22", video_link="https://www.youtube.com/watch?v=J58TvCpUd4U"),
+ ToyotaCarDocs("Toyota Prius Prime 2021-22", video_link="https://www.youtube.com/watch?v=J58TvCpUd4U"),
+ ],
+ CarSpecs(mass=3115. * CV.LB_TO_KG, wheelbase=2.70002, steerRatio=13.4, tireStiffnessFactor=0.6371),
+ )
+ TOYOTA_RAV4 = PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota RAV4 2016", "Toyota Safety Sense P"),
+ ToyotaCarDocs("Toyota RAV4 2017-18")
+ ],
+ CarSpecs(mass=3650. * CV.LB_TO_KG, wheelbase=2.65, steerRatio=16.88, tireStiffnessFactor=0.5533),
+ dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'),
+ )
+ TOYOTA_RAV4H = PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota RAV4 Hybrid 2016", "Toyota Safety Sense P", video_link="https://youtu.be/LhT5VzJVfNI?t=26"),
+ ToyotaCarDocs("Toyota RAV4 Hybrid 2017-18", video_link="https://youtu.be/LhT5VzJVfNI?t=26")
+ ],
+ TOYOTA_RAV4.specs,
+ dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
+ # Note that the ICE RAV4 does not respect positive acceleration commands under 19 mph
+ flags=ToyotaFlags.NO_STOP_TIMER | ToyotaFlags.SNG_WITHOUT_DSU,
+ )
+ TOYOTA_RAV4_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota RAV4 2019-21", video_link="https://www.youtube.com/watch?v=wJxjDd42gGA"),
+ ToyotaCarDocs("Toyota RAV4 Hybrid 2019-21"),
+ ],
+ CarSpecs(mass=3585. * CV.LB_TO_KG, wheelbase=2.68986, steerRatio=14.3, tireStiffnessFactor=0.7933),
+ )
+ TOYOTA_RAV4_TSS2_2022 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota RAV4 2022"),
+ ToyotaCarDocs("Toyota RAV4 Hybrid 2022", video_link="https://youtu.be/U0nH9cnrFB0"),
+ ],
+ TOYOTA_RAV4_TSS2.specs,
+ flags=ToyotaFlags.RADAR_ACC,
+ )
+ TOYOTA_RAV4_TSS2_2023 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Toyota RAV4 2023-24"),
+ ToyotaCarDocs("Toyota RAV4 Hybrid 2023-24"),
+ ],
+ TOYOTA_RAV4_TSS2.specs,
+ flags=ToyotaFlags.RADAR_ACC | ToyotaFlags.ANGLE_CONTROL,
+ )
+ TOYOTA_MIRAI = ToyotaTSS2PlatformConfig( # TSS 2.5
+ [ToyotaCarDocs("Toyota Mirai 2021")],
+ CarSpecs(mass=4300. * CV.LB_TO_KG, wheelbase=2.91, steerRatio=14.8, tireStiffnessFactor=0.8),
+ )
+ TOYOTA_SIENNA = PlatformConfig(
+ [ToyotaCarDocs("Toyota Sienna 2018-20", video_link="https://www.youtube.com/watch?v=q1UPOo4Sh68", min_enable_speed=MIN_ACC_SPEED)],
+ CarSpecs(mass=4590. * CV.LB_TO_KG, wheelbase=3.03, steerRatio=15.5, tireStiffnessFactor=0.444),
+ dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
+ flags=ToyotaFlags.NO_STOP_TIMER,
+ )
+
+ # Lexus
+ LEXUS_CTH = PlatformConfig(
+ [ToyotaCarDocs("Lexus CT Hybrid 2017-18", "Lexus Safety System+")],
+ CarSpecs(mass=3108. * CV.LB_TO_KG, wheelbase=2.6, steerRatio=18.6, tireStiffnessFactor=0.517),
+ dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'),
+ )
+ LEXUS_ES = PlatformConfig(
+ [
+ ToyotaCarDocs("Lexus ES 2017-18"),
+ ToyotaCarDocs("Lexus ES Hybrid 2017-18"),
+ ],
+ CarSpecs(mass=3677. * CV.LB_TO_KG, wheelbase=2.8702, steerRatio=16.0, tireStiffnessFactor=0.444),
+ dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'),
+ )
+ LEXUS_ES_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Lexus ES 2019-24"),
+ ToyotaCarDocs("Lexus ES Hybrid 2019-24", video_link="https://youtu.be/BZ29osRVJeg?t=12"),
+ ],
+ LEXUS_ES.specs,
+ )
+ LEXUS_IS = PlatformConfig(
+ [ToyotaCarDocs("Lexus IS 2017-19")],
+ CarSpecs(mass=3736.8 * CV.LB_TO_KG, wheelbase=2.79908, steerRatio=13.3, tireStiffnessFactor=0.444),
+ dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
+ flags=ToyotaFlags.UNSUPPORTED_DSU,
+ )
+ LEXUS_IS_TSS2 = ToyotaTSS2PlatformConfig(
+ [ToyotaCarDocs("Lexus IS 2022-23")],
+ LEXUS_IS.specs,
+ )
+ LEXUS_NX = PlatformConfig(
+ [
+ ToyotaCarDocs("Lexus NX 2018-19"),
+ ToyotaCarDocs("Lexus NX Hybrid 2018-19"),
+ ],
+ CarSpecs(mass=4070. * CV.LB_TO_KG, wheelbase=2.66, steerRatio=14.7, tireStiffnessFactor=0.444),
+ dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
+ )
+ LEXUS_NX_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Lexus NX 2020-21"),
+ ToyotaCarDocs("Lexus NX Hybrid 2020-21"),
+ ],
+ LEXUS_NX.specs,
+ )
+ LEXUS_LC_TSS2 = ToyotaTSS2PlatformConfig(
+ [ToyotaCarDocs("Lexus LC 2024")],
+ CarSpecs(mass=4500. * CV.LB_TO_KG, wheelbase=2.87, steerRatio=13.0, tireStiffnessFactor=0.444),
+ )
+ LEXUS_RC = PlatformConfig(
+ [ToyotaCarDocs("Lexus RC 2018-20")],
+ LEXUS_IS.specs,
+ dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
+ flags=ToyotaFlags.UNSUPPORTED_DSU,
+ )
+ LEXUS_RX = PlatformConfig(
+ [
+ ToyotaCarDocs("Lexus RX 2016", "Lexus Safety System+"),
+ ToyotaCarDocs("Lexus RX 2017-19"),
+ # Hybrid platforms
+ ToyotaCarDocs("Lexus RX Hybrid 2016", "Lexus Safety System+"),
+ ToyotaCarDocs("Lexus RX Hybrid 2017-19"),
+ ],
+ CarSpecs(mass=4481. * CV.LB_TO_KG, wheelbase=2.79, steerRatio=16., tireStiffnessFactor=0.5533),
+ dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
+ )
+ LEXUS_RX_TSS2 = ToyotaTSS2PlatformConfig(
+ [
+ ToyotaCarDocs("Lexus RX 2020-22"),
+ ToyotaCarDocs("Lexus RX Hybrid 2020-22"),
+ ],
+ LEXUS_RX.specs,
+ )
+ LEXUS_GS_F = PlatformConfig(
+ [ToyotaCarDocs("Lexus GS F 2016")],
+ CarSpecs(mass=4034. * CV.LB_TO_KG, wheelbase=2.84988, steerRatio=13.3, tireStiffnessFactor=0.444),
+ dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'),
+ flags=ToyotaFlags.UNSUPPORTED_DSU,
+ )
+
+
+# (addr, cars, bus, 1/freq*100, vl)
+STATIC_DSU_MSGS = [
+ (0x128, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX, CAR.LEXUS_NX, CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_AVALON), \
+ 1, 3, b'\xf4\x01\x90\x83\x00\x37'),
+ (0x128, (CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES), 1, 3, b'\x03\x00\x20\x00\x00\x52'),
+ (0x141, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX, CAR.LEXUS_NX, CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_AVALON,
+ CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.TOYOTA_PRIUS_V), 1, 2, b'\x00\x00\x00\x46'),
+ (0x160, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX, CAR.LEXUS_NX, CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_AVALON,
+ CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.TOYOTA_PRIUS_V), 1, 7, b'\x00\x00\x08\x12\x01\x31\x9c\x51'),
+ (0x161, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX, CAR.LEXUS_NX, CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_AVALON, CAR.TOYOTA_PRIUS_V),
+ 1, 7, b'\x00\x1e\x00\x00\x00\x80\x07'),
+ (0X161, (CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES), 1, 7, b'\x00\x1e\x00\xd4\x00\x00\x5b'),
+ (0x283, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX, CAR.LEXUS_NX, CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_AVALON,
+ CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.TOYOTA_PRIUS_V), 0, 3, b'\x00\x00\x00\x00\x00\x00\x8c'),
+ (0x2E6, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX), 0, 3, b'\xff\xf8\x00\x08\x7f\xe0\x00\x4e'),
+ (0x2E7, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX), 0, 3, b'\xa8\x9c\x31\x9c\x00\x00\x00\x02'),
+ (0x33E, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX), 0, 20, b'\x0f\xff\x26\x40\x00\x1f\x00'),
+ (0x344, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX, CAR.LEXUS_NX, CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_AVALON,
+ CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.TOYOTA_PRIUS_V), 0, 5, b'\x00\x00\x01\x00\x00\x00\x00\x50'),
+ (0x365, (CAR.TOYOTA_PRIUS, CAR.LEXUS_NX, CAR.TOYOTA_HIGHLANDER), 0, 20, b'\x00\x00\x00\x80\x03\x00\x08'),
+ (0x365, (CAR.TOYOTA_RAV4, CAR.TOYOTA_RAV4H, CAR.TOYOTA_COROLLA, CAR.TOYOTA_AVALON, CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.LEXUS_RX,
+ CAR.TOYOTA_PRIUS_V), 0, 20, b'\x00\x00\x00\x80\xfc\x00\x08'),
+ (0x366, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX, CAR.LEXUS_NX, CAR.TOYOTA_HIGHLANDER), 0, 20, b'\x00\x00\x4d\x82\x40\x02\x00'),
+ (0x366, (CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_AVALON, CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.TOYOTA_PRIUS_V),
+ 0, 20, b'\x00\x72\x07\xff\x09\xfe\x00'),
+ (0x470, (CAR.TOYOTA_PRIUS, CAR.LEXUS_RX), 1, 100, b'\x00\x00\x02\x7a'),
+ (0x470, (CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_RAV4H, CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.TOYOTA_PRIUS_V), 1, 100, b'\x00\x00\x01\x79'),
+ (0x4CB, (CAR.TOYOTA_PRIUS, CAR.TOYOTA_RAV4H, CAR.LEXUS_RX, CAR.LEXUS_NX, CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_AVALON,
+ CAR.TOYOTA_SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_ES, CAR.TOYOTA_PRIUS_V), 0, 100, b'\x0c\x00\x00\x00\x00\x00\x00\x00'),
+]
+
+
+def get_platform_codes(fw_versions: list[bytes]) -> dict[bytes, set[bytes]]:
+ # Returns sub versions in a dict so comparisons can be made within part-platform-major_version combos
+ codes = defaultdict(set) # Optional[part]-platform-major_version: set of sub_version
+ for fw in fw_versions:
+ # FW versions returned from UDS queries can return multiple fields/chunks of data (different ECU calibrations, different data?)
+ # and are prefixed with a byte that describes how many chunks of data there are.
+ # But FW returned from KWP requires querying of each sub-data id and does not have a length prefix.
+
+ length_code = 1
+ length_code_match = FW_LEN_CODE.search(fw)
+ if length_code_match is not None:
+ length_code = length_code_match.group()[0]
+ fw = fw[1:]
+
+ # fw length should be multiple of 16 bytes (per chunk, even if no length code), skip parsing if unexpected length
+ if length_code * FW_CHUNK_LEN != len(fw):
+ continue
+
+ chunks = [fw[FW_CHUNK_LEN * i:FW_CHUNK_LEN * i + FW_CHUNK_LEN].strip(b'\x00 ') for i in range(length_code)]
+
+ # only first is considered for now since second is commonly shared (TODO: understand that)
+ first_chunk = chunks[0]
+ if len(first_chunk) == 8:
+ # TODO: no part number, but some short chunks have it in subsequent chunks
+ fw_match = SHORT_FW_PATTERN.search(first_chunk)
+ if fw_match is not None:
+ platform, major_version, sub_version = fw_match.groups()
+ codes[b'-'.join((platform, major_version))].add(sub_version)
+
+ elif len(first_chunk) == 10:
+ fw_match = MEDIUM_FW_PATTERN.search(first_chunk)
+ if fw_match is not None:
+ part, platform, major_version, sub_version = fw_match.groups()
+ codes[b'-'.join((part, platform, major_version))].add(sub_version)
+
+ elif len(first_chunk) == 12:
+ fw_match = LONG_FW_PATTERN.search(first_chunk)
+ if fw_match is not None:
+ part, platform, major_version, sub_version = fw_match.groups()
+ codes[b'-'.join((part, platform, major_version))].add(sub_version)
+
+ return dict(codes)
+
+
+def match_fw_to_car_fuzzy(live_fw_versions, vin, offline_fw_versions) -> set[str]:
+ candidates = set()
+
+ for candidate, fws in offline_fw_versions.items():
+ # Keep track of ECUs which pass all checks (platform codes, within sub-version range)
+ valid_found_ecus = set()
+ valid_expected_ecus = {ecu[1:] for ecu in fws if ecu[0] in PLATFORM_CODE_ECUS}
+ for ecu, expected_versions in fws.items():
+ addr = ecu[1:]
+ # Only check ECUs expected to have platform codes
+ if ecu[0] not in PLATFORM_CODE_ECUS:
+ continue
+
+ # Expected platform codes & versions
+ expected_platform_codes = get_platform_codes(expected_versions)
+
+ # Found platform codes & versions
+ found_platform_codes = get_platform_codes(live_fw_versions.get(addr, set()))
+
+ # Check part number + platform code + major version matches for any found versions
+ # Platform codes and major versions change for different physical parts, generation, API, etc.
+ # Sub-versions are incremented for minor recalls, do not need to be checked.
+ if not any(found_platform_code in expected_platform_codes for found_platform_code in found_platform_codes):
+ break
+
+ valid_found_ecus.add(addr)
+
+ # If all live ECUs pass all checks for candidate, add it as a match
+ if valid_expected_ecus.issubset(valid_found_ecus):
+ candidates.add(candidate)
+
+ return {str(c) for c in (candidates - FUZZY_EXCLUDED_PLATFORMS)}
+
+
+# Regex patterns for parsing more general platform-specific identifiers from FW versions.
+# - Part number: Toyota part number (usually last character needs to be ignored to find a match).
+# Each ECU address has just one part number.
+# - Platform: usually multiple codes per an openpilot platform, however this is the least variable and
+# is usually shared across ECUs and model years signifying this describes something about the specific platform.
+# This describes more generational changes (TSS-P vs TSS2), or manufacture region.
+# - Major version: second least variable part of the FW version. Seen splitting cars by model year/API such as
+# RAV4 2022/2023 and Avalon. Used to differentiate cars where API has changed slightly, but is not a generational change.
+# It is important to note that these aren't always consecutive, for example:
+# Avalon 2016-18's fwdCamera has these major versions: 01, 03 while 2019 has: 02
+# - Sub version: exclusive to major version, but shared with other cars. Should only be used for further filtering.
+# Seen bumped in TSB FW updates, and describes other minor differences.
+SHORT_FW_PATTERN = re.compile(b'[A-Z0-9](?P[A-Z0-9]{2})(?P[A-Z0-9]{2})(?P[A-Z0-9]{3})')
+MEDIUM_FW_PATTERN = re.compile(b'(?P[A-Z0-9]{5})(?P[A-Z0-9]{2})(?P[A-Z0-9]{1})(?P[A-Z0-9]{2})')
+LONG_FW_PATTERN = re.compile(b'(?P[A-Z0-9]{5})(?P[A-Z0-9]{2})(?P[A-Z0-9]{2})(?P[A-Z0-9]{3})')
+FW_LEN_CODE = re.compile(b'^[\x01-\x03]') # highest seen is 3 chunks, 16 bytes each
+FW_CHUNK_LEN = 16
+
+# List of ECUs that are most unique across openpilot platforms
+# - fwdCamera: describes actual features related to ADAS. For example, on the Avalon it describes
+# when TSS-P became standard, whether the car supports stop and go, and whether it's TSS2.
+# On the RAV4, it describes the move to the radar doing ACC, and the use of LTA for lane keeping.
+# Note that the platform codes & major versions do not describe features in plain text, only with
+# matching against other seen FW versions in the database they can describe features.
+# - fwdRadar: sanity check against fwdCamera, commonly shares a platform code.
+# For example the RAV4 2022's new radar architecture is shown for both with platform code.
+# - abs: differentiates hybrid/ICE on most cars (Corolla TSS2 is an exception, not used due to hybrid platform combination)
+# - eps: describes lateral API changes for the EPS, such as using LTA for lane keeping and rejecting LKA messages
+PLATFORM_CODE_ECUS = (Ecu.fwdCamera, Ecu.fwdRadar, Ecu.eps)
+
+# These platforms have at least one platform code for all ECUs shared with another platform.
+FUZZY_EXCLUDED_PLATFORMS: set[CAR] = set()
+
+# Some ECUs that use KWP2000 have their FW versions on non-standard data identifiers.
+# Toyota diagnostic software first gets the supported data ids, then queries them one by one.
+# For example, sends: 0x1a8800, receives: 0x1a8800010203, queries: 0x1a8801, 0x1a8802, 0x1a8803
+TOYOTA_VERSION_REQUEST_KWP = b'\x1a\x88\x01'
+TOYOTA_VERSION_RESPONSE_KWP = b'\x5a\x88\x01'
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ # TODO: look at data to whitelist new ECUs effectively
+ requests=[
+ Request(
+ [StdQueries.SHORT_TESTER_PRESENT_REQUEST, TOYOTA_VERSION_REQUEST_KWP],
+ [StdQueries.SHORT_TESTER_PRESENT_RESPONSE, TOYOTA_VERSION_RESPONSE_KWP],
+ whitelist_ecus=[Ecu.fwdCamera, Ecu.fwdRadar, Ecu.dsu, Ecu.abs, Ecu.eps, Ecu.srs, Ecu.transmission, Ecu.hvac],
+ bus=0,
+ ),
+ Request(
+ [StdQueries.SHORT_TESTER_PRESENT_REQUEST, StdQueries.OBD_VERSION_REQUEST],
+ [StdQueries.SHORT_TESTER_PRESENT_RESPONSE, StdQueries.OBD_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.engine, Ecu.hybrid, Ecu.srs, Ecu.transmission, Ecu.hvac],
+ bus=0,
+ ),
+ Request(
+ [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.DEFAULT_DIAGNOSTIC_REQUEST, StdQueries.EXTENDED_DIAGNOSTIC_REQUEST, StdQueries.UDS_VERSION_REQUEST],
+ [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.DEFAULT_DIAGNOSTIC_RESPONSE, StdQueries.EXTENDED_DIAGNOSTIC_RESPONSE, StdQueries.UDS_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.engine, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.abs, Ecu.eps,
+ Ecu.hybrid, Ecu.srs, Ecu.transmission, Ecu.hvac],
+ bus=0,
+ ),
+ ],
+ non_essential_ecus={
+ # FIXME: On some models, abs can sometimes be missing
+ Ecu.abs: [CAR.TOYOTA_RAV4, CAR.TOYOTA_COROLLA, CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_SIENNA, CAR.LEXUS_IS, CAR.TOYOTA_ALPHARD_TSS2],
+ # On some models, the engine can show on two different addresses
+ Ecu.engine: [CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_CAMRY, CAR.TOYOTA_COROLLA_TSS2, CAR.TOYOTA_CHR, CAR.TOYOTA_CHR_TSS2, CAR.LEXUS_IS,
+ CAR.LEXUS_IS_TSS2, CAR.LEXUS_RC, CAR.LEXUS_NX, CAR.LEXUS_NX_TSS2, CAR.LEXUS_RX, CAR.LEXUS_RX_TSS2],
+ },
+ extra_ecus=[
+ # All known ECUs on a late-model Toyota vehicle not queried here:
+ # Responds to UDS:
+ # - Combination Meter (0x7c0)
+ # - HV Battery (0x713, 0x747)
+ # - Motor Generator (0x716, 0x724)
+ # - 2nd ABS "Brake/EPB" (0x730)
+ # - Electronic Parking Brake ((0x750, 0x2c))
+ # - Telematics ((0x750, 0xc7))
+ # Responds to KWP (0x1a8801):
+ # - Steering Angle Sensor (0x7b3)
+ # - EPS/EMPS (0x7a0, 0x7a1)
+ # - 2nd SRS Airbag (0x784)
+ # - Central Gateway ((0x750, 0x5f))
+ # - Telematics ((0x750, 0xc7))
+ # Responds to KWP (0x1a8881):
+ # - Body Control Module ((0x750, 0x40))
+ # - Telematics ((0x750, 0xc7))
+
+ # Hybrid control computer can be on 0x7e2 (KWP) or 0x7d2 (UDS) depending on platform
+ (Ecu.hybrid, 0x7e2, None), # Hybrid Control Assembly & Computer
+ (Ecu.srs, 0x780, None), # SRS Airbag
+ # Transmission is combined with engine on some platforms, such as TSS-P RAV4
+ (Ecu.transmission, 0x701, None),
+ # A few platforms have a tester present response on this address, add to log
+ (Ecu.transmission, 0x7e1, None),
+ (Ecu.hvac, 0x7c4, None),
+ ],
+ match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
+)
+
+
+STEER_THRESHOLD = 100
+
+# These cars have non-standard EPS torque scale factors. All others are 73
+EPS_SCALE = defaultdict(lambda: 73,
+ {CAR.TOYOTA_PRIUS: 66, CAR.TOYOTA_COROLLA: 88, CAR.LEXUS_IS: 77, CAR.LEXUS_RC: 77, CAR.LEXUS_CTH: 100, CAR.TOYOTA_PRIUS_V: 100})
+
+# Toyota/Lexus Safety Sense 2.0 and 2.5
+TSS2_CAR = CAR.with_flags(ToyotaFlags.TSS2)
+
+NO_DSU_CAR = CAR.with_flags(ToyotaFlags.NO_DSU)
+
+# the DSU uses the AEB message for longitudinal on these cars
+UNSUPPORTED_DSU_CAR = CAR.with_flags(ToyotaFlags.UNSUPPORTED_DSU)
+
+# these cars have a radar which sends ACC messages instead of the camera
+RADAR_ACC_CAR = CAR.with_flags(ToyotaFlags.RADAR_ACC)
+
+ANGLE_CONTROL_CAR = CAR.with_flags(ToyotaFlags.ANGLE_CONTROL)
+
+# no resume button press required
+NO_STOP_TIMER_CAR = CAR.with_flags(ToyotaFlags.NO_STOP_TIMER)
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/car/values.py b/opendbc/car/values.py
new file mode 100644
index 0000000000..e2e378d8f9
--- /dev/null
+++ b/opendbc/car/values.py
@@ -0,0 +1,18 @@
+from typing import get_args
+from opendbc.car.body.values import CAR as BODY
+from opendbc.car.chrysler.values import CAR as CHRYSLER
+from opendbc.car.ford.values import CAR as FORD
+from opendbc.car.gm.values import CAR as GM
+from opendbc.car.honda.values import CAR as HONDA
+from opendbc.car.hyundai.values import CAR as HYUNDAI
+from opendbc.car.mazda.values import CAR as MAZDA
+from opendbc.car.mock.values import CAR as MOCK
+from opendbc.car.nissan.values import CAR as NISSAN
+from opendbc.car.subaru.values import CAR as SUBARU
+from opendbc.car.toyota.values import CAR as TOYOTA
+from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN
+
+Platform = BODY | CHRYSLER | FORD | GM | HONDA | HYUNDAI | MAZDA | MOCK | NISSAN | SUBARU | TOYOTA | VOLKSWAGEN
+BRANDS = get_args(Platform)
+
+PLATFORMS: dict[str, Platform] = {str(platform): platform for brand in BRANDS for platform in brand}
diff --git a/opendbc/car/vin.py b/opendbc/car/vin.py
new file mode 100644
index 0000000000..5f816524a1
--- /dev/null
+++ b/opendbc/car/vin.py
@@ -0,0 +1,58 @@
+import re
+
+from panda import uds
+from opendbc.car import carlog
+from opendbc.car.isotp_parallel_query import IsoTpParallelQuery
+from opendbc.car.fw_query_definitions import STANDARD_VIN_ADDRS, StdQueries
+
+VIN_UNKNOWN = "0" * 17
+VIN_RE = "[A-HJ-NPR-Z0-9]{17}"
+
+
+def is_valid_vin(vin: str):
+ return re.fullmatch(VIN_RE, vin) is not None
+
+
+def get_vin(can_recv, can_send, buses, timeout=0.1, retry=2, debug=False):
+ for i in range(retry):
+ for bus in buses:
+ for request, response, valid_buses, vin_addrs, functional_addrs, rx_offset in (
+ (StdQueries.UDS_VIN_REQUEST, StdQueries.UDS_VIN_RESPONSE, (0, 1), STANDARD_VIN_ADDRS, uds.FUNCTIONAL_ADDRS, 0x8),
+ (StdQueries.OBD_VIN_REQUEST, StdQueries.OBD_VIN_RESPONSE, (0, 1), STANDARD_VIN_ADDRS, uds.FUNCTIONAL_ADDRS, 0x8),
+ (StdQueries.GM_VIN_REQUEST, StdQueries.GM_VIN_RESPONSE, (0,), [0x24b], None, 0x400), # Bolt fwdCamera
+ (StdQueries.KWP_VIN_REQUEST, StdQueries.KWP_VIN_RESPONSE, (0,), [0x797], None, 0x3), # Nissan Leaf VCM
+ (StdQueries.UDS_VIN_REQUEST, StdQueries.UDS_VIN_RESPONSE, (0,), [0x74f], None, 0x6a), # Volkswagen fwdCamera
+ ):
+ if bus not in valid_buses:
+ continue
+
+ # When querying functional addresses, ideally we respond to everything that sends a first frame to avoid leaving the
+ # ECU in a temporary bad state. Note that we may not cover all ECUs and response offsets. TODO: query physical addrs
+ tx_addrs = vin_addrs
+ if functional_addrs is not None:
+ tx_addrs = [a for a in range(0x700, 0x800) if a != 0x7DF] + list(range(0x18DA00F1, 0x18DB00F1, 0x100))
+
+ try:
+ query = IsoTpParallelQuery(can_send, can_recv, bus, tx_addrs, [request, ], [response, ], response_offset=rx_offset,
+ functional_addrs=functional_addrs, debug=debug)
+ results = query.get_data(timeout)
+
+ for addr in vin_addrs:
+ vin = results.get((addr, None))
+ if vin is not None:
+ # Ford and Nissan pads with null bytes
+ if len(vin) in (19, 24):
+ vin = re.sub(b'\x00*$', b'', vin)
+
+ # Honda Bosch response starts with a length, trim to correct length
+ if vin.startswith(b'\x11'):
+ vin = vin[1:18]
+
+ carlog.error(f"got vin with {request=}")
+ return uds.get_rx_addr_for_tx_addr(addr, rx_offset=rx_offset), bus, vin.decode()
+ except Exception:
+ carlog.exception("VIN query exception")
+
+ carlog.error(f"vin query retry ({i+1}) ...")
+
+ return -1, -1, VIN_UNKNOWN
diff --git a/opendbc/car/volkswagen/__init__.py b/opendbc/car/volkswagen/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/opendbc/car/volkswagen/carcontroller.py b/opendbc/car/volkswagen/carcontroller.py
new file mode 100644
index 0000000000..8f42ca35be
--- /dev/null
+++ b/opendbc/car/volkswagen/carcontroller.py
@@ -0,0 +1,119 @@
+import copy
+from opendbc.can.packer import CANPacker
+from opendbc.car import DT_CTRL, apply_driver_steer_torque_limits, structs
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.common.numpy_fast import clip
+from opendbc.car.interfaces import CarControllerBase
+from opendbc.car.volkswagen import mqbcan, pqcan
+from opendbc.car.volkswagen.values import CANBUS, CarControllerParams, VolkswagenFlags
+
+VisualAlert = structs.CarControl.HUDControl.VisualAlert
+LongCtrlState = structs.CarControl.Actuators.LongControlState
+
+
+class CarController(CarControllerBase):
+ def __init__(self, dbc_name, CP):
+ super().__init__(dbc_name, CP)
+ self.CCP = CarControllerParams(CP)
+ self.CCS = pqcan if CP.flags & VolkswagenFlags.PQ else mqbcan
+ self.packer_pt = CANPacker(dbc_name)
+ self.ext_bus = CANBUS.pt if CP.networkLocation == structs.CarParams.NetworkLocation.fwdCamera else CANBUS.cam
+
+ self.apply_steer_last = 0
+ self.gra_acc_counter_last = None
+ self.eps_timer_soft_disable_alert = False
+ self.hca_frame_timer_running = 0
+ self.hca_frame_same_torque = 0
+
+ def update(self, CC, CS, now_nanos):
+ actuators = CC.actuators
+ hud_control = CC.hudControl
+ can_sends = []
+
+ # **** Steering Controls ************************************************ #
+
+ if self.frame % self.CCP.STEER_STEP == 0:
+ # Logic to avoid HCA state 4 "refused":
+ # * Don't steer unless HCA is in state 3 "ready" or 5 "active"
+ # * Don't steer at standstill
+ # * Don't send > 3.00 Newton-meters torque
+ # * Don't send the same torque for > 6 seconds
+ # * Don't send uninterrupted steering for > 360 seconds
+ # MQB racks reset the uninterrupted steering timer after a single frame
+ # of HCA disabled; this is done whenever output happens to be zero.
+
+ if CC.latActive:
+ new_steer = int(round(actuators.steer * self.CCP.STEER_MAX))
+ apply_steer = apply_driver_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.CCP)
+ self.hca_frame_timer_running += self.CCP.STEER_STEP
+ if self.apply_steer_last == apply_steer:
+ self.hca_frame_same_torque += self.CCP.STEER_STEP
+ if self.hca_frame_same_torque > self.CCP.STEER_TIME_STUCK_TORQUE / DT_CTRL:
+ apply_steer -= (1, -1)[apply_steer < 0]
+ self.hca_frame_same_torque = 0
+ else:
+ self.hca_frame_same_torque = 0
+ hca_enabled = abs(apply_steer) > 0
+ else:
+ hca_enabled = False
+ apply_steer = 0
+
+ if not hca_enabled:
+ self.hca_frame_timer_running = 0
+
+ self.eps_timer_soft_disable_alert = self.hca_frame_timer_running > self.CCP.STEER_TIME_ALERT / DT_CTRL
+ self.apply_steer_last = apply_steer
+ can_sends.append(self.CCS.create_steering_control(self.packer_pt, CANBUS.pt, apply_steer, hca_enabled))
+
+ if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
+ # Pacify VW Emergency Assist driver inactivity detection by changing its view of driver steering input torque
+ # to the greatest of actual driver input or 2x openpilot's output (1x openpilot output is not enough to
+ # consistently reset inactivity detection on straight level roads). See commaai/openpilot#23274 for background.
+ ea_simulated_torque = clip(apply_steer * 2, -self.CCP.STEER_MAX, self.CCP.STEER_MAX)
+ if abs(CS.out.steeringTorque) > abs(ea_simulated_torque):
+ ea_simulated_torque = CS.out.steeringTorque
+ can_sends.append(self.CCS.create_eps_update(self.packer_pt, CANBUS.cam, CS.eps_stock_values, ea_simulated_torque))
+
+ # **** Acceleration Controls ******************************************** #
+
+ if self.frame % self.CCP.ACC_CONTROL_STEP == 0 and self.CP.openpilotLongitudinalControl:
+ acc_control = self.CCS.acc_control_value(CS.out.cruiseState.available, CS.out.accFaulted, CC.longActive)
+ accel = clip(actuators.accel, self.CCP.ACCEL_MIN, self.CCP.ACCEL_MAX) if CC.longActive else 0
+ stopping = actuators.longControlState == LongCtrlState.stopping
+ starting = actuators.longControlState == LongCtrlState.pid and (CS.esp_hold_confirmation or CS.out.vEgo < self.CP.vEgoStopping)
+ can_sends.extend(self.CCS.create_acc_accel_control(self.packer_pt, CANBUS.pt, CS.acc_type, CC.longActive, accel,
+ acc_control, stopping, starting, CS.esp_hold_confirmation))
+
+ # **** HUD Controls ***************************************************** #
+
+ if self.frame % self.CCP.LDW_STEP == 0:
+ hud_alert = 0
+ if hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw):
+ hud_alert = self.CCP.LDW_MESSAGES["laneAssistTakeOver"]
+ can_sends.append(self.CCS.create_lka_hud_control(self.packer_pt, CANBUS.pt, CS.ldw_stock_values, CC.latActive,
+ CS.out.steeringPressed, hud_alert, hud_control))
+
+ if self.frame % self.CCP.ACC_HUD_STEP == 0 and self.CP.openpilotLongitudinalControl:
+ lead_distance = 0
+ if hud_control.leadVisible and self.frame * DT_CTRL > 1.0: # Don't display lead until we know the scaling factor
+ lead_distance = 512 if CS.upscale_lead_car_signal else 8
+ acc_hud_status = self.CCS.acc_hud_status_value(CS.out.cruiseState.available, CS.out.accFaulted, CC.longActive)
+ # FIXME: follow the recent displayed-speed updates, also use mph_kmh toggle to fix display rounding problem?
+ set_speed = hud_control.setSpeed * CV.MS_TO_KPH
+ can_sends.append(self.CCS.create_acc_hud_control(self.packer_pt, CANBUS.pt, acc_hud_status, set_speed,
+ lead_distance, hud_control.leadDistanceBars))
+
+ # **** Stock ACC Button Controls **************************************** #
+
+ gra_send_ready = self.CP.pcmCruise and CS.gra_stock_values["COUNTER"] != self.gra_acc_counter_last
+ if gra_send_ready and (CC.cruiseControl.cancel or CC.cruiseControl.resume):
+ can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, self.ext_bus, CS.gra_stock_values,
+ cancel=CC.cruiseControl.cancel, resume=CC.cruiseControl.resume))
+
+ new_actuators = copy.copy(actuators)
+ new_actuators.steer = self.apply_steer_last / self.CCP.STEER_MAX
+ new_actuators.steerOutputCan = self.apply_steer_last
+
+ self.gra_acc_counter_last = CS.gra_stock_values["COUNTER"]
+ self.frame += 1
+ return new_actuators, can_sends
diff --git a/opendbc/car/volkswagen/carstate.py b/opendbc/car/volkswagen/carstate.py
new file mode 100644
index 0000000000..b98f4ae06c
--- /dev/null
+++ b/opendbc/car/volkswagen/carstate.py
@@ -0,0 +1,402 @@
+import numpy as np
+from opendbc.can.parser import CANParser
+from opendbc.car import structs
+from opendbc.car.interfaces import CarStateBase
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car.volkswagen.values import DBC, CANBUS, NetworkLocation, TransmissionType, GearShifter, \
+ CarControllerParams, VolkswagenFlags
+
+
+class CarState(CarStateBase):
+ def __init__(self, CP):
+ super().__init__(CP)
+ self.frame = 0
+ self.eps_init_complete = False
+ self.CCP = CarControllerParams(CP)
+ self.button_states = {button.event_type: False for button in self.CCP.BUTTONS}
+ self.esp_hold_confirmation = False
+ self.upscale_lead_car_signal = False
+ self.eps_stock_values = False
+
+ def create_button_events(self, pt_cp, buttons):
+ button_events = []
+
+ for button in buttons:
+ state = pt_cp.vl[button.can_addr][button.can_msg] in button.values
+ if self.button_states[button.event_type] != state:
+ event = structs.CarState.ButtonEvent()
+ event.type = button.event_type
+ event.pressed = state
+ button_events.append(event)
+ self.button_states[button.event_type] = state
+
+ return button_events
+
+ def update(self, pt_cp, cam_cp, *_) -> structs.CarState:
+
+ ext_cp = pt_cp if self.CP.networkLocation == NetworkLocation.fwdCamera else cam_cp
+
+ if self.CP.flags & VolkswagenFlags.PQ:
+ return self.update_pq(pt_cp, cam_cp, ext_cp)
+
+ ret = structs.CarState()
+ # Update vehicle speed and acceleration from ABS wheel speeds.
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ pt_cp.vl["ESP_19"]["ESP_VL_Radgeschw_02"],
+ pt_cp.vl["ESP_19"]["ESP_VR_Radgeschw_02"],
+ pt_cp.vl["ESP_19"]["ESP_HL_Radgeschw_02"],
+ pt_cp.vl["ESP_19"]["ESP_HR_Radgeschw_02"],
+ )
+
+ ret.vEgoRaw = float(np.mean([ret.wheelSpeeds.fl, ret.wheelSpeeds.fr, ret.wheelSpeeds.rl, ret.wheelSpeeds.rr]))
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.standstill = ret.vEgoRaw == 0
+
+ # Update EPS position and state info. For signed values, VW sends the sign in a separate signal.
+ ret.steeringAngleDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradwinkel"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradwinkel"])]
+ ret.steeringRateDeg = pt_cp.vl["LWI_01"]["LWI_Lenkradw_Geschw"] * (1, -1)[int(pt_cp.vl["LWI_01"]["LWI_VZ_Lenkradw_Geschw"])]
+ ret.steeringTorque = pt_cp.vl["LH_EPS_03"]["EPS_Lenkmoment"] * (1, -1)[int(pt_cp.vl["LH_EPS_03"]["EPS_VZ_Lenkmoment"])]
+ ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE
+ ret.yawRate = pt_cp.vl["ESP_02"]["ESP_Gierrate"] * (1, -1)[int(pt_cp.vl["ESP_02"]["ESP_VZ_Gierrate"])] * CV.DEG_TO_RAD
+ hca_status = self.CCP.hca_status_values.get(pt_cp.vl["LH_EPS_03"]["EPS_HCA_Status"])
+ ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status)
+
+ # VW Emergency Assist status tracking and mitigation
+ self.eps_stock_values = pt_cp.vl["LH_EPS_03"]
+ if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
+ ret.carFaultedNonCritical = bool(cam_cp.vl["HCA_01"]["EA_Ruckfreigabe"]) or cam_cp.vl["HCA_01"]["EA_ACC_Sollstatus"] > 0
+
+ # Update gas, brakes, and gearshift.
+ ret.gas = pt_cp.vl["Motor_20"]["MO_Fahrpedalrohwert_01"] / 100.0
+ ret.gasPressed = ret.gas > 0
+ ret.brake = pt_cp.vl["ESP_05"]["ESP_Bremsdruck"] / 250.0 # FIXME: this is pressure in Bar, not sure what OP expects
+ brake_pedal_pressed = bool(pt_cp.vl["Motor_14"]["MO_Fahrer_bremst"])
+ brake_pressure_detected = bool(pt_cp.vl["ESP_05"]["ESP_Fahrer_bremst"])
+ ret.brakePressed = brake_pedal_pressed or brake_pressure_detected
+ ret.parkingBrake = bool(pt_cp.vl["Kombi_01"]["KBI_Handbremse"]) # FIXME: need to include an EPB check as well
+
+ # Update gear and/or clutch position data.
+ if self.CP.transmissionType == TransmissionType.automatic:
+ ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_11"]["GE_Fahrstufe"], None))
+ elif self.CP.transmissionType == TransmissionType.direct:
+ ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["EV_Gearshift"]["GearPosition"], None))
+ elif self.CP.transmissionType == TransmissionType.manual:
+ ret.clutchPressed = not pt_cp.vl["Motor_14"]["MO_Kuppl_schalter"]
+ if bool(pt_cp.vl["Gateway_72"]["BCM1_Rueckfahrlicht_Schalter"]):
+ ret.gearShifter = GearShifter.reverse
+ else:
+ ret.gearShifter = GearShifter.drive
+
+ # Update door and trunk/hatch lid open status.
+ ret.doorOpen = any([pt_cp.vl["Gateway_72"]["ZV_FT_offen"],
+ pt_cp.vl["Gateway_72"]["ZV_BT_offen"],
+ pt_cp.vl["Gateway_72"]["ZV_HFS_offen"],
+ pt_cp.vl["Gateway_72"]["ZV_HBFS_offen"],
+ pt_cp.vl["Gateway_72"]["ZV_HD_offen"]])
+
+ # Update seatbelt fastened status.
+ ret.seatbeltUnlatched = pt_cp.vl["Airbag_02"]["AB_Gurtschloss_FA"] != 3
+
+ # Consume blind-spot monitoring info/warning LED states, if available.
+ # Infostufe: BSM LED on, Warnung: BSM LED flashing
+ if self.CP.enableBsm:
+ ret.leftBlindspot = bool(ext_cp.vl["SWA_01"]["SWA_Infostufe_SWA_li"]) or bool(ext_cp.vl["SWA_01"]["SWA_Warnung_SWA_li"])
+ ret.rightBlindspot = bool(ext_cp.vl["SWA_01"]["SWA_Infostufe_SWA_re"]) or bool(ext_cp.vl["SWA_01"]["SWA_Warnung_SWA_re"])
+
+ # Consume factory LDW data relevant for factory SWA (Lane Change Assist)
+ # and capture it for forwarding to the blind spot radar controller
+ self.ldw_stock_values = cam_cp.vl["LDW_02"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {}
+
+ # Stock FCW is considered active if the release bit for brake-jerk warning
+ # is set. Stock AEB considered active if the partial braking or target
+ # braking release bits are set.
+ # Refer to VW Self Study Program 890253: Volkswagen Driver Assistance
+ # Systems, chapter on Front Assist with Braking: Golf Family for all MQB
+ ret.stockFcw = bool(ext_cp.vl["ACC_10"]["AWV2_Freigabe"])
+ ret.stockAeb = bool(ext_cp.vl["ACC_10"]["ANB_Teilbremsung_Freigabe"]) or bool(ext_cp.vl["ACC_10"]["ANB_Zielbremsung_Freigabe"])
+
+ # Update ACC radar status.
+ self.acc_type = ext_cp.vl["ACC_06"]["ACC_Typ"]
+
+ # ACC okay but disabled (1), ACC ready (2), a radar visibility or other fault/disruption (6 or 7)
+ # currently regulating speed (3), driver accel override (4), brake only (5)
+ ret.cruiseState.available = pt_cp.vl["TSK_06"]["TSK_Status"] in (2, 3, 4, 5)
+ ret.cruiseState.enabled = pt_cp.vl["TSK_06"]["TSK_Status"] in (3, 4, 5)
+
+ if self.CP.pcmCruise:
+ # Cruise Control mode; check for distance UI setting from the radar.
+ # ECM does not manage this, so do not need to check for openpilot longitudinal
+ ret.cruiseState.nonAdaptive = ext_cp.vl["ACC_02"]["ACC_Gesetzte_Zeitluecke"] == 0
+ else:
+ # Speed limiter mode; ECM faults if we command ACC while not pcmCruise
+ ret.cruiseState.nonAdaptive = bool(pt_cp.vl["TSK_06"]["TSK_Limiter_ausgewaehlt"])
+
+ ret.accFaulted = pt_cp.vl["TSK_06"]["TSK_Status"] in (6, 7)
+
+ self.esp_hold_confirmation = bool(pt_cp.vl["ESP_21"]["ESP_Haltebestaetigung"])
+ ret.cruiseState.standstill = self.CP.pcmCruise and self.esp_hold_confirmation
+
+ # Update ACC setpoint. When the setpoint is zero or there's an error, the
+ # radar sends a set-speed of ~90.69 m/s / 203mph.
+ if self.CP.pcmCruise:
+ ret.cruiseState.speed = ext_cp.vl["ACC_02"]["ACC_Wunschgeschw_02"] * CV.KPH_TO_MS
+ if ret.cruiseState.speed > 90:
+ ret.cruiseState.speed = 0
+
+ # Update button states for turn signals and ACC controls, capture all ACC button state/config for passthrough
+ ret.leftBlinker = bool(pt_cp.vl["Blinkmodi_02"]["Comfort_Signal_Left"])
+ ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["Comfort_Signal_Right"])
+ ret.buttonEvents = self.create_button_events(pt_cp, self.CCP.BUTTONS)
+ self.gra_stock_values = pt_cp.vl["GRA_ACC_01"]
+
+ # Additional safety checks performed in CarInterface.
+ ret.espDisabled = pt_cp.vl["ESP_21"]["ESP_Tastung_passiv"] != 0
+
+ # Digital instrument clusters expect the ACC HUD lead car distance to be scaled differently
+ self.upscale_lead_car_signal = bool(pt_cp.vl["Kombi_03"]["KBI_Variante"])
+
+ self.frame += 1
+ return ret
+
+ def update_pq(self, pt_cp, cam_cp, ext_cp) -> structs.CarState:
+ ret = structs.CarState()
+ # Update vehicle speed and acceleration from ABS wheel speeds.
+ ret.wheelSpeeds = self.get_wheel_speeds(
+ pt_cp.vl["Bremse_3"]["Radgeschw__VL_4_1"],
+ pt_cp.vl["Bremse_3"]["Radgeschw__VR_4_1"],
+ pt_cp.vl["Bremse_3"]["Radgeschw__HL_4_1"],
+ pt_cp.vl["Bremse_3"]["Radgeschw__HR_4_1"],
+ )
+
+ # vEgo obtained from Bremse_1 vehicle speed rather than Bremse_3 wheel speeds because Bremse_3 isn't present on NSF
+ ret.vEgoRaw = pt_cp.vl["Bremse_1"]["Geschwindigkeit_neu__Bremse_1_"] * CV.KPH_TO_MS
+ ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
+ ret.standstill = ret.vEgoRaw == 0
+
+ # Update EPS position and state info. For signed values, VW sends the sign in a separate signal.
+ ret.steeringAngleDeg = pt_cp.vl["Lenkhilfe_3"]["LH3_BLW"] * (1, -1)[int(pt_cp.vl["Lenkhilfe_3"]["LH3_BLWSign"])]
+ ret.steeringRateDeg = pt_cp.vl["Lenkwinkel_1"]["Lenkradwinkel_Geschwindigkeit"] * (1, -1)[int(pt_cp.vl["Lenkwinkel_1"]["Lenkradwinkel_Geschwindigkeit_S"])]
+ ret.steeringTorque = pt_cp.vl["Lenkhilfe_3"]["LH3_LM"] * (1, -1)[int(pt_cp.vl["Lenkhilfe_3"]["LH3_LMSign"])]
+ ret.steeringPressed = abs(ret.steeringTorque) > self.CCP.STEER_DRIVER_ALLOWANCE
+ ret.yawRate = pt_cp.vl["Bremse_5"]["Giergeschwindigkeit"] * (1, -1)[int(pt_cp.vl["Bremse_5"]["Vorzeichen_der_Giergeschwindigk"])] * CV.DEG_TO_RAD
+ hca_status = self.CCP.hca_status_values.get(pt_cp.vl["Lenkhilfe_2"]["LH2_Sta_HCA"])
+ ret.steerFaultTemporary, ret.steerFaultPermanent = self.update_hca_state(hca_status)
+
+ # Update gas, brakes, and gearshift.
+ ret.gas = pt_cp.vl["Motor_3"]["Fahrpedal_Rohsignal"] / 100.0
+ ret.gasPressed = ret.gas > 0
+ ret.brake = pt_cp.vl["Bremse_5"]["Bremsdruck"] / 250.0 # FIXME: this is pressure in Bar, not sure what OP expects
+ ret.brakePressed = bool(pt_cp.vl["Motor_2"]["Bremslichtschalter"])
+ ret.parkingBrake = bool(pt_cp.vl["Kombi_1"]["Bremsinfo"])
+
+ # Update gear and/or clutch position data.
+ if self.CP.transmissionType == TransmissionType.automatic:
+ ret.gearShifter = self.parse_gear_shifter(self.CCP.shifter_values.get(pt_cp.vl["Getriebe_1"]["Waehlhebelposition__Getriebe_1_"], None))
+ elif self.CP.transmissionType == TransmissionType.manual:
+ ret.clutchPressed = not pt_cp.vl["Motor_1"]["Kupplungsschalter"]
+ reverse_light = bool(pt_cp.vl["Gate_Komf_1"]["GK1_Rueckfahr"])
+ if reverse_light:
+ ret.gearShifter = GearShifter.reverse
+ else:
+ ret.gearShifter = GearShifter.drive
+
+ # Update door and trunk/hatch lid open status.
+ ret.doorOpen = any([pt_cp.vl["Gate_Komf_1"]["GK1_Fa_Tuerkont"],
+ pt_cp.vl["Gate_Komf_1"]["BSK_BT_geoeffnet"],
+ pt_cp.vl["Gate_Komf_1"]["BSK_HL_geoeffnet"],
+ pt_cp.vl["Gate_Komf_1"]["BSK_HR_geoeffnet"],
+ pt_cp.vl["Gate_Komf_1"]["BSK_HD_Hauptraste"]])
+
+ # Update seatbelt fastened status.
+ ret.seatbeltUnlatched = not bool(pt_cp.vl["Airbag_1"]["Gurtschalter_Fahrer"])
+
+ # Consume blind-spot monitoring info/warning LED states, if available.
+ # Infostufe: BSM LED on, Warnung: BSM LED flashing
+ if self.CP.enableBsm:
+ ret.leftBlindspot = bool(ext_cp.vl["SWA_1"]["SWA_Infostufe_SWA_li"]) or bool(ext_cp.vl["SWA_1"]["SWA_Warnung_SWA_li"])
+ ret.rightBlindspot = bool(ext_cp.vl["SWA_1"]["SWA_Infostufe_SWA_re"]) or bool(ext_cp.vl["SWA_1"]["SWA_Warnung_SWA_re"])
+
+ # Consume factory LDW data relevant for factory SWA (Lane Change Assist)
+ # and capture it for forwarding to the blind spot radar controller
+ self.ldw_stock_values = cam_cp.vl["LDW_Status"] if self.CP.networkLocation == NetworkLocation.fwdCamera else {}
+
+ # Stock FCW is considered active if the release bit for brake-jerk warning
+ # is set. Stock AEB considered active if the partial braking or target
+ # braking release bits are set.
+ # Refer to VW Self Study Program 890253: Volkswagen Driver Assistance
+ # Systems, chapters on Front Assist with Braking and City Emergency
+ # Braking for the 2016 Passat NMS
+ # TODO: deferred until we can collect data on pre-MY2016 behavior, AWV message may be shorter with fewer signals
+ ret.stockFcw = False
+ ret.stockAeb = False
+
+ # Update ACC radar status.
+ self.acc_type = ext_cp.vl["ACC_System"]["ACS_Typ_ACC"]
+ ret.cruiseState.available = bool(pt_cp.vl["Motor_5"]["GRA_Hauptschalter"])
+ ret.cruiseState.enabled = pt_cp.vl["Motor_2"]["GRA_Status"] in (1, 2)
+ if self.CP.pcmCruise:
+ ret.accFaulted = ext_cp.vl["ACC_GRA_Anzeige"]["ACA_StaACC"] in (6, 7)
+ else:
+ ret.accFaulted = pt_cp.vl["Motor_2"]["GRA_Status"] == 3
+
+ # Update ACC setpoint. When the setpoint reads as 255, the driver has not
+ # yet established an ACC setpoint, so treat it as zero.
+ ret.cruiseState.speed = ext_cp.vl["ACC_GRA_Anzeige"]["ACA_V_Wunsch"] * CV.KPH_TO_MS
+ if ret.cruiseState.speed > 70: # 255 kph in m/s == no current setpoint
+ ret.cruiseState.speed = 0
+
+ # Update button states for turn signals and ACC controls, capture all ACC button state/config for passthrough
+ ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_stalk(300, pt_cp.vl["Gate_Komf_1"]["GK1_Blinker_li"],
+ pt_cp.vl["Gate_Komf_1"]["GK1_Blinker_re"])
+ ret.buttonEvents = self.create_button_events(pt_cp, self.CCP.BUTTONS)
+ self.gra_stock_values = pt_cp.vl["GRA_Neu"]
+
+ # Additional safety checks performed in CarInterface.
+ ret.espDisabled = bool(pt_cp.vl["Bremse_1"]["ESP_Passiv_getastet"])
+
+ self.frame += 1
+ return ret
+
+ def update_hca_state(self, hca_status):
+ # Treat INITIALIZING and FAULT as temporary for worst likely EPS recovery time, for cars without factory Lane Assist
+ # DISABLED means the EPS hasn't been configured to support Lane Assist
+ self.eps_init_complete = self.eps_init_complete or (hca_status in ("DISABLED", "READY", "ACTIVE") or self.frame > 600)
+ perm_fault = hca_status == "DISABLED" or (self.eps_init_complete and hca_status in ("INITIALIZING", "FAULT"))
+ temp_fault = hca_status in ("REJECTED", "PREEMPTED") or not self.eps_init_complete
+ return temp_fault, perm_fault
+
+ @staticmethod
+ def get_can_parser(CP):
+ if CP.flags & VolkswagenFlags.PQ:
+ return CarState.get_can_parser_pq(CP)
+
+ messages = [
+ # sig_address, frequency
+ ("LWI_01", 100), # From J500 Steering Assist with integrated sensors
+ ("LH_EPS_03", 100), # From J500 Steering Assist with integrated sensors
+ ("ESP_19", 100), # From J104 ABS/ESP controller
+ ("ESP_05", 50), # From J104 ABS/ESP controller
+ ("ESP_21", 50), # From J104 ABS/ESP controller
+ ("Motor_20", 50), # From J623 Engine control module
+ ("TSK_06", 50), # From J623 Engine control module
+ ("ESP_02", 50), # From J104 ABS/ESP controller
+ ("GRA_ACC_01", 33), # From J533 CAN gateway (via LIN from steering wheel controls)
+ ("Gateway_72", 10), # From J533 CAN gateway (aggregated data)
+ ("Motor_14", 10), # From J623 Engine control module
+ ("Airbag_02", 5), # From J234 Airbag control module
+ ("Kombi_01", 2), # From J285 Instrument cluster
+ ("Blinkmodi_02", 1), # From J519 BCM (sent at 1Hz when no lights active, 50Hz when active)
+ ("Kombi_03", 0), # From J285 instrument cluster (not present on older cars, 1Hz when present)
+ ]
+
+ if CP.transmissionType == TransmissionType.automatic:
+ messages.append(("Getriebe_11", 20)) # From J743 Auto transmission control module
+ elif CP.transmissionType == TransmissionType.direct:
+ messages.append(("EV_Gearshift", 10)) # From J??? unknown EV control module
+
+ if CP.networkLocation == NetworkLocation.fwdCamera:
+ # Radars are here on CANBUS.pt
+ messages += MqbExtraSignals.fwd_radar_messages
+ if CP.enableBsm:
+ messages += MqbExtraSignals.bsm_radar_messages
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CANBUS.pt)
+
+ @staticmethod
+ def get_cam_can_parser(CP):
+ if CP.flags & VolkswagenFlags.PQ:
+ return CarState.get_cam_can_parser_pq(CP)
+
+ messages = []
+
+ if CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
+ messages += [
+ ("HCA_01", 1), # From R242 Driver assistance camera, 50Hz if steering/1Hz if not
+ ]
+
+ if CP.networkLocation == NetworkLocation.fwdCamera:
+ messages += [
+ # sig_address, frequency
+ ("LDW_02", 10) # From R242 Driver assistance camera
+ ]
+ else:
+ # Radars are here on CANBUS.cam
+ messages += MqbExtraSignals.fwd_radar_messages
+ if CP.enableBsm:
+ messages += MqbExtraSignals.bsm_radar_messages
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CANBUS.cam)
+
+ @staticmethod
+ def get_can_parser_pq(CP):
+ messages = [
+ # sig_address, frequency
+ ("Bremse_1", 100), # From J104 ABS/ESP controller
+ ("Bremse_3", 100), # From J104 ABS/ESP controller
+ ("Lenkhilfe_3", 100), # From J500 Steering Assist with integrated sensors
+ ("Lenkwinkel_1", 100), # From J500 Steering Assist with integrated sensors
+ ("Motor_3", 100), # From J623 Engine control module
+ ("Airbag_1", 50), # From J234 Airbag control module
+ ("Bremse_5", 50), # From J104 ABS/ESP controller
+ ("GRA_Neu", 50), # From J??? steering wheel control buttons
+ ("Kombi_1", 50), # From J285 Instrument cluster
+ ("Motor_2", 50), # From J623 Engine control module
+ ("Motor_5", 50), # From J623 Engine control module
+ ("Lenkhilfe_2", 20), # From J500 Steering Assist with integrated sensors
+ ("Gate_Komf_1", 10), # From J533 CAN gateway
+ ]
+
+ if CP.transmissionType == TransmissionType.automatic:
+ messages += [("Getriebe_1", 100)] # From J743 Auto transmission control module
+ elif CP.transmissionType == TransmissionType.manual:
+ messages += [("Motor_1", 100)] # From J623 Engine control module
+
+ if CP.networkLocation == NetworkLocation.fwdCamera:
+ # Extended CAN devices other than the camera are here on CANBUS.pt
+ messages += PqExtraSignals.fwd_radar_messages
+ if CP.enableBsm:
+ messages += PqExtraSignals.bsm_radar_messages
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CANBUS.pt)
+
+ @staticmethod
+ def get_cam_can_parser_pq(CP):
+
+ messages = []
+
+ if CP.networkLocation == NetworkLocation.fwdCamera:
+ messages += [
+ # sig_address, frequency
+ ("LDW_Status", 10) # From R242 Driver assistance camera
+ ]
+
+ if CP.networkLocation == NetworkLocation.gateway:
+ # Radars are here on CANBUS.cam
+ messages += PqExtraSignals.fwd_radar_messages
+ if CP.enableBsm:
+ messages += PqExtraSignals.bsm_radar_messages
+
+ return CANParser(DBC[CP.carFingerprint]["pt"], messages, CANBUS.cam)
+
+
+class MqbExtraSignals:
+ # Additional signal and message lists for optional or bus-portable controllers
+ fwd_radar_messages = [
+ ("ACC_06", 50), # From J428 ACC radar control module
+ ("ACC_10", 50), # From J428 ACC radar control module
+ ("ACC_02", 17), # From J428 ACC radar control module
+ ]
+ bsm_radar_messages = [
+ ("SWA_01", 20), # From J1086 Lane Change Assist
+ ]
+
+
+class PqExtraSignals:
+ # Additional signal and message lists for optional or bus-portable controllers
+ fwd_radar_messages = [
+ ("ACC_System", 50), # From J428 ACC radar control module
+ ("ACC_GRA_Anzeige", 25), # From J428 ACC radar control module
+ ]
+ bsm_radar_messages = [
+ ("SWA_1", 20), # From J1086 Lane Change Assist
+ ]
diff --git a/opendbc/car/volkswagen/fingerprints.py b/opendbc/car/volkswagen/fingerprints.py
new file mode 100644
index 0000000000..83756830a6
--- /dev/null
+++ b/opendbc/car/volkswagen/fingerprints.py
@@ -0,0 +1,1209 @@
+from opendbc.car.structs import CarParams
+from opendbc.car.volkswagen.values import CAR
+
+Ecu = CarParams.Ecu
+
+# TODO: Sharan Mk2 EPS and DQ250 auto trans both require KWP2000 support for fingerprinting
+
+
+FW_VERSIONS = {
+ CAR.VOLKSWAGEN_ARTEON_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x873G0906259AH\xf1\x890001',
+ b'\xf1\x873G0906259F \xf1\x890004',
+ b'\xf1\x873G0906259G \xf1\x890004',
+ b'\xf1\x873G0906259G \xf1\x890005',
+ b'\xf1\x873G0906259M \xf1\x890003',
+ b'\xf1\x873G0906259N \xf1\x890004',
+ b'\xf1\x873G0906259P \xf1\x890001',
+ b'\xf1\x875NA907115H \xf1\x890002',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x8709G927158L \xf1\x893611',
+ b'\xf1\x870DL300014C \xf1\x893704',
+ b'\xf1\x870GC300011L \xf1\x891401',
+ b'\xf1\x870GC300014M \xf1\x892802',
+ b'\xf1\x870GC300019G \xf1\x892804',
+ b'\xf1\x870GC300040P \xf1\x891401',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e1616001613121157161111572900',
+ b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e1616001613121177161113772900',
+ b'\xf1\x873Q0959655CK\xf1\x890711\xf1\x82\x0e1712141712141105121122052900',
+ b'\xf1\x873Q0959655DA\xf1\x890720\xf1\x82\x0e1712141712141105121122052900',
+ b'\xf1\x873Q0959655DL\xf1\x890732\xf1\x82\x0e1812141812171105141123052J00',
+ b'\xf1\x875QF959655AP\xf1\x890755\xf1\x82\x1311110011111311111100110200--1611125F49',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571B41815A1',
+ b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571B00817A1',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567B0020800',
+ b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x002MB4092M7N',
+ b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x002NB4202N7N',
+ b'\xf1\x875WA907145Q \xf1\x891063\xf1\x82\x002KB4092KOM',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ b'\xf1\x875Q0907572J \xf1\x890654',
+ b'\xf1\x875Q0907572R \xf1\x890771',
+ ],
+ },
+ CAR.VOLKSWAGEN_ATLAS_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8703H906026AA\xf1\x899970',
+ b'\xf1\x8703H906026AG\xf1\x899973',
+ b'\xf1\x8703H906026AJ\xf1\x890638',
+ b'\xf1\x8703H906026AJ\xf1\x891017',
+ b'\xf1\x8703H906026AT\xf1\x891922',
+ b'\xf1\x8703H906026BC\xf1\x892664',
+ b'\xf1\x8703H906026F \xf1\x896696',
+ b'\xf1\x8703H906026F \xf1\x899970',
+ b'\xf1\x8703H906026J \xf1\x896026',
+ b'\xf1\x8703H906026J \xf1\x899970',
+ b'\xf1\x8703H906026J \xf1\x899971',
+ b'\xf1\x8703H906026S \xf1\x896693',
+ b'\xf1\x8703H906026S \xf1\x899970',
+ b'\xf1\x873CN906259 \xf1\x890005',
+ b'\xf1\x873CN906259F \xf1\x890002',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x8709G927158A \xf1\x893387',
+ b'\xf1\x8709G927158DR\xf1\x893536',
+ b'\xf1\x8709G927158DR\xf1\x893742',
+ b'\xf1\x8709G927158EN\xf1\x893691',
+ b'\xf1\x8709G927158F \xf1\x893489',
+ b'\xf1\x8709G927158FT\xf1\x893835',
+ b'\xf1\x8709G927158GL\xf1\x893939',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655BC\xf1\x890503\xf1\x82\x0e1914151912001103111122031200',
+ b'\xf1\x873Q0959655BN\xf1\x890713\xf1\x82\x0e2214152212001105141122052900',
+ b'\xf1\x873Q0959655DB\xf1\x890720\xf1\x82\x0e1114151112001105111122052900',
+ b'\xf1\x873Q0959655DB\xf1\x890720\xf1\x82\x0e2214152212001105141122052900',
+ b'\xf1\x873Q0959655DM\xf1\x890732\xf1\x82\x0e1114151112001105111122052J00',
+ b'\xf1\x873Q0959655DM\xf1\x890732\xf1\x82\x0e1114151112001105161122052J00',
+ b'\xf1\x873Q0959655DM\xf1\x890732\xf1\x82\x0e1115151112001105171122052J00',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873QF909144B \xf1\x891582\xf1\x82\x0571B60924A1',
+ b'\xf1\x873QF909144B \xf1\x891582\xf1\x82\x0571B6G920A1',
+ b'\xf1\x873QF909144B \xf1\x891582\xf1\x82\x0571B6M921A1',
+ b'\xf1\x873QF909144B \xf1\x891582\xf1\x82\x0571B6N920A1',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820528B6080105',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820528B6090105',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572R \xf1\x890372',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ b'\xf1\x875Q0907572H \xf1\x890620',
+ b'\xf1\x875Q0907572J \xf1\x890654',
+ b'\xf1\x875Q0907572P \xf1\x890682',
+ b'\xf1\x875Q0907572S \xf1\x890780',
+ ],
+ },
+ CAR.VOLKSWAGEN_CADDY_MK3: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906027T \xf1\x892363',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x872K5959655E \xf1\x890018\xf1\x82\x05000P037605',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x877N0907572C \xf1\x890211\xf1\x82\x0155',
+ ],
+ },
+ CAR.VOLKSWAGEN_CRAFTER_MK2: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704L906056BP\xf1\x894729',
+ b'\xf1\x8704L906056EK\xf1\x896391',
+ b'\xf1\x8705L906023BC\xf1\x892688',
+ b'\xf1\x8705L906023MH\xf1\x892588',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655AL\xf1\x890505\xf1\x82\x0e1411001413001203151311031100',
+ b'\xf1\x873Q0959655BG\xf1\x890703\xf1\x82\x0e16120016130012051G1313052900',
+ b'\xf1\x875QF959655AS\xf1\x890755\xf1\x82\x1315140015150011111100050200--1311120749',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x872N0909143D\x00\xf1\x897010\xf1\x82\x05183AZ306A2',
+ b'\xf1\x872N0909143E \xf1\x897021\xf1\x82\x05163AZ306A2',
+ b'\xf1\x872N0909143H \xf1\x897045\xf1\x82\x05263AZ309A2',
+ b'\xf1\x872N0909144K \xf1\x897045\xf1\x82\x05233AZ810A2',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572J \xf1\x890156',
+ b'\xf1\x872Q0907572M \xf1\x890233',
+ ],
+ },
+ CAR.VOLKSWAGEN_GOLF_MK7: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906016A \xf1\x897697',
+ b'\xf1\x8704E906016AD\xf1\x895758',
+ b'\xf1\x8704E906016CE\xf1\x899096',
+ b'\xf1\x8704E906016CH\xf1\x899226',
+ b'\xf1\x8704E906016N \xf1\x899105',
+ b'\xf1\x8704E906023AG\xf1\x891726',
+ b'\xf1\x8704E906023BN\xf1\x894518',
+ b'\xf1\x8704E906024K \xf1\x896811',
+ b'\xf1\x8704E906024K \xf1\x899970',
+ b'\xf1\x8704E906027GR\xf1\x892394',
+ b'\xf1\x8704E906027HD\xf1\x892603',
+ b'\xf1\x8704E906027HD\xf1\x893742',
+ b'\xf1\x8704E906027MA\xf1\x894958',
+ b'\xf1\x8704L906021DT\xf1\x895520',
+ b'\xf1\x8704L906021DT\xf1\x898127',
+ b'\xf1\x8704L906021N \xf1\x895518',
+ b'\xf1\x8704L906021N \xf1\x898138',
+ b'\xf1\x8704L906026BN\xf1\x891197',
+ b'\xf1\x8704L906026BP\xf1\x897608',
+ b'\xf1\x8704L906026NF\xf1\x899528',
+ b'\xf1\x8704L906056CL\xf1\x893823',
+ b'\xf1\x8704L906056CR\xf1\x895813',
+ b'\xf1\x8704L906056HE\xf1\x893758',
+ b'\xf1\x8704L906056HN\xf1\x896590',
+ b'\xf1\x8704L906056HT\xf1\x896591',
+ b'\xf1\x8704L997022N \xf1\x899459',
+ b'\xf1\x870EA906016A \xf1\x898343',
+ b'\xf1\x870EA906016E \xf1\x894219',
+ b'\xf1\x870EA906016F \xf1\x894238',
+ b'\xf1\x870EA906016F \xf1\x895002',
+ b'\xf1\x870EA906016Q \xf1\x895993',
+ b'\xf1\x870EA906016S \xf1\x897207',
+ b'\xf1\x875G0906259 \xf1\x890007',
+ b'\xf1\x875G0906259D \xf1\x890002',
+ b'\xf1\x875G0906259J \xf1\x890002',
+ b'\xf1\x875G0906259L \xf1\x890002',
+ b'\xf1\x875G0906259N \xf1\x890003',
+ b'\xf1\x875G0906259Q \xf1\x890002',
+ b'\xf1\x875G0906259Q \xf1\x892313',
+ b'\xf1\x875G0906259T \xf1\x890003',
+ b'\xf1\x878V0906259H \xf1\x890002',
+ b'\xf1\x878V0906259J \xf1\x890003',
+ b'\xf1\x878V0906259J \xf1\x890103',
+ b'\xf1\x878V0906259K \xf1\x890001',
+ b'\xf1\x878V0906259K \xf1\x890003',
+ b'\xf1\x878V0906259P \xf1\x890001',
+ b'\xf1\x878V0906259Q \xf1\x890002',
+ b'\xf1\x878V0906259R \xf1\x890002',
+ b'\xf1\x878V0906264F \xf1\x890003',
+ b'\xf1\x878V0906264L \xf1\x890002',
+ b'\xf1\x878V0906264M \xf1\x890001',
+ b'\xf1\x878V09C0BB01 \xf1\x890001',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x8709G927749AP\xf1\x892943',
+ b'\xf1\x8709S927158A \xf1\x893585',
+ b'\xf1\x870CW300040H \xf1\x890606',
+ b'\xf1\x870CW300041D \xf1\x891004',
+ b'\xf1\x870CW300041H \xf1\x891010',
+ b'\xf1\x870CW300042F \xf1\x891604',
+ b'\xf1\x870CW300043B \xf1\x891601',
+ b'\xf1\x870CW300043E \xf1\x891603',
+ b'\xf1\x870CW300044S \xf1\x894530',
+ b'\xf1\x870CW300044T \xf1\x895245',
+ b'\xf1\x870CW300045 \xf1\x894531',
+ b'\xf1\x870CW300047D \xf1\x895261',
+ b'\xf1\x870CW300047E \xf1\x895261',
+ b'\xf1\x870CW300048J \xf1\x890611',
+ b'\xf1\x870CW300049H \xf1\x890905',
+ b'\xf1\x870CW300050G \xf1\x891905',
+ b'\xf1\x870D9300012 \xf1\x894904',
+ b'\xf1\x870D9300012 \xf1\x894913',
+ b'\xf1\x870D9300012 \xf1\x894937',
+ b'\xf1\x870D9300012 \xf1\x895045',
+ b'\xf1\x870D9300012 \xf1\x895046',
+ b'\xf1\x870D9300014M \xf1\x895004',
+ b'\xf1\x870D9300014Q \xf1\x895006',
+ b'\xf1\x870D9300018 \xf1\x895201',
+ b'\xf1\x870D9300020J \xf1\x894902',
+ b'\xf1\x870D9300020Q \xf1\x895201',
+ b'\xf1\x870D9300020S \xf1\x895201',
+ b'\xf1\x870D9300040A \xf1\x893613',
+ b'\xf1\x870D9300040S \xf1\x894311',
+ b'\xf1\x870D9300041H \xf1\x895220',
+ b'\xf1\x870D9300041N \xf1\x894512',
+ b'\xf1\x870D9300041P \xf1\x894507',
+ b'\xf1\x870DD300045K \xf1\x891120',
+ b'\xf1\x870DD300046F \xf1\x891601',
+ b'\xf1\x870GC300012A \xf1\x891401',
+ b'\xf1\x870GC300012A \xf1\x891403',
+ b'\xf1\x870GC300012A \xf1\x891422',
+ b'\xf1\x870GC300012M \xf1\x892301',
+ b'\xf1\x870GC300014B \xf1\x892401',
+ b'\xf1\x870GC300014B \xf1\x892403',
+ b'\xf1\x870GC300014B \xf1\x892405',
+ b'\xf1\x870GC300020G \xf1\x892401',
+ b'\xf1\x870GC300020G \xf1\x892403',
+ b'\xf1\x870GC300020G \xf1\x892404',
+ b'\xf1\x870GC300020N \xf1\x892804',
+ b'\xf1\x870GC300043T \xf1\x899999',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655AA\xf1\x890386\xf1\x82\x111413001113120043114317121C111C9113',
+ b'\xf1\x875Q0959655AA\xf1\x890386\xf1\x82\x111413001113120053114317121C111C9113',
+ b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\x111413001113120043114317121C111C9113',
+ b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\x111413001113120043114417121411149113',
+ b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\x111413001113120053114317121C111C9113',
+ b'\xf1\x875Q0959655AR\xf1\x890317\xf1\x82\x13141500111233003142114A2131219333313100',
+ b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\x1314160011123300314211012230229333423100',
+ b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\x1314160011123300314211012230229333463100',
+ b'\xf1\x875Q0959655BJ\xf1\x890339\xf1\x82\x13141600111233003142115A2232229333463100',
+ b'\xf1\x875Q0959655BS\xf1\x890403\xf1\x82\x1314160011123300314240012250229333463100',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x13141600111233003142404A2251229333463100',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x13141600111233003142404A2252229333463100',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x13141600111233003142405A2251229333463100',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x13141600111233003142405A2252229333463100',
+ b'\xf1\x875Q0959655C \xf1\x890361\xf1\x82\x111413001112120004110415121610169112',
+ b'\xf1\x875Q0959655CA\xf1\x890403\xf1\x82\x1314160011123300314240012250229333463100',
+ b'\xf1\x875Q0959655D \xf1\x890388\xf1\x82\x111413001113120006110417121A101A9113',
+ b'\xf1\x875Q0959655J \xf1\x890825\xf1\x82\x13271112111312--071104171825102591131211',
+ b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13271112111312--071104171825102591131211',
+ b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13271212111312--071104171838103891131211',
+ b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13272512111312--07110417182C102C91131211',
+ b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13341512112212--071104172328102891131211',
+ b'\xf1\x875Q0959655M \xf1\x890361\xf1\x82\x111413001112120041114115121611169112',
+ b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1315120011211200061104171717101791132111',
+ b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1315120011211200621143171717111791132111',
+ b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1324230011211200061104171724102491132111',
+ b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1324230011211200621143171724112491132111',
+ b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1324230011211200631143171724122491132111',
+ b'\xf1\x875Q0959655T \xf1\x890825\xf1\x82\x13271200111312--071104171837103791132111',
+ b'\xf1\x875Q0959655T \xf1\x890830\xf1\x82\x13271100111312--071104171826102691131211',
+ b'\xf1\x875QD959655 \xf1\x890388\xf1\x82\x111413001113120006110417121D101D9112',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873Q0909144F \xf1\x895043\xf1\x82\x0561A01612A0',
+ b'\xf1\x873Q0909144H \xf1\x895061\xf1\x82\x0566A0J612A1',
+ b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566A00514A1',
+ b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566A01613A1',
+ b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566A0J712A1',
+ b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571A0J714A1',
+ b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571A0JA15A1',
+ b'\xf1\x873Q0909144M \xf1\x895082\xf1\x82\x0571A01A18A1',
+ b'\xf1\x873Q0909144M \xf1\x895082\xf1\x82\x0571A02A16A1',
+ b'\xf1\x873Q0909144M \xf1\x895082\xf1\x82\x0571A0JA16A1',
+ b'\xf1\x873QM909144 \xf1\x895072\xf1\x82\x0571A01714A1',
+ b'\xf1\x875Q0909143K \xf1\x892033\xf1\x820519A9040203',
+ b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521A00441A1',
+ b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521A00608A1',
+ b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521A00641A1',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A00442A1',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A00642A1',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A07B05A1',
+ b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\x0521A00502A0',
+ b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\x0521A00602A0',
+ b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\x0522A00402A0',
+ b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0511A00403A0',
+ b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\x0516A00604A1',
+ b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A00404A1',
+ b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A00504A1',
+ b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A00604A1',
+ b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A07A02A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A00407A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A00507A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A07B04A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A20B03A1',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567A2000400',
+ b'\xf1\x875QD909144B \xf1\x891072\xf1\x82\x0521A00507A1',
+ b'\xf1\x875QM909144A \xf1\x891072\xf1\x82\x0521A20B03A1',
+ b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521A00442A1',
+ b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521A00642A1',
+ b'\xf1\x875QN909144A \xf1\x895081\xf1\x82\x0571A01A16A1',
+ b'\xf1\x875QN909144A \xf1\x895081\xf1\x82\x0571A01A17A1',
+ b'\xf1\x875QN909144A \xf1\x895081\xf1\x82\x0571A01A18A1',
+ b'\xf1\x875QN909144B \xf1\x895082\xf1\x82\x0571A01A18A1',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x875Q0907567G \xf1\x890390\xf1\x82\x0101',
+ b'\xf1\x875Q0907567J \xf1\x890396\xf1\x82\x0101',
+ b'\xf1\x875Q0907567L \xf1\x890098\xf1\x82\x0101',
+ b'\xf1\x875Q0907572A \xf1\x890141\xf1\x82\x0101',
+ b'\xf1\x875Q0907572B \xf1\x890200\xf1\x82\x0101',
+ b'\xf1\x875Q0907572C \xf1\x890210\xf1\x82\x0101',
+ b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\x0101',
+ b'\xf1\x875Q0907572E \xf1\x89X310\xf1\x82\x0101',
+ b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\x0101',
+ b'\xf1\x875Q0907572G \xf1\x890571',
+ b'\xf1\x875Q0907572H \xf1\x890620',
+ b'\xf1\x875Q0907572J \xf1\x890653',
+ b'\xf1\x875Q0907572J \xf1\x890654',
+ b'\xf1\x875Q0907572P \xf1\x890682',
+ b'\xf1\x875Q0907572R \xf1\x890771',
+ b'\xf1\x875Q0907572S \xf1\x890780',
+ ],
+ },
+ CAR.VOLKSWAGEN_JETTA_MK7: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906024AK\xf1\x899937',
+ b'\xf1\x8704E906024AS\xf1\x899912',
+ b'\xf1\x8704E906024B \xf1\x895594',
+ b'\xf1\x8704E906024BC\xf1\x899971',
+ b'\xf1\x8704E906024BG\xf1\x891057',
+ b'\xf1\x8704E906024C \xf1\x899970',
+ b'\xf1\x8704E906024C \xf1\x899971',
+ b'\xf1\x8704E906024L \xf1\x895595',
+ b'\xf1\x8704E906024L \xf1\x899970',
+ b'\xf1\x8704E906027MS\xf1\x896223',
+ b'\xf1\x8705E906013DB\xf1\x893361',
+ b'\xf1\x875G0906259T \xf1\x890003',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x8709G927158BQ\xf1\x893545',
+ b'\xf1\x8709S927158BS\xf1\x893642',
+ b'\xf1\x8709S927158BS\xf1\x893694',
+ b'\xf1\x8709S927158CK\xf1\x893770',
+ b'\xf1\x8709S927158JC\xf1\x894113',
+ b'\xf1\x8709S927158R \xf1\x893552',
+ b'\xf1\x8709S927158R \xf1\x893587',
+ b'\xf1\x870GC300020N \xf1\x892803',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655AG\xf1\x890336\xf1\x82\x1314171231313500314611011630169333463100',
+ b'\xf1\x875Q0959655AG\xf1\x890338\xf1\x82\x1314171231313500314611011630169333463100',
+ b'\xf1\x875Q0959655BM\xf1\x890403\xf1\x82\x1314171231313500314642011650169333463100',
+ b'\xf1\x875Q0959655BM\xf1\x890403\xf1\x82\x1314171231313500314643011650169333463100',
+ b'\xf1\x875Q0959655BR\xf1\x890403\xf1\x82\x1311170031313300314240011150119333433100',
+ b'\xf1\x875Q0959655BR\xf1\x890403\xf1\x82\x1319170031313300314240011550159333463100',
+ b'\xf1\x875Q0959655CB\xf1\x890421\xf1\x82\x1314171231313500314642021650169333613100',
+ b'\xf1\x875Q0959655CB\xf1\x890421\xf1\x82\x1314171231313500314643021650169333613100',
+ b'\xf1\x875Q0959655CB\xf1\x890421\xf1\x82\x1317171231313500314642023050309333613100',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873Q0909144M \xf1\x895082\xf1\x82\x0571A10A11A1',
+ b'\xf1\x875QM907144D \xf1\x891063\xf1\x82\x000_A1080_OM',
+ b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521A10A01A1',
+ b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521B00404A1',
+ b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521A00642A1',
+ b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521A10A01A1',
+ b'\xf1\x875QN909144B \xf1\x895082\xf1\x82\x0571A10A11A1',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x875Q0907572N \xf1\x890681',
+ b'\xf1\x875Q0907572P \xf1\x890682',
+ b'\xf1\x875Q0907572R \xf1\x890771',
+ ],
+ },
+ CAR.VOLKSWAGEN_PASSAT_MK8: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8703N906026E \xf1\x892114',
+ b'\xf1\x8704E906023AH\xf1\x893379',
+ b'\xf1\x8704E906023BM\xf1\x894522',
+ b'\xf1\x8704L906026DP\xf1\x891538',
+ b'\xf1\x8704L906026ET\xf1\x891990',
+ b'\xf1\x8704L906026FP\xf1\x892012',
+ b'\xf1\x8704L906026GA\xf1\x892013',
+ b'\xf1\x8704L906026GK\xf1\x899971',
+ b'\xf1\x8704L906026KD\xf1\x894798',
+ b'\xf1\x8705L906022A \xf1\x890827',
+ b'\xf1\x873G0906259 \xf1\x890004',
+ b'\xf1\x873G0906259B \xf1\x890002',
+ b'\xf1\x873G0906264 \xf1\x890004',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300041E \xf1\x891006',
+ b'\xf1\x870CW300042H \xf1\x891601',
+ b'\xf1\x870CW300042H \xf1\x891607',
+ b'\xf1\x870CW300043H \xf1\x891601',
+ b'\xf1\x870CW300048R \xf1\x890610',
+ b'\xf1\x870D9300013A \xf1\x894905',
+ b'\xf1\x870D9300014L \xf1\x895002',
+ b'\xf1\x870D9300018C \xf1\x895297',
+ b'\xf1\x870D9300041A \xf1\x894801',
+ b'\xf1\x870D9300042H \xf1\x894901',
+ b'\xf1\x870DD300045T \xf1\x891601',
+ b'\xf1\x870DD300046H \xf1\x891601',
+ b'\xf1\x870DL300011H \xf1\x895201',
+ b'\xf1\x870GC300042H \xf1\x891404',
+ b'\xf1\x870GC300043 \xf1\x892301',
+ b'\xf1\x870GC300046P \xf1\x892805',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655AE\xf1\x890195\xf1\x82\r56140056130012416612124111',
+ b'\xf1\x873Q0959655AF\xf1\x890195\xf1\x82\r56140056130012026612120211',
+ b'\xf1\x873Q0959655AN\xf1\x890305\xf1\x82\r58160058140013036914110311',
+ b'\xf1\x873Q0959655AN\xf1\x890306\xf1\x82\r58160058140013036914110311',
+ b'\xf1\x873Q0959655BA\xf1\x890195\xf1\x82\r56140056130012416612124111',
+ b'\xf1\x873Q0959655BA\xf1\x890195\xf1\x82\r56140056130012516612125111',
+ b'\xf1\x873Q0959655BB\xf1\x890195\xf1\x82\r56140056130012026612120211',
+ b'\xf1\x873Q0959655BG\xf1\x890712\xf1\x82\x0e5915005914001305701311052900',
+ b'\xf1\x873Q0959655BJ\xf1\x890703\xf1\x82\x0e5915005914001305701311052900',
+ b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e5915005914001344701311442900',
+ b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e5915005914001354701311542900',
+ b'\xf1\x873Q0959655CN\xf1\x890720\xf1\x82\x0e5915005914001305701311052900',
+ b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1315120011111200631145171716121691132111',
+ b'\xf1\x875QF959655S \xf1\x890639\xf1\x82\x13131100131300111111000120----2211114A48',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566B00611A1',
+ b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566B00711A1',
+ b'\xf1\x875Q0909143K \xf1\x892033\xf1\x820514B0060703',
+ b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0060803',
+ b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0080803',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820526B0060905',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820531B0062105',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521B00606A1',
+ b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516B00501A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521B00603A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521B00703A1',
+ b'\xf1\x875Q0910143B \xf1\x892201\xf1\x82\x0563B0000600',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567B0020600',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x873Q0907572A \xf1\x890126',
+ b'\xf1\x873Q0907572A \xf1\x890130',
+ b'\xf1\x873Q0907572B \xf1\x890192',
+ b'\xf1\x873Q0907572B \xf1\x890194',
+ b'\xf1\x873Q0907572C \xf1\x890195',
+ b'\xf1\x873Q0907572C \xf1\x890196',
+ b'\xf1\x875Q0907572P \xf1\x890682',
+ b'\xf1\x875Q0907572R \xf1\x890771',
+ b'\xf1\x875Q0907572S \xf1\x890780',
+ ],
+ },
+ CAR.VOLKSWAGEN_PASSAT_NMS: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8706K906016C \xf1\x899609',
+ b'\xf1\x8706K906016E \xf1\x899830',
+ b'\xf1\x8706K906016G \xf1\x891124',
+ b'\xf1\x8706K906071BJ\xf1\x894891',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x8709G927158AB\xf1\x893318',
+ b'\xf1\x8709G927158BD\xf1\x893121',
+ b'\xf1\x8709G927158DK\xf1\x893594',
+ b'\xf1\x8709G927158FQ\xf1\x893745',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x87561959655 \xf1\x890210\xf1\x82\x1212121111113000102011--121012--101312',
+ b'\xf1\x87561959655C \xf1\x890508\xf1\x82\x1215141111121100314919--153015--304831',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x87561907567A \xf1\x890132',
+ b'\xf1\x877N0907572C \xf1\x890211\xf1\x82\x0152',
+ ],
+ },
+ CAR.VOLKSWAGEN_POLO_MK6: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704C906025H \xf1\x895177',
+ b'\xf1\x8705C906032J \xf1\x891702',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300042D \xf1\x891612',
+ b'\xf1\x870CW300050D \xf1\x891908',
+ b'\xf1\x870CW300051G \xf1\x891909',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x872Q0959655AG\xf1\x890248\xf1\x82\x1218130411110411--04040404231811152H14',
+ b'\xf1\x872Q0959655AJ\xf1\x890250\xf1\x82\x1248130411110416--04040404784811152H14',
+ b'\xf1\x872Q0959655AS\xf1\x890411\xf1\x82\x1384830511110516041405820599841215391471',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x872Q1909144M \xf1\x896041',
+ b'\xf1\x872Q2909144AB\xf1\x896050',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572R \xf1\x890372',
+ ],
+ },
+ CAR.VOLKSWAGEN_SHARAN_MK2: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704L906016HE\xf1\x894635',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x877N0959655D \xf1\x890016\xf1\x82\x0801100705----10--',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x877N0907572C \xf1\x890211\xf1\x82\x0153',
+ ],
+ },
+ CAR.VOLKSWAGEN_TAOS_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906025CK\xf1\x892228',
+ b'\xf1\x8704E906027NJ\xf1\x891445',
+ b'\xf1\x8704E906027NP\xf1\x891286',
+ b'\xf1\x8705E906013BD\xf1\x892496',
+ b'\xf1\x8705E906013E \xf1\x891624',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x8709G927158EM\xf1\x893812',
+ b'\xf1\x8709S927158BL\xf1\x893791',
+ b'\xf1\x8709S927158CR\xf1\x893924',
+ b'\xf1\x8709S927158DN\xf1\x893946',
+ b'\xf1\x8709S927158FF\xf1\x893876',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655CB\xf1\x890421\xf1\x82\x1311111111333500314646021450149333613100',
+ b'\xf1\x875Q0959655CB\xf1\x890421\xf1\x82\x1312111111333500314646021550159333613100',
+ b'\xf1\x875Q0959655CE\xf1\x890421\xf1\x82\x1311110011333300314240021350139333613100',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x875QM907144D \xf1\x891063\xf1\x82\x001O06081OOM',
+ b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521060405A1',
+ b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521060605A1',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ ],
+ },
+ CAR.VOLKSWAGEN_TCROSS_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704C906025AK\xf1\x897053',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300050E \xf1\x891903',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x872Q0959655AJ\xf1\x890250\xf1\x82\x1212130411110411--04041104141311152H14',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x872Q1909144M \xf1\x896041',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ ],
+ },
+ CAR.VOLKSWAGEN_TIGUAN_MK2: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8703N906026D \xf1\x893680',
+ b'\xf1\x8704E906024AP\xf1\x891461',
+ b'\xf1\x8704E906027NB\xf1\x899504',
+ b'\xf1\x8704L906026EJ\xf1\x893661',
+ b'\xf1\x8704L906026EJ\xf1\x893916',
+ b'\xf1\x8704L906027G \xf1\x899893',
+ b'\xf1\x8705E906018BS\xf1\x890914',
+ b'\xf1\x875N0906259 \xf1\x890002',
+ b'\xf1\x875NA906259H \xf1\x890002',
+ b'\xf1\x875NA907115E \xf1\x890003',
+ b'\xf1\x875NA907115E \xf1\x890005',
+ b'\xf1\x875NA907115J \xf1\x890002',
+ b'\xf1\x875NA907115K \xf1\x890004',
+ b'\xf1\x8783A907115 \xf1\x890007',
+ b'\xf1\x8783A907115B \xf1\x890005',
+ b'\xf1\x8783A907115F \xf1\x890002',
+ b'\xf1\x8783A907115G \xf1\x890001',
+ b'\xf1\x8783A907115K \xf1\x890001',
+ b'\xf1\x8783A907115K \xf1\x890002',
+ b'\xf1\x8783A907115Q \xf1\x890001',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x8709G927158DS\xf1\x893699',
+ b'\xf1\x8709G927158DT\xf1\x893698',
+ b'\xf1\x8709G927158FM\xf1\x893757',
+ b'\xf1\x8709G927158GC\xf1\x893821',
+ b'\xf1\x8709G927158GD\xf1\x893820',
+ b'\xf1\x8709G927158GM\xf1\x893936',
+ b'\xf1\x8709G927158GN\xf1\x893938',
+ b'\xf1\x8709G927158HB\xf1\x894069',
+ b'\xf1\x8709G927158HC\xf1\x894070',
+ b'\xf1\x870D9300043 \xf1\x895202',
+ b'\xf1\x870DD300046K \xf1\x892302',
+ b'\xf1\x870DL300011N \xf1\x892001',
+ b'\xf1\x870DL300011N \xf1\x892012',
+ b'\xf1\x870DL300011N \xf1\x892014',
+ b'\xf1\x870DL300012M \xf1\x892107',
+ b'\xf1\x870DL300012P \xf1\x892103',
+ b'\xf1\x870DL300013A \xf1\x893005',
+ b'\xf1\x870DL300013G \xf1\x892119',
+ b'\xf1\x870DL300013G \xf1\x892120',
+ b'\xf1\x870DL300014C \xf1\x893703',
+ b'\xf1\x870GC300013P \xf1\x892401',
+ b'\xf1\x870GC300046Q \xf1\x892802',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655AG\xf1\x890336\xf1\x82\x1316143231313500314617011730179333423100',
+ b'\xf1\x875Q0959655AG\xf1\x890338\xf1\x82\x1316143231313500314617011730179333423100',
+ b'\xf1\x875Q0959655AR\xf1\x890317\xf1\x82\x1331310031333334313132573732379333313100',
+ b'\xf1\x875Q0959655BJ\xf1\x890336\xf1\x82\x1311140031333300314232583632369333423100',
+ b'\xf1\x875Q0959655BJ\xf1\x890336\xf1\x82\x1312110031333300314232583732379333423100',
+ b'\xf1\x875Q0959655BJ\xf1\x890339\xf1\x82\x1331310031333334313132013730379333423100',
+ b'\xf1\x875Q0959655BM\xf1\x890403\xf1\x82\x1316143231313500314641011750179333423100',
+ b'\xf1\x875Q0959655BS\xf1\x890403\xf1\x82\x1312110031333300314240013750379333423100',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1312110031333300314240583752379333423100',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1331310031333334313140013750379333423100',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1331310031333334313140573752379333423100',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1331310031333336313140013950399333423100',
+ b'\xf1\x875Q0959655CB\xf1\x890421\xf1\x82\x1316143231313500314647021750179333613100',
+ b'\xf1\x875Q0959655CD\xf1\x890421\xf1\x82\x13123112313333003145406F6154619333613100',
+ b'\xf1\x875Q0959655CG\xf1\x890421\xf1\x82\x1331310031333300314240024050409333613100',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820529A6060603',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820527A6050705',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820527A6070705',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A60604A1',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567A6000600',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567A6017A00',
+ b'\xf1\x875QF909144 \xf1\x895572\xf1\x82\x0571A60833A1',
+ b'\xf1\x875QF909144A \xf1\x895581\xf1\x82\x0571A60834A1',
+ b'\xf1\x875QF909144B \xf1\x895582\xf1\x82\x0571A60634A1',
+ b'\xf1\x875QF909144B \xf1\x895582\xf1\x82\x0571A62A32A1',
+ b'\xf1\x875QM907144D \xf1\x891063\xf1\x82\x002RA60A2ROM',
+ b'\xf1\x875QM907144D \xf1\x891063\xf1\x82\x002SA6092SOM',
+ b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521A60604A1',
+ b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521A60804A1',
+ b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521A60604A1',
+ b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521A60804A1',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572AB\xf1\x890397',
+ b'\xf1\x872Q0907572J \xf1\x890156',
+ b'\xf1\x872Q0907572M \xf1\x890233',
+ b'\xf1\x872Q0907572Q \xf1\x890342',
+ b'\xf1\x872Q0907572R \xf1\x890372',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ ],
+ },
+ CAR.VOLKSWAGEN_TOURAN_MK2: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906025BE\xf1\x890720',
+ b'\xf1\x8704E906027HQ\xf1\x893746',
+ b'\xf1\x8704L906026HM\xf1\x893017',
+ b'\xf1\x8705E906018CQ\xf1\x890808',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300020A \xf1\x891936',
+ b'\xf1\x870CW300041E \xf1\x891005',
+ b'\xf1\x870CW300041Q \xf1\x891606',
+ b'\xf1\x870CW300051M \xf1\x891926',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655AS\xf1\x890318\xf1\x82\x1336350021353335314132014730479333313100',
+ b'\xf1\x875Q0959655AS\xf1\x890318\xf1\x82\x13363500213533353141324C4732479333313100',
+ b'\xf1\x875Q0959655CH\xf1\x890421\xf1\x82\x1336350021353336314740025250529333613100',
+ b'\xf1\x875QD959655AJ\xf1\x890421\xf1\x82\x1336350021313300314240023330339333663100',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820531B0062105',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567A8090400',
+ b'\xf1\x875QD909144F \xf1\x891082\xf1\x82\x0521A00642A1',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x873Q0907572C \xf1\x890195',
+ b'\xf1\x875Q0907572R \xf1\x890771',
+ ],
+ },
+ CAR.VOLKSWAGEN_TRANSPORTER_T61: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704L906056AG\xf1\x899970',
+ b'\xf1\x8704L906056AL\xf1\x899970',
+ b'\xf1\x8704L906057AP\xf1\x891186',
+ b'\xf1\x8704L906057N \xf1\x890413',
+ b'\xf1\x8705L906023E \xf1\x891352',
+ b'\xf1\x8705L906023MR\xf1\x892582',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870BT300012E \xf1\x893105',
+ b'\xf1\x870BT300012G \xf1\x893102',
+ b'\xf1\x870BT300046R \xf1\x893102',
+ b'\xf1\x870DV300012B \xf1\x893701',
+ b'\xf1\x870DV300012B \xf1\x893702',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x872Q0959655AE\xf1\x890506\xf1\x82\x1316170411110411--04041704161611152S1411',
+ b'\xf1\x872Q0959655AE\xf1\x890506\xf1\x82\x1316170411110411--04041704171711152S1411',
+ b'\xf1\x872Q0959655AF\xf1\x890506\xf1\x82\x1316171111110411--04041711121211152S1413',
+ b'\xf1\x872Q0959655AQ\xf1\x890511\xf1\x82\x1316170411110411--0404170426261215391421',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x877LA909144F \xf1\x897150\xf1\x82\x0532380518A2',
+ b'\xf1\x877LA909144F \xf1\x897150\xf1\x82\x05323A5519A2',
+ b'\xf1\x877LA909144G \xf1\x897160\xf1\x82\x05333A5519A2',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572R \xf1\x890372',
+ ],
+ },
+ CAR.VOLKSWAGEN_TROC_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8705E906018AT\xf1\x899640',
+ b'\xf1\x8705E906018CK\xf1\x890863',
+ b'\xf1\x8705E906018P \xf1\x896020',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300041S \xf1\x891615',
+ b'\xf1\x870CW300050J \xf1\x891911',
+ b'\xf1\x870CW300051M \xf1\x891925',
+ b'\xf1\x870CW300051M \xf1\x891928',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655BH\xf1\x890712\xf1\x82\x0e1111001111001105111111052900',
+ b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1311110012333300314240681152119333463100',
+ b'\xf1\x875Q0959655CF\xf1\x890421\xf1\x82\x1311110012333300314240021150119333613100',
+ b'\xf1\x875Q0959655CG\xf1\x890421\xf1\x82\x13111100123333003142404M1152119333613100',
+ b'\xf1\x875Q0959655CG\xf1\x890421\xf1\x82\x13111100123333003142404M1154119333613100',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521060403A1',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521060405A1',
+ b'\xf1\x875WA907144M \xf1\x891051\xf1\x82\x001T06081T7N',
+ b'\xf1\x875WA907144Q \xf1\x891063\xf1\x82\x001O06081OOM',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572M \xf1\x890233',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ ],
+ },
+ CAR.AUDI_A3_MK3: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906023AN\xf1\x893695',
+ b'\xf1\x8704E906023AR\xf1\x893440',
+ b'\xf1\x8704E906023BL\xf1\x895190',
+ b'\xf1\x8704E906027CJ\xf1\x897798',
+ b'\xf1\x8704L997022N \xf1\x899459',
+ b'\xf1\x875G0906259A \xf1\x890004',
+ b'\xf1\x875G0906259D \xf1\x890002',
+ b'\xf1\x875G0906259L \xf1\x890002',
+ b'\xf1\x875G0906259Q \xf1\x890002',
+ b'\xf1\x878V0906259E \xf1\x890001',
+ b'\xf1\x878V0906259F \xf1\x890002',
+ b'\xf1\x878V0906259H \xf1\x890002',
+ b'\xf1\x878V0906259J \xf1\x890002',
+ b'\xf1\x878V0906259K \xf1\x890001',
+ b'\xf1\x878V0906264B \xf1\x890003',
+ b'\xf1\x878V0907115B \xf1\x890007',
+ b'\xf1\x878V0907404A \xf1\x890005',
+ b'\xf1\x878V0907404G \xf1\x890005',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300044T \xf1\x895245',
+ b'\xf1\x870CW300048 \xf1\x895201',
+ b'\xf1\x870D9300012 \xf1\x894912',
+ b'\xf1\x870D9300012 \xf1\x894931',
+ b'\xf1\x870D9300012K \xf1\x894513',
+ b'\xf1\x870D9300012L \xf1\x894521',
+ b'\xf1\x870D9300013B \xf1\x894902',
+ b'\xf1\x870D9300013B \xf1\x894931',
+ b'\xf1\x870D9300041N \xf1\x894512',
+ b'\xf1\x870D9300043T \xf1\x899699',
+ b'\xf1\x870DD300046 \xf1\x891604',
+ b'\xf1\x870DD300046A \xf1\x891602',
+ b'\xf1\x870DD300046F \xf1\x891602',
+ b'\xf1\x870DD300046G \xf1\x891601',
+ b'\xf1\x870DL300012E \xf1\x892012',
+ b'\xf1\x870DL300012H \xf1\x892112',
+ b'\xf1\x870GC300011 \xf1\x890403',
+ b'\xf1\x870GC300013M \xf1\x892402',
+ b'\xf1\x870GC300042J \xf1\x891402',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655AB\xf1\x890388\xf1\x82\x111111001111111206110412111321139114',
+ b'\xf1\x875Q0959655AM\xf1\x890315\xf1\x82\x1311111111111111311411011231129321212100',
+ b'\xf1\x875Q0959655AM\xf1\x890318\xf1\x82\x1311111111111112311411011531159321212100',
+ b'\xf1\x875Q0959655AR\xf1\x890315\xf1\x82\x1311110011131115311211012331239321212100',
+ b'\xf1\x875Q0959655BJ\xf1\x890339\xf1\x82\x1311110011131100311111011731179321342100',
+ b'\xf1\x875Q0959655J \xf1\x890825\xf1\x82\x13111112111111--171115141112221291163221',
+ b'\xf1\x875Q0959655J \xf1\x890825\xf1\x82\x13111112111111--241115141112221291163221',
+ b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13111112111111--241115141112221291163221',
+ b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13121111111111--341117141212231291163221',
+ b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13121111111211--261117141112231291163221',
+ b'\xf1\x875Q0959655N \xf1\x890361\xf1\x82\x111212001112110004110411111421149114',
+ b'\xf1\x875Q0959655N \xf1\x890361\xf1\x82\x111212001112111104110411111521159114',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873Q0909144F \xf1\x895043\xf1\x82\x0561G01A13A0',
+ b'\xf1\x873Q0909144H \xf1\x895061\xf1\x82\x0566G0HA14A1',
+ b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566G0HA14A1',
+ b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571G01A16A1',
+ b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571G0HA16A1',
+ b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571G0JA13A1',
+ b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571G0JA14A1',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521G0G809A1',
+ b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0503G00303A0',
+ b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0503G00803A0',
+ b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0503G0G803A0',
+ b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\x0516G00804A1',
+ b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516G00804A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521G00807A1',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x875Q0907567M \xf1\x890398\xf1\x82\x0101',
+ b'\xf1\x875Q0907567N \xf1\x890400\xf1\x82\x0101',
+ b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\x0101',
+ b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\x0101',
+ b'\xf1\x875Q0907572G \xf1\x890571',
+ b'\xf1\x875Q0907572H \xf1\x890620',
+ b'\xf1\x875Q0907572P \xf1\x890682',
+ ],
+ },
+ CAR.AUDI_Q2_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906027JT\xf1\x894145',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300041F \xf1\x891006',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655BD\xf1\x890336\xf1\x82\x1311111111111100311211011231129321312111',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571F60511A1',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572M \xf1\x890233',
+ ],
+ },
+ CAR.AUDI_Q3_MK2: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8705E906018N \xf1\x899970',
+ b'\xf1\x8705L906022M \xf1\x890901',
+ b'\xf1\x8783A906259 \xf1\x890001',
+ b'\xf1\x8783A906259 \xf1\x890005',
+ b'\xf1\x8783A906259C \xf1\x890002',
+ b'\xf1\x8783A906259D \xf1\x890001',
+ b'\xf1\x8783A906259F \xf1\x890001',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x8709G927158CN\xf1\x893608',
+ b'\xf1\x8709G927158FL\xf1\x893758',
+ b'\xf1\x8709G927158GG\xf1\x893825',
+ b'\xf1\x8709G927158GP\xf1\x893937',
+ b'\xf1\x870GC300045D \xf1\x892802',
+ b'\xf1\x870GC300046F \xf1\x892701',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655BF\xf1\x890403\xf1\x82\x1321211111211200311121232152219321422111',
+ b'\xf1\x875Q0959655BQ\xf1\x890421\xf1\x82\x132121111121120031112124218A219321532111',
+ b'\xf1\x875Q0959655BQ\xf1\x890421\xf1\x82\x132121111121120031112124218C219321532111',
+ b'\xf1\x875Q0959655CC\xf1\x890421\xf1\x82\x131111111111120031111224118A119321532111',
+ b'\xf1\x875Q0959655CC\xf1\x890421\xf1\x82\x131111111111120031111237116A119321532111',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567G6000300',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567G6000800',
+ b'\xf1\x875QF909144B \xf1\x895582\xf1\x82\x0571G60533A1',
+ b'\xf1\x875QF909144B \xf1\x895582\xf1\x82\x0571G60733A1',
+ b'\xf1\x875TA907145D \xf1\x891051\xf1\x82\x001PG60A1P7N',
+ b'\xf1\x875TA907145F \xf1\x891063\xf1\x82\x002VG60A2VOM',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572R \xf1\x890372',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ ],
+ },
+ CAR.SEAT_ATECA_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906027KA\xf1\x893749',
+ b'\xf1\x8704L906021EL\xf1\x897542',
+ b'\xf1\x8704L906026BP\xf1\x891198',
+ b'\xf1\x8704L906026BP\xf1\x897608',
+ b'\xf1\x8704L906056CR\xf1\x892181',
+ b'\xf1\x8704L906056CR\xf1\x892797',
+ b'\xf1\x8705E906018AS\xf1\x899596',
+ b'\xf1\x8781A906259B \xf1\x890003',
+ b'\xf1\x878V0906264H \xf1\x890005',
+ b'\xf1\x878V0907115E \xf1\x890002',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300041D \xf1\x891004',
+ b'\xf1\x870CW300041G \xf1\x891003',
+ b'\xf1\x870CW300050J \xf1\x891908',
+ b'\xf1\x870D9300014S \xf1\x895202',
+ b'\xf1\x870D9300042M \xf1\x895016',
+ b'\xf1\x870GC300014P \xf1\x892801',
+ b'\xf1\x870GC300043A \xf1\x892304',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655AC\xf1\x890189\xf1\x82\r11110011110011021511110200',
+ b'\xf1\x873Q0959655AS\xf1\x890200\xf1\x82\r11110011110011021511110200',
+ b'\xf1\x873Q0959655AS\xf1\x890200\xf1\x82\r12110012120012021612110200',
+ b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e1212001211001305121211052900',
+ b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e1312001313001305171311052900',
+ b'\xf1\x873Q0959655BH\xf1\x890712\xf1\x82\x0e1312001313001305171311052900',
+ b'\xf1\x873Q0959655CM\xf1\x890720\xf1\x82\x0e1312001313001305171311052900',
+ b'\xf1\x875QF959655AT\xf1\x890755\xf1\x82\x1311110011110011111100110200--1113121149',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571N60511A1',
+ b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521N01842A1',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521N01342A1',
+ b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0511N01805A0',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521N01309A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521N05808A1',
+ b'\xf1\x875WA907145M \xf1\x891051\xf1\x82\x0013N619137N',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572M \xf1\x890233',
+ b'\xf1\x875Q0907572B \xf1\x890200\xf1\x82\x0101',
+ b'\xf1\x875Q0907572H \xf1\x890620',
+ b'\xf1\x875Q0907572K \xf1\x890402\xf1\x82\x0101',
+ b'\xf1\x875Q0907572P \xf1\x890682',
+ b'\xf1\x875Q0907572R \xf1\x890771',
+ ],
+ },
+ CAR.SKODA_FABIA_MK4: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8705E906018CF\xf1\x891905',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300051M \xf1\x891936',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875QF959655AT\xf1\x890755\xf1\x82\x1311110011110011111100110200--1111120749',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x872Q1909144S \xf1\x896042',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ ],
+ },
+ CAR.SKODA_KAMIQ_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704C906025AK\xf1\x897053',
+ b'\xf1\x8705C906032M \xf1\x891333',
+ b'\xf1\x8705C906032M \xf1\x892365',
+ b'\xf1\x8705E906013CK\xf1\x892540',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300020 \xf1\x891906',
+ b'\xf1\x870CW300020 \xf1\x891907',
+ b'\xf1\x870CW300020T \xf1\x892204',
+ b'\xf1\x870CW300050 \xf1\x891709',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x872Q0959655AJ\xf1\x890250\xf1\x82\x1211110411110411--04040404131111112H14',
+ b'\xf1\x872Q0959655AM\xf1\x890351\xf1\x82\x12111104111104112104040404111111112H14',
+ b'\xf1\x872Q0959655AM\xf1\x890351\xf1\x82\x122221042111042121040404042E2711152H14',
+ b'\xf1\x872Q0959655AS\xf1\x890411\xf1\x82\x1311150411110411210404040417151215391413',
+ b'\xf1\x872Q0959655BJ\xf1\x890412\xf1\x82\x132223042111042121040404042B251215391423',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x872Q1909144AB\xf1\x896050',
+ b'\xf1\x872Q1909144M \xf1\x896041',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572R \xf1\x890372',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ ],
+ },
+ CAR.SKODA_KAROQ_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8705E906013CL\xf1\x892541',
+ b'\xf1\x8705E906013H \xf1\x892407',
+ b'\xf1\x8705E906018P \xf1\x895472',
+ b'\xf1\x8705E906018P \xf1\x896020',
+ b'\xf1\x8705L906022BS\xf1\x890913',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300020T \xf1\x892202',
+ b'\xf1\x870CW300041S \xf1\x891615',
+ b'\xf1\x870GC300014L \xf1\x892802',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e1213001211001101131112012100',
+ b'\xf1\x873Q0959655BH\xf1\x890712\xf1\x82\x0e1213001211001101131122012100',
+ b'\xf1\x873Q0959655DE\xf1\x890731\xf1\x82\x0e1213001211001101131121012J00',
+ b'\xf1\x875QF959655AT\xf1\x890755\xf1\x82\x1312110012120011111100010200--2521210749',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x875Q0910143B \xf1\x892201\xf1\x82\x0563T6090500',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T6100500',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T6100600',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T6100700',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572AB\xf1\x890397',
+ b'\xf1\x872Q0907572M \xf1\x890233',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ ],
+ },
+ CAR.SKODA_KODIAQ_MK1: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906027DD\xf1\x893123',
+ b'\xf1\x8704E906027LD\xf1\x893433',
+ b'\xf1\x8704E906027NB\xf1\x896517',
+ b'\xf1\x8704E906027NB\xf1\x899504',
+ b'\xf1\x8704L906026DE\xf1\x895418',
+ b'\xf1\x8704L906026EJ\xf1\x893661',
+ b'\xf1\x8704L906026HT\xf1\x893617',
+ b'\xf1\x8705E906018DJ\xf1\x890915',
+ b'\xf1\x8705E906018DJ\xf1\x891903',
+ b'\xf1\x8705L906022GM\xf1\x893411',
+ b'\xf1\x875NA906259E \xf1\x890003',
+ b'\xf1\x875NA907115D \xf1\x890003',
+ b'\xf1\x875NA907115E \xf1\x890003',
+ b'\xf1\x875NA907115E \xf1\x890005',
+ b'\xf1\x8783A907115E \xf1\x890001',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870D9300014S \xf1\x895201',
+ b'\xf1\x870D9300043 \xf1\x895202',
+ b'\xf1\x870DL300011N \xf1\x892014',
+ b'\xf1\x870DL300012G \xf1\x892006',
+ b'\xf1\x870DL300012M \xf1\x892107',
+ b'\xf1\x870DL300012N \xf1\x892110',
+ b'\xf1\x870DL300013G \xf1\x892119',
+ b'\xf1\x870GC300014N \xf1\x892801',
+ b'\xf1\x870GC300018S \xf1\x892803',
+ b'\xf1\x870GC300019H \xf1\x892806',
+ b'\xf1\x870GC300046Q \xf1\x892802',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655AN\xf1\x890306\xf1\x82\r11110011110011031111310311',
+ b'\xf1\x873Q0959655AP\xf1\x890306\xf1\x82\r11110011110011421111314211',
+ b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e1213001211001205212111052100',
+ b'\xf1\x873Q0959655BJ\xf1\x890703\xf1\x82\x0e1213001211001205212111052100',
+ b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\x0e1213001211001244212111442100',
+ b'\xf1\x873Q0959655CN\xf1\x890720\xf1\x82\x0e1213001211001205212112052100',
+ b'\xf1\x873Q0959655CQ\xf1\x890720\xf1\x82\x0e1213111211001205212112052111',
+ b'\xf1\x873Q0959655DJ\xf1\x890731\xf1\x82\x0e1513001511001205232113052J00',
+ b'\xf1\x875QF959655AT\xf1\x890755\xf1\x82\x1311110011110011111100010200--1121240749',
+ b'\xf1\x875QF959655AT\xf1\x890755\xf1\x82\x1311110011110011111100010200--1121246149',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820527T6050405',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820527T6060405',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820527T6070405',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T600G500',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T600G600',
+ b'\xf1\x875TA907145F \xf1\x891063\xf1\x82\x0025T6BA25OM',
+ b'\xf1\x875TA907145F \xf1\x891063\xf1\x82\x002LT61A2LOM',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x872Q0907572AA\xf1\x890396',
+ b'\xf1\x872Q0907572AB\xf1\x890397',
+ b'\xf1\x872Q0907572M \xf1\x890233',
+ b'\xf1\x872Q0907572Q \xf1\x890342',
+ b'\xf1\x872Q0907572R \xf1\x890372',
+ b'\xf1\x872Q0907572T \xf1\x890383',
+ ],
+ },
+ CAR.SKODA_OCTAVIA_MK3: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704C906025L \xf1\x896198',
+ b'\xf1\x8704E906016ER\xf1\x895823',
+ b'\xf1\x8704E906027HD\xf1\x893742',
+ b'\xf1\x8704E906027MH\xf1\x894786',
+ b'\xf1\x8704L906021DT\xf1\x898127',
+ b'\xf1\x8704L906021ER\xf1\x898361',
+ b'\xf1\x8704L906026BP\xf1\x897608',
+ b'\xf1\x8704L906026BS\xf1\x891541',
+ b'\xf1\x8704L906026BT\xf1\x897612',
+ b'\xf1\x875G0906259C \xf1\x890002',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300041L \xf1\x891601',
+ b'\xf1\x870CW300041N \xf1\x891605',
+ b'\xf1\x870CW300043B \xf1\x891601',
+ b'\xf1\x870CW300043P \xf1\x891605',
+ b'\xf1\x870D9300012H \xf1\x894518',
+ b'\xf1\x870D9300014T \xf1\x895221',
+ b'\xf1\x870D9300041C \xf1\x894936',
+ b'\xf1\x870D9300041H \xf1\x895220',
+ b'\xf1\x870D9300041J \xf1\x894902',
+ b'\xf1\x870D9300041P \xf1\x894507',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x873Q0959655AC\xf1\x890200\xf1\x82\r11120011100010022212110200',
+ b'\xf1\x873Q0959655AK\xf1\x890306\xf1\x82\r31210031210021033733310331',
+ b'\xf1\x873Q0959655AP\xf1\x890305\xf1\x82\r11110011110011213331312131',
+ b'\xf1\x873Q0959655AQ\xf1\x890200\xf1\x82\r11120011100010312212113100',
+ b'\xf1\x873Q0959655AS\xf1\x890200\xf1\x82\r11120011100010022212110200',
+ b'\xf1\x873Q0959655BH\xf1\x890703\xf1\x82\x0e3221003221002105755331052100',
+ b'\xf1\x873Q0959655CM\xf1\x890720\xf1\x82\x0e3221003221002105755331052100',
+ b'\xf1\x873Q0959655CN\xf1\x890720\xf1\x82\x0e3221003221002105755331052100',
+ b'\xf1\x875QD959655 \xf1\x890388\xf1\x82\x111101000011110006110411111111119111',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566A01513A1',
+ b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521T00403A1',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521T00403A1',
+ b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521T00603A1',
+ b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\x0516A00604A1',
+ b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521T00601A1',
+ b'\xf1\x875QD909144E \xf1\x891081\xf1\x82\x0521T00503A1',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x875Q0907567P \xf1\x890100\xf1\x82\x0101',
+ b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\x0101',
+ b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\x0101',
+ b'\xf1\x875Q0907572H \xf1\x890620',
+ b'\xf1\x875Q0907572J \xf1\x890654',
+ b'\xf1\x875Q0907572K \xf1\x890402\xf1\x82\x0101',
+ b'\xf1\x875Q0907572P \xf1\x890682',
+ b'\xf1\x875Q0907572R \xf1\x890771',
+ ],
+ },
+ CAR.SKODA_SUPERB_MK3: {
+ (Ecu.engine, 0x7e0, None): [
+ b'\xf1\x8704E906027BS\xf1\x892887',
+ b'\xf1\x8704E906027BT\xf1\x899042',
+ b'\xf1\x8704L906026ET\xf1\x891343',
+ b'\xf1\x8704L906026ET\xf1\x891990',
+ b'\xf1\x8704L906026FP\xf1\x891196',
+ b'\xf1\x8704L906026KA\xf1\x896014',
+ b'\xf1\x8704L906026KB\xf1\x894071',
+ b'\xf1\x8704L906026KD\xf1\x894798',
+ b'\xf1\x8704L906026MT\xf1\x893076',
+ b'\xf1\x8705L906022BK\xf1\x899971',
+ b'\xf1\x873G0906259 \xf1\x890004',
+ b'\xf1\x873G0906259B \xf1\x890002',
+ b'\xf1\x873G0906259L \xf1\x890003',
+ b'\xf1\x873G0906264A \xf1\x890002',
+ ],
+ (Ecu.transmission, 0x7e1, None): [
+ b'\xf1\x870CW300042H \xf1\x891601',
+ b'\xf1\x870CW300043B \xf1\x891603',
+ b'\xf1\x870CW300049Q \xf1\x890906',
+ b'\xf1\x870D9300011T \xf1\x894801',
+ b'\xf1\x870D9300012 \xf1\x894940',
+ b'\xf1\x870D9300013A \xf1\x894905',
+ b'\xf1\x870D9300014K \xf1\x895006',
+ b'\xf1\x870D9300041H \xf1\x894905',
+ b'\xf1\x870D9300042M \xf1\x895013',
+ b'\xf1\x870D9300043F \xf1\x895202',
+ b'\xf1\x870GC300013K \xf1\x892403',
+ b'\xf1\x870GC300014M \xf1\x892801',
+ b'\xf1\x870GC300019G \xf1\x892803',
+ b'\xf1\x870GC300043 \xf1\x892301',
+ b'\xf1\x870GC300046D \xf1\x892402',
+ ],
+ (Ecu.srs, 0x715, None): [
+ b'\xf1\x875Q0959655AE\xf1\x890130\xf1\x82\x12111200111121001121110012211292221111',
+ b'\xf1\x875Q0959655AE\xf1\x890130\xf1\x82\x12111200111121001121118112231292221111',
+ b'\xf1\x875Q0959655AK\xf1\x890130\xf1\x82\x12111200111121001121110012211292221111',
+ b'\xf1\x875Q0959655AS\xf1\x890317\xf1\x82\x1331310031313100313131823133319331313100',
+ b'\xf1\x875Q0959655AT\xf1\x890317\xf1\x82\x1331310031313100313131013131319331313100',
+ b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\x1331310031313100313131013141319331413100',
+ b'\xf1\x875Q0959655BK\xf1\x890336\xf1\x82\x1331310031313100313131013141319331413100',
+ b'\xf1\x875Q0959655BS\xf1\x890403\xf1\x82\x1333310031313100313152015351539331423100',
+ b'\xf1\x875Q0959655CA\xf1\x890403\xf1\x82\x1331310031313100313151013141319331423100',
+ b'\xf1\x875Q0959655CA\xf1\x890403\xf1\x82\x1331310031313100313151823143319331423100',
+ b'\xf1\x875Q0959655CH\xf1\x890421\xf1\x82\x1333310031313100313152025350539331463100',
+ b'\xf1\x875Q0959655CH\xf1\x890421\xf1\x82\x1333310031313100313152855372539331463100',
+ ],
+ (Ecu.eps, 0x712, None): [
+ b'\xf1\x875Q0909143K \xf1\x892033\xf1\x820514UZ070203',
+ b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522UZ050303',
+ b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522UZ070303',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820526UZ060505',
+ b'\xf1\x875Q0909143P \xf1\x892051\xf1\x820526UZ070505',
+ b'\xf1\x875Q0910143B \xf1\x892201\xf1\x82\x0563UZ060600',
+ b'\xf1\x875Q0910143B \xf1\x892201\xf1\x82\x0563UZ060700',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567UZ070500',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567UZ070600',
+ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567UZ070700',
+ ],
+ (Ecu.fwdRadar, 0x757, None): [
+ b'\xf1\x873Q0907572B \xf1\x890192',
+ b'\xf1\x873Q0907572B \xf1\x890194',
+ b'\xf1\x873Q0907572C \xf1\x890195',
+ b'\xf1\x875Q0907572R \xf1\x890771',
+ b'\xf1\x875Q0907572S \xf1\x890780',
+ ],
+ },
+}
diff --git a/opendbc/car/volkswagen/interface.py b/opendbc/car/volkswagen/interface.py
new file mode 100644
index 0000000000..2361062fa5
--- /dev/null
+++ b/opendbc/car/volkswagen/interface.py
@@ -0,0 +1,87 @@
+from panda import Panda
+from opendbc.car import get_safety_config, structs
+from opendbc.car.interfaces import CarInterfaceBase
+from opendbc.car.volkswagen.values import CAR, NetworkLocation, TransmissionType, VolkswagenFlags
+
+
+class CarInterface(CarInterfaceBase):
+ @staticmethod
+ def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, experimental_long, docs) -> structs.CarParams:
+ ret.carName = "volkswagen"
+ ret.radarUnavailable = True
+
+ if ret.flags & VolkswagenFlags.PQ:
+ # Set global PQ35/PQ46/NMS parameters
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.volkswagenPq)]
+ ret.enableBsm = 0x3BA in fingerprint[0] # SWA_1
+
+ if 0x440 in fingerprint[0] or docs: # Getriebe_1
+ ret.transmissionType = TransmissionType.automatic
+ else:
+ ret.transmissionType = TransmissionType.manual
+
+ if any(msg in fingerprint[1] for msg in (0x1A0, 0xC2)): # Bremse_1, Lenkwinkel_1
+ ret.networkLocation = NetworkLocation.gateway
+ else:
+ ret.networkLocation = NetworkLocation.fwdCamera
+
+ # The PQ port is in dashcam-only mode due to a fixed six-minute maximum timer on HCA steering. An unsupported
+ # EPS flash update to work around this timer, and enable steering down to zero, is available from:
+ # https://github.com/pd0wm/pq-flasher
+ # It is documented in a four-part blog series:
+ # https://blog.willemmelching.nl/carhacking/2022/01/02/vw-part1/
+ # Panda ALLOW_DEBUG firmware required.
+ ret.dashcamOnly = True
+
+ else:
+ # Set global MQB parameters
+ ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.volkswagen)]
+ ret.enableBsm = 0x30F in fingerprint[0] # SWA_01
+
+ if 0xAD in fingerprint[0] or docs: # Getriebe_11
+ ret.transmissionType = TransmissionType.automatic
+ elif 0x187 in fingerprint[0]: # EV_Gearshift
+ ret.transmissionType = TransmissionType.direct
+ else:
+ ret.transmissionType = TransmissionType.manual
+
+ if any(msg in fingerprint[1] for msg in (0x40, 0x86, 0xB2, 0xFD)): # Airbag_01, LWI_01, ESP_19, ESP_21
+ ret.networkLocation = NetworkLocation.gateway
+ else:
+ ret.networkLocation = NetworkLocation.fwdCamera
+
+ if 0x126 in fingerprint[2]: # HCA_01
+ ret.flags |= VolkswagenFlags.STOCK_HCA_PRESENT.value
+
+ # Global lateral tuning defaults, can be overridden per-vehicle
+
+ ret.steerLimitTimer = 0.4
+ if ret.flags & VolkswagenFlags.PQ:
+ ret.steerActuatorDelay = 0.2
+ CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
+ else:
+ ret.steerActuatorDelay = 0.1
+ ret.lateralTuning.pid.kpBP = [0.]
+ ret.lateralTuning.pid.kiBP = [0.]
+ ret.lateralTuning.pid.kf = 0.00006
+ ret.lateralTuning.pid.kpV = [0.6]
+ ret.lateralTuning.pid.kiV = [0.2]
+
+ # Global longitudinal tuning defaults, can be overridden per-vehicle
+
+ ret.experimentalLongitudinalAvailable = ret.networkLocation == NetworkLocation.gateway or docs
+ if experimental_long:
+ # Proof-of-concept, prep for E2E only. No radar points available. Panda ALLOW_DEBUG firmware required.
+ ret.openpilotLongitudinalControl = True
+ ret.safetyConfigs[0].safetyParam |= Panda.FLAG_VOLKSWAGEN_LONG_CONTROL
+ if ret.transmissionType == TransmissionType.manual:
+ ret.minEnableSpeed = 4.5
+
+ ret.pcmCruise = not ret.openpilotLongitudinalControl
+ ret.stoppingControl = True
+ ret.stopAccel = -0.55
+ ret.vEgoStarting = 0.1
+ ret.vEgoStopping = 0.5
+ ret.autoResumeSng = ret.minEnableSpeed == -1
+
+ return ret
diff --git a/opendbc/car/volkswagen/mqbcan.py b/opendbc/car/volkswagen/mqbcan.py
new file mode 100644
index 0000000000..763908b6b2
--- /dev/null
+++ b/opendbc/car/volkswagen/mqbcan.py
@@ -0,0 +1,137 @@
+def create_steering_control(packer, bus, apply_steer, lkas_enabled):
+ values = {
+ "HCA_01_Status_HCA": 5 if lkas_enabled else 3,
+ "HCA_01_LM_Offset": abs(apply_steer),
+ "HCA_01_LM_OffSign": 1 if apply_steer < 0 else 0,
+ "HCA_01_Vib_Freq": 18,
+ "HCA_01_Sendestatus": 1 if lkas_enabled else 0,
+ "EA_ACC_Wunschgeschwindigkeit": 327.36,
+ }
+ return packer.make_can_msg("HCA_01", bus, values)
+
+
+def create_eps_update(packer, bus, eps_stock_values, ea_simulated_torque):
+ values = {s: eps_stock_values[s] for s in [
+ "COUNTER", # Sync counter value to EPS output
+ "EPS_Lenkungstyp", # EPS rack type
+ "EPS_Berechneter_LW", # Absolute raw steering angle
+ "EPS_VZ_BLW", # Raw steering angle sign
+ "EPS_HCA_Status", # EPS HCA control status
+ ]}
+
+ values.update({
+ # Absolute driver torque input and sign, with EA inactivity mitigation
+ "EPS_Lenkmoment": abs(ea_simulated_torque),
+ "EPS_VZ_Lenkmoment": 1 if ea_simulated_torque < 0 else 0,
+ })
+
+ return packer.make_can_msg("LH_EPS_03", bus, values)
+
+
+def create_lka_hud_control(packer, bus, ldw_stock_values, lat_active, steering_pressed, hud_alert, hud_control):
+ values = {}
+ if len(ldw_stock_values):
+ values = {s: ldw_stock_values[s] for s in [
+ "LDW_SW_Warnung_links", # Blind spot in warning mode on left side due to lane departure
+ "LDW_SW_Warnung_rechts", # Blind spot in warning mode on right side due to lane departure
+ "LDW_Seite_DLCTLC", # Direction of most likely lane departure (left or right)
+ "LDW_DLC", # Lane departure, distance to line crossing
+ "LDW_TLC", # Lane departure, time to line crossing
+ ]}
+
+ values.update({
+ "LDW_Status_LED_gelb": 1 if lat_active and steering_pressed else 0,
+ "LDW_Status_LED_gruen": 1 if lat_active and not steering_pressed else 0,
+ "LDW_Lernmodus_links": 3 if hud_control.leftLaneDepart else 1 + hud_control.leftLaneVisible,
+ "LDW_Lernmodus_rechts": 3 if hud_control.rightLaneDepart else 1 + hud_control.rightLaneVisible,
+ "LDW_Texte": hud_alert,
+ })
+ return packer.make_can_msg("LDW_02", bus, values)
+
+
+def create_acc_buttons_control(packer, bus, gra_stock_values, cancel=False, resume=False):
+ values = {s: gra_stock_values[s] for s in [
+ "GRA_Hauptschalter", # ACC button, on/off
+ "GRA_Typ_Hauptschalter", # ACC main button type
+ "GRA_Codierung", # ACC button configuration/coding
+ "GRA_Tip_Stufe_2", # unknown related to stalk type
+ "GRA_ButtonTypeInfo", # unknown related to stalk type
+ ]}
+
+ values.update({
+ "COUNTER": (gra_stock_values["COUNTER"] + 1) % 16,
+ "GRA_Abbrechen": cancel,
+ "GRA_Tip_Wiederaufnahme": resume,
+ })
+
+ return packer.make_can_msg("GRA_ACC_01", bus, values)
+
+
+def acc_control_value(main_switch_on, acc_faulted, long_active):
+ if acc_faulted:
+ acc_control = 6
+ elif long_active:
+ acc_control = 3
+ elif main_switch_on:
+ acc_control = 2
+ else:
+ acc_control = 0
+
+ return acc_control
+
+
+def acc_hud_status_value(main_switch_on, acc_faulted, long_active):
+ # TODO: happens to resemble the ACC control value for now, but extend this for init/gas override later
+ return acc_control_value(main_switch_on, acc_faulted, long_active)
+
+
+def create_acc_accel_control(packer, bus, acc_type, acc_enabled, accel, acc_control, stopping, starting, esp_hold):
+ commands = []
+
+ acc_06_values = {
+ "ACC_Typ": acc_type,
+ "ACC_Status_ACC": acc_control,
+ "ACC_StartStopp_Info": acc_enabled,
+ "ACC_Sollbeschleunigung_02": accel if acc_enabled else 3.01,
+ "ACC_zul_Regelabw_unten": 0.2, # TODO: dynamic adjustment of comfort-band
+ "ACC_zul_Regelabw_oben": 0.2, # TODO: dynamic adjustment of comfort-band
+ "ACC_neg_Sollbeschl_Grad_02": 4.0 if acc_enabled else 0, # TODO: dynamic adjustment of jerk limits
+ "ACC_pos_Sollbeschl_Grad_02": 4.0 if acc_enabled else 0, # TODO: dynamic adjustment of jerk limits
+ "ACC_Anfahren": starting,
+ "ACC_Anhalten": stopping,
+ }
+ commands.append(packer.make_can_msg("ACC_06", bus, acc_06_values))
+
+ if starting:
+ acc_hold_type = 4 # hold release / startup
+ elif esp_hold:
+ acc_hold_type = 3 # hold standby
+ elif stopping:
+ acc_hold_type = 1 # hold request
+ else:
+ acc_hold_type = 0
+
+ acc_07_values = {
+ "ACC_Anhalteweg": 0.3 if stopping else 20.46, # Distance to stop (stopping coordinator handles terminal roll-out)
+ "ACC_Freilauf_Info": 2 if acc_enabled else 0,
+ "ACC_Folgebeschl": 3.02, # Not using secondary controller accel unless and until we understand its impact
+ "ACC_Sollbeschleunigung_02": accel if acc_enabled else 3.01,
+ "ACC_Anforderung_HMS": acc_hold_type,
+ "ACC_Anfahren": starting,
+ "ACC_Anhalten": stopping,
+ }
+ commands.append(packer.make_can_msg("ACC_07", bus, acc_07_values))
+
+ return commands
+
+
+def create_acc_hud_control(packer, bus, acc_hud_status, set_speed, lead_distance, distance):
+ values = {
+ "ACC_Status_Anzeige": acc_hud_status,
+ "ACC_Wunschgeschw_02": set_speed if set_speed < 250 else 327.36,
+ "ACC_Gesetzte_Zeitluecke": distance + 2,
+ "ACC_Display_Prio": 3,
+ "ACC_Abstandsindex": lead_distance,
+ }
+
+ return packer.make_can_msg("ACC_02", bus, values)
diff --git a/opendbc/car/volkswagen/pqcan.py b/opendbc/car/volkswagen/pqcan.py
new file mode 100644
index 0000000000..f8d161b970
--- /dev/null
+++ b/opendbc/car/volkswagen/pqcan.py
@@ -0,0 +1,105 @@
+def create_steering_control(packer, bus, apply_steer, lkas_enabled):
+ values = {
+ "LM_Offset": abs(apply_steer),
+ "LM_OffSign": 1 if apply_steer < 0 else 0,
+ "HCA_Status": 5 if (lkas_enabled and apply_steer != 0) else 3,
+ "Vib_Freq": 16,
+ }
+
+ return packer.make_can_msg("HCA_1", bus, values)
+
+
+def create_lka_hud_control(packer, bus, ldw_stock_values, lat_active, steering_pressed, hud_alert, hud_control):
+ values = {}
+ if len(ldw_stock_values):
+ values = {s: ldw_stock_values[s] for s in [
+ "LDW_SW_Warnung_links", # Blind spot in warning mode on left side due to lane departure
+ "LDW_SW_Warnung_rechts", # Blind spot in warning mode on right side due to lane departure
+ "LDW_Seite_DLCTLC", # Direction of most likely lane departure (left or right)
+ "LDW_DLC", # Lane departure, distance to line crossing
+ "LDW_TLC", # Lane departure, time to line crossing
+ ]}
+
+ values.update({
+ "LDW_Lampe_gelb": 1 if lat_active and steering_pressed else 0,
+ "LDW_Lampe_gruen": 1 if lat_active and not steering_pressed else 0,
+ "LDW_Lernmodus_links": 3 if hud_control.leftLaneDepart else 1 + hud_control.leftLaneVisible,
+ "LDW_Lernmodus_rechts": 3 if hud_control.rightLaneDepart else 1 + hud_control.rightLaneVisible,
+ "LDW_Textbits": hud_alert,
+ })
+
+ return packer.make_can_msg("LDW_Status", bus, values)
+
+
+def create_acc_buttons_control(packer, bus, gra_stock_values, cancel=False, resume=False):
+ values = {s: gra_stock_values[s] for s in [
+ "GRA_Hauptschalt", # ACC button, on/off
+ "GRA_Typ_Hauptschalt", # ACC button, momentary vs latching
+ "GRA_Kodierinfo", # ACC button, configuration
+ "GRA_Sender", # ACC button, CAN message originator
+ ]}
+
+ values.update({
+ "COUNTER": (gra_stock_values["COUNTER"] + 1) % 16,
+ "GRA_Abbrechen": cancel,
+ "GRA_Recall": resume,
+ })
+
+ return packer.make_can_msg("GRA_Neu", bus, values)
+
+
+def acc_control_value(main_switch_on, acc_faulted, long_active):
+ if long_active:
+ acc_control = 1
+ elif main_switch_on:
+ acc_control = 2
+ else:
+ acc_control = 0
+
+ return acc_control
+
+
+def acc_hud_status_value(main_switch_on, acc_faulted, long_active):
+ if acc_faulted:
+ hud_status = 6
+ elif long_active:
+ hud_status = 3
+ elif main_switch_on:
+ hud_status = 2
+ else:
+ hud_status = 0
+
+ return hud_status
+
+
+def create_acc_accel_control(packer, bus, acc_type, acc_enabled, accel, acc_control, stopping, starting, esp_hold):
+ commands = []
+
+ values = {
+ "ACS_Sta_ADR": acc_control,
+ "ACS_StSt_Info": acc_enabled,
+ "ACS_Typ_ACC": acc_type,
+ "ACS_Anhaltewunsch": acc_type == 1 and stopping,
+ "ACS_FreigSollB": acc_enabled,
+ "ACS_Sollbeschl": accel if acc_enabled else 3.01,
+ "ACS_zul_Regelabw": 0.2 if acc_enabled else 1.27,
+ "ACS_max_AendGrad": 3.0 if acc_enabled else 5.08,
+ }
+
+ commands.append(packer.make_can_msg("ACC_System", bus, values))
+
+ return commands
+
+
+def create_acc_hud_control(packer, bus, acc_hud_status, set_speed, lead_distance, distance):
+ values = {
+ "ACA_StaACC": acc_hud_status,
+ "ACA_Zeitluecke": distance + 2,
+ "ACA_V_Wunsch": set_speed,
+ "ACA_gemZeitl": lead_distance,
+ "ACA_PrioDisp": 3,
+ # TODO: restore dynamic pop-to-foreground/highlight behavior with ACA_PrioDisp and ACA_AnzDisplay
+ # TODO: ACA_kmh_mph handling probably needed to resolve rounding errors in displayed setpoint
+ }
+
+ return packer.make_can_msg("ACC_GRA_Anzeige", bus, values)
diff --git a/opendbc/car/volkswagen/radar_interface.py b/opendbc/car/volkswagen/radar_interface.py
new file mode 100644
index 0000000000..6e552bf618
--- /dev/null
+++ b/opendbc/car/volkswagen/radar_interface.py
@@ -0,0 +1,4 @@
+from opendbc.car.interfaces import RadarInterfaceBase
+
+class RadarInterface(RadarInterfaceBase):
+ pass
diff --git a/opendbc/car/volkswagen/tests/test_volkswagen.py b/opendbc/car/volkswagen/tests/test_volkswagen.py
new file mode 100644
index 0000000000..cc046e5720
--- /dev/null
+++ b/opendbc/car/volkswagen/tests/test_volkswagen.py
@@ -0,0 +1,60 @@
+import random
+import re
+
+from opendbc.car.structs import CarParams
+from opendbc.car.volkswagen.values import CAR, FW_QUERY_CONFIG, WMI
+from opendbc.car.volkswagen.fingerprints import FW_VERSIONS
+
+Ecu = CarParams.Ecu
+
+CHASSIS_CODE_PATTERN = re.compile('[A-Z0-9]{2}')
+# TODO: determine the unknown groups
+SPARE_PART_FW_PATTERN = re.compile(b'\xf1\x87(?P[0-9][0-9A-Z]{2})(?P[0-9][0-9A-Z][0-9])(?P[0-9A-Z]{2}[0-9])([A-Z0-9]| )')
+
+
+class TestVolkswagenPlatformConfigs:
+ def test_spare_part_fw_pattern(self, subtests):
+ # Relied on for determining if a FW is likely VW
+ for platform, ecus in FW_VERSIONS.items():
+ with subtests.test(platform=platform.value):
+ for fws in ecus.values():
+ for fw in fws:
+ assert SPARE_PART_FW_PATTERN.match(fw) is not None, f"Bad FW: {fw}"
+
+ def test_chassis_codes(self, subtests):
+ for platform in CAR:
+ with subtests.test(platform=platform.value):
+ assert len(platform.config.wmis) > 0, "WMIs not set"
+ assert len(platform.config.chassis_codes) > 0, "Chassis codes not set"
+ assert all(CHASSIS_CODE_PATTERN.match(cc) for cc in \
+ platform.config.chassis_codes), "Bad chassis codes"
+
+ # No two platforms should share chassis codes
+ for comp in CAR:
+ if platform == comp:
+ continue
+ assert set() == platform.config.chassis_codes & comp.config.chassis_codes, \
+ f"Shared chassis codes: {comp}"
+
+ def test_custom_fuzzy_fingerprinting(self, subtests):
+ all_radar_fw = list({fw for ecus in FW_VERSIONS.values() for fw in ecus[Ecu.fwdRadar, 0x757, None]})
+
+ for platform in CAR:
+ with subtests.test(platform=platform.name):
+ for wmi in WMI:
+ for chassis_code in platform.config.chassis_codes | {"00"}:
+ vin = ["0"] * 17
+ vin[0:3] = wmi
+ vin[6:8] = chassis_code
+ vin = "".join(vin)
+
+ # Check a few FW cases - expected, unexpected
+ for radar_fw in random.sample(all_radar_fw, 5) + [b'\xf1\x875Q0907572G \xf1\x890571', b'\xf1\x877H9907572AA\xf1\x890396']:
+ should_match = ((wmi in platform.config.wmis and chassis_code in platform.config.chassis_codes) and
+ radar_fw in all_radar_fw)
+
+ live_fws = {(0x757, None): [radar_fw]}
+ matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fws, vin, FW_VERSIONS)
+
+ expected_matches = {platform} if should_match else set()
+ assert expected_matches == matches, "Bad match"
diff --git a/opendbc/car/volkswagen/values.py b/opendbc/car/volkswagen/values.py
new file mode 100644
index 0000000000..ca13f37ad2
--- /dev/null
+++ b/opendbc/car/volkswagen/values.py
@@ -0,0 +1,516 @@
+from collections import defaultdict, namedtuple
+from dataclasses import dataclass, field
+from enum import Enum, IntFlag, StrEnum
+
+from panda import uds
+from opendbc.can.can_define import CANDefine
+from opendbc.car import dbc_dict, CarSpecs, DbcDict, PlatformConfig, Platforms
+from opendbc.car.common.conversions import Conversions as CV
+from opendbc.car import structs
+from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \
+ Device
+from opendbc.car.fw_query_definitions import EcuAddrSubAddr, FwQueryConfig, Request, p16
+
+Ecu = structs.CarParams.Ecu
+NetworkLocation = structs.CarParams.NetworkLocation
+TransmissionType = structs.CarParams.TransmissionType
+GearShifter = structs.CarState.GearShifter
+Button = namedtuple('Button', ['event_type', 'can_addr', 'can_msg', 'values'])
+
+
+class CarControllerParams:
+ STEER_STEP = 2 # HCA_01/HCA_1 message frequency 50Hz
+ ACC_CONTROL_STEP = 2 # ACC_06/ACC_07/ACC_System frequency 50Hz
+
+ # Documented lateral limits: 3.00 Nm max, rate of change 5.00 Nm/sec.
+ # MQB vs PQ maximums are shared, but rate-of-change limited differently
+ # based on safety requirements driven by lateral accel testing.
+
+ STEER_MAX = 300 # Max heading control assist torque 3.00 Nm
+ STEER_DRIVER_MULTIPLIER = 3 # weight driver torque heavily
+ STEER_DRIVER_FACTOR = 1 # from dbc
+
+ STEER_TIME_MAX = 360 # Max time that EPS allows uninterrupted HCA steering control
+ STEER_TIME_ALERT = STEER_TIME_MAX - 10 # If mitigation fails, time to soft disengage before EPS timer expires
+ STEER_TIME_STUCK_TORQUE = 1.9 # EPS limits same torque to 6 seconds, reset timer 3x within that period
+
+ DEFAULT_MIN_STEER_SPEED = 0.4 # m/s, newer EPS racks fault below this speed, don't show a low speed alert
+
+ ACCEL_MAX = 2.0 # 2.0 m/s max acceleration
+ ACCEL_MIN = -3.5 # 3.5 m/s max deceleration
+
+ def __init__(self, CP):
+ can_define = CANDefine(DBC[CP.carFingerprint]["pt"])
+
+ if CP.flags & VolkswagenFlags.PQ:
+ self.LDW_STEP = 5 # LDW_1 message frequency 20Hz
+ self.ACC_HUD_STEP = 4 # ACC_GRA_Anzeige frequency 25Hz
+ self.STEER_DRIVER_ALLOWANCE = 80 # Driver intervention threshold 0.8 Nm
+ self.STEER_DELTA_UP = 6 # Max HCA reached in 1.00s (STEER_MAX / (50Hz * 1.00))
+ self.STEER_DELTA_DOWN = 10 # Min HCA reached in 0.60s (STEER_MAX / (50Hz * 0.60))
+
+ if CP.transmissionType == TransmissionType.automatic:
+ self.shifter_values = can_define.dv["Getriebe_1"]["Waehlhebelposition__Getriebe_1_"]
+ self.hca_status_values = can_define.dv["Lenkhilfe_2"]["LH2_Sta_HCA"]
+
+ self.BUTTONS = [
+ Button(structs.CarState.ButtonEvent.Type.setCruise, "GRA_Neu", "GRA_Neu_Setzen", [1]),
+ Button(structs.CarState.ButtonEvent.Type.resumeCruise, "GRA_Neu", "GRA_Recall", [1]),
+ Button(structs.CarState.ButtonEvent.Type.accelCruise, "GRA_Neu", "GRA_Up_kurz", [1]),
+ Button(structs.CarState.ButtonEvent.Type.decelCruise, "GRA_Neu", "GRA_Down_kurz", [1]),
+ Button(structs.CarState.ButtonEvent.Type.cancel, "GRA_Neu", "GRA_Abbrechen", [1]),
+ Button(structs.CarState.ButtonEvent.Type.gapAdjustCruise, "GRA_Neu", "GRA_Zeitluecke", [1]),
+ ]
+
+ self.LDW_MESSAGES = {
+ "none": 0, # Nothing to display
+ "laneAssistUnavail": 1, # "Lane Assist currently not available."
+ "laneAssistUnavailSysError": 2, # "Lane Assist system error"
+ "laneAssistUnavailNoSensorView": 3, # "Lane Assist not available. No sensor view."
+ "laneAssistTakeOver": 4, # "Lane Assist: Please Take Over Steering"
+ "laneAssistDeactivTrailer": 5, # "Lane Assist: no function with trailer"
+ }
+
+ else:
+ self.LDW_STEP = 10 # LDW_02 message frequency 10Hz
+ self.ACC_HUD_STEP = 6 # ACC_02 message frequency 16Hz
+ self.STEER_DRIVER_ALLOWANCE = 80 # Driver intervention threshold 0.8 Nm
+ self.STEER_DELTA_UP = 4 # Max HCA reached in 1.50s (STEER_MAX / (50Hz * 1.50))
+ self.STEER_DELTA_DOWN = 10 # Min HCA reached in 0.60s (STEER_MAX / (50Hz * 0.60))
+
+ if CP.transmissionType == TransmissionType.automatic:
+ self.shifter_values = can_define.dv["Getriebe_11"]["GE_Fahrstufe"]
+ elif CP.transmissionType == TransmissionType.direct:
+ self.shifter_values = can_define.dv["EV_Gearshift"]["GearPosition"]
+ self.hca_status_values = can_define.dv["LH_EPS_03"]["EPS_HCA_Status"]
+
+ self.BUTTONS = [
+ Button(structs.CarState.ButtonEvent.Type.setCruise, "GRA_ACC_01", "GRA_Tip_Setzen", [1]),
+ Button(structs.CarState.ButtonEvent.Type.resumeCruise, "GRA_ACC_01", "GRA_Tip_Wiederaufnahme", [1]),
+ Button(structs.CarState.ButtonEvent.Type.accelCruise, "GRA_ACC_01", "GRA_Tip_Hoch", [1]),
+ Button(structs.CarState.ButtonEvent.Type.decelCruise, "GRA_ACC_01", "GRA_Tip_Runter", [1]),
+ Button(structs.CarState.ButtonEvent.Type.cancel, "GRA_ACC_01", "GRA_Abbrechen", [1]),
+ Button(structs.CarState.ButtonEvent.Type.gapAdjustCruise, "GRA_ACC_01", "GRA_Verstellung_Zeitluecke", [1]),
+ ]
+
+ self.LDW_MESSAGES = {
+ "none": 0, # Nothing to display
+ "laneAssistUnavailChime": 1, # "Lane Assist currently not available." with chime
+ "laneAssistUnavailNoSensorChime": 3, # "Lane Assist not available. No sensor view." with chime
+ "laneAssistTakeOverUrgent": 4, # "Lane Assist: Please Take Over Steering" with urgent beep
+ "emergencyAssistUrgent": 6, # "Emergency Assist: Please Take Over Steering" with urgent beep
+ "laneAssistTakeOverChime": 7, # "Lane Assist: Please Take Over Steering" with chime
+ "laneAssistTakeOver": 8, # "Lane Assist: Please Take Over Steering" silent
+ "emergencyAssistChangingLanes": 9, # "Emergency Assist: Changing lanes..." with urgent beep
+ "laneAssistDeactivated": 10, # "Lane Assist deactivated." silent with persistent icon afterward
+ }
+
+
+class CANBUS:
+ pt = 0
+ cam = 2
+
+
+class WMI(StrEnum):
+ VOLKSWAGEN_USA_SUV = "1V2"
+ VOLKSWAGEN_USA_CAR = "1VW"
+ VOLKSWAGEN_MEXICO_SUV = "3VV"
+ VOLKSWAGEN_MEXICO_CAR = "3VW"
+ VOLKSWAGEN_ARGENTINA = "8AW"
+ VOLKSWAGEN_BRASIL = "9BW"
+ SAIC_VOLKSWAGEN = "LSV"
+ SKODA = "TMB"
+ SEAT = "VSS"
+ AUDI_EUROPE_MPV = "WA1"
+ AUDI_GERMANY_CAR = "WAU"
+ MAN = "WMA"
+ AUDI_SPORT = "WUA"
+ VOLKSWAGEN_COMMERCIAL = "WV1"
+ VOLKSWAGEN_COMMERCIAL_BUS_VAN = "WV2"
+ VOLKSWAGEN_EUROPE_SUV = "WVG"
+ VOLKSWAGEN_EUROPE_CAR = "WVW"
+ VOLKSWAGEN_GROUP_RUS = "XW8"
+
+
+class VolkswagenFlags(IntFlag):
+ # Detected flags
+ STOCK_HCA_PRESENT = 1
+
+ # Static flags
+ PQ = 2
+
+
+@dataclass
+class VolkswagenMQBPlatformConfig(PlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('vw_mqb_2010', None))
+ # Volkswagen uses the VIN WMI and chassis code to match in the absence of the comma power
+ # on camera-integrated cars, as we lose too many ECUs to reliably identify the vehicle
+ chassis_codes: set[str] = field(default_factory=set)
+ wmis: set[WMI] = field(default_factory=set)
+
+
+@dataclass
+class VolkswagenPQPlatformConfig(VolkswagenMQBPlatformConfig):
+ dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('vw_golf_mk4', None))
+
+ def init(self):
+ self.flags |= VolkswagenFlags.PQ
+
+
+@dataclass(frozen=True, kw_only=True)
+class VolkswagenCarSpecs(CarSpecs):
+ centerToFrontRatio: float = 0.45
+ steerRatio: float = 15.6
+ minSteerSpeed: float = CarControllerParams.DEFAULT_MIN_STEER_SPEED
+
+
+class Footnote(Enum):
+ KAMIQ = CarFootnote(
+ "Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.",
+ Column.MODEL)
+ PASSAT = CarFootnote(
+ "Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.",
+ Column.MODEL)
+ SKODA_HEATED_WINDSHIELD = CarFootnote(
+ "Some Škoda vehicles are equipped with heated windshields, which are known " +
+ "to block GPS signal needed for some comma 3X functionality.",
+ Column.MODEL)
+ VW_EXP_LONG = CarFootnote(
+ "Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness " +
+ "are limited to using stock ACC.",
+ Column.LONGITUDINAL)
+ VW_MQB_A0 = CarFootnote(
+ "Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot " +
+ "in software, but doesn't yet have a harness available from the comma store.",
+ Column.HARDWARE)
+
+
+@dataclass
+class VWCarDocs(CarDocs):
+ package: str = "Adaptive Cruise Control (ACC) & Lane Assist"
+ car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.vw_j533]))
+
+ def init_make(self, CP: structs.CarParams):
+ self.footnotes.append(Footnote.VW_EXP_LONG)
+ if "SKODA" in CP.carFingerprint:
+ self.footnotes.append(Footnote.SKODA_HEATED_WINDSHIELD)
+
+ if CP.carFingerprint in (CAR.VOLKSWAGEN_CRAFTER_MK2, CAR.VOLKSWAGEN_TRANSPORTER_T61):
+ self.car_parts = CarParts([Device.threex_angled_mount, CarHarness.vw_j533])
+
+ if abs(CP.minSteerSpeed - CarControllerParams.DEFAULT_MIN_STEER_SPEED) < 1e-3:
+ self.min_steer_speed = 0
+
+
+# Check the 7th and 8th characters of the VIN before adding a new CAR. If the
+# chassis code is already listed below, don't add a new CAR, just add to the
+# FW_VERSIONS for that existing CAR.
+
+class CAR(Platforms):
+ config: VolkswagenMQBPlatformConfig | VolkswagenPQPlatformConfig
+
+ VOLKSWAGEN_ARTEON_MK1 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Arteon 2018-23", video_link="https://youtu.be/FAomFKPFlDA"),
+ VWCarDocs("Volkswagen Arteon R 2020-23", video_link="https://youtu.be/FAomFKPFlDA"),
+ VWCarDocs("Volkswagen Arteon eHybrid 2020-23", video_link="https://youtu.be/FAomFKPFlDA"),
+ VWCarDocs("Volkswagen Arteon Shooting Brake 2020-23", video_link="https://youtu.be/FAomFKPFlDA"),
+ VWCarDocs("Volkswagen CC 2018-22", video_link="https://youtu.be/FAomFKPFlDA"),
+ ],
+ VolkswagenCarSpecs(mass=1733, wheelbase=2.84),
+ chassis_codes={"AN", "3H"},
+ wmis={WMI.VOLKSWAGEN_EUROPE_CAR},
+ )
+ VOLKSWAGEN_ATLAS_MK1 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Atlas 2018-23"),
+ VWCarDocs("Volkswagen Atlas Cross Sport 2020-22"),
+ VWCarDocs("Volkswagen Teramont 2018-22"),
+ VWCarDocs("Volkswagen Teramont Cross Sport 2021-22"),
+ VWCarDocs("Volkswagen Teramont X 2021-22"),
+ ],
+ VolkswagenCarSpecs(mass=2011, wheelbase=2.98),
+ chassis_codes={"CA"},
+ wmis={WMI.VOLKSWAGEN_USA_SUV, WMI.VOLKSWAGEN_EUROPE_SUV},
+ )
+ VOLKSWAGEN_CADDY_MK3 = VolkswagenPQPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Caddy 2019"),
+ VWCarDocs("Volkswagen Caddy Maxi 2019"),
+ ],
+ VolkswagenCarSpecs(mass=1613, wheelbase=2.6, minSteerSpeed=21 * CV.KPH_TO_MS),
+ chassis_codes={"2K"},
+ wmis={WMI.VOLKSWAGEN_COMMERCIAL_BUS_VAN},
+ )
+ VOLKSWAGEN_CRAFTER_MK2 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Crafter 2017-24", video_link="https://youtu.be/4100gLeabmo"),
+ VWCarDocs("Volkswagen e-Crafter 2018-24", video_link="https://youtu.be/4100gLeabmo"),
+ VWCarDocs("Volkswagen Grand California 2019-24", video_link="https://youtu.be/4100gLeabmo"),
+ VWCarDocs("MAN TGE 2017-24", video_link="https://youtu.be/4100gLeabmo"),
+ VWCarDocs("MAN eTGE 2020-24", video_link="https://youtu.be/4100gLeabmo"),
+ ],
+ VolkswagenCarSpecs(mass=2100, wheelbase=3.64, minSteerSpeed=50 * CV.KPH_TO_MS),
+ chassis_codes={"SY", "SZ", "UY", "UZ"},
+ wmis={WMI.VOLKSWAGEN_COMMERCIAL, WMI.MAN},
+ )
+ VOLKSWAGEN_GOLF_MK7 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen e-Golf 2014-20"),
+ VWCarDocs("Volkswagen Golf 2015-20", auto_resume=False),
+ VWCarDocs("Volkswagen Golf Alltrack 2015-19", auto_resume=False),
+ VWCarDocs("Volkswagen Golf GTD 2015-20"),
+ VWCarDocs("Volkswagen Golf GTE 2015-20"),
+ VWCarDocs("Volkswagen Golf GTI 2015-21", auto_resume=False),
+ VWCarDocs("Volkswagen Golf R 2015-19"),
+ VWCarDocs("Volkswagen Golf SportsVan 2015-20"),
+ ],
+ VolkswagenCarSpecs(mass=1397, wheelbase=2.62),
+ chassis_codes={"5G", "AU", "BA", "BE"},
+ wmis={WMI.VOLKSWAGEN_MEXICO_CAR, WMI.VOLKSWAGEN_EUROPE_CAR},
+ )
+ VOLKSWAGEN_JETTA_MK7 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Jetta 2018-24"),
+ VWCarDocs("Volkswagen Jetta GLI 2021-24"),
+ ],
+ VolkswagenCarSpecs(mass=1328, wheelbase=2.71),
+ chassis_codes={"BU"},
+ wmis={WMI.VOLKSWAGEN_MEXICO_CAR, WMI.VOLKSWAGEN_EUROPE_CAR},
+ )
+ VOLKSWAGEN_PASSAT_MK8 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Passat 2015-22", footnotes=[Footnote.PASSAT]),
+ VWCarDocs("Volkswagen Passat Alltrack 2015-22"),
+ VWCarDocs("Volkswagen Passat GTE 2015-22"),
+ ],
+ VolkswagenCarSpecs(mass=1551, wheelbase=2.79),
+ chassis_codes={"3C", "3G"},
+ wmis={WMI.VOLKSWAGEN_EUROPE_CAR},
+ )
+ VOLKSWAGEN_PASSAT_NMS = VolkswagenPQPlatformConfig(
+ [VWCarDocs("Volkswagen Passat NMS 2017-22")],
+ VolkswagenCarSpecs(mass=1503, wheelbase=2.80, minSteerSpeed=50 * CV.KPH_TO_MS, minEnableSpeed=20 * CV.KPH_TO_MS),
+ chassis_codes={"A3"},
+ wmis={WMI.VOLKSWAGEN_USA_CAR},
+ )
+ VOLKSWAGEN_POLO_MK6 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Polo 2018-23", footnotes=[Footnote.VW_MQB_A0]),
+ VWCarDocs("Volkswagen Polo GTI 2018-23", footnotes=[Footnote.VW_MQB_A0]),
+ ],
+ VolkswagenCarSpecs(mass=1230, wheelbase=2.55),
+ chassis_codes={"AW"},
+ wmis={WMI.VOLKSWAGEN_EUROPE_CAR},
+ )
+ VOLKSWAGEN_SHARAN_MK2 = VolkswagenPQPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Sharan 2018-22"),
+ VWCarDocs("SEAT Alhambra 2018-20"),
+ ],
+ VolkswagenCarSpecs(mass=1639, wheelbase=2.92, minSteerSpeed=50 * CV.KPH_TO_MS),
+ chassis_codes={"7N"},
+ wmis={WMI.VOLKSWAGEN_EUROPE_CAR},
+ )
+ VOLKSWAGEN_TAOS_MK1 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Volkswagen Taos 2022-23")],
+ VolkswagenCarSpecs(mass=1498, wheelbase=2.69),
+ chassis_codes={"B2"},
+ wmis={WMI.VOLKSWAGEN_MEXICO_SUV, WMI.VOLKSWAGEN_ARGENTINA},
+ )
+ VOLKSWAGEN_TCROSS_MK1 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Volkswagen T-Cross 2021", footnotes=[Footnote.VW_MQB_A0])],
+ VolkswagenCarSpecs(mass=1150, wheelbase=2.60),
+ chassis_codes={"C1"},
+ wmis={WMI.VOLKSWAGEN_EUROPE_SUV},
+ )
+ VOLKSWAGEN_TIGUAN_MK2 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Tiguan 2018-24"),
+ VWCarDocs("Volkswagen Tiguan eHybrid 2021-23"),
+ ],
+ VolkswagenCarSpecs(mass=1715, wheelbase=2.74),
+ chassis_codes={"5N", "AD", "AX", "BW"},
+ wmis={WMI.VOLKSWAGEN_EUROPE_SUV, WMI.VOLKSWAGEN_MEXICO_SUV},
+ )
+ VOLKSWAGEN_TOURAN_MK2 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Volkswagen Touran 2016-23")],
+ VolkswagenCarSpecs(mass=1516, wheelbase=2.79),
+ chassis_codes={"1T"},
+ wmis={WMI.VOLKSWAGEN_EUROPE_SUV},
+ )
+ VOLKSWAGEN_TRANSPORTER_T61 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Volkswagen Caravelle 2020"),
+ VWCarDocs("Volkswagen California 2021-23"),
+ ],
+ VolkswagenCarSpecs(mass=1926, wheelbase=3.00, minSteerSpeed=14.0),
+ chassis_codes={"7H", "7L"},
+ wmis={WMI.VOLKSWAGEN_COMMERCIAL_BUS_VAN},
+ )
+ VOLKSWAGEN_TROC_MK1 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Volkswagen T-Roc 2018-23")],
+ VolkswagenCarSpecs(mass=1413, wheelbase=2.63),
+ chassis_codes={"A1"},
+ wmis={WMI.VOLKSWAGEN_EUROPE_SUV},
+ )
+ AUDI_A3_MK3 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Audi A3 2014-19"),
+ VWCarDocs("Audi A3 Sportback e-tron 2017-18"),
+ VWCarDocs("Audi RS3 2018"),
+ VWCarDocs("Audi S3 2015-17"),
+ ],
+ VolkswagenCarSpecs(mass=1335, wheelbase=2.61),
+ chassis_codes={"8V", "FF"},
+ wmis={WMI.AUDI_GERMANY_CAR, WMI.AUDI_SPORT},
+ )
+ AUDI_Q2_MK1 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Audi Q2 2018")],
+ VolkswagenCarSpecs(mass=1205, wheelbase=2.61),
+ chassis_codes={"GA"},
+ wmis={WMI.AUDI_GERMANY_CAR},
+ )
+ AUDI_Q3_MK2 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Audi Q3 2019-23")],
+ VolkswagenCarSpecs(mass=1623, wheelbase=2.68),
+ chassis_codes={"8U", "F3", "FS"},
+ wmis={WMI.AUDI_EUROPE_MPV, WMI.AUDI_GERMANY_CAR},
+ )
+ SEAT_ATECA_MK1 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("CUPRA Ateca 2018-23"),
+ VWCarDocs("SEAT Ateca 2016-23"),
+ VWCarDocs("SEAT Leon 2014-20"),
+ ],
+ VolkswagenCarSpecs(mass=1300, wheelbase=2.64),
+ chassis_codes={"5F"},
+ wmis={WMI.SEAT},
+ )
+ SKODA_FABIA_MK4 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Škoda Fabia 2022-23", footnotes=[Footnote.VW_MQB_A0])],
+ VolkswagenCarSpecs(mass=1266, wheelbase=2.56),
+ chassis_codes={"PJ"},
+ wmis={WMI.SKODA},
+ )
+ SKODA_KAMIQ_MK1 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Škoda Kamiq 2021-23", footnotes=[Footnote.VW_MQB_A0, Footnote.KAMIQ]),
+ VWCarDocs("Škoda Scala 2020-23", footnotes=[Footnote.VW_MQB_A0]),
+ ],
+ VolkswagenCarSpecs(mass=1230, wheelbase=2.66),
+ chassis_codes={"NW"},
+ wmis={WMI.SKODA},
+ )
+ SKODA_KAROQ_MK1 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Škoda Karoq 2019-23")],
+ VolkswagenCarSpecs(mass=1278, wheelbase=2.66),
+ chassis_codes={"NU"},
+ wmis={WMI.SKODA},
+ )
+ SKODA_KODIAQ_MK1 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Škoda Kodiaq 2017-23")],
+ VolkswagenCarSpecs(mass=1569, wheelbase=2.79),
+ chassis_codes={"NS"},
+ wmis={WMI.SKODA, WMI.VOLKSWAGEN_GROUP_RUS},
+ )
+ SKODA_OCTAVIA_MK3 = VolkswagenMQBPlatformConfig(
+ [
+ VWCarDocs("Škoda Octavia 2015-19"),
+ VWCarDocs("Škoda Octavia RS 2016"),
+ VWCarDocs("Škoda Octavia Scout 2017-19"),
+ ],
+ VolkswagenCarSpecs(mass=1388, wheelbase=2.68),
+ chassis_codes={"NE"},
+ wmis={WMI.SKODA},
+ )
+ SKODA_SUPERB_MK3 = VolkswagenMQBPlatformConfig(
+ [VWCarDocs("Škoda Superb 2015-22")],
+ VolkswagenCarSpecs(mass=1505, wheelbase=2.84),
+ chassis_codes={"3V", "NP"},
+ wmis={WMI.SKODA},
+ )
+
+
+def match_fw_to_car_fuzzy(live_fw_versions, vin, offline_fw_versions) -> set[str]:
+ candidates = set()
+
+ # Compile all FW versions for each ECU
+ all_ecu_versions: dict[EcuAddrSubAddr, set[str]] = defaultdict(set)
+ for ecus in offline_fw_versions.values():
+ for ecu, versions in ecus.items():
+ all_ecu_versions[ecu] |= set(versions)
+
+ # Check the WMI and chassis code to determine the platform
+ wmi = vin[:3]
+ chassis_code = vin[6:8]
+
+ for platform in CAR:
+ valid_ecus = set()
+ for ecu in offline_fw_versions[platform]:
+ addr = ecu[1:]
+ if ecu[0] not in CHECK_FUZZY_ECUS:
+ continue
+
+ # Sanity check that live FW is in the superset of all FW, Volkswagen ECU part numbers are commonly shared
+ found_versions = live_fw_versions.get(addr, [])
+ expected_versions = all_ecu_versions[ecu]
+ if not any(found_version in expected_versions for found_version in found_versions):
+ break
+
+ valid_ecus.add(ecu[0])
+
+ if valid_ecus != CHECK_FUZZY_ECUS:
+ continue
+
+ if wmi in platform.config.wmis and chassis_code in platform.config.chassis_codes:
+ candidates.add(platform)
+
+ return {str(c) for c in candidates}
+
+
+# These ECUs are required to match to gain a VIN match
+# TODO: do we want to check camera when we add its FW?
+CHECK_FUZZY_ECUS = {Ecu.fwdRadar}
+
+# All supported cars should return FW from the engine, srs, eps, and fwdRadar. Cars
+# with a manual trans won't return transmission firmware, but all other cars will.
+#
+# The 0xF187 SW part number query should return in the form of N[NX][NX] NNN NNN [X[X]],
+# where N=number, X=letter, and the trailing two letters are optional. Performance
+# tuners sometimes tamper with that field (e.g. 8V0 9C0 BB0 1 from COBB/EQT). Tampered
+# ECU SW part numbers are invalid for vehicle ID and compatibility checks. Try to have
+# them repaired by the tuner before including them in openpilot.
+
+VOLKSWAGEN_VERSION_REQUEST_MULTI = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_SPARE_PART_NUMBER) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER) + \
+ p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_DATA_IDENTIFICATION)
+VOLKSWAGEN_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40])
+
+VOLKSWAGEN_RX_OFFSET = 0x6a
+
+FW_QUERY_CONFIG = FwQueryConfig(
+ requests=[request for bus, obd_multiplexing in [(1, True), (1, False), (0, False)] for request in [
+ Request(
+ [VOLKSWAGEN_VERSION_REQUEST_MULTI],
+ [VOLKSWAGEN_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar, Ecu.fwdCamera],
+ rx_offset=VOLKSWAGEN_RX_OFFSET,
+ bus=bus,
+ obd_multiplexing=obd_multiplexing,
+ ),
+ Request(
+ [VOLKSWAGEN_VERSION_REQUEST_MULTI],
+ [VOLKSWAGEN_VERSION_RESPONSE],
+ whitelist_ecus=[Ecu.engine, Ecu.transmission],
+ bus=bus,
+ obd_multiplexing=obd_multiplexing,
+ ),
+ ]],
+ non_essential_ecus={Ecu.eps: list(CAR)},
+ extra_ecus=[(Ecu.fwdCamera, 0x74f, None)],
+ match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
+)
+
+DBC = CAR.create_dbc_map()
diff --git a/opendbc/dbc/README.md b/opendbc/dbc/README.md
new file mode 100644
index 0000000000..20e31a9034
--- /dev/null
+++ b/opendbc/dbc/README.md
@@ -0,0 +1,49 @@
+## DBC file basics
+
+A DBC file encodes, in a humanly readable way, the information needed to understand a vehicle's CAN bus traffic. A vehicle might have multiple CAN buses and every CAN bus is represented by its own dbc file.
+Wondering what's the DBC file format? [Here](http://www.socialledge.com/sjsu/index.php?title=DBC_Format) and [Here](https://github.com/stefanhoelzl/CANpy/blob/master/docs/DBC_Specification.md) a couple of good overviews.
+
+## How to start reverse engineering cars
+
+[opendbc](https://github.com/commaai/opendbc) is integrated with [cabana](https://github.com/commaai/openpilot/tree/master/tools/cabana).
+
+Use [panda](https://github.com/commaai/panda) to connect your car to a computer.
+
+## How to use reverse engineered DBC
+To create custom CAN simulations or send reverse engineered signals back to the car you can use [CANdevStudio](https://github.com/GENIVI/CANdevStudio) project.
+
+## DBC file preprocessor
+
+DBC files for different models of the same brand have a lot of overlap. Therefore, we wrote a preprocessor to create DBC files from a brand DBC file and a model specific DBC file. The source DBC files can be found in the generator folder. After changing one of the files run the generator.py script to regenerate the output files. These output files will be placed in the root of the opendbc repository and are suffixed by _generated.
+
+## Good practices for contributing to opendbc
+
+- Comments: the best way to store comments is to add them directly to the DBC files. For example:
+ ```
+ CM_ SG_ 490 LONG_ACCEL "wheel speed derivative, noisy and zero snapping";
+ ```
+ is a comment that refers to signal `LONG_ACCEL` in message `490`. Using comments is highly recommended, especially for doubts and uncertainties. [cabana](https://community.comma.ai/cabana/) can easily display/add/edit comments to signals and messages.
+
+- Units: when applicable, it's recommended to convert signals into physical units, by using a proper signal factor. Using a SI unit is preferred, unless a non-SI unit rounds the signal factor much better.
+For example:
+ ```
+ SG_ VEHICLE_SPEED : 7|15@0+ (0.00278,0) [0|70] "m/s" PCM
+ ```
+ is better than:
+ ```
+ SG_ VEHICLE_SPEED : 7|15@0+ (0.00620,0) [0|115] "mph" PCM
+ ```
+ However, the cleanest option is really:
+ ```
+ SG_ VEHICLE_SPEED : 7|15@0+ (0.01,0) [0|250] "kph" PCM
+ ```
+
+- Signal size: always use the smallest amount of bits possible. For example, let's say I'm reverse engineering the gas pedal position and I've determined that it's in a 3 bytes message. For 0% pedal position I read a message value of `0x00 0x00 0x00`, while for 100% of pedal position I read `0x64 0x00 0x00`: clearly, the gas pedal position is within the first byte of the message and I might be tempted to define the signal `GAS_POS` as:
+ ```
+ SG_ GAS_POS : 7|8@0+ (1,0) [0|100] "%" PCM
+ ```
+ However, I can't be sure that the very first bit of the message is referred to the pedal position: I haven't seen it changing! Therefore, a safer way of defining the signal is:
+ ```
+ SG_ GAS_POS : 6|7@0+ (1,0) [0|100] "%" PCM
+ ```
+ which leaves the first bit unallocated. This prevents from very erroneous reading of the gas pedal position, in case the first bit is indeed used for something else.
diff --git a/opendbc/dbc/generator/generator.py b/opendbc/dbc/generator/generator.py
index 214c8fb1a5..d0f41fc80a 100755
--- a/opendbc/dbc/generator/generator.py
+++ b/opendbc/dbc/generator/generator.py
@@ -27,13 +27,13 @@ def create_dbc(src_dir: str, filename: str, output_path: str):
dbc_file_out.write('CM_ "AUTOGENERATED FILE, DO NOT EDIT";\n')
for include_filename in includes:
- include_file_header = '\n\nCM_ "Imported file %s starts here";\n' % include_filename
+ 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('\nCM_ "%s starts here";\n' % filename)
+ dbc_file_out.write(f'\nCM_ "{filename} starts here";\n')
core_dbc = include_pattern.sub('', dbc_file_in)
dbc_file_out.write(core_dbc)
diff --git a/opendbc/dbc/generator/toyota/_toyota_2017.dbc b/opendbc/dbc/generator/toyota/_toyota_2017.dbc
index ba9c71ad9e..3689504acc 100644
--- a/opendbc/dbc/generator/toyota/_toyota_2017.dbc
+++ b/opendbc/dbc/generator/toyota/_toyota_2017.dbc
@@ -45,6 +45,13 @@ BO_ 37 STEER_ANGLE_SENSOR: 8 XXX
SG_ STEER_FRACTION : 39|4@0- (0.1,0) [-0.7|0.7] "deg" XXX
SG_ STEER_RATE : 35|12@0- (1,0) [-2000|2000] "deg/s" XXX
+BO_ 120 ENG2F42: 4 CGW
+ SG_ FAVLMCHH : 7|16@0- (2,0) [0|0] "N" Vector__XX228X
+ SG_ CCRNG : 23|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ FDRVTYPD : 22|3@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ GEARHD : 18|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ ENG2F42S : 31|8@0+ (1,0) [0|0] "" Vector__XXX
+
BO_ 166 BRAKE: 8 XXX
SG_ BRAKE_AMOUNT : 7|8@0+ (1,0) [0|255] "" XXX
SG_ BRAKE_PEDAL : 23|8@0+ (1,0) [0|255] "" XXX
@@ -71,6 +78,7 @@ BO_ 353 DSU_SPEED: 7 XXX
BO_ 452 ENGINE_RPM: 8 CGW
SG_ RPM : 7|16@0- (0.78125,0) [0|0] "rpm" SCS
+ SG_ ENGINE_RUNNING : 27|1@0+ (1,0) [0|1] "" XXX
BO_ 466 PCM_CRUISE: 8 XXX
SG_ GAS_RELEASED : 4|1@0+ (1,0) [0|1] "" XXX
@@ -91,9 +99,10 @@ BO_ 467 PCM_CRUISE_2: 8 XXX
SG_ ACC_FAULTED : 47|1@0+ (1,0) [0|1] "" XXX
SG_ CHECKSUM : 63|8@0+ (1,0) [0|255] "" XXX
-BO_ 552 ACCELEROMETER: 8 XXX
- SG_ ACCEL_Z : 22|15@0- (1,0) [0|32767] "m/s^2" XXX
- SG_ ACCEL_X : 6|15@0- (0.001,0) [-20|20] "m/s^2" XXX
+BO_ 552 VSC1S29: 4 CGW
+ SG_ ICBACT : 7|1@0+ (1,0) [0|0] "" DS1
+ SG_ DVS0PCS : 6|15@0- (0.001,0) [0|0] "m/s^2" DS1
+ SG_ SM228 : 31|8@0+ (1,0) [0|0] "" DS1
BO_ 560 BRAKE_2: 7 XXX
SG_ BRAKE_PRESSED : 26|1@0+ (1,0) [0|1] "" XXX
@@ -147,6 +156,31 @@ BO_ 742 LEAD_INFO: 8 DSU
SG_ LEAD_REL_SPEED : 23|12@0- (0.025,0) [-100|100] "m/s" HCU
SG_ LEAD_LONG_DIST : 7|13@0+ (0.05,0) [0|300] "m" HCU
+BO_ 800 VSC1S07: 8 CGW
+ SG_ FBKRLY : 6|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCM : 4|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCSFT : 3|1@0+ (1,0) [0|0] "" DS1
+ SG_ FABS : 2|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ TSVSC : 1|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCL : 0|1@0+ (1,0) [0|0] "" DS1
+ SG_ RQCSTBKB : 15|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ PSBSTBY : 14|1@0+ (1,0) [0|0] "" DS1
+ SG_ P2BRXMK : 13|1@0+ (1,0) [0|0] "" DS1
+ SG_ MCC : 11|1@0+ (1,0) [0|0] "" DS1
+ SG_ RQBKB : 10|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRSTOP : 9|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ BRKON : 8|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ ASLP : 23|8@0- (1,0) [0|0] "deg" DS1
+ SG_ BRTYPACC : 31|2@0+ (1,0) [0|0] "" DS1
+ SG_ BRKABT3 : 26|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRKABT2 : 25|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRKABT1 : 24|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ GVC : 39|8@0- (0.04,0) [0|0] "m/s^2" DS1
+ SG_ XGVCINV : 43|1@0+ (1,0) [0|0] "" DS1
+ SG_ S07CNT : 52|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ PCSBRSTA : 50|2@0+ (1,0) [0|0] "" DS1
+ SG_ VSC07SUM : 63|8@0+ (1,0) [0|0] "" DS1,FCM
+
BO_ 835 ACC_CONTROL: 8 DSU
SG_ ACCEL_CMD : 7|16@0- (0.001,0) [-20|20] "m/s^2" HCU
SG_ ALLOW_LONG_PRESS : 17|2@0+ (1,0) [0|2] "" XXX
@@ -269,6 +303,18 @@ BO_ 1044 AUTO_HIGH_BEAM: 8 FCM
SG_ F_AHB : 55|4@0+ (1,0) [0|0] "" Vector__XXX
SG_ C_AHB : 51|4@0+ (1,0) [0|0] "" Vector__XXX
+BO_ 1056 VSC1S08: 8 CGW
+ SG_ YR1Z : 7|16@0- (1,0) [0|0] "rad/s" DS1,FCM,MAV
+ SG_ YR2Z : 23|16@0- (1,0) [0|0] "rad/s" DS1,FCM,MAV
+ SG_ GL1Z : 39|8@0- (0.0359,0) [0|0] "m/s^2" DS1,FCM,KSS,MAV,SCS
+ SG_ GL2Z : 47|8@0- (0.0359,0) [0|0] "m/s^2" DS1,FCM,KSS,MAV,SCS
+ SG_ YRGSDIR : 55|1@0+ (1,0) [0|0] "" DS1,FCM,KSS,SCS
+ SG_ GLZS : 51|1@0+ (1,0) [0|0] "" DS1,FCM,KSS,MAV,SCS
+ SG_ YRZF : 50|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ YRZS : 49|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ YRZKS : 48|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ VSC08SUM : 63|8@0+ (1,0) [0|0] "" DS1,FCM,MAV
+
BO_ 1083 AUTOPARK_STATUS: 8 IPAS
SG_ STATE : 7|4@0+ (1,0) [0|15] "" XXX
@@ -347,9 +393,11 @@ BO_ 1552 BODY_CONTROL_STATE_2: 8 XXX
BO_ 1553 UI_SETTING: 8 XXX
SG_ UNITS : 26|2@0+ (1,0) [0|3] "" XXX
SG_ ODOMETER : 39|32@0+ (1,0) [0|1048575] "" XXX
+
BO_ 1556 BLINKERS_STATE: 8 XXX
- SG_ TURN_SIGNALS : 29|2@0+ (1,0) [0|3] "" XXX
+ SG_ BLINKER_BUTTON_PRESSED : 15|1@0+ (1,0) [0|1] "" XXX
SG_ HAZARD_LIGHT : 27|1@0+ (1,0) [0|1] "" XXX
+ SG_ TURN_SIGNALS : 29|2@0+ (1,0) [0|3] "" XXX
BO_ 1568 BODY_CONTROL_STATE: 8 XXX
SG_ METER_DIMMED : 38|1@0+ (1,0) [0|1] "" XXX
@@ -378,6 +426,20 @@ BO_ 1592 DOOR_LOCKS: 8 XXX
SG_ LOCK_STATUS : 20|1@0+ (1,0) [0|1] "" XXX
SG_ LOCKED_VIA_KEYFOB : 23|1@0+ (1,0) [0|1] "" XXX
+BO_ 1779 ADAS_TOGGLE_STATE: 8 XXX
+ SG_ OK_BUTTON_PRESSED : 15|1@0+ (1,0) [0|1] "" BCM
+ SG_ SWS_TOGGLE_CMD : 24|1@0+ (1,0) [0|1] "" XXX
+ SG_ SWS_SENSITIVITY_CMD : 26|2@0+ (1,0) [0|3] "" XXX
+ SG_ LKAS_ON_CMD : 28|1@0+ (1,0) [0|1] "" XXX
+ SG_ LKAS_OFF_CMD : 29|1@0+ (1,0) [0|1] "" XXX
+ SG_ LDA_SENSITIVITY_HI_CMD : 30|1@0+ (1,0) [0|1] "" XXX
+ SG_ LDA_SENSITIVITY_STD_CMD : 31|1@0+ (1,0) [0|1] "" XXX
+ SG_ IPAS_TOGGLE : 34|1@0+ (1,0) [0|1] "" XXX
+ SG_ BSM_TOGGLE_CMD : 37|1@0+ (1,0) [0|1] "" XXX
+ SG_ IPAS_SONAR_TOGGLE : 38|1@0+ (1,0) [0|1] "" XXX
+ SG_ PCS_TOGGLE_CMD : 40|1@0+ (1,0) [0|1] "" XXX
+ SG_ PCS_SENSITIVITY_CMD : 41|1@0+ (1,0) [0|1] "" XXX
+
CM_ SG_ 36 YAW_RATE "verify";
CM_ SG_ 36 ACCEL_X "x-axis accel";
CM_ SG_ 37 STEER_FRACTION "1/15th of the signal STEER_ANGLE, which is 1.5 deg; note that 0x8 is never set";
@@ -397,6 +459,8 @@ CM_ SG_ 614 ANGLE "set to measured angle when ipas control isn't active";
CM_ SG_ 643 _COUNTER "only used on cars that use this msg for cruise control";
CM_ SG_ 643 BRAKE_STATUS "only used on cars that use this msg for cruise control";
CM_ SG_ 643 PRECOLLISION_ACTIVE "set 0.5s before any braking";
+CM_ SG_ 800 SLOPE_ANGLE "potentially used by the PCM to compensate for road pitch";
+CM_ SG_ 800 ACCEL "filtered ego acceleration";
CM_ SG_ 835 ACC_TYPE "if 2, car is likely to have a permanent low speed lockout. 1 is ok";
CM_ SG_ 835 RADAR_DIRTY "Display Clean Radar Sensor message on HUD";
CM_ SG_ 835 ACC_MALFUNCTION "display ACC fault on dash if set to 1";
@@ -509,4 +573,5 @@ VAL_ 1552 METER_SLIDER_DIMMED 1 "Dimmed" 0 "Not Dimmed";
VAL_ 1552 UNITS 1 "km (km/L)" 2 "km (L/100km)" 3 "miles (MPG US)" 4 "miles (MPG Imperial)";
VAL_ 1553 UNITS 1 "km" 2 "miles";
VAL_ 1556 TURN_SIGNALS 3 "none" 2 "right" 1 "left";
+VAL_ 1556 BLINKER_BUTTON_PRESSED 1 "button pressed" 0 "not pressed";
VAL_ 1592 LOCK_STATUS 0 "locked" 1 "unlocked";
diff --git a/opendbc/dbc/toyota_new_mc_pt_generated.dbc b/opendbc/dbc/toyota_new_mc_pt_generated.dbc
index 0233f53b0f..54f988fc13 100644
--- a/opendbc/dbc/toyota_new_mc_pt_generated.dbc
+++ b/opendbc/dbc/toyota_new_mc_pt_generated.dbc
@@ -93,6 +93,13 @@ BO_ 37 STEER_ANGLE_SENSOR: 8 XXX
SG_ STEER_FRACTION : 39|4@0- (0.1,0) [-0.7|0.7] "deg" XXX
SG_ STEER_RATE : 35|12@0- (1,0) [-2000|2000] "deg/s" XXX
+BO_ 120 ENG2F42: 4 CGW
+ SG_ FAVLMCHH : 7|16@0- (2,0) [0|0] "N" Vector__XX228X
+ SG_ CCRNG : 23|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ FDRVTYPD : 22|3@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ GEARHD : 18|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ ENG2F42S : 31|8@0+ (1,0) [0|0] "" Vector__XXX
+
BO_ 166 BRAKE: 8 XXX
SG_ BRAKE_AMOUNT : 7|8@0+ (1,0) [0|255] "" XXX
SG_ BRAKE_PEDAL : 23|8@0+ (1,0) [0|255] "" XXX
@@ -119,6 +126,7 @@ BO_ 353 DSU_SPEED: 7 XXX
BO_ 452 ENGINE_RPM: 8 CGW
SG_ RPM : 7|16@0- (0.78125,0) [0|0] "rpm" SCS
+ SG_ ENGINE_RUNNING : 27|1@0+ (1,0) [0|1] "" XXX
BO_ 466 PCM_CRUISE: 8 XXX
SG_ GAS_RELEASED : 4|1@0+ (1,0) [0|1] "" XXX
@@ -139,9 +147,10 @@ BO_ 467 PCM_CRUISE_2: 8 XXX
SG_ ACC_FAULTED : 47|1@0+ (1,0) [0|1] "" XXX
SG_ CHECKSUM : 63|8@0+ (1,0) [0|255] "" XXX
-BO_ 552 ACCELEROMETER: 8 XXX
- SG_ ACCEL_Z : 22|15@0- (1,0) [0|32767] "m/s^2" XXX
- SG_ ACCEL_X : 6|15@0- (0.001,0) [-20|20] "m/s^2" XXX
+BO_ 552 VSC1S29: 4 CGW
+ SG_ ICBACT : 7|1@0+ (1,0) [0|0] "" DS1
+ SG_ DVS0PCS : 6|15@0- (0.001,0) [0|0] "m/s^2" DS1
+ SG_ SM228 : 31|8@0+ (1,0) [0|0] "" DS1
BO_ 560 BRAKE_2: 7 XXX
SG_ BRAKE_PRESSED : 26|1@0+ (1,0) [0|1] "" XXX
@@ -195,6 +204,31 @@ BO_ 742 LEAD_INFO: 8 DSU
SG_ LEAD_REL_SPEED : 23|12@0- (0.025,0) [-100|100] "m/s" HCU
SG_ LEAD_LONG_DIST : 7|13@0+ (0.05,0) [0|300] "m" HCU
+BO_ 800 VSC1S07: 8 CGW
+ SG_ FBKRLY : 6|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCM : 4|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCSFT : 3|1@0+ (1,0) [0|0] "" DS1
+ SG_ FABS : 2|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ TSVSC : 1|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCL : 0|1@0+ (1,0) [0|0] "" DS1
+ SG_ RQCSTBKB : 15|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ PSBSTBY : 14|1@0+ (1,0) [0|0] "" DS1
+ SG_ P2BRXMK : 13|1@0+ (1,0) [0|0] "" DS1
+ SG_ MCC : 11|1@0+ (1,0) [0|0] "" DS1
+ SG_ RQBKB : 10|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRSTOP : 9|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ BRKON : 8|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ ASLP : 23|8@0- (1,0) [0|0] "deg" DS1
+ SG_ BRTYPACC : 31|2@0+ (1,0) [0|0] "" DS1
+ SG_ BRKABT3 : 26|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRKABT2 : 25|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRKABT1 : 24|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ GVC : 39|8@0- (0.04,0) [0|0] "m/s^2" DS1
+ SG_ XGVCINV : 43|1@0+ (1,0) [0|0] "" DS1
+ SG_ S07CNT : 52|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ PCSBRSTA : 50|2@0+ (1,0) [0|0] "" DS1
+ SG_ VSC07SUM : 63|8@0+ (1,0) [0|0] "" DS1,FCM
+
BO_ 835 ACC_CONTROL: 8 DSU
SG_ ACCEL_CMD : 7|16@0- (0.001,0) [-20|20] "m/s^2" HCU
SG_ ALLOW_LONG_PRESS : 17|2@0+ (1,0) [0|2] "" XXX
@@ -317,6 +351,18 @@ BO_ 1044 AUTO_HIGH_BEAM: 8 FCM
SG_ F_AHB : 55|4@0+ (1,0) [0|0] "" Vector__XXX
SG_ C_AHB : 51|4@0+ (1,0) [0|0] "" Vector__XXX
+BO_ 1056 VSC1S08: 8 CGW
+ SG_ YR1Z : 7|16@0- (1,0) [0|0] "rad/s" DS1,FCM,MAV
+ SG_ YR2Z : 23|16@0- (1,0) [0|0] "rad/s" DS1,FCM,MAV
+ SG_ GL1Z : 39|8@0- (0.0359,0) [0|0] "m/s^2" DS1,FCM,KSS,MAV,SCS
+ SG_ GL2Z : 47|8@0- (0.0359,0) [0|0] "m/s^2" DS1,FCM,KSS,MAV,SCS
+ SG_ YRGSDIR : 55|1@0+ (1,0) [0|0] "" DS1,FCM,KSS,SCS
+ SG_ GLZS : 51|1@0+ (1,0) [0|0] "" DS1,FCM,KSS,MAV,SCS
+ SG_ YRZF : 50|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ YRZS : 49|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ YRZKS : 48|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ VSC08SUM : 63|8@0+ (1,0) [0|0] "" DS1,FCM,MAV
+
BO_ 1083 AUTOPARK_STATUS: 8 IPAS
SG_ STATE : 7|4@0+ (1,0) [0|15] "" XXX
@@ -395,9 +441,11 @@ BO_ 1552 BODY_CONTROL_STATE_2: 8 XXX
BO_ 1553 UI_SETTING: 8 XXX
SG_ UNITS : 26|2@0+ (1,0) [0|3] "" XXX
SG_ ODOMETER : 39|32@0+ (1,0) [0|1048575] "" XXX
+
BO_ 1556 BLINKERS_STATE: 8 XXX
- SG_ TURN_SIGNALS : 29|2@0+ (1,0) [0|3] "" XXX
+ SG_ BLINKER_BUTTON_PRESSED : 15|1@0+ (1,0) [0|1] "" XXX
SG_ HAZARD_LIGHT : 27|1@0+ (1,0) [0|1] "" XXX
+ SG_ TURN_SIGNALS : 29|2@0+ (1,0) [0|3] "" XXX
BO_ 1568 BODY_CONTROL_STATE: 8 XXX
SG_ METER_DIMMED : 38|1@0+ (1,0) [0|1] "" XXX
@@ -426,6 +474,20 @@ BO_ 1592 DOOR_LOCKS: 8 XXX
SG_ LOCK_STATUS : 20|1@0+ (1,0) [0|1] "" XXX
SG_ LOCKED_VIA_KEYFOB : 23|1@0+ (1,0) [0|1] "" XXX
+BO_ 1779 ADAS_TOGGLE_STATE: 8 XXX
+ SG_ OK_BUTTON_PRESSED : 15|1@0+ (1,0) [0|1] "" BCM
+ SG_ SWS_TOGGLE_CMD : 24|1@0+ (1,0) [0|1] "" XXX
+ SG_ SWS_SENSITIVITY_CMD : 26|2@0+ (1,0) [0|3] "" XXX
+ SG_ LKAS_ON_CMD : 28|1@0+ (1,0) [0|1] "" XXX
+ SG_ LKAS_OFF_CMD : 29|1@0+ (1,0) [0|1] "" XXX
+ SG_ LDA_SENSITIVITY_HI_CMD : 30|1@0+ (1,0) [0|1] "" XXX
+ SG_ LDA_SENSITIVITY_STD_CMD : 31|1@0+ (1,0) [0|1] "" XXX
+ SG_ IPAS_TOGGLE : 34|1@0+ (1,0) [0|1] "" XXX
+ SG_ BSM_TOGGLE_CMD : 37|1@0+ (1,0) [0|1] "" XXX
+ SG_ IPAS_SONAR_TOGGLE : 38|1@0+ (1,0) [0|1] "" XXX
+ SG_ PCS_TOGGLE_CMD : 40|1@0+ (1,0) [0|1] "" XXX
+ SG_ PCS_SENSITIVITY_CMD : 41|1@0+ (1,0) [0|1] "" XXX
+
CM_ SG_ 36 YAW_RATE "verify";
CM_ SG_ 36 ACCEL_X "x-axis accel";
CM_ SG_ 37 STEER_FRACTION "1/15th of the signal STEER_ANGLE, which is 1.5 deg; note that 0x8 is never set";
@@ -445,6 +507,8 @@ CM_ SG_ 614 ANGLE "set to measured angle when ipas control isn't active";
CM_ SG_ 643 _COUNTER "only used on cars that use this msg for cruise control";
CM_ SG_ 643 BRAKE_STATUS "only used on cars that use this msg for cruise control";
CM_ SG_ 643 PRECOLLISION_ACTIVE "set 0.5s before any braking";
+CM_ SG_ 800 SLOPE_ANGLE "potentially used by the PCM to compensate for road pitch";
+CM_ SG_ 800 ACCEL "filtered ego acceleration";
CM_ SG_ 835 ACC_TYPE "if 2, car is likely to have a permanent low speed lockout. 1 is ok";
CM_ SG_ 835 RADAR_DIRTY "Display Clean Radar Sensor message on HUD";
CM_ SG_ 835 ACC_MALFUNCTION "display ACC fault on dash if set to 1";
@@ -557,6 +621,7 @@ VAL_ 1552 METER_SLIDER_DIMMED 1 "Dimmed" 0 "Not Dimmed";
VAL_ 1552 UNITS 1 "km (km/L)" 2 "km (L/100km)" 3 "miles (MPG US)" 4 "miles (MPG Imperial)";
VAL_ 1553 UNITS 1 "km" 2 "miles";
VAL_ 1556 TURN_SIGNALS 3 "none" 2 "right" 1 "left";
+VAL_ 1556 BLINKER_BUTTON_PRESSED 1 "button pressed" 0 "not pressed";
VAL_ 1592 LOCK_STATUS 0 "locked" 1 "unlocked";
diff --git a/opendbc/dbc/toyota_nodsu_pt_generated.dbc b/opendbc/dbc/toyota_nodsu_pt_generated.dbc
index e8fffc1638..c34029a845 100644
--- a/opendbc/dbc/toyota_nodsu_pt_generated.dbc
+++ b/opendbc/dbc/toyota_nodsu_pt_generated.dbc
@@ -93,6 +93,13 @@ BO_ 37 STEER_ANGLE_SENSOR: 8 XXX
SG_ STEER_FRACTION : 39|4@0- (0.1,0) [-0.7|0.7] "deg" XXX
SG_ STEER_RATE : 35|12@0- (1,0) [-2000|2000] "deg/s" XXX
+BO_ 120 ENG2F42: 4 CGW
+ SG_ FAVLMCHH : 7|16@0- (2,0) [0|0] "N" Vector__XX228X
+ SG_ CCRNG : 23|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ FDRVTYPD : 22|3@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ GEARHD : 18|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ ENG2F42S : 31|8@0+ (1,0) [0|0] "" Vector__XXX
+
BO_ 166 BRAKE: 8 XXX
SG_ BRAKE_AMOUNT : 7|8@0+ (1,0) [0|255] "" XXX
SG_ BRAKE_PEDAL : 23|8@0+ (1,0) [0|255] "" XXX
@@ -119,6 +126,7 @@ BO_ 353 DSU_SPEED: 7 XXX
BO_ 452 ENGINE_RPM: 8 CGW
SG_ RPM : 7|16@0- (0.78125,0) [0|0] "rpm" SCS
+ SG_ ENGINE_RUNNING : 27|1@0+ (1,0) [0|1] "" XXX
BO_ 466 PCM_CRUISE: 8 XXX
SG_ GAS_RELEASED : 4|1@0+ (1,0) [0|1] "" XXX
@@ -139,9 +147,10 @@ BO_ 467 PCM_CRUISE_2: 8 XXX
SG_ ACC_FAULTED : 47|1@0+ (1,0) [0|1] "" XXX
SG_ CHECKSUM : 63|8@0+ (1,0) [0|255] "" XXX
-BO_ 552 ACCELEROMETER: 8 XXX
- SG_ ACCEL_Z : 22|15@0- (1,0) [0|32767] "m/s^2" XXX
- SG_ ACCEL_X : 6|15@0- (0.001,0) [-20|20] "m/s^2" XXX
+BO_ 552 VSC1S29: 4 CGW
+ SG_ ICBACT : 7|1@0+ (1,0) [0|0] "" DS1
+ SG_ DVS0PCS : 6|15@0- (0.001,0) [0|0] "m/s^2" DS1
+ SG_ SM228 : 31|8@0+ (1,0) [0|0] "" DS1
BO_ 560 BRAKE_2: 7 XXX
SG_ BRAKE_PRESSED : 26|1@0+ (1,0) [0|1] "" XXX
@@ -195,6 +204,31 @@ BO_ 742 LEAD_INFO: 8 DSU
SG_ LEAD_REL_SPEED : 23|12@0- (0.025,0) [-100|100] "m/s" HCU
SG_ LEAD_LONG_DIST : 7|13@0+ (0.05,0) [0|300] "m" HCU
+BO_ 800 VSC1S07: 8 CGW
+ SG_ FBKRLY : 6|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCM : 4|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCSFT : 3|1@0+ (1,0) [0|0] "" DS1
+ SG_ FABS : 2|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ TSVSC : 1|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCL : 0|1@0+ (1,0) [0|0] "" DS1
+ SG_ RQCSTBKB : 15|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ PSBSTBY : 14|1@0+ (1,0) [0|0] "" DS1
+ SG_ P2BRXMK : 13|1@0+ (1,0) [0|0] "" DS1
+ SG_ MCC : 11|1@0+ (1,0) [0|0] "" DS1
+ SG_ RQBKB : 10|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRSTOP : 9|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ BRKON : 8|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ ASLP : 23|8@0- (1,0) [0|0] "deg" DS1
+ SG_ BRTYPACC : 31|2@0+ (1,0) [0|0] "" DS1
+ SG_ BRKABT3 : 26|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRKABT2 : 25|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRKABT1 : 24|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ GVC : 39|8@0- (0.04,0) [0|0] "m/s^2" DS1
+ SG_ XGVCINV : 43|1@0+ (1,0) [0|0] "" DS1
+ SG_ S07CNT : 52|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ PCSBRSTA : 50|2@0+ (1,0) [0|0] "" DS1
+ SG_ VSC07SUM : 63|8@0+ (1,0) [0|0] "" DS1,FCM
+
BO_ 835 ACC_CONTROL: 8 DSU
SG_ ACCEL_CMD : 7|16@0- (0.001,0) [-20|20] "m/s^2" HCU
SG_ ALLOW_LONG_PRESS : 17|2@0+ (1,0) [0|2] "" XXX
@@ -317,6 +351,18 @@ BO_ 1044 AUTO_HIGH_BEAM: 8 FCM
SG_ F_AHB : 55|4@0+ (1,0) [0|0] "" Vector__XXX
SG_ C_AHB : 51|4@0+ (1,0) [0|0] "" Vector__XXX
+BO_ 1056 VSC1S08: 8 CGW
+ SG_ YR1Z : 7|16@0- (1,0) [0|0] "rad/s" DS1,FCM,MAV
+ SG_ YR2Z : 23|16@0- (1,0) [0|0] "rad/s" DS1,FCM,MAV
+ SG_ GL1Z : 39|8@0- (0.0359,0) [0|0] "m/s^2" DS1,FCM,KSS,MAV,SCS
+ SG_ GL2Z : 47|8@0- (0.0359,0) [0|0] "m/s^2" DS1,FCM,KSS,MAV,SCS
+ SG_ YRGSDIR : 55|1@0+ (1,0) [0|0] "" DS1,FCM,KSS,SCS
+ SG_ GLZS : 51|1@0+ (1,0) [0|0] "" DS1,FCM,KSS,MAV,SCS
+ SG_ YRZF : 50|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ YRZS : 49|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ YRZKS : 48|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ VSC08SUM : 63|8@0+ (1,0) [0|0] "" DS1,FCM,MAV
+
BO_ 1083 AUTOPARK_STATUS: 8 IPAS
SG_ STATE : 7|4@0+ (1,0) [0|15] "" XXX
@@ -395,9 +441,11 @@ BO_ 1552 BODY_CONTROL_STATE_2: 8 XXX
BO_ 1553 UI_SETTING: 8 XXX
SG_ UNITS : 26|2@0+ (1,0) [0|3] "" XXX
SG_ ODOMETER : 39|32@0+ (1,0) [0|1048575] "" XXX
+
BO_ 1556 BLINKERS_STATE: 8 XXX
- SG_ TURN_SIGNALS : 29|2@0+ (1,0) [0|3] "" XXX
+ SG_ BLINKER_BUTTON_PRESSED : 15|1@0+ (1,0) [0|1] "" XXX
SG_ HAZARD_LIGHT : 27|1@0+ (1,0) [0|1] "" XXX
+ SG_ TURN_SIGNALS : 29|2@0+ (1,0) [0|3] "" XXX
BO_ 1568 BODY_CONTROL_STATE: 8 XXX
SG_ METER_DIMMED : 38|1@0+ (1,0) [0|1] "" XXX
@@ -426,6 +474,20 @@ BO_ 1592 DOOR_LOCKS: 8 XXX
SG_ LOCK_STATUS : 20|1@0+ (1,0) [0|1] "" XXX
SG_ LOCKED_VIA_KEYFOB : 23|1@0+ (1,0) [0|1] "" XXX
+BO_ 1779 ADAS_TOGGLE_STATE: 8 XXX
+ SG_ OK_BUTTON_PRESSED : 15|1@0+ (1,0) [0|1] "" BCM
+ SG_ SWS_TOGGLE_CMD : 24|1@0+ (1,0) [0|1] "" XXX
+ SG_ SWS_SENSITIVITY_CMD : 26|2@0+ (1,0) [0|3] "" XXX
+ SG_ LKAS_ON_CMD : 28|1@0+ (1,0) [0|1] "" XXX
+ SG_ LKAS_OFF_CMD : 29|1@0+ (1,0) [0|1] "" XXX
+ SG_ LDA_SENSITIVITY_HI_CMD : 30|1@0+ (1,0) [0|1] "" XXX
+ SG_ LDA_SENSITIVITY_STD_CMD : 31|1@0+ (1,0) [0|1] "" XXX
+ SG_ IPAS_TOGGLE : 34|1@0+ (1,0) [0|1] "" XXX
+ SG_ BSM_TOGGLE_CMD : 37|1@0+ (1,0) [0|1] "" XXX
+ SG_ IPAS_SONAR_TOGGLE : 38|1@0+ (1,0) [0|1] "" XXX
+ SG_ PCS_TOGGLE_CMD : 40|1@0+ (1,0) [0|1] "" XXX
+ SG_ PCS_SENSITIVITY_CMD : 41|1@0+ (1,0) [0|1] "" XXX
+
CM_ SG_ 36 YAW_RATE "verify";
CM_ SG_ 36 ACCEL_X "x-axis accel";
CM_ SG_ 37 STEER_FRACTION "1/15th of the signal STEER_ANGLE, which is 1.5 deg; note that 0x8 is never set";
@@ -445,6 +507,8 @@ CM_ SG_ 614 ANGLE "set to measured angle when ipas control isn't active";
CM_ SG_ 643 _COUNTER "only used on cars that use this msg for cruise control";
CM_ SG_ 643 BRAKE_STATUS "only used on cars that use this msg for cruise control";
CM_ SG_ 643 PRECOLLISION_ACTIVE "set 0.5s before any braking";
+CM_ SG_ 800 SLOPE_ANGLE "potentially used by the PCM to compensate for road pitch";
+CM_ SG_ 800 ACCEL "filtered ego acceleration";
CM_ SG_ 835 ACC_TYPE "if 2, car is likely to have a permanent low speed lockout. 1 is ok";
CM_ SG_ 835 RADAR_DIRTY "Display Clean Radar Sensor message on HUD";
CM_ SG_ 835 ACC_MALFUNCTION "display ACC fault on dash if set to 1";
@@ -557,6 +621,7 @@ VAL_ 1552 METER_SLIDER_DIMMED 1 "Dimmed" 0 "Not Dimmed";
VAL_ 1552 UNITS 1 "km (km/L)" 2 "km (L/100km)" 3 "miles (MPG US)" 4 "miles (MPG Imperial)";
VAL_ 1553 UNITS 1 "km" 2 "miles";
VAL_ 1556 TURN_SIGNALS 3 "none" 2 "right" 1 "left";
+VAL_ 1556 BLINKER_BUTTON_PRESSED 1 "button pressed" 0 "not pressed";
VAL_ 1592 LOCK_STATUS 0 "locked" 1 "unlocked";
diff --git a/opendbc/dbc/toyota_tnga_k_pt_generated.dbc b/opendbc/dbc/toyota_tnga_k_pt_generated.dbc
index 67dbabb4df..7af77f2c02 100644
--- a/opendbc/dbc/toyota_tnga_k_pt_generated.dbc
+++ b/opendbc/dbc/toyota_tnga_k_pt_generated.dbc
@@ -93,6 +93,13 @@ BO_ 37 STEER_ANGLE_SENSOR: 8 XXX
SG_ STEER_FRACTION : 39|4@0- (0.1,0) [-0.7|0.7] "deg" XXX
SG_ STEER_RATE : 35|12@0- (1,0) [-2000|2000] "deg/s" XXX
+BO_ 120 ENG2F42: 4 CGW
+ SG_ FAVLMCHH : 7|16@0- (2,0) [0|0] "N" Vector__XX228X
+ SG_ CCRNG : 23|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ FDRVTYPD : 22|3@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ GEARHD : 18|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ ENG2F42S : 31|8@0+ (1,0) [0|0] "" Vector__XXX
+
BO_ 166 BRAKE: 8 XXX
SG_ BRAKE_AMOUNT : 7|8@0+ (1,0) [0|255] "" XXX
SG_ BRAKE_PEDAL : 23|8@0+ (1,0) [0|255] "" XXX
@@ -119,6 +126,7 @@ BO_ 353 DSU_SPEED: 7 XXX
BO_ 452 ENGINE_RPM: 8 CGW
SG_ RPM : 7|16@0- (0.78125,0) [0|0] "rpm" SCS
+ SG_ ENGINE_RUNNING : 27|1@0+ (1,0) [0|1] "" XXX
BO_ 466 PCM_CRUISE: 8 XXX
SG_ GAS_RELEASED : 4|1@0+ (1,0) [0|1] "" XXX
@@ -139,9 +147,10 @@ BO_ 467 PCM_CRUISE_2: 8 XXX
SG_ ACC_FAULTED : 47|1@0+ (1,0) [0|1] "" XXX
SG_ CHECKSUM : 63|8@0+ (1,0) [0|255] "" XXX
-BO_ 552 ACCELEROMETER: 8 XXX
- SG_ ACCEL_Z : 22|15@0- (1,0) [0|32767] "m/s^2" XXX
- SG_ ACCEL_X : 6|15@0- (0.001,0) [-20|20] "m/s^2" XXX
+BO_ 552 VSC1S29: 4 CGW
+ SG_ ICBACT : 7|1@0+ (1,0) [0|0] "" DS1
+ SG_ DVS0PCS : 6|15@0- (0.001,0) [0|0] "m/s^2" DS1
+ SG_ SM228 : 31|8@0+ (1,0) [0|0] "" DS1
BO_ 560 BRAKE_2: 7 XXX
SG_ BRAKE_PRESSED : 26|1@0+ (1,0) [0|1] "" XXX
@@ -195,6 +204,31 @@ BO_ 742 LEAD_INFO: 8 DSU
SG_ LEAD_REL_SPEED : 23|12@0- (0.025,0) [-100|100] "m/s" HCU
SG_ LEAD_LONG_DIST : 7|13@0+ (0.05,0) [0|300] "m" HCU
+BO_ 800 VSC1S07: 8 CGW
+ SG_ FBKRLY : 6|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCM : 4|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCSFT : 3|1@0+ (1,0) [0|0] "" DS1
+ SG_ FABS : 2|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ TSVSC : 1|1@0+ (1,0) [0|0] "" DS1
+ SG_ FVSCL : 0|1@0+ (1,0) [0|0] "" DS1
+ SG_ RQCSTBKB : 15|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ PSBSTBY : 14|1@0+ (1,0) [0|0] "" DS1
+ SG_ P2BRXMK : 13|1@0+ (1,0) [0|0] "" DS1
+ SG_ MCC : 11|1@0+ (1,0) [0|0] "" DS1
+ SG_ RQBKB : 10|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRSTOP : 9|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ BRKON : 8|1@0+ (1,0) [0|0] "" DS1,FCM
+ SG_ ASLP : 23|8@0- (1,0) [0|0] "deg" DS1
+ SG_ BRTYPACC : 31|2@0+ (1,0) [0|0] "" DS1
+ SG_ BRKABT3 : 26|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRKABT2 : 25|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ BRKABT1 : 24|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ GVC : 39|8@0- (0.04,0) [0|0] "m/s^2" DS1
+ SG_ XGVCINV : 43|1@0+ (1,0) [0|0] "" DS1
+ SG_ S07CNT : 52|1@0+ (1,0) [0|0] "" Vector__XXX
+ SG_ PCSBRSTA : 50|2@0+ (1,0) [0|0] "" DS1
+ SG_ VSC07SUM : 63|8@0+ (1,0) [0|0] "" DS1,FCM
+
BO_ 835 ACC_CONTROL: 8 DSU
SG_ ACCEL_CMD : 7|16@0- (0.001,0) [-20|20] "m/s^2" HCU
SG_ ALLOW_LONG_PRESS : 17|2@0+ (1,0) [0|2] "" XXX
@@ -317,6 +351,18 @@ BO_ 1044 AUTO_HIGH_BEAM: 8 FCM
SG_ F_AHB : 55|4@0+ (1,0) [0|0] "" Vector__XXX
SG_ C_AHB : 51|4@0+ (1,0) [0|0] "" Vector__XXX
+BO_ 1056 VSC1S08: 8 CGW
+ SG_ YR1Z : 7|16@0- (1,0) [0|0] "rad/s" DS1,FCM,MAV
+ SG_ YR2Z : 23|16@0- (1,0) [0|0] "rad/s" DS1,FCM,MAV
+ SG_ GL1Z : 39|8@0- (0.0359,0) [0|0] "m/s^2" DS1,FCM,KSS,MAV,SCS
+ SG_ GL2Z : 47|8@0- (0.0359,0) [0|0] "m/s^2" DS1,FCM,KSS,MAV,SCS
+ SG_ YRGSDIR : 55|1@0+ (1,0) [0|0] "" DS1,FCM,KSS,SCS
+ SG_ GLZS : 51|1@0+ (1,0) [0|0] "" DS1,FCM,KSS,MAV,SCS
+ SG_ YRZF : 50|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ YRZS : 49|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ YRZKS : 48|1@0+ (1,0) [0|0] "" DS1,FCM,MAV
+ SG_ VSC08SUM : 63|8@0+ (1,0) [0|0] "" DS1,FCM,MAV
+
BO_ 1083 AUTOPARK_STATUS: 8 IPAS
SG_ STATE : 7|4@0+ (1,0) [0|15] "" XXX
@@ -395,9 +441,11 @@ BO_ 1552 BODY_CONTROL_STATE_2: 8 XXX
BO_ 1553 UI_SETTING: 8 XXX
SG_ UNITS : 26|2@0+ (1,0) [0|3] "" XXX
SG_ ODOMETER : 39|32@0+ (1,0) [0|1048575] "" XXX
+
BO_ 1556 BLINKERS_STATE: 8 XXX
- SG_ TURN_SIGNALS : 29|2@0+ (1,0) [0|3] "" XXX
+ SG_ BLINKER_BUTTON_PRESSED : 15|1@0+ (1,0) [0|1] "" XXX
SG_ HAZARD_LIGHT : 27|1@0+ (1,0) [0|1] "" XXX
+ SG_ TURN_SIGNALS : 29|2@0+ (1,0) [0|3] "" XXX
BO_ 1568 BODY_CONTROL_STATE: 8 XXX
SG_ METER_DIMMED : 38|1@0+ (1,0) [0|1] "" XXX
@@ -426,6 +474,20 @@ BO_ 1592 DOOR_LOCKS: 8 XXX
SG_ LOCK_STATUS : 20|1@0+ (1,0) [0|1] "" XXX
SG_ LOCKED_VIA_KEYFOB : 23|1@0+ (1,0) [0|1] "" XXX
+BO_ 1779 ADAS_TOGGLE_STATE: 8 XXX
+ SG_ OK_BUTTON_PRESSED : 15|1@0+ (1,0) [0|1] "" BCM
+ SG_ SWS_TOGGLE_CMD : 24|1@0+ (1,0) [0|1] "" XXX
+ SG_ SWS_SENSITIVITY_CMD : 26|2@0+ (1,0) [0|3] "" XXX
+ SG_ LKAS_ON_CMD : 28|1@0+ (1,0) [0|1] "" XXX
+ SG_ LKAS_OFF_CMD : 29|1@0+ (1,0) [0|1] "" XXX
+ SG_ LDA_SENSITIVITY_HI_CMD : 30|1@0+ (1,0) [0|1] "" XXX
+ SG_ LDA_SENSITIVITY_STD_CMD : 31|1@0+ (1,0) [0|1] "" XXX
+ SG_ IPAS_TOGGLE : 34|1@0+ (1,0) [0|1] "" XXX
+ SG_ BSM_TOGGLE_CMD : 37|1@0+ (1,0) [0|1] "" XXX
+ SG_ IPAS_SONAR_TOGGLE : 38|1@0+ (1,0) [0|1] "" XXX
+ SG_ PCS_TOGGLE_CMD : 40|1@0+ (1,0) [0|1] "" XXX
+ SG_ PCS_SENSITIVITY_CMD : 41|1@0+ (1,0) [0|1] "" XXX
+
CM_ SG_ 36 YAW_RATE "verify";
CM_ SG_ 36 ACCEL_X "x-axis accel";
CM_ SG_ 37 STEER_FRACTION "1/15th of the signal STEER_ANGLE, which is 1.5 deg; note that 0x8 is never set";
@@ -445,6 +507,8 @@ CM_ SG_ 614 ANGLE "set to measured angle when ipas control isn't active";
CM_ SG_ 643 _COUNTER "only used on cars that use this msg for cruise control";
CM_ SG_ 643 BRAKE_STATUS "only used on cars that use this msg for cruise control";
CM_ SG_ 643 PRECOLLISION_ACTIVE "set 0.5s before any braking";
+CM_ SG_ 800 SLOPE_ANGLE "potentially used by the PCM to compensate for road pitch";
+CM_ SG_ 800 ACCEL "filtered ego acceleration";
CM_ SG_ 835 ACC_TYPE "if 2, car is likely to have a permanent low speed lockout. 1 is ok";
CM_ SG_ 835 RADAR_DIRTY "Display Clean Radar Sensor message on HUD";
CM_ SG_ 835 ACC_MALFUNCTION "display ACC fault on dash if set to 1";
@@ -557,6 +621,7 @@ VAL_ 1552 METER_SLIDER_DIMMED 1 "Dimmed" 0 "Not Dimmed";
VAL_ 1552 UNITS 1 "km (km/L)" 2 "km (L/100km)" 3 "miles (MPG US)" 4 "miles (MPG Imperial)";
VAL_ 1553 UNITS 1 "km" 2 "miles";
VAL_ 1556 TURN_SIGNALS 3 "none" 2 "right" 1 "left";
+VAL_ 1556 BLINKER_BUTTON_PRESSED 1 "button pressed" 0 "not pressed";
VAL_ 1592 LOCK_STATUS 0 "locked" 1 "unlocked";
diff --git a/pyproject.toml b/pyproject.toml
index cfb6c2c6e7..945ee631e1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,23 +1,102 @@
-[tool.poetry]
+[project]
name = "opendbc"
version = "1.0.0"
description = "CAN bus databases and tools"
-license = "MIT"
-authors = ["Vehicle Researcher "]
+license = { file = "LICENSE" }
+authors = [{ name = "Vehicle Researcher", email = "user@comma.ai" }]
readme = "README.md"
-repository = "https://github.com/commaai/opendbc"
+requires-python = ">=3.9"
+
+urls = { "homepage" = "https://github.com/commaai/opendbc" }
+
+dependencies = [
+ "scons",
+ "numpy",
+ "Cython",
+ "crcmod",
+ "tqdm",
+ "pandacan@git+https://github.com/commaai/panda.git@master",
+]
+
+[project.optional-dependencies]
+testing = [
+ "ruff",
+ "pytest",
+ "pytest-mock",
+ "pytest-xdist",
+ "pytest-subtests",
+ "hypothesis==6.47.*",
+ "parameterized>=0.8,<0.9",
+ "pre-commit",
+]
+docs = [
+ "Jinja2",
+ "natsort",
+]
+examples = [
+ "inputs",
+ "matplotlib",
+]
+
+[tool.pytest.ini_options]
+addopts = "--ignore=panda/ -Werror --strict-config --strict-markers --durations=10 -n auto"
+python_files = "test_*.py"
+testpaths = [
+ "opendbc"
+]
+
+[tool.codespell]
+quiet-level = 3
+ignore-words-list = "alo,ba,bu,deque,hda,grey"
+builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
+check-hidden = true
[tool.cython-lint]
max-line-length = 120
ignore = ["E111", "E114"]
[tool.mypy]
+# helpful warnings
+warn_redundant_casts=true
+warn_unreachable=true
+warn_unused_ignores=true
+
+# restrict dynamic typing
+warn_return_any=true
+
+# allow implicit optionals for default args
+implicit_optional = true
+
explicit_package_bases=true
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
[tool.ruff]
-select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF100", "A"]
-ignore = ["W292", "E741", "E402", "C408", "ISC003"]
line-length = 160
+indent-width = 2
target-version="py311"
+
+[tool.ruff.lint]
+select = [
+ "E", "F", "W", "PIE", "C4", "ISC", "A", "B",
+ "NPY", # numpy
+ "UP", # pyupgrade
+ "TRY302", "TRY400", "TRY401", # try/excepts
+ "RUF008", "RUF100",
+ "TID251",
+ "PLR1704",
+]
+ignore = [
+ "W292",
+ "E741",
+ "E402",
+ "C408",
+ "ISC003",
+ "B027",
+ "B024",
+ "NPY002", # new numpy random syntax is worse
+]
flake8-implicit-str-concat.allow-multiline=false
+
+[tool.ruff.lint.flake8-tidy-imports.banned-api]
+"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!"
+"unittest".msg = "Use pytest"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 3eab8fb0c8..0000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-ruff
-Cython
-Jinja2
-numpy
-pycapnp
-pyyaml
-scons
-pytest
-pytest-xdist
-pytest-subtests