Merge remote-tracking branch 'origin/master' into equinox

pull/31257/head
Eric Brown 1 year ago
commit 5cfa3ef355
  1. 120
      .github/labeler.yaml
  2. 2
      .github/workflows/auto_pr_review.yaml
  3. 4
      .github/workflows/badges.yaml
  4. 7
      .github/workflows/prebuilt.yaml
  5. 6
      .github/workflows/release.yaml
  6. 12
      .github/workflows/selfdrive_tests.yaml
  7. 2
      .github/workflows/stale.yaml
  8. 8
      .pre-commit-config.yaml
  9. 28
      Jenkinsfile
  10. 4
      RELEASES.md
  11. 7
      SConstruct
  12. 2
      cereal
  13. 2
      common/params.cc
  14. 45
      common/profiler.py
  15. 2
      common/realtime.py
  16. 4
      docs/BOUNTIES.md
  17. 9
      docs/CARS.md
  18. 2
      docs/CONTRIBUTING.md
  19. 2
      opendbc
  20. 2
      panda
  21. 1884
      poetry.lock
  22. 2
      pyproject.toml
  23. 2
      rednose_repo
  24. 7
      release/files_common
  25. 11
      selfdrive/athena/athenad.py
  26. 60
      selfdrive/car/README.md
  27. 6
      selfdrive/car/docs_definitions.py
  28. 15
      selfdrive/car/gm/carcontroller.py
  29. 1
      selfdrive/car/gm/values.py
  30. 3
      selfdrive/car/honda/fingerprints.py
  31. 3
      selfdrive/car/hyundai/carstate.py
  32. 3
      selfdrive/car/hyundai/values.py
  33. 2
      selfdrive/car/subaru/interface.py
  34. 24
      selfdrive/car/subaru/subarucan.py
  35. 4
      selfdrive/car/tests/test_lateral_limits.py
  36. 149
      selfdrive/car/tests/test_models.py
  37. 26
      selfdrive/car/toyota/fingerprints.py
  38. 4
      selfdrive/car/toyota/values.py
  39. 13
      selfdrive/car/volkswagen/carcontroller.py
  40. 13
      selfdrive/car/volkswagen/carstate.py
  41. 11
      selfdrive/car/volkswagen/interface.py
  42. 20
      selfdrive/car/volkswagen/mqbcan.py
  43. 6
      selfdrive/car/volkswagen/values.py
  44. 9
      selfdrive/controls/controlsd.py
  45. 9
      selfdrive/controls/lib/longitudinal_planner.py
  46. 12
      selfdrive/debug/can_print_changes.py
  47. 48
      selfdrive/debug/count_events.py
  48. 37
      selfdrive/debug/filter_log_message.py
  49. 6
      selfdrive/debug/fingerprint_from_route.py
  50. 7
      selfdrive/debug/print_docs_diff.py
  51. 9
      selfdrive/debug/run_process_on_route.py
  52. 111
      selfdrive/debug/sensor_data_to_hist.py
  53. 4
      selfdrive/debug/test_fw_query_on_routes.py
  54. 6
      selfdrive/debug/toyota_eps_factor.py
  55. 2
      selfdrive/manager/manager.py
  56. 4
      selfdrive/modeld/models/navmodel.onnx
  57. 2
      selfdrive/modeld/models/navmodel_q.dlc
  58. 4
      selfdrive/modeld/models/supercombo.onnx
  59. 16
      selfdrive/test/helpers.py
  60. 2
      selfdrive/test/process_replay/model_replay_ref_commit
  61. 2
      selfdrive/test/process_replay/ref_commit
  62. 2
      selfdrive/test/test_onroad.py
  63. 3
      selfdrive/thermald/thermald.py
  64. 2
      selfdrive/ui/qt/maps/map_eta.cc
  65. 148
      selfdrive/ui/tests/test_translations.py
  66. 138
      selfdrive/ui/translations/auto_translate.py
  67. 4
      selfdrive/ui/translations/create_badges.py
  68. 20
      selfdrive/ui/update_translations.py
  69. 15
      selfdrive/updated.py
  70. 2
      system/camerad/SConscript
  71. 1
      system/camerad/cameras/camera_common.cc
  72. 1
      system/camerad/cameras/camera_common.h
  73. 22
      system/camerad/cameras/camera_qcom2.cc
  74. 5
      system/camerad/sensors/ar0231.cc
  75. 105
      system/camerad/sensors/os04c10.cc
  76. 298
      system/camerad/sensors/os04c10_registers.h
  77. 5
      system/camerad/sensors/ox03c10.cc
  78. 16
      system/camerad/sensors/sensor.h
  79. 15
      system/camerad/test/get_thumbnails_for_segment.py
  80. 3
      system/hardware/base.py
  81. 11
      system/hardware/tici/hardware.py
  82. 34
      system/timezoned.py
  83. 35
      tools/cabana/chart/sparkline.cc
  84. 74
      tools/car_porting/README.md
  85. 6
      tools/car_porting/auto_fingerprint.py
  86. 120
      tools/car_porting/examples/subaru_long_accel.ipynb
  87. 110
      tools/car_porting/examples/subaru_steer_temp_fault.ipynb
  88. 0
      tools/car_porting/test_car_model.py
  89. 2
      tools/joystick/joystickd.py
  90. 4
      tools/latencylogger/latency_logger.py
  91. 5
      tools/lib/helpers.py
  92. 26
      tools/lib/route.py
  93. 141
      tools/lib/srreader.py
  94. 79
      tools/lib/tests/test_caching.py
  95. 88
      tools/lib/tests/test_srreader.py
  96. 2
      tools/lib/url_file.py
  97. 71
      tools/plotjuggler/juggle.py
  98. 6
      tools/plotjuggler/test_plotjuggler.py
  99. 0
      tools/profiling/clpeak/.gitignore
  100. 0
      tools/profiling/clpeak/build.sh
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,69 +1,79 @@
CI / testing: CI / testing:
- all: - changed-files:
- changed-files: ['.github/**', '**/test_*', 'Jenkinsfile'] - any-glob-to-all-files: "{.github/**,**/test_*,Jenkinsfile}"
car: car:
- all: - changed-files:
- changed-files: ['selfdrive/car/**'] - any-glob-to-all-files: 'selfdrive/car/**'
body: body:
- all: - changed-files:
- changed-files: ['selfdrive/car/body/*'] - any-glob-to-all-files: 'selfdrive/car/body/*'
chrysler: chrysler:
- all: - changed-files:
- changed-files: ['selfdrive/car/chrysler/*'] - any-glob-to-all-files: 'selfdrive/car/chrysler/*'
ford:
- all: ford:
- changed-files: ['selfdrive/car/ford/*'] - changed-files:
gm: - any-glob-to-all-files: 'selfdrive/car/ford/*'
- all:
- changed-files: ['selfdrive/car/gm/*'] gm:
honda: - changed-files:
- all: - any-glob-to-all-files: 'selfdrive/car/gm/*'
- changed-files: ['selfdrive/car/honda/*']
hyundai: honda:
- all: - changed-files:
- changed-files: ['selfdrive/car/hyundai/*'] - any-glob-to-all-files: 'selfdrive/car/honda/*'
mazda:
- all: hyundai:
- changed-files: ['selfdrive/car/mazda/*'] - changed-files:
nissan: - any-glob-to-all-files: 'selfdrive/car/hyundai/*'
- all:
- changed-files: ['selfdrive/car/nissan/*'] mazda:
subaru: - changed-files:
- all: - any-glob-to-all-files: 'selfdrive/car/mazda/*'
- changed-files: ['selfdrive/car/subaru/*']
tesla: nissan:
- all: - changed-files:
- changed-files: ['selfdrive/car/tesla/*'] - any-glob-to-all-files: 'selfdrive/car/nissan/*'
toyota:
- all: subaru:
- changed-files: ['selfdrive/car/toyota/*'] - changed-files:
volkswagen: - any-glob-to-all-files: 'selfdrive/car/subaru/*'
- all:
- changed-files: ['selfdrive/car/volkswagen/*'] tesla:
- changed-files:
- any-glob-to-all-files: 'selfdrive/car/telsa/*'
toyota:
- changed-files:
- any-glob-to-all-files: 'selfdrive/car/toyota/*'
volkswagen:
- changed-files:
- any-glob-to-all-files: 'selfdrive/car/volkswagen/*'
fingerprint: fingerprint:
- all: - changed-files:
- changed-files: ['selfdrive/car/*/fingerprints.py'] - any-glob-to-all-files: 'selfdrive/car/*/fingerprints.py'
simulation: simulation:
- all: - changed-files:
- changed-files: ['tools/sim/**'] - any-glob-to-all-files: 'tools/sim/**'
ui: ui:
- all: - changed-files:
- changed-files: ['selfdrive/ui/**'] - any-glob-to-all-files: 'selfdrive/ui/**'
tools:
- all: tools:
- changed-files: ['tools/**'] - changed-files:
- any-glob-to-all-files: 'tools/**'
multilanguage: multilanguage:
- all: - changed-files:
- changed-files: ['selfdrive/ui/translations/**'] - any-glob-to-all-files: 'selfdrive/ui/translations/**'
research: research:
- all: - changed-files:
- changed-files: [ - any-glob-to-all-files: "{selfdrive/modeld/models/**,selfdrive/test/process_replay/model_replay_ref_commit}"
'selfdrive/modeld/models/**',
'selfdrive/test/process_replay/model_replay_ref_commit',
]

@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: false submodules: false
- uses: actions/labeler@v5.0.0-alpha.1 - uses: actions/labeler@v5.0.0
with: with:
dot: true dot: true
configuration-path: .github/labeler.yaml configuration-path: .github/labeler.yaml

@ -14,6 +14,8 @@ jobs:
name: create badges name: create badges
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
if: github.repository == 'commaai/openpilot' if: github.repository == 'commaai/openpilot'
permissions:
contents: write
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -23,6 +25,8 @@ jobs:
run: | run: |
${{ env.RUN }} "scons -j$(nproc) && python selfdrive/ui/translations/create_badges.py" ${{ env.RUN }} "scons -j$(nproc) && python selfdrive/ui/translations/create_badges.py"
rm .gitattributes
git checkout --orphan badges git checkout --orphan badges
git rm -rf --cached . git rm -rf --cached .
git config user.email "badge-researcher@comma.ai" git config user.email "badge-researcher@comma.ai"

@ -15,14 +15,19 @@ jobs:
if: github.repository == 'commaai/openpilot' if: github.repository == 'commaai/openpilot'
env: env:
PUSH_IMAGE: true PUSH_IMAGE: true
permissions:
checks: read
contents: read
packages: write
steps: steps:
- name: Wait for green check mark - name: Wait for green check mark
if: ${{ github.event_name != 'workflow_dispatch' }} if: ${{ github.event_name != 'workflow_dispatch' }}
uses: lewagon/wait-on-check-action@e2558238c09778af25867eb5de5a3ce4bbae3dcd uses: lewagon/wait-on-check-action@595dabb3acf442d47e29c9ec9ba44db0c6bdd18f
with: with:
ref: master ref: master
wait-interval: 30 wait-interval: 30
running-workflow-name: 'build prebuilt' running-workflow-name: 'build prebuilt'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build master-ci).*).)*$ check-regexp: ^((?!.*(build master-ci).*).)*$
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:

@ -14,6 +14,9 @@ jobs:
image: ghcr.io/commaai/openpilot-base:latest image: ghcr.io/commaai/openpilot-base:latest
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
if: github.repository == 'commaai/openpilot' if: github.repository == 'commaai/openpilot'
permissions:
checks: read
contents: write
steps: steps:
- name: Install wait-on-check-action dependencies - name: Install wait-on-check-action dependencies
run: | run: |
@ -21,11 +24,12 @@ jobs:
sudo apt-get install -y libyaml-dev sudo apt-get install -y libyaml-dev
- name: Wait for green check mark - name: Wait for green check mark
if: ${{ github.event_name != 'workflow_dispatch' }} if: ${{ github.event_name != 'workflow_dispatch' }}
uses: lewagon/wait-on-check-action@e2558238c09778af25867eb5de5a3ce4bbae3dcd uses: lewagon/wait-on-check-action@595dabb3acf442d47e29c9ec9ba44db0c6bdd18f
with: with:
ref: master ref: master
wait-interval: 30 wait-interval: 30
running-workflow-name: 'build master-ci' running-workflow-name: 'build master-ci'
repo-token: ${{ secrets.GITHUB_TOKEN }}
check-regexp: ^((?!.*(build prebuilt).*).)*$ check-regexp: ^((?!.*(build prebuilt).*).)*$
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:

@ -20,11 +20,11 @@ env:
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 $PWD:/tmp/openpilot -w /tmp/openpilot -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/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 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/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 BUILD_CL: selfdrive/test/docker_build.sh cl
RUN_CL: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -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 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 PYTEST: pytest --continue-on-collection-errors --cov --cov-report=xml --cov-append --durations=0 --durations-min=5 --hypothesis-seed 0
@ -51,7 +51,7 @@ 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: | run: |
cd $STRIPPED_DIR cd $STRIPPED_DIR
${{ env.RUN }} "CI=1 python selfdrive/manager/build.py" ${{ env.RUN }} "python selfdrive/manager/build.py"
- name: Run tests - name: Run tests
timeout-minutes: 3 timeout-minutes: 3
run: | run: |
@ -213,7 +213,7 @@ jobs:
- name: Run replay - name: Run replay
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
${{ env.RUN }} "CI=1 coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ ${{ env.RUN }} "coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \
chmod -R 777 /tmp/comma_download_cache && \ chmod -R 777 /tmp/comma_download_cache && \
coverage combine && \ coverage combine && \
coverage xml" coverage xml"
@ -230,7 +230,7 @@ jobs:
- name: Upload reference logs - name: Upload reference logs
if: ${{ failure() && steps.print-diff.outcome == 'success' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} if: ${{ failure() && steps.print-diff.outcome == 'success' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }}
run: | run: |
${{ env.RUN }} "unset PYTHONWARNINGS && CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" ${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only"
- name: "Upload coverage to Codecov" - name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
with: with:
@ -286,7 +286,7 @@ jobs:
timeout-minutes: 4 timeout-minutes: 4
run: | run: |
${{ env.RUN_CL }} "unset PYTHONWARNINGS && \ ${{ env.RUN_CL }} "unset PYTHONWARNINGS && \
ONNXCPU=1 CI=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

@ -20,7 +20,7 @@ jobs:
stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.' stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.'
close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.' close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.'
delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo
exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale exempt-pr-labels: "ignore stale,needs testing,car port" # if wip or it needs testing from the community, don't mark as stale
days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }}
days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }}

@ -39,10 +39,12 @@ repos:
entry: mypy entry: mypy
language: system language: system
types: [python] types: [python]
args: ['--explicit-package-bases', '--local-partial-types'] args:
- --local-partial-types
- --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 - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6 rev: v0.1.11
hooks: hooks:
- id: ruff - id: ruff
exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)' exclude: '^(third_party/)|(cereal/)|(panda/)|(rednose/)|(rednose_repo/)|(tinygrad/)|(tinygrad_repo/)|(teleoprtc/)|(teleoprtc_repo/)'
@ -87,7 +89,7 @@ repos:
args: args:
- --lock - --lock
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.27.2 rev: 0.27.3
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
# - repo: local # - repo: local

28
Jenkinsfile vendored

