Merge branch 'master' into db10_2

pull/31674/head
ZwX1616 1 year ago
commit fe0f2b91e3
  1. 2
      .github/workflows/repo-maintenance.yaml
  2. 25
      .github/workflows/selfdrive_tests.yaml
  3. 9
      .github/workflows/tools_tests.yaml
  4. 20
      .pre-commit-config.yaml
  5. 38
      Dockerfile.openpilot_base
  6. 37
      Dockerfile.openpilot_base_cl
  7. 46
      Jenkinsfile
  8. 11
      RELEASES.md
  9. 2
      cereal
  10. 3
      common/file_helpers.py
  11. 7
      common/gpio.py
  12. 5
      common/mock/__init__.py
  13. 1
      common/params.cc
  14. 2
      common/params_pyx.pyx
  15. 3
      common/prefix.py
  16. 7
      common/realtime.py
  17. 2
      common/transformations/orientation.py
  18. 2
      common/transformations/transformations.pxd
  19. 7
      common/transformations/transformations.pyx
  20. 2
      common/version.h
  21. 50
      conftest.py
  22. 30
      docs/CARS.md
  23. 5
      launch_chffrplus.sh
  24. 2
      launch_env.sh
  25. 2
      opendbc
  26. 3
      openpilot/__init__.py
  27. 2
      panda
  28. 458
      poetry.lock
  29. 10
      pyproject.toml
  30. 68
      selfdrive/athena/athenad.py
  31. 9
      selfdrive/athena/registration.py
  32. 4
      selfdrive/athena/tests/test_athenad.py
  33. 16
      selfdrive/athena/tests/test_athenad_ping.py
  34. 6
      selfdrive/boardd/boardd.cc
  35. 9
      selfdrive/boardd/pandad.py
  36. 2
      selfdrive/boardd/tests/test_boardd_loopback.py
  37. 57
      selfdrive/car/__init__.py
  38. 24
      selfdrive/car/body/values.py
  39. 13
      selfdrive/car/car_helpers.py
  40. 37
      selfdrive/car/chrysler/fingerprints.py
  41. 5
      selfdrive/car/chrysler/interface.py
  42. 8
      selfdrive/car/chrysler/values.py
  43. 15
      selfdrive/car/docs.py
  44. 33
      selfdrive/car/docs_definitions.py
  45. 13
      selfdrive/car/ecu_addrs.py
  46. 9
      selfdrive/car/ford/carstate.py
  47. 2
      selfdrive/car/ford/interface.py
  48. 4
      selfdrive/car/ford/tests/test_ford.py
  49. 30
      selfdrive/car/ford/values.py
  50. 33
      selfdrive/car/fw_query_definitions.py
  51. 29
      selfdrive/car/fw_versions.py
  52. 4
      selfdrive/car/gm/fingerprints.py
  53. 2
      selfdrive/car/gm/interface.py
  54. 5
      selfdrive/car/gm/values.py
  55. 8
      selfdrive/car/honda/carstate.py
  56. 13
      selfdrive/car/honda/fingerprints.py
  57. 6
      selfdrive/car/honda/interface.py
  58. 18
      selfdrive/car/honda/values.py
  59. 7
      selfdrive/car/hyundai/fingerprints.py
  60. 43
      selfdrive/car/hyundai/interface.py
  61. 56
      selfdrive/car/hyundai/values.py
  62. 43
      selfdrive/car/interfaces.py
  63. 11
      selfdrive/car/isotp_parallel_query.py
  64. 2
      selfdrive/car/mazda/carstate.py
  65. 3
      selfdrive/car/mazda/values.py
  66. 2
      selfdrive/car/mock/interface.py
  67. 3
      selfdrive/car/mock/values.py
  68. 15
      selfdrive/car/nissan/fingerprints.py
  69. 3
      selfdrive/car/nissan/values.py
  70. 3
      selfdrive/car/subaru/fingerprints.py
  71. 40
      selfdrive/car/subaru/interface.py
  72. 9
      selfdrive/car/subaru/subarucan.py
  73. 172
      selfdrive/car/subaru/values.py
  74. 3
      selfdrive/car/tesla/values.py
  75. 12
      selfdrive/car/tests/big_cars_test.sh
  76. 14
      selfdrive/car/tests/routes.py
  77. 9
      selfdrive/car/tests/test_car_interfaces.py
  78. 4
      selfdrive/car/tests/test_docs.py
  79. 3
      selfdrive/car/tests/test_fingerprints.py
  80. 60
      selfdrive/car/tests/test_fw_fingerprint.py
  81. 3
      selfdrive/car/tests/test_lateral_limits.py
  82. 45
      selfdrive/car/tests/test_models.py
  83. 2
      selfdrive/car/torque_data/override.toml
  84. 3
      selfdrive/car/torque_data/substitute.toml
  85. 4
      selfdrive/car/toyota/carcontroller.py
  86. 6
      selfdrive/car/toyota/carstate.py
  87. 24
      selfdrive/car/toyota/fingerprints.py
  88. 18
      selfdrive/car/toyota/interface.py
  89. 13
      selfdrive/car/toyota/values.py
  90. 9
      selfdrive/car/vin.py
  91. 2
      selfdrive/car/volkswagen/fingerprints.py
  92. 21
      selfdrive/car/volkswagen/interface.py
  93. 20
      selfdrive/car/volkswagen/values.py
  94. 255
      selfdrive/controls/controlsd.py
  95. 11
      selfdrive/controls/lib/alertmanager.py
  96. 24
      selfdrive/controls/lib/events.py
  97. 3
      selfdrive/controls/lib/vehicle_model.py
  98. 3
      selfdrive/controls/plannerd.py
  99. 40
      selfdrive/controls/radard.py
  100. 5
      selfdrive/debug/can_print_changes.py
  101. Some files were not shown because too many files have changed in this diff Show More

@ -11,6 +11,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
container: container:
image: ghcr.io/commaai/openpilot-base:latest image: ghcr.io/commaai/openpilot-base:latest
if: github.repository == 'commaai/openpilot'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -36,6 +37,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
container: container:
image: ghcr.io/commaai/openpilot-base:latest image: ghcr.io/commaai/openpilot-base:latest
if: github.repository == 'commaai/openpilot'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: poetry lock - name: poetry lock

@ -14,7 +14,6 @@ concurrency:
env: env:
PYTHONWARNINGS: error PYTHONWARNINGS: error
BASE_IMAGE: openpilot-base BASE_IMAGE: openpilot-base
CL_BASE_IMAGE: openpilot-base-cl
AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }} AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }}
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
@ -22,10 +21,6 @@ env:
RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PRE_COMMIT_HOME=/tmp/pre-commit -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/pre-commit:/tmp/pre-commit -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PRE_COMMIT_HOME=/tmp/pre-commit -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/pre-commit:/tmp/pre-commit -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
BUILD_CL: selfdrive/test/docker_build.sh cl
RUN_CL: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/bash -c
PYTEST: pytest --continue-on-collection-errors --cov --cov-report=xml --cov-append --durations=0 --durations-min=5 --hypothesis-seed 0 -n logical PYTEST: pytest --continue-on-collection-errors --cov --cov-report=xml --cov-append --durations=0 --durations-min=5 --hypothesis-seed 0 -n logical
jobs: jobs:
@ -106,11 +101,6 @@ jobs:
- uses: ./.github/workflows/setup-with-retry - uses: ./.github/workflows/setup-with-retry
with: with:
docker_hub_pat: ${{ secrets.DOCKER_HUB_PAT }} docker_hub_pat: ${{ secrets.DOCKER_HUB_PAT }}
- name: Build and push CL Docker image
if: matrix.arch == 'x86_64'
run: |
unset TARGET_ARCHITECTURE
eval "$BUILD_CL"
docker_push_multiarch: docker_push_multiarch:
name: docker push multiarch tag name: docker push multiarch tag
@ -127,7 +117,7 @@ jobs:
- name: Merge x64 and arm64 tags - name: Merge x64 and arm64 tags
run: | run: |
export PUSH_IMAGE=true export PUSH_IMAGE=true
selfdrive/test/docker_tag_multiarch.sh base x86_64 aarch64 scripts/retry.sh selfdrive/test/docker_tag_multiarch.sh base x86_64 aarch64
static_analysis: static_analysis:
name: static analysis name: static analysis
@ -258,15 +248,13 @@ jobs:
key: regen-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/test_regen.py') }} key: regen-${{ hashFiles('.github/workflows/selfdrive_tests.yaml', 'selfdrive/test/process_replay/test_regen.py') }}
- name: Build base Docker image - name: Build base Docker image
run: eval "$BUILD" run: eval "$BUILD"
- name: Build Docker image
run: eval "$BUILD_CL"
- name: Build openpilot - name: Build openpilot
run: | run: |
${{ env.RUN }} "scons -j$(nproc)" ${{ env.RUN }} "scons -j$(nproc)"
- name: Run regen - name: Run regen
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
${{ env.RUN_CL }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \ ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \
chmod -R 777 /tmp/comma_download_cache" chmod -R 777 /tmp/comma_download_cache"
test_modeld: test_modeld:
@ -279,9 +267,6 @@ jobs:
- uses: ./.github/workflows/setup-with-retry - uses: ./.github/workflows/setup-with-retry
- name: Build base Docker image - name: Build base Docker image
run: eval "$BUILD" run: eval "$BUILD"
- name: Build Docker image
# Sim docker is needed to get the OpenCL drivers
run: eval "$BUILD_CL"
- name: Build openpilot - name: Build openpilot
run: | run: |
${{ env.RUN }} "scons -j$(nproc)" ${{ env.RUN }} "scons -j$(nproc)"
@ -289,14 +274,14 @@ jobs:
- name: Run model replay with ONNX - name: Run model replay with ONNX
timeout-minutes: 4 timeout-minutes: 4
run: | run: |
${{ env.RUN_CL }} "unset PYTHONWARNINGS && \ ${{ env.RUN }} "unset PYTHONWARNINGS && \
ONNXCPU=1 NO_NAV=1 coverage run selfdrive/test/process_replay/model_replay.py && \ ONNXCPU=1 NO_NAV=1 coverage run selfdrive/test/process_replay/model_replay.py && \
coverage combine && \ coverage combine && \
coverage xml" coverage xml"
- name: Run unit tests - name: Run unit tests
timeout-minutes: 4 timeout-minutes: 4
run: | run: |
${{ env.RUN_CL }} "unset PYTHONWARNINGS && \ ${{ env.RUN }} "unset PYTHONWARNINGS && \
$PYTEST selfdrive/modeld" $PYTEST selfdrive/modeld"
- name: "Upload coverage to Codecov" - name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
@ -328,7 +313,7 @@ jobs:
- name: Build openpilot - name: Build openpilot
run: ${{ env.RUN }} "scons -j$(nproc)" run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Test car models - name: Test car models
timeout-minutes: 25 timeout-minutes: 10
run: | run: |
${{ env.RUN }} "$PYTEST selfdrive/car/tests/test_models.py && \ ${{ env.RUN }} "$PYTEST selfdrive/car/tests/test_models.py && \
chmod -R 777 /tmp/comma_download_cache" chmod -R 777 /tmp/comma_download_cache"

@ -12,17 +12,12 @@ concurrency:
env: env:
BASE_IMAGE: openpilot-base BASE_IMAGE: openpilot-base
CL_BASE_IMAGE: openpilot-base-cl
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
BUILD: selfdrive/test/docker_build.sh base BUILD: selfdrive/test/docker_build.sh base
RUN: docker run --shm-size 1G -v $GITHUB_WORKSPACE:/tmp/openpilot -w /tmp/openpilot -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c RUN: docker run --shm-size 1G -v $GITHUB_WORKSPACE:/tmp/openpilot -w /tmp/openpilot -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
BUILD_CL: selfdrive/test/docker_build.sh cl
RUN_CL: docker run --shm-size 1G -v $GITHUB_WORKSPACE:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/bash -c
jobs: jobs:
plotjuggler: plotjuggler:
@ -52,8 +47,6 @@ jobs:
with: with:
submodules: true submodules: true
- uses: ./.github/workflows/setup-with-retry - uses: ./.github/workflows/setup-with-retry
- name: Build base cl image
run: eval "$BUILD_CL"
- name: Setup to push to repo - name: Setup to push to repo
if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot' if: github.ref == 'refs/heads/master' && github.repository == 'commaai/openpilot'
run: | run: |
@ -104,6 +97,6 @@ jobs:
timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache
run: ${{ env.RUN }} "scons -j$(nproc)" run: ${{ env.RUN }} "scons -j$(nproc)"
- name: Test notebooks - name: Test notebooks
timeout-minutes: 2 timeout-minutes: 3
run: | run: |
${{ env.RUN }} "pip install nbmake && pytest --nbmake tools/car_porting/examples/" ${{ env.RUN }} "pip install nbmake && pytest --nbmake tools/car_porting/examples/"

@ -32,6 +32,11 @@ repos:
# if you've got a short variable name that's getting flagged, add it here # if you've got a short variable name that's getting flagged, add it here
- -L bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints - -L bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints
- --builtins clear,rare,informal,usage,code,names,en-GB_to_en-US - --builtins clear,rare,informal,usage,code,names,en-GB_to_en-US
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.2
hooks:
- id: ruff
exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)'
- repo: local - repo: local
hooks: hooks:
- id: mypy - id: mypy
@ -43,11 +48,6 @@ repos:
- --local-partial-types - --local-partial-types
- --explicit-package-bases - --explicit-package-bases
exclude: '^(third_party/)|(cereal/)|(opendbc/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)|(xx/)' exclude: '^(third_party/)|(cereal/)|(opendbc/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)|(xx/)'
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
hooks:
- id: ruff
exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)'
- repo: local - repo: local
hooks: hooks:
- id: cppcheck - id: cppcheck
@ -74,6 +74,14 @@ repos:
# https://google.github.io/styleguide/cppguide.html # https://google.github.io/styleguide/cppguide.html
# relevant rules are whitelisted, see all options with: cpplint --filter= # relevant rules are whitelisted, see all options with: cpplint --filter=
- --filter=-build,-legal,-readability,-runtime,-whitespace,+build/include_subdir,+build/forward_decl,+build/include_what_you_use,+build/deprecated,+whitespace/comma,+whitespace/line_length,+whitespace/empty_if_body,+whitespace/empty_loop_body,+whitespace/empty_conditional_body,+whitespace/forcolon,+whitespace/parens,+whitespace/semicolon,+whitespace/tab,+readability/braces - --filter=-build,-legal,-readability,-runtime,-whitespace,+build/include_subdir,+build/forward_decl,+build/include_what_you_use,+build/deprecated,+whitespace/comma,+whitespace/line_length,+whitespace/empty_if_body,+whitespace/empty_loop_body,+whitespace/empty_conditional_body,+whitespace/forcolon,+whitespace/parens,+whitespace/semicolon,+whitespace/tab,+readability/braces
- repo: https://github.com/MarcoGorelli/cython-lint
rev: v0.16.0
hooks:
- id: cython-lint
exclude: '^(third_party/)|(cereal/)|(body/)|(rednose/)|(rednose_repo/)|(opendbc/)|(panda/)|(generated/)'
args:
- --max-line-length=240
- --ignore=E111, E302, E305
- repo: local - repo: local
hooks: hooks:
- id: test_translations - id: test_translations
@ -90,6 +98,6 @@ repos:
args: args:
- --lock - --lock
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.27.4 rev: 0.28.0
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows

@ -22,6 +22,44 @@ RUN cd /tmp && \
rm -rf arm/ && \ rm -rf arm/ && \
rm -rf thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp rm -rf thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp
# Add OpenCL
RUN apt-get update && apt-get install -y --no-install-recommends \
apt-utils \
alien \
unzip \
tar \
curl \
xz-utils \
dbus \
gcc-arm-none-eabi \
tmux \
vim \
lsb-core \
libx11-6 \
&& rm -rf /var/lib/apt/lists/*
ARG INTEL_DRIVER=l_opencl_p_18.1.0.015.tgz
ARG INTEL_DRIVER_URL=https://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/15532
RUN mkdir -p /tmp/opencl-driver-intel
RUN cd /tmp/opencl-driver-intel && \
echo INTEL_DRIVER is $INTEL_DRIVER && \
curl -O $INTEL_DRIVER_URL/$INTEL_DRIVER && \
tar -xzf $INTEL_DRIVER && \
for i in $(basename $INTEL_DRIVER .tgz)/rpm/*.rpm; do alien --to-deb $i; done && \
dpkg -i *.deb && \
rm -rf $INTEL_DRIVER $(basename $INTEL_DRIVER .tgz) *.deb && \
mkdir -p /etc/OpenCL/vendors && \
echo /opt/intel/opencl_compilers_and_libraries_18.1.0.015/linux/compiler/lib/intel64_lin/libintelocl.so > /etc/OpenCL/vendors/intel.icd && \
cd / && \
rm -rf /tmp/opencl-driver-intel
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES graphics,utility,compute
ENV QTWEBENGINE_DISABLE_SANDBOX 1
RUN dbus-uuidgen > /etc/machine-id
ARG USER=batman ARG USER=batman
ARG USER_UID=1000 ARG USER_UID=1000
RUN useradd -m -s /bin/bash -u $USER_UID $USER RUN useradd -m -s /bin/bash -u $USER_UID $USER

@ -1,37 +0,0 @@
FROM ghcr.io/commaai/openpilot-base:latest
RUN apt-get update && apt-get install -y --no-install-recommends\
apt-utils \
alien \
unzip \
tar \
curl \
xz-utils \
dbus \
gcc-arm-none-eabi \
tmux \
vim \
lsb-core \
libx11-6 \
&& rm -rf /var/lib/apt/lists/*
# Intel OpenCL driver
ARG INTEL_DRIVER=l_opencl_p_18.1.0.015.tgz
ARG INTEL_DRIVER_URL=https://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/15532
RUN mkdir -p /tmp/opencl-driver-intel
WORKDIR /tmp/opencl-driver-intel
RUN echo INTEL_DRIVER is $INTEL_DRIVER && \
curl -O $INTEL_DRIVER_URL/$INTEL_DRIVER && \
tar -xzf $INTEL_DRIVER && \
for i in $(basename $INTEL_DRIVER .tgz)/rpm/*.rpm; do alien --to-deb $i; done && \
dpkg -i *.deb && \
rm -rf $INTEL_DRIVER $(basename $INTEL_DRIVER .tgz) *.deb && \
mkdir -p /etc/OpenCL/vendors && \
echo /opt/intel/opencl_compilers_and_libraries_18.1.0.015/linux/compiler/lib/intel64_lin/libintelocl.so > /etc/OpenCL/vendors/intel.icd && \
rm -rf /tmp/opencl-driver-intel
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES graphics,utility,compute
ENV QTWEBENGINE_DISABLE_SANDBOX 1
RUN dbus-uuidgen > /etc/machine-id

46
Jenkinsfile vendored

@ -12,10 +12,12 @@ def retryWithDelay(int maxRetries, int delay, Closure body) {
def device(String ip, String step_label, String cmd) { def device(String ip, String step_label, String cmd) {
withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) { withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) {
def ssh_cmd = """ def ssh_cmd = """
ssh -tt -o ConnectTimeout=30 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' /usr/bin/bash <<'END' ssh -tt -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' /usr/bin/bash <<'END'
set -e set -e
shopt -s huponexit # kill all child processes when the shell exits
export CI=1 export CI=1
export PYTHONWARNINGS=error export PYTHONWARNINGS=error
export LOGPRINT=debug export LOGPRINT=debug
@ -85,10 +87,6 @@ def deviceStage(String stageName, String deviceType, List extra_env, def steps)
device(device_ip, "git checkout", extra + "\n" + readFile("selfdrive/test/setup_device_ci.sh")) device(device_ip, "git checkout", extra + "\n" + readFile("selfdrive/test/setup_device_ci.sh"))
} }
steps.each { item -> steps.each { item ->
if (branch != "master" && item.size() == 3 && !hasDirectoryChanged(item[2])) {
println "Skipped '${item[0]}', no relevant changes were detected."
return;
}
device(device_ip, item[0], item[1]) device(device_ip, item[0], item[1])
} }
} }
@ -143,20 +141,6 @@ def setupCredentials() {
} }
} }
def hasDirectoryChanged(List<String> paths) {
for (change in currentBuild.changeSets) {
for (item in change.items) {
for (affectedPath in item.affectedPaths) {
for (path in paths) {
if (affectedPath.startsWith(path)) {
return true
}
}
}
}
}
return false
}
node { node {
env.CI = "1" env.CI = "1"
@ -207,7 +191,7 @@ node {
'HW + Unit Tests': { 'HW + Unit Tests': {
deviceStage("tici-hardware", "tici-common", ["UNSAFE=1"], [ deviceStage("tici-hardware", "tici-common", ["UNSAFE=1"], [
["build", "cd selfdrive/manager && ./build.py"], ["build", "cd selfdrive/manager && ./build.py"],
["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py", ["panda/", "selfdrive/boardd/"]], ["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py"],
["test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"], ["test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"],
["test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py"], ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py"],
["test pigeond", "pytest system/sensord/tests/test_pigeond.py"], ["test pigeond", "pytest system/sensord/tests/test_pigeond.py"],
@ -252,31 +236,13 @@ node {
deviceStage("tizi", "tizi", ["UNSAFE=1"], [ deviceStage("tizi", "tizi", ["UNSAFE=1"], [
["build openpilot", "cd selfdrive/manager && ./build.py"], ["build openpilot", "cd selfdrive/manager && ./build.py"],
["test boardd loopback", "SINGLE_PANDA=1 pytest selfdrive/boardd/tests/test_boardd_loopback.py"], ["test boardd loopback", "SINGLE_PANDA=1 pytest selfdrive/boardd/tests/test_boardd_loopback.py"],
["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py", ["panda/", "selfdrive/boardd/"]], ["test pandad", "pytest selfdrive/boardd/tests/test_pandad.py"],
["test amp", "pytest system/hardware/tici/tests/test_amplifier.py"], ["test amp", "pytest system/hardware/tici/tests/test_amplifier.py"],
["test hw", "pytest system/hardware/tici/tests/test_hardware.py"], ["test hw", "pytest system/hardware/tici/tests/test_hardware.py"],
["test qcomgpsd", "pytest system/qcomgpsd/tests/test_qcomgpsd.py", ["system/qcomgpsd/"]], ["test qcomgpsd", "pytest system/qcomgpsd/tests/test_qcomgpsd.py"],
]) ])
}, },
// *** PC tests ***
'PC tests': {
pcStage("PC tests") {
// tests that our build system's dependencies are configured properly,
// needs a machine with lots of cores
sh label: "test multi-threaded build",
script: '''#!/bin/bash
scons --no-cache --random -j$(nproc)'''
}
},
'car tests': {
pcStage("car tests") {
sh label: "build", script: "selfdrive/manager/build.py"
sh label: "run car tests", script: "cd selfdrive/car/tests && MAX_EXAMPLES=300 INTERNAL_SEG_CNT=300 FILEREADER_CACHE=1 \
INTERNAL_SEG_LIST=selfdrive/car/tests/test_models_segs.txt pytest test_models.py test_car_interfaces.py"
}
},
) )
} }
} catch (Exception e) { } catch (Exception e) {

@ -1,4 +1,9 @@
Version 0.9.6 (2024-02-XX) Version 0.9.7 (2024-XX-XX)
========================
* New driving model
* Support for many hybrid Ford models
Version 0.9.6 (2024-02-27)
======================== ========================
* New driving model * New driving model
* Vision model trained on more data * Vision model trained on more data
@ -9,8 +14,12 @@ Version 0.9.6 (2024-02-XX)
* AGNOS 9 * AGNOS 9
* comma body streaming and controls over WebRTC * comma body streaming and controls over WebRTC
* Improved fuzzy fingerprinting for many makes and models * Improved fuzzy fingerprinting for many makes and models
* Alpha longitudinal support for new Toyota models
* Chevrolet Equinox 2019-22 support thanks to JasonJShuler and nworb-cire!
* Dodge Durango 2020-21 support
* Hyundai Staria 2023 support thanks to sunnyhaibin! * Hyundai Staria 2023 support thanks to sunnyhaibin!
* Kia Niro Plug-in Hybrid 2022 support thanks to sunnyhaibin! * Kia Niro Plug-in Hybrid 2022 support thanks to sunnyhaibin!
* Lexus LC 2024 support thanks to nelsonjchen!
* Toyota RAV4 2023-24 support * Toyota RAV4 2023-24 support
* Toyota RAV4 Hybrid 2023-24 support * Toyota RAV4 Hybrid 2023-24 support

@ -1 +1 @@
Subproject commit 80e1e55f0dd71cea7f596e8b80c7c33865b689f3 Subproject commit 2fba1381f40df2654f42a2e4bed869f2b7d01a52

@ -1,7 +1,6 @@
import os import os
import tempfile import tempfile
import contextlib import contextlib
from typing import Optional
class CallbackReader: class CallbackReader:
@ -24,7 +23,7 @@ class CallbackReader:
@contextlib.contextmanager @contextlib.contextmanager
def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: Optional[str] = None, newline: Optional[str] = None, def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None,
overwrite: bool = False): overwrite: bool = False):
"""Write to a file atomically using a temporary file in the same directory as the destination file.""" """Write to a file atomically using a temporary file in the same directory as the destination file."""
dir_name = os.path.dirname(path) dir_name = os.path.dirname(path)

@ -1,6 +1,5 @@
import os import os
from functools import lru_cache from functools import lru_cache
from typing import Optional, List
def gpio_init(pin: int, output: bool) -> None: def gpio_init(pin: int, output: bool) -> None:
try: try:
@ -16,7 +15,7 @@ def gpio_set(pin: int, high: bool) -> None:
except Exception as e: except Exception as e:
print(f"Failed to set gpio {pin} value: {e}") print(f"Failed to set gpio {pin} value: {e}")
def gpio_read(pin: int) -> Optional[bool]: def gpio_read(pin: int) -> bool | None:
val = None val = None
try: try:
with open(f"/sys/class/gpio/gpio{pin}/value", 'rb') as f: with open(f"/sys/class/gpio/gpio{pin}/value", 'rb') as f:
@ -37,7 +36,7 @@ def gpio_export(pin: int) -> None:
print(f"Failed to export gpio {pin}") print(f"Failed to export gpio {pin}")
@lru_cache(maxsize=None) @lru_cache(maxsize=None)
def get_irq_action(irq: int) -> List[str]: def get_irq_action(irq: int) -> list[str]:
try: try:
with open(f"/sys/kernel/irq/{irq}/actions") as f: with open(f"/sys/kernel/irq/{irq}/actions") as f:
actions = f.read().strip().split(',') actions = f.read().strip().split(',')
@ -45,7 +44,7 @@ def get_irq_action(irq: int) -> List[str]:
except FileNotFoundError: except FileNotFoundError:
return [] return []
def get_irqs_for_action(action: str) -> List[str]: def get_irqs_for_action(action: str) -> list[str]:
ret = [] ret = []
with open("/proc/interrupts") as f: with open("/proc/interrupts") as f:
for l in f.readlines(): for l in f.readlines():

@ -6,7 +6,6 @@ example in common/tests/test_mock.py
import functools import functools
import threading import threading
from typing import List, Union
from cereal.messaging import PubMaster from cereal.messaging import PubMaster
from cereal.services import SERVICE_LIST from cereal.services import SERVICE_LIST
from openpilot.common.mock.generators import generate_liveLocationKalman from openpilot.common.mock.generators import generate_liveLocationKalman
@ -18,7 +17,7 @@ MOCK_GENERATOR = {
} }
def generate_messages_loop(services: List[str], done: threading.Event): def generate_messages_loop(services: list[str], done: threading.Event):
pm = PubMaster(services) pm = PubMaster(services)
rk = Ratekeeper(100) rk = Ratekeeper(100)
i = 0 i = 0
@ -32,7 +31,7 @@ def generate_messages_loop(services: List[str], done: threading.Event):
rk.keep_time() rk.keep_time()
def mock_messages(services: Union[List[str], str]): def mock_messages(services: list[str] | str):
if isinstance(services, str): if isinstance(services, str):
services = [services] services = [services]

@ -125,6 +125,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"ForcePowerDown", PERSISTENT}, {"ForcePowerDown", PERSISTENT},
{"GitBranch", PERSISTENT}, {"GitBranch", PERSISTENT},
{"GitCommit", PERSISTENT}, {"GitCommit", PERSISTENT},
{"GitCommitDate", PERSISTENT},
{"GitDiff", PERSISTENT}, {"GitDiff", PERSISTENT},
{"GithubSshKeys", PERSISTENT}, {"GithubSshKeys", PERSISTENT},
{"GithubUsername", PERSISTENT}, {"GithubUsername", PERSISTENT},

@ -29,7 +29,7 @@ cdef extern from "common/params.h":
def ensure_bytes(v): def ensure_bytes(v):
return v.encode() if isinstance(v, str) else v; return v.encode() if isinstance(v, str) else v
class UnknownKeyName(Exception): class UnknownKeyName(Exception):
pass pass

@ -2,14 +2,13 @@ import os
import shutil import shutil
import uuid import uuid
from typing import Optional
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
class OpenpilotPrefix: class OpenpilotPrefix:
def __init__(self, prefix: Optional[str] = None, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): def __init__(self, prefix: str | None = None, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False):
self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15])
self.msgq_path = os.path.join('/dev/shm', self.prefix) self.msgq_path = os.path.join('/dev/shm', self.prefix)
self.clean_dirs_on_exit = clean_dirs_on_exit self.clean_dirs_on_exit = clean_dirs_on_exit

@ -3,7 +3,6 @@ import gc
import os import os
import time import time
from collections import deque from collections import deque
from typing import Optional, List, Union
from setproctitle import getproctitle from setproctitle import getproctitle
@ -33,12 +32,12 @@ def set_realtime_priority(level: int) -> None:
os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(level)) os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(level))
def set_core_affinity(cores: List[int]) -> None: def set_core_affinity(cores: list[int]) -> None:
if not PC: if not PC:
os.sched_setaffinity(0, cores) os.sched_setaffinity(0, cores)
def config_realtime_process(cores: Union[int, List[int]], priority: int) -> None: def config_realtime_process(cores: int | list[int], priority: int) -> None:
gc.disable() gc.disable()
set_realtime_priority(priority) set_realtime_priority(priority)
c = cores if isinstance(cores, list) else [cores, ] c = cores if isinstance(cores, list) else [cores, ]
@ -46,7 +45,7 @@ def config_realtime_process(cores: Union[int, List[int]], priority: int) -> None
class Ratekeeper: class Ratekeeper:
def __init__(self, rate: float, print_delay_threshold: Optional[float] = 0.0) -> None: def __init__(self, rate: float, print_delay_threshold: float | None = 0.0) -> None:
"""Rate in Hz for ratekeeping. print_delay_threshold must be nonnegative.""" """Rate in Hz for ratekeeping. print_delay_threshold must be nonnegative."""
self._interval = 1. / rate self._interval = 1. / rate
self._next_frame_time = time.monotonic() + self._interval self._next_frame_time = time.monotonic() + self._interval

@ -1,5 +1,5 @@
import numpy as np import numpy as np
from typing import Callable from collections.abc import Callable
from openpilot.common.transformations.transformations import (ecef_euler_from_ned_single, from openpilot.common.transformations.transformations import (ecef_euler_from_ned_single,
euler2quat_single, euler2quat_single,

@ -1,4 +1,4 @@
#cython: language_level=3 # cython: language_level=3
from libcpp cimport bool from libcpp cimport bool
cdef extern from "orientation.cc": cdef extern from "orientation.cc":

@ -17,7 +17,6 @@ from openpilot.common.transformations.transformations cimport ecef2geodetic as e
from openpilot.common.transformations.transformations cimport LocalCoord_c from openpilot.common.transformations.transformations cimport LocalCoord_c
import cython
import numpy as np import numpy as np
cimport numpy as np cimport numpy as np
@ -34,14 +33,14 @@ cdef Matrix3 numpy2matrix(np.ndarray[double, ndim=2, mode="fortran"] m):
return Matrix3(<double*>m.data) return Matrix3(<double*>m.data)
cdef ECEF list2ecef(ecef): cdef ECEF list2ecef(ecef):
cdef ECEF e; cdef ECEF e
e.x = ecef[0] e.x = ecef[0]
e.y = ecef[1] e.y = ecef[1]
e.z = ecef[2] e.z = ecef[2]
return e return e
cdef NED list2ned(ned): cdef NED list2ned(ned):
cdef NED n; cdef NED n
n.n = ned[0] n.n = ned[0]
n.e = ned[1] n.e = ned[1]
n.d = ned[2] n.d = ned[2]
@ -61,7 +60,7 @@ def euler2quat_single(euler):
def quat2euler_single(quat): def quat2euler_single(quat):
cdef Quaternion q = Quaternion(quat[0], quat[1], quat[2], quat[3]) cdef Quaternion q = Quaternion(quat[0], quat[1], quat[2], quat[3])
cdef Vector3 e = quat2euler_c(q); cdef Vector3 e = quat2euler_c(q)
return [e(0), e(1), e(2)] return [e(0), e(1), e(2)]
def quat2rot_single(quat): def quat2rot_single(quat):

@ -1 +1 @@
#define COMMA_VERSION "0.9.6" #define COMMA_VERSION "0.9.7"

@ -1,3 +1,5 @@
import contextlib
import gc
import os import os
import pytest import pytest
import random import random
@ -24,46 +26,50 @@ def pytest_runtest_call(item):
yield yield
@pytest.fixture(scope="function", autouse=True) @contextlib.contextmanager
def openpilot_function_fixture(request): def clean_env():
starting_env = dict(os.environ) starting_env = dict(os.environ)
yield
os.environ.clear()
os.environ.update(starting_env)
@pytest.fixture(scope="function", autouse=True)
def openpilot_function_fixture(request):
random.seed(0) random.seed(0)
# setup a clean environment for each test with clean_env():
with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix: # setup a clean environment for each test
prefix = os.environ["OPENPILOT_PREFIX"] with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix:
prefix = os.environ["OPENPILOT_PREFIX"]
yield yield
# ensure the test doesn't change the prefix # ensure the test doesn't change the prefix
assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"] assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"]
os.environ.clear() # cleanup any started processes
os.environ.update(starting_env) manager.manager_cleanup()
# cleanup any started processes # some processes disable gc for performance, re-enable here
manager.manager_cleanup() if not gc.isenabled():
gc.enable()
gc.collect()
# If you use setUpClass, the environment variables won't be cleared properly, # If you use setUpClass, the environment variables won't be cleared properly,
# so we need to hook both the function and class pytest fixtures # so we need to hook both the function and class pytest fixtures
@pytest.fixture(scope="class", autouse=True) @pytest.fixture(scope="class", autouse=True)
def openpilot_class_fixture(): def openpilot_class_fixture():
starting_env = dict(os.environ) with clean_env():
yield
yield
os.environ.clear()
os.environ.update(starting_env)
@pytest.fixture(scope="class") @pytest.fixture(scope="function")
def tici_setup_fixture(): def tici_setup_fixture(openpilot_function_fixture):
"""Ensure a consistent state for tests on-device""" """Ensure a consistent state for tests on-device. Needs the openpilot function fixture to run first."""
HARDWARE.initialize_hardware() HARDWARE.initialize_hardware()
HARDWARE.set_power_save(False) HARDWARE.set_power_save(False)
os.system("pkill -9 -f athena") os.system("pkill -9 -f athena")
os.system("rm /dev/shm/*")
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)

@ -4,7 +4,7 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 276 Supported Cars # 288 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video| |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@ -23,6 +23,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Cadillac|Escalade ESV 2019[<sup>4</sup>](#footnotes)|Adaptive Cruise Control (ACC) & LKAS|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Cadillac&model=Escalade ESV 2019">Buy Here</a></sub></details>|| |Cadillac|Escalade ESV 2019[<sup>4</sup>](#footnotes)|Adaptive Cruise Control (ACC) & LKAS|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Cadillac&model=Escalade ESV 2019">Buy Here</a></sub></details>||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EV 2022-23">Buy Here</a></sub></details>|| |Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Bolt EV 2022-23">Buy Here</a></sub></details>||
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Equinox 2019-22">Buy Here</a></sub></details>||
|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Silverado 1500 2020-21">Buy Here</a></sub></details>|| |Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Silverado 1500 2020-21">Buy Here</a></sub></details>||
|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Trailblazer 2021-22">Buy Here</a></sub></details>|| |Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Trailblazer 2021-22">Buy Here</a></sub></details>||
|Chevrolet|Volt 2017-18[<sup>4</sup>](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Volt 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/QeMCN_4TFfQ" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Chevrolet|Volt 2017-18[<sup>4</sup>](#footnotes)|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 comma 3X<br>- 2 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chevrolet&model=Volt 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/QeMCN_4TFfQ" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
@ -33,13 +34,22 @@ A supported vehicle is one that just works when you install a comma device. All
|Chrysler|Pacifica Hybrid 2018|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chrysler&model=Pacifica Hybrid 2018">Buy Here</a></sub></details>|| |Chrysler|Pacifica Hybrid 2018|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chrysler&model=Pacifica Hybrid 2018">Buy Here</a></sub></details>||
|Chrysler|Pacifica Hybrid 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chrysler&model=Pacifica Hybrid 2019-23">Buy Here</a></sub></details>|| |Chrysler|Pacifica Hybrid 2019-23|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Chrysler&model=Pacifica Hybrid 2019-23">Buy Here</a></sub></details>||
|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Dodge&model=Durango 2020-21">Buy Here</a></sub></details>||
|Ford|Bronco Sport 2021-22|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Bronco Sport 2021-22">Buy Here</a></sub></details>|| |Ford|Bronco Sport 2021-22|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Bronco Sport 2021-22">Buy Here</a></sub></details>||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape 2020-22">Buy Here</a></sub></details>|| |Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape 2020-22">Buy Here</a></sub></details>||
|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Hybrid 2020-22">Buy Here</a></sub></details>||
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>||
|Ford|Explorer 2020-23|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer 2020-23">Buy Here</a></sub></details>|| |Ford|Explorer 2020-23|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer 2020-23">Buy Here</a></sub></details>||
|Ford|Explorer Hybrid 2020-23|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Explorer Hybrid 2020-23">Buy Here</a></sub></details>||
|Ford|Focus 2018[<sup>3</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Focus 2018">Buy Here</a></sub></details>|| |Ford|Focus 2018[<sup>3</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Focus 2018">Buy Here</a></sub></details>||
|Ford|Focus Hybrid 2018[<sup>3</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Focus Hybrid 2018">Buy Here</a></sub></details>||
|Ford|Kuga 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Kuga 2020-22">Buy Here</a></sub></details>|| |Ford|Kuga 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Kuga 2020-22">Buy Here</a></sub></details>||
|Ford|Kuga Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Kuga Hybrid 2020-22">Buy Here</a></sub></details>||
|Ford|Kuga Plug-in Hybrid 2020-22|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Kuga Plug-in Hybrid 2020-22">Buy Here</a></sub></details>||
|Ford|Maverick 2022|LARIAT Luxury|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick 2022">Buy Here</a></sub></details>|| |Ford|Maverick 2022|LARIAT Luxury|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick 2022">Buy Here</a></sub></details>||
|Ford|Maverick 2023|Co-Pilot360 Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick 2023">Buy Here</a></sub></details>|| |Ford|Maverick 2023|Co-Pilot360 Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick 2023">Buy Here</a></sub></details>||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2022">Buy Here</a></sub></details>||
|Ford|Maverick Hybrid 2023|Co-Pilot360 Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Ford&model=Maverick Hybrid 2023">Buy Here</a></sub></details>||
|Genesis|G70 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2018-19">Buy Here</a></sub></details>|| |Genesis|G70 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2018-19">Buy Here</a></sub></details>||
|Genesis|G70 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2020">Buy Here</a></sub></details>|| |Genesis|G70 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G70 2020">Buy Here</a></sub></details>||
|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai J connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G80 2017">Buy Here</a></sub></details>|| |Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai J connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Genesis&model=G80 2017">Buy Here</a></sub></details>||
@ -72,7 +82,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Odyssey 2018-20|Honda Sensing|openpilot|25 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Odyssey 2018-20">Buy Here</a></sub></details>|| |Honda|Odyssey 2018-20|Honda Sensing|openpilot|25 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Odyssey 2018-20">Buy Here</a></sub></details>||
|Honda|Passport 2019-23|All|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Passport 2019-23">Buy Here</a></sub></details>|| |Honda|Passport 2019-23|All|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Passport 2019-23">Buy Here</a></sub></details>||
|Honda|Pilot 2016-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Pilot 2016-22">Buy Here</a></sub></details>|| |Honda|Pilot 2016-22|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Pilot 2016-22">Buy Here</a></sub></details>||
|Honda|Ridgeline 2017-23|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Ridgeline 2017-23">Buy Here</a></sub></details>|| |Honda|Ridgeline 2017-24|Honda Sensing|openpilot|25 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Honda&model=Ridgeline 2017-24">Buy Here</a></sub></details>||
|Hyundai|Azera 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Azera 2022">Buy Here</a></sub></details>|| |Hyundai|Azera 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Azera 2022">Buy Here</a></sub></details>||
|Hyundai|Azera Hybrid 2019|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Azera Hybrid 2019">Buy Here</a></sub></details>|| |Hyundai|Azera Hybrid 2019|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Azera Hybrid 2019">Buy Here</a></sub></details>||
|Hyundai|Azera Hybrid 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Azera Hybrid 2020">Buy Here</a></sub></details>|| |Hyundai|Azera Hybrid 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Hyundai&model=Azera Hybrid 2020">Buy Here</a></sub></details>||
@ -162,6 +172,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=GS F 2016">Buy Here</a></sub></details>|| |Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=GS F 2016">Buy Here</a></sub></details>||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2017-19">Buy Here</a></sub></details>|| |Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2017-19">Buy Here</a></sub></details>||
|Lexus|IS 2022-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2022-23">Buy Here</a></sub></details>|| |Lexus|IS 2022-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=IS 2022-23">Buy Here</a></sub></details>||
|Lexus|LC 2024|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=LC 2024">Buy Here</a></sub></details>||
|Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2018-19">Buy Here</a></sub></details>|| |Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2018-19">Buy Here</a></sub></details>||
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2020-21">Buy Here</a></sub></details>|| |Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX 2020-21">Buy Here</a></sub></details>||
|Lexus|NX Hybrid 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX Hybrid 2018-19">Buy Here</a></sub></details>|| |Lexus|NX Hybrid 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=NX Hybrid 2018-19">Buy Here</a></sub></details>||
@ -174,7 +185,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Lexus|RX Hybrid 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=RX Hybrid 2017-19">Buy Here</a></sub></details>|| |Lexus|RX Hybrid 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=RX Hybrid 2017-19">Buy Here</a></sub></details>||
|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=RX Hybrid 2020-22">Buy Here</a></sub></details>|| |Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=RX Hybrid 2020-22">Buy Here</a></sub></details>||
|Lexus|UX Hybrid 2019-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=UX Hybrid 2019-23">Buy Here</a></sub></details>|| |Lexus|UX Hybrid 2019-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lexus&model=UX Hybrid 2019-23">Buy Here</a></sub></details>||
|Lincoln|Aviator 2020-21|Co-Pilot360 Plus|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lincoln&model=Aviator 2020-21">Buy Here</a></sub></details>|| |Lincoln|Aviator 2020-23|Co-Pilot360 Plus|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lincoln&model=Aviator 2020-23">Buy Here</a></sub></details>||
|Lincoln|Aviator Plug-in Hybrid 2020-23|Co-Pilot360 Plus|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Lincoln&model=Aviator Plug-in Hybrid 2020-23">Buy Here</a></sub></details>||
|MAN|eTGE 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=MAN&model=eTGE 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |MAN|eTGE 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=MAN&model=eTGE 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|MAN|TGE 2017-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=MAN&model=TGE 2017-23">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |MAN|TGE 2017-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=MAN&model=TGE 2017-23">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Mazda|CX-5 2022-24|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Mazda&model=CX-5 2022-24">Buy Here</a></sub></details>|| |Mazda|CX-5 2022-24|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 RJ45 cable (7 ft)<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Mazda&model=CX-5 2022-24">Buy Here</a></sub></details>||
@ -213,9 +225,9 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|Avalon Hybrid 2019-21|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Avalon Hybrid 2019-21">Buy Here</a></sub></details>|| |Toyota|Avalon Hybrid 2019-21|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Avalon Hybrid 2019-21">Buy Here</a></sub></details>||
|Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Avalon Hybrid 2022">Buy Here</a></sub></details>|| |Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Avalon Hybrid 2022">Buy Here</a></sub></details>||
|Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=C-HR 2017-20">Buy Here</a></sub></details>|| |Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=C-HR 2017-20">Buy Here</a></sub></details>||
|Toyota|C-HR 2021|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=C-HR 2021">Buy Here</a></sub></details>|| |Toyota|C-HR 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=C-HR 2021">Buy Here</a></sub></details>||
|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=C-HR Hybrid 2017-20">Buy Here</a></sub></details>|| |Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=C-HR Hybrid 2017-20">Buy Here</a></sub></details>||
|Toyota|C-HR Hybrid 2021-22|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=C-HR Hybrid 2021-22">Buy Here</a></sub></details>|| |Toyota|C-HR Hybrid 2021-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=C-HR Hybrid 2021-22">Buy Here</a></sub></details>||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>9</sup>](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Toyota|Camry 2018-20|All|Stock|0 mph[<sup>9</sup>](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>9</sup>](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Camry 2021-24">Buy Here</a></sub></details>|| |Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>9</sup>](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Camry 2021-24">Buy Here</a></sub></details>||
|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
@ -241,13 +253,13 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2016">Buy Here</a></sub></details>|| |Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2016">Buy Here</a></sub></details>||
|Toyota|RAV4 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2017-18">Buy Here</a></sub></details>|| |Toyota|RAV4 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2017-18">Buy Here</a></sub></details>||
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Toyota|RAV4 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2022">Buy Here</a></sub></details>|| |Toyota|RAV4 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2022">Buy Here</a></sub></details>||
|Toyota|RAV4 2023-24|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2023-24">Buy Here</a></sub></details>|| |Toyota|RAV4 2023-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 2023-24">Buy Here</a></sub></details>||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Toyota|RAV4 Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2019-21">Buy Here</a></sub></details>|| |Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2019-21">Buy Here</a></sub></details>||
|Toyota|RAV4 Hybrid 2022|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Toyota|RAV4 Hybrid 2023-24|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2023-24">Buy Here</a></sub></details>|| |Toyota|RAV4 Hybrid 2023-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=RAV4 Hybrid 2023-24">Buy Here</a></sub></details>||
|Toyota|Sienna 2018-20|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Toyota|Sienna 2018-20|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 RJ45 cable (7 ft)<br>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v2<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Toyota&model=Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,13</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 J533 connector<br>- 1 USB-C coupler<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x.html?make=Volkswagen&model=Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|

@ -15,6 +15,11 @@ function agnos_init {
# set success flag for current boot slot # set success flag for current boot slot
sudo abctl --set_success sudo abctl --set_success
# TODO: do this without udev in AGNOS
# udev does this, but sometimes we startup faster
sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
# Check if AGNOS update is required # Check if AGNOS update is required
if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
AGNOS_PY="$DIR/system/hardware/tici/agnos.py" AGNOS_PY="$DIR/system/hardware/tici/agnos.py"

@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="9.3" export AGNOS_VERSION="9.7"
fi fi
export STAGING_ROOT="/data/safe_staging" export STAGING_ROOT="/data/safe_staging"

@ -1 +1 @@
Subproject commit 7397e466d9cfd7f5bc1f49218b8d2afeedec582b Subproject commit 951ab07fdcbce023a5c927f56bbf94e0f2322366

@ -1 +1 @@
Subproject commit f48fc21a17079bc04cfb3d8042fd2d67d0aac104 Subproject commit 6aa4b550336136bc20a6abb307cf310e876eba28

458
poetry.lock generated

@ -825,43 +825,43 @@ files = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "42.0.2" version = "42.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a"},
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, {file = "cryptography-42.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, {file = "cryptography-42.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, {file = "cryptography-42.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b"},
{file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, {file = "cryptography-42.0.3-cp37-abi3-win32.whl", hash = "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5"},
{file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, {file = "cryptography-42.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54"},
{file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, {file = "cryptography-42.0.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, {file = "cryptography-42.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, {file = "cryptography-42.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65"},
{file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, {file = "cryptography-42.0.3-cp39-abi3-win32.whl", hash = "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3"},
{file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, {file = "cryptography-42.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, {file = "cryptography-42.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, {file = "cryptography-42.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, {file = "cryptography-42.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, {file = "cryptography-42.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, {file = "cryptography-42.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, {file = "cryptography-42.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd"},
{file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, {file = "cryptography-42.0.3.tar.gz", hash = "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe"},
] ]
[package.dependencies] [package.dependencies]
@ -989,22 +989,22 @@ files = [
[[package]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.5.0" version = "2.6.1"
description = "DNS toolkit" description = "DNS toolkit"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "dnspython-2.5.0-py3-none-any.whl", hash = "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6"}, {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
{file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
] ]
[package.extras] [package.extras]
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=5.0.3)", "mypy (>=1.0.1)", "pylint (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "sphinx (>=7.0.0)", "twine (>=4.0.0)", "wheel (>=0.41.0)"] dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
dnssec = ["cryptography (>=41)"] dnssec = ["cryptography (>=41)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.25.1)"] doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
doq = ["aioquic (>=0.9.20)"] doq = ["aioquic (>=0.9.25)"]
idna = ["idna (>=2.1)"] idna = ["idna (>=3.6)"]
trio = ["trio (>=0.14)"] trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"] wmi = ["wmi (>=1.5.1)"]
[[package]] [[package]]
@ -1131,60 +1131,60 @@ files = [
[[package]] [[package]]
name = "fonttools" name = "fonttools"
version = "4.47.2" version = "4.49.0"
description = "Tools to manipulate font files" description = "Tools to manipulate font files"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df"}, {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d970ecca0aac90d399e458f0b7a8a597e08f95de021f17785fb68e2dc0b99717"},
{file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1"}, {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac9a745b7609f489faa65e1dc842168c18530874a5f5b742ac3dd79e26bca8bc"},
{file = "fonttools-4.47.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c"}, {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ba0e00620ca28d4ca11fc700806fd69144b463aa3275e1b36e56c7c09915559"},
{file = "fonttools-4.47.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8"}, {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdee3ab220283057e7840d5fb768ad4c2ebe65bdba6f75d5d7bf47f4e0ed7d29"},
{file = "fonttools-4.47.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670"}, {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ce7033cb61f2bb65d8849658d3786188afd80f53dad8366a7232654804529532"},
{file = "fonttools-4.47.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c"}, {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:07bc5ea02bb7bc3aa40a1eb0481ce20e8d9b9642a9536cde0218290dd6085828"},
{file = "fonttools-4.47.2-cp310-cp310-win32.whl", hash = "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0"}, {file = "fonttools-4.49.0-cp310-cp310-win32.whl", hash = "sha256:86eef6aab7fd7c6c8545f3ebd00fd1d6729ca1f63b0cb4d621bccb7d1d1c852b"},
{file = "fonttools-4.47.2-cp310-cp310-win_amd64.whl", hash = "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1"}, {file = "fonttools-4.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fac1b7eebfce75ea663e860e7c5b4a8831b858c17acd68263bc156125201abf"},
{file = "fonttools-4.47.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b"}, {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edc0cce355984bb3c1d1e89d6a661934d39586bb32191ebff98c600f8957c63e"},
{file = "fonttools-4.47.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac"}, {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83a0d9336de2cba86d886507dd6e0153df333ac787377325a39a2797ec529814"},
{file = "fonttools-4.47.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c"}, {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36c8865bdb5cfeec88f5028e7e592370a0657b676c6f1d84a2108e0564f90e22"},
{file = "fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70"}, {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33037d9e56e2562c710c8954d0f20d25b8386b397250d65581e544edc9d6b942"},
{file = "fonttools-4.47.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e"}, {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8fb022d799b96df3eaa27263e9eea306bd3d437cc9aa981820850281a02b6c9a"},
{file = "fonttools-4.47.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703"}, {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33c584c0ef7dc54f5dd4f84082eabd8d09d1871a3d8ca2986b0c0c98165f8e86"},
{file = "fonttools-4.47.2-cp311-cp311-win32.whl", hash = "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c"}, {file = "fonttools-4.49.0-cp311-cp311-win32.whl", hash = "sha256:cbe61b158deb09cffdd8540dc4a948d6e8f4d5b4f3bf5cd7db09bd6a61fee64e"},
{file = "fonttools-4.47.2-cp311-cp311-win_amd64.whl", hash = "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9"}, {file = "fonttools-4.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc11e5114f3f978d0cea7e9853627935b30d451742eeb4239a81a677bdee6bf6"},
{file = "fonttools-4.47.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635"}, {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d647a0e697e5daa98c87993726da8281c7233d9d4ffe410812a4896c7c57c075"},
{file = "fonttools-4.47.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d"}, {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f3bbe672df03563d1f3a691ae531f2e31f84061724c319652039e5a70927167e"},
{file = "fonttools-4.47.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb"}, {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bebd91041dda0d511b0d303180ed36e31f4f54b106b1259b69fade68413aa7ff"},
{file = "fonttools-4.47.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07"}, {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4145f91531fd43c50f9eb893faa08399816bb0b13c425667c48475c9f3a2b9b5"},
{file = "fonttools-4.47.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71"}, {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea329dafb9670ffbdf4dbc3b0e5c264104abcd8441d56de77f06967f032943cb"},
{file = "fonttools-4.47.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f"}, {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c076a9e548521ecc13d944b1d261ff3d7825048c338722a4bd126d22316087b7"},
{file = "fonttools-4.47.2-cp312-cp312-win32.whl", hash = "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085"}, {file = "fonttools-4.49.0-cp312-cp312-win32.whl", hash = "sha256:b607ea1e96768d13be26d2b400d10d3ebd1456343eb5eaddd2f47d1c4bd00880"},
{file = "fonttools-4.47.2-cp312-cp312-win_amd64.whl", hash = "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4"}, {file = "fonttools-4.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:a974c49a981e187381b9cc2c07c6b902d0079b88ff01aed34695ec5360767034"},
{file = "fonttools-4.47.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc"}, {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b85ec0bdd7bdaa5c1946398cbb541e90a6dfc51df76dfa88e0aaa41b335940cb"},
{file = "fonttools-4.47.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952"}, {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af20acbe198a8a790618ee42db192eb128afcdcc4e96d99993aca0b60d1faeb4"},
{file = "fonttools-4.47.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa"}, {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d418b1fee41a1d14931f7ab4b92dc0bc323b490e41d7a333eec82c9f1780c75"},
{file = "fonttools-4.47.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b"}, {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b44a52b8e6244b6548851b03b2b377a9702b88ddc21dcaf56a15a0393d425cb9"},
{file = "fonttools-4.47.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6"}, {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7125068e04a70739dad11857a4d47626f2b0bd54de39e8622e89701836eabd"},
{file = "fonttools-4.47.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946"}, {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29e89d0e1a7f18bc30f197cfadcbef5a13d99806447c7e245f5667579a808036"},
{file = "fonttools-4.47.2-cp38-cp38-win32.whl", hash = "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b"}, {file = "fonttools-4.49.0-cp38-cp38-win32.whl", hash = "sha256:9d95fa0d22bf4f12d2fb7b07a46070cdfc19ef5a7b1c98bc172bfab5bf0d6844"},
{file = "fonttools-4.47.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae"}, {file = "fonttools-4.49.0-cp38-cp38-win_amd64.whl", hash = "sha256:768947008b4dc552d02772e5ebd49e71430a466e2373008ce905f953afea755a"},
{file = "fonttools-4.47.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6"}, {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08877e355d3dde1c11973bb58d4acad1981e6d1140711230a4bfb40b2b937ccc"},
{file = "fonttools-4.47.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506"}, {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdb54b076f25d6b0f0298dc706acee5052de20c83530fa165b60d1f2e9cbe3cb"},
{file = "fonttools-4.47.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37"}, {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af65c720520710cc01c293f9c70bd69684365c6015cc3671db2b7d807fe51f2"},
{file = "fonttools-4.47.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c"}, {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f255ce8ed7556658f6d23f6afd22a6d9bbc3edb9b96c96682124dc487e1bf42"},
{file = "fonttools-4.47.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899"}, {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d00af0884c0e65f60dfaf9340e26658836b935052fdd0439952ae42e44fdd2be"},
{file = "fonttools-4.47.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7"}, {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:263832fae27481d48dfafcc43174644b6706639661e242902ceb30553557e16c"},
{file = "fonttools-4.47.2-cp39-cp39-win32.whl", hash = "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50"}, {file = "fonttools-4.49.0-cp39-cp39-win32.whl", hash = "sha256:0404faea044577a01bb82d47a8fa4bc7a54067fa7e324785dd65d200d6dd1133"},
{file = "fonttools-4.47.2-cp39-cp39-win_amd64.whl", hash = "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8"}, {file = "fonttools-4.49.0-cp39-cp39-win_amd64.whl", hash = "sha256:b050d362df50fc6e38ae3954d8c29bf2da52be384649ee8245fdb5186b620836"},
{file = "fonttools-4.47.2-py3-none-any.whl", hash = "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184"}, {file = "fonttools-4.49.0-py3-none-any.whl", hash = "sha256:af281525e5dd7fa0b39fb1667b8d5ca0e2a9079967e14c4bfe90fd1cd13e0f18"},
{file = "fonttools-4.47.2.tar.gz", hash = "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3"}, {file = "fonttools-4.49.0.tar.gz", hash = "sha256:ebf46e7f01b7af7861310417d7c49591a85d99146fc23a5ba82fdb28af156321"},
] ]
[package.extras] [package.extras]
all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"]
graphite = ["lz4 (>=1.7.4.2)"] graphite = ["lz4 (>=1.7.4.2)"]
interpolatable = ["munkres", "pycairo", "scipy"] interpolatable = ["munkres", "pycairo", "scipy"]
lxml = ["lxml (>=4.0,<5)"] lxml = ["lxml (>=4.0)"]
pathops = ["skia-pathops (>=0.5.0)"] pathops = ["skia-pathops (>=0.5.0)"]
plot = ["matplotlib"] plot = ["matplotlib"]
repacker = ["uharfbuzz (>=0.23.0)"] repacker = ["uharfbuzz (>=0.23.0)"]
@ -1563,13 +1563,13 @@ zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.1)"]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.33" version = "2.5.35"
description = "File identification library for Python" description = "File identification library for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
{file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
] ]
[package.extras] [package.extras]
@ -2116,39 +2116,39 @@ files = [
[[package]] [[package]]
name = "matplotlib" name = "matplotlib"
version = "3.8.2" version = "3.8.3"
description = "Python plotting package" description = "Python plotting package"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7"}, {file = "matplotlib-3.8.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cf60138ccc8004f117ab2a2bad513cc4d122e55864b4fe7adf4db20ca68a078f"},
{file = "matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367"}, {file = "matplotlib-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f557156f7116be3340cdeef7f128fa99b0d5d287d5f41a16e169819dcf22357"},
{file = "matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18"}, {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f386cf162b059809ecfac3bcc491a9ea17da69fa35c8ded8ad154cd4b933d5ec"},
{file = "matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31"}, {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c5f96f57b0369c288bf6f9b5274ba45787f7e0589a34d24bdbaf6d3344632f"},
{file = "matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a"}, {file = "matplotlib-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83e0f72e2c116ca7e571c57aa29b0fe697d4c6425c4e87c6e994159e0c008635"},
{file = "matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a"}, {file = "matplotlib-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c5c8290074ba31a41db1dc332dc2b62def469ff33766cbe325d32a3ee291aea"},
{file = "matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63"}, {file = "matplotlib-3.8.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5184e07c7e1d6d1481862ee361905b7059f7fe065fc837f7c3dc11eeb3f2f900"},
{file = "matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8"}, {file = "matplotlib-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7e7e0993d0758933b1a241a432b42c2db22dfa37d4108342ab4afb9557cbe3e"},
{file = "matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6"}, {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b36ad07eac9740fc76c2aa16edf94e50b297d6eb4c081e3add863de4bb19a7"},
{file = "matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788"}, {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c42dae72a62f14982f1474f7e5c9959fc4bc70c9de11cc5244c6e766200ba65"},
{file = "matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0"}, {file = "matplotlib-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf5932eee0d428192c40b7eac1399d608f5d995f975cdb9d1e6b48539a5ad8d0"},
{file = "matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717"}, {file = "matplotlib-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:40321634e3a05ed02abf7c7b47a50be50b53ef3eaa3a573847431a545585b407"},
{file = "matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627"}, {file = "matplotlib-3.8.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09074f8057917d17ab52c242fdf4916f30e99959c1908958b1fc6032e2d0f6d4"},
{file = "matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4"}, {file = "matplotlib-3.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5745f6d0fb5acfabbb2790318db03809a253096e98c91b9a31969df28ee604aa"},
{file = "matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d"}, {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97653d869a71721b639714b42d87cda4cfee0ee74b47c569e4874c7590c55c5"},
{file = "matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331"}, {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242489efdb75b690c9c2e70bb5c6550727058c8a614e4c7716f363c27e10bba1"},
{file = "matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213"}, {file = "matplotlib-3.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:83c0653c64b73926730bd9ea14aa0f50f202ba187c307a881673bad4985967b7"},
{file = "matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630"}, {file = "matplotlib-3.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef6c1025a570354297d6c15f7d0f296d95f88bd3850066b7f1e7b4f2f4c13a39"},
{file = "matplotlib-3.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f"}, {file = "matplotlib-3.8.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c4af3f7317f8a1009bbb2d0bf23dfaba859eb7dd4ccbd604eba146dccaaaf0a4"},
{file = "matplotlib-3.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89"}, {file = "matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c6e00a65d017d26009bac6808f637b75ceade3e1ff91a138576f6b3065eeeba"},
{file = "matplotlib-3.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917"}, {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7b49ab49a3bea17802df6872f8d44f664ba8f9be0632a60c99b20b6db2165b7"},
{file = "matplotlib-3.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843"}, {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6728dde0a3997396b053602dbd907a9bd64ec7d5cf99e728b404083698d3ca01"},
{file = "matplotlib-3.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8"}, {file = "matplotlib-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:813925d08fb86aba139f2d31864928d67511f64e5945ca909ad5bc09a96189bb"},
{file = "matplotlib-3.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4"}, {file = "matplotlib-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:cd3a0c2be76f4e7be03d34a14d49ded6acf22ef61f88da600a18a5cd8b3c5f3c"},
{file = "matplotlib-3.8.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b"}, {file = "matplotlib-3.8.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fa93695d5c08544f4a0dfd0965f378e7afc410d8672816aff1e81be1f45dbf2e"},
{file = "matplotlib-3.8.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20"}, {file = "matplotlib-3.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9764df0e8778f06414b9d281a75235c1e85071f64bb5d71564b97c1306a2afc"},
{file = "matplotlib-3.8.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa"}, {file = "matplotlib-3.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5e431a09e6fab4012b01fc155db0ce6dccacdbabe8198197f523a4ef4805eb26"},
{file = "matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1"}, {file = "matplotlib-3.8.3.tar.gz", hash = "sha256:7b416239e9ae38be54b028abbf9048aff5054a9aba5416bef0bd17f9162ce161"},
] ]
[package.dependencies] [package.dependencies]
@ -2194,13 +2194,13 @@ files = [
[[package]] [[package]]
name = "metadrive-simulator" name = "metadrive-simulator"
version = "0.4.2.2" version = "0.4.2.3"
description = "An open-ended driving simulator with infinite scenes" description = "An open-ended driving simulator with infinite scenes"
optional = false optional = false
python-versions = ">=3.6, <3.12" python-versions = ">=3.6, <3.12"
files = [ files = [
{file = "metadrive-simulator-0.4.2.2.tar.gz", hash = "sha256:dcce9f9c73b6055e70480af8543058b95e8c4f68d2595107f3ef36c9be03f6bb"}, {file = "metadrive-simulator-0.4.2.3.tar.gz", hash = "sha256:bcda7d07146128161b0bc2cc337e01612b9222202706370043b52f7936a8a277"},
{file = "metadrive_simulator-0.4.2.2-py3-none-any.whl", hash = "sha256:165ba0b5275313a71090ba0e73d51b7b5e05391b071d3a2114d999ac9287d150"}, {file = "metadrive_simulator-0.4.2.3-py3-none-any.whl", hash = "sha256:f6fff20b931bb956c55e0e81bf1f6b641169a84a4c853435a8b26bd51c8e9876"},
] ]
[package.dependencies] [package.dependencies]
@ -3041,17 +3041,17 @@ testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "polyline" name = "polyline"
version = "2.0.1" version = "2.0.2"
description = "A Python implementation of Google's Encoded Polyline Algorithm Format." description = "A Python implementation of Google's Encoded Polyline Algorithm Format."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "polyline-2.0.1-py3-none-any.whl", hash = "sha256:7b1ff647be393143c1b9268738d9efb98a327dd0b29f9c3b84552dff0e34ea3c"}, {file = "polyline-2.0.2-py3-none-any.whl", hash = "sha256:389655c893bdabf2863c6aaa49490cf83dcdcec86ae715f67044ee98be57bef5"},
{file = "polyline-2.0.1.tar.gz", hash = "sha256:74cb5cea098dddf09d1a5a1f17af9184d371cbf3e9723de0194e530ec39ca1f6"}, {file = "polyline-2.0.2.tar.gz", hash = "sha256:10541e759c5fd51f746ee304e9af94744089a4055b6257b293b3afd1df64e369"},
] ]
[package.extras] [package.extras]
dev = ["pylint (>=2.15.10,<2.16.0)", "pytest (>=7.0,<8.0)", "pytest-cov (>=4.0,<5.0)", "sphinx (>=4.2.0,<4.3.0)", "sphinx-rtd-theme (>=1.0.0,<1.1.0)", "toml (>=0.10.2,<0.11.0)"] dev = ["pylint (>=3.0.3,<3.1.0)", "pytest (>=7.0,<8.0)", "pytest-cov (>=4.0,<5.0)", "sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.0,<1.3.0)", "toml (>=0.10.2,<0.11.0)"]
publish = ["build (>=0.8,<1.0)", "twine (>=4.0,<5.0)"] publish = ["build (>=0.8,<1.0)", "twine (>=4.0,<5.0)"]
[[package]] [[package]]
@ -3085,13 +3085,13 @@ files = [
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "3.6.0" version = "3.6.2"
description = "A framework for managing and maintaining multi-language pre-commit hooks." description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"},
{file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"},
] ]
[package.dependencies] [package.dependencies]
@ -3113,22 +3113,22 @@ files = [
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "4.25.2" version = "4.25.3"
description = "" description = ""
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"},
{file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"},
{file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"},
{file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"},
{file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"},
{file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"},
{file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"},
{file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"},
{file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"},
{file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"},
{file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
] ]
[[package]] [[package]]
@ -6468,13 +6468,13 @@ cp2110 = ["hidapi"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.0.0" version = "8.0.1"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, {file = "pytest-8.0.1-py3-none-any.whl", hash = "sha256:3e4f16fe1c0a9dc9d9389161c127c3edc5d810c38d6793042fb81d9f48a59fca"},
{file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, {file = "pytest-8.0.1.tar.gz", hash = "sha256:267f6563751877d772019b13aacbe4e860d73fe8f651f28112e9ac37de7513ae"},
] ]
[package.dependencies] [package.dependencies]
@ -6639,12 +6639,12 @@ numpy = ["numpy (>=1.6.0)"]
[[package]] [[package]]
name = "pytweening" name = "pytweening"
version = "1.0.7" version = "1.1.0"
description = "A collection of tweening / easing functions." description = "A collection of tweening / easing functions."
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "pytweening-1.0.7.tar.gz", hash = "sha256:767134f1bf57b76c1ce9f692dd1cfc776d9a279de6724e8d04854508fd7ededb"}, {file = "pytweening-1.1.0.tar.gz", hash = "sha256:0d8e14af529dd816ad4aa4a86757dfb5fe2fc2897e06f5db60183706a9370828"},
] ]
[[package]] [[package]]
@ -6924,28 +6924,28 @@ docs = ["furo (==2023.9.10)", "pyenchant (==3.2.2)", "sphinx (==7.1.2)", "sphinx
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.2.0" version = "0.2.2"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:638ea3294f800d18bae84a492cb5a245c8d29c90d19a91d8e338937a4c27fca0"}, {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"},
{file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff35433fcf4dff6d610738712152df6b7d92351a1bde8e00bd405b08b3d5759"}, {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"},
{file = "ruff-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9faafbdcf4f53917019f2c230766da437d4fd5caecd12ddb68bb6a17d74399"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"},
{file = "ruff-0.2.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8153a3e4128ed770871c47545f1ae7b055023e0c222ff72a759f5a341ee06483"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"},
{file = "ruff-0.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a75a98ae989a27090e9c51f763990ad5bbc92d20626d54e9701c7fe597f399"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"},
{file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:87057dd2fdde297130ff99553be8549ca38a2965871462a97394c22ed2dfc19d"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"},
{file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d232f99d3ab00094ebaf88e0fb7a8ccacaa54cc7fa3b8993d9627a11e6aed7a"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"},
{file = "ruff-0.2.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3c641f95f435fc6754b05591774a17df41648f0daf3de0d75ad3d9f099ab92"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"},
{file = "ruff-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3826fb34c144ef1e171b323ed6ae9146ab76d109960addca730756dc19dc7b22"}, {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"},
{file = "ruff-0.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eceab7d85d09321b4de18b62d38710cf296cb49e98979960a59c6b9307c18cfe"}, {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"},
{file = "ruff-0.2.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:30ad74687e1f4a9ff8e513b20b82ccadb6bd796fe5697f1e417189c5cde6be3e"}, {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"},
{file = "ruff-0.2.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7e3818698f8460bd0f8d4322bbe99db8327e9bc2c93c789d3159f5b335f47da"}, {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"},
{file = "ruff-0.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:edf23041242c48b0d8295214783ef543847ef29e8226d9f69bf96592dba82a83"}, {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"},
{file = "ruff-0.2.0-py3-none-win32.whl", hash = "sha256:e155147199c2714ff52385b760fe242bb99ea64b240a9ffbd6a5918eb1268843"}, {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"},
{file = "ruff-0.2.0-py3-none-win_amd64.whl", hash = "sha256:ba918e01cdd21e81b07555564f40d307b0caafa9a7a65742e98ff244f5035c59"}, {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"},
{file = "ruff-0.2.0-py3-none-win_arm64.whl", hash = "sha256:3fbaff1ba9564a2c5943f8f38bc221f04bac687cc7485e45237579fee7ccda79"}, {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"},
{file = "ruff-0.2.0.tar.gz", hash = "sha256:63856b91837606c673537d2889989733d7dffde553828d3b0f0bacfa6def54be"}, {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
] ]
[[package]] [[package]]
@ -7027,13 +7027,13 @@ stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "1.40.0" version = "1.40.5"
description = "Python client for Sentry (https://sentry.io)" description = "Python client for Sentry (https://sentry.io)"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "sentry-sdk-1.40.0.tar.gz", hash = "sha256:34ad8cfc9b877aaa2a8eb86bfe5296a467fffe0619b931a05b181c45f6da59bf"}, {file = "sentry-sdk-1.40.5.tar.gz", hash = "sha256:d2dca2392cc5c9a2cc9bb874dd7978ebb759682fe4fe889ee7e970ee8dd1c61e"},
{file = "sentry_sdk-1.40.0-py2.py3-none-any.whl", hash = "sha256:78575620331186d32f34b7ece6edea97ce751f58df822547d3ab85517881a27a"}, {file = "sentry_sdk-1.40.5-py2.py3-none-any.whl", hash = "sha256:d188b407c9bacbe2a50a824e1f8fb99ee1aeb309133310488c570cb6d7056643"},
] ]
[package.dependencies] [package.dependencies]
@ -7172,72 +7172,72 @@ test = ["pytest"]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "69.0.3" version = "69.1.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, {file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"},
{file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, {file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"},
] ]
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
name = "shapely" name = "shapely"
version = "2.0.2" version = "2.0.3"
description = "Manipulation and analysis of geometric objects" description = "Manipulation and analysis of geometric objects"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "shapely-2.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6ca8cffbe84ddde8f52b297b53f8e0687bd31141abb2c373fd8a9f032df415d6"}, {file = "shapely-2.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:af7e9abe180b189431b0f490638281b43b84a33a960620e6b2e8d3e3458b61a1"},
{file = "shapely-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:baa14fc27771e180c06b499a0a7ba697c7988c7b2b6cba9a929a19a4d2762de3"}, {file = "shapely-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98040462b36ced9671e266b95c326b97f41290d9d17504a1ee4dc313a7667b9c"},
{file = "shapely-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36480e32c434d168cdf2f5e9862c84aaf4d714a43a8465ae3ce8ff327f0affb7"}, {file = "shapely-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71eb736ef2843f23473c6e37f6180f90f0a35d740ab284321548edf4e55d9a52"},
{file = "shapely-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef753200cbffd4f652efb2c528c5474e5a14341a473994d90ad0606522a46a2"}, {file = "shapely-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:881eb9dbbb4a6419667e91fcb20313bfc1e67f53dbb392c6840ff04793571ed1"},
{file = "shapely-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9a41ff4323fc9d6257759c26eb1cf3a61ebc7e611e024e6091f42977303fd3a"}, {file = "shapely-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10d2ccf0554fc0e39fad5886c839e47e207f99fdf09547bc687a2330efda35b"},
{file = "shapely-2.0.2-cp310-cp310-win32.whl", hash = "sha256:72b5997272ae8c25f0fd5b3b967b3237e87fab7978b8d6cd5fa748770f0c5d68"}, {file = "shapely-2.0.3-cp310-cp310-win32.whl", hash = "sha256:6dfdc077a6fcaf74d3eab23a1ace5abc50c8bce56ac7747d25eab582c5a2990e"},
{file = "shapely-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:34eac2337cbd67650248761b140d2535855d21b969d76d76123317882d3a0c1a"}, {file = "shapely-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:64c5013dacd2d81b3bb12672098a0b2795c1bf8190cfc2980e380f5ef9d9e4d9"},
{file = "shapely-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b0c052709c8a257c93b0d4943b0b7a3035f87e2d6a8ac9407b6a992d206422f"}, {file = "shapely-2.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:56cee3e4e8159d6f2ce32e421445b8e23154fd02a0ac271d6a6c0b266a8e3cce"},
{file = "shapely-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d217e56ae067e87b4e1731d0dc62eebe887ced729ba5c2d4590e9e3e9fdbd88"}, {file = "shapely-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:619232c8276fded09527d2a9fd91a7885ff95c0ff9ecd5e3cb1e34fbb676e2ae"},
{file = "shapely-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94ac128ae2ab4edd0bffcd4e566411ea7bdc738aeaf92c32a8a836abad725f9f"}, {file = "shapely-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2a7d256db6f5b4b407dc0c98dd1b2fcf1c9c5814af9416e5498d0a2e4307a4b"},
{file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3ee28f5e63a130ec5af4dc3c4cb9c21c5788bb13c15e89190d163b14f9fb89"}, {file = "shapely-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45f0c8cd4583647db3216d965d49363e6548c300c23fd7e57ce17a03f824034"},
{file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:737dba15011e5a9b54a8302f1748b62daa207c9bc06f820cd0ad32a041f1c6f2"}, {file = "shapely-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13cb37d3826972a82748a450328fe02a931dcaed10e69a4d83cc20ba021bc85f"},
{file = "shapely-2.0.2-cp311-cp311-win32.whl", hash = "sha256:45ac6906cff0765455a7b49c1670af6e230c419507c13e2f75db638c8fc6f3bd"}, {file = "shapely-2.0.3-cp311-cp311-win32.whl", hash = "sha256:9302d7011e3e376d25acd30d2d9e70d315d93f03cc748784af19b00988fc30b1"},
{file = "shapely-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:dc9342fc82e374130db86a955c3c4525bfbf315a248af8277a913f30911bed9e"}, {file = "shapely-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6b464f2666b13902835f201f50e835f2f153f37741db88f68c7f3b932d3505fa"},
{file = "shapely-2.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:06f193091a7c6112fc08dfd195a1e3846a64306f890b151fa8c63b3e3624202c"}, {file = "shapely-2.0.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e86e7cb8e331a4850e0c2a8b2d66dc08d7a7b301b8d1d34a13060e3a5b4b3b55"},
{file = "shapely-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eebe544df5c018134f3c23b6515877f7e4cd72851f88a8d0c18464f414d141a2"}, {file = "shapely-2.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c91981c99ade980fc49e41a544629751a0ccd769f39794ae913e53b07b2f78b9"},
{file = "shapely-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e92e7c255f89f5cdf777690313311f422aa8ada9a3205b187113274e0135cd8"}, {file = "shapely-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd45d456983dc60a42c4db437496d3f08a4201fbf662b69779f535eb969660af"},
{file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be46d5509b9251dd9087768eaf35a71360de6afac82ce87c636990a0871aa18b"}, {file = "shapely-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:882fb1ffc7577e88c1194f4f1757e277dc484ba096a3b94844319873d14b0f2d"},
{file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5533a925d8e211d07636ffc2fdd9a7f9f13d54686d00577eeb11d16f00be9c4"}, {file = "shapely-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9f2d93bff2ea52fa93245798cddb479766a18510ea9b93a4fb9755c79474889"},
{file = "shapely-2.0.2-cp312-cp312-win32.whl", hash = "sha256:084b023dae8ad3d5b98acee9d3bf098fdf688eb0bb9b1401e8b075f6a627b611"}, {file = "shapely-2.0.3-cp312-cp312-win32.whl", hash = "sha256:99abad1fd1303b35d991703432c9481e3242b7b3a393c186cfb02373bf604004"},
{file = "shapely-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ea84d1cdbcf31e619d672b53c4532f06253894185ee7acb8ceb78f5f33cbe033"}, {file = "shapely-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:6f555fe3304a1f40398977789bc4fe3c28a11173196df9ece1e15c5bc75a48db"},
{file = "shapely-2.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed1e99702125e7baccf401830a3b94d810d5c70b329b765fe93451fe14cf565b"}, {file = "shapely-2.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983cc418c1fa160b7d797cfef0e0c9f8c6d5871e83eae2c5793fce6a837fad9"},
{file = "shapely-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7d897e6bdc6bc64f7f65155dbbb30e49acaabbd0d9266b9b4041f87d6e52b3a"}, {file = "shapely-2.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18bddb8c327f392189a8d5d6b9a858945722d0bb95ccbd6a077b8e8fc4c7890d"},
{file = "shapely-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0521d76d1e8af01e712db71da9096b484f081e539d4f4a8c97342e7971d5e1b4"}, {file = "shapely-2.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442f4dcf1eb58c5a4e3428d88e988ae153f97ab69a9f24e07bf4af8038536325"},
{file = "shapely-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:5324be299d4c533ecfcfd43424dfd12f9428fd6f12cda38a4316da001d6ef0ea"}, {file = "shapely-2.0.3-cp37-cp37m-win32.whl", hash = "sha256:31a40b6e3ab00a4fd3a1d44efb2482278642572b8e0451abdc8e0634b787173e"},
{file = "shapely-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:78128357a0cee573257a0c2c388d4b7bf13cb7dbe5b3fe5d26d45ebbe2a39e25"}, {file = "shapely-2.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:59b16976c2473fec85ce65cc9239bef97d4205ab3acead4e6cdcc72aee535679"},
{file = "shapely-2.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87dc2be34ac3a3a4a319b963c507ac06682978a5e6c93d71917618b14f13066e"}, {file = "shapely-2.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:705efbce1950a31a55b1daa9c6ae1c34f1296de71ca8427974ec2f27d57554e3"},
{file = "shapely-2.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:42997ac806e4583dad51c80a32d38570fd9a3d4778f5e2c98f9090aa7db0fe91"}, {file = "shapely-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:601c5c0058a6192df704cb889439f64994708563f57f99574798721e9777a44b"},
{file = "shapely-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ccfd5fa10a37e67dbafc601c1ddbcbbfef70d34c3f6b0efc866ddbdb55893a6c"}, {file = "shapely-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f24ecbb90a45c962b3b60d8d9a387272ed50dc010bfe605f1d16dfc94772d8a1"},
{file = "shapely-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7c95d3379ae3abb74058938a9fcbc478c6b2e28d20dace38f8b5c587dde90aa"}, {file = "shapely-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c2a2989222c6062f7a0656e16276c01bb308bc7e5d999e54bf4e294ce62e76"},
{file = "shapely-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a21353d28209fb0d8cc083e08ca53c52666e0d8a1f9bbe23b6063967d89ed24"}, {file = "shapely-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42bceb9bceb3710a774ce04908fda0f28b291323da2688f928b3f213373b5aee"},
{file = "shapely-2.0.2-cp38-cp38-win32.whl", hash = "sha256:03e63a99dfe6bd3beb8d5f41ec2086585bb969991d603f9aeac335ad396a06d4"}, {file = "shapely-2.0.3-cp38-cp38-win32.whl", hash = "sha256:54d925c9a311e4d109ec25f6a54a8bd92cc03481a34ae1a6a92c1fe6729b7e01"},
{file = "shapely-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:c6fd29fbd9cd76350bd5cc14c49de394a31770aed02d74203e23b928f3d2f1aa"}, {file = "shapely-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:300d203b480a4589adefff4c4af0b13919cd6d760ba3cbb1e56275210f96f654"},
{file = "shapely-2.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f217d28ecb48e593beae20a0082a95bd9898d82d14b8fcb497edf6bff9a44d7"}, {file = "shapely-2.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:083d026e97b6c1f4a9bd2a9171c7692461092ed5375218170d91705550eecfd5"},
{file = "shapely-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:394e5085b49334fd5b94fa89c086edfb39c3ecab7f669e8b2a4298b9d523b3a5"}, {file = "shapely-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:27b6e1910094d93e9627f2664121e0e35613262fc037051680a08270f6058daf"},
{file = "shapely-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd3ad17b64466a033848c26cb5b509625c87d07dcf39a1541461cacdb8f7e91c"}, {file = "shapely-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:71b2de56a9e8c0e5920ae5ddb23b923490557ac50cb0b7fa752761bf4851acde"},
{file = "shapely-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d41a116fcad58048d7143ddb01285e1a8780df6dc1f56c3b1e1b7f12ed296651"}, {file = "shapely-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d279e56bbb68d218d63f3efc80c819cedcceef0e64efbf058a1df89dc57201b"},
{file = "shapely-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dea9a0651333cf96ef5bb2035044e3ad6a54f87d90e50fe4c2636debf1b77abc"}, {file = "shapely-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88566d01a30f0453f7d038db46bc83ce125e38e47c5f6bfd4c9c287010e9bf74"},
{file = "shapely-2.0.2-cp39-cp39-win32.whl", hash = "sha256:b8eb0a92f7b8c74f9d8fdd1b40d395113f59bd8132ca1348ebcc1f5aece94b96"}, {file = "shapely-2.0.3-cp39-cp39-win32.whl", hash = "sha256:58afbba12c42c6ed44c4270bc0e22f3dadff5656d711b0ad335c315e02d04707"},
{file = "shapely-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:794affd80ca0f2c536fc948a3afa90bd8fb61ebe37fe873483ae818e7f21def4"}, {file = "shapely-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:5026b30433a70911979d390009261b8c4021ff87c7c3cbd825e62bb2ffa181bc"},
{file = "shapely-2.0.2.tar.gz", hash = "sha256:1713cc04c171baffc5b259ba8531c58acc2a301707b7f021d88a15ed090649e7"}, {file = "shapely-2.0.3.tar.gz", hash = "sha256:4d65d0aa7910af71efa72fd6447e02a8e5dd44da81a983de9d736d6e6ccbe674"},
] ]
[package.dependencies] [package.dependencies]
numpy = ">=1.14" numpy = ">=1.14,<2"
[package.extras] [package.extras]
docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"]
@ -7574,13 +7574,13 @@ files = [
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.66.1" version = "4.66.2"
description = "Fast, Extensible Progress Meter" description = "Fast, Extensible Progress Meter"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
{file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
] ]
[package.dependencies] [package.dependencies]
@ -7594,13 +7594,13 @@ telegram = ["requests"]
[[package]] [[package]]
name = "types-requests" name = "types-requests"
version = "2.31.0.20240125" version = "2.31.0.20240218"
description = "Typing stubs for requests" description = "Typing stubs for requests"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"}, {file = "types-requests-2.31.0.20240218.tar.gz", hash = "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"},
{file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"}, {file = "types_requests-2.31.0.20240218-py3-none-any.whl", hash = "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b"},
] ]
[package.dependencies] [package.dependencies]
@ -7630,24 +7630,24 @@ files = [
[[package]] [[package]]
name = "tzdata" name = "tzdata"
version = "2023.4" version = "2024.1"
description = "Provider of IANA time zone data" description = "Provider of IANA time zone data"
optional = false optional = false
python-versions = ">=2" python-versions = ">=2"
files = [ files = [
{file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
{file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
] ]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.2.0" version = "2.2.1"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
{file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
] ]
[package.extras] [package.extras]
@ -7829,4 +7829,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "~3.11" python-versions = "~3.11"
content-hash = "8af315a175ab43dbc5a777f68fca6d76af1443b5b574ab8570ef5dad59f288fc" content-hash = "cb63112bfe7ee2b3fc194a422479f788c9a8027d445ea51a31f4ee20299beb53"

@ -126,7 +126,7 @@ inputs = "*"
Jinja2 = "*" Jinja2 = "*"
lru-dict = "*" lru-dict = "*"
matplotlib = "*" matplotlib = "*"
metadrive-simulator = { version = "0.4.2.2", markers = "platform_machine != 'aarch64'" } # no linux/aarch64 wheels for certain dependencies metadrive-simulator = { version = "0.4.2.3", markers = "platform_machine != 'aarch64'" } # no linux/aarch64 wheels for certain dependencies
mpld3 = "*" mpld3 = "*"
mypy = "*" mypy = "*"
myst-parser = "*" myst-parser = "*"
@ -167,8 +167,8 @@ build-backend = "poetry.core.masonry.api"
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml # https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
[tool.ruff] [tool.ruff]
select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF008", "RUF100", "A", "B", "TID251"] lint.select = ["E", "F", "W", "PIE", "C4", "ISC", "RUF008", "RUF100", "A", "B", "TID251"]
ignore = ["E741", "E402", "C408", "ISC003", "B027", "B024"] lint.ignore = ["E741", "E402", "C408", "ISC003", "B027", "B024"]
line-length = 160 line-length = 160
target-version="py311" target-version="py311"
exclude = [ exclude = [
@ -180,8 +180,8 @@ exclude = [
"teleoprtc_repo", "teleoprtc_repo",
"third_party", "third_party",
] ]
flake8-implicit-str-concat.allow-multiline=false lint.flake8-implicit-str-concat.allow-multiline=false
[tool.ruff.flake8-tidy-imports.banned-api] [tool.ruff.lint.flake8-tidy-imports.banned-api]
"selfdrive".msg = "Use openpilot.selfdrive" "selfdrive".msg = "Use openpilot.selfdrive"
"common".msg = "Use openpilot.common" "common".msg = "Use openpilot.common"
"system".msg = "Use openpilot.system" "system".msg = "Use openpilot.system"

@ -19,7 +19,8 @@ from dataclasses import asdict, dataclass, replace
from datetime import datetime from datetime import datetime
from functools import partial from functools import partial
from queue import Queue from queue import Queue
from typing import Callable, Dict, List, Optional, Set, Union, cast from typing import cast
from collections.abc import Callable
import requests import requests
from jsonrpc import JSONRPCResponseManager, dispatcher from jsonrpc import JSONRPCResponseManager, dispatcher
@ -55,17 +56,17 @@ WS_FRAME_SIZE = 4096
NetworkType = log.DeviceState.NetworkType NetworkType = log.DeviceState.NetworkType
UploadFileDict = Dict[str, Union[str, int, float, bool]] UploadFileDict = dict[str, str | int | float | bool]
UploadItemDict = Dict[str, Union[str, bool, int, float, Dict[str, str]]] UploadItemDict = dict[str, str | bool | int | float | dict[str, str]]
UploadFilesToUrlResponse = Dict[str, Union[int, List[UploadItemDict], List[str]]] UploadFilesToUrlResponse = dict[str, int | list[UploadItemDict] | list[str]]
@dataclass @dataclass
class UploadFile: class UploadFile:
fn: str fn: str
url: str url: str
headers: Dict[str, str] headers: dict[str, str]
allow_cellular: bool allow_cellular: bool
@classmethod @classmethod
@ -77,9 +78,9 @@ class UploadFile:
class UploadItem: class UploadItem:
path: str path: str
url: str url: str
headers: Dict[str, str] headers: dict[str, str]
created_at: int created_at: int
id: Optional[str] id: str | None
retry_count: int = 0 retry_count: int = 0
current: bool = False current: bool = False
progress: float = 0 progress: float = 0
@ -97,9 +98,9 @@ send_queue: Queue[str] = queue.Queue()
upload_queue: Queue[UploadItem] = queue.Queue() upload_queue: Queue[UploadItem] = queue.Queue()
low_priority_send_queue: Queue[str] = queue.Queue() low_priority_send_queue: Queue[str] = queue.Queue()
log_recv_queue: Queue[str] = queue.Queue() log_recv_queue: Queue[str] = queue.Queue()
cancelled_uploads: Set[str] = set() cancelled_uploads: set[str] = set()
cur_upload_items: Dict[int, Optional[UploadItem]] = {} cur_upload_items: dict[int, UploadItem | None] = {}
def strip_bz2_extension(fn: str) -> str: def strip_bz2_extension(fn: str) -> str:
@ -127,14 +128,14 @@ class UploadQueueCache:
@staticmethod @staticmethod
def cache(upload_queue: Queue[UploadItem]) -> None: def cache(upload_queue: Queue[UploadItem]) -> None:
try: try:
queue: List[Optional[UploadItem]] = list(upload_queue.queue) queue: list[UploadItem | None] = list(upload_queue.queue)
items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)] items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)]
Params().put("AthenadUploadQueue", json.dumps(items)) Params().put("AthenadUploadQueue", json.dumps(items))
except Exception: except Exception:
cloudlog.exception("athena.UploadQueueCache.cache.exception") cloudlog.exception("athena.UploadQueueCache.cache.exception")
def handle_long_poll(ws: WebSocket, exit_event: Optional[threading.Event]) -> None: def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
end_event = threading.Event() end_event = threading.Event()
threads = [ threads = [
@ -206,13 +207,17 @@ def retry_upload(tid: int, end_event: threading.Event, increase_count: bool = Tr
break break
def cb(sm, item, tid, sz: int, cur: int) -> None: def cb(sm, item, tid, end_event: threading.Event, sz: int, cur: int) -> None:
# Abort transfer if connection changed to metered after starting upload # Abort transfer if connection changed to metered after starting upload
# or if athenad is shutting down to re-connect the websocket
sm.update(0) sm.update(0)
metered = sm['deviceState'].networkMetered metered = sm['deviceState'].networkMetered
if metered and (not item.allow_cellular): if metered and (not item.allow_cellular):
raise AbortTransferException raise AbortTransferException
if end_event.is_set():
raise AbortTransferException
cur_upload_items[tid] = replace(item, progress=cur / sz if sz else 1) cur_upload_items[tid] = replace(item, progress=cur / sz if sz else 1)
@ -252,7 +257,7 @@ def upload_handler(end_event: threading.Event) -> None:
sz = -1 sz = -1
cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type, metered=metered, retry_count=item.retry_count) cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type, metered=metered, retry_count=item.retry_count)
response = _do_upload(item, partial(cb, sm, item, tid)) response = _do_upload(item, partial(cb, sm, item, tid, end_event))
if response.status_code not in (200, 201, 401, 403, 412): if response.status_code not in (200, 201, 401, 403, 412):
cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type, metered=metered) cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type, metered=metered)
@ -274,7 +279,7 @@ def upload_handler(end_event: threading.Event) -> None:
cloudlog.exception("athena.upload_handler.exception") cloudlog.exception("athena.upload_handler.exception")
def _do_upload(upload_item: UploadItem, callback: Optional[Callable] = None) -> requests.Response: def _do_upload(upload_item: UploadItem, callback: Callable | None = None) -> requests.Response:
path = upload_item.path path = upload_item.path
compress = False compress = False
@ -313,7 +318,7 @@ def getMessage(service: str, timeout: int = 1000) -> dict:
@dispatcher.add_method @dispatcher.add_method
def getVersion() -> Dict[str, str]: def getVersion() -> dict[str, str]:
return { return {
"version": get_version(), "version": get_version(),
"remote": get_normalized_origin(), "remote": get_normalized_origin(),
@ -323,7 +328,7 @@ def getVersion() -> Dict[str, str]:
@dispatcher.add_method @dispatcher.add_method
def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: Optional[str] = None, place_details: Optional[str] = None) -> Dict[str, int]: def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str | None = None, place_details: str | None = None) -> dict[str, int]:
destination = { destination = {
"latitude": latitude, "latitude": latitude,
"longitude": longitude, "longitude": longitude,
@ -335,7 +340,7 @@ def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: Optiona
return {"success": 1} return {"success": 1}
def scan_dir(path: str, prefix: str) -> List[str]: def scan_dir(path: str, prefix: str) -> list[str]:
files = [] files = []
# only walk directories that match the prefix # only walk directories that match the prefix
# (glob and friends traverse entire dir tree) # (glob and friends traverse entire dir tree)
@ -355,12 +360,12 @@ def scan_dir(path: str, prefix: str) -> List[str]:
return files return files
@dispatcher.add_method @dispatcher.add_method
def listDataDirectory(prefix='') -> List[str]: def listDataDirectory(prefix='') -> list[str]:
return scan_dir(Paths.log_root(), prefix) return scan_dir(Paths.log_root(), prefix)
@dispatcher.add_method @dispatcher.add_method
def uploadFileToUrl(fn: str, url: str, headers: Dict[str, str]) -> UploadFilesToUrlResponse: def uploadFileToUrl(fn: str, url: str, headers: dict[str, str]) -> UploadFilesToUrlResponse:
# this is because mypy doesn't understand that the decorator doesn't change the return type # this is because mypy doesn't understand that the decorator doesn't change the return type
response: UploadFilesToUrlResponse = uploadFilesToUrls([{ response: UploadFilesToUrlResponse = uploadFilesToUrls([{
"fn": fn, "fn": fn,
@ -371,11 +376,11 @@ def uploadFileToUrl(fn: str, url: str, headers: Dict[str, str]) -> UploadFilesTo
@dispatcher.add_method @dispatcher.add_method
def uploadFilesToUrls(files_data: List[UploadFileDict]) -> UploadFilesToUrlResponse: def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlResponse:
files = map(UploadFile.from_dict, files_data) files = map(UploadFile.from_dict, files_data)
items: List[UploadItemDict] = [] items: list[UploadItemDict] = []
failed: List[str] = [] failed: list[str] = []
for file in files: for file in files:
if len(file.fn) == 0 or file.fn[0] == '/' or '..' in file.fn or len(file.url) == 0: if len(file.fn) == 0 or file.fn[0] == '/' or '..' in file.fn or len(file.url) == 0:
failed.append(file.fn) failed.append(file.fn)
@ -414,13 +419,13 @@ def uploadFilesToUrls(files_data: List[UploadFileDict]) -> UploadFilesToUrlRespo
@dispatcher.add_method @dispatcher.add_method
def listUploadQueue() -> List[UploadItemDict]: def listUploadQueue() -> list[UploadItemDict]:
items = list(upload_queue.queue) + list(cur_upload_items.values()) items = list(upload_queue.queue) + list(cur_upload_items.values())
return [asdict(i) for i in items if (i is not None) and (i.id not in cancelled_uploads)] return [asdict(i) for i in items if (i is not None) and (i.id not in cancelled_uploads)]
@dispatcher.add_method @dispatcher.add_method
def cancelUpload(upload_id: Union[str, List[str]]) -> Dict[str, Union[int, str]]: def cancelUpload(upload_id: str | list[str]) -> dict[str, int | str]:
if not isinstance(upload_id, list): if not isinstance(upload_id, list):
upload_id = [upload_id] upload_id = [upload_id]
@ -433,7 +438,7 @@ def cancelUpload(upload_id: Union[str, List[str]]) -> Dict[str, Union[int, str]]
return {"success": 1} return {"success": 1}
@dispatcher.add_method @dispatcher.add_method
def setRouteViewed(route: str) -> Dict[str, Union[int, str]]: def setRouteViewed(route: str) -> dict[str, int | str]:
# maintain a list of the last 10 routes viewed in connect # maintain a list of the last 10 routes viewed in connect
params = Params() params = Params()
@ -448,7 +453,7 @@ def setRouteViewed(route: str) -> Dict[str, Union[int, str]]:
return {"success": 1} return {"success": 1}
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> Dict[str, int]: def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
try: try:
if local_port not in LOCAL_PORT_WHITELIST: if local_port not in LOCAL_PORT_WHITELIST:
raise Exception("Requested local port not whitelisted") raise Exception("Requested local port not whitelisted")
@ -482,7 +487,7 @@ def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local
@dispatcher.add_method @dispatcher.add_method
def getPublicKey() -> Optional[str]: def getPublicKey() -> str | None:
if not os.path.isfile(Paths.persist_root() + '/comma/id_rsa.pub'): if not os.path.isfile(Paths.persist_root() + '/comma/id_rsa.pub'):
return None return None
@ -522,7 +527,7 @@ def getNetworks():
@dispatcher.add_method @dispatcher.add_method
def takeSnapshot() -> Optional[Union[str, Dict[str, str]]]: def takeSnapshot() -> str | dict[str, str] | None:
from openpilot.system.camerad.snapshot.snapshot import jpeg_write, snapshot from openpilot.system.camerad.snapshot.snapshot import jpeg_write, snapshot
ret = snapshot() ret = snapshot()
if ret is not None: if ret is not None:
@ -539,7 +544,7 @@ def takeSnapshot() -> Optional[Union[str, Dict[str, str]]]:
raise Exception("not available while camerad is started") raise Exception("not available while camerad is started")
def get_logs_to_send_sorted() -> List[str]: def get_logs_to_send_sorted() -> list[str]:
# TODO: scan once then use inotify to detect file creation/deletion # TODO: scan once then use inotify to detect file creation/deletion
curr_time = int(time.time()) curr_time = int(time.time())
logs = [] logs = []
@ -746,6 +751,9 @@ def ws_manage(ws: WebSocket, end_event: threading.Event) -> None:
onroad_prev = onroad onroad_prev = onroad
if sock is not None: if sock is not None:
# While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s
# offroad, we can expect to time out in 30 + (10 * 3) = 60s
# FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10)
@ -759,7 +767,7 @@ def backoff(retries: int) -> int:
return random.randrange(0, min(128, int(2 ** retries))) return random.randrange(0, min(128, int(2 ** retries)))
def main(exit_event: Optional[threading.Event] = None): def main(exit_event: threading.Event | None = None):
try: try:
set_core_affinity([0, 1, 2, 3]) set_core_affinity([0, 1, 2, 3])
except Exception: except Exception:

@ -3,7 +3,6 @@ import time
import json import json
import jwt import jwt
from pathlib import Path from pathlib import Path
from typing import Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from openpilot.common.api import api_get from openpilot.common.api import api_get
@ -23,12 +22,12 @@ def is_registered_device() -> bool:
return dongle not in (None, UNREGISTERED_DONGLE_ID) return dongle not in (None, UNREGISTERED_DONGLE_ID)
def register(show_spinner=False) -> Optional[str]: def register(show_spinner=False) -> str | None:
params = Params() params = Params()
IMEI = params.get("IMEI", encoding='utf8') IMEI = params.get("IMEI", encoding='utf8')
HardwareSerial = params.get("HardwareSerial", encoding='utf8') HardwareSerial = params.get("HardwareSerial", encoding='utf8')
dongle_id: Optional[str] = params.get("DongleId", encoding='utf8') dongle_id: str | None = params.get("DongleId", encoding='utf8')
needs_registration = None in (IMEI, HardwareSerial, dongle_id) needs_registration = None in (IMEI, HardwareSerial, dongle_id)
pubkey = Path(Paths.persist_root()+"/comma/id_rsa.pub") pubkey = Path(Paths.persist_root()+"/comma/id_rsa.pub")
@ -48,8 +47,8 @@ def register(show_spinner=False) -> Optional[str]:
# Block until we get the imei # Block until we get the imei
serial = HARDWARE.get_serial() serial = HARDWARE.get_serial()
start_time = time.monotonic() start_time = time.monotonic()
imei1: Optional[str] = None imei1: str | None = None
imei2: Optional[str] = None imei2: str | None = None
while imei1 is None and imei2 is None: while imei1 is None and imei2 is None:
try: try:
imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1)

@ -12,7 +12,6 @@ import unittest
from dataclasses import asdict, replace from dataclasses import asdict, replace
from datetime import datetime, timedelta from datetime import datetime, timedelta
from parameterized import parameterized from parameterized import parameterized
from typing import Optional
from unittest import mock from unittest import mock
from websocket import ABNF from websocket import ABNF
@ -97,7 +96,7 @@ class TestAthenadMethods(unittest.TestCase):
break break
@staticmethod @staticmethod
def _create_file(file: str, parent: Optional[str] = None, data: bytes = b'') -> str: def _create_file(file: str, parent: str | None = None, data: bytes = b'') -> str:
fn = os.path.join(Paths.log_root() if parent is None else parent, file) fn = os.path.join(Paths.log_root() if parent is None else parent, file)
os.makedirs(os.path.dirname(fn), exist_ok=True) os.makedirs(os.path.dirname(fn), exist_ok=True)
with open(fn, 'wb') as f: with open(fn, 'wb') as f:
@ -233,6 +232,7 @@ class TestAthenadMethods(unittest.TestCase):
time.sleep(0.1) time.sleep(0.1)
# TODO: verify that upload actually succeeded # TODO: verify that upload actually succeeded
# TODO: also check that end_event and metered network raises AbortTransferException
self.assertEqual(athenad.upload_queue.qsize(), 0) self.assertEqual(athenad.upload_queue.qsize(), 0)
@parameterized.expand([(500, True), (412, False)]) @parameterized.expand([(500, True), (412, False)])

@ -3,7 +3,7 @@ import subprocess
import threading import threading
import time import time
import unittest import unittest
from typing import cast, Optional from typing import cast
from unittest import mock from unittest import mock
from openpilot.common.params import Params from openpilot.common.params import Params
@ -12,6 +12,8 @@ from openpilot.selfdrive.athena import athenad
from openpilot.selfdrive.manager.helpers import write_onroad_params from openpilot.selfdrive.manager.helpers import write_onroad_params
from openpilot.system.hardware import TICI from openpilot.system.hardware import TICI
TIMEOUT_TOLERANCE = 20 # seconds
def wifi_radio(on: bool) -> None: def wifi_radio(on: bool) -> None:
if not TICI: if not TICI:
@ -27,8 +29,8 @@ class TestAthenadPing(unittest.TestCase):
athenad: threading.Thread athenad: threading.Thread
exit_event: threading.Event exit_event: threading.Event
def _get_ping_time(self) -> Optional[str]: def _get_ping_time(self) -> str | None:
return cast(Optional[str], self.params.get("LastAthenaPingTime", encoding="utf-8")) return cast(str | None, self.params.get("LastAthenaPingTime", encoding="utf-8"))
def _clear_ping_time(self) -> None: def _clear_ping_time(self) -> None:
self.params.remove("LastAthenaPingTime") self.params.remove("LastAthenaPingTime")
@ -55,7 +57,7 @@ class TestAthenadPing(unittest.TestCase):
self.exit_event.set() self.exit_event.set()
self.athenad.join() self.athenad.join()
@mock.patch('openpilot.selfdrive.athena.athenad.create_connection', autospec=True) @mock.patch('openpilot.selfdrive.athena.athenad.create_connection', new_callable=lambda: mock.MagicMock(wraps=athenad.create_connection))
def assertTimeout(self, reconnect_time: float, mock_create_connection: mock.MagicMock) -> None: def assertTimeout(self, reconnect_time: float, mock_create_connection: mock.MagicMock) -> None:
self.athenad.start() self.athenad.start()
@ -63,7 +65,7 @@ class TestAthenadPing(unittest.TestCase):
mock_create_connection.assert_called_once() mock_create_connection.assert_called_once()
mock_create_connection.reset_mock() mock_create_connection.reset_mock()
# check normal behaviour # check normal behaviour, server pings on connection
with self.subTest("Wi-Fi: receives ping"), Timeout(70, "no ping received"): with self.subTest("Wi-Fi: receives ping"), Timeout(70, "no ping received"):
while not self._received_ping(): while not self._received_ping():
time.sleep(0.1) time.sleep(0.1)
@ -92,12 +94,12 @@ class TestAthenadPing(unittest.TestCase):
@unittest.skipIf(not TICI, "only run on desk") @unittest.skipIf(not TICI, "only run on desk")
def test_offroad(self) -> None: def test_offroad(self) -> None:
write_onroad_params(False, self.params) write_onroad_params(False, self.params)
self.assertTimeout(100) # expect approx 90s self.assertTimeout(60 + TIMEOUT_TOLERANCE) # based using TCP keepalive settings
@unittest.skipIf(not TICI, "only run on desk") @unittest.skipIf(not TICI, "only run on desk")
def test_onroad(self) -> None: def test_onroad(self) -> None:
write_onroad_params(True, self.params) write_onroad_params(True, self.params)
self.assertTimeout(30) # expect 20-30s self.assertTimeout(21 + TIMEOUT_TOLERANCE)
if __name__ == "__main__": if __name__ == "__main__":

@ -362,11 +362,11 @@ std::optional<bool> send_panda_states(PubMaster *pm, const std::vector<Panda *>
ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt)); ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt));
ps.setAlternativeExperience(health.alternative_experience_pkt); ps.setAlternativeExperience(health.alternative_experience_pkt);
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt)); ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
ps.setInterruptLoad(health.interrupt_load); ps.setInterruptLoad(health.interrupt_load_pkt);
ps.setFanPower(health.fan_power); ps.setFanPower(health.fan_power);
ps.setFanStallCount(health.fan_stall_count); ps.setFanStallCount(health.fan_stall_count);
ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid)); ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt));
ps.setSpiChecksumErrorCount(health.spi_checksum_error_count); ps.setSpiChecksumErrorCount(health.spi_checksum_error_count_pkt);
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);

@ -4,7 +4,7 @@ import os
import usb1 import usb1
import time import time
import subprocess import subprocess
from typing import List, NoReturn from typing import NoReturn
from functools import cmp_to_key from functools import cmp_to_key
from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH from panda import Panda, PandaDFU, PandaProtocolMismatch, FW_PATH
@ -93,6 +93,11 @@ def main() -> NoReturn:
cloudlog.event("pandad.flash_and_connect", count=count) cloudlog.event("pandad.flash_and_connect", count=count)
params.remove("PandaSignatures") params.remove("PandaSignatures")
# TODO: remove this in the next AGNOS
# wait until USB is up before counting
if time.monotonic() < 25.:
no_internal_panda_count = 0
# Handle missing internal panda # Handle missing internal panda
if no_internal_panda_count > 0: if no_internal_panda_count > 0:
if no_internal_panda_count == 3: if no_internal_panda_count == 3:
@ -119,7 +124,7 @@ def main() -> NoReturn:
cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}") cloudlog.info(f"{len(panda_serials)} panda(s) found, connecting - {panda_serials}")
# Flash pandas # Flash pandas
pandas: List[Panda] = [] pandas: list[Panda] = []
for serial in panda_serials: for serial in panda_serials:
pandas.append(flash_panda(serial)) pandas.append(flash_panda(serial))

@ -32,7 +32,7 @@ class TestBoardd(unittest.TestCase):
with Timeout(90, "boardd didn't start"): with Timeout(90, "boardd didn't start"):
sm = messaging.SubMaster(['pandaStates']) sm = messaging.SubMaster(['pandaStates'])
while sm.rcv_frame['pandaStates'] < 1 or len(sm['pandaStates']) == 0 or \ while sm.recv_frame['pandaStates'] < 1 or len(sm['pandaStates']) == 0 or \
any(ps.pandaType == log.PandaState.PandaType.unknown for ps in sm['pandaStates']): any(ps.pandaType == log.PandaState.PandaType.unknown for ps in sm['pandaStates']):
sm.update(1000) sm.update(1000)

@ -1,11 +1,13 @@
# functions common among cars # functions common among cars
from collections import namedtuple from collections import namedtuple
from typing import Dict, List, Optional from dataclasses import dataclass
from enum import ReprEnum
import capnp import capnp
from cereal import car from cereal import car
from openpilot.common.numpy_fast import clip, interp from openpilot.common.numpy_fast import clip, interp
from openpilot.selfdrive.car.docs_definitions import CarInfo
# kg of standard extra cargo to count for drive, gas, etc... # kg of standard extra cargo to count for drive, gas, etc...
@ -24,9 +26,9 @@ def apply_hysteresis(val: float, val_steady: float, hyst_gap: float) -> float:
return val_steady return val_steady
def create_button_events(cur_btn: int, prev_btn: int, buttons_dict: Dict[int, capnp.lib.capnp._EnumModule], def create_button_events(cur_btn: int, prev_btn: int, buttons_dict: dict[int, capnp.lib.capnp._EnumModule],
unpressed_btn: int = 0) -> List[capnp.lib.capnp._DynamicStructBuilder]: unpressed_btn: int = 0) -> list[capnp.lib.capnp._DynamicStructBuilder]:
events: List[capnp.lib.capnp._DynamicStructBuilder] = [] events: list[capnp.lib.capnp._DynamicStructBuilder] = []
if cur_btn == prev_btn: if cur_btn == prev_btn:
return events return events
@ -73,7 +75,10 @@ def scale_tire_stiffness(mass, wheelbase, center_to_front, tire_stiffness_factor
return tire_stiffness_front, tire_stiffness_rear return tire_stiffness_front, tire_stiffness_rear
def dbc_dict(pt_dbc, radar_dbc, chassis_dbc=None, body_dbc=None) -> Dict[str, str]: 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} return {'pt': pt_dbc, 'radar': radar_dbc, 'chassis': chassis_dbc, 'body': body_dbc}
@ -208,7 +213,7 @@ def get_safety_config(safety_model, safety_param = None):
class CanBusBase: class CanBusBase:
offset: int offset: int
def __init__(self, CP, fingerprint: Optional[Dict[int, Dict[int, int]]]) -> None: def __init__(self, CP, fingerprint: dict[int, dict[int, int]] | None) -> None:
if CP is None: if CP is None:
assert fingerprint is not None assert fingerprint is not None
num = max([k for k, v in fingerprint.items() if len(v)], default=0) // 4 + 1 num = max([k for k, v in fingerprint.items() if len(v)], default=0) // 4 + 1
@ -236,3 +241,43 @@ class CanSignalRateCalculator:
self.previous_value = current_value self.previous_value = current_value
return self.rate return self.rate
CarInfos = CarInfo | list[CarInfo]
@dataclass
class CarSpecs:
mass: float
wheelbase: float
steerRatio: float
@dataclass(order=True)
class PlatformConfig:
platform_str: str
car_info: CarInfos
dbc_dict: DbcDict
specs: CarSpecs | None = None
def __hash__(self) -> int:
return hash(self.platform_str)
class Platforms(str, ReprEnum):
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
@classmethod
def create_dbc_map(cls) -> dict[str, DbcDict]:
return {p: p.config.dbc_dict for p in cls}
@classmethod
def create_carinfo_map(cls) -> dict[str, CarInfos]:
return {p: p.config.car_info for p in cls}

@ -1,8 +1,5 @@
from enum import StrEnum
from typing import Dict
from cereal import car from cereal import car
from openpilot.selfdrive.car import dbc_dict from openpilot.selfdrive.car import PlatformConfig, Platforms, dbc_dict
from openpilot.selfdrive.car.docs_definitions import CarInfo from openpilot.selfdrive.car.docs_definitions import CarInfo
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
@ -22,13 +19,12 @@ class CarControllerParams:
pass pass
class CAR(StrEnum): class CAR(Platforms):
BODY = "COMMA BODY" BODY = PlatformConfig(
"COMMA BODY",
CarInfo("comma body", package="All"),
CAR_INFO: Dict[str, CarInfo] = { dbc_dict('comma_body', None),
CAR.BODY: CarInfo("comma body", package="All"), )
}
FW_QUERY_CONFIG = FwQueryConfig( FW_QUERY_CONFIG = FwQueryConfig(
@ -41,7 +37,5 @@ FW_QUERY_CONFIG = FwQueryConfig(
], ],
) )
CAR_INFO = CAR.create_carinfo_map()
DBC = { DBC = CAR.create_dbc_map()
CAR.BODY: dbc_dict('comma_body', None),
}

@ -1,6 +1,6 @@
import os import os
import time import time
from typing import Callable, Dict, List, Optional, Tuple from collections.abc import Callable
from cereal import car from cereal import car
from openpilot.common.params import Params from openpilot.common.params import Params
@ -63,7 +63,7 @@ def load_interfaces(brand_names):
return ret return ret
def _get_interface_names() -> Dict[str, List[str]]: def _get_interface_names() -> dict[str, list[str]]:
# returns a dict of brand name and its respective models # returns a dict of brand name and its respective models
brand_names = {} brand_names = {}
for brand_name, brand_models in get_interface_attr("CAR").items(): for brand_name, brand_models in get_interface_attr("CAR").items():
@ -77,7 +77,7 @@ interface_names = _get_interface_names()
interfaces = load_interfaces(interface_names) interfaces = load_interfaces(interface_names)
def can_fingerprint(next_can: Callable) -> Tuple[Optional[str], Dict[int, dict]]: def can_fingerprint(next_can: Callable) -> tuple[str | None, dict[int, dict]]:
finger = gen_empty_fingerprint() finger = gen_empty_fingerprint()
candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1 candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1
frame = 0 frame = 0
@ -141,9 +141,10 @@ def fingerprint(logcan, sendcan, num_pandas):
cached = True cached = True
else: else:
cloudlog.warning("Getting VIN & FW versions") cloudlog.warning("Getting VIN & FW versions")
# enable OBD multiplexing for Vin query, also allows time for sendcan subscriber to connect # 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(params, True) set_obd_multiplexing(params, True)
# Vin query only reliably works through OBDII # VIN query only reliably works through OBDII
vin_rx_addr, vin_rx_bus, vin = get_vin(logcan, sendcan, (0, 1)) vin_rx_addr, vin_rx_bus, vin = get_vin(logcan, sendcan, (0, 1))
ecu_rx_addrs = get_present_ecus(logcan, sendcan, num_pandas=num_pandas) ecu_rx_addrs = get_present_ecus(logcan, sendcan, num_pandas=num_pandas)
car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, num_pandas=num_pandas) car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, num_pandas=num_pandas)
@ -161,7 +162,7 @@ def fingerprint(logcan, sendcan, num_pandas):
cloudlog.warning("VIN %s", vin) cloudlog.warning("VIN %s", vin)
params.put("CarVin", vin) params.put("CarVin", vin)
# disable OBD multiplexing for potential ECU knockouts # disable OBD multiplexing for CAN fingerprinting and potential ECU knockouts
set_obd_multiplexing(params, False) set_obd_multiplexing(params, False)
params.put_bool("FirmwareQueryDone", True) params.put_bool("FirmwareQueryDone", True)

@ -67,6 +67,7 @@ FW_VERSIONS = {
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
b'68267018AO ', b'68267018AO ',
b'68267020AJ ', b'68267020AJ ',
b'68303534AJ ',
b'68340762AD ', b'68340762AD ',
b'68340764AD ', b'68340764AD ',
b'68352652AE ', b'68352652AE ',
@ -299,6 +300,7 @@ FW_VERSIONS = {
CAR.JEEP_GRAND_CHEROKEE_2019: { CAR.JEEP_GRAND_CHEROKEE_2019: {
(Ecu.combinationMeter, 0x742, None): [ (Ecu.combinationMeter, 0x742, None): [
b'68402703AB', b'68402703AB',
b'68402704AB',
b'68402708AB', b'68402708AB',
b'68402971AD', b'68402971AD',
b'68454144AD', b'68454144AD',
@ -326,6 +328,7 @@ FW_VERSIONS = {
(Ecu.eps, 0x75a, None): [ (Ecu.eps, 0x75a, None): [
b'68417279AA', b'68417279AA',
b'68417280AA', b'68417280AA',
b'68417281AA',
b'68453431AA', b'68453431AA',
b'68453433AA', b'68453433AA',
b'68453435AA', b'68453435AA',
@ -336,6 +339,7 @@ FW_VERSIONS = {
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
b'05035674AB ', b'05035674AB ',
b'68412635AG ', b'68412635AG ',
b'68412660AD ',
b'68422860AB', b'68422860AB',
b'68449435AE ', b'68449435AE ',
b'68496223AA ', b'68496223AA ',
@ -346,6 +350,7 @@ FW_VERSIONS = {
(Ecu.transmission, 0x7e1, None): [ (Ecu.transmission, 0x7e1, None): [
b'05035707AA', b'05035707AA',
b'68419672AC', b'68419672AC',
b'68419678AB',
b'68423905AB', b'68423905AB',
b'68449258AC', b'68449258AC',
b'68495807AA', b'68495807AA',
@ -396,6 +401,7 @@ FW_VERSIONS = {
b'68527383AD', b'68527383AD',
b'68527387AE', b'68527387AE',
b'68527403AC', b'68527403AC',
b'68546047AF',
b'68631938AA', b'68631938AA',
b'68631942AA', b'68631942AA',
], ],
@ -475,6 +481,7 @@ FW_VERSIONS = {
b'05149591AD ', b'05149591AD ',
b'05149591AE ', b'05149591AE ',
b'05149592AE ', b'05149592AE ',
b'05149599AE ',
b'05149600AD ', b'05149600AD ',
b'05149605AE ', b'05149605AE ',
b'05149846AA ', b'05149846AA ',
@ -606,4 +613,34 @@ FW_VERSIONS = {
b'M2421132MB', 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',
],
},
} }

@ -28,13 +28,13 @@ class CarInterface(CarInterfaceBase):
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
if candidate not in RAM_CARS: 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. # 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.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.JEEP_GRAND_CHEROKEE_2019) new_eps_platform = candidate in (CAR.PACIFICA_2019_HYBRID, CAR.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) 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: if new_eps_platform or new_eps_firmware:
ret.flags |= ChryslerFlags.HIGHER_MIN_STEERING_SPEED.value ret.flags |= ChryslerFlags.HIGHER_MIN_STEERING_SPEED.value
# Chrysler # Chrysler
if candidate in (CAR.PACIFICA_2017_HYBRID, CAR.PACIFICA_2018, CAR.PACIFICA_2018_HYBRID, CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020): if candidate in (CAR.PACIFICA_2017_HYBRID, CAR.PACIFICA_2018, CAR.PACIFICA_2018_HYBRID, CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.DODGE_DURANGO):
ret.mass = 2242. ret.mass = 2242.
ret.wheelbase = 3.089 ret.wheelbase = 3.089
ret.steerRatio = 16.2 # Pacifica Hybrid 2017 ret.steerRatio = 16.2 # Pacifica Hybrid 2017
@ -80,6 +80,7 @@ class CarInterface(CarInterfaceBase):
if ret.flags & ChryslerFlags.HIGHER_MIN_STEERING_SPEED: if ret.flags & ChryslerFlags.HIGHER_MIN_STEERING_SPEED:
# TODO: allow these cars to steer down to 13 m/s if already engaged. # 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.minSteerSpeed = 17.5 # m/s 17 on the way up, 13 on the way down once engaged.
ret.centerToFront = ret.wheelbase * 0.44 ret.centerToFront = ret.wheelbase * 0.44

@ -1,6 +1,5 @@
from enum import IntFlag, StrEnum from enum import IntFlag, StrEnum
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, List, Optional, Union
from cereal import car from cereal import car
from panda.python import uds from panda.python import uds
@ -23,6 +22,9 @@ class CAR(StrEnum):
PACIFICA_2018 = "CHRYSLER PACIFICA 2018" PACIFICA_2018 = "CHRYSLER PACIFICA 2018"
PACIFICA_2020 = "CHRYSLER PACIFICA 2020" PACIFICA_2020 = "CHRYSLER PACIFICA 2020"
# Dodge
DODGE_DURANGO = "DODGE DURANGO 2021"
# Jeep # Jeep
JEEP_GRAND_CHEROKEE = "JEEP GRAND CHEROKEE V6 2018" # includes 2017 Trailhawk JEEP_GRAND_CHEROKEE = "JEEP GRAND CHEROKEE V6 2018" # includes 2017 Trailhawk
JEEP_GRAND_CHEROKEE_2019 = "JEEP GRAND CHEROKEE 2019" # includes 2020 Trailhawk JEEP_GRAND_CHEROKEE_2019 = "JEEP GRAND CHEROKEE 2019" # includes 2020 Trailhawk
@ -63,7 +65,7 @@ class ChryslerCarInfo(CarInfo):
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.fca])) car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.fca]))
CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { CAR_INFO: dict[str, ChryslerCarInfo | list[ChryslerCarInfo] | None] = {
CAR.PACIFICA_2017_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2017"), CAR.PACIFICA_2017_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2017"),
CAR.PACIFICA_2018_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2018"), CAR.PACIFICA_2018_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2018"),
CAR.PACIFICA_2019_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2019-23"), CAR.PACIFICA_2019_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2019-23"),
@ -74,6 +76,7 @@ CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = {
], ],
CAR.JEEP_GRAND_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), CAR.JEEP_GRAND_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"),
CAR.JEEP_GRAND_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), CAR.JEEP_GRAND_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"),
CAR.DODGE_DURANGO: ChryslerCarInfo("Dodge Durango 2020-21"),
CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-24", car_parts=CarParts.common([CarHarness.ram])), CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-24", car_parts=CarParts.common([CarHarness.ram])),
CAR.RAM_HD: [ CAR.RAM_HD: [
ChryslerCarInfo("Ram 2500 2020-24", car_parts=CarParts.common([CarHarness.ram])), ChryslerCarInfo("Ram 2500 2020-24", car_parts=CarParts.common([CarHarness.ram])),
@ -128,6 +131,7 @@ DBC = {
CAR.PACIFICA_2020: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.PACIFICA_2020: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
CAR.PACIFICA_2018_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.PACIFICA_2018_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
CAR.PACIFICA_2019_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.PACIFICA_2019_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
CAR.DODGE_DURANGO: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
CAR.JEEP_GRAND_CHEROKEE: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.JEEP_GRAND_CHEROKEE: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
CAR.JEEP_GRAND_CHEROKEE_2019: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.JEEP_GRAND_CHEROKEE_2019: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
CAR.RAM_1500: dbc_dict('chrysler_ram_dt_generated', None), CAR.RAM_1500: dbc_dict('chrysler_ram_dt_generated', None),

@ -5,7 +5,6 @@ import jinja2
import os import os
from enum import Enum from enum import Enum
from natsort import natsorted from natsort import natsorted
from typing import Dict, List
from cereal import car from cereal import car
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
@ -14,7 +13,7 @@ from openpilot.selfdrive.car.docs_definitions import CarInfo, Column, CommonFoot
from openpilot.selfdrive.car.car_helpers import interfaces, get_interface_attr from openpilot.selfdrive.car.car_helpers import interfaces, get_interface_attr
def get_all_footnotes() -> Dict[Enum, int]: def get_all_footnotes() -> dict[Enum, int]:
all_footnotes = list(CommonFootnote) all_footnotes = list(CommonFootnote)
for footnotes in get_interface_attr("Footnote", ignore_none=True).values(): for footnotes in get_interface_attr("Footnote", ignore_none=True).values():
all_footnotes.extend(footnotes) all_footnotes.extend(footnotes)
@ -25,8 +24,8 @@ CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS.md")
CARS_MD_TEMPLATE = os.path.join(BASEDIR, "selfdrive", "car", "CARS_template.md") CARS_MD_TEMPLATE = os.path.join(BASEDIR, "selfdrive", "car", "CARS_template.md")
def get_all_car_info() -> List[CarInfo]: def get_all_car_info() -> list[CarInfo]:
all_car_info: List[CarInfo] = [] all_car_info: list[CarInfo] = []
footnotes = get_all_footnotes() footnotes = get_all_footnotes()
for model, car_info in get_interface_attr("CAR_INFO", combine_brands=True).items(): for model, car_info in get_interface_attr("CAR_INFO", combine_brands=True).items():
# If available, uses experimental longitudinal limits for the docs # If available, uses experimental longitudinal limits for the docs
@ -47,19 +46,19 @@ def get_all_car_info() -> List[CarInfo]:
all_car_info.append(_car_info) all_car_info.append(_car_info)
# Sort cars by make and model + year # Sort cars by make and model + year
sorted_cars: List[CarInfo] = natsorted(all_car_info, key=lambda car: car.name.lower()) sorted_cars: list[CarInfo] = natsorted(all_car_info, key=lambda car: car.name.lower())
return sorted_cars return sorted_cars
def group_by_make(all_car_info: List[CarInfo]) -> Dict[str, List[CarInfo]]: def group_by_make(all_car_info: list[CarInfo]) -> dict[str, list[CarInfo]]:
sorted_car_info = defaultdict(list) sorted_car_info = defaultdict(list)
for car_info in all_car_info: for car_info in all_car_info:
sorted_car_info[car_info.make].append(car_info) sorted_car_info[car_info.make].append(car_info)
return dict(sorted_car_info) return dict(sorted_car_info)
def generate_cars_md(all_car_info: List[CarInfo], template_fn: str) -> str: def generate_cars_md(all_car_info: list[CarInfo], template_fn: str) -> str:
with open(template_fn, "r") as f: with open(template_fn) as f:
template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True) template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True)
footnotes = [fn.value.text for fn in get_all_footnotes()] footnotes = [fn.value.text for fn in get_all_footnotes()]

@ -3,7 +3,6 @@ from collections import namedtuple
import copy import copy
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import Dict, List, Optional, Tuple, Union
from cereal import car from cereal import car
from openpilot.common.conversions import Conversions as CV from openpilot.common.conversions import Conversions as CV
@ -35,7 +34,7 @@ class Star(Enum):
@dataclass @dataclass
class BasePart: class BasePart:
name: str name: str
parts: List[Enum] = field(default_factory=list) parts: list[Enum] = field(default_factory=list)
def all_parts(self): def all_parts(self):
# Recursively get all parts # Recursively get all parts
@ -76,7 +75,7 @@ class Accessory(EnumBase):
@dataclass @dataclass
class BaseCarHarness(BasePart): class BaseCarHarness(BasePart):
parts: List[Enum] = field(default_factory=lambda: [Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft]) 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 has_connector: bool = True # without are hidden on the harness connector page
@ -149,18 +148,18 @@ class PartType(Enum):
tool = Tool tool = Tool
DEFAULT_CAR_PARTS: List[EnumBase] = [Device.threex] DEFAULT_CAR_PARTS: list[EnumBase] = [Device.threex]
@dataclass @dataclass
class CarParts: class CarParts:
parts: List[EnumBase] = field(default_factory=list) parts: list[EnumBase] = field(default_factory=list)
def __call__(self): def __call__(self):
return copy.deepcopy(self) return copy.deepcopy(self)
@classmethod @classmethod
def common(cls, add: Optional[List[EnumBase]] = None, remove: Optional[List[EnumBase]] = None): def common(cls, add: list[EnumBase] | None = None, remove: list[EnumBase] | None = None):
p = [part for part in (add or []) + DEFAULT_CAR_PARTS if part not in (remove or [])] p = [part for part in (add or []) + DEFAULT_CAR_PARTS if part not in (remove or [])]
return cls(p) return cls(p)
@ -186,7 +185,7 @@ class CommonFootnote(Enum):
Column.LONGITUDINAL) Column.LONGITUDINAL)
def get_footnotes(footnotes: List[Enum], column: Column) -> List[Enum]: def get_footnotes(footnotes: list[Enum], column: Column) -> list[Enum]:
# Returns applicable footnotes given current column # Returns applicable footnotes given current column
return [fn for fn in footnotes if fn.value.column == column] return [fn for fn in footnotes if fn.value.column == column]
@ -209,7 +208,7 @@ def get_year_list(years):
return years_list return years_list
def split_name(name: str) -> Tuple[str, str, str]: def split_name(name: str) -> tuple[str, str, str]:
make, model = name.split(" ", 1) make, model = name.split(" ", 1)
years = "" years = ""
match = re.search(MODEL_YEARS_RE, model) match = re.search(MODEL_YEARS_RE, model)
@ -233,13 +232,13 @@ class CarInfo:
# the minimum compatibility requirements for this model, regardless # the minimum compatibility requirements for this model, regardless
# of market. can be a package, trim, or list of features # of market. can be a package, trim, or list of features
requirements: Optional[str] = None requirements: str | None = None
video_link: Optional[str] = None video_link: str | None = None
footnotes: List[Enum] = field(default_factory=list) footnotes: list[Enum] = field(default_factory=list)
min_steer_speed: Optional[float] = None min_steer_speed: float | None = None
min_enable_speed: Optional[float] = None min_enable_speed: float | None = None
auto_resume: Optional[bool] = None auto_resume: bool | None = None
# all the parts needed for the supported car # all the parts needed for the supported car
car_parts: CarParts = field(default_factory=CarParts) car_parts: CarParts = field(default_factory=CarParts)
@ -248,7 +247,7 @@ class CarInfo:
self.make, self.model, self.years = split_name(self.name) self.make, self.model, self.years = split_name(self.name)
self.year_list = get_year_list(self.years) self.year_list = get_year_list(self.years)
def init(self, CP: car.CarParams, all_footnotes: Dict[Enum, int]): def init(self, CP: car.CarParams, all_footnotes: dict[Enum, int]):
self.car_name = CP.carName self.car_name = CP.carName
self.car_fingerprint = CP.carFingerprint self.car_fingerprint = CP.carFingerprint
@ -293,7 +292,7 @@ class CarInfo:
if len(tools_docs): if len(tools_docs):
hardware_col += f'<details><summary>Tools</summary><sub>{display_func(tools_docs)}</sub></details>' hardware_col += f'<details><summary>Tools</summary><sub>{display_func(tools_docs)}</sub></details>'
self.row: Dict[Enum, Union[str, Star]] = { self.row: dict[Enum, str | Star] = {
Column.MAKE: self.make, Column.MAKE: self.make,
Column.MODEL: self.model, Column.MODEL: self.model,
Column.PACKAGE: self.package, Column.PACKAGE: self.package,
@ -352,7 +351,7 @@ class CarInfo:
raise Exception(f"This notCar does not have a detail sentence: {CP.carFingerprint}") 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: def get_column(self, column: Column, star_icon: str, video_icon: str, footnote_tag: str) -> str:
item: Union[str, Star] = self.row[column] item: str | Star = self.row[column]
if isinstance(item, Star): if isinstance(item, Star):
item = star_icon.format(item.value) item = star_icon.format(item.value)
elif column == Column.MODEL and len(self.years): elif column == Column.MODEL and len(self.years):

@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import capnp import capnp
import time import time
from typing import Optional, Set
import cereal.messaging as messaging import cereal.messaging as messaging
from panda.python.uds import SERVICE_TYPE from panda.python.uds import SERVICE_TYPE
@ -20,7 +19,7 @@ def make_tester_present_msg(addr, bus, subaddr=None):
return make_can_msg(addr, bytes(dat), bus) return make_can_msg(addr, bytes(dat), bus)
def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subaddr: Optional[int] = None) -> bool: def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subaddr: int | None = None) -> bool:
# ISO-TP messages are always padded to 8 bytes # ISO-TP messages are always padded to 8 bytes
# tester present response is always a single frame # tester present response is always a single frame
dat_offset = 1 if subaddr is not None else 0 dat_offset = 1 if subaddr is not None else 0
@ -34,16 +33,16 @@ def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subadd
return False return False
def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, bus: int, timeout: float = 1, debug: bool = True) -> Set[EcuAddrBusType]: def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, 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)] 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} queries: set[EcuAddrBusType] = {(addr, None, bus) for addr in addr_list}
responses = queries responses = queries
return get_ecu_addrs(logcan, sendcan, queries, responses, timeout=timeout, debug=debug) return get_ecu_addrs(logcan, sendcan, queries, responses, timeout=timeout, debug=debug)
def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: Set[EcuAddrBusType], def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: set[EcuAddrBusType],
responses: Set[EcuAddrBusType], timeout: float = 1, debug: bool = False) -> Set[EcuAddrBusType]: responses: set[EcuAddrBusType], timeout: float = 1, debug: bool = False) -> set[EcuAddrBusType]:
ecu_responses: Set[EcuAddrBusType] = set() # set((addr, subaddr, bus),) ecu_responses: set[EcuAddrBusType] = set() # set((addr, subaddr, bus),)
try: try:
msgs = [make_tester_present_msg(addr, bus, subaddr) for addr, subaddr, bus in queries] msgs = [make_tester_present_msg(addr, bus, subaddr) for addr, subaddr, bus in queries]

@ -18,17 +18,10 @@ class CarState(CarStateBase):
self.shifter_values = can_define.dv["Gear_Shift_by_Wire_FD1"]["TrnRng_D_RqGsm"] self.shifter_values = can_define.dv["Gear_Shift_by_Wire_FD1"]["TrnRng_D_RqGsm"]
self.vehicle_sensors_valid = False self.vehicle_sensors_valid = False
self.unsupported_platform = False
def update(self, cp, cp_cam): def update(self, cp, cp_cam):
ret = car.CarState.new_message() ret = car.CarState.new_message()
# Ford Q3 hybrid variants experience a bug where a message from the PCM sends invalid checksums,
# this must be root-caused before enabling support. Ford Q4 hybrids do not have this problem.
# TrnAin_Tq_Actl and its quality flag are only set on ICE platform variants
self.unsupported_platform = (cp.vl["VehicleOperatingModes"]["TrnAinTq_D_Qf"] == 0 and
self.CP.carFingerprint not in CANFD_CAR)
# Occasionally on startup, the ABS module recalibrates the steering pinion offset, so we need to block engagement # 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 # The vehicle usually recovers out of this state within a minute of normal driving
self.vehicle_sensors_valid = cp.vl["SteeringPinion_Data"]["StePinCompAnEst_D_Qf"] == 3 self.vehicle_sensors_valid = cp.vl["SteeringPinion_Data"]["StePinCompAnEst_D_Qf"] == 3
@ -54,7 +47,7 @@ class CarState(CarStateBase):
ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CarControllerParams.STEER_DRIVER_ALLOWANCE, 5) ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CarControllerParams.STEER_DRIVER_ALLOWANCE, 5)
ret.steerFaultTemporary = cp.vl["EPAS_INFO"]["EPAS_Failure"] == 1 ret.steerFaultTemporary = cp.vl["EPAS_INFO"]["EPAS_Failure"] == 1
ret.steerFaultPermanent = cp.vl["EPAS_INFO"]["EPAS_Failure"] in (2, 3) ret.steerFaultPermanent = cp.vl["EPAS_INFO"]["EPAS_Failure"] in (2, 3)
# ret.espDisabled = False # TODO: find traction control signal ret.espDisabled = cp.vl["Cluster_Info1_FD1"]["DrvSlipCtlMde_D_Rq"] != 0 # 0 is default mode
if self.CP.carFingerprint in CANFD_CAR: if self.CP.carFingerprint in CANFD_CAR:
# this signal is always 0 on non-CAN FD cars # this signal is always 0 on non-CAN FD cars

@ -109,8 +109,6 @@ class CarInterface(CarInterfaceBase):
events = self.create_common_events(ret, extra_gears=[GearShifter.manumatic]) events = self.create_common_events(ret, extra_gears=[GearShifter.manumatic])
if not self.CS.vehicle_sensors_valid: if not self.CS.vehicle_sensors_valid:
events.add(car.CarEvent.EventName.vehicleSensorsInvalid) events.add(car.CarEvent.EventName.vehicleSensorsInvalid)
if self.CS.unsupported_platform:
events.add(car.CarEvent.EventName.startupNoControl)
ret.events = events.to_msg() ret.events = events.to_msg()

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import unittest import unittest
from parameterized import parameterized from parameterized import parameterized
from typing import Dict, Iterable, Optional, Tuple from collections.abc import Iterable
import capnp import capnp
@ -50,7 +50,7 @@ class TestFordFW(unittest.TestCase):
self.assertIsNone(subaddr, "Unexpected ECU subaddress") self.assertIsNone(subaddr, "Unexpected ECU subaddress")
@parameterized.expand(FW_VERSIONS.items()) @parameterized.expand(FW_VERSIONS.items())
def test_fw_versions(self, car_model: str, fw_versions: Dict[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]], Iterable[bytes]]): def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[capnp.lib.capnp._EnumModule, int, int | None], Iterable[bytes]]):
for (ecu, addr, subaddr), fws in fw_versions.items(): for (ecu, addr, subaddr), fws in fw_versions.items():
self.assertIn(ecu, ECU_FW_CORE, "Unexpected ECU") self.assertIn(ecu, ECU_FW_CORE, "Unexpected ECU")
self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch") self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch")

@ -1,7 +1,6 @@
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, StrEnum from enum import Enum, StrEnum
from typing import Dict, List, Union
from cereal import car from cereal import car
from openpilot.selfdrive.car import AngleRateLimit, dbc_dict from openpilot.selfdrive.car import AngleRateLimit, dbc_dict
@ -59,7 +58,7 @@ class RADAR:
DELPHI_MRR = 'FORD_CADS' DELPHI_MRR = 'FORD_CADS'
DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict("ford_lincoln_base_pt", RADAR.DELPHI_MRR)) DBC: dict[str, dict[str, str]] = defaultdict(lambda: dbc_dict("ford_lincoln_base_pt", RADAR.DELPHI_MRR))
# F-150 radar is not yet supported # F-150 radar is not yet supported
DBC[CAR.F_150_MK14] = dbc_dict("ford_lincoln_base_pt", None) DBC[CAR.F_150_MK14] = dbc_dict("ford_lincoln_base_pt", None)
@ -87,23 +86,37 @@ class FordCarInfo(CarInfo):
self.car_parts = CarParts([Device.threex, harness]) self.car_parts = CarParts([Device.threex, harness])
CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { CAR_INFO: dict[str, CarInfo | list[CarInfo]] = {
CAR.BRONCO_SPORT_MK1: FordCarInfo("Ford Bronco Sport 2021-22"), CAR.BRONCO_SPORT_MK1: FordCarInfo("Ford Bronco Sport 2021-22"),
CAR.ESCAPE_MK4: [ CAR.ESCAPE_MK4: [
FordCarInfo("Ford Escape 2020-22"), FordCarInfo("Ford Escape 2020-22"),
FordCarInfo("Ford Escape Hybrid 2020-22"),
FordCarInfo("Ford Escape Plug-in Hybrid 2020-22"),
FordCarInfo("Ford Kuga 2020-22", "Adaptive Cruise Control with Lane Centering"), FordCarInfo("Ford Kuga 2020-22", "Adaptive Cruise Control with Lane Centering"),
FordCarInfo("Ford Kuga Hybrid 2020-22", "Adaptive Cruise Control with Lane Centering"),
FordCarInfo("Ford Kuga Plug-in Hybrid 2020-22", "Adaptive Cruise Control with Lane Centering"),
], ],
CAR.EXPLORER_MK6: [ CAR.EXPLORER_MK6: [
FordCarInfo("Ford Explorer 2020-23"), FordCarInfo("Ford Explorer 2020-23"),
FordCarInfo("Lincoln Aviator 2020-21", "Co-Pilot360 Plus"), FordCarInfo("Ford Explorer Hybrid 2020-23"), # Limited and Platinum only
FordCarInfo("Lincoln Aviator 2020-23", "Co-Pilot360 Plus"),
FordCarInfo("Lincoln Aviator Plug-in Hybrid 2020-23", "Co-Pilot360 Plus"), # Grand Touring only
],
CAR.F_150_MK14: [
FordCarInfo("Ford F-150 2023", "Co-Pilot360 Active 2.0"),
FordCarInfo("Ford F-150 Hybrid 2023", "Co-Pilot360 Active 2.0"),
], ],
CAR.F_150_MK14: FordCarInfo("Ford F-150 2023", "Co-Pilot360 Active 2.0"),
CAR.F_150_LIGHTNING_MK1: FordCarInfo("Ford F-150 Lightning 2021-23", "Co-Pilot360 Active 2.0"), CAR.F_150_LIGHTNING_MK1: FordCarInfo("Ford F-150 Lightning 2021-23", "Co-Pilot360 Active 2.0"),
CAR.MUSTANG_MACH_E_MK1: FordCarInfo("Ford Mustang Mach-E 2021-23", "Co-Pilot360 Active 2.0"), CAR.MUSTANG_MACH_E_MK1: FordCarInfo("Ford Mustang Mach-E 2021-23", "Co-Pilot360 Active 2.0"),
CAR.FOCUS_MK4: FordCarInfo("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS]), CAR.FOCUS_MK4: [
FordCarInfo("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS]),
FordCarInfo("Ford Focus Hybrid 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS]), # mHEV only
],
CAR.MAVERICK_MK1: [ CAR.MAVERICK_MK1: [
FordCarInfo("Ford Maverick 2022", "LARIAT Luxury"), FordCarInfo("Ford Maverick 2022", "LARIAT Luxury"),
FordCarInfo("Ford Maverick Hybrid 2022", "LARIAT Luxury"),
FordCarInfo("Ford Maverick 2023", "Co-Pilot360 Assist"), FordCarInfo("Ford Maverick 2023", "Co-Pilot360 Assist"),
FordCarInfo("Ford Maverick Hybrid 2023", "Co-Pilot360 Assist"),
], ],
} }
@ -111,6 +124,11 @@ FW_QUERY_CONFIG = FwQueryConfig(
requests=[ requests=[
# CAN and CAN FD queries are combined. # CAN and CAN FD queries are combined.
# FIXME: For CAN FD, ECUs respond with frames larger than 8 bytes on the powertrain bus # 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],
logging=True,
),
Request( Request(
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST], [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE], [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],

@ -3,19 +3,21 @@ import capnp
import copy import copy
from dataclasses import dataclass, field from dataclasses import dataclass, field
import struct import struct
from typing import Callable, Dict, List, Optional, Set, Tuple from collections.abc import Callable
import panda.python.uds as uds import panda.python.uds as uds
AddrType = Tuple[int, Optional[int]] AddrType = tuple[int, int | None]
EcuAddrBusType = Tuple[int, Optional[int], int] EcuAddrBusType = tuple[int, int | None, int]
EcuAddrSubAddr = Tuple[int, int, Optional[int]] EcuAddrSubAddr = tuple[int, int, int | None]
LiveFwVersions = Dict[AddrType, Set[bytes]] LiveFwVersions = dict[AddrType, set[bytes]]
OfflineFwVersions = Dict[str, Dict[EcuAddrSubAddr, List[bytes]]] OfflineFwVersions = dict[str, dict[EcuAddrSubAddr, list[bytes]]]
# A global list of addresses we will only ever consider for VIN responses # A global list of addresses we will only ever consider for VIN responses
STANDARD_VIN_ADDRS = [0x7e0, 0x7e2, 0x760, 0x18da10f1, 0x18da0ef1] # engine, hybrid controller, Ford abs, 29-bit engine, PGM-FI # 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): def p16(val):
@ -63,12 +65,15 @@ class StdQueries:
GM_VIN_REQUEST = b'\x1a\x90' GM_VIN_REQUEST = b'\x1a\x90'
GM_VIN_RESPONSE = b'\x5a\x90' GM_VIN_RESPONSE = b'\x5a\x90'
KWP_VIN_REQUEST = b'\x21\x81'
KWP_VIN_RESPONSE = b'\x61\x81'
@dataclass @dataclass
class Request: class Request:
request: List[bytes] request: list[bytes]
response: List[bytes] response: list[bytes]
whitelist_ecus: List[int] = field(default_factory=list) whitelist_ecus: list[int] = field(default_factory=list)
rx_offset: int = 0x8 rx_offset: int = 0x8
bus: int = 1 bus: int = 1
# Whether this query should be run on the first auxiliary panda (CAN FD cars for example) # Whether this query should be run on the first auxiliary panda (CAN FD cars for example)
@ -81,15 +86,15 @@ class Request:
@dataclass @dataclass
class FwQueryConfig: class FwQueryConfig:
requests: List[Request] requests: list[Request]
# TODO: make this automatic and remove hardcoded lists, or do fingerprinting with ecus # 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) # Overrides and removes from essential ecus for specific models and ecus (exact matching)
non_essential_ecus: Dict[capnp.lib.capnp._EnumModule, List[str]] = field(default_factory=dict) non_essential_ecus: dict[capnp.lib.capnp._EnumModule, list[str]] = field(default_factory=dict)
# Ecus added for data collection, not to be fingerprinted on # Ecus added for data collection, not to be fingerprinted on
extra_ecus: List[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]]] = field(default_factory=list) extra_ecus: list[tuple[capnp.lib.capnp._EnumModule, int, int | None]] = field(default_factory=list)
# Function a brand can implement to provide better fuzzy matching. Takes in FW versions, # Function a brand can implement to provide better fuzzy matching. Takes in FW versions,
# returns set of candidates. Only will match if one candidate is returned # returns set of candidates. Only will match if one candidate is returned
match_fw_to_car_fuzzy: Optional[Callable[[LiveFwVersions, OfflineFwVersions], Set[str]]] = None match_fw_to_car_fuzzy: Callable[[LiveFwVersions, OfflineFwVersions], set[str]] | None = None
def __post_init__(self): def __post_init__(self):
for i in range(len(self.requests)): for i in range(len(self.requests)):

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from collections import defaultdict from collections import defaultdict
from typing import Any, DefaultDict, Dict, List, Optional, Set from typing import Any, TypeVar
from collections.abc import Iterator
from tqdm import tqdm from tqdm import tqdm
import capnp import capnp
@ -24,20 +25,22 @@ VERSIONS = get_interface_attr('FW_VERSIONS', ignore_none=True)
MODEL_TO_BRAND = {c: b for b, e in VERSIONS.items() for c in e} 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] REQUESTS = [(brand, config, r) for brand, config in FW_QUERY_CONFIGS.items() for r in config.requests]
T = TypeVar('T')
def chunks(l, n=128):
def chunks(l: list[T], n: int = 128) -> Iterator[list[T]]:
for i in range(0, len(l), n): for i in range(0, len(l), n):
yield l[i:i + n] yield l[i:i + n]
def is_brand(brand: str, filter_brand: Optional[str]) -> bool: def is_brand(brand: str, filter_brand: str | None) -> bool:
"""Returns if brand matches filter_brand or no brand filter is specified""" """Returns if brand matches filter_brand or no brand filter is specified"""
return filter_brand is None or brand == filter_brand return filter_brand is None or brand == filter_brand
def build_fw_dict(fw_versions: List[capnp.lib.capnp._DynamicStructBuilder], def build_fw_dict(fw_versions: list[capnp.lib.capnp._DynamicStructBuilder],
filter_brand: Optional[str] = None) -> Dict[AddrType, Set[bytes]]: filter_brand: str | None = None) -> dict[AddrType, set[bytes]]:
fw_versions_dict: DefaultDict[AddrType, Set[bytes]] = defaultdict(set) fw_versions_dict: defaultdict[AddrType, set[bytes]] = defaultdict(set)
for fw in fw_versions: for fw in fw_versions:
if is_brand(fw.brand, filter_brand) and not fw.logging: if is_brand(fw.brand, filter_brand) and not fw.logging:
sub_addr = fw.subAddress if fw.subAddress != 0 else None sub_addr = fw.subAddress if fw.subAddress != 0 else None
@ -95,7 +98,7 @@ def match_fw_to_car_fuzzy(live_fw_versions, match_brand=None, log=True, exclude=
return set() return set()
def match_fw_to_car_exact(live_fw_versions, match_brand=None, log=True, extra_fw_versions=None) -> Set[str]: def match_fw_to_car_exact(live_fw_versions, match_brand=None, log=True, extra_fw_versions=None) -> set[str]:
"""Do an exact FW match. Returns all cars that match the given """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 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 essential the FW version can be missing to get a fingerprint, but if it's present it
@ -162,11 +165,11 @@ def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True, log=True):
return True, set() return True, set()
def get_present_ecus(logcan, sendcan, num_pandas=1) -> Set[EcuAddrBusType]: def get_present_ecus(logcan, sendcan, num_pandas=1) -> set[EcuAddrBusType]:
params = Params() params = Params()
# queries are split by OBD multiplexing mode # queries are split by OBD multiplexing mode
queries: Dict[bool, List[List[EcuAddrBusType]]] = {True: [], False: []} queries: dict[bool, list[list[EcuAddrBusType]]] = {True: [], False: []}
parallel_queries: Dict[bool, List[EcuAddrBusType]] = {True: [], False: []} parallel_queries: dict[bool, list[EcuAddrBusType]] = {True: [], False: []}
responses = set() responses = set()
for brand, config, r in REQUESTS: for brand, config, r in REQUESTS:
@ -201,7 +204,7 @@ def get_present_ecus(logcan, sendcan, num_pandas=1) -> Set[EcuAddrBusType]:
return ecu_responses return ecu_responses
def get_brand_ecu_matches(ecu_rx_addrs: Set[EcuAddrBusType]) -> dict[str, set[AddrType]]: 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""" """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_addrs = {brand: {(addr, subaddr) for _, addr, subaddr in config.get_all_ecus(VERSIONS[brand])} for
@ -229,7 +232,7 @@ def set_obd_multiplexing(params: Params, obd_multiplexing: bool):
def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \
List[capnp.lib.capnp._DynamicStructBuilder]: list[capnp.lib.capnp._DynamicStructBuilder]:
"""Queries for FW versions ordering brands by likelihood, breaks when exact match is found""" """Queries for FW versions ordering brands by likelihood, breaks when exact match is found"""
all_car_fw = [] all_car_fw = []
@ -252,7 +255,7 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pand
def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \
List[capnp.lib.capnp._DynamicStructBuilder]: list[capnp.lib.capnp._DynamicStructBuilder]:
versions = VERSIONS.copy() versions = VERSIONS.copy()
params = Params() params = Params()

@ -2,6 +2,7 @@
from openpilot.selfdrive.car.gm.values import CAR from openpilot.selfdrive.car.gm.values import CAR
# Trailblazer also matches as a SILVERADO, TODO: split with fw versions # 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 = { FINGERPRINTS = {
@ -52,6 +53,9 @@ FINGERPRINTS = {
}], }],
CAR.EQUINOX: [{ CAR.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, 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
}], }],
} }

@ -137,7 +137,7 @@ class CarInterface(CarInterfaceBase):
# These cars have been put into dashcam only due to both a lack of users and test coverage. # 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 # These cars likely still work fine. Once a user confirms each car works and a test route is
# added to selfdrive/car/tests/routes.py, we can remove it from this list. # added to selfdrive/car/tests/routes.py, we can remove it from this list.
ret.dashcamOnly = candidate in {CAR.CADILLAC_ATS, CAR.HOLDEN_ASTRA, CAR.MALIBU, CAR.BUICK_REGAL, CAR.EQUINOX} or \ ret.dashcamOnly = candidate in {CAR.CADILLAC_ATS, CAR.HOLDEN_ASTRA, CAR.MALIBU, CAR.BUICK_REGAL} or \
(ret.networkLocation == NetworkLocation.gateway and ret.radarUnavailable) (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. # Start with a baseline tuning for all GM vehicles. Override tuning as needed in each model section below.

@ -1,7 +1,6 @@
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, StrEnum from enum import Enum, StrEnum
from typing import Dict, List, Union
from cereal import car from cereal import car
from openpilot.selfdrive.car import dbc_dict from openpilot.selfdrive.car import dbc_dict
@ -98,7 +97,7 @@ class GMCarInfo(CarInfo):
self.footnotes.append(Footnote.OBD_II) self.footnotes.append(Footnote.OBD_II)
CAR_INFO: Dict[str, Union[GMCarInfo, List[GMCarInfo]]] = { CAR_INFO: dict[str, GMCarInfo | list[GMCarInfo]] = {
CAR.HOLDEN_ASTRA: GMCarInfo("Holden Astra 2017"), CAR.HOLDEN_ASTRA: GMCarInfo("Holden Astra 2017"),
CAR.VOLT: GMCarInfo("Chevrolet Volt 2017-18", min_enable_speed=0, video_link="https://youtu.be/QeMCN_4TFfQ"), CAR.VOLT: GMCarInfo("Chevrolet Volt 2017-18", min_enable_speed=0, video_link="https://youtu.be/QeMCN_4TFfQ"),
CAR.CADILLAC_ATS: GMCarInfo("Cadillac ATS Premium Performance 2018"), CAR.CADILLAC_ATS: GMCarInfo("Cadillac ATS Premium Performance 2018"),
@ -181,7 +180,7 @@ FW_QUERY_CONFIG = FwQueryConfig(
extra_ecus=[(Ecu.fwdCamera, 0x24b, None)], extra_ecus=[(Ecu.fwdCamera, 0x24b, None)],
) )
DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis')) DBC: dict[str, dict[str, str]] = defaultdict(lambda: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'))
EV_CAR = {CAR.VOLT, CAR.BOLT_EUV} EV_CAR = {CAR.VOLT, CAR.BOLT_EUV}

@ -7,8 +7,8 @@ from opendbc.can.can_define import CANDefine
from opendbc.can.parser import CANParser from opendbc.can.parser import CANParser
from openpilot.selfdrive.car.honda.hondacan import get_cruise_speed_conversion, get_pt_bus from openpilot.selfdrive.car.honda.hondacan import get_cruise_speed_conversion, get_pt_bus
from openpilot.selfdrive.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, \ from openpilot.selfdrive.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, \
HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_ALT_BRAKE_SIGNAL, \ HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_RADARLESS, \
HONDA_BOSCH_RADARLESS HondaFlags
from openpilot.selfdrive.car.interfaces import CarStateBase from openpilot.selfdrive.car.interfaces import CarStateBase
TransmissionType = car.CarParams.TransmissionType TransmissionType = car.CarParams.TransmissionType
@ -44,7 +44,7 @@ def get_can_messages(CP, gearbox_msg):
else: else:
messages.append((gearbox_msg, 100)) messages.append((gearbox_msg, 100))
if CP.carFingerprint in HONDA_BOSCH_ALT_BRAKE_SIGNAL: if CP.flags & HondaFlags.BOSCH_ALT_BRAKE:
messages.append(("BRAKE_MODULE", 50)) messages.append(("BRAKE_MODULE", 50))
if CP.carFingerprint in (HONDA_BOSCH | {CAR.CIVIC, CAR.ODYSSEY, CAR.ODYSSEY_CHN}): if CP.carFingerprint in (HONDA_BOSCH | {CAR.CIVIC, CAR.ODYSSEY, CAR.ODYSSEY_CHN}):
@ -217,7 +217,7 @@ class CarState(CarStateBase):
else: else:
ret.cruiseState.speed = cp.vl["CRUISE"]["CRUISE_SPEED_PCM"] * CV.KPH_TO_MS ret.cruiseState.speed = cp.vl["CRUISE"]["CRUISE_SPEED_PCM"] * CV.KPH_TO_MS
if self.CP.carFingerprint in HONDA_BOSCH_ALT_BRAKE_SIGNAL: if self.CP.flags & HondaFlags.BOSCH_ALT_BRAKE:
ret.brakePressed = cp.vl["BRAKE_MODULE"]["BRAKE_PRESSED"] != 0 ret.brakePressed = cp.vl["BRAKE_MODULE"]["BRAKE_PRESSED"] != 0
else: else:
# brake switch has shown some single time step noise, so only considered when # brake switch has shown some single time step noise, so only considered when

@ -101,10 +101,6 @@ FW_VERSIONS = {
b'39990-TVA-X040\x00\x00', b'39990-TVA-X040\x00\x00',
b'39990-TVE-H130\x00\x00', b'39990-TVE-H130\x00\x00',
], ],
(Ecu.unknown, 0x18da3af1, None): [
b'39390-TVA-A020\x00\x00',
b'39390-TVA-A120\x00\x00',
],
(Ecu.srs, 0x18da53f1, None): [ (Ecu.srs, 0x18da53f1, None): [
b'77959-TBX-H230\x00\x00', b'77959-TBX-H230\x00\x00',
b'77959-TVA-A460\x00\x00', b'77959-TVA-A460\x00\x00',
@ -299,6 +295,7 @@ FW_VERSIONS = {
(Ecu.srs, 0x18da53f1, None): [ (Ecu.srs, 0x18da53f1, None): [
b'77959-TBA-A030\x00\x00', b'77959-TBA-A030\x00\x00',
b'77959-TBA-A040\x00\x00', b'77959-TBA-A040\x00\x00',
b'77959-TBG-A020\x00\x00',
b'77959-TBG-A030\x00\x00', b'77959-TBG-A030\x00\x00',
b'77959-TEA-Q820\x00\x00', b'77959-TEA-Q820\x00\x00',
], ],
@ -680,6 +677,7 @@ FW_VERSIONS = {
b'36802-TLA-A040\x00\x00', b'36802-TLA-A040\x00\x00',
b'36802-TLA-A050\x00\x00', b'36802-TLA-A050\x00\x00',
b'36802-TLA-A060\x00\x00', b'36802-TLA-A060\x00\x00',
b'36802-TLA-A070\x00\x00',
b'36802-TMC-Q040\x00\x00', b'36802-TMC-Q040\x00\x00',
b'36802-TMC-Q070\x00\x00', b'36802-TMC-Q070\x00\x00',
b'36802-TNY-A030\x00\x00', b'36802-TNY-A030\x00\x00',
@ -834,6 +832,7 @@ FW_VERSIONS = {
(Ecu.programmedFuelInjection, 0x18da10f1, None): [ (Ecu.programmedFuelInjection, 0x18da10f1, None): [
b'37805-5MR-3050\x00\x00', b'37805-5MR-3050\x00\x00',
b'37805-5MR-3250\x00\x00', b'37805-5MR-3250\x00\x00',
b'37805-5MR-4070\x00\x00',
b'37805-5MR-4080\x00\x00', b'37805-5MR-4080\x00\x00',
b'37805-5MR-4180\x00\x00', b'37805-5MR-4180\x00\x00',
b'37805-5MR-A240\x00\x00', b'37805-5MR-A240\x00\x00',
@ -879,6 +878,7 @@ FW_VERSIONS = {
b'28102-5MX-A900\x00\x00', b'28102-5MX-A900\x00\x00',
b'28102-5MX-A910\x00\x00', b'28102-5MX-A910\x00\x00',
b'28102-5MX-C001\x00\x00', b'28102-5MX-C001\x00\x00',
b'28102-5MX-C910\x00\x00',
b'28102-5MX-D001\x00\x00', b'28102-5MX-D001\x00\x00',
b'28102-5MX-D710\x00\x00', b'28102-5MX-D710\x00\x00',
b'28102-5MX-K610\x00\x00', b'28102-5MX-K610\x00\x00',
@ -916,6 +916,7 @@ FW_VERSIONS = {
b'78109-THR-C320\x00\x00', b'78109-THR-C320\x00\x00',
b'78109-THR-C330\x00\x00', b'78109-THR-C330\x00\x00',
b'78109-THR-CE20\x00\x00', b'78109-THR-CE20\x00\x00',
b'78109-THR-CL10\x00\x00',
b'78109-THR-DA20\x00\x00', b'78109-THR-DA20\x00\x00',
b'78109-THR-DA30\x00\x00', b'78109-THR-DA30\x00\x00',
b'78109-THR-DA40\x00\x00', b'78109-THR-DA40\x00\x00',
@ -1206,6 +1207,7 @@ FW_VERSIONS = {
b'39990-T6Z-A020\x00\x00', b'39990-T6Z-A020\x00\x00',
b'39990-T6Z-A030\x00\x00', b'39990-T6Z-A030\x00\x00',
b'39990-T6Z-A050\x00\x00', b'39990-T6Z-A050\x00\x00',
b'39990-T6Z-A110\x00\x00',
], ],
(Ecu.fwdRadar, 0x18dab0f1, None): [ (Ecu.fwdRadar, 0x18dab0f1, None): [
b'36161-T6Z-A020\x00\x00', b'36161-T6Z-A020\x00\x00',
@ -1213,6 +1215,7 @@ FW_VERSIONS = {
b'36161-T6Z-A420\x00\x00', b'36161-T6Z-A420\x00\x00',
b'36161-T6Z-A520\x00\x00', b'36161-T6Z-A520\x00\x00',
b'36161-T6Z-A620\x00\x00', b'36161-T6Z-A620\x00\x00',
b'36161-T6Z-A720\x00\x00',
b'36161-TJZ-A120\x00\x00', b'36161-TJZ-A120\x00\x00',
], ],
(Ecu.gateway, 0x18daeff1, None): [ (Ecu.gateway, 0x18daeff1, None): [
@ -1220,6 +1223,7 @@ FW_VERSIONS = {
b'38897-T6Z-A110\x00\x00', b'38897-T6Z-A110\x00\x00',
], ],
(Ecu.combinationMeter, 0x18da60f1, None): [ (Ecu.combinationMeter, 0x18da60f1, None): [
b'78108-T6Z-AF10\x00\x00',
b'78109-T6Z-A420\x00\x00', b'78109-T6Z-A420\x00\x00',
b'78109-T6Z-A510\x00\x00', b'78109-T6Z-A510\x00\x00',
b'78109-T6Z-A710\x00\x00', b'78109-T6Z-A710\x00\x00',
@ -1237,6 +1241,7 @@ FW_VERSIONS = {
b'57114-T6Z-A120\x00\x00', b'57114-T6Z-A120\x00\x00',
b'57114-T6Z-A130\x00\x00', b'57114-T6Z-A130\x00\x00',
b'57114-T6Z-A520\x00\x00', b'57114-T6Z-A520\x00\x00',
b'57114-T6Z-A610\x00\x00',
b'57114-TJZ-A520\x00\x00', b'57114-TJZ-A520\x00\x00',
], ],
}, },

@ -3,8 +3,9 @@ from cereal import car
from panda import Panda from panda import Panda
from openpilot.common.conversions import Conversions as CV from openpilot.common.conversions import Conversions as CV
from openpilot.common.numpy_fast import interp from openpilot.common.numpy_fast import interp
from openpilot.selfdrive.car.honda.hondacan import get_pt_bus
from openpilot.selfdrive.car.honda.values import CarControllerParams, CruiseButtons, HondaFlags, CAR, HONDA_BOSCH, HONDA_NIDEC_ALT_SCM_MESSAGES, \ from openpilot.selfdrive.car.honda.values import CarControllerParams, CruiseButtons, HondaFlags, CAR, HONDA_BOSCH, HONDA_NIDEC_ALT_SCM_MESSAGES, \
HONDA_BOSCH_ALT_BRAKE_SIGNAL, HONDA_BOSCH_RADARLESS HONDA_BOSCH_RADARLESS
from openpilot.selfdrive.car import create_button_events, get_safety_config from openpilot.selfdrive.car import create_button_events, get_safety_config
from openpilot.selfdrive.car.interfaces import CarInterfaceBase from openpilot.selfdrive.car.interfaces import CarInterfaceBase
from openpilot.selfdrive.car.disable_ecu import disable_ecu from openpilot.selfdrive.car.disable_ecu import disable_ecu
@ -275,7 +276,8 @@ class CarInterface(CarInterfaceBase):
raise ValueError(f"unsupported car {candidate}") raise ValueError(f"unsupported car {candidate}")
# These cars use alternate user brake msg (0x1BE) # These cars use alternate user brake msg (0x1BE)
if candidate in HONDA_BOSCH_ALT_BRAKE_SIGNAL: if 0x1BE in fingerprint[get_pt_bus(candidate)] and candidate in HONDA_BOSCH:
ret.flags |= HondaFlags.BOSCH_ALT_BRAKE.value
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_ALT_BRAKE ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_ALT_BRAKE
# These cars use alternate SCM messages (SCM_FEEDBACK AND SCM_BUTTON) # These cars use alternate SCM messages (SCM_FEEDBACK AND SCM_BUTTON)

@ -1,6 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, IntFlag, StrEnum from enum import Enum, IntFlag, StrEnum
from typing import Dict, List, Optional, Union
from cereal import car from cereal import car
from openpilot.common.conversions import Conversions as CV from openpilot.common.conversions import Conversions as CV
@ -49,6 +48,7 @@ class CarControllerParams:
class HondaFlags(IntFlag): class HondaFlags(IntFlag):
# Bosch models with alternate set of LKAS_HUD messages # Bosch models with alternate set of LKAS_HUD messages
BOSCH_EXT_HUD = 1 BOSCH_EXT_HUD = 1
BOSCH_ALT_BRAKE = 2
# Car button codes # Car button codes
@ -115,7 +115,7 @@ class HondaCarInfo(CarInfo):
self.car_parts = CarParts.common([CarHarness.nidec]) self.car_parts = CarParts.common([CarHarness.nidec])
CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { CAR_INFO: dict[str, HondaCarInfo | list[HondaCarInfo] | None] = {
CAR.ACCORD: [ CAR.ACCORD: [
HondaCarInfo("Honda Accord 2018-22", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS), HondaCarInfo("Honda Accord 2018-22", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS),
HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS), HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS),
@ -149,7 +149,7 @@ CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = {
HondaCarInfo("Honda Pilot 2016-22", min_steer_speed=12. * CV.MPH_TO_MS), HondaCarInfo("Honda Pilot 2016-22", min_steer_speed=12. * CV.MPH_TO_MS),
HondaCarInfo("Honda Passport 2019-23", "All", min_steer_speed=12. * CV.MPH_TO_MS), HondaCarInfo("Honda Passport 2019-23", "All", min_steer_speed=12. * CV.MPH_TO_MS),
], ],
CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-23", min_steer_speed=12. * CV.MPH_TO_MS), CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-24", min_steer_speed=12. * CV.MPH_TO_MS),
CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS), CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS),
CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS), CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS),
} }
@ -195,10 +195,19 @@ FW_QUERY_CONFIG = FwQueryConfig(
[StdQueries.UDS_VERSION_REQUEST], [StdQueries.UDS_VERSION_REQUEST],
[StdQueries.UDS_VERSION_RESPONSE], [StdQueries.UDS_VERSION_RESPONSE],
bus=1, bus=1,
logging=True,
obd_multiplexing=False, 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.programmedFuelInjection: [CAR.CIVIC_BOSCH, CAR.CRV_5G],
Ecu.transmission: [CAR.CIVIC_BOSCH, CAR.CRV_5G],
Ecu.vsa: [CAR.CIVIC_BOSCH, CAR.CRV_5G],
Ecu.combinationMeter: [CAR.CIVIC_BOSCH, CAR.CRV_5G],
Ecu.gateway: [CAR.CIVIC_BOSCH, CAR.CRV_5G],
Ecu.electricBrakeBooster: [CAR.CIVIC_BOSCH, CAR.CRV_5G],
},
extra_ecus=[ extra_ecus=[
# The only other ECU on PT bus accessible by camera on radarless Civic # The only other ECU on PT bus accessible by camera on radarless Civic
(Ecu.unknown, 0x18DAB3F1, None), (Ecu.unknown, 0x18DAB3F1, None),
@ -243,5 +252,4 @@ HONDA_NIDEC_ALT_SCM_MESSAGES = {CAR.ACURA_ILX, CAR.ACURA_RDX, CAR.CRV, CAR.CRV_E
CAR.PILOT, CAR.RIDGELINE} CAR.PILOT, CAR.RIDGELINE}
HONDA_BOSCH = {CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_5G, HONDA_BOSCH = {CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_5G,
CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022, CAR.HRV_3G} CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022, CAR.HRV_3G}
HONDA_BOSCH_ALT_BRAKE_SIGNAL = {CAR.ACCORD, CAR.CRV_5G, CAR.ACURA_RDX_3G, CAR.HRV_3G}
HONDA_BOSCH_RADARLESS = {CAR.CIVIC_2022, CAR.HRV_3G} HONDA_BOSCH_RADARLESS = {CAR.CIVIC_2022, CAR.HRV_3G}

@ -973,6 +973,7 @@ FW_VERSIONS = {
b'\xf1\x00BD MDPS C 1.00 1.02 56310-XX000 4BD2C102', 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 56310/M6300 4BDDC108',
b'\xf1\x00BD MDPS C 1.00 1.08 56310M6300\x00 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', b'\xf1\x00BDm MDPS C A.01 1.03 56310M7800\x00 4BPMC103',
], ],
(Ecu.fwdCamera, 0x7c4, None): [ (Ecu.fwdCamera, 0x7c4, None): [
@ -989,11 +990,13 @@ FW_VERSIONS = {
b'\xf1\x81616F2051\x00\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x81616F2051\x00\x00\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.abs, 0x7d1, None): [ (Ecu.abs, 0x7d1, None): [
b'\xf1\x00\x00\x00\x00\x00\x00\x00',
b'\xf1\x816VGRAH00018.ELF\xf1\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x816VGRAH00018.ELF\xf1\x00\x00\x00\x00\x00\x00\x00',
b'\xf1\x8758900-M7AB0 \xf1\x816VQRAD00127.ELF\xf1\x00\x00\x00\x00\x00\x00\x00', b'\xf1\x8758900-M7AB0 \xf1\x816VQRAD00127.ELF\xf1\x00\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.transmission, 0x7e1, None): [ (Ecu.transmission, 0x7e1, None): [
b'\xf1\x006V2B0_C2\x00\x006V2C6051\x00\x00CBD0N20NL1\x00\x00\x00\x00', b'\xf1\x006V2B0_C2\x00\x006V2C6051\x00\x00CBD0N20NL1\x00\x00\x00\x00',
b'\xf1\x006V2B0_C2\x00\x006V2C6051\x00\x00CBD0N20NL1\x90@\xc6\xae',
b'\xf1\x816U2VC051\x00\x00\xf1\x006U2V0_C2\x00\x006U2VC051\x00\x00DBD0T16SS0\x00\x00\x00\x00', b'\xf1\x816U2VC051\x00\x00\xf1\x006U2V0_C2\x00\x006U2VC051\x00\x00DBD0T16SS0\x00\x00\x00\x00',
b"\xf1\x816U2VC051\x00\x00\xf1\x006U2V0_C2\x00\x006U2VC051\x00\x00DBD0T16SS0\xcf\x1e'\xc3", b"\xf1\x816U2VC051\x00\x00\xf1\x006U2V0_C2\x00\x006U2VC051\x00\x00DBD0T16SS0\xcf\x1e'\xc3",
], ],
@ -1546,6 +1549,7 @@ FW_VERSIONS = {
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-GI000 210813',
b'\xf1\x00NE1 MFC AT EUR RHD 1.00 1.01 99211-GI010 211007', 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 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 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-GI020 230719',
b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.01 99211-GI010 211007', b'\xf1\x00NE1 MFC AT USA LHD 1.00 1.01 99211-GI010 211007',
@ -1566,6 +1570,7 @@ FW_VERSIONS = {
}, },
CAR.TUCSON_4TH_GEN: { CAR.TUCSON_4TH_GEN: {
(Ecu.fwdCamera, 0x7c4, None): [ (Ecu.fwdCamera, 0x7c4, None): [
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 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-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-N9220 14K',
@ -1578,6 +1583,7 @@ FW_VERSIONS = {
(Ecu.fwdRadar, 0x7d0, None): [ (Ecu.fwdRadar, 0x7d0, None): [
b'\xf1\x00NX4__ 1.00 1.00 99110-N9100 ', b'\xf1\x00NX4__ 1.00 1.00 99110-N9100 ',
b'\xf1\x00NX4__ 1.00 1.01 99110-N9000 ', 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 ', b'\xf1\x00NX4__ 1.01 1.00 99110-N9100 ',
], ],
}, },
@ -1636,6 +1642,7 @@ FW_VERSIONS = {
], ],
(Ecu.fwdRadar, 0x7d0, None): [ (Ecu.fwdRadar, 0x7d0, None): [
b'\xf1\x00MQ4_ SCC F-CUP 1.00 1.06 99110-P2000 ', 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.06 99110-P2000 ',
b'\xf1\x00MQ4_ SCC FHCUP 1.00 1.08 99110-P2000 ', b'\xf1\x00MQ4_ SCC FHCUP 1.00 1.08 99110-P2000 ',
], ],

@ -11,7 +11,6 @@ from openpilot.selfdrive.car.interfaces import CarInterfaceBase
from openpilot.selfdrive.car.disable_ecu import disable_ecu from openpilot.selfdrive.car.disable_ecu import disable_ecu
Ecu = car.CarParams.Ecu Ecu = car.CarParams.Ecu
SafetyModel = car.CarParams.SafetyModel
ButtonType = car.CarState.ButtonEvent.Type ButtonType = car.CarState.ButtonEvent.Type
EventName = car.CarEvent.EventName EventName = car.CarEvent.EventName
ENABLE_BUTTONS = (Buttons.RES_ACCEL, Buttons.SET_DECEL, Buttons.CANCEL) ENABLE_BUTTONS = (Buttons.RES_ACCEL, Buttons.SET_DECEL, Buttons.CANCEL)
@ -19,17 +18,6 @@ BUTTONS_DICT = {Buttons.RES_ACCEL: ButtonType.accelCruise, Buttons.SET_DECEL: Bu
Buttons.GAP_DIST: ButtonType.gapAdjustCruise, Buttons.CANCEL: ButtonType.cancel} Buttons.GAP_DIST: ButtonType.gapAdjustCruise, Buttons.CANCEL: ButtonType.cancel}
def set_safety_config_hyundai(candidate, CAN, can_fd=False):
platform = SafetyModel.hyundaiCanfd if can_fd else \
SafetyModel.hyundaiLegacy if candidate in LEGACY_SAFETY_MODE_CAR else \
SafetyModel.hyundai
cfgs = [get_safety_config(platform), ]
if CAN.ECAN >= 4:
cfgs.insert(0, get_safety_config(SafetyModel.noOutput))
return cfgs
class CarInterface(CarInterfaceBase): class CarInterface(CarInterfaceBase):
@staticmethod @staticmethod
def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs): def _get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs):
@ -54,6 +42,7 @@ class CarInterface(CarInterfaceBase):
# detect HDA2 with ADAS Driving ECU # detect HDA2 with ADAS Driving ECU
if hda2: if hda2:
ret.flags |= HyundaiFlags.CANFD_HDA2.value
if 0x110 in fingerprint[CAN.CAM]: if 0x110 in fingerprint[CAN.CAM]:
ret.flags |= HyundaiFlags.CANFD_HDA2_ALT_STEERING.value ret.flags |= HyundaiFlags.CANFD_HDA2_ALT_STEERING.value
else: else:
@ -303,20 +292,30 @@ class CarInterface(CarInterfaceBase):
ret.enableBsm = 0x58b in fingerprint[0] ret.enableBsm = 0x58b in fingerprint[0]
# *** panda safety config *** # *** panda safety config ***
ret.safetyConfigs = set_safety_config_hyundai(candidate, CAN, can_fd=(candidate in CANFD_CAR))
if hda2:
ret.flags |= HyundaiFlags.CANFD_HDA2.value
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2
if candidate in CANFD_CAR: if candidate in CANFD_CAR:
if hda2 and ret.flags & HyundaiFlags.CANFD_HDA2_ALT_STEERING: cfgs = [get_safety_config(car.CarParams.SafetyModel.hyundaiCanfd), ]
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CANFD_HDA2_ALT_STEERING if CAN.ECAN >= 4:
cfgs.insert(0, get_safety_config(car.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: if ret.flags & HyundaiFlags.CANFD_ALT_BUTTONS:
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_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(car.CarParams.SafetyModel.hyundaiLegacy)]
else:
ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.hyundai, 0)]
if candidate in CAMERA_SCC_CAR:
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC
if ret.flags & HyundaiFlags.CANFD_CAMERA_SCC or candidate in CAMERA_SCC_CAR:
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_CAMERA_SCC
if ret.openpilotLongitudinalControl: if ret.openpilotLongitudinalControl:
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_LONG ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_HYUNDAI_LONG
if ret.flags & HyundaiFlags.HYBRID: if ret.flags & HyundaiFlags.HYBRID:

@ -1,7 +1,6 @@
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, IntFlag, StrEnum from enum import Enum, IntFlag, StrEnum
from typing import Dict, List, Optional, Set, Tuple, Union
from cereal import car from cereal import car
from panda.python import uds from panda.python import uds
@ -157,7 +156,7 @@ class HyundaiCarInfo(CarInfo):
self.footnotes.insert(0, Footnote.CANFD) self.footnotes.insert(0, Footnote.CANFD)
CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { CAR_INFO: dict[str, HyundaiCarInfo | list[HyundaiCarInfo] | None] = {
CAR.AZERA_6TH_GEN: HyundaiCarInfo("Hyundai Azera 2022", "All", car_parts=CarParts.common([CarHarness.hyundai_k])), CAR.AZERA_6TH_GEN: HyundaiCarInfo("Hyundai Azera 2022", "All", car_parts=CarParts.common([CarHarness.hyundai_k])),
CAR.AZERA_HEV_6TH_GEN: [ CAR.AZERA_HEV_6TH_GEN: [
HyundaiCarInfo("Hyundai Azera Hybrid 2019", "All", car_parts=CarParts.common([CarHarness.hyundai_c])), HyundaiCarInfo("Hyundai Azera Hybrid 2019", "All", car_parts=CarParts.common([CarHarness.hyundai_c])),
@ -316,7 +315,7 @@ class Buttons:
CANCEL = 4 # on newer models, this is a pause/resume button CANCEL = 4 # on newer models, this is a pause/resume button
def get_platform_codes(fw_versions: List[bytes]) -> Set[Tuple[bytes, Optional[bytes]]]: def get_platform_codes(fw_versions: list[bytes]) -> set[tuple[bytes, bytes | None]]:
# Returns unique, platform-specific identification codes for a set of versions # Returns unique, platform-specific identification codes for a set of versions
codes = set() # (code-Optional[part], date) codes = set() # (code-Optional[part], date)
for fw in fw_versions: for fw in fw_versions:
@ -335,12 +334,12 @@ def get_platform_codes(fw_versions: List[bytes]) -> Set[Tuple[bytes, Optional[by
return codes return codes
def match_fw_to_car_fuzzy(live_fw_versions, offline_fw_versions) -> Set[str]: def match_fw_to_car_fuzzy(live_fw_versions, offline_fw_versions) -> set[str]:
# Non-electric CAN FD platforms often do not have platform code specifiers needed # 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 # to distinguish between hybrid and ICE. All EVs so far are either exclusively
# electric or specify electric in the platform code. # electric or specify electric in the platform code.
fuzzy_platform_blacklist = {str(c) for c in (CANFD_CAR - EV_CAR - CANFD_FUZZY_WHITELIST)} fuzzy_platform_blacklist = {str(c) for c in (CANFD_CAR - EV_CAR - CANFD_FUZZY_WHITELIST)}
candidates: Set[str] = set() candidates: set[str] = set()
for candidate, fws in offline_fw_versions.items(): for candidate, fws in offline_fw_versions.items():
# Keep track of ECUs which pass all checks (platform codes, within date range) # Keep track of ECUs which pass all checks (platform codes, within date range)
@ -396,6 +395,9 @@ HYUNDAI_VERSION_REQUEST_MULTI = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]
p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION) + \ p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION) + \
p16(0xf100) p16(0xf100)
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]) 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 # Regex patterns for parsing platform code, FW date, and part number from FW versions
@ -414,6 +416,8 @@ PLATFORM_CODE_ECUS = [Ecu.fwdRadar, Ecu.fwdCamera, Ecu.eps]
# TODO: there are date codes in the ABS firmware versions in hex # TODO: there are date codes in the ABS firmware versions in hex
DATE_FW_ECUS = [Ecu.fwdCamera] DATE_FW_ECUS = [Ecu.fwdCamera]
ALL_HYUNDAI_ECUS = [Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.engine, Ecu.parkingAdas, Ecu.transmission, Ecu.adas, Ecu.hvac, Ecu.cornerRadar]
FW_QUERY_CONFIG = FwQueryConfig( FW_QUERY_CONFIG = FwQueryConfig(
requests=[ requests=[
# TODO: minimize shared whitelists for CAN and cornerRadar for CAN-FD # TODO: minimize shared whitelists for CAN and cornerRadar for CAN-FD
@ -447,13 +451,52 @@ FW_QUERY_CONFIG = FwQueryConfig(
obd_multiplexing=False, obd_multiplexing=False,
), ),
# CAN-FD debugging queries # 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],
whitelist_ecus=[Ecu.fwdCamera],
bus=0,
auxiliary=True,
logging=True,
),
# CAN & CAN FD logging queries (from camera)
Request(
[HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=ALL_HYUNDAI_ECUS,
bus=0,
auxiliary=True,
logging=True,
),
Request(
[HYUNDAI_VERSION_REQUEST_MULTI],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=ALL_HYUNDAI_ECUS,
bus=0,
auxiliary=True,
logging=True,
),
Request(
[HYUNDAI_VERSION_REQUEST_LONG],
[HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=ALL_HYUNDAI_ECUS,
bus=1,
auxiliary=True,
obd_multiplexing=False,
logging=True,
),
# CAN-FD alt request logging queries
Request( Request(
[HYUNDAI_VERSION_REQUEST_ALT], [HYUNDAI_VERSION_REQUEST_ALT],
[HYUNDAI_VERSION_RESPONSE], [HYUNDAI_VERSION_RESPONSE],
whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac], whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac],
bus=0, bus=0,
auxiliary=True, auxiliary=True,
logging=True,
), ),
Request( Request(
[HYUNDAI_VERSION_REQUEST_ALT], [HYUNDAI_VERSION_REQUEST_ALT],
@ -461,6 +504,7 @@ FW_QUERY_CONFIG = FwQueryConfig(
whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac], whitelist_ecus=[Ecu.parkingAdas, Ecu.hvac],
bus=1, bus=1,
auxiliary=True, auxiliary=True,
logging=True,
obd_multiplexing=False, obd_multiplexing=False,
), ),
], ],

@ -1,11 +1,11 @@
import json import json
import os import os
import time
import numpy as np import numpy as np
import tomllib import tomllib
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from enum import StrEnum from enum import StrEnum
from typing import Any, Dict, Optional, Tuple, List, Callable, NamedTuple from typing import Any, NamedTuple, cast
from collections.abc import Callable
from cereal import car from cereal import car
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
@ -13,7 +13,7 @@ from openpilot.common.conversions import Conversions as CV
from openpilot.common.simple_kalman import KF1D, get_kalman_gain from openpilot.common.simple_kalman import KF1D, get_kalman_gain
from openpilot.common.numpy_fast import clip from openpilot.common.numpy_fast import clip
from openpilot.common.realtime import DT_CTRL from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car import apply_hysteresis, gen_empty_fingerprint, scale_rot_inertia, scale_tire_stiffness, STD_CARGO_KG from openpilot.selfdrive.car import PlatformConfig, apply_hysteresis, gen_empty_fingerprint, scale_rot_inertia, scale_tire_stiffness, STD_CARGO_KG
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, get_friction from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, get_friction
from openpilot.selfdrive.controls.lib.events import Events from openpilot.selfdrive.controls.lib.events import Events
from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel
@ -108,8 +108,16 @@ class CarInterfaceBase(ABC):
return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False) return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False)
@classmethod @classmethod
def get_params(cls, candidate: str, fingerprint: Dict[int, Dict[int, int]], car_fw: List[car.CarParams.CarFw], experimental_long: bool, docs: bool): def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[car.CarParams.CarFw], experimental_long: bool, docs: bool):
ret = CarInterfaceBase.get_std_params(candidate) ret = CarInterfaceBase.get_std_params(candidate)
if hasattr(candidate, "config"):
platform_config = cast(PlatformConfig, candidate.config)
if platform_config.specs is not None:
ret.mass = platform_config.specs.mass
ret.wheelbase = platform_config.specs.wheelbase
ret.steerRatio = platform_config.specs.steerRatio
ret = cls._get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs) 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 # Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload
@ -124,8 +132,8 @@ class CarInterfaceBase(ABC):
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def _get_params(ret: car.CarParams, candidate: str, fingerprint: Dict[int, Dict[int, int]], def _get_params(ret: car.CarParams, candidate: str, fingerprint: dict[int, dict[int, int]],
car_fw: List[car.CarParams.CarFw], experimental_long: bool, docs: bool): car_fw: list[car.CarParams.CarFw], experimental_long: bool, docs: bool):
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
@ -205,7 +213,7 @@ class CarInterfaceBase(ABC):
def _update(self, c: car.CarControl) -> car.CarState: def _update(self, c: car.CarControl) -> car.CarState:
pass pass
def update(self, c: car.CarControl, can_strings: List[bytes]) -> car.CarState: def update(self, c: car.CarControl, can_strings: list[bytes]) -> car.CarState:
# parse can # parse can
for cp in self.can_parsers: for cp in self.can_parsers:
if cp is not None: if cp is not None:
@ -239,7 +247,7 @@ class CarInterfaceBase(ABC):
return reader return reader
@abstractmethod @abstractmethod
def apply(self, c: car.CarControl, now_nanos: int) -> Tuple[car.CarControl.Actuators, List[bytes]]: def apply(self, c: car.CarControl, now_nanos: int) -> tuple[car.CarControl.Actuators, list[bytes]]:
pass pass
def create_common_events(self, cs_out, extra_gears=None, pcm_enable=True, allow_enable=True, def create_common_events(self, cs_out, extra_gears=None, pcm_enable=True, allow_enable=True,
@ -322,13 +330,14 @@ class RadarInterfaceBase(ABC):
self.pts = {} self.pts = {}
self.delay = 0 self.delay = 0
self.radar_ts = CP.radarTimeStep self.radar_ts = CP.radarTimeStep
self.frame = 0
self.no_radar_sleep = 'NO_RADAR_SLEEP' in os.environ self.no_radar_sleep = 'NO_RADAR_SLEEP' in os.environ
def update(self, can_strings): def update(self, can_strings):
ret = car.RadarData.new_message() self.frame += 1
if not self.no_radar_sleep: if (self.frame % int(100 * self.radar_ts)) == 0:
time.sleep(self.radar_ts) # radard runs on RI updates return car.RadarData.new_message()
return ret return None
class CarStateBase(ABC): class CarStateBase(ABC):
@ -409,11 +418,11 @@ class CarStateBase(ABC):
return bool(left_blinker_stalk or self.left_blinker_cnt > 0), bool(right_blinker_stalk or self.right_blinker_cnt > 0) return bool(left_blinker_stalk or self.left_blinker_cnt > 0), bool(right_blinker_stalk or self.right_blinker_cnt > 0)
@staticmethod @staticmethod
def parse_gear_shifter(gear: Optional[str]) -> car.CarState.GearShifter: def parse_gear_shifter(gear: str | None) -> car.CarState.GearShifter:
if gear is None: if gear is None:
return GearShifter.unknown return GearShifter.unknown
d: Dict[str, car.CarState.GearShifter] = { d: dict[str, car.CarState.GearShifter] = {
'P': GearShifter.park, 'PARK': GearShifter.park, 'P': GearShifter.park, 'PARK': GearShifter.park,
'R': GearShifter.reverse, 'REVERSE': GearShifter.reverse, 'R': GearShifter.reverse, 'REVERSE': GearShifter.reverse,
'N': GearShifter.neutral, 'NEUTRAL': GearShifter.neutral, 'N': GearShifter.neutral, 'NEUTRAL': GearShifter.neutral,
@ -450,7 +459,7 @@ INTERFACE_ATTR_FILE = {
# interface-specific helpers # interface-specific helpers
def get_interface_attr(attr: str, combine_brands: bool = False, ignore_none: bool = False) -> Dict[str | StrEnum, Any]: def get_interface_attr(attr: str, combine_brands: bool = False, ignore_none: bool = False) -> dict[str | StrEnum, Any]:
# read all the folders in selfdrive/car and return a dict where: # read all the folders in selfdrive/car and return a dict where:
# - keys are all the car models or brand names # - keys are all the car models or brand names
# - values are attr values from all car folders # - values are attr values from all car folders
@ -483,7 +492,7 @@ class NanoFFModel:
self.load_weights(platform) self.load_weights(platform)
def load_weights(self, platform: str): def load_weights(self, platform: str):
with open(self.weights_loc, 'r') as fob: with open(self.weights_loc) as fob:
self.weights = {k: np.array(v) for k, v in json.load(fob)[platform].items()} self.weights = {k: np.array(v) for k, v in json.load(fob)[platform].items()}
def relu(self, x: np.ndarray): def relu(self, x: np.ndarray):
@ -498,7 +507,7 @@ class NanoFFModel:
x = np.dot(x, self.weights['w_4']) + self.weights['b_4'] x = np.dot(x, self.weights['w_4']) + self.weights['b_4']
return x return x
def predict(self, x: List[float], do_sample: bool = False): def predict(self, x: list[float], do_sample: bool = False):
x = self.forward(np.array(x)) x = self.forward(np.array(x))
if do_sample: if do_sample:
pred = np.random.laplace(x[0], np.exp(x[1]) / self.weights['temperature']) pred = np.random.laplace(x[0], np.exp(x[1]) / self.weights['temperature'])

@ -5,11 +5,14 @@ from functools import partial
import cereal.messaging as messaging import cereal.messaging as messaging
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp
from openpilot.selfdrive.car.fw_query_definitions import AddrType
from panda.python.uds import CanClient, IsoTpMessage, FUNCTIONAL_ADDRS, get_rx_addr_for_tx_addr from panda.python.uds import CanClient, IsoTpMessage, FUNCTIONAL_ADDRS, get_rx_addr_for_tx_addr
class IsoTpParallelQuery: class IsoTpParallelQuery:
def __init__(self, sendcan, logcan, bus, addrs, request, response, response_offset=0x8, functional_addrs=None, debug=False, response_pending_timeout=10): def __init__(self, sendcan: messaging.PubSocket, logcan: messaging.SubSocket, bus: int, addrs: list[int] | list[AddrType],
request: list[bytes], response: list[bytes], response_offset: int = 0x8,
functional_addrs: list[int] | None = None, debug: bool = False, response_pending_timeout: float = 10) -> None:
self.sendcan = sendcan self.sendcan = sendcan
self.logcan = logcan self.logcan = logcan
self.bus = bus self.bus = bus
@ -24,7 +27,7 @@ class IsoTpParallelQuery:
assert tx_addr not in FUNCTIONAL_ADDRS, f"Functional address should be defined in functional_addrs: {hex(tx_addr)}" assert tx_addr not in FUNCTIONAL_ADDRS, f"Functional address should be defined in functional_addrs: {hex(tx_addr)}"
self.msg_addrs = {tx_addr: get_rx_addr_for_tx_addr(tx_addr[0], rx_offset=response_offset) for tx_addr in real_addrs} self.msg_addrs = {tx_addr: get_rx_addr_for_tx_addr(tx_addr[0], rx_offset=response_offset) for tx_addr in real_addrs}
self.msg_buffer = defaultdict(list) self.msg_buffer: dict[int, list[tuple[int, int, bytes, int]]] = defaultdict(list)
def rx(self): def rx(self):
"""Drain can socket and sort messages into buffers based on address""" """Drain can socket and sort messages into buffers based on address"""
@ -63,7 +66,7 @@ class IsoTpParallelQuery:
messaging.drain_sock_raw(self.logcan) messaging.drain_sock_raw(self.logcan)
self.msg_buffer = defaultdict(list) self.msg_buffer = defaultdict(list)
def _create_isotp_msg(self, tx_addr, sub_addr, rx_addr): def _create_isotp_msg(self, tx_addr: int, sub_addr: int | None, rx_addr: int):
can_client = CanClient(self._can_tx, partial(self._can_rx, rx_addr, sub_addr=sub_addr), tx_addr, rx_addr, can_client = 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) self.bus, sub_addr=sub_addr, debug=self.debug)
@ -73,7 +76,7 @@ class IsoTpParallelQuery:
# as well as reduces chances we process messages from previous queries # as well as reduces chances we process messages from previous queries
return IsoTpMessage(can_client, timeout=0, separation_time=0.01, debug=self.debug, max_len=max_len) return IsoTpMessage(can_client, timeout=0, separation_time=0.01, debug=self.debug, max_len=max_len)
def get_data(self, timeout, total_timeout=60.): def get_data(self, timeout: float, total_timeout: float = 60.) -> dict[AddrType, bytes]:
self._drain_rx() self._drain_rx()
# Create message objects # Create message objects

@ -32,7 +32,7 @@ class CarState(CarStateBase):
# Match panda speed reading # Match panda speed reading
speed_kph = cp.vl["ENGINE_DATA"]["SPEED"] speed_kph = cp.vl["ENGINE_DATA"]["SPEED"]
ret.standstill = speed_kph < .1 ret.standstill = speed_kph <= .1
can_gear = int(cp.vl["GEAR"]["GEAR"]) can_gear = int(cp.vl["GEAR"]["GEAR"])
ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None)) ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None))

@ -1,6 +1,5 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import StrEnum from enum import StrEnum
from typing import Dict, List, Union
from cereal import car from cereal import car
from openpilot.selfdrive.car import dbc_dict from openpilot.selfdrive.car import dbc_dict
@ -41,7 +40,7 @@ class MazdaCarInfo(CarInfo):
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.mazda])) car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.mazda]))
CAR_INFO: Dict[str, Union[MazdaCarInfo, List[MazdaCarInfo]]] = { CAR_INFO: dict[str, MazdaCarInfo | list[MazdaCarInfo]] = {
CAR.CX5: MazdaCarInfo("Mazda CX-5 2017-21"), CAR.CX5: MazdaCarInfo("Mazda CX-5 2017-21"),
CAR.CX9: MazdaCarInfo("Mazda CX-9 2016-20"), CAR.CX9: MazdaCarInfo("Mazda CX-9 2016-20"),
CAR.MAZDA3: MazdaCarInfo("Mazda 3 2017-18"), CAR.MAZDA3: MazdaCarInfo("Mazda 3 2017-18"),

@ -22,7 +22,7 @@ class CarInterface(CarInterfaceBase):
def _update(self, c): def _update(self, c):
self.sm.update(0) self.sm.update(0)
gps_sock = 'gpsLocationExternal' if self.sm.rcv_frame['gpsLocationExternal'] > 1 else 'gpsLocation' gps_sock = 'gpsLocationExternal' if self.sm.recv_frame['gpsLocationExternal'] > 1 else 'gpsLocation'
ret = car.CarState.new_message() ret = car.CarState.new_message()
ret.vEgo = self.sm[gps_sock].speed ret.vEgo = self.sm[gps_sock].speed

@ -1,5 +1,4 @@
from enum import StrEnum from enum import StrEnum
from typing import Dict, List, Optional, Union
from openpilot.selfdrive.car.docs_definitions import CarInfo from openpilot.selfdrive.car.docs_definitions import CarInfo
@ -8,6 +7,6 @@ class CAR(StrEnum):
MOCK = 'mock' MOCK = 'mock'
CAR_INFO: Dict[str, Optional[Union[CarInfo, List[CarInfo]]]] = { CAR_INFO: dict[str, CarInfo | list[CarInfo] | None] = {
CAR.MOCK: None, CAR.MOCK: None,
} }

@ -45,15 +45,30 @@ FW_VERSIONS = {
}, },
CAR.LEAF: { CAR.LEAF: {
(Ecu.abs, 0x740, None): [ (Ecu.abs, 0x740, None): [
b'476605SA1C',
b'476605SA7D',
b'476605SC2D',
b'476606WK7B',
b'476606WK9B', b'476606WK9B',
], ],
(Ecu.eps, 0x742, None): [ (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', b'5SN2A\xb7A\x05\x02N126F\x15\xb2\x00\x00\x00\x00\x00\x00\x00\x80',
], ],
(Ecu.fwdCamera, 0x707, None): [ (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', b'6WK2CDB\x04\x18\x00\x00\x00\x00\x00R=1\x18\x99\x10\x00\x00\x00\x80',
], ],
(Ecu.gateway, 0x18dad0f1, None): [ (Ecu.gateway, 0x18dad0f1, None): [
b'284U25SA3C',
b'284U25SP0C',
b'284U25SP1C',
b'284U26WK0A',
b'284U26WK0C', b'284U26WK0C',
], ],
}, },

@ -1,6 +1,5 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import StrEnum from enum import StrEnum
from typing import Dict, List, Optional, Union
from cereal import car from cereal import car
from panda.python import uds from panda.python import uds
@ -37,7 +36,7 @@ class NissanCarInfo(CarInfo):
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.nissan_a])) car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.nissan_a]))
CAR_INFO: Dict[str, Optional[Union[NissanCarInfo, List[NissanCarInfo]]]] = { CAR_INFO: dict[str, NissanCarInfo | list[NissanCarInfo] | None] = {
CAR.XTRAIL: NissanCarInfo("Nissan X-Trail 2017"), CAR.XTRAIL: NissanCarInfo("Nissan X-Trail 2017"),
CAR.LEAF: NissanCarInfo("Nissan Leaf 2018-23", video_link="https://youtu.be/vaMbtAh_0cY"), CAR.LEAF: NissanCarInfo("Nissan Leaf 2018-23", video_link="https://youtu.be/vaMbtAh_0cY"),
CAR.LEAF_IC: None, # same platforms CAR.LEAF_IC: None, # same platforms

@ -509,17 +509,20 @@ FW_VERSIONS = {
(Ecu.fwdCamera, 0x787, None): [ (Ecu.fwdCamera, 0x787, None): [
b'\x04!\x01\x1eD\x07!\x00\x04,', b'\x04!\x01\x1eD\x07!\x00\x04,',
b'\x04!\x08\x01.\x07!\x08\x022', b'\x04!\x08\x01.\x07!\x08\x022',
b'\r!\x08\x017\n!\x08\x003',
], ],
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
b'\xd5"`0\x07', b'\xd5"`0\x07',
b'\xd5"a0\x07', b'\xd5"a0\x07',
b'\xf1"`q\x07', b'\xf1"`q\x07',
b'\xf1"aq\x07', b'\xf1"aq\x07',
b'\xfa"ap\x07',
], ],
(Ecu.transmission, 0x7e1, None): [ (Ecu.transmission, 0x7e1, None): [
b'\x1d\x86B0\x00', b'\x1d\x86B0\x00',
b'\x1d\xf6B0\x00', b'\x1d\xf6B0\x00',
b'\x1e\x86B0\x00', b'\x1e\x86B0\x00',
b'\x1e\x86F0\x00',
b'\x1e\xf6D0\x00', b'\x1e\xf6D0\x00',
], ],
}, },

@ -40,11 +40,10 @@ class CarInterface(CarInterfaceBase):
else: else:
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
ret.centerToFront = ret.wheelbase * 0.5
if candidate in (CAR.ASCENT, CAR.ASCENT_2023): if candidate in (CAR.ASCENT, CAR.ASCENT_2023):
ret.mass = 2031.
ret.wheelbase = 2.89
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 13.5
ret.steerActuatorDelay = 0.3 # end-to-end angle controller ret.steerActuatorDelay = 0.3 # end-to-end angle controller
ret.lateralTuning.init('pid') ret.lateralTuning.init('pid')
ret.lateralTuning.pid.kf = 0.00003 ret.lateralTuning.pid.kf = 0.00003
@ -52,10 +51,6 @@ class CarInterface(CarInterfaceBase):
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.0025, 0.1], [0.00025, 0.01]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.0025, 0.1], [0.00025, 0.01]]
elif candidate == CAR.IMPREZA: elif candidate == CAR.IMPREZA:
ret.mass = 1568.
ret.wheelbase = 2.67
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 15
ret.steerActuatorDelay = 0.4 # end-to-end angle controller ret.steerActuatorDelay = 0.4 # end-to-end angle controller
ret.lateralTuning.init('pid') ret.lateralTuning.init('pid')
ret.lateralTuning.pid.kf = 0.00005 ret.lateralTuning.pid.kf = 0.00005
@ -63,58 +58,31 @@ class CarInterface(CarInterfaceBase):
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2, 0.3], [0.02, 0.03]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2, 0.3], [0.02, 0.03]]
elif candidate == CAR.IMPREZA_2020: elif candidate == CAR.IMPREZA_2020:
ret.mass = 1480.
ret.wheelbase = 2.67
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 17 # learned, 14 stock
ret.lateralTuning.init('pid') ret.lateralTuning.init('pid')
ret.lateralTuning.pid.kf = 0.00005 ret.lateralTuning.pid.kf = 0.00005
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]] 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]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.045, 0.042, 0.20], [0.04, 0.035, 0.045]]
elif candidate == CAR.CROSSTREK_HYBRID: elif candidate == CAR.CROSSTREK_HYBRID:
ret.mass = 1668.
ret.wheelbase = 2.67
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 17
ret.steerActuatorDelay = 0.1 ret.steerActuatorDelay = 0.1
elif candidate in (CAR.FORESTER, CAR.FORESTER_2022, CAR.FORESTER_HYBRID): elif candidate in (CAR.FORESTER, CAR.FORESTER_2022, CAR.FORESTER_HYBRID):
ret.mass = 1568.
ret.wheelbase = 2.67
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 17 # learned, 14 stock
ret.lateralTuning.init('pid') ret.lateralTuning.init('pid')
ret.lateralTuning.pid.kf = 0.000038 ret.lateralTuning.pid.kf = 0.000038
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]] 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]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.01, 0.065, 0.2], [0.001, 0.015, 0.025]]
elif candidate in (CAR.OUTBACK, CAR.LEGACY, CAR.OUTBACK_2023): elif candidate in (CAR.OUTBACK, CAR.LEGACY, CAR.OUTBACK_2023):
ret.mass = 1568.
ret.wheelbase = 2.67
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 17
ret.steerActuatorDelay = 0.1 ret.steerActuatorDelay = 0.1
elif candidate in (CAR.FORESTER_PREGLOBAL, CAR.OUTBACK_PREGLOBAL_2018): elif candidate in (CAR.FORESTER_PREGLOBAL, CAR.OUTBACK_PREGLOBAL_2018):
ret.safetyConfigs[0].safetyParam = Panda.FLAG_SUBARU_PREGLOBAL_REVERSED_DRIVER_TORQUE # Outback 2018-2019 and Forester have reversed driver torque signal ret.safetyConfigs[0].safetyParam = Panda.FLAG_SUBARU_PREGLOBAL_REVERSED_DRIVER_TORQUE # Outback 2018-2019 and Forester have reversed driver torque signal
ret.mass = 1568
ret.wheelbase = 2.67
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 20 # learned, 14 stock
elif candidate == CAR.LEGACY_PREGLOBAL: elif candidate == CAR.LEGACY_PREGLOBAL:
ret.mass = 1568
ret.wheelbase = 2.67
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 12.5 # 14.5 stock
ret.steerActuatorDelay = 0.15 ret.steerActuatorDelay = 0.15
elif candidate == CAR.OUTBACK_PREGLOBAL: elif candidate == CAR.OUTBACK_PREGLOBAL:
ret.mass = 1568 pass
ret.wheelbase = 2.67
ret.centerToFront = ret.wheelbase * 0.5
ret.steerRatio = 20 # learned, 14 stock
else: else:
raise ValueError(f"unknown car: {candidate}") raise ValueError(f"unknown car: {candidate}")

@ -13,6 +13,15 @@ def create_steering_control(packer, apply_steer, steer_req):
return packer.make_can_msg("ES_LKAS", 0, values) 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): def create_steering_status(packer):
return packer.make_can_msg("ES_LKAS_State", 0, {}) return packer.make_can_msg("ES_LKAS_State", 0, {})

@ -1,10 +1,9 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, IntFlag, StrEnum from enum import Enum, IntFlag
from typing import Dict, List, Union
from cereal import car from cereal import car
from panda.python import uds from panda.python import uds
from openpilot.selfdrive.car import dbc_dict from openpilot.selfdrive.car import CarSpecs, DbcDict, PlatformConfig, Platforms, dbc_dict
from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarInfo, CarParts, Tool, Column from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarInfo, CarParts, Tool, Column
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16 from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16
@ -68,27 +67,6 @@ class CanBus:
camera = 2 camera = 2
class CAR(StrEnum):
# Global platform
ASCENT = "SUBARU ASCENT LIMITED 2019"
ASCENT_2023 = "SUBARU ASCENT 2023"
IMPREZA = "SUBARU IMPREZA LIMITED 2019"
IMPREZA_2020 = "SUBARU IMPREZA SPORT 2020"
FORESTER = "SUBARU FORESTER 2019"
OUTBACK = "SUBARU OUTBACK 6TH GEN"
CROSSTREK_HYBRID = "SUBARU CROSSTREK HYBRID 2020"
FORESTER_HYBRID = "SUBARU FORESTER HYBRID 2020"
LEGACY = "SUBARU LEGACY 7TH GEN"
FORESTER_2022 = "SUBARU FORESTER 2022"
OUTBACK_2023 = "SUBARU OUTBACK 7TH GEN"
# Pre-global
FORESTER_PREGLOBAL = "SUBARU FORESTER 2017 - 2018"
LEGACY_PREGLOBAL = "SUBARU LEGACY 2015 - 2018"
OUTBACK_PREGLOBAL = "SUBARU OUTBACK 2015 - 2017"
OUTBACK_PREGLOBAL_2018 = "SUBARU OUTBACK 2018 - 2019"
class Footnote(Enum): class Footnote(Enum):
GLOBAL = CarFootnote( GLOBAL = CarFootnote(
"In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.", "In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.",
@ -102,7 +80,7 @@ class Footnote(Enum):
class SubaruCarInfo(CarInfo): class SubaruCarInfo(CarInfo):
package: str = "EyeSight Driver Assistance" package: str = "EyeSight Driver Assistance"
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.subaru_a])) car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.subaru_a]))
footnotes: List[Enum] = field(default_factory=lambda: [Footnote.GLOBAL]) footnotes: list[Enum] = field(default_factory=lambda: [Footnote.GLOBAL])
def init_make(self, CP: car.CarParams): def init_make(self, CP: car.CarParams):
self.car_parts.parts.extend([Tool.socket_8mm_deep, Tool.pry_tool]) self.car_parts.parts.extend([Tool.socket_8mm_deep, Tool.pry_tool])
@ -110,32 +88,107 @@ class SubaruCarInfo(CarInfo):
if CP.experimentalLongitudinalAvailable: if CP.experimentalLongitudinalAvailable:
self.footnotes.append(Footnote.EXP_LONG) self.footnotes.append(Footnote.EXP_LONG)
CAR_INFO: Dict[str, Union[SubaruCarInfo, List[SubaruCarInfo]]] = {
CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-21", "All"), @dataclass
CAR.OUTBACK: SubaruCarInfo("Subaru Outback 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])), class SubaruPlatformConfig(PlatformConfig):
CAR.LEGACY: SubaruCarInfo("Subaru Legacy 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])), dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('subaru_global_2017_generated', None))
CAR.IMPREZA: [
SubaruCarInfo("Subaru Impreza 2017-19"),
SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), class CAR(Platforms):
SubaruCarInfo("Subaru XV 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), # Global platform
], ASCENT = SubaruPlatformConfig(
CAR.IMPREZA_2020: [ "SUBARU ASCENT LIMITED 2019",
SubaruCarInfo("Subaru Impreza 2020-22"), SubaruCarInfo("Subaru Ascent 2019-21", "All"),
SubaruCarInfo("Subaru Crosstrek 2020-23"), specs=CarSpecs(mass=2031, wheelbase=2.89, steerRatio=13.5),
SubaruCarInfo("Subaru XV 2020-21"), )
], OUTBACK = SubaruPlatformConfig(
"SUBARU OUTBACK 6TH GEN",
SubaruCarInfo("Subaru Outback 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])),
specs=CarSpecs(mass=1568, wheelbase=2.67, steerRatio=17),
)
LEGACY = SubaruPlatformConfig(
"SUBARU LEGACY 7TH GEN",
SubaruCarInfo("Subaru Legacy 2020-22", "All", car_parts=CarParts.common([CarHarness.subaru_b])),
specs=OUTBACK.specs,
)
IMPREZA = SubaruPlatformConfig(
"SUBARU IMPREZA LIMITED 2019",
[
SubaruCarInfo("Subaru Impreza 2017-19"),
SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"),
SubaruCarInfo("Subaru XV 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"),
],
specs=CarSpecs(mass=1568, wheelbase=2.67, steerRatio=15),
)
IMPREZA_2020 = SubaruPlatformConfig(
"SUBARU IMPREZA SPORT 2020",
[
SubaruCarInfo("Subaru Impreza 2020-22"),
SubaruCarInfo("Subaru Crosstrek 2020-23"),
SubaruCarInfo("Subaru XV 2020-21"),
],
specs=CarSpecs(mass=1480, wheelbase=2.67, steerRatio=17),
)
# TODO: is there an XV and Impreza too? # TODO: is there an XV and Impreza too?
CAR.CROSSTREK_HYBRID: SubaruCarInfo("Subaru Crosstrek Hybrid 2020", car_parts=CarParts.common([CarHarness.subaru_b])), CROSSTREK_HYBRID = SubaruPlatformConfig(
CAR.FORESTER_HYBRID: SubaruCarInfo("Subaru Forester Hybrid 2020"), "SUBARU CROSSTREK HYBRID 2020",
CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-21", "All"), SubaruCarInfo("Subaru Crosstrek Hybrid 2020", car_parts=CarParts.common([CarHarness.subaru_b])),
CAR.FORESTER_PREGLOBAL: SubaruCarInfo("Subaru Forester 2017-18"), dbc_dict('subaru_global_2020_hybrid_generated', None),
CAR.LEGACY_PREGLOBAL: SubaruCarInfo("Subaru Legacy 2015-18"), specs=CarSpecs(mass=1668, wheelbase=2.67, steerRatio=17),
CAR.OUTBACK_PREGLOBAL: SubaruCarInfo("Subaru Outback 2015-17"), )
CAR.OUTBACK_PREGLOBAL_2018: SubaruCarInfo("Subaru Outback 2018-19"), FORESTER = SubaruPlatformConfig(
CAR.FORESTER_2022: SubaruCarInfo("Subaru Forester 2022-23", "All", car_parts=CarParts.common([CarHarness.subaru_c])), "SUBARU FORESTER 2019",
CAR.OUTBACK_2023: SubaruCarInfo("Subaru Outback 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d])), SubaruCarInfo("Subaru Forester 2019-21", "All"),
CAR.ASCENT_2023: SubaruCarInfo("Subaru Ascent 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d])), specs=CarSpecs(mass=1668, wheelbase=2.67, steerRatio=17),
} )
FORESTER_HYBRID = SubaruPlatformConfig(
"SUBARU FORESTER HYBRID 2020",
SubaruCarInfo("Subaru Forester Hybrid 2020"),
dbc_dict('subaru_global_2020_hybrid_generated', None),
specs=FORESTER.specs,
)
# Pre-global
FORESTER_PREGLOBAL = SubaruPlatformConfig(
"SUBARU FORESTER 2017 - 2018",
SubaruCarInfo("Subaru Forester 2017-18"),
dbc_dict('subaru_forester_2017_generated', None),
specs=CarSpecs(mass=1568, wheelbase=2.67, steerRatio=20),
)
LEGACY_PREGLOBAL = SubaruPlatformConfig(
"SUBARU LEGACY 2015 - 2018",
SubaruCarInfo("Subaru Legacy 2015-18"),
dbc_dict('subaru_outback_2015_generated', None),
specs=CarSpecs(mass=1568, wheelbase=2.67, steerRatio=12.5),
)
OUTBACK_PREGLOBAL = SubaruPlatformConfig(
"SUBARU OUTBACK 2015 - 2017",
SubaruCarInfo("Subaru Outback 2015-17"),
dbc_dict('subaru_outback_2015_generated', None),
specs=FORESTER_PREGLOBAL.specs,
)
OUTBACK_PREGLOBAL_2018 = SubaruPlatformConfig(
"SUBARU OUTBACK 2018 - 2019",
SubaruCarInfo("Subaru Outback 2018-19"),
dbc_dict('subaru_outback_2019_generated', None),
specs=FORESTER_PREGLOBAL.specs,
)
# Angle LKAS
FORESTER_2022 = SubaruPlatformConfig(
"SUBARU FORESTER 2022",
SubaruCarInfo("Subaru Forester 2022-24", "All", car_parts=CarParts.common([CarHarness.subaru_c])),
specs=FORESTER.specs,
)
OUTBACK_2023 = SubaruPlatformConfig(
"SUBARU OUTBACK 7TH GEN",
SubaruCarInfo("Subaru Outback 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d])),
specs=OUTBACK.specs,
)
ASCENT_2023 = SubaruPlatformConfig(
"SUBARU ASCENT 2023",
SubaruCarInfo("Subaru Ascent 2023", "All", car_parts=CarParts.common([CarHarness.subaru_d])),
specs=ASCENT.specs,
)
LKAS_ANGLE = {CAR.FORESTER_2022, CAR.OUTBACK_2023, CAR.ASCENT_2023} LKAS_ANGLE = {CAR.FORESTER_2022, CAR.OUTBACK_2023, CAR.ASCENT_2023}
GLOBAL_GEN2 = {CAR.OUTBACK, CAR.LEGACY, CAR.OUTBACK_2023, CAR.ASCENT_2023} GLOBAL_GEN2 = {CAR.OUTBACK, CAR.LEGACY, CAR.OUTBACK_2023, CAR.ASCENT_2023}
@ -186,20 +239,5 @@ FW_QUERY_CONFIG = FwQueryConfig(
} }
) )
DBC = { CAR_INFO = CAR.create_carinfo_map()
CAR.ASCENT: dbc_dict('subaru_global_2017_generated', None), DBC = CAR.create_dbc_map()
CAR.ASCENT_2023: dbc_dict('subaru_global_2017_generated', None),
CAR.IMPREZA: dbc_dict('subaru_global_2017_generated', None),
CAR.IMPREZA_2020: dbc_dict('subaru_global_2017_generated', None),
CAR.FORESTER: dbc_dict('subaru_global_2017_generated', None),
CAR.FORESTER_2022: dbc_dict('subaru_global_2017_generated', None),
CAR.OUTBACK: dbc_dict('subaru_global_2017_generated', None),
CAR.FORESTER_HYBRID: dbc_dict('subaru_global_2020_hybrid_generated', None),
CAR.CROSSTREK_HYBRID: dbc_dict('subaru_global_2020_hybrid_generated', None),
CAR.OUTBACK_2023: dbc_dict('subaru_global_2017_generated', None),
CAR.LEGACY: dbc_dict('subaru_global_2017_generated', None),
CAR.FORESTER_PREGLOBAL: dbc_dict('subaru_forester_2017_generated', None),
CAR.LEGACY_PREGLOBAL: dbc_dict('subaru_outback_2015_generated', None),
CAR.OUTBACK_PREGLOBAL: dbc_dict('subaru_outback_2015_generated', None),
CAR.OUTBACK_PREGLOBAL_2018: dbc_dict('subaru_outback_2019_generated', None),
}

@ -1,6 +1,5 @@
from collections import namedtuple from collections import namedtuple
from enum import StrEnum from enum import StrEnum
from typing import Dict, List, Union
from cereal import car from cereal import car
from openpilot.selfdrive.car import AngleRateLimit, dbc_dict from openpilot.selfdrive.car import AngleRateLimit, dbc_dict
@ -17,7 +16,7 @@ class CAR(StrEnum):
AP2_MODELS = 'TESLA AP2 MODEL S' AP2_MODELS = 'TESLA AP2 MODEL S'
CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = { CAR_INFO: dict[str, CarInfo | list[CarInfo]] = {
CAR.AP1_MODELS: CarInfo("Tesla AP1 Model S", "All"), CAR.AP1_MODELS: CarInfo("Tesla AP1 Model S", "All"),
CAR.AP2_MODELS: CarInfo("Tesla AP2 Model S", "All"), CAR.AP2_MODELS: CarInfo("Tesla AP2 Model S", "All"),
} }

@ -0,0 +1,12 @@
#!/bin/bash
SCRIPT_DIR=$(dirname "$0")
BASEDIR=$(realpath "$SCRIPT_DIR/../../../")
cd $BASEDIR
MAX_EXAMPLES=300
INTERNAL_SEG_CNT=300
FILEREADER_CACHE=1
INTERNAL_SEG_LIST=selfdrive/car/tests/test_models_segs.txt
cd selfdrive/car/tests && pytest test_models.py test_car_interfaces.py

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typing import NamedTuple, Optional from typing import NamedTuple
from openpilot.selfdrive.car.chrysler.values import CAR as CHRYSLER from openpilot.selfdrive.car.chrysler.values import CAR as CHRYSLER
from openpilot.selfdrive.car.gm.values import CAR as GM from openpilot.selfdrive.car.gm.values import CAR as GM
@ -20,7 +20,6 @@ non_tested_cars = [
GM.CADILLAC_ATS, GM.CADILLAC_ATS,
GM.HOLDEN_ASTRA, GM.HOLDEN_ASTRA,
GM.MALIBU, GM.MALIBU,
GM.EQUINOX,
HYUNDAI.GENESIS_G90, HYUNDAI.GENESIS_G90,
HONDA.ODYSSEY_CHN, HONDA.ODYSSEY_CHN,
VOLKSWAGEN.CRAFTER_MK2, # need a route from an ACC-equipped Crafter VOLKSWAGEN.CRAFTER_MK2, # need a route from an ACC-equipped Crafter
@ -30,8 +29,8 @@ non_tested_cars = [
class CarTestRoute(NamedTuple): class CarTestRoute(NamedTuple):
route: str route: str
car_model: Optional[str] car_model: str | None
segment: Optional[int] = None segment: int | None = None
routes = [ routes = [
@ -46,6 +45,7 @@ routes = [
CarTestRoute("3d84727705fecd04|2021-05-25--08-38-56", CHRYSLER.PACIFICA_2020), CarTestRoute("3d84727705fecd04|2021-05-25--08-38-56", CHRYSLER.PACIFICA_2020),
CarTestRoute("221c253375af4ee9|2022-06-15--18-38-24", CHRYSLER.RAM_1500), CarTestRoute("221c253375af4ee9|2022-06-15--18-38-24", CHRYSLER.RAM_1500),
CarTestRoute("8fb5eabf914632ae|2022-08-04--17-28-53", CHRYSLER.RAM_HD, segment=6), CarTestRoute("8fb5eabf914632ae|2022-08-04--17-28-53", CHRYSLER.RAM_HD, segment=6),
CarTestRoute("3379c85aeedc8285|2023-12-07--17-49-39", CHRYSLER.DODGE_DURANGO),
CarTestRoute("54827bf84c38b14f|2023-01-25--14-14-11", FORD.BRONCO_SPORT_MK1), CarTestRoute("54827bf84c38b14f|2023-01-25--14-14-11", FORD.BRONCO_SPORT_MK1),
CarTestRoute("f8eaaccd2a90aef8|2023-05-04--15-10-09", FORD.ESCAPE_MK4), CarTestRoute("f8eaaccd2a90aef8|2023-05-04--15-10-09", FORD.ESCAPE_MK4),
@ -59,6 +59,7 @@ routes = [
CarTestRoute("7cc2a8365b4dd8a9|2018-12-02--12-10-44", GM.ACADIA), CarTestRoute("7cc2a8365b4dd8a9|2018-12-02--12-10-44", GM.ACADIA),
CarTestRoute("aa20e335f61ba898|2019-02-05--16-59-04", GM.BUICK_REGAL), CarTestRoute("aa20e335f61ba898|2019-02-05--16-59-04", GM.BUICK_REGAL),
CarTestRoute("75a6bcb9b8b40373|2023-03-11--22-47-33", GM.BUICK_LACROSSE), CarTestRoute("75a6bcb9b8b40373|2023-03-11--22-47-33", GM.BUICK_LACROSSE),
CarTestRoute("e746f59bc96fd789|2024-01-31--22-25-58", GM.EQUINOX),
CarTestRoute("ef8f2185104d862e|2023-02-09--18-37-13", GM.ESCALADE), CarTestRoute("ef8f2185104d862e|2023-02-09--18-37-13", GM.ESCALADE),
CarTestRoute("46460f0da08e621e|2021-10-26--07-21-46", GM.ESCALADE_ESV), CarTestRoute("46460f0da08e621e|2021-10-26--07-21-46", GM.ESCALADE_ESV),
CarTestRoute("168f8b3be57f66ae|2023-09-12--21-44-42", GM.ESCALADE_ESV_2019), CarTestRoute("168f8b3be57f66ae|2023-09-12--21-44-42", GM.ESCALADE_ESV_2019),
@ -186,6 +187,7 @@ routes = [
CarTestRoute("5f5afb36036506e4|2019-05-14--02-09-54", TOYOTA.COROLLA_TSS2), CarTestRoute("5f5afb36036506e4|2019-05-14--02-09-54", TOYOTA.COROLLA_TSS2),
CarTestRoute("5ceff72287a5c86c|2019-10-19--10-59-02", TOYOTA.COROLLA_TSS2), # hybrid CarTestRoute("5ceff72287a5c86c|2019-10-19--10-59-02", TOYOTA.COROLLA_TSS2), # hybrid
CarTestRoute("d2525c22173da58b|2021-04-25--16-47-04", TOYOTA.PRIUS), CarTestRoute("d2525c22173da58b|2021-04-25--16-47-04", TOYOTA.PRIUS),
CarTestRoute("b14c5b4742e6fc85|2020-07-28--19-50-11", TOYOTA.RAV4),
CarTestRoute("32a7df20486b0f70|2020-02-06--16-06-50", TOYOTA.RAV4H), CarTestRoute("32a7df20486b0f70|2020-02-06--16-06-50", TOYOTA.RAV4H),
CarTestRoute("cdf2f7de565d40ae|2019-04-25--03-53-41", TOYOTA.RAV4_TSS2), CarTestRoute("cdf2f7de565d40ae|2019-04-25--03-53-41", TOYOTA.RAV4_TSS2),
CarTestRoute("a5c341bb250ca2f0|2022-05-18--16-05-17", TOYOTA.RAV4_TSS2_2022), CarTestRoute("a5c341bb250ca2f0|2022-05-18--16-05-17", TOYOTA.RAV4_TSS2_2022),
@ -207,6 +209,7 @@ routes = [
CarTestRoute("ec429c0f37564e3c|2020-02-01--17-28-12", TOYOTA.LEXUS_NX), # hybrid 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("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("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.HIGHLANDER_TSS2), CarTestRoute("0a302ffddbb3e3d3|2020-02-08--16-19-08", TOYOTA.HIGHLANDER_TSS2),
CarTestRoute("437e4d2402abf524|2021-05-25--07-58-50", TOYOTA.HIGHLANDER_TSS2), # hybrid CarTestRoute("437e4d2402abf524|2021-05-25--07-58-50", TOYOTA.HIGHLANDER_TSS2), # hybrid
CarTestRoute("3183cd9b021e89ce|2021-05-25--10-34-44", TOYOTA.HIGHLANDER), CarTestRoute("3183cd9b021e89ce|2021-05-25--10-34-44", TOYOTA.HIGHLANDER),
@ -221,7 +224,7 @@ routes = [
CarTestRoute("ea8fbe72b96a185c|2023-02-08--15-11-46", TOYOTA.CHR_TSS2), CarTestRoute("ea8fbe72b96a185c|2023-02-08--15-11-46", TOYOTA.CHR_TSS2),
CarTestRoute("ea8fbe72b96a185c|2023-02-22--09-20-34", TOYOTA.CHR_TSS2), # openpilot longitudinal, with smartDSU CarTestRoute("ea8fbe72b96a185c|2023-02-22--09-20-34", TOYOTA.CHR_TSS2), # openpilot longitudinal, with smartDSU
CarTestRoute("6719965b0e1d1737|2023-02-09--22-44-05", TOYOTA.CHR_TSS2), # hybrid CarTestRoute("6719965b0e1d1737|2023-02-09--22-44-05", TOYOTA.CHR_TSS2), # hybrid
# CarTestRoute("6719965b0e1d1737|2023-08-29--06-40-05", TOYOTA.CHR_TSS2), # hybrid, openpilot longitudinal, radar disabled CarTestRoute("6719965b0e1d1737|2023-08-29--06-40-05", TOYOTA.CHR_TSS2), # hybrid, openpilot longitudinal, radar disabled
CarTestRoute("14623aae37e549f3|2021-10-24--01-20-49", TOYOTA.PRIUS_V), CarTestRoute("14623aae37e549f3|2021-10-24--01-20-49", TOYOTA.PRIUS_V),
CarTestRoute("202c40641158a6e5|2021-09-21--09-43-24", VOLKSWAGEN.ARTEON_MK1), CarTestRoute("202c40641158a6e5|2021-09-21--09-43-24", VOLKSWAGEN.ARTEON_MK1),
@ -289,7 +292,6 @@ routes = [
# Segments that test specific issues # Segments that test specific issues
# Controls mismatch due to interceptor threshold # Controls mismatch due to interceptor threshold
CarTestRoute("cfb32f0fb91b173b|2022-04-06--14-54-45", HONDA.CIVIC, segment=21), CarTestRoute("cfb32f0fb91b173b|2022-04-06--14-54-45", HONDA.CIVIC, segment=21),
CarTestRoute("5a8762b91fc70467|2022-04-14--21-26-20", TOYOTA.RAV4, segment=2),
# Controls mismatch due to standstill threshold # Controls mismatch due to standstill threshold
CarTestRoute("bec2dcfde6a64235|2022-04-08--14-21-32", HONDA.CRV_HYBRID, segment=22), CarTestRoute("bec2dcfde6a64235|2022-04-08--14-21-32", HONDA.CRV_HYBRID, segment=22),
] ]

@ -46,11 +46,6 @@ def get_fuzzy_car_interface_args(draw: DrawType) -> dict:
class TestCarInterfaces(unittest.TestCase): class TestCarInterfaces(unittest.TestCase):
@classmethod
def setUpClass(cls):
os.environ['NO_RADAR_SLEEP'] = '1'
# FIXME: Due to the lists used in carParams, Phase.target is very slow and will cause # 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 # many generated examples to overrun when max_examples > ~20, don't use it
@parameterized.expand([(car,) for car in sorted(all_known_cars())]) @parameterized.expand([(car,) for car in sorted(all_known_cars())])
@ -79,6 +74,10 @@ class TestCarInterfaces(unittest.TestCase):
self.assertEqual(len(car_params.longitudinalTuning.kiV), len(car_params.longitudinalTuning.kiBP)) self.assertEqual(len(car_params.longitudinalTuning.kiV), len(car_params.longitudinalTuning.kiBP))
self.assertEqual(len(car_params.longitudinalTuning.deadzoneV), len(car_params.longitudinalTuning.deadzoneBP)) self.assertEqual(len(car_params.longitudinalTuning.deadzoneV), len(car_params.longitudinalTuning.deadzoneBP))
# If we're using the interceptor for gasPressed, we should be commanding gas with it
if car_params.enableGasInterceptor:
self.assertTrue(car_params.openpilotLongitudinalControl)
# Lateral sanity checks # Lateral sanity checks
if car_params.steerControlType != car.CarParams.SteerControlType.angle: if car_params.steerControlType != car.CarParams.SteerControlType.angle:
tune = car_params.lateralTuning tune = car_params.lateralTuning

@ -20,7 +20,7 @@ class TestCarDocs(unittest.TestCase):
def test_generator(self): def test_generator(self):
generated_cars_md = generate_cars_md(self.all_cars, CARS_MD_TEMPLATE) generated_cars_md = generate_cars_md(self.all_cars, CARS_MD_TEMPLATE)
with open(CARS_MD_OUT, "r") as f: with open(CARS_MD_OUT) as f:
current_cars_md = f.read() current_cars_md = f.read()
self.assertEqual(generated_cars_md, current_cars_md, self.assertEqual(generated_cars_md, current_cars_md,
@ -45,7 +45,7 @@ class TestCarDocs(unittest.TestCase):
all_car_info_platforms = get_interface_attr("CAR_INFO", combine_brands=True).keys() all_car_info_platforms = get_interface_attr("CAR_INFO", combine_brands=True).keys()
for platform in sorted(interfaces.keys()): for platform in sorted(interfaces.keys()):
with self.subTest(platform=platform): with self.subTest(platform=platform):
self.assertTrue(platform in all_car_info_platforms, "Platform: {} doesn't exist in CarInfo".format(platform)) self.assertTrue(platform in all_car_info_platforms, f"Platform: {platform} doesn't exist in CarInfo")
def test_naming_conventions(self): def test_naming_conventions(self):
# Asserts market-standard car naming conventions by brand # Asserts market-standard car naming conventions by brand

@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import sys import sys
from typing import Dict, List
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
@ -64,7 +63,7 @@ def check_can_ignition_conflicts(fingerprints, brands):
if __name__ == "__main__": if __name__ == "__main__":
fingerprints = _get_fingerprints() fingerprints = _get_fingerprints()
fingerprints_flat: List[Dict] = [] fingerprints_flat: list[dict] = []
car_names = [] car_names = []
brand_names = [] brand_names = []
for brand in fingerprints: for brand in fingerprints:

@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import pytest
import random import random
import time import time
import unittest import unittest
@ -34,18 +33,29 @@ class TestFwFingerprint(unittest.TestCase):
self.assertEqual(len(candidates), 1, f"got more than one candidate: {candidates}") self.assertEqual(len(candidates), 1, f"got more than one candidate: {candidates}")
self.assertEqual(candidates[0], expected) self.assertEqual(candidates[0], expected)
@parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e]) @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): def test_exact_match(self, brand, car_model, ecus, test_non_essential):
config = FW_QUERY_CONFIGS[brand]
CP = car.CarParams.new_message() CP = car.CarParams.new_message()
for _ in range(200): for _ in range(100):
fw = [] fw = []
for ecu, fw_versions in ecus.items(): 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 ecu_name, addr, sub_addr = ecu
fw.append({"ecu": ecu_name, "fwVersion": random.choice(fw_versions), 'brand': brand, fw.append({"ecu": ecu_name, "fwVersion": random.choice(fw_versions), 'brand': brand,
"address": addr, "subAddress": 0 if sub_addr is None else sub_addr}) "address": addr, "subAddress": 0 if sub_addr is None else sub_addr})
CP.carFw = fw CP.carFw = fw
_, matches = match_fw_to_car(CP.carFw, allow_fuzzy=False) _, matches = match_fw_to_car(CP.carFw, allow_fuzzy=False)
self.assertFingerprints(matches, car_model) 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]) @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): def test_custom_fuzzy_match(self, brand, car_model, ecus):
@ -226,7 +236,7 @@ class TestFwFingerprintTiming(unittest.TestCase):
def test_startup_timing(self): def test_startup_timing(self):
# Tests worse-case VIN query time and typical present ECU query time # Tests worse-case VIN query time and typical present ECU query time
vin_ref_times = {'worst': 1.5, 'best': 0.5} # best assumes we go through all queries to get a match vin_ref_times = {'worst': 1.2, 'best': 0.6} # best assumes we go through all queries to get a match
present_ecu_ref_time = 0.75 present_ecu_ref_time = 0.75
def fake_get_ecu_addrs(*_, timeout): def fake_get_ecu_addrs(*_, timeout):
@ -252,48 +262,50 @@ class TestFwFingerprintTiming(unittest.TestCase):
self._assert_timing(self.total_time / self.N, vin_ref_times[name]) 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') print(f'get_vin {name} case, query time={self.total_time / self.N} seconds')
@pytest.mark.timeout(60)
def test_fw_query_timing(self): def test_fw_query_timing(self):
total_ref_time = 6.8 total_ref_time = {1: 6.5, 2: 7.4}
brand_ref_times = { brand_ref_times = {
1: { 1: {
'gm': 0.5, 'gm': 0.5,
'body': 0.1, 'body': 0.1,
'chrysler': 0.3, 'chrysler': 0.3,
'ford': 0.1, 'ford': 0.2,
'honda': 0.55, 'honda': 0.55,
'hyundai': 0.65, 'hyundai': 1.05,
'mazda': 0.1, 'mazda': 0.1,
'nissan': 0.8, 'nissan': 0.8,
'subaru': 0.45, 'subaru': 0.45,
'tesla': 0.2, 'tesla': 0.2,
'toyota': 1.6, 'toyota': 1.6,
'volkswagen': 0.2, 'volkswagen': 0.65,
}, },
2: { 2: {
'ford': 0.2, 'ford': 0.3,
'hyundai': 1.05, 'hyundai': 1.85,
} }
} }
total_time = 0 total_times = {1: 0.0, 2: 0.0}
for num_pandas in (1, 2): for num_pandas in (1, 2):
for brand, config in FW_QUERY_CONFIGS.items(): for brand, config in FW_QUERY_CONFIGS.items():
with self.subTest(brand=brand, num_pandas=num_pandas): with self.subTest(brand=brand, num_pandas=num_pandas):
multi_panda_requests = [r for r in config.requests if r.bus > 3]
if not len(multi_panda_requests) and num_pandas > 1:
raise unittest.SkipTest("No multi-panda FW queries")
avg_time = self._benchmark_brand(brand, num_pandas) avg_time = self._benchmark_brand(brand, num_pandas)
total_time += avg_time total_times[num_pandas] += avg_time
avg_time = round(avg_time, 2) avg_time = round(avg_time, 2)
self._assert_timing(avg_time, brand_ref_times[num_pandas][brand])
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') print(f'{brand=}, {num_pandas=}, {len(config.requests)=}, avg FW query time={avg_time} seconds')
with self.subTest(brand='all_brands'): for num_pandas in (1, 2):
total_time = round(total_time, 2) with self.subTest(brand='all_brands', num_pandas=num_pandas):
self._assert_timing(total_time, total_ref_time) total_time = round(total_times[num_pandas], 2)
print(f'all brands, total FW query time={total_time} seconds') self._assert_timing(total_time, total_ref_time[num_pandas])
print(f'all brands, total FW query time={total_time} seconds')
if __name__ == "__main__": if __name__ == "__main__":

@ -3,7 +3,6 @@ from collections import defaultdict
import importlib import importlib
from parameterized import parameterized_class from parameterized import parameterized_class
import sys import sys
from typing import DefaultDict, Dict
import unittest import unittest
from openpilot.common.realtime import DT_CTRL from openpilot.common.realtime import DT_CTRL
@ -29,7 +28,7 @@ ABOVE_LIMITS_CARS = [
SUBARU.OUTBACK, SUBARU.OUTBACK,
] ]
car_model_jerks: DefaultDict[str, Dict[str, float]] = defaultdict(dict) car_model_jerks: defaultdict[str, dict[str, float]] = defaultdict(dict)
@parameterized_class('car_model', [(c,) for c in sorted(CAR_MODELS)]) @parameterized_class('car_model', [(c,) for c in sorted(CAR_MODELS)])

@ -8,7 +8,6 @@ import unittest
from collections import defaultdict, Counter from collections import defaultdict, Counter
import hypothesis.strategies as st import hypothesis.strategies as st
from hypothesis import Phase, given, settings from hypothesis import Phase, given, settings
from typing import List, Optional, Tuple
from parameterized import parameterized_class from parameterized import parameterized_class
from cereal import messaging, log, car from cereal import messaging, log, car
@ -23,9 +22,8 @@ from openpilot.selfdrive.car.tests.routes import non_tested_cars, routes, CarTes
from openpilot.selfdrive.controls.controlsd import Controls from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.selfdrive.test.helpers import read_segment_list from openpilot.selfdrive.test.helpers import read_segment_list
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
from openpilot.tools.lib.comma_car_segments import get_url from openpilot.tools.lib.logreader import LogReader, internal_source, openpilotci_source
from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.route import SegmentName
from openpilot.tools.lib.route import Route, SegmentName, RouteName
from panda.tests.libpanda import libpanda_py from panda.tests.libpanda import libpanda_py
@ -37,11 +35,11 @@ NUM_JOBS = int(os.environ.get("NUM_JOBS", "1"))
JOB_ID = int(os.environ.get("JOB_ID", "0")) JOB_ID = int(os.environ.get("JOB_ID", "0"))
INTERNAL_SEG_LIST = os.environ.get("INTERNAL_SEG_LIST", "") INTERNAL_SEG_LIST = os.environ.get("INTERNAL_SEG_LIST", "")
INTERNAL_SEG_CNT = int(os.environ.get("INTERNAL_SEG_CNT", "0")) INTERNAL_SEG_CNT = int(os.environ.get("INTERNAL_SEG_CNT", "0"))
MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "50")) MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "300"))
CI = os.environ.get("CI", None) is not None CI = os.environ.get("CI", None) is not None
def get_test_cases() -> List[Tuple[str, Optional[CarTestRoute]]]: def get_test_cases() -> list[tuple[str, CarTestRoute | None]]:
# build list of test cases # build list of test cases
test_cases = [] test_cases = []
if not len(INTERNAL_SEG_LIST): if not len(INTERNAL_SEG_LIST):
@ -51,7 +49,7 @@ def get_test_cases() -> List[Tuple[str, Optional[CarTestRoute]]]:
for i, c in enumerate(sorted(all_known_cars())): for i, c in enumerate(sorted(all_known_cars())):
if i % NUM_JOBS == JOB_ID: if i % NUM_JOBS == JOB_ID:
test_cases.extend(sorted((c.value, r) for r in routes_by_car.get(c, (None,)))) test_cases.extend(sorted((c, r) for r in routes_by_car.get(c, (None,))))
else: else:
segment_list = read_segment_list(os.path.join(BASEDIR, INTERNAL_SEG_LIST)) segment_list = read_segment_list(os.path.join(BASEDIR, INTERNAL_SEG_LIST))
@ -66,22 +64,14 @@ def get_test_cases() -> List[Tuple[str, Optional[CarTestRoute]]]:
@pytest.mark.slow @pytest.mark.slow
@pytest.mark.shared_download_cache @pytest.mark.shared_download_cache
class TestCarModelBase(unittest.TestCase): class TestCarModelBase(unittest.TestCase):
car_model: Optional[str] = None car_model: str | None = None
test_route: Optional[CarTestRoute] = None test_route: CarTestRoute | None = None
test_route_on_bucket: bool = True # whether the route is on the preserved CI bucket test_route_on_bucket: bool = True # whether the route is on the preserved CI bucket
can_msgs: List[capnp.lib.capnp._DynamicStructReader] can_msgs: list[capnp.lib.capnp._DynamicStructReader]
fingerprint: dict[int, dict[int, int]] fingerprint: dict[int, dict[int, int]]
elm_frame: Optional[int] elm_frame: int | None
car_safety_mode_frame: Optional[int] car_safety_mode_frame: int | None
@classmethod
def get_logreader(cls, seg):
if len(INTERNAL_SEG_LIST):
route_name = RouteName(cls.test_route.route)
return LogReader(f"cd:/{route_name.dongle_id}/{route_name.time_str}/{seg}/rlog.bz2")
else:
return LogReader(get_url(cls.test_route.route, seg))
@classmethod @classmethod
def get_testing_data_from_logreader(cls, lr): def get_testing_data_from_logreader(cls, lr):
@ -133,22 +123,26 @@ class TestCarModelBase(unittest.TestCase):
if cls.test_route.segment is not None: if cls.test_route.segment is not None:
test_segs = (cls.test_route.segment,) test_segs = (cls.test_route.segment,)
# Try the primary method first (CI or internal) is_internal = len(INTERNAL_SEG_LIST)
for seg in test_segs: for seg in test_segs:
segment_range = f"{cls.test_route.route}/{seg}"
try: try:
lr = cls.get_logreader(seg) lr = LogReader(segment_range, default_source=internal_source if is_internal else openpilotci_source)
return cls.get_testing_data_from_logreader(lr) return cls.get_testing_data_from_logreader(lr)
except Exception: except Exception:
pass pass
# Route is not in CI bucket, assume either user has access (private), or it is public # Route is not in CI bucket, assume either user has access (private), or it is public
# test_route_on_ci_bucket will fail when running in CI # test_route_on_ci_bucket will fail when running in CI
if not len(INTERNAL_SEG_LIST): if not is_internal:
cls.test_route_on_bucket = False cls.test_route_on_bucket = False
for seg in test_segs: for seg in test_segs:
segment_range = f"{cls.test_route.route}/{seg}"
try: try:
lr = LogReader(Route(cls.test_route.route).log_paths()[seg]) lr = LogReader(segment_range)
return cls.get_testing_data_from_logreader(lr) return cls.get_testing_data_from_logreader(lr)
except Exception: except Exception:
pass pass
@ -239,7 +233,6 @@ class TestCarModelBase(unittest.TestCase):
self.assertEqual(can_invalid_cnt, 0) self.assertEqual(can_invalid_cnt, 0)
def test_radar_interface(self): def test_radar_interface(self):
os.environ['NO_RADAR_SLEEP'] = "1"
RadarInterface = importlib.import_module(f'selfdrive.car.{self.CP.carName}.radar_interface').RadarInterface RadarInterface = importlib.import_module(f'selfdrive.car.{self.CP.carName}.radar_interface').RadarInterface
RI = RadarInterface(self.CP) RI = RadarInterface(self.CP)
assert RI assert RI
@ -414,7 +407,7 @@ class TestCarModelBase(unittest.TestCase):
controls_allowed_prev = False controls_allowed_prev = False
CS_prev = car.CarState.new_message() CS_prev = car.CarState.new_message()
checks = defaultdict(lambda: 0) checks = defaultdict(int)
controlsd = Controls(CI=self.CI) controlsd = Controls(CI=self.CI)
controlsd.initialized = True controlsd.initialized = True
for idx, can in enumerate(self.can_msgs): for idx, can in enumerate(self.can_msgs):

@ -42,7 +42,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
"CHEVROLET BOLT EUV 2022" = [2.0, 2.0, 0.05] "CHEVROLET BOLT EUV 2022" = [2.0, 2.0, 0.05]
"CHEVROLET SILVERADO 1500 2020" = [1.9, 1.9, 0.112] "CHEVROLET SILVERADO 1500 2020" = [1.9, 1.9, 0.112]
"CHEVROLET TRAILBLAZER 2021" = [1.33, 1.9, 0.16] "CHEVROLET TRAILBLAZER 2021" = [1.33, 1.9, 0.16]
"CHEVROLET EQUINOX 2019" = [2.0, 2.0, 0.05] "CHEVROLET EQUINOX 2019" = [2.5, 2.5, 0.05]
"VOLKSWAGEN PASSAT NMS" = [2.5, 2.5, 0.1] "VOLKSWAGEN PASSAT NMS" = [2.5, 2.5, 0.1]
"VOLKSWAGEN SHARAN 2ND GEN" = [2.5, 2.5, 0.1] "VOLKSWAGEN SHARAN 2ND GEN" = [2.5, 2.5, 0.1]
"HYUNDAI SANTA CRUZ 1ST GEN" = [2.7, 2.7, 0.1] "HYUNDAI SANTA CRUZ 1ST GEN" = [2.7, 2.7, 0.1]

@ -5,12 +5,15 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
"MAZDA CX-5 2022" = "MAZDA CX-9 2021" "MAZDA CX-5 2022" = "MAZDA CX-9 2021"
"MAZDA CX-9" = "MAZDA CX-9 2021" "MAZDA CX-9" = "MAZDA CX-9 2021"
"DODGE DURANGO 2021" = "CHRYSLER PACIFICA 2020"
"TOYOTA ALPHARD 2020" = "TOYOTA SIENNA 2018" "TOYOTA ALPHARD 2020" = "TOYOTA SIENNA 2018"
"TOYOTA PRIUS v 2017" = "TOYOTA PRIUS 2017" "TOYOTA PRIUS v 2017" = "TOYOTA PRIUS 2017"
"LEXUS IS 2018" = "LEXUS NX 2018" "LEXUS IS 2018" = "LEXUS NX 2018"
"LEXUS CT HYBRID 2018" = "LEXUS NX 2018" "LEXUS CT HYBRID 2018" = "LEXUS NX 2018"
"LEXUS ES 2018" = "TOYOTA CAMRY 2018" "LEXUS ES 2018" = "TOYOTA CAMRY 2018"
"LEXUS RC 2020" = "LEXUS NX 2020" "LEXUS RC 2020" = "LEXUS NX 2020"
"LEXUS LC 2024" = "LEXUS NX 2020"
"KIA OPTIMA 4TH GEN" = "HYUNDAI SONATA 2020" "KIA OPTIMA 4TH GEN" = "HYUNDAI SONATA 2020"
"KIA OPTIMA 4TH GEN FACELIFT" = "HYUNDAI SONATA 2020" "KIA OPTIMA 4TH GEN FACELIFT" = "HYUNDAI SONATA 2020"

@ -55,10 +55,10 @@ class CarController:
apply_steer = apply_meas_steer_torque_limits(new_steer, self.last_steer, CS.out.steeringTorqueEps, self.params) apply_steer = apply_meas_steer_torque_limits(new_steer, self.last_steer, CS.out.steeringTorqueEps, self.params)
# >100 degree/sec steering fault prevention # >100 degree/sec steering fault prevention
self.steer_rate_counter, apply_steer_req = common_fault_avoidance(abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, CC.latActive, 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) self.steer_rate_counter, MAX_STEER_RATE_FRAMES)
if not CC.latActive: if not lat_active:
apply_steer = 0 apply_steer = 0
# *** steer angle *** # *** steer angle ***

@ -95,6 +95,9 @@ class CarState(CarStateBase):
ret.leftBlinker = cp.vl["BLINKERS_STATE"]["TURN_SIGNALS"] == 1 ret.leftBlinker = cp.vl["BLINKERS_STATE"]["TURN_SIGNALS"] == 1
ret.rightBlinker = cp.vl["BLINKERS_STATE"]["TURN_SIGNALS"] == 2 ret.rightBlinker = cp.vl["BLINKERS_STATE"]["TURN_SIGNALS"] == 2
if self.CP.carFingerprint != CAR.MIRAI:
ret.engineRpm = cp.vl["ENGINE_RPM"]["RPM"]
ret.steeringTorque = cp.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_DRIVER"] ret.steeringTorque = cp.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_DRIVER"]
ret.steeringTorqueEps = cp.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_EPS"] * self.eps_torque_scale 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 # we could use the override bit from dbc, but it's triggered at too high torque values
@ -180,6 +183,9 @@ class CarState(CarStateBase):
("STEER_TORQUE_SENSOR", 50), ("STEER_TORQUE_SENSOR", 50),
] ]
if CP.carFingerprint != CAR.MIRAI:
messages.append(("ENGINE_RPM", 42))
if CP.carFingerprint in UNSUPPORTED_DSU_CAR: if CP.carFingerprint in UNSUPPORTED_DSU_CAR:
messages.append(("DSU_CRUISE", 5)) messages.append(("DSU_CRUISE", 5))
messages.append(("PCM_CRUISE_ALT", 1)) messages.append(("PCM_CRUISE_ALT", 1))

@ -38,6 +38,7 @@ FW_VERSIONS = {
b'F152607180\x00\x00\x00\x00\x00\x00', b'F152607180\x00\x00\x00\x00\x00\x00',
b'F152641040\x00\x00\x00\x00\x00\x00', b'F152641040\x00\x00\x00\x00\x00\x00',
b'F152641050\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', b'F152641061\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.dsu, 0x791, None): [ (Ecu.dsu, 0x791, None): [
@ -55,10 +56,12 @@ FW_VERSIONS = {
b'\x01896630725100\x00\x00\x00\x00', b'\x01896630725100\x00\x00\x00\x00',
b'\x01896630725200\x00\x00\x00\x00', b'\x01896630725200\x00\x00\x00\x00',
b'\x01896630725300\x00\x00\x00\x00', b'\x01896630725300\x00\x00\x00\x00',
b'\x01896630725400\x00\x00\x00\x00',
b'\x01896630735100\x00\x00\x00\x00', b'\x01896630735100\x00\x00\x00\x00',
b'\x01896630738000\x00\x00\x00\x00', b'\x01896630738000\x00\x00\x00\x00',
b'\x02896630724000\x00\x00\x00\x00897CF3302002\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'\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', b'\x02896630737000\x00\x00\x00\x00897CF3305001\x00\x00\x00\x00',
], ],
(Ecu.fwdRadar, 0x750, 0xf): [ (Ecu.fwdRadar, 0x750, 0xf): [
@ -1361,16 +1364,19 @@ FW_VERSIONS = {
b'\x018966378G3000\x00\x00\x00\x00', b'\x018966378G3000\x00\x00\x00\x00',
], ],
(Ecu.engine, 0x7e0, None): [ (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'\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'\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', b'\x02378F4000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.abs, 0x7b0, None): [ (Ecu.abs, 0x7b0, None): [
b'\x01F152678221\x00\x00\x00\x00\x00\x00', 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'F152678210\x00\x00\x00\x00\x00\x00',
b'F152678211\x00\x00\x00\x00\x00\x00', b'F152678211\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.eps, 0x7a1, None): [ (Ecu.eps, 0x7a1, None): [
b'8965B78110\x00\x00\x00\x00\x00\x00',
b'8965B78120\x00\x00\x00\x00\x00\x00', b'8965B78120\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.fwdRadar, 0x750, 0xf): [ (Ecu.fwdRadar, 0x750, 0xf): [
@ -1383,6 +1389,23 @@ FW_VERSIONS = {
b'\x028646F7803100\x00\x00\x00\x008646G2601400\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'\x028646F1105200\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
],
},
CAR.LEXUS_RC: { CAR.LEXUS_RC: {
(Ecu.engine, 0x700, None): [ (Ecu.engine, 0x700, None): [
b'\x01896632461100\x00\x00\x00\x00', b'\x01896632461100\x00\x00\x00\x00',
@ -1417,6 +1440,7 @@ FW_VERSIONS = {
}, },
CAR.LEXUS_RX: { CAR.LEXUS_RX: {
(Ecu.engine, 0x700, None): [ (Ecu.engine, 0x700, None): [
b'\x01896630E36100\x00\x00\x00\x00',
b'\x01896630E36200\x00\x00\x00\x00', b'\x01896630E36200\x00\x00\x00\x00',
b'\x01896630E36300\x00\x00\x00\x00', b'\x01896630E36300\x00\x00\x00\x00',
b'\x01896630E37100\x00\x00\x00\x00', b'\x01896630E37100\x00\x00\x00\x00',

@ -181,6 +181,12 @@ class CarInterface(CarInterfaceBase):
ret.tireStiffnessFactor = 0.444 # not optimized yet ret.tireStiffnessFactor = 0.444 # not optimized yet
ret.mass = 4070 * CV.LB_TO_KG ret.mass = 4070 * CV.LB_TO_KG
elif candidate == CAR.LEXUS_LC_TSS2:
ret.wheelbase = 2.87
ret.steerRatio = 13.0
ret.tireStiffnessFactor = 0.444 # not optimized yet
ret.mass = 4500 * CV.LB_TO_KG
elif candidate == CAR.PRIUS_TSS2: elif candidate == CAR.PRIUS_TSS2:
ret.wheelbase = 2.70002 # from toyota online sepc. ret.wheelbase = 2.70002 # from toyota online sepc.
ret.steerRatio = 13.4 # True steerRatio from older prius ret.steerRatio = 13.4 # True steerRatio from older prius
@ -219,20 +225,16 @@ class CarInterface(CarInterfaceBase):
found_ecus = [fw.ecu for fw in car_fw] 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) \ ret.enableDsu = len(found_ecus) > 0 and Ecu.dsu not in found_ecus and candidate not in (NO_DSU_CAR | UNSUPPORTED_DSU_CAR) \
and not (ret.flags & ToyotaFlags.SMART_DSU) and not (ret.flags & ToyotaFlags.SMART_DSU)
ret.enableGasInterceptor = 0x201 in fingerprint[0]
if ret.enableGasInterceptor:
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_GAS_INTERCEPTOR
# if the smartDSU is detected, openpilot can send ACC_CONTROL and the smartDSU will block it from the DSU or radar. # if the smartDSU is detected, openpilot can send ACC_CONTROL and the smartDSU will block it from the DSU or radar.
# since we don't yet parse radar on TSS2/TSS-P radar-based ACC cars, gate longitudinal behind experimental toggle # since we don't yet parse radar on TSS2/TSS-P radar-based ACC cars, gate longitudinal behind experimental toggle
use_sdsu = bool(ret.flags & ToyotaFlags.SMART_DSU) use_sdsu = bool(ret.flags & ToyotaFlags.SMART_DSU)
if candidate in (RADAR_ACC_CAR | NO_DSU_CAR): if candidate in (RADAR_ACC_CAR | NO_DSU_CAR):
ret.experimentalLongitudinalAvailable = use_sdsu ret.experimentalLongitudinalAvailable = use_sdsu or candidate in RADAR_ACC_CAR
if not use_sdsu: if not use_sdsu:
# Disabling radar is only supported on TSS2 radar-ACC cars # Disabling radar is only supported on TSS2 radar-ACC cars
if experimental_long and candidate in RADAR_ACC_CAR and False: # TODO: disabling radar isn't supported yet if experimental_long and candidate in RADAR_ACC_CAR:
ret.flags |= ToyotaFlags.DISABLE_RADAR.value ret.flags |= ToyotaFlags.DISABLE_RADAR.value
else: else:
use_sdsu = use_sdsu and experimental_long use_sdsu = use_sdsu and experimental_long
@ -247,10 +249,14 @@ class CarInterface(CarInterfaceBase):
# - TSS-P DSU-less cars w/ CAN filter installed (no radar parser yet) # - TSS-P DSU-less cars w/ CAN filter installed (no radar parser yet)
ret.openpilotLongitudinalControl = use_sdsu or ret.enableDsu or candidate in (TSS2_CAR - RADAR_ACC_CAR) or bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value) ret.openpilotLongitudinalControl = use_sdsu or 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 ret.autoResumeSng = ret.openpilotLongitudinalControl and candidate in NO_STOP_TIMER_CAR
ret.enableGasInterceptor = 0x201 in fingerprint[0] and ret.openpilotLongitudinalControl
if not ret.openpilotLongitudinalControl: if not ret.openpilotLongitudinalControl:
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL
if ret.enableGasInterceptor:
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_TOYOTA_GAS_INTERCEPTOR
# min speed to enable ACC. if car can do stop and go, then set enabling speed # 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. # to a negative value, so it won't matter.
ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptor) else MIN_ACC_SPEED ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptor) else MIN_ACC_SPEED

@ -2,7 +2,6 @@ import re
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, IntFlag, StrEnum from enum import Enum, IntFlag, StrEnum
from typing import Dict, List, Set, Union
from cereal import car from cereal import car
from openpilot.common.conversions import Conversions as CV from openpilot.common.conversions import Conversions as CV
@ -81,6 +80,7 @@ class CAR(StrEnum):
LEXUS_IS_TSS2 = "LEXUS IS 2023" LEXUS_IS_TSS2 = "LEXUS IS 2023"
LEXUS_NX = "LEXUS NX 2018" LEXUS_NX = "LEXUS NX 2018"
LEXUS_NX_TSS2 = "LEXUS NX 2020" LEXUS_NX_TSS2 = "LEXUS NX 2020"
LEXUS_LC_TSS2 = "LEXUS LC 2024"
LEXUS_RC = "LEXUS RC 2020" LEXUS_RC = "LEXUS RC 2020"
LEXUS_RX = "LEXUS RX 2016" LEXUS_RX = "LEXUS RX 2016"
LEXUS_RX_TSS2 = "LEXUS RX 2020" LEXUS_RX_TSS2 = "LEXUS RX 2020"
@ -99,7 +99,7 @@ class ToyotaCarInfo(CarInfo):
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.toyota_a])) car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.toyota_a]))
CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { CAR_INFO: dict[str, ToyotaCarInfo | list[ToyotaCarInfo]] = {
# Toyota # Toyota
CAR.ALPHARD_TSS2: [ CAR.ALPHARD_TSS2: [
ToyotaCarInfo("Toyota Alphard 2019-20"), ToyotaCarInfo("Toyota Alphard 2019-20"),
@ -206,6 +206,7 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = {
ToyotaCarInfo("Lexus NX 2020-21"), ToyotaCarInfo("Lexus NX 2020-21"),
ToyotaCarInfo("Lexus NX Hybrid 2020-21"), ToyotaCarInfo("Lexus NX Hybrid 2020-21"),
], ],
CAR.LEXUS_LC_TSS2: ToyotaCarInfo("Lexus LC 2024"),
CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2018-20"), CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2018-20"),
CAR.LEXUS_RX: [ CAR.LEXUS_RX: [
ToyotaCarInfo("Lexus RX 2016", "Lexus Safety System+"), ToyotaCarInfo("Lexus RX 2016", "Lexus Safety System+"),
@ -251,7 +252,7 @@ STATIC_DSU_MSGS = [
] ]
def get_platform_codes(fw_versions: List[bytes]) -> Dict[bytes, Set[bytes]]: 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 # 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 codes = defaultdict(set) # Optional[part]-platform-major_version: set of sub_version
for fw in fw_versions: for fw in fw_versions:
@ -295,7 +296,7 @@ def get_platform_codes(fw_versions: List[bytes]) -> Dict[bytes, Set[bytes]]:
return dict(codes) return dict(codes)
def match_fw_to_car_fuzzy(live_fw_versions, offline_fw_versions) -> Set[str]: def match_fw_to_car_fuzzy(live_fw_versions, offline_fw_versions) -> set[str]:
candidates = set() candidates = set()
for candidate, fws in offline_fw_versions.items(): for candidate, fws in offline_fw_versions.items():
@ -444,6 +445,7 @@ DBC = {
CAR.PRIUS: dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'), CAR.PRIUS: dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'),
CAR.PRIUS_V: dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'), CAR.PRIUS_V: dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'),
CAR.COROLLA: dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'), CAR.COROLLA: dbc_dict('toyota_new_mc_pt_generated', 'toyota_adas'),
CAR.LEXUS_LC_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'),
CAR.LEXUS_RC: dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'), CAR.LEXUS_RC: dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
CAR.LEXUS_RX: dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'), CAR.LEXUS_RX: dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'),
CAR.LEXUS_RX_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.LEXUS_RX_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'),
@ -480,7 +482,8 @@ EPS_SCALE = defaultdict(lambda: 73, {CAR.PRIUS: 66, CAR.COROLLA: 88, CAR.LEXUS_I
# Toyota/Lexus Safety Sense 2.0 and 2.5 # Toyota/Lexus Safety Sense 2.0 and 2.5
TSS2_CAR = {CAR.RAV4_TSS2, CAR.RAV4_TSS2_2022, CAR.RAV4_TSS2_2023, CAR.COROLLA_TSS2, CAR.LEXUS_ES_TSS2, TSS2_CAR = {CAR.RAV4_TSS2, CAR.RAV4_TSS2_2022, CAR.RAV4_TSS2_2023, CAR.COROLLA_TSS2, CAR.LEXUS_ES_TSS2,
CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2, CAR.PRIUS_TSS2, CAR.CAMRY_TSS2, CAR.LEXUS_IS_TSS2, CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2, CAR.PRIUS_TSS2, CAR.CAMRY_TSS2, CAR.LEXUS_IS_TSS2,
CAR.MIRAI, CAR.LEXUS_NX_TSS2, CAR.ALPHARD_TSS2, CAR.AVALON_TSS2, CAR.CHR_TSS2} CAR.MIRAI, CAR.LEXUS_NX_TSS2, CAR.LEXUS_LC_TSS2, CAR.ALPHARD_TSS2, CAR.AVALON_TSS2,
CAR.CHR_TSS2}
NO_DSU_CAR = TSS2_CAR | {CAR.CHR, CAR.CAMRY} NO_DSU_CAR = TSS2_CAR | {CAR.CHR, CAR.CAMRY}

@ -15,13 +15,14 @@ def is_valid_vin(vin: str):
return re.fullmatch(VIN_RE, vin) is not None return re.fullmatch(VIN_RE, vin) is not None
def get_vin(logcan, sendcan, buses, timeout=0.1, retry=3, debug=False): def get_vin(logcan, sendcan, buses, timeout=0.1, retry=2, debug=False):
for i in range(retry): for i in range(retry):
for bus in buses: for bus in buses:
for request, response, valid_buses, vin_addrs, functional_addrs, rx_offset in ( 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, FUNCTIONAL_ADDRS, 0x8), (StdQueries.UDS_VIN_REQUEST, StdQueries.UDS_VIN_RESPONSE, (0, 1), STANDARD_VIN_ADDRS, FUNCTIONAL_ADDRS, 0x8),
(StdQueries.OBD_VIN_REQUEST, StdQueries.OBD_VIN_RESPONSE, (0, 1), STANDARD_VIN_ADDRS, FUNCTIONAL_ADDRS, 0x8), (StdQueries.OBD_VIN_REQUEST, StdQueries.OBD_VIN_RESPONSE, (0, 1), STANDARD_VIN_ADDRS, FUNCTIONAL_ADDRS, 0x8),
(StdQueries.GM_VIN_REQUEST, StdQueries.GM_VIN_RESPONSE, (0,), [0x24b], None, 0x400), # Bolt fwdCamera (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
): ):
if bus not in valid_buses: if bus not in valid_buses:
continue continue
@ -40,8 +41,8 @@ def get_vin(logcan, sendcan, buses, timeout=0.1, retry=3, debug=False):
for addr in vin_addrs: for addr in vin_addrs:
vin = results.get((addr, None)) vin = results.get((addr, None))
if vin is not None: if vin is not None:
# Ford pads with null bytes # Ford and Nissan pads with null bytes
if len(vin) == 24: if len(vin) in (19, 24):
vin = re.sub(b'\x00*$', b'', vin) vin = re.sub(b'\x00*$', b'', vin)
# Honda Bosch response starts with a length, trim to correct length # Honda Bosch response starts with a length, trim to correct length
@ -49,7 +50,7 @@ def get_vin(logcan, sendcan, buses, timeout=0.1, retry=3, debug=False):
vin = vin[1:18] vin = vin[1:18]
cloudlog.error(f"got vin with {request=}") cloudlog.error(f"got vin with {request=}")
return get_rx_addr_for_tx_addr(addr), bus, vin.decode() return get_rx_addr_for_tx_addr(addr, rx_offset=rx_offset), bus, vin.decode()
except Exception: except Exception:
cloudlog.exception("VIN query exception") cloudlog.exception("VIN query exception")

@ -1043,6 +1043,7 @@ FW_VERSIONS = {
], ],
(Ecu.srs, 0x715, None): [ (Ecu.srs, 0x715, None): [
b'\xf1\x873Q0959655AC\xf1\x890200\xf1\x82\r11120011100010022212110200', 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\x873Q0959655AP\xf1\x890305\xf1\x82\r11110011110011213331312131',
b'\xf1\x873Q0959655AQ\xf1\x890200\xf1\x82\r11120011100010312212113100', b'\xf1\x873Q0959655AQ\xf1\x890200\xf1\x82\r11120011100010312212113100',
b'\xf1\x873Q0959655AS\xf1\x890200\xf1\x82\r11120011100010022212110200', b'\xf1\x873Q0959655AS\xf1\x890200\xf1\x82\r11120011100010022212110200',
@ -1062,6 +1063,7 @@ FW_VERSIONS = {
(Ecu.fwdRadar, 0x757, None): [ (Ecu.fwdRadar, 0x757, None): [
b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\x0101', b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\x0101',
b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\x0101', b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\x0101',
b'\xf1\x875Q0907572H \xf1\x890620',
b'\xf1\x875Q0907572J \xf1\x890654', b'\xf1\x875Q0907572J \xf1\x890654',
b'\xf1\x875Q0907572K \xf1\x890402\xf1\x82\x0101', b'\xf1\x875Q0907572K \xf1\x890402\xf1\x82\x0101',
b'\xf1\x875Q0907572P \xf1\x890682', b'\xf1\x875Q0907572P \xf1\x890682',

@ -72,14 +72,18 @@ class CarInterface(CarInterfaceBase):
# Global lateral tuning defaults, can be overridden per-vehicle # Global lateral tuning defaults, can be overridden per-vehicle
ret.steerActuatorDelay = 0.1
ret.steerLimitTimer = 0.4
ret.steerRatio = 15.6 # Let the params learner figure this out ret.steerRatio = 15.6 # Let the params learner figure this out
ret.lateralTuning.pid.kpBP = [0.] ret.steerLimitTimer = 0.4
ret.lateralTuning.pid.kiBP = [0.] if candidate in PQ_CARS:
ret.lateralTuning.pid.kf = 0.00006 ret.steerActuatorDelay = 0.2
ret.lateralTuning.pid.kpV = [0.6] CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
ret.lateralTuning.pid.kiV = [0.2] 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 # Global longitudinal tuning defaults, can be overridden per-vehicle
@ -131,8 +135,6 @@ class CarInterface(CarInterfaceBase):
ret.wheelbase = 2.80 ret.wheelbase = 2.80
ret.minEnableSpeed = 20 * CV.KPH_TO_MS # ACC "basic", no FtS ret.minEnableSpeed = 20 * CV.KPH_TO_MS # ACC "basic", no FtS
ret.minSteerSpeed = 50 * CV.KPH_TO_MS ret.minSteerSpeed = 50 * CV.KPH_TO_MS
ret.steerActuatorDelay = 0.2
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
elif candidate == CAR.POLO_MK6: elif candidate == CAR.POLO_MK6:
ret.mass = 1230 ret.mass = 1230
@ -142,7 +144,6 @@ class CarInterface(CarInterfaceBase):
ret.mass = 1639 ret.mass = 1639
ret.wheelbase = 2.92 ret.wheelbase = 2.92
ret.minSteerSpeed = 50 * CV.KPH_TO_MS ret.minSteerSpeed = 50 * CV.KPH_TO_MS
ret.steerActuatorDelay = 0.2
elif candidate == CAR.TAOS_MK1: elif candidate == CAR.TAOS_MK1:
ret.mass = 1498 ret.mass = 1498

@ -1,7 +1,6 @@
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, IntFlag, StrEnum from enum import Enum, IntFlag, StrEnum
from typing import Dict, List, Union
from cereal import car from cereal import car
from panda.python import uds from panda.python import uds
@ -151,7 +150,7 @@ class CAR(StrEnum):
PQ_CARS = {CAR.PASSAT_NMS, CAR.SHARAN_MK2} PQ_CARS = {CAR.PASSAT_NMS, CAR.SHARAN_MK2}
DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict("vw_mqb_2010", None)) DBC: dict[str, dict[str, str]] = defaultdict(lambda: dbc_dict("vw_mqb_2010", None))
for car_type in PQ_CARS: for car_type in PQ_CARS:
DBC[car_type] = dbc_dict("vw_golf_mk4", None) DBC[car_type] = dbc_dict("vw_golf_mk4", None)
@ -191,7 +190,7 @@ class VWCarInfo(CarInfo):
self.car_parts = CarParts([Device.threex_angled_mount, CarHarness.j533]) self.car_parts = CarParts([Device.threex_angled_mount, CarHarness.j533])
CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { CAR_INFO: dict[str, VWCarInfo | list[VWCarInfo]] = {
CAR.ARTEON_MK1: [ CAR.ARTEON_MK1: [
VWCarInfo("Volkswagen Arteon 2018-23", video_link="https://youtu.be/FAomFKPFlDA"), VWCarInfo("Volkswagen Arteon 2018-23", video_link="https://youtu.be/FAomFKPFlDA"),
VWCarInfo("Volkswagen Arteon R 2020-23", video_link="https://youtu.be/FAomFKPFlDA"), VWCarInfo("Volkswagen Arteon R 2020-23", video_link="https://youtu.be/FAomFKPFlDA"),
@ -293,17 +292,24 @@ VOLKSWAGEN_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER +
VOLKSWAGEN_RX_OFFSET = 0x6a VOLKSWAGEN_RX_OFFSET = 0x6a
FW_QUERY_CONFIG = FwQueryConfig( FW_QUERY_CONFIG = FwQueryConfig(
requests=[ # TODO: add back whitelists after we gather enough data
requests=[request for bus, obd_multiplexing in [(1, True), (1, False), (0, False)] for request in [
Request( Request(
[VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_REQUEST_MULTI],
[VOLKSWAGEN_VERSION_RESPONSE], [VOLKSWAGEN_VERSION_RESPONSE],
whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar], # whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar],
rx_offset=VOLKSWAGEN_RX_OFFSET, rx_offset=VOLKSWAGEN_RX_OFFSET,
bus=bus,
logging=(bus != 1 or not obd_multiplexing),
obd_multiplexing=obd_multiplexing,
), ),
Request( Request(
[VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_REQUEST_MULTI],
[VOLKSWAGEN_VERSION_RESPONSE], [VOLKSWAGEN_VERSION_RESPONSE],
whitelist_ecus=[Ecu.engine, Ecu.transmission], # whitelist_ecus=[Ecu.engine, Ecu.transmission],
bus=bus,
logging=(bus != 1 or not obd_multiplexing),
obd_multiplexing=obd_multiplexing,
), ),
], ]],
) )

@ -5,28 +5,34 @@ import time
import threading import threading
from typing import SupportsFloat from typing import SupportsFloat
from cereal import car, log
from openpilot.common.numpy_fast import clip
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL
from openpilot.common.params import Params
import cereal.messaging as messaging import cereal.messaging as messaging
from cereal import car, log
from cereal.visionipc import VisionIpcClient, VisionStreamType from cereal.visionipc import VisionIpcClient, VisionStreamType
from openpilot.common.conversions import Conversions as CV
from panda import ALTERNATIVE_EXPERIENCE from panda import ALTERNATIVE_EXPERIENCE
from openpilot.common.conversions import Conversions as CV
from openpilot.common.numpy_fast import clip
from openpilot.common.params import Params
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.version import get_short_branch
from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp
from openpilot.selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can from openpilot.selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
from openpilot.selfdrive.controls.lib.alertmanager import AlertManager, set_offroad_alert
from openpilot.selfdrive.controls.lib.drive_helpers import VCruiseHelper, clip_curvature from openpilot.selfdrive.controls.lib.drive_helpers import VCruiseHelper, clip_curvature
from openpilot.selfdrive.controls.lib.events import Events, ET
from openpilot.selfdrive.controls.lib.latcontrol import LatControl, MIN_LATERAL_CONTROL_SPEED from openpilot.selfdrive.controls.lib.latcontrol import LatControl, MIN_LATERAL_CONTROL_SPEED
from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
from openpilot.selfdrive.controls.lib.events import Events, ET from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.selfdrive.controls.lib.alertmanager import AlertManager, set_offroad_alert
from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel
from openpilot.system.hardware import HARDWARE from openpilot.system.hardware import HARDWARE
from openpilot.system.version import get_short_branch
SOFT_DISABLE_TIME = 3 # seconds SOFT_DISABLE_TIME = 3 # seconds
LDW_MIN_SPEED = 31 * CV.MPH_TO_MS LDW_MIN_SPEED = 31 * CV.MPH_TO_MS
@ -55,32 +61,19 @@ ACTIVE_STATES = (State.enabled, State.softDisabling, State.overriding)
ENABLED_STATES = (State.preEnabled, *ACTIVE_STATES) ENABLED_STATES = (State.preEnabled, *ACTIVE_STATES)
class Controls: class CarD:
def __init__(self, CI=None): CI: CarInterfaceBase
config_realtime_process(4, Priority.CTRL_HIGH) CS: car.CarState
# Ensure the current branch is cached, otherwise the first iteration of controlsd lags
self.branch = get_short_branch()
# Setup sockets
self.pm = messaging.PubMaster(['sendcan', 'controlsState', 'carState',
'carControl', 'onroadEvents', 'carParams'])
self.sensor_packets = ["accelerometer", "gyroscope"]
self.camera_packets = ["roadCameraState", "driverCameraState", "wideRoadCameraState"]
self.log_sock = messaging.sub_sock('androidLog') def __init__(self, CI=None):
self.can_sock = messaging.sub_sock('can', timeout=20) self.can_sock = messaging.sub_sock('can', timeout=20)
self.sm = messaging.SubMaster(['pandaStates'])
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams'])
self.can_rcv_timeout_counter = 0 # conseuctive timeout count
self.can_rcv_cum_timeout_counter = 0 # cumulative timeout count
self.params = Params() self.params = Params()
ignore = self.sensor_packets + ['testJoystick']
if SIMULATION:
ignore += ['driverCameraState', 'managerState']
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
'testJoystick'] + self.camera_packets + self.sensor_packets,
ignore_alive=ignore, ignore_avg_freq=['radarState', 'testJoystick'], ignore_valid=['testJoystick', ])
if CI is None: if CI is None:
# wait for one pandaState and one CAN packet # wait for one pandaState and one CAN packet
@ -93,25 +86,17 @@ class Controls:
else: else:
self.CI, self.CP = CI, CI.CP self.CI, self.CP = CI, CI.CP
self.joystick_mode = self.params.get_bool("JoystickDebugMode")
# set alternative experiences from parameters # set alternative experiences from parameters
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
self.CP.alternativeExperience = 0 self.CP.alternativeExperience = 0
if not self.disengage_on_accelerator: if not disengage_on_accelerator:
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
# read params
self.is_metric = self.params.get_bool("IsMetric")
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
# detect sound card presence and ensure successful init
sounds_available = HARDWARE.get_sound_card_online()
car_recognized = self.CP.carName != 'mock' car_recognized = self.CP.carName != 'mock'
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly
self.CP.passive = not car_recognized or not controller_available or self.CP.dashcamOnly self.CP.passive = not car_recognized or not controller_available or self.CP.dashcamOnly
if self.CP.passive: if self.CP.passive:
safety_config = car.CarParams.SafetyConfig.new_message() safety_config = car.CarParams.SafetyConfig.new_message()
@ -129,6 +114,113 @@ class Controls:
self.params.put_nonblocking("CarParamsCache", cp_bytes) self.params.put_nonblocking("CarParamsCache", cp_bytes)
self.params.put_nonblocking("CarParamsPersistent", cp_bytes) self.params.put_nonblocking("CarParamsPersistent", cp_bytes)
def initialize(self):
"""Initialize CarInterface, once controls are ready"""
self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan'])
def state_update(self, CC: car.CarControl):
"""carState update loop, driven by can"""
# TODO: This should not depend on carControl
# Update carState from CAN
can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True)
self.CS = self.CI.update(CC, can_strs)
self.sm.update(0)
can_rcv_valid = len(can_strs) > 0
# Check for CAN timeout
if not can_rcv_valid:
self.can_rcv_timeout_counter += 1
self.can_rcv_cum_timeout_counter += 1
else:
self.can_rcv_timeout_counter = 0
self.can_rcv_timeout = self.can_rcv_timeout_counter >= 5
if can_rcv_valid and REPLAY:
self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime
return self.CS
def state_publish(self, car_events):
"""carState and carParams publish loop"""
# TODO: carState should be independent of the event loop
# carState
cs_send = messaging.new_message('carState')
cs_send.valid = self.CS.canValid
cs_send.carState = self.CS
cs_send.carState.events = car_events
self.pm.send('carState', cs_send)
# carParams - logged every 50 seconds (> 1 per segment)
if (self.sm.frame % int(50. / DT_CTRL) == 0):
cp_send = messaging.new_message('carParams')
cp_send.valid = True
cp_send.carParams = self.CP
self.pm.send('carParams', cp_send)
def controls_update(self, CC: car.CarControl):
"""control update loop, driven by carControl"""
# send car controls over can
now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9)
actuators_output, can_sends = self.CI.apply(CC, now_nanos)
self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=self.CS.canValid))
return actuators_output
class Controls:
def __init__(self, CI=None):
self.card = CarD(CI)
self.CP = self.card.CP
self.CI = self.card.CI
config_realtime_process(4, Priority.CTRL_HIGH)
# Ensure the current branch is cached, otherwise the first iteration of controlsd lags
self.branch = get_short_branch()
# Setup sockets
self.pm = messaging.PubMaster(['controlsState', 'carControl', 'onroadEvents'])
self.sensor_packets = ["accelerometer", "gyroscope"]
self.camera_packets = ["roadCameraState", "driverCameraState", "wideRoadCameraState"]
self.log_sock = messaging.sub_sock('androidLog')
self.params = Params()
ignore = self.sensor_packets + ['testJoystick']
if SIMULATION:
ignore += ['driverCameraState', 'managerState']
self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration',
'driverMonitoringState', 'longitudinalPlan', 'liveLocationKalman',
'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters',
'testJoystick'] + self.camera_packets + self.sensor_packets,
ignore_alive=ignore, ignore_avg_freq=ignore+['radarState', 'testJoystick'], ignore_valid=['testJoystick', ],
frequency=int(1/DT_CTRL))
self.joystick_mode = self.params.get_bool("JoystickDebugMode")
# read params
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
self.is_metric = self.params.get_bool("IsMetric")
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
# detect sound card presence and ensure successful init
sounds_available = HARDWARE.get_sound_card_online()
car_recognized = self.CP.carName != 'mock'
controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly
# cleanup old params # cleanup old params
if not self.CP.experimentalLongitudinalAvailable: if not self.CP.experimentalLongitudinalAvailable:
self.params.remove("ExperimentalLongitudinalEnabled") self.params.remove("ExperimentalLongitudinalEnabled")
@ -158,8 +250,6 @@ class Controls:
self.soft_disable_timer = 0 self.soft_disable_timer = 0
self.mismatch_counter = 0 self.mismatch_counter = 0
self.cruise_mismatch_counter = 0 self.cruise_mismatch_counter = 0
self.can_rcv_timeout_counter = 0 # conseuctive timeout count
self.can_rcv_cum_timeout_counter = 0 # cumulative timeout count
self.last_blinker_frame = 0 self.last_blinker_frame = 0
self.last_steering_pressed_frame = 0 self.last_steering_pressed_frame = 0
self.distance_traveled = 0 self.distance_traveled = 0
@ -311,7 +401,8 @@ class Controls:
else: else:
safety_mismatch = pandaState.safetyModel not in IGNORED_SAFETY_MODES safety_mismatch = pandaState.safetyModel not in IGNORED_SAFETY_MODES
if safety_mismatch or pandaState.safetyRxChecksInvalid or self.mismatch_counter >= 200: # safety mismatch allows some time for boardd to set the safety mode and publish it back from panda
if (safety_mismatch and self.sm.frame*DT_CTRL > 10.) or pandaState.safetyRxChecksInvalid or self.mismatch_counter >= 200:
self.events.add(EventName.controlsMismatch) self.events.add(EventName.controlsMismatch)
if log.PandaState.FaultType.relayMalfunction in pandaState.faults: if log.PandaState.FaultType.relayMalfunction in pandaState.faults:
@ -323,7 +414,7 @@ class Controls:
num_events = len(self.events) num_events = len(self.events)
not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning} not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning}
if self.sm.rcv_frame['managerState'] and (not_running - IGNORE_PROCESSES): if self.sm.recv_frame['managerState'] and (not_running - IGNORE_PROCESSES):
self.events.add(EventName.processNotRunning) self.events.add(EventName.processNotRunning)
if not_running != self.not_running_prev: if not_running != self.not_running_prev:
cloudlog.event("process_not_running", not_running=not_running, error=True) cloudlog.event("process_not_running", not_running=not_running, error=True)
@ -346,10 +437,9 @@ class Controls:
self.events.add(EventName.canError) self.events.add(EventName.canError)
# generic catch-all. ideally, a more specific event should be added above instead # generic catch-all. ideally, a more specific event should be added above instead
can_rcv_timeout = self.can_rcv_timeout_counter >= 5
has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE)) has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE))
no_system_errors = (not has_disable_events) or (len(self.events) == num_events) no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
if (not self.sm.all_checks() or can_rcv_timeout) and no_system_errors: if (not self.sm.all_checks() or self.card.can_rcv_timeout) and no_system_errors:
if not self.sm.all_alive(): if not self.sm.all_alive():
self.events.add(EventName.commIssue) self.events.add(EventName.commIssue)
elif not self.sm.all_freq_ok(): elif not self.sm.all_freq_ok():
@ -361,7 +451,7 @@ class Controls:
'invalid': [s for s, valid in self.sm.valid.items() if not valid], 'invalid': [s for s, valid in self.sm.valid.items() if not valid],
'not_alive': [s for s, alive in self.sm.alive.items() if not alive], 'not_alive': [s for s, alive in self.sm.alive.items() if not alive],
'not_freq_ok': [s for s, freq_ok in self.sm.freq_ok.items() if not freq_ok], 'not_freq_ok': [s for s, freq_ok in self.sm.freq_ok.items() if not freq_ok],
'can_rcv_timeout': can_rcv_timeout, 'can_rcv_timeout': self.card.can_rcv_timeout,
} }
if logs != self.logged_comm_issue: if logs != self.logged_comm_issue:
cloudlog.event("commIssue", error=True, **logs) cloudlog.event("commIssue", error=True, **logs)
@ -380,7 +470,7 @@ class Controls:
self.events.add(EventName.paramsdTemporaryError) self.events.add(EventName.paramsdTemporaryError)
# conservative HW alert. if the data or frequency are off, locationd will throw an error # conservative HW alert. if the data or frequency are off, locationd will throw an error
if any((self.sm.frame - self.sm.rcv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets): if any((self.sm.frame - self.sm.recv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets):
self.events.add(EventName.sensorDataInvalid) self.events.add(EventName.sensorDataInvalid)
if not REPLAY: if not REPLAY:
@ -410,9 +500,12 @@ class Controls:
# TODO: fix simulator # TODO: fix simulator
if not SIMULATION or REPLAY: if not SIMULATION or REPLAY:
# Not show in first 1 km to allow for driving out of garage. This event shows after 5 minutes
if not self.sm['liveLocationKalman'].gpsOK and self.sm['liveLocationKalman'].inputsOK and (self.distance_traveled > 1000): if not self.sm['liveLocationKalman'].gpsOK and self.sm['liveLocationKalman'].inputsOK and (self.distance_traveled > 1000):
# Not show in first 1 km to allow for driving out of garage. This event shows after 5 minutes
self.events.add(EventName.noGps) self.events.add(EventName.noGps)
if self.sm['liveLocationKalman'].gpsOK:
self.distance_traveled = 0
self.distance_traveled += CS.vEgo * DT_CTRL
if self.sm['modelV2'].frameDropPerc > 20: if self.sm['modelV2'].frameDropPerc > 20:
self.events.add(EventName.modeldLagging) self.events.add(EventName.modeldLagging)
@ -420,17 +513,13 @@ class Controls:
def data_sample(self): def data_sample(self):
"""Receive data from sockets and update carState""" """Receive data from sockets and update carState"""
# Update carState from CAN CS = self.card.state_update(self.CC)
can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True)
CS = self.CI.update(self.CC, can_strs)
if len(can_strs) and REPLAY:
self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime
self.sm.update(0) self.sm.update(0)
if not self.initialized: if not self.initialized:
all_valid = CS.canValid and self.sm.all_checks() all_valid = CS.canValid and self.sm.all_checks()
timed_out = self.sm.frame * DT_CTRL > (6. if REPLAY else 3.5) timed_out = self.sm.frame * DT_CTRL > 6.
if all_valid or timed_out or (SIMULATION and not REPLAY): if all_valid or timed_out or (SIMULATION and not REPLAY):
available_streams = VisionIpcClient.available_streams("camerad", block=False) available_streams = VisionIpcClient.available_streams("camerad", block=False)
if VisionStreamType.VISION_STREAM_ROAD not in available_streams: if VisionStreamType.VISION_STREAM_ROAD not in available_streams:
@ -439,18 +528,22 @@ class Controls:
self.sm.ignore_alive.append('wideRoadCameraState') self.sm.ignore_alive.append('wideRoadCameraState')
if not self.CP.passive: if not self.CP.passive:
self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan']) self.card.initialize()
self.initialized = True self.initialized = True
self.set_initial_state() self.set_initial_state()
self.params.put_bool_nonblocking("ControlsReady", True) self.params.put_bool_nonblocking("ControlsReady", True)
# Check for CAN timeout cloudlog.event(
if not can_strs: "controlsd.initialized",
self.can_rcv_timeout_counter += 1 dt=self.sm.frame*DT_CTRL,
self.can_rcv_cum_timeout_counter += 1 timeout=timed_out,
else: canValid=CS.canValid,
self.can_rcv_timeout_counter = 0 invalid=[s for s, valid in self.sm.valid.items() if not valid],
not_alive=[s for s, alive in self.sm.alive.items() if not alive],
not_freq_ok=[s for s, freq_ok in self.sm.freq_ok.items() if not freq_ok],
error=True,
)
# When the panda and controlsd do not agree on controls_allowed # When the panda and controlsd do not agree on controls_allowed
# we want to disengage openpilot. However the status from the panda goes through # we want to disengage openpilot. However the status from the panda goes through
@ -464,8 +557,6 @@ class Controls:
if ps.safetyModel not in IGNORED_SAFETY_MODES): if ps.safetyModel not in IGNORED_SAFETY_MODES):
self.mismatch_counter += 1 self.mismatch_counter += 1
self.distance_traveled += CS.vEgo * DT_CTRL
return CS return CS
def state_transition(self, CS): def state_transition(self, CS):
@ -603,7 +694,7 @@ class Controls:
if not self.joystick_mode: if not self.joystick_mode:
# accel PID loop # accel PID loop
pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, CS.vEgo, self.v_cruise_helper.v_cruise_kph * CV.KPH_TO_MS) pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, CS.vEgo, self.v_cruise_helper.v_cruise_kph * CV.KPH_TO_MS)
t_since_plan = (self.sm.frame - self.sm.rcv_frame['longitudinalPlan']) * DT_CTRL t_since_plan = (self.sm.frame - self.sm.recv_frame['longitudinalPlan']) * DT_CTRL
actuators.accel = self.LoC.update(CC.longActive, CS, long_plan, pid_accel_limits, t_since_plan) actuators.accel = self.LoC.update(CC.longActive, CS, long_plan, pid_accel_limits, t_since_plan)
# Steering PID loop and lateral MPC # Steering PID loop and lateral MPC
@ -614,9 +705,9 @@ class Controls:
self.sm['liveLocationKalman']) self.sm['liveLocationKalman'])
else: else:
lac_log = log.ControlsState.LateralDebugState.new_message() lac_log = log.ControlsState.LateralDebugState.new_message()
if self.sm.rcv_frame['testJoystick'] > 0: if self.sm.recv_frame['testJoystick'] > 0:
# reset joystick if it hasn't been received in a while # reset joystick if it hasn't been received in a while
should_reset_joystick = (self.sm.frame - self.sm.rcv_frame['testJoystick'])*DT_CTRL > 0.2 should_reset_joystick = (self.sm.frame - self.sm.recv_frame['testJoystick'])*DT_CTRL > 0.2
if not should_reset_joystick: if not should_reset_joystick:
joystick_axes = self.sm['testJoystick'].axes joystick_axes = self.sm['testJoystick'].axes
else: else:
@ -691,7 +782,7 @@ class Controls:
CC.cruiseControl.override = self.enabled and not CC.longActive and self.CP.openpilotLongitudinalControl CC.cruiseControl.override = self.enabled and not CC.longActive and self.CP.openpilotLongitudinalControl
CC.cruiseControl.cancel = CS.cruiseState.enabled and (not self.enabled or not self.CP.pcmCruise) CC.cruiseControl.cancel = CS.cruiseState.enabled and (not self.enabled or not self.CP.pcmCruise)
if self.joystick_mode and self.sm.rcv_frame['testJoystick'] > 0 and self.sm['testJoystick'].buttons[0]: if self.joystick_mode and self.sm.recv_frame['testJoystick'] > 0 and self.sm['testJoystick'].buttons[0]:
CC.cruiseControl.cancel = True CC.cruiseControl.cancel = True
speeds = self.sm['longitudinalPlan'].speeds speeds = self.sm['longitudinalPlan'].speeds
@ -742,10 +833,7 @@ class Controls:
hudControl.visualAlert = current_alert.visual_alert hudControl.visualAlert = current_alert.visual_alert
if not self.CP.passive and self.initialized: if not self.CP.passive and self.initialized:
# send car controls over can self.last_actuators = self.card.controls_update(CC)
now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9)
self.last_actuators, can_sends = self.CI.apply(CC, now_nanos)
self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=CS.canValid))
CC.actuatorsOutput = self.last_actuators CC.actuatorsOutput = self.last_actuators
if self.CP.steerControlType == car.CarParams.SteerControlType.angle: if self.CP.steerControlType == car.CarParams.SteerControlType.angle:
self.steer_limited = abs(CC.actuators.steeringAngleDeg - CC.actuatorsOutput.steeringAngleDeg) > \ self.steer_limited = abs(CC.actuators.steeringAngleDeg - CC.actuatorsOutput.steeringAngleDeg) > \
@ -793,7 +881,7 @@ class Controls:
controlsState.cumLagMs = -self.rk.remaining * 1000. controlsState.cumLagMs = -self.rk.remaining * 1000.
controlsState.startMonoTime = int(start_time * 1e9) controlsState.startMonoTime = int(start_time * 1e9)
controlsState.forceDecel = bool(force_decel) controlsState.forceDecel = bool(force_decel)
controlsState.canErrorCounter = self.can_rcv_cum_timeout_counter controlsState.canErrorCounter = self.card.can_rcv_cum_timeout_counter
controlsState.experimentalMode = self.experimental_mode controlsState.experimentalMode = self.experimental_mode
lat_tuning = self.CP.lateralTuning.which() lat_tuning = self.CP.lateralTuning.which()
@ -808,13 +896,9 @@ class Controls:
self.pm.send('controlsState', dat) self.pm.send('controlsState', dat)
# carState
car_events = self.events.to_msg() car_events = self.events.to_msg()
cs_send = messaging.new_message('carState')
cs_send.valid = CS.canValid self.card.state_publish(car_events)
cs_send.carState = CS
cs_send.carState.events = car_events
self.pm.send('carState', cs_send)
# onroadEvents - logged every second or on change # onroadEvents - logged every second or on change
if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events.names != self.events_prev): if (self.sm.frame % int(1. / DT_CTRL) == 0) or (self.events.names != self.events_prev):
@ -824,13 +908,6 @@ class Controls:
self.pm.send('onroadEvents', ce_send) self.pm.send('onroadEvents', ce_send)
self.events_prev = self.events.names.copy() self.events_prev = self.events.names.copy()
# carParams - logged every 50 seconds (> 1 per segment)
if (self.sm.frame % int(50. / DT_CTRL) == 0):
cp_send = messaging.new_message('carParams')
cp_send.valid = True
cp_send.carParams = self.CP
self.pm.send('carParams', cp_send)
# carControl # carControl
cc_send = messaging.new_message('carControl') cc_send = messaging.new_message('carControl')
cc_send.valid = CS.canValid cc_send.valid = CS.canValid

@ -3,7 +3,6 @@ import os
import json import json
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Dict, Optional
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params from openpilot.common.params import Params
@ -14,7 +13,7 @@ with open(os.path.join(BASEDIR, "selfdrive/controls/lib/alerts_offroad.json")) a
OFFROAD_ALERTS = json.load(f) OFFROAD_ALERTS = json.load(f)
def set_offroad_alert(alert: str, show_alert: bool, extra_text: Optional[str] = None) -> None: def set_offroad_alert(alert: str, show_alert: bool, extra_text: str | None = None) -> None:
if show_alert: if show_alert:
a = copy.copy(OFFROAD_ALERTS[alert]) a = copy.copy(OFFROAD_ALERTS[alert])
a['extra'] = extra_text or '' a['extra'] = extra_text or ''
@ -25,7 +24,7 @@ def set_offroad_alert(alert: str, show_alert: bool, extra_text: Optional[str] =
@dataclass @dataclass
class AlertEntry: class AlertEntry:
alert: Optional[Alert] = None alert: Alert | None = None
start_frame: int = -1 start_frame: int = -1
end_frame: int = -1 end_frame: int = -1
@ -34,9 +33,9 @@ class AlertEntry:
class AlertManager: class AlertManager:
def __init__(self): def __init__(self):
self.alerts: Dict[str, AlertEntry] = defaultdict(AlertEntry) self.alerts: dict[str, AlertEntry] = defaultdict(AlertEntry)
def add_many(self, frame: int, alerts: List[Alert]) -> None: def add_many(self, frame: int, alerts: list[Alert]) -> None:
for alert in alerts: for alert in alerts:
entry = self.alerts[alert.alert_type] entry = self.alerts[alert.alert_type]
entry.alert = alert entry.alert = alert
@ -45,7 +44,7 @@ class AlertManager:
min_end_frame = entry.start_frame + alert.duration min_end_frame = entry.start_frame + alert.duration
entry.end_frame = max(frame + 1, min_end_frame) entry.end_frame = max(frame + 1, min_end_frame)
def process_alerts(self, frame: int, clear_event_types: set) -> Optional[Alert]: def process_alerts(self, frame: int, clear_event_types: set) -> Alert | None:
current_alert = AlertEntry() current_alert = AlertEntry()
for v in self.alerts.values(): for v in self.alerts.values():
if not v.alert: if not v.alert:

@ -2,7 +2,7 @@
import math import math
import os import os
from enum import IntEnum from enum import IntEnum
from typing import Dict, Union, Callable, List, Optional from collections.abc import Callable
from cereal import log, car from cereal import log, car
import cereal.messaging as messaging import cereal.messaging as messaging
@ -48,12 +48,12 @@ EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()}
class Events: class Events:
def __init__(self): def __init__(self):
self.events: List[int] = [] self.events: list[int] = []
self.static_events: List[int] = [] self.static_events: list[int] = []
self.events_prev = dict.fromkeys(EVENTS.keys(), 0) self.events_prev = dict.fromkeys(EVENTS.keys(), 0)
@property @property
def names(self) -> List[int]: def names(self) -> list[int]:
return self.events return self.events
def __len__(self) -> int: def __len__(self) -> int:
@ -71,7 +71,7 @@ class Events:
def contains(self, event_type: str) -> bool: def contains(self, event_type: str) -> bool:
return any(event_type in EVENTS.get(e, {}) for e in self.events) return any(event_type in EVENTS.get(e, {}) for e in self.events)
def create_alerts(self, event_types: List[str], callback_args=None): def create_alerts(self, event_types: list[str], callback_args=None):
if callback_args is None: if callback_args is None:
callback_args = [] callback_args = []
@ -132,7 +132,7 @@ class Alert:
self.creation_delay = creation_delay self.creation_delay = creation_delay
self.alert_type = "" self.alert_type = ""
self.event_type: Optional[str] = None self.event_type: str | None = None
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}"
@ -333,7 +333,7 @@ def joystick_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster,
EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = { EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
# ********** events with no alerts ********** # ********** events with no alerts **********
EventName.stockFcw: {}, EventName.stockFcw: {},
@ -767,12 +767,12 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = {
# is thrown. This can mean a service crashed, did not broadcast a message for # is thrown. This can mean a service crashed, did not broadcast a message for
# ten times the regular interval, or the average interval is more than 10% too high. # ten times the regular interval, or the average interval is more than 10% too high.
EventName.commIssue: { EventName.commIssue: {
ET.SOFT_DISABLE: soft_disable_alert("Communication Issue between Processes"), ET.SOFT_DISABLE: soft_disable_alert("Communication Issue Between Processes"),
ET.NO_ENTRY: comm_issue_alert, ET.NO_ENTRY: comm_issue_alert,
}, },
EventName.commIssueAvgFreq: { EventName.commIssueAvgFreq: {
ET.SOFT_DISABLE: soft_disable_alert("Low Communication Rate between Processes"), ET.SOFT_DISABLE: soft_disable_alert("Low Communication Rate Between Processes"),
ET.NO_ENTRY: NoEntryAlert("Low Communication Rate between Processes"), ET.NO_ENTRY: NoEntryAlert("Low Communication Rate Between Processes"),
}, },
EventName.controlsdLagging: { EventName.controlsdLagging: {
@ -965,7 +965,7 @@ if __name__ == '__main__':
from collections import defaultdict from collections import defaultdict
event_names = {v: k for k, v in EventName.schema.enumerants.items()} event_names = {v: k for k, v in EventName.schema.enumerants.items()}
alerts_by_type: Dict[str, Dict[Priority, List[str]]] = defaultdict(lambda: defaultdict(list)) alerts_by_type: dict[str, dict[Priority, list[str]]] = defaultdict(lambda: defaultdict(list))
CP = car.CarParams.new_message() CP = car.CarParams.new_message()
CS = car.CarState.new_message() CS = car.CarState.new_message()
@ -977,7 +977,7 @@ if __name__ == '__main__':
alert = alert(CP, CS, sm, False, 1) alert = alert(CP, CS, sm, False, 1)
alerts_by_type[et][alert.priority].append(event_names[i]) alerts_by_type[et][alert.priority].append(event_names[i])
all_alerts: Dict[str, List[tuple[Priority, List[str]]]] = {} all_alerts: dict[str, list[tuple[Priority, list[str]]]] = {}
for et, priority_alerts in alerts_by_type.items(): for et, priority_alerts in alerts_by_type.items():
all_alerts[et] = sorted(priority_alerts.items(), key=lambda x: x[0], reverse=True) all_alerts[et] = sorted(priority_alerts.items(), key=lambda x: x[0], reverse=True)

@ -12,7 +12,6 @@ x_dot = A*x + B*u
A depends on longitudinal speed, u [m/s], and vehicle parameters CP A depends on longitudinal speed, u [m/s], and vehicle parameters CP
""" """
from typing import Tuple
import numpy as np import numpy as np
from numpy.linalg import solve from numpy.linalg import solve
@ -169,7 +168,7 @@ def kin_ss_sol(sa: float, u: float, VM: VehicleModel) -> np.ndarray:
return K * sa return K * sa
def create_dyn_state_matrices(u: float, VM: VehicleModel) -> Tuple[np.ndarray, np.ndarray]: def create_dyn_state_matrices(u: float, VM: VehicleModel) -> tuple[np.ndarray, np.ndarray]:
"""Returns the A and B matrix for the dynamics system """Returns the A and B matrix for the dynamics system
Args: Args:

@ -29,11 +29,10 @@ def plannerd_thread():
longitudinal_planner = LongitudinalPlanner(CP) longitudinal_planner = LongitudinalPlanner(CP)
pm = messaging.PubMaster(['longitudinalPlan', 'uiPlan']) pm = messaging.PubMaster(['longitudinalPlan', 'uiPlan'])
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'radarState', 'modelV2'], sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'radarState', 'modelV2'],
poll=['radarState', 'modelV2'], ignore_avg_freq=['radarState']) poll='modelV2', ignore_avg_freq=['radarState'])
while True: while True:
sm.update() sm.update()
if sm.updated['modelV2']: if sm.updated['modelV2']:
longitudinal_planner.update(sm) longitudinal_planner.update(sm)
longitudinal_planner.publish(sm, pm) longitudinal_planner.publish(sm, pm)

@ -2,13 +2,13 @@
import importlib import importlib
import math import math
from collections import deque from collections import deque
from typing import Optional, Dict, Any from typing import Any, Optional
import capnp import capnp
from cereal import messaging, log, car from cereal import messaging, log, car
from openpilot.common.numpy_fast import interp from openpilot.common.numpy_fast import interp
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper, Priority, config_realtime_process from openpilot.common.realtime import DT_CTRL, Ratekeeper, Priority, config_realtime_process
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.simple_kalman import KF1D from openpilot.common.simple_kalman import KF1D
@ -125,7 +125,7 @@ def laplacian_pdf(x: float, mu: float, b: float):
return math.exp(-abs(x-mu)/b) return math.exp(-abs(x-mu)/b)
def match_vision_to_track(v_ego: float, lead: capnp._DynamicStructReader, tracks: Dict[int, Track]): def match_vision_to_track(v_ego: float, lead: capnp._DynamicStructReader, tracks: dict[int, Track]):
offset_vision_dist = lead.x[0] - RADAR_TO_CAMERA offset_vision_dist = lead.x[0] - RADAR_TO_CAMERA
def prob(c): def prob(c):
@ -166,8 +166,8 @@ def get_RadarState_from_vision(lead_msg: capnp._DynamicStructReader, v_ego: floa
} }
def get_lead(v_ego: float, ready: bool, tracks: Dict[int, Track], lead_msg: capnp._DynamicStructReader, def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capnp._DynamicStructReader,
model_v_ego: float, low_speed_override: bool = True) -> Dict[str, Any]: model_v_ego: float, low_speed_override: bool = True) -> dict[str, Any]:
# Determine leads, this is where the essential logic happens # Determine leads, this is where the essential logic happens
if len(tracks) > 0 and ready and lead_msg.prob > .5: if len(tracks) > 0 and ready and lead_msg.prob > .5:
track = match_vision_to_track(v_ego, lead_msg, tracks) track = match_vision_to_track(v_ego, lead_msg, tracks)
@ -196,18 +196,20 @@ class RadarD:
def __init__(self, radar_ts: float, delay: int = 0): def __init__(self, radar_ts: float, delay: int = 0):
self.current_time = 0.0 self.current_time = 0.0
self.tracks: Dict[int, Track] = {} self.tracks: dict[int, Track] = {}
self.kalman_params = KalmanParams(radar_ts) self.kalman_params = KalmanParams(radar_ts)
self.v_ego = 0.0 self.v_ego = 0.0
self.v_ego_hist = deque([0.0], maxlen=delay+1) self.v_ego_hist = deque([0.0], maxlen=delay+1)
self.last_v_ego_frame = -1
self.radar_state: Optional[capnp._DynamicStructBuilder] = None self.radar_state: capnp._DynamicStructBuilder | None = None
self.radar_state_valid = False self.radar_state_valid = False
self.ready = False self.ready = False
def update(self, sm: messaging.SubMaster, rr: Optional[car.RadarData]): def update(self, sm: messaging.SubMaster, rr: Optional[car.RadarData]):
self.ready = sm.seen['modelV2']
self.current_time = 1e-9*max(sm.logMonoTime.values()) self.current_time = 1e-9*max(sm.logMonoTime.values())
radar_points = [] radar_points = []
@ -216,11 +218,10 @@ class RadarD:
radar_points = rr.points radar_points = rr.points
radar_errors = rr.errors radar_errors = rr.errors
if sm.updated['carState']: if sm.recv_frame['carState'] != self.last_v_ego_frame:
self.v_ego = sm['carState'].vEgo self.v_ego = sm['carState'].vEgo
self.v_ego_hist.append(self.v_ego) self.v_ego_hist.append(self.v_ego)
if sm.updated['modelV2']: self.last_v_ego_frame = sm.recv_frame['carState']
self.ready = True
ar_pts = {} ar_pts = {}
for pt in radar_points: for pt in radar_points:
@ -282,7 +283,7 @@ class RadarD:
# fuses camera and radar data for best lead detection # fuses camera and radar data for best lead detection
def radard_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[messaging.PubMaster] = None, can_sock: Optional[messaging.SubSocket] = None): def main():
config_realtime_process(5, Priority.CTRL_LOW) config_realtime_process(5, Priority.CTRL_LOW)
# wait for stats about the car to come in from controls # wait for stats about the car to come in from controls
@ -296,12 +297,9 @@ def radard_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[messagi
RadarInterface = importlib.import_module(f'selfdrive.car.{CP.carName}.radar_interface').RadarInterface RadarInterface = importlib.import_module(f'selfdrive.car.{CP.carName}.radar_interface').RadarInterface
# *** setup messaging # *** setup messaging
if can_sock is None: can_sock = messaging.sub_sock('can')
can_sock = messaging.sub_sock('can') sm = messaging.SubMaster(['modelV2', 'carState'], frequency=int(1./DT_CTRL))
if sm is None: pm = messaging.PubMaster(['radarState', 'liveTracks'])
sm = messaging.SubMaster(['modelV2', 'carState'], ignore_avg_freq=['modelV2', 'carState']) # Can't check average frequency, since radar determines timing
if pm is None:
pm = messaging.PubMaster(['radarState', 'liveTracks'])
RI = RadarInterface(CP) RI = RadarInterface(CP)
@ -311,21 +309,15 @@ def radard_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[messagi
while 1: while 1:
can_strings = messaging.drain_sock_raw(can_sock, wait_for_one=True) can_strings = messaging.drain_sock_raw(can_sock, wait_for_one=True)
rr = RI.update(can_strings) rr = RI.update(can_strings)
sm.update(0)
if rr is None: if rr is None:
continue continue
sm.update(0)
RD.update(sm, rr) RD.update(sm, rr)
RD.publish(pm, -rk.remaining*1000.0) RD.publish(pm, -rk.remaining*1000.0)
rk.monitor_time() rk.monitor_time()
def main(sm: Optional[messaging.SubMaster] = None, pm: Optional[messaging.PubMaster] = None, can_sock: messaging.SubSocket = None):
radard_thread(sm, pm, can_sock)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

@ -3,7 +3,6 @@ import argparse
import binascii import binascii
import time import time
from collections import defaultdict from collections import defaultdict
from typing import Optional
import cereal.messaging as messaging import cereal.messaging as messaging
from openpilot.selfdrive.debug.can_table import can_table from openpilot.selfdrive.debug.can_table import can_table
@ -96,8 +95,8 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
init_lr: Optional[LogIterable] = None init_lr: LogIterable | None = None
new_lr: Optional[LogIterable] = None new_lr: LogIterable | None = None
if args.init: if args.init:
if args.init == '': if args.init == '':

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save