@ -110,18 +110,20 @@ def pcStage(String stageName, Closure body) {
return docker.build("openpilot-base:build-${env.GIT_COMMIT}", "-f Dockerfile.openpilot_base .") return docker.build("openpilot-base:build-${env.GIT_COMMIT}", "-f Dockerfile.openpilot_base .")
} }
openpilot_base.inside(dockerArgs) { lock(resource: "", label: 'pc', inversePrecedence: true, quantity: 1) {
timeout(time: 20, unit: 'MINUTES') { openpilot_base.inside(dockerArgs) {
try { timeout(time: 20, unit: 'MINUTES') {
retryWithDelay (3, 15) { try {
sh "git config --global --add safe.directory '*'" retryWithDelay (3, 15) {
sh "git submodule update --init --recursive" sh "git config --global --add safe.directory '*'"
sh "git lfs pull" sh "git submodule update --init --recursive"
} sh "git lfs pull"
body() }
} finally { body()
sh "rm -rf ${env.WORKSPACE}/* || true" } finally {
sh "rm -rf .* || true" sh "rm -rf ${env.WORKSPACE}/* || true"
sh "rm -rf .* || true"
}
} }
} }
} }
@ -241,7 +243,7 @@ node {
'replay': { 'replay': {
deviceStage("tici", "tici-replay", ["UNSAFE=1"], [ deviceStage("tici", "tici-replay", ["UNSAFE=1"], [
["build", "cd selfdrive/manager && ./build.py"], ["build", "cd selfdrive/manager && ./build.py"],
["model replay", "selfdrive/test/process_replay/model_replay.py", ["tinygrad/", "selfdrive/modeld/"]], ["model replay", "selfdrive/test/process_replay/model_replay.py"],
]) ])
}, },
'tizi': { 'tizi': {

@ -7,8 +7,8 @@ Version 0.9.6 (20XX-XX-XX)
* comma body streaming and controls over WebRTC * comma body streaming and controls over WebRTC
* 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!
* Toyota RAV4 2023 support * Toyota RAV4 2023-24 support
* Toyota RAV4 Hybrid 2023 support * Toyota RAV4 Hybrid 2023-24 support
Version 0.9.5 (2023-11-17) Version 0.9.5 (2023-11-17)
======================== ========================

@ -9,11 +9,16 @@ import SCons.Errors
SCons.Warnings.warningAsException(True) SCons.Warnings.warningAsException(True)
# pending upstream fix - https://github.com/SCons/scons/issues/4461
#SetOption('warn', 'all')
TICI = os.path.isfile('/TICI') TICI = os.path.isfile('/TICI')
AGNOS = TICI AGNOS = TICI
Decider('MD5-timestamp') Decider('MD5-timestamp')
SetOption('num_jobs', int(os.cpu_count()/2))
AddOption('--kaitai', AddOption('--kaitai',
action='store_true', action='store_true',
help='Regenerate kaitai struct parsers') help='Regenerate kaitai struct parsers')
@ -37,7 +42,7 @@ AddOption('--clazy',
AddOption('--compile_db', AddOption('--compile_db',
action='store_true', action='store_true',
help='build clang compilation database') help='build clang compilation database')
AddOption('--ccflags', AddOption('--ccflags',
action='store', action='store',
type='string', type='string',

@ -1 +1 @@
Subproject commit bceb8b942d3e622c2476e197102950efc4fe0bfd Subproject commit d81d86e7cd83d1eb40314964a4d194231381d557

@ -199,7 +199,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"UbloxAvailable", PERSISTENT}, {"UbloxAvailable", PERSISTENT},
{"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"UpdateFailedCount", CLEAR_ON_MANAGER_START}, {"UpdateFailedCount", CLEAR_ON_MANAGER_START},
{"UpdaterAvailableBranches", CLEAR_ON_MANAGER_START}, {"UpdaterAvailableBranches", PERSISTENT},
{"UpdaterCurrentDescription", CLEAR_ON_MANAGER_START}, {"UpdaterCurrentDescription", CLEAR_ON_MANAGER_START},
{"UpdaterCurrentReleaseNotes", CLEAR_ON_MANAGER_START}, {"UpdaterCurrentReleaseNotes", CLEAR_ON_MANAGER_START},
{"UpdaterFetchAvailable", CLEAR_ON_MANAGER_START}, {"UpdaterFetchAvailable", CLEAR_ON_MANAGER_START},

@ -1,45 +0,0 @@
import time
class Profiler():
def __init__(self, enabled=False):
self.enabled = enabled
self.cp = {}
self.cp_ignored = []
self.iter = 0
self.start_time = time.time()
self.last_time = self.start_time
self.tot = 0.
def reset(self, enabled=False):
self.enabled = enabled
self.cp = {}
self.cp_ignored = []
self.iter = 0
self.start_time = time.time()
self.last_time = self.start_time
def checkpoint(self, name, ignore=False):
# ignore flag needed when benchmarking threads with ratekeeper
if not self.enabled:
return
tt = time.time()
if name not in self.cp:
self.cp[name] = 0.
if ignore:
self.cp_ignored.append(name)
self.cp[name] += tt - self.last_time
if not ignore:
self.tot += tt - self.last_time
self.last_time = tt
def display(self):
if not self.enabled:
return
self.iter += 1
print("******* Profiling %d *******" % self.iter)
for n, ms in sorted(self.cp.items(), key=lambda x: -x[1]):
if n in self.cp_ignored:
print("%30s: %9.2f avg: %7.2f percent: %3.0f IGNORED" % (n, ms*1000.0, ms*1000.0/self.iter, ms/self.tot*100))
else:
print("%30s: %9.2f avg: %7.2f percent: %3.0f" % (n, ms*1000.0, ms*1000.0/self.iter, ms/self.tot*100))
print(f"Iter clock: {self.tot / self.iter:2.6f} TOTAL: {self.tot:2.2f}")

@ -78,7 +78,7 @@ class Ratekeeper:
time.sleep(self._remaining) time.sleep(self._remaining)
return lagged return lagged
# this only monitor the cumulative lag, but does not enforce a rate # Monitors the cumulative lag, but does not enforce a rate
def monitor_time(self) -> bool: def monitor_time(self) -> bool:
prev = self._last_monitor_time prev = self._last_monitor_time
self._last_monitor_time = time.monotonic() self._last_monitor_time = time.monotonic()

@ -10,11 +10,11 @@ Get paid to improve openpilot!
* open a ticket at [comma.ai/support](https://comma.ai/support/shop-order) with links to your PRs to claim * open a ticket at [comma.ai/support](https://comma.ai/support/shop-order) with links to your PRs to claim
* get an extra 20% if you redeem your bounty in [comma shop](https://comma.ai/shop) credit * get an extra 20% if you redeem your bounty in [comma shop](https://comma.ai/shop) credit
New bounties can be proposed in the **#contributing** channel in Discord. New bounties can be proposed in the [**#contributing**](https://discord.com/channels/469524606043160576/1183173332531687454) channel in Discord.
## Issue bounties ## Issue bounties
We've tagged bounty eligible issues across openpilot and the rest of our repos; check out all the open ones [here](https://github.com/orgs/commaai/projects/26/views/1). These bounties roughly work out like this: We've tagged bounty-eligible issues across openpilot and the rest of our repos; check out all the open ones [here](https://github.com/orgs/commaai/projects/26/views/1). These bounties roughly work out like this:
* **$100** - a few hours of work for an experienced openpilot developer; a good intro for someone new to openpilot * **$100** - a few hours of work for an experienced openpilot developer; a good intro for someone new to openpilot
* **$300** - a day of work for an experienced openpilot developer * **$300** - a day of work for an experienced openpilot developer
* **$500** - a few days of work for an experienced openpilot developer * **$500** - a few days of work for an experienced openpilot developer

@ -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.
# 273 Supported Cars # 274 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|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@ -132,7 +132,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|Niro EV 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 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=Kia&model=Niro EV 2021">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Kia|Niro EV 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 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=Kia&model=Niro EV 2021">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Kia|Niro EV 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 H 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=Kia&model=Niro EV 2022">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>| |Kia|Niro EV 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 H 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=Kia&model=Niro EV 2022">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|Kia|Niro EV 2023[<sup>6</sup>](#footnotes)|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 A 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=Kia&model=Niro EV 2023">Buy Here</a></sub></details>|| |Kia|Niro EV 2023[<sup>6</sup>](#footnotes)|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 A 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=Kia&model=Niro EV 2023">Buy Here</a></sub></details>||
|Kia|Niro Hybrid 2021-22|Smart Cruise Control (SCC)|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=Kia&model=Niro Hybrid 2021-22">Buy Here</a></sub></details>|| |Kia|Niro Hybrid 2021|Smart Cruise Control (SCC)|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 D 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=Kia&model=Niro Hybrid 2021">Buy Here</a></sub></details>||
|Kia|Niro Hybrid 2022|Smart Cruise Control (SCC)|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=Kia&model=Niro Hybrid 2022">Buy Here</a></sub></details>||
|Kia|Niro Hybrid 2023[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|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 A 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=Kia&model=Niro Hybrid 2023">Buy Here</a></sub></details>|| |Kia|Niro Hybrid 2023[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|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 A 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=Kia&model=Niro Hybrid 2023">Buy Here</a></sub></details>||
|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 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=Kia&model=Niro Plug-in Hybrid 2018-19">Buy Here</a></sub></details>|| |Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 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=Kia&model=Niro Plug-in Hybrid 2018-19">Buy Here</a></sub></details>||
|Kia|Niro Plug-in Hybrid 2020|All|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai D 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=Kia&model=Niro Plug-in Hybrid 2020">Buy Here</a></sub></details>|| |Kia|Niro Plug-in Hybrid 2020|All|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai D 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=Kia&model=Niro Plug-in Hybrid 2020">Buy Here</a></sub></details>||
@ -239,12 +240,12 @@ A supported vehicle is one that just works when you install a comma device. All
|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|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 2023|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">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 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|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 2023|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">Buy Here</a></sub></details>|| |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|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>|

@ -11,7 +11,7 @@ Our software is open source so you can solve your own problems without needing h
## What contributions are we looking for? ## What contributions are we looking for?
**openpilot's priorities are [safety](SAFETY.md), stability, quality, and features, in that order.** openpilot is part of comma's mission to *solve self-driving cars while delivering shippable intermediaries*, and **all** developoment is towards that goal. **openpilot's priorities are [safety](SAFETY.md), stability, quality, and features, in that order.** openpilot is part of comma's mission to *solve self-driving cars while delivering shippable intermediaries*, and **all** development is towards that goal.
### What gets merged? ### What gets merged?

@ -1 +1 @@
Subproject commit 0cff7a8aa2df3be711cab4570bb422cd8661cb8b Subproject commit 40d9c723d48496229fecc436046538a53af19c11

@ -1 +1 @@
Subproject commit 114b85a649341d55d6beb36d7414eda5e6d324a2 Subproject commit 2a0536c63148a02add52555386b5533f3555ef58

1884
poetry.lock generated

File diff suppressed because it is too large Load Diff

@ -125,7 +125,7 @@ inputs = "*"
Jinja2 = "*" Jinja2 = "*"
lru-dict = "*" lru-dict = "*"
matplotlib = "*" matplotlib = "*"
metadrive-simulator = { git = "https://github.com/metadriverse/metadrive.git", rev ="72e842cd1d025bf676e4af8797a01e4aa282109f", markers = "platform_machine != 'aarch64'" } # no linux/aarch64 wheels for certain dependencies metadrive-simulator = { git = "https://github.com/metadriverse/metadrive.git", rev ="main", markers = "platform_machine != 'aarch64'" } # no linux/aarch64 wheels for certain dependencies
mpld3 = "*" mpld3 = "*"
mypy = "*" mypy = "*"
myst-parser = "*" myst-parser = "*"

@ -1 +1 @@
Subproject commit 44e8a891a2810f274a1fa980775155d9463e87b9 Subproject commit 18b91458fd396530d43e1a2fe9a3ac9055fa9109

@ -291,11 +291,8 @@ system/camerad/main.cc
system/camerad/snapshot/* system/camerad/snapshot/*
system/camerad/cameras/camera_common.h system/camerad/cameras/camera_common.h
system/camerad/cameras/camera_common.cc system/camerad/cameras/camera_common.cc
system/camerad/sensors/ar0231.cc system/camerad/sensors/*.h
system/camerad/sensors/ar0231_registers.h system/camerad/sensors/*.cc
system/camerad/sensors/ox03c10.cc
system/camerad/sensors/ox03c10_registers.h
system/camerad/sensors/sensor.h
selfdrive/manager/__init__.py selfdrive/manager/__init__.py
selfdrive/manager/build.py selfdrive/manager/build.py

@ -637,6 +637,8 @@ def ws_proxy_recv(ws: WebSocket, local_sock: socket.socket, ssock: socket.socket
while not (end_event.is_set() or global_end_event.is_set()): while not (end_event.is_set() or global_end_event.is_set()):
try: try:
data = ws.recv() data = ws.recv()
if isinstance(data, str):
data = data.encode("utf-8")
local_sock.sendall(data) local_sock.sendall(data)
except WebSocketTimeoutException: except WebSocketTimeoutException:
pass pass
@ -728,10 +730,11 @@ def ws_manage(ws: WebSocket, end_event: threading.Event) -> None:
if onroad != onroad_prev: if onroad != onroad_prev:
onroad_prev = onroad onroad_prev = onroad
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0) if sock is not None:
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 2 if onroad else 3) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 2 if onroad else 3)
if end_event.wait(5): if end_event.wait(5):
break break

@ -1,63 +1,3 @@
# selfdrive/car
Check out [this blog post](https://blog.comma.ai/how-to-write-a-car-port-for-openpilot/) for a high-level overview of porting a car.
## Useful car porting utilities
Testing car ports in your car is very time-consuming. Check out these utilities to do basic checks on your work before running it in your car.
### [Cabana](/tools/cabana/README.md)
View your car's CAN signals through DBC files, which openpilot uses to parse and create messages that talk to the car.
Example:
```bash
> tools/cabana/cabana '1bbe6bf2d62f58a8|2022-07-14--17-11-43'
```
### [selfdrive/debug/auto_fingerprint.py](/selfdrive/debug/auto_fingerprint.py)
Given a route and platform, automatically inserts FW fingerprints from the platform into the correct place in values.py
Example:
```bash
> python selfdrive/debug/auto_fingerprint.py '1bbe6bf2d62f58a8|2022-07-14--17-11-43' 'SUBARU OUTBACK 6TH GEN'
Attempting to add fw version for: SUBARU OUTBACK 6TH GEN
```
### [selfdrive/car/tests/test_car_interfaces.py](/selfdrive/car/tests/test_car_interfaces.py)
Finds common bugs for car interfaces, without even requiring a route.
#### Example: Typo in signal name
```bash
> pytest selfdrive/car/tests/test_car_interfaces.py -k subaru # replace with the brand you are working on
=====================================================================
FAILED selfdrive/car/tests/test_car_interfaces.py::TestCarInterfaces::test_car_interfaces_165_SUBARU_LEGACY_7TH_GEN - KeyError: 'CruiseControlOOPS'
```
### [selfdrive/debug/test_car_model.py](/selfdrive/debug/test_car_model.py)
Given a route, runs most of the car interface to check for common errors like missing signals, blocked panda messages, and safety mismatches.
#### Example: panda safety mismatch for gasPressed
```bash
> python selfdrive/debug/test_car_model.py '4822a427b188122a|2023-08-14--16-22-21'
=====================================================================
FAIL: test_panda_safety_carstate (__main__.CarModelTestCase.test_panda_safety_carstate)
Assert that panda safety matches openpilot's carState
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/batman/xx/openpilot/openpilot/selfdrive/car/tests/test_models.py", line 380, in test_panda_safety_carstate
self.assertFalse(len(failed_checks), f"panda safety doesn't agree with openpilot: {failed_checks}")
AssertionError: 1 is not false : panda safety doesn't agree with openpilot: {'gasPressed': 116}
```
## Car port structure ## Car port structure
### interface.py ### interface.py

@ -244,10 +244,13 @@ class CarInfo:
# 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)
def __post_init__(self):
self.make, self.model, self.years = split_name(self.name)
self.year_list = get_year_list(self.years)
def init(self, CP: 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
self.make, self.model, self.years = split_name(self.name)
# longitudinal column # longitudinal column
op_long = "Stock" op_long = "Stock"
@ -309,7 +312,6 @@ class CarInfo:
self.row[Column.STEERING_TORQUE] = Star.FULL self.row[Column.STEERING_TORQUE] = Star.FULL
self.all_footnotes = all_footnotes self.all_footnotes = all_footnotes
self.year_list = get_year_list(self.years)
self.detail_sentence = self.get_detail_sentence(CP) self.detail_sentence = self.get_detail_sentence(CP)
return self return self

@ -154,21 +154,6 @@ class CarController:
if self.frame % 10 == 0: if self.frame % 10 == 0:
can_sends.append(gmcan.create_pscm_status(self.packer_pt, CanBus.CAMERA, CS.pscm_status)) can_sends.append(gmcan.create_pscm_status(self.packer_pt, CanBus.CAMERA, CS.pscm_status))
# Show green icon when LKA torque is applied, and
# alarming orange icon when approaching torque limit.
# If not sent again, LKA icon disappears in about 5 seconds.
# Conveniently, sending camera message periodically also works as a keepalive.
lka_active = CS.lkas_status == 1
lka_critical = lka_active and abs(actuators.steer) > 0.9
lka_icon_status = (lka_active, lka_critical)
# SW_GMLAN not yet on cam harness, no HUD alerts
if self.CP.networkLocation != NetworkLocation.fwdCamera and \
(self.frame % self.params.CAMERA_KEEPALIVE_STEP == 0 or lka_icon_status != self.lka_icon_status_last):
steer_alert = hud_alert in (VisualAlert.steerRequired, VisualAlert.ldw)
can_sends.append(gmcan.create_lka_icon_command(CanBus.SW_GMLAN, lka_active, lka_critical, steer_alert))
self.lka_icon_status_last = lka_icon_status
new_actuators = actuators.copy() new_actuators = actuators.copy()
new_actuators.steer = self.apply_steer_last / self.params.STEER_MAX new_actuators.steer = self.apply_steer_last / self.params.STEER_MAX
new_actuators.steerOutputCan = self.apply_steer_last new_actuators.steerOutputCan = self.apply_steer_last

@ -139,7 +139,6 @@ class CanBus:
OBSTACLE = 1 OBSTACLE = 1
CAMERA = 2 CAMERA = 2
CHASSIS = 2 CHASSIS = 2
SW_GMLAN = 3
LOOPBACK = 128 LOOPBACK = 128
DROPPED = 192 DROPPED = 192

@ -744,9 +744,6 @@ FW_VERSIONS = {
b'78109-TPG-A110\x00\x00', b'78109-TPG-A110\x00\x00',
b'78109-TPG-A210\x00\x00', b'78109-TPG-A210\x00\x00',
], ],
(Ecu.hud, 0x18da61f1, None): [
b'78209-TLA-X010\x00\x00',
],
(Ecu.fwdRadar, 0x18dab0f1, None): [ (Ecu.fwdRadar, 0x18dab0f1, None): [
b'36802-TMB-H040\x00\x00', b'36802-TMB-H040\x00\x00',
b'36802-TPA-E040\x00\x00', b'36802-TPA-E040\x00\x00',

@ -147,8 +147,9 @@ class CarState(CarStateBase):
aeb_src = "FCA11" if self.CP.flags & HyundaiFlags.USE_FCA.value else "SCC12" aeb_src = "FCA11" if self.CP.flags & HyundaiFlags.USE_FCA.value else "SCC12"
aeb_sig = "FCA_CmdAct" if self.CP.flags & HyundaiFlags.USE_FCA.value else "AEB_CmdAct" aeb_sig = "FCA_CmdAct" if self.CP.flags & HyundaiFlags.USE_FCA.value else "AEB_CmdAct"
aeb_warning = cp_cruise.vl[aeb_src]["CF_VSM_Warn"] != 0 aeb_warning = cp_cruise.vl[aeb_src]["CF_VSM_Warn"] != 0
scc_warning = cp_cruise.vl["SCC12"]["TakeOverReq"] == 1 # sometimes only SCC system shows an FCW
aeb_braking = cp_cruise.vl[aeb_src]["CF_VSM_DecCmdAct"] != 0 or cp_cruise.vl[aeb_src][aeb_sig] != 0 aeb_braking = cp_cruise.vl[aeb_src]["CF_VSM_DecCmdAct"] != 0 or cp_cruise.vl[aeb_src][aeb_sig] != 0
ret.stockFcw = aeb_warning and not aeb_braking ret.stockFcw = (aeb_warning or scc_warning) and not aeb_braking
ret.stockAeb = aeb_warning and aeb_braking ret.stockAeb = aeb_warning and aeb_braking
if self.CP.enableBsm: if self.CP.enableBsm:

@ -251,7 +251,8 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = {
], ],
CAR.KIA_NIRO_PHEV_2022: HyundaiCarInfo("Kia Niro Plug-in Hybrid 2022", "All", car_parts=CarParts.common([CarHarness.hyundai_f])), CAR.KIA_NIRO_PHEV_2022: HyundaiCarInfo("Kia Niro Plug-in Hybrid 2022", "All", car_parts=CarParts.common([CarHarness.hyundai_f])),
CAR.KIA_NIRO_HEV_2021: [ CAR.KIA_NIRO_HEV_2021: [
HyundaiCarInfo("Kia Niro Hybrid 2021-22", car_parts=CarParts.common([CarHarness.hyundai_f])), # TODO: 2021 could be hyundai_d, verify HyundaiCarInfo("Kia Niro Hybrid 2021", car_parts=CarParts.common([CarHarness.hyundai_d])),
HyundaiCarInfo("Kia Niro Hybrid 2022", car_parts=CarParts.common([CarHarness.hyundai_f])),
], ],
CAR.KIA_NIRO_HEV_2ND_GEN: HyundaiCarInfo("Kia Niro Hybrid 2023", car_parts=CarParts.common([CarHarness.hyundai_a])), CAR.KIA_NIRO_HEV_2ND_GEN: HyundaiCarInfo("Kia Niro Hybrid 2023", car_parts=CarParts.common([CarHarness.hyundai_a])),
CAR.KIA_OPTIMA_G4: HyundaiCarInfo("Kia Optima 2017", "Advanced Smart Cruise Control", CAR.KIA_OPTIMA_G4: HyundaiCarInfo("Kia Optima 2017", "Advanced Smart Cruise Control",

@ -122,7 +122,7 @@ class CarInterface(CarInterfaceBase):
ret.openpilotLongitudinalControl = experimental_long and ret.experimentalLongitudinalAvailable ret.openpilotLongitudinalControl = experimental_long and ret.experimentalLongitudinalAvailable
if candidate in GLOBAL_GEN2 and ret.openpilotLongitudinalControl: if candidate in GLOBAL_GEN2 and ret.openpilotLongitudinalControl:
ret.flags |= SubaruFlags.DISABLE_EYESIGHT ret.flags |= SubaruFlags.DISABLE_EYESIGHT.value
if ret.openpilotLongitudinalControl: if ret.openpilotLongitudinalControl:
ret.longitudinalTuning.kpBP = [0., 5., 35.] ret.longitudinalTuning.kpBP = [0., 5., 35.]

@ -48,8 +48,7 @@ def create_es_distance(packer, frame, es_distance_msg, bus, pcm_cancel_cmd, long
values["Cruise_Soft_Disable"] = 0 values["Cruise_Soft_Disable"] = 0
values["Cruise_Fault"] = 0 values["Cruise_Fault"] = 0
if brake_cmd: values["Cruise_Brake_Active"] = brake_cmd
values["Cruise_Brake_Active"] = 1
if pcm_cancel_cmd: if pcm_cancel_cmd:
values["Cruise_Cancel"] = 1 values["Cruise_Cancel"] = 1
@ -153,14 +152,14 @@ def create_es_dashstatus(packer, frame, dashstatus_msg, enabled, long_enabled, l
values["COUNTER"] = frame % 0x10 values["COUNTER"] = frame % 0x10
if enabled and long_active: if long_enabled:
values["Cruise_State"] = 0 values["Cruise_State"] = 0
values["Cruise_Activated"] = 1 values["Cruise_Activated"] = enabled
values["Cruise_Disengaged"] = 0 values["Cruise_Disengaged"] = 0
values["Car_Follow"] = int(lead_visible) values["Car_Follow"] = int(lead_visible)
if long_enabled:
values["PCB_Off"] = 1 # AEB is not presevered, so show the PCB_Off on dash values["PCB_Off"] = 1 # AEB is not presevered, so show the PCB_Off on dash
values["LDW_Off"] = 0
values["Cruise_Fault"] = 0 values["Cruise_Fault"] = 0
# Filter stock LKAS disabled and Keep hands on steering wheel OFF alerts # Filter stock LKAS disabled and Keep hands on steering wheel OFF alerts
@ -186,15 +185,12 @@ def create_es_brake(packer, frame, es_brake_msg, long_enabled, long_active, brak
if long_enabled: if long_enabled:
values["Cruise_Brake_Fault"] = 0 values["Cruise_Brake_Fault"] = 0
values["Cruise_Activated"] = long_active
if long_active: values["Brake_Pressure"] = brake_value
values["Cruise_Activated"] = 1
values["Brake_Pressure"] = brake_value values["Cruise_Brake_Active"] = brake_value > 0
values["Cruise_Brake_Lights"] = brake_value >= 70
if brake_value > 0:
values["Cruise_Brake_Active"] = 1
values["Cruise_Brake_Lights"] = 1 if brake_value >= 70 else 0
return packer.make_can_msg("ES_Brake", CanBus.main, values) return packer.make_can_msg("ES_Brake", CanBus.main, values)
@ -204,7 +200,6 @@ def create_es_status(packer, frame, es_status_msg, long_enabled, long_active, cr
"Signal1", "Signal1",
"Cruise_Fault", "Cruise_Fault",
"Cruise_RPM", "Cruise_RPM",
"Signal2",
"Cruise_Activated", "Cruise_Activated",
"Brake_Lights", "Brake_Lights",
"Cruise_Hold", "Cruise_Hold",
@ -217,8 +212,7 @@ def create_es_status(packer, frame, es_status_msg, long_enabled, long_active, cr
values["Cruise_RPM"] = cruise_rpm values["Cruise_RPM"] = cruise_rpm
values["Cruise_Fault"] = 0 values["Cruise_Fault"] = 0
if long_active: values["Cruise_Activated"] = long_active
values["Cruise_Activated"] = 1
return packer.make_can_msg("ES_Status", CanBus.main, values) return packer.make_can_msg("ES_Status", CanBus.main, values)

@ -95,8 +95,8 @@ if __name__ == "__main__":
_jerks["down_jerk"] > MAX_LAT_JERK_DOWN _jerks["down_jerk"] > MAX_LAT_JERK_DOWN
violation_str = " - VIOLATION" if violation else "" violation_str = " - VIOLATION" if violation else ""
print(f"{car_model:{max_car_model_len}} - up jerk: {round(_jerks['up_jerk'], 2):5} \ print(f"{car_model:{max_car_model_len}} - up jerk: {round(_jerks['up_jerk'], 2):5} " +
m/s^3, down jerk: {round(_jerks['down_jerk'], 2):5} m/s^3{violation_str}") f"m/s^3, down jerk: {round(_jerks['down_jerk'], 2):5} m/s^3{violation_str}")
# exit with test result # exit with test result
sys.exit(not result.result.wasSuccessful()) sys.exit(not result.result.wasSuccessful())

@ -21,6 +21,7 @@ from openpilot.selfdrive.car.car_helpers import FRAME_FINGERPRINT, interfaces
from openpilot.selfdrive.car.honda.values import CAR as HONDA, HONDA_BOSCH from openpilot.selfdrive.car.honda.values import CAR as HONDA, HONDA_BOSCH
from openpilot.selfdrive.car.tests.routes import non_tested_cars, routes, CarTestRoute from openpilot.selfdrive.car.tests.routes import non_tested_cars, routes, CarTestRoute
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.openpilotci import get_url from openpilot.selfdrive.test.openpilotci import get_url
from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.logreader import LogReader
from openpilot.tools.lib.route import Route, SegmentName, RouteName from openpilot.tools.lib.route import Route, SegmentName, RouteName
@ -36,9 +37,9 @@ 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", "50"))
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, Optional[CarTestRoute]]]:
# build list of test cases # build list of test cases
test_cases = [] test_cases = []
@ -52,12 +53,9 @@ def get_test_cases() -> List[Tuple[str, Optional[CarTestRoute]]]:
test_cases.extend(sorted((c.value, r) for r in routes_by_car.get(c, (None,)))) test_cases.extend(sorted((c.value, r) for r in routes_by_car.get(c, (None,))))
else: else:
with open(os.path.join(BASEDIR, INTERNAL_SEG_LIST), "r") as f: segment_list = read_segment_list(os.path.join(BASEDIR, INTERNAL_SEG_LIST))
seg_list = f.read().splitlines() segment_list = random.sample(segment_list, INTERNAL_SEG_CNT or len(segment_list))
for platform, segment in segment_list:
seg_list_grouped = [(platform[2:], segment) for platform, segment in zip(seg_list[::2], seg_list[1::2], strict=True)]
seg_list_grouped = random.sample(seg_list_grouped, INTERNAL_SEG_CNT or len(seg_list_grouped))
for platform, segment in seg_list_grouped:
segment_name = SegmentName(segment) segment_name = SegmentName(segment)
test_cases.append((platform, CarTestRoute(segment_name.route_name.canonical_name, platform, test_cases.append((platform, CarTestRoute(segment_name.route_name.canonical_name, platform,
segment=segment_name.segment_num))) segment=segment_name.segment_num)))
@ -68,7 +66,7 @@ def get_test_cases() -> List[Tuple[str, Optional[CarTestRoute]]]:
class TestCarModelBase(unittest.TestCase): class TestCarModelBase(unittest.TestCase):
car_model: Optional[str] = None car_model: Optional[str] = None
test_route: Optional[CarTestRoute] = None test_route: Optional[CarTestRoute] = 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]]
@ -81,19 +79,80 @@ class TestCarModelBase(unittest.TestCase):
route_name = RouteName(cls.test_route.route) route_name = RouteName(cls.test_route.route)
return LogReader(f"cd:/{route_name.dongle_id}/{route_name.time_str}/{seg}/rlog.bz2") return LogReader(f"cd:/{route_name.dongle_id}/{route_name.time_str}/{seg}/rlog.bz2")
else: else:
# Attempt to use CI bucket first return LogReader(get_url(cls.test_route.route, seg))
try:
return LogReader(get_url(cls.test_route.route, seg)) @classmethod
except Exception: def get_testing_data_from_logreader(cls, lr):
cls.test_route_on_bucket = False car_fw = []
can_msgs = []
cls.elm_frame = None
cls.car_safety_mode_frame = None
cls.fingerprint = gen_empty_fingerprint()
experimental_long = False
for msg in lr:
if msg.which() == "can":
can_msgs.append(msg)
if len(can_msgs) <= FRAME_FINGERPRINT:
for m in msg.can:
if m.src < 64:
cls.fingerprint[m.src][m.address] = len(m.dat)
elif msg.which() == "carParams":
car_fw = msg.carParams.carFw
if msg.carParams.openpilotLongitudinalControl:
experimental_long = True
if cls.car_model is None and not cls.ci:
cls.car_model = msg.carParams.carFingerprint
# Log which can frame the panda safety mode left ELM327, for CAN validity checks
elif msg.which() == 'pandaStates':
for ps in msg.pandaStates:
if cls.elm_frame is None and ps.safetyModel != SafetyModel.elm327:
cls.elm_frame = len(can_msgs)
if cls.car_safety_mode_frame is None and ps.safetyModel not in \
(SafetyModel.elm327, SafetyModel.noOutput):
cls.car_safety_mode_frame = len(can_msgs)
# Fallback to public route, which will fail the test_route_on_ci_bucket when running in CI elif msg.which() == 'pandaStateDEPRECATED':
if cls.elm_frame is None and msg.pandaStateDEPRECATED.safetyModel != SafetyModel.elm327:
cls.elm_frame = len(can_msgs)
if cls.car_safety_mode_frame is None and msg.pandaStateDEPRECATED.safetyModel not in \
(SafetyModel.elm327, SafetyModel.noOutput):
cls.car_safety_mode_frame = len(can_msgs)
if len(can_msgs) > int(50 / DT_CTRL):
return car_fw, can_msgs, experimental_long
raise Exception("no can data found")
@classmethod
def get_testing_data(cls):
test_segs = (2, 1, 0)
if cls.test_route.segment is not None:
test_segs = (cls.test_route.segment,)
# Try the primary method first (CI or internal)
for seg in test_segs:
try: try:
return LogReader(Route(cls.test_route.route).log_paths()[seg]) lr = cls.get_logreader(seg)
return cls.get_testing_data_from_logreader(lr)
except Exception: except Exception:
pass pass
raise Exception("Unable to get route. Check that the route is valid, and either public or uploaded to the CI bucket.") # 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
if not len(INTERNAL_SEG_LIST):
cls.test_route_on_bucket = False
for seg in test_segs:
try:
lr = LogReader(Route(cls.test_route.route).log_paths()[seg])
return cls.get_testing_data_from_logreader(lr)
except Exception:
pass
raise Exception(f"Route: {repr(cls.test_route.route)} with segments: {test_segs} not found or no CAN msgs found. Is it uploaded and public?")
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -110,57 +169,7 @@ class TestCarModelBase(unittest.TestCase):
raise unittest.SkipTest raise unittest.SkipTest
raise Exception(f"missing test route for {cls.car_model}") raise Exception(f"missing test route for {cls.car_model}")
test_segs = (2, 1, 0) car_fw, can_msgs, experimental_long = cls.get_testing_data()
if cls.test_route.segment is not None:
test_segs = (cls.test_route.segment,)
for seg in test_segs:
try:
lr = cls.get_logreader(seg)
except Exception:
continue
car_fw = []
can_msgs = []
cls.elm_frame = None
cls.car_safety_mode_frame = None
cls.fingerprint = gen_empty_fingerprint()
experimental_long = False
for msg in lr:
if msg.which() == "can":
can_msgs.append(msg)
if len(can_msgs) <= FRAME_FINGERPRINT:
for m in msg.can:
if m.src < 64:
cls.fingerprint[m.src][m.address] = len(m.dat)
elif msg.which() == "carParams":
car_fw = msg.carParams.carFw
if msg.carParams.openpilotLongitudinalControl:
experimental_long = True
if cls.car_model is None and not cls.ci:
cls.car_model = msg.carParams.carFingerprint
# Log which can frame the panda safety mode left ELM327, for CAN validity checks
elif msg.which() == 'pandaStates':
for ps in msg.pandaStates:
if cls.elm_frame is None and ps.safetyModel != SafetyModel.elm327:
cls.elm_frame = len(can_msgs)
if cls.car_safety_mode_frame is None and ps.safetyModel not in \
(SafetyModel.elm327, SafetyModel.noOutput):
cls.car_safety_mode_frame = len(can_msgs)
elif msg.which() == 'pandaStateDEPRECATED':
if cls.elm_frame is None and msg.pandaStateDEPRECATED.safetyModel != SafetyModel.elm327:
cls.elm_frame = len(can_msgs)
if cls.car_safety_mode_frame is None and msg.pandaStateDEPRECATED.safetyModel not in \
(SafetyModel.elm327, SafetyModel.noOutput):
cls.car_safety_mode_frame = len(can_msgs)
if len(can_msgs) > int(50 / DT_CTRL):
break
else:
raise Exception(f"Route: {repr(cls.test_route.route)} with segments: {test_segs} not found or no CAN msgs found. Is it uploaded and public?")
# if relay is expected to be open in the route # if relay is expected to be open in the route
cls.openpilot_enabled = cls.car_safety_mode_frame is not None cls.openpilot_enabled = cls.car_safety_mode_frame is not None
@ -466,10 +475,10 @@ class TestCarModelBase(unittest.TestCase):
failed_checks = {k: v for k, v in checks.items() if v > 0} failed_checks = {k: v for k, v in checks.items() if v > 0}
self.assertFalse(len(failed_checks), f"panda safety doesn't agree with openpilot: {failed_checks}") self.assertFalse(len(failed_checks), f"panda safety doesn't agree with openpilot: {failed_checks}")
@pytest.mark.skipif(not CI, reason="When running in CI we want to make sure all the routes are uploaded to the preserved CI bucket.") @unittest.skipIf(not CI, "Accessing non CI-bucket routes is allowed only when not in CI")
def test_route_on_ci_bucket(self): def test_route_on_ci_bucket(self):
assert self.test_route_on_bucket, "Route not on CI bucket. \ self.assertTrue(self.test_route_on_bucket, "Route not on CI bucket. " +
This is fine to fail for WIP car ports, just let us know and we can upload your routes to the CI bucket." "This is fine to fail for WIP car ports, just let us know and we can upload your routes to the CI bucket.")
@parameterized_class(('car_model', 'test_route'), get_test_cases()) @parameterized_class(('car_model', 'test_route'), get_test_cases())

@ -23,6 +23,7 @@ FW_VERSIONS = {
(Ecu.fwdRadar, 0x750, 0xf): [ (Ecu.fwdRadar, 0x750, 0xf): [
b'8821F4702000\x00\x00\x00\x00', b'8821F4702000\x00\x00\x00\x00',
b'8821F4702100\x00\x00\x00\x00', b'8821F4702100\x00\x00\x00\x00',
b'8821F4702300\x00\x00\x00\x00',
], ],
(Ecu.fwdCamera, 0x750, 0x6d): [ (Ecu.fwdCamera, 0x750, 0x6d): [
b'8646F0701100\x00\x00\x00\x00', b'8646F0701100\x00\x00\x00\x00',
@ -51,6 +52,7 @@ FW_VERSIONS = {
b'8965B41090\x00\x00\x00\x00\x00\x00', b'8965B41090\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.engine, 0x700, None): [ (Ecu.engine, 0x700, None): [
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'\x01896630735100\x00\x00\x00\x00', b'\x01896630735100\x00\x00\x00\x00',
@ -142,6 +144,8 @@ FW_VERSIONS = {
(Ecu.dsu, 0x791, None): [ (Ecu.dsu, 0x791, None): [
b'8821F0601200 ', b'8821F0601200 ',
b'8821F0601300 ', b'8821F0601300 ',
b'8821F0601400 ',
b'8821F0601500 ',
b'8821F0602000 ', b'8821F0602000 ',
b'8821F0603300 ', b'8821F0603300 ',
b'8821F0603400 ', b'8821F0603400 ',
@ -185,6 +189,8 @@ FW_VERSIONS = {
(Ecu.fwdRadar, 0x750, 0xf): [ (Ecu.fwdRadar, 0x750, 0xf): [
b'8821F0601200 ', b'8821F0601200 ',
b'8821F0601300 ', b'8821F0601300 ',
b'8821F0601400 ',
b'8821F0601500 ',
b'8821F0602000 ', b'8821F0602000 ',
b'8821F0603300 ', b'8821F0603300 ',
b'8821F0603400 ', b'8821F0603400 ',
@ -497,6 +503,7 @@ FW_VERSIONS = {
b'\x018965B12510\x00\x00\x00\x00\x00\x00', b'\x018965B12510\x00\x00\x00\x00\x00\x00',
b'\x018965B12520\x00\x00\x00\x00\x00\x00', b'\x018965B12520\x00\x00\x00\x00\x00\x00',
b'\x018965B12530\x00\x00\x00\x00\x00\x00', b'\x018965B12530\x00\x00\x00\x00\x00\x00',
b'\x018965B1254000\x00\x00\x00\x00',
b'\x018965B1255000\x00\x00\x00\x00', b'\x018965B1255000\x00\x00\x00\x00',
b'\x018965B1256000\x00\x00\x00\x00', b'\x018965B1256000\x00\x00\x00\x00',
b'8965B12361\x00\x00\x00\x00\x00\x00', b'8965B12361\x00\x00\x00\x00\x00\x00',
@ -571,6 +578,7 @@ FW_VERSIONS = {
b'\x028646F1202000\x00\x00\x00\x008646G2601200\x00\x00\x00\x00', b'\x028646F1202000\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
b'\x028646F1202100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F1202100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
b'\x028646F1202200\x00\x00\x00\x008646G2601500\x00\x00\x00\x00', b'\x028646F1202200\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
b'\x028646F1206000\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
b'\x028646F1601100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F1601100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
b'\x028646F1601200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F1601200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
b'\x028646F1601300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F1601300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
@ -578,6 +586,7 @@ FW_VERSIONS = {
b'\x028646F76020C0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00', b'\x028646F76020C0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00',
b'\x028646F7603100\x00\x00\x00\x008646G2601200\x00\x00\x00\x00', b'\x028646F7603100\x00\x00\x00\x008646G2601200\x00\x00\x00\x00',
b'\x028646F7603200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F7603200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
b'\x028646F7603300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
b'\x028646F7605100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00', b'\x028646F7605100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
], ],
}, },
@ -588,6 +597,7 @@ FW_VERSIONS = {
b'\x01896630E43100\x00\x00\x00\x00', b'\x01896630E43100\x00\x00\x00\x00',
b'\x01896630E43200\x00\x00\x00\x00', b'\x01896630E43200\x00\x00\x00\x00',
b'\x01896630E44200\x00\x00\x00\x00', b'\x01896630E44200\x00\x00\x00\x00',
b'\x01896630E44400\x00\x00\x00\x00',
b'\x01896630E45000\x00\x00\x00\x00', b'\x01896630E45000\x00\x00\x00\x00',
b'\x01896630E45100\x00\x00\x00\x00', b'\x01896630E45100\x00\x00\x00\x00',
b'\x01896630E45200\x00\x00\x00\x00', b'\x01896630E45200\x00\x00\x00\x00',
@ -728,6 +738,7 @@ FW_VERSIONS = {
b'\x018966353Q4000\x00\x00\x00\x00', b'\x018966353Q4000\x00\x00\x00\x00',
b'\x018966353R1100\x00\x00\x00\x00', b'\x018966353R1100\x00\x00\x00\x00',
b'\x018966353R7100\x00\x00\x00\x00', b'\x018966353R7100\x00\x00\x00\x00',
b'\x018966353R8000\x00\x00\x00\x00',
b'\x018966353R8100\x00\x00\x00\x00', b'\x018966353R8100\x00\x00\x00\x00',
], ],
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
@ -792,6 +803,7 @@ FW_VERSIONS = {
b'\x02896634761100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634761100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634761200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634761200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634762000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634762000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634762100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634763000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634763000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634763100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634763100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634765000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634765000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
@ -800,6 +812,7 @@ FW_VERSIONS = {
b'\x02896634769100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634769100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634769200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634769200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634770000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634770000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634770100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634774000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634774000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634774100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634774100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
b'\x02896634774200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634774200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00',
@ -812,6 +825,7 @@ FW_VERSIONS = {
b'\x03896634759100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634759100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
b'\x03896634759200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634759200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
b'\x03896634759200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00', b'\x03896634759200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00',
b'\x03896634759300\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
b'\x03896634759300\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00', b'\x03896634759300\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00',
b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701002\x00\x00\x00\x00', b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701002\x00\x00\x00\x00',
b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00',
@ -1123,9 +1137,11 @@ FW_VERSIONS = {
CAR.RAV4_TSS2_2023: { CAR.RAV4_TSS2_2023: {
(Ecu.abs, 0x7b0, None): [ (Ecu.abs, 0x7b0, None): [
b'\x01F15260R450\x00\x00\x00\x00\x00\x00', b'\x01F15260R450\x00\x00\x00\x00\x00\x00',
b'\x01F15260R51000\x00\x00\x00\x00',
b'\x01F15264283200\x00\x00\x00\x00', b'\x01F15264283200\x00\x00\x00\x00',
b'\x01F15264283300\x00\x00\x00\x00', b'\x01F15264283300\x00\x00\x00\x00',
b'\x01F152642F1000\x00\x00\x00\x00', b'\x01F152642F1000\x00\x00\x00\x00',
b'\x01F152642F8000\x00\x00\x00\x00',
], ],
(Ecu.eps, 0x7a1, None): [ (Ecu.eps, 0x7a1, None): [
b'\x028965B0R11000\x00\x00\x00\x008965B0R12000\x00\x00\x00\x00', b'\x028965B0R11000\x00\x00\x00\x008965B0R12000\x00\x00\x00\x00',
@ -1137,6 +1153,7 @@ FW_VERSIONS = {
b'\x01896634AE1001\x00\x00\x00\x00', b'\x01896634AE1001\x00\x00\x00\x00',
b'\x01896634AF0000\x00\x00\x00\x00', b'\x01896634AF0000\x00\x00\x00\x00',
b'\x01896634AJ2000\x00\x00\x00\x00', b'\x01896634AJ2000\x00\x00\x00\x00',
b'\x01896634AL5000\x00\x00\x00\x00',
], ],
(Ecu.fwdRadar, 0x750, 0xf): [ (Ecu.fwdRadar, 0x750, 0xf): [
b'\x018821F0R03100\x00\x00\x00\x00', b'\x018821F0R03100\x00\x00\x00\x00',
@ -1144,6 +1161,7 @@ FW_VERSIONS = {
(Ecu.fwdCamera, 0x750, 0x6d): [ (Ecu.fwdCamera, 0x750, 0x6d): [
b'\x028646F0R05100\x00\x00\x00\x008646G0R02100\x00\x00\x00\x00', b'\x028646F0R05100\x00\x00\x00\x008646G0R02100\x00\x00\x00\x00',
b'\x028646F0R05200\x00\x00\x00\x008646G0R02200\x00\x00\x00\x00', b'\x028646F0R05200\x00\x00\x00\x008646G0R02200\x00\x00\x00\x00',
b'\x028646F0R11000\x00\x00\x00\x008646G0R04000\x00\x00\x00\x00',
], ],
}, },
CAR.SIENNA: { CAR.SIENNA: {
@ -1237,6 +1255,7 @@ FW_VERSIONS = {
b'\x018821F3301200\x00\x00\x00\x00', b'\x018821F3301200\x00\x00\x00\x00',
b'\x018821F3301300\x00\x00\x00\x00', b'\x018821F3301300\x00\x00\x00\x00',
b'\x018821F3301400\x00\x00\x00\x00', b'\x018821F3301400\x00\x00\x00\x00',
b'\x018821F6201200\x00\x00\x00\x00',
b'\x018821F6201300\x00\x00\x00\x00', b'\x018821F6201300\x00\x00\x00\x00',
b'\x018821F6201400\x00\x00\x00\x00', b'\x018821F6201400\x00\x00\x00\x00',
], ],
@ -1249,6 +1268,7 @@ FW_VERSIONS = {
b'\x028646F3304200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F3304200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00',
b'\x028646F3304300\x00\x00\x00\x008646G2601500\x00\x00\x00\x00', b'\x028646F3304300\x00\x00\x00\x008646G2601500\x00\x00\x00\x00',
b'\x028646F3309100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00', b'\x028646F3309100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00',
b'\x028646F3309100\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',
], ],
}, },
CAR.LEXUS_ES: { CAR.LEXUS_ES: {
@ -1319,6 +1339,7 @@ FW_VERSIONS = {
b'\x01896637851000\x00\x00\x00\x00', b'\x01896637851000\x00\x00\x00\x00',
b'\x01896637852000\x00\x00\x00\x00', b'\x01896637852000\x00\x00\x00\x00',
b'\x01896637854000\x00\x00\x00\x00', b'\x01896637854000\x00\x00\x00\x00',
b'\x01896637873000\x00\x00\x00\x00',
b'\x01896637878000\x00\x00\x00\x00', b'\x01896637878000\x00\x00\x00\x00',
], ],
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
@ -1391,6 +1412,7 @@ FW_VERSIONS = {
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',
b'\x01896632478100\x00\x00\x00\x00',
b'\x01896632478200\x00\x00\x00\x00', b'\x01896632478200\x00\x00\x00\x00',
], ],
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
@ -1439,6 +1461,7 @@ FW_VERSIONS = {
b'\x018966348R1300\x00\x00\x00\x00', b'\x018966348R1300\x00\x00\x00\x00',
b'\x018966348R8500\x00\x00\x00\x00', b'\x018966348R8500\x00\x00\x00\x00',
b'\x018966348W1300\x00\x00\x00\x00', b'\x018966348W1300\x00\x00\x00\x00',
b'\x018966348W2300\x00\x00\x00\x00',
], ],
(Ecu.abs, 0x7b0, None): [ (Ecu.abs, 0x7b0, None): [
b'F152648472\x00\x00\x00\x00\x00\x00', b'F152648472\x00\x00\x00\x00\x00\x00',
@ -1483,6 +1506,7 @@ FW_VERSIONS = {
b'\x02348Q4000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02348Q4000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x02348Q4100\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02348Q4100\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x02348T1100\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02348T1100\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x02348T1200\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x02348T3000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02348T3000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x02348V6000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02348V6000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x02348Z3000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02348Z3000\x00\x00\x00\x00\x00\x00\x00\x00A4802000\x00\x00\x00\x00\x00\x00\x00\x00',
@ -1503,6 +1527,7 @@ FW_VERSIONS = {
(Ecu.eps, 0x7a1, None): [ (Ecu.eps, 0x7a1, None): [
b'8965B0E011\x00\x00\x00\x00\x00\x00', b'8965B0E011\x00\x00\x00\x00\x00\x00',
b'8965B0E012\x00\x00\x00\x00\x00\x00', b'8965B0E012\x00\x00\x00\x00\x00\x00',
b'8965B48102\x00\x00\x00\x00\x00\x00',
b'8965B48111\x00\x00\x00\x00\x00\x00', b'8965B48111\x00\x00\x00\x00\x00\x00',
b'8965B48112\x00\x00\x00\x00\x00\x00', b'8965B48112\x00\x00\x00\x00\x00\x00',
], ],
@ -1638,6 +1663,7 @@ FW_VERSIONS = {
b'8965B58052\x00\x00\x00\x00\x00\x00', b'8965B58052\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.abs, 0x7b0, None): [ (Ecu.abs, 0x7b0, None): [
b'F152658320\x00\x00\x00\x00\x00\x00',
b'F152658341\x00\x00\x00\x00\x00\x00', b'F152658341\x00\x00\x00\x00\x00\x00',
], ],
(Ecu.fwdRadar, 0x750, 0xf): [ (Ecu.fwdRadar, 0x750, 0xf): [

@ -180,8 +180,8 @@ CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = {
ToyotaCarInfo("Toyota RAV4 Hybrid 2022", video_link="https://youtu.be/U0nH9cnrFB0"), ToyotaCarInfo("Toyota RAV4 Hybrid 2022", video_link="https://youtu.be/U0nH9cnrFB0"),
], ],
CAR.RAV4_TSS2_2023: [ CAR.RAV4_TSS2_2023: [
ToyotaCarInfo("Toyota RAV4 2023"), ToyotaCarInfo("Toyota RAV4 2023-24"),
ToyotaCarInfo("Toyota RAV4 Hybrid 2023"), ToyotaCarInfo("Toyota RAV4 Hybrid 2023-24"),
], ],
CAR.MIRAI: ToyotaCarInfo("Toyota Mirai 2021"), CAR.MIRAI: ToyotaCarInfo("Toyota Mirai 2021"),
CAR.SIENNA: ToyotaCarInfo("Toyota Sienna 2018-20", video_link="https://www.youtube.com/watch?v=q1UPOo4Sh68", min_enable_speed=MIN_ACC_SPEED), CAR.SIENNA: ToyotaCarInfo("Toyota Sienna 2018-20", video_link="https://www.youtube.com/watch?v=q1UPOo4Sh68", min_enable_speed=MIN_ACC_SPEED),

@ -5,7 +5,7 @@ from openpilot.common.conversions import Conversions as CV
from openpilot.common.realtime import DT_CTRL from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.car import apply_driver_steer_torque_limits from openpilot.selfdrive.car import apply_driver_steer_torque_limits
from openpilot.selfdrive.car.volkswagen import mqbcan, pqcan from openpilot.selfdrive.car.volkswagen import mqbcan, pqcan
from openpilot.selfdrive.car.volkswagen.values import CANBUS, PQ_CARS, CarControllerParams from openpilot.selfdrive.car.volkswagen.values import CANBUS, PQ_CARS, CarControllerParams, VolkswagenFlags
VisualAlert = car.CarControl.HUDControl.VisualAlert VisualAlert = car.CarControl.HUDControl.VisualAlert
LongCtrlState = car.CarControl.Actuators.LongControlState LongCtrlState = car.CarControl.Actuators.LongControlState
@ -65,13 +65,22 @@ class CarController:
self.apply_steer_last = apply_steer self.apply_steer_last = apply_steer
can_sends.append(self.CCS.create_steering_control(self.packer_pt, CANBUS.pt, apply_steer, hca_enabled)) can_sends.append(self.CCS.create_steering_control(self.packer_pt, CANBUS.pt, apply_steer, hca_enabled))
if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
# Pacify VW Emergency Assist driver inactivity detection by changing its view of driver steering input torque
# to the greatest of actual driver input or 2x openpilot's output (1x openpilot output is not enough to
# consistently reset inactivity detection on straight level roads). See commaai/openpilot#23274 for background.
ea_simulated_torque = clip(apply_steer * 2, -self.CCP.STEER_MAX, self.CCP.STEER_MAX)
if abs(CS.out.steeringTorque) > abs(ea_simulated_torque):
ea_simulated_torque = CS.out.steeringTorque
can_sends.append(self.CCS.create_eps_update(self.packer_pt, CANBUS.cam, CS.eps_stock_values, ea_simulated_torque))
# **** Acceleration Controls ******************************************** # # **** Acceleration Controls ******************************************** #
if self.frame % self.CCP.ACC_CONTROL_STEP == 0 and self.CP.openpilotLongitudinalControl: if self.frame % self.CCP.ACC_CONTROL_STEP == 0 and self.CP.openpilotLongitudinalControl:
acc_control = self.CCS.acc_control_value(CS.out.cruiseState.available, CS.out.accFaulted, CC.longActive) acc_control = self.CCS.acc_control_value(CS.out.cruiseState.available, CS.out.accFaulted, CC.longActive)
accel = clip(actuators.accel, self.CCP.ACCEL_MIN, self.CCP.ACCEL_MAX) if CC.longActive else 0 accel = clip(actuators.accel, self.CCP.ACCEL_MIN, self.CCP.ACCEL_MAX) if CC.longActive else 0
stopping = actuators.longControlState == LongCtrlState.stopping stopping = actuators.longControlState == LongCtrlState.stopping
starting = actuators.longControlState == LongCtrlState.starting starting = actuators.longControlState == LongCtrlState.pid and (CS.esp_hold_confirmation or CS.out.vEgo < self.CP.vEgoStopping)
can_sends.extend(self.CCS.create_acc_accel_control(self.packer_pt, CANBUS.pt, CS.acc_type, CC.longActive, accel, can_sends.extend(self.CCS.create_acc_accel_control(self.packer_pt, CANBUS.pt, CS.acc_type, CC.longActive, accel,
acc_control, stopping, starting, CS.esp_hold_confirmation)) acc_control, stopping, starting, CS.esp_hold_confirmation))

@ -4,7 +4,7 @@ from openpilot.common.conversions import Conversions as CV
from openpilot.selfdrive.car.interfaces import CarStateBase from openpilot.selfdrive.car.interfaces import CarStateBase
from opendbc.can.parser import CANParser from opendbc.can.parser import CANParser
from openpilot.selfdrive.car.volkswagen.values import DBC, CANBUS, PQ_CARS, NetworkLocation, TransmissionType, GearShifter, \ from openpilot.selfdrive.car.volkswagen.values import DBC, CANBUS, PQ_CARS, NetworkLocation, TransmissionType, GearShifter, \
CarControllerParams CarControllerParams, VolkswagenFlags
class CarState(CarStateBase): class CarState(CarStateBase):
@ -14,6 +14,7 @@ class CarState(CarStateBase):
self.button_states = {button.event_type: False for button in self.CCP.BUTTONS} self.button_states = {button.event_type: False for button in self.CCP.BUTTONS}
self.esp_hold_confirmation = False self.esp_hold_confirmation = False
self.upscale_lead_car_signal = False self.upscale_lead_car_signal = False
self.eps_stock_values = False
def create_button_events(self, pt_cp, buttons): def create_button_events(self, pt_cp, buttons):
button_events = [] button_events = []
@ -59,6 +60,11 @@ class CarState(CarStateBase):
ret.steerFaultPermanent = hca_status in ("DISABLED", "FAULT") ret.steerFaultPermanent = hca_status in ("DISABLED", "FAULT")
ret.steerFaultTemporary = hca_status in ("INITIALIZING", "REJECTED") ret.steerFaultTemporary = hca_status in ("INITIALIZING", "REJECTED")
# VW Emergency Assist status tracking and mitigation
self.eps_stock_values = pt_cp.vl["LH_EPS_03"]
if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
ret.carFaultedNonCritical = bool(cam_cp.vl["HCA_01"]["EA_Ruckfreigabe"]) or cam_cp.vl["HCA_01"]["EA_ACC_Sollstatus"] > 0
# Update gas, brakes, and gearshift. # Update gas, brakes, and gearshift.
ret.gas = pt_cp.vl["Motor_20"]["MO_Fahrpedalrohwert_01"] / 100.0 ret.gas = pt_cp.vl["Motor_20"]["MO_Fahrpedalrohwert_01"] / 100.0
ret.gasPressed = ret.gas > 0 ret.gasPressed = ret.gas > 0
@ -293,6 +299,11 @@ class CarState(CarStateBase):
messages = [] messages = []
if CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
messages += [
("HCA_01", 1), # From R242 Driver assistance camera, 50Hz if steering/1Hz if not
]
if CP.networkLocation == NetworkLocation.fwdCamera: if CP.networkLocation == NetworkLocation.fwdCamera:
messages += [ messages += [
# sig_address, frequency # sig_address, frequency

@ -3,7 +3,7 @@ from panda import Panda
from openpilot.common.conversions import Conversions as CV from openpilot.common.conversions import Conversions as CV
from openpilot.selfdrive.car import get_safety_config from openpilot.selfdrive.car import get_safety_config
from openpilot.selfdrive.car.interfaces import CarInterfaceBase from openpilot.selfdrive.car.interfaces import CarInterfaceBase
from openpilot.selfdrive.car.volkswagen.values import CAR, PQ_CARS, CANBUS, NetworkLocation, TransmissionType, GearShifter from openpilot.selfdrive.car.volkswagen.values import CAR, PQ_CARS, CANBUS, NetworkLocation, TransmissionType, GearShifter, VolkswagenFlags
ButtonType = car.CarState.ButtonEvent.Type ButtonType = car.CarState.ButtonEvent.Type
EventName = car.CarEvent.EventName EventName = car.CarEvent.EventName
@ -67,6 +67,9 @@ class CarInterface(CarInterfaceBase):
else: else:
ret.networkLocation = NetworkLocation.fwdCamera ret.networkLocation = NetworkLocation.fwdCamera
if 0x126 in fingerprint[2]: # HCA_01
ret.flags |= VolkswagenFlags.STOCK_HCA_PRESENT.value
# Global lateral tuning defaults, can be overridden per-vehicle # Global lateral tuning defaults, can be overridden per-vehicle
ret.steerActuatorDelay = 0.1 ret.steerActuatorDelay = 0.1
@ -90,11 +93,9 @@ class CarInterface(CarInterfaceBase):
ret.pcmCruise = not ret.openpilotLongitudinalControl ret.pcmCruise = not ret.openpilotLongitudinalControl
ret.stoppingControl = True ret.stoppingControl = True
ret.startingState = True
ret.startAccel = 1.0
ret.stopAccel = -0.55 ret.stopAccel = -0.55
ret.vEgoStarting = 1.0 ret.vEgoStarting = 0.1
ret.vEgoStopping = 1.0 ret.vEgoStopping = 0.5
ret.longitudinalTuning.kpV = [0.1] ret.longitudinalTuning.kpV = [0.1]
ret.longitudinalTuning.kiV = [0.0] ret.longitudinalTuning.kiV = [0.0]

@ -10,6 +10,24 @@ def create_steering_control(packer, bus, apply_steer, lkas_enabled):
return packer.make_can_msg("HCA_01", bus, values) return packer.make_can_msg("HCA_01", bus, values)
def create_eps_update(packer, bus, eps_stock_values, ea_simulated_torque):
values = {s: eps_stock_values[s] for s in [
"COUNTER", # Sync counter value to EPS output
"EPS_Lenkungstyp", # EPS rack type
"EPS_Berechneter_LW", # Absolute raw steering angle
"EPS_VZ_BLW", # Raw steering angle sign
"EPS_HCA_Status", # EPS HCA control status
]}
values.update({
# Absolute driver torque input and sign, with EA inactivity mitigation
"EPS_Lenkmoment": abs(ea_simulated_torque),
"EPS_VZ_Lenkmoment": 1 if ea_simulated_torque < 0 else 0,
})
return packer.make_can_msg("LH_EPS_03", bus, values)
def create_lka_hud_control(packer, bus, ldw_stock_values, enabled, steering_pressed, hud_alert, hud_control): def create_lka_hud_control(packer, bus, ldw_stock_values, enabled, steering_pressed, hud_alert, hud_control):
values = {} values = {}
if len(ldw_stock_values): if len(ldw_stock_values):
@ -94,7 +112,7 @@ def create_acc_accel_control(packer, bus, acc_type, acc_enabled, accel, acc_cont
acc_hold_type = 0 acc_hold_type = 0
acc_07_values = { acc_07_values = {
"ACC_Anhalteweg": 0.75 if stopping else 20.46, # Distance to stop (stopping coordinator handles terminal roll-out) "ACC_Anhalteweg": 0.3 if stopping else 20.46, # Distance to stop (stopping coordinator handles terminal roll-out)
"ACC_Freilauf_Info": 2 if acc_enabled else 0, "ACC_Freilauf_Info": 2 if acc_enabled else 0,
"ACC_Folgebeschl": 3.02, # Not using secondary controller accel unless and until we understand its impact "ACC_Folgebeschl": 3.02, # Not using secondary controller accel unless and until we understand its impact
"ACC_Sollbeschleunigung_02": accel if acc_enabled else 3.01, "ACC_Sollbeschleunigung_02": accel if acc_enabled else 3.01,

@ -1,6 +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, StrEnum from enum import Enum, IntFlag, StrEnum
from typing import Dict, List, Union from typing import Dict, List, Union
from cereal import car from cereal import car
@ -109,6 +109,10 @@ class CANBUS:
cam = 2 cam = 2
class VolkswagenFlags(IntFlag):
STOCK_HCA_PRESENT = 1
# Check the 7th and 8th characters of the VIN before adding a new CAR. If the # Check the 7th and 8th characters of the VIN before adding a new CAR. If the
# chassis code is already listed below, don't add a new CAR, just add to the # chassis code is already listed below, don't add a new CAR, just add to the
# FW_VERSIONS for that existing CAR. # FW_VERSIONS for that existing CAR.

@ -8,7 +8,6 @@ from typing import SupportsFloat
from cereal import car, log from cereal import car, log
from openpilot.common.numpy_fast import clip from openpilot.common.numpy_fast import clip
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL
from openpilot.common.profiler import Profiler
from openpilot.common.params import Params from openpilot.common.params import Params
import cereal.messaging as messaging import cereal.messaging as messaging
from cereal.visionipc import VisionIpcClient, VisionStreamType from cereal.visionipc import VisionIpcClient, VisionStreamType
@ -195,7 +194,6 @@ class Controls:
# controlsd is driven by can recv, expected at 100Hz # controlsd is driven by can recv, expected at 100Hz
self.rk = Ratekeeper(100, print_delay_threshold=None) self.rk = Ratekeeper(100, print_delay_threshold=None)
self.prof = Profiler(False) # off by default
def set_initial_state(self): def set_initial_state(self):
if REPLAY: if REPLAY:
@ -851,12 +849,10 @@ class Controls:
def step(self): def step(self):
start_time = time.monotonic() start_time = time.monotonic()
self.prof.checkpoint("Ratekeeper", ignore=True)
# Sample data from sockets and get a carState # Sample data from sockets and get a carState
CS = self.data_sample() CS = self.data_sample()
cloudlog.timestamp("Data sampled") cloudlog.timestamp("Data sampled")
self.prof.checkpoint("Sample")
self.update_events(CS) self.update_events(CS)
cloudlog.timestamp("Events updated") cloudlog.timestamp("Events updated")
@ -864,16 +860,12 @@ class Controls:
if not self.CP.passive and self.initialized: if not self.CP.passive and self.initialized:
# Update control state # Update control state
self.state_transition(CS) self.state_transition(CS)
self.prof.checkpoint("State transition")
# Compute actuators (runs PID loops and lateral MPC) # Compute actuators (runs PID loops and lateral MPC)
CC, lac_log = self.state_control(CS) CC, lac_log = self.state_control(CS)
self.prof.checkpoint("State Control")
# Publish data # Publish data
self.publish_logs(CS, start_time, CC, lac_log) self.publish_logs(CS, start_time, CC, lac_log)
self.prof.checkpoint("Sent")
self.CS_prev = CS self.CS_prev = CS
@ -893,7 +885,6 @@ class Controls:
while True: while True:
self.step() self.step()
self.rk.monitor_time() self.rk.monitor_time()
self.prof.display()
except SystemExit: except SystemExit:
e.set() e.set()
t.join() t.join()

@ -47,13 +47,14 @@ def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
class LongitudinalPlanner: class LongitudinalPlanner:
def __init__(self, CP, init_v=0.0, init_a=0.0): def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
self.CP = CP self.CP = CP
self.mpc = LongitudinalMpc() self.mpc = LongitudinalMpc()
self.fcw = False self.fcw = False
self.dt = dt
self.a_desired = init_a self.a_desired = init_a
self.v_desired_filter = FirstOrderFilter(init_v, 2.0, DT_MDL) self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt)
self.v_model_error = 0.0 self.v_model_error = 0.0
self.v_desired_trajectory = np.zeros(CONTROL_N) self.v_desired_trajectory = np.zeros(CONTROL_N)
@ -148,8 +149,8 @@ class LongitudinalPlanner:
# Interpolate 0.05 seconds and save as starting point for next iteration # Interpolate 0.05 seconds and save as starting point for next iteration
a_prev = self.a_desired a_prev = self.a_desired
self.a_desired = float(interp(DT_MDL, ModelConstants.T_IDXS[:CONTROL_N], self.a_desired_trajectory)) self.a_desired = float(interp(self.dt, ModelConstants.T_IDXS[:CONTROL_N], self.a_desired_trajectory))
self.v_desired_filter.x = self.v_desired_filter.x + DT_MDL * (self.a_desired + a_prev) / 2.0 self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.a_desired + a_prev) / 2.0
def publish(self, sm, pm): def publish(self, sm, pm):
plan_send = messaging.new_message('longitudinalPlan') plan_send = messaging.new_message('longitudinalPlan')

@ -3,10 +3,12 @@ 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
from openpilot.tools.lib.logreader import logreader_from_route_or_segment from openpilot.tools.lib.logreader import LogIterable
from openpilot.tools.lib.srreader import SegmentRangeReader
RED = '\033[91m' RED = '\033[91m'
CLEAR = '\033[0m' CLEAR = '\033[0m'
@ -95,13 +97,15 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
init_lr, new_lr = None, None init_lr: Optional[LogIterable] = None
new_lr: Optional[LogIterable] = None
if args.init: if args.init:
if args.init == '': if args.init == '':
init_lr = [] init_lr = []
else: else:
init_lr = logreader_from_route_or_segment(args.init) init_lr = SegmentRangeReader(args.init)
if args.comp: if args.comp:
new_lr = logreader_from_route_or_segment(args.comp) new_lr = SegmentRangeReader(args.comp)
can_printer(args.bus, init_msgs=init_lr, new_msgs=new_lr, table=args.table) can_printer(args.bus, init_msgs=init_lr, new_msgs=new_lr, table=args.table)

@ -4,16 +4,12 @@ import math
import datetime import datetime
from collections import Counter from collections import Counter
from pprint import pprint from pprint import pprint
from tqdm import tqdm
from typing import List, Tuple, cast from typing import List, Tuple, cast
from cereal.services import SERVICE_LIST from cereal.services import SERVICE_LIST
from openpilot.tools.lib.route import Route from openpilot.tools.lib.srreader import SegmentRangeReader, ReadMode
from openpilot.tools.lib.logreader import LogReader
if __name__ == "__main__": if __name__ == "__main__":
r = Route(sys.argv[1])
cnt_valid: Counter = Counter() cnt_valid: Counter = Counter()
cnt_events: Counter = Counter() cnt_events: Counter = Counter()
@ -24,31 +20,29 @@ if __name__ == "__main__":
start_time = math.inf start_time = math.inf
end_time = -math.inf end_time = -math.inf
ignition_off = None ignition_off = None
for q in tqdm(r.qlog_paths()): for msg in SegmentRangeReader(sys.argv[1], ReadMode.QLOG):
if q is None: end_time = max(end_time, msg.logMonoTime)
continue start_time = min(start_time, msg.logMonoTime)
lr = list(LogReader(q))
for msg in lr:
end_time = max(end_time, msg.logMonoTime)
start_time = min(start_time, msg.logMonoTime)
if msg.which() == 'onroadEvents': if msg.which() == 'onroadEvents':
for e in msg.onroadEvents: for e in msg.onroadEvents:
cnt_events[e.name] += 1 cnt_events[e.name] += 1
elif msg.which() == 'controlsState': elif msg.which() == 'controlsState':
if len(alerts) == 0 or alerts[-1][1] != msg.controlsState.alertType: at = msg.controlsState.alertType
if "/override" not in at or "lanechange" in at.lower():
if len(alerts) == 0 or alerts[-1][1] != at:
t = (msg.logMonoTime - start_time) / 1e9 t = (msg.logMonoTime - start_time) / 1e9
alerts.append((t, msg.controlsState.alertType)) alerts.append((t, at))
elif msg.which() == 'pandaStates': elif msg.which() == 'pandaStates':
if ignition_off is None: if ignition_off is None:
ign = any(ps.ignitionLine or ps.ignitionCan for ps in msg.pandaStates) ign = any(ps.ignitionLine or ps.ignitionCan for ps in msg.pandaStates)
if not ign: if not ign:
ignition_off = msg.logMonoTime ignition_off = msg.logMonoTime
elif msg.which() in cams: elif msg.which() in cams:
cnt_cameras[msg.which()] += 1 cnt_cameras[msg.which()] += 1
if not msg.valid: if not msg.valid:
cnt_valid[msg.which()] += 1 cnt_valid[msg.which()] += 1
duration = (end_time - start_time) / 1e9 duration = (end_time - start_time) / 1e9

@ -1,11 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import argparse import argparse
import json import json
import cereal.messaging as messaging import cereal.messaging as messaging
from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.srreader import SegmentRangeReader
from openpilot.tools.lib.route import Route
LEVELS = { LEVELS = {
"DEBUG": 10, "DEBUG": 10,
@ -53,31 +51,18 @@ if __name__ == "__main__":
parser.add_argument("route", type=str, nargs='*', help="route name + segment number for offline usage") parser.add_argument("route", type=str, nargs='*', help="route name + segment number for offline usage")
args = parser.parse_args() args = parser.parse_args()
logs = None
if len(args.route):
if os.path.exists(args.route[0]):
logs = [args.route[0]]
else:
r = Route(args.route[0])
logs = [q_log if r_log is None else r_log for (q_log, r_log) in zip(r.qlog_paths(), r.log_paths(), strict=True)]
if len(args.route) == 2 and logs:
n = int(args.route[1])
logs = [logs[n]]
min_level = LEVELS[args.level] min_level = LEVELS[args.level]
if logs: if args.route:
for log in logs: for route in args.route:
if log: lr = SegmentRangeReader(route)
lr = LogReader(log) for m in lr:
for m in lr: if m.which() == 'logMessage':
if m.which() == 'logMessage': print_logmessage(m.logMonoTime, m.logMessage, min_level)
print_logmessage(m.logMonoTime, m.logMessage, min_level) elif m.which() == 'errorLogMessage':
elif m.which() == 'errorLogMessage' and 'qlog' in log: print_logmessage(m.logMonoTime, m.errorLogMessage, min_level)
print_logmessage(m.logMonoTime, m.errorLogMessage, min_level) elif m.which() == 'androidLog':
elif m.which() == 'androidLog': print_androidlog(m.logMonoTime, m.androidLog)
print_androidlog(m.logMonoTime, m.androidLog)
else: else:
sm = messaging.SubMaster(['logMessage', 'androidLog'], addr=args.addr) sm = messaging.SubMaster(['logMessage', 'androidLog'], addr=args.addr)
while True: while True:

@ -1,8 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
from openpilot.tools.lib.route import Route from openpilot.tools.lib.srreader import ReadMode, SegmentRangeReader
from openpilot.tools.lib.logreader import MultiLogIterator
def get_fingerprint(lr): def get_fingerprint(lr):
@ -40,6 +39,5 @@ if __name__ == "__main__":
print("Usage: ./fingerprint_from_route.py <route>") print("Usage: ./fingerprint_from_route.py <route>")
sys.exit(1) sys.exit(1)
route = Route(sys.argv[1]) lr = SegmentRangeReader(sys.argv[1], ReadMode.QLOG)
lr = MultiLogIterator(route.log_paths()[:5])
get_fingerprint(lr) get_fingerprint(lr)

@ -8,9 +8,10 @@ from openpilot.selfdrive.car.docs import get_all_car_info
from openpilot.selfdrive.car.docs_definitions import Column from openpilot.selfdrive.car.docs_definitions import Column
FOOTNOTE_TAG = "<sup>{}</sup>" FOOTNOTE_TAG = "<sup>{}</sup>"
STAR_ICON = '<a href="##"><img valign="top" src="https://raw.githubusercontent.com/commaai/openpilot/master/docs/assets/icon-star-{}.svg" width="22" /></a>' STAR_ICON = '<a href="##"><img valign="top" ' + \
VIDEO_ICON = '<a href="{}" target="_blank">\ 'src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-star-{}.svg" width="22" /></a>'
<img height="18px" src="https://raw.githubusercontent.com/commaai/openpilot/master/docs/assets/icon-youtube.svg"></img></a>' VIDEO_ICON = '<a href="{}" target="_blank">' + \
'<img height="18px" src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-youtube.svg"></img></a>'
COLUMNS = "|" + "|".join([column.value for column in Column]) + "|" COLUMNS = "|" + "|".join([column.value for column in Column]) + "|"
COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3))) COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3)))
ARROW_SYMBOL = "" ARROW_SYMBOL = ""

@ -3,14 +3,12 @@
import argparse import argparse
from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, replay_process from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, replay_process
from openpilot.tools.lib.logreader import MultiLogIterator
from openpilot.tools.lib.route import Route
from openpilot.tools.lib.helpers import save_log from openpilot.tools.lib.helpers import save_log
from openpilot.tools.lib.srreader import SegmentRangeReader
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run process on route and create new logs", parser = argparse.ArgumentParser(description="Run process on route and create new logs",
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--qlog", help="Use qlog instead of log", action="store_true")
parser.add_argument("--fingerprint", help="The fingerprint to use") parser.add_argument("--fingerprint", help="The fingerprint to use")
parser.add_argument("route", help="The route name to use") parser.add_argument("route", help="The route name to use")
parser.add_argument("process", help="The process to run") parser.add_argument("process", help="The process to run")
@ -18,8 +16,7 @@ if __name__ == "__main__":
cfg = [c for c in CONFIGS if c.proc_name == args.process][0] cfg = [c for c in CONFIGS if c.proc_name == args.process][0]
route = Route(args.route) lr = SegmentRangeReader(args.route)
lr = MultiLogIterator(route.qlog_paths() if args.qlog else route.log_paths())
inputs = list(lr) inputs = list(lr)
outputs = replay_process(cfg, inputs, fingerprint=args.fingerprint) outputs = replay_process(cfg, inputs, fingerprint=args.fingerprint)
@ -29,5 +26,5 @@ if __name__ == "__main__":
inputs = [i for i in inputs if i.which() not in produces] inputs = [i for i in inputs if i.which() not in produces]
outputs = sorted(inputs + outputs, key=lambda x: x.logMonoTime) outputs = sorted(inputs + outputs, key=lambda x: x.logMonoTime)
fn = f"{args.route}_{args.process}.bz2" fn = f"{args.route.replace('/', '_')}_{args.process}.bz2"
save_log(fn, outputs) save_log(fn, outputs)

@ -1,111 +0,0 @@
#!/usr/bin/env python3
'''
printing the gap between interrupts in a histogram to check if the
frequency is what we expect, the bmx is not interrupt driven for as we
get interrupts in a 2kHz rate.
'''
import argparse
import sys
import numpy as np
from collections import defaultdict
from openpilot.tools.lib.logreader import LogReader
from openpilot.tools.lib.route import Route
import matplotlib.pyplot as plt
SRC_BMX = "bmx055"
SRC_LSM = "lsm6ds3"
def parseEvents(log_reader):
bmx_data = defaultdict(list)
lsm_data = defaultdict(list)
for m in log_reader:
if m.which() not in ['accelerometer', 'gyroscope']:
continue
d = getattr(m, m.which()).to_dict()
if d["source"] == SRC_BMX and "acceleration" in d:
bmx_data["accel"].append(d["timestamp"] / 1e9)
if d["source"] == SRC_BMX and "gyroUncalibrated" in d:
bmx_data["gyro"].append(d["timestamp"] / 1e9)
if d["source"] == SRC_LSM and "acceleration" in d:
lsm_data["accel"].append(d["timestamp"] / 1e9)
if d["source"] == SRC_LSM and "gyroUncalibrated" in d:
lsm_data["gyro"].append(d["timestamp"] / 1e9)
return bmx_data, lsm_data
def cleanData(data):
if len(data) == 0:
return [], []
data.sort()
diffs = np.diff(data)
return data, diffs
def logAvgValues(data, sensor):
if len(data) == 0:
print(f"{sensor}: no data to average")
return
avg = sum(data) / len(data)
hz = 1 / avg
print(f"{sensor}: data_points: {len(data)} avg [ns]: {avg} avg [Hz]: {hz}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("route", type=str, help="route name")
parser.add_argument("segment", type=int, help="segment number")
args = parser.parse_args()
r = Route(args.route)
logs = r.log_paths()
if len(logs) == 0:
print("NO data routes")
sys.exit(0)
if args.segment >= len(logs):
print(f"RouteID: {args.segment} out of range, max: {len(logs) -1}")
sys.exit(0)
lr = LogReader(logs[args.segment])
bmx_data, lsm_data = parseEvents(lr)
# sort bmx accel data, and then cal all the diffs, and to a histogram of those
bmx_accel, bmx_accel_diffs = cleanData(bmx_data["accel"])
bmx_gyro, bmx_gyro_diffs = cleanData(bmx_data["gyro"])
lsm_accel, lsm_accel_diffs = cleanData(lsm_data["accel"])
lsm_gyro, lsm_gyro_diffs = cleanData(lsm_data["gyro"])
# get out the averages
logAvgValues(bmx_accel_diffs, "bmx accel")
logAvgValues(bmx_gyro_diffs, "bmx gyro ")
logAvgValues(lsm_accel_diffs, "lsm accel")
logAvgValues(lsm_gyro_diffs, "lsm gyro ")
fig, axs = plt.subplots(1, 2, tight_layout=True)
axs[0].hist(bmx_accel_diffs, bins=50)
axs[0].set_title("bmx_accel")
axs[1].hist(bmx_gyro_diffs, bins=50)
axs[1].set_title("bmx_gyro")
figl, axsl = plt.subplots(1, 2, tight_layout=True)
axsl[0].hist(lsm_accel_diffs, bins=50)
axsl[0].set_title("lsm_accel")
axsl[1].hist(lsm_gyro_diffs, bins=50)
axsl[1].set_title("lsm_gyro")
print("check plot...")
plt.show()

@ -112,8 +112,8 @@ if __name__ == "__main__":
padding = max([len(fw.brand or UNKNOWN_BRAND) for fw in car_fw]) padding = max([len(fw.brand or UNKNOWN_BRAND) for fw in car_fw])
for version in sorted(car_fw, key=lambda fw: fw.brand): for version in sorted(car_fw, key=lambda fw: fw.brand):
subaddr = None if version.subAddress == 0 else hex(version.subAddress) subaddr = None if version.subAddress == 0 else hex(version.subAddress)
print(f" Brand: {version.brand or UNKNOWN_BRAND:{padding}}, bus: {version.bus} - \ print(f" Brand: {version.brand or UNKNOWN_BRAND:{padding}}, bus: {version.bus} - " +
(Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],") "(Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],")
print("Mismatches") print("Mismatches")
found = False found = False

@ -5,8 +5,7 @@ import matplotlib.pyplot as plt
from sklearn import linear_model from sklearn import linear_model
from openpilot.selfdrive.car.toyota.values import STEER_THRESHOLD from openpilot.selfdrive.car.toyota.values import STEER_THRESHOLD
from openpilot.tools.lib.route import Route from openpilot.tools.lib.srreader import SegmentRangeReader
from openpilot.tools.lib.logreader import MultiLogIterator
MIN_SAMPLES = 30 * 100 MIN_SAMPLES = 30 * 100
@ -58,7 +57,6 @@ def get_eps_factor(lr, plot=False):
if __name__ == "__main__": if __name__ == "__main__":
r = Route(sys.argv[1]) lr = SegmentRangeReader(sys.argv[1])
lr = MultiLogIterator(r.log_paths())
n = get_eps_factor(lr, plot="--plot" in sys.argv) n = get_eps_factor(lr, plot="--plot" in sys.argv)
print("EPS torque factor: ", n) print("EPS torque factor: ", n)

@ -229,6 +229,8 @@ if __name__ == "__main__":
try: try:
main() main()
except KeyboardInterrupt:
print("got CTRL-C, exiting")
except Exception: except Exception:
add_file_handler(cloudlog) add_file_handler(cloudlog)
cloudlog.exception("Manager failed to start") cloudlog.exception("Manager failed to start")

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:adc5aca6753b6ae0a1469f3e5bcb943d00cc9de75218489f2e4c3d960e7af048 oid sha256:4971931accb5ba2e534bb3e0c591826ee507e2988df2eccf1fe862c303ddf9c5
size 14138061 size 14221074

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c808717d073a0bb347f9ba929953c0b2b792ce9997f343f7e44a0b2b0e139132 oid sha256:fa346ada6f8c6326a5ee5fcd27e45e3e710049358079413c6a4624b20c6e1e47
size 3630942 size 3630942

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cf6133c5bff295a3ee69eeb01297ba77adb6b83dbc1d774442a48117dbaf4626 oid sha256:ae44fe832fe48b89998f09cebb1bcd129864a8f51497b636cd38e66e46d69a89
size 48457192 size 48457850

@ -8,6 +8,7 @@ from openpilot.common.params import Params
from openpilot.selfdrive.manager.process_config import managed_processes from openpilot.selfdrive.manager.process_config import managed_processes
from openpilot.system.hardware import PC from openpilot.system.hardware import PC
from openpilot.system.version import training_version, terms_version from openpilot.system.version import training_version, terms_version
from openpilot.tools.lib.logreader import LogIterable
def set_params_enabled(): def set_params_enabled():
@ -72,3 +73,18 @@ def with_processes(processes, init_time=0, ignore_stopped=None):
def noop(*args, **kwargs): def noop(*args, **kwargs):
pass pass
def read_segment_list(segment_list_path):
with open(segment_list_path, "r") as f:
seg_list = f.read().splitlines()
return [(platform[2:], segment) for platform, segment in zip(seg_list[::2], seg_list[1::2], strict=True)]
# Utilities for sanitizing routes of only essential data for testing car ports and doing validation.
PRESERVE_SERVICES = ["can", "carParams", "pandaStates", "pandaStateDEPRECATED"]
def sanitize(lr: LogIterable) -> LogIterable:
return filter(lambda msg: msg.which() in PRESERVE_SERVICES, lr)

@ -1 +1 @@
91cd2bf71771c2770c0effc26c0bb23d27208138 ad64b6f38c1362e9d184f3fc95299284eacb56d4

@ -1 +1 @@
ea96f935a7a16c53623c3b03e70c0fbfa6b249e7 1b981ce7f817974d4a7a28b06f01f727a5a7ea7b

@ -34,7 +34,7 @@ PROCS = {
"./encoderd": 17.0, "./encoderd": 17.0,
"./camerad": 14.5, "./camerad": 14.5,
"./locationd": 11.0, "./locationd": 11.0,
"./mapsd": (1.0, 10.0), "./mapsd": (0.5, 10.0),
"selfdrive.controls.plannerd": 11.0, "selfdrive.controls.plannerd": 11.0,
"./ui": 18.0, "./ui": 18.0,
"selfdrive.locationd.paramsd": 9.0, "selfdrive.locationd.paramsd": 9.0,

@ -312,6 +312,9 @@ def thermald_thread(end_event, hw_queue) -> None:
# must be at an engageable thermal band to go onroad # must be at an engageable thermal band to go onroad
startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.red startup_conditions["device_temp_engageable"] = thermal_status < ThermalStatus.red
# ensure device is fully booted
startup_conditions["device_booted"] = startup_conditions.get("device_booted", False) or HARDWARE.booted()
# if the temperature enters the danger zone, go offroad to cool down # if the temperature enters the danger zone, go offroad to cool down
onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger
extra_text = f"{offroad_comp_temp:.1f}C" extra_text = f"{offroad_comp_temp:.1f}C"

@ -20,7 +20,7 @@ void MapETA::paintEvent(QPaintEvent *event) {
QPainter p(this); QPainter p(this);
p.setRenderHint(QPainter::Antialiasing); p.setRenderHint(QPainter::Antialiasing);
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
p.setBrush(QColor(0, 0, 0, 150)); p.setBrush(QColor(0, 0, 0, 255));
QSizeF txt_size = eta_doc.size(); QSizeF txt_size = eta_doc.size();
p.drawRoundedRect((width() - txt_size.width()) / 2 - UI_BORDER_SIZE, 0, txt_size.width() + UI_BORDER_SIZE * 2, height() + 25, 25, 25); p.drawRoundedRect((width() - txt_size.width()) / 2 - UI_BORDER_SIZE, 0, txt_size.width() + UI_BORDER_SIZE * 2, height() + 25, 25, 25);
p.translate((width() - txt_size.width()) / 2, (height() - txt_size.height()) / 2); p.translate((width() - txt_size.width()) / 2, (height() - txt_size.height()) / 2);

@ -2,30 +2,28 @@
import json import json
import os import os
import re import re
import shutil
import unittest import unittest
import shutil
import tempfile
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import string
import requests
from parameterized import parameterized_class
from openpilot.selfdrive.ui.update_translations import TRANSLATIONS_DIR, LANGUAGES_FILE, update_translations from openpilot.selfdrive.ui.update_translations import TRANSLATIONS_DIR, LANGUAGES_FILE, update_translations
TMP_TRANSLATIONS_DIR = os.path.join(TRANSLATIONS_DIR, "tmp") with open(LANGUAGES_FILE, "r") as f:
translation_files = json.load(f)
UNFINISHED_TRANSLATION_TAG = "<translation type=\"unfinished\"" # non-empty translations can be marked unfinished UNFINISHED_TRANSLATION_TAG = "<translation type=\"unfinished\"" # non-empty translations can be marked unfinished
LOCATION_TAG = "<location " LOCATION_TAG = "<location "
FORMAT_ARG = re.compile("%[0-9]+") FORMAT_ARG = re.compile("%[0-9]+")
@parameterized_class(("name", "file"), translation_files.items())
class TestTranslations(unittest.TestCase): class TestTranslations(unittest.TestCase):
@classmethod name: str
def setUpClass(cls): file: str
with open(LANGUAGES_FILE, "r") as f:
cls.translation_files = json.load(f)
# Set up temp directory
shutil.copytree(TRANSLATIONS_DIR, TMP_TRANSLATIONS_DIR, dirs_exist_ok=True)
@classmethod
def tearDownClass(cls):
shutil.rmtree(TMP_TRANSLATIONS_DIR, ignore_errors=True)
@staticmethod @staticmethod
def _read_translation_file(path, file): def _read_translation_file(path, file):
@ -34,39 +32,29 @@ class TestTranslations(unittest.TestCase):
return f.read() return f.read()
def test_missing_translation_files(self): def test_missing_translation_files(self):
for name, file in self.translation_files.items(): self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")),
with self.subTest(name=name, file=file): f"{self.name} has no XML translation file, run selfdrive/ui/update_translations.py")
self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")),
f"{name} has no XML translation file, run selfdrive/ui/update_translations.py")
def test_translations_updated(self): def test_translations_updated(self):
update_translations(plural_only=["main_en"], translations_dir=TMP_TRANSLATIONS_DIR) with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(TRANSLATIONS_DIR, tmpdir, dirs_exist_ok=True)
for name, file in self.translation_files.items(): update_translations(translation_files=[self.file], translations_dir=tmpdir)
with self.subTest(name=name, file=file):
# caught by test_missing_translation_files
if not os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")):
self.skipTest(f"{name} missing translation file")
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file) cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
new_translations = self._read_translation_file(TMP_TRANSLATIONS_DIR, file) new_translations = self._read_translation_file(tmpdir, self.file)
self.assertEqual(cur_translations, new_translations, self.assertEqual(cur_translations, new_translations,
f"{file} ({name}) XML translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files") f"{self.file} ({self.name}) XML translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files")
@unittest.skip("Only test unfinished translations before going to release") @unittest.skip("Only test unfinished translations before going to release")
def test_unfinished_translations(self): def test_unfinished_translations(self):
for name, file in self.translation_files.items(): cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
with self.subTest(name=name, file=file): self.assertTrue(UNFINISHED_TRANSLATION_TAG not in cur_translations,
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file) f"{self.file} ({self.name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist")
self.assertTrue(UNFINISHED_TRANSLATION_TAG not in cur_translations,
f"{file} ({name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist")
def test_vanished_translations(self): def test_vanished_translations(self):
for name, file in self.translation_files.items(): cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
with self.subTest(name=name, file=file): self.assertTrue("<translation type=\"vanished\">" not in cur_translations,
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file) f"{self.file} ({self.name}) translation file has obsolete translations. Run selfdrive/ui/update_translations.py --vanish to remove them")
self.assertTrue("<translation type=\"vanished\">" not in cur_translations,
f"{file} ({name}) translation file has obsolete translations. Run selfdrive/ui/update_translations.py --vanish to remove them")
def test_finished_translations(self): def test_finished_translations(self):
""" """
@ -78,48 +66,68 @@ class TestTranslations(unittest.TestCase):
- that translation is not empty - that translation is not empty
- that translation format arguments are consistent - that translation format arguments are consistent
""" """
for name, file in self.translation_files.items(): tr_xml = ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts"))
with self.subTest(name=name, file=file):
tr_xml = ET.parse(os.path.join(TRANSLATIONS_DIR, f"{file}.ts"))
for context in tr_xml.getroot(): for context in tr_xml.getroot():
for message in context.iterfind("message"): for message in context.iterfind("message"):
translation = message.find("translation") translation = message.find("translation")
source_text = message.find("source").text source_text = message.find("source").text
# Do not test unfinished translations # Do not test unfinished translations
if translation.get("type") == "unfinished": if translation.get("type") == "unfinished":
continue continue
if message.get("numerus") == "yes": if message.get("numerus") == "yes":
numerusform = [t.text for t in translation.findall("numerusform")] numerusform = [t.text for t in translation.findall("numerusform")]
for nf in numerusform: for nf in numerusform:
self.assertIsNotNone(nf, f"Ensure all plural translation forms are completed: {source_text}") self.assertIsNotNone(nf, f"Ensure all plural translation forms are completed: {source_text}")
self.assertIn("%n", nf, "Ensure numerus argument (%n) exists in translation.") self.assertIn("%n", nf, "Ensure numerus argument (%n) exists in translation.")
self.assertIsNone(FORMAT_ARG.search(nf), "Plural translations must use %n, not %1, %2, etc.: {}".format(numerusform)) self.assertIsNone(FORMAT_ARG.search(nf), "Plural translations must use %n, not %1, %2, etc.: {}".format(numerusform))
else: else:
self.assertIsNotNone(translation.text, f"Ensure translation is completed: {source_text}") self.assertIsNotNone(translation.text, f"Ensure translation is completed: {source_text}")
source_args = FORMAT_ARG.findall(source_text) source_args = FORMAT_ARG.findall(source_text)
translation_args = FORMAT_ARG.findall(translation.text) translation_args = FORMAT_ARG.findall(translation.text)
self.assertEqual(sorted(source_args), sorted(translation_args), self.assertEqual(sorted(source_args), sorted(translation_args),
f"Ensure format arguments are consistent: `{source_text}` vs. `{translation.text}`") f"Ensure format arguments are consistent: `{source_text}` vs. `{translation.text}`")
def test_no_locations(self): def test_no_locations(self):
for name, file in self.translation_files.items(): for line in self._read_translation_file(TRANSLATIONS_DIR, self.file).splitlines():
with self.subTest(name=name, file=file): self.assertFalse(line.strip().startswith(LOCATION_TAG),
for line in self._read_translation_file(TRANSLATIONS_DIR, file).splitlines(): f"Line contains location tag: {line.strip()}, remove all line numbers.")
self.assertFalse(line.strip().startswith(LOCATION_TAG),
f"Line contains location tag: {line.strip()}, remove all line numbers.")
def test_entities_error(self): def test_entities_error(self):
for name, file in self.translation_files.items(): cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
with self.subTest(name=name, file=file): matches = re.findall(r'@(\w+);', cur_translations)
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file) self.assertEqual(len(matches), 0, f"The string(s) {matches} were found with '@' instead of '&'")
matches = re.findall(r'@(\w+);', cur_translations)
self.assertEqual(len(matches), 0, f"The string(s) {matches} were found with '@' instead of '&'") def test_bad_language(self):
IGNORED_WORDS = {'pédale'}
match = re.search(r'_([a-zA-Z]{2,3})', self.file)
assert match, f"{self.name} - could not parse language"
response = requests.get(f"https://raw.githubusercontent.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/master/{match.group(1)}")
response.raise_for_status()
banned_words = {line.strip() for line in response.text.splitlines()}
for context in ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")).getroot():
for message in context.iterfind("message"):
translation = message.find("translation")
if translation.get("type") == "unfinished":
continue
translation_text = " ".join([t.text for t in translation.findall("numerusform")]) if message.get("numerus") == "yes" else translation.text
if not translation_text:
continue
words = set(translation_text.translate(str.maketrans('', '', string.punctuation + '%n')).lower().split())
bad_words_found = words & (banned_words - IGNORED_WORDS)
assert not bad_words_found, f"Bad language found in {self.name}: '{translation_text}'. Bad word(s): {', '.join(bad_words_found)}"
if __name__ == "__main__": if __name__ == "__main__":

@ -0,0 +1,138 @@
#!/usr/bin/env python3
import argparse
import json
import os
import pathlib
import xml.etree.ElementTree as ET
from typing import cast
import requests
TRANSLATIONS_DIR = pathlib.Path(__file__).resolve().parent
TRANSLATIONS_LANGUAGES = TRANSLATIONS_DIR / "languages.json"
OPENAI_MODEL = "gpt-4"
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
OPENAI_PROMPT = "You are a professional translator from English to {language} (ISO 639 language code). " + \
"The following sentence or word is in the GUI of a software called openpilot, translate it accordingly."
def get_language_files(languages: list[str] | None = None) -> dict[str, pathlib.Path]:
files = {}
with open(TRANSLATIONS_LANGUAGES) as fp:
language_dict = json.load(fp)
for filename in language_dict.values():
path = TRANSLATIONS_DIR / f"{filename}.ts"
language = path.stem.split("main_")[1]
if languages is None or language in languages:
files[language] = path
return files
def translate_phrase(text: str, language: str) -> str:
response = requests.post(
"https://api.openai.com/v1/chat/completions",
json={
"model": OPENAI_MODEL,
"messages": [
{
"role": "system",
"content": OPENAI_PROMPT.format(language=language),
},
{
"role": "user",
"content": text,
},
],
"temperature": 0.8,
"max_tokens": 1024,
"top_p": 1,
},
headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json",
},
)
if 400 <= response.status_code < 600:
raise requests.HTTPError(f'Error {response.status_code}: {response.json()}', response=response)
data = response.json()
return cast(str, data["choices"][0]["message"]["content"])
def translate_file(path: pathlib.Path, language: str, all_: bool) -> None:
tree = ET.parse(path)
root = tree.getroot()
for context in root.findall("./context"):
name = context.find("name")
if name is None:
raise ValueError("name not found")
print(f"Context: {name.text}")
for message in context.findall("./message"):
source = message.find("source")
translation = message.find("translation")
if source is None or translation is None:
raise ValueError("source or translation not found")
if not all_ and translation.attrib.get("type") != "unfinished":
continue
llm_translation = translate_phrase(cast(str, source.text), language)
print(f"Source: {source.text}\n" +
f"Current translation: {translation.text}\n" +
f"LLM translation: {llm_translation}")
translation.text = llm_translation
with path.open("w", encoding="utf-8") as fp:
fp.write('<?xml version="1.0" encoding="utf-8"?>\n' +
'<!DOCTYPE TS>\n' +
ET.tostring(root, encoding="utf-8").decode())
def main():
arg_parser = argparse.ArgumentParser("Auto translate")
group = arg_parser.add_mutually_exclusive_group(required=True)
group.add_argument("-a", "--all-files", action="store_true", help="Translate all files")
group.add_argument("-f", "--file", nargs="+", help="Translate the selected files. (Example: -f fr de)")
arg_parser.add_argument("-t", "--all-translations", action="store_true", default=False, help="Translate all sections. (Default: only unfinished)")
args = arg_parser.parse_args()
if OPENAI_API_KEY is None:
print("OpenAI API key is missing. (Hint: use `export OPENAI_API_KEY=YOUR-KEY` before you run the script).\n" +
"If you don't have one go to: https://beta.openai.com/account/api-keys.")
exit(1)
files = get_language_files(None if args.all_files else args.file)
if args.file:
missing_files = set(args.file) - set(files)
if len(missing_files):
print(f"No language files found: {missing_files}")
exit(1)
print(f"Translation mode: {'all' if args.all_translations else 'only unfinished'}. Files: {list(files)}")
for lang, path in files.items():
print(f"Translate {lang} ({path})")
translate_file(path, lang, args.all_translations)
if __name__ == "__main__":
main()

@ -54,8 +54,8 @@ if __name__ == "__main__":
badge_svg.extend([f'<g transform="translate(0, {idx * BADGE_HEIGHT})">', content_svg, "</g>"]) badge_svg.extend([f'<g transform="translate(0, {idx * BADGE_HEIGHT})">', content_svg, "</g>"])
badge_svg.insert(0, f'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \ badge_svg.insert(0, '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
height="{len(translation_files) * BADGE_HEIGHT}" width="{max_badge_width}">') f'height="{len(translation_files) * BADGE_HEIGHT}" width="{max_badge_width}">')
badge_svg.append("</svg>") badge_svg.append("</svg>")
with open(os.path.join(BASEDIR, "translation_badge.svg"), "w") as badge_f: with open(os.path.join(BASEDIR, "translation_badge.svg"), "w") as badge_f:

@ -9,6 +9,7 @@ UI_DIR = os.path.join(BASEDIR, "selfdrive", "ui")
TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations") TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations")
LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json") LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json")
TRANSLATIONS_INCLUDE_FILE = os.path.join(TRANSLATIONS_DIR, "alerts_generated.h") TRANSLATIONS_INCLUDE_FILE = os.path.join(TRANSLATIONS_DIR, "alerts_generated.h")
PLURAL_ONLY = ["main_en"] # base language, only create entries for strings with plural forms
def generate_translations_include(): def generate_translations_include():
@ -22,21 +23,20 @@ def generate_translations_include():
with open(TRANSLATIONS_INCLUDE_FILE, "w") as f: with open(TRANSLATIONS_INCLUDE_FILE, "w") as f:
f.write(content) f.write(content)
def update_translations(vanish=False, plural_only=None, translations_dir=TRANSLATIONS_DIR):
generate_translations_include()
if plural_only is None: def update_translations(vanish: bool = False, translation_files: None | list[str] = None, translations_dir: str = TRANSLATIONS_DIR):
plural_only = [] generate_translations_include()
with open(LANGUAGES_FILE, "r") as f: if translation_files is None:
translation_files = json.load(f) with open(LANGUAGES_FILE, "r") as f:
translation_files = json.load(f).values()
for file in translation_files.values(): for file in translation_files:
tr_file = os.path.join(translations_dir, f"{file}.ts") tr_file = os.path.join(translations_dir, f"{file}.ts")
args = f"lupdate -locations none -recursive {UI_DIR} -ts {tr_file} -I {BASEDIR}" args = f"lupdate -locations none -recursive {UI_DIR} -ts {tr_file} -I {BASEDIR}"
if vanish: if vanish:
args += " -no-obsolete" args += " -no-obsolete"
if file in plural_only: if file in PLURAL_ONLY:
args += " -pluralonly" args += " -pluralonly"
ret = os.system(args) ret = os.system(args)
assert ret == 0 assert ret == 0
@ -46,8 +46,6 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update translation files for UI", parser = argparse.ArgumentParser(description="Update translation files for UI",
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--vanish", action="store_true", help="Remove translations with source text no longer found") parser.add_argument("--vanish", action="store_true", help="Remove translations with source text no longer found")
parser.add_argument("--plural-only", type=str, nargs="*", default=["main_en"],
help="Translation codes to only create plural translations for (ie. the base language)")
args = parser.parse_args() args = parser.parse_args()
update_translations(args.vanish, args.plural_only) update_translations(args.vanish)

@ -230,7 +230,6 @@ class Updater:
b: Union[str, None] = self.params.get("UpdaterTargetBranch", encoding='utf-8') b: Union[str, None] = self.params.get("UpdaterTargetBranch", encoding='utf-8')
if b is None: if b is None:
b = self.get_branch(BASEDIR) b = self.get_branch(BASEDIR)
self.params.put("UpdaterTargetBranch", b)
return b return b
@property @property
@ -245,7 +244,7 @@ class Updater:
@property @property
def update_available(self) -> bool: def update_available(self) -> bool:
if os.path.isdir(OVERLAY_MERGED): if os.path.isdir(OVERLAY_MERGED) and len(self.branches) > 0:
hash_mismatch = self.get_commit_hash(OVERLAY_MERGED) != self.branches[self.target_branch] hash_mismatch = self.get_commit_hash(OVERLAY_MERGED) != self.branches[self.target_branch]
branch_mismatch = self.get_branch(OVERLAY_MERGED) != self.target_branch branch_mismatch = self.get_branch(OVERLAY_MERGED) != self.target_branch
return hash_mismatch or branch_mismatch return hash_mismatch or branch_mismatch
@ -259,9 +258,11 @@ class Updater:
def set_params(self, update_success: bool, failed_count: int, exception: Optional[str]) -> None: def set_params(self, update_success: bool, failed_count: int, exception: Optional[str]) -> None:
self.params.put("UpdateFailedCount", str(failed_count)) self.params.put("UpdateFailedCount", str(failed_count))
self.params.put("UpdaterTargetBranch", self.target_branch)
self.params.put_bool("UpdaterFetchAvailable", self.update_available) self.params.put_bool("UpdaterFetchAvailable", self.update_available)
self.params.put("UpdaterAvailableBranches", ','.join(self.branches.keys())) if len(self.branches):
self.params.put("UpdaterAvailableBranches", ','.join(self.branches.keys()))
last_update = datetime.datetime.utcnow() last_update = datetime.datetime.utcnow()
if update_success: if update_success:
@ -428,10 +429,11 @@ def main() -> None:
# invalidate old finalized update # invalidate old finalized update
set_consistent_flag(False) set_consistent_flag(False)
# wait a bit before first cycle # set initial state
wait_helper.sleep(60) params.put("UpdaterState", "idle")
# Run the update loop # Run the update loop
first_run = True
while True: while True:
wait_helper.ready_event.clear() wait_helper.ready_event.clear()
@ -444,7 +446,8 @@ def main() -> None:
# ensure we have some params written soon after startup # ensure we have some params written soon after startup
updater.set_params(False, update_failed_count, exception) updater.set_params(False, update_failed_count, exception)
if not system_time_valid(): if not system_time_valid() or first_run:
first_run = False
wait_helper.sleep(60) wait_helper.sleep(60)
continue continue

@ -3,7 +3,7 @@ Import('env', 'arch', 'cereal', 'messaging', 'common', 'gpucommon', 'visionipc')
libs = ['m', 'pthread', common, 'jpeg', 'OpenCL', 'yuv', cereal, messaging, 'zmq', 'capnp', 'kj', visionipc, gpucommon, 'atomic'] libs = ['m', 'pthread', common, 'jpeg', 'OpenCL', 'yuv', cereal, messaging, 'zmq', 'capnp', 'kj', visionipc, gpucommon, 'atomic']
camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/camera_util.cc', camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/camera_util.cc',
'sensors/ar0231.cc', 'sensors/ox03c10.cc']) 'sensors/ar0231.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc'])
env.Program('camerad', ['main.cc', camera_obj], LIBS=libs) env.Program('camerad', ['main.cc', camera_obj], LIBS=libs)
if GetOption("extras") and arch == "x86_64": if GetOption("extras") and arch == "x86_64":

@ -136,6 +136,7 @@ void CameraBuf::queue(size_t buf_idx) {
void fill_frame_data(cereal::FrameData::Builder &framed, const FrameMetadata &frame_data, CameraState *c) { void fill_frame_data(cereal::FrameData::Builder &framed, const FrameMetadata &frame_data, CameraState *c) {
framed.setFrameId(frame_data.frame_id); framed.setFrameId(frame_data.frame_id);
framed.setRequestId(frame_data.request_id);
framed.setTimestampEof(frame_data.timestamp_eof); framed.setTimestampEof(frame_data.timestamp_eof);
framed.setTimestampSof(frame_data.timestamp_sof); framed.setTimestampSof(frame_data.timestamp_sof);
framed.setIntegLines(frame_data.integ_lines); framed.setIntegLines(frame_data.integ_lines);

@ -26,6 +26,7 @@ const bool env_ctrl_exp_from_params = getenv("CTRL_EXP_FROM_PARAMS") != NULL;
typedef struct FrameMetadata { typedef struct FrameMetadata {
uint32_t frame_id; uint32_t frame_id;
uint32_t request_id;
// Timestamps // Timestamps
uint64_t timestamp_sof; uint64_t timestamp_sof;

@ -150,7 +150,7 @@ int CameraState::sensors_init() {
power->count = 1; power->count = 1;
power->cmd_type = CAMERA_SENSOR_CMD_TYPE_PWR_UP; power->cmd_type = CAMERA_SENSOR_CMD_TYPE_PWR_UP;
power->power_settings[0].power_seq_type = 0; power->power_settings[0].power_seq_type = 0;
power->power_settings[0].config_val_low = ci->power_config_val_low; power->power_settings[0].config_val_low = ci->mclk_frequency;
power = power_set_wait(power, 1); power = power_set_wait(power, 1);
// reset high // reset high
@ -316,10 +316,10 @@ void CameraState::config_isp(int io_mem_handle, int fence, int request_id, int b
.h_init = 0x0, .h_init = 0x0,
.v_init = 0x0, .v_init = 0x0,
}; };
io_cfg[0].format = CAM_FORMAT_MIPI_RAW_12; // CAM_FORMAT_UBWC_TP10 for YUV io_cfg[0].format = ci->mipi_format; // CAM_FORMAT_UBWC_TP10 for YUV
io_cfg[0].color_space = CAM_COLOR_SPACE_BASE; // CAM_COLOR_SPACE_BT601_FULL for YUV io_cfg[0].color_space = CAM_COLOR_SPACE_BASE; // CAM_COLOR_SPACE_BT601_FULL for YUV
io_cfg[0].color_pattern = 0x5; // 0x0 for YUV io_cfg[0].color_pattern = 0x5; // 0x0 for YUV
io_cfg[0].bpp = 0xc; io_cfg[0].bpp = (ci->mipi_format == CAM_FORMAT_MIPI_RAW_10 ? 0xa : 0xc); // bits per pixel
io_cfg[0].resource_type = CAM_ISP_IFE_OUT_RES_RDI_0; // CAM_ISP_IFE_OUT_RES_FULL for YUV io_cfg[0].resource_type = CAM_ISP_IFE_OUT_RES_RDI_0; // CAM_ISP_IFE_OUT_RES_FULL for YUV
io_cfg[0].fence = fence; io_cfg[0].fence = fence;
io_cfg[0].direction = CAM_BUF_OUTPUT; io_cfg[0].direction = CAM_BUF_OUTPUT;
@ -459,7 +459,8 @@ void CameraState::camera_open(MultiCameraState *multi_cam_state_, int camera_num
// Try different sensors one by one until it success. // Try different sensors one by one until it success.
if (!init_sensor_lambda(new AR0231) && if (!init_sensor_lambda(new AR0231) &&
!init_sensor_lambda(new OX03C10)) { !init_sensor_lambda(new OX03C10) &&
!init_sensor_lambda(new OS04C10)) {
LOGE("** sensor %d FAILED bringup, disabling", camera_num); LOGE("** sensor %d FAILED bringup, disabling", camera_num);
enabled = false; enabled = false;
return; return;
@ -481,7 +482,6 @@ void CameraState::camera_open(MultiCameraState *multi_cam_state_, int camera_num
LOG("-- Configuring sensor"); LOG("-- Configuring sensor");
sensors_i2c(ci->init_reg_array.data(), ci->init_reg_array.size(), CAM_SENSOR_PACKET_OPCODE_SENSOR_CONFIG, ci->data_word); sensors_i2c(ci->init_reg_array.data(), ci->init_reg_array.size(), CAM_SENSOR_PACKET_OPCODE_SENSOR_CONFIG, ci->data_word);
printf("dt is %x\n", ci->in_port_info_dt);
// NOTE: to be able to disable road and wide road, we still have to configure the sensor over i2c // NOTE: to be able to disable road and wide road, we still have to configure the sensor over i2c
// If you don't do this, the strobe GPIO is an output (even in reset it seems!) // If you don't do this, the strobe GPIO is an output (even in reset it seems!)
@ -495,8 +495,8 @@ void CameraState::camera_open(MultiCameraState *multi_cam_state_, int camera_num
.lane_cfg = 0x3210, .lane_cfg = 0x3210,
.vc = 0x0, .vc = 0x0,
.dt = ci->in_port_info_dt, .dt = ci->frame_data_type,
.format = CAM_FORMAT_MIPI_RAW_12, .format = ci->mipi_format,
.test_pattern = 0x2, // 0x3? .test_pattern = 0x2, // 0x3?
.usage_type = 0x0, .usage_type = 0x0,
@ -522,7 +522,7 @@ void CameraState::camera_open(MultiCameraState *multi_cam_state_, int camera_num
.num_out_res = 0x1, .num_out_res = 0x1,
.data[0] = (struct cam_isp_out_port_info){ .data[0] = (struct cam_isp_out_port_info){
.res_type = CAM_ISP_IFE_OUT_RES_RDI_0, .res_type = CAM_ISP_IFE_OUT_RES_RDI_0,
.format = CAM_FORMAT_MIPI_RAW_12, .format = ci->mipi_format,
.width = ci->frame_width, .width = ci->frame_width,
.height = ci->frame_height + ci->extra_height, .height = ci->frame_height + ci->extra_height,
.comp_grp_id = 0x0, .split_point = 0x0, .secure_mode = 0x0, .comp_grp_id = 0x0, .split_point = 0x0, .secure_mode = 0x0,
@ -766,6 +766,7 @@ void CameraState::handle_camera_event(void *evdat) {
auto &meta_data = buf.camera_bufs_metadata[buf_idx]; auto &meta_data = buf.camera_bufs_metadata[buf_idx];
meta_data.frame_id = main_id - idx_offset; meta_data.frame_id = main_id - idx_offset;
meta_data.request_id = real_id;
meta_data.timestamp_sof = timestamp; meta_data.timestamp_sof = timestamp;
exp_lock.lock(); exp_lock.lock();
meta_data.gain = analog_gain_frac * (1 + dc_gain_weight * (ci->dc_gain_factor-1) / ci->dc_gain_max_weight); meta_data.gain = analog_gain_frac * (1 + dc_gain_weight * (ci->dc_gain_factor-1) / ci->dc_gain_max_weight);
@ -970,6 +971,9 @@ void cameras_run(MultiCameraState *s) {
event_data->u.frame_msg.frame_id, event_data->u.frame_msg.request_id, event_data->u.frame_msg.timestamp/1e6, event_data->u.frame_msg.sof_status); event_data->u.frame_msg.frame_id, event_data->u.frame_msg.request_id, event_data->u.frame_msg.timestamp/1e6, event_data->u.frame_msg.sof_status);
} }
// for debugging
//do_exit = do_exit || event_data->u.frame_msg.frame_id > (30*20);
if (event_data->session_hdl == s->road_cam.session_handle) { if (event_data->session_hdl == s->road_cam.session_handle) {
s->road_cam.handle_camera_event(event_data); s->road_cam.handle_camera_event(event_data);
} else if (event_data->session_hdl == s->wide_road_cam.session_handle) { } else if (event_data->session_hdl == s->wide_road_cam.session_handle) {
@ -980,6 +984,8 @@ void cameras_run(MultiCameraState *s) {
LOGE("Unknown vidioc event source"); LOGE("Unknown vidioc event source");
assert(false); assert(false);
} }
} else {
LOGE("unhandled event %d\n", ev.type);
} }
} else { } else {
LOGE("VIDIOC_DQEVENT failed, errno=%d", errno); LOGE("VIDIOC_DQEVENT failed, errno=%d", errno);

@ -93,8 +93,9 @@ AR0231::AR0231() {
init_reg_array.assign(std::begin(init_array_ar0231), std::end(init_array_ar0231)); init_reg_array.assign(std::begin(init_array_ar0231), std::end(init_array_ar0231));
probe_reg_addr = 0x3000; probe_reg_addr = 0x3000;
probe_expected_data = 0x354; probe_expected_data = 0x354;
in_port_info_dt = 0x12; // Changing stats to 0x2C doesn't work, so change pixels to 0x12 instead mipi_format = CAM_FORMAT_MIPI_RAW_12;
power_config_val_low = 19200000; //Hz frame_data_type = 0x12; // Changing stats to 0x2C doesn't work, so change pixels to 0x12 instead
mclk_frequency = 19200000; //Hz
dc_gain_factor = 2.5; dc_gain_factor = 2.5;
dc_gain_min_weight = 0; dc_gain_min_weight = 0;

@ -0,0 +1,105 @@
#include "system/camerad/sensors/sensor.h"
namespace {
const float sensor_analog_gains_OS04C10[] = {
1.0, 1.0625, 1.125, 1.1875, 1.25, 1.3125, 1.375, 1.4375, 1.5, 1.5625, 1.6875,
1.8125, 1.9375, 2.0, 2.125, 2.25, 2.375, 2.5, 2.625, 2.75, 2.875, 3.0,
3.125, 3.375, 3.625, 3.875, 4.0, 4.25, 4.5, 4.75, 5.0, 5.25, 5.5,
5.75, 6.0, 6.25, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0,
10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.5, 14.0, 14.5, 15.0, 15.5};
const uint32_t os04c10_analog_gains_reg[] = {
0x100, 0x110, 0x120, 0x130, 0x140, 0x150, 0x160, 0x170, 0x180, 0x190, 0x1B0,
0x1D0, 0x1F0, 0x200, 0x220, 0x240, 0x260, 0x280, 0x2A0, 0x2C0, 0x2E0, 0x300,
0x320, 0x360, 0x3A0, 0x3E0, 0x400, 0x440, 0x480, 0x4C0, 0x500, 0x540, 0x580,
0x5C0, 0x600, 0x640, 0x680, 0x700, 0x780, 0x800, 0x880, 0x900, 0x980, 0xA00,
0xA80, 0xB00, 0xB80, 0xC00, 0xC80, 0xD00, 0xD80, 0xE00, 0xE80, 0xF00, 0xF80};
const uint32_t VS_TIME_MIN_OS04C10 = 1;
//const uint32_t VS_TIME_MAX_OS04C10 = 34; // vs < 35
} // namespace
OS04C10::OS04C10() {
image_sensor = cereal::FrameData::ImageSensor::OS04C10;
data_word = false;
frame_width = 1920;
frame_height = 1080;
frame_stride = (1920*10/8);
/*
frame_width = 0xa80;
frame_height = 0x5f0;
frame_stride = 0xd20;
*/
extra_height = 0;
frame_offset = 0;
start_reg_array.assign(std::begin(start_reg_array_os04c10), std::end(start_reg_array_os04c10));
init_reg_array.assign(std::begin(init_array_os04c10), std::end(init_array_os04c10));
probe_reg_addr = 0x300a;
probe_expected_data = 0x5304;
mipi_format = CAM_FORMAT_MIPI_RAW_10;
frame_data_type = 0x2b;
mclk_frequency = 24000000; // Hz
dc_gain_factor = 7.32;
dc_gain_min_weight = 1; // always on is fine
dc_gain_max_weight = 1;
dc_gain_on_grey = 0.9;
dc_gain_off_grey = 1.0;
exposure_time_min = 2; // 1x
exposure_time_max = 2016;
analog_gain_min_idx = 0x0;
analog_gain_rec_idx = 0x0; // 1x
analog_gain_max_idx = 0x36;
analog_gain_cost_delta = -1;
analog_gain_cost_low = 0.4;
analog_gain_cost_high = 6.4;
for (int i = 0; i <= analog_gain_max_idx; i++) {
sensor_analog_gains[i] = sensor_analog_gains_OS04C10[i];
}
min_ev = (exposure_time_min + VS_TIME_MIN_OS04C10) * sensor_analog_gains[analog_gain_min_idx];
max_ev = exposure_time_max * dc_gain_factor * sensor_analog_gains[analog_gain_max_idx];
target_grey_factor = 0.01;
}
std::vector<i2c_random_wr_payload> OS04C10::getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const {
// t_HCG&t_LCG + t_VS on LPD, t_SPD on SPD
uint32_t hcg_time = exposure_time;
//uint32_t lcg_time = hcg_time;
//uint32_t spd_time = std::min(std::max((uint32_t)exposure_time, (exposure_time_max + VS_TIME_MAX_OS04C10) / 3), exposure_time_max + VS_TIME_MAX_OS04C10);
//uint32_t vs_time = std::min(std::max((uint32_t)exposure_time / 40, VS_TIME_MIN_OS04C10), VS_TIME_MAX_OS04C10);
uint32_t real_gain = os04c10_analog_gains_reg[new_exp_g];
hcg_time = 100;
real_gain = 0x320;
return {
{0x3501, hcg_time>>8}, {0x3502, hcg_time&0xFF},
//{0x3581, lcg_time>>8}, {0x3582, lcg_time&0xFF},
//{0x3541, spd_time>>8}, {0x3542, spd_time&0xFF},
//{0x35c2, vs_time&0xFF},
{0x3508, real_gain>>8}, {0x3509, real_gain&0xFF},
};
}
int OS04C10::getSlaveAddress(int port) const {
assert(port >= 0 && port <= 2);
return (int[]){0x6C, 0x20, 0x6C}[port];
}
float OS04C10::getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const {
float score = std::abs(desired_ev - (exp_t * exp_gain));
float m = exp_g_idx > analog_gain_rec_idx ? analog_gain_cost_high : analog_gain_cost_low;
score += std::abs(exp_g_idx - (int)analog_gain_rec_idx) * m;
score += ((1 - analog_gain_cost_delta) +
analog_gain_cost_delta * (exp_g_idx - analog_gain_min_idx) / (analog_gain_max_idx - analog_gain_min_idx)) *
std::abs(exp_g_idx - gain_idx) * 5.0;
return score;
}

@ -0,0 +1,298 @@
#pragma once
const struct i2c_random_wr_payload start_reg_array_os04c10[] = {{0x100, 1}};
const struct i2c_random_wr_payload stop_reg_array_os04c10[] = {{0x100, 0}};
const struct i2c_random_wr_payload init_array_os04c10[] = {
// OS04C10_AA_00_02_17_wAO_1920x1080_MIPI728Mbps_Linear12bit_20FPS_4Lane_MCLK24MHz
{0x0103, 0x01},
{0x0301, 0x84},
{0x0303, 0x01},
{0x0305, 0x5b},
{0x0306, 0x01},
{0x0307, 0x17},
{0x0323, 0x04},
{0x0324, 0x01},
{0x0325, 0x62},
{0x3012, 0x06},
{0x3013, 0x02},
{0x3016, 0x72},
{0x3021, 0x03},
{0x3106, 0x21},
{0x3107, 0xa1},
{0x3500, 0x00},
{0x3501, 0x00},
{0x3502, 0x40},
{0x3503, 0x88},
{0x3508, 0x07},
{0x3509, 0xc0},
{0x350a, 0x04},
{0x350b, 0x00},
{0x350c, 0x07},
{0x350d, 0xc0},
{0x350e, 0x04},
{0x350f, 0x00},
{0x3510, 0x00},
{0x3511, 0x00},
{0x3512, 0x20},
{0x3624, 0x00},
{0x3625, 0x4c},
{0x3660, 0x00},
{0x3666, 0xa5},
{0x3667, 0xa5},
{0x366a, 0x64},
{0x3673, 0x0d},
{0x3672, 0x0d},
{0x3671, 0x0d},
{0x3670, 0x0d},
{0x3685, 0x00},
{0x3694, 0x0d},
{0x3693, 0x0d},
{0x3692, 0x0d},
{0x3691, 0x0d},
{0x3696, 0x4c},
{0x3697, 0x4c},
{0x3698, 0x40},
{0x3699, 0x80},
{0x369a, 0x18},
{0x369b, 0x1f},
{0x369c, 0x14},
{0x369d, 0x80},
{0x369e, 0x40},
{0x369f, 0x21},
{0x36a0, 0x12},
{0x36a1, 0x5d},
{0x36a2, 0x66},
{0x370a, 0x00},
{0x370e, 0x0c},
{0x3710, 0x00},
{0x3713, 0x00},
{0x3725, 0x02},
{0x372a, 0x03},
{0x3738, 0xce},
{0x3748, 0x00},
{0x374a, 0x00},
{0x374c, 0x00},
{0x374e, 0x00},
{0x3756, 0x00},
{0x3757, 0x0e},
{0x3767, 0x00},
{0x3771, 0x00},
{0x377b, 0x20},
{0x377c, 0x00},
{0x377d, 0x0c},
{0x3781, 0x03},
{0x3782, 0x00},
{0x3789, 0x14},
{0x3795, 0x02},
{0x379c, 0x00},
{0x379d, 0x00},
{0x37b8, 0x04},
{0x37ba, 0x03},
{0x37bb, 0x00},
{0x37bc, 0x04},
{0x37be, 0x08},
{0x37c4, 0x11},
{0x37c5, 0x80},
{0x37c6, 0x14},
{0x37c7, 0x08},
{0x37da, 0x11},
{0x381f, 0x08},
{0x3829, 0x03},
{0x3881, 0x00},
{0x3888, 0x04},
{0x388b, 0x00},
{0x3c80, 0x10},
{0x3c86, 0x00},
{0x3c8c, 0x20},
{0x3c9f, 0x01},
{0x3d85, 0x1b},
{0x3d8c, 0x71},
{0x3d8d, 0xe2},
{0x3f00, 0x0b},
{0x3f06, 0x04},
{0x400a, 0x01},
{0x400b, 0x50},
{0x400e, 0x08},
{0x4043, 0x7e},
{0x4045, 0x7e},
{0x4047, 0x7e},
{0x4049, 0x7e},
{0x4090, 0x14},
{0x40b0, 0x00},
{0x40b1, 0x00},
{0x40b2, 0x00},
{0x40b3, 0x00},
{0x40b4, 0x00},
{0x40b5, 0x00},
{0x40b7, 0x00},
{0x40b8, 0x00},
{0x40b9, 0x00},
{0x40ba, 0x00},
{0x4301, 0x00},
{0x4303, 0x00},
{0x4502, 0x04},
{0x4503, 0x00},
{0x4504, 0x06},
{0x4506, 0x00},
{0x4507, 0x64},
{0x4803, 0x00},
{0x480c, 0x32},
{0x480e, 0x00},
{0x4813, 0x00},
{0x4819, 0x70},
{0x481f, 0x30},
{0x4823, 0x3f},
{0x4825, 0x30},
{0x4833, 0x10},
{0x484b, 0x07},
{0x488b, 0x00},
{0x4d00, 0x04},
{0x4d01, 0xad},
{0x4d02, 0xbc},
{0x4d03, 0xa1},
{0x4d04, 0x1f},
{0x4d05, 0x4c},
{0x4d0b, 0x01},
{0x4e00, 0x2a},
{0x4e0d, 0x00},
{0x5001, 0x09},
{0x5004, 0x00},
{0x5080, 0x04},
{0x5036, 0x00},
{0x5180, 0x70},
{0x5181, 0x10},
{0x520a, 0x03},
{0x520b, 0x06},
{0x520c, 0x0c},
{0x580b, 0x0f},
{0x580d, 0x00},
{0x580f, 0x00},
{0x5820, 0x00},
{0x5821, 0x00},
{0x301c, 0xf8},
{0x301e, 0xb4},
{0x301f, 0xd0},
{0x3022, 0x01},
{0x3109, 0xe7},
{0x3600, 0x00},
{0x3610, 0x65},
{0x3611, 0x85},
{0x3613, 0x3a},
{0x3615, 0x60},
{0x3621, 0x90},
{0x3620, 0x0c},
{0x3629, 0x00},
{0x3661, 0x04},
{0x3664, 0x70},
{0x3665, 0x00},
{0x3681, 0xa6},
{0x3682, 0x53},
{0x3683, 0x2a},
{0x3684, 0x15},
{0x3700, 0x2a},
{0x3701, 0x12},
{0x3703, 0x28},
{0x3704, 0x0e},
{0x3706, 0x4a},
{0x3709, 0x4a},
{0x370b, 0xa2},
{0x370c, 0x01},
{0x370f, 0x04},
{0x3714, 0x24},
{0x3716, 0x24},
{0x3719, 0x11},
{0x371a, 0x1e},
{0x3720, 0x00},
{0x3724, 0x13},
{0x373f, 0xb0},
{0x3741, 0x4a},
{0x3743, 0x4a},
{0x3745, 0x4a},
{0x3747, 0x4a},
{0x3749, 0xa2},
{0x374b, 0xa2},
{0x374d, 0xa2},
{0x374f, 0xa2},
{0x3755, 0x10},
{0x376c, 0x00},
{0x378d, 0x30},
{0x3790, 0x4a},
{0x3791, 0xa2},
{0x3798, 0x40},
{0x379e, 0x00},
{0x379f, 0x04},
{0x37a1, 0x10},
{0x37a2, 0x1e},
{0x37a8, 0x10},
{0x37a9, 0x1e},
{0x37ac, 0xa0},
{0x37b9, 0x01},
{0x37bd, 0x01},
{0x37bf, 0x26},
{0x37c0, 0x11},
{0x37c2, 0x04},
{0x37cd, 0x19},
{0x37e0, 0x08},
{0x37e6, 0x04},
{0x37e5, 0x02},
{0x37e1, 0x0c},
{0x3737, 0x04},
{0x37d8, 0x02},
{0x37e2, 0x10},
{0x3739, 0x10},
{0x3662, 0x10},
{0x37e4, 0x20},
{0x37e3, 0x08},
{0x37d9, 0x08},
{0x4040, 0x00},
{0x4041, 0x07},
{0x4008, 0x02},
{0x4009, 0x0d},
{0x3800, 0x01},
{0x3801, 0x80},
{0x3802, 0x00},
{0x3803, 0xdc},
{0x3804, 0x09},
{0x3805, 0x0f},
{0x3806, 0x05},
{0x3807, 0x23},
{0x3808, 0x07},
{0x3809, 0x80},
{0x380a, 0x04},
{0x380b, 0x38},
{0x380c, 0x04},
{0x380d, 0x2e},
{0x380e, 0x12},
{0x380f, 0x70},
{0x3811, 0x08},
{0x3813, 0x08},
{0x3814, 0x01},
{0x3815, 0x01},
{0x3816, 0x01},
{0x3817, 0x01},
{0x3820, 0x88},
{0x3821, 0x00},
{0x3880, 0x25},
{0x3882, 0x20},
{0x3c91, 0x0b},
{0x3c94, 0x45},
{0x3cad, 0x00},
{0x3cae, 0x00},
{0x4000, 0xf3},
{0x4001, 0x60},
{0x4003, 0x40},
{0x4300, 0xff},
{0x4302, 0x0f},
{0x4305, 0x83},
{0x4505, 0x84},
{0x4809, 0x1e},
{0x480a, 0x04},
{0x4837, 0x15},
{0x4c00, 0x08},
{0x4c01, 0x08},
{0x4c04, 0x00},
{0x4c05, 0x00},
{0x5000, 0xf9},
{0x3c8c, 0x10},
};

@ -34,8 +34,9 @@ OX03C10::OX03C10() {
init_reg_array.assign(std::begin(init_array_ox03c10), std::end(init_array_ox03c10)); init_reg_array.assign(std::begin(init_array_ox03c10), std::end(init_array_ox03c10));
probe_reg_addr = 0x300a; probe_reg_addr = 0x300a;
probe_expected_data = 0x5803; probe_expected_data = 0x5803;
in_port_info_dt = 0x2c; // one is 0x2a, two are 0x2b mipi_format = CAM_FORMAT_MIPI_RAW_12;
power_config_val_low = 24000000; //Hz frame_data_type = 0x2c; // one is 0x2a, two are 0x2b
mclk_frequency = 24000000; //Hz
dc_gain_factor = 7.32; dc_gain_factor = 7.32;
dc_gain_min_weight = 1; // always on is fine dc_gain_min_weight = 1; // always on is fine

@ -9,12 +9,14 @@
#include "system/camerad/cameras/camera_common.h" #include "system/camerad/cameras/camera_common.h"
#include "system/camerad/sensors/ar0231_registers.h" #include "system/camerad/sensors/ar0231_registers.h"
#include "system/camerad/sensors/ox03c10_registers.h" #include "system/camerad/sensors/ox03c10_registers.h"
#include "system/camerad/sensors/os04c10_registers.h"
#define ANALOG_GAIN_MAX_CNT 55 #define ANALOG_GAIN_MAX_CNT 55
const size_t FRAME_WIDTH = 1928; const size_t FRAME_WIDTH = 1928;
const size_t FRAME_HEIGHT = 1208; const size_t FRAME_HEIGHT = 1208;
const size_t FRAME_STRIDE = 2896; // for 12 bit output. 1928 * 12 / 8 + 4 (alignment) const size_t FRAME_STRIDE = 2896; // for 12 bit output. 1928 * 12 / 8 + 4 (alignment)
class SensorInfo { class SensorInfo {
public: public:
SensorInfo() = default; SensorInfo() = default;
@ -56,8 +58,10 @@ public:
uint32_t probe_expected_data; uint32_t probe_expected_data;
std::vector<i2c_random_wr_payload> start_reg_array; std::vector<i2c_random_wr_payload> start_reg_array;
std::vector<i2c_random_wr_payload> init_reg_array; std::vector<i2c_random_wr_payload> init_reg_array;
uint32_t in_port_info_dt;
uint32_t power_config_val_low; uint32_t mipi_format;
uint32_t mclk_frequency;
uint32_t frame_data_type;
}; };
class AR0231 : public SensorInfo { class AR0231 : public SensorInfo {
@ -79,3 +83,11 @@ public:
float getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const override; float getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const override;
int getSlaveAddress(int port) const override; int getSlaveAddress(int port) const override;
}; };
class OS04C10 : public SensorInfo {
public:
OS04C10();
std::vector<i2c_random_wr_payload> getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const override;
float getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const override;
int getSlaveAddress(int port) const override;
};

@ -1,26 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse
import os import os
from tqdm import tqdm from tqdm import tqdm
from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.srreader import SegmentRangeReader
from openpilot.tools.lib.route import Route
import argparse
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("route", help="The route name") parser.add_argument("route", help="The route name")
parser.add_argument("segment", type=int, help="The index of the segment")
args = parser.parse_args() args = parser.parse_args()
out_path = os.path.join("jpegs", f"{args.route.replace('|', '_')}_{args.segment}") out_path = os.path.join("jpegs", f"{args.route.replace('|', '_').replace('/', '_')}")
os.makedirs(out_path, exist_ok=True) os.makedirs(out_path, exist_ok=True)
r = Route(args.route) lr = SegmentRangeReader(args.route)
path = r.log_paths()[args.segment] or r.qlog_paths()[args.segment]
lr = list(LogReader(path))
for msg in tqdm(lr): for msg in tqdm(lr):
if msg.which() == 'thumbnail': if msg.which() == 'thumbnail':

@ -23,6 +23,9 @@ class HardwareBase(ABC):
except Exception: except Exception:
return default return default
def booted(self) -> bool:
return True
@abstractmethod @abstractmethod
def reboot(self, reason=None): def reboot(self, reason=None):
pass pass

@ -74,6 +74,11 @@ def sudo_write(val, path):
# fallback for debugfs files # fallback for debugfs files
os.system(f"sudo su -c 'echo {val} > {path}'") os.system(f"sudo su -c 'echo {val} > {path}'")
def sudo_read(path: str) -> str:
try:
return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8')
except Exception:
return ""
def affine_irq(val, action): def affine_irq(val, action):
irqs = get_irqs_for_action(action) irqs = get_irqs_for_action(action)
@ -554,6 +559,12 @@ class Tici(HardwareBase):
time.sleep(0.5) time.sleep(0.5)
gpio_set(GPIO.STM_BOOT0, 0) gpio_set(GPIO.STM_BOOT0, 0)
def booted(self):
# this normally boots within 8s, but on rare occasions takes 30+s
encoder_state = sudo_read("/sys/kernel/debug/msm_vidc/core0/info")
if "Core state: 0" in encoder_state and (time.monotonic() < 60*2):
return False
return True
if __name__ == "__main__": if __name__ == "__main__":
t = Tici() t = Tici()

@ -5,7 +5,6 @@ import time
import subprocess import subprocess
from typing import NoReturn from typing import NoReturn
import requests
from timezonefinder import TimezoneFinder from timezonefinder import TimezoneFinder
from openpilot.common.params import Params from openpilot.common.params import Params
@ -41,39 +40,18 @@ def main() -> NoReturn:
# Get allowed timezones # Get allowed timezones
valid_timezones = subprocess.check_output('timedatectl list-timezones', shell=True, encoding='utf8').strip().split('\n') valid_timezones = subprocess.check_output('timedatectl list-timezones', shell=True, encoding='utf8').strip().split('\n')
timezone = params.get("Timezone", encoding='utf8')
if timezone is not None:
cloudlog.debug("Setting timezone based on param")
set_timezone(valid_timezones, timezone)
while True: while True:
time.sleep(60) time.sleep(60)
is_onroad = not params.get_bool("IsOffroad")
if is_onroad:
continue
# Set based on param
timezone = params.get("Timezone", encoding='utf8')
if timezone is not None:
cloudlog.debug("Setting timezone based on param")
set_timezone(valid_timezones, timezone)
continue
location = params.get("LastGPSPosition", encoding='utf8') location = params.get("LastGPSPosition", encoding='utf8')
# Find timezone based on IP geolocation if no gps location is available
if location is None:
cloudlog.debug("Setting timezone based on IP lookup")
try:
r = requests.get("https://ipapi.co/timezone", headers=REQUEST_HEADERS, timeout=10)
if r.status_code == 200:
set_timezone(valid_timezones, r.text)
else:
cloudlog.error(f"Unexpected status code from api {r.status_code}")
time.sleep(3600) # Don't make too many API requests
except requests.exceptions.RequestException:
cloudlog.exception("Error getting timezone based on IP")
continue
# Find timezone by reverse geocoding the last known gps location # Find timezone by reverse geocoding the last known gps location
else: if location is not None:
cloudlog.debug("Setting timezone based on GPS location") cloudlog.debug("Setting timezone based on GPS location")
try: try:
location = json.loads(location) location = json.loads(location)

@ -13,23 +13,30 @@ void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, doubl
auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), first_ts, CompareCanEvent()); auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), first_ts, CompareCanEvent());
auto last = std::upper_bound(first, msgs.cend(), ts, CompareCanEvent()); auto last = std::upper_bound(first, msgs.cend(), ts, CompareCanEvent());
if (first != last && !size.isEmpty()) { if (first == last || size.isEmpty()) {
points.clear(); pixmap = QPixmap();
double value = 0; return;
for (auto it = first; it != last; ++it) { }
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value); points.clear();
} double value = 0;
for (auto it = first; it != last; ++it) {
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value);
} }
const auto [min, max] = std::minmax_element(points.begin(), points.end(), }
[](auto &l, auto &r) { return l.y() < r.y(); });
min_val = min->y() == max->y() ? min->y() - 1 : min->y(); if (points.empty()) {
max_val = min->y() == max->y() ? max->y() + 1 : max->y();
freq_ = points.size() / std::max(points.back().x() - points.front().x(), 1.0);
render(sig->color, range, size);
} else {
pixmap = QPixmap(); pixmap = QPixmap();
return;
} }
const auto [min, max] = std::minmax_element(points.begin(), points.end(),
[](auto &l, auto &r) { return l.y() < r.y(); });
min_val = min->y() == max->y() ? min->y() - 1 : min->y();
max_val = min->y() == max->y() ? max->y() + 1 : max->y();
freq_ = points.size() / std::max(points.back().x() - points.front().x(), 1.0);
render(sig->color, range, size);
} }
void Sparkline::render(const QColor &color, int range, QSize size) { void Sparkline::render(const QColor &color, int range, QSize size) {

@ -0,0 +1,74 @@
# tools/car_porting
Check out [this blog post](https://blog.comma.ai/how-to-write-a-car-port-for-openpilot/) for a high-level overview of porting a car.
## Useful car porting utilities
Testing car ports in your car is very time-consuming. Check out these utilities to do basic checks on your work before running it in your car.
### [Cabana](/tools/cabana/README.md)
View your car's CAN signals through DBC files, which openpilot uses to parse and create messages that talk to the car.
Example:
```bash
> tools/cabana/cabana '1bbe6bf2d62f58a8|2022-07-14--17-11-43'
```
### [tools/car_porting/auto_fingerprint.py](/tools/car_porting/auto_fingerprint.py)
Given a route and platform, automatically inserts FW fingerprints from the platform into the correct place in fingerprints.py
Example:
```bash
> python tools/car_porting/auto_fingerprint.py '1bbe6bf2d62f58a8|2022-07-14--17-11-43' 'SUBARU OUTBACK 6TH GEN'
Attempting to add fw version for: SUBARU OUTBACK 6TH GEN
```
### [selfdrive/car/tests/test_car_interfaces.py](/selfdrive/car/tests/test_car_interfaces.py)
Finds common bugs for car interfaces, without even requiring a route.
#### Example: Typo in signal name
```bash
> pytest selfdrive/car/tests/test_car_interfaces.py -k subaru # replace with the brand you are working on
=====================================================================
FAILED selfdrive/car/tests/test_car_interfaces.py::TestCarInterfaces::test_car_interfaces_165_SUBARU_LEGACY_7TH_GEN - KeyError: 'CruiseControlOOPS'
```
### [tools/car_porting/test_car_model.py](/tools/car_porting/test_car_model.py)
Given a route, runs most of the car interface to check for common errors like missing signals, blocked panda messages, and safety mismatches.
#### Example: panda safety mismatch for gasPressed
```bash
> python tools/car_porting/test_car_model.py '4822a427b188122a|2023-08-14--16-22-21'
=====================================================================
FAIL: test_panda_safety_carstate (__main__.CarModelTestCase.test_panda_safety_carstate)
Assert that panda safety matches openpilot's carState
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/batman/xx/openpilot/openpilot/selfdrive/car/tests/test_models.py", line 380, in test_panda_safety_carstate
self.assertFalse(len(failed_checks), f"panda safety doesn't agree with openpilot: {failed_checks}")
AssertionError: 1 is not false : panda safety doesn't agree with openpilot: {'gasPressed': 116}
```
### [tools/car_porting/examples/subaru_steer_temp_fault.ipynb](/tools/car_porting/examples/subaru_steer_temp_fault.ipynb)
An example of searching through a database of segments for a specific condition, and plotting the results.
![steer warning example](https://github.com/commaai/openpilot/assets/9648890/d60ad120-4b44-4974-ac79-adc660fb8fe2)
*a plot of the steer_warning vs steering angle, where we can see it is clearly caused by a large steering angle change*
### [tools/car_porting/examples/subaru_long_accel.ipynb](/tools/car_porting/examples/subaru_long_accel.ipynb)
An example of plotting the response of an actuator when it is active.
![brake pressure example](https://github.com/commaai/openpilot/assets/9648890/8f32cf1d-8fc0-4407-b540-70625ebbf082)
*a plot of the brake_pressure vs acceleration, where we can see it is a fairly linear response.*

@ -5,10 +5,9 @@ from collections import defaultdict
from typing import Optional from typing import Optional
from openpilot.selfdrive.debug.format_fingerprints import format_brand_fw_versions from openpilot.selfdrive.debug.format_fingerprints import format_brand_fw_versions
from openpilot.tools.lib.logreader import MultiLogIterator
from openpilot.tools.lib.route import Route
from openpilot.selfdrive.car.fw_versions import match_fw_to_car from openpilot.selfdrive.car.fw_versions import match_fw_to_car
from openpilot.selfdrive.car.interfaces import get_interface_attr from openpilot.selfdrive.car.interfaces import get_interface_attr
from openpilot.tools.lib.srreader import SegmentRangeReader, ReadMode
ALL_FW_VERSIONS = get_interface_attr("FW_VERSIONS") ALL_FW_VERSIONS = get_interface_attr("FW_VERSIONS")
@ -25,8 +24,7 @@ if __name__ == "__main__":
parser.add_argument("platform", help="The platform, or leave empty to auto-determine using fuzzy", default=None, nargs='?') parser.add_argument("platform", help="The platform, or leave empty to auto-determine using fuzzy", default=None, nargs='?')
args = parser.parse_args() args = parser.parse_args()
route = Route(args.route) lr = SegmentRangeReader(args.route, ReadMode.QLOG)
lr = MultiLogIterator(route.qlog_paths())
carFw = None carFw = None
carVin = None carVin = None

@ -0,0 +1,120 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"segments = [\n",
" \"d9df6f87e8feff94|2023-03-28--17-41-10/1:12\"\n",
"]\n",
"platform = \"SUBARU OUTBACK 6TH GEN\"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import copy\n",
"import numpy as np\n",
"\n",
"from opendbc.can.parser import CANParser\n",
"\n",
"from openpilot.selfdrive.car.subaru.values import DBC\n",
"from openpilot.tools.lib.srreader import SegmentRangeReader\n",
"\n",
"\"\"\"\n",
"In this example, we plot the relationship between Cruise_Brake and Acceleration for stock eyesight.\n",
"\"\"\"\n",
"\n",
"for segment in segments:\n",
" lr = SegmentRangeReader(segment)\n",
"\n",
" messages = [\n",
" (\"ES_Distance\", 20),\n",
" (\"ES_Brake\", 20),\n",
" (\"ES_Status\", 20),\n",
" ]\n",
"\n",
" cp = CANParser(DBC[platform][\"pt\"], messages, 1)\n",
"\n",
" es_distance_history = []\n",
" es_status_history = []\n",
" es_brake_history = []\n",
" acceleration_history = []\n",
"\n",
" last_acc = 0\n",
"\n",
" for msg in lr:\n",
" if msg.which() == \"can\":\n",
" cp.update_strings([msg.as_builder().to_bytes()])\n",
" es_distance_history.append(copy.copy(cp.vl[\"ES_Distance\"]))\n",
" es_brake_history.append(copy.copy(cp.vl[\"ES_Brake\"]))\n",
" es_status_history.append(copy.copy(cp.vl[\"ES_Status\"]))\n",
"\n",
" acceleration_history.append(last_acc)\n",
" \n",
" if msg.which() == \"carState\":\n",
" last_acc = msg.carState.aEgo"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def process(history, func):\n",
" return np.array([func(h) for h in history])\n",
"\n",
"cruise_activated = process(es_status_history, lambda es_status: es_status[\"Cruise_Activated\"])\n",
"cruise_throttle = process(es_distance_history, lambda es_distance: es_distance[\"Cruise_Throttle\"])\n",
"cruise_rpm = process(es_status_history, lambda es_status: es_status[\"Cruise_RPM\"])\n",
"cruise_brake = process(es_brake_history, lambda es_brake: es_brake[\"Brake_Pressure\"])\n",
"acceleration = process(acceleration_history, lambda acc: acc)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"valid_brake = (cruise_activated==1) & (cruise_brake>0) # only when cruise is activated and eyesight is braking\n",
"\n",
"ax = plt.figure().add_subplot()\n",
"\n",
"ax.set_title(\"Brake_Pressure vs Acceleration\")\n",
"ax.set_xlabel(\"Brake_Pessure\")\n",
"ax.set_ylabel(\"Acceleration\")\n",
"ax.scatter(cruise_brake[valid_brake], -acceleration[valid_brake])"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

@ -0,0 +1,110 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# An example of searching through a database of segments for a specific condition, and plotting the results.\n",
"\n",
"segments = [\n",
" \"c3d1ccb52f5f9d65|2023-07-22--01-23-20/6:10\",\n",
"]\n",
"platform = \"SUBARU OUTBACK 6TH GEN\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import copy\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"from opendbc.can.parser import CANParser\n",
"\n",
"from openpilot.selfdrive.car.subaru.values import CanBus, DBC\n",
"from openpilot.tools.lib.srreader import SegmentRangeReader\n",
"\n",
"\"\"\"\n",
"In this example, we search for positive transitions of Steer_Warning, which indicate that the EPS\n",
"has stopped responding to our messages. This analysis would allow you to find the cause of these\n",
"steer warnings and potentially work around them.\n",
"\"\"\"\n",
"\n",
"for segment in segments:\n",
" lr = SegmentRangeReader(segment)\n",
"\n",
" can_msgs = [msg for msg in lr if msg.which() == \"can\"]\n",
"\n",
" messages = [\n",
" (\"Steering_Torque\", 50)\n",
" ]\n",
"\n",
" cp = CANParser(DBC[platform][\"pt\"], messages, CanBus.main)\n",
"\n",
" steering_torque_history = []\n",
" examples = []\n",
"\n",
" for msg in can_msgs:\n",
" cp.update_strings([msg.as_builder().to_bytes()])\n",
" steering_torque_history.append(copy.copy(cp.vl[\"Steering_Torque\"]))\n",
" \n",
" steer_warning_last = False\n",
" for i, steering_torque_msg in enumerate(steering_torque_history):\n",
" steer_warning = steering_torque_msg[\"Steer_Warning\"]\n",
"\n",
" steer_angle = steering_torque_msg[\"Steering_Angle\"]\n",
"\n",
" if steer_warning and not steer_warning_last: # positive transition of \"Steer_Warning\"\n",
" examples.append(i)\n",
"\n",
" steer_warning_last = steer_warning\n",
"\n",
" FRAME_DELTA = 100 # plot this many frames around the positive transition\n",
"\n",
" for example in examples:\n",
" fig, axs = plt.subplots(2)\n",
"\n",
" min_frame = int(example-FRAME_DELTA/2)\n",
" max_frame = int(example+FRAME_DELTA/2)\n",
"\n",
" steering_angle_history = [msg[\"Steering_Angle\"] for msg in steering_torque_history[min_frame:max_frame]]\n",
" steering_warning_history = [msg[\"Steer_Warning\"] for msg in steering_torque_history[min_frame:max_frame]]\n",
"\n",
" xs = np.arange(-FRAME_DELTA/2, FRAME_DELTA/2)\n",
"\n",
" axs[0].plot(xs, steering_angle_history)\n",
" axs[0].set_ylabel(\"Steering Angle (deg)\")\n",
" axs[1].plot(xs, steering_warning_history)\n",
" axs[1].set_ylabel(\"Steer Warning\")\n",
"\n",
" plt.show()\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

@ -47,7 +47,7 @@ class Joystick:
else: else:
self.cancel_button = 'BTN_TRIGGER' self.cancel_button = 'BTN_TRIGGER'
accel_axis = 'ABS_Y' accel_axis = 'ABS_Y'
steer_axis = 'ABS_RZ' steer_axis = 'ABS_RX'
self.min_axis_value = {accel_axis: 0., steer_axis: 0.} self.min_axis_value = {accel_axis: 0., steer_axis: 0.}
self.max_axis_value = {accel_axis: 255., steer_axis: 255.} self.max_axis_value = {accel_axis: 255., steer_axis: 255.}
self.axes_values = {accel_axis: 0., steer_axis: 0.} self.axes_values = {accel_axis: 0., steer_axis: 0.}

@ -8,7 +8,7 @@ import sys
from bisect import bisect_left, bisect_right from bisect import bisect_left, bisect_right
from collections import defaultdict from collections import defaultdict
from openpilot.tools.lib.logreader import logreader_from_route_or_segment from openpilot.tools.lib.srreader import SegmentRangeReader
DEMO_ROUTE = "9f583b1d93915c31|2022-05-18--10-49-51--0" DEMO_ROUTE = "9f583b1d93915c31|2022-05-18--10-49-51--0"
@ -236,7 +236,7 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
r = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip() r = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip()
lr = logreader_from_route_or_segment(r, sort_by_time=True) lr = SegmentRangeReader(r, sort_by_time=True)
data, _ = get_timestamps(lr) data, _ = get_timestamps(lr)
print_timestamps(data['timestamp'], data['duration'], data['start'], args.relative) print_timestamps(data['timestamp'], data['duration'], data['start'], args.relative)

@ -7,8 +7,11 @@ TIME_FMT = "%Y-%m-%d--%H-%M-%S"
class RE: class RE:
DONGLE_ID = r'(?P<dongle_id>[a-z0-9]{16})' DONGLE_ID = r'(?P<dongle_id>[a-z0-9]{16})'
TIMESTAMP = r'(?P<timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}--[0-9]{2}-[0-9]{2}-[0-9]{2})' TIMESTAMP = r'(?P<timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}--[0-9]{2}-[0-9]{2}-[0-9]{2})'
ROUTE_NAME = r'{}[|_/]{}'.format(DONGLE_ID, TIMESTAMP) ROUTE_NAME = r'(?P<route_name>{}[|_/]{})'.format(DONGLE_ID, TIMESTAMP)
SEGMENT_NAME = r'{}(?:--|/)(?P<segment_num>[0-9]+)'.format(ROUTE_NAME) SEGMENT_NAME = r'{}(?:--|/)(?P<segment_num>[0-9]+)'.format(ROUTE_NAME)
INDEX = r'-?[0-9]+'
SLICE = r'(?P<start>{})?:?(?P<end>{})?:?(?P<step>{})?'.format(INDEX, INDEX, INDEX)
SEGMENT_RANGE = r'{}(?:--|/)?(?P<slice>({}))?/?(?P<selector>([qr]))?'.format(ROUTE_NAME, SLICE)
BOOTLOG_NAME = ROUTE_NAME BOOTLOG_NAME = ROUTE_NAME
EXPLORER_FILE = r'^(?P<segment_name>{})--(?P<file_name>[a-z]+\.[a-z0-9]+)$'.format(SEGMENT_NAME) EXPLORER_FILE = r'^(?P<segment_name>{})--(?P<file_name>[a-z]+\.[a-z0-9]+)$'.format(SEGMENT_NAME)

@ -229,3 +229,29 @@ class SegmentName:
def data_dir(self) -> Optional[str]: return self._data_dir def data_dir(self) -> Optional[str]: return self._data_dir
def __str__(self) -> str: return self._canonical_name def __str__(self) -> str: return self._canonical_name
class SegmentRange:
def __init__(self, segment_range: str):
self.m = re.fullmatch(RE.SEGMENT_RANGE, segment_range)
assert self.m, f"Segment range is not valid {segment_range}"
@property
def route_name(self):
return self.m.group("route_name")
@property
def dongle_id(self):
return self.m.group("dongle_id")
@property
def timestamp(self):
return self.m.group("timestamp")
@property
def _slice(self):
return self.m.group("slice")
@property
def selector(self):
return self.m.group("selector")

@ -0,0 +1,141 @@
import enum
import numpy as np
import pathlib
import re
from urllib.parse import parse_qs, urlparse
from openpilot.selfdrive.test.openpilotci import get_url
from openpilot.tools.lib.helpers import RE
from openpilot.tools.lib.logreader import LogReader
from openpilot.tools.lib.route import Route, SegmentRange
class ReadMode(enum.StrEnum):
RLOG = "r" # only read rlogs
QLOG = "q" # only read qlogs
#AUTO = "a" # default to rlogs, fallback to qlogs, not supported yet
def create_slice_from_string(s: str):
m = re.fullmatch(RE.SLICE, s)
assert m is not None, f"Invalid slice: {s}"
start, end, step = m.groups()
start = int(start) if start is not None else None
end = int(end) if end is not None else None
step = int(step) if step is not None else None
if start is not None and ":" not in s and end is None and step is None:
return start
return slice(start, end, step)
def parse_slice(sr: SegmentRange):
route = Route(sr.route_name)
segs = np.arange(route.max_seg_number+1)
s = create_slice_from_string(sr._slice)
return segs[s] if isinstance(s, slice) else [segs[s]]
def comma_api_source(sr: SegmentRange, mode=ReadMode.RLOG, sort_by_time=False):
segs = parse_slice(sr)
route = Route(sr.route_name)
log_paths = route.log_paths() if mode == ReadMode.RLOG else route.qlog_paths()
invalid_segs = [seg for seg in segs if log_paths[seg] is None]
assert not len(invalid_segs), f"Some of the requested segments are not available: {invalid_segs}"
for seg in segs:
yield LogReader(log_paths[seg], sort_by_time=sort_by_time)
def internal_source(sr: SegmentRange, mode=ReadMode.RLOG, sort_by_time=False):
segs = parse_slice(sr)
for seg in segs:
yield LogReader(f"cd:/{sr.dongle_id}/{sr.timestamp}/{seg}/{'rlog' if mode == ReadMode.RLOG else 'qlog'}.bz2", sort_by_time=sort_by_time)
def openpilotci_source(sr: SegmentRange, mode=ReadMode.RLOG, sort_by_time=False):
segs = parse_slice(sr)
for seg in segs:
yield LogReader(get_url(sr.route_name, seg, 'rlog' if mode == ReadMode.RLOG else 'qlog'), sort_by_time=sort_by_time)
def direct_source(file_or_url, sort_by_time):
yield LogReader(file_or_url, sort_by_time=sort_by_time)
def auto_source(*args, **kwargs):
# Automatically determine viable source
try:
next(internal_source(*args, **kwargs))
return internal_source(*args, **kwargs)
except Exception:
pass
try:
next(openpilotci_source(*args, **kwargs))
return openpilotci_source(*args, **kwargs)
except Exception:
pass
return comma_api_source(*args, **kwargs)
def parse_useradmin(identifier):
if "useradmin.comma.ai" in identifier:
query = parse_qs(urlparse(identifier).query)
return query["onebox"][0]
return None
def parse_cabana(identifier):
if "cabana.comma.ai" in identifier:
query = parse_qs(urlparse(identifier).query)
return query["route"][0]
return None
def parse_cd(identifier):
if "cd:/" in identifier:
return identifier.replace("cd:/", "")
return None
def parse_direct(identifier):
if "https://" in identifier or "http://" in identifier or pathlib.Path(identifier).exists():
return identifier
return None
def parse_indirect(identifier):
parsed = parse_useradmin(identifier) or parse_cabana(identifier)
if parsed is not None:
return parsed, comma_api_source, True
parsed = parse_cd(identifier)
if parsed is not None:
return parsed, internal_source, True
return identifier, None, False
class SegmentRangeReader:
def _logreaders_from_identifier(self, identifier):
parsed, source, is_indirect = parse_indirect(identifier)
if not is_indirect:
direct_parsed = parse_direct(identifier)
if direct_parsed is not None:
return direct_source(identifier, sort_by_time=self.sort_by_time)
sr = SegmentRange(parsed)
mode = self.default_mode if sr.selector is None else ReadMode(sr.selector)
source = self.default_source if source is None else source
return source(sr, mode, sort_by_time=self.sort_by_time)
def __init__(self, identifier: str, default_mode=ReadMode.RLOG, default_source=auto_source, sort_by_time=False):
self.default_mode = default_mode
self.default_source = default_source
self.sort_by_time = sort_by_time
self.lrs = self._logreaders_from_identifier(identifier)
def __iter__(self):
for lr in self.lrs:
for m in lr:
yield m

@ -1,15 +1,58 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from functools import wraps
import http.server
import os import os
import threading
import time
import unittest import unittest
from pathlib import Path
from parameterized import parameterized from parameterized import parameterized
from unittest import mock
from openpilot.system.hardware.hw import Paths
from openpilot.tools.lib.url_file import URLFile from openpilot.tools.lib.url_file import URLFile
class CachingTestRequestHandler(http.server.BaseHTTPRequestHandler):
FILE_EXISTS = True
def do_GET(self):
if self.FILE_EXISTS:
self.send_response(200, b'1234')
else:
self.send_response(404)
self.end_headers()
def do_HEAD(self):
if self.FILE_EXISTS:
self.send_response(200)
self.send_header("Content-Length", "4")
else:
self.send_response(404)
self.end_headers()
class CachingTestServer(threading.Thread):
def run(self):
self.server = http.server.HTTPServer(("127.0.0.1", 0), CachingTestRequestHandler)
self.port = self.server.server_port
self.server.serve_forever()
def stop(self):
self.server.server_close()
self.server.shutdown()
def with_caching_server(func):
@wraps(func)
def wrapper(*args, **kwargs):
server = CachingTestServer()
server.start()
time.sleep(0.25) # wait for server to get it's port
try:
func(*args, **kwargs, port=server.port)
finally:
server.stop()
return wrapper
class TestFileDownload(unittest.TestCase): class TestFileDownload(unittest.TestCase):
def compare_loads(self, url, start=0, length=None): def compare_loads(self, url, start=0, length=None):
@ -66,32 +109,20 @@ class TestFileDownload(unittest.TestCase):
self.compare_loads(large_file_url) self.compare_loads(large_file_url)
@parameterized.expand([(True, ), (False, )]) @parameterized.expand([(True, ), (False, )])
def test_recover_from_missing_file(self, cache_enabled): @with_caching_server
def test_recover_from_missing_file(self, cache_enabled, port):
os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0"
file_url = "http://localhost:5001/test.png" file_url = f"http://localhost:{port}/test.png"
file_exists = False CachingTestRequestHandler.FILE_EXISTS = False
length = URLFile(file_url).get_length()
self.assertEqual(length, -1)
def get_length_online_mock(self): CachingTestRequestHandler.FILE_EXISTS = True
if file_exists: length = URLFile(file_url).get_length()
return 4 self.assertEqual(length, 4)
return -1
patch_length = mock.patch.object(URLFile, "get_length_online", get_length_online_mock)
patch_length.start()
try:
length = URLFile(file_url).get_length()
self.assertEqual(length, -1)
file_exists = True
length = URLFile(file_url).get_length()
self.assertEqual(length, 4)
finally:
tempfile_length = Path(Paths.download_cache_root()) / "ba2119904385654cb0105a2da174875f8e7648db175f202ecae6d6428b0e838f_length"
if tempfile_length.exists():
tempfile_length.unlink()
patch_length.stop()
if __name__ == "__main__": if __name__ == "__main__":

@ -0,0 +1,88 @@
import shutil
import tempfile
import numpy as np
import unittest
from parameterized import parameterized
import requests
from openpilot.tools.lib.route import SegmentRange
from openpilot.tools.lib.srreader import ReadMode, SegmentRangeReader, parse_slice, parse_indirect
NUM_SEGS = 17 # number of segments in the test route
ALL_SEGS = list(np.arange(NUM_SEGS))
TEST_ROUTE = "344c5c15b34f2d8a/2024-01-03--09-37-12"
QLOG_FILE = "https://commadataci.blob.core.windows.net/openpilotci/0375fdf7b1ce594d/2019-06-13--08-32-25/3/qlog.bz2"
class TestSegmentRangeReader(unittest.TestCase):
@parameterized.expand([
(f"{TEST_ROUTE}", ALL_SEGS),
(f"{TEST_ROUTE.replace('/', '|')}", ALL_SEGS),
(f"{TEST_ROUTE}--0", [0]),
(f"{TEST_ROUTE}--5", [5]),
(f"{TEST_ROUTE}/0", [0]),
(f"{TEST_ROUTE}/5", [5]),
(f"{TEST_ROUTE}/0:10", ALL_SEGS[0:10]),
(f"{TEST_ROUTE}/0:0", []),
(f"{TEST_ROUTE}/4:6", ALL_SEGS[4:6]),
(f"{TEST_ROUTE}/0:-1", ALL_SEGS[0:-1]),
(f"{TEST_ROUTE}/:5", ALL_SEGS[:5]),
(f"{TEST_ROUTE}/2:", ALL_SEGS[2:]),
(f"{TEST_ROUTE}/2:-1", ALL_SEGS[2:-1]),
(f"{TEST_ROUTE}/-1", [ALL_SEGS[-1]]),
(f"{TEST_ROUTE}/-2", [ALL_SEGS[-2]]),
(f"{TEST_ROUTE}/-2:-1", ALL_SEGS[-2:-1]),
(f"{TEST_ROUTE}/-4:-2", ALL_SEGS[-4:-2]),
(f"{TEST_ROUTE}/:10:2", ALL_SEGS[:10:2]),
(f"{TEST_ROUTE}/5::2", ALL_SEGS[5::2]),
(f"https://useradmin.comma.ai/?onebox={TEST_ROUTE}", ALL_SEGS),
(f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '|')}", ALL_SEGS),
(f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '%7C')}", ALL_SEGS),
(f"https://cabana.comma.ai/?route={TEST_ROUTE}", ALL_SEGS),
(f"cd:/{TEST_ROUTE}", ALL_SEGS),
])
def test_indirect_parsing(self, identifier, expected):
parsed, _, _ = parse_indirect(identifier)
sr = SegmentRange(parsed)
segs = parse_slice(sr)
self.assertListEqual(list(segs), expected)
def test_direct_parsing(self):
qlog = tempfile.NamedTemporaryFile(mode='wb', delete=False)
with requests.get(QLOG_FILE, stream=True) as r:
with qlog as f:
shutil.copyfileobj(r.raw, f)
for f in [QLOG_FILE, qlog.name]:
l = len(list(SegmentRangeReader(f)))
self.assertGreater(l, 100)
@parameterized.expand([
(f"{TEST_ROUTE}///",),
(f"{TEST_ROUTE}---",),
(f"{TEST_ROUTE}/-4:--2",),
(f"{TEST_ROUTE}/-a",),
(f"{TEST_ROUTE}/j",),
(f"{TEST_ROUTE}/0:1:2:3",),
(f"{TEST_ROUTE}/:::3",),
])
def test_bad_ranges(self, segment_range):
with self.assertRaises(AssertionError):
sr = SegmentRange(segment_range)
parse_slice(sr)
def test_modes(self):
qlog_len = len(list(SegmentRangeReader(f"{TEST_ROUTE}/0", ReadMode.QLOG)))
rlog_len = len(list(SegmentRangeReader(f"{TEST_ROUTE}/0", ReadMode.RLOG)))
self.assertLess(qlog_len * 6, rlog_len)
def test_modes_from_name(self):
qlog_len = len(list(SegmentRangeReader(f"{TEST_ROUTE}/0/q")))
rlog_len = len(list(SegmentRangeReader(f"{TEST_ROUTE}/0/r")))
self.assertLess(qlog_len * 6, rlog_len)
if __name__ == "__main__":
unittest.main()

@ -57,6 +57,8 @@ class URLFile:
def get_length_online(self): def get_length_online(self):
timeout = Timeout(connect=50.0, read=500.0) timeout = Timeout(connect=50.0, read=500.0)
response = self._http_client.request('HEAD', self._url, timeout=timeout, preload_content=False) response = self._http_client.request('HEAD', self._url, timeout=timeout, preload_content=False)
if not (200 <= response.status <= 299):
return -1
length = response.headers.get('content-length', 0) length = response.headers.get('content-length', 0)
return int(length) return int(length)

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import multiprocessing
import os import os
import sys import sys
import multiprocessing
import platform import platform
import shutil import shutil
import subprocess import subprocess
@ -9,13 +9,12 @@ import tarfile
import tempfile import tempfile
import requests import requests
import argparse import argparse
from functools import partial
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.selfdrive.test.openpilotci import get_url
from openpilot.tools.lib.logreader import LogReader
from openpilot.tools.lib.route import Route, SegmentName
from openpilot.tools.lib.helpers import save_log from openpilot.tools.lib.helpers import save_log
from urllib.parse import urlparse, parse_qs
from openpilot.tools.lib.srreader import SegmentRangeReader
juggle_dir = os.path.dirname(os.path.realpath(__file__)) juggle_dir = os.path.dirname(os.path.realpath(__file__))
@ -53,17 +52,6 @@ def get_plotjuggler_version():
return tuple(map(int, version.split("."))) return tuple(map(int, version.split(".")))
def load_segment(segment_name):
if segment_name is None:
return []
try:
return list(LogReader(segment_name))
except (AssertionError, ValueError) as e:
print(f"Error parsing {segment_name}: {e}")
return []
def start_juggler(fn=None, dbc=None, layout=None, route_or_segment_name=None): def start_juggler(fn=None, dbc=None, layout=None, route_or_segment_name=None):
env = os.environ.copy() env = os.environ.copy()
env["BASEDIR"] = BASEDIR env["BASEDIR"] = BASEDIR
@ -82,48 +70,16 @@ def start_juggler(fn=None, dbc=None, layout=None, route_or_segment_name=None):
cmd = f'{PLOTJUGGLER_BIN} --buffer_size {MAX_STREAMING_BUFFER_SIZE} --plugin_folders {INSTALL_DIR}{extra_args}' cmd = f'{PLOTJUGGLER_BIN} --buffer_size {MAX_STREAMING_BUFFER_SIZE} --plugin_folders {INSTALL_DIR}{extra_args}'
subprocess.call(cmd, shell=True, env=env, cwd=juggle_dir) subprocess.call(cmd, shell=True, env=env, cwd=juggle_dir)
def process(can, lr):
return [d for d in lr if can or d.which() not in ['can', 'sendcan']]
def juggle_route(route_or_segment_name, segment_count, qlog, can, layout, dbc=None, ci=False): def juggle_route(route_or_segment_name, can, layout, dbc=None):
segment_start = 0 sr = SegmentRangeReader(route_or_segment_name)
if 'cabana' in route_or_segment_name:
query = parse_qs(urlparse(route_or_segment_name).query)
route_or_segment_name = query["route"][0]
if route_or_segment_name.startswith(("http://", "https://", "cd:/")) or os.path.isfile(route_or_segment_name):
logs = [route_or_segment_name]
elif ci:
route_or_segment_name = SegmentName(route_or_segment_name, allow_route_name=True)
route = route_or_segment_name.route_name.canonical_name
segment_start = max(route_or_segment_name.segment_num, 0)
logs = [get_url(route, i) for i in range(100)] # Assume there not more than 100 segments
else:
route_or_segment_name = SegmentName(route_or_segment_name, allow_route_name=True)
segment_start = max(route_or_segment_name.segment_num, 0)
if route_or_segment_name.segment_num != -1 and segment_count is None:
segment_count = 1
r = Route(route_or_segment_name.route_name.canonical_name, route_or_segment_name.data_dir)
logs = r.qlog_paths() if qlog else r.log_paths()
segment_end = segment_start + segment_count if segment_count else None
logs = logs[segment_start:segment_end]
if None in logs:
resp = input(f"{logs.count(None)}/{len(logs)} of the rlogs in this segment are missing, would you like to fall back to the qlogs? (y/n) ")
if resp == 'y':
logs = r.qlog_paths()[segment_start:segment_end]
else:
print("Please try a different route or segment")
return
all_data = []
with multiprocessing.Pool(24) as pool: with multiprocessing.Pool(24) as pool:
for d in pool.map(load_segment, logs): all_data = []
all_data += d for p in pool.map(partial(process, can), sr.lrs):
all_data.extend(p)
if not can:
all_data = [d for d in all_data if d.which() not in ['can', 'sendcan']]
# Infer DBC name from logs # Infer DBC name from logs
if dbc is None: if dbc is None:
@ -146,15 +102,12 @@ if __name__ == "__main__":
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one")
parser.add_argument("--qlog", action="store_true", help="Use qlogs")
parser.add_argument("--ci", action="store_true", help="Download data from openpilot CI bucket")
parser.add_argument("--can", action="store_true", help="Parse CAN data") parser.add_argument("--can", action="store_true", help="Parse CAN data")
parser.add_argument("--stream", action="store_true", help="Start PlotJuggler in streaming mode") parser.add_argument("--stream", action="store_true", help="Start PlotJuggler in streaming mode")
parser.add_argument("--layout", nargs='?', help="Run PlotJuggler with a pre-defined layout") parser.add_argument("--layout", nargs='?', help="Run PlotJuggler with a pre-defined layout")
parser.add_argument("--install", action="store_true", help="Install or update PlotJuggler + plugins") parser.add_argument("--install", action="store_true", help="Install or update PlotJuggler + plugins")
parser.add_argument("--dbc", help="Set the DBC name to load for parsing CAN data. If not set, the DBC will be automatically inferred from the logs.") parser.add_argument("--dbc", help="Set the DBC name to load for parsing CAN data. If not set, the DBC will be automatically inferred from the logs.")
parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name to plot (cabana share URL accepted)") parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name to plot (cabana share URL accepted)")
parser.add_argument("segment_count", type=int, nargs='?', help="The number of segments to plot")
if len(sys.argv) == 1: if len(sys.argv) == 1:
parser.print_help() parser.print_help()
@ -177,4 +130,4 @@ if __name__ == "__main__":
start_juggler(layout=args.layout) start_juggler(layout=args.layout)
else: else:
route_or_segment_name = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip() route_or_segment_name = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip()
juggle_route(route_or_segment_name, args.segment_count, args.qlog, args.can, args.layout, args.dbc, args.ci) juggle_route(route_or_segment_name, args.can, args.layout, args.dbc)

@ -8,7 +8,7 @@ import unittest
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.common.timeout import Timeout from openpilot.common.timeout import Timeout
from openpilot.tools.plotjuggler.juggle import install from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE, install
PJ_DIR = os.path.join(BASEDIR, "tools/plotjuggler") PJ_DIR = os.path.join(BASEDIR, "tools/plotjuggler")
@ -18,8 +18,8 @@ class TestPlotJuggler(unittest.TestCase):
install() install()
pj = os.path.join(PJ_DIR, "juggle.py") pj = os.path.join(PJ_DIR, "juggle.py")
with subprocess.Popen(f'QT_QPA_PLATFORM=offscreen {pj} --demo None 1 --qlog', with subprocess.Popen(f'QT_QPA_PLATFORM=offscreen {pj} "{DEMO_ROUTE}/:2"',
stderr=subprocess.PIPE, shell=True, start_new_session=True) as p: stderr=subprocess.PIPE, shell=True, start_new_session=True) as p:
# Wait for "Done reading Rlog data" signal from the plugin # Wait for "Done reading Rlog data" signal from the plugin
output = "\n" output = "\n"
with Timeout(180, error_msg=output): with Timeout(180, error_msg=output):

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

Loading…
Cancel
Save